Introduction : Traitement de langage naturel et plongement lexical
Dans ce TD, nous allons nous intéresser à la génération automatique de texte.
Le traitement de langage naturel (NLP - Natural Language Processing) a connu des grands succès dans la dernière décennie, notamment avec les grands modèles de langage (LLM -Large Language Models). Contrairement à ce qui a été fait historiquement en traitement automatique du langage, qui essayait de donner explicitement des étiquettes aux différents types de mots (verbes, sujet, objet, etc.), aujourd’hui, tous les sens des mots sont appris par apprentissage automatique.
A la base est un grand corpus de texte provenant de différentes sources (internet, livres, …), et des modèle de langage apprennent “en lisant”, en regardant par exemple la proximité des mots (avec l’approche bag of words par exemple), ou les mots entourant (skipgrams). Aujourd’hui, les réseaux de neurones sont majoritairement utilisé pour cet apprentissage, on dit aussi traitement de langage neuronal (neural language processing).
L’élément central de ces apprentissages est le plongement lexical (Word embedding). Chaque entité, le plus souvent un mot, est appelé un token. Parfois, on peut aussi utiliser des caractères comme unité, mais nous allons ici nous concentrer à des mots. Le plongement lexical associe à chaque token un vecteur de nombres - et la taille de ce vecteur est appelé la dimension du embedding. Cette dimension est souvent dans l’order des centaines ou milliers, afin de pouvoir capter des relations de voisinage de mots selon différents critères.
Le déroulement de ce TD est comme suit, nous allons d’abord découvrir le plongement lexical avec le modèle Word2Vec, qui a été un des premier modèle à base de réseau de neurones a fournir des résultats surprenant. En particulier, nous allons travailler sur un modèle déjà entrainé, et explorer la proximité des mots.
Ensuite, nous allons voir plus en détail comment fonctionne ce plongement lexical, avec un exemple sur un très petit corpus (moins que 10 mots), sans l’entrainer.
Puis, nous allons travailler sur un corpus plus grand, qui consiste d’un texte contenant tous les fables de Lafontaine. Nous allons explorer le plongement lexical, et entrainer un modèle de traitement de langage naturel permettant de générer la suite d’une phrase. Plus précisément, à partir d’une séquence des mots, nous allons prédire le prochain mot. Tout comme pour la prédiction d’une prochaine donnée d’une fonction comme dans le dernier TD, nous allons utiliser un réseau de neurones récurrent (RNN - recurrent neural networks).
Contrairement au dernier TD qui traitement de la prédiction d’une donnée de type tâche de régression, nous allons cette fois-ci prédire une classification : à partir d’une séquence de mots, nous allons apprendre pour prédire le prochain mot. Nous pouvons ensuite faire des prédictions multiples, comme dans le dernier TD, pour générer un plus long texte.
Le premier objectif de ce TD est de comprendre le principe du plongement lexical et l’apprentissage automatique pour le langage.
Le deuxième objectif est d’expérimenter avec le modèle Word2Vec déjà entrainé.
Le troisième objectif est d’entraîner un propre modèle de langage, avec un réseau de neurones récurrent.
Exercice 6.0.A : Préparation de l’environnement JupyterHub
Comme dans les derniers TD, nous allons travailler sur JupyterHub dans votre navigateur préféré. Ouvrez l’URL suivant et authentifiez-vous :
https://jupyterhub.emi.u-bordeaux.fr/
Sur votre espace, vous avez déjà créée un dossier s’appelant deeplearning. Dans ce dossier, créez un sous-dossier td6.
Exercice 6.1 : Un modèle pré-entrainé Word2Vec
Dans cet exercice, nous allons exploré le modèle pré-entrainé Word2Vec. Pour cela, nous allons explorer l’application interactive sur la page web http://nlp.polytechnique.fr/word2vec.
Laissez le choix du modèle Word2Vec sur “General Word2Vec”. C’est un modèle de langage Word2Vec qui utilise un plongement lexical, donc word embedding d’une dimension 300 - c’est à dire que chaque mot est représenté avec 300 nombres - et qui a été entrainé sur une base de texte de 33 Gigaoctets, et qui utilise une approche de continuous bag of words d’une proximité de 20 mots (la window size est donc de 20).
Premièrement, nous utilisons un grand classique, la relation linéaire entre des mots d’un embedding. En calculant la distance dans cet espace de plongement lexical entre deux mots A et B, nous pouvons ajouter cette distance à un mot C pour générer un mot D qui aura une même relation entre des mots que C et D que A et B. Cela fait donc D = A-B + C. Essayez l’exemple fourni dans some more examples, qui est “allumé - éteint + court” (donc A = “allumé”, B = “éteint”, et C = “court”), et calculez D. Ensuite, essayez les autres exemples. Pour D, les 10 mots les plus proches dans l’espace d’embedding* sont affichés, avec leurs probabilités.
- Expérimentez et essayez de trouver un exemple surprenant avec un A, B, et C de votre choix. Notez votre résultat de la façon suivante en donnant les 3 réponses les plus probables pour D (les valeurs sont bien sur des exemples, remplacez les avec votre résultat.)
A = ""
B = ""
C = ""
D : "" avec 0.64
D : "" avec 0.13
D : "" avec 0.04 Nous allons maintenant passer à l’exemple des mots similaires sur ce site.
- Prenez un mot de votre choix et calculez le top 10 des mots similaires, et notez votre mot et les 3 mots les plus similaires ici :
mon_mot = ""
"" avec 0.64
"" avec 0.13
"" avec 0.04 Nous allons maintenant passer à la visualisation sur ce site. Cette visualisation utilise une réduction de dimension de 300 à 2 afin de visualiser la proximité entre des mots. Expérimentez. Est-ce que vous voyez votre mot ? Quel est le mot le plus proche dans cette visualisation ? Quel est un mot le plus loin ?
Est-ce que c’est des mots similaires ? Comprenez vous pourquoi (pas) ?
Exercice 6.2 : Un plongement lexical (word embedding) simple
Dans votre sous-dossier td6, créez un nouveau notebook sous le nom td6-simple-word-embedding.ipynb.
Il faudra d’abord installer le module du préprocessing de Keras :
!pip install keras-preprocessingN’oubliez pas de relancer votre noyau python (kernel) !!!!!
Nous allons les bibliothèques dont on aura besoin pour ce TD. Pour cela, créez une nouvelle cellule Code dans votre notebook, mettez le code suivant et exécutez.
#from tensorflow.keras.preprocessing.text import Tokenizer
from keras_preprocessing.text import Tokenizer
from keras import layers
from keras.models import SequentialCréez une nouvelle cellule Markdown dans votre notebook, et ajoutez le titre suivant, et exécutez la cellule :
# 1. Préparer les données.
Ensuite, créez une nouvelle cellule Code dans votre notebook, mettez le code suivant et exécutez.
texts = ["Tu aimes les chiens", "Tu détestes les chats"]
tokenizer = Tokenizer()
tokenizer.fit_on_texts(texts)
word_index = tokenizer.word_index
index_word = {v: k for k, v in word_index.items()}
vocab_size = len(word_index) + 1 # Taille du vocabulaire (+1 pour le padding)
print("Vocab size : ", vocab_size)
print("word_index : ",tokenizer.word_index)
print("index_word", index_word)Analysez la sortie et essayez de comprendre. Notez que la taille du vocabulaire inclut aussi un “padding”, donc un espacement entre des mots, qui n’apparait pas ici.
Créez une nouvelle cellule Markdown dans votre notebook, et ajoutez le titre suivant, et exécutez la cellule :
# 2. Explorer les données.
Et dans une nouvelle cellule Code dans votre notebook, mettez le code suivant et exécutez.
# Convertir texte en séquences
sequences = tokenizer.texts_to_sequences(texts)
print(sequences) Reconnaissez-vous ce que ça représente ?
Créez une nouvelle cellule Markdown :
# 3. Plongement lexical
embedding_no_dimensions = 4
# Création du modèle
model = Sequential()
model.add(layers.Embedding(input_dim=vocab_size, output_dim=embedding_no_dimensions))
model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])
model.build(input_shape=(None,1)) #pour connaitre le nombre de paramètres
print(model.summary())Exécutez :
- Combien y-a-t’il de paramètres dans la couche de plongement lexical (embedding) ?
Nous pouvons maintenant récupérer la *Embedding Matrix* :
embeddings = model.layers[0].get_weights()[0]
print(embeddings)Le plongement lexical du premier mot (index 0) est ici :
print(embeddings[0])- Créez le code pour afficher le plongement lexical de la première phrase “Tu aimes les chiens”.
- Expliquez maintenant pourquoi il y a ce nombre de paramètres dans la couche Embedding.
Exercice 6.3 : Entrainez votre propre modèle :
Dans cet exercice, nous allons travailler sur un corpus plus grand, qui consiste d’un texte contenant tous les fables de Lafontaine. Nous allons explorer le plongement lexical, et entrainer un modèle de traitement de langage naturel permettant de générer la suite d’une phrase. Plus précisément, à partir d’une séquence des mots, nous allons prédire le prochain mot. Tout comme pour la prédiction d’une prochaine donnée d’une fonction comme dans le dernier TD, nous allons utiliser un réseau de neurones récurrent (RNN - recurrent neural networks), mais nous l’associant aussi à une couche de plongement lexical.
Ce sera dans un contexte d’apprentissage supervisé (supervised learning).
Nous reconstruisons donc de nouveau une fonction \[ f(X) = y \]
avec \(X \in R^{t x dim}\) avec \(t\) la taille du contexte, et \(dim\) la dimension du plongement lexical, et \(y \in R^{vocabsize}\) avec \(vocabsize\) le nombre de mots différents dans votre jeu de données (corpus).
Donc, le vecteur \(X\) sera une séquence de mots encodés dans l’espace de plongement lexical, et \(y\) contiendra la probabilité pour chaque mot. Pendant l’entrainement, nous montrons aux réseaux de neurones une séquence de mots, associé à un encodage one-hot pour le prochain mot (donc probabilité 1 pour ce mot, et 0 pour les autres).
Dans votre sous-dossier td6`, créez un nouveau notebook sous le nomtd6-fables-word-embedding.ipynb``.
Il faudra d’abord installer le module du préprocessing de Keras dans une cellule Code :
!pip install keras-preprocessingN’oubliez pas de relancer votre noyau python (kernel) !!!!!
Allez sur le site https://www.gutenberg.org/ebooks/56327.
Dans votre dossier td6, téléchargez la version “Plain Text UTF-8” sous le nom fables_lafontaine.txt.
Explorez le contenu. Effacez toutes les lignes au début avant la première fable (environ 1000 lignes), et toutes les lignes après la dernière fable.
Nous allons importer maintenant toutes les bibliothèques dont on aura besoin pour ce TD. Pour cela, créez une nouvelle cellule Code dans votre notebook, mettez le code suivant et exécutez.
import numpy as np
import re
#from tensorflow.keras.preprocessing.text import Tokenizer
from keras_preprocessing.text import Tokenizer
from keras.utils import pad_sequences
from keras.models import Sequential
from keras.layers import SimpleRNN, Dense, Embedding
import matplotlib.pyplot as plt
from sklearn.decomposition import PCACréez une nouvelle cellule Markdown dans votre notebook, et ajoutez le titre suivant, et exécutez la cellule :
# 1. Charger et préparer les données.
Ensuite, créez une nouvelle cellule Code dans votre notebook, mettez le code suivant et exécutez.
sequence_length = 5 # Longueur de séquences contextuelles
embedding_dim = 50 # Dimension du plongement lexical (word embedding)
m = 100 # nombre de hidden units dans la couche SimpleRNN, comme dans le dernier TD
# Charger et nettoyer le texte
with open("fables_lafontaine.txt", "r", encoding="utf-8") as f:
raw_text = f.read().lower()
text = re.sub(r"[^a-zéèàùâêîôûçëïü\s]", "", raw_text)
text = re.sub(r"\s+", " ", text)
# Tokenisation
tokenizer = Tokenizer()
tokenizer.fit_on_texts([text])
word_index = tokenizer.word_index
index_word = {v: k for k, v in word_index.items()}
vocab_size = len(word_index) + 1
# Séquences pour entraînement
words = text.split()
sequences = []
for i in range(sequence_length, len(words)):
seq = words[i-sequence_length:i+1] # Inclut le mot suivant comme label
sequences.append([word_index[word] for word in seq])
# Conversion en tableau numpy
sequences = np.array(sequences)
print(f"Nombre de séquences générées : {len(sequences)}")
print("Extrait : ",text[0:100])
print("Nombre de mots", len(words))
print("Vocab size : ",vocab_size)
print("Quelques séquences")
print(sequences[0])
print(sequences[1])
print(sequences[2])Analysez la sortie.
Créez une nouvelle cellule Code dans votre notebook, mettez le code suivant et exécutez.
# Entrées (mots contextuels) et labels (prochain mot)
X, y_int = sequences[:, :-1], sequences[:, -1]
# Encodage one-hot pour les labels
y = np.eye(vocab_size)[y_int]
print(X.shape)
print(y.shape)
print("Une donnée d'entrainement : ")
print(X[0])
print("Avec étiquette : ")
print(y_int[0])
print("Etiquette encodé en 'one-hot' : ")
print(y[0])Nous allons maintenant définir le modèle. Créez une cellule Markdown :
# 2. Définir le modèle :
et une cellule Code :
# Création du modèle
model = Sequential()
model.add(Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=sequence_length))
model.add(SimpleRNN(m, return_sequences=False)) # Ajouter SimpleRNN avec les hidden units
model.add(Dense(vocab_size, activation="softmax")) # Couche de sortie avec activation softmax
model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])
model.build(input_shape=(None, sequence_length)) #pour connaitre le nombre de paramètres
print(model.summary())Combien de paramètres sont dans la couche Embedding ?
Combien de paramètres sont dans la couche RNN ?
Combien de paramètres sont dans la couche dense (fully-connected) ?
Cela fait combien de paramètres au total ?
Nous allons maintenant entrainer le modèle. Créez une cellule Markdown :
# 3. Entrainer le modèle :
et une cellule Code :
model.fit(X, y, epochs=1, batch_size=128, verbose=1)Nous avons fait pour l’instant juste une seule époque, pour suivre le processus de l’apprentissage.
Maintenant nous allons faire de la prédiction avec notre propre modèle de langage, basé sur une couche Embedding, RNN, et Dense (fully-connected).
def generate_text(seed_text, num_words, model, tokenizer, sequence_length):
for _ in range(num_words):
# Transformer la séquence en indices
tokenized_input = [tokenizer.word_index[word] for word in seed_text.split() if word in tokenizer.word_index]
tokenized_input = pad_sequences([tokenized_input], maxlen=sequence_length, truncating="pre")
# Prédire le prochain mot
predicted_id = np.argmax(model.predict(tokenized_input), axis=-1)[0]
predicted_word = tokenizer.index_word.get(predicted_id, "")
# Ajouter le mot prédit au texte
seed_text += " " + predicted_word
return seed_text
# Exemple de génération
seed_text = "Dans ma propre fable, le corbeau et le renard sont"
generated_text = generate_text(seed_text, num_words=15, model=model, tokenizer=tokenizer, sequence_length=sequence_length)
print("Texte généré :", generated_text)Essayez ce code et regardez le texte généré à partir du texte stocké dans la variable seed_text.
Exercice 6.4 : Expérimentez avec votre modèle :
Maintenant, entrainez votre modèle avec quelques époques de plus (donc reexécutre la cellule avec `model.fit(...)) en mettant 10 ou 20 époques de plus.
Reessayez de générer un texte maintenant. Le résultat est-il meilleur ?
Nous pouvons maintenant expérimenter :
- changez le début du texte de la variable
seed_text, et relancez une prédiction.
Exercice 6.5 : Modifiez votre modèle :
Vous pouvez maintenant ré-entrainer votre modèle, en expérimentant avec les paramètres. Par exemple :
Changez le contexte
sequence_lengthen 3, 7, 10, par exemple (c’est équivalent au timesteps du TD précédent.)Changez les dimensions
embedding_dimen 10 et 100, par exempleChangez le nombre de hidden units
mdans la couche SimpleRNN, par exemple en 50 et 200.
1 Pour aller plus loin
1.1 Notre corpus collecté du cours (animaux/foot/tennis)
Dans votre sous-dossier td6`, créez un nouveau notebook sous le nomtd6-corpus-cours.ipynb``.
Téléchargez le corpus que vous avez constitué :
https://www.labri.fr/perso/preuter/dl2025/files/corpus-cours-dl2025.txt.
Reproduisez un entrainement et une prédiction multiple, comme dans le dernier exercice, en utilisant ce corpus.
Exercice optionnel : LSTM
Testez un autre modèle récurrent, le LSTM (*long short term memory).
Il suffit de changer la couche de SimpleRNN et la modifier en
model.add(LSTM(m, return_sequences=False)) # Identique à SimpleRNN mais avec LSTMLe modèle transformeurs avec attention
Les modèles basés sur des réseaux de neurones récurrents ne sont pas parallélisables, pour cela, un nouveau modèle est utilisé aujourd’hui avec un grand succès : les transformer with attention mechanism. Ce sont ces modèles qui interviennent majoritairement dans le grands modèles de langage de type ChatGPT.
TD à rendre
A rendre sur moodle avant le 8 Décembre 2025 à 23h59. Pour cela, il y a des liens sur la page du cours (https://moodle.u-bordeaux.fr/course/view.php?id=22721).
Réponses aux questions
Soumettez un exemple de génération de texte.