Neuroverkkojen matematiikkaa

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:

f(x) = 2*x - 1
x = collect(-1.0:4.0)
y = f.(x)
x, y
([-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
    m = length(x)
    a = zeros(m)
    z = zeros(m)
    J = zeros(epochs)

    for k=1:epochs

        # forward propagation
        L = 0.0
        for i=1:m
            z[i] = w*x[i] + b
            a[i] = 1.0 * z[i]
            L = L + (a[i] - y[i])^2
        end

        # backward propagation
        dw = 0.0
        db = 0.0
        for i=1:m
            dLda = a[i] - y[i]
            dadz = 1.0
            dzdw = x[i]
            dzdb = 1.0
            dw = dw + dLda * dadz * dzdw
            db = db + dLda * dadz * dzdb
        end

        # update parameters
        w = w - alpha * 1.0 / m * dw
        b = b - alpha * 1.0 / m * db
        J[k] = 1.0 / m * L
    end
    return J, w, b
end

using Random
Random.seed!(0)
J, w, b = gd(x, y)
J[1:10]
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
plot(J, title="Neuroverkon kustannusfunktio", label="J", linewidth=1, linecolor=:black)
svg
w, b
(1.9517980579111849, -0.8505583310279576)
plot(x, y, title="Neuroverkkomallin data ja sovitus", markersize=5, marker=:circle, label="data")
plot!(x, w .* x .+ b, label="sovitus", markersize=5, marker=:circle, legend=:topleft)
svg

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.

data = CSV.File("2020-02-09-corona.csv") |> DataFrame

18 rows × 5 columns

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:

y = data[!, :daily_deaths]
x = collect(1.0:length(y))
Random.seed!(0)
J, w, b = gd(x, y)
J[1:10]
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:

J, w, b = gd(x, y; alpha = 0.0003)
Random.seed!(0)
J[1:10]
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
plot(J, title="COVID-19-China: neuroverkon kustannusfunktio", label="J", linewidth=1, linecolor=:black)
svg
title = "Kiinan covid-kuolleisuus, 23.1.2020 - 9.2.2020, data ja sovitus"
plot(x, y, title=title, linewidth=0, markersize=5, marker=:circle, label="data")
plot!(x, w .* x .+ b, label="sovitus", legend=:topleft)
svg

Muistaakseni tuolloin ajattelin, että näin karkea malli voisi toimia noin viikon verran eteenpäin. Ekstrapoloidaan siis sinne:

fit = cumsum(w .* collect(1:18) .+ b)
pred = sum(y) .+ cumsum(w .* collect(19:25) .+ b)
title = "Covid-kuolleisuus Kiinassa 23.1.2020 - 9.2.2020"
plot(title=title)
scatter!(1:18, data[!, :total_deaths], label="data", title=title)
plot!(1:18, fit, label="mallin sovitus", legend=:topleft)
plot!(19:25, pred, label="mallin ennuste")
svg
round(Int, pred[end])
1670

Jälkeenpäin tarkasteltuna mallin ennuste oli yllättävänkin hyvä. Viikkoa myöhemmin tilanne oli seuraava:

data2 = CSV.File("2020-02-16-corona.csv") |> DataFrame

7 rows × 5 columns

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

abs(round(Int, pred[end]) - data2[end, :total_deaths])
105
fit = cumsum(w .* collect(1:18) .+ b)
pred = sum(y) .+ cumsum(w .* collect(19:25) .+ b)
title = "Covid-kuolleisuus Kiinassa 23.1.2020 - 9.2.2020"
scatter(1:18, data[!, :total_deaths], label="data", title=title)
plot!(1:18, fit, label="mallin sovitus", legend=:topleft)
plot!(19:25, pred, label="mallin ennuste")
scatter!(19:25, data2[!, :total_deaths], label="uusi data")
svg

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.