Tutorial: Perfeccionamiento de BERT para el análisis de sentimientos
- Tutorial: Ajuste fino de BERT para el análisis de sentimientos
- A - Introducción¶
- B - Setup¶
- C - Línea de base: Clasificador TF-IDF + Naive Bayes¶
- D - Ajuste del BERT¶
- E - Conclusión¶
Tutorial: Ajuste fino de BERT para el análisis de sentimientos
Publicado originalmente por Chris Tran, investigador de aprendizaje automático de Skim AI.
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]:
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 de @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]:
# EspecificarMAX_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 # Runpreprocesamiento_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): EstableceFalso
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.