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
- História dos Transformers
- Arquiteturas e checkpoints
- A API de Inferência
- 2. Usando o pipeline para processamento de PLN
- 3. Por trás do pipeline
- Pré-processamento com tokenizer
- Seleção do modelo
- Model heads
- Pós-processamento
- 4. Ajuste de modelos pré-treinados com a API Trainer
- Download de datasets do Hub
- Pré-processamento de datasets
- Utilizando a API Trainer no PyTorch
- Treinamento
- Funções de avaliação
- 5. Informações complementares
- Por que usar o Trainer para ajuste fino?
- Principais parâmetros do TrainingArguments
- Diferentes métodos de carregamento de modelos
- Técnica de padding dinâmico
- 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.
- 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:
- Pré-processamento: O texto é preparado no formato que o modelo pode entender.
- Entrada no modelo: O modelo é construído e a entrada pré-processada é passada para ele.
- 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 |
- 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'}
- 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()
- 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.