Semaine 1 : Construire un mini modèle de langage — charger le corpus
🎯 Objectifs d'apprentissage
À la fin de cette semaine vous serez capable de :
- Expliquer, à haut niveau, ce qu'est un modèle de langage : un système qui prédit le mot suivant à partir des mots précédents.
- Charger un petit corpus de texte depuis un fichier CSV dans une liste Python de phrases, en utilisant uniquement la bibliothèque standard.
- Découper des phrases en listes de mots (un premier tokeniseur naïf), et expliquer ses limites.
- Calculer des statistiques descriptives de base sur un corpus de texte : nombre de phrases, taille du vocabulaire, longueur moyenne.
Leçon
Qu'est-ce qu'un modèle de langage ?
Un modèle de langage attribue une probabilité à des séquences de mots — de façon informelle, il répond à « étant donné les mots jusqu'ici, quel mot est susceptible de venir ensuite ? » Si vous avez déjà vu la phrase « le chat s'est assis sur le ___ », vous avez déjà prédit « tapis » avant même de finir de lire cette phrase. C'est la tâche, formalisée : étant donné des mots , estimer
Les vrais modèles (y compris les grands) estiment cette probabilité à l'aide d'immenses réseaux de neurones entraînés sur d'immenses jeux de données. Cette semaine démarre une version bien plus petite : nous allons l'estimer en n'utilisant rien d'autre que le comptage, sur un tout petit corpus écrit à la main, en Python pur. Cela fonctionnera, et dès la semaine 5, ce sera aussi lent — délibérément, pour que la raison d'être d'outils comme pandas et numpy cesse d'être une affirmation abstraite et devienne quelque chose que vous aurez ressenti personnellement.
Pourquoi le comptage permet-il d'estimer une probabilité ? Parce que la probabilité elle-même, dans son sens le plus simple (« fréquentiste »), n'est rien d'autre que « combien de fois cela s'est-il produit, sur tout ce qui aurait pu se produire » — . Tout ce que ce parcours construit, des fréquences de mots de la semaine 2 jusqu'à la génération de texte de la semaine 4, n'est rien d'autre que ce même ratio, appliqué à différentes questions sur quel mot suit quel autre.
Le corpus
Ce parcours utilise slm-corpus.csv, un petit ensemble de phrases simples écrites à la main, une par ligne sous une colonne sentence. Un « corpus » est simplement le jeu de données textuel à partir duquel un modèle de langage apprend — le nôtre est délibérément minuscule (20 phrases) pour que chaque étape reste assez rapide à exécuter et à inspecter à la main, à ce stade précoce du cours.
Le bac à sable du FAB a déjà slm-corpus.csv préchargé — pas besoin de copier-coller quoi que ce soit, load_corpus("slm-corpus.csv") ci-dessous le trouvera directement.
Le charger
Vous connaissez déjà csv.DictReader grâce à... en fait, pas encore — c'est la semaine 5 du parcours normal, une semaine en avance sur où en est le parcours difficile actuellement. Puisque ce parcours plonge directement dans un vrai projet, voici la même idée, en autonomie :
import csv
def load_corpus(path):
sentences = []
with open(path, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
sentences.append(row["sentence"])
return sentences
corpus = load_corpus("slm-corpus.csv")
print(len(corpus), "sentences loaded")
print(corpus[0])
load_corpus retourne une simple list[str] — une chaîne par phrase. Chaque valeur issue de csv.DictReader est une str, tout comme input(). Remarquez que la fonction fait exactement un travail (transformer un chemin de fichier en liste de chaînes de phrases) et rien d'autre — elle ne tokenise pas, ne compte pas, n'affiche rien au-delà de ce que l'appelant demande. Garder chaque fonction ainsi étroitement délimitée est ce qui vous permettra de construire le reste du pipeline de ce parcours comme des pièces petites et testables séparément, plutôt qu'un long script enchevêtré.
Un premier tokeniseur
Avant de pouvoir compter les mots, il faut découper une chaîne de phrase en une liste de mots individuels — cette étape s'appelle la tokenisation. Le tokeniseur le plus simple possible découpe simplement sur les espaces :
def tokenize(sentence):
return sentence.lower().split()
tokenize("The cat sat on the mat")
# ['the', 'cat', 'sat', 'on', 'the', 'mat']
.lower() compte : sans cela, "The" et "the" seraient comptés plus tard comme deux mots différents, ce qui rendrait nos comptages de mots (la semaine prochaine) moins pertinents. .split() sans argument découpe sur toute suite d'espaces, ce qui est suffisant pour les phrases simples et sans ponctuation de ce corpus.
Une version légèrement plus robuste retire la ponctuation de base avant de découper, ce qui compte dès que votre corpus cesse d'être délibérément propre :
import string
def tokenize_robust(sentence):
no_punct = sentence.translate(str.maketrans("", "", string.punctuation))
return no_punct.lower().split()
tokenize_robust("The cat sat on the mat.")
# ['the', 'cat', 'sat', 'on', 'the', 'mat'] -- le point final a disparu
string.punctuation est une chaîne toute faite des caractères de ponctuation courants ; str.maketrans("", "", string.punctuation) construit une table de traduction qui fait correspondre chacun de ces caractères à rien, et .translate(...) l'applique, les supprimant effectivement. Ce parcours s'en tient au tokenize plus simple pour le reste des semaines (le corpus est sans ponctuation exprès), mais il vaut la peine de voir à quoi ressemble une « vraie » étape de prétraitement — cet écart précis est l'une des nombreuses raisons pour lesquelles les outils de NLP en production utilisent des bibliothèques de tokenisation dédiées plutôt qu'un simple appel à .split().
Statistiques de base du corpus
Avant de construire quoi que ce soit de probabiliste, il vaut la peine de simplement regarder les données — le même réflexe que le parcours d'analyse exploratoire de la Section 2 formalisera en toute une méthodologie :
tokenized = [tokenize(s) for s in corpus]
lengths = [len(tokens) for tokens in tokenized]
print("Sentences:", len(corpus))
print("Shortest sentence:", min(lengths), "words")
print("Longest sentence:", max(lengths), "words")
print("Average sentence length:", sum(lengths) / len(lengths))
Même cette petite quantité de profilage vous apprend quelque chose d'utile : si chaque phrase du corpus avait exactement la même longueur, cela suggérerait un jeu de données très artificiel (ou très répétitif) — bon à savoir avant de faire confiance à toute statistique calculée à partir de lui plus tard.
⚠️ Erreurs courantes
- Oublier
.lower(). Sans cela,"The"et"the"sont deux tokens différents pour Python, séparant silencieusement en deux ce qui devrait être le comptage d'un seul mot. - Supposer que
.split()gère la ponctuation."mat.".split()sur une phrase entière vous donne un token"mat."(avec le point attaché) comme un seul morceau, pas"mat"et"."séparément — un vrai écart que les défis de cette semaine vous demandent de constater directement. - Relire le fichier à chaque fois que vous avez besoin du corpus.
load_corpuseffectue des entrées/sorties fichier, relativement lentes — appelez-la une fois, stockez le résultat dans une variable, et réutilisez cette variable, plutôt que de rappelerload_corpus(...)à l'intérieur d'une boucle.
🧩 Défis
En utilisant load_corpus et tokenize ensemble, produisez une liste où chaque élément est la version tokenisée (liste de mots) d'une phrase du corpus.
Calculez le nombre moyen de mots par phrase dans le corpus.
Calculez la taille du vocabulaire du corpus — le nombre de mots distincts qui apparaissent n'importe où dans le corpus (sans compter les répétitions).
Les phrases du corpus ne contiennent délibérément aucune ponctuation. Qu'irait-il de travers avec l'approche simple de .split() de tokenize si une phrase contenait effectivement de la ponctuation, comme "The cat sat on the mat." ? Essayez-le sur cette chaîne et inspectez le résultat.
Passez la même phrase ponctuée du Défi 4 par tokenize_robust cette fois. Produit-elle la liste de tokens attendue ?
Trouvez la longueur de phrase la plus courante dans le corpus (en mots) — pas la moyenne, la longueur qui apparaît le plus souvent.
🤔 Questions socratiques
- conditionne sur chaque mot précédent. Notre petit corpus n'a que 20 phrases — pensez-vous que nous aurons assez de données pour estimer une probabilité conditionnée sur de longs historiques ? Que pourrions-nous devoir simplifier ?
- Pourquoi
tokenizemet-elle la phrase en minuscules avant de la découper ? Quelle question de fréquence de mots de la semaine prochaine donnerait une réponse trompeuse si nous sautions cette étape ? load_corpusettokenizesont toutes deux de petites fonctions à but unique plutôt qu'une seule grande fonction qui fait tout. Qu'avez-vous appris à la semaine 4 du parcours normal de Python 101 qui explique pourquoi c'est utile ici, au-delà de « c'est plus propre » ?tokenize_robustretire toute la ponctuation, y compris les apostrophes à l'intérieur des contractions comme"don't", la transformant en"dont". Est-ce réellement le comportement correct pour un vrai tokeniseur ? Que faudrait-il changer pour traiter les contractions spécialement ?- Étant donné l'idée fréquentiste que « probabilité ≈ comptage / total », que pensez-vous qu'il arrive à vos estimations de probabilité lorsque le corpus passe de 20 phrases à 20 000 ? Vous attendriez-vous à ce que les estimations deviennent plus ou moins fiables, et pourquoi ?