En esta tarea construiremos un detector de sismos: un clasificador que decide si una ventana de 30 segundos de movimiento del suelo en la componente vertical a 100 Hz contiene un sismo o solo ruido. Las ventanas provienen del paquete del curso mlgeo_synth con una semilla fija, así que cada evento tiene una magnitud, una distancia y una relación señal-ruido conocidas; el instructor conserva una variante con semilla oculta que usa para verificar por muestreo los resultados entregados.
Esta es la misma tarea de detección que la CNN 1D de la lección 4.3, sobre un conjunto de datos nuevo y con una familia de modelos distinta. Un MLP no tiene invarianza a la traslación: no puede aprender que un sismo en el segundo 8 y un sismo en el segundo 14 son la misma cosa. Por eso le entregamos el espectro logarítmico de amplitud de cada ventana, que descarta el tiempo de llegada y conserva el contenido de frecuencias que separa a los eventos (energía de ondícula limitada en banda) del ruido (un espectro de ley de potencias). El capítulo 2.6 construyó esta transformación; aquí se gana su lugar.
Practicaremos las habilidades de las lecciones 4.1, 4.2 y 4.5: construir y entrenar un MLP en PyTorch, correr un experimento controlado de arquitectura, diagnosticar entrenamientos rotos a partir de sus curvas, evaluar con honestidad contra un modelo de referencia y cuantificar la incertidumbre con un ensamble profundo pequeño.
Importación de bibliotecas¶
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
%matplotlib inline1. Construir y entrenar un detector MLP (20 puntos)¶
Seguimos estos pasos:
- inspeccionar los datos (2 puntos)
- división y escalado conscientes de la fuga de datos (4 puntos)
- completar el esqueleto del modelo (4 puntos)
- escribir el bucle de entrenamiento (8 puntos)
- curvas de aprendizaje (2 puntos)
import mlgeo_synth
fs = 100.0 # sampling rate (Hz)
X_wave, y, metas = mlgeo_synth.seismogram_dataset(
n_events=600, n_noise=600, fs=fs, duration_s=30.0, seed=2026)
print(X_wave.shape, y.shape)(1200, 3000) (1200,)
Cada fila de X_wave es una ventana de 30 s (3000 muestras); y vale 1 para las ventanas de evento y 0 para las ventanas de ruido. Para las ventanas de evento, metas registra los tiempos verdaderos de llegada de P y de S y la relación señal-ruido.
La celda de abajo calcula la entrada del modelo: el espectro logarítmico de amplitud de cada ventana, restringido a la banda de 0.2–30 Hz donde viven tanto la energía de la ondícula como el ruido. Esta es la representación lista para IA que verá el MLP.
freqs = np.fft.rfftfreq(X_wave.shape[1], d=1/fs)
band = (freqs >= 0.2) & (freqs <= 30.0)
X_spec = np.log10(np.abs(np.fft.rfft(X_wave, axis=1))[:, band] + 1e-10).astype(np.float32)
f_band = freqs[band]
print(X_spec.shape)(1200, 895)
1.1 Inspeccionar los datos¶
Tarea: reporte los conteos por clase y luego grafique una ventana de evento y una ventana de ruido — la forma de onda a la izquierda, su espectro logarítmico de amplitud a la derecha (2 puntos). Rotule los ejes con unidades (tiempo en s, frecuencia en Hz). Use metas para marcar los tiempos de llegada de P y de S sobre la forma de onda del evento.
# TODO: class counts; one event and one noise window, waveform + spectrum1.2 División y escalado conscientes de la fuga de datos¶
Tarea: divida en conjuntos de entrenamiento (60 %), validación (20 %) y prueba (20 %), estratificados por la etiqueta, y luego estandarice los espectros (4 puntos).
- Primero separe el 20 % del conjunto de prueba (
random_state=42,stratify=y), y después divida el resto 75/25 en entrenamiento y validación (random_state=42, estratificado de nuevo). - Ajuste un
StandardScalersolo sobre el conjunto de entrenamiento y aplíquelo a los tres conjuntos. Convierta los resultados afloat32. - Nombre los arreglos
X_train, X_val, X_testy las etiquetasy_train, y_val, y_test.
El conjunto de validación guía las decisiones de entrenamiento (arquitectura, criterio de parada); el conjunto de prueba se toca una sola vez, en la sección 4. En la sección 4.2 usted explicará qué se habría filtrado si el escalador se hubiera ajustado sobre el conjunto de datos completo.
# TODO: train/val/test split, scaler fit on train only1.3 El esqueleto del modelo¶
Tarea: complete el esqueleto de abajo (4 puntos). Dos capas ocultas, ambas de tamaño width, una ReLU después de cada una, y una capa lineal final hacia n_classes salidas sin activación — nn.CrossEntropyLoss espera logits crudos.
class DetectorMLP(nn.Module):
"""MLP detector: log amplitude spectrum in, 2 class logits out."""
def __init__(self, n_in, width=32, n_classes=2):
super().__init__()
# TODO: define the layers
def forward(self, x):
# TODO: return the logits
raise NotImplementedError1.4 El bucle de entrenamiento¶
Tarea: escriba desde cero una función de entrenamiento y entrene un modelo con width=32 durante 25 épocas (8 puntos). Esta es la receta de cinco pasos de la lección 4.1 — conjunto de datos, modelo, pérdida, optimizador, bucle — y usted debe ser capaz de producirla sin ayuda.
Requisitos para train_detector:
- minilotes de 64, rebarajados en cada época (basta con una permutación aleatoria nueva de los índices de entrenamiento — no se requiere ningún
DataLoader); nn.CrossEntropyLossytorch.optim.Adamconlr=1e-3;- por época, registre la pérdida media de entrenamiento, la pérdida de validación y la exactitud de validación en un diccionario
history— calcule las cantidades de validación bajotorch.no_grad()con el modelo en modoeval(), y vuelva a ponerlo en modotrain()después; - fije todas las semillas (
torch.manual_seedpara la inicialización del modelo antes de construirlo, el generador denumpypara el barajado) de modo que volver a correr reproduzca sus números.
# TODO: write train_detector(model, X_train, y_train, X_val, y_val,
# n_epochs=25, lr=1e-3, batch_size=64, seed=0)
# returning history = {"train_loss": [...], "val_loss": [...], "val_acc": [...]}
# then train a width=32 DetectorMLP with seed 01.5 Curvas de aprendizaje¶
Tarea: grafique la pérdida de entrenamiento y la de validación en un panel y la exactitud de validación en otro (2 puntos). Diga en una oración si el modelo está sobreajustando, subajustando o ninguna de las dos cosas, y señale la evidencia en las curvas.
# TODO: learning curves2. Experimento de arquitectura: el ancho (15 puntos)¶
Un experimento controlado: mantenga todo fijo y varíe el ancho de las capas ocultas. Como una sola corrida de entrenamiento es un sorteo aleatorio (inicialización y orden de los lotes), cada configuración se entrena con tres semillas, y la dispersión entre semillas es parte del resultado — la lección 4.5 le pone nombre a una diferencia menor que la dispersión entre semillas: ruido.
- barrido (8 puntos)
- gráfica con barras de error (4 puntos)
- elegir y justificar (3 puntos)
2.1 El barrido¶
Tarea: entrene un DetectorMLP para cada ancho en [8, 32, 128] y cada semilla en [0, 1, 2] — nueve corridas — y registre la exactitud final de validación de cada una (8 puntos). Reutilice train_detector sin cambios: 25 épocas, lr=1e-3, tamaño de lote 64. Las nueve corridas toman menos de un minuto en una laptop.
# TODO: 3 widths x 3 seeds, record final validation accuracy2.2 Barras de error¶
Tarea: grafique la exactitud de validación frente al ancho (eje x en escala logarítmica), mostrando para cada ancho la media entre semillas y barras de error que abarquen el rango mín-máx entre semillas (4 puntos).
# TODO: mean with min-max error bars across seeds2.3 Elegir y justificar¶
Tarea: elija un ancho y defienda la elección en dos o tres oraciones (3 puntos). Su justificación debe comparar las diferencias de exactitud entre anchos contra la dispersión entre semillas, y tener en cuenta el conteo de parámetros: una ganancia que cuesta 16 veces los parámetros más vale que sea mayor que las barras de error.
3. Diagnosticar dos entrenamientos rotos (10 puntos)¶
La celda de abajo entrena dos configuraciones amañadas sobre este conjunto de datos y grafica, para cada una, la pérdida de entrenamiento por paso y la exactitud de validación por época. También imprime la fracción de ventanas de validación que cada modelo final llama evento. Ambas corridas están rotas de una manera distinta — las mismas patologías que la lección 4.5 le enseñó a leer.
Para cada corrida, anote en la celda de respuesta: la patología (2 puntos), la evidencia en las curvas que la identifica (2 puntos) y el primer arreglo que intentaría (1 punto). Diagnostique a partir de las curvas antes de leer el código de configuración — esa es la habilidad que se califica.
def make_broken_runs():
"""Two rigged training runs. Diagnose from the curves before reading this code."""
Xb_trainval, _, yb_trainval, _ = train_test_split(
X_spec, y, test_size=0.2, random_state=7, stratify=y)
Xb_train, Xb_val, yb_train, yb_val = train_test_split(
Xb_trainval, yb_trainval, test_size=0.25, random_state=7, stratify=yb_trainval)
scaler_b = StandardScaler().fit(Xb_train)
Xb_train = scaler_b.transform(Xb_train).astype(np.float32)
Xb_val = scaler_b.transform(Xb_val).astype(np.float32)
configs = {
"Run A": dict(lr=5.0, ordered=False, n_epochs=12),
"Run B": dict(lr=0.5, ordered=True, n_epochs=12),
}
Xt, yt = torch.from_numpy(Xb_train), torch.from_numpy(yb_train)
Xv, yv = torch.from_numpy(Xb_val), torch.from_numpy(yb_val)
results = {}
for name, cfg in configs.items():
torch.manual_seed(0)
model = nn.Sequential(
nn.Linear(Xb_train.shape[1], 32), nn.ReLU(),
nn.Linear(32, 32), nn.ReLU(), nn.Linear(32, 2))
optimizer = torch.optim.SGD(model.parameters(), lr=cfg["lr"])
loss_fn = nn.CrossEntropyLoss()
rng = np.random.default_rng(0)
step_loss, val_acc = [], []
for epoch in range(cfg["n_epochs"]):
if cfg["ordered"]:
order = np.argsort(yb_train, kind="stable") # noise first, events last
else:
order = rng.permutation(len(yb_train))
for i in range(0, len(yb_train), 64):
idx = order[i:i + 64]
optimizer.zero_grad()
loss = loss_fn(model(Xt[idx]), yt[idx])
loss.backward()
optimizer.step()
step_loss.append(loss.item())
model.eval()
with torch.no_grad():
pred_val = model(Xv).argmax(dim=1)
val_acc.append((pred_val == yv).float().mean().item())
model.train()
results[name] = dict(step_loss=np.array(step_loss), val_acc=val_acc,
frac_event=pred_val.float().mean().item())
return results
broken = make_broken_runs()fig, axes = plt.subplots(2, 2, figsize=(10, 6))
for row, (name, res) in enumerate(broken.items()):
ax = axes[row, 0]
ax.semilogy(res["step_loss"])
ax.set_xlabel("training step")
ax.set_ylabel("training loss")
ax.set_title(f"{name}: per-step training loss")
ax.grid(alpha=0.3)
ax = axes[row, 1]
ax.plot(np.arange(1, len(res["val_acc"]) + 1), res["val_acc"], marker="o")
ax.axhline(0.5, color="gray", ls="--", label="chance")
ax.set_ylim(0.3, 1.0)
ax.set_xlabel("epoch")
ax.set_ylabel("validation accuracy")
ax.set_title(f"{name}: validation accuracy")
ax.legend()
ax.grid(alpha=0.3)
print(f"{name}: final model calls {res['frac_event']:.0%} of validation windows an event")
plt.tight_layout()Run A: final model calls 3% of validation windows an event
Run B: final model calls 100% of validation windows an event

Corrida A
- Patología:
- Evidencia:
- Arreglo:
Corrida B
- Patología:
- Evidencia:
- Arreglo:
4. Evaluación honesta (15 puntos)¶
El puntaje de un detector no significa nada por sí solo. Establecemos qué logran un modelo trivial y un modelo lineal sobre la misma división, auditamos el flujo de trabajo en busca de fugas de datos y reportamos cada métrica con su incertidumbre — el diseño de la lección 4.5 y las reglas de la tabla de clasificación de 3.5 y 4.10.
- modelos de referencia primero (4 puntos)
- auditoría de fuga de datos (3 puntos)
- métricas con incertidumbre (6 puntos)
- veredicto (2 puntos)
4.1 Modelos de referencia primero¶
Tarea: calcule dos modelos de referencia sobre el conjunto de prueba (4 puntos): el modelo de referencia de la clase mayoritaria, y una LogisticRegression(max_iter=5000) entrenada sobre los mismos espectros estandarizados (X_train). Reporte ambas exactitudes de prueba.
# TODO: majority-class baseline and logistic-regression baseline on the test set4.2 Auditoría de fuga de datos¶
Tarea: responda en tres oraciones (3 puntos). (1) ¿Por qué debe ajustarse el StandardScaler solo sobre el conjunto de entrenamiento — qué se filtra exactamente si se ajusta sobre el conjunto de datos completo antes de dividir? (2) ¿Por qué es seguro aplicar la transformación de espectro por ventana (la celda de X_spec) antes de dividir? (3) Nombre una decisión que usted tomó en este cuaderno usando el conjunto de validación, y confirme que el conjunto de prueba no participó en ella.
4.3 Métricas con incertidumbre¶
Un conjunto de prueba de 240 ventanas es una muestra, no la verdad; el ayudante de abajo lo remuestrea por bootstrap para ponerle un intervalo de confianza a una exactitud.
Tarea: reporte la exactitud de prueba con un intervalo de confianza bootstrap del 95 % para su MLP entrenado de la sección 1 y para el modelo de referencia de regresión logística (6 puntos).
def bootstrap_ci(y_true, y_pred, n_boot=2000, seed=0):
"""95% bootstrap confidence interval for accuracy."""
rng = np.random.default_rng(seed)
n = len(y_true)
accs = np.empty(n_boot)
for b in range(n_boot):
idx = rng.integers(0, n, n)
accs[b] = np.mean(y_true[idx] == y_pred[idx])
return np.percentile(accs, [2.5, 97.5])# TODO: test accuracy with 95% CI, MLP and logistic baseline4.4 Veredicto¶
Tarea: diga si el MLP le gana al modelo de referencia logístico, usando los intervalos de confianza, y explique el resultado en una o dos oraciones (2 puntos). Si los intervalos se solapan, dígalo con todas sus letras — aplica la regla de la lección 4.5: si su modelo profundo no puede ganarle al modelo de referencia lineal, el problema son los datos o las características, no una capa faltante. ¿Qué tiene esta representación que vuelve tan competitivo a un modelo lineal, y a qué modelo del capítulo 4 recurriría usted para hacerlo mejor sobre las formas de onda crudas?
5. Incertidumbre a partir de un ensamble profundo pequeño (12 puntos)¶
Reentrenar la misma arquitectura con semillas distintas da un ensamble profundo (lección 4.5, Pilar 2): los miembros coinciden donde los datos hablan con claridad y discrepan donde no.
- entrenar 3 miembros (4 puntos)
- dispersión frente a error (5 puntos)
- enunciado de calibración (3 puntos)
5.1 Entrenar tres miembros¶
Tarea: entrene tres modelos con width=32 con las semillas 100, 101 y 102 — los mismos datos, los mismos hiperparámetros que en la sección 1 — y recopile la probabilidad de «evento» predicha por cada miembro sobre el conjunto de prueba (4 puntos). Apílelas en un arreglo member_probs de forma (3, n_test) (aplique softmax a los logits y quédese con la columna 1).
# TODO: three members, member_probs of shape (3, n_test)5.2 Dispersión frente a error¶
Tarea: reporte la exactitud de la predicción media del ensamble, luego llame a plot_spread_vs_error(member_probs, y_test) y describa el patrón en una oración (5 puntos). El ayudante agrupa las ventanas de prueba en terciles de dispersión del ensamble (la desviación estándar de las tres probabilidades predichas) y grafica la exactitud en cada grupo.
def plot_spread_vs_error(member_probs, y_true):
"""Accuracy within terciles of ensemble spread."""
P = np.asarray(member_probs)
mean_p, std_p = P.mean(axis=0), P.std(axis=0)
correct = (mean_p > 0.5).astype(int) == np.asarray(y_true)
edges = np.quantile(std_p, [0, 1/3, 2/3, 1.0])
edges[-1] += 1e-9
labels = ["low spread", "medium spread", "high spread"]
accs = []
for lo, hi in zip(edges[:-1], edges[1:]):
in_bin = (std_p >= lo) & (std_p < hi)
accs.append(correct[in_bin].mean())
fig, ax = plt.subplots(figsize=(5.5, 3.5))
ax.bar(labels, accs, edgecolor="black")
ax.axhline(correct.mean(), color="black", ls="--",
label=f"overall accuracy {correct.mean():.2f}")
ax.set_ylabel("accuracy in bin")
ax.set_ylim(0.4, 1.0)
ax.set_title("Ensemble spread vs error")
ax.legend()
ax.grid(axis="y", alpha=0.3)
return accs# TODO: ensemble-mean accuracy, then plot_spread_vs_error(member_probs, y_test)5.3 Enunciado de calibración¶
Tarea: compare la confianza media del ensamble contra su exactitud y escriba una oración de calibración (3 puntos). Calcule la confianza de cada predicción de prueba (el mayor entre mean_p y 1 - mean_p), promédiela sobre el conjunto de prueba y póngala junto a la exactitud del ensamble de 5.2. Su oración debe nombrar la dirección y el tamaño aproximado de la mala calibración — por ejemplo: «el ensamble afirma un X% de confianza pero acierta el Y% de las veces, así que está sobreconfiado en unos Z puntos».
# TODO: mean confidence vs accuracy6. Declaración del uso de IA (3 puntos)¶
Toda entrega de este curso lleva una declaración (1.8, 6.4). Llene la tabla: una fila por herramienta, indicando la tarea que hizo y qué verificó usted. La tercera columna es la que se califica — «no verifiqué nada» es al menos honesto y cuesta menos que una afirmación de verificación que se derrumba con una sola pregunta. Si no usó ningún asistente, escriba una fila que lo diga. Recuerde que el bucle de entrenamiento de 1.4 y los diagnósticos de la sección 3 eran 🔒 A mano: no deben aparecer aquí como tareas del asistente, y se le puede pedir que los defienda oralmente.
Tarea: complete la tabla de declaración (3 puntos).
| Herramienta | Tarea | Qué verifiqué |
|---|---|---|
Notas de calificación para el instructor
Los números de referencia de abajo provienen de la solución de referencia (datos con semilla 2026, 25 épocas, Adam lr=1e-3, lote 64). Las decisiones de implementación (generador aleatorio del barajado, tipo de dato de los tensores) desplazan las exactitudes en 1–2 puntos; califique el razonamiento y use la variante con semilla oculta para verificar por muestreo cualquier resultado que parezca copiado en lugar de corrido.
1. MLP (20 pts). 1.1 (2): conteos 600/600; forma de onda del evento con P y S marcadas, los espectros muestran una joroba limitada en banda frente a ruido de ley de potencias. 1.2 (4): división 60/20/20 estratificada; el crédito completo exige que el escalador se ajuste solo sobre X_train — ajustarlo sobre los datos completos pierde 2. 1.3 (4): dos capas ocultas de tamaño width con ReLU, cabeza lineal, sin softmax en forward (softmax en forward: −1). 1.4 (8): 🔒 a mano. Busque: una permutación nueva en cada época (2), el orden correcto de zero_grad/backward/step (2), validación en eval() + no_grad y regreso a train() (2), historial registrado por época y fijación de semillas (2). Un bucle con DataLoader es aceptable. 1.5 (2): las curvas más una oración sustentada — se espera: una brecha leve, y tanto «ninguna de las dos» como «ligero sobreajuste» son defendibles.
2. Experimento de ancho (15 pts). 2.1 (8): nueve corridas, la exactitud final de validación de cada una; las medias de referencia son ≈ 0.81 (w=8), 0.83 (w=32), 0.85 (w=128) con una dispersión entre semillas de ≈ ±0.01–0.02. 2.2 (4): la media con barras mín-máx, eje x logarítmico. 2.3 (3): cualquier ancho es aceptable si el argumento compara las brechas contra la dispersión y menciona el costo en parámetros; «128 porque es el número más grande», sin la comparación con la dispersión, obtiene 1.
3. Corridas rotas (10 pts, 🔒 a mano).
Corrida A — tasa de aprendizaje demasiado alta (SGD, lr=5.0). Evidencia: la pérdida por paso explota hasta ~1e36 en las primeras épocas y sigue dando picos en el eje logarítmico; la exactitud de validación se queda al nivel del azar durante las 12 épocas; el modelo final llama evento a ~3% de las ventanas de validación. Arreglo: recortar la tasa de aprendizaje en un factor de 10 a 100 (o cambiar a Adam con 1e-3).
Corrida B — lotes sin barajar y ordenados por clase (ordered=True: primero todos los lotes de ruido, luego todos los de evento, con el mismo orden en cada época). Evidencia: la exactitud de validación queda clavada exactamente en 0.50 durante las 12 épocas mientras la pérdida de entrenamiento por paso se ve sana (~0.4–0.7) y repite un diente de sierra idéntico en cada época; la línea impresa muestra que el modelo final llama evento al 100% de las ventanas de validación — se balancea hacia la clase que vio de último. Arreglo: barajar los datos de entrenamiento en cada época.
Por corrida: patología 2, evidencia 2, arreglo 1. Acepte formulaciones del tipo «datos ordenados por clase / sin barajado»; «sobreajuste» para la corrida B obtiene 0 en el punto de la patología.
4. Evaluación honesta (15 pts). 4.1 (4): modelo de referencia mayoritario 0.50; regresión logística ≈ 0.85 de exactitud de prueba. 4.2 (3): un punto por oración — (1) un escalador ajustado con todos los datos filtra las medias y varianzas del conjunto de prueba hacia las entradas de entrenamiento; (2) el espectro se calcula por ventana a partir de esa ventana sola, así que no fluye ninguna estadística entre ventanas; (3) la elección del ancho usó solo la validación. 4.3 (6): MLP ≈ 0.83 [0.78, 0.88], logística ≈ 0.85 [0.81, 0.90] — la semianchura del IC es ≈ ±0.045. 4.4 (2): veredicto esperado — los intervalos se solapan; el MLP no le gana al modelo de referencia lineal. La representación en espectro logarítmico casi lineariza el problema (la amplitud espectral logarítmica frente a la clase es casi linealmente separable); el modelo al que hay que recurrir sobre las formas de onda crudas es la CNN 1D de 4.3. Penalice las afirmaciones de superioridad del MLP construidas sobre estimaciones puntuales que los IC contradicen.
5. Ensamble (12 pts). 5.1 (4): semillas 100/101/102, exactitudes de prueba de los miembros ≈ 0.82–0.85. 5.2 (5): exactitud de la media del ensamble ≈ 0.83; exactitudes por tercil ≈ 0.96 / 0.85 / 0.69 — descenso monótono, es decir, la dispersión ordena los errores. 5.3 (3): confianza media ≈ 0.87 frente a una exactitud ≈ 0.83 → sobreconfiado en ≈ 4 puntos; la oración debe enunciar dirección y magnitud, no solo «razonablemente calibrado».
6. Declaración (3 pts). Se califica según 6.4: tareas específicas y verificación concreta obtienen 3; una declaración vacía o genérica («lo revisé y se ve bien») obtiene 1; una declaración que liste las secciones 🔒 como trabajo del asistente activa la defensa oral.