Tutorial: Ajuste fino do BERT para análise de sentimentos
- Tutorial: Afinação do BERT para Análise de Sentimentos
- A - Introdução¶
- B - Setup¶
- C - Linha de base: TF-IDF + Classificador Naive Bayes¶
- D - Afinação fina do BERT¶
- E - Conclusão¶
Tutorial: Afinação do BERT para Análise de Sentimentos
Originalmente publicado por Chris Tran, investigador de aprendizagem automática da Skim AI.
A - Introdução¶
Nos últimos anos, a comunidade de PNL assistiu a muitos avanços no Processamento de Linguagem Natural, especialmente a mudança para a aprendizagem por transferência. Modelos como o ELMo, o ULMFiT da fast.ai, o Transformer e o GPT da OpenAI permitiram aos investigadores obter resultados de ponta em vários parâmetros de referência e forneceram à comunidade grandes modelos pré-treinados com elevado desempenho. Esta mudança na PNL é vista como o momento ImageNet da PNL, uma mudança na visão computacional há alguns anos, quando as camadas inferiores das redes de aprendizagem profunda com milhões de parâmetros treinados numa tarefa específica podem ser reutilizadas e afinadas para outras tarefas, em vez de treinar novas redes a partir do zero.
Um dos maiores marcos na evolução da PNL recentemente é o lançamento do BERT da Google, que é descrito como o início de uma nova era na PNL. Neste bloco de notas, vou utilizar a função HuggingFace's transformadores
para afinar o modelo BERT pré-treinado para uma tarefa de classificação. Em seguida, comparo o desempenho do BERT com um modelo de base, no qual utilizo um vectorizador TF-IDF e um classificador Naive Bayes. O transformadores
A biblioteca ajuda-nos a afinar rápida e eficazmente o modelo BERT de última geração e a obter uma taxa de precisão 10% mais elevado do que o modelo de base.
Referência:
Para compreender Transformador (a arquitetura em que se baseia o BERT) e aprender a implementar o BERT, recomendo vivamente a leitura das seguintes fontes:
- O BERT ilustrado, ELMo, e co.: Um guia muito claro e bem escrito para compreender o BERT.
- A documentação do
transformadores
biblioteca - Tutorial de afinação do BERT com PyTorch por Chris McCormick: Um tutorial muito detalhado que mostra como usar o BERT com a biblioteca HuggingFace do PyTorch.
B - Configuração¶
1. Carregar bibliotecas essenciais¶
Em [0]:
importar os importar re from tqdm import tqdm import numpy as np import pandas as pd import matplotlib.pyplot as plt %matplotlib inline
2. Conjunto de dados¶
2.1. Descarregar o conjunto de dados¶
Em [0]:
# Descarregar dados importar pedidos request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf") com open("dados.zip", "wb") as file: file.write(request.content) # Descompactar dados importar zipfile with zipfile.ZipFile('data.zip') as zip: zip.extractall('dados')
2.2. Dados do comboio de carga¶
Os dados do comboio têm 2 ficheiros, cada um contendo 1700 tweets com queixas/não queixas. Todos os tweets nos dados contêm pelo menos uma hashtag de uma companhia aérea.
Vamos carregar os dados de treino e rotulá-los. Como utilizamos apenas os dados de texto para classificar, eliminaremos as colunas sem importância e manteremos apenas id
, tweet
e etiqueta
colunas.
Em [0]:
# Carregar dados e definir etiquetas data_complaint = pd.read_csv('data/complaint1700.csv') data_complaint['label'] = 0 data_non_complaint = pd.read_csv('data/noncomplaint1700.csv') data_non_complaint['label'] = 1 # Concatenar dados de reclamação e de não reclamação dados = pd.concat([dados_reclamação, dados_não_reclamação], eixo=0).reset_index(drop=True) # Drop 'companhia aérea' coluna data.drop(['companhia aérea'], inplace=True, axis=1) # Apresentar 5 amostras aleatórias data.sample(5)
Saída[0]:
id | tweet | etiqueta | |
---|---|---|---|
1988 | 24991 | Que grande receção de regresso. É de rir. Desembarque... | 1 |
1294 | 72380 | Muito desiludido com a @JetBlue esta noite. O voo... | 0 |
1090 | 127893 | @united os meus amigos estão a passar um mau bocado... | 0 |
553 | 58278 | @united tudo o que eu quero para o Natal é um saco perdido que... | 0 |
2075 | 30695 | sim, não vou mentir... super interessado em experimentar... | 1 |
Dividiremos aleatoriamente todos os dados de treino em dois conjuntos: um conjunto de treino com 90% dos dados e um conjunto de validação com 10% dos dados. Efectuaremos a afinação dos hiperparâmetros utilizando a validação cruzada no conjunto de treino e utilizaremos o conjunto de validação para comparar modelos.
Em [0]:
from sklearn.model_selection import train_test_split X = dados.tweet.values y = dados.label.values X_treinamento, X_val, y_treinamento, y_val = train_test_split(X, y, test_size=0.1, random_state=2020)
2.3. Dados do ensaio de carga¶
Os dados de teste contêm 4555 exemplos sem etiqueta. Cerca de 300 exemplos são tweets não queixosos. A nossa tarefa é identificar os seus id
e verificar manualmente se os nossos resultados estão correctos.
Em [0]:
# Dados do teste de carga dados_teste = pd.read_csv('dados/teste_dados.csv') # Manter colunas importantes dados_teste = dados_teste[['id', 'tweet']] # Apresentar 5 amostras dos dados de teste dados_teste.amostra(5)
Saída[0]:
id | tweet | |
---|---|---|
1539 | 59336 | Voo da @AmericanAir atrasado mais de 2 horas por n... |
607 | 24101 | @SouthwestAir Continuo a receber esta mensagem de erro... |
333 | 13179 | à espera no #SeaTac para embarcar no meu voo da @JetBlue... |
2696 | 102948 | Odeio quando passo pelo processo de seleção antecipada de lugares... |
3585 | 135638 | que vergonha @AlaskaAir |
3. Criar uma GPU para a formação¶
O Google Colab oferece GPUs e TPUs gratuitas. Uma vez que vamos treinar uma rede neural grande, é melhor utilizar estas funcionalidades.
É possível adicionar uma GPU indo ao menu e seleccionando:
Tempo de execução -> Alterar tipo de tempo de execução -> Acelerador de hardware: GPU
Em seguida, precisamos de executar a seguinte célula para especificar a GPU como o dispositivo.
Em [0]:
importar torch se torch.cuda.is_available(): device = torch.device("cuda") print(f'Existem {torch.cuda.device_count()} GPU(s) disponíveis.') print('Nome do dispositivo:', torch.cuda.get_device_name(0)) senão: print('Nenhuma GPU disponível, usando a CPU em seu lugar.') dispositivo = torch.device("cpu")
Há 1 GPU(s) disponível(is). Nome do dispositivo: Tesla T4
C - Linha de base: TF-IDF + Classificador Naive Bayes¶¶
Nesta abordagem de base, primeiro utilizamos o TF-IDF para vetorizar os nossos dados de texto. De seguida, utilizamos o modelo Naive Bayes como classificador.
Porquê o Naive Bayse? Experimentei diferentes algoritmos de aprendizagem automática, incluindo Random Forest, Support Vectors Machine, XGBoost e observei que o Naive Bayes apresenta o melhor desempenho. Em Guia do Scikit-learn para escolher o estimador correto, também se sugere que o Naive Bayes deve ser utilizado para dados de texto. Também tentei utilizar o SVD para reduzir a dimensionalidade; no entanto, não obteve um melhor desempenho.
1. Preparação dos dados¶
1.1. Pré-processamento¶
No modelo de saco de palavras, um texto é representado como o saco das suas palavras, sem ter em conta a gramática e a ordem das palavras. Por isso, queremos remover palavras de paragem, pontuações e caracteres que não contribuem muito para o significado da frase.
Em [0]:
importar nltk # Descomente para descarregar "stopwords" nltk.download("stopwords") from nltk.corpus import stopwords def text_preprocessing(s): """ - Reduzir a frase a minúsculas - Alterar "'t" para "não" - Remover "@nome" - Isolar e remover as pontuações, exceto "?" - Remover outros caracteres especiais - Remover palavras de paragem, exceto "not" e "can" - Remover os espaços em branco no final """ s = s.lower() # Alterar 't para 'not' s = re.sub(r"'t", " not", s) # Remover @nome s = re.sub(r'(@.*?)[s]', ' ' ', s) # Isolar e remover as pontuações, exceto '?' s = re.sub(r'(['".()!?\/,])', r' 1 ', s) s = re.sub(r'[^ws?]', ' ' ', s) # Remover alguns caracteres especiais s = re.sub(r'([;:|-"n])', ' ' ', s) # Remover as stopwords exceto 'not' e 'can' s = " ".join([palavra for palavra in s.split() se palavra não estiver em stopwords.words('english') ou palavra em ['not', 'can']]) # Remover espaços em branco à direita s = re.sub(r's+', ' ' ', s).strip() retornar s
[nltk_data] Baixando o pacote stopwords para /root/nltk_data... [nltk_data] O pacote stopwords já está atualizado!
1.2. Vectorização TF-IDF¶
Na recuperação de informação, TF-IDF, abreviatura de frequência de termos - frequência inversa de documentosé uma estatística numérica que se destina a refletir a importância de uma palavra para um documento numa coleção ou corpus. Utilizaremos o TF-IDF para vetorizar os nossos dados de texto antes de os alimentarmos com algoritmos de aprendizagem automática.
Em [0]:
%%time from sklearn.feature_extraction.text import TfidfVectorizer # Pré-processar texto X_train_preprocessed = np.array([text_preprocessing(text) for text in X_train]) X_val_preprocessed = np.array([text_preprocessing(text) for text in X_val]) # Calcular TF-IDF tf_idf = TfidfVectorizer(ngram_range=(1, 3), binary=True, smooth_idf=False) X_train_tfidf = tf_idf.fit_transform(X_train_preprocessed) X_val_tfidf = tf_idf.transform(X_val_preprocessed)
Tempos de CPU: utilizador 5.47 s, sys: 519 ms, total: 5,99 s Tempo de parede: 6 s
2. Treinar o classificador Naive Bayes¶
2.1. Afinação de hiperparâmetros¶
Utilizaremos a validação cruzada e a pontuação AUC para ajustar os hiperparâmetros do nosso modelo. A função get_auc_CV
devolverá a pontuação AUC média da validação cruzada.
Em [0]:
from sklearn.model_selection import StratifiedKFold, cross_val_score def get_auc_CV(model): """ Devolve a pontuação AUC média da validação cruzada. """ # Definir KFold para baralhar os dados antes da divisão kf = StratifiedKFold(5, shuffle=True, random_state=1) # Obter pontuações AUC auc = cross_val_score( modelo, X_treino_tfidf, y_treino, pontuação="roc_auc", cv=kf) return auc.mean()
O MultinominalNB
têm apenas um hipterparâmetro - alfa. O código abaixo ajudar-nos-á a encontrar o valor alfa que nos dá a pontuação CV AUC mais elevada.
Em [0]:
de sklearn.naive_bayes import MultinomialNB res = pd.Series([get_auc_CV(MultinomialNB(i)) for i in np.arange(1, 10, 0.1)], index=np.arange(1, 10, 0.1)) melhor_alfa = np.round(res.idxmax(), 2) print('Melhor alfa: ', melhor_alfa) plt.plot(res) plt.title('AUC vs. Alpha') plt.xlabel('Alpha') plt.ylabel('AUC') plt.show()
Melhor alfa: 1.3
2.2. Avaliação no conjunto de validação¶
Para avaliar o desempenho do nosso modelo, calcularemos a taxa de precisão e a pontuação AUC do nosso modelo no conjunto de validação.
Em [0]:
from sklearn.metrics import accuracy_score, roc_curve, auc def evaluate_roc(probs, y_true): """ - Imprimir AUC e exatidão no conjunto de teste - Traçar ROC @params probs (np.array): um conjunto de probabilidades previstas com a forma (len(y_true), 2) @params y_true (np.array): uma matriz dos valores verdadeiros com forma (len(y_true),) """ preds = probs[:, 1] fpr, tpr, threshold = roc_curve(y_true, preds) roc_auc = auc(fpr, tpr) print(f'AUC: {roc_auc:.4f}') # Obter a exatidão do conjunto de teste y_pred = np.where(preds >= 0.5, 1, 0) exatidão = exactidão_pontuação(y_verdadeiro, y_pred) print(f'Accuracy: {accuracy*100:.2f}%') # Plotar ROC AUC plt.title('Característica de funcionamento do recetor') plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc) plt.legend(loc = 'lower right') plt.plot([0, 1], [0, 1],'r--') plt.xlim([0, 1]) plt.ylim([0, 1]) plt.ylabel('Taxa de Verdadeiros Positivos') plt.xlabel('Taxa de falsos positivos') plt.show()
Combinando o TF-IDF e o algoritmo Naive Bayes, atingimos uma taxa de precisão de 72.65% no conjunto de validação. Este valor é o desempenho de base e será utilizado para avaliar o desempenho do nosso modelo BERT de afinação fina.
Em [0]:
# Calcular probabilidades previstas nb_model = MultinomialNB(alpha=1.8) nb_model.fit(X_train_tfidf, y_train) probs = nb_model.predict_proba(X_val_tfidf) # Avaliar o classificador avaliar_roc(probs, y_val)
AUC: 0.8451 Precisão: 75,59%
D - Afinação do BERT¶
1. Instalar a biblioteca Hugging Face¶
A biblioteca de transformadores do Hugging Face contém a implementação PyTorch de modelos de PNL de última geração, incluindo BERT (da Google), GPT (da OpenAI) ... e pesos de modelos pré-treinados.
Em [1]:
#!pip instalar transformadores
2. Tokenização e formatação de entrada¶
Antes de tokenizar o nosso texto, vamos efetuar um ligeiro processamento no nosso texto, incluindo a remoção de menções a entidades (por exemplo, @united) e alguns caracteres especiais. O nível de processamento aqui é muito menor do que nas abordagens anteriores porque o BERT foi treinado com as frases inteiras.
Em [0]:
def text_preprocessing(text): """ - Remover menções de entidades (ex.: '@united') - Corrigir erros (por exemplo, '&' para '&') @param text (str): uma cadeia de caracteres a ser processada. @retorno text (Str): a cadeia de caracteres processada. """ # Remove '@nome' text = re.sub(r'(@.*?)[s]', ' ' ', text) # Substituir '&' por '&' texto = re.sub(r'&', '&', texto) # Remover os espaços em branco à direita text = re.sub(r's+', ' ' ', text).strip() devolver texto
Em [0]:
# Imprimir frase 0 print('Original: ', X[0]) print('Processado: ', text_preprocessing(X[0]))
Original: @united I'estou a ter problemas. Ontem fiz uma nova reserva para 24 horas depois da data prevista para o voo e agora não consigo fazer o login e o check-in. Podem ajudar-me? Processado: Estou a ter problemas. Ontem fiz uma nova reserva para 24 horas depois da data prevista para o voo e agora não consigo'fazer o login e o check-in. Podem ajudar-me?
2.1. Tokenizador BERT¶
Para aplicar o BERT pré-treinado, temos de utilizar o tokenizador fornecido pela biblioteca. Isto deve-se ao facto de (1) o modelo ter um vocabulário específico e fixo e (2) o tokenizador do BERT ter uma forma particular de lidar com palavras fora do vocabulário.
Além disso, temos de acrescentar símbolos especiais no início e no fim de cada frase, preencher e truncar todas as frases para um comprimento único e constante e especificar explicitamente quais são os símbolos de preenchimento com a "máscara de atenção".
O codificar_mais
do tokenizador BERT:
(1) dividir o nosso texto em tokens,
(2) acrescentar a menção especial [CLS]
e [SEP]
fichas, e
(3) converter esses tokens em índices do vocabulário do tokenizador,
(4) preencher ou truncar frases até ao comprimento máximo, e
(5) criar uma máscara de atenção.
Em [0]:
from transformers import BertTokenizer # Carrega o tokenizador BERT tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True) # Criar uma função para tokenizar um conjunto de textos def preprocessing_for_bert(data): """Executa os passos de pré-processamento necessários para o BERT pré-treinado. @param dados (np.array): Conjunto de textos a serem processados. @return input_ids (torch.Tensor): Tensor de ids de tokens a serem alimentados para um modelo. @return attention_masks (torch.Tensor): Tensor de índices que especificam quais tokens devem ser atendidos pelo modelo. """ # Criar listas vazias para armazenar as saídas input_ids = [] máscaras_de_atenção = [] # Para cada frase... para enviada nos dados: #codificar_mais
vai: # (1) Tokenizar a frase # (2) Acrescentar o[CLS]
e[SEP]
ficha no início e no fim # (3) Truncar/encaminhar a frase para o comprimento máximo # (4) Mapear os tokens para os seus IDs # (5) Criar máscara de atenção # (6) Devolver um dicionário de resultados codificado_sent = tokenizer.encode_plus( text=text_preprocessing(sent), # Pré-processar frase add_special_tokens=True, # Adicionar[CLS]
e[SEP]
max_length=MAX_LEN, # Comprimento máximo para truncar/colocar pad_to_max_length=True, # Comprimento máximo da frase #return_tensors='pt', # Devolver tensor PyTorch return_attention_mask=True # Devolver máscara de atenção ) # Acrescentar os outputs às listas input_ids.append(encoded_sent.get('input_ids')) attention_masks.append(encoded_sent.get('attention_mask')) # Converter listas em tensores input_ids = torch.tensor(input_ids) attention_masks = torch.tensor(attention_masks) return input_ids, attention_masks
Antes da tokenização, precisamos de especificar o comprimento máximo das nossas frases.
Em [0]:
# Concatenar dados de treino e dados de teste todos_tweets = np.concatenate([dados.tweet.valores, dados_teste.tweet.valores]) # Codificar os nossos dados concatenados tweets_codificados = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets] # Encontrar o comprimento máximo max_len = max([len(sent) for sent in encoded_tweets]) print('Comprimento máximo: ', max_len)
Comprimento máximo: 68
Agora vamos tokenizar os nossos dados.
Em [0]:
# EspecificarMAX_LEN
MAX_LEN = 64 # Imprimir a frase 0 e as suas identificações simbólicas codificadas token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy()) print('Original: ', X[0]) print('Token IDs: ', token_ids) # Função de execuçãopré-processamento_para_bert
no conjunto de treino e no conjunto de validação print('Tokenizar dados...') train_inputs, train_masks = preprocessing_for_bert(X_train) val_inputs, val_masks = preprocessing_for_bert(X_val)
Original: @united I'estou a ter problemas. Ontem fiz uma nova reserva para 24 horas depois da data prevista para o voo e agora não consigo fazer o login e o check-in. Podem ajudar-me? IDs de token: [101, 1045, 1005, 1049, 2383, 3314, 1012, 7483, 1045, 2128, 8654, 2098, 2005, 2484, 2847, 2044, 1045, 2001, 4011, 2000, 4875, 1010, 2085, 1045, 2064, 1005, 1056, 8833, 2006, 1004, 4638, 1999, 1012, 2064, 2017, 2393, 1029, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] Tokenização de dados...
2.2. Criar DataLoader PyTorch¶
Vamos criar um iterador para o nosso conjunto de dados utilizando a classe DataLoader do torch. Isto ajudará a poupar memória durante o treino e aumentará a velocidade de treino.
Em [0]:
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler # Converter outros tipos de dados para torch.Tensor train_labels = torch.tensor(y_train) val_labels = torch.tensor(y_val) # Para um ajuste fino do BERT, os autores recomendam um tamanho de lote de 16 ou 32. tamanho_do_lote = 32 # Criar o DataLoader para o nosso conjunto de treino train_data = TensorDataset(train_inputs, train_masks, train_labels) train_sampler = RandomSampler(train_data) train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size) # Criar o DataLoader para o nosso conjunto de validação val_data = TensorDataset(val_inputs, val_masks, val_labels) val_sampler = SequentialSampler(val_data) val_dataloader = DataLoader(val_data, sampler=val_sampler, batch_size=batch_size)
3. Treinar o nosso modelo¶
3.1. Criar BertClassifier¶
O BERT-base é composto por 12 camadas transformadoras, cada uma das quais recebe uma lista de token embeddings e produz o mesmo número de embeddings com o mesmo tamanho (ou dimensões) oculto na saída. A saída da camada de transformação final da [CLS]
é utilizado como caraterística da sequência para alimentar um classificador.
O transformadores
biblioteca tem o BertForSequenceClassification
que foi concebida para tarefas de classificação. No entanto, vamos criar uma nova classe para podermos especificar a nossa própria escolha de classificadores.
De seguida, vamos criar uma classe BertClassifier com um modelo BERT para extrair a última camada oculta do [CLS]
e uma rede neural feed-forward de camada única oculta como classificador.
Em [0]:
%%time importar torch import torch.nn as nn from transformers import BertModel # Criar a classe BertClassfier class BertClassifier(nn.Module): """Modelo Bert para tarefas de classificação. """ def __init__(self, freeze_bert=False): """ @param bert: um objeto BertModel @param classificador: um classificador torch.nn.Module @param freeze_bert (bool): DefinirFalso
para afinar o modelo BERT """ super(BertClassifier, self).__init__() # Especificar o tamanho oculto do BERT, o tamanho oculto do nosso classificador e o número de etiquetas D_in, H, D_out = 768, 50, 2 # Instanciar o modelo BERT self.bert = BertModel.from_pretrained('bert-base-uncased') # Instanciar um classificador de avanço de uma camada self.classifier = nn.Sequential( nn.Linear(D_in, H), nn.ReLU(), #nn.Dropout(0.5), nn.Linear(H, D_out) ) # Congelar o modelo BERT if freeze_bert: for param in self.bert.parameters(): param.requires_grad = False def forward(self, input_ids, attention_mask): """ Fornece a entrada ao BERT e ao classificador para calcular os logits. @param input_ids (torch.Tensor): um tensor de entrada com forma (batch_size, max_length) @param mascara_de_atenção (torch.Tensor): um tensor que contém informações sobre a máscara de com a forma (tamanho_do_lote, comprimento_máximo) @return logits (torch.Tensor): um tensor de saída com a forma (batch_size, num_labels) """ # Alimentar o BERT com dados de entrada outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask) # Extrair o último estado oculto da ficha[CLS]
para a tarefa de classificação last_hidden_state_cls = outputs[0][:, 0, :] # Introduzir a entrada no classificador para calcular logits logits = self.classifier(last_hidden_state_cls) return logits
Tempos de CPU: utilizador 38 µs, sys: 0 ns, total: 38 µs Tempo de parede: 40,1 µs
3.2. Optimizador e programador da taxa de aprendizagem¶
Para afinar o nosso classificador Bert, precisamos de criar um optimizador. Os autores recomendam os seguintes hiper-parâmetros:
- Tamanho do lote: 16 ou 32
- Taxa de aprendizagem (Adam): 5e-5, 3e-5 ou 2e-5
- Número de épocas: 2, 3, 4
Huggingface forneceu a executar_cola.py um exemplo de implementação do script transformadores
biblioteca. No script, é utilizado o optimizador AdamW.
Em [0]:
from transformers import AdamW, get_linear_schedule_with_warmup def initialize_model(epochs=4): """Inicializar o Classificador Bert, o optimizador e o programador da taxa de aprendizagem. """ # Instanciar o Classificador Bert bert_classificador = BertClassificador(freeze_bert=False) # Dizer ao PyTorch para executar o modelo na GPU bert_classificador.to(dispositivo) # Criar o optimizador optimizador = AdamW(bert_classifier.parameters(), lr=5e-5, # Taxa de aprendizagem predefinida eps=1e-8 # Valor epsilon por defeito ) # Número total de passos de treino total_steps = len(train_dataloader) * epochs # Configurar o programador da taxa de aprendizagem programador = get_linear_schedule_with_warmup(optimizador, num_warmup_steps=0, # Valor por defeito num_training_steps=total_steps) return bert_classifier, optimizer, scheduler
3.3. Circuito de formação¶
Treinaremos o nosso classificador Bert durante 4 épocas. Em cada época, treinaremos o nosso modelo e avaliaremos o seu desempenho no conjunto de validação. Em mais pormenor, iremos:
Formação:
- Desempacotar os nossos dados do carregador de dados e carregá-los na GPU
- Zerar os gradientes calculados na passagem anterior
- Efetuar uma passagem para a frente para calcular logits e perdas
- Efetuar uma passagem para trás para calcular gradientes (
perda.para trás()
) - Recorte a norma dos gradientes para 1,0 para evitar "gradientes explosivos"
- Atualizar os parâmetros do modelo (
optimizador.passo()
) - Atualizar a taxa de aprendizagem (
programador.step()
)
Avaliação:
- Descompactar os nossos dados e carregá-los na GPU
- Passe para a frente
- Calcular a perda e a taxa de precisão no conjunto de validação
O guião abaixo é comentado com os detalhes do nosso ciclo de formação e avaliação.
Em [0]:
import random import time # Specify loss function loss_fn = nn.CrossEntropyLoss() def set_seed(seed_value=42): """Set seed for reproducibility. """ random.seed(seed_value) np.random.seed(seed_value) torch.manual_seed(seed_value) torch.cuda.manual_seed_all(seed_value) def train(model, train_dataloader, val_dataloader=None, epochs=4, evaluation=False): """Train the BertClassifier model. """ # Start training loop print("Start training...n") for epoch_i in range(epochs): # ======================================= # Training # ======================================= # Print the header of the result table print(f"{'Epoch':^7} | {'Batch':^7} | {'Train Loss':^12} | {'Val Loss':^10} | {'Val Acc':^9} | {'Elapsed':^9}") print("-"*70) # Measure the elapsed time of each epoch t0_epoch, t0_batch = time.time(), time.time() # Reset tracking variables at the beginning of each epoch total_loss, batch_loss, batch_counts = 0, 0, 0 # Put the model into the training mode model.train() # For each batch of training data... for step, batch in enumerate(train_dataloader): batch_counts +=1 # Load batch to GPU b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch) # Zero out any previously calculated gradients model.zero_grad() # Perform a forward pass. This will return logits. logits = model(b_input_ids, b_attn_mask) # Compute loss and accumulate the loss values loss = loss_fn(logits, b_labels) batch_loss += loss.item() total_loss += loss.item() # Perform a backward pass to calculate gradients loss.backward() # Clip the norm of the gradients to 1.0 to prevent "exploding gradients" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # Update parameters and the learning rate optimizer.step() scheduler.step() # Print the loss values and time elapsed for every 20 batches if (step % 20 == 0 and step != 0) or (step == len(train_dataloader) - 1): # Calculate time elapsed for 20 batches time_elapsed = time.time() - t0_batch # Print training results print(f"{epoch_i + 1:^7} | {step:^7} | {batch_loss / batch_counts:^12.6f} | {'-':^10} | {'-':^9} | {time_elapsed:^9.2f}") # Reset batch tracking variables batch_loss, batch_counts = 0, 0 t0_batch = time.time() # Calculate the average loss over the entire training data avg_train_loss = total_loss / len(train_dataloader) print("-"*70) # ======================================= # Evaluation # ======================================= if evaluation == True: # After the completion of each training epoch, measure the model's performance # on our validation set. val_loss, val_accuracy = evaluate(model, val_dataloader) # Print performance over the entire training data time_elapsed = time.time() - t0_epoch print(f"{epoch_i + 1:^7} | {'-':^7} | {avg_train_loss:^12.6f} | {val_loss:^10.6f} | {val_accuracy:^9.2f} | {time_elapsed:^9.2f}") print("-"*70) print("n") print("Training complete!") def evaluate(model, val_dataloader): """After the completion of each training epoch, measure the model's performance on our validation set. """ # Put the model into the evaluation mode. The dropout layers are disabled during # the test time. model.eval() # Tracking variables val_accuracy = [] val_loss = [] # For each batch in our validation set... for batch in val_dataloader: # Load batch to GPU b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch) # Compute logits with torch.no_grad(): logits = model(b_input_ids, b_attn_mask) # Compute loss loss = loss_fn(logits, b_labels) val_loss.append(loss.item()) # Get the predictions preds = torch.argmax(logits, dim=1).flatten() # Calculate the accuracy rate accuracy = (preds == b_labels).cpu().numpy().mean() * 100 val_accuracy.append(accuracy) # Compute the average accuracy and loss over the validation set. val_loss = np.mean(val_loss) val_accuracy = np.mean(val_accuracy) return val_loss, val_accuracy
Agora, vamos começar a treinar o nosso BertClassifier!
Em [0]:
set_seed(42) # Definir semente para reprodutibilidade bert_classifier, optimizer, scheduler = initialize_model(epochs=2) train(bert_classifier, train_dataloader, val_dataloader, epochs=2, evaluation=True)
Iniciar a formação... Época | Lote | Perda de trem | Perda de valor | Acerto de valor | Decorrido ---------------------------------------------------------------------- 1 | 20 | 0.630467 | - | - | 7.58 1 | 40 | 0.497330 | - | - | 7.01 1 | 60 | 0.502320 | - | - | 7.11 1 | 80 | 0.491438 | - | - | 7.19 1 | 95 | 0.486125 | - | - | 5.35 ---------------------------------------------------------------------- 1 | - | 0.524515 | 0.439601 | 78.81 | 35.54 ---------------------------------------------------------------------- Época | Lote | Perda de comboio | Perda de valor | Acerto de valor | Decorrido ---------------------------------------------------------------------- 2 | 20 | 0.287401 | - | - | 7.83 2 | 40 | 0.260870 | - | - | 7.60 2 | 60 | 0.287706 | - | - | 7.67 2 | 80 | 0.283311 | - | - | 7.87 2 | 95 | 0.280315 | - | - | 5.87 ---------------------------------------------------------------------- 2 | - | 0.279978 | 0.454067 | 80.40 | 38.31 ---------------------------------------------------------------------- Formação concluída!
3.4. Avaliação no conjunto de validação¶
O passo de previsão é semelhante ao passo de avaliação que efectuámos no ciclo de formação, mas mais simples. Vamos efetuar uma passagem para a frente para calcular logits e aplicar softmax para calcular probabilidades.
Em [0]:
import torch.nn.functional as F def bert_predict(model, test_dataloader): """Efectua uma passagem para a frente no modelo BERT treinado para prever probabilidades no conjunto de teste. """ # Colocar o modelo no modo de avaliação. As camadas de abandono são desactivadas durante # o tempo de teste. model.eval() todos_logits = [] # Para cada lote do nosso conjunto de testes... for batch in test_dataloader: # Carrega o lote para a GPU b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2] # Calcular logits com torch.no_grad(): logits = model(b_input_ids, b_attn_mask) all_logits.append(logits) # Concatenar os logits de cada lote todos_logits = torch.cat(todos_logits, dim=0) # Aplicar softmax para calcular as probabilidades probs = F.softmax(todos_logits, dim=1).cpu().numpy() return probs
Em [0]:
# Calcular as probabilidades previstas no conjunto de teste probs = bert_predict(bert_classifier, val_dataloader) # Avaliar o classificador de Bert evaluate_roc(probs, y_val)
AUC: 0.9048 Precisão: 80,59%
O Bert Classifer atinge uma pontuação AUC de 0,90 e uma taxa de precisão de 82,65% no conjunto de validação. Este resultado é 10 pontos melhor do que o método de base.
3.5. Treinar o nosso modelo em todos os dados de treino¶
Em [0]:
# Concatenar o conjunto de treino e o conjunto de validação dados_treino_completos = torch.utils.data.ConcatDataset([dados_treino, dados_valor]) full_train_sampler = RandomSampler(full_train_data) full_train_dataloader = DataLoader(full_train_data, sampler=full_train_sampler, batch_size=32) # Treinar o classificador Bert em todos os dados de treino set_seed(42) bert_classifier, optimizer, scheduler = initialize_model(epochs=2) train(bert_classifier, full_train_dataloader, epochs=2)
Iniciar a formação... Época | Lote | Perda de trem | Perda de valor | Acerto de valor | Decorrido ---------------------------------------------------------------------- 1 | 20 | 0.664452 | - | - | 8.63 1 | 40 | 0.587205 | - | - | 8.42 1 | 60 | 0.522831 | - | - | 8.44 1 | 80 | 0.476442 | - | - | 8.23 1 | 100 | 0.467542 | - | - | 8.10 1 | 106 | 0.483039 | - | - | 2.14 ---------------------------------------------------------------------- Época | Lote | Perda de comboio | Perda de valor | Acerto de valor | Decorrido ---------------------------------------------------------------------- 2 | 20 | 0.338174 | - | - | 8.36 2 | 40 | 0.296080 | - | - | 7.93 2 | 60 | 0.295626 | - | - | 7.96 2 | 80 | 0.277470 | - | - | 7.99 2 | 100 | 0.314746 | - | - | 8.07 2 | 106 | 0.293359 | - | - | 2.17 ---------------------------------------------------------------------- Formação concluída!
4. Previsões no conjunto de teste¶
4.1. Preparação dos dados¶
Vamos rever o nosso conjunto de testes em breve.
Em [0]:
dados_teste.amostra(5)
Saída[0]:
id | tweet | |
---|---|---|
471 | 18654 | Amigos e familiares: Nunca voem na @JetBlue. Absolutamente... |
1971 | 76265 | @DeltaAssist @rogerioad Nunca tive um profissional... |
23 | 672 | Primeiro voo em semanas. Estou a contar convosco @Americ... |
2702 | 103263 | "@USAirways: Vocês sabem que nós não podemos ficar no m... |
135 | 5137 | @southwestair Aqui no Aeroporto SA a ver o ... |
Antes de fazer previsões no conjunto de teste, precisamos de refazer os passos de processamento e codificação efectuados nos dados de treino. Felizmente, escrevemos o pré-processamento_para_bert
para o fazer por nós.
Em [0]:
# Executar pré-processamento_para_bert
no conjunto de teste
print('Tokenizando dados...')
entradas_teste, máscaras_teste = pré-processamento_para_bert(dados_teste.tweet)
# Criar o DataLoader para o nosso conjunto de teste
test_dataset = TensorDataset(test_inputs, test_masks)
test_sampler = SequentialSampler(test_dataset)
test_dataloader = DataLoader(test_dataset, sampler=test_sampler, batch_size=32)
Tokenização de dados...
4.2. Previsões¶
Existem cerca de 300 tweets não negativos no nosso conjunto de teste. Por conseguinte, continuaremos a ajustar o limiar de decisão até termos cerca de 300 tweets não negativos.
O limite que usaremos é 0,992, o que significa que os tweets com uma probabilidade prevista maior que 99,2% serão considerados positivos. Esse valor é muito alto em comparação com o limite padrão de 0,5.
Depois de examinar manualmente o conjunto de testes, verifico que a tarefa de classificação do sentimento é difícil até para o ser humano. Por conseguinte, um limiar elevado permite-nos fazer previsões seguras.
Em [0]:
# Calcular as probabilidades previstas no conjunto de teste probs = bert_predict(bert_classifier, test_dataloader) # Obter previsões a partir das probabilidades limiar = 0,9 preds = np.where(probs[:, 1] > limiar, 1, 0) # Número de tweets previstos não negativos print("Número de tweets previstos como não-negativos: ", preds.sum())
Número de tweets com previsão de não-negatividade: 454
Agora vamos examinar 20 tweets aleatórios das nossas previsões. 17 deles estão correctos, mostrando que o classificador BERT obtém uma taxa de precisão de cerca de 0,85.
Em [0]:
output = dados_teste[preds==1] lista(output.sample(20).tweet)
Saída[0]:
["@Delta @DeltaAssist A Delta volta a atacar. Sky lounge no aeroporto mais movimentado do país'fechado aos fins-de-semana. Estúpido e barato. saudades vossas @united", '.@SouthwestAir trouxe de volta os amendoins torrados com mel. É triste que esta constatação possa ser o ponto alto do meu dia?#SmallThingsInLife', '@DeltaAssist Enviei um e-mail para kana@delta e contactus.delta@delta para resolver problemas há duas semanas sem resposta. conselhos sobre quem contactar? ', "Mulher expulsa do voo pela @AlaskaAir porque tem cancro de #C planeia doar a passagem aérea da sua família'http://t.co/Uj6rispWLb", "@united (2/2) Eu não'parti o saco. Se não tivesse de pagar para a despachar, não estaria tão aborrecido. Prefiro voar na @AmericanAir @SouthwestAir etc", "Já voei em quase todas as companhias aéreas e nunca tive uma experiência melhor do que voar na @JetBlue. Qualidade, serviço, conforto e preço acessível. A++", '@JetBlue A melhor companhia aérea para se trabalhar sinto muito a sua falta #keepingitminty ', 'Convenceu @firetweet a reservar uma viagem de última hora para se juntar a mim em Austin! Desde então, tenho cantado a canção de segurança da @VirginAmerica. Pobre Eric... ', '@AmericanAir espera pacientemente para descolar de #DFW para #ord http://t.co/j1oDSc6fht', 'Oh @JetBlue hoje é um dia triste para os fiéis à B6. Sei que estão a promover as vossas novas "opções", mas as vossas taxas de serviço/sem bagagem SÃO o que nos torna excelentes', 'Coisas boas deste voo: @Gogo e a óptima experiência da @VirginAmerica. Coisas menos boas: o cheiro a vomitado de bebé/atum podre', '@USAirways @AmericanAir vai sentir falta da USAir :(', '@altonbrown @united Está na altura de mudar para a @VirginAmerica', 'Nunca é a altura errada para Chobani, @AmericanAir Admirals Club! #brokenrecord #toomanywasabipeas #lunch', "No meu voo, roubei o telemóvel do meu humano' para experimentar o novo streaming IFE da @alaskaair'. É bom! É pena que ela não tenha um iPad", "Mal posso esperar pela conclusão da fusão entre a @USAirways e a @AmericanAir, que chatice para o cliente!", "@JetBlue I'm a broke college kid so $150 is a huge deal.", "Mal posso esperar para voar de volta para a Bay Area esta noite no voo 2256 da @SouthwestAir!!!!", 'Pendurado em #SFO à espera que o nevoeiro se dissipe para a próxima ligação @VirginAmerica para #sxsw! #SXSW2015 #Austin', "@DeltaAssist, como posso mudar de voo do 1308 para um que não esteja indefinidamente atrasado.... e regressar a dc!"].
E - Conclusão¶
Adicionando um classificador simples de rede neural de uma camada oculta ao BERT e afinando o BERT, podemos alcançar um desempenho próximo do estado da arte, que é 10 pontos melhor do que o método de base, embora só tenhamos 3400 pontos de dados.
Além disso, embora o BERT seja muito grande, complicado e tenha milhões de parâmetros, só precisamos de o afinar em apenas 2-4 épocas. Este resultado pode ser alcançado porque o BERT foi treinado numa quantidade enorme e já codifica muita informação sobre a nossa língua. Um desempenho impressionante alcançado num curto espaço de tempo, com uma pequena quantidade de dados, mostrou porque é que o BERT é um dos mais poderosos modelos de PNL disponíveis neste momento.