Ajuste Fino de Modelos Pré-treinados com Hugging Face Transformers

Introdução

Este artigo explora como utilizar a biblioteca 🤗 Transformers do Hugging Face para processamento de linguagem natural (PLN). O conteúdo foi adaptado dos recursos disponíveis no site oficial do Hugging Face, com explicações adicionais e detalhamento dos principais parâmetros da API Trainer e seus argumentos.

O material está dividido em duas partes principais:

  • Demonstração de tarefas de PLN utilizando a ferramenta pipeline
  • Construção de um modelo ajustado com a API Trainer

Sumário

  1. Introdução

Nesta seção, utilizaremos as bibliotecas do ecossistema Hugging Face, especificamente 🤗 Transformers, para realizar tarefas de processamento de linguagem natural (PLN).

História dos Transformers

A seguir, alguns marcos na história dos modelos Transformer:

  • Junho de 2017: Lançamento da arquitetura Transformer, com foco inicial em tarefas de tradução.
  • Junho de 2018: GPT, primeiro modelo Transformer pré-treinado, ajustado para diversas tarefas de PLN.
  • Outubro de 2018: BERT, outro modelo grande pré-treinado, projetado para gerar melhores representações de sentenças.
  • Fevereiro de 2019: GPT-2, versão aprimorada (e maior) do GPT, inicialmente não divulgada por questões éticas.
  • Outubro de 2019: DistilBERT, versão destilada do BERT, 60% mais rápida e 40% mais leve, mantendo 97% do desempenho.
  • Outubro de 2019: BART e T5, modelos grandes pré-treinados utilizando a arquitetura Transformer original.
  • Maio de 2020: GPT-3, versão maior do GPT-2, que funciona bem em várias tarefas sem necessidade de ajuste fino (aprendizado de zero-shot).

Esses modelos podem ser categorizados em três tipos principais:

  • Modelos GPT (usam apenas a parte decoder, modelo Transformer autorregressivo)
  • Modelos BERT (usam apenas a parte encoder, modelo Transformer autorcodificador)
  • Modelos BART/T5 (usam encoder-decoder)

Arquiteturas e Checkpoints

Na pesquisa com modelos Transformer, alguns termos aparecem com frequência: arquitetura (architecture), checkpoint e modelo. Seus significados diferem ligeiramente:

  • Arquitetura: Define a estrutura básica e as operações fundamentais do modelo.
  • Checkpoint: Um estado específico do treinamento do modelo, contendo os pesos naquele momento.
  • Modelo: Termo genérico que pode se referir tanto à arquitetura quanto ao checkpoint.

Por exemplo, BERT é uma arquitetura, enquanto bert-base-cased é um checkpoint treinado pela equipe do Google. No entanto, podemos referir-nos a "o modelo BERT" ou "o modelo bert-base-cased".

A API de Inferência

O Model Hub contém checkpoints de modelos multilíngues. Você pode otimizar a busca por modelos clicando nas etiquetas de idioma e selecionando modelos que geram texto em outro idioma.

Ao selecionar um modelo, você encontrará um widget chamado Inference API, que permite testar o modelo diretamente na página, inserindo texto personalizado e visualizando os resultados. Isso permite testar rapidamente a funcionalidade do modelo antes de baixá-lo.

  1. Usando o pipeline para processamento de PLN

Nesta seção, exploraremos o que os modelos Transformer podem fazer e utilizaremos a primeira ferramenta da biblioteca 🤗 Transformers: o pipeline.

A biblioteca Transformers fornece funcionalidades para criar e compartilhar modelos. O Model Hub contém milhares de modelos pré-treinados disponíveis para download e uso. Você também pode enviar seus próprios modelos para o Hub.

O objeto mais básico na biblioteca Transformers é o pipeline. Ele conecta o modelo com seus pré-processamento e pós-processamento necessários, permitindo inserir qualquer texto diretamente e obter uma resposta compreensível:

from transformers import pipeline

classificador = pipeline("análise-de-sentimento")
classificador("Estou esperando por um curso da HuggingFace há toda a minha vida.")

Resultado:

[{'label': 'POSITIVO', 'score': 0.9598047137260437}]

Podemos até passar várias frases:

classificador([
    "Estou esperando por um curso da HuggingFace há toda a minha vida.", 
    "Eu odeio isso tanto!"
])

Resultado:

[{'label': 'POSITIVO', 'score': 0.9598047137260437},
 {'label': 'NEGATIVO', 'score': 0.9994558095932007}]

Por padrão, este pipeline seleciona um modelo específico pré-treinado e ajustado para aálise de sentimento em inglês. Ao criar o objeto classificador, o modelo é baixado e armazenado em cache. Se você executar o comando novaemnte, o modelo em cache será usado, sem necessidade de download.

Quando texto é passado para o pipeline, três etapas principais estão envolvidas:

  1. Pré-processamento: O texto é preparado no formato que o modelo pode entender.
  2. Entrada no modelo: O modelo é construído e a entrada pré-processada é passada para ele.
  3. Pós-processamento: As previsões do modelo são procesadas para que possam ser compreendidas.

Alguns pipelines disponíveis incluem:

  • feature-extraction (obter representações vetoriais do texto)
  • fill-mask (preencher lacunas em texto fornecido)
  • ner (reconhecimento de entidades nomeadas)
  • question-answering (resposta a perguntas)
  • sentiment-analysis (análise de sentimento)
  • summarization (geração de resumos)
  • text-generation (geração de texto)
  • translation (tradução)
  • zero-shot-classification (classificação de zero-shot)

A tabela a seguir resume quais arquiteturas são usadas para diferentes tarefas:

Modelo Exemplos Tarefa
Encoder ALBERT, BERT, DistilBERT, ELECTRA, RoBERTa Classificação de sentenças, reconhecimento de entidades, resposta extrativa
Decoder CTRL, GPT, GPT-2, Transformer XL Geração de texto
Encoder-decoder BART, T5, Marian, mBART Gerência de resumos, tradução, resposta gerativa
  1. Por trás do pipeline

Vamos examinar o que acontece nos bastidores quando executamos o código da seção anterior:

from transformers import pipeline

classificador = pipeline("análise-de-sentimento")
classificador([
    "Estou esperando por um curso da HuggingFace há toda a minha vida.", 
    "Eu odeio isso tanto!"
])

Como vimos no Capítulo 1, este pipeline combina três etapas: pré-processamento, passagem da entrada pelo modelo e pós-processamento.

Pré-processamento com tokenizer

Assim como outras redes neurais, os modelos Transformer não podem processar texto bruto diretamente. Portanto, o primeiro passo do nosso pipeline é converter o texto de entrada em números que o modelo possa entender. Para isso, usamos um tokenizer, que é responsável por:

  • Dividir a entrada em tokens (palavras, subpalavras ou símbolos)
  • Mapear cada token para um inteiro
  • Adicionar outras entradas que podem ser úteis para o modelo

Usando a classe AutoTokenizer e seu método from_pretrained, garantimos que todo esse pré-processamento seja feito exatamente da mesma forma que durante o pré-treinamento do modelo.

from transformers import AutoTokenizer

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

textos_brutos = [
    "Estou esperando por um curso da HuggingFace há toda a minha vida.", 
    "Eu odeio isso tanto!"
]
entradas = tokenizer(textos_brutos, padding=True, truncation=True, return_tensors="pt")

Seleção do modelo

Podemos baixar nosso modelo pré-treinado da mesma forma que o tokenizer. A biblioteca Transformers fornece a classe AutoModel, que também tem um método from_pretrained:

from transformers import AutoModel

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
modelo = AutoModel.from_pretrained(checkpoint)

A classe AutoModel e suas classes relacionadas são, na verdade, wrappers simples para os vários modelos disponíveis na biblioteca. Ela pode adivinhar automaticamente a arquitetura de modelo adequada para seu checkpoint e instanciar o modelo usando essa arquitetura.

Neste código, baixamos o mesmo checkpoint usado anteriormente no pipeline (que já deve estar em cache) e o usamos para instanciar um modelo. No entanto, esta arquitetura contém apenas os módulos básicos do Transformer: dada alguma entrada, ela nos retorna o que chamamos de estados ocultos (hidden states). Embora esses estados ocultos sejam úteis por si só, eles geralmente são a entrada para outra parte do modelo (model head).

Model heads

Podemos usar a mesma arquitetura de modelo para executar diferentes tarefas, mas cada tarefa está associada a um model head diferente.

Os model heads pegam os vetores de alta dimensão (logits) como entrada e os projetam em diferentes dimensões. Eles geralmente consistem em uma ou mais camadas lineares:

Na PLN, existem diferentes arquiteturas de modelo, cada uma projetada para lidar com tarefas específicas. Alguns exemplos de model heads incluem:

  • Model (recupera os estados ocultos)
  • ForCausalLM
  • ForMaskedLM
  • ForMultipleChoice
  • ForQuestionAnswering
  • ForSequenceClassification
  • ForTokenClassification

Para classificação de sentimento, precisamos de um model head para classificação de sequência (capaz de classificar frases como positivas ou negativas). Portanto, na verdade não usaremos a classe AutoModel, mas sim AutoModelForSequenceClassification:

from transformers import AutoModelForSequenceClassification

checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
modelo = AutoModelForSequenceClassification.from_pretrained(checkpoint)
saidas = modelo(**entradas)

Pós-processamento

Os valores que obtemos como saída do modelo nem sempre são imediatamente compreensíveis. Vamos examinar:

print(saidas.logits)

Saída:

tensor([[-1.5607,  1.6123],
        [ 4.1692, -3.3464]], grad_fn=<addmmbackward>)
</addmmbackward>

Nosso modelo previu [-1.5607, 1.6123] para a primeira frase e [4.1692, -3.3464] para a segunda. Esses não são probabilidades, mas sim logits - os escores brutos não normalizados da saída da última camada do modelo. Para convertê-los em probabilidades, eles precisam passar por uma camada SoftMax.

import torch

previsoes = torch.nn.functional.softmax(saidas.logits, dim=-1)
print(previsoes)

Saída:

tensor([[4.0195e-02, 9.5980e-01],
        [9.9946e-01, 5.4418e-04]], grad_fn=<softmaxbackward>)
</softmaxbackward>

Agora a saída são escores de probéis reconhecíveis. Para obter os rótulos correspondentes a cada posição, podemos verificar a propriedade id2label da configuração do modelo:

modelo.config.id2label

Saída:

{0: 'NEGATIVO', 1: 'POSITIVO'}

  1. Ajuste de modelos pré-treinados com a API Trainer

Nesta seção, exploraremos como ajustar um modelo pré-treinado para seu próprio dataset. Você aprenderá:

  • Como preparar grandes datasets do Model Hub
  • Como usar a API de alto nível Trainer para ajustar o modelo
  • Como usar um loop de treinamento personalizado
  • Como utilizar a biblioteca 🤗 Accelerate para executar loops personalizados em configurações distribuídas

Download de datasets do Hub

O Hub não contém apenas modelos; também possui múltiplos datasets em diferentes línguas. O dataset MRPC é um dos 10 datasets que compõem o benchmark GLUE, usado para medir o desempenho de modelos de ML em 10 diferentes tarefas de classificação de texto.

A biblioteca Datasets fornece um comando simples para baixar e armazenar em cache datasets do Hub. Podemos baixar o dataset MRPC da seguinte forma:

from datasets import load_dataset

datasets_brutos = load_dataset("glue", "mrpc")

Isso nos dá um objeto DatasetDict contendo conjuntos de treinamento, validação e teste. O conjunto de treinamento tem 3.668 pares de frases, o de validação tem 408 pares, e o de teste tem 1.725 pares. Cada par contém quatro colunas: 'sentence1', 'sentence2', 'label' e 'idx'.

Pré-processamento de datasets

Podemos usar o tokenizer para converter o texto em números que o modelo possa entender:

from transformers import AutoTokenizer

checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

def funcao_tokenizacao(exemplo):
    return tokenizer(exemplo["sentence1"], exemplo["sentence2"], truncation=True)

datasets_tokenizados = datasets_brutos.map(funcao_tokenizacao, batched=True)

Para manter nossos dados no formato de dataset, usaremos o método mais flexível Dataset.map. Este método pode concluir mais tarefas de pré-processamento além da tokenization. O método map aplica a mesma função a cada elemento do dataset, então definimos uma função para tokenizar as entradas.

Na função de tokenization, omitimos o parâmetro padding, porque é mais eficiente preencher até o comprimento máximo do lote do que preencher todos os comprimentos máximos do dataset. Quando os comprimentos das sequências de entrada são muito inconsistentes, isso pode economizar muito tempo e poder de processamento!

Usando a API Trainer no PyTorch

Como o PyTorch não fornece um loop de treinamento pronto para uso, a biblioteca 🤗 Transformers escreveu a API Trainer, que é um loop de treinamento e avaliação simples mas completo para PyTorch, otimizado para 🤗 Transformers, com muitas opções de treinamento e funcionalidades integradas, suporte para treinamento distribuído multi-GPU/TPU e precisão mista.

Os principais parâmetros do Trainer incluem:

  • Modelo: O modelo para treinamento, avaliação ou previsão
  • args (TrainingArguments): Parâmetros de ajuste de treinamento
  • data_collator: Função para processar em lote train_dataset ou eval_dataset
  • train_dataset: Conjunto de treinamento
  • eval_dataset: Conjunto de validação
  • compute_metrics: Função para calcular métricas de avaliação

Treinamento

Primeiro, definimos os argumentos de treinamento:

from transformers import TrainingArguments

argumentos_treinamento = TrainingArguments("test-trainer")

O único parâmetro obrigatório é o diretório onde o modelo ou checkpoints serão salvos. Todos os outros parâmetros podem usar valores padrão.

Em seguida, definimos o modelo:

from transformers import AutoModelForSequenceClassification

modelo = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

Ao instanciar este modelo pré-treinado, um aviso será exibido. Isso ocorre porque o BERT não foi pré-treinado em classificação de pares de frases, então o head pré-treinado foi descartado e um novo head adequado para classificação de sequência foi adicionado. O aviso indica que alguns pesos não foram usados (correspondentes ao head pré-treinamento descartado) enquanto outros foram inicializados aleatoriamente (parte do novo head).

Agora podemos definir um Trainer, passando todos os objetos que construímos:

from transformers import Trainer, DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

treinador = Trainer(
    modelo,
    argumentos_treinamento,
    train_dataset=datasets_tokenizados["train"],
    eval_dataset=datasets_tokenizados["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
)

Para ajustar o modelo em nosso dataset, simplesmente chamamos o método train do Trainer:

treinador.train()

Funções de avaliação

O Trainer não calcula métricas de avaliação automaticamente. Para fazer isso, precisamos fornecer uma função compute_metrics. Esta função deve:

  • Receber um parâmetro EvalPrediction (contendo previsões e rótulos)
  • Retornar um dicionário com nomes de métricas como chaves e valores de métricas como valores

Vamos construir nossa função compute_metrics:

from datasets import load_metric
import numpy as np

def compute_metrics(eval_preds):
    metric = load_metric("glue", "mrpc")
    logits, labels = eval_preds
    previsoes = np.argmax(logits, axis=-1)
    return metric.compute(previsoes=previsoes, referencias=labels)

Agora, definimos os argumentos de treinamento para avaliar a cada época e criamos um novo Trainer com nossa função compute_metrics:

argumentos_treinamento = TrainingArguments("test-trainer", evaluation_strategy="epoch")
modelo = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

treinador = Trainer(
    modelo,
    argumentos_treinamento,
    train_dataset=datasets_tokenizados["train"],
    eval_dataset=datasets_tokenizados["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics,
)

Iniciamos um novo treinamento executando:

treinador.train()

  1. Informações complementares

Por que usar o Trainer para ajuste fino?

Modelos pré-treinados podem ser usados de duas maneiras:

  • Extração de características: O modelo pré-treinado não é treinado posteriormente, seus pesos não são ajustados
  • Ajuste fino: O modelo pré-treinado é treinado por algumas épocas para uma tarefa específica, ajustando seus pesos

Embora o artigo do BERT mencione ambos os métodos, não é recomendado usar o modelo sem treinamento para prever resultados na prática. Os autores do Hugging Face recomendam usar o Trainer para treinar modelos, pois é uma API de loop de treinamento e avaliação simples mas completa para PyTorch, otimizada para Transformers.

Principais parâmetros do TrainingArguments

Os parâmetros mais comuns do TrainingArguments incluem:

  • output_dir: Diretório para salvar as previsões e checkpoints do modelo
  • evaluation_strategy: Estratégia de avaliação ("no", "step" ou "epoch")
  • learning_rate: Taxa de aprendizado do otimizador AdamW (padrão: 5e-5)
  • weight_decay: Decaimento de peso (padrão: 0)
  • save_strategy: Estratégia de salvamento ("no", "epoch" ou "steps")
  • fp16: Usar precisão de 16 bits durante o treinamento
  • num_train_epochs: Número de épocas para treinamento (padrão: 3)
  • load_best_model_at_end: Carregar o melhor modelo no final do treinamento

Diferentes métodos de carregamento de modelos

A classe AutoModel e suas classes relacionadas são wrappers simples para os vários modelos disponíveis na biblioteca. Ela pode adivinhar automaticamente a arquitetura de modelo adequada para seu checkpoint.

No entanto, se você souber qual tipo de modelo deseja usar, pode usar diretamente a classe que define sua arquitetura. Por exemplo, para um modelo BERT:

from transformers import BertConfig, BertModel

# Construindo a configuração
config = BertConfig()

# Construindo o modelo a partir da configuração
modelo = BertModel(config)

A configuração contém muitas propriedades usadas para construir o modelo, como tamanho oculto, número de camadas, etc.

Para carregar um modelo pré-treinado, usamos o método from_pretrained:

from transformers import BertModel

modelo = BertModel.from_pretrained("bert-base-cased")

Podemos substituir BertModel pela classe AutoModel, que fará o mesmo trabalho. Usar AutoModel é benéfico porque permite que seu código funcione com diferentes checkpoints, mesmo que tenham arquiteturas ligeiramente diferentes, desde que sejam compatíveis com a tarefa.

Técnica de padding dinâmico

No PyTorch, o DataLoader tem um parâmetro chamado função collate. É responsável por agrupar um lote de amostras. Como temos sequências de entrada de comprimentos diferentes, precisamos preenchê-las (como entradas do modelo, os tensores em um lote devem ter o mesmo comprimento).

É mais eficiente preencher até o comprimento máximo do lote do que preencher todos os comprimentos máximos do dataset. Para fazer isso na prática, devemos definir uma função collate que aplique a quantidade correta de preenchimento para cada lote de dados.

A biblioteca 🤗 Transformers fornece essa funcionalidade através de DataCollatorWithPadding. Ao instanciá-lo, precisamos de um tokenizer (para saber qual token de preenchimento usar e se o modelo prefere preenchimento à esquerda ou à direita) e ele executará todas as operações necessárias:

from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

Para testar, selecionamos amostras do conjunto de treinamento que queremos processar em lote. Removemos as colunas idx, sentence1 e sentence2, pois não precisamos delas e contêm strings (não podem criar tensores). Verificamos o comprimento de cada entrada no lote:

amostras = datasets_tokenizados["train"][:8]
amostras = {k: v for k, v in amostras.items() if k not in ["idx", "sentence1", "sentence2"]}
[len(x) for x in amostras["input_ids"]]

Obtemos sequências de comprimentos diferentes. O padding dinâmico significa que todas as sequências no lote devem ser preenchidas para o comprimento máximo do lote (67 neste caso). Sem padding dinâmico, todas as amostras deveriam ser preenchidas para o comprimento máximo do dataset ou para o comprimento máximo que o modelo pode aceitar.

Tags: Transformers Hugging Face Fine-tuning nlp Pytorch

Publicado em 6-28 09:34