In [0]:
# 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

Znižovanie rozmeru dát

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:

  • chceme znížiť výpočtové nároky na spracovanie dát;
  • vizualizácia vysokorozmerných dát;
  • ...

My si teraz budeme ilustrovať postup znižovania rozmeru dát na účel vizualizácie.

Načítanie dát

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

In [2]:
(X_train, Y_train), (X_test, Y_test) = fashion_mnist.load_data()
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/train-labels-idx1-ubyte.gz
32768/29515 [=================================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/train-images-idx3-ubyte.gz
26427392/26421880 [==============================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/t10k-labels-idx1-ubyte.gz
8192/5148 [===============================================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/t10k-images-idx3-ubyte.gz
4423680/4422102 [==============================] - 0s 0us/step
In [0]:
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:

In [33]:
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)

Znižovanie rozmeru dát pomocou PCA a vizualizácia

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čí.

In [0]:
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é.

In [0]:
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ý.

In [13]:
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")
Out[13]:
Text(0, 0.5, 'dim 2')

Ešte menej by sme videli, keby sme body nezafarbili:

In [15]:
plt.figure(figsize=[10, 7])
plt.scatter(xx[:, 0], xx[:, 1], rasterized=True)
plt.xlabel("dim 1")
plt.ylabel("dim 2")
Out[15]:
Text(0, 0.5, 'dim 2')

Poznámka: Rasterizácia časti obrázka

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 UMAP a vizualizácia

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.

In [18]:
um = umap.UMAP(verbose=True)
points = um.fit_transform(X_train.reshape((X_train.shape[0], -1)))
UMAP(a=None, angular_rp_forest=False, b=None, init='spectral',
     learning_rate=1.0, local_connectivity=1.0, metric='euclidean',
     metric_kwds=None, min_dist=0.1, n_components=2, n_epochs=None,
     n_neighbors=15, negative_sample_rate=5, random_state=None,
     repulsion_strength=1.0, set_op_mix_ratio=1.0, spread=1.0,
     target_metric='categorical', target_metric_kwds=None,
     target_n_neighbors=-1, target_weight=0.5, transform_queue_size=4.0,
     transform_seed=42, verbose=True)
Construct fuzzy simplicial set
Tue Aug 20 06:40:36 2019 Finding Nearest Neighbors
Tue Aug 20 06:40:36 2019 Building RP forest with 17 trees
Tue Aug 20 06:40:45 2019 NN descent for 16 iterations
	 0  /  16
	 1  /  16
	 2  /  16
	 3  /  16
Tue Aug 20 06:41:05 2019 Finished Nearest Neighbor Search
Tue Aug 20 06:41:07 2019 Construct embedding
	completed  0  /  200 epochs
	completed  20  /  200 epochs
	completed  40  /  200 epochs
	completed  60  /  200 epochs
	completed  80  /  200 epochs
	completed  100  /  200 epochs
	completed  120  /  200 epochs
	completed  140  /  200 epochs
	completed  160  /  200 epochs
	completed  180  /  200 epochs
Tue Aug 20 06:42:17 2019 Finished embedding
In [0]:
perm_ind = np.random.permutation(points.shape[0])
xx = points[perm_ind]
yy = Y_train[perm_ind]
xt = X_train[perm_ind]
In [54]:
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")
Out[54]:
Text(0, 0.5, '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é.

Pokročilejšia vizualizácia

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.

In [0]:
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
In [60]:
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")
Out[60]:
Text(0, 0.5, '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.