Texto como datos: tokens, vocabulario y clasificación

Tema 21 · Fase 5, Lenguaje y atención · Toda la ruta

En esta ficha · 5 secciones
  1. Antes de empezar
  2. Ejercicios
  3. Mini-proyecto: un clasificador de sentimiento y sus errores
  4. Errores comunes
  5. ¿Lo lograste?

Cuaderno del tema. La teoría y los ejercicios con todo su código, listos para ejecutar en Colab sin instalar nada.

Antes de empezar

Varios ejercicios usan las variables y funciones de la teoría: quijote, todas, conteo, palabras, sin_tildes, construir_vocabulario, entrenar_bpe, tokenizar_bpe, tok_gpt2, contar_ngramas, cuentas, perplejidad, valid, mini, y_mini, cv_mini, X_mini, nueva, entrenar_nb, textos, etiquetas, X_ent, X_pru, y_ent, y_pru, X_sub, X_val, y_sub, y_val, candidatos y resenas. Abre el cuaderno del tema en Colab (botón «Abrir en Colab», en la ficha Código), ejecuta las celdas de la teoría y trabaja en los ejercicios, que vienen debajo. Intenta cada ejercicio antes de abrir la respuesta: equivocarse primero y corregir después es como se aprende de verdad.

Ejercicios

1. ¿Qué normalizar?

Para cada caso, decide si conviene pasar a minúsculas, quitar las tildes y quitar la puntuación, y explica por qué:

  1. Un buscador de recetas donde la gente escribe rápido desde el celular («receta de pique macho sin cebolla»).
  2. Un modelo que corrige la ortografía de tareas escolares.
  3. Un clasificador que decide si un mensaje es una pregunta o una afirmación.
  4. Un contador de cuántas veces se nombra a cada personaje en el Quijote.
Respuesta
  1. Minúsculas sí, tildes fuera, puntuación fuera. La gente escribe sin tildes desde el celular; para encontrar «limón» tiene que servir «limon». Aquí juntar variantes importa más que distinguir «él» de «el».
  2. Nada. Las mayúsculas, las tildes y la puntuación son justamente lo que se corrige: si las quitas, el modelo no puede ver los errores.
  3. La puntuación se queda. Los signos «¿» y «?» son la mejor pista. Las minúsculas no molestan.
  4. Minúsculas no. «Sancho» con mayúscula es el personaje; las tildes pueden quedarse. Pero cuidado: al principio de una oración cualquier palabra va con mayúscula, así que también hay que mirar el contexto.

No hay una normalización correcta para todo: se elige según la tarea.

2. BPE a mano, con 5 fusiones

Entrena BPE a mano con este texto de cinco palabras: «sol sola solo sol solos». Empieza partiendo cada palabra en letras con «_» al final, cuenta los pares de piezas vecinas (cada palabra cuenta tantas veces como aparece) y haz 5 fusiones. Si hay empate, gana el par que aparece primero leyendo las palabras en orden, de izquierda a derecha. Al final, ¿en qué tokens queda partida cada palabra? Después compruébalo con entrenar_bpe.

Respuesta

Las palabras distintas son sol (2 veces), sola, solo y solos (1 vez cada una).

  1. Los pares «s o» y «o l» aparecen 5 veces cada uno (en todas las palabras). Empate: gana «s o», que aparece primero. Nueva pieza: so.
  2. Ahora «so l» aparece 5 veces. Nueva pieza: sol.
  3. «sol _» aparece 2 veces (las dos de sol) y «sol o» también 2 (solo y solos). Empate: gana «sol », porque sol es la primera palabra. Nueva pieza: **sol**.
  4. «sol o» es el único par con 2. Nueva pieza: solo.
  5. Todos los pares que quedan aparecen 1 vez; el primero es «sol a», de sola. Nueva pieza: sola.

Resultado: sol = sol_, sola = sola + _, solo = solo + _, solos = solo + s + _.

fus, _ = entrenar_bpe(Counter("sol sola solo sol solos".split()), 5)
print(["".join(par) for par in fus])
for p in ["sol", "sola", "solo", "solos"]:
    print(p, tokenizar_bpe(p, fus))
['so', 'sol', 'sol_', 'solo', 'sola']
sol ['sol_']
sola ['sola', '_']
solo ['solo', '_']
solos ['solo', 's', '_']

3. ¿Cuántas palabras raras dejar fuera?

Usa el primer 80 % del Quijote para construir el vocabulario y el 10 % siguiente (la validación de la teoría) para medirlo; la prueba no se toca para elegir. Con minimo igual a 1, 2, 5 y 20, calcula el tamaño del vocabulario y qué porcentaje de las palabras de la validación quedan como <UNK>. ¿Qué gana y qué pierde un vocabulario chico?

Respuesta
corte_ent = int(0.8 * len(todas))
corte_val = int(0.9 * len(todas))
conteo_ent = Counter(todas[:corte_ent])
validacion = todas[corte_ent:corte_val]
for minimo in [1, 2, 5, 20]:
    v = construir_vocabulario(conteo_ent, minimo)
    unk = sum(p not in v for p in validacion) / len(validacion)
    print(f"mínimo {minimo:>2}: vocabulario {len(v):>6,}   "
          f"desconocidas en la validación: {unk:.1%}")
mínimo  1: vocabulario 20,379   desconocidas en la validación: 4.0%
mínimo  2: vocabulario 10,208   desconocidas en la validación: 6.3%
mínimo  5: vocabulario  4,488   desconocidas en la validación: 10.6%
mínimo 20: vocabulario  1,375   desconocidas en la validación: 19.0%

Con minimo=1 (todas las palabras del entrenamiento) el vocabulario casi se duplica respecto de minimo=2 (20 379 contra 10 208), pero las desconocidas solo bajan de 6,3 % a 4,0 %: casi todas las palabras que aparecen una sola vez en el entrenamiento no vuelven a aparecer en la validación. Con minimo=20, el vocabulario es muy chico (un modelo más liviano, con menos que aprender), pero casi una de cada cinco palabras de la validación se pierde como <UNK>. Por eso los modelos modernos usan subpalabras: con BPE no hay que tirar ninguna palabra.

4. Perplejidad con n = 1, 2 y 3

Calcula la perplejidad del modelo de la teoría sobre la validación con n = 1, 2 y 3, primero con lam=0.6 y después con lam=0.3. ¿El trigrama es mejor que el bigrama? ¿Por qué crees que pasa eso?

Respuesta
for lam in [0.6, 0.3]:
    resultado = [perplejidad(valid, n, lam) for n in (1, 2, 3)]
    print(f"lam = {lam}:  " + "   ".join(
        f"n = {n}: {p:.0f}" for n, p in zip((1, 2, 3), resultado)))

trigramas, _ = cuentas[3]
nuevos = sum(tuple(valid[i:i + 3]) not in trigramas
             for i in range(len(valid) - 2))
print(f"Trigramas de la validación nunca vistos: "
      f"{nuevos / (len(valid) - 2):.0%}")
lam = 0.6:  n = 1: 469   n = 2: 210   n = 3: 269
lam = 0.3:  n = 1: 469   n = 2: 239   n = 3: 216
Trigramas de la validación nunca vistos: 65%

El unigrama no usa lam, así que da lo mismo en las dos filas. El bigrama mejora mucho al unigrama. El trigrama, en cambio, empeora con lam=0.6 (269 contra 210), y con lam=0.3 llega a 216, todavía un poco peor que el mejor bigrama. Por eso en la teoría nos quedamos con el bigrama y lam=0.6 antes de mirar la prueba. La razón está en la última línea: la mayoría de los trigramas de la validación nunca aparecieron en el entrenamiento. Un solo libro no alcanza para estimar bien tantas combinaciones de tres palabras, y confiar mucho en esas cuentas (un lam alto) castiga al modelo. Más contexto solo ayuda si hay datos para aprenderlo; los modelos neuronales del tema 24 lo logran porque comparten lo aprendido entre palabras parecidas.

5. La bolsa no ve el orden

Estas dos reseñas dicen lo contrario: «no es bueno, es malo» y «no es malo, es bueno». Calcula su bolsa de palabras con CountVectorizer(). ¿Son distintas? Repite con CountVectorizer(ngram_range=(1, 2)), que además de las palabras cuenta los bigramas (pares de palabras seguidas).

Respuesta
dos = ["no es bueno, es malo", "no es malo, es bueno"]
for rango in [(1, 1), (1, 2)]:
    cv = CountVectorizer(ngram_range=rango)
    X2 = cv.fit_transform(dos).toarray()
    print(rango, "¿iguales?", (X2[0] == X2[1]).all())
print(list(cv.get_feature_names_out()))
(1, 1) ¿iguales? True
(1, 2) ¿iguales? False
['bueno', 'bueno es', 'es', 'es bueno', 'es malo', 'malo', 'malo es', 'no', 'no es']

Con palabras sueltas, las dos reseñas son el mismo vector: ningún modelo podría separarlas. Con bigramas aparecen «no es», «es bueno» y «es malo», y ahora sí son distintas: «bueno es» solo está en la primera y «malo es» solo en la segunda. Los bigramas recuperan un poco de orden, a cambio de un vocabulario mucho más grande.

6. Bigramas en el clasificador

Compara el clasificador de reseñas de la teoría (solo palabras) con uno que use TfidfVectorizer(min_df=2, ngram_range=(1, 2)): tamaño del vocabulario y exactitud. Para comparar no uses la prueba: entrena los dos con X_sub y mídelos en la validación X_val, como en la teoría. ¿Qué peso recibe el bigrama «not bad»?

Respuesta
for rango in [(1, 1), (1, 2)]:
    m = make_pipeline(TfidfVectorizer(min_df=2, ngram_range=rango),
                      LogisticRegression(max_iter=1000))
    m.fit(X_sub, y_sub)
    print(f"{rango}: vocabulario {len(m[0].vocabulary_):>6,}   "
          f"exactitud en la validación {m.score(X_val, y_val):.3f}")
# m quedó con el último modelo del bucle: el de bigramas
vocab_bi = m[0].vocabulary_
pesos_bi = m[1].coef_[0]
for frase in ["not", "not bad", "not good", "not worth"]:
    print(f"{frase:<10} {pesos_bi[vocab_bi[frase]]:+.2f}")
(1, 1): vocabulario  9,181   exactitud en la validación 0.847
(1, 2): vocabulario 35,958   exactitud en la validación 0.855
not        -5.37
not bad    +0.08
not good   -0.46
not worth  -1.35

El vocabulario se multiplica casi por cuatro, pero la exactitud en la validación apenas sube (de 0,847 a 0,855): con 3840 reseñas de entrenamiento, la mayoría de los bigramas aparecen muy pocas veces para aprender bien su peso. Aun así, el modelo aprendió algo con sentido: «not bad» tiene peso positivo, al revés que «not» solo, y «not worth» (no vale la pena) es claramente negativo.

7. Formas con padding

Tienes tres secuencias de IDs de largos 4, 7 y 2. Sin computadora, responde: ¿qué forma tiene el lote que devuelve pad_sequence(..., batch_first=True)? ¿Cuántos ceros de relleno tiene? ¿Cuánto suma la máscara? ¿Y si después truncas el lote a 5 posiciones con lote[:, :5]? Compruébalo con código.

Respuesta

El lote tiene forma (3, 7): tres secuencias, rellenadas hasta 7, la más larga. Hay 3 + 0 + 5 = 8 ceros de relleno, y la máscara suma 4 + 7 + 2 = 13 (los tokens reales). Truncado a 5 queda (3, 5); la segunda secuencia pierde 2 tokens, y la máscara suma 4 + 5 + 2 = 11.

seqs = [torch.arange(1, n + 1) for n in [4, 7, 2]]
l = pad_sequence(seqs, batch_first=True)
m = l != 0
print(l.shape, "relleno:", (~m).sum().item(),
      " máscara:", m.sum().item())
print(l[:, :5].shape, "máscara:", m[:, :5].sum().item())
torch.Size([3, 7]) relleno: 8  máscara: 13
torch.Size([3, 5]) máscara: 11

8. ¿Cuánto cuesta el español en GPT-2?

Cuenta cuántos tokens de GPT-2 hay por palabra en las primeras 2000 palabras del Quijote y en las primeras 2000 palabras de las reseñas (en inglés). ¿Cuántas veces más tokens necesita el español? Para comparar, haz lo mismo con el BPE de la teoría (300 fusiones).

Respuesta
texto_es = " ".join(quijote.split()[:2000])
texto_en = " ".join(" ".join(textos).split()[:2000])
for nombre, t in [("Quijote", texto_es), ("Reseñas", texto_en)]:
    n_pal = len(palabras(t))
    gpt2 = len(tok_gpt2.tokenize(t)) / n_pal
    nuestro = sum(len(tokenizar_bpe(p, fusiones))
                  for p in palabras(t)) / n_pal
    print(f"{nombre}: GPT-2 {gpt2:.2f}   nuestro BPE {nuestro:.2f}"
          "   tokens por palabra")
Quijote: GPT-2 1.87   nuestro BPE 2.07   tokens por palabra
Reseñas: GPT-2 1.24   nuestro BPE 3.36   tokens por palabra

GPT-2 usa bastante más tokens por palabra en el español del Quijote que en las reseñas en inglés, y nuestro BPE hace lo contrario. (Los tokens de GPT-2 incluyen la puntuación, que palabras no cuenta, así que sus números salen un poco inflados en los dos idiomas.) Un modelo con un tokenizador entrenado en inglés lee menos texto en español con el mismo largo máximo, y le cuesta más tiempo procesarlo.

9. Sin suavizado

Con las seis opiniones del restaurante de la teoría, entrena entrenar_nb con alpha=0, es decir, sin suavizado. ¿Cuánto valen P(buena | negativa) y P(caro | positiva)? Calcula, sin logaritmos, el producto P(clase) · P(w₁ | clase) · P(w₂ | clase) · … de «buena comida pero caro» para cada clase. ¿Qué pasa? Después elige el alpha de MultinomialNB para las reseñas entre 0.01, 0.1, 1 y 3, con la validación. ¿Alguno le gana a la logística (0,847)?

Respuesta
prior0, log_p0 = entrenar_nb(X_mini, y_mini, alpha=0)
p0 = np.exp(log_p0)
voc = cv_mini.vocabulary_
print("P(buena | negativa) =", p0[0, voc["buena"]],
      "  P(caro | positiva) =", p0[1, voc["caro"]])
# Cada P(w | clase) elevada a las veces que w aparece en el texto
producto = np.exp(prior0) * np.prod(p0 ** nueva[0], axis=1)
print("Producto (negativa, positiva):", producto)

for a in [0.01, 0.1, 1, 3]:
    m = make_pipeline(CountVectorizer(min_df=2), MultinomialNB(alpha=a))
    m.fit(X_sub, y_sub)
    print(f"alpha = {a:<4}  exactitud en la validación "
          f"{m.score(X_val, y_val):.3f}")
P(buena | negativa) = 0.0   P(caro | positiva) = 0.0
Producto (negativa, positiva): [0. 0.]
alpha = 0.01  exactitud en la validación 0.776
alpha = 0.1   exactitud en la validación 0.800
alpha = 1     exactitud en la validación 0.830
alpha = 3     exactitud en la validación 0.831

(NumPy avisa divide by zero encountered in log: es el ln 0 = −∞ de las palabras que faltan en una clase.) «buena» nunca aparece en las negativas y «caro» nunca en las positivas, así que sin suavizado las dos probabilidades son 0, y los dos productos dan 0: el modelo no puede decidir, y queda una división 0/0. Una sola palabra ausente borra toda la demás evidencia del texto. Con las reseñas, un alpha muy chico es mucho peor (0,776 con 0.01): una palabra rara que solo apareció en una clase se vuelve casi una prueba definitiva. alpha = 1 y 3 quedan casi empatados (0,830 y 0,831), y ninguno alcanza a la logística.

10. ¿Qué tan ingenuo?

Calcula P(positiva) para «great», «great great» y «great great great great» con los dos modelos de candidatos (entrenados con X_sub). Antes de correrlo, predice: ¿qué le pasa a cada modelo cuando la palabra se repite? Calcula también P(great | positiva) / P(great | negativa) en Naive Bayes.

Respuesta
nb_res = candidatos["Naive Bayes"]
log_res = candidatos["Logística"]
for texto in ["great", "great great", "great great great great"]:
    p_nb = nb_res.predict_proba([texto])[0, 1]
    p_log = log_res.predict_proba([texto])[0, 1]
    print(f"{texto:<24} Naive Bayes {p_nb:.3f}   logística {p_log:.3f}")

lp = nb_res[1].feature_log_prob_
i = nb_res[0].vocabulary_["great"]
print(f"P(great | positiva) / P(great | negativa) = "
      f"{np.exp(lp[1, i] - lp[0, i]):.1f}")
great                    Naive Bayes 0.780   logística 0.997
great great              Naive Bayes 0.928   logística 0.997
great great great great  Naive Bayes 0.994   logística 0.997
P(great | positiva) / P(great | negativa) = 3.6

En Naive Bayes, cada «great» cuenta como una evidencia nueva e independiente: multiplica por 3,6 la razón entre las dos clases, y la probabilidad sube de 0,78 a 0,93 y a 0,99 sin que el texto diga nada nuevo. Esa es la suposición ingenua en acción, y por eso las probabilidades de Naive Bayes suelen ser demasiado seguras: sirven para decidir la clase, pero no conviene creerles el número. La logística da lo mismo con las tres frases, porque TF-IDF divide cada fila por su norma y las tres quedan como el mismo vector. Eso no la hace más sabia: con una sola palabra, TF-IDF le da a «great» el valor 1, mucho más de lo que pesa dentro de una reseña normal, y por eso responde 0,997.

Mini-proyecto: un clasificador de sentimiento y sus errores

Construye el mejor clasificador de reseñas que puedas con TF-IDF y regresión logística, y después mira sus errores: es la parte que más enseña.

  1. Toma 20 000 reseñas de resenas (con shuffle(seed=1), para que no sean las mismas de la teoría) y divídelas en entrenamiento (70 %), validación (15 %) y prueba (15 %), estratificando.
  2. Elige con la validación la mejor combinación de ngram_range ((1, 1) o (1, 2)) y de C de la regresión logística (0.3, 1, 3 y 10; recuerda del tema 8 que un C más chico regulariza más).
  3. Con la mejor combinación, entrena con entrenamiento y validación juntos, y mide una sola vez en la prueba: exactitud, precisión y exhaustividad (tema 9).
  4. Busca los errores en los que el modelo estaba más seguro: las reseñas negativas con mayor probabilidad de ser positivas, y al revés. Léelas. ¿Por qué se equivocó? Busca negaciones, ironía, reseñas que hablan bien de una cosa y mal de otra, o etiquetas que parecen equivocadas.
  5. Escribe tres o cuatro reseñas propias en inglés que creas que van a engañar al modelo, y comprueba si lo logras.

Opcional: escribe 20 reseñas cortas en español (10 positivas y 10 negativas), entrena un clasificador solo con ellas y pruébalo con otras 5. ¿Qué pasa con tan pocos datos?

Resultados de referencia, para comprobar los tuyos

Este código hace los pasos 1 a 4; tus números deberían coincidir si usas la misma semilla y la misma división.

from sklearn.metrics import precision_score, recall_score

datos = resenas.shuffle(seed=1).select(range(20000))
txt = np.array([f"{t}. {c}" for t, c in zip(datos["title"],
                                            datos["content"])])
lab = np.array(datos["label"])
txt_ent, txt_resto, lab_ent, lab_resto = train_test_split(
    txt, lab, test_size=0.3, random_state=0, stratify=lab)
txt_val, txt_pru, lab_val, lab_pru = train_test_split(
    txt_resto, lab_resto, test_size=0.5, random_state=0,
    stratify=lab_resto)

resultados = {}
for rango in [(1, 1), (1, 2)]:
    for C in [0.3, 1, 3, 10]:
        m = make_pipeline(TfidfVectorizer(min_df=2, ngram_range=rango),
                          LogisticRegression(C=C, max_iter=1000))
        resultados[rango, C] = m.fit(txt_ent, lab_ent).score(txt_val,
                                                             lab_val)
        print(f"{rango}  C = {C:<4} validación: "
              f"{resultados[rango, C]:.3f}")

rango, C = max(resultados, key=resultados.get)
final = make_pipeline(TfidfVectorizer(min_df=2, ngram_range=rango),
                      LogisticRegression(C=C, max_iter=1000))
final.fit(np.concatenate([txt_ent, txt_val]),
          np.concatenate([lab_ent, lab_val]))
pred = final.predict(txt_pru)
print(f"Elegido: {rango}, C = {C}")
print(f"Prueba: exactitud {(pred == lab_pru).mean():.3f}   "
      f"precisión {precision_score(lab_pru, pred):.3f}   "
      f"exhaustividad {recall_score(lab_pru, pred):.3f}")
(1, 1)  C = 0.3  validación: 0.863
(1, 1)  C = 1    validación: 0.881
(1, 1)  C = 3    validación: 0.885
(1, 1)  C = 10   validación: 0.882
(1, 2)  C = 0.3  validación: 0.865
(1, 2)  C = 1    validación: 0.880
(1, 2)  C = 3    validación: 0.885
(1, 2)  C = 10   validación: 0.891
Elegido: (1, 2), C = 10
Prueba: exactitud 0.901   precisión 0.898   exhaustividad 0.903
p_pos = final.predict_proba(txt_pru)[:, 1]
negativas_altas = np.argsort(np.where(lab_pru == 0, p_pos, -1))[::-1]
positivas_bajas = np.argsort(np.where(lab_pru == 1, p_pos, 2))
for titulo, indices in [("Negativas que parecían positivas",
                         negativas_altas),
                        ("Positivas que parecían negativas",
                         positivas_bajas)]:
    print(titulo)
    # Saltamos las reseñas con insultos: son reales y algunas los tienen
    limpias = [i for i in indices
               if not set(palabras(txt_pru[i])) & {"stupid", "idiot",
                                                    "idiots", "dumb",
                                                    "crap", "hell",
                                                    "damn", "sucks"}]
    for i in limpias[:3]:
        print(f"  {p_pos[i]:.2f}  {txt_pru[i][:66]}")
Negativas que parecían positivas
  0.99  good for a b-day prsent?. my brother loves these game. )hes favori
  0.98  a good water saver. My well produces 6 GPM. I have a tankless wate
  0.98  WARNING DONNIE DARKO FANS. Donnie Darko is one of the greatest mov
Positivas que parecían negativas
  0.01  Amazon: Please Help!. This is not a book. It is not a publication.
  0.01  Wrong Disc!. Of course, i'm a huge fan of Gilmore Girls. That's wh
  0.04  EXCELENT BOOK. HAY THERE DO NOT LET THE TITLE OF THIS BOOK FOOL YO

Con validación se eligió (1, 2) y C = 10, aunque las ocho combinaciones quedan a menos de 3 puntos entre sí. En la prueba, con 14 000 + 3000 reseñas para entrenar, la exactitud sube a 0,90: más datos ayudan más que afinar el modelo. Al leer los errores más seguros aparecen los casos típicos:

  • Reseñas con palabras positivas que critican otra cosa: el título de la segunda dice good (bueno) y la tercera empieza diciendo que una película es de las mejores (greatest), pero las dos están marcadas como negativas.
  • Reseñas positivas llenas de not: la primera y la tercera de la segunda lista niegan cosas («no es un libro», «no dejes que el título te engañe») para defender el producto.
  • Etiquetas que parecen equivocadas: la primera de la lista de arriba suena a una reseña contenta. En un dataset real siempre hay algo de ruido en las etiquetas.

Una bolsa de palabras no puede entender a qué se refiere cada palabra ni el tono de la frase; para eso hacen falta los modelos que leen el texto en orden, desde el tema 23.

Errores comunes

  • Normalizar sin pensar. Quitar tildes, mayúsculas o puntuación a todo es una decisión que puede borrar justo la información que importa («él» y «el», o el «¿» de una pregunta).
  • Construir el vocabulario con todos los datos. El vocabulario, los IDF y cualquier conteo se aprenden solo con el entrenamiento; si no, hay fuga de datos (tema 10). Un Pipeline lo hace bien solo.
  • Llamar fit_transform en la prueba. En la prueba va transform: con fit_transform el vectorizador aprende otro vocabulario y las columnas ya no significan lo mismo que en el entrenamiento.
  • Olvidar <UNK>. Si el vocabulario no tiene un token para lo desconocido, la primera palabra nueva hace fallar el código con un KeyError.
  • Usar como ID de relleno un número que también es un token real. Si <PAD> y una palabra comparten el 0, la máscara borra esa palabra. Reserva un ID solo para el relleno.
  • Rellenar y olvidar la máscara. Sin máscara, el modelo trata el relleno como si fueran palabras: un promedio de la frase sale distinto según cuánto relleno tenga.
  • Mezclar tokenizadores. Los IDs de un tokenizador solo tienen sentido para el modelo que se entrenó con él: el 2000 de GPT-2 no es el 2000 de DistilBERT.
  • Creer que un peso grande es una regla del idioma. «and» con peso positivo no significa que «y» sea una palabra alegre: el modelo encuentra correlaciones en sus datos, no significados.
  • Usar un modelo en un idioma distinto del de su entrenamiento. Un clasificador entrenado con reseñas en inglés no sabe nada de una reseña en español: casi todas sus palabras le son desconocidas.
  • Olvidar el suavizado en Naive Bayes. Sin sumar 1 a las cuentas, una sola palabra que nunca apareció en una clase pone en 0 la probabilidad de esa clase, por más que el resto del texto la apoye.
  • Multiplicar probabilidades sin logaritmos. Con textos largos, el producto de cientos de probabilidades pequeñas se redondea a 0; se suman sus logaritmos.
  • Comparar perplejidades con vocabularios distintos. La perplejidad depende del vocabulario y de cómo se tratan los desconocidos; solo se comparan modelos medidos sobre el mismo texto y con el mismo vocabulario.

¿Lo lograste?

Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.

  • Normalizar un texto en español y decidir qué conservar (ejercicio 1).
  • Tokenizar por palabras, por caracteres y por subpalabras con BPE (actividad, ejercicio 2 y ejercicio 8).
  • Construir un vocabulario con un token desconocido y elegir su tamaño (ejercicio 3).
  • Representar textos con bolsa de palabras y TF-IDF y clasificarlos con regresión logística (ejercicios 5 y 6, y el mini-proyecto).
  • Aplicar padding, truncado y máscaras a secuencias de largo variable (ejercicio 7).
  • Construir un modelo de lenguaje de n-gramas y medir su perplejidad (ejercicio 4).
  • Programar Naive Bayes con conteos y logaritmos, y compararlo con la regresión logística en la validación (ejercicios 9 y 10).