# Import potrebných balíčkov
from tensorflow.keras.datasets import fashion_mnist
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np
import numbers
import umap
Metódy znižovania rozmeru dát predstavujú jednu z možných aplikácií učenia bez učiteľa. Cieľom je znížiť rozmer dát tak, aby sa pritom stratilo čo najmenej užitočnej informácie. Znižovanie rozmeru môže mať rôzne ciele, napríklad:
My si teraz budeme ilustrovať postup znižovania rozmeru dát na účel vizualizácie.
Na ilustráciu použijeme dátovú množinu Fashion MNIST, ktorá obsahuje obrázky rôznych typov obuvi a oblečenia v malom rozlíšení $28 \times 28$ pixelov.
Obrázky v dátovej množine sú roztriedené do nasledujúcich tried:
label id | label | label id | label | |
---|---|---|---|---|
0 | T-shirt/top | 5 | Sandal | |
1 | Trouser | 6 | Shirt | |
2 | Pullover | 7 | Sneaker | |
3 | Dress | 8 | Bag | |
4 | Coat | 9 | Ankle boot |
Dátovú množinu načítame veľmi jednoducho, pretože balíček tensorflow
obsahuje pribalenú funkciu, ktorá to umožňuje
(X_train, Y_train), (X_test, Y_test) = fashion_mnist.load_data()
class_names = ["top", "trouser", "pullover", "dress",
"coat", "sandal", "shirt", "sneaker",
"bag", "ankle boot"]
Aby sme mali predstavu, ako dáta vyzerajú, zobrazíme si ďalej niekoľko náhodne zvolených vzoriek:
fig, axes = plt.subplots(5, 5)
fig.set_size_inches([7, 7])
for ax_row in axes:
for ax in ax_row:
ind = np.random.randint(0, X_train.shape[0])
ax.imshow(X_train[ind], cmap='Greys')
ax.set_title(class_names[Y_train[ind]])
ax.axis('off')
plt.subplots_adjust(hspace=0.5)
Keďže obrázky majú rozmer $28 \times 28$ pixelov, priestor je 784-rozmerný. Ak chceme vizualizovať jeho štruktúru, musíme dáta redukovať do 2-rozmerného priestoru. Tým prirodzene veľké množstvo informácie stratíme, ale v dobrom prípade sa budeme stále schopní dozvedieť veľa o štruktúre priestoru.
Ako prvú metódu na znižovanie rozmeru otestujeme metódu PCA. Ide o metódu, ktorá je veľmi rýchla, ale vie využiť len lineárne závislosti v dátach – nie nelineárne. Pri niektorých dátových množinách to však stačí.
pca = PCA()
points = pca.fit_transform(X_train.reshape((X_train.shape[0], -1)))
Body pred vizualizáciou premiešame – v pôvodnej dátovej množine sú zotriedené podľa tried. My chceme vidieť, či sú triedy vo vizualizácii oddelené alebo premiešané.
perm_ind = np.random.permutation(points.shape[0])
xx = points[perm_ind]
yy = Y_train[perm_ind]
Nakoniec nám zostáva už len vizualizovať všetky body, zafarbené podľa triedy. Ako vidno, metóda PCA nie je schopná dobre oddeliť jednotlivé triedy. Hoci niektoré triedy sú pomerne jasne separované (napr. bag a trouser), celkovo obrázok nie je čitateľný.
plt.figure(figsize=[10, 7])
plt.scatter(xx[:, 0], xx[:, 1], c=yy,
cmap=plt.cm.get_cmap('jet', len(class_names)),
rasterized=True)
cbar = plt.colorbar()
cbar.set_ticks(range(len(class_names)))
cbar.set_ticklabels(class_names)
plt.xlabel("dim 1")
plt.ylabel("dim 2")
Ešte menej by sme videli, keby sme body nezafarbili:
plt.figure(figsize=[10, 7])
plt.scatter(xx[:, 0], xx[:, 1], rasterized=True)
plt.xlabel("dim 1")
plt.ylabel("dim 2")
Všimnite si, že pri vykresľovaní bodov používame argument rasterized=True
. Ten indikuje, že príslušná časť grafu sa má rasterizovať. Pri zobrazovaní veľmi veľkých počtov bodov je to výhodné urobiť – inak by obrázok po uložení do vektorového formátu bolo veľmi ťažké zobraziť.
Obrázok sa prirodzene dá uložiť do rastrového formátu aj ako celok — ibaže potom sú v rastrovom formáte aj osi a ďalšie časti obrázka. Tomu sa je vo všeobecnosti lepšie vyhnúť: hlavne v prípade, že sa má obrázok použiť v nejakom texte: napríklad v záverečnej práci, v článku a pod.
V prípade, kedy je vektorový obrázok príliš zložitý, rasterizácia len jednej, problematickej časti predstavuje dobrý kompromis.
Znižovanie rozmeru dát pomocou metódy UMAP bude trvať podstatne dlhšie než pomocou metódy PCA. Na druhej strane sa dá očakávať, že aj výsledky budú podstatne lepšie, pretože metóda UMAP nie je obmedzená len na lineárne zákonitosti v dátach.
V kóde, ktorý sme použili vyššie, stačí vymeniť metódu PCA za metódu UMAP – inak môže zostať rovnaký, keďže metóda UMAP implementuje unifikované rozhrania podľa balíčka scikit-learn
.
um = umap.UMAP(verbose=True)
points = um.fit_transform(X_train.reshape((X_train.shape[0], -1)))
perm_ind = np.random.permutation(points.shape[0])
xx = points[perm_ind]
yy = Y_train[perm_ind]
xt = X_train[perm_ind]
plt.figure(figsize=[10, 7])
cmap = plt.cm.get_cmap('jet', len(class_names))
plt.scatter(xx[:, 0], xx[:, 1], c=yy,
cmap=cmap,
rasterized=True)
cbar = plt.colorbar()
cbar.set_ticks(range(len(class_names)))
cbar.set_ticklabels(class_names)
plt.xlabel("dim 1")
plt.ylabel("dim 2")
Z tohto obrázka sa už o štruktúre dátovej množiny dozvedáme omnoho viac. Vidno, že vzorky sú rozdelené do 4 veľkých skupín. Jedna obsahuje nohavice, druhá kabelky, tretia zmiešava rôzne typy topánok a štvrtá rôzne typy tričiek, šiat a kabátov.
Vidíme tiež, že zatiaľ čo tričká a kabáty sú dosť premiešané, topánky sú aj vo vnútri spoločného zhluku pomerne dobre oddeliteľné.
Z vizualizácie pomocou metódy UMAP vidíme, že z nejakého dôvodu existuje spojitý prechod medzi tričkami a kabelkami. Bolo by zaujímavé zistiť, aké vzorky sú na rozhraní oboch zhlukov. Aby sme to zistili, môžeme si do grafu namiesto všetkých bodov vykrelisť len časť z nich, ale vizualizovať na ich pozíciách aj pôvodné obrázky. To nám poskytne plnšiu vizuálnu informáciu o charaktere zhlukov.
Najprv si definujme pomocnú funkciu, ktorá bude fungovať podobne ako funkcia scatter
ibaže namiesto bodov bude vykresľovať obrázky.
def imscatter(x, y, images, ax=None, zoom=1,
frame_cmap=None, frame_c=None,
frame_linewidth=1, **kwargs):
if ax is None:
ax = plt.gca()
if isinstance(frame_cmap, str):
frame_cmap = plt.cm.get_cmap(frame_cmap)
elif frame_cmap is None:
frame_cmap = plt.cm.get_cmap('jet')
if len(images) == 1:
images = [images[0] for i in range(len(x))]
if frame_c is None:
frame_c = ['k' for i in range(len(x))]
x, y = np.atleast_1d(x, y)
artists = []
for i, (x0, y0) in enumerate(zip(x, y)):
fc = frame_c[i]
if isinstance(fc, numbers.Number):
fc = frame_cmap(fc)
im = OffsetImage(images[i], zoom=zoom, **kwargs)
ab = AnnotationBbox(im, (x0, y0), xycoords='data', frameon=True,
bboxprops=dict(edgecolor=fc,
linewidth=frame_linewidth))
artists.append(ax.add_artist(ab))
ax.update_datalim(np.column_stack([x, y]))
ax.autoscale()
return artists
num2show = 800
plt.figure(figsize=[15, 10])
imscatter(xx[:num2show, 0], xx[:num2show, 1],
xt[:num2show], cmap='Greys', zoom=1.2,
frame_c=yy[:num2show], frame_cmap=cmap,
frame_linewidth=2)
plt.xlabel("dim 1")
plt.ylabel("dim 2")
Z obrázka by malo byť vidno, že kabelky, ktoré susedia s obrázkami tričiek a šiat naozaj menia postupne tvar, takže niektoré môžu byť v nízkom rozlíšení a čiernobielych farbách zameniteľné s oblečením.