Faza 05 · lecția 02
Sacul de cuvinte, TF-IDF și reprezentarea textului
Scopul lecției: Numărați mai întâi, gândiți mai târziu. TF-IDF încă învinge reprezentările vectoriale (embeddings) în sarcini bine definite în 2026.
Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.
Cuprinsul lecției
- Problema
- Conceptul
- Construiți
- Pasul 1: construiți vocabularul
- Pasul 2: sacul de cuvinte
- Pasul 3: frecvența termenilor și frecvența în documente
- Pasul 4: TF-IDF
- Pasul 5: normalizați rândurile L2
- Utilizați-l
- Când TF-IDF încă învinge (în 2026)
- Când TF-IDF eșuează
- Hibrid: reprezentări vectoriale ponderate cu TF-IDF
- Livrați
- Exerciții
- Termeni-cheie
- Lecturi suplimentare
Numărați mai întâi, gândiți mai târziu. TF-IDF încă învinge reprezentările vectoriale (embeddings) în sarcini bine definite în 2026.
Tip: Construiți Limbaje: Python Cerințe prealabile: Faza 5 · 01 (Prelucrarea textului), Faza 2 · 02 (Regresia liniară de la zero) Timp: ~75 de minute
Problema
Modelul are nevoie de numere. Aveți șiruri de caractere.
Fiecare flux NLP trebuie să răspundă la aceeași întrebare. Cum transformăm un flux de tokenuri cu lungime variabilă într-un vector cu dimensiune fixă, pe care un clasificator îl poate consuma? Primul răspuns la care a ajuns domeniul a fost cel mai simplu care funcționează. Numărați cuvintele. Construiți un vector.
Acest vector a susținut mai mult NLP în producție decât orice model de reprezentări vectoriale. Filtre de spam, clasificatoare de subiecte, detectarea anomaliilor din jurnale, clasificarea rezultatelor căutării (înainte de BM25), primul val de analiză a sentimentului, primul deceniu de repere NLP academice. Practicienii din 2026 încă apelează mai întâi la el pentru sarcini restrânse de clasificare. Este rapid, interpretabil și adesea imposibil de deosebit de un model de reprezentări vectoriale cu 400M de parametri în sarcini în care prezența cuvintelor contează.
Notă tehnică a traducerii: TF-IDF poate fi o bază de referință foarte competitivă, dar nu există o regulă universală conform căreia ar învinge sau ar fi imposibil de deosebit de un model de 400M de parametri. Rezultatul depinde de date, etichete, tokenizer, modelul dens, antrenare, metrică, latență și cerințele de implementare; comparați pe aceeași împărțire de date și același protocol de evaluare.
Această lecție construiește sacul de cuvinte, apoi TF-IDF, de la zero. Apoi arată cum scikit-learn face același lucru în trei linii. Apoi numește modul de eșec care vă face să apelați la reprezentări vectoriale.
Conceptul
Sacul de cuvinte (Bag of Words, BoW) ignoră ordinea. Pentru fiecare document, numărați de câte ori apare fiecare cuvânt din vocabular. Lungimea vectorului este dimensiunea vocabularului. Poziția i este numărul de apariții ale cuvântului i.
TF-IDF reponderează BoW. Un cuvânt care apare în fiecare document este neinformativ, astfel că îi reduceți ponderea. Un cuvânt rar în corpus, dar frecvent într-un singur document, este semnal, astfel că îi creșteți ponderea.
TF-IDF(w, d) = TF(w, d) * IDF(w)
= count(w in d) / |d| * log(N / df(w))
Unde TF este frecvența termenului în document, df este frecvența în documente (în câte documente apare cuvântul), iar N este numărul total de documente. log păstrează ponderea mărginită pentru cuvintele omniprezente.
Notă tehnică a traducerii: Formula afișată este forma generică nesmoothed, dar codul lecției calculează IDF-ul smooth al scikit-learn:
log((n_docs + 1) / (df + 1)) + 1. Pentru forma brută, un cuvânt prezent în toate documentele are IDF0; pentru forma smooth are IDF1. Logaritmul atenuează creșterea ponderilor termenilor rari, nu este singurul motiv pentru valoarea termenilor omniprezenți. Valorile exacte cer declararea variantei TF, a smoothing-ului și a normalizării L2.
Proprietate importantă: ambele produc vectori rari, cu axe interpretabile. Puteți examina ponderile unui clasificator antrenat și puteți citi ce cuvinte împing un document spre fiecare clasă. Nu puteți face aceasta cu o reprezentare BERT cu 768 de dimensiuni.
bow-tfidf
Notă tehnică a traducerii: Cheia figurii
bow-tfidfeste mapată explicit la asset-ul-sursă originalbow-tfidf.svg; copia locală are același conținut după normalizarea documentată CRLF→LF. Ilustrația originală este păstrată exact ca figură de sursă, însă vectorii TF-IDF numerici din ea sunt schematici și nu pot fi reproduși din formula afișată sau din cod fără a declara toate convențiile. Pentru un calcul verificabil, specificați formula IDF, smoothing-ul, definiția TF, tokenizerul, vocabularul și dacă se aplică normalizare L2.
Construiți
Pasul 1: construiți vocabularul
def build_vocab(docs):
vocab = {}
for doc in docs:
for token in doc:
if token not in vocab:
vocab[token] = len(vocab)
return vocab
Intrare: listă de documente tokenizate (orice tokenizer la nivel de cuvânt este suficient; code/main.py din această lecție folosește o variantă simplificată cu litere mici). Ieșire: dicționar {word: index}. Ordinea stabilă de inserare înseamnă că indicele 0 al unui cuvânt aparține primului cuvânt observat în primul document. Convenția variază; scikit-learn sortează alfabetic.
Pasul 2: sacul de cuvinte
def bag_of_words(docs, vocab):
matrix = [[0] * len(vocab) for _ in docs]
for i, doc in enumerate(docs):
for token in doc:
if token in vocab:
matrix[i][vocab[token]] += 1
return matrix
>>> docs = [["cat", "sat", "on", "mat"], ["cat", "cat", "ran"]]
>>> vocab = build_vocab(docs)
>>> bag_of_words(docs, vocab)
[[1, 1, 1, 1, 0], [2, 0, 0, 0, 1]]
Rândurile sunt documente. Coloanele sunt indicii vocabularului. Elementul [i][j] reprezintă „de câte ori apare cuvântul j în documentul i”. Documentul 1 are cat de două ori deoarece apare de două ori. Documentul 0 are ran de zero ori deoarece nu apare.
Pasul 3: frecvența termenilor și frecvența în documente
import math
def term_frequency(doc_bow, doc_length):
return [c / doc_length if doc_length else 0 for c in doc_bow]
def document_frequency(bow_matrix):
df = [0] * len(bow_matrix[0])
for row in bow_matrix:
for j, count in enumerate(row):
if count > 0:
df[j] += 1
return df
def inverse_document_frequency(df, n_docs):
return [math.log((n_docs + 1) / (d + 1)) + 1 for d in df]
Două trucuri de smoothing merită numite. (n+1)/(d+1) evită log(x/0). +1 final asigură că un cuvânt aflat în fiecare document are totuși IDF 1 (nu 0), corespunzând comportamentului implicit din scikit-learn. Alte implementări folosesc log(N/df) brut. Ambele funcționează; versiunea smooth este mai prietenoasă.
Notă tehnică a traducerii: Formulele IDF ale codului corespund lui scikit-learn doar pentru
smooth_idf=True, însă întregul flux nu este identic înainte de normalizare: exemplul de la zero împarte TF la lungimea documentului, iarTfidfVectorizerimplicit pornește de la numărări brute și aplică L2. După L2, factorul constant al lungimii se poate anula pentru un document, dar tokenizerul, filtrarea și vocabularul pot produce în continuare alte caracteristici. În plus,document_frequencypresupune o matrice nenulă; tratați explicit corpusurile sau vocabularul goale în cod de producție.
Pasul 4: TF-IDF
def tfidf(bow_matrix):
n_docs = len(bow_matrix)
df = document_frequency(bow_matrix)
idf = inverse_document_frequency(df, n_docs)
out = []
for row in bow_matrix:
length = sum(row)
tf = term_frequency(row, length)
out.append([tf_j * idf_j for tf_j, idf_j in zip(tf, idf)])
return out
>>> docs = [
... ["the", "cat", "sat"],
... ["the", "dog", "sat"],
... ["the", "cat", "ran"],
... ]
>>> vocab = build_vocab(docs)
>>> bow = bag_of_words(docs, vocab)
>>> tfidf(bow)
Trei documente, cinci cuvinte în vocabular (the, cat, sat, dog, ran). the apare în toate cele trei, deci IDF-ul său este mic. dog apare într-unul, deci IDF-ul său este mare. Vectorii sunt rari (majoritatea elementelor sunt mici), iar cuvintele discriminante ies în evidență.
Pasul 5: normalizați rândurile L2
def l2_normalize(matrix):
out = []
for row in matrix:
norm = math.sqrt(sum(x * x for x in row))
out.append([x / norm if norm else 0 for x in row])
return out
Fără normalizare, un document mai lung primește un vector mai mare și domină scorurile de similitudine. Normalizarea L2 plasează fiecare document pe hipersfera unitară. Similitudinea cosinusului dintre rânduri devine acum doar un produs scalar.
Utilizați-l
scikit-learn oferă versiunea pentru producție.
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
docs = ["the cat sat on the mat", "the dog sat on the mat", "the cat ran"]
bow_vectorizer = CountVectorizer()
bow = bow_vectorizer.fit_transform(docs)
print(bow_vectorizer.get_feature_names_out())
print(bow.toarray())
tfidf_vectorizer = TfidfVectorizer()
tfidf = tfidf_vectorizer.fit_transform(docs)
print(tfidf.toarray().round(3))
CountVectorizer face tokenizarea, vocabularul și BoW într-un singur apel. TfidfVectorizer adaugă ponderarea IDF și normalizarea L2. Ambele întorc matrici rare. Pentru 100k de documente, versiunea densă nu încape în memorie; păstrați reprezentarea rară până când clasificatorul cere o reprezentare densă.
Notă tehnică a traducerii: Rezultatele din exemplul de la zero și cele din scikit-learn nu sunt interschimbabile implicit.
CountVectorizeraplică propriul analyzer: prin setările implicite extrage tokenuri de cel puțin două caractere și normalizează literele; codul de la zero primește deja liste de tokenuri. Pentru reproducibilitate, fixați versiunea scikit-learn și configurați explicitanalyzer,token_pattern,lowercase,ngram_range, IDF șinormsau furnizați același tokenizer.
Parametri care schimbă totul:
| Argument | Efect |
|---|---|
ngram_range=(1, 2) |
Include bigrame. De obicei îmbunătățește clasificarea. |
min_df=2 |
Elimină cuvintele din mai puțin de 2 documente. Reduce vocabularul pentru date zgomotoase. |
max_df=0.95 |
Elimină cuvintele din mai mult de 95% dintre documente. Aproximează eliminarea cuvintelor de oprire fără o listă codificată fix. |
stop_words="english" |
Lista internă de cuvinte de oprire a scikit-learn. Depinde de sarcină — analiza sentimentului nu ar trebui să elimine negațiile. |
sublinear_tf=True |
Utilizează 1 + log(tf) în locul lui tf brut. Ajută atunci când un termen se repetă de multe ori într-un document. |
Când TF-IDF încă învinge (în 2026)
- Detectarea spamului, etichetarea subiectelor, semnalarea anomaliilor din jurnale. Contează prezența cuvintelor; nuanța semantică nu contează.
- Regimuri cu puține date (sute de exemple etichetate). TF-IDF plus regresie logistică nu are cost de preantrenare.
- Oriunde contează latența. TF-IDF plus un model liniar răspunde în microsecunde. Reprezentarea unui document printr-un Transformer durează 10–100 ms.
- Sisteme care trebuie să își explice predicțiile. Examinați coeficienții clasificatorului. Cuvintele pozitive de vârf sunt motivul.
Notă tehnică a traducerii: Acestea sunt euristici de selecție, nu proprietăți garantate. Latența depinde de hardware, implementare, dimensiunea vocabularului, lungime, lot, cache și costurile complete ale fluxului; intervalele în microsecunde și 10–100 ms nu se transferă automat. Preantrenarea unui model dens poate fi disponibilă fără cost de antrenare locală, iar calitatea și explicabilitatea trebuie măsurate pentru sarcina concretă.
Când TF-IDF eșuează
Eșecul orbirii semantice. Luați în considerare aceste două documente:
- „Filmul nu a fost deloc bun.”
- „Filmul a fost excelent.”
Unul este o recenzie negativă. Unul este pozitiv. Suprapunerea lor TF-IDF este exact {the, movie, was}. Un clasificator bag-of-words trebuie să memoreze că termenul not lângă good inversează eticheta. Poate învăța aceasta cu suficiente date, dar niciodată la fel de natural ca un model care înțelege sintaxa.
Celălalt eșec: cuvinte din afara vocabularului la inferență. Un model BoW antrenat pe recenzii IMDb nu știe ce să facă cu Zoomer-approved dacă acel token nu a apărut niciodată la antrenare. Reprezentările vectoriale pe subcuvinte (lecția 04) gestionează acest lucru. TF-IDF nu poate.
Hibrid: reprezentări vectoriale ponderate cu TF-IDF
Valoarea implicită pragmatică din 2026 pentru clasificarea cu date medii: utilizați ponderile TF-IDF drept atenție asupra reprezentărilor vectoriale ale cuvintelor.
def tfidf_weighted_embedding(doc, tfidf_scores, embedding_table, dim):
vec = [0.0] * dim
total_weight = 0.0
for token in doc:
if token not in embedding_table or token not in tfidf_scores:
continue
weight = tfidf_scores[token]
emb = embedding_table[token]
for i in range(dim):
vec[i] += weight * emb[i]
total_weight += weight
if total_weight == 0:
return vec
return [v / total_weight for v in vec]
Obțineți capacitate semantică de la reprezentări vectoriale și accent pe cuvinte rare de la TF-IDF. Clasificatorul se antrenează pe vectorul agregat. Aceasta depășește fiecare metodă separat pentru clasificarea sentimentului, a subiectelor și a intențiilor sub aproximativ 50k de exemple etichetate.
Notă tehnică a traducerii: Aceasta este o medie ponderată statică, nu mecanism de atenție învățat și condiționat de interogare.
tfidf_scorestrebuie definit ca scoruri ale termenilor pentru documentul curent; dacă acel scor include deja frecvența termenului, parcurgerea fiecărei apariții repetate poate număra TF de două ori. Precizați dacă ponderile sunt pe tip de token sau pe apariție, cum sunt tratate OOV-urile și normalizați evaluarea; pragul de 50k de exemple nu este universal.
Livrați
Salvați ca outputs/prompt-vectorization-picker.md:
---
name: vectorization-picker
description: Recomandă BoW, TF-IDF, reprezentări vectoriale sau un hibrid pentru o sarcină de clasificare a textului.
phase: 5
lesson: 02
---
Oferiți recomandări despre o strategie de vectorizare a textului. Pentru o descriere a sarcinii, furnizați:
1. Reprezentarea (BoW, TF-IDF, reprezentări vectoriale Transformer sau un hibrid). Explicați de ce într-o propoziție.
2. Configurația specifică a vectorizatorului. Numiți biblioteca. Citați argumentele (`ngram_range`, `min_df`, `max_df`, `sublinear_tf`, `stop_words`).
3. Un mod de eșec de testat înainte de livrare.
Refuzați să recomandați reprezentări vectoriale când utilizatorul are sub 500 de exemple etichetate, cu excepția cazului în care demonstrează eșec semantic într-o bază de referință TF-IDF. Refuzați să eliminați cuvintele de oprire pentru analiza sentimentului (negațiile poartă semnal). Semnalați dezechilibrul claselor ca necesitând mai mult decât schimbarea vectorizatorului.
Exemplu de intrare: „Clasific 30k de tichete de asistență pentru clienți în 12 categorii. Majoritatea tichetelor au 2–3 propoziții. Numai engleză. Am nevoie de explicabilitate pentru jurnalele de audit.”
Exemplu de ieșire:
- Reprezentare: TF-IDF. 30k de exemple nu înseamnă puține date; cerința de explicabilitate exclude reprezentările vectoriale dense.
- Configurație: `TfidfVectorizer(ngram_range=(1, 2), min_df=3, max_df=0.95, sublinear_tf=True, stop_words=None)`. Păstrați cuvintele de oprire deoarece uneori cuvintele-cheie de categorie sunt cuvinte de oprire (`"not working"` vs `"working"`).
- Eșec de testat: verificați că `min_df=3` nu elimină cuvinte-cheie rare ale categoriei. Rulați `get_feature_names_out` filtrat după clasă și inspectați vizual.
Notă tehnică a traducerii: Regula promptului de a refuza reprezentări vectoriale sub 500 de exemple este prea absolută. Transferul, reprezentările preantrenate, zero-shot, datele neetichetate, domeniul și criteriul de evaluare pot schimba alegerea; folosiți o bază TF-IDF și comparați experimental, nu o limită numerică rigidă.
Exerciții
- Ușor. Implementați
cosine_similarity(doc_vec_a, doc_vec_b)pe ieșirea TF-IDF normalizată L2. Verificați că documentele identice au scorul 1.0, iar documentele cu vocabulare disjuncte au scorul 0.0. - Mediu. Adăugați suport pentru
n-gramlabag_of_words. Parametrulnproduce numărări pentrun-grame. Verificați căn=2pentru["the", "cat", "sat"]produce numărări de bigrame pentru["the cat", "cat sat"]. - Dificil. Construiți hibridul de mai sus cu reprezentări GloVe 100d ponderate cu TF-IDF (descărcați o dată, păstrați în cache). Comparați acuratețea clasificării cu TF-IDF simplu și cu reprezentări agregate prin medie simplă pe setul de date 20 Newsgroups. Raportați ce învinge și unde.
Termeni-cheie
| Termen | Ce spun oamenii | Ce înseamnă de fapt |
|---|---|---|
| BoW | Vector de frecvențe ale cuvintelor | Numărări ale cuvintelor din vocabular într-un document. Ignoră ordinea. |
| TF | Frecvența termenului | Numărul de apariții ale unui cuvânt într-un document, opțional normalizat după lungimea documentului. |
| DF | Frecvența în documente | Numărul documentelor care conțin cuvântul cel puțin o dată. |
| IDF | Frecvența inversă în documente | log(N / df) cu smoothing. Reduce ponderea cuvintelor care apar peste tot. |
| Vector rar | În mare parte zerouri | Vocabularul are de obicei 10k–100k de cuvinte; cele mai multe lipsesc din orice document dat. |
| Similitudinea cosinusului | Unghiul dintre vectori | Produsul scalar al vectorilor normalizați L2. 1 înseamnă identic, 0 înseamnă ortogonal. |
Lecturi suplimentare
- scikit-learn — extragerea caracteristicilor din text — referința API canonică și note pentru fiecare parametru.
- Salton, G., & Buckley, C. (1988). Abordări de ponderare a termenilor în regăsirea automată de text — lucrarea care a făcut TF-IDF valoarea implicită timp de un deceniu.
- „De ce TF-IDF încă învinge reprezentările vectoriale” — Ashfaque Thonikkadavan (Medium) — perspectiva din 2026 despre când câștigă metoda veche și de ce.
Sursă: Originalul în limba engleză
Navigare: ← Lecția 05.01 — Prelucrarea textului — tokenizare, stemming, lematizare · Faza 5 — Fundamente și tehnici avansate NLP · Lecția 05.03 — Reprezentări vectoriale ale cuvintelor — Word2Vec de la zero → · Catalog complet