Tutorial: Ajuste fino do BERT para análise de sentimentos
Tutorial: Afinação do BERT para Análise de Sentimentos
Originally published by Skim AI's Machine Learning Researcher, Chris Tran.
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]:
import os import 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]:
# Download data import requests request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf") with open("data.zip", "wb") as file: file.write(request.content) # Unzip data import zipfile with zipfile.ZipFile('data.zip') as zip: zip.extractall('data')
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]:
# Load data and set labels 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 # Concatenate complaining and non-complaining data data = pd.concat([data_complaint, data_non_complaint], axis=0).reset_index(drop=True) # Drop 'airline' column data.drop(['airline'], inplace=True, axis=1) # Display 5 random samples data.sample(5)
Saída[0]:
id | tweet | etiqueta | |
---|---|---|---|
1988 | 24991 | Que grande regresso de boas-vindas. É 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 = data.tweet.values y = data.label.values X_train, X_val, y_train, 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]:
# Load test data test_data = pd.read_csv('data/test_data.csv') # Keep important columns test_data = test_data[['id', 'tweet']] # Display 5 samples from the test data test_data.sample(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 @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]:
import torch if torch.cuda.is_available(): device = torch.device("cuda") print(f'There are {torch.cuda.device_count()} GPU(s) available.') print('Device name:', torch.cuda.get_device_name(0)) else: print('No GPU available, using the CPU instead.') device = 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]:
import nltk # Uncomment to download "stopwords" nltk.download("stopwords") from nltk.corpus import stopwords def text_preprocessing(s): """ - Lowercase the sentence - Change "'t" to "not" - Remove "@name" - Isolate and remove punctuations except "?" - Remove other special characters - Remove stop words except "not" and "can" - Remove trailing whitespace """ s = s.lower() # Change 't to 'not' s = re.sub(r"'t", " not", s) # Remove @name s = re.sub(r'(@.*?)[s]', ' ', s) # Isolate and remove punctuations except '?' s = re.sub(r'(['".()!?\/,])', r' 1 ', s) s = re.sub(r'[^ws?]', ' ', s) # Remove some special characters s = re.sub(r'([;:|•«n])', ' ', s) # Remove stopwords except 'not' and 'can' s = " ".join([word for word in s.split() if word not in stopwords.words('english') or word in ['not', 'can']]) # Remove trailing whitespace s = re.sub(r's+', ' ', s).strip() return 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 # Preprocess text 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]) # Calculate 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): """ Return the average AUC score from cross-validation. """ # Set KFold to shuffle data before the split kf = StratifiedKFold(5, shuffle=True, random_state=1) # Get AUC scores auc = cross_val_score( model, X_train_tfidf, y_train, scoring="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]:
from 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)) best_alpha = np.round(res.idxmax(), 2) print('Best alpha: ', best_alpha) 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): """ - Print AUC and accuracy on the test set - Plot ROC @params probs (np.array): an array of predicted probabilities with shape (len(y_true), 2) @params y_true (np.array): an array of the true values with shape (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}') # Get accuracy over the test set y_pred = np.where(preds >= 0.5, 1, 0) accuracy = accuracy_score(y_true, y_pred) print(f'Accuracy: {accuracy*100:.2f}%') # Plot ROC AUC plt.title('Receiver Operating Characteristic') 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('True Positive Rate') plt.xlabel('False Positive Rate') 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]:
# Compute predicted probabilities nb_model = MultinomialNB(alpha=1.8) nb_model.fit(X_train_tfidf, y_train) probs = nb_model.predict_proba(X_val_tfidf) # Evaluate the classifier evaluate_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): """ - Remove entity mentions (eg. '@united') - Correct errors (eg. '&' to '&') @param text (str): a string to be processed. @return text (Str): the processed string. """ # Remove '@name' text = re.sub(r'(@.*?)[s]', ' ', text) # Replace '&' with '&' text = re.sub(r'&', '&', text) # Remove trailing whitespace text = re.sub(r's+', ' ', text).strip() return text
Em [0]:
# Print sentence 0 print('Original: ', X[0]) print('Processed: ', 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 o que são 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 # Load the BERT tokenizer tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True) # Create a function to tokenize a set of texts def preprocessing_for_bert(data): """Perform required preprocessing steps for pretrained BERT. @param data (np.array): Array of texts to be processed. @return input_ids (torch.Tensor): Tensor of token ids to be fed to a model. @return attention_masks (torch.Tensor): Tensor of indices specifying which tokens should be attended to by the model. """ # Create empty lists to store outputs input_ids = [] attention_masks = [] # For every sentence... for sent in data: #codificar_mais
will: # (1) Tokenize the sentence # (2) Add the[CLS]
e[SEP]
token to the start and end # (3) Truncate/Pad sentence to max length # (4) Map tokens to their IDs # (5) Create attention mask # (6) Return a dictionary of outputs encoded_sent = tokenizer.encode_plus( text=text_preprocessing(sent), # Preprocess sentence add_special_tokens=True, # Add[CLS]
e[SEP]
max_length=MAX_LEN, # Max length to truncate/pad pad_to_max_length=True, # Pad sentence to max length #return_tensors='pt', # Return PyTorch tensor return_attention_mask=True # Return attention mask ) # Add the outputs to the lists input_ids.append(encoded_sent.get('input_ids')) attention_masks.append(encoded_sent.get('attention_mask')) # Convert lists to tensors 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]:
# Concatenate train data and test data all_tweets = np.concatenate([data.tweet.values, test_data.tweet.values]) # Encode our concatenated data encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets] # Find the maximum length max_len = max([len(sent) for sent in encoded_tweets]) print('Max length: ', max_len)
Comprimento máximo: 68
Agora vamos tokenizar os nossos dados.
Em [0]:
# SpecifyMAX_LEN
MAX_LEN = 64 # Print sentence 0 and its encoded token ids token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy()) print('Original: ', X[0]) print('Token IDs: ', token_ids) # Run functionpré-processamento_para_bert
on the train set and the validation set print('Tokenizing data...') 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 # Convert other data types to torch.Tensor train_labels = torch.tensor(y_train) val_labels = torch.tensor(y_val) # For fine-tuning BERT, the authors recommend a batch size of 16 or 32. batch_size = 32 # Create the DataLoader for our training set 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) # Create the DataLoader for our validation set 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 import torch import torch.nn as nn from transformers import BertModel # Create the BertClassfier class class BertClassifier(nn.Module): """Bert Model for Classification Tasks. """ def __init__(self, freeze_bert=False): """ @param bert: a BertModel object @param classifier: a torch.nn.Module classifier @param freeze_bert (bool): SetFalso
to fine-tune the BERT model """ super(BertClassifier, self).__init__() # Specify hidden size of BERT, hidden size of our classifier, and number of labels D_in, H, D_out = 768, 50, 2 # Instantiate BERT model self.bert = BertModel.from_pretrained('bert-base-uncased') # Instantiate an one-layer feed-forward classifier self.classifier = nn.Sequential( nn.Linear(D_in, H), nn.ReLU(), #nn.Dropout(0.5), nn.Linear(H, D_out) ) # Freeze the BERT model if freeze_bert: for param in self.bert.parameters(): param.requires_grad = False def forward(self, input_ids, attention_mask): """ Feed input to BERT and the classifier to compute logits. @param input_ids (torch.Tensor): an input tensor with shape (batch_size, max_length) @param attention_mask (torch.Tensor): a tensor that hold attention mask information with shape (batch_size, max_length) @return logits (torch.Tensor): an output tensor with shape (batch_size, num_labels) """ # Feed input to BERT outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask) # Extract the last hidden state of the token[CLS]
for classification task last_hidden_state_cls = outputs[0][:, 0, :] # Feed input to classifier to compute 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): """Initialize the Bert Classifier, the optimizer and the learning rate scheduler. """ # Instantiate Bert Classifier bert_classifier = BertClassifier(freeze_bert=False) # Tell PyTorch to run the model on GPU bert_classifier.to(device) # Create the optimizer optimizer = AdamW(bert_classifier.parameters(), lr=5e-5, # Default learning rate eps=1e-8 # Default epsilon value ) # Total number of training steps total_steps = len(train_dataloader) * epochs # Set up the learning rate scheduler scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, # Default value 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) # Set seed for reproducibility 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): """Perform a forward pass on the trained BERT model to predict probabilities on the test set. """ # Put the model into the evaluation mode. The dropout layers are disabled during # the test time. model.eval() all_logits = [] # For each batch in our test set... for batch in test_dataloader: # Load batch to GPU b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2] # Compute logits with torch.no_grad(): logits = model(b_input_ids, b_attn_mask) all_logits.append(logits) # Concatenate logits from each batch all_logits = torch.cat(all_logits, dim=0) # Apply softmax to calculate probabilities probs = F.softmax(all_logits, dim=1).cpu().numpy() return probs
Em [0]:
# Compute predicted probabilities on the test set probs = bert_predict(bert_classifier, val_dataloader) # Evaluate the Bert classifier 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]:
# Concatenate the train set and the validation set full_train_data = torch.utils.data.ConcatDataset([train_data, val_data]) full_train_sampler = RandomSampler(full_train_data) full_train_dataloader = DataLoader(full_train_data, sampler=full_train_sampler, batch_size=32) # Train the Bert Classifier on the entire training data 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]:
test_data.sample(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]:
# Run pré-processamento_para_bert
on the test set
print('Tokenizing data...')
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# Create the DataLoader for our test set
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]:
# Compute predicted probabilities on the test set probs = bert_predict(bert_classifier, test_dataloader) # Get predictions from the probabilities threshold = 0.9 preds = np.where(probs[:, 1] > threshold, 1, 0) # Number of tweets predicted non-negative print("Number of tweets predicted non-negative: ", 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 = test_data[preds==1] list(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.