"BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding" https://arxiv.org/pdf/1810.04805.pdf
Desarrollado por Google en 2018, marcó un hito significativo en el campo del NLP. Este modelo se basa en la arquitectura Transformer, introducida en 2017 por Vaswani et al., y es conocido por su capacidad de entender el contexto de las palabras en el texto de una manera más sofisticada que los modelos anteriores. A diferencia de los enfoques anteriores que procesaban el texto de manera secuencial (de izquierda a derecha o viceversa), BERT analiza el texto en ambas direcciones simultáneamente. Esta bidireccionalidad permite a BERT capturar el contexto completo de una palabra, mirando tanto el texto anterior como el siguiente. Esto lo hace particularmente eficaz para entender el significado y la intención detrás de las palabras en oraciones complejas.
BERT se preentrena en un corpus masivo de texto no etiquetado y utiliza dos estrategias principales:
Masked Language Model (MLM): donde se ocultan aleatoriamente palabras del texto y el modelo aprende a predecirlas.
Next Sentence Prediction (NSP): donde el modelo aprende a predecir si una oración es la continuación lógica de otra.
Enmascaramiento de Tokens:
[MASK]
.Diversificación en el Enmascaramiento:
No todos los tokens seleccionados se enmascaran de la misma manera:
[MASK]
.Al diversificar el enmascaramiento, se asegura que el modelo no se sobreajuste a los tokens [MASK] y que aprenda a utilizar el contexto para predecir palabras, lo cual es más representativo de cómo se utilizará el modelo en aplicaciones del mundo real.
Al dejar algunos tokens seleccionados sin cambios, BERT aprende a predecir la idoneidad de las palabras actuales en su contexto original.
La sustitución con tokens aleatorios ayuda a que el modelo sea robusto frente a entradas inesperadas o ruidosas, mejorando su capacidad para manejar errores o variaciones en los datos de entrada.
Predicción de Tokens Enmascarados:
Durante el entrenamiento de BERT, solo los tokens seleccionados para predicción (aproximadamente el 15% de los tokens en cada secuencia de entrada) contribuyen al cálculo de la función de pérdida. Esto significa que el modelo únicamente calcula el error para estos tokens específicos, permitiéndole aprender a "adivinar" palabras en función de su contexto sin procesar cada token de la secuencia.
Dentro de estos tokens seleccionados, algunos se reemplazan por el token [MASK], otros se sustituyen por una palabra aleatoria, y un pequeño porcentaje se deja sin cambios. Aunque estos tokens no se alteran, la diferencia clave es que el modelo sí intenta predecirlos como parte de los tokens seleccionados para la predicción. Esto contrasta con los tokens no seleccionados (aquellos que no se modifican ni se predicen), los cuales no participan en el cálculo de la pérdida y, por lo tanto, el modelo no tiene que aprender nada sobre ellos.
Este enfoque, donde algunos tokens se dejan "sin cambios" pero aún participan en el entrenamiento, enseña al modelo a inferir palabras en su contexto sin depender exclusivamente de "pistas" explícitas de enmascaramiento, lo que mejora su capacidad para manejar el lenguaje en situaciones del mundo real.
Entendimiento de Relaciones entre Oraciones:
Preparación de Datos de Entrenamiento:
Uso del Token [CLS]
:
[CLS]
se añade al principio, y un token [SEP]
se utiliza para separar las dos oraciones.[CLS]
para hacer la predicción.Aprendizaje de Contexto y Relaciones:
Aplicabilidad en Tareas de NLP:
Vamos a crear un modelo BERT con la librería HuggingFace que nos permita utilizar modelos preentrenados y re-entrenarlos para las tareas que queramos. En este caso, vamos a realizar la clasificación de noticias en tres categorías: deportes, cultura y política.
from transformers import AutoTokenizer, AutoModelForSequenceClassification
tokenizer = AutoTokenizer.from_pretrained('bert-base-multilingual-cased')
model = AutoModelForSequenceClassification.from_pretrained("bert-base-multilingual-cased", num_labels=3)
Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-multilingual-cased and are newly initialized: ['classifier.weight', 'classifier.bias'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
Revisa el modelo que acabamos de crear. Comprueba que los elementos que lo componen y su estructura corresponden con lo que hemos visto en la teoría. ¿A qué corresponde el pooler?
model
BertForSequenceClassification( (bert): BertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(119547, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0-11): 12 x BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) (intermediate_act_fn): GELUActivation() ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) (pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh() ) ) (dropout): Dropout(p=0.1, inplace=False) (classifier): Linear(in_features=768, out_features=3, bias=True) )
Fíjate en el parámetro add_special_tokens=False. ¿Qué ocurre si lo cambiamos a True? ¿Qué son los tokens especiales? ¿Qué son los token_type_ids? ¿Y los attention_mask?
result = tokenizer.encode_plus("Hello, my dog is cute", add_special_tokens=False, return_tensors="pt")
print(result.keys())
print(result['input_ids'])
print(result['token_type_ids'])
print(result['attention_mask'])
dict_keys(['input_ids', 'token_type_ids', 'attention_mask']) tensor([[31178, 117, 15127, 17835, 10124, 21610, 10112]]) tensor([[0, 0, 0, 0, 0, 0, 0]]) tensor([[1, 1, 1, 1, 1, 1, 1]])
result = tokenizer.encode_plus("Hello, my dog is cute", add_special_tokens=True, return_tensors="pt")
print(result.keys())
print(result['input_ids'])
print(result['token_type_ids'])
print(result['attention_mask'])
dict_keys(['input_ids', 'token_type_ids', 'attention_mask']) tensor([[ 101, 31178, 117, 15127, 17835, 10124, 21610, 10112, 102]]) tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0]]) tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1]])
result = tokenizer.decode([101, 31178, 117, 15127, 17835, 10124, 21610, 10112, 102])
print(result)
[CLS] Hello, my dog is cute [SEP]
import torch
from torch.utils.data import Dataset
class TextClassificationDataset(Dataset):
def __init__(self, filename, tokenizer, max_length=256):
self.tokenizer = tokenizer
self.sentences = []
self.labels = []
self.max_length = max_length
with open(filename, 'r', encoding='utf-8') as file:
for line in file:
start = line.find('"')
end = line.find('"', start + 1)
sentence = line[start + 1:end].strip()
label = int(line[end + 1:].strip()[-1])
# Crear los tokens para alimentar a BERT de HuggingFace
tokens = self.tokenizer.encode_plus(sentence, add_special_tokens=True, max_length=self.max_length, truncation=True, padding='max_length', return_tensors='pt')
self.sentences.append(tokens)
self.labels.append(label)
def __len__(self):
return len(self.sentences)
def __getitem__(self, idx):
item = {key: val for key, val in self.sentences[idx].items()}
item['labels'] = torch.tensor(self.labels[idx])
item['input_ids'] = item['input_ids'].squeeze()
item['attention_mask'] = item['attention_mask'].squeeze()
item['token_type_ids'] = item['token_type_ids'].squeeze()
return item
# Creamos el conjunto de datos
dataset = TextClassificationDataset('data/dataset_clas_texto.txt', tokenizer)
# Separamos el conjunto de datos en entrenamiento y validación
train_size = int(0.8 * len(dataset))
val_size = len(dataset) - train_size
train_dataset, val_dataset = torch.utils.data.random_split(dataset, [train_size, val_size])
Usaremos el objeto Trainer de la librería transformers para entrenar nuestro modelo. Para ello, necesitamos el modelo, el tokenizador, los datos de entrenamiento y los datos de validación. Todos los parámetros que necesitamos para el entrenamiento están definidos en el objeto TrainingArguments.
from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
output_dir="test_trainer",
num_train_epochs=3,
logging_steps=1,
logging_strategy='steps'
)
trainer = Trainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=train_dataset,
eval_dataset=val_dataset
)
trainer.train()
huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks... To disable this warning, you can either: - Avoid using `tokenizers` before the fork if possible - Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)
0%| | 0/30 [00:00<?, ?it/s]
You're using a BertTokenizerFast tokenizer. Please note that with a fast tokenizer, using the `__call__` method is faster than using a method to encode the text followed by a call to the `pad` method to get a padded encoding.
{'loss': 1.1429, 'learning_rate': 4.8333333333333334e-05, 'epoch': 0.1} {'loss': 1.0467, 'learning_rate': 4.666666666666667e-05, 'epoch': 0.2} {'loss': 0.7933, 'learning_rate': 4.5e-05, 'epoch': 0.3} {'loss': 1.2766, 'learning_rate': 4.3333333333333334e-05, 'epoch': 0.4} {'loss': 0.9082, 'learning_rate': 4.166666666666667e-05, 'epoch': 0.5} {'loss': 0.641, 'learning_rate': 4e-05, 'epoch': 0.6} {'loss': 0.5573, 'learning_rate': 3.8333333333333334e-05, 'epoch': 0.7} {'loss': 0.4203, 'learning_rate': 3.6666666666666666e-05, 'epoch': 0.8} {'loss': 0.4596, 'learning_rate': 3.5e-05, 'epoch': 0.9} {'loss': 0.4265, 'learning_rate': 3.3333333333333335e-05, 'epoch': 1.0} {'loss': 0.3439, 'learning_rate': 3.1666666666666666e-05, 'epoch': 1.1} {'loss': 0.3159, 'learning_rate': 3e-05, 'epoch': 1.2} {'loss': 0.1833, 'learning_rate': 2.8333333333333335e-05, 'epoch': 1.3} {'loss': 0.1578, 'learning_rate': 2.6666666666666667e-05, 'epoch': 1.4} {'loss': 0.3174, 'learning_rate': 2.5e-05, 'epoch': 1.5} {'loss': 0.1195, 'learning_rate': 2.3333333333333336e-05, 'epoch': 1.6} {'loss': 0.1169, 'learning_rate': 2.1666666666666667e-05, 'epoch': 1.7} {'loss': 0.0985, 'learning_rate': 2e-05, 'epoch': 1.8} {'loss': 0.1015, 'learning_rate': 1.8333333333333333e-05, 'epoch': 1.9} {'loss': 0.0841, 'learning_rate': 1.6666666666666667e-05, 'epoch': 2.0} {'loss': 0.0716, 'learning_rate': 1.5e-05, 'epoch': 2.1} {'loss': 0.0569, 'learning_rate': 1.3333333333333333e-05, 'epoch': 2.2} {'loss': 0.0653, 'learning_rate': 1.1666666666666668e-05, 'epoch': 2.3} {'loss': 0.0501, 'learning_rate': 1e-05, 'epoch': 2.4} {'loss': 0.0442, 'learning_rate': 8.333333333333334e-06, 'epoch': 2.5} {'loss': 0.0476, 'learning_rate': 6.666666666666667e-06, 'epoch': 2.6} {'loss': 0.0427, 'learning_rate': 5e-06, 'epoch': 2.7} {'loss': 0.0418, 'learning_rate': 3.3333333333333333e-06, 'epoch': 2.8} {'loss': 0.037, 'learning_rate': 1.6666666666666667e-06, 'epoch': 2.9} {'loss': 0.0391, 'learning_rate': 0.0, 'epoch': 3.0} {'train_runtime': 22.9298, 'train_samples_per_second': 10.467, 'train_steps_per_second': 1.308, 'train_loss': 0.33358038067817686, 'epoch': 3.0}
TrainOutput(global_step=30, training_loss=0.33358038067817686, metrics={'train_runtime': 22.9298, 'train_samples_per_second': 10.467, 'train_steps_per_second': 1.308, 'train_loss': 0.33358038067817686, 'epoch': 3.0})
import os
import matplotlib.pyplot as plt
import json
def plot_training_loss(log_dir):
history = trainer.state.log_history
training_loss = [log['loss'] for log in history if 'loss' in log]
plt.plot(training_loss, label='Training Loss')
plt.xlabel('Steps')
plt.ylabel('Loss')
plt.title('Training Loss Over Time')
plt.legend()
plt.show()
plot_training_loss('./logs')
Vamos a probar nuestro modelo con algunas noticias de ejemplo.
sentences = []
labels = []
with open('data/dataset_clas_texto_test.txt', 'r', encoding='utf-8') as file:
for line in file:
start = line.find('"')
end = line.find('"', start + 1)
sentence = line[start + 1:end].strip()
label = int(line[end + 1:].strip()[-1])
sentences.append(sentence)
labels.append(label)
clases = {
0: 'Deportes',
1: 'Cultura',
2: 'Política'
}
device = torch.device("cpu")
model = model.to(device)
aciertos = 0
for sentence, label in zip(sentences, labels):
tokens = tokenizer.encode_plus(sentence, add_special_tokens=True, max_length=256, truncation=True, padding='max_length', return_tensors='pt')
tokens.to(device)
outputs = model(**tokens)
y_pred = torch.argmax(outputs.logits).item()
y = label
if y_pred == y:
aciertos += 1
print(f"{sentence} -> {clases[y]}, {clases[y_pred]}")
print("-"*50)
print(f"Aciertos: {aciertos}, Total: {len(sentences)}, Accuracy: {aciertos / len(sentences)}")
La selección de fútbol sub-21 se corona campeona en el torneo europeo juvenil. -> Deportes, Deportes Gran inauguración de la exposición de arte contemporáneo dedicada a los nuevos talentos iberoamericanos. -> Cultura, Cultura El congreso debate una nueva ley de reforma educativa centrada en la tecnología y la innovación. -> Política, Política La ciclista nacional se lleva el oro en el campeonato mundial de ciclismo en pista. -> Deportes, Deportes El festival de teatro de calle este año se enfoca en obras que exploran temas de sostenibilidad. -> Cultura, Cultura El gobierno lanza un plan de ayuda económica para el sector de las artes afectado por la pandemia. -> Política, Política El equipo de baloncesto universitario gana el campeonato nacional después de una temporada invicta. -> Deportes, Deportes Se inaugura el festival anual de cine documental, con énfasis en temas ambientales y sociales. -> Cultura, Cultura Anuncian nuevos subsidios para proyectos de investigación en energías limpias. -> Política, Política El joven boxeador gana el título mundial en su categoría con una victoria por K.O. -> Deportes, Deportes La nueva exposición en el museo de historia natural explora la biodiversidad del Amazonas. -> Cultura, Cultura Aprueban una nueva legislación para fortalecer la privacidad y seguridad en internet. -> Política, Política El equipo de rugby femenino celebra su primera victoria en el torneo internacional. -> Deportes, Deportes Anuncian los ganadores del premio nacional de literatura, con una notable diversidad de géneros. -> Cultura, Cultura El parlamento discute una nueva reforma en el sistema de salud pública. -> Política, Política El nadador establece un nuevo récord nacional en los 200 metros estilo espalda. -> Deportes, Deportes Se celebra el 50º aniversario de la orquesta sinfónica nacional con un concierto especial. -> Cultura, Cultura El gobierno introduce un nuevo plan de becas para estudiantes en áreas de ciencia y tecnología. -> Política, Política Victoria sorpresiva del equipo de hockey sobre césped en el campeonato internacional. -> Deportes, Deportes El festival de música barroca de este año incluye una serie de masterclasses y conferencias. -> Cultura, Cultura -------------------------------------------------- Aciertos: 20, Total: 20, Accuracy: 1.0