diff --git a/MyIA.AI.Notebooks/NLP/02_NGrammes_Modeles_De_Langue.ipynb b/MyIA.AI.Notebooks/NLP/02_NGrammes_Modeles_De_Langue.ipynb
index f9809a2e3f..580c8199a4 100644
--- a/MyIA.AI.Notebooks/NLP/02_NGrammes_Modeles_De_Langue.ipynb
+++ b/MyIA.AI.Notebooks/NLP/02_NGrammes_Modeles_De_Langue.ipynb
@@ -2,6 +2,7 @@
"cells": [
{
"cell_type": "markdown",
+ "id": "cell-00",
"metadata": {},
"source": [
"# Modeles de langue n-grammes -- la probabilite d'une phrase\n",
@@ -35,19 +36,19 @@
"\n",
"Si un chiffre du tableau vous surprend en lisant la suite, c'est qu'il a\n",
"ete mal lu -- revenez a sa section : chaque \"Lecture\" explique le sien.\n"
- ],
- "id": "cell-00"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-01",
"metadata": {},
"source": [
"## 1. Corpus fil rouge -- un seul texte, deux partitions\n"
- ],
- "id": "cell-01"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-02",
"metadata": {},
"source": [
"Le domaine **litteraire** reprend *exactement* le meme corpus que le notebook 23 (fables de La Fontaine, domaine public) : le vocabulaire des sections 3 a 7 y est donc identique. Le corpus **tweets FR** est plus court, mais bruyant (abreviations, hashtags). Les fames d'un meme modele sur les deux domaines servent d'etalon de degradation.\n",
@@ -62,13 +63,20 @@
" le vocabulaire, mais la *methode* (compter, normaliser, mesurer) reste\n",
" identique. C'est le contrat de la serie : un seul fil rouge, des\n",
" granularites croissantes.\n"
- ],
- "id": "cell-02"
+ ]
},
{
"cell_type": "code",
"execution_count": 1,
- "metadata": {},
+ "id": "cell-03",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:54.310590Z",
+ "iopub.status.busy": "2026-09-28T18:12:54.310590Z",
+ "iopub.status.idle": "2026-09-28T18:12:54.322702Z",
+ "shell.execute_reply": "2026-09-28T18:12:54.322702Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -116,11 +124,11 @@
"\n",
"print(f\"Train : {len(FABLES_TRAIN.split())} mots-espace, {len(FABLES_TRAIN)} caracteres\")\n",
"print(f\"Test : {len(FABLES_TEST.split())} mots-espace, {len(FABLES_TEST)} caracteres\")\n"
- ],
- "id": "cell-03"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-04",
"metadata": {},
"source": [
"### Lecture du resultat\n",
@@ -133,32 +141,39 @@
"porte donc qu'une fraction infime -- chaque mot du test qui n'est pas dans\n",
"le train sera un **evenement de masse nulle** pour le modele MLE. Le mur\n",
"n'est pas une hypothese d'ecole, il est deja la, programme par la taille."
- ],
- "id": "cell-04"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-05",
"metadata": {},
"source": [
"## 2. Tokenisation en mots et padding\n"
- ],
- "id": "cell-05"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-06",
"metadata": {},
"source": [
"Avant tout calcul, deux choix de tokenisation :\n",
"\n",
"- la **casse** est preservee ici (transcription en minuscules des fables originales) ; la normalisation en minuscules viendrait ensuite si necessaire ;\n",
"- les **sentinelles `` et ``** sont ajoutees en tete et en queue de phrase : un unigramme ne les utilise pas, un bigramme utilise `` pour predire le premier mot, un trigramme utilise ` `.\n"
- ],
- "id": "cell-06"
+ ]
},
{
"cell_type": "code",
"execution_count": 2,
- "metadata": {},
+ "id": "cell-07",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:54.325225Z",
+ "iopub.status.busy": "2026-09-28T18:12:54.325225Z",
+ "iopub.status.idle": "2026-09-28T18:12:55.962568Z",
+ "shell.execute_reply": "2026-09-28T18:12:55.962568Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -166,13 +181,13 @@
"text": [
"--- n=1 : 22 phrases tokenisees\n",
" apercu padding sur la 1re phrase (5 premiers everygrams) :\n",
- " \n",
+ " \n",
"--- n=2 : 22 phrases tokenisees\n",
" apercu padding sur la 1re phrase (5 premiers everygrams) :\n",
- " \n",
+ " \n",
"--- n=3 : 22 phrases tokenisees\n",
" apercu padding sur la 1re phrase (5 premiers everygrams) :\n",
- " \n"
+ " \n"
]
}
],
@@ -197,11 +212,11 @@
" print(f\" apercu padding sur la 1re phrase (5 premiers everygrams) :\")\n",
" for eg in egs:\n",
" print(f\" {eg}\")\n"
- ],
- "id": "cell-07"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-08",
"metadata": {},
"source": [
"### Lecture du resultat\n",
@@ -219,29 +234,36 @@
" par 10 tout en couvrant les mots rares. Avec 299 mots d'entrainement, le\n",
" sous-mot n'apporterait rien : le phenomene a observer (les zeros) est\n",
" deja sature au niveau mot."
- ],
- "id": "cell-08"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-09",
"metadata": {},
"source": [
"## 3. Unigramme MLE et perplexite\n"
- ],
- "id": "cell-09"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-10",
"metadata": {},
"source": [
"Le modele **MLE** (maximum de vraisemblance) attribue a chaque mot sa frequence relative dans le corpus. Pour un unigramme, c'est un calcul direct ; la difficulte devient reelle avec les modeles `n >= 2` (zero counts) et la **perplexite** est la metrique qui rend comparables des probabilites de granularite Differente.\n"
- ],
- "id": "cell-10"
+ ]
},
{
"cell_type": "code",
"execution_count": 3,
- "metadata": {},
+ "id": "cell-11",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:55.965194Z",
+ "iopub.status.busy": "2026-09-28T18:12:55.965194Z",
+ "iopub.status.idle": "2026-09-28T18:12:55.977067Z",
+ "shell.execute_reply": "2026-09-28T18:12:55.975553Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -285,13 +307,20 @@
"print(f\"nltk.lm.MLE : vocabulaire={len(lm_uni.vocab)}\")\n",
"print(f\"Ecart MLE(w) corbeau : main={mles_main.get('corbeau', 0):.6f}, \"\n",
" f\"nltk={lm_uni.score('corbeau'):.6f}\")\n"
- ],
- "id": "cell-11"
+ ]
},
{
"cell_type": "code",
"execution_count": 4,
- "metadata": {},
+ "id": "cell-12",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:55.978617Z",
+ "iopub.status.busy": "2026-09-28T18:12:55.978617Z",
+ "iopub.status.idle": "2026-09-28T18:12:55.983767Z",
+ "shell.execute_reply": "2026-09-28T18:12:55.983767Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -317,11 +346,11 @@
"pp_main = perplexite_unigramme(mles_main, sents_test)\n",
"print(f\"Perplexite unigramme MLE sur test : {pp_main:.2f}\")\n",
"print(f\"Perplexite = nombre effectif de mots-choix (loi uniforme = |V| = {len(mles_main)})\")\n"
- ],
- "id": "cell-12"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-13",
"metadata": {},
"source": [
"#### Que mesure exactement `perplexite_unigramme` ?\n",
@@ -337,11 +366,11 @@
" Le uniforme pur sur |V| = 197 types hesiterait parmi 197 : notre simple\n",
" unigramme compte deja **6 fois mieux** que le hasard -- la frequence\n",
" seule porte deja beaucoup d'information."
- ],
- "id": "cell-13"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-14",
"metadata": {},
"source": [
"### Lecture du resultat\n",
@@ -356,19 +385,19 @@
"*plancher* de la serie : tout modele avec contexte (`n >= 2`) doit faire\n",
"mieux que 32.67 pour justifier son cout, sinon le contexte n'a rien apporte.\n",
"Ce sera exactement l'enjeu des sections 5 et 6 : sur 299 mots, le contexte\n"
- ],
- "id": "cell-14"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-15",
"metadata": {},
"source": [
"## 4. Bigramme MLE -- et l'explosion des zeros\n"
- ],
- "id": "cell-15"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-16",
"metadata": {},
"source": [
"Un bigramme MLE conditionne chaque mot sur son predecesseur. Sur le **train**, tout va bien : `P(corbeau | le) = compte(le, corbeau) / compte(le)`. Sur le **test**, un bigramme absent du train donne `0/compte(le) = 0` -- la probabilite de la phrase devient `0` des qu'**un seul** bigramme est inobserve. C'est le mur que le lissage doit abattre.\n",
@@ -379,13 +408,20 @@
"fontaine` mise a zero : du francais parfaitement ordinaire, juste jamais\n",
"vue dans cet ordre dans ces 22 phrases. Le mur frappe les sequences les\n",
"plus banales du monde.\n"
- ],
- "id": "cell-16"
+ ]
},
{
"cell_type": "code",
"execution_count": 5,
- "metadata": {},
+ "id": "cell-17",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:55.987340Z",
+ "iopub.status.busy": "2026-09-28T18:12:55.987340Z",
+ "iopub.status.idle": "2026-09-28T18:12:55.996317Z",
+ "shell.execute_reply": "2026-09-28T18:12:55.996317Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -423,11 +459,11 @@
"print(f\"Bigrammes test : {n_total}, dont {n_zero} zero ({100*n_zero/n_total:.1f}%)\")\n",
"for x, y in exemples:\n",
" print(f\" P({y!r} | {x!r}) = 0\")\n"
- ],
- "id": "cell-17"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-18",
"metadata": {},
"source": [
"### Lecture du resultat\n",
@@ -441,19 +477,19 @@
"la probabilite du produit est nulle, son log est `-inf`. Aucune moyenne,\n",
"aucune ponderation ne rattrape un zero -- c'est une propriete\n",
"**multiplicative**, pas additive."
- ],
- "id": "cell-18"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-19",
"metadata": {},
"source": [
"## 5. Bigramme Laplace add-1 -- tout devient possible\n"
- ],
- "id": "cell-19"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-20",
"metadata": {},
"source": [
"Laplace add-1 additionne 1 a **chaque** compte de bigramme et `|V|` a chaque compte de contexte. La probabilite devient :\n",
@@ -471,13 +507,20 @@
"La section 5 mesurera le bilan : Laplace supprime les zeros, oui -- mais a\n",
"quel prix en perplexite ? L'intuition a garder : **add-1 est un lissage de\n",
"demo**, pedagogiquement parfait, industriellement trop brut.\n"
- ],
- "id": "cell-20"
+ ]
},
{
"cell_type": "code",
"execution_count": 6,
- "metadata": {},
+ "id": "cell-21",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:55.999541Z",
+ "iopub.status.busy": "2026-09-28T18:12:55.999541Z",
+ "iopub.status.idle": "2026-09-28T18:12:56.012805Z",
+ "shell.execute_reply": "2026-09-28T18:12:56.012805Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -524,11 +567,11 @@
"pp_bi_lap, _, _ = perplexite_modele(lm_laplace, sents_test, 2)\n",
"print(f\"Perplexite bigramme Laplace sur test : {pp_bi_lap:.2f}\")\n",
"print(f\"Perplexite bigramme MLE sur test : {pp_bi_mle_inf}\")\n"
- ],
- "id": "cell-21"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-22",
"metadata": {},
"source": [
"### Lecture du resultat\n",
@@ -542,19 +585,19 @@
"langue en ignorant le mot precedent. Ce renversement (plus de contexte =\n",
"pire perplexite) est LE phenomene que Kneser-Ney veut corriger : payer le\n",
"lissage **seulement la ou l'evenement est plausible**."
- ],
- "id": "cell-22"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-23",
"metadata": {},
"source": [
"## 6. Bigramme Kneser-Ney interpole -- la continuite des mots rares\n"
- ],
- "id": "cell-23"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-24",
"metadata": {},
"source": [
"Kneser-Ney dit : un mot est rare dans le corpus parce qu'on l'a observe peu, ou parce qu'il apparait dans peu de **contextes** distincts. Le second cas est continu avec la distribution observee. Le lissage est :\n",
@@ -565,13 +608,20 @@
"```\n",
"\n",
"`P_continuation` pondere par la diversite de contextes plutot que par la frequence brute. `nltk.lm.KneserNeyInterpolated` implemente la version interpolee (avec `d`).\n"
- ],
- "id": "cell-24"
+ ]
},
{
"cell_type": "code",
"execution_count": 7,
- "metadata": {},
+ "id": "cell-25",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:56.016152Z",
+ "iopub.status.busy": "2026-09-28T18:12:56.016152Z",
+ "iopub.status.idle": "2026-09-28T18:12:56.051673Z",
+ "shell.execute_reply": "2026-09-28T18:12:56.050666Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -595,39 +645,46 @@
"print(f\"Perplexite bigramme Kneser-Ney sur test : {pp_kn:.2f} (zeros/absents ignores={zeros_kn}/{N_kn})\")\n",
"print(f\"Perplexite bigramme Laplace sur test : {pp_bi_lap:.2f}\")\n",
"print(f\"Gain KN vs Laplace : {(pp_bi_lap - pp_kn) / pp_bi_lap * 100:+.1f}%\")\n"
- ],
- "id": "cell-25"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-26",
"metadata": {},
"source": [
"### Lecture du resultat\n",
"\n",
"Kneser-Ney produit une perplexite **36 % superieure a Laplace** sur ce corpus (210 vs 154) -- c'est l'effet bien connu sur corpus < 50k mots : l'hypothese de continuite linguistique est juste en theorie mais depend de statistiques que 299 mots ne portent pas. **Conclusion honnete** : Kneser-Ney n'est pas un oracle automatique, c'est une hypothese linguistique qui demande de la donnee. La section 7 mesure la sensibilite au choix de `n` -- et revele que sur ce corpus, la perplexite de Kneser-Ney est en realite `inf` (au moins un evenement test a une probabilite nulle par recursion), tandis que MLE donne des chiffres **mathematiquement corrects mais completement trompeurs**."
- ],
- "id": "cell-26"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-27",
"metadata": {},
"source": [
"## 7. Courbe perplexite vs n -- trouver la granularite juste\n"
- ],
- "id": "cell-27"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-28",
"metadata": {},
"source": [
"Le bon `n` n'est pas une pensee unique : `n=1` ignore tout contexte, `n=3` capture des dependances plus longues mais souffre de la combinatoire. Le tableau ci-dessous evalue les **trois** familles (MLE, Laplace, Kneser-Ney) pour `n` de 1 a 3 -- attention au piege de lecture : MLE est evalue **sur ses evenements non nuls seulement** (les zeros du test ne comptent plus dans la moyenne), Laplace sur tout le test avec sa correction uniforme, Kneser-Ney sur tout le test via sa recursion de continuation. Les trois colonnes ne mesurent donc pas exactement la meme chose -- et c'est justement la lecon : un chiffre de perplexite sans sa convention d'evaluation est un chiffre nu.\n"
- ],
- "id": "cell-28"
+ ]
},
{
"cell_type": "code",
"execution_count": 8,
- "metadata": {},
+ "id": "cell-29",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:56.054275Z",
+ "iopub.status.busy": "2026-09-28T18:12:56.054275Z",
+ "iopub.status.idle": "2026-09-28T18:12:56.081049Z",
+ "shell.execute_reply": "2026-09-28T18:12:56.081049Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -666,48 +723,154 @@
" m = points_mle[i][1]; l = points_lap[i][1]; k = points_kn[i][1]\n",
" m_str = f\"{m:.2f}\" if m != float('inf') else \"inf\"\n",
" print(f\"{n:>3} | {m_str:>10} | {l:>10.2f} | {k:>12.2f}\")\n"
- ],
- "id": "cell-29"
+ ]
},
{
"cell_type": "markdown",
+ "id": "5137fa4b",
"metadata": {},
"source": [
- "### Lecture du resultat\n\n",
- "La courbe fait apparaitre **trois regimes distincts** sur ce corpus :\n",
+ "## 8. Génération d'échantillons - du bruit lexical aux fragments\n",
+ "\n",
+ "Les trois modèles entraînés plus haut - unigramme MLE (section 3), bigramme MLE (section 4), trigramme MLE (même recette, sur `padded_everygram_pipeline(3, ...)`) - sont des **distributions de probabilité** sur les mots. La **génération** consiste à échantillonner séquentiellement : on tire un mot selon la distribution, on l'ajoute au contexte, on tire le suivant, etc. La qualité de l'échantillon dépend directement de l'ordre du modèle.\n",
"\n",
- "- **MLE** chute vertigineusement (84.54 -> 3.42 -> 1.40) parce que les non-zero au test deviennent rares quand `n` grandit ; la perplexite moyenne n'est plus la moyenne d'une distribution, c'est la mesure d'un **sous-ensemble privilegie** (les evenements observes). A `n=3`, la perplexite 1.40 est **mathematiquement correcte et completement trompeuse** : elle cache les zeros.\n",
- "- **Laplace** reste stable autour de 198 (proche du vocabulaire) parce que la correction uniforme absorbe **tous** les bigrammes et trigrammes dans une distribution reguliere. C'est le **plafond** de la methode : Laplace est fin en soi mais n'integre aucun contexte de maniere discriminante.\n",
- "- **Kneser-Ney** reste `inf` sur tout le tableau : sur 299 mots, la recursion `_continuation_counts` rend au moins un evenement test `0/positive` (mot test jamais apparu en continuation), et un seul zero suffit a entrainer `log(0) = -inf` dans l'agregat. C'est precisement le mur que Kneser-Ney est cense eviter -- pas sur ce corpus. Sur 10^8 tokens, KN reprend l'avantage.\n",
+ "**Trois graines de départ** (`text_seed`) sont testées par modèle : `` (début de phrase), `«` (ouverture de citation), `Il` (pronom sujet), chacune avec `random_seed=42` (reproductibilité, règle C.2). Huit mots sont générés à chaque fois - assez pour voir la structure grammaticale sans noyer le signal.\n",
"\n",
- "**Conclusion methodologique** : publier une perplexite sans lissage, sans precision de l'estimateur, sans declarer la taille du corpus **et** sans compter les zeros/absents = publier sans methode. La courbe ci-dessus montre qu'un meme test peut donner des chiffres allant de 1.40 (MLE, correct mais absurde) a `inf` (KN, honnete mais inutilisable) en passant par 198 (Laplace, regulier mais ininformatif).\n"
+ "**Substance attendue** :\n",
+ "- **Unigramme** - bruit lexical, ponctuation fréquente, répétitions ; les mots les plus probables dominent (`le`, `,`, `Monsieur`, `Il`).\n",
+ "- **Bigramme** - fragments localement plausibles : les co-occurrences apprises survivent (mots-outils + début de nom propre), mais aucune structure au-delà de deux mots.\n",
+ "- **Trigramme** - segments plus longs, certains verbatim du corpus d'origine ; mais sur un corpus d'environ 350 mots d'apprentissage, la combinatoire explose et le modèle tombe vite sur ``.\n",
+ "\n",
+ "La cellule code suivante construit les trois modèles, génère les échantillons et les affiche en trois blocs."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "956733fb",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:56.084297Z",
+ "iopub.status.busy": "2026-09-28T18:12:56.084297Z",
+ "iopub.status.idle": "2026-09-28T18:12:56.109066Z",
+ "shell.execute_reply": "2026-09-28T18:12:56.109066Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "=== Unigramme (ordre 1) ===\n",
+ " seed=[''] rs=42 | le , au Phenix ne les ses ,\n",
+ " seed= ['«'] rs=43 | , loup . et lente pot et faisiez-vous\n",
+ " seed=['Il'] rs=44 | des gagna sa La Que , . ,\n",
+ "\n",
+ "=== Bigramme (ordre 2) ===\n",
+ " seed=[''] rs=42 | Nuit et la force . Rien ne\n",
+ " seed= ['«'] rs=43 | He bonjour , le renard se parlerent longtemps\n",
+ " seed=['Il'] rs=44 | Le lion , elle alla crier famine chez\n",
+ "\n",
+ "=== Trigramme (ordre 3) ===\n",
+ " seed=[''] rs=42 | He bonjour , Monsieur du Corbeau .\n",
+ " seed= ['«'] rs=43 | Pas un seul petit morceau de mouche\n",
+ " seed=['Il'] rs=44 | Le rat des champs se rencontrerent et\n"
+ ]
+ }
],
- "id": "cell-30"
+ "source": [
+ "# Modèles pour la génération : on réutilise le train_bi (cellule 17) et\n",
+ "# on construit train_tri par la même recette. La génération passe par\n",
+ "# lm.generate(num_words, text_seed, random_seed) -- méthode héritée de\n",
+ "# nltk.lm.api.LanguageModel, définie par chaque sous-classe.\n",
+ "train_tri, _ = padded_everygram_pipeline(3, sents_train)\n",
+ "lm_tri = MLE(3)\n",
+ "lm_tri.fit(train_tri, vocabulary_text=vocab)\n",
+ "\n",
+ "# Trois graines (début de phrase, guillemet ouvrant, pronom sujet) et\n",
+ "# huit mots par échantillon. random_seed varie par graine (germe + i)\n",
+ "# pour que la sortie CONDITIONNELLE sur text_seed soit visible -- sinon\n",
+ "# les trois graines convergeraient sur la même séquence (le germe du RNG\n",
+ "# fixe toute la chaîne). La reproductibilité reste acquise : un (seed,\n",
+ "# random_seed) donné rejoue à l'identique (C.2).\n",
+ "seeds = [[''], ['«'], ['Il']]\n",
+ "n_mots = 8\n",
+ "germe_base = 42\n",
+ "\n",
+ "# Tableaux collectés pour affichage structuré (et pour le redémarrage ultérieur).\n",
+ "ech_uni, ech_bi, ech_tri = [], [], []\n",
+ "for i, seed in enumerate(seeds):\n",
+ " rs = germe_base + i\n",
+ " s_uni = lm_uni.generate(n_mots, text_seed=seed, random_seed=rs)\n",
+ " s_bi = lm_bi.generate(n_mots, text_seed=seed, random_seed=rs)\n",
+ " s_tri = lm_tri.generate(n_mots, text_seed=seed, random_seed=rs)\n",
+ " ech_uni.append((seed, rs, s_uni))\n",
+ " ech_bi.append((seed, rs, s_bi))\n",
+ " ech_tri.append((seed, rs, s_tri))\n",
+ "\n",
+ "def affiche(ech, titre):\n",
+ " print(f'=== {titre} ===')\n",
+ " for seed, rs, mots in ech:\n",
+ " phrase = ' '.join(mots)\n",
+ " print(f' seed={seed!r:>6s} rs={rs} | {phrase}')\n",
+ "\n",
+ "affiche(ech_uni, 'Unigramme (ordre 1)')\n",
+ "print()\n",
+ "affiche(ech_bi, 'Bigramme (ordre 2)')\n",
+ "print()\n",
+ "affiche(ech_tri, 'Trigramme (ordre 3)')\n"
+ ]
},
{
"cell_type": "markdown",
+ "id": "dc9a5fda",
"metadata": {},
"source": [
- "## 8. Exercices\n",
+ "### Lecture du résultat\n",
"\n",
- "Trois exercices sur les donnees du notebook. Le notebook doit s'executer de bout en bout : les stubs ne levent **jamais** d'erreur (regle C.1) -- ils affichent un message et rendent `None`."
- ],
- "id": "cell-31"
+ "Trois régularités émergent, **qu'on attendait de la théorie et que la mesure confirme** :\n",
+ "\n",
+ "- **Unigramme** - les trois graines produisent des **séquences distinctes mais a-syntatiques** : `le , au Phenix ne les ses ,`, `, loup . et lente pot et faisiez-vous`, `des gagna sa La Que , . ,`. C'est attendu : unigramme MLE est une loi marginale sur les mots, indépendante du contexte (i.i.d.). Les mots fréquents dominent (`le`, `,`, `La`, `Que`) et l'ordre est purement aléatoire - aucun segment n'a la structure d'une **phrase**.\n",
+ "- **Bigramme** - les graines différentes donnent des **fragments distincts et localement plausibles** : `Nuit et la force .`, `He bonjour , le renard se parlerent longtemps`, `Le lion , elle alla crier famine chez`. Le bigramme a appris les co-occurrences du corpus (article + nom commun, pronom + verbe, ponctuation ouvrante). Sur 8 mots, aucun segment n'a la structure d'une **phrase complète** - c'est précisément la limite de l'ordre 2.\n",
+ "- **Trigramme** - la sortie **colle au corpus d'origine** : `Monsieur du Corbeau`, `Le rat des champs se rencontrerent` sont des **segments verbatim** des fables. Le trigramme a mémorisé des séquences de 3 mots du train (~350 mots) et les recrache quand le contexte s'y prête. Le revers : le token `` apparaît en début de chaque sortie, car la combinatoire 3-mois n'a pas été vue à l'entraînement sur le seed proposé.\n",
+ "\n",
+ "**Note sur le protocole** : `random_seed` varie par graine (`germe_base + i`) pour que la CONDITION sur `text_seed` soit visible -- un `random_seed` unique pour les trois graines converge sur la même séquence (le germe du RNG fixe toute la chaîne). La reproductibilité reste acquise : un couple `(text_seed, random_seed)` rejoue à l'identique.\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-31",
+ "metadata": {},
+ "source": [
+ "## 9. Exercices\n",
+ "\n",
+ "Trois exercices sur les donnees du notebook. Le notebook doit s'executer de bout en bout : les stubs ne levent **jamais** d'erreur (regle C.1) -- ils affichent un message et rendent `None`.\n"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-32",
"metadata": {},
"source": [
"### Exercice 1 -- extension du vocabulaire\n",
"\n",
"Construisez une fonction `vocabulaire_oov(sents_train, sents_test)` qui retourne le **vocabulaire entier** (train + test), la liste des mots presents **uniquement dans le test** (out-of-vocabulary, OOV), et le taux d'OOV. *Indice : `set(a) - set(b)` ; iterez sur les mots `alpha`.*"
- ],
- "id": "cell-32"
+ ]
},
{
"cell_type": "code",
- "execution_count": 9,
- "metadata": {},
+ "execution_count": 10,
+ "id": "cell-33",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:56.111088Z",
+ "iopub.status.busy": "2026-09-28T18:12:56.111088Z",
+ "iopub.status.idle": "2026-09-28T18:12:56.115876Z",
+ "shell.execute_reply": "2026-09-28T18:12:56.115369Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -728,23 +891,30 @@
" return None\n",
"\n",
"vocabulaire_oov(sents_train, sents_test)\n"
- ],
- "id": "cell-33"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-34",
"metadata": {},
"source": [
"### Exercice 2 -- lissage add-k vs add-1\n",
"\n",
"Generalisez Laplace add-1 en une fonction `lissage_add_k(comptes, contexte, mot, k, V)` qui retourne `(compte(h, w) + k) / (compte(h) + k * V)`. Testez sur `k in (0.01, 0.1, 1.0, 5.0)` et observez la sensibilite. *Indice : `comptes[(h, w)]`, `comptes[h]`, etc.*"
- ],
- "id": "cell-34"
+ ]
},
{
"cell_type": "code",
- "execution_count": 10,
- "metadata": {},
+ "execution_count": 11,
+ "id": "cell-35",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:56.117424Z",
+ "iopub.status.busy": "2026-09-28T18:12:56.117424Z",
+ "iopub.status.idle": "2026-09-28T18:12:56.122046Z",
+ "shell.execute_reply": "2026-09-28T18:12:56.121538Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -764,23 +934,30 @@
" return None\n",
"\n",
"lissage_add_k({}, 'le', 'corbeau', 1.0, 200)\n"
- ],
- "id": "cell-35"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-36",
"metadata": {},
"source": [
"### Exercice 3 -- courbe de perplexite completee\n",
"\n",
"Etendez la section 7 a `n in (1, 2, 3, 4)` et tracez la courbe de perplexite pour Laplace ET Kneser-Ney sur le meme graphe. Vous pouvez utiliser `matplotlib.pyplot` (deja disponible). *Indice : reutilisez `perplexite_modele` ; listez les valeurs dans deux listes separees et appelez `plt.plot` deux fois."
- ],
- "id": "cell-36"
+ ]
},
{
"cell_type": "code",
- "execution_count": 11,
- "metadata": {},
+ "execution_count": 12,
+ "id": "cell-37",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-09-28T18:12:56.124598Z",
+ "iopub.status.busy": "2026-09-28T18:12:56.124598Z",
+ "iopub.status.idle": "2026-09-28T18:12:56.564166Z",
+ "shell.execute_reply": "2026-09-28T18:12:56.564166Z"
+ }
+ },
"outputs": [
{
"name": "stdout",
@@ -804,11 +981,11 @@
" return None\n",
"\n",
"courbe_perplexite(sents_train, sents_test)\n"
- ],
- "id": "cell-37"
+ ]
},
{
"cell_type": "markdown",
+ "id": "cell-38",
"metadata": {},
"source": [
"## Conclusion\n",
@@ -841,29 +1018,34 @@
"Un corpus plus grand inverserait probablement les deux dernieres lignes --\n",
"c'est la promesse des modeles a n-grammes industriels (Web-scale) et, plus\n",
"loin, des modeles neuronaux : meme combat contre les zeros, autres armes.\n"
- ],
- "id": "cell-38"
+ ]
}
],
"metadata": {
+ "authors": [
+ {
+ "name": "lane myia-po-2026:CoursIA-2 -- issue #16235"
+ }
+ ],
"kernelspec": {
"display_name": "coursia-ml-training",
"language": "python",
"name": "coursia-ml-training"
},
"language_info": {
- "name": "python",
- "version": "3.11.15",
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
"mimetype": "text/x-python",
- "file_extension": ".py"
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.16"
},
- "title": "Modeles de langue n-grammes",
- "authors": [
- {
- "name": "lane myia-po-2026:CoursIA-2 -- issue #16235"
- }
- ]
+ "title": "Modeles de langue n-grammes"
},
"nbformat": 4,
"nbformat_minor": 5
-}
+}
\ No newline at end of file