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