Tutorial: Perfeccionamiento de BERT para el análisis de sentimientos
Tutorial: Ajuste fino de BERT para el análisis de sentimientos
Originally published by Skim AI's Machine Learning Researcher, Chris Tran.
A - Introducción¶
En los últimos años, la comunidad de la PLN ha sido testigo de numerosos avances en el procesamiento del lenguaje natural, especialmente el cambio hacia el aprendizaje por transferencia. Modelos como ELMo, ULMFiT de fast.ai, Transformer y GPT de OpenAI han permitido a los investigadores obtener resultados punteros en múltiples pruebas de referencia y han proporcionado a la comunidad grandes modelos preentrenados de alto rendimiento. Este cambio en la PLN se considera el momento ImageNet de la PLN, un cambio que se produjo en la visión por ordenador hace unos años, cuando las capas inferiores de las redes de aprendizaje profundo con millones de parámetros entrenadas en una tarea específica pueden reutilizarse y ajustarse para otras tareas, en lugar de entrenar nuevas redes desde cero.
Uno de los mayores hitos en la evolución de la PNL recientemente es el lanzamiento del BERT de Google, que se describe como el comienzo de una nueva era en PNL. En este cuaderno voy a utilizar el HuggingFace's transformadores
para afinar el modelo BERT preentrenado para una tarea de clasificación. A continuación, compararé el rendimiento del BERT con un modelo de referencia, en el que utilizo un vectorizador TF-IDF y un clasificador Naive Bayes. El sitio transformadores
nos ayudan a afinar rápida y eficazmente el modelo BERT de última generación y a obtener un índice de precisión 10% superior al modelo de referencia.
Referencia:
Para comprender Transformador (la arquitectura en la que se basa BERT) y aprender a implementar BERT, recomiendo encarecidamente la lectura de las siguientes fuentes:
- El BERT Ilustrado, ELMo y co.: Una guía muy clara y bien escrita para entender el BERT.
- La documentación del
transformadores
biblioteca - Tutorial de ajuste fino de BERT con PyTorch por Chris McCormick: Un tutorial muy detallado que muestra cómo utilizar BERT con la biblioteca HuggingFace PyTorch.
B - Configuración¶
1. Cargar bibliotecas esenciales¶
En [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 datos¶
2.1. Descargar conjunto de datos¶
En [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. Datos del tren de carga¶
Los datos del tren constan de 2 archivos, cada uno de los cuales contiene 1.700 tuits de queja/no queja. Todos los tuits de los datos contienen al menos un hashtag de una aerolínea.
Cargaremos los datos del tren y los etiquetaremos. Dado que sólo utilizamos los datos de texto para clasificar, eliminaremos las columnas sin importancia y sólo mantendremos id
, tuitee
y etiqueta
columnas.
En [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)
Out[0]:
id | tuitee | etiqueta | |
---|---|---|---|
1988 | 24991 | Qué gran bienvenida. De risa. Deplanin... | 1 |
1294 | 72380 | Muy decepcionado con @JetBlue esta noche. Fligh... | 0 |
1090 | 127893 | @united mis amigos lo están pasando fatal... | 0 |
553 | 58278 | @united todo lo que quiero para Navidad es una bolsa perdida que... | 0 |
2075 | 30695 | sip no voy a mentir... super interesado en probar... | 1 |
Dividiremos aleatoriamente todos los datos de entrenamiento en dos conjuntos: un conjunto de entrenamiento con 90% de los datos y un conjunto de validación con 10% de los datos. Realizaremos el ajuste de hiperparámetros mediante validación cruzada en el conjunto de entrenamiento y utilizaremos el conjunto de validación para comparar modelos.
En [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. Datos de la prueba de carga¶
Los datos de prueba contienen 4555 ejemplos sin etiqueta. Unos 300 ejemplos son tuits no quejicas. Nuestra tarea consiste en identificar sus id
y examinar manualmente si nuestros resultados son correctos.
En [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)
Out[0]:
id | tuitee | |
---|---|---|
1539 | 59336 | Vuelo de @AmericanAir retrasado más de 2 horas por n... |
607 | 24101 | @SouthwestAir Sigo recibiendo este mensaje de error... |
333 | 13179 | esperando en #SeaTac para embarcar en mi vuelo @JetBlue... |
2696 | 102948 | Odio cuando voy a través de avance de selección de asiento pro ... |
3585 | 135638 | vergüenza @AlaskaAir |
3. Configurar la GPU para la formación¶
Google Colab ofrece GPUs y TPUs gratuitas. Dado que vamos a entrenar una red neuronal de gran tamaño, lo mejor es utilizar estas funciones.
Se puede añadir una GPU yendo al menú y seleccionando:
Tiempo de ejecución -> Cambiar tipo de tiempo de ejecución -> Acelerador de hardware: GPU
Entonces necesitamos ejecutar la siguiente celda para especificar la GPU como dispositivo.
En [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")
Hay 1 GPU(s) disponible(s). Nombre del dispositivo: Tesla T4
C - Línea de base: Clasificador TF-IDF + Naive Bayes¶
En este enfoque básico, primero utilizaremos TF-IDF para vectorizar nuestros datos de texto. A continuación, utilizaremos el modelo Naive Bayes como clasificador.
¿Por qué Naive Bayse? He experimentado con distintos algoritmos de aprendizaje automático, como Random Forest, Support Vectors Machine y XGBoost, y he observado que Naive Bayes ofrece el mejor rendimiento. En Guía de Scikit-learn para elegir el estimador adecuado, también se sugiere utilizar Naive Bayes para los datos de texto. También probé a utilizar SVD para reducir la dimensionalidad, pero no obtuve mejores resultados.
1. Preparación de los datos¶
1.1. Preprocesamiento¶
En el modelo de bolsa de palabras, un texto se representa como la bolsa de sus palabras, sin tener en cuenta la gramática ni el orden de las palabras. Por tanto, hay que eliminar las palabras vacías, los signos de puntuación y los caracteres que no contribuyen mucho al significado de la frase.
En [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] Descargando paquete stopwords a /root/nltk_data... [nltk_data] ¡El paquete stopwords ya está actualizado!
1.2. Vectorizador TF-IDF¶
En recuperación de información, TF-IDFabreviatura de frecuencia de términos-frecuencia inversa de documentoses una estadística numérica que pretende reflejar la importancia de una palabra en un documento de una colección o corpus. Utilizaremos TF-IDF para vectorizar nuestros datos de texto antes de introducirlos en los algoritmos de aprendizaje automático.
En [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)
Tiempos de CPU: usuario 5,47 s, sys: 519 ms, total: 5,99 s Tiempo de muro: 6 s
2. Entrenar el clasificador Naive Bayes¶
2.1. Ajuste de hiperparámetros¶
Utilizaremos la validación cruzada y la puntuación AUC para ajustar los hiperparámetros de nuestro modelo. La función get_auc_CV
devolverá la puntuación AUC media de la validación cruzada.
En [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()
En MultinominalNB
sólo tienen un hipterparámetro - alfa. El código siguiente nos ayudará a encontrar el valor alfa que nos proporcione la puntuación CV AUC más alta.
En [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()
Mejor alfa: 1.3
2.2. Evaluación en el conjunto de validación¶
Para evaluar el rendimiento de nuestro modelo, calcularemos la tasa de precisión y la puntuación AUC de nuestro modelo en el conjunto de validación.
En [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 el TF-IDF y el algoritmo Naive Bayes, conseguimos un índice de precisión del 72.65% en el conjunto de validación. Este valor es el rendimiento de referencia y se utilizará para evaluar el rendimiento de nuestro modelo BERT de ajuste fino.
En [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 Precisión: 75,59%
D - Ajuste del BERT¶
1. Instalar la biblioteca Cara abrazada¶
La librería de transformadores de Hugging Face contiene implementaciones en PyTorch de modelos NLP de última generación, incluyendo BERT (de Google), GPT (de OpenAI) ... y pesos de modelos pre-entrenados.
En [1]:
#!pip instalar transformadores
2. Tokenización y formato de entrada¶
Antes de tokenizar el texto, lo procesaremos ligeramente, eliminando las menciones a entidades (por ejemplo, @united) y algunos caracteres especiales. El nivel de procesamiento aquí es mucho menor que en los enfoques anteriores porque BERT se entrenó con las frases completas.
En [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
En [0]:
# Print sentence 0 print('Original: ', X[0]) print('Processed: ', text_preprocessing(X[0]))
Original: @united Estoy teniendo problemas. Ayer volví a reservar para 24 horas después de que se suponía que iba a volar, ahora no puedo conectarme y facturar. ¿Pueden ayudarme? Procesado: Tengo problemas. Ayer hice una nueva reserva 24 horas después de la hora a la que tenía que volar y ahora no puedo conectarme ni facturar. ¿Pueden ayudarme?
2.1. Tokenizador BERT¶
Para aplicar el BERT preentrenado, debemos utilizar el tokenizador proporcionado por la biblioteca. Esto se debe a que (1) el modelo tiene un vocabulario específico y fijo y (2) el tokenizador BERT tiene una forma particular de tratar las palabras fuera de vocabulario.
Además, se nos pide que añadamos tokens especiales al principio y al final de cada frase, que rellenemos y trunquemos todas las frases a una única longitud constante y que especifiquemos explícitamente qué son tokens de relleno con la "máscara de atención".
En codificar_plus
del tokenizador BERT:
(1) dividir nuestro texto en tokens,
(2) añadir el especial [CLS]
y [SEP]
fichas, y
(3) convertir estos tokens en índices del vocabulario del tokenizador,
(4) rellenar o truncar las frases hasta la longitud máxima, y
(5) crear máscara de atención.
En [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_plus
will: # (1) Tokenize the sentence # (2) Add the[CLS]
y[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]
y[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 de proceder a la tokenización, debemos especificar la longitud máxima de nuestras frases.
En [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)
Longitud máxima: 68
Ahora vamos a tokenizar nuestros datos.
En [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 functionpreprocesamiento_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 Estoy teniendo problemas. Ayer volví a reservar para 24 horas después de que se suponía que iba a volar, ahora no puedo conectarme y facturar. ¿Pueden ayudarme? Token IDs: [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] Tokenizando datos...
2.2. Crear PyTorch DataLoader¶
Crearemos un iterador para nuestro conjunto de datos utilizando la clase torch DataLoader. Esto ayudará a ahorrar memoria durante el entrenamiento y aumentar la velocidad de entrenamiento.
En [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. Entrenar nuestro modelo¶
3.1. Crear BertClassifier¶
BERT-base consta de 12 capas transformadoras, cada capa transformadora recibe una lista de incrustaciones de tokens y produce el mismo número de incrustaciones con el mismo tamaño oculto (o dimensiones) en la salida. La salida de la última capa transformadora del modelo [CLS]
se utiliza como las características de la secuencia para alimentar un clasificador.
En transformadores
tiene la BertForSequenceClassification
que está diseñada para tareas de clasificación. Sin embargo, crearemos una nueva clase para poder especificar nuestra propia elección de clasificadores.
A continuación crearemos una clase BertClassifier con un modelo BERT para extraer la última capa oculta del [CLS]
y una red neuronal de una sola capa como clasificador.
En [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
Tiempos de CPU: usuario 38 µs, sys: 0 ns, total: 38 µs Tiempo de pared 40,1 µs
3.2. Optimizador y programador de la tasa de aprendizaje¶
Para afinar nuestro clasificador Bert, necesitamos crear un optimizador. Los autores recomiendan los siguientes hiperparámetros:
- Tamaño de lote: 16 ó 32
- Tasa de aprendizaje (Adam): 5e-5, 3e-5 o 2e-5
- Número de épocas: 2, 3, 4
Huggingface proporcionó el run_glue.py un ejemplo de aplicación del transformadores
biblioteca. En el script se utiliza el optimizador AdamW.
En [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. Bucle de formación¶
Entrenaremos nuestro clasificador Bert durante 4 épocas. En cada época, entrenaremos nuestro modelo y evaluaremos su rendimiento en el conjunto de validación. En más detalles, vamos a:
Formación:
- Descomprimir los datos del cargador de datos y cargarlos en la GPU.
- Poner a cero los gradientes calculados en la pasada anterior
- Realiza un forward pass para calcular los logits y las pérdidas
- Realizar una pasada hacia atrás para calcular los gradientes (
loss.backward()
) - Recorte la norma de los gradientes a 1,0 para evitar "gradientes explosivos".
- Actualizar los parámetros del modelo (
optimizador.paso()
) - Actualizar el ritmo de aprendizaje (
scheduler.step()
)
Evaluación:
- Descomprimir los datos y cargarlos en la GPU
- Pase hacia delante
- Calcular las pérdidas y el índice de precisión en el conjunto de validación
El guión que figura a continuación se comenta con los detalles de nuestro bucle de formación y evaluación.
En [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
Ahora, ¡empecemos a entrenar nuestro BertClassifier!
En [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)
Empieza a entrenar... Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- ¡Entrenamiento completado!
3.4. Evaluación en el conjunto de validación¶
El paso de predicción es similar al paso de evaluación que hicimos en el bucle de entrenamiento, pero más sencillo. Realizaremos una pasada hacia delante para calcular logits y aplicaremos softmax para calcular probabilidades.
En [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
En [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 Precisión: 80,59%
Bert Classifer obtiene una puntuación AUC de 0,90 y una tasa de precisión de 82,65% en el conjunto de validación. Este resultado es 10 puntos mejor que el del método de referencia.
3.5. Entrenar nuestro modelo con todos los datos de entrenamiento¶
En [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)
Empieza a entrenar... Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- ¡Entrenamiento completado!
4. Predicciones sobre el conjunto de pruebas¶
4.1. Preparación de los datos¶
Volvamos en breve a nuestro conjunto de pruebas.
En [0]:
test_data.sample(5)
Out[0]:
id | tuitee | |
---|---|---|
471 | 18654 | Amigos y familiares: Nunca vueles con @JetBlue. Absol... |
1971 | 76265 | @DeltaAssist @rogerioad Nunca he tenido un pro... |
23 | 672 | Primer vuelo en semanas. Contando contigo @Americ... |
2702 | 103263 | "@USAirways: Sabes que no podemos quedarnos m... |
135 | 5137 | @southwestair Aquí en el aeropuerto de SA viendo el ... |
Antes de realizar predicciones sobre el conjunto de prueba, tenemos que volver a realizar los pasos de procesamiento y codificación realizados sobre los datos de entrenamiento. Afortunadamente, hemos escrito el preprocesamiento_para_bert
que lo haga por nosotros.
En [0]:
# Run preprocesamiento_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)
Tokenizar datos...
4.2. Predicciones¶
Hay unos 300 tuits no negativos en nuestro conjunto de prueba. Por lo tanto, seguiremos ajustando el umbral de decisión hasta que tengamos unos 300 tuits no negativos.
El umbral que utilizaremos es 0,992, lo que significa que los tweets con una probabilidad de predicción superior a 99,2% se predecirán como positivos. Este valor es muy alto en comparación con el umbral predeterminado de 0,5.
Tras examinar manualmente el conjunto de pruebas, descubro que la tarea de clasificación de sentimientos aquí es incluso difícil para el ser humano. Por tanto, un umbral alto nos dará predicciones seguras.
En [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 tuits con predicción no negativa: 454
Ahora examinaremos 20 tuits aleatorios de nuestras predicciones. 17 de ellos son correctos, lo que demuestra que el clasificador BERT adquiere alrededor de un 0,85 de tasa de precisión.
En [0]:
output = test_data[preds==1] list(output.sample(20).tweet)
Out[0]:
["@Delta @DeltaAssist Delta ataca de nuevo. La sala VIP del aeropuerto más concurrido del país cierra los fines de semana. Dumb & cheap. miss you @united", '.@SouthwestAir trajo de vuelta los cacahuetes tostados con miel. ¿Es triste que esta realización puede ser el punto culminante de mi día? #SmallThingsInLife', '@DeltaAssist Envié un correo electrónico a kana@delta y a contactus.delta@delta para resolver problemas hace dos semanas sin respuesta. ¿consejo a quién contactar?', "Una mujer expulsada de un vuelo de @AlaskaAir porque tiene cáncer planea donar el billete de avión de su familia a http://t.co/Uj6rispWLb", "@united (2/2) Yo no'rompí la bolsa. Si no tuviera que pagar para facturarla, no estaría tan disgustado. Prefiero volar con @AmericanAir @SouthwestAir etc", "He volado con casi todas las aerolíneas y nunca he tenido una experiencia mejor que volando con JetBlue. Calidad, servicio, comodidad y asequibilidad. A++", '@JetBlue La mejor aerolínea para la que trabajar Os echo mucho de menos #keepingitminty ', '¡Convencí a @firetweet para que reservara un viaje de último minuto para acompañarme a Austin tom! He estado cantando la canción de seguridad de @VirginAmerica desde entonces. Pobre Eric.', '@AmericanAir esperando pacientemente para despegar de #DFW a #ord http://t.co/j1oDSc6fht', 'Oh @JetBlue hoy es un día triste para los leales al B6. Sé que estáis promocionando vuestras nuevas "opciones", pero vuestro servicio/sin tarifas de equipaje SON lo que os hace grandes', 'Cosas buenas de este vuelo: @Gogo y la gran experiencia de @VirginAmerica. No tan buenas: el olor a vómito de bebé/atún podrido', '@USAirways @AmericanAir echaré de menos USAir :(', '@altonbrown @united Hora de cambiarse a @VirginAmerica', '¡Nunca es mal momento para Chobani, @AmericanAir Admirals Club! #brokenrecord #toomanywasabipeas #lunch', "En mi vuelo, robé el teléfono de mi humano para probar el nuevo sistema de streaming de @alaskaair. ¡It's good! Lástima que no tenga un iPad", "Can't wait for the @USAirways and @AmericanAir merger to be completed, what a hassle for the customer!", "@JetBlue Yo'soy un universitario sin blanca así que $150 es un gran trato", "Can't wait to fly back to the Bay Area tonight on @SouthwestAir flight 2256!!!!", '¡Colgado en #SFO esperando a que se disipe la niebla para la próxima conexión @VirginAmerica a #sxsw! #SXSW2015 #Austin', "@DeltaAssist de todos modos puedo cambiar vuelos de 1308 a uno que no sea'indefinidamente retrasado.... y volver a dc!"]
E - Conclusión¶
Añadiendo un simple clasificador de red neuronal de una capa oculta sobre BERT y ajustando BERT, podemos lograr un rendimiento cercano al de la tecnología punta, que es 10 puntos mejor que el método de referencia aunque sólo tengamos 3.400 puntos de datos.
Además, aunque BERT es muy grande, complicado y tiene millones de parámetros, sólo necesitamos afinarlo en sólo 2-4 épocas. Ese resultado puede lograrse porque BERT se entrenó con una cantidad enorme y ya codifica mucha información sobre nuestra lengua. Un rendimiento impresionante conseguido en poco tiempo, con una pequeña cantidad de datos, ha demostrado por qué BERT es uno de los modelos de PNL más potentes disponibles en la actualidad.