Salta el contingut

Aprenentatge No Supervisat

Introducció

L'aprenentatge no supervisat aborda un dels reptes més fascinants del Machine Learning: trobar estructura, patrons i relacions en dades sense cap etiqueta prèvia. En el món real, la majoria de les dades no estan etiquetades —etiquetar manualment és car, lent i sovint impossible a gran escala. L'aprenentatge no supervisat permet extreure coneixement valuós d'aquests mars de dades sense supervisió humana.

Les aplicacions pràctiques són omnipresents: segmentació de clients per a màrqueting personalitzat, detecció d'anomalies en sistemes industrials, reducció de dimensionalitat per a visualització i preprocés, sistemes de recomanació i models generatius com VAEs i GANs.


Clustering

El clustering agrupa les dades en clústers de manera que els elements dins d'un clúster siguin similars entre si i dissimilars dels elements d'altres clústers, sense que s'hagi especificat prèviament quins grups existeixen.

K-Means

K-Means és l'algoritme de clustering més popular per la seva simplicitat i eficiència. Cerca K centroïdes que minimitzin la suma de distàncies quadràtiques de cada punt al seu centroïde més proper (inertia):

Algorisme: 1. Inicialitza K centroïdes aleatòriament (o amb K-Means++ per a millor convergència). 2. Assigna cada punt al centroïde més proper. 3. Recalcula cada centroïde com la mitjana dels punts del seu clúster. 4. Repeteix els passos 2-3 fins a convergència.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score

# Exemple: segmentació de clients per RFM (Recency, Frequency, Monetary)
# Dataset simulat de comerç electrònic
np.random.seed(42)
n_clients = 1000

data = {
    "recency_dies": np.random.exponential(60, n_clients),  # Dies des de l'última compra
    "frequency": np.random.poisson(5, n_clients) + 1,       # Nombre de compres
    "monetary_eur": np.random.lognormal(4.5, 1.2, n_clients) # Valor total gastat
}
df = pd.DataFrame(data)

# Escalar les dades (CRÍTIC per a K-Means!)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(df)

# Mètode del colze (Elbow Method) per trobar K òptim
inerties = []
silhouette_scores = []
K_range = range(2, 11)

for k in K_range:
    kmeans = KMeans(n_clusters=k, init="k-means++", n_init=10, random_state=42)
    kmeans.fit(X_scaled)
    inerties.append(kmeans.inertia_)
    silhouette_scores.append(silhouette_score(X_scaled, kmeans.labels_))

# Visualitzar mètode del colze
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(K_range, inerties, "o-", color="steelblue", linewidth=2)
axes[0].set_xlabel("Nombre de clústers K")
axes[0].set_ylabel("Inertia (WCSS)")
axes[0].set_title("Metode del Colze")
axes[0].grid(True, alpha=0.3)

axes[1].plot(K_range, silhouette_scores, "o-", color="darkorange", linewidth=2)
axes[1].set_xlabel("Nombre de clústers K")
axes[1].set_ylabel("Silhouette Score")
axes[1].set_title("Silhouette Score per K")
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("elbow_method.png", dpi=150)
print("Gràfic desat.")

# Entrenar K-Means amb K òptim (suposem K=4)
K_OPTIM = 4
kmeans_final = KMeans(n_clusters=K_OPTIM, init="k-means++", n_init=20, random_state=42)
df["cluster"] = kmeans_final.fit_predict(X_scaled)

# Anàlisi dels perfils de cada clúster
perfils = df.groupby("cluster").agg({
    "recency_dies": "mean",
    "frequency": "mean",
    "monetary_eur": "mean",
    "cluster": "count"
}).rename(columns={"cluster": "n_clients"})

print("\nPerfils dels clústers (valors originals, no escalats):")
print(perfils.round(2))

# Etiquetar els segments (interpretació de negoci)
etiquetes = {0: "Clients perduts", 1: "Champions", 2: "Clients nous", 3: "En risc"}
df["segment"] = df["cluster"].map(etiquetes)

DBSCAN

DBSCAN (Density-Based Spatial Clustering of Applications with Noise) és un algoritme basat en densitat que identifica clústers de formes arbitràries i detecta automàticament els outliers (punts "soroll"). No requereix especificar K prèviament.

from sklearn.cluster import DBSCAN
from sklearn.neighbors import NearestNeighbors

# Trobar eps òptim amb el gràfic de distàncies als veïns
# La distància al K-è veí (knee) suggereix un bon valor d'eps
k = 5
nbrs = NearestNeighbors(n_neighbors=k).fit(X_scaled)
distancies, _ = nbrs.kneighbors(X_scaled)
distancies_k = np.sort(distancies[:, k-1])[::-1]

plt.figure(figsize=(10, 5))
plt.plot(distancies_k)
plt.xlabel("Punts ordenats per distància")
plt.ylabel(f"Distancia al {k}-e vei")
plt.title("Grafic de distancies per trobar eps optim")
plt.grid(True, alpha=0.3)
plt.savefig("dbscan_eps.png", dpi=150)

# Entrenar DBSCAN
dbscan = DBSCAN(
    eps=0.5,       # Radi del veïnatge. -1 → punts de soroll
    min_samples=5  # Mínim de punts per formar un clúster
)
labels_db = dbscan.fit_predict(X_scaled)

n_clusters = len(set(labels_db)) - (1 if -1 in labels_db else 0)
n_outliers = (labels_db == -1).sum()
print(f"DBSCAN: {n_clusters} clusters, {n_outliers} outliers ({n_outliers/len(labels_db):.1%})")

Clustering Jeràrquic (Hierarchical)

El clustering jeràrquic construeix un dendrograma —un arbre de clustering— que permet visualitzar la estructura jeràrquica de les dades i decidir el nombre de clústers a posteriori.

from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage

# Dendrograma per a una submostra (el dendrograma complet és ilegible per a datasets grans)
mostra = X_scaled[:100]

Z = linkage(mostra, method="ward")  # Ward minimitza la variança intraclúster

plt.figure(figsize=(16, 6))
dendrogram(Z, leaf_rotation=90, leaf_font_size=8)
plt.title("Dendrograma - Clustering Jerarquic (Ward)")
plt.xlabel("Index de la mostra")
plt.ylabel("Distancia")
plt.axhline(y=3.5, color="red", linestyle="--", label="Tall: 4 clusters")
plt.legend()
plt.tight_layout()
plt.savefig("dendrograma.png", dpi=150)

# Aplicar AgglomerativeClustering
hier = AgglomerativeClustering(n_clusters=4, linkage="ward")
labels_hier = hier.fit_predict(X_scaled)

Miniactivitat — Comparació de clustering

Aplica K-Means, DBSCAN i AgglomerativeClustering al dataset make_moons i make_circles de scikit-learn. Compara visualment els resultats. Per a quins tipus de formes és millor DBSCAN? Per a quins és millor K-Means? Argumenta la teva resposta.


Reducció de Dimensionalitat

Molts datasets reals contenen desenes o centenars de features. La reducció de dimensionalitat transforma les dades a un espai de menor dimensió, preservant la informació rellevant. Té dos usos principals: visualització (fins a 2-3 dimensions) i preprocés per millorar el rendiment d'algoritmes posteriors.

PCA — Anàlisi de Components Principals

PCA troba les direccions de màxima variança en les dades (components principals) i projecta les dades sobre un subespai d'aquestes direccions. És una transformació lineal i reversible.

from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

# PCA per a visualització
pca_2d = PCA(n_components=2, random_state=42)
X_pca_2d = pca_2d.fit_transform(X_scaled)

plt.figure(figsize=(10, 7))
scatter = plt.scatter(X_pca_2d[:, 0], X_pca_2d[:, 1],
                      c=df["cluster"], cmap="tab10", alpha=0.6, s=20)
plt.colorbar(scatter, label="Cluster")
plt.xlabel(f"PC1 ({pca_2d.explained_variance_ratio_[0]:.1%} varianca)")
plt.ylabel(f"PC2 ({pca_2d.explained_variance_ratio_[1]:.1%} varianca)")
plt.title("Clients segmentats en espai PCA (2D)")
plt.grid(True, alpha=0.3)
plt.savefig("pca_visualitzacio.png", dpi=150)

# PCA per a preprocés: retenir el 95% de la variança
pca_95 = PCA(n_components=0.95, random_state=42)
X_pca = pca_95.fit_transform(X_scaled)
print(f"Dimensions originals: {X_scaled.shape[1]}")
print(f"Dimensions PCA (95% var): {X_pca.shape[1]}")
print(f"Variança explicada acumulada: {pca_95.explained_variance_ratio_.sum():.3f}")

# Scree plot
pca_full = PCA(random_state=42)
pca_full.fit(X_scaled)
plt.figure(figsize=(10, 5))
plt.bar(range(1, len(pca_full.explained_variance_ratio_)+1),
        pca_full.explained_variance_ratio_, alpha=0.7, color="steelblue")
plt.plot(range(1, len(pca_full.explained_variance_ratio_)+1),
         np.cumsum(pca_full.explained_variance_ratio_),
         "o-", color="red", label="Varianca acumulada")
plt.axhline(y=0.95, color="gray", linestyle="--", label="Llindar 95%")
plt.xlabel("Nombre de components principals")
plt.ylabel("Varianca explicada")
plt.title("Scree Plot - PCA")
plt.legend()
plt.tight_layout()

t-SNE

t-SNE (t-distributed Stochastic Neighbor Embedding) és una tècnica de reducció de dimensionalitat no lineal especialment eficaç per a visualització. Preserva les distàncies locals —punts similars en l'espai original queden propers en la visualització— però distorsiona les distàncies globals.

from sklearn.manifold import TSNE

# t-SNE és computacionalment car per a datasets grans
# Recomanació: aplicar PCA primer per reduir a ~50 dimensions, llavors t-SNE
X_pca_50 = PCA(n_components=min(50, X_scaled.shape[1]), random_state=42).fit_transform(X_scaled)

tsne = TSNE(
    n_components=2,
    perplexity=30,    # Controla el nombre efectiu de veïns: 5-50
    learning_rate="auto",
    n_iter=1000,
    random_state=42
)
X_tsne = tsne.fit_transform(X_pca_50)

plt.figure(figsize=(10, 7))
scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1],
                      c=df["cluster"], cmap="tab10", alpha=0.6, s=20)
plt.colorbar(scatter, label="Cluster")
plt.title("t-SNE — Visualitzacio de clients segmentats")
plt.axis("off")
plt.savefig("tsne_visualitzacio.png", dpi=150)

UMAP

UMAP (Uniform Manifold Approximation and Projection) és una alternativa moderna a t-SNE que preserva millor l'estructura global, és més ràpid i produeix embeddings que es poden aplicar a noves dades (no és el cas de t-SNE).

import umap  # pip install umap-learn

reducer = umap.UMAP(
    n_components=2,
    n_neighbors=15,   # Com t-SNE perplexity: controla l'equilibri local/global
    min_dist=0.1,     # Distància mínima entre punts en l'espai reduït
    metric="euclidean",
    random_state=42
)
X_umap = reducer.fit_transform(X_scaled)

# UMAP permet transformar noves dades
nous_clients = scaler.transform(pd.DataFrame([{
    "recency_dies": 10, "frequency": 15, "monetary_eur": 5000
}]))
X_umap_nous = reducer.transform(nous_clients)

Detecció d'Anomalies

La detecció d'anomalies (o detecció d'outliers) identifica punts de dades que es desvien significativament del patró general. És crítica per a seguretat informàtica (intrusions), qualitat industrial (defectes), fraude financer i monitorització de sistemes.

Isolation Forest

Isolation Forest aïlla les anomalies construint arbres de decisió aleatoris. Les anomalies —punts inusuals— s'aïllen en menys particions (profunditat menor a l'arbre) que els punts normals.

from sklearn.ensemble import IsolationForest

iso_forest = IsolationForest(
    n_estimators=100,
    contamination=0.05,  # Proporció esperada d'anomalies (5%)
    random_state=42
)
# Retorna 1 (normal) o -1 (anomalia)
anomalies = iso_forest.fit_predict(X_scaled)
scores = iso_forest.score_samples(X_scaled)  # Score d'anomalia (negatiu, més negatiu = més anòmal)

n_anomalies = (anomalies == -1).sum()
print(f"Anomalies detectades: {n_anomalies} ({n_anomalies/len(anomalies):.1%})")

# Visualització
plt.figure(figsize=(10, 7))
plt.scatter(X_pca_2d[anomalies==1, 0], X_pca_2d[anomalies==1, 1],
            c="steelblue", s=15, alpha=0.5, label="Normal")
plt.scatter(X_pca_2d[anomalies==-1, 0], X_pca_2d[anomalies==-1, 1],
            c="red", s=50, marker="x", label="Anomalia")
plt.title("Isolation Forest — Deteccio d'Anomalies")
plt.legend()
plt.savefig("anomalies.png", dpi=150)

AutoEncoder per a Detecció d'Anomalies

Els AutoEncoders (xarxes neuronals que aprenen a comprimir i reconstruir les dades) s'entrenen sobre dades normals. Les anomalies generen errors de reconstrucció elevats perquè el model mai les ha vist.

import torch
import torch.nn as nn

class AutoEncoder(nn.Module):
    def __init__(self, input_dim, latent_dim=8):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 32),
            nn.ReLU(),
            nn.Linear(32, 16),
            nn.ReLU(),
            nn.Linear(16, latent_dim)
        )
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 16),
            nn.ReLU(),
            nn.Linear(16, 32),
            nn.ReLU(),
            nn.Linear(32, input_dim)
        )

    def forward(self, x):
        z = self.encoder(x)
        return self.decoder(z)

    def reconstruction_error(self, x):
        with torch.no_grad():
            x_recon = self.forward(x)
            return torch.mean((x - x_recon)**2, dim=1)

# Entrenament de l'autoencoder sobre dades normals
input_dim = X_scaled.shape[1]
model = AutoEncoder(input_dim=input_dim)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()

X_tensor = torch.FloatTensor(X_scaled)

for epoch in range(50):
    optimizer.zero_grad()
    output = model(X_tensor)
    loss = criterion(output, X_tensor)
    loss.backward()
    optimizer.step()
    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1}/50 — Loss: {loss.item():.6f}")

# Detectar anomalies per llindar d'error
errors = model.reconstruction_error(X_tensor).numpy()
threshold = np.percentile(errors, 95)  # 5% superior = anomalies
anomalies_ae = errors > threshold
print(f"Anomalies detectades per AutoEncoder: {anomalies_ae.sum()}")

Sistemes de Recomanació

Els sistemes de recomanació prediuen les preferències dels usuaris per a elements que no han valorat encara.

Filtrat Col·laboratiu (Collaborative Filtering)

Assumeix que usuaris amb comportament similar en el passat tindran preferències similars en el futur.

from surprise import Dataset, Reader, SVD, accuracy
from surprise.model_selection import cross_validate, train_test_split as surprise_split

# Carregar dades de ratings (user_id, item_id, rating)
reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(
    df_ratings[["user_id", "item_id", "rating"]], reader
)

# SVD (Singular Value Decomposition) — l'algoritme de referència
svd = SVD(n_factors=100, n_epochs=20, lr_all=0.005, reg_all=0.02, random_state=42)

# Validació creuada
resultats = cross_validate(svd, data, measures=["RMSE", "MAE"], cv=5, verbose=True)
print(f"RMSE mig: {resultats['test_rmse'].mean():.4f}")

# Predicció per a un usuari específic
trainset, testset = surprise_split(data, test_size=0.2)
svd.fit(trainset)
predic = svd.predict(uid="user_123", iid="item_456")
print(f"Prediccio de rating: {predic.est:.2f}")

Self-Supervised Learning

El self-supervised learning (aprenentatge autosupervisat) és un paradigma a cavall entre supervisat i no supervisat: el model genera les seves pròpies etiquetes a partir de les dades. Ha revolucionat el NLP (BERT, GPT) i la visió per computador (SimCLR, DINO, MAE).

graph TD
    A["Dades sense etiquetar"] --> B["Tasca pretext auto-generada"]
    B --> C["Preentrenament del model"]
    C --> D["Representacions riques"]
    D --> E["Fine-tuning amb poques etiquetes"]
    E --> F["Model final d'alta qualitat"]

    subgraph Exemples_tasques ["Exemples de tasques pretext"]
        G["BERT: predir paraules enmascarades"]
        H["GPT: predir la seguent paraula"]
        I["SimCLR: maximitzar similitud de vistes del mateix objecte"]
        J["MAE: reconstruir pedacos d'imatge enmascarats"]
    end

L'avantatge clau és que permet aprofitar quantitats massives de dades sense etiquetar (la web, llibres digitalitzats, Wikipedia) per preentrenar models rics que després s'adapten a tasques específiques amb molt poques etiquetes.


Cas d'Ús Real: Segmentació de Clients

Presentem un exemple complet de segmentació de clients per a una empresa de comerç electrònic usant dades RFM.

"""
Segmentació de clients amb K-Means i anàlisi RFM
Per a un e-commerce amb 50.000 clients
"""
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
import seaborn as sns

# ============================================================
# 1. CÀLCUL DE LES MÈTRIQUES RFM
# ============================================================
df_comandes = pd.read_csv("comandes.csv", parse_dates=["data_compra"])

DATA_REFERENCIA = df_comandes["data_compra"].max() + pd.Timedelta(days=1)

rfm = df_comandes.groupby("client_id").agg({
    "data_compra": lambda x: (DATA_REFERENCIA - x.max()).days,  # Recency
    "comanda_id": "count",                                       # Frequency
    "import_total": "sum"                                        # Monetary
}).rename(columns={
    "data_compra": "recency",
    "comanda_id": "frequency",
    "import_total": "monetary"
})

print(f"Clients únics: {len(rfm)}")
print(rfm.describe().round(2))

# ============================================================
# 2. TRACTAMENT D'OUTLIERS I ESCALAT
# ============================================================
# Cap les variables al percentil 99 per evitar l'efecte dels outliers extrem
for col in ["recency", "frequency", "monetary"]:
    cap_val = rfm[col].quantile(0.99)
    rfm[f"{col}_capped"] = rfm[col].clip(upper=cap_val)

# Escalar
cols_rfm = ["recency_capped", "frequency_capped", "monetary_capped"]
scaler = StandardScaler()
rfm_scaled = scaler.fit_transform(rfm[cols_rfm])

# ============================================================
# 3. TROBAR K ÒPTIM
# ============================================================
resultats_k = []
for k in range(2, 9):
    km = KMeans(n_clusters=k, init="k-means++", n_init=15, random_state=42)
    labels = km.fit_predict(rfm_scaled)
    resultats_k.append({
        "k": k,
        "inertia": km.inertia_,
        "silhouette": silhouette_score(rfm_scaled, labels, sample_size=5000)
    })
df_resultats = pd.DataFrame(resultats_k)
print("\nResultats per K:")
print(df_resultats.to_string(index=False))

# K=5 és freqüentment el millor equilibri per a segmentació RFM
K_OPTIM = 5

# ============================================================
# 4. MODEL FINAL
# ============================================================
kmeans = KMeans(n_clusters=K_OPTIM, init="k-means++", n_init=20, random_state=42)
rfm["cluster"] = kmeans.fit_predict(rfm_scaled)

# ============================================================
# 5. INTERPRETACIÓ DELS SEGMENTS
# ============================================================
perfils = rfm.groupby("cluster")[["recency", "frequency", "monetary"]].mean()
perfils["n_clients"] = rfm.groupby("cluster").size()
perfils["pct_clients"] = perfils["n_clients"] / len(rfm) * 100

print("\nPerfils dels segments:")
print(perfils.round(2))

# Assignar noms de segment basats en els perfils
# (Adaptar a partir de l'output real de l'anàlisi)
noms_segments = {
    0: "Champions",
    1: "Clients fidels",
    2: "En risc",
    3: "Clients nous",
    4: "Clients perduts"
}
rfm["segment"] = rfm["cluster"].map(noms_segments)

# ============================================================
# 6. VISUALITZACIÓ
# ============================================================
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

for i, (col, ax) in enumerate(zip(["recency", "frequency", "monetary"], axes)):
    for segment in rfm["segment"].unique():
        data_seg = rfm[rfm["segment"]==segment][col]
        ax.hist(data_seg, bins=30, alpha=0.5, label=segment)
    ax.set_title(f"Distribucio de {col.capitalize()}")
    ax.set_xlabel(col)
    ax.legend(fontsize=7)

plt.suptitle("Distribucions per Segment RFM", fontsize=14, y=1.02)
plt.tight_layout()
plt.savefig("segmentacio_rfm.png", dpi=150, bbox_inches="tight")

print("\nFitxer de segmentació desat. Resum:")
print(rfm.groupby("segment").size().sort_values(ascending=False))

Miniactivitat — Segmentació de clients

Utilitza el dataset Online Retail II (disponible a Kaggle o UCI ML Repository) que conté transaccions reals d'un e-commerce britànic. Calcula les mètriques RFM, segmenta amb K-Means i interpreta els segments des d'una perspectiva de negoci. Quines accions de màrqueting proposaries per a cada segment?


Exercici Pràctic — AC5072/04

Objectiu: Pipeline complet de clustering i segmentació sobre un dataset real.

Dataset: Mall Customer Segmentation (Kaggle) — 200 clients de centre comercial amb edat, gènere, ingressos anuals i puntuació de despesa.

Tasques:

  1. EDA: distribucions, correlacions, detecció d'outliers.
  2. Aplica K-Means, DBSCAN i Clustering Jeràrquic.
  3. Usa mètode del colze i silhouette score per trobar K òptim per a K-Means.
  4. Visualitza els clústers en 2D amb PCA i t-SNE.
  5. Interpreta els segments identificats des d'una perspectiva de negoci.
  6. Detecció d'anomalies amb Isolation Forest.
  7. Conclusió escrita (400 paraules): quins segments has trobat? Quines accions de màrqueting proposes?

Codi de l'activitat: AC5072/04