Phase 05 - Lesson 02
Bag of Words, TF-IDF y Representacion de Texto
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Cuenta primero, piensa despues. El TF-IDF todavia le gana a los embeddings en tareas bien definidas en 2026.
Tipo: Build Lenguajes: Python Requisitos previos: Fase 5 · 01 (Procesamiento de Texto), Fase 2 · 02 (Regresion Lineal desde Cero) Tiempo: ~75 minutos
El Problema
El modelo necesita numeros. Tu tienes cadenas de texto.
Todo pipeline de NLP tiene que responder la misma pregunta. Como convertir un flujo de tokens de longitud variable en un vector de tamano fijo que un clasificador pueda consumir. La primera respuesta a la que llego el campo fue la mas tonta que funciona. Cuenta las palabras. Arma un vector.
Ese vector ha sostenido mas NLP en produccion que cualquier modelo de embedding. Filtros de spam, clasificadores de temas, deteccion de anomalias en logs, ranking de busqueda (antes de BM25), la primera ola de analisis de sentimiento, la primera decada de benchmarks academicos de NLP. Los practicantes de 2026 todavia lo eligen primero en tareas de clasificacion acotadas. Es rapido, interpretable y a menudo indistinguible de un modelo de embedding de 400M de parametros en tareas donde lo que importa es la presencia de la palabra.
Esta leccion construye el bag of words y luego el TF-IDF, desde cero. Despues muestra a scikit-learn haciendo lo mismo en tres lineas. Por ultimo nombra el modo de falla que te hace recurrir a los embeddings.
El Concepto
Bag of Words (BoW) descarta el orden. Para cada documento, cuenta cuantas veces aparece cada palabra del vocabulario. La longitud del vector es el tamano del vocabulario. La posicion i es el conteo de la palabra i.
TF-IDF reponderada el BoW. Una palabra que aparece en todos los documentos es poco informativa, asi que reduce su peso. Una palabra rara en todo el corpus pero frecuente en un solo documento es senal, asi que aumenta su peso.
TF-IDF(w, d) = TF(w, d) * IDF(w)
= count(w in d) / |d| * log(N / df(w))
Donde TF es la frecuencia del termino en el documento, df es la frecuencia de documento (cuantos docs contienen la palabra), N es el total de documentos. El log mantiene acotado el peso de las palabras ubicuas.
Propiedad clave: ambos producen vectores dispersos con ejes interpretables. Puedes mirar los pesos de un clasificador entrenado y leer que palabras empujan a un documento hacia cada clase. No puedes hacer esto con un embedding BERT de 768 dimensiones.
Construyelo
Paso 1: construir el vocabulario
def build_vocab(docs):
vocab = {}
for doc in docs:
for token in doc:
if token not in vocab:
vocab[token] = len(vocab)
return vocab
Entrada: lista de documentos tokenizados (cualquier tokenizador a nivel de palabra sirve; el code/main.py de esta leccion usa una variante simplificada en minusculas). Salida: dict {word: index}. El orden de insercion estable significa que el indice de palabra 0 es la primera palabra vista en el primer documento. La convencion varia; scikit-learn ordena alfabeticamente.
Paso 2: bag of words
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]]
Las filas son documentos. Las columnas son indices del vocabulario. La entrada [i][j] es "cuantas veces aparece la palabra j en el documento i". El Doc 1 tiene cat dos veces porque asi fue. El Doc 0 tiene ran cero veces porque no fue asi.
Paso 3: frecuencia del termino y frecuencia de documento
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]
Dos trucos de suavizado que vale la pena nombrar. El (n+1)/(d+1) evita log(x/0). El +1 al final garantiza que una palabra presente en todos los documentos siga teniendo IDF 1 (no 0), coincidiendo con el comportamiento por defecto de scikit-learn. Otras implementaciones usan el log(N/df) crudo. Ambos funcionan; la version suavizada es mas amigable.
Paso 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)
Tres documentos, cinco palabras de vocabulario (the, cat, sat, dog, ran). the aparece en los tres, asi que su IDF es bajo. dog aparece en uno, asi que su IDF es alto. Los vectores son dispersos (la mayoria de las entradas son pequenas) y las palabras discriminativas resaltan.
Paso 5: normalizar las filas en 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
Sin normalizacion, un documento mas largo obtiene un vector mas grande y domina los puntajes de similitud. La normalizacion L2 coloca cada documento sobre la hiperesfera unitaria. La similitud del coseno entre filas pasa a ser solo un producto punto.
Usalo
scikit-learn entrega la version de produccion.
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))
El CountVectorizer hace tokenizacion, vocabulario y BoW en una sola llamada. El TfidfVectorizer agrega la ponderacion IDF y la normalizacion L2. Ambos devuelven matrices dispersas. Para 100k documentos, la version densa no cabe en memoria; mantente disperso hasta que el clasificador exija algo denso.
Parametros que lo cambian todo:
| Arg | Efecto |
|---|---|
ngram_range=(1, 2) |
Incluye bigramas. Por lo general mejora la clasificacion. |
min_df=2 |
Descarta palabras presentes en menos de 2 docs. Recorta el vocabulario en datos ruidosos. |
max_df=0.95 |
Descarta palabras presentes en mas del 95% de los docs. Aproxima la eliminacion de stopwords sin una lista fija. |
stop_words="english" |
Lista de stopwords integrada de scikit-learn. Depende de la tarea — el analisis de sentimiento no debe descartar negaciones. |
sublinear_tf=True |
Usa 1 + log(tf) en lugar del tf crudo. Ayuda cuando un termino se repite muchas veces en un doc. |
Cuando el TF-IDF todavia gana (a partir de 2026)
- Deteccion de spam, etiquetado de temas, marcado de anomalias en logs. La presencia de la palabra es lo que importa; el matiz semantico no.
- Regimenes de pocos datos (cientos de ejemplos etiquetados). TF-IDF mas regresion logistica no tiene costo de preentrenamiento.
- Donde sea que importe la latencia. TF-IDF mas un modelo lineal responde en microsegundos. Embeddar un documento a traves de un transformer toma de 10 a 100ms.
- Sistemas que deben explicar sus predicciones. Inspecciona los coeficientes del clasificador. Las principales palabras positivas son la razon.
Cuando el TF-IDF falla
La falla de la ceguera semantica. Considera estos dos documentos:
- "The movie was not good at all."
- "The movie was excellent."
Una es una resena negativa. La otra es positiva. Su solapamiento TF-IDF es exactamente {the, movie, was}. Un clasificador bag-of-words tiene que memorizar que la palabra not cerca de good invierte la etiqueta. Puede aprenderlo con suficientes datos, pero nunca con la elegancia de un modelo que entiende la sintaxis.
La otra falla: palabras fuera del vocabulario en la inferencia. Un modelo BoW entrenado con resenas de IMDb no tiene idea de que hacer con Zoomer-approved si ese token nunca aparecio en el entrenamiento. Los embeddings de subpalabra (leccion 04) manejan esto. El TF-IDF no puede.
Hibrido: embeddings ponderados por TF-IDF
El default pragmatico de 2026 para clasificacion con datos medios: usar los pesos TF-IDF como atencion sobre embeddings de palabras.
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]
Obtienes capacidad semantica de los embeddings y enfasis en palabras raras del TF-IDF. El clasificador se entrena sobre el vector agregado. Esto supera a cualquiera de los dos por separado para clasificacion de sentimiento, tema e intencion por debajo de unos 50k ejemplos etiquetados.
Entregalo
Guarda como outputs/prompt-vectorization-picker.md:
---
name: vectorization-picker
description: Given a text-classification task, recommend BoW, TF-IDF, embeddings, or a hybrid.
phase: 5
lesson: 02
---
You recommend a text-vectorization strategy. Given a task description, output:
1. Representation (BoW, TF-IDF, transformer embeddings, or a hybrid). Explain why in one sentence.
2. Specific vectorizer configuration. Name the library. Quote the arguments (`ngram_range`, `min_df`, `max_df`, `sublinear_tf`, `stop_words`).
3. One failure mode to test before shipping.
Refuse to recommend embeddings when the user has under 500 labeled examples unless they show evidence of semantic failure in a TF-IDF baseline. Refuse to remove stopwords for sentiment analysis (negations carry signal). Flag class imbalance as needing more than a vectorizer change.
Example input: "Classifying 30k customer support tickets into 12 categories. Most tickets are 2-3 sentences. English only. Need explainability for audit logs."
Example output:
- Representation: TF-IDF. 30k examples is not small; explainability requirement rules out dense embeddings.
- Config: `TfidfVectorizer(ngram_range=(1, 2), min_df=3, max_df=0.95, sublinear_tf=True, stop_words=None)`. Keep stopwords because category keywords sometimes are stopwords ("not working" vs "working").
- Failure to test: verify `min_df=3` does not drop rare category keywords. Run `get_feature_names_out` filtered by class and eyeball.
Ejercicios
- Facil. Implementa
cosine_similarity(doc_vec_a, doc_vec_b)sobre la salida TF-IDF normalizada en L2. Verifica que documentos identicos puntuan 1.0 y documentos con vocabularios disjuntos puntuan 0.0. - Medio. Agrega soporte de
n-gramabag_of_words. El parametronproduce conteos sobren-gramas. Prueba quen=2sobre["the", "cat", "sat"]produce conteos de bigramas para["the cat", "cat sat"]. - Dificil. Construye el hibrido de embedding ponderado por TF-IDF anterior usando vectores GloVe 100d (descarga una vez, cachea). Compara la exactitud de clasificacion contra el TF-IDF puro y los embeddings puros con mean-pooling en el dataset 20 Newsgroups. Reporta quien gana donde.
Terminos Clave
| Termino | Lo que la gente dice | Lo que realmente significa |
|---|---|---|
| BoW | Vector de frecuencia de palabras | Conteos de las palabras del vocabulario en un documento. Descarta el orden. |
| TF | Frecuencia del termino | Conteo de una palabra en un documento, opcionalmente normalizado por la longitud del documento. |
| DF | Frecuencia de documento | Conteo de documentos que contienen la palabra al menos una vez. |
| IDF | Frecuencia inversa de documento | log(N / df) suavizado. Reduce el peso de palabras que aparecen en todas partes. |
| Vector disperso | Casi todo ceros | El vocabulario suele tener entre 10k y 100k palabras; la mayoria esta ausente de cualquier documento dado. |
| Similitud del coseno | Angulo entre vectores | Producto punto de vectores normalizados en L2. 1 es identico, 0 es ortogonal. |
Lecturas Adicionales
- scikit-learn — feature extraction from text — la referencia canonica de la API, mas notas sobre cada parametro.
- Salton, G., & Buckley, C. (1988). Term-weighting approaches in automatic text retrieval — el articulo que convirtio al TF-IDF en el default por una decada.
- "Why TF-IDF Still Beats Embeddings" — Ashfaque Thonikkadavan (Medium) — la perspectiva de 2026 sobre cuando gana el metodo antiguo y por que.