Esercizio - Clustering gerarchico agglomerativo e divisivo
Questa pagina non ha ancora la versione ripasso: qui sotto c'è il testo completo.
In questa pagina 4
Testo (2° appello 2025, esercizi 4 e 6; laboratorio LAB6).
- Scrivere lo pseudocodice del clustering gerarchico agglomerativo con la distanza media come criterio di fusione, implementarlo e applicarlo a un dataset sintetico con 4 cluster (100 punti,
make_blobs(centers=4, cluster_std=0.2)). - Se il dataset ha campioni e si vogliono cluster, quante iterazioni servono?
- Descrivere e implementare una versione divisiva (top-down): quale cluster dividere a ogni passo e con che criterio.
- Eseguire a mano l'agglomerativo con linkage single, complete e average sui punti , , , , e la versione divisiva.
Teoria usata: Clustering e k-meansIl clustering raggruppa osservazioni simili senza etichette, come preprocessing (un modello per ogni cluster) o come obiettivo (segmentazione clienti, organizzazione di documenti). K-means: si sceglie $K$, si inizializzano $K$ centroidi, si alterna assegnazione di ogni punto al centroide più vicino e aggiornamento di ogni centroide alla media dei suoi punti, fino a convergenza; minimizza $\mathrm{MSE}{\text{within}}=\frac1N\sum_k\sum{x_i\in C_k}|x_i-\mu_k|^2$ ma solo fino a un minimo locale, quindi dipende dall'inizializzazione. Il numero di cluster si sceglie col metodo del gomito (la dispersione cala sempre, si cerca dove rallenta) o con la gap statistic $\mathrm{Gap}(K)=E[\log W_K^{ref}]-\log W_K$ (si prende il più piccolo $K$ con $\mathrm{Gap}(K)\ge\mathrm{Gap}(K+1)-s_{K+1}$). Il clustering gerarchico agglomerativo parte da un cluster per punto e fonde i due più vicini (linkage single, complete, average, Ward) costruendo un dendrogramma; quello divisivo parte da un solo cluster. Programma di Telecomunicazioni: clustering.Clustering e k-means → (gerarchico, dendrogramma, linkage, k-means per la divisione); distanza euclidea in Prodotto scalare, norma e angoliIl prodotto scalare aggiunge a uno spazio vettoriale lunghezze e angoli: norma, disuguaglianza di Cauchy-Schwarz, angolo tra vettori in R^n, ortogonalità, proiezione su una retta, aree e volumi con il determinante della matrice dei prodotti scalari.Prodotto scalare, norma e angoli →.
Pseudocodice agglomerativo (distanza media)
clusters = [[0], [1], ..., [n-1]] # un cluster per campione
ripeti finché len(clusters) > m:
per ogni coppia di cluster (Ci, Cj):
d(Ci, Cj) = (1 / (|Ci| |Cj|)) * somma, su a in Ci e b in Cj, di ||a - b||
scegli la coppia con d minima, unisci Ci e Cj in un solo clusterNumero di iterazioni. Si parte con cluster e a ogni fusione ne resta uno in meno: per arrivare a cluster servono iterazioni. Con e : 96. Per arrivare a un cluster unico: . (Risposta esatta dell'appello: n_samples - n_clusters.)
Implementazione. Si conserva una matrice di assegnazione cmatrix con una riga per iterazione e una colonna per campione (riga = etichette dopo fusioni; la riga iniziale è ); le etichette finali sono cmatrix[-1]. La distanza media tra due cluster usa due cicli annidati: costo circa in tutto (tra tutte le coppie di cluster a ogni passo).
def fit(self, X):
clusters = [[i] for i in range(len(X))]
for step in range(1, len(X) - self.n_clusters + 1):
best, bd = None, np.inf
for j in range(len(clusters)):
for k in range(j + 1, len(clusters)):
d = average_cluster_distance(X[clusters[j]], X[clusters[k]])
if d < bd: best, bd = (j, k), d
j, k = best; clusters[j] += clusters[k]; del clusters[k] # fusione
labels = np.zeros(len(X), int)
for idx, cl in enumerate(clusters): labels[cl] = idx
self.cmatrix[step] = labelsCon 4 gruppi molto compatti () i 4 cluster finali coincidono con i gruppi veri.
Versione divisiva
Si parte da un unico cluster con tutti i punti. A ogni passo: (1) scelta del cluster da dividere: quello con la dispersione maggiore, per esempio la distanza media tra le coppie di punti del cluster, (un cluster con un solo punto non si divide); (2) divisione del cluster in due con il k-means con . Per ottenere cluster servono divisioni (non ).
for step in range(1, n_clusters):
cid = max((c for c in clusters if len(clusters[c]) > 1), key=lambda c: avg_pairwise_dist(X[clusters[c]]))
labels = KMeans(2).fit(X[clusters[cid]]).labels_
clusters[new_id] = clusters[cid][labels == 1]; clusters[cid] = clusters[cid][labels == 0](Nota sulla soluzione ufficiale: nella classe KMeans dell'appello, se il ciclo di iterazioni esce alla prima iterazione per convergenza immediata, la variabile labels usata dopo il ciclo non è mai stata assegnata, mentre l'assegnazione iniziale è chiamata labels_: con inizializzazioni fortunate può dare un errore. Nel k-means del laboratorio labels è assegnato prima del ciclo e il problema non c'è.)
4. Esempio a mano: , , , ,
Distanze: , , , , , , , , , .
Single linkage (minimo): 1) e a distanza (si fondono; pari merito: prima , poi ); 2) -: ; 3) -: . Altezze del dendrogramma: .
Complete linkage (massimo): , ; -: ; con : . Altezze .
Average linkage (media): , ; -: ; - e -; il minimo tra , e è , quindi si fondono e ; ultima fusione -: . Altezze .
I tre linkage danno la stessa gerarchia (gruppi , , poi ) e diverse altezze. Il salto grande prima dell'ultima fusione ( in average, in single) suggerisce cluster oltre a e , o cluster (, , ) se si taglia prima.
Matrice di assegnazione (average, , ): riga 0: ; dopo la fusione : ; dopo : ; dopo : ; finale: (sono fusioni).
Divisivo. Si parte con ; la distanza media tra coppie è . Il k-means con divide in e (dispersione : ha media e ; l'alternativa , ha ). Poi il cluster con la dispersione maggiore è (distanza media ; ha un solo punto): si divide in e . La gerarchia coincide con quella agglomerativa; in generale i due approcci possono dare risultati diversi.
Verifica
import numpy as np
x = np.array([1, 2, 6, 7, 15.]); D = np.abs(x[:, None] - x[None])
# average: (D[0,2]+D[0,3]+D[1,2]+D[1,3]) / 4 = 5 ; single: min = 4 ; complete: max = 6