Tarkastellaan yksinkertaista lineaarista neuroverkkoa, jolle opetetaan funktio \(y = 2x - 1\). Kirjoituksen huomio on lähinnä matemaattisessa notaatiossa ja verkon optimoinnissa.
Tarkastellaan funktiota välillä \(-1 \leq x \leq 4\). Diskretoidaan väli siten, että \(\Delta x = 1\) ja sovitetaan dataan lineaarinen malli. Datapisteet siis ovat:
= 2*x - 1
f(x) = collect(-1.0:4.0)
x = f.(x)
y , y x
([-1.0, 0.0, 1.0, 2.0, 3.0, 4.0], [-3.0, -1.0, 1.0, 3.0, 5.0, 7.0])
Suoran sovitus dataan ei sinänsä ihmeellinen temppu ole. Tätä voidaan kuitenkin tarkastella koneoppimisen näkökulmasta, jolloin saamme ongelmaan uusia vivahteita. Tässä on nimittäin peruste kaikille monimutkaisemmille neuroverkoille. Malli on
\[ y = wx + b, \]
missä, alan termistöä käyttäen, \(w\) on painokerroin ja \(b\) on bias-termi.
Nyt pitäisi löytää parametreille \((w, b)\) sellaiset luvut, että malli vastaisi parhaimmalla mahdollisella tavalla dataa. Tyypillisesti tämä tapahtuu vaatimalla, että datan \(y_i\) ja mallin antaman ennusteen \(\hat{y}_i = wx_{i}+b\) välinen virhe kaikille \(i\) minimoituu jonkin normin suhteen. Varsin yleinen valinta on \(L_2\) normi, ja määritellään “loss-funktio”
\[ J\left(w,b\right)=\frac{1}{m}\sum_{i=1}^{m}\mathcal{L}\left(\hat{y}^{\left(i\right)},y^{\left(i\right)}\right), \]
missä yhden näytteen virhe on
\[ \mathcal{L}\left(\hat{y}^{\left(i\right)},y^{\left(i\right)}\right)=\frac{1}{2}\left(\hat{y}-y\right)^{2} \]
Tehtävänä nyt on löytää mallin parametrien \(w\) ja \(b\) arvot siten, että \(J\left(w,b\right)\) minimoituu.
Tyypillisesti neuroverkoissa parametreja on kymmeniä miljoonia. Päivitys tapahtuu yleensä gradient descent -menetelmällä1 tai jollakin sen variaatiolla. Parametrit alustetaan alussa satunnaisesti, valitaan esimerkiksi lukuja standardinormaalijakaumasta. Tämän jälkeen lasketaan loss-funktion gradientti ja päivitetään parametreja negatiivisen gradientin suuntaan jollakin kertoimella \(\alpha\), jota kutsutaan learning rateksi: \[\begin{align} w*{i+1} =w*{i}-\alpha\frac{dJ}{dw} \\ b*{i+1} =b*{i}-\alpha\frac{dJ}{db}. \end{align}\]
Neuroverkkokirjallisuudessa keskeiset käsitteet ovat forward propagation ja backpropagation. Forward propagationissa edetään neuroverkossa inputista outputtiin päin ja back propagationissa toiseen suuntaa, eli derivoidaan.
Jotta merkinnät olisivat konsistentteja tulevaisuudessakin, muutetaan hieman merkintöjä. Ensinnäkin \(z\) on neuroverkon yksi kerros (layer), tässä tapauksessa \(z = wx + b\). Yhdellä kerroksella \(y = z\). Lisäksi on aktivointifunktio \(a\) siten, että
\[ \hat{y} = a = I\left(z\right) \]
missä \(I\) on identiteettifunktio 1 (lineaarinen aktivointifunktio). Näillä merkinnöillä voidaan laajentaa neuroverkkoa useampiin kerroksiin sekä erilaisiin aktivointifunktioihin ja merkinnät pysyvät jokseenkin samoina. Kun mennään verkossa eteenpäin, kaavat siis ovat yhteenvetona \[\begin{align} z & =wx+b\\ a & =z\\ \mathcal{L}\left(a,y\right) & =\frac{1}{2}\left(a-y\right)^{2} \end{align}\]
Takaisin päin tultaessa pitää derivoida. Ketjusääntöä on käytettävä:
\[\begin{align} \frac{\mathrm{d}\mathcal{L}\left(a\left(z\left(w,b\right)\right),y\right)}{dw} & =\frac{\mathrm{d}\mathcal{L}}{\mathrm{d}a}\cdot\frac{\mathrm{d}a}{\mathrm{d}z}\cdot\frac{\mathrm{d}z}{\mathrm{d}w}\\ \frac{\mathrm{d}\mathcal{L}\left(a\left(z\left(w,b\right)\right),y\right)}{db} & =\frac{\mathrm{d}\mathcal{L}}{\mathrm{d}a}\cdot\frac{\mathrm{d}a}{\mathrm{d}z}\cdot\frac{\mathrm{d}z}{\mathrm{d}b} \end{align}\]
Derivaatat ovat \[\begin{align} \frac{\mathrm{d}\mathcal{L}}{\mathrm{d}a} & =a-y\\ \frac{\mathrm{d}a}{\mathrm{d}z} & =1\\ \frac{\mathrm{d}z}{\mathrm{d}w} & =x\\ \frac{\mathrm{d}z}{\mathrm{d}b} & =1 \end{align}\]
Joten \[\begin{align} \frac{\mathrm{d}\mathcal{L}}{dw} & =\left(a-y\right)x\\ \frac{\mathrm{d}\mathcal{L}}{db} & =\left(a-y\right) \end{align}\]
Auki kirjoitettuna gradient descent askel siis on: \[\begin{align} w*{i+1} & =w*{i}-\alpha\frac{1}{m}\sum*{i=1}^{m}\left[\left(a^{\left(i\right)}-y^{\left(i\right)}\right)x^{\left(i\right)}\right]\\ b*{i+1} & =b*{i}-\alpha\frac{1}{m}\sum*{i=1}^{m}\left[\left(a^{\left(i\right)}-y^{\left(i\right)}\right)\right] \end{align}\]
Yksinkertainen gradient descent algoritmin implementaatio on:
function gd(x, y; w=randn(), b=randn(), alpha=0.05, epochs=100)
@assert length(x) == length(y)
# initialization
= length(x)
m = zeros(m)
a = zeros(m)
z = zeros(epochs)
J
for k=1:epochs
# forward propagation
= 0.0
L for i=1:m
= w*x[i] + b
z[i] = 1.0 * z[i]
a[i] = L + (a[i] - y[i])^2
L end
# backward propagation
= 0.0
dw = 0.0
db for i=1:m
= a[i] - y[i]
dLda = 1.0
dadz = x[i]
dzdw = 1.0
dzdb = dw + dLda * dadz * dzdw
dw = db + dLda * dadz * dzdb
db end
# update parameters
= w - alpha * 1.0 / m * dw
w = b - alpha * 1.0 / m * db
b = 1.0 / m * L
J[k] end
return J, w, b
end
using Random
Random.seed!(0)
, w, b = gd(x, y)
J1:10] J[
10-element Array{Float64,1}:
5.11226131444927
3.663623523618105
2.8653251408411387
2.4044819382652385
2.1198935861229318
1.9284492288999342
1.7872428329353884
1.6740709179246402
1.577393029414151
1.4911633751432603
using Plots
, title="Neuroverkon kustannusfunktio", label="J", linewidth=1, linecolor=:black) plot(J
, b w
(1.9517980579111849, -0.8505583310279576)
, y, title="Neuroverkkomallin data ja sovitus", markersize=5, marker=:circle, label="data")
plot(x!(x, w .* x .+ b, label="sovitus", markersize=5, marker=:circle, legend=:topleft) plot
Vaikka malli oli varsin yksinkertainen, herää jo tästäkin lukuisia kysymyksiä.
Ensinnäkin, nyt hatusta kiskaisiin learning rate 0.05. Onko se hyvä vai huono? Pienempi learning rate hidastaa tarpeettomasti verkon opettamista, liian suuri taas aiheuttaa että malli ei suppene.
Hatusta myös kiskaisiin iteraatioiden määrä 100. Iteroitiinko liian vähän, tarpeeksi vai liikaa?
Onko malli tarkka vai ei. Yleistyykö se?
Näitä asioita olisi tarkoitus käsitellä tulevissa kirjoituksissa.
Case study: Covid-kuolemat Kiinassa kriisin alussa
Tarkastelin melko intensiivisesti koronaviruksen leviämistä aivan kriisin alkuaikoina. Keräsin dataa ja yritin päätellä että mihinkä suuntaan tauti lähtee Kiinassa etenemään. Kun data kerran on valmiina, voimme opettaa neuroverkon datalla ja yrittää tehdä sillä estimaatteja.
using DataFrames, CSV
Dataa ei tuolloin ollut juurikaan saatavilla, sillä virallista tietoa oli tullut vasta 18 päivää. Kiinnostuksen kohteena on kuolleisuus.
= CSV.File("2020-02-09-corona.csv") |> DataFrame data
Column1 | daily_cases | total_cases | daily_deaths | total_deaths | |
---|---|---|---|---|---|
Dates… | Int64 | Int64 | Int64 | Int64 | |
1 | 2020-01-23 | 266 | 845 | 8 | 25 |
2 | 2020-01-24 | 472 | 1317 | 16 | 41 |
3 | 2020-01-25 | 698 | 2015 | 15 | 56 |
4 | 2020-01-26 | 785 | 2800 | 24 | 80 |
5 | 2020-01-27 | 1781 | 4581 | 26 | 106 |
6 | 2020-01-28 | 1477 | 6058 | 26 | 132 |
7 | 2020-01-29 | 1755 | 7813 | 38 | 170 |
8 | 2020-01-30 | 2008 | 9821 | 43 | 213 |
9 | 2020-01-31 | 2127 | 11948 | 46 | 259 |
10 | 2020-02-01 | 2604 | 14552 | 45 | 304 |
11 | 2020-02-02 | 2837 | 17389 | 58 | 362 |
12 | 2020-02-03 | 3239 | 20628 | 64 | 426 |
13 | 2020-02-04 | 3925 | 24553 | 66 | 492 |
14 | 2020-02-05 | 3723 | 28276 | 73 | 565 |
15 | 2020-02-06 | 3163 | 31439 | 73 | 638 |
16 | 2020-02-07 | 3437 | 34876 | 86 | 724 |
17 | 2020-02-08 | 2676 | 37552 | 89 | 813 |
18 | 2020-02-09 | 3001 | 40553 | 97 | 910 |
Tavoitteena oli mallintaa koronavirukseen kuolleiden määrää. Perushavinto oli tuolloin, että päivittäin kuolleiden määrä vaikutti kasvavan lineaarisesti. Sillä oletuksella tämä meidän neuroverkkomalli itse asiasssa voi antaa ihan tolkullisen ennusteen jollekin lyhyelle aikavälille. Mallin sovitus gd()
-funktiolla on suoraviivaista:
= data[!, :daily_deaths]
y = collect(1.0:length(y))
x Random.seed!(0)
, w, b = gd(x, y)
J1:10] J[
10-element Array{Float64,1}:
2329.049574848616
55625.00301483141
1.3336537170135388e6
3.198058444106884e7
7.668892390723577e8
1.838988501357496e10
4.409865956536768e11
1.057479029900696e13
2.5358183439678866e14
6.080853134471482e15
Mutta eipäs olekaan! Oletuksena annettu learning rate 0.05 on liian iso, joten malli ei suppene. Täytyy kokeilla jotakin muuta parametria. Pienen harjoittelun jälkeen:
, w, b = gd(x, y; alpha = 0.0003)
JRandom.seed!(0)
1:10] J[
10-element Array{Float64,1}:
2472.656866335288
2301.463876719181
2142.170718754915
1993.950220729654
1856.032708638312
1727.7020094482361
1608.2917321818022
1497.181807505469
1393.7952678561865
1297.5952513851173
, title="COVID-19-China: neuroverkon kustannusfunktio", label="J", linewidth=1, linecolor=:black) plot(J
= "Kiinan covid-kuolleisuus, 23.1.2020 - 9.2.2020, data ja sovitus"
title , y, title=title, linewidth=0, markersize=5, marker=:circle, label="data")
plot(x!(x, w .* x .+ b, label="sovitus", legend=:topleft) plot
Muistaakseni tuolloin ajattelin, että näin karkea malli voisi toimia noin viikon verran eteenpäin. Ekstrapoloidaan siis sinne:
= cumsum(w .* collect(1:18) .+ b)
fit = sum(y) .+ cumsum(w .* collect(19:25) .+ b)
pred = "Covid-kuolleisuus Kiinassa 23.1.2020 - 9.2.2020"
title =title)
plot(title!(1:18, data[!, :total_deaths], label="data", title=title)
scatter!(1:18, fit, label="mallin sovitus", legend=:topleft)
plot!(19:25, pred, label="mallin ennuste") plot
Int, pred[end]) round(
1670
Jälkeenpäin tarkasteltuna mallin ennuste oli yllättävänkin hyvä. Viikkoa myöhemmin tilanne oli seuraava:
= CSV.File("2020-02-16-corona.csv") |> DataFrame data2
Column1 | daily_cases | total_cases | daily_deaths | total_deaths | |
---|---|---|---|---|---|
Dates… | Int64 | Int64 | Int64 | Int64 | |
1 | 2020-02-10 | 2546 | 43099 | 108 | 1018 |
2 | 2020-02-11 | 2071 | 45170 | 97 | 1115 |
3 | 2020-02-12 | 14113 | 59283 | 146 | 1261 |
4 | 2020-02-13 | 5154 | 64437 | 122 | 1383 |
5 | 2020-02-14 | 2663 | 67100 | 143 | 1526 |
6 | 2020-02-15 | 2097 | 69197 | 143 | 1669 |
7 | 2020-02-16 | 2132 | 71329 | 106 | 1775 |
Mallin ennusteen virhe
Int, pred[end]) - data2[end, :total_deaths]) abs(round(
105
= cumsum(w .* collect(1:18) .+ b)
fit = sum(y) .+ cumsum(w .* collect(19:25) .+ b)
pred = "Covid-kuolleisuus Kiinassa 23.1.2020 - 9.2.2020"
title 1:18, data[!, :total_deaths], label="data", title=title)
scatter(!(1:18, fit, label="mallin sovitus", legend=:topleft)
plot!(19:25, pred, label="mallin ennuste")
plot!(19:25, data2[!, :total_deaths], label="uusi data") scatter
Yhteenveto
Rakennettiin yksinkertaisin mahdollinen neuroverkko \(y = wx + b\) sekä sovitettiin malli dataan gradient descent menetelmällä. Tulevissa kirjoituksissa keskityn aiheisiin kuten aktivointifunktiot, useampi neuroni, useampi kerros, piilokerros, syväoppiminen. Tämä olkoon nyt tällainen johdanto aiheeseen.