Long Short-Term Memory¶
Dans le notebook précédent, nous avons présenté la couche classique d'un RNN. Depuis l'invention de cette couche classique, de nombreuses autres couches récurrentes ont été inventées.
Dans ce notebook, nous présentons la couche LSTM (long short-term memory) qui offre une alternative à la couche classique RNN.
Qu'est ce qu'une couche LSTM ?¶
La couche LSTM est consistituée d'une memory unit qui comporte 4 couches fully connected.
3 de ces 4 couches sont utilisées pour la sélection des informations précédentes pertinentes. Il s'agit de la forget gate, l'input gate et l'output gate :
- forget gate : Permet de supprimer de l'information de la mémoire
- input gate : Permet d'insérer de l'information dans la mémoire
- output gate : Permet d'utiliser l'information présente dans la mémoire
La dernière couche fully connected va créer une "information candidate" pour l'insertion dans la mémoire de la couche LSTM.
Figure extraite du blogpost.
Comme on le voit sur la figure, la couche LSTM reçoit 3 vecteurs en entrée $H_{t-1}$, $C_{t-1}$ et $X_{t}$. Les deux premiers viennent du LSTM directement et le troisième correspond à l'entrée au temps $t$ (le caractère pour nous).
Pour expliquer le concept sans entrer dans les détails : $H_{t-1}$ contient la mémoire à court terme (short term) et $C_{t-1}$ la mémoire à long terme. Cela permet de conserver les informations importantes sur un contexte important tout en ne négligeant pas le contexte plus local.
Le but de cette architecture est de pallier le problème de la propagation de l'information sur de longues séquences rencontré dans les RNN classiques.
Pour comprendre la couche LSTM en détail, vous pouvez lire l'article ou consulter le blogpost.
Implémentation pytorch¶
import torch
import torch.nn as nn
Dataset¶
Pour la création du dataset, nous utilisons à nouveau le fichier moliere.txt et nous reprenons le code du notebook précédent.
with open('moliere.txt', 'r', encoding='utf-8') as f:
text = f.read()
print("Nombre de caractères dans le dataset : ", len(text))
Nombre de caractères dans le dataset : 1687290
On réduit le nombre d'éléments pour avoir un entraînement rapide (à commenter si vous voulez entraîner sur l'ensemble des données).
text=text[:100000]
print("Nombre de caractères dans le dataset : ", len(text))
Nombre de caractères dans le dataset : 100000
chars = sorted(list(set(text)))
vocab_size = len(chars)
print(''.join(chars))
print("Nombre de caractères différents : ", vocab_size)
!'(),-.:;?ABCDEFGHIJLMNOPQRSTUVYabcdefghijlmnopqrstuvxyz«»ÇÈÉÊàâæçèéêîïôùû Nombre de caractères différents : 76
stoi = { ch:i for i,ch in enumerate(chars) }
itos = { i:ch for i,ch in enumerate(chars) }
encode = lambda s: [stoi[c] for c in s] # encode : prend un string et output une liste d'entiers
decode = lambda l: ''.join([itos[i] for i in l]) # decode: prend une liste d'entiers et output un string
data = torch.tensor(encode(text), dtype=torch.long)
Séparation en train et test.
n = int(0.9*len(data)) # 90% pour le train et 10% pour le test
train_data = data[:n]
test = data[n:]
Création du modèle¶
Pour créer notre modèle, nous allons utiliser directement l'implémentation pytorch de la couche du LSTM. A l'inverse des couches linéaires ou convolutives, la couche nn.LSTM permet de stacker plusieurs couches grâce au paramètre num_layers. Si on veut définir les couches une par une, il faut utiliser nn.LSTMCell.
class lstm(nn.Module):
def __init__(self, vocab_size, hidden_size,num_layers=1):
super(lstm, self).__init__()
self.hidden_size = hidden_size
# On utilise un embedding pour transformer les entiers(caractères) en vecteurs
self.embedding = nn.Embedding(vocab_size, hidden_size)
# La couche LSTM peut prendre l'argument num_layers pour empiler plusieurs couches LSTM
self.lstm = nn.LSTM(hidden_size, hidden_size, num_layers=num_layers)
# Une dernière couche linéaire pour prédire le prochain caractère
self.fc = nn.Linear(hidden_size, vocab_size)
def forward(self, x, hidden):
x = self.embedding(x)
x, hidden = self.lstm(x, hidden)
x = self.fc(x)
return x, (hidden[0].detach(), hidden[1].detach())
def init_hidden(self, batch_size):
return (torch.zeros(1, batch_size, self.hidden_size), torch.zeros(1, batch_size, self.hidden_size))
Entraînement¶
epochs = 20
lr=0.001
hidden_dim=128
seq_len=100
num_layers=1
model=lstm(vocab_size,hidden_dim,num_layers)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
La couche LSTM prend en entrée directement une séquence et renvoie une séquence de la même taille. Cela permet d'accélerer l'entraînement car on peut traîter plusieurs exemples en même temps.
Note : Il est aussi possible d'accélerer l'entraînement en faisant un traîtement en batch avec plusieurs séquences en parallèle.
for epoch in range(epochs):
state=None
running_loss = 0
n=0
data_ptr = torch.randint(100,(1,1)).item()
# On train sur des séquences de seq_len caractères et on break si on dépasse la taille du dataset
while True:
x = train_data[data_ptr : data_ptr+seq_len]
y = train_data[data_ptr+1 : data_ptr+seq_len+1]
optimizer.zero_grad()
y_pred,state = model.forward(x,state)
loss = criterion(y_pred, y)
running_loss += loss.item()
n+=1
loss.backward()
optimizer.step()
data_ptr+=seq_len
# Pour éviter de sortir de l'index du dataset
if data_ptr + seq_len + 1 > len(train_data):
break
print("Epoch: {0} \t Loss: {1:.8f}".format(epoch, running_loss/n))
Epoch: 0 Loss: 2.17804336 Epoch: 1 Loss: 1.76270216 Epoch: 2 Loss: 1.62740668 Epoch: 3 Loss: 1.54147145 Epoch: 4 Loss: 1.47995140 Epoch: 5 Loss: 1.43100239 Epoch: 6 Loss: 1.39074463 Epoch: 7 Loss: 1.35526441 Epoch: 8 Loss: 1.32519794 Epoch: 9 Loss: 1.29712536 Epoch: 10 Loss: 1.27268774 Epoch: 11 Loss: 1.24876227 Epoch: 12 Loss: 1.22720749 Epoch: 13 Loss: 1.20663312 Epoch: 14 Loss: 1.18768359 Epoch: 15 Loss: 1.16936996 Epoch: 16 Loss: 1.15179397 Epoch: 17 Loss: 1.13514291 Epoch: 18 Loss: 1.11997525 Epoch: 19 Loss: 1.10359089
On peut maintenant évaluer le loss sur nos données de test.
state=None
running_loss = 0
n=0
data_ptr = torch.randint(100,(1,1)).item()
while True:
with torch.no_grad():
x = test[data_ptr : data_ptr+seq_len]
y = test[data_ptr+1 : data_ptr+seq_len+1]
y_pred,state = model.forward(x,state)
loss = criterion(y_pred, y)
running_loss += loss.item()
n+=1
data_ptr+=seq_len
if data_ptr + seq_len + 1 > len(test):
break
print("Loss de test: {0:.8f}".format(running_loss/n))
Loss de test: 1.51168611
Le modèle overfit pas mal ... Essayez de corriger ça par vous-même.
Génération¶
On va maintenant pouvoir test la génération de texte !
import torch.nn.functional as F
moliere='.'
sequence_length=250
state=None
for i in range(sequence_length):
x = torch.tensor(encode(moliere[-1]), dtype=torch.long).squeeze()
y_pred,state = model.forward(x.unsqueeze(0),state)
probs=F.softmax(torch.squeeze(y_pred), dim=0)
sample=torch.multinomial(probs, 1)
moliere+=itos[sample.item()]
print(moliere)
. Çà coeuse, et bon enfin l'avoir faire. MASCARILLE. En me donner d vous, Le pas. MASCARILLE, à dans un pour sûte matinix! cette ma foi. PANDOLFE. Ma foi, tu te le sy sois touves d'arrête sa bien sans les bonheur. MASCARILLE. Moi, je me suis to
La génération est peut-être un peu mieux que pour le modèle RNN de base mais ce n'est pas encore convaincant. Vous pouvez essayer d'améliorer les performances du modèle en faisant varier les paramètres (nombre de couches en série, hidden dim etc ...)