fast.ai · Practical Deep Learning for Coders · Lección 5
Modelo desde cero, otra vez: por qué repetirlo con datos tabulares
Esta lección no enseña conceptos nuevos de SGD — los repite, a propósito, sobre un dominio distinto (datos tabulares del Titanic en vez de imágenes de MNIST) para consolidar que el mecanismo es genuinamente el mismo en cualquier tipo de dato. Y cierra con el argumento más pragmático del curso: por qué, aun sabiendo programarlo a mano, conviene usar un framework.
Antes de leerlección 3, SGD y una red a mano
🎯 Al terminar vas a poder
- Convertir un dataframe con columnas heterogéneas (categóricas, continuas, faltantes) en un tensor listo para
indeps@coeffs, con pandas:fillna,np.log,get_dummies, normalización. - Escribir de cero un modelo lineal, una red de dos capas y una de varias capas sobre el Titanic, con
calc_preds,calc_loss,one_epochytrain_model, y explicar por qué la sigmoid al final fue la mejora clave. - Reproducir el mismo modelo con
TabularPandas+tabular_learneren diez líneas, y promediar cinco modelos (ensembling) para ganar estabilidad. - Diagnosticar los errores típicos de datos tabulares: categóricas como números arbitrarios, columnas sin normalizar, booleanos como
object, evaluar sobre train.
1. El mismo proceso, otro dominio: de MNIST a Titanic
En la Lección 3 armaste, a mano, un modelo lineal y después una red de dos capas para clasificar dígitos. Acá el ejercicio se repite con el dataset del Titanic de Kaggle (predecir quién sobrevivió al hundimiento, a partir de clase de pasaje, sexo, edad, tarifa pagada, cantidad de familiares a bordo) — deliberadamente, para que quede claro que no hay nada específico de imágenes en SGD: los mismos siete pasos (predecir, medir loss, calcular gradiente, dar un step) aplican sin cambios a filas de una tabla.
Lo único que cambia entre MNIST y Titanic es el preprocesamiento de la entrada — cómo se
convierte cada fila en un vector de números que un modelo lineal pueda multiplicar. El material de la
lección son dos notebooks de Kaggle de Jeremy Howard: Linear model and neural net from scratch
(todo a mano, con pandas y PyTorch pelado) y Why you should use a framework (lo mismo con fastai).
Esta página sigue los dos, en ese orden. El dataset (train.csv, 891 pasajeros, 12 columnas)
se baja de la competencia "Titanic - Machine Learning from Disaster" de Kaggle.
2. Preprocesar datos tabulares para un modelo lineal
A diferencia de una imagen (ya es una grilla de números), una tabla mezcla tipos de dato heterogéneos que
hay que normalizar a una representación numérica uniforme antes de poder aplicar xb@weights +
bias:
- Missing values (edad faltante en 177 de los 891 pasajeros, cabina en la mayoría): se rellenan con un valor razonable. El notebook usa la moda de cada columna, la opción más simple que funciona para numéricas y categóricas a la vez; fastai usa la mediana por defecto y, cuando importa no perder esa información, agrega una columna booleana extra marcando qué filas tenían el dato faltante.
- Variables con distribución muy sesgada (la tarifa: la mayoría pagó poco, unos pocos pagaron muchísimo): se les aplica un logaritmo para que la cola larga no domine.
np.log(Fare + 1)— el+1es para que la tarifa 0 no dé-inf. - Variables categóricas (sexo, clase de pasaje, puerto de embarque): se convierten a columnas dummy/one-hot — una columna binaria por categoría posible, en vez de un único número arbitrario (asignar 0/1/2 a categorías sin orden real induciría una relación numérica falsa entre ellas: "Cherbourg es la mitad de Southampton").
- Variables continuas (edad, tarifa, familiares): se normalizan para que ninguna columna domine el gradiente solo por tener una escala numérica más grande. El notebook divide cada columna por su máximo (queda en 0–1); fastai resta la media y divide por el desvío estándar. Cualquiera de las dos sirve; lo que no sirve es no hacerlo.
El código, tal como está en el notebook (con un ajuste para pandas 2.x, ver el comentario):
import numpy as np, pandas as pd, torch
from torch import tensor
df = pd.read_csv('train.csv')
df.isna().sum() # Age 177, Cabin 687, Embarked 2
modes = df.mode().iloc[0] # la moda de cada columna
df.fillna(modes, inplace=True)
df['LogFare'] = np.log(df['Fare'] + 1)
df = pd.get_dummies(df, columns=['Sex', 'Pclass', 'Embarked'])
added_cols = ['Sex_male', 'Sex_female', 'Pclass_1', 'Pclass_2', 'Pclass_3',
'Embarked_C', 'Embarked_Q', 'Embarked_S']
indep_cols = ['Age', 'SibSp', 'Parch', 'LogFare'] + added_cols
t_dep = tensor(df.Survived)
# pandas 2.x crea las dummies como bool; el .astype(float) evita un array 'object' que tensor() rechaza
t_indep = tensor(df[indep_cols].astype(float).values, dtype=torch.float)
t_indep.shape # torch.Size([891, 12])
vals, indices = t_indep.max(dim=0)
t_indep = t_indep / vals # cada columna dividida por su máximo → 0..1
Fijate por qué la normalización es obligatoria acá y no en MNIST: en MNIST todas las columnas eran
píxeles en 0–1. En el Titanic, Age llega a 80, SibSp a 8 y las dummies son 0 o
1. Sin dividir por el máximo, el producto indeps*coeffs está dominado por la edad y la tarifa,
el gradiente respecto de esos coeficientes es enorme comparado con el resto, y un learning rate que
funciona para uno diverge para los otros. Es exactamente el mismo problema que "no dividir por 255" en la
Lección 3, con otro disfraz.
3. El modelo lineal a mano: coeficientes, loss, sigmoid
Con la tabla convertida en un tensor [891, 12], el modelo lineal es el de siempre: un
coeficiente por columna. El notebook no pone bias explícito porque las dummies ya lo cubren (para cada fila,
exactamente una de Sex_male/Sex_female vale 1 — la suma de sus coeficientes actúa
como constante). La loss es el error absoluto medio, más simple que MSE y perfectamente
diferenciable donde importa:
torch.manual_seed(442)
n_coeff = t_indep.shape[1]
coeffs = torch.rand(n_coeff) - 0.5 # 12 coeficientes entre -0.5 y 0.5
def calc_preds(coeffs, indeps): return (indeps*coeffs).sum(axis=1)
def calc_loss(coeffs, indeps, deps): return torch.abs(calc_preds(coeffs, indeps) - deps).mean()
# un paso de gradiente, a mano
coeffs.requires_grad_()
loss = calc_loss(coeffs, t_indep, t_dep)
loss.backward()
with torch.no_grad():
coeffs.sub_(coeffs.grad * 0.1)
coeffs.grad.zero_()
print(calc_loss(coeffs, t_indep, t_dep)) # bajó
(indeps*coeffs).sum(axis=1) es el producto matriz-vector escrito con broadcasting: multiplica
cada fila por los coeficientes elemento a elemento y suma. Es lo mismo que indeps@coeffs, y el
notebook lo escribe así primero para que se vea qué hace; después lo cambia por @. Ahora el
split y el loop de entrenamiento, que son apply_step de la Lección 3 con otros nombres:
from fastai.data.transforms import RandomSplitter
trn_split, val_split = RandomSplitter(seed=42)(df) # 80/20 de índices
trn_indep, val_indep = t_indep[trn_split], t_indep[val_split]
trn_dep, val_dep = t_dep[trn_split], t_dep[val_split]
def init_coeffs(): return (torch.rand(n_coeff) - 0.5).requires_grad_()
def update_coeffs(coeffs, lr):
coeffs.sub_(coeffs.grad * lr)
coeffs.grad.zero_()
def one_epoch(coeffs, lr):
loss = calc_loss(coeffs, trn_indep, trn_dep) # todo el train set de una: 712 filas, no hace falta batch
loss.backward()
with torch.no_grad(): update_coeffs(coeffs, lr)
print(f"{loss:.3f}", end="; ")
def train_model(epochs=30, lr=0.01):
torch.manual_seed(442)
coeffs = init_coeffs()
for i in range(epochs): one_epoch(coeffs, lr=lr)
return coeffs
def acc(coeffs): return (val_dep.bool() == (calc_preds(coeffs, val_indep) > 0.5)).float().mean()
coeffs = train_model(18, lr=0.2)
acc(coeffs)
0.536; 0.502; 0.477; 0.454; 0.431; … 0.301; acc ≈ 0.79
Alrededor de 79% de accuracy en validación con un modelo lineal — ya mejor que "todos mueren" (~62%) y que
"sobreviven las mujeres" (~78%). Pero mirá las predicciones: calc_preds devuelve cualquier
número real. Para un pasajero de primera clase, mujer, joven, puede dar 1.4; para otro, -0.3. La loss
castiga esos excesos (|1.4 − 1| = 0.4 de error en una predicción que en realidad es correcta), y el
modelo gasta capacidad en no pasarse de 1 en vez de en separar mejor a los pasajeros.
Justificación Matemática y Dinámica del Gradiente con Sigmoid
def calc_preds(coeffs, indeps): return torch.sigmoid((indeps*coeffs).sum(axis=1))
coeffs = train_model(lr=100) # sí, 100: la derivada de la sigmoid aplasta los gradientes
acc(coeffs)
acc ≈ 0.83Cambiar una única línea incrementa la accuracy de 79% a 83%. Pero ¿cuál es el fundamento matemático exacto de este salto cualitativo?
Consideremos el modelo lineal sin sigmoid: produce una combinación no acotada (un logit crudo) . Supongamos que evaluamos bajo la función de pérdida de error absoluto medio (MAE) .
Analicemos el caso de un pasajero que efectivamente sobrevivió ():
- Supongamos que el modelo realiza una predicción correcta y con alta certeza: .
- La pérdida lineal incurre en un error espurio: .
- El gradiente respecto a la salida del modelo es:
- La actualización de pesos mediante SGD aplica:
La patología del modelo lineal: A pesar de haber clasificado correctamente la instancia como superviviente (), el gradiente penaliza al modelo forzando a los pesos a reducir la predicción de vuelta hacia 1.0. La optimización dilapida grados de libertad intentando encajar una salida continua exactamente en los valores discretos , en lugar de concentrarse en separar los casos difíciles y ambiguos.
Al introducir la función logística Sigmoid :
Cuando y la predicción es asertiva (), y la derivada , por lo que . El modelo no recibe penalización alguna por emitir logits arbitrariamente altos para casos indudables.
¿Por qué el learning rate salta de 0.2 a 100? La derivada máxima de la función sigmoid se alcanza en y vale exactamente . Para valores más alejados del umbral, decrece exponencialmente. Para mantener una magnitud de actualización suficiente en los pesos (), la tasa de aprendizaje debe amplificarse drásticamente.
Un último cambio de forma antes de la red: pasar de (indeps*coeffs).sum(axis=1) a
indeps@coeffs con coeffs de shape [12, 1], y los targets a
[n, 1] con trn_dep[:, None]. Es solo para que el producto matricial escale a
varias capas; los números no cambian.
4. De lineal a red de dos capas, y a "deep learning"
La red de dos capas es simple_net de la Lección 3 con otros nombres: una matriz
[12, n_hidden], ReLU, una matriz [n_hidden, 1], un bias y la sigmoid final. El
notebook mantiene train_model y one_epoch sin tocarlos — solo
redefine las tres funciones que saben cómo son los coeficientes:
import torch.nn.functional as F
def init_coeffs(n_hidden=20):
layer1 = (torch.rand(n_coeff, n_hidden) - 0.5) / n_hidden
layer2 = torch.rand(n_hidden, 1) - 0.3
const = torch.rand(1)[0]
return layer1.requires_grad_(), layer2.requires_grad_(), const.requires_grad_()
def calc_preds(coeffs, indeps):
l1, l2, const = coeffs
res = F.relu(indeps@l1)
res = res@l2 + const
return torch.sigmoid(res)
def update_coeffs(coeffs, lr):
for layer in coeffs: layer.sub_(layer.grad * lr)
for layer in coeffs: layer.grad.zero_()
coeffs = train_model(lr=20)
acc(coeffs)
Las constantes raras de init_coeffs (/n_hidden, -0.3) son
inicialización a ojo para que las activaciones arranquen en un rango razonable — el notebook admite que
las eligió probando, y ese es justamente uno de los argumentos de §5 para usar un framework: la
inicialización correcta (Kaiming/He para ReLU) ya está estudiada y no hace falta adivinarla.
"Deep learning" es lo mismo con una lista de capas en vez de dos. La versión del notebook parametriza cuántas capas ocultas y de qué tamaño:
def init_coeffs():
hiddens = [10, 10] # tamaño de cada capa oculta; probá [20], [10,10,10]…
sizes = [n_coeff] + hiddens + [1]
n = len(sizes)
layers = [(torch.rand(sizes[i], sizes[i+1]) - 0.3) / sizes[i+1] * 4 for i in range(n-1)]
consts = [(torch.rand(1)[0] - 0.5) * 0.1 for i in range(n-1)]
for l in layers + consts: l.requires_grad_()
return layers, consts
def calc_preds(coeffs, indeps):
layers, consts = coeffs
n = len(layers)
res = indeps
for i, l in enumerate(layers):
res = res@l + consts[i]
if i != n-1: res = F.relu(res) # ReLU entre capas, no después de la última
return torch.sigmoid(res)
def update_coeffs(coeffs, lr):
layers, consts = coeffs
for layer in layers + consts: layer.sub_(layer.grad * lr)
for layer in layers + consts: layer.grad.zero_()
coeffs = train_model(lr=4)
acc(coeffs)
Y acá viene la parte honesta del notebook: con 891 filas y 12 columnas, la red de dos capas y la "profunda" dan una accuracy de validación prácticamente igual a la del modelo lineal con sigmoid, alrededor de 82–83%. No es que las redes no sirvan — es que un dataset tan chico no tiene información suficiente para que la capacidad extra se traduzca en algo, y la varianza entre corridas (178 filas de validación: cada pasajero es más de medio punto) es mayor que la diferencia entre modelos. Ese es el argumento para el ensembling de §5 y para los árboles de §6.
5. Por qué usar un framework (y qué esconde)
Habiendo ya escrito linear1, el loop de gradientes y simple_net a mano dos veces
(MNIST y Titanic), el curso hace explícito el argumento pedagógico completo: entender el mecanismo desde
cero te da la capacidad de diagnosticar cuando algo sale mal — pero escribirlo a mano en cada
proyecto real es un desperdicio de tiempo y una fuente de bugs sutiles. Un framework como fastai o PyTorch
de alto nivel no reemplaza el conocimiento de qué está pasando por dentro — automatiza exactamente eso que
ya escribiste, y agrega detalles de estabilidad numérica y performance que no vale la pena reinventar cada
vez: inicialización cuidadosa de pesos, manejo de NaN/overflow, batching eficiente en GPU,
schedulers de learning rate.
El segundo notebook de la lección, Why you should use a framework, hace exactamente lo de §2–§4
con fastai. Todo el preprocesamiento son tres procs, y la red es una línea:
from fastai.tabular.all import *
df = pd.read_csv('train.csv')
df['LogFare'] = np.log1p(df['Fare']) # log(x+1), la misma transformación de §2
df['Family'] = df.SibSp + df.Parch # feature engineering, lo único que el framework no hace
splits = RandomSplitter(seed=42)(df)
dls = TabularPandas(
df, splits=splits,
procs=[Categorify, FillMissing, Normalize], # one-hot/embeddings, imputación + columna _na, (x-media)/std
cat_names=['Sex', 'Pclass', 'Embarked'],
cont_names=['Age', 'SibSp', 'Parch', 'LogFare', 'Family'],
y_names='Survived', y_block=CategoryBlock(),
).dataloaders(path='.')
learn = tabular_learner(dls, metrics=accuracy, layers=[10, 10]) # las mismas dos capas ocultas de §4
learn.lr_find(suggest_funcs=(slide, valley))
learn.fit(16, lr=0.03)
Entity Embeddings para Variables Categóricas (Guo & Berkhahn, 2016)
El preprocesador Categorify y tabular_learner reemplazan la codificación One-Hot por Entity Embeddings.
En la codificación One-Hot, cada categoría de cardinalidad se representa como un vector ortogonal en la base estándar de . La distancia euclidiana entre cualquier par de categorías distintas es idéntica:
Esto impone un sesgo nulo: el modelo asume que primera clase y segunda clase son tan disímiles como primera clase y sexo masculino. Los Entity Embeddings proyectan cada categoría en un espacio vectorial continuo de dimensión reducida () mediante una matriz de búsqueda .
Para determinar automáticamente la dimensión óptima de embedding , fastai implementa una regla empírica no lineal:
Esta fórmula otorga suficientes grados de libertad para cardinalidades bajas sin caer en la maldición de la dimensionalidad para categorías masivas:
- Para (puerto de embarque o clase): .
- Para (categorías comerciales): .
- Para (códigos postales o IDs de usuario): .
Durante el entrenamiento, el gradiente propaga actualizaciones hacia . Categorías con comportamientos predictivos afines convergen hacia cúmulos cercanos en el espacio latente, permitiendo que la red generalice inferencias hacia categorías con pocas observaciones en el conjunto de entrenamiento.
La mejora final: promediar cinco modelos
Como cada entrenamiento arranca de pesos aleatorios distintos y el dataset es chico, dos corridas del mismo modelo pueden diferir en uno o dos puntos de accuracy. El notebook cierra con el truco más barato que existe para bajar esa varianza: entrenar cinco veces y promediar las probabilidades predichas (un ensemble).
def ensemble():
learn = tabular_learner(dls, metrics=accuracy, layers=[10, 10])
with learn.no_bar(), learn.no_logging(): learn.fit(16, lr=0.03)
return learn.get_preds()[0] # probabilidades sobre validación, shape [178, 2]
learns = [ensemble() for _ in range(5)]
ens_preds = torch.stack(learns).mean(0) # promedio de las 5 corridas
(ens_preds.argmax(dim=1) == tensor(df.Survived.values)[splits[1]]).float().mean()
Cada modelo individual se equivoca en pasajeros distintos por su inicialización; el promedio cancela parte de ese ruido. En el notebook, el ensemble sube la accuracy de validación y, más importante, la hace estable entre corridas. Es la misma idea que los random forests de §6 (promediar muchos árboles), aplicada a redes.
📜 Una analogía en el espíritu del curso
Saber cómo funciona SGD por dentro y aun así usar Learner.fit() de fastai es como saber
cómo funciona un motor de combustión y aun así manejar un auto con caja automática: el conocimiento no
se desperdicia — te permite reconocer cuándo algo anda raro (el motor suena distinto, el loss no baja
como debería) que alguien que solo sabe "girar la llave" no puede diagnosticar.
🔧 En tu proyecto
Tu gate anti-alucinación para artículos financieros es, en el fondo, un clasificador binario sobre features tabulares (scores, longitudes, conteos de entidades) — y todo lo de esta lección le aplica: normalizar las features antes de cualquier capa lineal, terminar en sigmoid para que el umbral sea una probabilidad, y promediar varias corridas si la decisión de bloquear un artículo no puede depender de la semilla con la que entrenaste.
6. Redes Neuronales Tabulares vs. Gradient Boosted Trees (GBDTs)
Para datos tabulares heterogéneos, existe una tensión arquitectónica constante entre dos familias con sesgos inductivos (inductive biases) opuestos: las Redes Neuronales y los Árboles de Decisión Ensemblados (Random Forests y GBDTs: XGBoost, LightGBM, CatBoost).
| Propiedad | Gradient Boosted Decision Trees (GBDTs) | Redes Neuronales Tabulares (MLP / TabNet) |
|---|---|---|
| Geometría de Separación | Particiones ortogonales alineadas a los ejes: funciones escalonadas constantes a trozos (). | Combinaciones lineales continuas seguidas de no linealidades suaves: hiperplanos rotados (). |
| Invarianza a Escalas | Completamente invariante: los splits solo dependen del ordenamiento ordinal. No requiere normalización ni log transforms. | Extremadamente sensible: variables con magnitudes grandes distorsionan el gradiente y requieren normalización estricta. |
| Variables Categóricas | Soportan target encoding o splits por partición de conjuntos discretos nativamente (CatBoost/LightGBM). | Exigen Entity Embeddings densos (), que destacan en cardinalidades masivas (). |
| Fusión Multimodal | Imposible de forma end-to-end; requiere extraer embeddings previamente como vectores estáticos. | Nativo y diferenciable: permite concatenar features tabulares con representaciones de LLMs (texto) y Vision Transformers (imágenes). |
Criterio de Selección de Ingeniería:
- Elegí GBDTs (XGBoost / LightGBM) cuando: El problema involucra datos tabulares puros, columnas numéricas con distribuciones multimodales o valores atípicos severos, y datasets medianos donde el costo de inferencia en CPU deba ser sub-milisegundo.
- Elegí Redes Tabulares (fastai / PyTorch) cuando: Existen variables categóricas de cardinalidad extrema (millones de usuarios o productos) donde los embeddings capturen similitud semántica latente, o cuando el sistema deba fusionar datos estructurados con texto no estructurado o embeddings de un LLM en un único forward pass end-to-end.
7. Errores comunes
En datos tabulares, casi todos los errores están en la tabla antes de que exista el modelo — y ninguno lanza una excepción.
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| El modelo lineal rinde peor de lo esperado y los coeficientes de una columna categórica no tienen sentido. | Codificaste la categórica como un número arbitrario (Embarked → 0/1/2, Pclass como 1/2/3 directo): el modelo asume que "Q" está entre "C" y "S", y que segunda clase es el promedio de primera y tercera. | pd.get_dummies (una columna por categoría) o, con fastai, Categorify + embeddings. Ojo con Pclass: parece número pero es categoría. |
| El loss baja lentísimo con lr chico y explota con lr grande; no hay lr que funcione. | Columnas sin normalizar: Age (hasta 80) y Fare (hasta 512) dominan el gradiente, y el lr que les sirve a ellas es demasiado grande para las dummies. | Dividir por el máximo (notebook) o Normalize (fastai), siempre con estadísticas de train. Log a las columnas con cola larga. |
TypeError: can't convert np.ndarray of type numpy.object_ al hacer tensor(df[cols].values). | pandas 2.x crea las dummies como bool; mezcladas con floats, .values devuelve un array object. | df[cols].astype(float).values, o pd.get_dummies(..., dtype=float). |
| Con una red de dos capas escrita a mano, la accuracy queda clavada por debajo del modelo lineal. | Olvidaste el bias (la constante) en alguna capa: sin él, la capa oculta no puede desplazar el punto donde ReLU corta, y muchas neuronas quedan siempre en cero. | Una constante por capa (el const/consts del notebook). nn.Linear lo trae por defecto. |
| La red profunda no converge con la lr que funcionaba para la red de dos capas (o al revés). | Cada arquitectura tiene su propio rango de lr útil — 0.2 para el lineal sin sigmoid, 100 con sigmoid, 20 para dos capas, 4 para tres: la escala de los gradientes cambia con cada capa que agregás. | No reutilices la lr entre modelos. Con fastai, lr_find cada vez que cambiás la arquitectura. |
| Accuracy de 90% "en validación" que nunca se reproduce en Kaggle. | Evaluaste sobre train (acc escrita con trn_indep), o normalizaste con estadísticas del dataset completo, o imputaste la edad con la moda calculada incluyendo validación. | Split primero, todo lo demás después, con estadísticas solo de train. TabularPandas lo hace bien si le pasás splits. |
| Dos corridas del mismo modelo difieren en 2 puntos y no sabés cuál es "mejor". | Varianza de inicialización sobre un validation set de 178 filas: cada pasajero vale 0,56 puntos. | Ensembling de varias corridas (§5) y, para comparar arquitecturas, validación cruzada o al menos varias semillas. |
8. Resumen
- SGD no tiene nada específico de imágenes — el mismo mecanismo de la Lección 3 aplica sin cambios a datos tabulares del Titanic; el notebook reutiliza
train_modelyone_epochsin tocarlos para el lineal, la red de dos capas y la profunda. - Lo que cambia entre dominios es el preprocesamiento: imputación (moda o mediana), log para colas largas,
get_dummiespara categóricas, normalización (dividir por el máximo o estandarizar) para que ninguna columna domine el gradiente. - La mejora clave del notebook es una sigmoid al final: acota las predicciones a (0, 1), así la loss deja de castigar aciertos exagerados y el modelo usa su capacidad donde importa. El precio: gradientes más chicos, lr mucho más grande.
- Con 891 filas, lineal + sigmoid, red de dos capas y red profunda empatan en ~82–83%: el techo lo pone el dataset. Promediar cinco modelos (ensembling) baja la varianza y estabiliza el resultado.
- Escribir SGD a mano una vez te da el diagnóstico;
TabularPandas+tabular_learnerdespués te ahorran reinventar imputación, normalización con estadísticas de train, inicialización ylr_finden cada proyecto. - Para datos tabulares, los árboles de decisión y random forests son una alternativa a las redes neuronales — más simples, casi sin preprocesamiento, muy interpretables — y suelen ser el primer modelo a probar.
9. Ponete a prueba
Respondé antes de abrir cada respuesta. Si fallás una, volvé a la sección indicada.
1. ¿Por qué Pclass (1, 2, 3) se pasa por get_dummies si ya es un número?
Porque es una categoría con apariencia numérica: nada garantiza que el efecto de segunda clase sea el promedio de primera y tercera. Con un coeficiente único, el modelo está obligado a asumir esa relación lineal; con tres dummies, cada clase tiene su propio peso. (§2, §7)
2. ¿Qué pasaría si no dividieras t_indep por el máximo de cada columna?
Age y Fare dominarían indeps*coeffs y sus gradientes; un lr que sirve para esos coeficientes sería demasiado grande para las dummies (o al revés), y el entrenamiento diverge o avanza lentísimo. Es el "no dividir por 255" de MNIST. (§2)
3. ¿Por qué agregar torch.sigmoid al final de calc_preds mejora la accuracy, si la loss no cambió?
Sin sigmoid, una predicción de 1.4 para un sobreviviente cuenta como 0.4 de error aunque sea correcta, y el modelo gasta capacidad en no pasarse. Con sigmoid la salida queda en (0, 1): los casos claros pueden ser "muy claros" sin penalización, y los coeficientes se dedican a la zona ambigua. (§3)
4. El lr pasa de 0.2 (lineal) a 100 (lineal + sigmoid). ¿Qué cambió?
La derivada de la sigmoid es como mucho 0.25 y mucho menor lejos de cero, así que los gradientes que llegan a los coeficientes son mucho más chicos; hace falta un lr proporcionalmente mayor para moverse lo mismo. Cada modelo tiene su propio rango de lr. (§3, §7)
5. ¿Por qué la red profunda no le gana al modelo lineal con sigmoid en este dataset?
Con 891 filas y 12 columnas no hay información suficiente para que la capacidad extra se traduzca en mejor generalización, y la varianza de un validation set de 178 filas (0,56 puntos por pasajero) es mayor que la diferencia entre modelos. El techo lo pone el dataset. (§4)
6. ¿Qué hace exactamente el ensemble de §5 y por qué ayuda?
Entrena cinco veces el mismo modelo desde inicializaciones distintas y promedia las probabilidades predichas. Cada corrida se equivoca en pasajeros distintos por azar de inicialización; el promedio cancela parte de ese ruido y el resultado es más estable entre corridas. (§5)
10. Para practicar
✏️ Ejercicio 1 — Correr las tres versiones y anotar la accuracy
Bajá train.csv de la competencia Titanic de Kaggle (o cualquier copia del mismo archivo
con las 12 columnas originales) y corré §2, §3 y §4 de corrido en CPU: tarda segundos. Anotá la accuracy
de validación de (a) lineal, (b) lineal + sigmoid, (c) dos capas, (d) profunda con
hiddens=[10, 10]. Después cambiá torch.manual_seed(442) por otras dos semillas
y repetí.
# después de §2 y del split de §3
for seed in [442, 1, 7]:
torch.manual_seed(seed)
coeffs = init_coeffs()
for i in range(30): one_epoch(coeffs, lr=4) # versión profunda
print(f'\nseed {seed}: acc {acc(coeffs):.3f}')
Pista / qué esperar
(a) alrededor de 0.79; (b), (c) y (d) alrededor de 0.82–0.83, con diferencias entre semillas del orden de 1–2 puntos — más grandes que la diferencia entre arquitecturas. Esa es la conclusión del ejercicio.
✏️ Ejercicio 2 — Ver el efecto de no normalizar
Repetí el modelo lineal + sigmoid de §3 pero comentá la línea t_indep = t_indep / vals.
Probá con lr=100, lr=1 y lr=0.01, y mirá los coeficientes
finales con dict(zip(indep_cols, coeffs.detach())).
Pista / qué esperar
Con lr=100 el loss salta o se queda en 0.5 (sigmoid saturada por Age y LogFare); con lr chico baja despacio y las dummies apenas se mueven. Los coeficientes de Age y Fare terminan minúsculos comparados con el resto — no porque importen poco, sino porque su escala es grande.
✏️ Ejercicio 3 — Lo mismo con fastai, y el ensemble
Corré el código de §5 y compará con tu mejor resultado a mano. Después subí el ensemble de 5 a 10
modelos y mirá si la accuracy cambia o solo se estabiliza. Con pip install fastai corre en
CPU en menos de un minuto.
Pista / qué esperar
Accuracy individual en la misma zona (~82–84%) que a mano — el framework no hace magia, hace lo mismo bien. El ensemble de 10 no debería ser claramente mejor que el de 5; lo que gana es que dos corridas completas del ensemble den casi el mismo número.
11. Fuentes y para seguir
- Practical Deep Learning for Coders — Lesson 5: From-scratch model — la lección en video (2022), con los dos notebooks de Kaggle recorridos en vivo.
- Linear model and neural net from scratch — el notebook fuente de §2–§4: preprocesamiento con pandas, modelo lineal, sigmoid, dos capas y "deep learning" con PyTorch pelado.
- Why you should use a framework — el notebook de §5: lo mismo con
TabularPandasytabular_learner, más feature engineering y el ensemble de cinco modelos. - Titanic - Machine Learning from Disaster — la competencia de Kaggle con el
train.csvque usan los notebooks y el leaderboard para probar tus predicciones. - fastai — Tabular core (TabularPandas, Categorify, FillMissing, Normalize) — la documentación oficial de los
procsque §5 describe. - fastbook — 09_tabular.ipynb — el Capítulo 9 del libro: árboles de decisión, random forests y embeddings para categóricas, el material de la Lección 6.