En este tutorial aprenderemos a diseñar y entrenar un modelo de Inteligencia Artificial capaz de realizar el procesamiento de lenguaje natural (NLP) para la interpretación de sentimientos.
Y para ello, utilizaremos BERT (Bidirectional Encoder Representations from Transformers), uno de los Grandes Modelos de Lenguaje más potentes en la actualidad, para determinar si el contenido de un texto expresa un sentimiento positivo o negativo.
A lo largo de esta guía, cubriremos desde la preparación de los datos y la tokenización específica de los textos de entrada a BERT, hasta el ajuste fino (fine-tuning) del modelo utilizando las librerías Hugging Face y PyTorch.
Al final del tutorial se encuentran los enlaces de descarga del código fuente y del set de datos.
Introducción al Análisis de Sentimientos con BERT
El análisis de sentimientos es una tarea fundamental del procesamiento de lenguaje natural que busca capturar la intención y emoción detrás del texto.
Para este tutorial, utilizaremos un modelo BERT pre-entrenado, el cual ya posee una base sólida de conocimiento tras haber sido entrenado con conjuntos masivos de datos como Wikipedia y Google Books (los cuales tienen miles de millones de palabras).
Nuestra estrategia consistirá en tomar el modelo BERT Base Cased (con aproximadamente 100 millones de parámetros) y añadirle una capa de clasificación consistente en una Red Neuronal Sencilla, con lo cual lograremos especializar el modelo en nuestro dataset específico.
El Set de Datos: IMDB Movie Reviews
Entrenaremos la capa de clasificación (la Red Neuronal que conectaremos a BERT) usando una base de datos proveniente de IMDb (Internet Movie Database), que contiene críticas de películas de los últimos 40 años.
El dataset original cuenta con 50,000 registros etiquetados como «positivo» o «negativo». Sin embargo, para agilizar el proceso de entrenamiento en este tutorial usaremos una muestra de 10,000 datos de este dataset.
Configuración del entorno
Como BERT es un modelo gigantesco (100 millones de parámetros aproximadamente) resulta esencial que hagamos uso de una GPU, para lo cual haremos uso de los recursos que nos proporciona Google Colab.
Veamos entonces cómo configurar este entorno.
Instalación de la librería «transformers»
El primer paso es instalar la librería transformers de Hugging Face en la máquina virtual de Google Colab. Esta librería nos permitirá precisamente acceder al modelo BERT pre-entrenado y luego nos permitirá igualmente llevar a cabo el proceso de afinación.
Para instalar la librería simplemente usamos el comando «pip»:
!pip install transformers
Importación de librerías y dependencias
Para este tutorial necesitaremos herramientas de manipulación de datos, aprendizaje profundo y procesamiento de texto:
from transformers import BertModel, BertTokenizer, AdamW, get_linear_schedule_with_warmup
import torch
import numpy as np
from sklearn.model_selection import train_test_split
from torch import nn
from torch.utils.data import Dataset, DataLoader
import pandas as pd
from textwrap import wrap
Veamos brevemente qué hace cada uno de estos módulos:
BertModelyBertTokenizer: son el núcleo de BERT. Nos permitirán cargar el modelo y «tokenizar» el texto de entrada al mismo.torchynn: para construir la arquitectura de la Red Neuronal que agregaremos a la salida de BERTpandasywrap: para la lectura, manipulación y visualización del set de datostrain_test_split: para realizar la partición del set de datos en entrenamiento, validación y pruebaDatasetyDataLoader: estos módulos nos permiten crear el set de datos y alimentar el modelo usando la lógica de la librería PyTorchAdamW: un optimizador diseñado específicamente para Redes Transformer.
Inicialización de parámetros
Definiremos una semilla aleatoria para garantizar la reproducibilidad de los resultados y configuraremos los límites de memoria de nuestra GPU:
# Inicialización
RANDOM_SEED = 42
MAX_LEN = 200
BATCH_SIZE = 16
DATASET_PATH = '/content/drive/My Drive/videos/2020-07-20/BERT_sentiment_IMDB_Dataset.csv'
NCLASSES = 2
np.random.seed(RANDOM_SEED)
torch.manual_seed(RANDOM_SEED)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
Desglosemos el código anterior:
- RANDOM_SEED (línea 2): el valor que tendrá la semilla del generador aleatorio
- MAX_LEN (línea 3): truncaremos las reseñas a las primeras 200 palabras para evitar saturar la memoria RAM de Colab.
- BATCH_SIZE (línea 4): el tamaño del lote que presentaremos al modelo durante el entrenamiento
- NCLASSES (6): para indicar al modelo que clasificaremos los textos de entrada en una de dos posibles categorías (sentimiento «negativo» o «positivo»)
- device: Configuramos el código para que se ejecute en la GPU (
cuda:0) si está disponible. - Líneas 8 y 9: fijamos la semilla del generador aleatorio tanto de NumPy como de PyTorch para garantizar la reproducibilidad del entrenamiento
Carga y limpieza del set de datos
Montaremos Google Drive para acceder al dataset y realizaremos un reajuste de las etiquetas para convertirlas a formato numérico (0 para negativo, 1 para positivo).
Comencemos montando Google Drive, leyendo el set de datos y tomando los primeros 10.000 registros (para reducir la cantidad de datos a usar en la afinación):
# Cargar dataset
from google.colab import drive
drive.mount('/content/drive')
df = pd.read_csv(DATASET_PATH)
df = df[0:10000] # Usamos 10,000 registros para mayor velocidad
Veamos algunas características del set de datos. Mostremos los 5 primeros registros e imprimamos en pantalla el tamaño del dataset:
print(df.head())
print(df.shape)
con lo cual obtenemos un resultado similar a este:
review sentiment
0 One of the other reviewers has mentioned that ... positive
1 A wonderful little production. <br /><br />The... positive
2 I thought this was a wonderful way to spend ti... positive
3 Basically there's a family where a little boy ... negative
4 Petter Mattei's "Love in the Time of Money" is... positive
(10000, 2)
Es decir que en la columna «review» está el texto que presentaremos a la entrada de BERT, mientras que en la columna «sentiment» está la categoría correspondiente («positive» o «negative»). Además, el set de datos tiene un total de 10.000 registros y dos columnas.
Veamos en detalle uno de estos «reviews», por ejemplo el número 200:
print("\n".join(wrap(df['review'][200])))
al hacer esto obtenemos el siguiente texto:
Interesting and short television movie describes some of the
machinations surrounding Jay Leno's replacing Carson as host of the
Tonight Show. Film is currently very topical given the public drama
surrounding Conan O'Brien and Jay Leno.<br /><br />The film does a
good job of sparking viewers' interest in the events and showing some
of the concerns of the stakeholders, particularly of the NBC
executives. The portrayal of Ovitz was particularly compelling and
interesting, I thought.<br /><br />Still, many of the characters were
only very briefly limned or touched upon, and some of the acting
seemed perfunctory. Nevertheless, an interesting story.
El siguiente paso es reajustar la columna «sentiment» para representar las categorías como cantidades numéricas y no como «positive» o «negative». Así que crearemos una columna «label» que contendrá estas etiquetas numéricas; y después de hacer esta conversión eliminaremos la columna «sentiment»:
# Reajustar dataset
df['label'] = (df['sentiment']=='positive').astype(int)
df.drop('sentiment', axis=1, inplace=True)
df.head()
El dataset resultante tendrá entonces esta forma:
review label
0 One of the other reviewers has mentioned that ... 1
1 A wonderful little production. <br /><br />The... 1
2 I thought this was a wonderful way to spend ti... 1
3 Basically there's a family where a little boy ... 0
4 Petter Mattei's "Love in the Time of Money" is... 1
Perfecto, en este punto ya estamos listos para la fase de «tokenización».
Ejemplo de tokenización de los «reviews»
Como todo Gran Modelo de Lenguaje, BERT no procesa el texto directamente. En lugar de ello es necesario convertir el texto en tokens numéricos.
Todo esto lo podemos implementar con muy pocas líneas de código usando el módulo «BertTokenizer» importado previamente. Así que simplemente definimos el modelo pre-entrenado que queremos usar («bert-base-cased») y luego cargamos el tokenizer correspondiente desde Hugging Face:
PRE_TRAINED_MODEL_NAME = 'bert-base-cased'
tokenizer = BertTokenizer.from_pretrained(PRE_TRAINED_MODEL_NAME)
Veamos por ejemplo cómo usar el tokenizer para codificar la frase «I really loved that movie!»:
sample_txt = 'I really loved that movie!'
tokens = tokenizer.tokenize(sample_txt)
token_ids = tokenizer.convert_tokens_to_ids(tokens)
print('Frase: ', sample_txt)
print('Tokens: ', tokens)
print('Tokens numéricos: ', token_ids)
En este ejemplo:
- Usamos el método «tokenize» para realizar la codificación del texto de entrada (línea 2). La variable «tokens» contendrá las palabras o sub-palabras resultado de este proceso de «tokenización»
- Luego usamos el método «convert_tokens_to_ids» (línea 3) para extraer la codificación numérica correspondiente a cada token
- Y finalmente imprimimos en pantalla la frase original, los tokens correspondientes y la codificación numérica de dichos tokens (líneas 4, 5 y 6)
Veamos el resultado de ejecutar el bloque de código anterior:
Frase: I really loved that movie!
Tokens: ['I', 'really', 'loved', 'that', 'movie', '!']
Tokens numéricos: [146, 1541, 3097, 1115, 2523, 106]
Vemos que en este caso se ha asignado un token a cada palabra así como al símbolo «!», y además, en la tercera línea, vemos la codificación numérica correspondiente a dichos tokens.
A continuación usaremos este principio de tokenización para preparar el dataset que presentaremos a BERT.
Creación del Dataset y DataLoader
Al cargar datos en PyTorch es fundamental organizar los datos en una clase que herede de Dataset para facilitar el consumo de datos por lotes (batches).
Así que antes de implementar esta clase, comenzaremos creando la variable «encoding» que permitirá codificar cada review usando el «tokenizer», garantizando de esta forma que el formato de los datos será consistente con los requerimientos del modelo BERT.
Usaremos entonces el método «encode_plus» del tokenizer para definir todos los parámetros de la codificación:
encoding = tokenizer.encode_plus(
sample_txt,
max_length = 10,
truncation = True,
add_special_tokens = True,
return_token_type_ids = False,
pad_to_max_length = True,
return_attention_mask = True,
return_tensors = 'pt'
)
Veamos qué hace cada línea de código en el bloque anterior:
- Línea 2: contendrá el texto del «review» a pre-procesar. De momento usaremos el texto del ejemplo anterior («I really loved that movie!») pero luego modificaremos esta entrada para procesar el texto de cada review.
- Línea 3: especificamos la cantidad máxima de tokens (10) que contendrá cada secuencia codificada (podemos incrementar este valor pero esto incrementará la capacidad de cómputo requerida para la afinación del modelo)
- Línea 4: para truncar aquellos «reviews» que superen el máximo número de tokens
- Línea 5: añadiremos tokens especiales de inicio y finalización de secuencias así como tokens de padding (para rellenar con ceros aquellos reviews que contengan menos de 10 tokens)
- Línea 6: usamos «False» para codificar sólo un texto (el «review») y no pares de textos (debido a que el modelo fue pre-entrenado con pares de textos)
- Línea 7: para hacer rellenado con ceros de los «reviews» con menos de 10 tokens
- Línea 8: para que procese únicamente los tokens y no los ceros añadidos durante el padding
- Línea 9: para que el texto codificado esté en el formato PyTorch
Veamos un ejemplo de la codificación que obtendríamos con esta variable «encoding» aplicado a la frase «I really loved that movie!»:
print(tokenizer.convert_ids_to_tokens(encoding['input_ids'][0]))
print(encoding['input_ids'][0])
print(encoding['attention_mask'][0])
En la primera línea obtendremos cada uno de los tokens provenientes del texto original. Por otra parte, en la segunda línea extraemos las codificaciones numéricas correspondientes, mientras que en la última línea extraemos la máscara de atención. Veamos el resultado:
print(tokenizer.convert_ids_to_tokens(encoding['input_ids'][0]))
print(encoding['input_ids'][0])
print(encoding['attention_mask'][0])
En la primera línea vemos la frase tokenizada, y específicamente vemos que se ha incluido el token de inicio ([CLS]), el token de finalización ([SEP]) y dos tokens de padding ([PAD]). Si hacemos el conteo de tokens desde [CLS] hasta [SEP] tendremos un total de 8, que es menor que el máximo número de tokens especificado (10). Y es por esto que aparecen precisamente dos tokens de padding al final, para completar un total de 10 tokens.
En la segunda línea vemos la codificación numérica correspondiente a los 10 tokens anteriores, mientras que en la última línea vemos que en la máscara de atención los primeros 8 valores son iguales a 1 (pues corresponden a los primeros 8 tokens que contienen el texto de entrada), mientras que los últimos 2 valores son iguales a 0 (pues corresponden a los tokens de «padding»). Esto hace que durante el procesamiento BERT aceptará a la entrada esta secuencia con 10 tokens pero sólo procesará los 8 primeros.
Creación del Dataset de PyTorch
Ahora implementaremos la clase «IMDBDataset» que tomará cada reseña individual, aplicar á la tokenización y entregará el resultado en un formato de diccionario compatible con lo requerido por BERT durante el entrenamiento.
Veamos el código para esta clase:
class IMDBDataset(Dataset):
def __init__(self, reviews, labels, tokenizer, max_len):
self.reviews = reviews
self.labels = labels
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.reviews)
def __getitem__(self, item):
review = str(self.reviews[item])
label = self.labels[item]
encoding = tokenizer.encode_plus(
review,
max_length = self.max_len,
truncation = True,
add_special_tokens = True,
return_token_type_ids = False,
pad_to_max_length = True,
return_attention_mask = True,
return_tensors = 'pt'
)
return {
'review': review,
'input_ids': encoding['input_ids'].flatten(),
'attention_mask': encoding['attention_mask'].flatten(),
'label': torch.tensor(label, dtype=torch.long)
}
Vemos que la clase contiene los tres métodos que por defecto siempre deben estar presentes en un Dataset de Pytorch:
- «__init__» (líneas 2 a 6), que permite inicializar la clase. Allí especificamos cuáles serán los «reviews» y sus categorías correspondientes («labels»), cuál tokenizer usaremos y cuál será la longitud máxima de cada secuencia
- «__len__» (líneas 8 y 9), que permite calcular la cantidad de datos (reviews) en el dataset
- «__getitem__» (líneas 11 a 30) que permite extraer un dato del Dataset de PyTorch. Este dato será precisamente un diccionario (líneas 25 a 30) que contendrá el «review», los tokens numéricos («input_ids»), la máscara de atención («attention_mask») y la categoría del dato («label»)
El DataLoader en PyTorch
Muy bien, ahora crearemos el DataLoader que permitirá tomar lotes de datos (provenientes del dataset que acabamos de crear) y alimentará el modelo con dichos datos:
def data_loader(df, tokenizer, max_len, batch_size):
dataset = IMDBDataset(
reviews = df.review.to_numpy(),
labels = df.label.to_numpy(),
tokenizer = tokenizer,
max_len = MAX_LEN
)
return DataLoader(dataset, batch_size = BATCH_SIZE, num_workers = 4)
Este DataLoader:
- Acepta como entrada un DataFrame (con los reviews y los labels), el tokenizer, la longitud máxima de las secuencias (max_len) y el tamaño de cada lote (línea 1)
- Crea una instancia del Dataset (líneas 2 a 7)
- Y retorna un lote de dicho dataset (línea 9)
Partición de los datos en entrenamiento y validación
¡Perfecto! Ahora dividimos los datos en entrenamiento (80%) y validación (20%) usando train_test_split de Scikit-Learn y el DataLoader que acabamos de crear:
df_train, df_test = train_test_split(df, test_size = 0.2, random_state=RANDOM_SEED)
train_data_loader = data_loader(df_train, tokenizer, MAX_LEN, BATCH_SIZE)
test_data_loader = data_loader(df_test, tokenizer, MAX_LEN, BATCH_SIZE)
Creación y modificación del modelo BERT
Ahora la idea es tomar el modelo de Hugging Face pre-entrenado y modificar su capa de salida para acomodar el número de categorías en las que queremos clasificar nuestros datos (que son los dos sentimientos, positivo y negativo).
Entre el modelo pre-entrenado y dicha capa de salida agregaremos una capa de Dropout (para evitar el overfitting).
Y para lograr todo esto crearemos la clase «BERTSentimentClassifier» que posteriormente nos permitirá crear el modelo con todos estos elementos:
class BERTSentimentClassifier(nn.Module):
def __init__(self, n_classes):
super(BERTSentimentClassifier, self).__init__()
self.bert = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME)
self.drop = nn.Dropout(p=0.3)
self.linear = nn.Linear(self.bert.config.hidden_size, n_classes)
def forward(self, input_ids, attention_mask):
_, cls_output = self.bert(
input_ids = input_ids,
attention_mask = attention_mask
)
drop_output = self.drop(cls_output)
output = self.linear(drop_output)
return output
En el código anterior:
- Las líneas 2 a 6 contienen el método «init» que define la arquitectura del modelo. Esta arquitectura contiene el modelo pre-entrenado (línea 4), la capa dropout (línea 5) y la capa de salida con dos neuronas (línea 6). De momento esta capa de salida tendrá una función de activación lineal.
- En las líneas 8 a 15 implementamos el método «forward» que permitirá al modelo tomar una entrada y generar una predicción. En particular, cls_output (línea 9) es el vector que contiene la información condensada de toda la frase. Este vector es llevado a la capa dropout (línea 13) y finalmente a la salida del modelo (línea 14)
Afinación y evaluación de BERT
Perfecto, en este punto ya tenemos todos los elementos necesarios para realizar la afinación y evaluación de nuestro modelo.
Comencemos creando una instancia del modelo a partir de la clase «BERTSentimentClassifier»:
model = BERTSentimentClassifier(NCLASSES)
model = model.to(device)
Donde en la línea 2 hemos movido el modelo a la GPU (la variable «device» creada al inicio).
A continuación debemos configurar el optimizador AdamW y un planificador de tasa de aprendizaje (scheduler) que reduce gradualmente el learning rate durante las 5 épocas que usaremos para la afinación:
EPOCHS = 5
optimizer = AdamW(model.parameters(), lr=2e-5, correct_bias=False)
total_steps = len(train_data_loader) * EPOCHS
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps = 0,
num_training_steps = total_steps
)
loss_fn = nn.CrossEntropyLoss().to(device)
Funciones para el entrenamiento y la validación
Ahora implementaremos la función train_model que nos permitirá realizar el entrenamiento del modelo usando precisamente los datos de entrenamiento:
def train_model(model, data_loader, loss_fn, optimizer, device, scheduler, n_examples):
model = model.train()
losses = []
correct_predictions = 0
for batch in data_loader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['label'].to(device)
outputs = model(input_ids = input_ids, attention_mask = attention_mask)
_, preds = torch.max(outputs, dim=1)
loss = loss_fn(outputs, labels)
correct_predictions += torch.sum(preds == labels)
losses.append(loss.item())
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
optimizer.zero_grad()
return correct_predictions.double()/n_examples, np.mean(losses)
En esta función:
- Ponemos el modelo en modo entrenamiento (línea 2)
- Creamos las variables que almacenarán las pérdidas en cada iteración así como el número de aciertos (líneas 3 y 4)
- Y entrenamos el modelo (líneas 5 a 18). En particular, iteramos por cada lote del DataLoader (línea 5) y en cada iteración:
- Le presentamos al modelo los datos de entrenamiento (líneas 6 a 9)
- Obtenemos las categorías predichas (línea 10)
- Calculamos la pérdida y actualizamos el número de aciertos (líneas 11 a 13)
- Y aplicamos el algoritmo de back-propagation para actualizar los parámetros del modelo (líneas 14 a 18)
- Y al final retornamos el número de aciertos y la pérdida (línea 19)
Por otra parte, la validación del modelo se hace después de cada iteración de entrenamiento. En este caso se presenta al modelo el set de validación, se generan las predicciones y se calculan tanto la pérdida de validación como el número de aciertos pero no se actualizan los parámetros. Haremos esto con la función eval_model:
def eval_model(model, data_loader, loss_fn, device, n_examples):
model = model.eval()
losses = []
correct_predictions = 0
with torch.no_grad():
for batch in data_loader:
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['label'].to(device)
outputs = model(input_ids = input_ids, attention_mask = attention_mask)
_, preds = torch.max(outputs, dim=1)
loss = loss_fn(outputs, labels)
correct_predictions += torch.sum(preds == labels)
losses.append(loss.item())
return correct_predictions.double()/n_examples, np.mean(losses)
En este caso:
- Llevamos el modelo al modo «evaluación» (línea 2)
- Inicializamos las variables que almacenarán las pérdidas y los aciertos (líneas 3 y 4)
- Le indicamos a PyTorch que no se calcularán gradientes (es decir que no se realizará el entrenamiento, línea 5)
- Y luego, por cada lote en el set de validación (línea 6):
- generamos las predicciones (líneas 7 a 11)
- Calculamos y actualizamos las pérdidas y los aciertos (líneas 12 a 14)
- Y al final retornamos las pérdidas y los aciertos para el set de validación (línea 15)
¡Muy bien, ya estamos listos para afinar y validar el modelo. Así que simplemente creamos un loop donde por 5 iteraciones ejecutaremos las funciones anteriores:
for epoch in range(EPOCHS):
print('Epoch {} de {}'.format(epoch+1, EPOCHS))
print('------------------')
train_acc, train_loss = train_model(
model, train_data_loader, loss_fn, optimizer, device, scheduler, len(df_train)
)
test_acc, test_loss = eval_model(
model, test_data_loader, loss_fn, device, len(df_test)
)
print('Entrenamiento: Loss: {}, accuracy: {}'.format(train_loss, train_acc))
print('Validación: Loss: {}, accuracy: {}'.format(test_loss, test_acc))
print('')
Tras ejecutar el código anterior veremos que se imprime en pantalla el desempeño del modelo (pérdida y exactitud para los sets de entrenamiento y validación) en cada una de las 5 iteraciones:
Epoch 1 de 5
------------------
Entrenamiento: Loss: 0.2651677090637386, accuracy: 0.90175
Validación: Loss: 0.3484566710740328, accuracy: 0.892
Epoch 2 de 5
------------------
Entrenamiento: Loss: 0.178699440728873, accuracy: 0.9460000000000001
Validación: Loss: 0.40424482348561286, accuracy: 0.8935000000000001
Epoch 3 de 5
------------------
Entrenamiento: Loss: 0.11829778066650033, accuracy: 0.971
Validación: Loss: 0.4401103568077087, accuracy: 0.898
Epoch 4 de 5
------------------
Entrenamiento: Loss: 0.07990703093074263, accuracy: 0.982625
Validación: Loss: 0.49894495904445646, accuracy: 0.8965
Epoch 5 de 5
------------------
Entrenamiento: Loss: 0.06776802394539118, accuracy: 0.9855
Validación: Loss: 0.49894495904445646, accuracy: 0.8965
Y al finalizar el entrenamiento vemos que en tan sólo 5 iteraciones el modelo alcanza una exactitud de 0.98 (o 98%) con el set de entrenamiento y de 0.89 (ú 89%) con el set de validación.
Aunque el modelo tiene algo de overfitting, es impresionante que con tan sólo 5 iteraciones se haya alcanzado un desempeño cercano al 90% con el set de validación.
Así que con esto ya estamos listos para la prueba final: la clasificación de sentimientos.
Clasificación de sentimientos
Con el modelo ya entrenado sólo nos resta presentarle nuevos textos para que clasifique el sentimiento como positivo o negativo.
Para facilitar las cosas, crearemos una función para clasificar cualquier texto nuevo:
def classifySentiment(review_text):
encoding_review = tokenizer.encode_plus(
review_text,
max_length = MAX_LEN,
truncation = True,
add_special_tokens = True,
return_token_type_ids = False,
pad_to_max_length = True,
return_attention_mask = True,
return_tensors = 'pt'
)
input_ids = encoding_review['input_ids'].to(device)
attention_mask = encoding_review['attention_mask'].to(device)
output = model(input_ids, attention_mask)
_, prediction = torch.max(output, dim=1)
print("\n".join(wrap(review_text)))
if prediction:
print('Sentimiento predicho: * * * * *')
else:
print('Sentimiento predicho: *')
Esta función:
- Toma como entrada el texto de un review (línea 1)
- Codifica dicho texto usando el tokenizer (líneas 2 a 11)
- Genera una predicción con el modelo afinado (líneas 13 a 16)
- Y finalmente imprime en pantalla el sentimiento predicho (líneas 17 a 21)
Veamos cómo clasifica esta crítica real: «Avengers: Infinity War at least had the good taste to abstain from Jeremy Renner. No such luck in Endgame.»
review_text = "Avengers: Infinity War at least had the good taste to abstain from Jeremy Renner. No such luck in Endgame."
classifySentiment(review_text)
Cuando ejecutamos el bloque de código anterior obtenemos un resultado similar a este:
Avengers: Infinity War at least had the good taste to abstain from
Jeremy Renner. No such luck in Endgame.
Sentiemiento predicho: *
Es decir que el sentimiento predicho es negativo (una estrella, «*»).
Pero lo más interesante es que hemos visto que el modelo es capaz de capturar matices complejos. En el ejemplo anterior, a pesar de que el texto no usa palabras explícitamente negativas, el modelo detecta la connotación de «mala suerte» y decepción, clasificándolo correctamente como negativo.
Enlaces de descarga set de datos y código fuente
▼ El código fuente de este tutorial
▼ El set de datos de este tutorial
Conclusiones
Acabamos de comprobar lo poderosa que es esta arquitectura de BERT al momento de procesar el lenguaje natural. Gracias a su arquitectura basada en Redes Transformer y al pre-entrenamiento bidireccional, es capaz de entender el contexto de las palabras de una forma mucho más profunda que los modelos tradicionales como RNNs o LSTMs.
En particular, en este tutorial hemos visto que:
- La preparación de datos es el paso más laborioso (tokenización, máscaras y tensores).
- El Fine-Tuning (o afinación) permite obtener resultados impresionantes incluso con pocas épocas de entrenamiento.
- Hugging Face y PyTorch proporcionan un ecosistema robusto para implementar estas soluciones de IA de manera relativamente sencilla.
