# Funciones reutilizables de medición, visualización y diagnóstico
RESULTADOS = [] # tabla viva de la sesión
def _series(history):
h = history.history
return {
"loss": np.asarray(h["loss"], dtype=float),
"val_loss": np.asarray(h["val_loss"], dtype=float),
"acc": np.asarray(h["accuracy"], dtype=float),
"val_acc": np.asarray(h["val_accuracy"], dtype=float),
}
def plot_history(history, titulo="", marcar_mejor=True, ax=None):
# Grafica perdida y accuracy de entrenamiento frente a validacion.
s = _series(history)
ep = np.arange(1, len(s["loss"]) + 1)
best = int(np.argmin(s["val_loss"])) + 1
fig, axes = plt.subplots(1, 2, figsize=(11.2, 4.0))
axes[0].plot(ep, s["loss"], label="loss (train)", lw=2)
axes[0].plot(ep, s["val_loss"], label="val_loss", lw=2)
axes[0].set_xlabel("Época")
axes[0].set_ylabel("Pérdida (entropía cruzada)")
axes[0].set_title("Pérdida")
axes[0].legend()
if marcar_mejor:
axes[0].axvline(best, color="crimson", ls="--", lw=1.1,
label=f"mejor val_loss (ép. {best})")
axes[0].legend()
axes[1].plot(ep, s["acc"], label="accuracy (train)", lw=2)
axes[1].plot(ep, s["val_acc"], label="val_accuracy", lw=2)
axes[1].set_xlabel("Época")
axes[1].set_ylabel("Accuracy")
axes[1].set_title("Accuracy")
axes[1].legend()
if marcar_mejor:
axes[1].axvline(best, color="crimson", ls="--", lw=1.1)
fig.suptitle(titulo if titulo else "Evolución del entrenamiento", y=1.02)
plt.tight_layout()
plt.show()
return best
def clasificar_patron_curvas(train_bajo, n_despues, train_despues, val_despues):
# Casos A-D: descriptores de evidencia, no etiquetas juridicas.
if train_bajo:
return (
"D",
"Train y validation permanecen en un desempeno bajo.",
"Los resultados son compatibles con underfitting, o con un problema de "
"optimizacion/representacion. Esto NO es, por si solo, evidencia de sobreajuste.",
"Antes de regularizar, convendria revisar capacidad, learning rate, epocas o la escala de X.",
)
if n_despues >= 2 and train_despues < -1e-3 and val_despues > 1e-3:
return (
"C",
"Tras la mejor epoca, train_loss siguio disminuyendo mientras val_loss aumento.",
"Este patron es compatible con evidencia de sobreajuste: las mejoras de "
"entrenamiento dejan de trasladarse a validacion.",
"Un siguiente paso razonable es Early Stopping, regularizacion o reducir capacidad; luego volver a medir.",
)
if n_despues >= 2 and train_despues < -1e-3 and abs(val_despues) <= 1e-3:
return (
"B",
"Train continua mejorando mientras validation se estabiliza.",
"Las mejoras adicionales sobre entrenamiento parecen dejar de trasladarse "
"al conjunto de validacion. Puede ser el inicio de un sobreajuste; no es una demostracion.",
"Conviene vigilar val_loss y considerar Early Stopping si la meseta persiste.",
)
if n_despues == 0 or (train_despues <= 1e-3 and val_despues <= 1e-3):
return (
"A",
"Train y validation evolucionan en la misma direccion, o el optimo de val_loss esta al final.",
"El modelo parece seguir aprendiendo patrones que tambien funcionan fuera de D_train. "
"Eso no demuestra que el nivel absoluto sea satisfactorio.",
"Revisar el nivel absoluto de las metricas. No celebrar solo la ausencia de divergencia.",
)
return (
"mixto",
"El history no encaja de forma nitida en un unico patron A-D.",
"Con esta evidencia unicamente no podemos etiquetar el regimen de forma tajante.",
"Combinar con el gap, el nivel absoluto y evaluate() en los tres conjuntos.",
)
def analyze_history(history, nombre="modelo", verbose=True):
# Lee un history y produce un reporte prudente (evidencia, no veredicto).
s = _series(history)
n = len(s["loss"])
best_i = int(np.argmin(s["val_loss"]))
best_ep = best_i + 1
n_despues = n - best_ep
train_despues = s["loss"][-1] - s["loss"][best_i]
val_despues = s["val_loss"][-1] - s["val_loss"][best_i]
gap_acc_best = s["acc"][best_i] - s["val_acc"][best_i]
gap_acc_final = s["acc"][-1] - s["val_acc"][-1]
gap_loss_best = s["val_loss"][best_i] - s["loss"][best_i]
gap_loss_final = s["val_loss"][-1] - s["loss"][-1]
# Tendencia posterior: pendiente de un ajuste lineal si hay suficientes épocas
if n_despues >= 3:
t = np.arange(n_despues + 1)
pendiente_val = float(np.polyfit(t, s["val_loss"][best_i:], 1)[0])
pendiente_tr = float(np.polyfit(t, s["loss"][best_i:], 1)[0])
else:
pendiente_val = float(val_despues)
pendiente_tr = float(train_despues)
# Diagnóstico prudente (indicadores, no umbrales universales)
train_bajo = s["acc"][best_i] < 0.62 and s["val_acc"][best_i] < 0.62
divergencia = (train_despues < -1e-3) and (val_despues > 1e-3) and n_despues >= 2
val_mejora_poco = abs(val_despues) <= 1e-3 and n_despues >= 2 and train_despues < -1e-3
if verbose:
_imprimir_reporte_history(
nombre, n, s, best_i, best_ep, n_despues,
train_despues, val_despues, gap_acc_best, gap_acc_final,
gap_loss_best, gap_loss_final, pendiente_tr, pendiente_val,
train_bajo, divergencia, val_mejora_poco,
)
return {
"nombre": nombre,
"epocas": n,
"mejor_epoca_val_loss": best_ep,
"min_val_loss": float(s["val_loss"][best_i]),
"train_loss_en_mejor": float(s["loss"][best_i]),
"train_acc_en_mejor": float(s["acc"][best_i]),
"val_acc_en_mejor": float(s["val_acc"][best_i]),
"train_acc_mejor": float(s["acc"].max()),
"val_acc_mejor": float(s["val_acc"].max()),
"train_loss_final": float(s["loss"][-1]),
"val_loss_final": float(s["val_loss"][-1]),
"train_acc_final": float(s["acc"][-1]),
"val_acc_final": float(s["val_acc"][-1]),
"gap_acc_mejor_epoca": float(gap_acc_best),
"gap_acc_final": float(gap_acc_final),
"gap_loss_final": float(gap_loss_final),
"n_despues": n_despues,
"delta_train_despues": float(train_despues),
"delta_val_despues": float(val_despues),
}
def _imprimir_reporte_history(
nombre, n, s, best_i, best_ep, n_despues,
train_despues, val_despues, gap_acc_best, gap_acc_final,
gap_loss_best, gap_loss_final, pendiente_tr, pendiente_val,
train_bajo, divergencia, val_mejora_poco,
):
print("=" * 72)
print(f"REPORTE DE HISTORY — {nombre}")
print("=" * 72)
print(f"Épocas ejecutadas: {n}")
print(f"Mejor val_loss: {s['val_loss'][best_i]:.4f} en la época {best_ep}")
print(f" loss (train) en esa época: {s['loss'][best_i]:.4f}")
print(f" accuracy (train) en esa época: {s['acc'][best_i]:.4f}")
print(f" val_accuracy en esa época: {s['val_acc'][best_i]:.4f}")
print(f" Gap_acc (train - val): {gap_acc_best:+.4f}")
print(f" Gap_loss (val - train): {gap_loss_best:+.4f}")
print(f"Valores FINALES (época {n}):")
print(f" loss={s['loss'][-1]:.4f} val_loss={s['val_loss'][-1]:.4f} "
f"acc={s['acc'][-1]:.4f} val_acc={s['val_acc'][-1]:.4f}")
print(f" Gap_acc final: {gap_acc_final:+.4f} Gap_loss final: {gap_loss_final:+.4f}")
if n_despues == 0:
print("La mejor val_loss ocurrió en la última época: no hay tramo posterior que analizar.")
else:
print(f"Después de la época {best_ep} hay {n_despues} época(s) adicionales.")
print(f" Δ loss train (final - mejor): {train_despues:+.4f} "
f"(negativo = el entrenamiento SIGUIÓ bajando la pérdida)")
print(f" Δ val_loss (final - mejor): {val_despues:+.4f} "
f"(positivo = la validación EMPEORÓ)")
if n_despues >= 3:
print(f" Pendiente lineal post-óptimo: train={pendiente_tr:+.5f}, val={pendiente_val:+.5f}")
caso, obs, inf, dec = clasificar_patron_curvas(
train_bajo, n_despues, train_despues, val_despues
)
print("\n--- Observacion / inferencia / decision (curvas) ---")
print(f"Patron compatible con el caso {caso} (guia, no regla absoluta).")
print(f"OBSERVACION: {obs}")
if n_despues > 0:
print(
f" Numeros: mejor epoca={best_ep}, "
f"Delta train_loss={train_despues:+.4f}, "
f"Delta val_loss={val_despues:+.4f}, "
f"tramo posterior={n_despues} epocas."
)
print(f"INFERENCIA: {inf}")
print(
"LIMITACION: un solo history no mide varianza entre semillas "
"ni el riesgo verdadero. El gap debe leerse junto al nivel absoluto."
)
print(f"DECISION: {dec}")
print("=" * 72)
def evaluar_conjuntos(model):
# evaluate() en los tres conjuntos (modo inferencia: Dropout inactivo).
tr = model.evaluate(X_train, y_train, verbose=0)
va = model.evaluate(X_val, y_val, verbose=0)
te = model.evaluate(X_test, y_test, verbose=0)
return {
"train_loss": float(tr[0]), "train_acc": float(tr[1]),
"val_loss": float(va[0]), "val_acc": float(va[1]),
"test_loss": float(te[0]), "test_acc": float(te[1]),
}
def recolectar_pesos(model):
vectores = []
for layer in model.layers:
pesos = layer.get_weights()
if not pesos:
continue
# El primer arreglo de Dense es el kernel
if isinstance(layer, Dense):
vectores.append(pesos[0].ravel())
if not vectores:
return np.array([])
return np.concatenate(vectores)
def registrar(nombre, model, history, tiempo, hparams=None, extra=None):
# Anade una fila a RESULTADOS con metricas de history y de evaluate().
info = analyze_history(history, nombre=nombre)
ev = evaluar_conjuntos(model)
n_params = int(model.count_params())
fila = {
"modelo": nombre,
"parametros": n_params,
"tiempo_s": float(tiempo),
"epocas_ejecutadas": info["epocas"],
"mejor_epoca": info["mejor_epoca_val_loss"],
"mejor_val_loss": info["min_val_loss"],
"train_acc_hist_mejor": info["train_acc_mejor"],
"val_acc_hist_mejor": info["val_acc_mejor"],
"train_acc": ev["train_acc"],
"val_acc": ev["val_acc"],
"test_acc": ev["test_acc"],
"train_loss": ev["train_loss"],
"val_loss": ev["val_loss"],
"test_loss": ev["test_loss"],
"gap_acc": ev["train_acc"] - ev["val_acc"],
"gap_loss": ev["val_loss"] - ev["train_loss"],
"hparams": hparams or {},
}
if extra:
fila.update(extra)
RESULTADOS.append(fila)
print(f"\n[registro] {nombre}: test_acc={fila['test_acc']:.4f} "
f"val_acc={fila['val_acc']:.4f} gap_acc={fila['gap_acc']:+.4f} "
f"params={n_params} tiempo={tiempo:.1f}s")
return fila
def diagnosticar_modelo(fila):
# Recibe metricas train/val/test y produce evidencia, no un veredicto.
ta, va, te = fila["train_acc"], fila["val_acc"], fila["test_acc"]
gap = fila["gap_acc"]
d_vt = abs(va - te)
print(f"\n--- Diagnostico: {fila['modelo']} ---")
print("OBSERVACION:")
print(f" train_acc={ta:.3f} val_acc={va:.3f} test_acc={te:.3f}")
print(f" gap_acc (train-val)={gap:+.3f} |val-test|={d_vt:.3f}")
if "train_loss" in fila:
tl, vl = fila["train_loss"], fila["val_loss"]
tesl = fila.get("test_loss", float("nan"))
print(f" train_loss={tl:.3f} val_loss={vl:.3f} test_loss={tesl:.3f}")
print("COMPARACION:")
print(" El gap mide si el modelo rinde mejor donde actualizo pesos que en validacion.")
print(" Un gap pequeno NO implica automaticamente un buen modelo: hay que mirar el nivel absoluto.")
print("INFERENCIA:")
if ta < 0.62 and va < 0.62:
print(" Desempeno bajo en train y val. Existe evidencia compatible con underfitting,")
print(" o con optimizacion/representacion insuficiente. Regularizar ahora podria empeorar el ajuste.")
elif gap > 0.10 and ta > 0.85:
print(" Train alto y brecha amplia respecto a validacion. Esto constituye evidencia")
print(" compatible con sobreajuste. No 'demuestra' que cualquier gap>0.10 sea inaceptable.")
elif abs(gap) <= 0.06 and va < 0.62:
print(" Gap pequeno con desempeno pobre: NO es buena generalizacion; es mal ajuste simetrico.")
elif abs(gap) <= 0.06 and va >= 0.70:
print(" Train y val proximos en un nivel no trivial. Compatible con generalizacion razonable")
print(" en ESTE corpus. Con una sola semilla no medimos varianza.")
else:
print(" Patron mixto. Con estas metricas unicamente no podemos etiquetar el regimen.")
print(" Conviene mirar las curvas de loss y el test como evaluacion, no como selector.")
if d_vt <= 0.03:
print(" Validation y test son relativamente consistentes: evidencia de comportamiento")
print(" similar sobre datos no usados para actualizar pesos.")
elif te + 0.03 < va:
print(" Test quedo por debajo de validacion. La validacion pudo ser algo optimista;")
print(" no implica, por si sola, que el protocolo este roto.")
print("LIMITACION: una metrica no es una conclusion. Es evidencia para construir una conclusion.")
print("DECISION: si hay divergencia train/val, revisar curvas; si confirman, probar UNA")
print(" intervencion (regularizar, Early Stopping o reducir capacidad) y volver a medir.")
def analizar_generalizacion(fila):
diagnosticar_modelo(fila)
def comparar_modelos(df, referencia=None):
# Produce evidencia para decidir. NO declara mecanicamente 'el mejor'.
print("\n--- Comparacion de modelos (evidencia, no veredicto) ---")
if df is None or len(df) == 0:
print("No hay filas que comparar.")
return
d = df.copy().reset_index(drop=True)
i_tr = d["train_acc"].idxmax()
i_va = d["val_acc"].idxmax()
i_te = d["test_acc"].idxmax()
i_gp = d["gap_acc"].abs().idxmin()
print("1. Mayor train_acc:", f"{d.loc[i_tr, 'modelo']} ({d.loc[i_tr, 'train_acc']:.3f}).")
print(" Ese NO es necesariamente el mejor: el objetivo no es memorizar D_train.")
print("2. Mayor val_acc:", f"{d.loc[i_va, 'modelo']} ({d.loc[i_va, 'val_acc']:.3f}).")
print(" Este es el criterio honesto de seleccion en desarrollo.")
print("3. Menor |gap_acc|:", f"{d.loc[i_gp, 'modelo']} ({d.loc[i_gp, 'gap_acc']:+.3f}),",
f"val_acc={d.loc[i_gp, 'val_acc']:.3f}.")
print(" Gap pequeno + val bajo no es 'buena generalizacion'; es mal ajuste simetrico.")
pobres = d[(d["gap_acc"].abs() <= 0.06) & (d["val_acc"] < 0.62)]
if len(pobres):
print(" Filas con gap pequeno y desempeno pobre:",
", ".join(pobres["modelo"].astype(str)))
print("4. Mayor test_acc:", f"{d.loc[i_te, 'modelo']} ({d.loc[i_te, 'test_acc']:.3f}).")
print(" Se reporta para evaluacion retrospectiva; NO debio usarse para elegir.")
print("5. Coherencia val/test: si el ganador de val no coincide con el de test,")
print(" D_val es finito y puede ordenar mal modelos cercanos. No se 'corrige' con test.")
if str(d.loc[i_va, "modelo"]) != str(d.loc[i_te, "modelo"]):
print(f" En esta tabla, val apunta a {d.loc[i_va, 'modelo']} y test a {d.loc[i_te, 'modelo']}.")
print(" La seleccion de desarrollo sigue siendo la de validacion.")
else:
print(" En esta tabla, validacion y test senalan el mismo modelo. Evidencia coherente, no prueba.")
if "parametros" in d.columns:
cand = d.loc[i_va]
print(f"6. Equilibrio desempeno/complejidad (ganador de val): "
f"{int(cand['parametros']):,} parametros, "
f"epocas={cand.get('epocas_ejecutadas', '?')}, "
f"tiempo={cand.get('tiempo_s', float('nan')):.1f}s.")
print("7. Regularizacion excesiva (hipotesis): train y val bajan juntos respecto de la referencia.")
if referencia is not None and referencia in set(d["modelo"]):
ref = d.set_index("modelo").loc[referencia]
print(f"\nRespecto de '{referencia}':")
for _, row in d.iterrows():
if row["modelo"] == referencia:
continue
dval = row["val_acc"] - ref["val_acc"]
dtr = row["train_acc"] - ref["train_acc"]
dgap = row["gap_acc"] - ref["gap_acc"]
dte = row["test_acc"] - ref["test_acc"]
nota = ""
if dtr < -0.03 and dval < -0.03:
nota = " -> posible perdida excesiva de capacidad."
elif dtr < -0.03 and dval > 0.005:
nota = " -> se sacrifica train y validation mejora: puede ser regularizacion util."
print(f" {row['modelo']}: Dtrain={dtr:+.3f} Dval={dval:+.3f} "
f"Dgap={dgap:+.3f} Dtest={dte:+.3f} (test no decide).{nota}")
print("LIMITACION: diferencias de ~1 punto en 1500 ejemplos pueden ser ruido.")
print("DECISION: elegir por validacion; usar test una vez; no apilar tecnicas por fe.")
def interpretar_intervencion(ref, nueva, nombre="intervencion"):
# L1/L2/Dropout vs baseline: responder con numeros reales.
dtr = nueva["train_acc"] - ref["train_acc"]
dva = nueva["val_acc"] - ref["val_acc"]
dte = nueva["test_acc"] - ref["test_acc"]
dgap = nueva["gap_acc"] - ref["gap_acc"]
print(f"\n--- Lectura de la intervencion: {nombre} vs {ref['modelo']} ---")
print("OBSERVACION:")
print(f" train {ref['train_acc']:.3f}->{nueva['train_acc']:.3f} ({dtr:+.3f})")
print(f" val {ref['val_acc']:.3f}->{nueva['val_acc']:.3f} ({dva:+.3f})")
print(f" test {ref['test_acc']:.3f}->{nueva['test_acc']:.3f} ({dte:+.3f})")
print(f" gap {ref['gap_acc']:+.3f}->{nueva['gap_acc']:+.3f} ({dgap:+.3f})")
print("INFERENCIA:")
if dtr < -0.01:
print(" El accuracy de entrenamiento bajo: la intervencion redujo capacidad efectiva.")
if dva > 0.005:
print(" Validation mejoro: sacrificar train puede ser aceptable si generaliza mejor.")
elif dva < -0.01:
print(" Validation tambien bajo: evidencia compatible con regularizacion excesiva.")
else:
print(" Validation no mejoro de forma clara: la perdida de capacidad podria ser innecesaria.")
else:
print(" El entrenamiento no se deterioro de forma clara.")
if dva > 0.005:
print(" Validation mejoro sin un sacrificio evidente de train.")
else:
print(" Validation no mejoro de forma clara en esta corrida.")
if dgap < -0.01 and dva <= 0:
print(" El gap se redujo pero validation no mejoro: reducir el gap no es, por si solo, exito.")
print("LIMITACION: un lambda/r distinto podria cambiar el signo de estas diferencias.")
print("DECISION: si val mejoro, la tecnica es candidata; si no, ajustar intensidad o probar otra.")
print(" Test se reporta para inspeccion, no para elegir.")
def interpretar_barrido(df, col_x, nombre_eje="intensidad"):
print(f"\n--- Lectura del barrido ({nombre_eje}) ---")
print("OBSERVACION: train/val/test en cada nivel (evaluate, inferencia).")
d = df.sort_values(col_x).reset_index(drop=True)
for _, r in d.iterrows():
print(f" {nombre_eje}={r[col_x]}: train={r['train_acc']:.3f} "
f"val={r['val_acc']:.3f} test={r['test_acc']:.3f} gap={r['gap_acc']:+.3f}")
i_best = d["val_acc"].idxmax()
i_low, i_high = 0, len(d) - 1
print(f"COMPARACION: el mejor val_acc de la grilla es {nombre_eje}={d.loc[i_best, col_x]} "
f"({d.loc[i_best, 'val_acc']:.3f}).")
print("INFERENCIA:")
if d.loc[i_low, "val_acc"] + 0.01 < d.loc[i_best, "val_acc"] and i_best != i_low:
print(f" El nivel mas bajo ({d.loc[i_low, col_x]}) no es el de mayor val: "
"regularizacion nula/debil pudo ser insuficiente.")
if d.loc[i_high, "train_acc"] < 0.70 and d.loc[i_high, "val_acc"] < 0.70:
print(f" El nivel mas alto ({d.loc[i_high, col_x]}) deja train y val bajos: "
"evidencia compatible con intensidad excesiva / underfitting inducido.")
elif i_best == i_high:
print(" El nivel mas alto gano en validacion en ESTA grilla. No generaliza a 'mas es mejor'.")
print(" Mas regularizacion no significa automaticamente mejor modelo.")
print("LIMITACION: la grilla es discreta y pedagogica; un valor no explorado podria ser preferible.")
print("DECISION: elegir el nivel por VALIDACION. test_acc se grafica para inspeccion, no para seleccionar.")
def interpretar_early_stopping(techo, ejecutadas, mejor_epoca, fila_es, fila_base=None):
ahorradas = techo - ejecutadas
pct = 100.0 * ahorradas / techo if techo else 0.0
print("\n--- Lectura de Early Stopping ---")
print("OBSERVACION (tres relojes):")
print(f" maximo permitido = {techo}")
print(f" epocas ejecutadas = {ejecutadas}")
print(f" mejor epoca segun val_loss = {mejor_epoca}")
print(f" epocas_ahorradas = {ahorradas} porcentaje_ahorro = {pct:.1f}%")
print(" restore_best_weights=True: los pesos finales son los de la mejor epoca monitorizada,")
print(" no los de la ultima epoca ejecutada.")
print("INFERENCIA:")
if ahorradas > 0:
print(" Se evito entrenamiento posterior al criterio de parada. Compatible con")
print(" que continuar hasta el techo no aportaba (o no aportaba lo suficiente) segun val_loss.")
else:
print(" Se alcanzo el techo: la validacion aun mejoraba o el ruido supero min_delta.")
if mejor_epoca + 3 < ejecutadas:
print(f" El optimo ocurrio {ejecutadas - mejor_epoca} epocas antes del corte (patience).")
if fila_base is not None:
dval = fila_es["val_acc"] - fila_base["val_acc"]
print(f" Frente al baseline largo: Dval_acc={dval:+.3f}, "
f"Depocas={fila_es['epocas_ejecutadas'] - fila_base['epocas_ejecutadas']:+.0f}.")
print("LIMITACION: monitor=val_loss no maximiza necesariamente val_accuracy.")
print("DECISION: si el optimo esta claramente antes del techo, Early Stopping es una")
print(" intervencion barata y combinable. No sustituye a un lambda bien elegido.")
def interpretar_batchnorm(fila_sin, fila_con, hist_sin, hist_con, e_sin, e_con):
print("\n--- Lectura de Batch Normalization ---")
print("OBSERVACION:")
print(f" sin BN: val_acc={fila_sin['val_acc']:.3f} train_acc={fila_sin['train_acc']:.3f} "
f"tiempo={fila_sin['tiempo_s']:.1f}s epoca_mitad_val={e_sin}")
print(f" con BN: val_acc={fila_con['val_acc']:.3f} train_acc={fila_con['train_acc']:.3f} "
f"tiempo={fila_con['tiempo_s']:.1f}s epoca_mitad_val={e_con}")
print("INFERENCIA:")
dval = fila_con["val_acc"] - fila_sin["val_acc"]
if e_con < e_sin:
print(" Con BN, val_loss cruzo antes el punto medio inicio-minimo: compatible con")
print(" una dinamica de optimizacion mas favorable, no automaticamente con menos overfitting.")
elif e_con > e_sin:
print(" En esta corrida BN no acelero ese cruce. Tampoco 'refuta' a BatchNorm en general.")
else:
print(" La epoca de cruce fue similar. El valor de BN, si existe, habria que buscarlo")
print(" en la estabilidad de las curvas o en el accuracy, no en un atajo universal.")
if abs(dval) < 0.01:
print(" El accuracy final es similar. Eso NO vacia de informacion a BN: convergencia")
print(" o estabilidad tambien son resultados relevantes.")
elif dval > 0:
print(" Validation mejoro en ESTA configuracion. No se convierte en teorema.")
else:
print(" Validation no mejoro. BatchNorm no debe venderse solo como anti-overfitting.")
print("LIMITACION: BN anade parametros (gamma, beta) y cambia el regimen train/inferencia.")
print("DECISION: incluir BN si aporta estabilidad o val; no incluirlo por catalogo.")
def interpretar_matriz_confusion(cm, nombres=None):
nombres = list(nombres if nombres is not None else CLASS_NAMES)
cm_off = cm.copy().astype(int)
np.fill_diagonal(cm_off, 0)
pares = []
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
if i != j and cm_off[i, j] > 0:
pares.append((int(cm_off[i, j]), i, j))
pares.sort(reverse=True)
print("OBSERVACION (matriz): la diagonal son aciertos; fuera de ella, confusiones.")
if not pares:
print(" No hay confusiones extra-diagonales en este conjunto.")
return
print(" Confusiones mas frecuentes:")
for n, i, j in pares[:3]:
sim = abs(cm[j, i] - n) <= max(2, 0.35 * n)
sim_txt = "asimetrica" if not sim else f"relativamente simetrica (el inverso es {int(cm[j, i])})"
print(f" {nombres[i]} -> {nombres[j]}: {n} casos ({sim_txt}).")
top_n, i0, j0 = pares[0]
proximas = {"T-shirt/top", "Pullover", "Coat", "Shirt"}
print("INFERENCIA:")
if nombres[i0] in proximas and nombres[j0] in proximas:
print(f" {nombres[i0]} vs {nombres[j0]} son categorias visualmente proximas en Fashion-MNIST.")
print(" El error es compatible con solapamiento del problema; no demuestra un bug de codigo.")
else:
print(" La celda dominante no se interpreta aqui como error tipico de prendas parecidas.")
print(" No inventamos una causa visual si las clases no son las usualmente solapadas.")
print("LIMITACION: la matriz no dice si el umbral argmax es el adecuado para una clase;")
print(" precision y recall del reporte complementan esta lectura.")
print("DECISION: si una confusion domina, regularizar el promedio puede no resolverla;")
print(" habria que mirar esas dos clases en el classification_report.")
def interpretar_classification_report(y_true, y_pred, nombres=CLASS_NAMES):
rep = classification_report(
y_true, y_pred, target_names=nombres, digits=3, output_dict=True, zero_division=0
)
print(classification_report(y_true, y_pred, target_names=nombres,
digits=3, zero_division=0))
print("Como leer este reporte (y que inferir de ESTOS numeros):")
print(" precision = entre las veces que el modelo dijo k, cuantas acerto.")
print(" recall = entre los ejemplos que SI son k, cuantos recupero.")
print(" f1 = media armonica (penaliza el desequilibrio precision/recall).")
print(" support = ejemplos verdaderos de esa clase en ESTE conjunto.")
print(" macro avg = media no ponderada (cada clase pesa igual).")
print(" weighted = media ponderada por support.")
print("OBSERVACION / INFERENCIA por clase:")
hallazgo = False
for c in nombres:
if c not in rep:
continue
p, r = rep[c]["precision"], rep[c]["recall"]
if p >= 0.70 and r <= 0.55:
hallazgo = True
print(f" {c}: precision={p:.3f}, recall={r:.3f}. Cuando predice {c} suele acertar,")
print(" pero deja escapar una proporcion importante de los casos reales (conservador).")
elif r >= 0.70 and p <= 0.55:
hallazgo = True
print(f" {c}: precision={p:.3f}, recall={r:.3f}. Recupera gran parte de los casos reales,")
print(" pero genera mas falsos positivos (agresivo).")
if not hallazgo:
print(" Ninguna clase muestra un desequilibrio extremo precision/recall con los umbrales")
print(" usados aqui (0.70/0.55). Eso no implica que no haya clases mas debiles en F1.")
f1s = {c: rep[c]["f1-score"] for c in nombres if c in rep}
peores = sorted(f1s, key=f1s.get)[:3]
print(f" F1 mas bajos: {peores} -> {[round(f1s[c], 3) for c in peores]}")
macro, weighted = rep["macro avg"]["f1-score"], rep["weighted avg"]["f1-score"]
print(f" macro F1={macro:.3f} | weighted F1={weighted:.3f}")
if abs(macro - weighted) > 0.02:
print(" La diferencia macro vs weighted sugiere desempeno no uniforme entre clases")
print(" (o supports distintos). Con split estratificado, lo mas plausible es heterogeneidad de F1,")
print(" no un desbalance grave. No afirmamos causalidad solo por esa diferencia.")
else:
print(" Macro y weighted proximos: no hay una discrepancia fuerte atribuible a desbalance.")
print("LIMITACION: accuracy global puede ocultar una clase (p. ej. Shirt) que no generaliza.")
print(" Precision alta no implica recall alto. AUC, si se usa, no sustituye este perfil.")
return rep
def entrenar(model, epochs, nombre, callbacks_list=None, verbose=0):
# Envuelve fit() midiendo tiempo. El baseline tambien mostrara fit explicito.
t0 = time.perf_counter()
history = model.fit(
X_train, y_train,
validation_data=(X_val, y_val),
epochs=epochs,
batch_size=BATCH_SIZE,
callbacks=callbacks_list or [],
verbose=verbose,
)
tiempo = time.perf_counter() - t0
print(f"{nombre}: {tiempo:.1f} s, {len(history.history['loss'])} épocas ejecutadas")
return history, tiempo
print("Funciones listas. RESULTADOS tiene", len(RESULTADOS), "filas.")