Capítulo 7 de 15 7 secciones 16 min

La red completa sobre las ventas, y todo lo que la empeora

Más neuronas, más vueltas y más paso: los tres botones que todo el mundo sube, medidos uno por uno. Los tres la empeoran.

Una red se entrena vigilando dos números a la vez: cómo va en los datos que ve y cómo va en los que no. Aquí, con 32 neuronas y 10.000 vueltas, la red llega a 1,0000 de AUC en entrenamiento y 0,5870 en prueba. Con parada temprana sobre un conjunto de validación, los cuatro tamaños que probé paran entre la vuelta 200 y la 300 y todos sacan entre 0,7172 y 0,7205, empatando con la regresión logística.

Ya tenemos todas las piezas: la neurona, la activación, las capas, el gradiente y la retropropagación. Toca montarlas bien y soltarlas sobre las ventas de la distribuidora 🚀

Y el capítulo va a consistir, básicamente, en subir botones y ver cómo todo empeora. Que suena raro y es lo más útil que te puedo enseñar aquí.

El punto de partida

import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'

def sigmoide(z):
    return 1 / (1 + np.exp(-z))

def carga_limpia(url):
    v = pd.read_csv(url).drop_duplicates()
    v['ciudad'] = (v['ciudad'].str.strip().str.lower()
                   .str.normalize('NFKD')
                   .str.encode('ascii', 'ignore').str.decode('utf-8'))
    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))
    for col in ['fecha', 'fecha_ultima_compra']:
        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')
        falta = f.isna() & v[col].notna()
        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')
        v[col] = f
    return v

def prepara(v):
    v = v.sort_values(['cliente_id', 'fecha']).copy()
    v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)
    v['sin_descuento'] = v['descuento'].isna().astype(int)
    v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)
    v['precio_unitario'] = v['monto'] / v['unidades']
    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1
    return v

NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',
             'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']
CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']

datos = prepara(carga_limpia(URL))
X = datos[NUMERICAS + CATEGORICAS]
y = datos['compro']
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,
                                          random_state=42, stratify=y)

logistica = Pipeline([
    ('pre', ColumnTransformer([
        ('num', Pipeline([('r', SimpleImputer(strategy='median')),
                          ('e', StandardScaler())]), NUMERICAS),
        ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),
                          ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),
    ])),
    ('mod', LogisticRegression(max_iter=1000, random_state=42)),
]).fit(X_tr, y_tr)

T_tr = logistica.named_steps['pre'].transform(X_tr)
T_te = logistica.named_steps['pre'].transform(X_te)
objetivo = y_tr.values.astype(float).reshape(-1, 1)

print('entreno con', T_tr.shape[0], 'filas y', T_tr.shape[1], 'columnas')
print('el listón a batir:', round(roc_auc_score(
    y_te, logistica.predict_proba(X_te)[:, 1]), 4))
entreno con 2250 filas y 28 columnas
el listón a batir: 0.7214

Uso el preprocesamiento de scikit-learn a propósito, porque ya lo tenemos resuelto del libro anterior y lo que interesa aquí es la red. De ahí para adelante, todo es nuestro 🔧

Las dos curvas de pérdida por época: la de entrenamiento baja sin parar, y la de validación baja con ella hasta un punto y a partir de ahí empieza a subir. Ese punto está marcado como detención temprana.
Este es el gráfico. El momento exacto en que la validación deja de bajar es el momento en que la red pasó de aprender a memorizar, y todo lo que entrenes después empeora el modelo mientras el número de entrenamiento sigue mejorando.

La red, con los dos números que hay que mirar

#parámetros=l(nl1nl+nl)

cada capa aporta un peso por cada par de neuronas conectadas más un sesgo por neurona, que es de dónde salen los millones de los que todo el mundo habla

def entrena(ocultas=8, paso=0.5, semilla=0, marcas=(0, 500, 2000, 5000, 10000)):
    r = np.random.default_rng(semilla)
    columnas = T_tr.shape[1]
    W1 = r.normal(0, 0.1, (columnas, ocultas)); b1 = np.zeros(ocultas)
    W2 = r.normal(0, 0.1, (ocultas, 1)); b2 = np.zeros(1)

    for i in range(max(marcas) + 1):
        if i in marcas:
            en_train = roc_auc_score(y_tr, sigmoide(np.tanh(T_tr @ W1 + b1) @ W2 + b2).ravel())
            en_prueba = roc_auc_score(y_te, sigmoide(np.tanh(T_te @ W1 + b1) @ W2 + b2).ravel())
            print(f'  vuelta {i:6d}  entrena {en_train:.4f}  prueba {en_prueba:.4f}  '
                  f'brecha {en_train - en_prueba:+.4f}')
        h = np.tanh(T_tr @ W1 + b1)
        p = sigmoide(h @ W2 + b2)
        d2 = (p - objetivo) / len(objetivo)
        d1 = (d2 @ W2.T) * (1 - h ** 2)
        W2 -= paso * (h.T @ d2); b2 -= paso * d2.sum(axis=0)
        W1 -= paso * (T_tr.T @ d1); b1 -= paso * d1.sum(axis=0)

print('8 neuronas ocultas, paso 0,5')
entrena()
8 neuronas ocultas, paso 0,5
  vuelta      0  entrena 0.4003  prueba 0.4044  brecha -0.0041
  vuelta    500  entrena 0.7343  prueba 0.7150  brecha +0.0194
  vuelta   2000  entrena 0.7948  prueba 0.6651  brecha +0.1296
  vuelta   5000  entrena 0.8245  prueba 0.6290  brecha +0.1955
  vuelta  10000  entrena 0.8328  prueba 0.6174  brecha +0.2154

Léelo de arriba abajo despacio, porque esta tabla es el capítulo 😨

En la vuelta 500 la red va bien: 0,7343 en entrenamiento y 0,7150 en prueba, con una brecha de dos centésimas.

En la vuelta 2.000 el entrenamiento ha subido a 0,7948 y la prueba ha bajado a 0,6651. En la 10.000, entrenamiento 0,8328 y prueba 0,6174.

O sea que a partir de la vuelta 500, cada vuelta que le das la empeora. No es que deje de mejorar: es que empeora activamente mientras el número que estabas mirando sube.

Eso es sobreajuste, y en el capítulo 6 ya lo habíamos visto en su forma más pura, con etiquetas de moneda. Aquí, con datos de verdad, tiene la misma pinta 🎲

El botón de más neuronas

print('32 neuronas ocultas, paso 0,5')
entrena(ocultas=32)
32 neuronas ocultas, paso 0,5
  vuelta      0  entrena 0.4230  prueba 0.4176  brecha +0.0054
  vuelta    500  entrena 0.7365  prueba 0.7165  brecha +0.0200
  vuelta   2000  entrena 0.8847  prueba 0.6210  brecha +0.2636
  vuelta   5000  entrena 0.9921  prueba 0.5887  brecha +0.4033
  vuelta  10000  entrena 1.0000  prueba 0.5870  brecha +0.4130

Con 32 neuronas la red llega a 1,0000 de AUC en entrenamiento en la vuelta 10.000. Perfecto, sin un solo error. Y su prueba en ese momento es 0,5870, o sea casi tirar una moneda.

Ahí tienes la memorización en su forma más literal: se aprendió las 2.250 filas una por una. La brecha llega a 0,4130 😱

Y fíjate en algo importante de las dos tablas: en la vuelta 500 las dos redes van casi igual (0,7150 y 0,7165). La red grande no aprendió más rápido, solo se estropeó más rápido.

Los tres botones, medidos

Vamos a subirlos uno por uno y quedarnos solo con la prueba:

def solo_prueba(ocultas=8, paso=0.5, vueltas=2000, semilla=0):
    r = np.random.default_rng(semilla)
    columnas = T_tr.shape[1]
    W1 = r.normal(0, 0.1, (columnas, ocultas)); b1 = np.zeros(ocultas)
    W2 = r.normal(0, 0.1, (ocultas, 1)); b2 = np.zeros(1)
    for _ in range(vueltas):
        h = np.tanh(T_tr @ W1 + b1)
        p = sigmoide(h @ W2 + b2)
        d2 = (p - objetivo) / len(objetivo)
        d1 = (d2 @ W2.T) * (1 - h ** 2)
        W2 -= paso * (h.T @ d2); b2 -= paso * d2.sum(axis=0)
        W1 -= paso * (T_tr.T @ d1); b1 -= paso * d1.sum(axis=0)
    return roc_auc_score(y_te, sigmoide(np.tanh(T_te @ W1 + b1) @ W2 + b2).ravel())

print('subiendo neuronas:')
for ocultas in [4, 8, 16, 32]:
    print(f'  {ocultas:3d} neuronas: {solo_prueba(ocultas=ocultas):.4f}')
print('subiendo el paso:')
for paso in [0.1, 0.5, 2.0]:
    print(f'  paso {paso:<4}: {solo_prueba(paso=paso):.4f}')
print('subiendo vueltas:')
for vueltas in [2000, 5000, 10000]:
    print(f'  {vueltas:6d} vueltas: {solo_prueba(vueltas=vueltas):.4f}')
subiendo neuronas:
    4 neuronas: 0.6934
    8 neuronas: 0.6651
   16 neuronas: 0.6380
   32 neuronas: 0.6210
subiendo el paso:
  paso 0.1 : 0.7183
  paso 0.5 : 0.6651
  paso 2.0 : 0.6422
subiendo vueltas:
    2000 vueltas: 0.6651
    5000 vueltas: 0.6290
   10000 vueltas: 0.6174

Los tres botones, y los tres van en la misma dirección: subirlos empeora 🎚️

Más neuronas: de 0,6934 con cuatro a 0,6210 con treinta y dos. Más paso: de 0,7183 con 0,1 a 0,6422 con 2,0. Más vueltas: de 0,6651 con dos mil a 0,6174 con diez mil.

Y mira dónde está el mejor de toda la tabla: paso 0,1, que es el ajuste más conservador de todos, con 0,7183. Casi el 0,7214 de la logística.

Esto va justo en contra de la intuición que vende el marketing de la IA, donde más grande siempre es mejor. Con 2.250 filas de datos tabulares, cada aumento de capacidad es capacidad para memorizar 🧠

Parar a tiempo, y hacerlo bien

Si en la vuelta 500 la red iba bien y luego se estropea, la solución es evidente: parar en la 500.

Y aquí viene la trampa que arruina esto en la mitad de los proyectos que veo: si eliges dónde parar mirando la prueba, la prueba deja de ser prueba. Es la fuga del capítulo 6 del libro de machine learning, con otro disfraz.

Se hace con un tercer corte, la validación, que sale de los datos de entrenamiento:

X_ent, X_val, y_ent, y_val = train_test_split(X_tr, y_tr, test_size=0.25,
                                              random_state=42, stratify=y_tr)
pre = logistica.named_steps['pre']
E = pre.transform(X_ent)
V = pre.transform(X_val)
P = pre.transform(X_te)
obj_ent = y_ent.values.astype(float).reshape(-1, 1)

def con_parada(ocultas=8, paso=0.5, paciencia=20, maximo=20000, semilla=0):
    r = np.random.default_rng(semilla)
    W1 = r.normal(0, 0.1, (E.shape[1], ocultas)); b1 = np.zeros(ocultas)
    W2 = r.normal(0, 0.1, (ocultas, 1)); b2 = np.zeros(1)
    mejor, guardados, espera, cuando = -1, None, 0, 0

    for i in range(maximo + 1):
        if i % 100 == 0:
            auc = roc_auc_score(y_val, sigmoide(np.tanh(V @ W1 + b1) @ W2 + b2).ravel())
            if auc > mejor:
                mejor, espera, cuando = auc, 0, i
                guardados = (W1.copy(), b1.copy(), W2.copy(), b2.copy())
            else:
                espera += 1
                if espera >= paciencia:
                    break
        h = np.tanh(E @ W1 + b1)
        p = sigmoide(h @ W2 + b2)
        d2 = (p - obj_ent) / len(obj_ent)
        d1 = (d2 @ W2.T) * (1 - h ** 2)
        W2 -= paso * (h.T @ d2); b2 -= paso * d2.sum(axis=0)
        W1 -= paso * (E.T @ d1); b1 -= paso * d1.sum(axis=0)

    W1, b1, W2, b2 = guardados
    return cuando, mejor, roc_auc_score(y_te, sigmoide(np.tanh(P @ W1 + b1) @ W2 + b2).ravel())

for ocultas in [4, 8, 16, 32]:
    cuando, val, prueba = con_parada(ocultas=ocultas)
    print(f'{ocultas:3d} neuronas: paró en la vuelta {cuando:5d}  '
          f'validación {val:.4f}  prueba {prueba:.4f}')
  4 neuronas: paró en la vuelta   300  validación 0.7048  prueba 0.7190
  8 neuronas: paró en la vuelta   300  validación 0.7061  prueba 0.7176
 16 neuronas: paró en la vuelta   200  validación 0.7046  prueba 0.7172
 32 neuronas: paró en la vuelta   200  validación 0.7038  prueba 0.7205

Y aquí está el final del capítulo, que a mí me parece precioso 💛

Los cuatro tamaños paran entre la vuelta 200 y la 300, de 20.000 posibles. Y los cuatro sacan entre 0,7172 y 0,7205, o sea prácticamente lo mismo.

Con parada temprana, el tamaño de la red dejó de importar. Cuatro neuronas o treinta y dos, da igual: todas encuentran lo mismo, porque lo mismo es lo único que hay en estos datos.

Y ese "lo mismo" es la solución lineal. Comparado con el 0,7217 de la logística sobre el mismo corte, la red no aporta nada y tampoco pierde nada, siempre que la pares a tiempo 🛑

La paciencia de esa función es lo que hace MLPClassifier con early_stopping=True, que en el capítulo 1 subió una red de 0,5834 a 0,7018. Ahora ya sabes exactamente qué hace por dentro.

Ejercicios

1. Guardar el mejor y no el último

Compara parar y quedarte con los pesos del momento, contra volver a los mejores que viste.

def sin_guardar(ocultas=8, paso=0.5, paciencia=20, maximo=20000, semilla=0):
    r = np.random.default_rng(semilla)
    W1 = r.normal(0, 0.1, (E.shape[1], ocultas)); b1 = np.zeros(ocultas)
    W2 = r.normal(0, 0.1, (ocultas, 1)); b2 = np.zeros(1)
    mejor, espera = -1, 0
    for i in range(maximo + 1):
        if i % 100 == 0:
            auc = roc_auc_score(y_val, sigmoide(np.tanh(V @ W1 + b1) @ W2 + b2).ravel())
            if auc > mejor:
                mejor, espera = auc, 0
            else:
                espera += 1
                if espera >= paciencia:
                    break
        h = np.tanh(E @ W1 + b1)
        p = sigmoide(h @ W2 + b2)
        d2 = (p - obj_ent) / len(obj_ent)
        d1 = (d2 @ W2.T) * (1 - h ** 2)
        W2 -= paso * (h.T @ d2); b2 -= paso * d2.sum(axis=0)
        W1 -= paso * (E.T @ d1); b1 -= paso * d1.sum(axis=0)
    return roc_auc_score(y_te, sigmoide(np.tanh(P @ W1 + b1) @ W2 + b2).ravel())

print('guardando el mejor :', round(con_parada()[2], 4))
print('quedándote el último:', round(sin_guardar(), 4))
guardando el mejor : 0.7176
quedándote el último: 0.6597

0,7176 guardando el mejor contra 0,6597 quedándote el último. Casi seis centésimas por una línea de código.

Porque con paciencia 20 sigues entrenando 2.000 vueltas después del mejor momento, y en esas 2.000 vueltas la red se sigue estropeando. La parada temprana sin guardar los pesos está a medio hacer 💾

2. Cuánta paciencia conviene

Prueba varias y mira dónde para y qué saca.

for paciencia in [3, 10, 20, 50]:
    cuando, val, prueba = con_parada(paciencia=paciencia)
    print(f'paciencia {paciencia:3d}: paró en {cuando:5d}  '
          f'validación {val:.4f}  prueba {prueba:.4f}')
paciencia   3: paró en   300  validación 0.7061  prueba 0.7176
paciencia  10: paró en   300  validación 0.7061  prueba 0.7176
paciencia  20: paró en   300  validación 0.7061  prueba 0.7176
paciencia  50: paró en   300  validación 0.7061  prueba 0.7176

Las cuatro paran exactamente en la vuelta 300 y sacan exactamente 0,7176. Da igual cuál pongas, y eso es buena señal.

La paciencia importa cuando la curva de validación es irregular y sube y baja: con poca paciencia paras en el primer bache y te pierdes un pico posterior. Aquí la curva baja limpio desde el máximo, así que cualquier valor la caza 🧘‍♀️

3. El mismo corte para la logística

Compara la red con parada contra la logística entrenada solo con la parte de entrenamiento.

log_corta = Pipeline([
    ('pre', ColumnTransformer([
        ('num', Pipeline([('r', SimpleImputer(strategy='median')),
                          ('e', StandardScaler())]), NUMERICAS),
        ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),
                          ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),
    ])),
    ('mod', LogisticRegression(max_iter=1000, random_state=42)),
]).fit(X_ent, y_ent)

print('red con parada  :', round(con_parada(ocultas=32)[2], 4))
print('logística       :', round(roc_auc_score(
    y_te, log_corta.predict_proba(X_te)[:, 1]), 4))
red con parada  : 0.7205
logística       : 0.7217

0,7205 la red y 0,7217 la logística, con las dos entrenadas sobre las mismas 1.687 filas. Doce diezmilésimas de diferencia.

Y la logística tarda dos décimas de segundo, se explica leyendo sus 28 coeficientes y no tiene ni un botón que ajustar. Empatar no es ganar 🏁

4. Diez semillas, para saber si el empate es real

Repite con arranques distintos y mira cuánto baila.

resultados = [con_parada(ocultas=8, semilla=s)[2] for s in range(10)]
print('mejor :', round(max(resultados), 4))
print('peor  :', round(min(resultados), 4))
print('media :', round(float(np.mean(resultados)), 4))
print('logística:', round(roc_auc_score(y_te, log_corta.predict_proba(X_te)[:, 1]), 4))
mejor : 0.7203
peor  : 0.7064
media : 0.7159
logística: 0.7217

Diez arranques distintos y la media queda en 0,7159, con el mejor en 0,7203 y el peor en 0,7064.

O sea que ni con suerte pasa a la logística: su mejor arranque de diez se queda catorce diezmilésimas por debajo del 0,7217.

Es la lección del capítulo 10 del libro de machine learning aplicada aquí. Si me hubiera quedado con el mejor de los diez y lo hubiera reportado como "la red saca 0,7203", habría contado una verdad engañosa: un número suelto no dice nada, hay que ver el rango 🎲

5. La curva de validación, vuelta a vuelta

Imprime la validación cada 100 vueltas para ver la forma que tiene.

r = np.random.default_rng(0)
W1 = r.normal(0, 0.1, (E.shape[1], 8)); b1 = np.zeros(8)
W2 = r.normal(0, 0.1, (8, 1)); b2 = np.zeros(1)
for i in range(1501):
    if i % 300 == 0:
        v = roc_auc_score(y_val, sigmoide(np.tanh(V @ W1 + b1) @ W2 + b2).ravel())
        e = roc_auc_score(y_ent, sigmoide(np.tanh(E @ W1 + b1) @ W2 + b2).ravel())
        print(f'vuelta {i:5d}  entrena {e:.4f}  validación {v:.4f}')
    h = np.tanh(E @ W1 + b1)
    p = sigmoide(h @ W2 + b2)
    d2 = (p - obj_ent) / len(obj_ent)
    d1 = (d2 @ W2.T) * (1 - h ** 2)
    W2 -= 0.5 * (h.T @ d2); b2 -= 0.5 * d2.sum(axis=0)
    W1 -= 0.5 * (E.T @ d1); b1 -= 0.5 * d1.sum(axis=0)
vuelta     0  entrena 0.4041  validación 0.3888
vuelta   300  entrena 0.7280  validación 0.7061
vuelta   600  entrena 0.7497  validación 0.6997
vuelta   900  entrena 0.7680  validación 0.6930
vuelta  1200  entrena 0.7845  validación 0.6801
vuelta  1500  entrena 0.7991  validación 0.6685

La validación sube hasta la vuelta 300 y a partir de ahí baja, mientras el entrenamiento sigue subiendo tan contento.

Esa forma de U al revés es la más importante de todo el deep learning práctico. El punto más alto de la curva de validación es dónde hay que parar, y todo lo que hay a la derecha es tiempo de cómputo gastado en empeorar el modelo 📈

6. Cuántos pesos contra cuántas filas

Pon los dos números al lado para cada tamaño.

for ocultas in [4, 8, 16, 32]:
    pesos = E.shape[1] * ocultas + ocultas + ocultas + 1
    print(f'{ocultas:3d} neuronas: {pesos:4d} pesos para {len(y_ent)} filas  '
          f'({pesos / len(y_ent):.3f} por fila)')
  4 neuronas:  121 pesos para 1687 filas  (0.072 por fila)
  8 neuronas:  241 pesos para 1687 filas  (0.143 por fila)
 16 neuronas:  481 pesos para 1687 filas  (0.285 por fila)
 32 neuronas:  961 pesos para 1687 filas  (0.570 por fila)

Con 32 neuronas hay 961 pesos para 1.687 filas, o sea 0,570 pesos por fila. Todavía no llega a uno por fila, y aun así memoriza hasta el 1,0000.

Eso desmonta la regla de oro que se repite mucho, la de "necesitas diez veces más filas que parámetros". No es falsa del todo, pero es muchísimo más optimista de lo que la realidad aguanta 📏

7. Entrenar sin mirar nada

Corre la red 30.000 vueltas sin validación y mira el desastre.

print('10.000 vueltas a ciegas, 32 neuronas:', round(
    solo_prueba(ocultas=32, vueltas=10000), 4))
print('con parada temprana                 :', round(con_parada(ocultas=32)[2], 4))
10.000 vueltas a ciegas, 32 neuronas: 0.587
con parada temprana                 : 0.7205

0,5870 a ciegas contra 0,7205 con parada. Trece puntos de AUC entre correr el bucle y vigilarlo.

Y el que corrió a ciegas hizo diez mil vueltas donde el otro hizo doscientas, o sea que tardó cincuenta veces más. Es de los pocos casos donde hacerlo bien es además más rápido 🏃‍♀️

8. El error de validar con lo que entrenaste

Pásale a con_parada el conjunto de entrenamiento como validación.

V_mal = E
roc_auc_score(y_val, sigmoide(np.tanh(V_mal @ W1 + b1) @ W2 + b2).ravel())
ValueError: Found input variables with inconsistent numbers of samples: [563, 1687]

Revienta porque y_val tiene 563 filas y E tiene 1.687. Los tamaños no cuadran y scikit-learn lo dice claro.

Menos mal, porque si hubieran cuadrado (que pasa si te confundes de variable entre dos cortes del mismo tamaño) no habría dado error: la validación sobre datos de entrenamiento nunca baja, así que la parada temprana no pararía jamás y volverías a las 20.000 vueltas 😬

Comprueba que lo tienes

La curva de entrenamiento sigue subiendo y la de validación empezó a bajar. ¿Qué haces?

  • Paro ahí y me quedo con los pesos de antes de que empezara a bajar
  • Sigo, porque el entrenamiento todavía mejora
  • Añado más neuronas
  • Bajo la tasa de aprendizaje

Lo que te llevas

  • 👀 Se entrena vigilando dos números: cómo va en lo que ve y cómo va en lo que no.
  • 📉 A partir de la vuelta 500 esta red empeora activamente mientras el número que estabas mirando sube.
  • 😱 Con 32 neuronas llega a 1,0000 de AUC en entrenamiento y 0,5870 en prueba.
  • 🎚️ Los tres botones (neuronas, paso, vueltas) empeoran al subirlos. El mejor resultado sale del ajuste más conservador.
  • 🛑 Con parada temprana los cuatro tamaños paran entre la vuelta 200 y la 300 y sacan lo mismo: el tamaño deja de importar.
  • 💾 Hay que guardar los mejores pesos, no quedarse los últimos: 0,7176 contra 0,6597.
  • 🏁 Y aun bien parada, la red empata con la logística (0,7205 contra 0,7217), que tarda dos décimas de segundo y se lee entera.

En el capítulo 8 nos dedicamos entero a lo que aquí salió por todas partes: el sobreajuste, y las tres formas de frenarlo.

Que tengas lindo día! 🌸

¿Tienes alguna duda o consulta?