304 KiB
304 KiB
In [1]:
try:
import optuna
except ImportError:
%pip install -q optuna
import optuna
import os, json, random, time, itertools, warnings
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
import tensorflow as tf
import torch
import torch.nn as nn
warnings.filterwarnings("ignore")
optuna.logging.set_verbosity(optuna.logging.WARNING)
tf.get_logger().setLevel("ERROR")
# Semillas: que la tarea sea reproducible para ti y para quien la revise
SEED = 42
random.seed(SEED); np.random.seed(SEED)
tf.random.set_seed(SEED); torch.manual_seed(SEED)
# Paleta Okabe-Ito (segura para daltonismo), en el orden fijo del curso
OKABE = ["#0072B2", "#E69F00", "#009E73", "#CC79A7", "#D55E00", "#56B4E9", "#F0E442"]
print("numpy ", np.__version__)
print("pandas ", pd.__version__)
print("tensorflow ", tf.__version__)
print("torch ", torch.__version__)
print("optuna ", optuna.__version__)[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.[0m[33m [0mNote: you may need to restart the kernel to use updated packages.
/usr/local/lib/python3.12/dist-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
numpy 2.1.0 pandas 2.3.3 tensorflow 2.21.0 torch 2.10.0a0+b4e4ee81d3.nv25.12 optuna 4.9.0
In [2]:
digits = load_digits()
X, y = digits.data, digits.target
print(f"X: {X.shape} (1,797 imágenes de 8x8 = 64 píxeles)")
print(f"y: {y.shape} clases: {np.unique(y)}")
print(f"Rango de los píxeles: {X.min():.0f} a {X.max():.0f}")
fig, axes = plt.subplots(2, 8, figsize=(11, 3))
for ax, img, lab in zip(axes.ravel(), digits.images, y):
ax.imshow(img, cmap="gray_r")
ax.set_title(str(lab), fontsize=10)
ax.axis("off")
fig.suptitle("Ejemplos del dataset — lecturas de medidor escritas a mano", y=1.04)
plt.tight_layout(); plt.show()
conteo = pd.Series(y).value_counts().sort_index()
fig, ax = plt.subplots(figsize=(7, 2.6))
ax.bar(conteo.index, conteo.values, color=OKABE[0])
ax.set_xlabel("dígito"); ax.set_ylabel("imágenes"); ax.set_xticks(range(10))
ax.set_title(f"Distribución de clases — entre {conteo.min()} y {conteo.max()} por dígito (balanceado)")
plt.tight_layout(); plt.show()X: (1797, 64) (1,797 imágenes de 8x8 = 64 píxeles) y: (1797,) clases: [0 1 2 3 4 5 6 7 8 9] Rango de los píxeles: 0 a 16
In [3]:
# ============================================================
# TU CÓDIGO AQUÍ
# ============================================================
# Paso 1: separar el test-set del resto (20% del total)
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.2, random_state=SEED, stratify=y)
# Paso 2: partir lo que queda (80% del total) en train (60% del total)
# y cross-validation (20% del total) -> 0.20 / 0.80 = 0.25 de lo que queda
X_train, X_cv, y_train, y_cv = train_test_split(
X_temp, y_temp, test_size=0.25, random_state=SEED, stratify=y_temp)In [4]:
# --- Verificación (no modifiques esta celda) ---
for _v in ["X_train", "X_cv", "X_test", "y_train", "y_cv", "y_test"]:
assert _v in dir(), f"No encuentro `{_v}` — revisa la celda anterior."
_n = len(X)
_frac = {"train": len(X_train) / _n, "cv": len(X_cv) / _n, "test": len(X_test) / _n}
assert abs(_frac["train"] - 0.60) < 0.02, f"train debería ser ~60% del total, es {_frac['train']:.1%}"
assert abs(_frac["cv"] - 0.20) < 0.02, f"cv debería ser ~20% del total, es {_frac['cv']:.1%}"
assert abs(_frac["test"] - 0.20) < 0.02, f"test debería ser ~20% del total, es {_frac['test']:.1%}"
assert len(X_train) + len(X_cv) + len(X_test) == _n, "Los tres conjuntos no suman el total"
# ¿estratificó? las proporciones por clase deben parecerse entre los tres
_p = [np.bincount(s, minlength=10) / len(s) for s in (y_train, y_cv, y_test)]
assert max(np.abs(_p[0] - _p[1]).max(), np.abs(_p[0] - _p[2]).max()) < 0.03, \
"Las proporciones por dígito difieren mucho entre conjuntos — ¿usaste stratify en las dos llamadas?"
print("✅ Split de 3 vías verificado")
print(f" train : {len(X_train):>5} imágenes ({_frac['train']:.0%})")
print(f" cv : {len(X_cv):>5} imágenes ({_frac['cv']:.0%})")
print(f" test : {len(X_test):>5} imágenes ({_frac['test']:.0%})")✅ Split de 3 vías verificado train : 1077 imágenes (60%) cv : 360 imágenes (20%) test : 360 imágenes (20%)
In [5]:
scaler = StandardScaler().fit(X_train) # <-- SOLO con train
X_train_s = scaler.transform(X_train)
X_cv_s = scaler.transform(X_cv)
X_test_s = scaler.transform(X_test)
print(f"Media del train escalado : {X_train_s.mean():+.4f} (debe ser ~0)")
print(f"Desv. del train escalado : {X_train_s.std():.4f} (debe ser ~1)")
print(f"Media del cv escalado : {X_cv_s.mean():+.4f} (NO es exactamente 0, y está bien:")
print(f" el cv no participó en el ajuste del scaler)")Media del train escalado : -0.0000 (debe ser ~0)
Desv. del train escalado : 0.9682 (debe ser ~1)
Media del cv escalado : +0.0001 (NO es exactamente 0, y está bien:
el cv no participó en el ajuste del scaler)
In [6]:
BITACORA = "experimentos.csv"
def iniciar_bitacora(path=BITACORA):
"""Borra la bitácora anterior. Útil si quieres re-correr la tarea desde cero."""
if os.path.exists(path):
os.remove(path)
print(f"Bitácora reiniciada: {path}")
def registrar_experimento(estrategia, framework, arquitectura, config, metricas,
segundos=None, path=BITACORA):
"""Agrega UNA fila a la bitácora de experimentos.
estrategia : 'grid' | 'random' | 'bayesiana' (cómo se eligió esta config)
framework : 'sklearn' | 'tensorflow' | 'pytorch'
arquitectura : nombre legible del tipo de modelo
config : dict de hiper-parámetros
metricas : dict con las métricas medidas en el CV-SET
segundos : cuánto tardó en entrenar (opcional)
"""
fila = {
"experimento": _siguiente_id(path),
"estrategia": estrategia,
"framework": framework,
"arquitectura": arquitectura,
"config": json.dumps(config, sort_keys=True), # reconstruible después
**{f"cv_{k}": v for k, v in metricas.items()},
"segundos": round(segundos, 2) if segundos is not None else None,
}
# también en columnas propias, para poder filtrar y graficar cómodamente
for k, v in config.items():
fila[f"hp_{k}"] = v
# Cada framework aporta columnas hp_* distintas, asi que NO se puede
# hacer append directo: pandas escribiria los valores por posicion y las
# filas quedarian desalineadas. Se reescribe el archivo completo, que con
# decenas de experimentos es instantaneo y siempre queda consistente.
previo = pd.read_csv(path) if os.path.exists(path) else pd.DataFrame()
completo = pd.concat([previo, pd.DataFrame([fila])], ignore_index=True)
completo.to_csv(path, index=False)
return fila
def _siguiente_id(path=BITACORA):
if not os.path.exists(path):
return 1
return len(pd.read_csv(path)) + 1
def leer_bitacora(path=BITACORA):
"""Devuelve la bitácora como DataFrame."""
if not os.path.exists(path):
return pd.DataFrame()
return pd.read_csv(path)
iniciar_bitacora()
print("Listo. Usa registrar_experimento(...) después de evaluar cada configuración.")Bitácora reiniciada: experimentos.csv Listo. Usa registrar_experimento(...) después de evaluar cada configuración.
In [7]:
# ============================================================
# TU CÓDIGO AQUÍ
# ============================================================
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
def evaluar(y_true, y_pred):
return {
"accuracy": accuracy_score(y_true, y_pred),
"precision": precision_score(y_true, y_pred, average="macro", zero_division=0),
"recall": recall_score(y_true, y_pred, average="macro", zero_division=0),
"f1": f1_score(y_true, y_pred, average="macro", zero_division=0),
}In [8]:
# --- Verificación (no modifiques esta celda) ---
assert "evaluar" in dir() and callable(evaluar), "No encuentro la función `evaluar`."
_yt = np.array([0, 1, 2, 3, 0, 1, 2, 3])
_yp = np.array([0, 1, 2, 3, 0, 1, 3, 3]) # 1 error de 8
_m = evaluar(_yt, _yp)
assert isinstance(_m, dict), "`evaluar` debe devolver un diccionario"
_esperadas = {"accuracy", "precision", "recall", "f1"}
assert set(_m.keys()) == _esperadas, f"Las llaves deben ser exactamente {_esperadas}, tienes {set(_m.keys())}"
assert abs(_m["accuracy"] - 0.875) < 1e-6, f"accuracy esperada 0.875, obtuve {_m['accuracy']}"
assert abs(_m["f1"] - 0.8666667) < 1e-3, \
f"f1 macro esperada ≈0.867, obtuve {_m['f1']:.4f} — ¿usaste average='macro'?"
print("✅ Función evaluar() verificada")
print(" sobre el ejemplo de prueba:", {k: round(v, 4) for k, v in _m.items()})✅ Función evaluar() verificada
sobre el ejemplo de prueba: {'accuracy': 0.875, 'precision': 0.9167, 'recall': 0.875, 'f1': 0.8667}
In [9]:
class ModeloEntrenado:
"""Envoltura con interfaz común para los tres frameworks."""
def __init__(self, framework, obj, arquitectura):
self.framework = framework
self.obj = obj
self.arquitectura = arquitectura
def predict(self, X):
if self.framework == "sklearn":
return self.obj.predict(X)
if self.framework == "tensorflow":
return self.obj.predict(X, verbose=0).argmax(axis=1)
# pytorch
self.obj.eval()
with torch.no_grad():
logits = self.obj(torch.tensor(X, dtype=torch.float32))
return logits.argmax(dim=1).numpy()
def _entrenar_sklearn(hp, X_tr, y_tr):
modelo = RandomForestClassifier(
n_estimators=hp["n_estimators"],
max_depth=hp["max_depth"],
min_samples_leaf=hp["min_samples_leaf"],
random_state=SEED, n_jobs=-1,
).fit(X_tr, y_tr)
return ModeloEntrenado("sklearn", modelo, "RandomForest")
def _entrenar_tensorflow(hp, X_tr, y_tr):
tf.keras.backend.clear_session()
tf.random.set_seed(SEED)
capas = [tf.keras.layers.Input(shape=(X_tr.shape[1],))]
for _ in range(hp["capas"]):
capas.append(tf.keras.layers.Dense(hp["unidades"], activation="relu"))
capas.append(tf.keras.layers.Dropout(hp["dropout"]))
capas.append(tf.keras.layers.Dense(10, activation="softmax"))
modelo = tf.keras.Sequential(capas)
modelo.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=hp["lr"]),
loss="sparse_categorical_crossentropy", metrics=["accuracy"])
modelo.fit(X_tr, y_tr, epochs=EPOCAS, batch_size=64, verbose=0)
return ModeloEntrenado("tensorflow", modelo, "MLP-Keras")
class _MLPTorch(nn.Module):
def __init__(self, n_entradas, capas, unidades, dropout):
super().__init__()
bloques, dim = [], n_entradas
for _ in range(capas):
bloques += [nn.Linear(dim, unidades), nn.ReLU(), nn.Dropout(dropout)]
dim = unidades
bloques.append(nn.Linear(dim, 10))
self.red = nn.Sequential(*bloques)
def forward(self, x):
return self.red(x)
def _entrenar_pytorch(hp, X_tr, y_tr):
torch.manual_seed(SEED)
modelo = _MLPTorch(X_tr.shape[1], hp["capas"], hp["unidades"], hp["dropout"])
opt = torch.optim.Adam(modelo.parameters(), lr=hp["lr"])
lossf = nn.CrossEntropyLoss()
Xt = torch.tensor(X_tr, dtype=torch.float32)
yt = torch.tensor(y_tr, dtype=torch.long)
ds = torch.utils.data.TensorDataset(Xt, yt)
dl = torch.utils.data.DataLoader(ds, batch_size=64, shuffle=True)
modelo.train()
for _ in range(EPOCAS):
for xb, yb in dl:
opt.zero_grad()
loss = lossf(modelo(xb), yb)
loss.backward()
opt.step()
return ModeloEntrenado("pytorch", modelo, "MLP-PyTorch")
EPOCAS = 30 # igual para los dos frameworks de redes, para que compitan parejo
def entrenar(framework, hp, X_tr=None, y_tr=None):
"""Entrena UNA configuración y devuelve un ModeloEntrenado con .predict(X).
framework: 'sklearn' | 'tensorflow' | 'pytorch'
hp : dict de hiper-parámetros de ese framework
"""
X_tr = X_train_s if X_tr is None else X_tr
y_tr = y_train if y_tr is None else y_tr
return {"sklearn": _entrenar_sklearn,
"tensorflow": _entrenar_tensorflow,
"pytorch": _entrenar_pytorch}[framework](hp, X_tr, y_tr)
# Prueba rápida de que los tres funcionan (y de cuánto tarda cada uno)
for _fw, _hp in [("sklearn", {"n_estimators": 50, "max_depth": 10, "min_samples_leaf": 1}),
("tensorflow", {"capas": 1, "unidades": 64, "dropout": 0.2, "lr": 1e-3}),
("pytorch", {"capas": 1, "unidades": 64, "dropout": 0.2, "lr": 1e-3})]:
_t0 = time.time()
_m = entrenar(_fw, _hp)
_seg = time.time() - _t0
_acc = (_m.predict(X_cv_s) == y_cv).mean()
print(f"{_fw:<12} arquitectura={_m.arquitectura:<14} accuracy_cv={_acc:.4f} ({_seg:.1f} s)")sklearn arquitectura=RandomForest accuracy_cv=0.9694 (0.1 s) tensorflow arquitectura=MLP-Keras accuracy_cv=0.9778 (1.1 s) pytorch arquitectura=MLP-PyTorch accuracy_cv=0.9639 (0.6 s)
In [10]:
REJILLAS = {
"sklearn": {"n_estimators": [50, 150], "max_depth": [5, 15], "min_samples_leaf": [1, 4]},
"tensorflow": {"capas": [1, 2], "unidades": [32, 128], "dropout": [0.0, 0.3]},
"pytorch": {"capas": [1, 2], "unidades": [32, 128], "dropout": [0.0, 0.3]},
}
LR_FIJO = 1e-3 # para los MLP en grid search
# ============================================================
# TU CÓDIGO AQUÍ
# ============================================================
for framework, rejilla in REJILLAS.items():
for combo in itertools.product(*rejilla.values()):
hp = dict(zip(rejilla.keys(), combo))
if framework in ("tensorflow", "pytorch"):
hp["lr"] = LR_FIJO
t0 = time.time()
modelo = entrenar(framework, hp)
segundos = time.time() - t0
y_pred = modelo.predict(X_cv_s) # sobre el CV-SET
metricas = evaluar(y_cv, y_pred)
registrar_experimento(estrategia="grid", framework=framework,
arquitectura=modelo.arquitectura, config=hp,
metricas=metricas, segundos=segundos)
print("Grid search terminado")Grid search terminado
In [11]:
# --- Verificación (no modifiques esta celda) ---
_b = leer_bitacora()
assert len(_b) > 0, "La bitácora está vacía — ¿llamaste a registrar_experimento?"
_g = _b[_b["estrategia"] == "grid"]
assert len(_g) == 24, f"Se esperaban 24 experimentos de grid (8 por framework), hay {len(_g)}"
assert set(_g["framework"].unique()) == {"sklearn", "tensorflow", "pytorch"}, \
f"Faltan frameworks en el grid: {set(_g['framework'].unique())}"
for _fw in ("sklearn", "tensorflow", "pytorch"):
assert len(_g[_g["framework"] == _fw]) == 8, f"{_fw} debería tener 8 combinaciones"
assert _g["cv_f1"].notna().all(), "Hay filas sin métrica cv_f1 — ¿pasaste bien el dict de métricas?"
assert _g["cv_f1"].max() > 0.80, \
f"El mejor f1 del grid es {_g['cv_f1'].max():.3f}, sospechosamente bajo — ¿evaluaste sobre el CV-set?"
print(f"✅ Grid search verificado: {len(_g)} experimentos registrados")
print(_g.groupby("framework")["cv_f1"].agg(["count", "mean", "max"]).round(4))✅ Grid search verificado: 24 experimentos registrados
count mean max
framework
pytorch 8 0.9701 0.9834
sklearn 8 0.9532 0.9752
tensorflow 8 0.9629 0.9777
In [12]:
rng = np.random.default_rng(SEED)
N_RANDOM = 8 # configuraciones por framework
# ============================================================
# TU CÓDIGO AQUÍ
# ============================================================
for framework in ("sklearn", "tensorflow", "pytorch"):
for _ in range(N_RANDOM):
if framework == "sklearn":
hp = {"n_estimators": int(rng.integers(30, 301)),
"max_depth": int(rng.integers(3, 26)),
"min_samples_leaf": int(rng.integers(1, 9))}
else:
hp = {"capas": int(rng.integers(1, 4)),
"unidades": int(rng.integers(16, 257)),
"dropout": float(rng.uniform(0.0, 0.5)),
"lr": float(10 ** rng.uniform(-4, -2))}
t0 = time.time()
modelo = entrenar(framework, hp)
segundos = time.time() - t0
y_pred = modelo.predict(X_cv_s) # sobre el CV-SET
metricas = evaluar(y_cv, y_pred)
registrar_experimento(estrategia="random", framework=framework,
arquitectura=modelo.arquitectura, config=hp,
metricas=metricas, segundos=segundos)
print("Random search terminado")Random search terminado
In [13]:
# --- Verificación (no modifiques esta celda) ---
_b = leer_bitacora()
_r = _b[_b["estrategia"] == "random"]
assert len(_r) == 24, f"Se esperaban 24 experimentos de random search, hay {len(_r)}"
for _fw in ("sklearn", "tensorflow", "pytorch"):
assert len(_r[_r["framework"] == _fw]) == N_RANDOM, f"{_fw} debería tener {N_RANDOM} configuraciones"
_nn = _r[_r["framework"].isin(["tensorflow", "pytorch"])]
assert _nn["hp_lr"].nunique() > 8, \
"Los learning rates se repiten demasiado — ¿los estás muestreando al azar?"
assert _nn["hp_lr"].min() < 1e-3 < _nn["hp_lr"].max(), \
"Los learning rates no cubren el rango 1e-4 a 1e-2 — revisa el muestreo log-uniforme"
assert _r["hp_n_estimators"].dropna().nunique() > 4, "n_estimators se repite demasiado"
print(f"✅ Random search verificado: {len(_r)} experimentos registrados")
print(f" learning rates muestreados: de {_nn['hp_lr'].min():.5f} a {_nn['hp_lr'].max():.5f}")
print(_r.groupby("framework")["cv_f1"].agg(["count", "mean", "max"]).round(4))✅ Random search verificado: 24 experimentos registrados
learning rates muestreados: de 0.00013 a 0.00611
count mean max
framework
pytorch 8 0.9560 0.9835
sklearn 8 0.9633 0.9780
tensorflow 8 0.9588 0.9834
In [14]:
N_TRIALS = 24
# ============================================================
# TU CÓDIGO AQUÍ
# ============================================================
def objetivo(trial):
framework = trial.suggest_categorical("framework", ["sklearn", "tensorflow", "pytorch"])
if framework == "sklearn":
hp = {"n_estimators": trial.suggest_int("n_estimators", 30, 300),
"max_depth": trial.suggest_int("max_depth", 3, 25),
"min_samples_leaf": trial.suggest_int("min_samples_leaf", 1, 8)}
else:
hp = {"capas": trial.suggest_int("capas", 1, 3),
"unidades": trial.suggest_int("unidades", 16, 256),
"dropout": trial.suggest_float("dropout", 0.0, 0.5),
"lr": trial.suggest_float("lr", 1e-4, 1e-2, log=True)}
t0 = time.time()
modelo = entrenar(framework, hp)
segundos = time.time() - t0
metricas = evaluar(y_cv, modelo.predict(X_cv_s))
registrar_experimento(estrategia="bayesiana", framework=framework,
arquitectura=modelo.arquitectura, config=hp,
metricas=metricas, segundos=segundos)
return metricas["f1"]
estudio = optuna.create_study(direction="maximize",
sampler=optuna.samplers.TPESampler(seed=SEED))
estudio.optimize(objetivo, n_trials=N_TRIALS)
print("Mejor f1 en cv:", estudio.best_value)
print("Mejores parámetros:", estudio.best_params)Mejor f1 en cv: 0.9833983001975856
Mejores parámetros: {'framework': 'tensorflow', 'capas': 1, 'unidades': 146, 'dropout': 0.07046211248738132, 'lr': 0.0040215545266902904}
In [15]:
# --- Verificación (no modifiques esta celda) ---
_b = leer_bitacora()
_o = _b[_b["estrategia"] == "bayesiana"]
assert len(_o) == N_TRIALS, f"Se esperaban {N_TRIALS} trials bayesianos, hay {len(_o)}"
assert "estudio" in dir(), "No encuentro el objeto `estudio` de Optuna."
assert len(_o["framework"].unique()) >= 2, \
"Optuna probó un solo framework — ¿incluiste 'framework' como suggest_categorical?"
assert _o["cv_f1"].max() > 0.80, f"El mejor f1 bayesiano es {_o['cv_f1'].max():.3f}, revisa la evaluación"
print(f"✅ Optimización bayesiana verificada: {len(_o)} trials registrados")
print(f" frameworks explorados: {dict(_o['framework'].value_counts())}")
print(f" mejor f1 en cv: {_o['cv_f1'].max():.4f}")✅ Optimización bayesiana verificada: 24 trials registrados
frameworks explorados: {'pytorch': np.int64(12), 'tensorflow': np.int64(8), 'sklearn': np.int64(4)}
mejor f1 en cv: 0.9834
In [16]:
bit = leer_bitacora()
print(f"Bitácora: {len(bit)} experimentos\n")
print(bit.groupby(["estrategia", "framework"])["cv_f1"].agg(["count", "mean", "max"]).round(4))Bitácora: 72 experimentos
count mean max
estrategia framework
bayesiana pytorch 12 0.9708 0.9806
sklearn 4 0.9096 0.9613
tensorflow 8 0.9422 0.9834
grid pytorch 8 0.9701 0.9834
sklearn 8 0.9532 0.9752
tensorflow 8 0.9629 0.9777
random pytorch 8 0.9560 0.9835
sklearn 8 0.9633 0.9780
tensorflow 8 0.9588 0.9834
In [17]:
fig, axes = plt.subplots(1, 3, figsize=(14, 3.8))
# (a) distribución del f1 por estrategia
ests = ["grid", "random", "bayesiana"]
datos = [bit[bit["estrategia"] == e]["cv_f1"].values for e in ests]
bp = axes[0].boxplot(datos, labels=ests, patch_artist=True)
for caja in bp["boxes"]:
caja.set_facecolor(OKABE[0]); caja.set_alpha(0.55)
axes[0].set_ylabel("f1 en cross-validation")
axes[0].set_title("(a) Distribución por estrategia")
axes[0].grid(alpha=0.3, axis="y")
# (b) f1 por framework
fws = ["sklearn", "tensorflow", "pytorch"]
datos2 = [bit[bit["framework"] == f]["cv_f1"].values for f in fws]
bp2 = axes[1].boxplot(datos2, labels=fws, patch_artist=True)
for caja in bp2["boxes"]:
caja.set_facecolor(OKABE[2]); caja.set_alpha(0.55)
axes[1].set_ylabel("f1 en cross-validation")
axes[1].set_title("(b) Distribución por framework")
axes[1].grid(alpha=0.3, axis="y")
# (c) mejor-hasta-ahora, por estrategia
for i, e in enumerate(ests):
sub = bit[bit["estrategia"] == e].reset_index(drop=True)
axes[2].plot(range(1, len(sub) + 1), sub["cv_f1"].cummax(),
marker="o", ms=3, color=OKABE[i], label=e)
axes[2].set_xlabel("experimento dentro de la estrategia")
axes[2].set_ylabel("mejor f1 hasta ahora")
axes[2].set_title("(c) Convergencia")
axes[2].legend(); axes[2].grid(alpha=0.3)
plt.tight_layout(); plt.show()In [18]:
# Costo vs beneficio: ¿los modelos más lentos son mejores?
fig, ax = plt.subplots(figsize=(7, 4))
for i, fw in enumerate(fws):
sub = bit[bit["framework"] == fw]
ax.scatter(sub["segundos"], sub["cv_f1"], s=42, alpha=0.75,
color=OKABE[i], label=fw, edgecolors="white", linewidths=0.6)
ax.set_xlabel("segundos de entrenamiento")
ax.set_ylabel("f1 en cross-validation")
ax.set_title("Costo de entrenamiento vs desempeño")
ax.legend(); ax.grid(alpha=0.3)
plt.tight_layout(); plt.show()
print("Top 5 configuraciones de toda la bitácora:")
cols = ["experimento", "estrategia", "framework", "arquitectura", "cv_f1", "cv_accuracy", "segundos"]
print(bit.nlargest(5, "cv_f1")[cols].to_string(index=False))Top 5 configuraciones de toda la bitácora:
experimento estrategia framework arquitectura cv_f1 cv_accuracy segundos
47 random pytorch MLP-PyTorch 0.983540 0.983333 0.54
58 bayesiana tensorflow MLP-Keras 0.983398 0.983333 1.26
24 grid pytorch MLP-PyTorch 0.983362 0.983333 0.40
36 random tensorflow MLP-Keras 0.983360 0.983333 1.50
37 random tensorflow MLP-Keras 0.980661 0.980556 2.00
In [19]:
# ============================================================
# TU CÓDIGO AQUÍ
# ============================================================
bit = leer_bitacora()
mejor_fila = bit.loc[bit["cv_f1"].idxmax()]
mejor_config = json.loads(mejor_fila["config"])
X_full = np.vstack([X_train_s, X_cv_s])
y_full = np.concatenate([y_train, y_cv])
modelo_final = entrenar(mejor_fila["framework"], mejor_config, X_full, y_full)
metricas_test = evaluar(y_test, modelo_final.predict(X_test_s))
print(metricas_test){'accuracy': 0.975, 'precision': 0.9757523257523258, 'recall': 0.9748369798369797, 'f1': 0.9749683984469085}
In [20]:
# --- Verificación (no modifiques esta celda) ---
for _v in ["mejor_fila", "mejor_config", "metricas_test", "modelo_final"]:
assert _v in dir(), f"No encuentro `{_v}` — revisa la celda anterior."
assert isinstance(mejor_config, dict), "`mejor_config` debe ser un dict (usa json.loads)"
assert set(metricas_test.keys()) == {"accuracy", "precision", "recall", "f1"}, \
"`metricas_test` debe tener las mismas 4 llaves que devuelve evaluar()"
_bit = leer_bitacora()
assert abs(mejor_fila["cv_f1"] - _bit["cv_f1"].max()) < 1e-9, \
"`mejor_fila` no es la de mayor cv_f1 de la bitácora"
assert metricas_test["f1"] > 0.85, \
f"f1 en test es {metricas_test['f1']:.3f}, muy bajo — ¿evaluaste con X_test_s (escalado)?"
print("✅ Selección final verificada")
print(f" modelo elegido : {mejor_fila['arquitectura']} ({mejor_fila['framework']}, "
f"estrategia {mejor_fila['estrategia']})")
print(f" f1 en cv : {mejor_fila['cv_f1']:.4f}")
print(f" f1 en test : {metricas_test['f1']:.4f}")✅ Selección final verificada modelo elegido : MLP-PyTorch (pytorch, estrategia random) f1 en cv : 0.9835 f1 en test : 0.9750
In [21]:
from sklearn.metrics import confusion_matrix, classification_report
bit = leer_bitacora()
print("=" * 68)
print("REPORTE FINAL — Lector automático de medidores")
print("=" * 68)
print(f"\nExperimentos corridos : {len(bit)}")
print(f" por grid search : {(bit['estrategia'] == 'grid').sum()}")
print(f" por random search : {(bit['estrategia'] == 'random').sum()}")
print(f" por optimización bayesiana: {(bit['estrategia'] == 'bayesiana').sum()}")
print(f"Tiempo total de entrenamiento: {bit['segundos'].sum():.0f} s")
print(f"\nMODELO ELEGIDO")
print(f" arquitectura : {mejor_fila['arquitectura']}")
print(f" framework : {mejor_fila['framework']}")
print(f" hallado por : {mejor_fila['estrategia']} (experimento #{mejor_fila['experimento']})")
print(f" hiper-parámetros:")
for k, v in mejor_config.items():
print(f" {k:<18} = {v}")
print(f"\nDESEMPEÑO")
print(f" {'métrica':<12} {'cross-validation':>18} {'test':>10} {'diferencia':>12}")
for m in ["accuracy", "precision", "recall", "f1"]:
cv_v, te_v = mejor_fila[f"cv_{m}"], metricas_test[m]
print(f" {m:<12} {cv_v:>18.4f} {te_v:>10.4f} {te_v - cv_v:>+12.4f}")
print(f"\n El número que va al informe es el de TEST: f1 = {metricas_test['f1']:.4f}")
print(" (el de cv está sesgado al alza: esa configuración se eligió justamente por ser la mejor ahí)")
print("=" * 68)====================================================================
REPORTE FINAL — Lector automático de medidores
====================================================================
Experimentos corridos : 72
por grid search : 24
por random search : 24
por optimización bayesiana: 24
Tiempo total de entrenamiento: 49 s
MODELO ELEGIDO
arquitectura : MLP-PyTorch
framework : pytorch
hallado por : random (experimento #47)
hiper-parámetros:
capas = 2
dropout = 0.39036451551098394
lr = 0.0008276210937283387
unidades = 185
DESEMPEÑO
métrica cross-validation test diferencia
accuracy 0.9833 0.9750 -0.0083
precision 0.9842 0.9758 -0.0084
recall 0.9832 0.9748 -0.0084
f1 0.9835 0.9750 -0.0086
El número que va al informe es el de TEST: f1 = 0.9750
(el de cv está sesgado al alza: esa configuración se eligió justamente por ser la mejor ahí)
====================================================================
In [22]:
fig, axes = plt.subplots(1, 2, figsize=(13, 4.4))
# matriz de confusión en test
cm = confusion_matrix(y_test, modelo_final.predict(X_test_s))
im = axes[0].imshow(cm, cmap="Blues")
axes[0].set_xticks(range(10)); axes[0].set_yticks(range(10))
axes[0].set_xlabel("dígito predicho"); axes[0].set_ylabel("dígito real")
axes[0].set_title(f"Matriz de confusión en TEST — {mejor_fila['arquitectura']}")
for i in range(10):
for j in range(10):
if cm[i, j]:
axes[0].text(j, i, cm[i, j], ha="center", va="center", fontsize=8,
color="white" if cm[i, j] > cm.max() / 2 else "black")
fig.colorbar(im, ax=axes[0], fraction=0.046)
# cv vs test del modelo elegido
ms = ["accuracy", "precision", "recall", "f1"]
xs = np.arange(len(ms)); w = 0.36
axes[1].bar(xs - w/2, [mejor_fila[f"cv_{m}"] for m in ms], w, label="cross-validation", color=OKABE[0])
axes[1].bar(xs + w/2, [metricas_test[m] for m in ms], w, label="test", color=OKABE[1])
axes[1].set_xticks(xs); axes[1].set_xticklabels(ms)
axes[1].set_ylim(0, 1.05); axes[1].legend(); axes[1].grid(alpha=0.3, axis="y")
axes[1].set_title("Modelo elegido: cross-validation vs test")
plt.tight_layout(); plt.show()
print("\nReporte por dígito (test-set):\n")
print(classification_report(y_test, modelo_final.predict(X_test_s), digits=3))
Reporte por dígito (test-set):
precision recall f1-score support
0 1.000 0.972 0.986 36
1 0.897 0.972 0.933 36
2 0.972 1.000 0.986 35
3 1.000 1.000 1.000 37
4 0.972 0.972 0.972 36
5 1.000 0.973 0.986 37
6 1.000 0.972 0.986 36
7 1.000 1.000 1.000 36
8 0.970 0.914 0.941 35
9 0.946 0.972 0.959 36
accuracy 0.975 360
macro avg 0.976 0.975 0.975 360
weighted avg 0.976 0.975 0.975 360
In [23]:
# Los dígitos que el modelo confunde: útil para el informe
y_pred_test = modelo_final.predict(X_test_s)
errores = np.where(y_pred_test != y_test)[0]
print(f"El modelo se equivocó en {len(errores)} de {len(y_test)} imágenes de test "
f"({100 * len(errores) / len(y_test):.1f}%)\n")
if len(errores):
n = min(10, len(errores))
fig, axes = plt.subplots(1, n, figsize=(1.35 * n, 2.1))
axes = np.atleast_1d(axes)
for ax, idx in zip(axes, errores[:n]):
ax.imshow(X_test[idx].reshape(8, 8), cmap="gray_r")
ax.set_title(f"real {y_test[idx]}\npred {y_pred_test[idx]}", fontsize=8)
ax.axis("off")
fig.suptitle("Errores del modelo final en el test-set", y=1.12)
plt.tight_layout(); plt.show()El modelo se equivocó en 9 de 360 imágenes de test (2.5%)