Des fréquences au sens : sac de mots, TF-IDF et plongements
- Leçon facultative 1
- 50 min
- Module 8 : Sous le capot : les mathématiques · Facultative, non requise pour le certificat
Leçon facultative 1 sur 4 · L'apprentissage profond et les transformeurs expliquésDes fréquences au sens : sac de mots, TF-IDF et plongements
1. Ce qu'est un réseau de neurones
2. Les tenseurs et PyTorch
3. Comment les réseaux de neurones apprennent
4. Diagnostiquer les problèmes d'entraînement
5. Représenter les séquences et le sens
6. Assembler un transformeur
7. Utiliser des modèles préentraînés
8. Sous le capot : les mathématiquesFacultatif
Ce que vous saurez faire
Écrivez exactement comment un billet de soutien devient un vecteur de trois façons : fréquences des mots, TF-IDF et plongement dense. Pour chacune, calculez la similarité de deux billets à la main et avec du code. Voyez ce qu’elle corrige et ce qu’elle évalue encore mal.
Carnet JupyterFaites cette leçon dans un carnet Jupyter
Le carnet (notebook) Jupyter calcule tf, df, idf et TF-IDF à la main avec NumPy. Il compare ces résultats à ceux de scikit-learn à quatre décimales près. Ensuite, il change les options d’idf, entraîne deux modèles d’acheminement sur les billets et mesure la paire de synonymes. Il utilise les fréquences, TF-IDF et les plongements de mots. Aucune installation locale n’est nécessaire. Environ 30 minutes.
Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Le vocabulaire et le sac de mots, présentés dans La propagation avant, et la table de plongements, dans Jetons et plongements.
- Le produit scalaire et la longueur d’un vecteur, présentés dans Le produit scalaire : un score pondéré et La longueur d’un vecteur du cours de mathématiques.
- La similarité cosinus, présentée dans Deux façons de dire « proche ».
- Le logarithme naturel ln et le signe de sommation Σ, présentés dans Les logarithmes et Le signe de sommation, lu à voix haute.
Ce qu’il vous faut sur votre ordinateur
- Rien si vous utilisez le carnet.
- Pour travailler localement : le dossier
ticket-routerde Installer PyTorch, avec scikit-learn (pip install scikit-learn). Le dernier bloc de code nécessite aussipip install sentence-transformers, qui télécharge un modèle d’environ 90 Mo.
Testé avec Python 3.12, scikit-learn 1.9.1 et PyTorch 2.14.1 sur un processeur.
Cette leçon est facultative : elle n’est pas requise pour obtenir le certificat.
Deux clients, un problème
Section intitulée « Deux clients, un problème »Vous avez vu l’idée dans La propagation avant et dans Jetons et plongements. Cette page présente les mathématiques. Dans ces leçons, un billet devenait une ligne de fréquences, avec un nombre pour chaque mot du vocabulaire. Plus tard, chaque mot recevait plutôt une courte ligne de nombres appris. Des mots semblables pouvaient ainsi recevoir des lignes semblables.
Deux clients écrivent à Larkfield le même matin. L’un écrit « my parcel is late » (mon colis est en retard). L’autre écrit « the package has not arrived » (le colis n’est pas arrivé). Une personne voit immédiatement que les deux veulent dire la même chose. Un troisième client écrit « my refund is late » (mon remboursement est en retard), une question de paiement. Voici donc la question de cette page : quand nous transformons le texte en vecteur, quels billets se retrouvent proches, et pourquoi?
Nous y répondons trois fois, avec trois représentations. Chacune corrige un problème de la précédente, et chacune a un coût. Le diagramme les place côte à côte.
Description textuelle du diagramme
Le diagramme contient trois cases, avec une flèche entre chaque paire. La case 1 représente les fréquences, le sac de mots. Un vecteur contient 2 864 nombres, un par mot du vocabulaire. Un billet d’entraînement moyen contient environ 43 nombres non nuls : 98,5 % de zéros. Un billet occupe 11 456 octets sous forme de vecteur complet. Il occupe environ 352 octets dans un format creux qui stocke seulement les nombres non nuls. La régression logistique sur les fréquences achemine correctement 97,83 % des billets de validation. La similarité cosinus entre « my parcel is late » et « the package has not arrived » vaut 0. La flèche vers la case 2 indique « pondérer chaque mot selon sa rareté ». La case 2 représente TF-IDF. Elle contient les mêmes 2 864 nombres et les mêmes zéros. Chaque fréquence est multipliée par l’idf du mot, puis le vecteur est ramené à une longueur de 1. Il occupe la même mémoire. L’exactitude sur les données de validation atteint 98,83 %; le cosinus des deux phrases vaut toujours 0. La flèche vers la case 3 indique « apprendre des vecteurs à partir des mots voisins ». La case 3 représente un plongement (embedding) dense : 64 à 384 nombres, presque aucun à 0. Ils sont appris à partir de texte, donc les mots utilisés de façon semblable reçoivent des vecteurs semblables. Un billet occupe 256 octets avec 64 nombres et 1 536 octets avec 384 nombres. Un modèle figé de plongement de phrases achemine correctement 94,50 % des billets. Il donne aux deux phrases un cosinus de 0,502. Une note en bas précise que la mémoire suppose des nombres float32 de 4 octets. Un vecteur creux stocke une valeur et une position, soit 4 + 4 octets, pour chaque nombre non nul.
Représentation 1 : les fréquences
Section intitulée « Représentation 1 : les fréquences »Étape 1 : un billet est un point dans un espace avec un axe par mot
Section intitulée « Étape 1 : un billet est un point dans un espace avec un axe par mot »Fixez un vocabulaire de V mots. Numérotez-les de 1 à V. Un document d, un billet ou une phrase, devient alors le vecteur des fréquences de ses mots :
Ici, c avec les indices d et j (écrit c_d,j) représente le nombre d’apparitions du mot j dans le document d. ℝ^V signifie « une liste de V nombres réels ». Chaque mot est un axe de l’espace. Un document est donc un point, ou une flèche partant de l’origine.
Un petit exemple. Prenez cinq documents inventés :
| Nom | Document |
|---|---|
| d1 | my parcel is late |
| d2 | the package has not arrived |
| d3 | refund my card |
| d4 | the parcel arrived broken |
| d5 | my refund is late |
Ensemble, ils utilisent 12 mots différents, donc V = 12. En ordre alphabétique, d1 contient un 1 sous « is », « late », « my » et « parcel ». Il contient 0 sous les 8 autres mots. Vérifiez-le avec du code. CountVectorizer de scikit-learn construit le vocabulaire et compte les mots. Nous lui fournissons le tokenize du cours pour qu’il découpe le texte de la même façon. Les lignes de préparation chargent les billets comme dans La propagation avant.
import numpy as npfrom ticketnet import load_split, tokenize, Vocab, bag_of_words
train_rows, valid_rows = load_split("data/train.csv"), load_split("data/valid.csv")vocab = Vocab([r["text"] for r in train_rows])from sklearn.feature_extraction.text import CountVectorizer
docs = ["my parcel is late", "the package has not arrived", "refund my card", "the parcel arrived broken", "my refund is late"]cv = CountVectorizer(tokenizer=tokenize, token_pattern=None, lowercase=False)C = cv.fit_transform(docs).toarray() # shape (5 documents, 12 words)print(cv.get_feature_names_out())print(C)La sortie :
['arrived' 'broken' 'card' 'has' 'is' 'late' 'my' 'not' 'package' 'parcel' 'refund' 'the'][[0 0 0 0 1 1 1 0 0 1 0 0] [1 0 0 1 0 0 0 1 1 0 0 1] [0 0 1 0 0 0 1 0 0 0 1 0] [1 1 0 0 0 0 0 0 0 1 0 1] [0 0 0 0 1 1 1 0 0 0 1 0]]Chaque ligne représente un document et chaque colonne représente un mot. Ce tableau de documents et de mots est la matrice documents-termes. Pour les billets, la même idée donne la matrice de La propagation avant, avec V = 2 864.
Étape 2 : presque tous les nombres sont zéro
Section intitulée « Étape 2 : presque tous les nombres sont zéro »Un billet utilise quelques dizaines des 2 864 mots du vocabulaire. Presque toutes les valeurs de son vecteur sont donc 0. Un tel vecteur est dit creux. L’image montre le vrai billet T-64697 de La propagation avant. Chaque petit carré représente une des 2 864 positions du vocabulaire, dans l’ordre du vocabulaire.
- une fois
- deux fois ou plus
- absent
Description textuelle du graphique
Une grille de 2 864 petits carrés, un pour chaque position du vocabulaire. Ils suivent l’ordre du vocabulaire, ligne par ligne : d’abord la ponctuation, puis les mots de a à z. Le billet T-64697 contient 92 jetons (tokens) et 70 mots différents du vocabulaire. Ainsi, 70 carrés sont colorés et 2 794 sont gris, soit 97,6 % des carrés. La plupart des carrés colorés représentent un mot qui apparaît une fois. Quelques-uns représentent des mots qui apparaissent deux fois ou plus. « the » apparaît 7 fois; le point, la virgule et « i », 4 fois chacun; « and », 3 fois. Les carrés colorés sont répartis dans toute la grille, car les mots du billet proviennent de tout l’alphabet.
Comptez maintenant les zéros de l’ensemble des 4 696 billets d’entraînement :
X = bag_of_words(train_rows, vocab) # counts, shape (4696, 2864)nonzero = (X > 0).sum(dim=1).float() # non-zero entries in each rowprint(tuple(X.shape), "| non-zeros per ticket: mean", round(nonzero.mean().item(), 1), "median", int(nonzero.median()), "max", int(nonzero.max()))print("share of zeros:", round(1 - nonzero.mean().item() / X.shape[1], 4))La sortie :
(4696, 2864) | non-zeros per ticket: mean 43.3 median 44 max 90share of zeros: 0.9849Un billet moyen contient 43,3 nombres non nuls sur 2 864 : 98,5 % de zéros. Cela a un coût direct pour Amira, qui doit stocker les vecteurs. Un vecteur complet de 2 864 nombres float32 occupe 2 864 × 4 = 11 456 octets. Un format creux stocke seulement les nombres non nuls, chacun avec sa position : environ 44 × (4 + 4) = 352 octets. Pour toutes les données d’entraînement, cela représente 53,8 Mo sous forme de vecteurs complets. La matrice creuse de scikit-learn occupe 2,5 Mo. Voilà pourquoi scikit-learn renvoie des matrices creuses et pourquoi nous appelons .toarray() seulement pour de petits exemples.
Étape 3 : la similarité est un produit scalaire divisé par les longueurs
Section intitulée « Étape 3 : la similarité est un produit scalaire divisé par les longueurs »Deux documents sont semblables quand ils utilisent les mêmes mots. Le produit scalaire additionne les produits des positions correspondantes :
En mots : x · y multiplie les fréquences de chaque mot dans les deux documents, puis additionne les résultats. Un produit est non nul seulement si les deux documents utilisent le mot. La longueur ‖x‖ est la racine carrée de la somme des fréquences au carré. La similarité cosinus divise le produit scalaire par les deux longueurs. Seule la direction compte alors. Pour les fréquences, elle se situe entre 0 et 1.
Vérifiez d1 et d5 à la main. Ils partagent « is », « late » et « my », donc d1 · d5 = 1 + 1 + 1 = 3. Chacun contient quatre mots de fréquence 1, donc chaque longueur vaut √4 = 2. Le cosinus vaut 3 ÷ (2 × 2) = 0,75. Maintenant, d1 et d2 : ils ne partagent aucun mot. Chaque produit vaut donc 0, et le cosinus vaut 0. Dans le code, nous ramenons d’abord chaque ligne à une longueur de 1. Un seul produit matriciel donne ensuite tous les cosinus à la fois :
def cosine_matrix(M): M = M / np.linalg.norm(M, axis=1, keepdims=True) # every row to length 1 return (M @ M.T).round(3) # all dot products at once
print(cosine_matrix(C.astype(float)))La sortie :
[[1. 0. 0.289 0.25 0.75 ] [0. 1. 0. 0.447 0. ] [0.289 0. 1. 0. 0.577] [0.25 0.447 0. 1. 0. ] [0.75 0. 0.577 0. 1. ]]La ligne 1, colonne 5 vaut 0,75, comme dans le calcul à la main. La ligne 1, colonne 2 vaut 0. Pour les fréquences, « my parcel is late » et « the package has not arrived » n’ont rien en commun. « my refund is late », une question de paiement, est le document le plus proche de d1.
Le problème : les mots courants décident, et la longueur compte
Section intitulée « Le problème : les mots courants décident, et la longueur compte »Deux problèmes surviennent avec les fréquences brutes de vrais billets.
Les mots courants dominent. « the », « . », « i » et « my » apparaissent dans presque tous les billets, souvent plusieurs fois. Ils contribuent beaucoup à chaque produit scalaire, mais ne disent rien sur l’équipe. Dans T-64697, les cinq fréquences les plus élevées correspondent toutes à de tels mots. « the » apparaît 7 fois, puis « . », « i » et « , », 4 fois chacun, puis « and », 3 fois. Nous avons mesuré l’effet sur les 600 billets de validation. Avec les fréquences, deux billets destinés à des équipes différentes ont un cosinus moyen de 0,380. Deux billets destinés à la même équipe atteignent seulement 0,415. La majeure partie de la similarité provient des mots que tous les billets partagent.
La longueur compte pour le produit scalaire. Écrivez d1 deux fois dans un billet : « my parcel is late my parcel is late ». Chaque fréquence double, donc son produit scalaire avec tout autre billet double. Les longs billets ont de grands produits scalaires avec tous les autres. Le cosinus élimine cet effet, car il divise par la longueur. Le billet doublé conserve la même direction, donc son cosinus ne change pas. C’est une des raisons pour lesquelles la recherche et TF-IDF utilisent le cosinus et des lignes de longueur 1.
La formule corrige le deuxième problème. Le premier exige une nouvelle idée : pondérer chaque mot selon l’information qu’il nous apporte.
Représentation 2 : TF-IDF
Section intitulée « Représentation 2 : TF-IDF »Étape 1 : tf, df et idf
Section intitulée « Étape 1 : tf, df et idf »Un mot présent dans tous les billets ne permet pas de les distinguer. Un mot présent dans quelques billets le permet. TF-IDF traduit cette idée en trois nombres. Pour un mot t, un document d et une collection de N documents :
En mots : la fréquence du terme tf est le nombre d’occurrences du mot t dans le document d. La fréquence documentaire df est le nombre de documents qui contiennent t au moins une fois. Les barres verticales signifient « combien ». La fréquence documentaire inverse idf est le logarithme naturel de N divisé par df. Son nom décrit son rôle : elle est grande quand la fréquence documentaire est petite. Le poids d’un mot dans un document est tf × idf.
Le logarithme atténue les écarts sur l’échelle. Un mot présent dans la moitié des documents obtient ln 2 = 0,69. Un mot présent dans 1 document sur 1 000 obtient ln 1 000 = 6,9. C’est dix fois plus, et non 500 fois plus. Un mot présent dans tous les documents obtient ln 1 = 0 : il disparaît.
Dans les cinq documents, N = 5. « my » apparaît dans d1, d3 et d5, donc df = 3 et idf = ln(5 ÷ 3) = 0,5108. « parcel » apparaît dans d1 et d4 : ln(5 ÷ 2) = 0,9163. « package » apparaît seulement dans d2 : ln 5 = 1,6094. Ainsi, « package » compte trois fois plus que « my ».
Étape 2 : la formule que scikit-learn utilise vraiment
Section intitulée « Étape 2 : la formule que scikit-learn utilise vraiment »C’est la formule des manuels. Le TfidfVectorizer de scikit-learn utilise une autre formule par défaut. Cet outil produit la référence de base TF-IDF du cours. Si vous comparez ses nombres à ceux du manuel, ils ne correspondront pas. Voici exactement ce qu’il calcule avec ses paramètres par défaut :
Trois changements, chacun avec une raison :
- Le « 1 + » dans la fraction (
smooth_idf=True) simule un document supplémentaire contenant chaque mot une fois. Ainsi, df ne vaut jamais 0, et la division fonctionne toujours. - Le « + 1 » à la fin conserve les mots présents dans tous les documents. Avec la formule du manuel, leur poids est 0 et ils disparaissent; ici, ils reçoivent un petit poids. Avec
smooth_idf=False, scikit-learn utilise ln(N ÷ df) + 1 : ce n’est toujours pas la formule du manuel. - Chaque ligne est divisée par sa longueur (
norm='l2'), pour que chaque vecteur de document v ait une longueur de 1. C’est la correction de longueur de la section sur les fréquences. tf reste le nombre brut d’occurrences (sublinear_tf=False); avecTrue, il devient 1 + ln tf.
Calculons maintenant une cellule à la main : « parcel » dans d1. tf = 1 et df = 2, donc idf = ln(6 ÷ 3) + 1 = 1,6931. Les autres mots de d1 obtiennent une idf de 1,6931 (« is », « late », dans 2 documents). « my », présent dans 3 documents, obtient 1,4055. Avant la mise à l’échelle, la ligne de d1 est donc (is 1,6931, late 1,6931, my 1,4055, parcel 1,6931). Sa longueur est √(3 × 1,6931² + 1,4055²) = 3,2520. Le poids de « parcel » dans d1 est 1,6931 ÷ 3,2520 = 0,5206. Vérifiez ce résultat, ainsi que la matrice entière, avec scikit-learn :
from sklearn.feature_extraction.text import TfidfVectorizer
N = len(docs)df = (C > 0).sum(axis=0) # in how many documents is each word?idf = np.log((1 + N) / (1 + df)) + 1 # scikit-learn's smoothed idfW = C * idf # tf x idf, word by wordW = W / np.linalg.norm(W, axis=1, keepdims=True) # every row to length 1 (l2)
sk = TfidfVectorizer(tokenizer=tokenize, token_pattern=None, lowercase=False)S = sk.fit_transform(docs).toarray()print("df: ", df)print("idf:", idf.round(4))print("parcel in d1:", W[0, 9].round(4), "| largest difference to scikit-learn:", np.abs(W - S).max())La sortie :
df: [2 1 1 1 2 2 3 1 1 2 2 2]idf: [1.6931 2.0986 2.0986 2.0986 1.6931 1.6931 1.4055 2.0986 2.0986 1.6931 1.6931 1.6931]parcel in d1: 0.5206 | largest difference to scikit-learn: 1.1102230246251565e-16La plus grande différence est 1,1 × 10⁻¹⁶, l’erreur d’arrondi d’un nombre float64. Nos six lignes calculent exactement ce que scikit-learn calcule. Essayez chaque mot dans l’explorateur. Choisissez un mot et suivez son poids, du nombre d’occurrences au nombre final.
Explorer
TF-IDF, un mot à la fois
Prédisez d’abord : Dans d1 « my parcel is late », quel mot obtient le plus petit poids TF-IDF : my, parcel, is ou late? Pourquoi?
- Documents qui contiennent le mot (df)
- 2 sur 5
- idf
- ln((1 + 5) / (1 + 2)) + 1 = 1,6931
« parcel » est dans 2 des 5 documents, donc son idf est de 1,6931. Les mots présents dans 2 documents reçoivent un idf moyen. Son poids final : d1 0,5206; d4 0,4695.
- poids TF-IDF final (la barre est plus longue pour un poids plus grand)
| Document | tf | tf × idf | longueur de la ligne | poids |
|---|---|---|---|---|
| d1 my parcel is late | 1 | 1,6931 | 3,2520 | 0,5206 |
| d2 the package has not arrived | 0 | 0 | 4,3526 | 0 |
| d3 refund my card | 0 | 0 | 3,0407 | 0 |
| d4 the parcel arrived broken | 1 | 1,6931 | 3,6061 | 0,4695 |
| d5 my refund is late | 0 | 0 | 3,2520 | 0 |
poids = tf × idf ÷ longueur de la ligne. La longueur de la ligne est la longueur du vecteur tf × idf de tout le document, donc chaque ligne de la matrice TF-IDF a une longueur de 1.
Description textuelle de l’explorateur
L’explorateur utilise cinq documents inventés. d1 est « my parcel is late », d2 « the package has not arrived », d3 « refund my card », d4 « the parcel arrived broken » et d5 « my refund is late ». Vous choisissez un de leurs 12 mots. Une deuxième commande permet de choisir la formule idf : ln((1 + N) / (1 + df)) + 1 de scikit-learn, ou ln(N / df) du manuel. L’affichage montre df et idf avec les nombres substitués dans la formule. Un tableau contient une ligne par document. Il montre tf, tf × idf, la longueur de la ligne du document et le poids final tf × idf ÷ longueur de la ligne, avec une barre. Au départ, le mot est « parcel » avec la formule de scikit-learn : df 2 sur 5, idf ln(6/3) + 1 = 1,6931. Dans d1, tf 1, tf × idf 1,6931, longueur de ligne 3,2520, poids 0,5206. Dans d4 « the parcel arrived broken », tf 1, longueur de ligne 3,6061, poids 0,4695. Dans les autres documents, le poids est 0. « my » apparaît dans 3 documents et obtient la plus petite idf, 1,4055. Il a donc le plus petit poids dans d1, 0,4322. Les mots présents dans un seul document, comme « package » et « card », obtiennent une idf de 2,0986. Avec la formule du manuel, « my » obtient 0,5108 et son poids dans d1 descend à 0,3064. Celui de « parcel » monte à 0,5496.
« my » obtient le plus petit poids dans d1, 0,4322. C’est le seul mot de d1 présent dans trois documents. Avec la formule du manuel, l’écart est plus grand : 0,3064 pour « my » contre 0,5496 pour les autres mots. L’image montre la matrice complète des fréquences à côté de sa matrice TF-IDF. Les mots rares ressortent, tandis que les mots courants « my » et « the » s’estompent.
Description textuelle du graphique
Deux tableaux avec les 5 documents en colonnes et les 12 mots en lignes : arrived, broken, card, has, is, late, my, not, package, parcel, refund, the. Dans le tableau des fréquences, chaque cellule non nulle vaut 1 et a la même teinte. Dans le tableau TF-IDF, les mêmes cellules sont non nulles, avec des valeurs différentes. Dans d1, « is », « late » et « parcel » valent 0,52 et « my » vaut 0,43. Dans d2, « has », « not » et « package » valent 0,48, et « arrived » et « the » valent 0,39. Dans d3, « card » a la plus grande valeur du tableau, 0,69, « refund » vaut 0,56 et « my » vaut 0,46. Dans d4, « broken » vaut 0,58, et « arrived », « parcel » et « the » valent 0,47. Dans d5, « is », « late » et « refund » valent 0,52 et « my » vaut 0,43. Les mots présents dans un seul document obtiennent toujours le plus grand poids dans leur document.
Étape 3 : l’effet des poids sur les billets
Section intitulée « Étape 3 : l’effet des poids sur les billets »Ajustez maintenant TF-IDF sur les 4 696 billets d’entraînement. min_df=2 conserve les mots présents dans au moins deux billets, comme le fait la référence de base TF-IDF du cours.
texts = [r["text"] for r in train_rows]tfidf = TfidfVectorizer(tokenizer=tokenize, token_pattern=None, lowercase=False, min_df=2).fit(texts)words, idf_t = tfidf.get_feature_names_out(), tfidf.idf_order = idf_t.argsort()print("lowest: ", [(words[i], round(float(idf_t[i]), 2)) for i in order[:5]])print("highest:", [(words[i], round(float(idf_t[i]), 2)) for i in order[-3:]])for w in ["refund", "parcel", "password", "late"]: print(f"{w:9} idf {idf_t[tfidf.vocabulary_[w]]:.2f}")La sortie :
lowest: [('.', 1.06), ('the', 1.12), (',', 1.14), ('i', 1.19), ('and', 1.27)]highest: [('spray', 8.36), ('mount', 8.36), ('fiddly', 8.36)]refund idf 2.36parcel idf 3.66password idf 4.15late idf 5.58Le point apparaît dans 4 431 des 4 696 billets, donc son idf approche le minimum de 1. Un mot présent dans seulement deux billets obtient ln(4 697 ÷ 3) + 1 = 8,36, le maximum ici. Le graphique place des mots choisis sur une même échelle.
Description textuelle du graphique
Un graphique à barres horizontales, une barre par mot, du plus courant au plus rare. Le point, dans 4 431 billets : idf 1,06. « the », 4 180 billets : 1,12. « my », 3 323 : 1,35. « refund », 1 206 : 2,36. « parcel », 329 : 3,66. « broken », 214 : 4,08. « password », 200 : 4,15. « invoice », 60 : 5,34. « warranty », 53 : 5,47. « late », 47 : 5,58. « fiddly », dans 2 billets : 8,36, la plus grande idf de toutes.
Deux remarques pour être honnête. D’abord, idf favorise les mots rares, pas les mots utiles. « fiddly » obtient le plus grand poids, mais indique peu de choses sur l’équipe. « refund » en dit beaucoup et obtient seulement 2,36. TF-IDF ne connaît pas les équipes. La régression logistique apprend quels mots comptent pour quelle équipe. Ensuite, un mot courant peut encore dominer s’il se répète. Dans T-64697, « the » apparaît 7 fois et reste donc parmi les poids les plus élevés :
row = next(r for r in valid_rows if r["ticket_id"] == "T-64697")counts = CountVectorizer(tokenizer=tokenize, token_pattern=None, lowercase=False, min_df=2).fit(texts)c, t = counts.transform([row["text"]]).toarray()[0], tfidf.transform([row["text"]]).toarray()[0]print("by count: ", [(words[i], int(c[i])) for i in c.argsort()[::-1][:5]])print("by TF-IDF:", [(words[i], round(float(t[i]), 2)) for i in t.argsort()[::-1][:5]])La sortie :
by count: [('the', 7), ('.', 4), ('i', 4), (',', 4), ('and', 3)]by TF-IDF: [('specific', 0.23), ('amount', 0.23), ('the', 0.21), ('recall', 0.2), ('receiving', 0.2)]Le billet concerne un montant facturé : les premiers mots TF-IDF comprennent maintenant « amount » et « specific », mais « the » arrive troisième. La pondération change-t-elle les billets qui se ressemblent? Répétons la mesure de la section sur les fréquences :
from sklearn.preprocessing import normalize
v_texts = [r["text"] for r in valid_rows]teams = np.array([r["team"] for r in valid_rows])same = teams[:, None] == teams[None, :] # pairs of tickets with the same teamother = ~np.eye(len(teams), dtype=bool) # leave out each ticket with itselffor name, vec in [("counts", counts), ("TF-IDF", tfidf)]: V = normalize(vec.transform(v_texts)) # rows of length 1 sims = (V @ V.T).toarray() # 600 x 600 cosine similarities print(f"{name:7} same team {sims[same & other].mean():.3f} different teams {sims[~same].mean():.3f}")La sortie :
counts same team 0.415 different teams 0.380TF-IDF same team 0.141 different teams 0.109Avec les fréquences, la similarité de fond entre billets sans rapport est de 0,380. L’appartenance à la même équipe n’ajoute que 9 % à cette valeur. Avec TF-IDF, la similarité de fond descend à 0,109, et la même équipe la dépasse de 29 %. Les mots courants partagés ne dominent plus la similarité.
Ce que cela signifie pour le routeur de Larkfield
Section intitulée « Ce que cela signifie pour le routeur de Larkfield »De meilleurs vecteurs devraient donner un meilleur routeur. Nous entraînons la même régression logistique sur chaque représentation et évaluons les 600 billets de validation. Le code se trouve dans le carnet Jupyter. Le graphique montre les résultats, avec une cinquième représentation expliquée dans la prochaine section.
Description textuelle du graphique
Cinq barres. Fréquences : 97,83 %. 0/1, un 1 pour chaque mot du billet et 0 sinon : 97,83 %. Log, ln(1 + fréquence) : 98,50 %. TF-IDF : 98,83 %, la plus élevée. MiniLM, un modèle préentraîné et figé de plongements de phrases avec 384 nombres par billet : 94,50 %, la plus faible. L’axe commence à 90 %, donc les différences semblent plus grandes qu’elles ne le sont : un billet représente 0,17 point.
TF-IDF est la meilleure des cinq : 98,83 %, soit 6 billets correctement acheminés de plus sur 600 qu’avec les fréquences brutes. Les fréquences logarithmiques se situent entre les deux, car ln(1 + fréquence) atténue aussi les répétitions de « the ». À cette taille, des différences de 1 à 3 billets relèvent du bruit. Une différence de 6 est plus nette, mais repose encore sur une seule partition et une seule graine aléatoire. C’est la référence de base TF-IDF du cours, et le routeur que Larkfield devrait encore utiliser.
Le problème que TF-IDF ne peut pas résoudre : les synonymes
Section intitulée « Le problème que TF-IDF ne peut pas résoudre : les synonymes »Revenons maintenant aux deux clients. TF-IDF change les valeurs du vecteur, jamais les zéros. Un mot absent d’un document reste à 0. Prenez d1 « my parcel is late » et d2 « the package has not arrived ». Chaque multiplication de leur produit scalaire donne encore 0, donc le cosinus reste 0. La raison tient à l’espace lui-même : « parcel » et « package » sont deux axes différents, perpendiculaires. Aucune pondération ne peut orienter un axe vers l’autre.
Ce n’est pas seulement un exemple inventé. « package » apparaît seulement 8 fois dans les billets d’entraînement et « parcel » 352 fois. Voici une vraie paire, dont les deux billets concernent l’équipe de livraison. Un billet dit « la boîte que j’attendais contenait le couvercle, mais il n’est pas dans le colis ». L’autre dit « Re : commande LK-905144 - colis en retard ». Ils ne partagent aucun jeton, donc leur cosinus TF-IDF est 0,000. Parmi les 2 817 600 paires formées d’un billet de validation et d’un billet d’entraînement, 29 920 ne partagent aucun jeton. Pour ces paires, les fréquences et TF-IDF ne peuvent rien dire.
Représentation 3 : plongements denses
Section intitulée « Représentation 3 : plongements denses »Étape 1 : les mots qui entourent un mot
Section intitulée « Étape 1 : les mots qui entourent un mot »La solution vient d’une phrase du linguiste J. R. Firth, en 1957 : « Vous reconnaîtrez un mot aux mots qui l’entourent. » C’est l’hypothèse distributionnelle : les mots présents dans des contextes semblables ont des sens semblables. Dans les textes anglais, « parcel » et « package » apparaissent tous deux près de « arrived », « late », « courier » et « delivery ». Une méthode qui apprend un vecteur pour chaque mot à partir de ses voisins leur donnera donc des vecteurs semblables.
Un tel vecteur est un plongement dense : k nombres par mot, dont presque aucun ne vaut 0. k est beaucoup plus petit que V, par exemple 64 ou 384 plutôt que 2 864. Les vecteurs forment un tableau E avec une ligne par mot, comme dans Jetons et plongements. Le vecteur d’un document peut être la moyenne des lignes de ses mots :
En mots : o_t est le vecteur one-hot du mot t, composé de zéros sauf un 1 à sa position. Sa multiplication par le tableau E sélectionne la ligne t, le plongement e_t du mot. Le vecteur de document s_d est la moyenne des plongements de ses n_d jetons. Un modèle de phrases entraîné, comme celui ci-dessous, combine d’abord le vecteur de chaque mot avec son contexte, puis calcule la moyenne. La sortie est le même type d’objet : un vecteur dense par texte.
Voici un petit exemple avec des plongements inventés de 2 nombres : parcel = (0,9, 0,2), package = (0,8, 0,3), refund = (0,1, 0,95). Les vecteurs one-hot de parcel et package ont un produit scalaire de 0. Leurs plongements ont un produit scalaire de 0,72 + 0,06 = 0,78, et des longueurs de 0,922 et 0,854. Leur cosinus est donc 0,78 ÷ (0,922 × 0,854) = 0,99. Pour parcel et refund : (0,09 + 0,19) ÷ (0,922 × 0,955) = 0,32. Les mots semblables pointent maintenant dans la même direction.
Étape 2 : mesurer la paire de synonymes
Section intitulée « Étape 2 : mesurer la paire de synonymes »Nous utilisons all-MiniLM-L6-v2, un petit modèle préentraîné de plongements de phrases (licence Apache-2.0, 22,7 millions de paramètres). Il a été entraîné sur une grande quantité de textes anglais et transforme tout texte en 384 nombres. Nous ne l’entraînons pas sur les billets : il est figé.
from sentence_transformers import SentenceTransformer
encoder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2", device="cpu")E = encoder.encode(docs, normalize_embeddings=True) # one row of 384 numbers per document, length 1print(E.shape)print((E @ E.T).round(3))La sortie :
(5, 384)[[1. 0.502 0.285 0.585 0.573] [0.502 1. 0.189 0.564 0.236] [0.285 0.189 1. 0.186 0.632] [0.585 0.564 0.186 1. 0.23 ] [0.573 0.236 0.632 0.23 1. ]]Le cosinus de d1 et d2 est maintenant 0,502, et non 0. Voici les trois représentations côte à côte :
Description textuelle du graphique
Trois tableaux de 5 sur 5 cellules, un par représentation, avec les documents d1 à d5 en lignes et en colonnes. La diagonale vaut 1 partout. Fréquences : d1 et d2 0, d1 et d3 0,29, d1 et d4 0,25, d1 et d5 0,75. Aussi, d2 et d4 0,45, d3 et d5 0,58; les autres paires 0. TF-IDF : les mêmes zéros; d1 et d5 0,73, d1 et d4 0,24, d1 et d3 0,20, d2 et d4 0,37, d3 et d5 0,49. Plongement de phrases : aucune cellule ne vaut 0. d1 et d2 0,50, d1 et d4 0,59, d1 et d5 0,57, d2 et d4 0,56, d3 et d5 0,63. Les plus petites valeurs, environ 0,19, se trouvent entre d3 « refund my card » et d2 ou d4.
Lisez les cellules encadrées et la ligne d1. Les fréquences et TF-IDF placent d5 « my refund is late » bien au-dessus de d2 : 0,750 et 0,729 contre 0. Le plongement place d2 près de d5 (0,502 contre 0,573), et d4 « the parcel arrived broken » en premier (0,585). Le graphique suit d1 dans les trois représentations :
- d2 « the package has not arrived » (même sens)
- d5 « my refund is late » (mots partagés, autre équipe)
Description textuelle du graphique
Trois lignes, une par représentation, sur une échelle de 0 à 1. Fréquences : d2 à 0,000, d5 à 0,750. TF-IDF : d2 à 0,000, d5 à 0,729. Plongement de phrases : d2 à 0,502 et d5 à 0,573, proches l’un de l’autre. Les fréquences et TF-IDF ne voient que les mots partagés, donc le document sur le remboursement l’emporte largement. Le plongement voit que d2 a le même sens que d1, et l’écart disparaît presque.
Sur la vraie paire de billets de livraison, le cosinus des plongements est de 0,255 contre 0,000 pour TF-IDF. C’est plus faible que pour les courtes phrases, mais il n’est plus aveugle à leur ressemblance. Une simple moyenne de vecteurs de mots généraux fait beaucoup moins bien. Dans le carnet Jupyter, la moyenne des vecteurs de mots GloVe donne un cosinus de 0,905 à d1 et d2. Mais elle donne 0,939 à d1 et d5. « my », « is » et « the » attirent toutes les moyennes au même endroit, donc toutes les paires se ressemblent.
Étape 3 : le résultat réel pour l’acheminement
Section intitulée « Étape 3 : le résultat réel pour l’acheminement »Alors, les plongements gagnent? Pas pour la tâche de Larkfield. Regardez de nouveau le graphique d’exactitude. La régression logistique sur les vecteurs MiniLM figés achemine correctement 94,50 % des billets de validation, contre 98,83 % pour TF-IDF. Cela fait 26 billets mal acheminés de plus sur 600.
La raison tient à la tâche. Des mots précis déterminent l’équipe d’un billet : « refund », « password », « warranty », « charged twice ». TF-IDF conserve chacun sur son propre axe, et la régression logistique apprend un poids pour chacun. MiniLM compresse le billet entier en 384 nombres qui décrivent son sens général. Il n’a jamais été entraîné sur les équipes de Larkfield. « My refund is late » et « my parcel is late » ont des sens proches, mais relèvent d’équipes différentes. Les plongements règlent le problème des synonymes, pas tous les problèmes.
Voici le coût de chaque représentation par billet : fréquences et TF-IDF, environ 352 octets en format creux; un plongement de 64 nombres, 256 octets; MiniLM, 1 536 octets. MiniLM doit aussi exécuter un modèle de 22,7 millions de paramètres pour chaque nouveau billet. Utilisez les fréquences ou TF-IDF quand les mots précis décident, comme pour l’acheminement. Utilisez les plongements quand le sens doit rester malgré des mots différents, comme pour la recherche. Par exemple, trouver « the package has not arrived » quand une personne saisit « parcel late ». Faire le réglage fin d’un petit transformeur mesure ce qui arrive quand on poursuit l’entraînement d’un modèle préentraîné sur les billets.
D’où viennent les vecteurs
Section intitulée « D’où viennent les vecteurs »Nous avons utilisé un modèle prêt à l’emploi. Comment les nombres des plongements obtiennent-ils leurs valeurs? La réponse prend la phrase de Firth au pied de la lettre. Comptez quels mots apparaissent près de quels autres, puis compressez ces fréquences en vecteurs courts. word2vec (Mikolov et ses collègues, 2013) les apprend en prédisant les voisins d’un mot. GloVe (Pennington, Socher et Manning, 2014) les ajuste aux logarithmes des fréquences de cooccurrence. La prochaine leçon construit les deux sur les billets et montre leur étroite parenté.
Exercice guidé : un poids TF-IDF à la main
Section intitulée « Exercice guidé : un poids TF-IDF à la main »- Prenez d3, « refund my card », avec N = 5 documents. Notez le df de « refund », « my » et « card » dans le tableau des documents.
- Calculez chaque idf avec la formule de scikit-learn, ln((1 + N) ÷ (1 + df)) + 1.
- Chaque tf dans d3 vaut 1. La ligne avant la mise à l’échelle contient donc les trois valeurs idf. Calculez sa longueur : la racine carrée de la somme des carrés.
- Divisez l’idf de « card » par la longueur. Vous devriez obtenir environ 0,690.
- Vérifiez votre réponse dans l’explorateur : choisissez « card ». Dans le carnet Jupyter, complétez
tfidf_by_hand()et exécutez sa cellule de vérification.
Afficher la réponse
« refund » apparaît dans d3 et d5 (df 2), « my » dans d1, d3 et d5 (df 3), et « card » seulement dans d3 (df 1). Les valeurs idf sont ln(6/3) + 1 = 1,6931, ln(6/4) + 1 = 1,4055 et ln(6/2) + 1 = 2,0986. La longueur est √(1,6931² + 1,4055² + 2,0986²) = √9,2461 = 3,0407. Le poids de « card » est 2,0986 ÷ 3,0407 = 0,6902, le poids le plus élevé de toute la matrice. « card » apparaît dans un seul document, et d3 ne contient que trois mots.
À vous de jouer : changez les options
Section intitulée « À vous de jouer : changez les options »TfidfVectorizer propose deux options qui changent la formule : smooth_idf=False utilise ln(N ÷ df) + 1, et sublinear_tf=True utilise 1 + ln tf au lieu de tf. Avant de les appliquer aux cinq documents, faites une prédiction : quelle option change le poids de « parcel » dans d1? Laquelle ne change rien du tout?
Afficher la réponse
smooth_idf=False change légèrement chaque poids. « parcel » dans d1 passe à 0,5255 au lieu de 0,5206, et le plus grand changement dans la matrice est 0,040. sublinear_tf=True ne change rien : aucun mot n’apparaît deux fois dans ces documents. Comme 1 + ln 1 = 1, chaque tf reste à 1. Sur de vrais billets, cette option compte, car « the » peut apparaître 7 fois : 1 + ln 7 = 2,9 au lieu de 7.
Erreur courante
Mon TF-IDF ne correspond pas à celui de scikit-learn
Symptôme : Tomás calcule le TF-IDF à la main avec la formule du manuel et obtient 0,5496 pour « parcel » dans d1. scikit-learn donne 0,5206. Le cosinus de d1 et d3 vaut 0,0815 dans sa version et 0,1998 dans celle de scikit-learn. Il conclut que l’un des deux se trompe.
Cause : les deux résultats sont corrects, mais les formules diffèrent. Le manuel utilise idf = ln(N ÷ df); scikit-learn utilise par défaut ln((1 + N) ÷ (1 + df)) + 1. Le « + 1 » donne une plus grande part aux mots courants comme « my ». Chaque poids et chaque cosinus changent donc, même si les zéros restent aux mêmes endroits.
Solution : écrivez la formule exacte à côté de chaque valeur TF-IDF, y compris les options par défaut de la bibliothèque : smooth_idf, sublinear_tf et norm. Pour vérifier une bibliothèque, reproduisez sa formule, comme dans nos six lignes : le plus grand écart est alors de 1,1 × 10⁻¹⁶.
Ce que cela signifie pour un vrai service d’IA : « TF-IDF » désigne une famille de formules, pas une seule. Si l’index de recherche utilise une variante et le code de calcul des scores des nouveaux billets une autre, les similarités changent sans avertissement. Ajustez le vectoriseur une seule fois, sur les billets d’entraînement uniquement. Enregistrez-le et utilisez cette version partout.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Pourquoi la similarité cosinus de « my parcel is late » et « the package has not arrived » vaut-elle exactement 0 pour les fréquences et le TF-IDF? Utilisez la formule dans votre réponse.
Afficher la réponse
Le cosinus est le produit scalaire divisé par les deux longueurs. Le produit scalaire est la somme de x_j × y_j pour tous les mots j. Un produit est non nul seulement pour un mot présent dans les deux documents. Ces deux documents n’ont aucun mot en commun. Chaque produit vaut donc 0, le produit scalaire vaut 0, et le cosinus vaut 0. Le TF-IDF multiplie les fréquences par l’idf et met les lignes à l’échelle. Mais un 0 reste un 0, donc le résultat ne change pas. Dans l’espace vectoriel, « parcel » et « package » sont des axes distincts qui forment un angle droit.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Un mot apparaît dans chacun des N documents. Quel est son idf avec la formule du manuel et celle par défaut de scikit-learn? Pourquoi scikit-learn ajoute-t-il le + 1?
Afficher la réponse
Manuel : ln(N / N) = ln 1 = 0. Le mot reçoit donc un poids de 0 et disparaît de chaque vecteur. scikit-learn : ln((1 + N) / (1 + N)) + 1 = 0 + 1 = 1. Le mot conserve donc un petit poids, le plus faible possible. Le + 1 à la fin garde ces mots dans le vecteur au lieu de les supprimer complètement. Le 1 + à l’intérieur de la fraction correspond au lissage. Il agit comme si un document supplémentaire contenait chaque mot une fois, pour que df ne soit jamais 0.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Omar lit que les plongements comprennent le sens et demande pourquoi le système d'acheminement de Larkfield ne les utilise pas. Que lui répondez-vous, chiffres à l'appui?
Afficher la réponse
Sur les billets de validation, la régression logistique avec TF-IDF en achemine correctement 98,83 %. Avec les plongements MiniLM figés, elle n’en achemine correctement que 94,50 %, soit 26 billets de moins sur 600. Des mots précis comme « refund », « password » ou « warranty » déterminent l’équipe, et TF-IDF garde chaque mot sur son propre axe. Un plongement général compresse un billet en 384 nombres qui représentent son sens, sans connaître les équipes de Larkfield. « my refund is late » et « my parcel is late » ont des sens proches, mais vont à des équipes différentes. Les plongements aident quand des mots différents doivent correspondre, comme dans la recherche. Ils donnent alors à « parcel » et « package » un cosinus de 0,502 au lieu de 0. Ils coûtent aussi plus cher : un modèle de 22,7 millions de paramètres doit s’exécuter pour chaque billet.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Les fréquences placent un document dans un espace avec un axe par mot : 2 864 nombres par billet, dont 98,5 % valent 0. Stockez-les sous forme creuse : environ 352 octets au lieu de 11 456.
- Similarité cosinus = x · y ÷ (‖x‖ ‖y‖). Elle élimine l’effet de la longueur, mais les mots courants contribuent le plus à la similarité avec les fréquences brutes.
- Le TF-IDF pondère chaque fréquence par idf = ln(N ÷ df). La formule par défaut de scikit-learn est ln((1 + N) ÷ (1 + df)) + 1, avec des lignes de longueur 1. Précisez toujours la formule utilisée.
- Sur les billets, le TF-IDF donne le meilleur acheminement : 98,83 %, contre 97,83 % pour les fréquences et 94,50 % pour les plongements MiniLM figés.
- Le TF-IDF ne change jamais un 0, donc les synonymes gardent un cosinus de 0. Les plongements denses apprennent à partir des mots qui apparaissent ensemble. Ils donnent à « my parcel is late » et « the package has not arrived » un cosinus de 0,502.
À suivre : comment word2vec et GloVe apprennent ces vecteurs denses à partir des fréquences de cooccurrence.
Leçon suivante : Comment les plongements apprennent le sens : word2vec et GloVe