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.



BERT_para_Analisis_de_Sentimientos





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:

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]:

# Specify MAX_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 function preprocesamiento_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): Set Falso 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.

Let’s Discuss Your Idea

    Related Posts

    Ready To Supercharge Your Business

    LET’S
    TALK
    es_ESEspañol