Masinõpe. Scikit-Learn ja TensorFlow.

Valter Kiisk
TÜ Füüsika Instituut
Viimati muudetud: 21.01.2022
$\renewcommand{\vec}[1]{{\bf #1}}$ $\newcommand{\aver}[1]{\langle #1 \rangle}$ $\newcommand{\eps}{\varepsilon}$

Sisukord

Sissejuhatus

Masinõpe tegeleb matemaatiliste (statistiliste) mudelitega, mida on võimalik treenida empiiriliste andmetega nii, et need mudelid suudavad teha piisavalt täpseid ennustusi või otsuseid ka sama tüüpi uute andmete korral. Masinõppe valdkondi võib klassifitseerida järgmiselt:

Objekti iseloomustab komplekt mõõdetavaid tunnuseid (features) ehk seletavaid muutujaid (explanatory variables). Need on masinõppe mudelile sisendiks. Juhendatud õppe korral lisandub ka väljundsuurus ehk märgend (label, target), mis regressiooni korral on pidev ehk reaalarvuline ja klassifitseerimise korral diskreetne ehk kategoriaalne. Viimasel juhul võib märgendit nimetada klassiks. Üks vaatlus ehk näidis ehk andmepunkt (sample) sisaldab tunnuste komplekti koos vastava märgendiga.

Seega juhendatud õppe korral peab masin õppima ennustama väljundit suvalise sisendi jaoks, tuginedes (inimese poolt) eelnevalt märgendatud treeningandmetele. Seevastu juhendamata õppe korral märgendid puuduvad ja algoritm ise peab tuvastama andmestikus leiduvad (varjatud) struktuurid, nt mitu erinevat objektitüüpi esineb või kui palju on tunnuste hulgas sõltumatuid muutujaid.

Et masinõppe mudel oleks õppimisvõimeline, sisaldab see vabu parameetreid, mida optimeeritakse treenimise käigus, püüdes minimeerida mõnesugust sihi- või kahjufunktsiooni (objective function, loss function). Samas võib mudeli või treenimisalgoritmi koosseisus olla ka nn hüperparameetreid, mis fikseeritakse enne treeningu algust ja mis mõjutavad õppe tulemust või kiirust.

Selles märkmeraamatus vaatame vaid juhendatud õpet. Sissejuhatava näitena vaatame sellist regressioonülesannet, kus on üksainus seletav muutuja $x$, mis määrab mõnesuguse tundmatu seose kaudu sõltuva muutuja $y$ väärtuse. See on piisavalt lihtne ülesanne, kus me masinõppe-spetsiifilisi Pythoni pakette veel ei vaja ja võime piirduda Numpy vahenditega. Impordime vajalikud vahendid:

Genereerime juhuslikud "katseandmed". Tegelikkuses andmed pärineksid tõenäoliselt mõnesugustest mõõtmistest.

Niisiis meie eesmärk on õpetada arvutile seos $x$ ja $y$ vahel, nii et etteantud $x$ väärtuse korral saaks mõnesuguse täpsusega ennustada $y$ väärtust. Seejuures tegelikku seost $x$ ja $y$ vahel me ei tea. Üks parajalt üldine ja paindlik matemaatiline funktsioon on polünoom: $$y=a_0x^n+a_1x^{n-1}+\ldots+a_{n-1}x+a_n.$$ Selle mudeli treenimine tähendab siis kordajate $a_i$ leidmist, nii et funktsioon kulgeks võimalikult katsepunktide lähedalt. Konkreetsemalt, võtame kahjufunktsiooniks ruutkeskmise erinevuse katselise $y$-väärtuse ja mudelfuktsiooni vastava ennustuse vahel. Vähimruutude meetodil polünoomi sobitamist teostabki numpy.polyfit.

Masinõppe korral on oluline, et treenitud mudel annaks adekvaatseid prognoose ka uute andmete korral, mida treenimise ajal ei kasutatud. Seega andmemassiivist mingi osa tuleb jätta hilisemaks testimiseks. Jätame näiteks pooled andmed mudeli treenimiseks ja ülejäänud testimiseks.

Esialgu me ei tea ka seda, milline võiks olla mõistlik polünoomi järk $n$. Viimane on seega mudeli hüperparameeter. Katsetame erinevate $n$ väärtustega. Mudeli sobivust kontrollime nii treening- kui ka testandmete peal, kasutades mõlemal juhul karakteristikuna ruutkeskmist viga.

Treeningandmete puhul funktsioon sobitub seda paremini, mida suurem on $n$, sest $n$ suurendamise teel on alati võimalik mudeli õppimisvõimet suurendada (ehk polünoomi aina täpsemalt antud katsepunktidest läbi juhtida). Esialgu ennustused paranevad ka testandmete jaoks kuni umbes $n=5$ juures hakkab ennustuste kvaliteet langema. Veel suuremate $n$ väärtuste kasutamine ei ole õigustatud ja tingib ületreeningu. Vaatame, kuidas variandid $n=1$ (liiga lihtne mudel), $n=5$ (optimaalne) ja $n=15$ (ületreening) graafiliselt välja näevad:

Nendest kolmest variandist $n=5$ on ilmselt kõige loomulikuma käitumisega ja sobivalt üldistub andmetele. Samas, kui me katsetaks näiteks mudeliga $$y=a_1e^{-k_1x} + a_2e^{-k_2x} + \ldots + a_ne^{-k_nx},$$ saaks tulemus veelgi parem.

Selles näites iseloomustas vaatlust üksainus tunnus, nii et kõik vaatlused sai esitada vektorina. Kui tunnuseid on rohkem, esitatakse andmed maatriksina $\vec X$, mille iga rida vastab ühele vaatlusele ja iga veerg ühele kindlale tunnusele. Märgendid tuleb samas järjestuses koondada vektorisse $\vec y$. $$\vec X=\stackrel{\Tiny\text{tunnused}\rightarrow}{\begin{bmatrix}x_{11} & x_{12} & \cdots & x_{1n}\\ x_{21} & x_{22} & \cdots & x_{2n}\\ \vdots & \vdots & \ddots & \vdots\\ x_{m1} & x_{m2} & \cdots & x_{mn}\end{bmatrix}} {\Tiny \begin{matrix}\text{vaatlused} \\ \downarrow\end{matrix}} \qquad \vec y=\begin{bmatrix}y_1\\y_2\\ \vdots \\ y_m\end{bmatrix}.$$ Näiteks Numpy andmestruktuure kasutades võiks maatriksit $\vec X$ esindada kahemõõtmeline massiiv, kus vaatlused kulgevad 1. telje sihis ja tunnused 2. telje sihis.

Regressioon (Scikit-Learn)

Scikit-Learn (sklearn) on üks vanimaid ja võrdlemisi üldotstarbeline Pythoni masinõppe teek. See sisaldab suure hulga lihtsasti kasutatavaid (valmiskujul) vahendeid ja algoritme, mis on koondatud vastavatesse klassidesse ja moodulitesse.

Reeglina masinõppeprobleemides tunnuseid/seletavaid muutujaid on rohkem kui üks. Et saaks regressiooni tulemust siiski veel graafiliselt esitada, piirdume kahe tunnusega. Simuleerime jälle andmed:

3D graafikute tegemiseks on tarvis veel mõningaid vahendeid:

Seekord andmete tükeldamise treening- ja testandmeteks teostame Scikit-Learn funktsiooniga train_test_split. Vastava proportsiooni saab seada parameetriga train_size või test_size. Vaikimisi andmete järjestus eelnevalt randomiseeritakse (shuffle=True), mis on antud juhul ka õigustatud.

Lineaarne regressioon

Lineaarse regressiooni korral sõltuva muutuja väärtus $y$ tunnusvektori $\vec x=(x_1,x_2,\ldots,x_n)$ jaoks prognoositakse mudeliga $$y=f(\vec x)=\vec w\vec x + b=\sum_i w_i x_i+b.$$ Vabad parameetrid on kordajad $\vec w=(w_1,w_2,\ldots,w_n)$, mis näitavad, millise kaaluga iga tunnust arvesse võtta, ja vabaliige $b$.

Regressiooni korral on sihifunktsiooniks (mille miinimumi otsitakse) harilikult ruutkeskmine hälve andmepunktide ja mudeli vastavate ennustuste vahel. Teatavasti sellisel juhul lineaarne regressioonülesanne taandub lineaarseks võrrandisüsteemiks, mis lahendub otsese algoritmiga. Seega lineaarse regressiooni teostamine on äärmiselt kiire ja annab ühese lahendi.

Teegis sklearn iga mudelit esindab vastav klass (näiteks LinearRegression). Selliste klasside koosseisus on rida standardsete nimedega meetodeid. Meetod fit treenib mudeli etteantud andmete peal. Kui mudel on treenitud, siis meetod predict ennustab tulemust etteantud tunnustemassiivi jaoks. Meetod score arvutab mõnesuguse statistiku, mis väljendab mudeli ennustuste täpsust. Sõltuvalt ülesandest ja mudelist, võib viimast arvutada mitmel viisil. Lineaarse regressiooni korral score arvutab variatsioonikordaja (coefficient of determination): $$R^2=1-\frac{\sum_i [y_i-f(\vec{x}_i)]^2}{\sum_i (y_i-\bar{y})^2},$$ kus summad on üle kõigi andmepunktide. $R^2$ väljendab seda, kui suurt osa signaali varieeruvusest suudab antud mudel kirjeldada. Ideaalselt sobituva mudeli korral $R^2=1$. Teises äärmuses, kui mudeliks oleks lihtsalt konstantne funktsioon (ühe vaba parameetriga), oleks $R^2=0$. Antud andmed on spetsiaalselt disainitud mittelineaarseina, seega lineaarne regressioon hästi ei sobitu:

$R^2$ on seotud ruutkeskmise veaga. On mõeldav, et praktikas pakub huvi mingi muu kvantiteet, mis on küll heas korrelatsioonis, aga mitte identne ruutkeskmise hälbega, nt keskmine absoluutne viga või keskmine suhteline viga. Neid võib Numpy massiivide baasil arvutada käsitsi, või kasutada sobivat funktsiooni moodulist sklearn.metrics:

Ootuspäraselt lineaarne mudel kirjeldab tasandit ruumis:

Objekti model atribuutide kaudu saab vajadusel (näiteks, et treenitud mudel üle kanda teise arvutuskeskkonda) teada ka optimeeritud mudeli parameetrite väärtused:

Mitmete masinõppealgoritmide puhul on vajalik, et tunnused oleks normeeritud, st keskväärtused oleks nulli lähedal ja mastaap suurusjärgus 1. Spetsiifilistel juhtudel on mõeldavad ka mittelineaarsed teisendused, et sõltuvust lineariseerida. StandardScaler nihutab tunnuste keskväärtuse nulli ja jagab standardhälbega. Kõigil seda laadi objektidel (moodulist sklearn.preprocessing) on kaks põhimeetodit: fit ja transform. Esimene sobitab ja jätab meelde teisenduse parameetrid (antud juhul keskväärtuse ja standardhälbe) ning teine rakendab teisenduse. fit_transform on sama mis funktsioonide fit ja transform kasutamine üksteise järel. Vajadusel, kui on tarvis taastada andmeid algkujul, on olemas ka inverse_transform.

Polünomiaalne regressioon

Mittelineaarse regressiooni saab kergesti sel teel, kui lihtsalt laiendada tunnuste komplekti, lisades kõikvõimalikud astmed ja korrutised (kuni teatud järguni), ja endiselt rakendada lineaarset regressiooni.

Nagu näha, hakkab variatsioonikordaja nüüd juba lähenema ühele. Saadud mudel kirjeldab võrdlemisi sujuvat kõverpinda, mis on end veidi vorminud andmepunktide järgi:

Sellise võttega ei saa siiski eriti keerulisi mudeleid luua, sest mudeli treenimiseks vajalike andmete arv kasvab eksponentsiaalselt polünoomi järguga (vt curse of dimensionality). Nagu veendusime juba töölehe alguses, vähesel hulgal andmetel treenitud kõrget järku polünoom hakkab ebaloomulikult ostsilleerima, st tekib ületreening.

Neuronvõrk

Tehisnärvivõrk simuleerib mingil määral bioloogilise aju toimimist. Bioloogilise neuroni analoogiks on pertseptron, mis võtab sisse teatud hulga sisendsignaale $x_i$ ja annab välja ühe väljundsignaali vastavalt mudelile $$y=f\left(\sum_i w_i x_i+b\right),$$ kus kaalud $w_i$ ja vabaliige $b$ on vabad parameetrid ning $f$ on teatav aktivatsioonifunktsioon. Viimane on mittelineaarne ja simuleerib neuroni aktiveerumist sisendsignaalide teatud kombinatsioonide korral. Sellised neuronid võib ühendada kihiti, kus ühe kihi väljundid on sisendeiks järgmise kihi kõigile neuroneile.

Kuna igal neuronil on oma komplekt vabu parameetreid, siis suurel neuronvõrgul on potentsiaalselt väga suur õppimisvõime. Treenimiseks kasutatakse mitmesuguseid stohhastilisel gradientlaskumisel põhinevaid algoritme, kuhu on kaasatud ka inerts ja adaptiivne õpisamm. Selliste algoritmide tuntud esindaja on Adam.

Täissidusat pärilevi neuronvõrku esindab sklearn.neural_network.MLPRegressor. See klass teeb kogu sisulise/tehnilise töö neuronvõrgu konstrueerimisel ja treenimisel. Neuronvõrgu ehituse määravad neuronite arvud peidetud kihtides (hidden_layer_sizes) ning aktivatsioonifunktsiooni valik. Treenimist saab mõjutada mõnede hüperparameetritega (sh õpisamm learning_rate_init), miniplokkide suurusega (batch_size) ja iteratsioonide arvuga (max_iter). Parameeter warm_start=True annab mõista, et korduval väljakutsel model.fit jätkab treenimist (mitte ei alusta otsast peale).

Saadud arvude jada demonstreerib, kuidas skoor paraneb treenimise käigus, st neuronvõrk õpib. Piisavalt kaua treenides siiski ühel hetkel testandmete jaoks hakkab ennustuste täpsus vähenema, st leiab aset ületreening. Neuronvõrgu korral üks ületreeningu vältimise võte ongi treeningu varajane lõpetamine, kuigi enamikel juhtudel oleks mõistlikum pigem lihtsustada masinõppe mudelit.

Kahju vähenemine treeningu käigus annab graafilise ülevaate närvivõrgu õppimisest:

Regressioon (TensorFlow)

TensorFlow paketis saab kasutaja masinõppe mudeli kõigis detailides ise ehitada, sh detailselt näidata, millised elemendid selles mudelis on treenitavad parameetrid, kuidas neid algväärtustada, kuidas on konstrueeritud kahjufunktsioon, jms. Mudeli ehitamisel kirjeldatakse TensorFlow objektide kaudu kogu arvutusahel (graaf). Reaalsed arvutused graafi järgi (sh treenimine) toimuvad spetsiaalse sessiooni käigus.

Lineaarne regressioon

Lihtsaima stsenaariumi korral võib treenimiseks kasutatavad andmed kirjeldada konstantidena (tf.constant) juba graafi koosseisus. See tähendab paraku, et muude andmetega sama skeemi läbi arvutada ei saa. tf.Variable-tüüpi sõlmi käsitletakse mudeli vabade parameetritena. Graafi konstrueerimise ajal tuleb vaid anda nende algväärtused (mis võivad olla konstandid või saadud juhuslike arvude generaatoriga).

Sageli tunnuseid on palju ja mudelis sisaldub nende kaalutud summa (nagu eespool korduvalt demonstreeritud). Sellise arvutusmustri realiseerimisel on abiks tf.matmul, mis arvutab maatrikskorrutise. Näiteks lineaarse regressiooni mudeli saame kirja panna maatrikskujul nii, et arvutused toimuvad paralleelselt kõigi andmetega: $$\vec y=\vec X\vec w+b.$$ Paraku tf.matmul eeldab, et mõlemad tensorid on vähemalt 2. järku, seega ka $\vec w$ tuleb defineerida maatriksina, kus teise mõõtme sihis on pikkus 1.

Siin on valitud kõige lihtsam gradientlaskumise algoritm. Seejuures operatsioon train_step esindab vaid ühte iteratsiooni. Teatavasti gradientlaskumise korral parameetrite vektorit $\vec p$ uuendatakse järgmiselt: $$\vec p_{t+1}=\vec p_t - \gamma \vec{\nabla}Q(\vec p_t),$$ kus $Q(\vec p)$ on kahjufunktsioon ja $\gamma$ on õpisamm (learning rate). Viimane on praegu ainus hüperparameeter ja on konstant. Kahjufunktsiooni väärtust esindab sõlm loss.

Mudeli optimeerimiseks jm arvutusteks tuleb luua spetsiaalne sessioon (tf.Session). Esmalt initsialiseeritakse muutujad ja seejärel luuakse tsükkel, kus korduvalt käivitatakse operatsioon train_step. Protsessi jälgimiseks võime igal iteratsioonisammul koguda või ekraanile trükkida kasulikku informatsiooni.

Neuronvõrk

Konstrueerime samasuguse neuronvõrgu nagu eelnevalt kasutatud sklearn.neural_network.MLPRegressor. Meil on kolm kihti (sh väljundkiht), mida karakteriseerivad kaalud W1, W2, W3 ja vabaliikmed B1, B2, B3. Kaalud on maatriksid, kus piki ridu kulgevad sisendsignaalid ja piki veerge neuronid kihis. Vabaliikmed on vektorid, ja liitmise käigus rakenduvad broadcast-reeglid (nagu Numpy-s).

Stohhastilise gradientlaskumise jaoks vajalikud juhuslikud (aga etteantud suurusega) miniplokid (mini-batches) peame ise tekitama, näiteks sellise funktsiooniga:

Klassifitseerimine (Scikit-Learn)

Teegis Scikit-Learn leidub hulganisti erinevaid klassifitseerimisalgoritme, näiteks:

Algoritmi valik sõltub andmete iseloomust ja mahust. Nende klasside kasutusmuster on üldjoontes sama ja sarnane regressiooni juhuga.

k-lähima naabri algoritm

Vähese hulga andmete korral võib mõistlik olla k-lähima naabri algoritm (k-nearest neighbor classifier). Sel juhul kõik treeningandmed talletatakse ja nende baasil mingit üldistatud mudelit ei looda. Uue vaatluse klassifitseerimiseks otsitakse lihtsalt treeningandmete hulgast $k$ lähimat objekti, loendatakse kokku nende klassid, ja valituks osutub prevaleeriv klass. Tulemust mõjutab see, kuidas arvutatakse objektide vahelisi kauguseid tunnuste ruumis (parameetrid metric ja p). Samuti on mõeldav, et objekti kaal sõltub kaugusest (parameeter weights).

Näidisandmeteks võtame objektid, mida iseloomustab 2 tunnust ja mis jaotuvad 3 klassi:

Vektoris y on seekord diskreetsed märgendid, mis identifitseerivad objektiklassi (need ei pea olema tingimata täisarvud).

Klassifitseerija treenimise käivitab endiselt meetod fit. Lihtsaim karakteristik klassifitseerija soorituse iseloomustamiseks on täpsus (accuracy), mis on õigete ennustuste ja katsete koguarvu suhe. Selle annab meetod score.

Detailselt karakteriseerib klassifitseerija sooritust eksimismaatriks (confusion matrix). Selle $i$-s rida näitab, milline on $i$-ndasse klassi kuuluvate näidiste klassifitseerimistulemuste jaotus. Ideaalse soorituse korral oleks nullist erinevad vaid maatriksi diagonaalelemendid.

Ilmselt täpsuse saab reprodutseerida eksimismaatriksi kaudu:

Kuvame nüüd koos treeningobjektidega ka vastava otsustuspinna (decision surface), kus värvusega näidatakse, millisesse klassi kuuluvaks osutuks vastava tunnusvektoriga objekt. Kuna antud mudeli kasutamine (st meetod predict) põhineb otsingul, on arvutus võrdlemisi aeganõudev.

$k$ on sisuliselt selle mudeli hüperparameeter, ja seda tuleks timmida, et saavutada optimaalset sooritust. Selles näites sai võetud $k=21$, mis andis nende testandmete peal võrdlemisi hea tulemuse. Võrdleme seda juhuga, kus $k=1$:

Nüüd treeningandmete puhul saavutati paratamatult maksimaalne täpsus, kuid saadud mudel ei üldistu enam nii hästi. See avaldub selles, et eraldusjooned/otsustuspinnad on keerulisema kujuga ehk mudel võtab liialt arvesse erandlikke katsepunkte.

Tugivektormasin

Lineaarse klassifitseerimise baasidee on eraldada erinevad klassid tunnuste ruumis sobiva hüperpinnaga. Binaarse klassifitseerimise korral tähendaks see selliste kaalude $w_i$ ja vabaliikme $b$ leidmist, et kui tunnusvektori $\vec x$ korral $\sum w_ix_i+b > 0$, siis loetakse vastav objekt kuuluvaks ühte klassi, vastasel korral teise klassi. Lineaarse klassifitseerija treenimiseks leidub erinevaid algoritme.

Tugivektormasina korral leitakse nimetatud eralduspind nii, et see asuks lähimatest andmepunktidest võrdsel kaugusel. Eristatakse kõva ja pehme äärega tugivektormasinaid. Esimesel juhul eeldatakse, et klassid on võimalik hüpertasandiga täielikult eraldada, teisel juhul saab algoritmi treenida ka kattuvuse korral. Seda kontrollitakse mittenegatiivse seadistusparameetriga $C$. Võttes $C$ piisavalt suure, saame maksimaalse marginaali eralduspinna ümber (tühja ala esimeste objektideni). Seega õigel pool eraldustasandit, kuid väljaspool marginaali asuvaid vaatlusi praktiliselt ei arvestata.

Järgmine näide kujutab objekte, mille eraldamiseks lineaarne klassifitseerija enam ei sobi.

Kasutades sobivat tuuma (kernel), saab tunnuseid laiendada kõrgema dimensiooniga ruumi, kus eeldatavasti klassid eralduvad.

Neuronvõrk

Olgu meil masinõppe mudel, mis sisendisse antud tunnuste komplekti $\vec x$ jaoks arvutab väljundsuuruse $z$, mis võib omandada mistahes väärtust vahemikus $(-\infty, \infty)$. Viimase saab teisendada suuruseks $p$ vahemikus $(0, 1)$ logistilise funktsiooni abil: $$p=\frac{e^z}{1+e^z}$$ Kui tegemist on binaarse klassifitseerimisülesandega, siis saadud suurust võib tõlgendada kui tõenäosust, nii et klassifitseerimise tulemuse määrab see, kas $p < 0{,}5$ või $p > 0{,}5$. Kui objektiklasse on $N$ ja mudel annab samuti välja $N$ reaalarvu $z_1,z_2,\ldots,z_N$, siis võime tõenäosused arvutada nn softmax-funktsiooni abil: $$p_k=\frac{e^{z_k}}{\sum_k e^{z_k}}$$ Ilmselt $\sum_k p_k=1$. Valituks osutub klass, mille tõenäosus on suurim.

Sellise probleemiasetuse korral ei saa ilmselt kasutada sama kahjufunktsiooni nagu regressiooni korral. Selle asemel kasutatakse ristentroopia mõistet. Kui vaatlusele vastav klass on $k$ ja mudel ennustab selle tõenäosuseks $p_k$, siis kahjufunktsiooni lisandub panus $-\log p_k$. Näiteks, kui $p_k$ on väike, siis kahju saab olema suur.

Lihtsaim seda laadi binaarne klassifitseerija oleks pertseptron, kus $z=\vec w\vec x + b$. See oleks ühtlasi lineaarne klassifitseerija (nagu on ka tugivektormasin). Suurema õppimisvõimega universaalse klassifitseerija saab realiseerida tehisnärvivõrgu baasil, mille ehitust sai kirjeldatud eespool. Scikit-Learn teegis realiseerib selle MLPClassifier. Selle väljundis ongi softmax-funktsioon ja kahjufunktsiooniks on ristentroopia. Rakendame seda viimati vaadeldud andmetele.

Pildituvastus

Masinõppe üks konkreetne rakendus on pildituvastus (image recognition). Funktsiooniga sklearn.datasets.load_digits saab laadida näidisandmestiku käsitsi kirjutatud numbritega. Selles sisaldub ligi 1800 halltoonis kujutist suurusega 8x8 pikslit.

Klassifitseerimiseks on tarvis defineerida tunnusvektor. Lihtsaim lahendus on võtta tunnusteks otseselt kõigi 64 piksli heledused, mis tuleb koondada kindlas järjestuses ühte vektorisse.

Edasine arvutusskeem on sarnane eelnenuga.

Nüüd saab mitmesugusel viisil väljendada mudeli täpsust. Seejuures, kuna tunnusvektor sisaldab kõiki piksleid, siis reshape meetodiga saame vajadusel ka algkujutised taastada.