A 05-02 vam destil·lar el principi dels autoencoders en una frase: «el que és rar es reconstrueix malament». I vam prometre aplicar-lo al frau. Aquest tercer projecte ho compleix: construiràs un detector de transaccions fraudulentes per a TecnoMarket entrenant un autoencoder només amb transaccions normals i fent servir l'error de reconstrucció com a puntuació d'anomalia. A més del model, aquest projecte t'ensenya dues disciplines imprescindibles en problemes reals: avaluar amb mètriques correctes quan les classes estan brutalment desbalancejades i traduir la matriu de confusió a cost de negoci.
Contingut
- Enunciat del projecte i per què no és classificació supervisada
- Fase 1: el dataset de transaccions
- Fase 2: partició i escalatge sense fuga
- Fase 3: l'autoencoder, entrenat només amb el que és normal
- Fase 4: error de reconstrucció com a puntuació d'anomalia
- Fase 5: tria del llindar
- Fase 6: mètriques davant del desbalanç i matriu de confusió en cost de negoci
- Fase 7: lliurament — cua de revisió i API
Enunciat del projecte i per què no és classificació supervisada
Context de negoci. TecnoMarket processa desenes de milers de comandes al dia; una fracció mínima és frau (targetes robades, comptes compromesos). L'equip antifrau no dona l'abast revisant a mà.
Per què no entrenar un classificador supervisat frau/no-frau com els de 04-03? Dues raons:
- Gairebé no hi ha exemples de frau (aquí, 0.5 %), i els que hi ha estan esbiaixats cap als fraus que ja sabem detectar.
- El frau muta: un patró nou no s'assembla als etiquetats. Un classificador supervisat només reconeix el frau del passat.
L'enfocament no supervisat de 05-02 esquiva totes dues: l'autoencoder aprèn a comprimir i reconstruir únicament transaccions normals. Qualsevol cosa que no encaixi en aquest motlle — frau vell o nou — es reconstruirà malament i farà saltar l'alarma.
Fase 1: el dataset de transaccions
Generem amb numpy un dataset fictici però amb estructura realista: cinc variables per transacció, patrons normals coherents i un 0.5 % de fraus amb patrons diferents (la mateixa filosofia de dades sintètiques controlades que vam fer servir amb la sèrie de vendes de 04-04):
import numpy as np
rng = np.random.default_rng(42) # llavor: 06-04
N_NORMAL, N_FRAU = 20_000, 100 # 0.5 % de frau
# --- Transaccions normals ---
import_ = 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) # pic a la tarda
n_items = rng.poisson(2, N_NORMAL) + 1 # 1-5 articles
dist_km = rng.exponential(30, N_NORMAL) # enviament proper
antig_dies= rng.exponential(400, N_NORMAL) # comptes veterans
X_normal = np.column_stack([import_, hora, n_items, dist_km, antig_dies])
# --- Fraus: patrons diferents, no una sola recepta ---
meitat = N_FRAU // 2
# Patró A: import alt, matinada, compte acabat de crear
frau_a = np.column_stack([
rng.lognormal(6.2, 0.4, meitat), # ~300-900 EUR
rng.uniform(1, 5, meitat), # 1-5 de la matinada
rng.poisson(1, meitat) + 1,
rng.exponential(30, meitat),
rng.uniform(0, 10, meitat), # compte de dies
])
# Patró B: molts articles cars a una adreça llunyana
frau_b = np.column_stack([
rng.lognormal(5.8, 0.3, N_FRAU - meitat),
np.clip(rng.normal(15, 4, N_FRAU - meitat) % 24, 0, 24),
rng.poisson(8, N_FRAU - meitat) + 3, # 8-15 articles
rng.exponential(600, N_FRAU - meitat), # enviament molt lluny
rng.uniform(0, 60, N_FRAU - meitat),
])
X_frau = np.vstack([frau_a, frau_b])
COLUMNES = ["import", "hora", "n_items", "dist_km", "antig_dies"]Explora abans de modelar (fase 1 de 07-01): histogrames per variable superposant normal i frau. Veuràs que cap variable separa el frau per si sola — és la combinació (import alt + matinada + compte nou) el que és anòmal. Just el que un autoencoder pot capturar i una regla manual per columnes no.
Fase 2: partició i escalatge sense fuga
Dues regles d'or, totes dues ja conegudes:
- L'autoencoder s'entrena NOMÉS amb transaccions normals. El frau no entra a l'entrenament: es reserva íntegre per avaluar.
- L'scaler s'ajusta només amb el train (la regla anti-fuga de 04-04): si l'escalatge veiés el test — o els fraus —, filtraríem informació del futur.
from sklearn.preprocessing import StandardScaler
rng.shuffle(X_normal)
n_train = 14_000
X_train = X_normal[:n_train] # només normals: entrenament
X_val_normal = X_normal[n_train:17_000] # només normals: llindar i aturada
X_test = np.vstack([X_normal[17_000:], X_frau]) # normals + TOT el frau
y_test = np.concatenate([np.zeros(3_000), np.ones(N_FRAU)])
scaler = StandardScaler().fit(X_train) # NOMÉS amb train
X_train_s = scaler.transform(X_train)
X_val_s = scaler.transform(X_val_normal)
X_test_s = scaler.transform(X_test)Fixa't en l'asimetria deliberada de la partició: el train és pur normal; el test concentra els 100 fraus al costat de 3 000 normals. Aquesta és l'estructura estàndard d'un problema de detecció d'anomalies.
Fase 3: l'autoencoder, entrenat només amb el que és normal
L'arquitectura és l'autoencoder dens de 05-02, adaptat d'imatges a tabular: 5 → coll d'ampolla → 5. El coll de 2-3 unitats obliga a comprimir; només els patrons freqüents (normals) sobreviuen a la compressió:
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="coll"), # compressio 5 -> 3
layers.Dense(16, activation="relu"),
layers.Dense(5, activation="linear"), # reconstrueix les 5 vars
])
autoencoder.compile(optimizer="adam", loss="mse")
historial = autoencoder.fit(
X_train_s, X_train_s, # entrada = objectiu: aprendre 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: {historial.history['val_loss'][-1]:.4f}") # típic ~0.05-0.15Detalls clau:
- Sortida
linear, pèrdua MSE: les dades escalades viuen en tota la recta real, no en [0,1] com els píxels de 05-02. - La validació aquí és de normals: mesura si el model generalitza a normals no vistos, i governa l'EarlyStopping (06-01). El frau continua sense aparèixer.
- Entrena en segons: el valor d'aquest projecte no és en la mida del model sinó en el protocol.
Fase 4: error de reconstrucció com a puntuació d'anomalia
Ara apliquem el principi de 05-02: passem cada transacció per l'autoencoder i mesurem com s'assembla la reconstrucció a l'original.
def puntuacio_anomalia(model, X):
reconstruccio = model.predict(X, verbose=0)
return np.mean((X - reconstruccio) ** 2, axis=1) # MSE per transacció
score_val = puntuacio_anomalia(autoencoder, X_val_s)
score_test = puntuacio_anomalia(autoencoder, X_test_s)
print(f"Score mitjà normals (test): {score_test[y_test == 0].mean():.3f}") # ~0.1
print(f"Score mitjà fraus (test): {score_test[y_test == 1].mean():.3f}") # ~3-15Pinta els dos histogrames (normal vs. frau) en escala logarítmica: veuràs dues distribucions separades però amb solapament — alguns normals atípics puntuen alt i algun frau discret puntua baix. Aquest solapament és la raó que triar llindar sigui un compromís i no una frontera màgica.
Fase 5: tria del llindar
El llindar converteix la puntuació contínua en decisió binària. Enfocament pràctic: fixar-lo com un percentil dels scores de validació (només normals) — per exemple, el percentil 99 significa «acceptem assenyalar ~1 % de transaccions legítimes»:
Però un percentil triat a cegues pot no ser el millor compromís. Explora'n uns quants i mesura'n les conseqüències:
from sklearn.metrics import precision_score, recall_score, f1_score
print(f"{'percentil':>9} | {'llindar':>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}")Resultat típic amb aquest dataset (les teves xifres variaran una mica):
| Percentil | Precision | Recall | Lectura de negoci |
|---|---|---|---|
| 90 | ~0.23 | ~0.98 | Caça gairebé tot el frau, però 3 de cada 4 alertes són falses |
| 95 | ~0.38 | ~0.96 | Menys soroll, continua caçant gairebé tot |
| 99 | ~0.72 | ~0.88 | Alertes majoritàriament certes; s'escapa ~12 % del frau |
| 99.5 | ~0.83 | ~0.80 | Molt fiable, però 1 de cada 5 fraus passa |
Apujar el llindar compra precision (menys clients molestats) venent recall (més frau colat). No existeix el llindar correcte en abstracte: existeix el llindar correcte per a uns costos.
Fase 6: mètriques davant del desbalanç i matriu de confusió en cost de negoci
Per què l'accuracy enganya aquí. Amb un 0.5 % de frau, un «model» que respongui sempre «normal» encerta el 99.5 %. Espectacular i inútil: recall 0. Amb desbalanç fort, l'accuracy mesura sobretot la mida de la classe majoritària. Per això aquest projecte s'avalua amb precision, recall i F1:
| Mètrica | Pregunta que respon | A TecnoMarket |
|---|---|---|
| Precision | Del que assenyalo, quant és frau de veritat? | Quants clients legítims molesto |
| Recall | Del frau real, quant en caço? | Quants diners deixo de perdre |
| F1 | Mitjana harmònica de totes dues | Compromís en un sol número |
La matriu de confusió, en euros. Suposem el llindar del percentil 99 i la seva matriu típica sobre el test (3 000 normals + 100 fraus):
| Predit normal | Predit frau | |
|---|---|---|
| Real normal | 2 966 (TN) | 34 (FP) |
| Real frau | 12 (FN) | 88 (TP) |
Traducció a cost, amb supòsits explícits (frau mitjà no detectat ≈ 400 EUR perduts; falsa alerta ≈ 5 EUR de cost de revisió + fricció amb el client):
- FN (frau perdut): 12 × 400 = 4 800 EUR de pèrdua directa.
- FP (client molestat): 34 × 5 = 170 EUR + risc reputacional.
Amb aquests supòsits, un FN costa ~80 vegades més que un FP: convé abaixar el llindar (percentil 95-97) i acceptar més alertes falses. Si el teu procés de revisió fos car o bloquegés comandes automàticament, la balança canviaria. El llindar és una decisió de negoci informada pel model, no un hiperparàmetre tècnic.
Fase 7: lliurament — cua de revisió i API
La decisió final no és binària sinó a tres bandes, reutilitzant la cua de revisió humana de 03-04:
LLINDAR_REVISAR = np.percentile(score_val, 95) # sospitós: a revisió
LLINDAR_BLOQUEJAR = np.percentile(score_val, 99.9) # flagrant: retenir la comanda
def avaluar_transaccio(x_raw):
x = scaler.transform(x_raw.reshape(1, -1)) # MATEIX scaler del train
score = float(puntuacio_anomalia(autoencoder, x)[0])
if score > LLINDAR_BLOQUEJAR:
return {"score": score, "accio": "retenir_i_verificar"}
if score > LLINDAR_REVISAR:
return {"score": score, "accio": "cua_revisio"}
return {"score": score, "accio": "aprovar"}Per desplegar seguim 06-05 al peu de la lletra: scaler i model es desen i es versionen com una unitat (per exemple, scaler serialitzat al costat del .keras, o integrat com a capa Normalization dins del model d'inferència), es valida sobre un conjunt congelat abans de cada actualització, i se serveix darrere d'un endpoint FastAPI (POST /avaluar-transaccio) idèntic en estructura al del classificador de ressenyes. Punt operatiu important: el que és «normal» canvia amb les temporades (recorda el Black Friday de la sèrie de 04-04) — programa reentrenaments periòdics amb dades normals recents perquè la campanya de rebaixes no es converteixi en una tempesta de falses alarmes.
Errors Comuns i Consells
- Colar fraus a l'entrenament: si l'autoencoder aprèn a reconstruir frau, el frau deixa de puntuar alt. La puresa del train és la premissa de tot el mètode.
- Ajustar l'scaler amb totes les dades: fuga clàssica (04-04). L'scaler s'ajusta amb el train normal i s'aplica, congelat, a tota la resta.
- Presumir d'accuracy: un 99 % d'accuracy aquí no significa res. Reporta sempre precision, recall i F1, i la matriu de confusió.
- Triar el llindar sobre el test: el test (amb els seus fraus) és el conjunt congelat. El llindar es fixa amb validació (percentils de normals) o amb costos de negoci; el test només mesura el resultat.
- Llindar únic i immòbil: la distribució de scores deriva amb el temps. Monitora el percentatge d'alertes diari: si es dispara sense cap campanya que ho expliqui, toca reentrenar o recalibrar.
Exercicis
- Redueix el coll d'ampolla a 2 unitats i augmenta'l a 4. Compara la separació entre scores de normals i fraus (per exemple, amb la mediana de cada grup) en els tres casos. Què passa amb un coll massa ample?
- Afegeix un tercer patró de frau que no existeixi a les dades actuals (per exemple, import normal però centenars d'articles) i avalua el detector sense reentrenar-lo. El detecta? Què et diu això davant d'un classificador supervisat?
- Defineix una funció de cost total
cost(llindar) = 400 * FN + 5 * FP, avalua-la per a percentils de 90 a 99.9 i troba el llindar que la minimitza. Compara'l amb el que vas triar a ull a la fase 5.
Solucions
- Amb coll de 2, la reconstrucció de normals empitjora una mica (puja l'score base) però la separació relativa sol mantenir-se o millorar; amb 4-5 unitats l'autoencoder s'acosta a la identitat i reconstrueix també els fraus raonablement bé: la separació s'enfonsa. Lliçó: el coll estret no és un defecte, és el mecanisme.
- Genera per exemple
np.column_stack([rng.lognormal(3.6,0.7,50), hores_normals, rng.poisson(200,50), dist_normals, antig_normals]), escala amb el mateix scaler i puntua. El detector típicament el caça amb scores altíssims:n_items=200és lluníssim de tot el que ha après. Un classificador supervisat entrenat només amb els patrons A i B no tindria cap garantia de detectar-lo — aquest és l'avantatge estructural de l'enfocament no supervisat. - Recorre percentils, calcula la matriu de confusió amb
confusion_matrix(y_test, pred).ravel()(ordretn, fp, fn, tp) i avalua400*fn + 5*fp. Amb els costos donats, el mínim sol caure entre els percentils 95 i 97 — més agressiu que el 99 «intuïtiu», confirmant l'anàlisi de la fase 6. Si ho repeteixes amb un cost de FP de 50 EUR, l'òptim es desplaça cap amunt: el llindar òptim és funció dels costos.
Conclusió
Tercer projecte lliurat: un detector de frau que no necessita exemples de frau per entrenar-se. Has aplicat l'autoencoder de 05-02 amb la disciplina de dades de 04-04 (escalatge sense fuga, particions honestes), convertit l'error de reconstrucció en puntuació d'anomalia, triat llindar com a compromís precision-recall i traduït la matriu de confusió a euros — el pas que converteix un exercici tècnic en una eina de negoci amb la seva cua de revisió (03-04) i la seva API (06-05). A la propera lliçó tornem a les imatges i saldem el deute més esperat del mòdul 5: el bucle d'entrenament complet d'una GAN amb GradientTape, per al prototip de generació de creativitats de TecnoMarket.
Curs de Deep Learning
Mòdul 1: Introducció al Deep Learning
- Què és el Deep Learning?
- Història i evolució del Deep Learning
- Aplicacions del Deep Learning
- Conceptes bàsics de xarxes neuronals
- Preparació de l'entorn de treball
Mòdul 2: Fonaments de Xarxes Neuronals
- Perceptró i Perceptró Multicapa
- Funció d'activació
- Propagació cap endavant i cap enrere
- Optimització i funció de pèrdua
- La teva primera xarxa neuronal completa
Mòdul 3: Xarxes Neuronals Convolucionals (CNN)
- Introducció a les CNN
- Capes convolucionals i de pooling
- Arquitectures populars de CNN
- Aplicacions de CNN en reconeixement d'imatges
Mòdul 4: Xarxes Neuronals Recurrents (RNN)
- Introducció a les RNN
- LSTM i GRU
- Aplicacions de RNN en processament del llenguatge natural
- Seqüències i sèries temporals
Mòdul 5: Tècniques Avançades en Deep Learning
- Xarxes Generatives Adversàries (GAN)
- Autoencoders
- Transfer Learning
- Regularització i tècniques de millora
- Mecanismes d'atenció i Transformers
Mòdul 6: Eines i Frameworks
- Introducció a TensorFlow
- Introducció a PyTorch
- Comparació de frameworks
- Entorns de desenvolupament i recursos addicionals
- Desar, carregar i desplegar models
Mòdul 7: Projectes Pràctics
- Classificació d'imatges amb CNN
- Generació de text amb RNN
- Detecció d'anomalies amb Autoencoders
- Creació d'una GAN per a generació d'imatges
- Fine-tuning d'un model preentrenat
