Nauč se Python > Kurzy > Datový kurz PyLadies > Shlukovací analýza > Shlukování (Clustering)

Shlukování (Clustering)

Shlukování (shluková analýza, clusterová analýza) je typickým představitelem strojového učení bez učitele. Problém k řešení spočívá v tom, že se neznámá data pokoušíme různými algoritmy rozdělit do skupin (shluků, clusterů) tak, aby v jedné skupině byla ta individua, která jsou si nejvíce podobná.

„Největší podoba“ přitom nejčastěji znamená, že jsou body v prostoru (individua), blízko u sebe. Prostor je definován jako v mnoha předchozích příkladech vlastnostmi zkoumaných objektů a počet těchto vlastností nám udává počet dimenzí. „Blízko u sebe“ je také možné definovat různými způsoby, které mají velký vliv na výsledek výpočtů. Asi nejznámějšími způsoby výpočtu vzdálenosti dvou bodů v prostoru jsou Euklidovská a Manhattanská vzdálenost. Obě je možné vidět na následujícím obrázku (zdroj).

vzdálenosti

Euklidovská vzdálenost je vyznačená zelenou čárou a jedná se o nejkratší vzdálenost mezi dvěma body. Manhattanská vzdálenost je inspirovaná pravoúhlým systémem křižovatek na Manhattanu a v obrázku ji reprezentují zbývající tři čáry, které, i když se to nemusí na první pohled zdát, mají všechny stejnou délku.

Tento obecný úvod má za cíl připomenout, že algoritmy strojového učení jsou často velmi obecné a finální výsledek velmi závisí na jejich správné volbě, nastavení, použitých metrikách a interpretaci výsledků. Takže ve finále výsledky rozhodně nejsou zadarmo.

My se v dnešních příkladech omezíme jen na Euklidovskou vzdálenost a pro názornost a snadnou vizualizaci na dvě dimenze, ale na konci si ukážeme použití i s více dimenzemi. Pro začátek si ukážeme, jak nejznámější shlukovací algoritmy fungují a jak si poradí s jednoduchými daty a pak vyzkoušíme složitější příklad.

k-means, k-median

Jedním z nejznámějších a také nejrychlejších shlukovacích algoritmů je k-means. Jeho princip je velice prostý:

  1. Na začátku se mezi body v prostoru vloží tzv. centroidy, což jsou body reprezentující středy výsledných shluků.
  2. Každý bod v prostoru se přiřadí k tomu centroidu, který je mu nejblíže. Dojde k prvnímu rozdělení na shluky.
  3. Centroidy se posunou tak, aby byly uprostřed mezi body, které v tu chvíli patří do jejich shluku. Zde je rozdíl mezi k-means a k-median ve výpočtu nové pozice pro centroidy, kdy první jmenovaný využívá průměr hodnot bodů ze stejného shluku, zatímco druhý jmenovaný využívá medián.

Body 2 a 3 se pak opakují tak dlouho, dokud nedojde k rovnovážnému stavu a pohyb centroidů se nezastaví. Celý proces je možné sledovat na následujících obrázcích, kdy každý z nich obsahuje jistou fázi od začátku až po ustálený stav.

k_means_empty

k_means_empty

Po umístění centroidů a přiřazení bodů ke shlukům vidíme, že rozdělení není dokonalé.

k_means_empty

Po další iteraci je to daleko lepší, ale některé body ještě zbývá přesněji určit.

k_means_empty

Velkou nevýhodou těchto jinak velmi dobře fungujících algoritmů je to, že jako vstupní parametr jim musíme zadat ono „k“, které určuje, kolik shluků hledáme a tedy kolik centroidů bude na začátku do prostoru umístěno. Je samozřejmě možné spustit k-means vícekrát s různým nastavením a sledovat vývoj výsledků.

Pojďme si to zkusit na náhodně generovaných datech.

In [1]:
from matplotlib import cm
from matplotlib import pyplot as plt
import seaborn as sns

from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans

%matplotlib inline
sns.set()

Nejprve si připravíme náhodně generovaná data pomocí funkce make_blobs.

In [2]:
X, y_true = make_blobs(n_samples=500, centers=4, cluster_std=0.50, random_state=0)

y_true obsahuje pro každý bod informaci, ke kterému shluku doopravdy patří. My se ovšem soustředíme na vizuální kontrolu výsledků.

In [3]:
plt.scatter(X[:, 0], X[:, 1]);

V prostoru máme celkem 4 shluky a celkem 500 bodů. Nejprve můžeme vyzkoušet, co se stane, když k-means nastavíme rozdílný počet hledaných shluků.

In [4]:
kmeans = KMeans(n_clusters=3)
kmeans.fit(X)
vysledek = kmeans.predict(X)
centroidy = kmeans.cluster_centers_
In [5]:
plt.scatter(X[:, 0], X[:, 1], c=vysledek, cmap=cm.viridis);
plt.scatter(centroidy[:, 0], centroidy[:, 1], color="blue", s=20);

Na výsledné vizualizaci je vidět, že špatně zvolený počet hledaných shluků měl za následek spojení dvou oblastí do jedné. I když výsledek není nejlepší, je dobré si všimnout, že na dva ze čtyř shluků to nemá příliš zásadní vliv. Zkusme to ještě jednou a tentokrát správně.

In [6]:
kmeans = KMeans(n_clusters=4)
kmeans.fit(X)
vysledek = kmeans.predict(X)
centroidy = kmeans.cluster_centers_
In [7]:
plt.scatter(X[:, 0], X[:, 1], c=vysledek, cmap=cm.viridis);
plt.scatter(centroidy[:, 0], centroidy[:, 1], color="blue", s=20);

Teď už výsledek odpovídá našim představám a je z něj patrná i příslušnost ke shlukům u těch bodů, které jsou spíše na okraji svých shluků.

Hierarchické shlukování

Hierarchické shlukování obsahuje skupinu algoritmů, které při svém snažení postupují hierarchicky od jednotlivých bodů postupným připojováním k jednomu velkému shluku (aglomerativní metody) nebo od jednoho velkého shluku postupným dělením až k jednotlivým bodům (divisivní metody).

Podstatou je i zde měření vzdálenosti mezi jednotlivými body, mezi bodem a již existujícím shlukem nebo mezi dvěma shluky. Tyto vzdálenosti se mohou v závislosti na zvoleném typu podstatně lišit, jak demonstruje následující obrázek (zdroj).

linkage

V praktickém pokusu se podíváme na stejná data jako před chvílí. K samotnému shlukování použijeme funkci linkage z modulu scipy.cluster.hierarchy. sklearn také obsahuje implementaci hierarchickéhu shlukování, konkrétně sklearn.cluster.AgglomerativeClustering, ale scipy nabízí i možnost snadného vykreslení tzv. dendrogramu. Dendrogram je grafickým znázorněním průběhu shlukování, který nám velmi názorně ukáže rozdíly ve vzdálenostech jednotlivých shluků.

In [8]:
from scipy.cluster.hierarchy import linkage, dendrogram

matice_vzdalenosti = linkage(X)
plt.figure()
dendrogram(matice_vzdalenosti)
plt.show()

Dendrogram výše znázorňuje proces shlukování. Úplně dole jsou nejdříve spojovány do menších shluků jednotlivé body, které postupně tvoří větší a větší shluky. Čím delší je svislá čára, tím větší vzdálenost musel algoritmus překonat, aby mohl dva shluky spojit v jeden. Na začátku jsou vzdálenosti krátké a tak spojení nic nebrání. Postupně je to ale čím dál tím horší a spojení dvou shluků napravo už není z hlediska vzdálenosti tak snadné. Následně se k těmto dvěma připojí téměř ihned shluk třetí a po nějaké chvíli dojde i ke spojení s posledním samostatným shlukem do jednoho velkého.

Dendrogram však sám o sobě není výsledkem shlukování. K tomu, abychom získali body rozdělené do shluků, musíme po prozkoumání dendrogramu říci, kolik shluků chceme případně jaká vzdálenost je pro nás již dostatečně velká, aby k dalšímu shlukování nedošlo. To vše se dá z dendrogramu vyčíst a následně aplikovat.

My známe počet shluků a tak je jejich zjištění z výsledku snadné.

In [9]:
from scipy.cluster.hierarchy import fcluster

vysledek = fcluster(matice_vzdalenosti, 4, criterion="maxclust")

plt.scatter(X[:, 0], X[:, 1], c=vysledek, cmap=cm.viridis);

Ovšem úplně stejně bychom si mohli říci, že vzdálenost 1.0 je pro nás hranice, nad kterou už shluky nechceme dále spojovat, a výsledek by byl stejný.

In [10]:
vysledek = fcluster(matice_vzdalenosti, 1.0, criterion="distance")

plt.scatter(X[:, 0], X[:, 1], c=vysledek, cmap=cm.viridis);

U vzdálenosti 1.5 už by došlo ke sloučení tří červených shluků a samostatně už by zůstal jen ten zelený.

In [11]:
vysledek = fcluster(matice_vzdalenosti, 1.5, criterion="distance")

plt.scatter(X[:, 0], X[:, 1], c=vysledek, cmap=cm.viridis);

DBSCAN

Třetím algoritmem do party je tzv. DBSCAN, tedy Density-Based Spatial Clustering of Applications with Noise, který, jak už jeho název napovídá, provádí shlukování na základě hustoty bodů v prostoru a je schopen detekovat šum — tedy body, které do žádného shluku nepatří.

DBSCAN si na začátku vezme jeden bod a prozkoumá jeho okolí. Pokud v tomto okolí najde dostatek sousedních bodů, označí je jako shluk a začne takto přeskakovat z jednoho bodu na další a přidávat je do stejného shluku. Pokud v okolí tohoto bodu není dostatek sousedů, je označen za šum. Jakmile je shluk kompletní a v okolí není žádný další bod k přezkoumání, vybere se další ještě nenavštívený bod a celý proces pokračuje stejným způsobem dále.

Z popisu je patrné, že na výsledek bude mít zásadní vliv prohledávané okolí bodu a počet sousedů, které je třeba najít, aby z nich mohl shluk vzniknout.

Zkusme tedy DBSCAN beze změny základních parametrů — prohledávané okolí 0,5; minimální počet sousedů 5 — pustit na naše testovací data.

In [12]:
from sklearn.cluster import DBSCAN

dbscan = DBSCAN()
vysledek = dbscan.fit_predict(X)
In [13]:
plt.scatter(X[:, 0], X[:, 1], c=vysledek, cmap=cm.viridis);

Tento výsledek vypadá velmi dobře a oproti dříve představeným možnostem má tu výhodu, že nás upozorňuje i na šum — tedy na body, které jsou ve větší vzdálenosti od zbytku shluku a tedy do něj již nemusejí patřit.

DBSCAN má ještě jednu velkou výhodu — poradí si i se shluky, u kterých by si například k-means vylámal zuby.

In [14]:
from sklearn.datasets import make_circles

X, y_true = make_circles(n_samples=300, factor=0.2, random_state=0)

plt.scatter(X[:, 0], X[:, 1]);
In [15]:
dbscan = DBSCAN()
vysledek = dbscan.fit_predict(X)

plt.scatter(X[:, 0], X[:, 1], c=vysledek, cmap=cm.viridis);

… a další

Shlukovacích algoritmů, principů a implementací existuje daleko více. My jsme si zde ukázali tři nejznámější zástupce tří různých kategorií. Hezký přehled je k dispozici na wikipedii a jejich implementace pak v sklearn.

Shluky kosatců

Zkusme si teď provést shlukovací analýzu známých kosatců z datasetu iris. Protože provádíme učení bez učitele, výsledky, které jsme používali u klasifikace, nás nebudou zajímat, a soustředíme se jen na vizuální kontrolu výsledku.

Kosatce, jak už víme, mají celkem 4 atributy (dva rozměry dvou druhů listů). U vizualizace si tedy nevystačíme s jedním grafem, ale využijeme pair plot nebo scatter matrix.

In [16]:
import pandas as pd  # Seaborn neumí vizualizovat matice z numpy
from sklearn.datasets import load_iris

iris = load_iris()
data = pd.DataFrame(iris.data, columns=iris.feature_names)

data
Out[16]:
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)
0 5.1 3.5 1.4 0.2
1 4.9 3.0 1.4 0.2
2 4.7 3.2 1.3 0.2
3 4.6 3.1 1.5 0.2
4 5.0 3.6 1.4 0.2
... ... ... ... ...
145 6.7 3.0 5.2 2.3
146 6.3 2.5 5.0 1.9
147 6.5 3.0 5.2 2.0
148 6.2 3.4 5.4 2.3
149 5.9 3.0 5.1 1.8

150 rows × 4 columns

In [17]:
sns.pairplot(data);

Bez barevného rozlišení se zdá, že máme co dočinění jen se dvěma shluky. Uvidíme, jak se s tím poperou shlukovací algoritmy. Začneme s hierarchickým shlukováním, protože dendrogram nám může poskytnout užitečné informace.

In [18]:
matice_vzdalenosti = linkage(iris.data)
plt.figure()
dendrogram(matice_vzdalenosti)
plt.show()

A opravdu. Zdá se, že dva druhy kosatců k sobě mají velmi blízko a jsou vzdáleny od třetího druhu. Jaký výsledek dostaneme, když si od hierarchického modelu poručíme dva nebo tři clustery?

In [19]:
data["vysledek"] = fcluster(matice_vzdalenosti, 2, criterion="maxclust")

sns.pairplot(data, hue="vysledek", vars=['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']);

A pro očekávané tři shluky?

In [20]:
data["vysledek"] = fcluster(matice_vzdalenosti, 3, criterion="maxclust")

sns.pairplot(data, hue="vysledek", vars=['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']);
/home/lbalhar/.virtualenvs/naucse/lib/python3.7/site-packages/seaborn/distributions.py:283: UserWarning: Data must have variance to compute a kernel density estimate.
  warnings.warn(msg, UserWarning)

V tomto případě jsme se dočkali oddělení dvou bodů, které se v několika grafech zobrazují ve větší vzdálenosti od zbytku, do samostatného shluku.

Dočkáme se lepšího výsledku od k-means?

In [21]:
kmeans = KMeans(n_clusters=3, random_state=0)
kmeans.fit(iris.data)
data["vysledek"] = kmeans.predict(iris.data)
In [22]:
sns.pairplot(data, hue="vysledek", vars=['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']);

Zde je výsledek velmi zdařilý. To si říká o porovnání s originálními shluky, které máme k dispozici.

In [23]:
data["label"] = iris.target
In [24]:
sns.pairplot(data, hue="label", vars=['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']);