La arquitectura seq2seq, o secuencia a secuencia, es un tipo de modelo de red neuronal utilizado en el aprendizaje profundo para convertir secuencias de entrada en secuencias de salida. Es especialmente popular para tareas como traducción automática, generación de texto, y reconocimiento de voz. Se forma mediante la unión de dos componentes principales: un codificador y un decodificador.
El codificador toma la secuencia de entrada y la procesa para generar una representación interna, a menudo en forma de un vector de contexto o una serie de estados ocultos. Aquí está el proceso detallado:
Entrada de la secuencia: El modelo toma una secuencia de entrada (por ejemplo, una frase en inglés si estamos haciendo traducción inglés-español).
Embedding de palabras: Cada palabra o token de la secuencia se transforma en un vector mediante una capa de embebido (embedding).
Procesamiento recurrente: Los vectores embebidos se pasan a través de capas recurrentes (como LSTM o GRU) para procesar la secuencia. En cada paso temporal, la red actualiza su estado oculto basándose en el token actual y el estado oculto anterior.
Captura de contexto: El último estado oculto de la red (o una combinación de todos los estados ocultos, dependiendo del diseño) se toma como la representación condensada de la secuencia completa. Este vector de contexto captura la información esencial de la entrada.
El decodificador utiliza la representación generada por el codificador para producir la secuencia de salida.
Inicialización: El decodificador se inicializa con el estado oculto final del codificador. En algunos diseños, el vector de contexto también se usa directamente como parte de la entrada en cada paso del decodificador.
Generación de la secuencia de salida: En cada paso, el decodificador genera un token de la secuencia de salida.
Terminación: El proceso continúa hasta que se genera un token de final de secuencia o se alcanza una longitud máxima de secuencia.
import random
import string
import torch
allowed_chars = string.digits + '+'
class Generator():
def __init__(self) -> None:
pass
# Método para crear un ejemplo de entrenamiento
def sample(self):
s1 = random.randint(0, 999)
s2 = random.randint(0, 999)
r = s1 + s2
s1_string = str(s1).zfill(3)
s2_string = str(s2).zfill(3)
output = str(r).zfill(4)
input = s1_string + "+" + s2_string
return input, output
# Método para crear un lote de ejemplos de entrenamiento
def batch(self, n):
inputs = []
outputs = []
for _ in range(n):
input, output = self.sample()
inputs.append(input)
outputs.append(output)
return inputs, outputs
# Método para codificar una cadena de caracteres en un tensor one-hot
def string_to_tensor(self, s):
tensor = torch.zeros(len(s), len(allowed_chars))
for i, char in enumerate(s):
tensor[i, allowed_chars.index(char)] = 1
return tensor
# Método para decodificar un tensor one-hot en una cadena de caracteres
def tensor_to_string(self, tensor):
_, max_idx = tensor.max(1)
return ''.join([allowed_chars[i] for i in max_idx])
# Método para generar un lote de ejemplos de entrenamiento codificados
def batch_to_tensor(self, n):
seq_in = []
seq_out = []
inputs, outputs = self.batch(n)
# print(inputs, outputs)
for input, output in zip(inputs, outputs):
seq_in.append(self.string_to_tensor(input))
seq_out.append(self.string_to_tensor(output))
return torch.stack(seq_in), torch.stack(seq_out)
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
# Definir la arquitectura del modelo seq2seq
class Seq2Seq(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(Seq2Seq, self).__init__()
self.hidden_size = hidden_size
self.encoder = nn.LSTM(input_size, hidden_size, batch_first=True)
self.decoder = nn.LSTM(hidden_size, hidden_size, batch_first=True)
self.output = nn.Linear(hidden_size, output_size)
def forward(self, input, hidden=None):
_, (hn_enc, cn_enc) = self.encoder(input, hidden)
latent_tensor = hn_enc[0].unsqueeze(1).repeat(1, 4, 1)
out_dec, (_, _) = self.decoder(latent_tensor, (hn_enc, cn_enc))
out = F.softmax(self.output(out_dec), dim=2)
return out
model = Seq2Seq(input_size=len(allowed_chars), hidden_size=128, output_size=len(allowed_chars))
import matplotlib.pyplot as plt
history = []
# Bucle de entrenamiento
def train(model, optimizer, loss_fn, n_epochs, batch_size):
for epoch in range(n_epochs):
total_loss = 0
optimizer.zero_grad()
x, y = dg.batch_to_tensor(batch_size)
y_pred = model(x)
loss = loss_fn(y_pred, y)
loss.backward()
optimizer.step()
total_loss += loss.item()
# Print the loss every 10 epochs
if epoch % 100 == 0:
print("Epoch: {}, Loss: {}".format(epoch, total_loss))
history.append(total_loss)
# Definir la función de pérdida y el optimizador
loss_fn = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
# Entrenar el modelo
dg = Generator()
train(model, optimizer, loss_fn, 5000, 128)
plt.plot(history, label='loss')
Epoch: 0, Loss: 0.0828605443239212 Epoch: 100, Loss: 0.06704497337341309 Epoch: 200, Loss: 0.053312793374061584 Epoch: 300, Loss: 0.04993734508752823 Epoch: 400, Loss: 0.048714280128479004 Epoch: 500, Loss: 0.046345826238393784 Epoch: 600, Loss: 0.04619750753045082 Epoch: 700, Loss: 0.043793633580207825 Epoch: 800, Loss: 0.04464493691921234 Epoch: 900, Loss: 0.04283585771918297 Epoch: 1000, Loss: 0.04297322779893875 Epoch: 1100, Loss: 0.04274420812726021 Epoch: 1200, Loss: 0.04618293419480324 Epoch: 1300, Loss: 0.041276123374700546 Epoch: 1400, Loss: 0.04062522575259209 Epoch: 1500, Loss: 0.038878101855516434 Epoch: 1600, Loss: 0.03686640039086342 Epoch: 1700, Loss: 0.03679293021559715 Epoch: 1800, Loss: 0.03583793342113495 Epoch: 1900, Loss: 0.033504918217659 Epoch: 2000, Loss: 0.03172338381409645 Epoch: 2100, Loss: 0.03093797154724598 Epoch: 2200, Loss: 0.02861001528799534 Epoch: 2300, Loss: 0.028335781767964363 Epoch: 2400, Loss: 0.02830021269619465 Epoch: 2500, Loss: 0.027093930169939995 Epoch: 2600, Loss: 0.027020571753382683 Epoch: 2700, Loss: 0.02591128461062908 Epoch: 2800, Loss: 0.022362468764185905 Epoch: 2900, Loss: 0.01789195090532303 Epoch: 3000, Loss: 0.015600097365677357 Epoch: 3100, Loss: 0.011381622403860092 Epoch: 3200, Loss: 0.009755373001098633 Epoch: 3300, Loss: 0.005409994162619114 Epoch: 3400, Loss: 0.004879375919699669 Epoch: 3500, Loss: 0.003630860708653927 Epoch: 3600, Loss: 0.00408724881708622 Epoch: 3700, Loss: 0.0028715708758682013 Epoch: 3800, Loss: 0.0023585204035043716 Epoch: 3900, Loss: 0.0036159458104521036 Epoch: 4000, Loss: 0.0015362270642071962 Epoch: 4100, Loss: 0.0026310868561267853 Epoch: 4200, Loss: 0.0020577835384756327 Epoch: 4300, Loss: 0.002376667922362685 Epoch: 4400, Loss: 0.0010008710669353604 Epoch: 4500, Loss: 0.002320927334949374 Epoch: 4600, Loss: 0.002437483984977007 Epoch: 4700, Loss: 0.0034853359684348106 Epoch: 4800, Loss: 0.0018539653392508626 Epoch: 4900, Loss: 0.0022051495034247637
[<matplotlib.lines.Line2D at 0x1370ac430>]
# Evaluar el modelo
def evaluate(model, n):
x, y = dg.batch_to_tensor(n)
y_pred = model(x)
for i in range(n):
print(dg.tensor_to_string(x[i]), dg.tensor_to_string(y_pred[i]), dg.tensor_to_string(y[i]))
evaluate(model, 10)
886+626 1512 1512 268+693 0961 0961 657+168 0825 0825 100+932 1032 1032 295+137 0432 0432 250+634 0885 0884 435+075 0510 0510 098+963 1061 1061 901+162 1063 1063 043+200 0243 0243
Es importante destacar que el modelo no realiza una suma de números siguiendo un procedimiento aritmético, sino que aprende a generar la secuencia de salida correcta mediante un proceso de traducción aprendido a partir de los datos de entrenamiento.
Modifica el código anterior para que el modelo pueda recibir secuencias de longitud variable. Para ello, usa el generador de datos siguiente. Otra de las cosas que debes tener en cuenta es que el generador devuelve una lista de tensores, esto es así para que puedas usar la función pad_sequence
para rellenar las secuencias más cortas con ceros. Consulta la referencia de la función para ver cómo se usa. El parámetro batch_first
es importante para que la función sepa si los datos están en formato (batch_size, seq_len, input_size)
o (seq_len, batch_size, input_size)
. Nos interesa que estén en el primer formato.
import random
import string
import torch
allowed_chars = string.digits + '+' + '#' # <-- Añadimos el caracter # como padding para la salida
class Generator2():
def __init__(self) -> None:
pass
# Método para crear un ejemplo de entrenamiento
def sample(self):
s1 = random.randint(0, 9999)
s2 = random.randint(0, 9999)
r = s1 + s2
s1_string = str(s1)
s2_string = str(s2)
output = str(r)
input = s1_string + "+" + s2_string
return input, output
# Método para crear un lote de ejemplos de entrenamiento
def batch(self, n):
inputs = []
outputs = []
for _ in range(n):
input, output = self.sample()
inputs.append(input)
outputs.append(output)
return inputs, outputs
# Método para codificar una cadena de caracteres en un tensor one-hot
def string_to_tensor(self, s):
tensor = torch.zeros(len(s), len(allowed_chars))
for i, char in enumerate(s):
tensor[i, allowed_chars.index(char)] = 1
return tensor
# Método para decodificar un tensor one-hot en una cadena de caracteres
def tensor_to_string(self, tensor):
_, max_idx = tensor.max(1)
return ''.join([allowed_chars[i] for i in max_idx])
# Método para generar un lote de ejemplos de entrenamiento codificados
def batch_to_tensor(self, n):
seq_in = []
seq_out = []
inputs, outputs = self.batch(n)
for input, output in zip(inputs, outputs):
seq_in.append(self.string_to_tensor(input))
seq_out.append(self.string_to_tensor(output))
return seq_in, seq_out # <-- Devolvemos dos listas de tensores
gen = Generator2()
batch_in, batch_out = gen.batch_to_tensor(2)
print(batch_in)
print("-"*50)
print(batch_out)
[tensor([[0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0.], [1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.], [0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.]]), tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.], [0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.], [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.], [0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.], [1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])] -------------------------------------------------- [tensor([[0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.], [1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.]]), tensor([[0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.], [1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.]])]