TD NLP - Bases

Author

Augustin Chevallier

1 Exercice 1 : les données

Le dataset provient de l’article “TinyStories: How Small Can Language Models Be and Still Speak Coherent English?” https://arxiv.org/abs/2305.07759. Il s’agit d’histoires courtes, écrites avec un vocabulaire d’enfant de 3-4 ans.

Charger le dataset avec le code suivant (le premier chargement télécharge environ 1 Go) :

from datasets import load_dataset

train = load_dataset("roneneldan/TinyStories", split="train[:1%]")
val = load_dataset("roneneldan/TinyStories", split="validation[:10%]")

textes_train = train["text"]
textes_val = val["text"]
  1. Combien y a-t-il d’histoires dans textes_train et textes_val ? Afficher la première histoire.
  2. Quelle est la longueur moyenne d’une histoire en caractères ? Combien de caractères y a-t-il au total dans le train ?

2 Exercice 2 : tokenizer caractère

On choisit le tokenizer le plus simple : un token = un caractère, espaces et retours à la ligne compris.

  1. Construire le vocabulaire : la liste triée de tous les caractères présents dans textes_train. Quelle est sa taille ?
  2. Afficher les 20 caractères les plus fréquents et les 20 plus rares (collections.Counter). Certains caractères sont surprenants (â, €, ™…). D’où viennent-ils ? Indice : afficher "’".encode("utf-8").decode("cp1252").
  3. Construire les dictionnaires stoi (caractère → id) et itos (id → caractère), puis écrire les fonctions :
def encode(texte: str) -> list[int]: ...
def decode(ids: list[int]) -> str: ...

Vérifier que decode(encode(s)) == s pour la première histoire. 4. Appliquer encode à toutes les histoires de textes_val. Que se passe-t-il ? Ajouter un token spécial <UNK> (id 0) pour les caractères inconnus. A-t-on toujours decode(encode(s)) == s ? 5. Question bonus: Comparer le nombre de tokens de la première histoire avec celui obtenu par le tokenizer de GPT-2 :

from transformers import AutoTokenizer
tokenizer_gpt2 = AutoTokenizer.from_pretrained("gpt2")

Quelle est la taille du vocabulaire de GPT-2 ? Quels sont les avantages et inconvénients de chaque approche ?

3 Exercice 3 : prédire le 11ème caractère

On veut un modèle qui prend les \(w = 10\) derniers tokens \(x_{t-9}, \dots, x_t\) et prédit \(x_{t+1}\). C’est un problème de classification à vocab_size classes.

Architecture (avec \(B\) la taille du batch, \(d\) la dimension de l’embedding) :

Étape Couche Taille de la sortie
entrée ids des 10 tokens \((B, 10)\)
embedding nn.Embedding(vocab_size, d) \((B, 10, d)\)
concaténation nn.Flatten() \((B, 10d)\)
MLP Linear + ReLU, 2 couches cachées \((B, h)\)
head nn.Linear(h, vocab_size) \((B, \text{vocab\_size})\)
  1. Dataset. Concaténer toutes les histoires du train en un seul tenseur d’ids (on pourra les séparer par "\n\n\n"). Faire de même pour la validation. Écrire une classe Dataset dont l’élément \(i\) est le couple (data[i:i+10], data[i+10]). Combien y a-t-il d’exemples ? Pourquoi n’est-il pas nécessaire de stocker toutes les fenêtres en mémoire ?
  2. Baselines. Calculer la cross-entropy loss sur l’ensemble de validation et d’entrainement de deux modèles triviaux :
    • le modèle uniforme, qui donne la probabilité \(1/\text{vocab\_size}\) à chaque caractère ;
    • le modèle unigramme, qui prédit toujours la fréquence de chaque caractère dans le train, sans regarder le contexte.
    Ces valeurs serviront de référence pour interpréter la loss du réseau.
  3. Modèle. Coder le modèle avec \(d = 32\) et \(h = 256\). Combien a-t-il de paramètres ? Comment sont-ils répartis entre les couches ?
  4. Entraînement. Entraîner avec nn.CrossEntropyLoss, Adam (lr=1e-3) et des batchs de 512. Une epoch complète est inutile : quelques milliers d’itérations suffisent. Suivre la loss de validation et la comparer aux baselines.
  5. Génération. Écrire une fonction generate(prompt, n, T) qui génère n caractères à partir d’un prompt, en échantillonnant dans \(\text{softmax}(y/T)\) (T = 0 correspond à l’argmax). Si le prompt fait moins de 10 caractères, le compléter à gauche par des espaces. Tester avec "Once upon a time" pour \(T = 0\), \(0.5\) et \(1\). Qu’observez-vous ?
  6. Le texte généré est-il localement correct (mots, ponctuation) ? Est-il cohérent sur plusieurs phrases ? Le modèle peut-il se souvenir du nom du personnage principal ? Pourquoi ?
  7. Taille du contexte. Reprendre l’entraînement avec \(w = 3\) puis \(w = 30\). Comparer la loss de validation et le nombre de paramètres. Quelle couche grossit avec \(w\) ? Pourquoi cette architecture ne permet-elle pas de travailler avec un contexte de 1000 tokens ?