En 05-02 destilamos el principio de los autoencoders en una frase: «lo raro se reconstruye mal». Y prometimos aplicarlo al fraude. Este tercer proyecto lo cumple: construirás un detector de transacciones fraudulentas para TecnoMarket entrenando un autoencoder solo con transacciones normales y usando el error de reconstrucción como puntuación de anomalía. Además del modelo, este proyecto te enseña dos disciplinas imprescindibles en problemas reales: evaluar con métricas correctas cuando las clases están brutalmente desbalanceadas y traducir la matriz de confusión a coste de negocio.
Contenido
- Enunciado del proyecto y por qué no es clasificación supervisada
- Fase 1: el dataset de transacciones
- Fase 2: partición y escalado sin fuga
- Fase 3: el autoencoder, entrenado solo con lo normal
- Fase 4: error de reconstrucción como puntuación de anomalía
- Fase 5: elección del umbral
- Fase 6: métricas ante desbalance y matriz de confusión en coste de negocio
- Fase 7: entrega — cola de revisión y API
Enunciado del proyecto y por qué no es clasificación supervisada
Contexto de negocio. TecnoMarket procesa decenas de miles de pedidos al día; una fracción mínima es fraude (tarjetas robadas, cuentas comprometidas). El equipo antifraude no da abasto revisando a mano.
¿Por qué no entrenar un clasificador supervisado fraude/no-fraude como los de 04-03? Dos razones:
- Casi no hay ejemplos de fraude (aquí, 0.5 %), y los que hay están sesgados hacia los fraudes que ya sabemos detectar.
- El fraude muta: un patrón nuevo no se parece a los etiquetados. Un clasificador supervisado solo reconoce el fraude del pasado.
El enfoque no supervisado de 05-02 esquiva ambas: el autoencoder aprende a comprimir y reconstruir únicamente transacciones normales. Cualquier cosa que no encaje en ese molde — fraude viejo o nuevo — se reconstruirá mal y disparará la alarma.
Fase 1: el dataset de transacciones
Generamos con numpy un dataset ficticio pero con estructura realista: cinco variables por transacción, patrones normales coherentes y un 0.5 % de fraudes con patrones distintos (la misma filosofía de datos sintéticos controlados que usamos con la serie de ventas de 04-04):
import numpy as np
rng = np.random.default_rng(42) # semilla: 06-04
N_NORMAL, N_FRAUDE = 20_000, 100 # 0.5 % de fraude
# --- Transacciones normales ---
importe = rng.lognormal(mean=3.6, sigma=0.7, size=N_NORMAL) # ~20-150 EUR
hora = np.clip(rng.normal(15, 4, N_NORMAL) % 24, 0, 24) # pico por la tarde
n_items = rng.poisson(2, N_NORMAL) + 1 # 1-5 articulos
dist_km = rng.exponential(30, N_NORMAL) # envio cercano
antig_dias= rng.exponential(400, N_NORMAL) # cuentas veteranas
X_normal = np.column_stack([importe, hora, n_items, dist_km, antig_dias])
# --- Fraudes: patrones distintos, no una sola receta ---
mitad = N_FRAUDE // 2
# Patron A: importe alto, madrugada, cuenta recien creada
fraude_a = np.column_stack([
rng.lognormal(6.2, 0.4, mitad), # ~300-900 EUR
rng.uniform(1, 5, mitad), # 1-5 de la madrugada
rng.poisson(1, mitad) + 1,
rng.exponential(30, mitad),
rng.uniform(0, 10, mitad), # cuenta de dias
])
# Patron B: muchos articulos caros a direccion lejana
fraude_b = np.column_stack([
rng.lognormal(5.8, 0.3, N_FRAUDE - mitad),
np.clip(rng.normal(15, 4, N_FRAUDE - mitad) % 24, 0, 24),
rng.poisson(8, N_FRAUDE - mitad) + 3, # 8-15 articulos
rng.exponential(600, N_FRAUDE - mitad), # envio muy lejos
rng.uniform(0, 60, N_FRAUDE - mitad),
])
X_fraude = np.vstack([fraude_a, fraude_b])
COLUMNAS = ["importe", "hora", "n_items", "dist_km", "antig_dias"]Explora antes de modelar (fase 1 de 07-01): histogramas por variable superponiendo normal y fraude. Verás que ninguna variable separa el fraude por sí sola — es la combinación (importe alto + madrugada + cuenta nueva) lo que es anómalo. Justo lo que un autoencoder puede capturar y una regla manual por columnas no.
Fase 2: partición y escalado sin fuga
Dos reglas de oro, ambas ya conocidas:
- El autoencoder se entrena SOLO con transacciones normales. El fraude no entra en el entrenamiento: se reserva íntegro para evaluar.
- El scaler se ajusta solo con el train (la regla anti-fuga de 04-04): si el escalado viera el test — o los fraudes —, filtraríamos información del futuro.
from sklearn.preprocessing import StandardScaler
rng.shuffle(X_normal)
n_train = 14_000
X_train = X_normal[:n_train] # solo normales: entrenamiento
X_val_normal = X_normal[n_train:17_000] # solo normales: umbral y parada
X_test = np.vstack([X_normal[17_000:], X_fraude]) # normales + TODO el fraude
y_test = np.concatenate([np.zeros(3_000), np.ones(N_FRAUDE)])
scaler = StandardScaler().fit(X_train) # SOLO con train
X_train_s = scaler.transform(X_train)
X_val_s = scaler.transform(X_val_normal)
X_test_s = scaler.transform(X_test)Fíjate en la asimetría deliberada de la partición: el train es puro normal; el test concentra los 100 fraudes junto a 3 000 normales. Esta es la estructura estándar de un problema de detección de anomalías.
Fase 3: el autoencoder, entrenado solo con lo normal
La arquitectura es el autoencoder denso de 05-02, adaptado de imágenes a tabular: 5 → cuello de botella → 5. El cuello de 2-3 unidades obliga a comprimir; solo los patrones frecuentes (normales) sobreviven a la compresión:
import tensorflow as tf
from tensorflow.keras import layers, models
tf.random.set_seed(42)
autoencoder = models.Sequential([
layers.Input(shape=(5,)),
layers.Dense(16, activation="relu"),
layers.Dense(3, activation="relu", name="cuello"), # compresion 5 -> 3
layers.Dense(16, activation="relu"),
layers.Dense(5, activation="linear"), # reconstruye las 5 vars
])
autoencoder.compile(optimizer="adam", loss="mse")
historia = autoencoder.fit(
X_train_s, X_train_s, # entrada = objetivo: aprender a copiar
validation_data=(X_val_s, X_val_s),
epochs=100, batch_size=64,
callbacks=[tf.keras.callbacks.EarlyStopping(patience=10,
restore_best_weights=True)],
verbose=0,
)
print(f"MSE final val: {historia.history['val_loss'][-1]:.4f}") # típico ~0.05-0.15Detalles clave:
- Salida
linear, pérdida MSE: los datos escalados viven en toda la recta real, no en [0,1] como los píxeles de 05-02. - La validación aquí es de normales: mide si el modelo generaliza a normales no vistos, y gobierna el EarlyStopping (06-01). El fraude sigue sin aparecer.
- Entrena en segundos: el valor de este proyecto no está en el tamaño del modelo sino en el protocolo.
Fase 4: error de reconstrucción como puntuación de anomalía
Ahora aplicamos el principio de 05-02: pasamos cada transacción por el autoencoder y medimos cuánto se parece la reconstrucción al original.
def puntuacion_anomalia(modelo, X):
reconstruccion = modelo.predict(X, verbose=0)
return np.mean((X - reconstruccion) ** 2, axis=1) # MSE por transaccion
score_val = puntuacion_anomalia(autoencoder, X_val_s)
score_test = puntuacion_anomalia(autoencoder, X_test_s)
print(f"Score medio normales (test): {score_test[y_test == 0].mean():.3f}") # ~0.1
print(f"Score medio fraudes (test): {score_test[y_test == 1].mean():.3f}") # ~3-15Pinta los dos histogramas (normal vs. fraude) en escala logarítmica: verás dos distribuciones separadas pero con solapamiento — algunos normales atípicos puntúan alto y algún fraude discreto puntúa bajo. Ese solapamiento es la razón de que elegir umbral sea un compromiso y no una frontera mágica.
Fase 5: elección del umbral
El umbral convierte la puntuación continua en decisión binaria. Enfoque práctico: fijarlo como un percentil de los scores de validación (solo normales) — por ejemplo, el percentil 99 significa «aceptamos señalar ~1 % de transacciones legítimas»:
Pero un percentil elegido a ciegas puede no ser el mejor compromiso. Explora varios y mide sus consecuencias:
from sklearn.metrics import precision_score, recall_score, f1_score
print(f"{'percentil':>9} | {'umbral':>7} | {'precision':>9} | {'recall':>6} | {'F1':>5}")
for p in [90, 95, 97, 99, 99.5]:
u = np.percentile(score_val, p)
pred = (score_test > u).astype(int)
print(f"{p:>9} | {u:>7.2f} | {precision_score(y_test, pred):>9.2f} "
f"| {recall_score(y_test, pred):>6.2f} | {f1_score(y_test, pred):>5.2f}")Resultado típico con este dataset (tus cifras variarán algo):
| Percentil | Precision | Recall | Lectura de negocio |
|---|---|---|---|
| 90 | ~0.23 | ~0.98 | Caza casi todo el fraude, pero 3 de cada 4 alertas son falsas |
| 95 | ~0.38 | ~0.96 | Menos ruido, sigue cazando casi todo |
| 99 | ~0.72 | ~0.88 | Alertas mayormente ciertas; se escapa ~12 % del fraude |
| 99.5 | ~0.83 | ~0.80 | Muy fiable, pero 1 de cada 5 fraudes pasa |
Subir el umbral compra precision (menos clientes molestados) vendiendo recall (más fraude colado). No existe el umbral correcto en abstracto: existe el umbral correcto para unos costes.
Fase 6: métricas ante desbalance y matriz de confusión en coste de negocio
Por qué la accuracy engaña aquí. Con 0.5 % de fraude, un «modelo» que responda siempre «normal» acierta el 99.5 %. Espectacular e inútil: recall 0. Con desbalance fuerte, la accuracy mide sobre todo el tamaño de la clase mayoritaria. Por eso este proyecto se evalúa con precision, recall y F1:
| Métrica | Pregunta que responde | En TecnoMarket |
|---|---|---|
| Precision | De lo que señalo, ¿cuánto es fraude de verdad? | Cuántos clientes legítimos molesto |
| Recall | Del fraude real, ¿cuánto cazo? | Cuánto dinero dejo de perder |
| F1 | Media armónica de ambas | Compromiso en un solo número |
La matriz de confusión, en euros. Supongamos el umbral del percentil 99 y su matriz típica sobre el test (3 000 normales + 100 fraudes):
| Predicho normal | Predicho fraude | |
|---|---|---|
| Real normal | 2 966 (TN) | 34 (FP) |
| Real fraude | 12 (FN) | 88 (TP) |
Traducción a coste, con supuestos explícitos (fraude medio no detectado ≈ 400 EUR perdidos; falsa alerta ≈ 5 EUR de coste de revisión + fricción con el cliente):
- FN (fraude perdido): 12 × 400 = 4 800 EUR de pérdida directa.
- FP (cliente molesto): 34 × 5 = 170 EUR + riesgo reputacional.
Con estos supuestos, un FN cuesta ~80 veces más que un FP: conviene bajar el umbral (percentil 95-97) y aceptar más alertas falsas. Si tu proceso de revisión fuera caro o bloqueara pedidos automáticamente, la balanza cambiaría. El umbral es una decisión de negocio informada por el modelo, no un hiperparámetro técnico.
Fase 7: entrega — cola de revisión y API
La decisión final no es binaria sino a tres bandas, reutilizando la cola de revisión humana de 03-04:
UMBRAL_REVISAR = np.percentile(score_val, 95) # sospechoso: a revision
UMBRAL_BLOQUEAR = np.percentile(score_val, 99.9) # flagrante: retener pedido
def evaluar_transaccion(x_raw):
x = scaler.transform(x_raw.reshape(1, -1)) # MISMO scaler del train
score = float(puntuacion_anomalia(autoencoder, x)[0])
if score > UMBRAL_BLOQUEAR:
return {"score": score, "accion": "retener_y_verificar"}
if score > UMBRAL_REVISAR:
return {"score": score, "accion": "cola_revision"}
return {"score": score, "accion": "aprobar"}Para desplegar seguimos 06-05 al pie de la letra: scaler y modelo se guardan y versionan como una unidad (por ejemplo, scaler serializado junto al .keras, o integrado como capa Normalization dentro del modelo de inferencia), se valida sobre un conjunto congelado antes de cada actualización, y se sirve tras un endpoint FastAPI (POST /evaluar-transaccion) idéntico en estructura al del clasificador de reseñas. Punto operativo importante: lo «normal» cambia con las temporadas (recuerda el Black Friday de la serie de 04-04) — programa reentrenos periódicos con datos normales recientes para que la campaña de rebajas no se convierta en una tormenta de falsas alarmas.
Errores Comunes y Consejos
- Colar fraudes en el entrenamiento: si el autoencoder aprende a reconstruir fraude, el fraude deja de puntuar alto. La pureza del train es la premisa de todo el método.
- Ajustar el scaler con todos los datos: fuga clásica (04-04). El scaler se ajusta con el train normal y se aplica, congelado, a todo lo demás.
- Presumir de accuracy: 99 % de accuracy aquí no significa nada. Reporta siempre precision, recall y F1, y la matriz de confusión.
- Elegir el umbral sobre el test: el test (con sus fraudes) es el conjunto congelado. El umbral se fija con validación (percentiles de normales) o con costes de negocio; el test solo mide el resultado.
- Umbral único e inamovible: la distribución de scores deriva con el tiempo. Monitoriza el porcentaje de alertas diario: si se dispara sin campaña que lo explique, toca reentrenar o recalibrar.
Ejercicios
- Reduce el cuello de botella a 2 unidades y auméntalo a 4. Compara la separación entre scores de normales y fraudes (por ejemplo, con la mediana de cada grupo) en los tres casos. ¿Qué pasa con un cuello demasiado ancho?
- Añade un tercer patrón de fraude que no exista en los datos actuales (por ejemplo, importe normal pero cientos de artículos) y evalúa el detector sin reentrenarlo. ¿Lo detecta? ¿Qué te dice esto frente a un clasificador supervisado?
- Define una función de coste total
coste(umbral) = 400 * FN + 5 * FP, evalúala para percentiles de 90 a 99.9 y encuentra el umbral que la minimiza. Compara con el que elegiste a ojo en la fase 5.
Soluciones
- Con cuello de 2, la reconstrucción de normales empeora un poco (sube el score base) pero la separación relativa suele mantenerse o mejorar; con 4-5 unidades el autoencoder se acerca a la identidad y reconstruye también los fraudes razonablemente bien: la separación se hunde. Lección: el cuello estrecho no es un defecto, es el mecanismo.
- Genera por ejemplo
np.column_stack([rng.lognormal(3.6,0.7,50), horas_normales, rng.poisson(200,50), dist_normales, antig_normales]), escala con el mismo scaler y puntúa. El detector típicamente lo caza con scores altísimos:n_items=200está lejísimos de todo lo aprendido. Un clasificador supervisado entrenado solo con los patrones A y B no tendría ninguna garantía de detectarlo — esta es la ventaja estructural del enfoque no supervisado. - Recorre percentiles, calcula la matriz de confusión con
confusion_matrix(y_test, pred).ravel()(ordentn, fp, fn, tp) y evalúa400*fn + 5*fp. Con los costes dados, el mínimo suele caer entre los percentiles 95 y 97 — más agresivo que el 99 «intuitivo», confirmando el análisis de la fase 6. Si repites con coste de FP de 50 EUR, el óptimo se desplaza hacia arriba: el umbral óptimo es función de los costes.
Conclusión
Tercer proyecto entregado: un detector de fraude que no necesita ejemplos de fraude para entrenarse. Has aplicado el autoencoder de 05-02 con la disciplina de datos de 04-04 (escalado sin fuga, particiones honestas), convertido el error de reconstrucción en puntuación de anomalía, elegido umbral como compromiso precision-recall y traducido la matriz de confusión a euros — el paso que convierte un ejercicio técnico en una herramienta de negocio con su cola de revisión (03-04) y su API (06-05). En la próxima lección volvemos a las imágenes y saldamos la deuda más esperada del módulo 5: el bucle de entrenamiento completo de una GAN con GradientTape, para el prototipo de generación de creatividades de TecnoMarket.
Curso de Deep Learning
Módulo 1: Introducción a Deep Learning
- ¿Qué es Deep Learning?
- Historia y evolución del Deep Learning
- Aplicaciones de Deep Learning
- Conceptos básicos de redes neuronales
- Preparación del entorno de trabajo
Módulo 2: Fundamentos de Redes Neuronales
- Perceptrón y Perceptrón Multicapa
- Función de activación
- Propagación hacia adelante y hacia atrás
- Optimización y función de pérdida
- Tu primera red neuronal completa
Módulo 3: Redes Neuronales Convolucionales (CNN)
- Introducción a las CNN
- Capas convolucionales y de pooling
- Arquitecturas populares de CNN
- Aplicaciones de CNN en reconocimiento de imágenes
Módulo 4: Redes Neuronales Recurrentes (RNN)
- Introducción a las RNN
- LSTM y GRU
- Aplicaciones de RNN en procesamiento del lenguaje natural
- Secuencias y series temporales
Módulo 5: Técnicas Avanzadas en Deep Learning
- Redes Generativas Adversariales (GAN)
- Autoencoders
- Transfer Learning
- Regularización y técnicas de mejora
- Mecanismos de atención y Transformers
Módulo 6: Herramientas y Frameworks
- Introducción a TensorFlow
- Introducción a PyTorch
- Comparación de frameworks
- Entornos de desarrollo y recursos adicionales
- Guardar, cargar y desplegar modelos
Módulo 7: Proyectos Prácticos
- Clasificación de imágenes con CNN
- Generación de texto con RNN
- Detección de anomalías con Autoencoders
- Creación de una GAN para generación de imágenes
- Fine-tuning de un modelo preentrenado
