Aprenentatge no supervisat: ampliació
- Detecció d’anomalies: com dividir les dades
- Isolation Forest: puntuacions i limitacions
- Quan usar detecció d’anomalies
- Quan usar aprenentatge supervisat
- Enginyeria de característiques
- Consells pràctics i workflow complet
- Selecció del nombre de components de PCA
Material d’ampliació d’Aprenentatge no supervisat: no cal per seguir el recorregut principal d’aquest bloc.
Detecció d’anomalies: com dividir les dades
El conjunt d’entrenament ha de contenir només exemples normals (o una proporció molt alta de normals). Si s’escapa alguna anomalia a l’entrenament, normalment no passa res: l’algoritme és robust a una petita contaminació.
Però quantes normals usar per entrenar? Això depèn del context. Hi ha dues estratègies principals:
Estratègia 1: Split 60/20/20 (ajust d’hiperparàmetres)
Quan usar-la:
- Necessites ajustar el paràmetre
contaminationexperimentalment - Tens un nombre limitat d’exemples normals (milers, no centenars de milers)
- Vols seguir les millors pràctiques de ML supervisat (separar train/val/test)
- Pots permetre’t “perdre” dades d’entrenament per tenir validació
Exemple pràctic amb motors d’avió:
Suposem que una fàbrica ha recollit dades de 10.000 motors durant anys:
- 10.000 motors normals (funcionen correctament)
- 20 motors defectuosos (anomalies detectades posteriorment)
Divisió recomanada:
Entrenament: 6.000 motors normals (60%, només y=0)
Validació: 2.000 motors normals + 10 defectuosos (20% + 50% anomalies)
Test: 2.000 motors normals + 10 defectuosos (20% + 50% anomalies)
Si tenim molt poques anomalies (menys de 20), podem prescindir del conjunt de test:
Entrenament: 6.000 motors normals
Validació: 4.000 motors normals + 20 defectuosos
Implementació de la divisió 60/20/20:
import numpy as np
# Dades d'exemple: 1.000 motors normals i 20 defectuosos, amb 3 mesures cadascun
rng = np.random.default_rng(42)
X = np.vstack([rng.normal(size=(1000, 3)), rng.normal(loc=3, size=(20, 3))])
y = np.array([0] * 1000 + [1] * 20) # 0 = normal, 1 = anomalia
# Separar normals i anomalies, barrejant-les perquè la divisió sigui aleatòria
normal_idx = rng.permutation(np.where(y == 0)[0])
anomaly_idx = rng.permutation(np.where(y == 1)[0])
n_train = int(0.6 * len(normal_idx))
n_val = int(0.2 * len(normal_idx))
half = len(anomaly_idx) // 2
# Entrenament: el 60% dels normals, cap anomalia
X_train = X[normal_idx[:n_train]]
# Validació: el 20% dels normals i la meitat de les anomalies
val_idx = np.concatenate([normal_idx[n_train:n_train + n_val], anomaly_idx[:half]])
X_val, y_val = X[val_idx], y[val_idx]
# Test: la resta
test_idx = np.concatenate([normal_idx[n_train + n_val:], anomaly_idx[half:]])
X_test, y_test = X[test_idx], y[test_idx]
print(f"Train: {len(X_train)} normals") # Train: 600 normals
print(f"Val: {len(X_val)} total ({y_val.sum()} anomalies)") # Val: 210 total (10 anomalies)
print(f"Test: {len(X_test)} total ({y_test.sum()} anomalies)") # Test: 210 total (10 anomalies)
Ajust del paràmetre contamination amb validació:
from sklearn.ensemble import IsolationForest
from sklearn.metrics import f1_score
resultats = []
for c in [0.01, 0.02, 0.05, 0.1, 0.15]:
clf = IsolationForest(contamination=c, random_state=42)
clf.fit(X_train) # Només normals!
y_pred = (clf.predict(X_val) == -1).astype(int) # -1 (anomalia) → 1
f1 = f1_score(y_val, y_pred)
resultats.append((c, f1))
print(f"Contaminació={c:.2f}, F1={f1:.3f}")
best_c = max(resultats, key=lambda r: r[1])[0]
print(f"Millor contamination: {best_c}")
# Avaluació final, una sola vegada, amb el test
clf = IsolationForest(contamination=best_c, random_state=42).fit(X_train)
print(f"F1 al test: {f1_score(y_test, (clf.predict(X_test) == -1).astype(int)):.3f}")
Estratègia 2: Sense conjunt de validació (producció)
Quan usar-la:
- Tens moltes dades normals (centenars de milers o milions)
- Les anomalies són molt rares (< 0.5% del total)
- El paràmetre
contaminationes pot fixar segons coneixement del domini - No necessites ajustar hiperparàmetres experimentalment
Exemple pràctic: detecció de frau bancari:
- 284.315 transaccions normals (abundants!)
- 492 transaccions fraudulentes (0.173%, molt rares)
- El
contaminationes fixa a 0.002 segons estadístiques del sector
Divisió recomanada:
Entrenament: 227.452 normals (80% de les normals)
Test: 56.863 normals + 492 fraus (totes les anomalies)
Per què així?
- Dades normals abundants: no cal reservar-ne per a validació, perquè no hi ha cap hiperparàmetre a ajustar.
- Anomalies precioses: amb només 492 fraus, totes van al test, que és on serveixen.
- Les normals del test són noves: si avaluéssim amb les mateixes normals de l’entrenament, el model les reconeixeria com a normals amb més facilitat que unes de noves, i la precisió sortiria massa optimista.
Implementació:
from sklearn.ensemble import IsolationForest
from sklearn.metrics import precision_score, recall_score, f1_score
# Dades d'exemple: 10.000 normals i 20 fraus (0.2%)
rng = np.random.default_rng(42)
X = np.vstack([rng.normal(size=(10000, 3)), rng.normal(loc=3, size=(20, 3))])
y = np.array([0] * 10000 + [1] * 20)
normal_idx = rng.permutation(np.where(y == 0)[0])
anomaly_idx = np.where(y == 1)[0]
n_train = int(0.8 * len(normal_idx))
X_train = X[normal_idx[:n_train]] # 80% de les normals
test_idx = np.concatenate([normal_idx[n_train:], anomaly_idx]) # 20% normals + totes les anomalies
X_test, y_test = X[test_idx], y[test_idx]
clf = IsolationForest(contamination=0.002, random_state=42) # fixat pel coneixement del domini
clf.fit(X_train)
y_pred = (clf.predict(X_test) == -1).astype(int)
print(f"Precisió: {precision_score(y_test, y_pred, zero_division=0):.3f}")
print(f"Recall: {recall_score(y_test, y_pred):.3f}")
print(f"F1: {f1_score(y_test, y_pred):.3f}")
Taula comparativa
| Criteri | 60/20/20 | Sense validació |
|---|---|---|
| Dades normals | Milers | Centenars de milers+ |
| Anomalies | Desenes | Centenars (però % baix) |
| Contamination | Ajustat amb validació | Fix (coneixement domini) |
| Cas d’ús | Dataset petit, tuning necessari | Producció fraud/defectes |
| Exemple | 10K motors, 20 defectuosos | 284K transaccions, 492 fraus |
| Validació | Conjunt explícit | No n’hi ha: contamination fixat |
Isolation Forest: puntuacions i limitacions
Continua l’exemple de codi d’Isolation Forest (clf, X i predictions).
Scores d’anomalia
A més de les prediccions binàries (-1 o 1), Isolation Forest proporciona un score continu per cada observació:
# Obtenir scores d'anomalia
scores = clf.decision_function(X)
# Valors més negatius = més anòmals
# Valors més positius = més normals
# Ordenar per score per veure els casos més sospitosos
import pandas as pd
df_scores = pd.DataFrame({
'score': scores,
'prediction': predictions
})
df_sorted = df_scores.sort_values('score')
print(df_sorted.head(10)) # 10 casos més anòmals
Aquest score és útil per:
- Prioritzar casos per revisió manual
- Establir llindars personalitzats segons el context de negoci
- Visualitzar la distribució d’anomalies detectades
Avantatges i limitacions
| Avantatges | Limitacions |
|---|---|
| No assumeix cap distribució específica de les dades | Cal ajustar el paràmetre contamination |
| Funciona bé en alta dimensionalitat (moltes features) | Pot tenir problemes si hi ha múltiples clusters normals |
| Computacionalment eficient fins i tot amb grans datasets | No proporciona probabilitats explícites com models generatius |
| No requereix normalització de dades | Sensible a característiques irrellevants (cal feature engineering) |
Quan usar detecció d’anomalies
✅ Usa anomaly detection quan:
- Tens menys de 50 exemples d’anomalies
- Les anomalies futures poden ser de tipus nous (diferents de les observades)
- El cost de no detectar una anomalia nova és molt alt
- Les anomalies són diverses i canvien amb el temps
Exemples:
- Frau financer: Els defraudadors constantment inventen noves tècniques. Un frau detectat avui pot ser molt diferent dels fraus del mes passat.
- Ciberseguretat: Els atacs informàtics evolucionen. Un sistema supervisat només detectaria atacs coneguts, mentre que anomaly detection pot alertar de patrons nous i sospitosos.
- Detecció de defectes nous: En una línia de producció, poden aparèixer tipus de defectes mai vistos abans a causa de nous materials, màquines o processos.
Quan usar aprenentatge supervisat
✅ Usa supervised learning quan:
- Tens centenars o milers d’exemples positius
- Les anomalies futures seran similars a les passades
- Els patrons són repetitius i estables
- Necessites alta precisió en casos coneguts
Exemples:
- Classificació de correu brossa: Els correus spam tendeixen a repetir patrons similars (ofertes, enllaços sospitosos, majúscules excessives). Els futurs spams seran similars als passats.
- Diagnòstic mèdic de malalties conegudes: Els símptomes d’una pneumònia o diabetis són estables i ben documentats.
- Detecció de defectes coneguts: Si una fàbrica de smartphones sap que el 2% de pantalles tenen un ratllat específic per una màquina defectuosa, pot entrenar un classificador per detectar aquest defecte concret.
Taula d’exemples aplicats
| Aplicació | Recomanació | Raó |
|---|---|---|
| Frau amb targeta de crèdit | Anomaly detection | Noves tècniques de frau emergeixen constantment |
| Classificació de correu brossa | Supervised learning | Patrons de spam són repetitius i coneguts |
| Defectes nous en fabricació | Anomaly detection | Tipus de defecte imprevisibles i canviants |
| Diagnòstic de grip vs pneumònia | Supervised learning | Símptomes ben establerts i documentats |
| Detecció d’intrusions a xarxes | Anomaly detection | Atacs nous i sofisticats apareixen regularment |
| Predicció meteorològica | Supervised learning | Només hi ha uns quants tipus de temps coneguts |
Enfocament híbrid
En alguns casos, es poden combinar ambdues tècniques:
- Fase 1: Usar anomaly detection per identificar casos sospitosos
- Fase 2: Revisió manual i etiquetatge d’aquests casos
- Fase 3: Quan s’acumulin prou exemples (>100), entrenar un model supervisat per als patrons coneguts
- Fase 4: Mantenir anomaly detection per detectar nous tipus no coberts pel supervisat
Aquest enfocament aprofita el millor de cada món: eficiència per patrons coneguts i capacitat de detectar anomalies noves.
Enginyeria de característiques
En detecció d’anomalies, l’elecció de bones característiques és crítica. A diferència de l’aprenentatge supervisat, on el model pot aprendre quines característiques ignorar, en anomaly detection no tenim senyal supervisada per guiar aquest procés.
Per què és tan important?
Isolation Forest (i altres mètodes no supervisats) no saben què fa que una observació sigui anòmala sense que nosaltres els hi indiquem a través de les característiques.
Exemple: En detecció de frau, tenir només “import de transacció” podria no ser suficient, perquè una transacció de 1.000€ pot ser normal per un client i anòmala per un altre. Però la característica import / mitjana_habitual_usuari és molt més informativa.
Creació de característiques rellevants
Principi general: Crear característiques que capturin desviacions del comportament normal.
A continuació es mostren alguns exemples per dominis.
Targetes de crèdit:
# df: un DataFrame de pandas amb una fila per transacció
# (columnes user_id, timestamp i amount), ordenat per timestamp
# Característiques bàsiques
df['hour_of_day'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
# Característiques relatives a l'usuari
df['amount_vs_avg'] = df['amount'] / df.groupby('user_id')['amount'].transform('mean')
df['time_since_last'] = df.groupby('user_id')['timestamp'].diff().dt.total_seconds()
# Característiques de freqüència: transaccions del mateix usuari el mateix dia
df['transactions_same_day'] = df.groupby(['user_id', df['timestamp'].dt.date])['amount'].transform('count')
Monitoratge de servidors:
# df: una fila per mesura (columnes server_id, cpu_load, network_traffic, memory_used, disk_io)
# Ràtios que capten comportaments anòmals
df['cpu_per_network'] = df['cpu_load'] / (df['network_traffic'] + 1) # +1 evita divisió per 0
df['memory_growth_rate'] = df.groupby('server_id')['memory_used'].diff()
df['disk_io_vs_cpu'] = df['disk_io'] / (df['cpu_load'] + 0.01)
Fabricació:
import numpy as np
# df: una fila per lectura dels sensors (columnes temperature, vibration, pressure)
# Relacions entre variables físiques
df['temp_vibration_ratio'] = df['temperature'] / (df['vibration'] + 0.001)
df['pressure_deviation'] = np.abs(df['pressure'] - df['pressure'].rolling(10).mean())
Transformacions de distribucions
Moltes vegades, les característiques originals tenen distribucions molt esbiaixades (skewed). Aplicar transformacions pot ajudar els algoritmes a funcionar millor.
A continuació es mostren transformacions habituals:
| Transformació | Quan usar-la | Exemple |
|---|---|---|
log(X + c) | Distribució molt esbiaixada a la dreta | Imports monetaris, freqüències |
sqrt(X) | Esbiaixament moderat | Comptadors, taxes |
X^(1/3) o altres potències | Valors extrems molt grans | Volums, poblacions |
1/X | Relació inversa | Temps entre esdeveniments |
⚠️ Important: Qualsevol transformació aplicada al conjunt d’entrenament s’ha d’aplicar també als conjunts de validació i test.
Anàlisi d’errors iteratiu
El procés de millorar un sistema de detecció d’anomalies és iteratiu:
Workflow recomanat:
- Entrenar model amb característiques actuals
- Avaluar sobre validation set (amb anomalies etiquetades)
- Identificar FALSOS NEGATIUS (anomalies no detectades)
- Analitzar què diferencia aquests casos dels normals
- Crear noves característiques que captin aquestes diferències
- Tornar a 1
Exemple pràctic:
Suposem que el nostre model no detecta un servidor compromès que està enviant dades a l’exterior.
Anàlisi:
- CPU: Normal (3.2 GHz) ✓
- Memòria: Normal (40% usat) ✓
- Tràfic de xarxa: Alt però no extremadament (podria ser legítim) ⚠️
Solució: Crear una nova característica:
# Ràtio de tràfic de xarxa vs càrrega de CPU
df['network_per_cpu'] = df['network_traffic'] / (df['cpu_load'] + 0.01)
Aquesta característica serà anormalment alta per un servidor que envia dades sense fer càlculs intensius, permetent detectar l’anomalia.
Normalització i escalat
Isolation Forest és relativament robust a escales diferents, però sovint és recomanable normalitzar les característiques:
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
# Normalitzar (mitjana=0, desviació estàndard=1)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val) # Usar mateixos paràmetres!
# Entrenar amb dades normalitzades
clf = IsolationForest(contamination=0.05, random_state=42)
clf.fit(X_train_scaled)
Això és especialment important si les característiques tenen unitats molt diferents (ex: temperatura en ºC vs pressió en Pa).
Consells pràctics i workflow complet
1. Context de negoci primer
L’algorisme més sofisticat no serveix de res sense coneixement del domini:
- Parla amb experts del sector (enginyers, auditors, metges…)
- Entén què fa que una observació sigui realment problemàtica
- Defineix el cost dels falsos positius vs falsos negatius
Pregunta clau: És millor revisar 100 casos normals per no perdre 1 anomalia? O preferim perdre algunes anomalies per evitar sobrecàrrega de revisions?
2. Comença simple, itera ràpid
No intentis crear el model perfecte des del principi:
Iteració 1: 2-3 característiques bàsiques
→ Avalua → Identifica errors
Iteració 2: Afegeix 1-2 característiques noves
→ Avalua → Millora?
Iteració 3: Prova transformacions
→ Avalua → Ajusta contamination
Cada iteració hauria de durar minuts o hores, no dies.
3. Quan considerar aprenentatge supervisat
Si després d’uns mesos d’ús del sistema tens:
- Més de 100 anomalies etiquetades → Planteja’t un model supervisat complementari
- Patrons repetitius clars → Random Forest o XGBoost poden funcionar millor
- Nous tipus d’anomalies emergeixen → Manté l’anomaly detection com a primera línia
Enfocament híbrid recomanat:
- Isolation Forest detecta casos sospitosos
- Revisió humana etiqueta aquests casos
- Model supervisat per patrons coneguts
- Isolation Forest per detectar patrons nous
Selecció del nombre de components de PCA
Un dels aspectes clau de PCA és decidir quants components conservar. Menys components significa un model més simple, però també implica perdre informació. La clau per prendre aquesta decisió és la variància explicada.
Variància explicada (explained variance ratio)
Cada component principal captura una part de la variabilitat total de les dades. L’atribut explained_variance_ratio_ retorna un array on cada valor indica la proporció de variància capturada per aquell component:
- Els valors estan ordenats de major a menor (PC1 sempre captura més que PC2)
- La suma de tots els valors és 1.0 (100% de la variància)
Exemple:
explained_variance_ratio_ = [0.72, 0.15, 0.08, 0.03, 0.02]
→ PC1 sol: 72% de la informació
→ PC1 + PC2: 87% de la informació
→ PC1 + PC2 + PC3: 95% de la informació
Si amb 2 components ja capturem el 87% de la variància, les altres 48 característiques originals eren probablement redundants o molt correlacionades entre si.
Variància retinguda
Quan reduïm de \(n\) característiques a \(k\) components, “perdem” part de la informació. La variància retinguda és la suma acumulada dels primers \(k\) valors de explained_variance_ratio_, i és la mètrica clau per decidir quants components conservar.
Per què descartem les direccions amb poca variància?
La intuïció clau és que variància alta = informació útil. Si una característica (o direcció en l’espai de dades) varia molt poc, aporta poca capacitat per distingir entre observacions:
- Una característica on tots els valors són gairebé iguals no ajuda a diferenciar res
- En canvi, una característica amb molta variació permet separar o agrupar les dades
PCA ordena les direccions de més a menys variància. Quan descartem els últims components (els de baixa variància), estem eliminant:
- Soroll: fluctuacions aleatòries que no aporten informació real
- Redundància: informació ja capturada pels primers components
- Detalls irrellevants: variacions mínimes que no afecten l’anàlisi
Per això, descartar components de baixa variància sovint millora els models: eliminem soroll sense perdre el senyal important.
Hi ha un compromís:
- Més components → més variància retinguda → model més complex
- Menys components → menys variància retinguda → model més simple però pot perdre patrons importants
Els llindars habituals són 90%, 95% o 99%, depenent de l’aplicació.
Criteris segons el propòsit
| Propòsit | Components recomanats | Raonament |
|---|---|---|
| Visualització | 2-3 | Permet representar les dades en un gràfic 2D o 3D |
| Compressió | 90-99% variància | Reté la informació essencial amb menys espai |
| Preprocessat per ML | Validació creuada | Trobar el punt òptim entre retenir senyal i descartar soroll |
PCA com a tècnica de regularització:
PCA no només serveix per reduir dimensions o visualitzar. També pot millorar el rendiment d’un model predictiu. Com? Els components de baixa variància sovint contenen més soroll que senyal. En descartar-los:
- Reduïm el sobreajust (overfitting): el model no aprèn patrons espuris del soroll
- Millorem la generalització: el model es concentra en les característiques realment informatives
- Accelerem l’entrenament: menys dimensions significa menys càlcul
En alguns casos, retenir menys del 90% de la variància pot donar millors resultats que usar totes les característiques originals. L’única manera de saber-ho és validar experimentalment amb cross-validation.
Selecció automàtica amb scikit-learn
import numpy as np
from sklearn.datasets import load_wine
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
X, _ = load_wine(return_X_y=True) # 178 vins, 13 mesures
X_scaled = StandardScaler().fit_transform(X)
# Opció 1: selecció automàtica per llindar de variància
pca = PCA(n_components=0.95) # retenir el 95% de la variància
X_reduced = pca.fit_transform(X_scaled)
print(f"Components seleccionats: {pca.n_components_}") # Components seleccionats: 10
# Opció 2: explorar la variància acumulada
pca_full = PCA().fit(X_scaled)
print(np.cumsum(pca_full.explained_variance_ratio_).round(2))
# [0.36 0.55 0.67 0.74 0.8 0.85 0.89 0.92 0.94 0.96 0.98 0.99 1. ]
Amb els vins, el 95% de la variància demana 10 dels 13 components: les mesures no són gaire redundants entre elles. En canvi, amb 5 components ja tenim el 80%. Quin llindar triar depèn de l’ús, i per a un model predictiu es decideix amb validació creuada.
Consells pràctics
- Per defecte, comença amb un llindar del 95% de variància retinguda
- Per visualització, utilitza sempre 2 o 3 components
- Per models predictius, valida amb cross-validation sobre la tasca final: a vegades menys components (que eliminen soroll) milloren el rendiment