Obsah
1. Framework Torch: základy práce s neuronovými sítěmi
2. Idealizovaný model neuronu používaný v umělých neuronových sítích
5. Vytvoření feed-forward sítě z jednotlivých neuronů
6. Vstupní vrstva, výstupní vrstva a skryté vrstvy neuronů
7. Trénink (učení) sítě s využitím trénovacích dat
8. Tvorba neuronové sítě ve frameworku Torch
10. Konstrukce neuronové sítě se třemi vrstvami
12. Použití neuronové sítě na sadě validačních dat
13. Úplný zdrojový kód dnešního prvního demonstračního příkladu
14. Parametry ovlivňující chování sítě
15. Příliš malý počet neuronů ve skryté vrstvě
16. Jednoduchá síť odhadující součet dvou prvků
17. Odhad součtu sítí využívající aktivační funkci Tanh
18. Použití aktivační funkce ReLU namísto Tanh
19. Úplný zdrojový kód dnešního druhého demonstračního příkladu
1. Framework Torch: základy práce s neuronovými sítěmi
V dnešním článku o frameworku Torch se budeme zabývat problematikou vytvoření jednoduchých umělých neuronových sítí (zkráceně jen neuronových sítí neboli neural network, nn) a taktéž způsobem tréninku (učení) těchto sítí. Již na začátku je nutné říct, že se jedná o dosti rozsáhlou problematiku, pro jejíž pochopení se navíc očekává alespoň základní znalost teorie neuronových sítí. I z tohoto důvodu se zpočátku zaměříme na jednoduché sítě typu feed-forward se třemi vrstvami neuronů, přičemž neurony na sousedních vrstvách budou propojeny (synapsemi) systémem „každý s každým“ a informace mezi neurony potečou pouze jedním směrem (forward). V dalších částech tohoto seriálu si popíšeme i další typy sítí, v nichž bude použito více vrstev neuronů, neurony budou propojeny odlišným způsobem, budou použity jiné aktivační funkce atd.
Neuronové sítě se používají zejména v těch projektech, v nichž je zapotřebí vytvořit funkční systém už ve chvíli, kdy ještě neznáme všechny možné kombinace vstupů a výstupů, popř. když je chování systému, který se implementuje, tak složité, že klasický návrh a implementace algoritmů by byl příliš zdlouhavý nebo s danými prostředky (čas, počet vývojářů a testerů) neefektivní. Jednou z nevýhod neuronových sítí může být to, že je někdy velmi obtížné zjistit, jaký problém jsme vlastně neuronovou síť naučili řešit. Výsledná síť se totiž (pokud se nebudeme hodně snažit zjistit více) chová jako blackbox, o němž není snadné říct, jaké konkrétní rozhodování ten který neuron provádí (v některých případech to ovšem možné je). Kritickou úlohu zde sehrává výběr vhodné množiny trénovacích dat. K tomu se však dostaneme až v dalších částech seriálu, v nichž se zmíníme o již existujících trénovacích datech.
2. Idealizovaný model neuronu používaný v umělých neuronových sítích
Při práci s umělými neuronovými sítěmi je vhodné vědět, jak je vlastně taková síť zkonstruována a z jakých prvků se skládá. Základním stavebním prvkem je umělý neuron, resp. velmi zjednodušený a idealizovaný model skutečného neuronu. Původní model neuronu byl navržen Warrenem McCullochem a Walterem Pittsem (MCP) již ve čtyřicátých letech minulého století, z čehož plyne, že neuronové sítě nejsou jen módním výstřelkem poslední doby (naopak, moderní GPU umožňují jejich nasazení i tam, kde to dříve nebylo možné, to je však téma na další článek.). Na dalším obrázku jsou naznačeny prvky modelu neuronu:
Obrázek 1: Idealizovaný model neuronu.
Vidíme, že neuron může mít libovolný počet vstupů (na obrázku jsou tři vstupy x1, x2 a x3, ovšem může to být jen jeden vstup nebo i sto vstupů) a má pouze jeden výstup y. Vstupem a výstupem jsou reálná čísla; typicky bývá výstup upraven aktivační funkcí tak, že leží v rozsahu ← 1..1> nebo <0..1>. Dále na schématu vidíme váhy w1, w2 a w3. Těmito váhami jsou vynásobeny vstupní hodnoty. Váhy vlastně představují stav neuronu, tj. o funkci, na kterou byl neuron natrénován (naučen). Vstupní hodnoty x1 až xn jsou tedy postupně vynásobeny váhami w1 až wn a výsledky součinu jsou v neuronu sečteny, takže získáme jediné reálné číslo. Toto číslo je zpracováno aktivační funkcí (ta již většinou žádný stav nemá, ostatně stejně jako funkce pro výpočet sumy) výsledek této funkce je poslán na výstup neuronu.
Neuron tedy provádí tento výpočet:
y = f(w1x1 + w2x2 + … + wnxn)
3. Role biasu
Ve skutečnosti není stav neuronu pro n vstupů x1 až xn určen pouze n vahami w1 až wn. Musíme přidat ještě váhu w0, na kterou je připojena konstanta 1 (někdy se proto můžeme setkat s nákresem neuronové sítě, v níž se nachází speciální neurony bez vstupů a s jedničkou na výstupu). Model neuronu se přidáním nového vstupu nepatrně zkomplikuje:
Obrázek 2: Idealizovaný model neuronu s biasem.
I výpočet bude vypadat odlišně, neboť do něho přidáme nový člen:
y = f(w0 + w1x1 + w2x2 + … + wnxn)
Tato přidaná váha se někdy nazývá bias, protože vlastně umožňuje posouvat průběh aktivační funkce nalevo a napravo, v závislosti na jeho hodnotě.
4. Aktivační funkce
Bez aktivační funkce by se neuron choval jednoduše – spočítal by vážený součet vstupů a výsledek by poslal na výstup. Aktivační funkce, kterou jsme v předchozích dvou kapitolách označovali symbolem f, do celého výpočtu vnáší nelinearitu. Nejjednodušší aktivační funkce může pro vstupní hodnoty <0 vracet –1 a pro hodnoty ≥0 vracet 1, což vlastně říká, že je nutné dosáhnout určité hraniční hodnoty váženého součtu vstupů, aby byl neuron aktivován (tj. na výstup vyslal jedničku a nikoli –1). Ostatně právě zde znovu vidíme význam biasu, který onu hraniční hodnotu posunuje.
Obrázek 3: Aktivační funkce ReLU.
V praxi je však aktivační funkce složitější, než zmíněný jednotkový skok. Často se používá ReLU (rectified linear unit), sigmoid nebo hyperbolický tangent. Pro specializované účely se však používají i další funkce, které dokonce nemusí mít monotonní průběh. S dalšími podporovanými funkcemi se seznámíme příště.
Obrázek 4: Aktivační funkce Tanh.
5. Vytvoření feed-forward sítě z jednotlivých neuronů
Samostatné neurony i s aktivační funkcí stále provádí velmi jednoduchou činnost, ovšem aby se mohly stát součástí složitějšího systému (řekněme automatického řízení auta), musíme z nich vytvořit síť. Jedna z nejjednodušších forem umělé neuronové sítě se nazývá feed-forward, a to z toho důvodu, že informace (tedy vstupní hodnoty, mezihodnoty i hodnoty výstupní) touto sítí tečou jen jedním směrem (při učení je tomu jinak). Neurony jsou uspořádány pravidelně do vrstev:
Obrázek 5: Uspořádání neuronů do vrstev ve feed-forward síti.
Kolečka na obrázku představují jednotlivé neurony, přičemž žlutě jsou označeny neurony na vstupu, zeleně „interní“ (skryté) neurony a červeně neurony, které produkují kýžený výstup neuronové sítě.
Zcela nalevo jsou šipkami naznačeny vstupy. Jejich počet je prakticky zcela závislý na řešeném problému. Může se jednat jen o několik vstupů (viz naše testovací síť popsaná níže, která zpracuje dvě reálná čísla), ovšem pokud například budeme tvořit síť určenou pro rozpoznání objektů v rastrovém obrázku, může být počet vstupů roven počtu pixelů.
6. Vstupní vrstva, výstupní vrstva a skryté vrstvy neuronů
Vraťme se ještě jednou k obrázku číslo 5.
Povšimněte si, že vstupní neurony mají vlastně zjednodušenou funkci, protože mají jen jeden vstup. V mnoha typech sítí tyto neurony jen rozesílají vstup na další neurony a neprovádí žádný složitější výpočet, například u nich není použita aktivační funkce, ovšem to již záleží na konkrétní konfiguraci sítě. Dále stojí za povšimnutí, že neurony posílají svůj výstup neuronům na nejbližší další vrstvě; nejsou zde tedy žádné zkratky, žádné zpětné vazby atd. Existují samozřejmě složitější typy sítí, těmi se teď ale nebudeme zabývat. Dále tato síť propojuje neurony na sousedních vrstvách systémem „každý s každým“. V našem konkrétním příkladu mají neurony na prostřední vrstvě dva vstupy, protože předchozí vrstva má jen dva neurony. Ovšem neurony na poslední vrstvě již musí mít tři vstupy.
Poznámka: může se stát, že síť bude po naučení obsahovat neurony, jejichž váhy na vstupu budou nulové. To vlastně značí, že ze sítě některé spoje (šipky) zmizí, protože vynásobením jakéhokoli vstupu nulou dostaneme zase jen nulu.
První vrstva s jednoduchými („hloupými“) neurony se nazývá vstupní vrstva, poslední vrstva je vrstva výstupní. Vrstvy mezi vrstvou vstupní a výstupní, kterých může být teoreticky libovolné množství, se nazývají skryté vrstvy.
„Paměť“ neuronové sítě je tvořena vahami na vstupech neuronů (včetně biasu):
Vrstva | Neuronů | Počet vstupů/neuron | Počet vah/neuron | Celkem |
---|---|---|---|---|
1 | 2 | 1 | 2 | 4 |
2 | 3 | 2 | 3 | 9 |
3 | 2 | 3 | 4 | 8 |
∑ | 7 | 21 |
V praxi se používají sítě s více vrstvami a především s větším počtem neuronů v každé vrstvě. Stavový prostor a tím i schopnosti sítě se tak prudce rozšiřují (viz již zmíněná problematika rozpoznávání objektů v rastrových obrázcích).
Poznámka: někdy se počet neuronů v umělých sítích porovnává s počtem neuronů v mozku, ale to je ve skutečnosti dost zavádějící, neboť záleží spíše na uspořádání sítě, složitosti neuronů (více výstupů) atd.
7. Trénink (učení) sítě s využitím trénovacích dat
Nejzajímavější je proces tréninku (učení) sítě. Ten může probíhat několika způsoby, ovšem nejčastější je učení založené na tom, že na vstup sítě přivedeme data, u nichž dopředu známe očekávaný výsledek (v Torchi se jedná o tenzor). Síť pro tato vstupní data provede svůj odhad a na základě rozdílů mezi odhadem sítě a očekávaným výsledkem se více či méně sofistikovanými algoritmy nepatrně pozmění váhy wi na vstupech do neuronů (včetně biasu, tedy w0). Konkrétní míra změn váhy na vstupech neuronů je globálně řízena dalším parametrem či parametry, z nichž ten nejdůležitější ovlivňuje rychlost učení. Ta by neměla být příliš nízká (to vyžaduje objemná trénovací data nebo jejich opakování), ale ani příliš vysoká. Základní algoritmus učení sítě se jmenuje backpropagation, protože se váhy skutečně mění v opačném směru – od výstupů (na něž se přivede vypočtená chyba) ke vstupům. Asi nejlépe je tento koncept popsán v článku dostupném na adrese https://mattmazur.com/2015/03/17/a-step-by-step-backpropagation-example/, tuto část za nás však vykoná Torch automaticky (navíc alternativně s dopomocí GPU).
8. Tvorba neuronové sítě ve frameworku Torch
V následujících čtyřech kapitolách si ukážeme, jak lze vytvořit jednoduchou neuronovou síť v využitím frameworku Torch. Navrhovaná síť bude mít dva vstupy, jeden výstup a jedinou skrytou vrstvu. Bude tedy vypadat takto (počet neuronů na skryté vrstvě je ve skutečnosti větší, tím pádem i počet spojů/synapsí):
Obrázek 6: Neuronová síť, kterou se budeme snažit vytvořit v dalších kapitolách.
Síť bude natrénována pro výpočet zobecněné funkce xor, což je poměrně typický „školní“ příklad, mnohdy zadávaný tak, aby studenti museli naučit síť sami (tj. „ručním“ provedením algoritmu backpropagace).
Při práci s Torchem se budeme používat objekty z těchto tří modulů:
- Neural network containers (kontejnery pro vrstvy neuronové sítě)
https://github.com/torch/nn/blob/master/doc/containers.md - Simple layers (jednotlivé vrstvy sítě)
https://github.com/torch/nn/blob/master/doc/simple.md#nn.Linear - Transfer Function Layers (nelineární funkce zabudované do sítě)
https://github.com/torch/nn/blob/master/doc/transfer.md#nn.transfer.dok
9. Příprava trénovacích dat
V první fázi našeho projektu jednoduché neuronové sítě provedeme přípravu trénovacích dat. Bude se jednat o dvojice čísel generovaných náhodně s normálním rozložením a o návratovou hodnotu funkce compute_xor. Dvojice náhodných čísel bude představovat vstupní hodnoty do trénované sítě, návratová hodnota funkce compute_xor pak očekávaný výsledek. Připravíme si 2000 takových záznamů:
TRAINING_DATA_SIZE = 2000
Naše varianta do značné míry zobecněné funkce xor vrátí hodnotu –1 v případě, že znaménka vstupních číselných hodnot jsou shodná a hodnotu 1 v opačném případě:
function compute_xor(x, y) if x * y > 0 then return -1 else return 1 end end
Funkce nazvaná prepare_training_data skutečně připraví trénovací data pro neuronovou síť. Povšimněte si, jak musí být výsledný objekt zkonstruován – základem je objekt představovaný poli dvojic, přičemž první prvek dvojice tvoří vstupní data (tenzor!) a druhý prvek pak data očekávaná, tedy výstupní. V případe výstupních dat se taktéž musí jednat o tenzor, v našem případě pak o tenzor s jediným prvkem. Navíc musíme do výsledného objektu přidat metodu size vracející počet záznamů (mimochodem – protože se jedná o metodu, je data možné připravit i generátorem):
function prepare_training_data(training_data_size) -- objekt který reprezentuje trénovací data local training_data = {} -- metoda size vrací počet záznamů function training_data:size() return training_data_size end -- příprava jednotlivých záznamů for i = 1,training_data_size do -- vstupem je tenzor se dvěma prvky/komponentami local input = torch.randn(2) -- výstupem je tenzor s jediným prvkem/komponentou local output = torch.Tensor(1) output[1] = compute_xor(input[1], input[2]) -- dvojice vstup+výstup tvoří jeden záznam trénovacích dat training_data[i] = {input, output} end return training_data end
To je vše – stačí jen zavolat funkci prepare_training_data a uložit si její výsledek do proměnné:
training_data = prepare_training_data(TRAINING_DATA_SIZE)
10. Konstrukce neuronové sítě se třemi vrstvami
Trénovací data máme připravena, takže se nyní zaměříme na poněkud složitější úkol. Budeme totiž muset vytvořit neuronovou síť se třemi vrstvami neuronů. Počet vstupních neuronů (neuronů na vstupní vrstvě) bude roven dvěma, protože odpovídá počtu vstupních hodnot. Výstupní neuron bude jediný, protože optimisticky na výstupu očekáváme hodnotu –1 nebo 1. Na prostřední skryté vrstvě vytvoříme dvacet neuronů, což je v tomto případě až absurdně velký počet, protože teoreticky budou postačovat tři neurony. Pro snadnou modifikaci kódu si tyto údaje zapíšeme do globálních proměnných:
INPUT_NEURONS = 2 HIDDEN_NEURONS = 20 OUTPUT_NEURONS = 1
Ve funkci pro konstrukci neuronové sítě nejprve deklarujeme typ sítě konstruktorem Sequential. Tento konstruktor vytvoří síť, v níž jsou jednotlivé vrstvy propojeny jednosměrně (feed-forward) a všechny neurony z jedné vrstvy jsou propojeny se všemi neurony na vrstvě další. Dále metodou add do sítě postupně přidáme:
- Propojení mezi vstupní vrstvou a skrytou vrstvou (nn.Linear, lineární transformace y=Ax+b)
- Aktivační funkci (nn.Tanh, ovšem pouze do prostřední/skryté vrstvy)
- Propojení mezi skrytou vrstvou a výstupní vrstvou (nn.Linear)
function construct_neural_network(input_neurons, hidden_neurons, output_neurons) -- typ sítě typu feed-forward, v níž jsou propojeny všechny neurony mezi vrstvami local network = nn.Sequential() -- specifikace vstupní vrstvy: počet vstupů, počet neuronů na skryté vrstvě network:add(nn.Linear(input_neurons, hidden_neurons)) -- aktivační funkce pro neurony na skryté vrstvě network:add(nn.Tanh()) -- specifikace výstupní vrstvy: počet vstupů ze skryté vrstvy, počet výstupních neuronů network:add(nn.Linear(hidden_neurons, output_neurons)) -- síť máme vytvořenou return network end
Zkusme si nyní zkonstruovat výše popsanou umělou neuronovou síť a následně si nechat vypsat její strukturu:
network = construct_neural_network(INPUT_NEURONS, HIDDEN_NEURONS, OUTPUT_NEURONS) print(network)
Příkaz print(network) by měl symbolicky naznačit strukturu neuronové sítě:
nn.Sequential { [input -> (1) -> (2) -> (3) -> output] (1): nn.Linear(2 -> 20) (2): nn.Tanh (3): nn.Linear(20 -> 1) }
Jediná nelinearita byla přidána do prostřední vrstvy, což není – jak uvidíme z výsledků – optimální. Jak tento problém napravit si ukážeme ve čtrnácté kapitole.
11. Trénink neuronové sítě
Neuronovou síť již máme vytvořenou, ovšem musíme ji něco „naučit“, což znamená postupně doupravit váhy wi vstupů jednotlivých neuronů. Váhy většinou jsou na začátku učení nastaveny náhodně, cílem učení je pak v ideálním případě dosáhnout globálního minima chyby odhadu sítě vůči očekávaným výsledkům. Pro trénink (s využitím algoritmu backpropagation) potřebujeme stanovit kritérium a taktéž určit směr hledání globálního minima. Parametrem learning_rate se stanoví rychlost učení (zjednodušeně řečeno míra ovlivnění vah wi v každé iteraci učení), která však nemůže být příliš vysoká, neboť by to ovlivnilo celkový výsledek (opět velmi zjednodušeně řečeno: poslední trénovací vzorky by ho příliš ovlivnily):
function train_neural_network(network, training_data, learning_rate, max_iteration) local criterion = nn.MSECriterion() local trainer = nn.StochasticGradient(network, criterion) trainer.learningRate = learning_rate trainer.maxIteration = max_iteration trainer:train(training_data) end
Spustíme celý trénink:
train_neural_network(network, training_data, LEARNING_RATE, MAX_ITERATION)
Můžeme sledovat průběh tréninku, kdy nám systém vypisuje, jaká je velikost chyby pro jednotlivé vstupy, tj. pro trénovací data (dvojice vstupů a očekávaných výstupů). Zpočátku může být chyba hodně velká, protože v prvotním stavu jsou váhy na vstupech neuronů nastaveny na náhodnou hodnotu:
# StochasticGradient: training # current error = 0.54242233556529 # current error = 0.37642565798334 # current error = 0.34209057999057 # current error = 0.30198045696604 # current error = 0.27091862342749 # current error = 0.25200259699324
Velká chyba na začátku nás však nemusí trápit, protože v ideálním případě by velikost chyby měla postupně (a poměrně rychle) klesat. Pokud chyba klesá pomalu, což je mimochodem i náš případ, může to znamenat, že je v síti málo nelineárních přechodů. Může se také stát, že velikost chyby bude oscilovat – klesat a zase růst. To může znamenat příliš malý počet neuronů, které nestačí pro zapamatování „funkce“ celé sítě:
# current error = 0.11337547166271 # current error = 0.11332347785516 # current error = 0.11327187176746 # current error = 0.11322063535328 # StochasticGradient: you have reached the maximum number of iterations # training error = 0.11322063535328
12. Použití neuronové sítě na sadě validačních dat
Neuronovou síť jsme s využitím trénovacího vzorku dat naučili vrátit výsledek zobecněné funkce xor, i když popravdě řečeno byla chyba na konci stále příliš velká. Pokusme se nyní validovat, jestli je tento předpoklad správný a jestli síť dává alespoň trošku rozumné výsledky. Validace může být velmi snadná, protože můžeme použít metodu forward, která pošle vstupní data (tenzor) do vstupní vrstvy neuronů, nechá síť vypočítat výstup a tento výstup vrátí, opět ve formě tenzoru. V našem případě musí být na vstup poslán tenzor se dvěma prvky (komponentami) a na výstupu očekáváme jednoprvkový tenzor:
x=torch.Tensor({0.5, -0.5}) prediction = network:forward(x) print(prediction)
Na standardní výstup by se měl vypsat výsledek výpočtu, například:
1.0478 [torch.DoubleTensor of size 1]
Očekávaná ideální hodnota je přitom rovna 1.
Poznámka: ve vašem případě může být výsledek odlišný, protože je závislý na stavu sítě na začátku tréninku i na vlastních trénovacích datech, které byly vygenerovány pomocí funkce randn.
Lepší bude si na validaci sítě vytvořit novou uživatelskou funkci, která bude na vstup sítě předávat zvolená data, vypočte exaktní výsledek funkcí compute_xor, zjistí predikci neuronové sítě, oba výsledky porovná a navíc vypočte relativní chybu (v procentech, což obecně znevýhodňuje predikce v okolí nuly; to ovšem není náš případ). Tato funkce může vypadat následovně:
function validate_neural_network(network, validation_data) for i,d in ipairs(validation_data) do -- dvě hodnoty, pro něž se vypočte xor d1, d2 = d[1], d[2] -- vstupem do sítě musí být tenzor input = torch.Tensor({d1, d2}) -- predikce sítě je také tenzor, takže přečteme jeho jediný prvek prediction = network:forward(input)[1] -- korektní výsledek correct = compute_xor(d1, d2) -- relativní chyba err = math.abs(100.0 * (prediction-correct)/correct) -- vše vypíšeme msg = string.format("%2d %+6.3f %+6.3f %+6.3f %+6.3f %4.0f%%", i, d1, d2, correct, prediction, err) print(msg) end end
Data pro validaci mohou vypadat například takto:
validation_data = { { 1.0, 1,0}, { 0.5, 0.5}, { 0.2, 0.2}, ------------- {-1.0, 1,0}, {-0.5, 0.5}, {-0.2, 0.2}, ------------- { 1.0, -1,0}, { 0.5, -0.5}, { 0.2, -0.2}, ------------- {-1.0, -1,0}, {-0.5, -0.5}, {-0.2, -0.2}, }
Spustíme validaci:
validate_neural_network(network, validation_data)
A dostaneme tyto výsledky, které ovšem nejsou příliš přesné (viz vypočtená relativní chyba):
1 +1.000 +1.000 -1.000 -1.278 28% 2 +0.500 +0.500 -1.000 -1.022 2% 3 +0.200 +0.200 -1.000 -1.116 12% 4 -1.000 +1.000 +1.000 +1.128 13% 5 -0.500 +0.500 +1.000 +0.861 14% 6 -0.200 +0.200 +1.000 +0.814 19% 7 +1.000 -1.000 +1.000 +0.722 28% 8 +0.500 -0.500 +1.000 +1.048 5% 9 +0.200 -0.200 +1.000 +0.745 25% 10 -1.000 -1.000 -1.000 -0.947 5% 11 -0.500 -0.500 -1.000 -0.685 32% 12 -0.200 -0.200 -1.000 -0.975 3%
V navazujících kapitolách si ukážeme možná vylepšení (i zhoršení kvality) naší sítě.
13. Úplný zdrojový kód dnešního prvního demonstračního příkladu
V předchozích kapitolách jsme si ukázali jednotlivé části příkladu s jednoduchou umělou neuronovou sítí. Úplný zdrojový kód tohoto příkladu můžete získat z adresy https://github.com/tisnik/torch-examples/blob/master/nn/01_xor_problem.lua, popř. si ho zkopírovat z následujícího výpisu:
require("nn") TRAINING_DATA_SIZE = 2000 INPUT_NEURONS = 2 HIDDEN_NEURONS = 20 OUTPUT_NEURONS = 1 MAX_ITERATION = 200 LEARNING_RATE = 0.01 function compute_xor(x, y) if x * y > 0 then return -1 else return 1 end end function prepare_training_data(training_data_size) local training_data = {} function training_data:size() return training_data_size end for i = 1,training_data_size do local input = torch.randn(2) local output = torch.Tensor(1) output[1] = compute_xor(input[1], input[2]) training_data[i] = {input, output} end return training_data end function construct_neural_network(input_neurons, hidden_neurons, output_neurons) local network = nn.Sequential() network:add(nn.Linear(input_neurons, hidden_neurons)) network:add(nn.Tanh()) network:add(nn.Linear(hidden_neurons, output_neurons)) return network end function train_neural_network(network, training_data, learning_rate, max_iteration) local criterion = nn.MSECriterion() local trainer = nn.StochasticGradient(network, criterion) trainer.learningRate = learning_rate trainer.maxIteration = max_iteration trainer:train(training_data) end function validate_neural_network(network, validation_data) for i,d in ipairs(validation_data) do d1, d2 = d[1], d[2] input = torch.Tensor({d1, d2}) prediction = network:forward(input)[1] correct = compute_xor(d1, d2) err = math.abs(100.0 * (prediction-correct)/correct) msg = string.format("%2d %+6.3f %+6.3f %+6.3f %+6.3f %4.0f%%", i, d1, d2, correct, prediction, err) print(msg) end end network = construct_neural_network(INPUT_NEURONS, HIDDEN_NEURONS, OUTPUT_NEURONS) training_data = prepare_training_data(TRAINING_DATA_SIZE) train_neural_network(network, training_data, LEARNING_RATE, MAX_ITERATION) print(network) x=torch.Tensor({0.5, -0.5}) prediction = network:forward(x) print(prediction) validation_data = { { 1.0, 1,0}, { 0.5, 0.5}, { 0.2, 0.2}, ------------- {-1.0, 1,0}, {-0.5, 0.5}, {-0.2, 0.2}, ------------- { 1.0, -1,0}, { 0.5, -0.5}, { 0.2, -0.2}, ------------- {-1.0, -1,0}, {-0.5, -0.5}, {-0.2, -0.2}, } validate_neural_network(network, validation_data)
14. Parametry ovlivňující chování sítě
Mezi základní parametry, které ovlivňují chování sítě, tj. zejména rychlost a přesnost učení i přesnost předpovědi výsledků, patří počet vrstev sítě, počet neuronů na jednotlivých vrstvách (kromě vrstvy první a poslední, protože tam je počet ovlivněn velikostí vstupních a výstupních vektorů). Počty neuronů můžeme ovlivnit snadno, například zmenšením počtu neuronů na prostřední (skryté) vrstvě:
INPUT_NEURONS = 2 HIDDEN_NEURONS = 10 OUTPUT_NEURONS = 1
Dále si povšimněte, že nelineární funkce byla původně nakonfigurována pouze pro prostřední (skrytou) vrstvu:
function construct_neural_network(input_neurons, hidden_neurons, output_neurons) -- typ sítě typu feed-forward, v níž jsou propojeny všechny neurony mezi vrstvami local network = nn.Sequential() -- specifikace vstupní vrstvy: počet vstupů, počet neuronů na skryté vrstvě network:add(nn.Linear(input_neurons, hidden_neurons)) -- aktivační funkce pro neurony na skryté vrstvě network:add(nn.Tanh()) -- specifikace výstupní vrstvy: počet vstupů ze skryté vrstvy, počet výstupních neuronů network:add(nn.Linear(hidden_neurons, output_neurons)) -- síť máme vytvořenou return network end
Můžete si samozřejmě přidat stejnou funkci i pro výstupní neurony:
function construct_neural_network(input_neurons, hidden_neurons, output_neurons) -- typ sítě typu feed-forward, v níž jsou propojeny všechny neurony mezi vrstvami local network = nn.Sequential() -- specifikace vstupní vrstvy: počet vstupů, počet neuronů na skryté vrstvě network:add(nn.Linear(input_neurons, hidden_neurons)) -- aktivační funkce pro neurony na skryté vrstvě network:add(nn.Tanh()) -- specifikace výstupní vrstvy: počet vstupů ze skryté vrstvy, počet výstupních neuronů network:add(nn.Linear(hidden_neurons, output_neurons)) -- druhá sada aktivačních funkcí network:add(nn.Tanh()) -- síť máme vytvořenou return network end
Struktura sítě se pozmění následovně:
nn.Sequential { [input -> (1) -> (2) -> (3) -> (4) -> output] (1): nn.Linear(2 -> 10) (2): nn.Tanh (3): nn.Linear(10 -> 1) (4): nn.Tanh }
Zajímavé je, že výsledky se přidáním další nelinearity do systému sítě razantně zlepší:
1 +1.000 +1.000 -1.000 -1.000 0% 2 +0.500 +0.500 -1.000 -1.000 0% 3 +0.200 +0.200 -1.000 -0.995 1% 4 -1.000 +1.000 +1.000 +1.000 0% 5 -0.500 +0.500 +1.000 +1.000 0% 6 -0.200 +0.200 +1.000 +0.996 0% 7 +1.000 -1.000 +1.000 +1.000 0% 8 +0.500 -0.500 +1.000 +1.000 0% 9 +0.200 -0.200 +1.000 +0.999 0% 10 -1.000 -1.000 -1.000 -1.000 0% 11 -0.500 -0.500 -1.000 -1.000 0% 12 -0.200 -0.200 -1.000 -0.951 5%
Co to znamená? Počet neuronů nemusí být vždy tím rozhodujícím parametrem určujícím kvalitu sítě.
15. Příliš malý počet neuronů ve skryté vrstvě
Neustálé zvyšování počtu neuronů ve skryté vrstvě nemusí vést k lepším výsledkům, ovšem snížení neuronů pod určitou hranici je ještě horší. Upravme si (již jednou upravený) příklad tak, že do prostřední vrstvy umístíme pouhé dva neurony:
INPUT_NEURONS = 2 HIDDEN_NEURONS = 2 OUTPUT_NEURONS = 1
Takto malý počet neuronů (resp. jejich vah, což je stav sítě) je již velmi malý a síť se nedokáže naučit správně předpovídat výslednou hodnotu. I po několika stech iteracích je chyba stále velká (a nebude klesat):
# current error = 0.68643851757531 # StochasticGradient: you have reached the maximum number of iterations # training error = 0.68643851757531
Chyby jsou nízké pro ty vstupní hodnoty, na které byly neurony naučeny v posledních iteracích, ovšem pro ostatní hodnoty jsou výsledky neakceptovatelné:
1 +1.000 +1.000 -1.000 -1.000 0% 2 +0.500 +0.500 -1.000 -0.871 13% 3 +0.200 +0.200 -1.000 -0.051 95% 4 -1.000 +1.000 +1.000 +0.005 100% 5 -0.500 +0.500 +1.000 +0.003 100% 6 -0.200 +0.200 +1.000 +0.001 100% 7 +1.000 -1.000 +1.000 +1.000 0% 8 +0.500 -0.500 +1.000 +0.924 8% 9 +0.200 -0.200 +1.000 +0.107 89% 10 -1.000 -1.000 -1.000 +0.005 101% 11 -0.500 -0.500 -1.000 +0.008 101% 12 -0.200 -0.200 -1.000 +0.013 101%
16. Jednoduchá síť odhadující součet dvou prvků
Zkusme si nyní vytvořit ještě jednodušší síť a naučit ji sčítat dva prvky (v omezeném intervalu okolo nuly). Od předchozích příkladů bude zdrojový kód odlišný jen v několika maličkostech.
Snížíme množství trénovacích dat na čtvrtinu:
TRAINING_DATA_SIZE = 500
Počet neuronů na prostřední vrstvě snížíme na pouhé dva (zde to však nebude tak vadit):
HIDDEN_NEURONS = 2
Funkce pro přípravu trénovacích dat se nepatrně pozmění:
function prepare_training_data(training_data_size) local training_data = {} function training_data:size() return training_data_size end for i = 1,training_data_size do local input = torch.randn(2) local output = torch.Tensor(1) output[1] = input[1] + input[2] training_data[i] = {input, output} end return training_data end
A samozřejmě se změní i výpočet chyby při ověřování funkce sítě:
function validate_neural_network(network, validation_data) for i,d in ipairs(validation_data) do d1, d2 = d[1], d[2] input = torch.Tensor({d1, d2}) prediction = network:forward(input)[1] correct = d1 + d2 err = math.abs(100.0 * (prediction-correct)/correct) msg = string.format("%2d %+6.3f %+6.3f %+6.3f %+6.3f %4.0f%%", i, d1, d2, correct, prediction, err) print(msg) end end
17. Odhad součtu sítí využívající aktivační funkci Tanh
Takto minimalisticky pojatá síť s pouhými pěti neurony se naučí sčítat dobře, což ale není příliš překvapivé, protože právě suma je jednou ze základních funkcí neuronů:
# StochasticGradient: training # current error = 0.23437645781551 # current error = 0.058120529550668 # current error = 0.037139957667369 # current error = 0.027343471089676 # current error = 0.021707754740601 # current error = 0.017588320527942 # current error = 0.014216556244307 ... ... ... # current error = 0.00072523375142578 # StochasticGradient: you have reached the maximum number of iterations # training error = 0.00072523375142578
Výsledky po natrénování:
1 +1.000 +1.000 +2.000 +2.011 1% 2 +0.500 +0.500 +1.000 +0.992 1% 3 +0.200 +0.200 +0.400 +0.394 2% 4 -1.000 +1.100 +0.100 +0.098 2% 5 -0.500 +0.600 +0.100 +0.097 3% 6 -0.200 +0.300 +0.100 +0.097 3% 7 +1.000 -1.100 -0.100 -0.103 3% 8 +0.500 -0.600 -0.100 -0.102 2% 9 +0.200 -0.300 -0.100 -0.101 1% 10 -1.000 -1.000 -2.000 -2.038 2% 11 -0.500 -0.500 -1.000 -1.003 0% 12 -0.200 -0.200 -0.400 -0.399 0%
18. Použití aktivační funkce ReLU namísto Tanh
Pokusme se nyní v prostřední (tj. ve skryté) vrstvě neuronů nahradit aktivační funkci Tanh za funkci nazvanou poněkud záhadně ReLU. Zkratka ReLU byla odvozena od jména „rectified linear unit“ a jedná se o nelineární funkci složenou ze dvou lineárních částí. Výpočet funkce ReLU je triviální:
f(x) = max(0,x)
Proč by měla být tato funkce výhodnější, než Tanh? Při výpočtu součtu je nám vlastně nelineární průběh funkcí spíše na obtíž, takže zkusíme zvolit funkci, která sice obsahuje „zlom“, ale budeme doufat, že díky biasu se tento zlom posune mimo vstupní hodnoty (vše je nutné ověřit na testovacích datech):
function construct_neural_network(input_neurons, hidden_neurons, output_neurons) local network = nn.Sequential() network:add(nn.Linear(input_neurons, hidden_neurons)) -- výběr nelineární funkce network:add(nn.ReLU()) network:add(nn.Linear(hidden_neurons, output_neurons)) return network end
Už při tréninku sítě můžeme vidět, že jsme zvolili správně, neboť chyba je po dokončení tréninku prakticky nulová (konkrétně 5,1×10-31):
# StochasticGradient: training # current error = 0.99281388376557 # current error = 0.10862510985273 # current error = 0.036692037190132 # current error = 0.026179566474767 # current error = 0.020993037406808 # current error = 0.015662650169726 # current error = 0.0096425797438203 # current error = 0.0048635968724402 ... ... ... # current error = 5.1172035659567e-31 # StochasticGradient: you have reached the maximum number of iterations # training error = 5.1172035659567e-31
Ještě lépe dopadne otestování sítě, neboť výpočet je přesný minimálně na tři desetinná místa:
1 +1.000 +1.000 +2.000 +2.000 0% 2 +0.500 +0.500 +1.000 +1.000 0% 3 +0.200 +0.200 +0.400 +0.400 0% 4 -1.000 +1.100 +0.100 +0.100 0% 5 -0.500 +0.600 +0.100 +0.100 0% 6 -0.200 +0.300 +0.100 +0.100 0% 7 +1.000 -1.100 -0.100 -0.100 0% 8 +0.500 -0.600 -0.100 -0.100 0% 9 +0.200 -0.300 -0.100 -0.100 0% 10 -1.000 -1.000 -2.000 -2.000 0% 11 -0.500 -0.500 -1.000 -1.000 0% 12 -0.200 -0.200 -0.400 -0.400 0%
Příště si mj. ukážeme, že je navíc možné drasticky snížit počet neuronů na prostřední vrstvě, a to bez vlivu na výsledky.
19. Úplný zdrojový kód dnešního druhého demonstračního příkladu
Podobně jako jsme si ukázali úplný kód prvního příkladu s neuronovou sítí pro odhad funkce xor, si ukážeme i druhý příklad, v němž je síť natrénována pro součet dvou čísel, ovšem v dosti omezeném rozsahu. Tento příklad v případě zájmu opět naleznete na GitHubu, konkrétně na adrese https://github.com/tisnik/torch-examples/blob/master/nn/04_adder.lua:
require("nn") TRAINING_DATA_SIZE = 500 INPUT_NEURONS = 2 HIDDEN_NEURONS = 2 OUTPUT_NEURONS = 1 MAX_ITERATION = 200 LEARNING_RATE = 0.01 function prepare_training_data(training_data_size) local training_data = {} function training_data:size() return training_data_size end for i = 1,training_data_size do local input = torch.randn(2) local output = torch.Tensor(1) output[1] = input[1] + input[2] training_data[i] = {input, output} end return training_data end function construct_neural_network(input_neurons, hidden_neurons, output_neurons) local network = nn.Sequential() network:add(nn.Linear(input_neurons, hidden_neurons)) -- výběr nelineární funkce network:add(nn.ReLU()) --network:add(nn.Tanh()) network:add(nn.Linear(hidden_neurons, output_neurons)) return network end function train_neural_network(network, training_data, learning_rate, max_iteration) local criterion = nn.MSECriterion() local trainer = nn.StochasticGradient(network, criterion) trainer.learningRate = learning_rate trainer.maxIteration = max_iteration trainer:train(training_data) end function validate_neural_network(network, validation_data) for i,d in ipairs(validation_data) do d1, d2 = d[1], d[2] input = torch.Tensor({d1, d2}) prediction = network:forward(input)[1] correct = d1 + d2 err = math.abs(100.0 * (prediction-correct)/correct) msg = string.format("%2d %+6.3f %+6.3f %+6.3f %+6.3f %4.0f%%", i, d1, d2, correct, prediction, err) print(msg) end end network = construct_neural_network(INPUT_NEURONS, HIDDEN_NEURONS, OUTPUT_NEURONS) training_data = prepare_training_data(TRAINING_DATA_SIZE) train_neural_network(network, training_data, LEARNING_RATE, MAX_ITERATION) print(network) x=torch.Tensor({0.5, -0.5}) prediction = network:forward(x) print(prediction) validation_data = { { 1.0, 1,0}, { 0.5, 0.5}, { 0.2, 0.2}, ------------- {-1.0, 1.1}, {-0.5, 0.6}, {-0.2, 0.3}, ------------- { 1.0, -1.1}, { 0.5, -0.6}, { 0.2, -0.3}, ------------- {-1.0, -1,0}, {-0.5, -0.5}, {-0.2, -0.2}, } validate_neural_network(network, validation_data)
20. Odkazy na Internetu
- Stránka projektu Torch
http://torch.ch/ - Torch: Serialization
https://github.com/torch/torch7/blob/master/doc/serialization.md - Torch: modul image
https://github.com/torch/image/blob/master/README.md - Torch na GitHubu (několik repositářů)
https://github.com/torch - Torch (machine learning), Wikipedia
https://en.wikipedia.org/wiki/Torch_%28machine_learning%29 - Torch Package Reference Manual
https://github.com/torch/torch7/blob/master/README.md - Torch Cheatsheet
https://github.com/torch/torch7/wiki/Cheatsheet - Neural network containres (Torch)
https://github.com/torch/nn/blob/master/doc/containers.md - Simple layers
https://github.com/torch/nn/blob/master/doc/simple.md#nn.Linear - Transfer Function Layers
https://github.com/torch/nn/blob/master/doc/transfer.md#nn.transfer.dok - Feedforward neural network
https://en.wikipedia.org/wiki/Feedforward_neural_network - Biologické algoritmy (4) – Neuronové sítě
https://www.root.cz/clanky/biologicke-algoritmy-4-neuronove-site/ - Biologické algoritmy (5) – Neuronové sítě
https://www.root.cz/clanky/biologicke-algoritmy-5-neuronove-site/ - Umělá neuronová síť (Wikipedia)
https://cs.wikipedia.org/wiki/Um%C4%9Bl%C3%A1_neuronov%C3%A1_s%C3%AD%C5%A5 - Učení s učitelem (Wikipedia)
https://cs.wikipedia.org/wiki/U%C4%8Den%C3%AD_s_u%C4%8Ditelem - Plotting with Torch7
http://www.lighting-torch.com/2015/08/24/plotting-with-torch7/ - Plotting Package Manual with Gnuplot
https://github.com/torch/gnuplot/blob/master/README.md - An Introduction to Tensors
https://math.stackexchange.com/questions/10282/an-introduction-to-tensors - Gaussian filter
https://en.wikipedia.org/wiki/Gaussian_filter - Gaussian function
https://en.wikipedia.org/wiki/Gaussian_function - Laplacian/Laplacian of Gaussian
http://homepages.inf.ed.ac.uk/rbf/HIPR2/log.htm - Odstranění šumu
https://cs.wikipedia.org/wiki/Odstran%C4%9Bn%C3%AD_%C5%A1umu - Binary image
https://en.wikipedia.org/wiki/Binary_image - Erosion (morphology)
https://en.wikipedia.org/wiki/Erosion_%28morphology%29 - Dilation (morphology)
https://en.wikipedia.org/wiki/Dilation_%28morphology%29 - Mathematical morphology
https://en.wikipedia.org/wiki/Mathematical_morphology - Cvičení 10 – Morfologické operace
http://midas.uamt.feec.vutbr.cz/ZVS/Exercise10/content_cz.php - Differences between a matrix and a tensor
https://math.stackexchange.com/questions/412423/differences-between-a-matrix-and-a-tensor - Qualitatively, what is the difference between a matrix and a tensor?
https://math.stackexchange.com/questions/1444412/qualitatively-what-is-the-difference-between-a-matrix-and-a-tensor? - BLAS (Basic Linear Algebra Subprograms)
http://www.netlib.org/blas/ - Basic Linear Algebra Subprograms (Wikipedia)
https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms - Comparison of deep learning software
https://en.wikipedia.org/wiki/Comparison_of_deep_learning_software - TensorFlow
https://www.tensorflow.org/ - Caffe2 (A New Lightweight, Modular, and Scalable Deep Learning Framework)
https://caffe2.ai/ - PyTorch
http://pytorch.org/ - Seriál o programovacím jazyku Lua
http://www.root.cz/serialy/programovaci-jazyk-lua/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (2)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-2/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (3)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-3/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (4)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-4/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (5 – tabulky a pole)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-5-tabulky-a-pole/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (6 – překlad programových smyček do mezijazyka LuaJITu)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-6-preklad-programovych-smycek-do-mezijazyka-luajitu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (7 – dokončení popisu mezijazyka LuaJITu)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-7-dokonceni-popisu-mezijazyka-luajitu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (8 – základní vlastnosti trasovacího JITu)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-8-zakladni-vlastnosti-trasovaciho-jitu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (9 – další vlastnosti trasovacího JITu)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-9-dalsi-vlastnosti-trasovaciho-jitu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (10 – JIT překlad do nativního kódu)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-10-jit-preklad-do-nativniho-kodu/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (11 – JIT překlad do nativního kódu procesorů s architekturami x86 a ARM)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-11-jit-preklad-do-nativniho-kodu-procesoru-s-architekturami-x86-a-arm/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua (12 – překlad operací s reálnými čísly)
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-12-preklad-operaci-s-realnymi-cisly/ - Lua Profiler (GitHub)
https://github.com/luaforge/luaprofiler - Lua Profiler (LuaForge)
http://luaforge.net/projects/luaprofiler/ - ctrace
http://webserver2.tecgraf.puc-rio.br/~lhf/ftp/lua/ - The Lua VM, on the Web
https://kripken.github.io/lua.vm.js/lua.vm.js.html - Lua.vm.js REPL
https://kripken.github.io/lua.vm.js/repl.html - lua2js
https://www.npmjs.com/package/lua2js - lua2js na GitHubu
https://github.com/basicer/lua2js-dist - Lua (programming language)
http://en.wikipedia.org/wiki/Lua_(programming_language) - LuaJIT 2.0 SSA IRhttp://wiki.luajit.org/SSA-IR-2.0
- The LuaJIT Project
http://luajit.org/index.html - LuaJIT FAQ
http://luajit.org/faq.html - LuaJIT Performance Comparison
http://luajit.org/performance.html - LuaJIT 2.0 intellectual property disclosure and research opportunities
http://article.gmane.org/gmane.comp.lang.lua.general/58908 - LuaJIT Wiki
http://wiki.luajit.org/Home - LuaJIT 2.0 Bytecode Instructions
http://wiki.luajit.org/Bytecode-2.0 - Programming in Lua (first edition)
http://www.lua.org/pil/contents.html - Lua 5.2 sources
http://www.lua.org/source/5.2/ - REPL
https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop - The LLVM Compiler Infrastructure
http://llvm.org/ProjectsWithLLVM/ - clang: a C language family frontend for LLVM
http://clang.llvm.org/ - LLVM Backend („Fastcomp“)
http://kripken.github.io/emscripten-site/docs/building_from_source/LLVM-Backend.html#llvm-backend - Lambda the Ultimate: Coroutines in Lua,
http://lambda-the-ultimate.org/node/438 - Coroutines Tutorial,
http://lua-users.org/wiki/CoroutinesTutorial - Lua Coroutines Versus Python Generators,
http://lua-users.org/wiki/LuaCoroutinesVersusPythonGenerators