El conjunto de datos "AG_NEWS" es un conjunto de datos de clasificación de texto ampliamente utilizado en el campo del procesamiento de lenguaje natural (NLP). Contiene noticias de diferentes categorías y se utiliza comúnmente para tareas de clasificación de texto. El conjunto de datos AG_NEWS consta de noticias de cuatro categorías principales, que son:
World: Noticias sobre eventos y acontecimientos globales, como política internacional, relaciones internacionales y noticias mundiales en general.
Sports: Noticias relacionadas con eventos deportivos, resultados de partidos, eventos deportivos nacionales e internacionales, etc.
Business: Noticias relacionadas con el mundo de los negocios, finanzas, economía, empresas, informes de ganancias y otros temas económicos.
Sci/Tech: Noticias relacionadas con ciencia y tecnología, incluyendo avances científicos, novedades tecnológicas, gadgets, investigaciones científicas y más.
Cada instancia del conjunto de datos AG_NEWS generalmente consiste en un título y un cuerpo de una noticia, junto con una etiqueta que indica la categoría a la que pertenece.
from torchtext import datasets
from torchtext.data import to_map_style_dataset
import numpy as np
# Load the dataset
train_iter, test_iter = datasets.AG_NEWS(split=('train', 'test'))
train_ds = to_map_style_dataset(train_iter)
test_ds = to_map_style_dataset(test_iter)
train = np.array(train_ds)
test = np.array(test_ds)
# Create vocabulary and embedding
from torchtext.vocab import build_vocab_from_iterator
from torchtext.data.utils import get_tokenizer
tokenizer = get_tokenizer("basic_english")
vocab = build_vocab_from_iterator(map(lambda x: tokenizer(x[1]), train_iter), specials=['<pad>','<unk>'])
vocab.set_default_index(vocab["<unk>"])
print("Tamaño del vocabulario:", len(vocab), "tokens")
print("Tokenización de la frase 'Here is an example sentence':", tokenizer("Here is an example sentence"))
print("Índices de las palabras 'here', 'is', 'an', 'example', 'supercalifragilisticexpialidocious':", vocab(['here', 'is', 'an', 'example', 'supercalifragilisticexpialidocious']))
print("Palabras correspondientes a los índices 475, 21, 30, 5297, 0:", vocab.lookup_tokens([475, 21, 30, 5297, 0]))
print("Las diez primeras palabras del vocabulario:", vocab.get_itos()[:10])
Tamaño del vocabulario: 95812 tokens Tokenización de la frase 'Here is an example sentence': ['here', 'is', 'an', 'example', 'sentence'] Índices de las palabras 'here', 'is', 'an', 'example', 'supercalifragilisticexpialidocious': [476, 22, 31, 5298, 1] Palabras correspondientes a los índices 475, 21, 30, 5297, 0: ['version', 'at', 'from', 'establish', '<pad>'] Las diez primeras palabras del vocabulario: ['<pad>', '<unk>', '.', 'the', ',', 'to', 'a', 'of', 'in', 'and']
text_pipeline = lambda x: vocab(tokenizer(x))
label_pipeline = lambda x: int(x) - 1
print("Tokenización de la frase 'Here is an example sentence':", text_pipeline("Here is an example sentence"))
Tokenización de la frase 'Here is an example sentence': [476, 22, 31, 5298, 2994]
En PyTorch, DataLoader se utiliza para cargar y manejar datos de manera eficiente durante el entrenamiento de modelos de aprendizaje profundo, especialmente en tareas de aprendizaje supervisado como la clasificación, la regresión y más. El DataLoader forma parte de la biblioteca torch.utils.data, y su objetivo principal es facilitar la administración de lotes (batches) de datos y la distribución de esos lotes al modelo de forma automática.
La función collate_batch
es una función personalizada que se utiliza como argumento para collate_fn al crear instancias de los objetos DataLoader. Tiene la responsabilidad de procesar y agrupar las muestras individuales de datos dentro de un lote (batch) de manera que sean compatibles para su posterior procesamiento dentro de la LSTM. Es especialmente útil cuando las secuencias de texto tienen longitudes diferentes y es necesario realizar un relleno (padding) para que todas tengan la misma longitud.
from torch.utils.data import DataLoader
import torch
def collate_batch(batch):
label_list, text_list = [], []
for sample in batch:
label, text = sample
text_list.append(torch.tensor(text_pipeline(text), dtype=torch.long))
label_list.append(label_pipeline(label))
return torch.tensor(label_list, dtype=torch.long), torch.nn.utils.rnn.pad_sequence(text_list, batch_first=True, padding_value=vocab["<pad>"])
train_dataloader = DataLoader(
train_iter, batch_size=64, shuffle=True, collate_fn=collate_batch
)
test_dataloader = DataLoader(
test_iter, batch_size=64, shuffle=True, collate_fn=collate_batch
)
Para verificar que estamos creando correctamente los lotes, vamos a imprimir las primeras cuatro instancias:
for batch in train_dataloader:
print(batch[1][:4])
print("\n")
print(batch[0][:4])
print("\n")
break
tensor([[ 152, 44, 11, 1618, 483, 343, 149, 24, 74, 14, 32, 15, 16, 6, 669, 8, 483, 93, 9, 6, 1194, 8, 453, 59, 1242, 58, 1878, 361, 5, 431, 2777, 1272, 69, 1417, 4, 21, 202, 12, 3, 950, 1315, 4, 9, 68, 1015, 152, 6540, 281, 2, 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, 0, 0, 0, 0, 0, 0, 0], [ 6090, 1510, 10335, 414, 12, 41858, 237, 14, 32, 15, 189, 191, 6090, 1696, 2795, 23, 48, 27190, 8, 3, 37, 483, 1829, 127, 57, 4, 604, 26, 96, 275, 16502, 55, 10335, 414, 88, 2, 30, 365, 8933, 191, 7, 2240, 12, 19579, 83, 8, 707, 2, 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, 0, 0, 0, 0, 0, 0, 0, 0], [11302, 145, 1024, 5, 466, 14, 32, 15, 32, 16, 8863, 300, 35, 257, 3535, 748, 1177, 257, 482, 91, 257, 835, 2905, 4, 47140, 6865, 27, 58, 569, 155, 5, 326, 337, 910, 12, 145, 8, 3, 7516, 120, 2, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 8064, 5, 276, 69, 261, 4, 1565, 330, 8152, 219, 2, 14, 12475, 15, 34, 276, 69, 261, 4, 1565, 330, 9, 415, 6, 192, 7, 23, 3628, 268, 6, 471, 886, 21, 830, 917, 532, 9, 6054, 11, 72656, 1898, 560, 20, 14537, 1351, 4, 3, 55, 27, 11, 58, 2, 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, 0, 0, 0, 0]]) tensor([2, 2, 1, 2])
Definimos una clase llamada LSTMTextClassificationModel
para la clasificación de texto basado en LSTM. Esta clase hereda de nn.Module
, la clase base para todos los modelos en PyTorch.
Inicialización del Modelo: En el método __init__
, se definen los componentes principales del modelo y se configuran sus parámetros:
Capa de Embedding: Se define una capa de embedding (nn.Embedding
) que se utilizará para representar las palabras como vectores densos. Esta capa no es pre-entrenada, lo que significa que los vectores de embedding se entrenarán junto con el modelo durante el proceso de entrenamiento.
Capa LSTM: Se define una capa LSTM (nn.LSTM
) que toma los vectores de embedding como entrada. embed_dim
es la dimensión de entrada de la capa LSTM, y hidden_dim
es la dimensión de su capa oculta. batch_first=True
indica que los datos de entrada tendrán la forma (batch_size, sequence_length, embed_dim)
.
Capa Fully Connected (FC): Se define una capa completamente conectada (nn.Linear
) que se utiliza para producir las salidas de clasificación. Toma las salidas de la capa LSTM correspondientes a la última iteración y las reduce a num_class
dimensiones, que corresponde al número de clases en la tarea de clasificación.
import torch
import torch.nn as nn
class LSTMTextClassificationModel(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):
super(LSTMTextClassificationModel, self).__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim) # <-- Capa de embedding genérica (no pre-entrenada)
self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True)
self.fc = nn.Linear(hidden_dim, num_class)
def forward(self, text):
embedded = self.embedding(text) # <-- Tras pasar por la capa de embedding, las palabras se representan como vectores
lstm_out, _ = self.lstm(embedded)
# Tomar la última salida de la secuencia LSTM
last_output = lstm_out[:, -1, :]
output = self.fc(last_output)
return output
Antes de seguir veamos cómo se conforma un batch de datos tras pasar la capa de embedding. Como se muestra en la figura siguiente, un batch es un vector de tres dimensiones, donde la primera dimensión es el tamaño del batch, la segunda dimensión es el número de palabras en cada secuencia y la tercera dimensión es el tamaño del vector de embedding (channels o features).
Por ejemplo, un token estaría ahora representado por un vector de embedding de tres componentes, en lugar de un escalar. Si lo quisiéramos referenciar sería: mini_lote[0,3,:]
.
Fíjate que la tercera dimensión del tensor de salida de la LSTM no tiene por qué tener el mismo tamaño que la tercera dimensión del tensor de entrada. La parte del tensor correspondiente a la línea de código last_output = lstm_out[:, -1, :]
sería parecida a la siguiente:
Ya podemos pasar un primer mini-lote de datos por la red. Para ello, vamos a crearlo a partir de los datos de entrenamiento. Esto solo lo hacemos para asegurarnos de que el código funciona correctamente.
model = LSTMTextClassificationModel(len(vocab), 32, 64, 4)
model.train()
for batch in train_dataloader:
predicted_label = model(batch[1])
label = batch[0]
break
print(batch[1][:4])
print(predicted_label[:4])
print(label[:4])
tensor([[ 203, 85, 1644, 1605, 29, 1480, 24, 74, 14, 28, 15, 16, 203, 65, 2, 27, 11, 57, 18, 3, 1605, 29, 1480, 12, 23, 1385, 385, 727, 2, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 754, 19257, 18337, 39, 17, 2675, 136, 13709, 17, 352, 14, 28, 15, 16, 8, 62, 7, 3, 144, 8149, 21035, 5, 10132, 21, 3, 352, 754, 4, 3, 425, 510, 17, 10, 631, 7911, 1485, 80, 18337, 11, 58, 12, 9978, 8, 5, 2675, 136, 13709, 2, 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, 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], [ 5060, 13, 10, 28480, 2172, 314, 146, 5, 110, 25803, 277, 5060, 13, 10, 2222, 28480, 193, 3, 25803, 19, 31, 21424, 7328, 7, 25181, 3382, 4, 442, 6282, 14, 2381, 2, 7938, 3440, 15, 11, 56, 4, 1323, 3, 354, 49, 277, 19075, 2, 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, 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, 0, 0, 0], [ 2206, 4, 8035, 1658, 8, 1284, 5530, 352, 4, 1148, 16, 579, 2206, 444, 1351, 7, 1852, 12, 3, 314, 8243, 5530, 2588, 92, 4, 9, 892, 327, 60, 50, 87, 82, 1121, 5, 3, 176, 112, 12, 3, 2435, 17, 10, 1565, 5530, 3761, 217, 2, 2206, 17, 920, 4487, 8035, 9, 18271, 2129, 10498, 17318, 9, 4329, 24249, 14464, 80, 1908, 73, 3, 4036, 8, 3, 1284, 379, 14564, 2, 2, 2, 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]]) tensor([[ 0.1983, 0.0697, -0.0783, 0.0542], [ 0.1983, 0.0697, -0.0783, 0.0542], [ 0.1983, 0.0697, -0.0783, 0.0542], [ 0.1983, 0.0697, -0.0783, 0.0542]], grad_fn=<SliceBackward0>) tensor([2, 1, 1, 0])
Vamos a construir las funciones train
y evaluate
. Dentro de la función evaluate
vamos a prestar atención al contexto with torch.no_grad():
, que sirve para indicar que no se van a calcular los gradientes. Esto es así porque en la fase de evaluación no se van a actualizar los pesos de la red, solo se van a utilizar para calcular la precisión de la red. Esto optimiza los recursos utilizados por PyTorch en cuanto a la gestión de la memoria.
Recuerda que en PyTorch, model.train()
y model.eval()
son métodos que se utilizan para cambiar el modo de entrenamiento de un modelo de aprendizaje profundo. Estos métodos afectan el comportamiento de ciertos módulos en el modelo, como las capas de dropout y normalización, que se comportan de manera diferente durante el entrenamiento y la evaluación. Específicamente hacen lo siguiente:
model.train():
model.train()
, el modelo se coloca en modo de entrenamiento.model.eval():
model.eval()
, el modelo se coloca en modo de evaluación o inferencia.En la práctica, es común utilizar model.train()
antes de cada época de entrenamiento y model.eval()
antes de realizar inferencia o evaluación en un modelo entrenado. Cambiar entre estos modos es esencial para garantizar que el modelo se comporte correctamente en diferentes etapas del proceso de entrenamiento y evaluación.
import time
# Hyperparameters
EPOCHS = 10 # epoch
LR = 5 # learning rate
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=LR)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1.0, gamma=0.1)
def train(dataloader):
model.train()
total_acc, total_count, max_acc = 0, 0, 0
log_interval = 500
start_time = time.time()
for idx, (label, text) in enumerate(dataloader):
optimizer.zero_grad()
predicted_label = model(text)
loss = criterion(predicted_label, label)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1)
optimizer.step()
total_acc += (predicted_label.argmax(1) == label).sum().item()
total_count += label.size(0)
if idx % log_interval == 0 and idx > 0:
elapsed = time.time() - start_time
print('| {:5d} batches '
'| accuracy {:8.3f}'.format(idx, total_acc / total_count))
if max_acc < total_acc / total_count:
max_acc = total_acc / total_count
total_acc, total_count = 0, 0
start_time = time.time()
return max_acc
def evaluate(dataloader):
model.eval()
total_acc, total_count = 0, 0
with torch.no_grad():
for idx, (label, text) in enumerate(dataloader):
predicted_label = model(text)
loss = criterion(predicted_label, label)
total_acc += (predicted_label.argmax(1) == label).sum().item()
total_count += label.size(0)
return total_acc / total_count
for epoch in range(1, EPOCHS + 1):
epoch_start_time = time.time()
accu_train = train(train_dataloader)
accu_val = evaluate(test_dataloader)
#if accu_train > accu_val:
# scheduler.step()
print("-" * 59)
print(
"| end of epoch {:3d} | time: {:5.2f}s | "
"valid accuracy {:8.3f} ".format(
epoch, time.time() - epoch_start_time, accu_val
)
)
print("-" * 59)
| 500 batches | accuracy 0.254 | 1000 batches | accuracy 0.256 | 1500 batches | accuracy 0.257 ----------------------------------------------------------- | end of epoch 1 | time: 59.81s | valid accuracy 0.252 ----------------------------------------------------------- | 500 batches | accuracy 0.259 | 1000 batches | accuracy 0.256 | 1500 batches | accuracy 0.255 ----------------------------------------------------------- | end of epoch 2 | time: 60.92s | valid accuracy 0.253 ----------------------------------------------------------- | 500 batches | accuracy 0.352 | 1000 batches | accuracy 0.464 | 1500 batches | accuracy 0.532 ----------------------------------------------------------- | end of epoch 3 | time: 61.16s | valid accuracy 0.567 ----------------------------------------------------------- | 500 batches | accuracy 0.588 | 1000 batches | accuracy 0.671 | 1500 batches | accuracy 0.741 ----------------------------------------------------------- | end of epoch 4 | time: 61.33s | valid accuracy 0.759 ----------------------------------------------------------- | 500 batches | accuracy 0.781 | 1000 batches | accuracy 0.812 | 1500 batches | accuracy 0.833 ----------------------------------------------------------- | end of epoch 5 | time: 61.38s | valid accuracy 0.822 ----------------------------------------------------------- | 500 batches | accuracy 0.838 | 1000 batches | accuracy 0.852 | 1500 batches | accuracy 0.867 ----------------------------------------------------------- | end of epoch 6 | time: 61.69s | valid accuracy 0.848 ----------------------------------------------------------- | 500 batches | accuracy 0.861 | 1000 batches | accuracy 0.874 | 1500 batches | accuracy 0.886 ----------------------------------------------------------- | end of epoch 7 | time: 61.59s | valid accuracy 0.856 ----------------------------------------------------------- | 500 batches | accuracy 0.875 | 1000 batches | accuracy 0.889 | 1500 batches | accuracy 0.899 ----------------------------------------------------------- | end of epoch 8 | time: 61.54s | valid accuracy 0.873 ----------------------------------------------------------- | 500 batches | accuracy 0.889 | 1000 batches | accuracy 0.900 | 1500 batches | accuracy 0.911 ----------------------------------------------------------- | end of epoch 9 | time: 61.79s | valid accuracy 0.877 ----------------------------------------------------------- | 500 batches | accuracy 0.900 | 1000 batches | accuracy 0.908 | 1500 batches | accuracy 0.919 ----------------------------------------------------------- | end of epoch 10 | time: 61.90s | valid accuracy 0.874 -----------------------------------------------------------
Modifica el código anterior para adaptar el modelo LSTM al uso de embeddings preentrenados. Para ello, usa from torchtext.vocab import GloVe
y elige el conjunto de embeddings GloVe que prefieras. Puedes encontrar más información en https://pytorch.org/text/stable/vocab.html#torchtext.vocab.GloVe
Verifica si se produce una mejora en la precisión del modelo. ¿Qué ocurre si usas un conjunto de embeddings preentrenados de diferentes tamaños?