fast.ai · Practical Deep Learning for Coders · Lección 4
NLP: la lección que el propio curso ya tuvo que reescribir
Esta es la lección más interesante del curso para leer con perspectiva de 2026 — no porque su contenido original esté mal, sino porque quedó tan superado que la página web del curso ya no sigue al libro: usa Hugging Face Transformers en vez del enfoque RNN que enseña el Capítulo 10.
Antes de leerlección 3, SGD y redes · HF cap. 6, tokenización (opcional)
🎯 Al terminar vas a poder
- Explicar las tres etapas de ULMFiT (LM genérico → LM adaptado al dominio → clasificador) y reconocer el mismo patrón en un pipeline moderno de CPT + SFT.
- Leer el código fastai de un language model y de un clasificador de texto —
TextBlock,vocab,save_encoder/load_encoder,freeze_to— y decir qué hace cada llamada. - Definir perplexity en una frase, explicar por qué
to_fp16()acelera, y justificar discriminative learning rates y gradual unfreezing. - Escribir el esqueleto de un fine-tuning con
AutoModelForSequenceClassification+Trainerpara la competencia de patentes, con un split que no filtre anchors entre train y validación.
1. Dos lecciones en una: el libro (RNN) y la web (Transformers)
El libro es de 2020. En ese momento, BERT y GPT-2 ya existían, pero fastai todavía enseñaba NLP con su
propio enfoque insignia: ULMFiT, basado en RNNs (LSTM), que había sido estado del arte en
2018. Para cuando se grabó la versión 2022 del curso — la que ves en course.fast.ai hoy —
los Transformers ya habían desplazado por completo a las RNN en NLP, y el proyecto de la lección cambió
para reflejar eso: clasificar similitud entre frases de patentes de EE.UU. usando la
librería transformers de Hugging Face en vez de AWD_LSTM.
Como ya hiciste el recorrido completo de Transformers en el curso de HF, esta página se enfoca en lo que
el libro enseña (ULMFiT/RNN) — que sigue siendo un capítulo brillante sobre transfer learning en
NLP, y vale la pena entender aunque la arquitectura de base haya cambiado — y lo contrasta explícitamente
con lo que ya sabés de transformers. La versión web queda resumida en §5, con el puente al Capítulo 3
de HF donde ya viste Trainer en detalle.
2. Tokenización con reglas, no solo minúsculas
La tokenización de fastai usa spaCy como base pero le agrega una capa de reglas propias diseñada para
preservar información en vez de descartarla. En lugar de simplemente poner todo en
minúsculas (perdiendo la señal de énfasis o nombres propios), inserta tokens especiales con prefijo
xx: xxbos (inicio de documento — le dice al modelo "olvidate del contexto
anterior"), xxmaj (la palabra siguiente empezaba con mayúscula), xxup (la
palabra entera estaba en mayúsculas), xxunk (palabra fuera del vocabulario), y variantes que
comprimen repeticiones de caracteres (xxrep) en vez de gastar un token distinto por cada
variante de "!!!!".
from fastai.text.all import *
spacy = WordTokenizer()
tkn = Tokenizer(spacy) # spaCy + las reglas de fastai
txt = "The U.S. dollar $1 is $1.00. THIS is a test!!!!"
print(coll_repr(tkn(txt), 31))
(#25) ['xxbos','xxmaj','the','xxmaj','u.s.','dollar','$','1','is','$','1.00','.','xxup','this','is','a','test','xxrep','4','!'...]
Después de tokenizar viene la numericalización: armar el vocabulario (fastai se queda con
los tokens que aparecen al menos 3 veces, hasta un máximo de 60.000, y manda el resto a
xxunk) y reemplazar cada token por su índice. Ese vocabulario es el objeto vocab
que aparece en el código de §3 — y la razón por la que hay que pasárselo explícitamente al clasificador.
La idea de fondo, que sigue siendo relevante hoy: en vez de perder información para simplificar el vocabulario, se traduce el texto a un "lenguaje" auxiliar diseñado para que el modelo aprenda un concepto general (la capitalización) con una matriz de embeddings chica, en vez de necesitar una entrada distinta por cada palabra en cada variante de mayúsculas posible.
🟢 El contraste con lo que ya sabés
En el Capítulo 6 de HF viste BPE
byte-level: en vez de reglas explícitas sobre mayúsculas, un tokenizer BPE moderno aprende directamente
del corpus qué unidades sub-palabra son frecuentes, sin necesitar tokens especiales xx*
diseñados a mano. Es un cambio de filosofía completo: de reglas lingüísticas escritas por humanos a
patrones estadísticos aprendidos — el mismo movimiento general que separa todo este capítulo (ULMFiT)
de los Transformers. Lo que sí sobrevive: la idea de tokens especiales ([CLS],
[SEP], <s>) y la de un vocabulario fijo con un token para lo desconocido.
3. ULMFiT: transfer learning en NLP, en 3 pasos
La idea central del capítulo, y del paper Universal Language Model Fine-tuning for Text Classification (Howard & Ruder, 2018, arXiv:1801.06146), es que el transfer learning en NLP no es un salto directo como en visión (donde ImageNet → tu dataset funciona en un solo paso). Hacen falta tres etapas:
- Language model preentrenado en un corpus genérico grande (Wikipedia, WikiText-103), con la tarea self-supervised de predecir la siguiente palabra — sin necesitar ninguna etiqueta humana, porque la etiqueta ya está "embebida" en el propio texto.
- Fine-tuning del language model en el corpus específico del dominio final (reviews de IMDb, incluyendo las que no tienen etiqueta de sentimiento). Este es el paso que el libro insiste en que la gente se salta y no debería: Wikipedia tiene un registro completamente distinto al de reviews de cine informales con nombres de actores. Saltarse este paso intermedio empeora significativamente los resultados, según el propio paper.
- Fine-tuning del clasificador, reusando el encoder del language model ya afinado (sin su capa final de predecir la próxima palabra) como cuerpo, con una cabeza nueva para la tarea final.
# etapa 2: afinar el LM en el dominio (todas las reviews, con y sin etiqueta)
dls_lm = DataBlock(
blocks=TextBlock.from_folder(path, is_lm=True),
get_items=partial(get_text_files, folders=['train', 'test', 'unsup']),
splitter=RandomSplitter(0.1)
).dataloaders(path, path=path, bs=128, seq_len=80)
learn = language_model_learner(dls_lm, AWD_LSTM, drop_mult=0.3,
metrics=[accuracy, Perplexity()]).to_fp16()
learn.fit_one_cycle(1, 2e-2) # solo los embeddings nuevos
learn.unfreeze(); learn.fit_one_cycle(10, 2e-3)
learn.save_encoder('finetuned') # el cuerpo, sin la cabeza de "próxima palabra"
# etapa 3: el clasificador, sobre el mismo vocab
dls_clas = DataBlock(
blocks=(TextBlock.from_folder(path, vocab=dls_lm.vocab), CategoryBlock),
get_y=parent_label,
get_items=partial(get_text_files, folders=['train', 'test']),
splitter=GrandparentSplitter(valid_name='test')
).dataloaders(path, path=path, bs=128, seq_len=72)
learn = text_classifier_learner(dls_clas, AWD_LSTM, drop_mult=0.5, metrics=accuracy).to_fp16()
learn = learn.load_encoder('finetuned')
Notá que el vocab del language model se pasa explícitamente al DataBlock del
clasificador — imprescindible para que los embeddings ya afinados sigan correspondiendo a los mismos
índices. Resultado del libro con este código: 94,3% de accuracy en IMDb, y 95,1% promediando con un
segundo modelo entrenado sobre el texto leído al revés (un ensemble de dos, no un modelo bidireccional);
el paper de 2018 reportaba un error de 4,6% en IMDb, estado del arte en ese momento.
Derivación Matemática de la Perplejidad (Perplexity)
En modelos de lenguaje autoregresivos, la métrica de accuracy (predecir con exactitud el siguiente token) resulta engañosa: un accuracy de 35% puede ser extraordinario en un vocabulario de 60.000 palabras, mientras que en un contexto predecible un 50% puede ser mediocre. La métrica canónica es la Perplejidad ().
Consideremos una secuencia de tokens . Por la regla de la cadena de probabilidades, la probabilidad conjunta asignada por el modelo autoregresivo es:
La función de pérdida de entropía cruzada promedio por token () se define como el logaritmo negativo de verosimilitud:
La perplejidad se define matemáticamente como la exponencial de la pérdida de entropía cruzada:
Interpretación Física y Teórica de la Información:
- La perplejidad es la media geométrica inversa de la probabilidad asignada a cada token.
- Representa el factor de ramificación efectivo (branching factor) del lenguaje. Si un modelo tiene , significa que al generar el siguiente token, el sistema está en promedio tan perplejo/incierto como si tuviera que elegir uniformemente al azar entre 55 palabras igualmente probables.
- Si el modelo fuera totalmente ignorante y predijera una distribución uniforme sobre un vocabulario de , su perplejidad sería exactamente . Un modelo que predice la secuencia con certeza absoluta () tiene .
En cuanto al rendimiento computacional, to_fp16() activa mixed precision: las operaciones de multiplicación matricial en el forward y backward pass se ejecutan en FP16 sobre los Tensor Cores, duplicando el throughput y reduciendo el consumo de VRAM a la mitad, mientras se mantiene una copia maestra de los pesos en FP32 (con dynamic loss scaling para evitar que gradientes subnormales colapsen a cero por underflow). En hardware moderno (GPUs NVIDIA Ampere, Hopper o Blackwell), se reemplaza por to_bf16(), cuyo rango dinámico idéntico a FP32 elimina la necesidad de loss scaling.
El backbone es AWD-LSTM (ASGD Weight-Dropped LSTM; Merity, Keskar & Socher, 2017,
arXiv:1708.02182): una LSTM estándar con varias técnicas de regularización superpuestas (dropout en
distintos puntos — embeddings, pesos recurrentes, capas ocultas — más una variante de optimización
promediada) para evitar el sobreajuste típico de RNNs sobre texto. El drop_mult del código
escala todos esos dropouts a la vez.
4. Discriminative learning rates y gradual unfreezing
Al afinar el clasificador, fastai recomienda algo distinto a lo que hiciste en visión: no descongelar todo el modelo de una vez. La receta:
- Entrenar 1 época con solo la cabeza nueva descongelada.
learn.freeze_to(-2): descongelar los últimos 2 grupos de parámetros, con discriminative learning rates — un rango de LR donde las capas más profundas/tempranas reciben un learning rate más bajo y las capas finales uno más alto, porque las capas tempranas capturan features más generales que conviene perturbar menos.freeze_to(-3): un grupo más, LR aún más bajo.learn.unfreeze(): finalmente todo el modelo, con LR mínimo.
learn.fit_one_cycle(1, 2e-2) # solo la cabeza
learn.freeze_to(-2); learn.fit_one_cycle(1, slice(1e-2/(2.6**4), 1e-2))
learn.freeze_to(-3); learn.fit_one_cycle(1, slice(5e-3/(2.6**4), 5e-3))
learn.unfreeze(); learn.fit_one_cycle(2, slice(1e-3/(2.6**4), 1e-3))
Formulación Matemática de Discriminative Learning Rates
El slice(lr_min, lr_max) formaliza la hipótesis de que las distintas profundidades de una red neuronal capturan abstracciones con niveles de generalidad radicalmente dispares. Las capas iniciales (embeddings y primeras capas recurrentes o de atención) codifican invariantes léxicas y gramaticales universales; actualizarlas con un learning rate agresivo destruye estas representaciones (un fenómeno conocido como olvido catastrófico o catastrophic forgetting). En cambio, la cabeza de clasificación recién inicializada necesita pasos de optimización enérgicos para converger.
Howard & Ruder (2018) derivaron la regla de decaimiento geométrico para grupos de capas. Para una capa (donde representa la cabeza final y las capas tempranas):
donde es el multiplicador de atenuación inter-capa, fijado empíricamente en . Para un modelo particionado en grupos de parámetros, la tasa de aprendizaje asignada a las capas base es:
Slanted Triangular Learning Rates (STLR)
Junto con las tasas discriminativas, ULMFiT introdujo el cronograma STLR (Slanted Triangular Learning Rates), precursor del One-Cycle Policy de Leslie Smith:
- Fase de Warmup Inclinado (Cut fraction): Durante el primer 10% de las iteraciones (), la tasa de aprendizaje se incrementa linealmente desde hasta . Esto estabiliza los momentos de primer y segundo orden del optimizador cuando los gradientes iniciales son altamente oscilatorios.
- Fase de Annealing Largo: Durante el 90% restante del entrenamiento, el learning rate decae linealmente (o por coseno) hacia cero, permitiendo que el optimizador escape de mínimos locales agudos y se asiente en valles planos y generalizables.
Este descongelamiento gradual, capa por capa, es lo que el paper ULMFiT llama gradual unfreezing,
y el libro es explícito en que para NLP hace una diferencia real frente a descongelar todo de una — a
diferencia de visión, donde fine_tune descongela todo después de la primera época sin mayor
drama. La razón es de catastrophic forgetting: una RNN afinada agresivamente con una cabeza nueva
aleatoria puede destruir lo que el language model aprendió antes de que la cabeza se estabilice.
5. La versión web: similaridad de patentes con Transformers
La Lección 4 grabada en 2022 no usa nada de lo anterior. Su notebook, Getting started with NLP for
absolute beginners, resuelve la competencia de Kaggle U.S. Patent Phrase to Phrase
Matching: dado un par de frases cortas (anchor, target) y un código de
contexto de la clasificación de patentes, predecir un score de similaridad entre 0 y 1. Es una regresión
sobre texto, evaluada con correlación de Pearson entre el score predicho y el real. El
esqueleto completo, con transformers y datasets:
import numpy as np, pandas as pd
from datasets import Dataset, DatasetDict
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer)
df = pd.read_csv('train.csv')
# un solo string por fila, con separadores que el modelo pueda aprender
df['input'] = 'TEXT1: ' + df.context + '; TEXT2: ' + df.target + '; ANC1: ' + df.anchor
model_nm = 'microsoft/deberta-v3-small'
tokz = AutoTokenizer.from_pretrained(model_nm)
ds = Dataset.from_pandas(df).map(lambda x: tokz(x['input']), batched=True)
ds = ds.rename_columns({'score': 'labels'}) # Trainer busca la columna 'labels'
# split por grupos de anchor (ver el callout de abajo), no por fila
anchors = df.anchor.unique()
rng = np.random.default_rng(42); rng.shuffle(anchors)
val_anchors = set(anchors[:len(anchors)//4])
is_val = df.anchor.isin(val_anchors).values
dds = DatasetDict(train=ds.select(np.where(~is_val)[0]), test=ds.select(np.where(is_val)[0]))
def corr_d(eval_pred):
preds, labels = eval_pred
return {'pearson': np.corrcoef(preds.squeeze(), labels)[0, 1]}
args = TrainingArguments('outputs', learning_rate=8e-5, warmup_ratio=0.1,
lr_scheduler_type='cosine', bf16=True, eval_strategy='epoch',
per_device_train_batch_size=128, per_device_eval_batch_size=256,
num_train_epochs=4, weight_decay=0.01, report_to='none')
model = AutoModelForSequenceClassification.from_pretrained(model_nm, num_labels=1) # 1 salida = regresión
trainer = Trainer(model, args, train_dataset=dds['train'], eval_dataset=dds['test'],
processing_class=tokz, compute_metrics=corr_d)
trainer.train()
Cada pieza la viste en el Capítulo 3 de
HF, así que acá solo lo que es específico de este problema. num_labels=1 convierte la
cabeza de clasificación en una regresión (un solo logit, loss MSE). El "prompt" con TEXT1:,
TEXT2:, ANC1: es la forma más simple de meter tres campos en una sola secuencia;
el modelo aprende qué significan los separadores. deberta-v3-small es la elección del
notebook por ser chico y bueno para tareas de este tipo; y los nombres eval_strategy y
processing_class son los de versiones recientes de transformers (antes se
llamaban evaluation_strategy y tokenizer; si tu versión se queja, usá los
viejos).
⚠️ Cuidado: el split por fila filtra información
El notebook original hace train_test_split(0.25) por fila, y Jeremy lo señala como una
simplificación con un problema real: cada anchor aparece en muchas filas (con distintos
target), así que con un split por fila el mismo anchor queda en train y en validación. El
modelo puede "recordar" cómo se comporta ese anchor, y la métrica de validación sale más alta de lo que
va a salir en el test set de Kaggle, donde los anchors son otros. Es el mismo problema que un split
aleatorio de pacientes con varias radiografías cada uno, o de reviews de un mismo usuario: cuando hay
grupos, el split se hace por grupo. El código de arriba separa por anchor por eso —
equivalente a GroupShuffleSplit de scikit-learn.
Lo que este notebook comparte con el capítulo del libro, y que es el punto pedagógico: en ambos casos hay
un modelo preentrenado de lenguaje, un paso de tokenización que tenés que entender (las reglas
xx* antes, el tokenizer de DeBERTa ahora), un cuerpo reusado y una cabeza nueva, y un
validation set que hay que diseñar con cuidado. Cambió la arquitectura; no cambió la estructura.
6. De ULMFiT a los LLMs Modernos: La Matriz de Correspondencia Técnica
Nadie entrena un AWD-LSTM en 2026 para un clasificador de texto nuevo: un modelo de atención densa tipo BERT o un LLM moderno le gana ampliamente en capacidad de representación y paralelismo en GPU. Sin embargo, la topología de entrenamiento en 3 etapas de ULMFiT es exactamente la misma que gobierna los pipelines modernos de LLMs.
| Fase de Ingeniería | Paradigma ULMFiT (Howard & Ruder, 2018) | Paradigma LLM Moderno (2026) |
|---|---|---|
| Fase 1: Preentrenamiento Base | AWD-LSTM autoregresivo entrenado en WikiText-103 (Wikipedia en inglés, ~100M tokens). Objetivo: Predecir la siguiente palabra. | Transformer Decoder-only (Llama-3, Gemma-4, Mistral) preentrenado en 15+ billones de tokens web. Objetivo: Next-token prediction autoregresivo. |
| Fase 2: Adaptación de Dominio | Language Model fine-tuning sobre el corpus específico no etiquetado (ej. todas las reviews de IMDb). Minimiza Perplejidad en el dominio. | Continued Pre-Training (CPT) sobre corpus corporativo, legal, médico o filosófico crudo (sin etiquetas de instrucción). Adapta el vocabulario latente. |
| Fase 3: Especialización en Tarea | Reemplazo de cabeza de LM por clasificador densamente conectado + Gradual Unfreezing sobre los grupos de capas del backbone. | Supervised Fine-Tuning (SFT) con prompts de instrucciones + LoRA / PEFT (matrices de bajo rango ) para preservar los pesos base intactos. |
| Control de Olvido Catastrófico | Gradual Unfreezing secuencial (freeze_to(-2), freeze_to(-3)) y tasas de aprendizaje discriminativas (). |
Congelamiento absoluto del 100% de pesos preentrenados; inyección de adaptadores de bajo rango () que garantiza matemáticamente perturbación cero en . |
🔧 Es el mismo patrón que ya corre en tus proyectos
Preentrenar en Wikipedia → afinar en el dominio (reviews de IMDb) → afinar en la tarea final (clasificación) es, estructuralmente, el mismo ciclo CPT + SFT (Continued Pre-Training + Supervised Fine-Tuning) que corre en interpretante-lacaniano: el modelo base Gemma-4 ya viene preentrenado en un corpus genérico; el CPT lo adapta al "dominio" (textos lacanianos/freudianos, el estilo del corpus curado); y el SFT final lo afina a la tarea concreta (responder en el estilo del intérprete). ULMFiT en 2018 y tu pipeline en 2026 resuelven el mismo problema estructural — que un modelo genérico no rinde bien de una en un dominio muy específico sin un paso intermedio de adaptación — con arquitecturas completamente distintas (LSTM vs. transformer) pero la misma lógica de tres etapas.
Y en cuanto a discriminative learning rates y gradual unfreezing: la idea de "las capas tempranas cambian menos, las capas finales cambian más" es precisamente la intuición detrás de por qué LoRA (que viste en el Capítulo 11 de HF) suele aplicarse selectivamente sobre ciertas proyecciones de atención y no sobre el modelo entero — no es una técnica nueva, es la misma pregunta ("¿qué tanto debería moverse cada parte del modelo pretrained?") resuelta con una herramienta distinta. Y la perplexity que medías en el LM de IMDb es la misma que hoy comparás antes y después de un CPT para saber si el modelo "absorbió" el dominio.
7. Errores comunes
Los errores de esta lección son en su mayoría de diseño del experimento, no de código — y por eso se ven recién en producción, cuando la métrica de validación no se cumple.
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| El clasificador entrena bien pero queda varios puntos por debajo de lo esperado, sobre todo en textos con jerga del dominio. | Te salteaste la etapa 2 (fine-tuning del LM en el dominio) y fuiste directo de Wikipedia al clasificador. | Afiná primero el language model sobre todo el texto del dominio, incluido el que no tiene etiqueta (es gratis: la etiqueta es la próxima palabra). Recién después save_encoder / load_encoder. |
| Accuracy del clasificador cerca del azar, o peor que entrenar desde cero, pese a cargar el encoder afinado. | No pasaste vocab=dls_lm.vocab al TextBlock del clasificador: se construyó un vocabulario nuevo, y el índice 1.234 ya no es la misma palabra que en el LM. Los embeddings cargados apuntan a palabras equivocadas. | Siempre el mismo vocab entre LM y clasificador. En Transformers, el equivalente es usar el mismo AutoTokenizer del checkpoint. |
| La accuracy de validación empeora en la primera época de fine-tuning del clasificador y después se recupera a medias. | Descongelaste todo de golpe con la cabeza nueva todavía aleatoria: los gradientes ruidosos de la cabeza pasaron al encoder y destruyeron parte de lo aprendido (catastrophic forgetting). | Gradual unfreezing (§4): cabeza sola → freeze_to(-2) → freeze_to(-3) → unfreeze(), con discriminative LRs. |
Métricas excelentes en validación, mucho peores con datos nuevos, y muchos xxunk en inferencia. | El texto de test se tokenizó con reglas distintas (otro tokenizer, sin las reglas xx*, otro lowercasing) o con otro vocabulario. | El mismo pipeline de tokenización + numericalización para train, valid y test. Con fastai, exportá el learner (incluye las transforms); con HF, guardá tokenizer y modelo juntos. |
| Pearson de 0,85 en validación; 0,78 en el leaderboard de Kaggle. | Data leakage por split por fila cuando hay grupos: los mismos anchors están en train y en validación (§5). La validación no mide lo que el test mide. | Split por grupo (anchor, paciente, usuario, documento). Si la métrica de validación baja al hacerlo, no es que el modelo empeoró: es que antes estaba mintiendo. |
to_fp16() produce nan en el loss. | Overflow en float16 (rango máximo ~65.504) en alguna activación o gradiente, sin loss scaling suficiente. | En GPUs Ampere o más nuevas, to_bf16(); si no, bajá la LR o dejá el modelo en float32 y aceptá la lentitud. |
8. Resumen
- Esta lección es un caso real de un curso que se actualizó más rápido que su propio libro: la web ya usa Transformers (patentes con DeBERTa +
Trainer), el libro sigue enseñando RNN/ULMFiT. - La tokenización de fastai preserva información (mayúsculas, énfasis) con tokens especiales
xx*en vez de descartarla — un enfoque basado en reglas, contra el enfoque estadístico de BPE que ya conocés. El vocabulario que sale de ahí tiene que ser el mismo en el LM y en el clasificador. - ULMFiT (Howard & Ruder, 2018) formalizó el transfer learning en NLP en tres etapas: LM genérico → LM adaptado al dominio → clasificador — el paso intermedio es el que la mayoría se salta y no debería. Perplexity (e^loss) es la métrica del LM;
to_fp16()/to_bf16()es mixed precision. - Discriminative learning rates + gradual unfreezing: capas tempranas se mueven poco, capas finales se mueven más — la misma intuición que hoy motiva dónde aplicar LoRA.
- Cuando los datos tienen grupos (anchors, pacientes, usuarios), el split se hace por grupo; un split por fila infla la métrica de validación y se descubre recién en el test.
- La arquitectura (LSTM) es historia; la estructura de tres etapas sigue viva en cualquier pipeline moderno de CPT + SFT.
9. Ponete a prueba
Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.
1. ¿Qué información guarda xxmaj, y por qué es mejor que dejar la palabra con mayúscula como token aparte?
Marca que la palabra siguiente empezaba con mayúscula. Como token separado, el modelo aprende "capitalización" una sola vez (un embedding) en vez de necesitar un embedding distinto para "the" y "The" — el vocabulario queda más chico y cada palabra recibe todo su conteo de apariciones. (§2)
2. ¿Por qué el paso 2 de ULMFiT puede usar las reviews de IMDb que no tienen etiqueta de sentimiento?
Porque la tarea del language model es predecir la próxima palabra: la "etiqueta" es el propio texto (self-supervised). Toda review, etiquetada o no, sirve para adaptar el LM al registro del dominio. (§3)
3. Un LM tiene loss 3.0 por token en validación. ¿Cuál es su perplexity y cómo se lee?
e³ ≈ 20: en promedio, el modelo "duda" entre unas 20 palabras igualmente plausibles en cada posición. Bajar la loss a 2.5 (perplexity ≈ 12) es una mejora grande aunque la accuracy de próxima palabra apenas se mueva. (§3)
4. ¿Qué pasa si armás dls_clas sin vocab=dls_lm.vocab y cargás el encoder afinado?
El clasificador construye un vocabulario nuevo con otros índices; la matriz de embeddings cargada del LM asigna a cada índice el vector de una palabra distinta. El encoder "afinado" pasa a ser ruido: accuracy cerca del azar o peor que sin transfer learning. (§3, §7)
5. ¿Qué significa slice(1e-2/(2.6**4), 1e-2) en fit_one_cycle?
Discriminative learning rates: el grupo de capas más temprano usa 1e-2/46 ≈ 2e-4, la cabeza usa 1e-2, y los grupos intermedios se reparten geométricamente entre ambos. El factor 2,6 por grupo viene del paper ULMFiT. (§4)
6. En el dataset de patentes, ¿por qué un split aleatorio por fila da un Pearson de validación demasiado optimista?
Cada anchor aparece en muchas filas; con split por fila, el mismo anchor cae en train y en validación, y el modelo puede memorizar cómo se puntúa ese anchor. El test de Kaggle tiene anchors nuevos, así que la validación no representa el test. Se resuelve separando por grupo (por anchor). (§5)
10. Para practicar
✏️ Ejercicio 1 — Ver las reglas de tokenización de fastai
Corre en CPU y en segundos (fastai instala spaCy como dependencia). Probá el ejemplo de §2 y después
tres textos propios: uno todo en mayúsculas, uno con una URL, uno con emojis. Contá cuántos tokens
xx* aparecen y qué información preserva cada uno.
from fastai.text.all import *
tkn = Tokenizer(WordTokenizer())
for txt in ["The U.S. dollar $1 is $1.00. THIS is a test!!!!",
"ESTO ES UN GRITO", "Mirá https://course.fast.ai ahora"]:
print(tkn(txt))
Pista / qué esperar
xxbos al inicio de cada uno; xxup antes de cada palabra en mayúsculas; xxrep 4 ! para los cuatro signos. Notá que los tokens en sí quedan en minúscula: la información de mayúsculas se movió a los marcadores.
✏️ Ejercicio 2 — Medir el leakage de un split por fila
Sin entrenar nada, con pandas solo. Descargá train.csv de la competencia de patentes
(o generá un dataframe sintético con la misma estructura, como abajo) y compará cuántos anchors de
validación aparecen también en train con un split por fila vs. uno por grupo.
import numpy as np, pandas as pd
rng = np.random.default_rng(0)
anchors = [f'anchor_{i}' for i in range(700)]
df = pd.DataFrame({'anchor': rng.choice(anchors, 36000),
'target': [f'target_{i}' for i in range(36000)],
'score': rng.choice([0, .25, .5, .75, 1], 36000)})
# split por fila
is_val_row = rng.random(len(df)) < 0.25
leak_row = df[is_val_row].anchor.isin(df[~is_val_row].anchor).mean()
# split por grupo
val_anchors = set(rng.permutation(df.anchor.unique())[:175])
is_val_grp = df.anchor.isin(val_anchors).values
leak_grp = df[is_val_grp].anchor.isin(df[~is_val_grp].anchor).mean()
print(f'anchors de valid que también están en train — por fila: {leak_row:.0%}, por grupo: {leak_grp:.0%}')
Pista / qué esperar
Por fila: cerca del 100% (casi todos los anchors de validación también están en train). Por grupo: 0%. Con el train.csv real el resultado es el mismo; son ~36.000 filas y ~730 anchors únicos.
✏️ Ejercicio 3 — Comparar los dos tokenizers sobre la misma frase
Tokenizá una frase de patente con el tokenizer de DeBERTa y con el de fastai, y compará. Requiere
pip install transformers sentencepiece; el tokenizer se descarga una vez (unos MB) y corre
en CPU.
from transformers import AutoTokenizer
tokz = AutoTokenizer.from_pretrained('microsoft/deberta-v3-small')
s = 'TEXT1: A47; TEXT2: abatement of pollution; ANC1: abatement'
print(tokz.tokenize(s))
print(tokz(s)['input_ids'][:10], tokz.cls_token, tokz.sep_token)
Pista / qué esperar
Subpalabras con el marcador ▁ de SentencePiece (por ejemplo ▁abate, ment), sin ningún xxmaj: la capitalización queda dentro de las piezas. Los ids empiezan con el [CLS] y terminan con [SEP] — el equivalente moderno de xxbos.
11. Fuentes y para seguir
- Practical Deep Learning for Coders — Lesson 4: Natural Language (NLP) — la lección en video (2022) que reemplaza el capítulo del libro por Transformers y la competencia de patentes.
- Getting started with NLP for absolute beginners — el notebook de Kaggle de la lección: DeBERTa-v3-small,
Trainer, Pearson, y la discusión sobre cómo armar el validation set. - fastbook — 10_nlp.ipynb — el Capítulo 10 como notebook: tokenización, numericalización, el LM de IMDb y el clasificador con gradual unfreezing.
- Universal Language Model Fine-tuning for Text Classification (Howard & Ruder, 2018) — el paper ULMFiT: las tres etapas, discriminative fine-tuning, slanted triangular LRs y gradual unfreezing, con los ablation studies que justifican cada uno.
- Regularizing and Optimizing LSTM Language Models (Merity, Keskar & Socher, 2017) — AWD-LSTM: el backbone del capítulo y de dónde salen todos los dropouts que
drop_multescala. - Hugging Face LLM Course — Chapter 3: Fine-tuning a pretrained model — la referencia completa de
AutoModelForSequenceClassification,TraineryTrainingArgumentsque §5 da por sabida.