Semaine 3 : Tables de probabilité bigramme
🎯 Objectifs d'apprentissage
À la fin de cette semaine vous serez capable de :
- Extraire des bigrammes (paires de mots consécutifs) à partir de phrases tokenisées.
- Construire un dictionnaire imbriqué
dict[str, dict[str, float]]associant chaque mot à une distribution de probabilité sur les mots qui le suivent. - Expliquer, avec un exemple, pourquoi les bigrammes capturent un contexte que le modèle unigramme ne pouvait pas.
- Gérer le problème du « contexte jamais vu » : que faire quand un mot n'a absolument aucun successeur connu.
Leçon
Bigrammes : des paires de mots consécutifs
Un bigramme est une paire de mots consécutifs. Pour la phrase tokenisée ["the", "cat", "sat"], les bigrammes sont ("the", "cat") et ("cat", "sat") — une paire pour chaque position adjacente :
def bigrams(tokens):
return [(tokens[i], tokens[i + 1]) for i in range(len(tokens) - 1)]
bigrams(["the", "cat", "sat"])
# [('the', 'cat'), ('cat', 'sat')]
C'est le même motif range(len(...) - 1) qui apparaît chaque fois que vous devez examiner des paires de voisins dans une séquence — le -1 existe car le dernier mot n'a pas de mot après lui avec lequel se coupler. Pour une phrase de tokens, il y a toujours exactement bigrammes.
Une table de probabilité conditionnelle
Nous estimons maintenant — la probabilité du mot suivant, étant donné seulement le mot immédiatement précédent. C'est un modèle bigramme : un contexte toujours limité (mémoire d'exactement un mot), mais strictement plus que la mémoire nulle du modèle unigramme.
La structure de données naturelle est un dict de dicts : pour chaque mot , un dict imbriqué associant chaque mot suivant possible à sa probabilité, conditionnée par le fait d'être précédé de :
def bigram_counts(tokenized_sentences):
table = {} # word -> {next_word: count}
for tokens in tokenized_sentences:
for first, second in bigrams(tokens):
if first not in table:
table[first] = {}
table[first][second] = table[first].get(second, 0) + 1
return table
def bigram_probabilities(counts_table):
probs_table = {}
for word, next_counts in counts_table.items():
total = sum(next_counts.values())
probs_table[word] = {w: c / total for w, c in next_counts.items()}
return probs_table
bigram_probabilities réutilise exactement la même idée « comptages → diviser par le total » que to_probabilities la semaine dernière — la seule différence est qu'elle s'applique séparément à chaque ligne de la table propre à un mot, puisque chaque mot a sa propre distribution sur ce qui le suit. Chaque dict interne probs_table[word] somme à 1 de lui-même, la même propriété « somme à 1 » que la semaine dernière, simplement une distribution par mot au lieu d'une distribution pour tout le vocabulaire.
probs_table["the"]
# {'cat': 0.35, 'dog': 0.3, 'mouse': 0.1, 'mat': 0.15, ...}
Lisez probs_table["the"]["cat"] comme : sachant que le mot précédent était « the », quelle est la probabilité que « cat » vienne ensuite ?
Le problème du contexte jamais vu
Un mot qui n'apparaît toujours qu'à la fin d'une phrase ne démarre jamais de bigramme, il est donc simplement absent en tant que clé de premier niveau dans probs_table — il n'y a aucune ligne pour lui du tout, puisque bigram_counts n'ajoute une clé que pour les mots qui apparaissent comme premier élément d'un bigramme. Cela compte beaucoup pour la semaine 4, où vous devrez vérifier word in probs_table avant d'accéder à quoi que ce soit, exactement le même motif défensif que vérifier qu'une clé existe avant d'indexer un dict ordinaire.
def next_word_distribution(word, probs_table):
if word not in probs_table:
return None # this word never starts a bigram in our corpus
return probs_table[word]
Ce cas « le modèle n'a littéralement jamais vu cette situation » est une limitation réelle et inévitable de toute approche basée sur le comptage — elle ne peut jamais dire quoi que ce soit sur des motifs qu'elle n'a pas réellement observés dans les données d'entraînement, un thème qui reviendra explicitement à la semaine 4.
⚠️ Erreurs courantes
- Supposer que chaque mot est une clé de premier niveau dans
probs_table. Seuls les mots qui apparaissent comme premier élément d'au moins un bigramme obtiennent une ligne — voir « le problème du contexte jamais vu » ci-dessus. - Confondre
probs_table[word]avecprobs_table[word][other_word]. Le premier est une distribution entière (un dict) ; le second est une seule probabilité (un float). Oublier lequel des deux vous avez mène à desTypeErrordéroutantes plus loin. - Construire la table de comptages et la table de probabilités dans la même passe. Garder
bigram_countsetbigram_probabilitiescomme deux fonctions séparées (plutôt que de les fusionner) signifie que vous avez toujours les comptages bruts disponibles ensuite — utile pour des vérifications de cohérence, et pour l'expérience de chronométrage de la semaine 5, qui s'intéresse spécifiquement à l'étape de comptage.
🧩 Défis
En utilisant le corpus de la semaine 1, calculez la table de probabilité bigramme. Lequel est le plus susceptible de suivre directement « the » : « cat » ou « dog » ?
Quel mot du corpus est suivi par le plus de mots distincts différents (c'est-à-dire a le plus grand dict interne dans probs_table) ?
Choisissez un mot qui n'apparaît toujours que comme dernier mot d'une phrase dans le corpus. Est-ce une clé de premier niveau dans probs_table ? Pourquoi, ou pourquoi pas, étant donné comment bigrams() est définie ?
Généralisez bigrams(tokens) en une fonction trigrams(tokens) qui retourne tous les triplets consécutifs de mots. Comment la généraliseriez-vous davantage en une fonction ngrams(tokens, n) ?
Utilisez next_word_distribution pour consulter en toute sécurité un mot que vous avez déjà identifié au Défi 3 comme ne démarrant jamais de bigramme. Confirmez qu'elle retourne None au lieu de planter.
Pour un mot qui apparaît à la fois dans le counts unigramme de la semaine 2 et dans le bigram_counts de cette semaine, comparez son comptage unigramme à la somme des valeurs de sa ligne bigramme (sum(bigram_counts[word].values())). Devraient-ils correspondre ? Vérifiez quelques mots et expliquez tout petit écart que vous trouvez.
🤔 Questions socratiques
- Consultez
probs_table["the"]et comparez-la auprobsglobal de la semaine dernière. S'agit-il de la même distribution ? Que cela vous apprend-il sur le fait que « the » change ou non ce qui est susceptible de venir ensuite ? - Un modèle trigramme (conditionnant sur les deux mots précédents) capture plus de contexte qu'un modèle bigramme. Étant donné à quel point notre corpus de 20 phrases est petit, quel problème pratique prédisez-vous que les comptages de trigrammes rencontreraient, que les comptages de bigrammes évitent en grande partie ?
bigram_probabilitiesconstruit une distribution de probabilité complète par mot du vocabulaire. Si le vocabulaire a mots distincts, combien de nombres environ la table de bigrammes complète pourrait-elle contenir dans le pire des cas (chaque mot suivant chaque autre mot au moins une fois) ? Qu'est-ce que cela suggère sur la façon dont la taille de la table évolue avec la taille du vocabulaire ?- Le problème du contexte jamais vu signifie qu'un modèle bigramme peut rester complètement silencieux sur des mots qu'il n'a jamais vus démarrer un bigramme. Pouvez-vous imaginer un moyen de faire en sorte que le modèle ait toujours quelque chose à dire, même pour un mot jamais vu — peut-être en se rabattant sur quelque chose de la semaine dernière ?
- Le Défi 6 vous demande de comparer les comptages unigrammes aux comptages bigrammes sommés. Pour la plupart des mots, ils correspondent, mais le tout dernier mot d'une phrase est systématiquement sous-compté d'une unité dans la version bigramme. Pourquoi exactement une unité, et pourquoi seulement le dernier mot ?