Lekce 2 - Neuronové sítě - Perceptron
V minulé lekci, Neuronové sítě - Úvod, jsme si nastínili vstupní požadavky a jmenné konvence pro kurz neuronové sítě.
V této lekci tutoriálu Neuronové sítě - Pokročilé si nejprve povíme o fungování neuronu. Poté se pustíme do tréninku perceptronu a psaní algoritmů.
Neuron
Nejprve si ukážeme zjednodušený pohled na neuron z biologického hlediska:
Dendrity jsou vstupy neuronu, na které se napojují okolní neurony. Vstupy jsou pak předány do těla neuronu (nucleus), který může být excitován (aktivován) nebo ne. Když je neuron excitován, jeho výstup prochází axonem do ostatních neuronů. To je sice obrovské zjednodušení, ale pro naše účely to postačí.
Jak můžeme něco takového modelovat pomocí umělého neuronu? Potřebujeme k tomu vytvořit funkci, která přijímá vstupy od ostatních neuronů (dendrity), spočítá excitaci (nucleus) a výsledek pošle dál (axon). Pro zjednodušení jsou vstupy váženy a sečteny, výsledek je pak předán aktivační funkci a jejím výsledkem je výstup neuronu. To můžeme vidět na následujícím obrázku:

(práh zatím nebereme v úvahu)
Obrázek můžeme přepsat do matematického vzorce:

Než budeme pokračovat, sjednoťme si terminologii. Vektory označíme tučným textem (𝑥) a všechny jednorozměrné vektory budeme považovat za řádkové vektory. Všechna vektorová násobení budou bodové součiny (pokud není uvedeno jinak) - takže 𝑥𝑤𝑇 je skalární součin a výsledek je skalár. Vektory budeme indexovat od nuly (všimněme si, že obrázek používá číslo 1 jako první index).
Jelikož je aktivační funkce většinou nastavena předem, při tréninku neuronu (nebo celé neuronové sítě), hledáme váhy 𝑤, které vedou k nejlepšímu řešení.
A to je vše, co zatím potřebujeme vědět. Nyní můžeme přejít k prvnímu modelu - perceptronu.
Perceptron
Perceptron je nejjednodušší model – používá funkci
sign()
jako aktivační funkci f
.
Funkci sign()
definujeme následovně:

Vzorec definuje dělící nadrovinu (tj. nadrovinu, která rozděluje prostor prvků dat na dvě poloviny). Pokud mají například vstupní data dva rozměry, bude oddělující nadrovinou přímka, kde všechna data z první třídy budou nad přímkou, zatímco všechna data z druhé třídy skončí pod přímkou.
Jak již bylo naznačeno v předchozím odstavci, perceptron můžeme použít pro klasifikační úlohy, tedy k rozdělení dat do dvou tříd. Je zaručeno, že pro lineárně separovatelná data (data, která lze separovat) algoritmus učení perceptronu (viz níže) vždy najde nějakou nadrovinu dělící obě třídy (více v PDF od Shivaram Kalyanakrishnan).
Algoritmus učení perceptronu
Algoritmus učení je velmi jednoduchý. Váhy jsou inicializovány náhodně. Algoritmus hledá instanci, která je špatně klasifikována. Pokud je skutečná třída instance kladná (a tedy byla klasifikována jako negativní), instance se přidá k vektoru váhy. Pokud je na druhou stranu instance záporná (a byla klasifikována jako kladná), instance se od vektoru váhy odečte. Algoritmus končí, když jsou všechny instance správně klasifikovány.
Pro klasifikaci použijeme náhodná data generovaná sklearn knihovnou:
# Načtení knihoven import sklearn.datasets import sklearn.model_selection import sklearn.metrics import matplotlib.pyplot as plt import numpy as np # Definování funkce sign def sign(x): return 0 if x < 0 else 1 # Generování dat data, classes = sklearn.datasets.make_blobs(n_samples=100, n_features=2, centers=2, random_state=42) # Vykreslení dat plt.scatter(data[:,0], data[:,1], c=classes) plt.show()
Výsledný graf potom vypadá následovně:

Toto jsou data, která se snažíme klasifikovat. Pojďme si nyní napsat algoritmus učení perceptronu:
# Inicializace vah weights = np.random.RandomState(42).uniform(-2, 2, 2) # Opakování až do konvergence weights_changed = True while weights_changed: weights_changed = False # pro každý výskyt v datech for instance, target in zip(data, classes): # předpověď výstupu perceptronu prediction = sign(instance @ weights) if prediction == target: # správná klasifikace continue elif target == 1: # pozitivní klasifikace jako negativní - přidání instance k vahám weights = weights + instance elif target == 0: # negativní klasifikace jako pozitivní - odečtení instance od vah weights = weights - instance weights_changed = True
Jak jsme si již řekli, perceptron definuje dělící nadrovinu. Nadrovina je definována vahami perceptronu, a protože jsme ve 2D, oddělující nadrovinou je přímka. Vzorec přímky v normálním tvaru je 𝛼𝑥+𝛽𝑦+𝛾=0. V našem případě je 𝑤 normála přímky, a tedy 𝛼=𝑤0, 𝛽=𝑤1 a 𝛾=0 (normála je kolmá na přímku). Pojďme teď nalezenou dělící nadrovinu vykreslit:
# Výpočet sklonu přímky slope = - weights[0] / weights[1] # Vykreslení dat plt.scatter(data[:,0], data[:,1], c=classes) # Vykreslení oddělovací přímky plt.plot( [data.min(axis=0)[0], data.max(0)[0]], [slope * data.min(axis=0)[0], slope * data.max(axis=0)[0]], c='r') plt.show()
Výsledný graf je:

Jak můžeme vidět, přímka je mezi těmito dvěma třídami, nicméně o něco blíže ke žluté třídě. Pravděpodobně bychom chtěli mít hranici přesně mezi třídami. Protože jsou už ale všechny body klasifikovány správně, algoritmus nemůže přímku nijak upravit. To je obecně nevýhoda perceptronu. Najde sice dělící nadrovinu, ale nemusí to být právě ta nejlepší. Zkusme tedy změnit data a znovu perceptron potrénovat:
# GENEROVÁNÍ DAT data, classes = sklearn.datasets.make_blobs(n_samples=100, n_features=2, centers=2, random_state=48) # TRÉNINK PERCEPTRONU # Inicializace vah weights = np.random.RandomState(42).uniform(-2, 2, 2) # Opakování až do konvergence weights_changed = True while weights_changed: weights_changed = False # pro každou instanci v datech for instance, target in zip(data, classes): # predikce výstupu perceptronu prediction = sign(instance @ weights) if prediction == target: # správné zařazení continue elif target == 1: # pozitivní klasifikováno jako negativní - přidání instance k vahám weights = weights + instance elif target == 0: # negativní klasifikováno jako pozitivní - odečtení instance od vah weights = weights - instance weights_changed = True # VYKRESLENÍ # Vypočítání sklonu přímky slope = - weights[0] / weights[1] # Vykreslení dat plt.scatter(data[:,0], data[:,1], c=classes) # Vykreslení oddělovací přímky plt.plot( [data.min(axis=0)[0], data.max(0)[0]], [slope * data.min(axis=0)[0], slope * data.max(axis=0)[0]], c='r') plt.show()
Výsledný graf vypadá nyní takto:

Jak můžeme vidět, všechny body jsou správně klasifikovány, nicméně dělící nadrovina je přesně vedle žlutých bodů. To rozhodně není přímka, kterou jsme chtěli získat! Algoritmus se můžeme pokusit spustit vícekrát s různou inicializací vah a vybrat nejlepší výsledek, který můžeme získat. Můžeme také iterovat data nikoliv sekvenčně, ale náhodně. Další přístupy (které nevyžadují zapojení programátora) probereme později.
Zjednodušení pravidla aktualizace
Pravidlo aktualizace můžeme trochu zjednodušit pomocí vzorce 𝑤 = 𝑤 + (target - prediction) ∗ 𝑥. Když je předpověď správná, rozdíl target - prediction je 0, a neprovádí se žádná aktualizace. Když je 𝑡𝑎𝑟𝑔𝑒𝑡 = 0 a 𝑝𝑟𝑒𝑑𝑖𝑐𝑡𝑖𝑜𝑛=1 (negativní instance je klasifikována kladně), instance se odečte. V případě 𝑡𝑎𝑟𝑔𝑒𝑡 = 1 a 𝑝𝑟𝑒𝑑𝑖𝑐𝑡𝑖𝑜𝑛 = 0 (pozitivní instance klasifikována jako negativní), přičte se instance k vahám. Můžeme tak celou aktualizaci přepsat na jeden řádek.
Nakonec se ještě musíme ujistit, že sledujeme změny vah w. Nové váhy můžeme porovnat s váhami z předchozí iterace. Pokud nebyly provedeny žádné změny, může se algoritmus ukončit:
# GENEROVÁNÍ DAT data, classes = sklearn.datasets.make_blobs(n_samples=100, n_features=2, centers=2, random_state=42) # TRÉNINK PERCEPTRONU # Inicializace vah weights = np.random.RandomState(42).uniform(-2, 2, 2) # Opakování až do konvergence old_weights = None while (weights != old_weights).any(): old_weights = weights # pro každou instanci v datech for instance, target in zip(data, classes): # predikce výstupu perceptronu prediction = sign(instance @ weights) # aktualizace vah weights = weights + (target - prediction) * instance # VYKRESLENÍ slope = - weights[0] / weights[1] plt.scatter(data[:,0], data[:,1], c=classes) plt.plot( [data.min(axis=0)[0], data.max(0)[0]], [slope * data.min(axis=0)[0], slope * data.max(axis=0)[0]], c='r') plt.show()
Takto pak vypadá řešení v grafu:

Základy máme úspěšně za sebou. Příště půjdeme více do detailu 🙂
V příští lekci, Neuronové sítě - Krokování, bias a více dimenzí, si ukážeme krokování, bias a případ s 10-ti dimenzemi.