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"]TD NLP - Bases
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) :
- Combien y a-t-il d’histoires dans
textes_trainettextes_val? Afficher la première histoire. - 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.
- Construire le vocabulaire : la liste triée de tous les caractères présents dans
textes_train. Quelle est sa taille ? - 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"). - Construire les dictionnaires
stoi(caractère → id) etitos(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})\) |
- 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 classeDatasetdont 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 ? - 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.
- 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 ?
- 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. - Génération. Écrire une fonction
generate(prompt, n, T)qui génèrencaractères à partir d’un prompt, en échantillonnant dans \(\text{softmax}(y/T)\) (T = 0correspond à 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 ? - 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 ?
- 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 ?