Tutorial: Perfeccionamiento de BERT para el análisis de sentimientos

Tutorial: Ajuste fino de BERT para el análisis de sentimientos

    Publicado originalmente por Chris Tran, investigador de aprendizaje automático de Skim AI.



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

importar os
importar re
from tqdm import tqdm
import numpy como np
import pandas como pd
import matplotlib.pyplot como plt
%matplotlib en línea

2. Conjunto de datos

2.1. Descargar conjunto de datos

En [0]:

# Descargar datos
importar peticiones
request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf")
con open("datos.zip", "wb") como archivo:
    file.write(request.content)
# Descomprimir datos
importar zipfile
with zipfile.ZipFile('datos.zip') as zip:
    zip.extractall('datos')

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

 # Cargar datos y establecer etiquetas
data_complaint = pd.read_csv('data/complaint1700.csv')
data_complaint['label'] = 0
data_non_complaint = pd.read_csv('data/noncomplaint1700.csv')
data_non_complaint['label'] = 1
# Concatenar los datos que se quejan y los que no se quejan
data = pd.concat([data_queja, data_no_queja], axis=0).reset_index(drop=True)
# Drop 'aerolínea' columna
data.drop(['compañía aérea'], inplace=True, axis=1)
# Visualizar 5 muestras aleatorias
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 dividir_prueba_entrenamiento
X = datos.tweet.valores
y = datos.etiqueta.valores
X_entrenamiento, X_valor, y_entrenamiento, y_valor =
    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]:

# Cargar datos de prueba
datos_prueba = pd.read_csv('datos/datos_prueba.csv')
# Conservar las columnas importantes
datos_de_prueba = datos_de_prueba[['id', 'tweet']]
# Mostrar 5 muestras de los datos de prueba
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]:

importar antorcha
si torch.cuda.is_available():       
    device = torch.device("cuda")
    print(f'Hay {torch.cuda.device_count()} GPU(s) disponibles.')
    print('Nombre_dispositivo:', torch.cuda.get_device_name(0))
si no
    print('No hay GPU disponible, se utiliza la CPU en su lugar.')
    dispositivo = 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]:

importar nltk
# Descomentar para descargar "stopwords"
nltk.download("stopwords")
from nltk.corpus import palabras_de_parada
def preprocesamiento_texto(s):
    """
    - Poner la frase en minúsculas
    - Cambiar "'t" por "not".
    - Eliminar "@nombre
    - Aislar y eliminar los signos de puntuación excepto "?"
    - Elimine otros caracteres especiales
    - Elimine las palabras vacías excepto "no" y "puede".
    - Eliminar los espacios en blanco finales
    """
    s = s.lower()
    # Cambiar 't por 'not'
    s = re.sub(r"'t", " not", s)
    # Eliminar @nombre
    s = re.sub(r'(@.*?)[s]', ' ', s)
    # Aísla y elimina los signos de puntuación excepto '?'
    s = re.sub(r'(['".()!?\/,])', r' 1 ', s)
    s = re.sub(r'[^ws?]', ' ', s)
    # Elimina algunos caracteres especiales
    s = re.sub(r'([;:|-"n])', ' ', s)
    # Elimina las palabras clave excepto 'not' y 'can'
    s = " ".join([palabra para palabra en s.split()
                  if word not in stopwords.words('english')
                  or word in ['not', 'can']])
    # Elimina los espacios en blanco finales
    s = re.sub(r's+', ' ', s).strip()
    devuelve 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]:

%%iempo
from sklearn.feature_extraction.text import TfidfVectorizer
# Preprocesar texto
X_entrenamiento_preprocesado = np.array([texto_preprocesado(texto) para texto en X_entrenamiento])
X_val_preprocessed = np.array([text_preprocessing(text) for text in X_val])
# Calcular TF-IDF
tf_idf = TfidfVectorizer(ngram_range=(1, 3),
                         binary=True,
                         smooth_idf=False)
X_entrenamiento_tfidf = tf_idf.fit_transform(X_entrenamiento_preprocesado)
X_val_tfidf = tf_idf.transform(X_val_preprocesado)
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(modelo):
    """
    Devuelve la puntuación media AUC de la validación cruzada.
    """
    # Establecer KFold para barajar los datos antes de la división
    kf = StratifiedKFold(5, shuffle=True, random_state=1)
    # Obtener puntuaciones AUC
    auc = cross_val_score(
        model, X_train_tfidf, y_train, scoring="roc_auc", cv=kf)
    return auc.media()

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))
mejor_alfa = np.round(res.idxmax(), 2)
print('Mejor alfa: ', mejor_alfa)
plt.plot(res)
plt.title('AUC vs. Alfa')
plt.xlabel('Alfa')
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 puntuación_exactitud, curva_roc, auc
def evaluate_roc(probs, y_true):
    """
    - Imprime el AUC y la precisión en el conjunto de pruebas
    - Trazar ROC
    @params probs (np.array): una matriz de probabilidades predichas con forma (len(y_true), 2)
    @params y_true (np.array): matriz de valores verdaderos con forma (len(y_true),)
    """
    preds = probs[:, 1]
    fpr, tpr, threshold = roc_curve(y_true, preds)
    roc_auc = auc(fpr, tpr)
    print(f'AUC: {roc_auc:.4f}')
    # Obtener la precisión sobre el conjunto de pruebas
    y_pred = np.where(preds >= 0.5, 1, 0)
    precisión = puntuación_precisión(y_true, y_pred)
    print(f'Accuracy: {accuracy*100:.2f}%')
    # Gráfico ROC AUC
    plt.title('Característica operativa del receptor')
    plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc)
    plt.legend(loc = 'inferior derecha')
    plt.plot([0, 1], [0, 1],'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('Tasa de verdaderos positivos')
    plt.xlabel('Tasa de falsos positivos')
    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]:

# Calcular las probabilidades previstas
nb_model = MultinomialNB(alfa=1,8)
nb_model.fit(X_entrenamiento_tfidf, y_entrenamiento)
probs = nb_model.predict_proba(X_val_tfidf)
# Evaluar el clasificador
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 preprocesamiento_texto(texto):
    """
    - Eliminar menciones de entidades (ej. '@united')
    - Corregir errores (ej. '&' a '&')
    @param text (str): una cadena a procesar.
    @return text (Str): la cadena procesada.
    """
    # Quitar '@nombre'
    text = re.sub(r'(@.*?)[s]', ' ', text)
    # Sustituye '&' por '&'
    text = re.sub(r'&', '&', text)
    # Elimina los espacios en blanco finales
    text = re.sub(r's+', ' ', text).strip()
    devolver texto

En [0]:

# Imprimir sentencia 0
print('Original: ', X[0])
print('Procesado: ', 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
# Carga el tokenizador BERT
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
# Crear una función para tokenizar un conjunto de textos
def preprocessing_for_bert(datos):
    """Realiza los pasos de preprocesamiento necesarios para el BERT preentrenado.
    @param datos (np.array): Array de textos a procesar.
    @return input_ids (torch.Tensor): Tensor de ids de tokens para alimentar el modelo.
    @return máscaras_atención (tensor.antorcha): Tensor de índices que especifican
                  tokens debe atender el modelo.
    """
    # Crear listas vacías para almacenar las salidas
    input_ids = []
    attention_masks = []
    # Para cada frase...
    para enviados en datos:
        # codificar_plus voluntad:
        # (1) Tokenizar la frase
        # (2) Añadir el [CLS] y [SEP] token al principio y al final
        # (3) Truncar/frases a la longitud máxima
        # (4) Asignar tokens a sus IDs
        # (5) Crear máscara de atención
        # (6) Devuelve un diccionario de salidas
        encoded_sent = tokenizer.encode_plus(
            text=preprocesamiento_texto(enviado), # Preprocesar frase
            add_special_tokens=True, # Añadir [CLS] y [SEP]
            max_length=MAX_LEN, # Longitud máxima a truncar/padear
            pad_to_max_length=True, # Rellenar frase a longitud máxima
            #return_tensors='pt', # Tensor PyTorch de retorno
            return_attention_mask=True # Devolver máscara de atención
            )
        # Añadir las salidas a las listas
        input_ids.append(encoded_sent.get('input_ids'))
        attention_masks.append(encoded_sent.get('attention_mask'))
    # Convertir listas en tensores
    input_ids = torch.tensor(input_ids)
    atención_máscaras = torch.tensor(atención_máscaras)
    return entrada_ids, atención_mascaras

Antes de proceder a la tokenización, debemos especificar la longitud máxima de nuestras frases.
En [0]:

# Concatenar datos de entrenamiento y datos de prueba
all_tweets = np.concatenate([datos.tweet.valores, datos_prueba.tweet.valores])
# Codifica los datos concatenados
encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets]
# Encontrar la longitud máxima
max_len = max([len(sent) for sent in encoded_tweets])
print('Longitud máxima: ', max_len)
Longitud máxima: 68

Ahora vamos a tokenizar nuestros datos.
En [0]:

# Especificar MAX_LEN
MAX_LEN = 64
# Imprime la frase 0 y sus identificadores codificados
token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy())
print('Original: ', X[0])
print('Identificadores: ', token_ids)
Función # Run preprocesamiento_para_bert en el conjunto de entrenamiento y el conjunto de validación
print('Tokenizando datos...')
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
# Convierte otros tipos de datos a torch.Tensor
entrenar_etiquetas = torch.tensor(y_entrenar)
val_labels = torch.tensor(y_val)
# Para el ajuste fino de BERT, los autores recomiendan un tamaño de lote de 16 o 32.
tamaño_lote = 32
# Crear el DataLoader para nuestro conjunto de entrenamiento
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=tamaño_lote)
# Crear el DataLoader para nuestro conjunto de validación
val_data = TensorDataset(val_inputs, val_masks, val_labels)
val_sampler = SequentialSampler(val_data)
val_dataloader = DataLoader(val_data, sampler=val_sampler, batch_size=tamaño_lote)

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

%%tiempo
importar antorcha
import antorcha.nn como nn
from transformadores import BertModel
# Crear la clase BertClassfier
clase BertClassifier(nn.Module):
    """"Modelo Bert para tareas de clasificación.
    """
    def __init__(self, freeze_bert=False):
        """
        @param bert: un objeto BertModel
        @param clasificador: un clasificador torch.nn.Module
        @param freeze_bert (bool): Establece Falso para afinar el modelo BERT
        """
        super(BertClassifier, self).__init__()
        # Especificar el tamaño oculto del BERT, el tamaño oculto de nuestro clasificador y el número de etiquetas
        D_in, H, D_out = 768, 50, 2
        # Instanciar el modelo BERT
        self.bert = BertModel.from_pretrained('bert-base-uncased')
        # Instanciar un clasificador feed-forward de una capa
        self.classifier = nn.Secuencial(
            nn.Lineal(D_in, H),
            nn.ReLU(),
            #nn.Dropout(0.5),
            nn.Lineal(H, D_out)
        )
        # Congelar el modelo BERT
        if congelar_bert:
            for param in self.bert.parameters():
                param.requires_grad = False
    def forward(self, input_ids, attention_mask):
        """
        Alimentar input a BERT y al clasificador para calcular logits.
        @param input_ids (torch.Tensor): un tensor de entrada con forma (batch_size,
                      max_length)
        @param attention_mask (torch.Tensor): un tensor que contiene información
                      con forma (tamaño_lote, longitud_máx)
        @return logits (torch.Tensor): un tensor de salida con forma (batch_size,
                      número_etiquetas)
        """
        # Alimentar BERT
        salidas = self.bert(entradas_ids=entradas_ids,
                            máscara_atención=máscara_atención)
        # Extraer el último estado oculto de la ficha [CLS] para la tarea de clasificación
        ultimo_estado_oculto_cls = salidas[0][:, 0, :]
        # Alimentar el clasificador para calcular logits
        logits = self.clasificador(último_estado_oculto_cls)
        devolver 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 transformadores import AdamW, get_linear_schedule_with_warmup
def inicializar_modelo(épocas=4):
    """"Inicializa el clasificador Bert, el optimizador y el programador de la tasa de aprendizaje.
    """
    # Instanciar el clasificador Bert
    bert_classifier = BertClassifier(freeze_bert=False)
    # Dile a PyTorch que ejecute el modelo en la GPU
    bert_classifier.to(dispositivo)
    # Crea el optimizador
    optimizador = AdamW(bert_clasificador.parámetros(),
                      lr=5e-5, # Tasa de aprendizaje por defecto
                      eps=1e-8 # Valor épsilon por defecto
                      )
    # Número total de pasos de entrenamiento
    pasos_totales = len(cargador_datos_entrenamiento) * épocas
    # Configurar el programador de la tasa de aprendizaje
    planificador = get_linear_schedule_with_warmup(optimizador,
                                                num_warmup_steps=0, # Valor por defecto
                                                num_training_steps=total_steps)
    return bert_classifier, optimizador, programador

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) # Establecer semilla para reproducibilidad
bert_classifier, optimizer, scheduler = inicializar_modelo(épocas=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):
    """Realiza un pase hacia adelante en el modelo BERT entrenado para predecir probabilidades
    en el conjunto de prueba.
    """
    # Poner el modelo en modo evaluación. Las capas de abandono se desactivan durante
    # el tiempo de prueba.
    model.eval()
    all_logits = []
    # Para cada lote de nuestro conjunto de pruebas...
    for batch in test_dataloader:
        # Carga el lote en la GPU
        b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2]
        # Calcula logits
        con torch.no_grad():
            logits = model(b_input_ids, b_attn_mask)
        all_logits.append(logits)
    # Concatenar los logits de cada lote
    all_logits = torch.cat(all_logits, dim=0)
    # Aplicar softmax para calcular las probabilidades
    probs = F.softmax(all_logits, dim=1).cpu().numpy()
    devolver probs

En [0]:

# Calcula las probabilidades predichas en el conjunto de prueba
probs = bert_predict(clasificador_bert, cargador_de_datos_val)
# Evalúa el clasificador Bert
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]:

# Concatenar el conjunto de entrenamiento y el conjunto de validación
datos_entrenamiento_completos = torch.utils.data.ConcatDataset([datos_entrenamiento, datos_validación])
muestreador_entrenamiento_completo = RandomSampler(datos_entrenamiento_completo)
full_train_dataloader = DataLoader(full_train_data, sampler=full_train_sampler, batch_size=32)
# Entrena el clasificador Bert con todos los datos de entrenamiento
set_seed(42)
clasificador_bert, optimizador, programador = inicializar_modelo(épocas=2)
entrenar(clasificador_bert, cargador_datos_entrenamiento_completo, épocas=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]:

datos_prueba.muestra(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]:

Carrera # preprocesamiento_para_bert en el conjunto de prueba
print('Tokenizando datos...')
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# Crear el DataLoader para nuestro conjunto de prueba
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]:

# Calcula las probabilidades predichas en el conjunto de prueba
probs = bert_predict(clasificador_bert, cargador_datos_prueba)
# Obtener predicciones a partir de las probabilidades
threshold = 0.9
preds = np.where(probs[:, 1] > umbral, 1, 0)
# Número de tweets con predicciones no negativas
print("Número de tweets con predicciones no negativas: ", 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]:

salida = datos_prueba[preds==1]
list(salida.muestra(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.

Hablemos de su idea

    Entradas relacionadas

    • 10 datos y estadísticas sobre stability.ai

      Pocas empresas de IA han ascendido tan meteóricamente o caído tan espectacularmente como Stability AI. Fundada en 2019 por Emad Mostaque, la startup detrás del tremendamente popular generador de imágenes Stable Diffusion parecía dispuesta a revolucionar el panorama de la IA y asegurar

      TV, cine y contenidos
    • Adobe AI Video se enfrenta a Open AI Sora

      El mercado de la creación de contenidos basados en IA está siendo testigo de una feroz batalla en la que los gigantes tecnológicos compiten por desarrollar tecnologías innovadoras de conversión de texto en vídeo. La reciente presentación por parte de OpenAI de Sora, un modelo de IA pionero capaz de generar vídeos a partir de descripciones de texto, ha sentado las bases...

      LLMs / PNL
    • Pila de herramientas de IA para la creación de contenidos

      La estadística de la semana: $5.200 millones es el tamaño estimado del mercado universal de creación de contenidos de IA, que se prevé que crezca hasta los $16.900 millones en 2028. En la edición de esta semana, ofrecemos una visión de las herramientas que componen

      Startups y capital riesgo

    Listo para potenciar su negocio

    VAMOS
    HABLAR
    es_ESEspañol