Õppematerjal · PyTorch

PyTorch nullist MLP-ni

Selle materjali lõpuks oskad kirjutada ja treenida mitmekihilise pertseptroni (MLP), mis eristab andmetes klasse. Liigume väikeste sammudega: kõigepealt tensor, siis väike mudel käsitsi, siis torchi automaatne gradient ja alles siis närvivõrk.

Eeldus: Pythoni põhitõed, NumPy tuleb kasuks Aeg: umbes 2–3 tundi Vahendid: Jupyter, Colab või VS Code

Miks torch, kui sklearnis on MLP olemas?

Kui oled kasutanud MLPClassifierit või MLPRegressorit, oled MLP-d juba treeninud. Sklearn teeb kõik ühe fit()-iga, torch nõuab rohkem koodi. Vaata sama mudelit kõrvuti.

sklearn
from sklearn.neural_network import MLPClassifier

clf = MLPClassifier(
    hidden_layer_sizes=(16, 16),
    activation="relu",
    solver="adam",
    learning_rate_init=0.01,
    batch_size=32,
    max_iter=50,
)
clf.fit(X_train, y_train)   # kogu treening on peidus siin
clf.predict(X_test)
pytorch
model = nn.Sequential(
    nn.Linear(2, 16), nn.ReLU(),
    nn.Linear(16, 16), nn.ReLU(),
    nn.Linear(16, 2),
)
opt = torch.optim.Adam(model.parameters(), lr=0.01)
loss_fn = nn.CrossEntropyLoss()

for epoch in range(50):        # see ongi fit()
    for xb, yb in train_loader:
        loss = loss_fn(model(xb), yb)
        opt.zero_grad()
        loss.backward()
        opt.step()

model(X_test).argmax(dim=1)
sklearnclf.fit(X, y)forward · loss · zero_gradbackward · steppeidus: näed ainult tulemustPyTorch1model(X)ennustus ŷ2loss_fn(ŷ, y)kui palju eksib3zero_grad()vanad gradiendid nulli4backward()uued gradiendid5step()w ja b liiguvad
Mõlemas toimub sama viis sammu. Sklearnis on need fit()-i sees peidus, PyTorchis kirjutad need ise ja saad igaüht muuta.

See on sama võrk: 2 sisendit, kaks peidetud kihti 16 neuroniga, ReLU, Adam, partii 32 ja 50 epohhi. Täisnäite andmetel (osa 8) said mõlemad testi täpsuseks umbes 99%. Vahe on selles, et sklearn peidab treeningtsükli fit()-i sisse. Torchis kirjutad selle ise ja saad seetõttu kõike muuta.

Mida tahad tehasklearn MLPPyTorch
Väike tabel, kiire esimene mudelVäga hea, vähe koodiSaab, aga koodi on rohkem
Pildid, tekst, heli, aegreadAinult täisühendatud kihidKonvolutsioon (CNN), RNN, transformer jt
Graafikakaart (GPU)Ainult protsessormodel.to("cuda")
Oma kaofunktsioonFikseeritud: ruutviga või log-lossSuvaline avaldis tensoritest
Ülesobitamise vastuL2 (alpha) ja early_stoppingLisaks Dropout, BatchNorm, oma reeglid
Näha ja muuta, mis treeningu ajal toimubPeidus fit()-i seesIga samm on sinu koodis
Eeltreenitud mudelidPoleHugging Face, torchvision jt
Andmed, mis ei mahu mälluKõik peab korraga mälus olemaDataLoader loeb osade kaupa
Millal kumba

Sklearn, kui andmed on väike tabel ja tahad MLP-d kiiresti võrrelda teiste mudelitega (otsustuspuu, random forest, logistiline regressioon) ühes Pipelineis ja GridSearchCV-s. Torch, kui tahad mõista, mis mudeli sees toimub, või kui ülesanne on suurem kui tavaline tabel. Peaaegu kogu tänapäeva süvaõpe, ka keelemudelid, on kirjutatud PyTorchis.

Selle materjali eesmärk: ehitame sama MLP-i, mida sklearn teeb, aga nii, et näeme iga sammu. Pärast seda on pildid ja tekst lihtsalt teised kihid samas tsüklis.

0Alustus

Paigalda PyTorch. Kõik siinsed näited töötavad tavalise protsessoriga, graafikakaarti pole vaja. Google Colabis on torch juba olemas.

terminal
pip install torch
python
import torch

print(torch.__version__)         # nt 2.5.1
print(torch.cuda.is_available()) # False on täiesti korras, kui GPU-d pole

Soovitus: ära kopeeri plokke korraga. Käivita iga plokk eraldi, vaata väljundit ja muuda mõnda arvu. Nii jääb rohkem meelde.

1Tensor

Tensor on PyTorchi põhiline andmetüüp. See on nagu NumPy massiiv: arvude tabel, millel on kuju (shape) ja tüüp (dtype). Lisaks oskab tensor kaht asja, mida NumPy ei oska: arvutada graafikakaardil ja meeles pidada, kuidas tulemus arvutati. Teisest tuleb gradient (osa 4).

python
import torch

a = torch.tensor([1.0, 2.0, 3.0])
print(a)          # tensor([1., 2., 3.])
print(a.shape)    # torch.Size([3])
print(a.dtype)    # torch.float32

M = torch.tensor([[1, 2, 3],
                  [4, 5, 6]])
print(M.shape)    # torch.Size([2, 3])  -> 2 rida, 3 veergu
print(M.dtype)    # torch.int64  (täisarvud)
MõõtmeidNäideKujuMasinõppes
0torch.tensor(3.5)()üks arv, nt kaofunktsiooni väärtus
1torch.tensor([1., 2., 3.])(3,)üks näide või klassisiltide veerg
2torch.zeros(150, 4)(150, 4)andmetabel: read = näited, veerud = tunnused
3.51.2.3.1234563 veergu2 rida5.13.51.40.24.93.01.40.24.73.21.30.24.63.11.50.2⋮F = 4 tunnust (veerud)N = 150 näidetskalaarkuju ()vektorkuju (3,)maatrikskuju (2, 3)andmetabelkuju (150, 4)
Kuju ütleb, mitu arvu on igas mõõtmes. Andmetabelis on üks rida üks näide (sinine rida: esimene iiris) ja üks veerg üks tunnus.
Meelespea

Masinõppes on andmetabel peaaegu alati kujuga (N, F): N rida on näited, F veergu on tunnused. 150 iirist 4 tunnusega on kujuga (150, 4).

Tensori loomine

python
torch.zeros(2, 3)         # 2×3 nullidest tabel
torch.ones(4)             # tensor([1., 1., 1., 1.])
torch.arange(5)           # tensor([0, 1, 2, 3, 4])
torch.linspace(0, 1, 5)   # [0.00, 0.25, 0.50, 0.75, 1.00]
torch.rand(2, 2)          # juhuarvud ühtlasest jaotusest [0, 1)
torch.randn(3, 4)         # juhuarvud normaaljaotusest (keskmine 0, sd 1)

torch.manual_seed(0)      # pärast seda tulevad juhuarvud iga kord samad

NumPy ja tensor

python
import numpy as np

x = np.array([[1.5, 2.5], [3.0, 4.0]])   # NumPy vaikimisi float64
t = torch.tensor(x, dtype=torch.float32)  # mudelid ootavad float32
back = t.numpy()                          # tagasi NumPy massiiviks
print(t.dtype)                            # torch.float32
Tüübid
  • Tunnused (sisend X) on torch.float32.
  • Klassisildid (y klassifitseerimisel) on täisarvud torch.long: 0, 1, 2, …
  • Kui tüüp on vale, annab torch vea, kus on sõnad Float, Double või Long. Siis vaata esimesena tüüpe.

2Kujud ja tehted

Tehted käivad nagu NumPys. Tähelepanu tasub pöörata kahele märgile: * korrutab elemendiviisi, @ teeb maatrikskorrutise. Närvivõrgu kiht on sisuliselt maatrikskorrutis.

python
x = torch.tensor([[1., 2.],
                  [3., 4.]])

x + 10          # [[11., 12.], [13., 14.]]
x * x           # elemendiviisi: [[1., 4.], [9., 16.]]
x @ x           # maatrikskorrutis: [[7., 10.], [15., 22.]]
x.sum()         # tensor(10.)
x.mean(dim=0)   # veergude keskmised: tensor([2., 3.])
x.sum(dim=1)    # ridade summad: tensor([3., 7.])
Kuidas dim töötab

Mõõde, mille nimetad, kaob ära. Kuju (2, 2) pärast sum(dim=0) on (2,): read liidetakse kokku ja jääb üks arv iga veeru kohta. dim=1 liidab veerud kokku ja jätab ühe arvu iga rea kohta.

1.2.3.4.4.6.dim=0x.sum(dim=0)read liidetakse kokku(2, 2) → (2,)1.2.3.4.3.7.dim=1x.sum(dim=1)veerud liidetaksekokku(2, 2) → (2,)
Nool näitab, mida mööda liidetakse. Mõõde, mida nimetad, kaob ära: kahest mõõtmest jääb üks.

Kuju muutmine

python
t = torch.arange(6)       # tensor([0, 1, 2, 3, 4, 5]), kuju (6,)
t.reshape(2, 3)           # [[0, 1, 2], [3, 4, 5]]
t.reshape(3, -1)          # -1 = "arvuta ise", kuju (3, 2)
t.unsqueeze(1).shape      # torch.Size([6, 1]): vektorist veerg
t.reshape(2, 3).T.shape   # torch.Size([3, 2]): transponeerimine

Ridade ja veergude valimine

python
X = torch.randn(100, 4)   # 100 näidet, 4 tunnust

X[0]              # esimene rida, kuju (4,)
X[:, 0]           # esimene veerg, kuju (100,)
X[:10]            # esimesed 10 rida, kuju (10, 4)
X[X[:, 0] > 0]    # read, kus esimene tunnus on positiivne

Broadcasting: standardiseerimine ühe reaga

Kui kujud ei klapi täpselt, venitab torch väiksema tensori üle suurema. Pisinäide: tabelile, kus on 2 rida, liidame ühe rea [10, 20]. Torch liidab selle rea mõlemale reale:

python
torch.tensor([[1, 2],
              [3, 4]]) + torch.tensor([10, 20])
# [[11, 22],
#  [13, 24]]

Sama nipiga saab ühe reaga standardiseerida kõik andmed korraga:

python
mu = X.mean(dim=0)        # kuju (4,): iga veeru keskmine
sd = X.std(dim=0)         # kuju (4,)
X_std = (X - mu) / sd     # (100, 4) ja (4,) -> rakendub igale reale

print(X_std.mean(dim=0))  # ligikaudu [0, 0, 0, 0]
⋮Xkuju (100, 4)−μ₁μ₂μ₃μ₄μ₁μ₂μ₃μ₄μ₁μ₂μ₃μ₄μ₁μ₂μ₃μ₄⋮mukuju (4,) → venitatakse=⋮X − mukuju (100, 4)iga ridasaab samamu
Torch kopeerib mu rea (oranž) mõtteliselt kõigile sajale reale (kriipsjoonega koopiad) ja lahutab siis elemendiviisi.

3Esimene mudel käsitsi

Alustame kõige väiksema mudeliga, mis üldse õpib: sirgega. Selles osas teeme kõik ise, tavaliste arvutustega. Torchi automaatset gradienti me veel ei kasuta. Nii näed, mis tegelikult toimub, ja saad aru, mida torch hiljem sinu eest ära teeb.

3.1 Ülesanne: andmed X ja y

Neli õpilast kirjutasid testi. Teame, mitu tundi igaüks õppis ja mitu punkti ta sai:

ÕpilaneÕppetunnid (X)Punktid (y)
A13
B25
C37
D49
  • X on see, mida me teame: sisend ehk tunnus.
  • y on see, mida tahame ennustada: sihtmuutuja.

Küsimus: mitu punkti saab õpilane, kes õppis 5 tundi? Selleks tuleb leida reegel, mis seob X-i ja y-t. Vaata tabelit: iga lisatund annab 2 punkti juurde ja 0 tunniga oleks 1 punkt. Reegel on y = 2·X + 1.

Meie nägime seda silmaga. Arvuti peab selle ise leidma, ja seda me nimetamegi õppimiseks. Õige reegli teame ette ainult selleks, et saaksime lõpus kontrollida, kas mudel leidis selle.

python
import torch

X = torch.tensor([1.0, 2.0, 3.0, 4.0])   # X: õppetunnid
y = torch.tensor([3.0, 5.0, 7.0, 9.0])   # y: testi punktid

print(X)
print(y)
väljund
tensor([1., 2., 3., 4.])
tensor([3., 5., 7., 9.])

3.2 Mudel: kaks arvu, w ja b

Mudel on lihtne valem, mis teeb X-ist ennustuse:

ŷ = w · X + b

Märk ŷ (loe „y-katus“, koodis y_hat) tähendab ennustust. Nii ei aeta seda segi tegeliku y-ga.

  • w (weight, kaal) ütleb, mitu punkti annab üks lisatund. See on sirge tõus.
  • b (bias, nihe) ütleb, mitu punkti saab õpilane, kes üldse ei õppinud (X = 0).

Neid kahte arvu me ei tea. Mudel peab need andmetest ise leidma. Neid arve nimetatakse mudeli parameetriteks.

Näide. Kui oleks w = 2 ja b = 1, siis õpilase C (X = 3) ennustus oleks 2·3 + 1 = 7. See on täpselt õige vastus. Aga mudel ei tea seda veel.

036912012345+1 tund+2 punkti = wABCDb = 1 (0 tundi → 1 punkt)y (punktid)X (õppetunnid)ŷ = w · X + bw = 2tõus: iga tund annab2 punkti juurdeb = 1nihe: punktid, kuiüldse ei õppinud
Sinised punktid on neli õpilast. Oranž sirge on see, mille mudel peab ise leidma. w on tõus: üks tund juurde annab 2 punkti juurde (lilla kolmnurk). b on koht, kus sirge lõikab y-telge: 0 tunniga on 1 punkt.

Alustame arvamisega: w = 0 ja b = 0. Siis ennustab mudel iga õpilase kohta 0 punkti.

python
w = torch.tensor(0.0)      # tõus: alguses ei tea mudel midagi, paneme 0
b = torch.tensor(0.0)      # nihe: samuti 0

y_hat = w * X + b          # ennustus kõigile õpilastele korraga
print(y_hat)
väljund
tensor([0., 0., 0., 0.])

Torch korrutab ühe arvu w kõigi X-i elementidega korraga, nagu broadcasting'u näites osas 2.

Sama on suures võrgus

Närvivõrgus on täpselt samasugused w-d ja b-d, ainult neid on tuhandeid. Kõik, mida siin nende kahega teeme, kehtib ka seal.

3.3 Viga: kui palju mudel eksib?

Mudel ennustas 0, aga õiged vastused on 3, 5, 7 ja 9. Leiame iga õpilase vea: ennustus miinus õige vastus.

Õpilaneyennustusvigaviga ruudus
A30−39
B50−525
C70−749
D90−981
0369012345ennustus ŷ = 0viga −3Aviga −5Bviga −7Cviga −9Dy (punktid)X (õppetunnid)vead ruutu:A: (−3)² = 9B: (−5)² = 25C: (−7)² = 49D: (−9)² = 81keskmine ruutviga(9+25+49+81) / 4 = 41
Praegu ennustab mudel iga õpilase kohta 0 punkti. Punane lõik on viga: kui kaugel on tegelik vastus ennustusest. Vead tõstetakse ruutu ja neist võetakse keskmine. See arv, 41, on mudeli viga.

Miks ruutu? Mõni ennustus võib olla liiga suur ja mõni liiga väike. Kui paneksime vead lihtsalt kokku, tühistaksid plussid ja miinused üksteist. Näiteks vead −3 ja +3 annaksid keskmiseks 0, kuigi mõlemad ennustused on valed. Ruut kõrvaldab selle probleemi ja teeb suure vea eriti kalliks.

Võtame ruutude keskmise: (9 + 25 + 49 + 81) / 4 = 41. See üks arv on mudeli viga (inglise keeles loss). Mida väiksem, seda parem mudel. Õppimine tähendab: muuda w-d ja b-d nii, et see arv väheneks.

python
error = y_hat - y                  # viga igal õpilasel
loss = (error ** 2).mean()         # vigade ruutude keskmine

print(error)
print(loss)
väljund
tensor([-3., -5., -7., -9.])
tensor(41.)

Seda viga nimetatakse keskmiseks ruutveaks (MSE, mean squared error).

3.4 Kuidas viga vähendada? Tuletis w jaoks

Mudelil on kaks nuppu, w ja b. Keerame neid ühekaupa. Alustame w-ga ja hoiame b-d seni paigal (b = 0). Küsimus: kummale poole w-d keerata, et viga väheneks? Selle jaoks on vaja ühte uut sõna, tuletist.

Tuletis ütleb, kui palju tulemus muutub, kui sisendit muuta. Vaatame esmalt meie enda reeglit y = 2·X + 1:

Xy = 2·X + 1muutus
13
25+2
37+2
49+2

Iga kord, kui X kasvab 1 võrra, kasvab y 2 võrra. Seega tuletis on 2. (Juurde liidetud 1 ei muuda midagi: see tõstab kõik y-d ühepalju üles, aga muutus sammult sammule jääb samaks.)

Tuletise saab leida jagamisega:

tuletis ≈ tulemuse muutus / sisendi muutus

Tabelist: X muutus 1 → 2 ja y muutus 3 → 5, seega (5 − 3) / (2 − 1) = 2.

Teeme nüüd sama meie veaga. Sisend on w ja tulemus on viga. Kõige lihtsam on proovida: nihutame w-d natuke (0 → 0.1), hoiame b-d nulli peal ja vaatame, mis veaga juhtub.

python
def loss_for(w, b):                        # viga antud w ja b korral
    y_hat = w * X + b
    return ((y_hat - y) ** 2).mean().item()

print(round(loss_for(0.0, 0.0), 3))        # praegune viga: w = 0, b = 0
print(round(loss_for(0.1, 0.0), 3))        # w on 0.1 võrra suurem, b jääb 0
väljund
41.0
37.575

Viga langes 41-lt 37.575-ni, seega w suurendamine aitab. Jagame samamoodi nagu tabelis:

(37.575 − 41) / (0.1 − 0) = −3.425 / 0.1 = −34.25

See arv ütleb: iga ühe w-ühiku kohta muutub viga umbes −34 võrra.

Miks „umbes“? Reegel y = 2·X + 1 oli sirge, aga viga w funktsioonina on kõver, ja kõvera kalle on igas kohas natuke teine. Mida väiksem on nihe, seda täpsemalt saame kalde just sealt, kus praegu oleme. Proovime kolme nihet:

python
for h in [0.1, 0.01, 0.001]:               # h = kui palju w-d nihutame
    slope_w = (loss_for(h, 0.0) - loss_for(0.0, 0.0)) / h      # b jääb 0
    print(h, round(slope_w, 2))
väljund
0.1 -34.25
0.01 -34.93
0.001 -34.99

Arvud lähenevad: vea tuletis w järgi on −35. Mida see tähendab?

  • Tuletis on negatiivne (nagu praegu): kui w kasvab, viga väheneb. Seega w-d tuleb suurendada.
  • Tuletis on positiivne: kui w kasvab, viga kasvab. Seega w-d tuleb vähendada.
  • Tuletis on 0: oleme põhjas ja w-d pole vaja muuta.

Kujuta ette, et seisad udus mäenõlval ja tahad orgu jõuda. Sa ei näe kaugele, aga tunned jalgadega, kuhu poole maapind langeb. Tuletis ongi see kalle jalge all. Joonisel on viga w funktsioonina:

0102030405001234praegu: w = 0, viga 41tuletis −35tuletis +25tuletis 0vigaw (b hoitud nulli juures)tuletis < 0→ suurenda wtuletis > 0→ vähenda wtuletis = 0→ oled põhjas
Kõver näitab, kui suur on meie näite viga erinevate w väärtuste korral (b on lihtsuse mõttes nulli peal). Tuletis on kõvera kalle selles kohas, kus parasjagu oleme (lilla kriipsjoon). Rohelised nooled näitavad, kuhu w-d nihutada.

3.5 Tuletis b jaoks

Nüüd teine nupp. Hoiame w nulli peal (w = 0) ja nihutame ainult b-d. Võte on täpselt sama: nihe, vea muutus, jagamine. Kui b läheb 0 → 0.1, langeb viga 41-lt 39.81-ni, seega (39.81 − 41) / 0.1 = −11.9. Väiksemate nihetega:

python
print(round(loss_for(0.0, 0.1), 3))        # nüüd b on 0.1 võrra suurem, w jääb 0

for h in [0.1, 0.01, 0.001]:               # h = kui palju b-d nihutame
    slope_b = (loss_for(0.0, h) - loss_for(0.0, 0.0)) / h      # w jääb 0
    print(h, round(slope_b, 2))
väljund
39.81
0.1 -11.9
0.01 -11.99
0.001 -12.0

Arvud lähenevad: vea tuletis b järgi on −12. Ka see on negatiivne, seega ka b-d tuleb suurendada.

Pane tähele, et tegime need kaks asja eraldi: w tuletise leidmisel hoidsime b-d paigal ja b tuletise leidmisel w-d. Nii tehakse alati, kui parameetreid on mitu: igaühe tuletis leitakse nii, et teised jäävad paigale. (Matemaatikas nimetatakse seda osatuletiseks, aga sõna pole vaja meelde jätta.)

Mõlemad tuletised koos nimetatakse gradiendiks. Meie gradient on praegu (−35, −12). See ütleb, kummale poole kumbagi nuppu keerata.

3.6 Tuletis valemiga

Proovimine (parameetrit nihutada ja vaadata) töötab, aga on aeglane ja ligikaudne. Meie mudeli jaoks on olemas täpne valem:

  • tuletis w järgi: keskmine(2 · viga · X)
  • tuletis b järgi: keskmine(2 · viga)

Arvutame selle oma näites käsitsi. Vead on −3, −5, −7 ja −9:

ÕpilaneXvigaviga · X
A1−3−3
B2−5−10
C3−7−21
D4−9−36
  • w jaoks: keskmine(viga · X) = (−3 − 10 − 21 − 36) / 4 = −17.5. Korda 2-ga: −35.
  • b jaoks: keskmine(viga) = (−3 − 5 − 7 − 9) / 4 = −6. Korda 2-ga: −12.

Täpselt samad arvud, mis proovimisega, ainult nüüd täpselt. Sama koodis:

python
grad_w = (2 * error * X).mean()     # tuletis w järgi
grad_b = (2 * error).mean()         # tuletis b järgi

print(grad_w, grad_b)
väljund
tensor(-35.) tensor(-12.)
Kust see valem tuleb?

Ühe õpilase viga on e = w·X + b − y ja tema panus mudeli veasse on e². Kui w kasvab 1 võrra, kasvab ennustus X võrra, seega ka e kasvab X võrra. Ruudu e² tuletis on 2·e, ja see tuleb korrutada e muutusega ehk X-iga. Tulemus on 2·e·X. Kõigi õpilaste peale keskmistades saame valemi.

b jaoks sama, aga b kasvades kasvab e ainult 1 võrra, seega saame 2·e.

Pane tähele

Kui mudelit muudad, tuleb valem uuesti välja tuletada. Kahe parameetriga läks see lihtsalt, aga närvivõrgus on tuhandeid parameetreid. Järgmises osas näed, et torch arvutab tuletised ise.

3.7 Üks samm

Nüüd teame, kuhu poole w-d ja b-d liigutada. Reegel on selline:

w = w − lr · tuletis_w
b = b − lr · tuletis_b

lr (learning rate, õppesamm) on väike arv, mis ütleb, kui suure sammu teeme. Võtame lr = 0.05.

Miks miinus? Meie w tuletis on negatiivne (−35), seega w-d tuleb suurendada. Valem teebki seda, sest miinus korda miinus on pluss:

w = 0 − 0.05 · (−35) = 0 + 1.75 = 1.75
b = 0 − 0.05 · (−12) = 0 + 0.6 = 0.6

Kui tuletis oleks positiivne, vähendaks sama valem w-d. Tuletise märk otsustab, kummale poole liigume, ja lr otsustab, kui kaugele.

python
lr = 0.05                           # õppesamm: kui suure sammu teeme

w = w - lr * grad_w
b = b - lr * grad_b
print(w, b)
väljund
tensor(1.7500) tensor(0.6000)

Kontrollime, kas ennustused paranesid:

python
y_hat = w * X + b
loss = ((y_hat - y) ** 2).mean()

print(y_hat)
print(loss)
väljund
tensor([2.3500, 4.1000, 5.8500, 7.6000])
tensor(1.1288)

Ennustused (2.35; 4.1; 5.85; 7.6) on õigetele vastustele (3; 5; 7; 9) nüüd palju lähemal. Viga langes 41-lt 1.13-ni ühe sammuga.

3.8 Kordame, kuni mudel on valmis

Üks samm ei ole veel kõik: ennustused pole veel täpsed. Seepärast kordame sama sammu tuhat korda. Üht täielikku läbimist kõigist andmetest nimetatakse epohhiks. Tsükli neli sammu (numbrid 1–4 koodis) on samad, mida me juba tegime: ennustus, viga, tuletised, samm.

python
w = torch.tensor(0.0)               # alustame uuesti nullist
b = torch.tensor(0.0)
lr = 0.05

for epoch in range(1000):
    y_hat = w * X + b                       # 1) ennustus
    error = y_hat - y                       # 2) viga
    loss = (error ** 2).mean()

    grad_w = (2 * error * X).mean()         # 3) tuletised
    grad_b = (2 * error).mean()

    if epoch in (0, 1, 2, 10, 100, 999):    # prindime ainult mõne epohhi
        print(f"epohh {epoch:3d}   w = {w:.3f}   b = {b:.3f}   viga = {loss:.4f}")

    w = w - lr * grad_w                     # 4) samm
    b = b - lr * grad_b
väljund
epohh   0   w = 0.000   b = 0.000   viga = 41.0000
epohh   1   w = 1.750   b = 0.600   viga = 1.1288
epohh   2   w = 2.037   b = 0.703   viga = 0.0433
epohh  10   w = 2.084   b = 0.754   viga = 0.0101
epohh 100   w = 2.022   b = 0.937   viga = 0.0007
epohh 999   w = 2.000   b = 1.000   viga = 0.0000
  • Epohhil 0 on w = 0, b = 0 ja viga 41. Pärast ühte sammu (epohh 1) on w = 1.75, b = 0.6 ja viga 1.13, täpselt nagu käsitsi arvutasime.
  • Esimesed sammud teevad kõige rohkem tööd. Hiljem on vaja ainult peenhäälestust.
  • Epohhiks 999 on w = 2.000 ja b = 1.000. Mudel leidis ise üles reegli y = 2·X + 1, mida me talle ei öelnud.
036912012345epohh 0epohh 1epohh 2epohh 999X10010.010.0001050100150200411.13viga (log-skaala)epohh
Vasakul: sirge asend mõne epohhi järel. Juba esimese sammuga hüppab sirge andmete lähedale (41 → 1.13), edasi tuleb peenhäälestus. Paremal: viga esimese 200 epohhi jooksul. Skaala on logaritmiline, et näha nii suuri kui ka väga väikeseid arve.

3.9 Ennustamine

Õpitud w ja b ongi mudel. Nendega saab ennustada uusi õpilasi. Mitu punkti saab see, kes õppis 5 tundi?

python
print(w * 5 + b)       # õpilane, kes õppis 5 tundi
väljund
tensor(11.0000)

Mudel ennustab 11 punkti. Reegel 2·5 + 1 annab sama.

Proovi

Muuda lr väärtuseks 0.2. Viga ei vähene, vaid kasvab iga sammuga ja lõpuks tuleb nan, sest samm on nii suur, et hüppame miinimumist üle. Pane lr = 0.0001: mudel õpib, aga nii aeglaselt, et 1000 epohhi järel on w alles umbes 1.7.

Meelespea
  • Mudel: ŷ = w · X + b. w ja b on parameetrid.
  • Viga: ennustuste ja õigete vastuste erinevuste ruutude keskmine.
  • Tuletis: kui palju viga muutub, kui parameetrit natuke muuta (teised jäävad paigale). Kõik tuletised koos on gradient.
  • Samm: w = w − lr · tuletis. Negatiivne tuletis suurendab w-d, positiivne vähendab.
  • Korda sammu sadu kordi.

Ainus tüütu osa oli tuletise valem. Kahe parameetri juures läks see lihtsalt, aga suure võrgu juures on see lootusetu. Järgmises osas näed, kuidas torch arvutab tuletised ise.

4Gradient: torch arvutab ise

Eelmises osas kirjutasime tuletise valemi ise: (2 * error * X).mean(). Kahe parameetri ja ühe lihtsa mudeli jaoks see õnnestus. Päris närvivõrgus on aga tuhandeid parameetreid ja mitu kihti. Selle valemi käsitsi tuletamine oleks lootusetu ja iga mudelimuudatusega peaks seda uuesti tegema.

Torch arvutab tuletised ise. Seda võimalust nimetatakse autograd.

4.1 Sama gradient ühe käsuga

Torchile tuleb öelda kolm asja:

KoodTähendus lihtsas keeles
requires_grad=True„Jälgi seda arvu, mul on vaja selle tuletist.“
loss.backward()„Arvuta nüüd tuletised.“
w.grad„Siin on vastus.“

Teeme samad arvutused, mis eelmises osas, aga tuletise valemit me seekord ei kirjuta:

python
w = torch.tensor(0.0, requires_grad=True)    # "jälgi seda"
b = torch.tensor(0.0, requires_grad=True)

y_hat = w * X + b
loss = ((y_hat - y) ** 2).mean()
print(loss)

loss.backward()                              # "arvuta tuletised"
print(w.grad, b.grad)                        # "siin on vastus"
väljund
tensor(41., grad_fn=<MeanBackward0>)
tensor(-35.) tensor(-12.)

Saime −35 ja −12, täpselt need arvud, mille eelmises osas ise välja arvutasime. Väljundis olev grad_fn=<MeanBackward0> on märk, et torch jättis meelde, kuidas loss arvutati (viimane tehe oli keskmine). Ilma selle mäluta ei saaks ta tagurpidi minna.

4.2 Kuidas torch seda teeb?

Torch ei tea sinu mudeli valemit. Ta teab ainult iga lihtsa tehte tuletist: korrutamise, liitmise, ruutu tõstmise ja nii edasi. Mudeli arvutamise ajal (forward) jätab ta meelde, milliseid tehteid tehti. Seejärel (backward) käib ta sama tee tagurpidi, kiht-kihilt, ja arvutab igal sammul ühe lihtsa tuletise.

Vaatame seda väikese närvivõrgu peal. Sisendi ja väljundi vahel on peidetud kiht kahe neuroniga (h₁ ja h₂) ning kihtide vahel on neli kaalu, w₁ kuni w₄.

Seletused on joonise all. Joonisel on sammud 0–4 ja joonise all on iga sammu kohta oma lõik, samade numbritega. Vajuta joonisel „↺ Algusest“ ja siis „Edasi“ ning loe samal ajal vastavat lõiku joonise alt. Lõik, mis parasjagu kehtib, tõuseb esile.

forward: väärtusedbackward: mõju vealearvud: mõju · väärtus · kaalx1h₁2h₂1ŷ5(5 − 3)²viga = 4y3õige vastusw₁ = 2w₂ = 1w₃ = 1w₄ = 3mõju 2·(5−3) = 44 × 2 = 84 × 1 = 4mõju 4×1 = 4mõju 4×3 = 124 × 1 = 412 × 1 = 12forward:  h₁ = w₁·x    h₂ = w₂·x    ŷ = w₃·h₁ + w₄·h₂    viga = (ŷ − y)²viga = (ŷ − y)²    →    mõju ŷ-le = 2 · (ŷ − y)kaalu gradient = mõju paremal × väärtus vasakul      mõju edasi = mõju paremal × kaalkaalu gradient = mõju paremal × väärtus vasakulw₁.grad = 4    w₂.grad = 12    w₃.grad = 8    w₄.grad = 4    ← üks käsk: loss.backward()
Sama väike võrk kahes suunas. Sinine: forward arvutab väärtused vasakult paremale. Punane: backward käib kiht-kihilt tagasi ja arvutab, kui palju iga arv ja iga kaal veale mõjutab. Pildil on kõik arvud korraga. Vajuta „↺ Algusest“ ja siis „Edasi“, et näha, kuidas punased arvud samm-sammult tekivad. Iga sammu seletus on joonise all, samade numbritega 0–4.

Mõtle igale arvule küsimus: kui see arv kasvaks 1 võrra, kui palju muutuks viga? Vastust nimetame arvu mõjuks veale. See ongi tuletis. Backward arvutab mõju iga arvu ja iga kaalu jaoks. Seletused sammude kaupa (numbrid on samad, mis joonisel):

  1. Forward (vasakult paremale). Iga nool korrutab arvu kaaluga ja neuron liidab sissetulevad arvud kokku. h₁ = w₁·x = 2·1 = 2 ja h₂ = w₂·x = 1·1 = 1. Väljund: ŷ = w₃·h₁ + w₄·h₂ = 1·2 + 3·1 = 5. Õige vastus on y = 3, seega viga = (5 − 3)² = 4. Torch jätab need tehted meelde.

  2. Backward algab veast. Küsime: kui ŷ kasvaks 1 võrra, kui palju muutuks viga? Viga on (ŷ − y)² ja ruudu tuletis on 2·(ŷ − y) = 2·(5 − 3) = 4. Kirjutame ŷ juurde: mõju 4.

  3. Üks kiht tagasi (w₃ ja w₄). Siin arvutame kaks asja.

    Kaalude gradiendid. Kui w₃ kasvab 1 võrra, kasvab ŷ h₁ = 2 võrra, mis annab veale 4 × 2 = 8. Samamoodi w₄: 4 × h₂ = 4 × 1 = 4.

    Mõju peidetud neuronitele. Kui h₁ kasvab 1 võrra, kasvab ŷ w₃ = 1 võrra, mis annab 4 × 1 = 4. Samamoodi h₂: 4 × w₄ = 4 × 3 = 12.

  4. Veel üks kiht tagasi (w₁ ja w₂). Sama reegel, nüüd tuleb mõju peidetud neuronitest: w₁: 4 × x = 4 × 1 = 4 ja w₂: 12 × x = 12 × 1 = 12. Sisendist x edasi pole vaja minna, sest x on andmed, mitte parameeter.

  5. Valmis. Igal kaalul on nüüd gradient: w₁ = 4, w₂ = 12, w₃ = 8, w₄ = 4. Täpselt seda teebki loss.backward(), ükskõik kui palju kihte on.

Kaks reeglit

Igal kihil kordab torch kahte lihtsat reeglit:

  • kaalu gradient = mõju paremal × väärtus vasakul
  • mõju edasi vasakule = mõju paremal × kaal

Matemaatikas nimetatakse seda ahelreegliks (chain rule).

Viimane samm käsitsi. Gradiendid on olemas, nüüd liigutame iga kaalu nagu osas 3: w = w − lr · gradient. Võtame lr = 0.01:

w₁ = 2 − 0.01 · 4 = 1.96
w₂ = 1 − 0.01 · 12 = 0.88
w₃ = 1 − 0.01 · 8 = 0.92
w₄ = 3 − 0.01 · 4 = 2.96

Teeme forward uuesti uute kaaludega: h₁ = 1.96, h₂ = 0.88 ja ŷ = 0.92 · 1.96 + 2.96 · 0.88 = 4.408. Viga on nüüd (4.408 − 3)² ≈ 1.98, enne oli 4. Viga langes, seega gradiendid osutasid õigesse suunda. Päris treeningus kordame sama sammu sadu kordi, nagu osas 3.

Sama võrk torchis. Kirjutame ainult forward-arvutuse, kõik muu teeb backward():

python
x = torch.tensor(1.0)                          # sisend
target = torch.tensor(3.0)                     # õige vastus (joonisel y)

w1 = torch.tensor(2.0, requires_grad=True)     # neli kaalu
w2 = torch.tensor(1.0, requires_grad=True)
w3 = torch.tensor(1.0, requires_grad=True)
w4 = torch.tensor(3.0, requires_grad=True)

h1 = w1 * x                                    # peidetud kiht
h2 = w2 * x
y_hat = w3 * h1 + w4 * h2                      # väljund
loss = (y_hat - target) ** 2                   # viga

print(h1.item(), h2.item(), y_hat.item(), loss.item())

loss.backward()
print(w1.grad, w2.grad, w3.grad, w4.grad)
väljund
2.0 1.0 5.0 4.0
tensor(4.) tensor(12.) tensor(8.) tensor(4.)

Need on täpselt pildi arvud: w₁ 4, w₂ 12, w₃ 8 ja w₄ 4.

Soovid näha ka vahearvude mõju?

Vaikimisi jätab torch gradiendi meelde ainult parameetritele (w₁ kuni w₄), sest ainult neid on vaja uuendada. Kui tahad näha ka vahearvude mõju (pildil punased „mõju“ arvud), palu torchil need meelde jätta:

python
w1.grad = w2.grad = w3.grad = w4.grad = None   # nulli eelmine tulemus

h1 = w1 * x
h2 = w2 * x
y_hat = w3 * h1 + w4 * h2
loss = (y_hat - target) ** 2

for t in (h1, h2, y_hat):
    t.retain_grad()                            # jäta ka vahearvu gradient meelde

loss.backward()
print(y_hat.grad, h1.grad, h2.grad)
väljund
tensor(4.) tensor(4.) tensor(12.)

Need on pildi arvud: ŷ mõju 4, h₁ mõju 4 ja h₂ mõju 12.

Pildil jätsime lihtsuse mõttes kihtide vahelt aktivatsiooni (näiteks ReLU) välja. Päris võrgus lisandub kihtide vahele üks tehe veel. Torch teab ka selle tuletise (ReLU puhul on see 1 või 0) ja reeglid jäävad samaks.

Meelespea
  • requires_grad=True → jälgi; backward() → arvuta; .grad → vastus.
  • Forward arvutab väärtused vasakult paremale, backward mõjud paremalt vasakule, kiht-kihilt.
  • Kaalu gradient = mõju paremal × väärtus vasakul. Mõju edasi = mõju paremal × kaal.
  • Kaalu liigutamine: w = w − lr · gradient. Viga väheneb.

Nüüd on gradient selge: tuletised näitavad, kuhu poole iga kaalu liigutada, ja torch leiab need ise. Järgmises osas paneme sama väikese mudeli kokku torchi valmis klotsidest.

5Sama mudel torchi tööriistadega

Osas 3 tegime kõik ise ja osas 4 nägid, et torch leiab tuletised ise. Torchis on ka valmis klotsid, mis teevad ülejäänud käsitsi töö ära. nn.Linear hoiab parameetreid w ja b, nn.MSELoss arvutab vea ja optim.SGD teeb uuendussammu, täpselt sama, mis osas 3 rida w = w - lr * grad_w, aga kõigile parameetritele korraga. Tsükkel jääb samaks.

Üks pisiasi: nn.Linear tahab sisendit kujuga (N, 1), ehk N rida ja igas reas üks tunnus. Meie X on praegu kujuga (4,), seega muudame kuju reshape-iga (nagu osas 2).

python
from torch import nn

Xc = X.reshape(-1, 1)                    # nn.Linear tahab kuju (N, 1)
yc = y.reshape(-1, 1)

model = nn.Linear(1, 1)                  # 1 sisend, 1 väljund; w ja b on sees
loss_fn = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

for epoch in range(1000):
    y_hat = model(Xc)                    # 1) forward: ennustus
    loss = loss_fn(y_hat, yc)            # 2) viga
    optimizer.zero_grad()                # 3) nulli vanad gradiendid
    loss.backward()                      # 4) backward: uued gradiendid
    optimizer.step()                     # 5) uuenda parameetreid

print(f"w = {model.weight.item():.3f}   b = {model.bias.item():.3f}")

with torch.no_grad():
    print(model(torch.tensor([[5.0]])))  # õpilane, kes õppis 5 tundi
väljund
w = 2.000   b = 1.000
tensor([[11.0000]])

Need viis rida on iga PyTorchi mudeli treenimise süda. Need jäävad samaks ka suures närvivõrgus:

1model(X)ennustus ŷ2loss_fn(ŷ, y)kui palju eksib3zero_grad()vanad gradiendid nulli4backward()uued gradiendid5step()w ja b liiguvadüks treeningsamm↺ korda igal epohhil
Iga treeningsamm käib selle ringi läbi. Järjekord on tähtis: zero_grad() peab olema enne backward()-i, sest muidu liituvad uued gradiendid vanadele.
Kaks uut asja koodis
  • optimizer.zero_grad(): backward() ei kirjuta vana gradienti üle, vaid liidab uue juurde. Kui vanu ei nullita, kasutaks iga samm ka kõigi eelmiste sammude gradiente ja mudel liiguks valesse kohta. Seepärast nullime enne iga backward()-i.
  • with torch.no_grad(): tähendab „siin ära jälgi“. Ennustamisel gradienti ei vajata, seega ei pea torch tehteid meelde jätma.
SammKäsitsi (osa 3)Torchiga (osad 4 ja 5)
parameetridw ja b tavaliste tensoritenann.Linear(1, 1)
ennustusw * X + bmodel(X)
viga((y_hat - y) ** 2).mean()nn.MSELoss()
tuletisedvalem (2 * error * X).mean()loss.backward()
sammw = w - lr * grad_woptimizer.step()
gradientide nulliminepole vaja, arvutame iga kord uuestioptimizer.zero_grad()

6Mitmekihiline pertseptron (MLP)

Lineaarne kiht

Osas 3 oli sirge w·x + b üks neuron: võta sisend, korruta kaaluga, liida nihe. Kui sisendeid on mitu, saab iga sisend oma kaalu ja tulemused liidetakse kokku.

Pisinäide. Kaks sisendit x₁ = 1 ja x₂ = 4, kaalud 2 ja 3, nihe 1. Neuron arvutab 2·1 + 3·4 + 1 = 15. Torchis:

python
from torch import nn

layer = nn.Linear(2, 1)                  # 2 sisendit, 1 väljund
with torch.no_grad():                    # paneme kaalud käsitsi, et tulemust kontrollida
    layer.weight[:] = torch.tensor([[2.0, 3.0]])
    layer.bias[:] = torch.tensor([1.0])

print(layer(torch.tensor([[1.0, 4.0]])))   # tensor([[15.]], grad_fn=<AddmmBackward0>)

Tavaliselt kaale käsitsi ei panda. Torch valib alguses juhuslikud kaalud ja treenimine leiab head väärtused.

nn.Linear(sisse, välja) on mitu sellist neuronit kõrvuti. Näiteks nn.Linear(4, 3) on 3 neuronit ja igaüks neist vaatab kõiki 4 sisendit. Igal neuronil on 4 kaalu ja 1 nihe, seega on parameetreid 3·4 + 3 = 15.

python
layer = nn.Linear(4, 3)
X = torch.randn(10, 4)       # 10 näidet, 4 tunnust

print(layer(X).shape)        # torch.Size([10, 3])
print(layer.weight.shape)    # torch.Size([3, 4])
print(layer.bias.shape)      # torch.Size([3])   -> kokku 12 + 3 = 15 parameetrit
w₁₁w₁₂w₁₃w₁₄x₁x₂x₃x₄h₁h₂h₃Σ+b₁sisend(N, 4)(N, 3)väljundW: kuju (3, 4)→ 12 kaalu (jooned)b: kuju (3,)→ 3 nihetkokku 15 parameetrith₁ = w₁₁·x₁ + w₁₂·x₂ + w₁₃·x₃ + w₁₄·x₄ + b₁
nn.Linear(4, 3): iga väljund on kõigi nelja sisendi kaalutud summa pluss nihe. Iga joon on üks kaal. Lillad jooned on väljundi h₁ kaalud, kollases kastis on sama valemina.

Miks üks kiht ei piisa ja miks kaks lineaarset kihti ka ei piisa

Lineaarne kiht oskab tõmmata ainult sirgjoont. Kui ühe klassi punktid on kaares või ringis, ei saa sirgjoonega neid eraldada.

Kas aitaks, kui paneme kaks lineaarset kihti järjest? Ei aita. Pisinäide: esimene kiht korrutab 2-ga, teine korrutab 3-ga. Kokku on see lihtsalt korrutamine 6-ga, ehk jälle üks sirge. Ükskõik kui palju lineaarseid kihte järjest panna, tulemus on ikka sirge.

Lahendus on panna kihtide vahele väike „murdja“, mida nimetatakse aktivatsioonifunktsiooniks. Kõige levinum on ReLU: ReLU(z) = max(0, z). Reegel on lihtne: negatiivne arv muutub nulliks, positiivne jääb samaks. Nii tekib sirgesse murdekoht ja paljudest murdekohtadest saab kokku kõvera piiri.

python
relu = nn.ReLU()
print(relu(torch.tensor([-2.0, -0.5, 0.0, 1.5])))   # tensor([0.0000, 0.0000, 0.0000, 1.5000])
-1123-3-2-1123−2 ja −0.5 → 01.5 → 1.5ReLU(z) = max(0, z)zLinear → Linearikka sirgeLinear → ReLU → Linearmurdekohad → kõver
Vasakul: ReLU paneb negatiivsed arvud nulliks ja jätab positiivsed samaks. Paremal: ilma ReLU-ta on mitu Linear-kihti kokku ikka sirge. ReLU-ga tekivad murdekohad ja paljudest murdekohtadest saab kõvera piiri.

Võrk kokku: nn.Sequential

python
model = nn.Sequential(
    nn.Linear(2, 16),    # sisendkiht: 2 tunnust -> 16 neuronit
    nn.ReLU(),
    nn.Linear(16, 16),   # peidetud kiht
    nn.ReLU(),
    nn.Linear(16, 2),    # väljund: iga klassi skoor (logit)
)
print(model)

n_params = sum(p.numel() for p in model.parameters())
print(n_params)          # 354 = (2·16+16) + (16·16+16) + (16·2+2)
+ ReLU+ ReLUx₁x₂⋮⋮01Linear(2, 16)48 param.Linear(16, 16)272 param.Linear(16, 2)34 param.(N, 2)sisend(N, 16)peidetud 1(N, 16)peidetud 2(N, 2)logitid
Võrk 2 → 16 → 16 → 2. Peidetud kihtides on 16 neuronit, joonisel on neist näha viis. Kriipsjoonega kast näitab, et selle kihi väljund läheb läbi ReLU. All on kuju pärast iga kihti. Parameetreid on kokku 48 + 272 + 34 = 354.
Reegel

Iga Linear kihi esimene arv peab võrduma eelmise kihi teise arvuga. Esimese kihi esimene arv on tunnuste arv (X.shape[1]), viimase kihi teine arv on klasside arv.

Sama mudel klassina

Päris projektides kirjutatakse mudel tihti klassina. Siis saab forward meetodisse panna mida iganes. Mõlemad kirjapildid annavad sama tulemuse.

python
class MLP(nn.Module):
    def __init__(self, n_in, n_hidden, n_out):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(n_in, n_hidden),
            nn.ReLU(),
            nn.Linear(n_hidden, n_hidden),
            nn.ReLU(),
            nn.Linear(n_hidden, n_out),
        )

    def forward(self, x):
        return self.net(x)

model = MLP(n_in=2, n_hidden=16, n_out=2)

Viimane kiht ja kaofunktsioon sõltuvad ülesandest

ÜlesanneViimane kihtKaofunktsioonSildid y
Regressioon (arv)nn.Linear(h, 1)nn.MSELoss()float32, kuju (N, 1)
Kaks klassinn.Linear(h, 1)nn.BCEWithLogitsLoss()float32 0/1, kuju (N, 1)
K klassi (ka K = 2)nn.Linear(h, K)nn.CrossEntropyLoss()long 0…K−1, kuju (N,)
Logitid

Klassifitseerimisel annab võrk iga klassi kohta ühe arvu, mida nimetatakse logitiks. Mida suurem arv, seda kindlam on võrk selles klassis. Logitid ei ole veel tõenäosused, need võivad olla ka negatiivsed.

Pisinäide. Võrk andis ühe punkti kohta logitid [2.0, 0.5]. Klass 0 sai suurema arvu, seega ennustus on klass 0. Softmax teeb logitidest tõenäosused, mis annavad kokku 1:

python
logits = torch.tensor([[2.0, 0.5]])

print(torch.softmax(logits, dim=1))   # tensor([[0.8176, 0.1824]])  -> 82% ja 18%
print(logits.argmax(dim=1))           # tensor([0])  -> ennustatud klass 0

Treenimisel softmaxi ise ära lisa, sest CrossEntropyLoss teeb selle sisemiselt ise. Viimane kiht on lihtsalt nn.Linear.

7Mänguväljak

Siin treenib brauseris päris väike MLP, sama tsükliga nagu eespool. Taustavärv näitab, millise klassi võrk igas punktis ennustaks. Muuda seadeid ja vaata, kuidas piir muutub. Paremal on sama mudel PyTorchi koodina.

klass 0klass 1
epohh0
loss–
täpsus–
param.–
sama torchis
  1. Vali aktivatsiooniks Puudub. Piir jääb sirgeks, ükskõik kui palju kihte või neuroneid lisad. See ongi põhjus, miks ReLU on vajalik.
  2. Vali Ring ringi sees ja pane neuroneid 2. Võrk ei suuda ringi ümbritseda. Lisa neuroneid ükshaaval ja leia, millal hakkab välja tulema.
  3. Pane õppesammuks 0.3. Loss hüppab ega lange ühtlaselt. Pane 0.001: õpib, aga aeglaselt.

8Täisnäide: MLP klassifitseerija

Nüüd kõik koos päris töövoona: andmed, jagamine treening- ja testihulgaks, skaleerimine, partiid, treenimine ja hindamine. Andmete jaoks kasutame scikit-learni (pip install scikit-learn).

python
import torch
from torch import nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

torch.manual_seed(42)

# 1. Andmed
X, y = make_moons(n_samples=1000, noise=0.2, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y)

scaler = StandardScaler().fit(X_train)    # sobita AINULT treeningandmetel
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)

# NumPy -> tensor: tunnused float32, sildid long
X_train = torch.tensor(X_train, dtype=torch.float32)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.long)
y_test = torch.tensor(y_test, dtype=torch.long)

train_loader = DataLoader(TensorDataset(X_train, y_train),
                          batch_size=32, shuffle=True)

# 2. Mudel, kaofunktsioon, optimeerija
model = nn.Sequential(
    nn.Linear(2, 16), nn.ReLU(),
    nn.Linear(16, 16), nn.ReLU(),
    nn.Linear(16, 2),
)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

# 3. Treenimine
for epoch in range(50):
    model.train()
    total = 0.0
    for xb, yb in train_loader:          # üks partii korraga
        logits = model(xb)
        loss = loss_fn(logits, yb)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total += loss.item() * len(xb)

    if epoch % 10 == 0:
        print(f"epohh {epoch:2d}   keskmine loss {total / len(X_train):.3f}")

# 4. Hindamine testihulgal
model.eval()
with torch.no_grad():
    pred = model(X_test).argmax(dim=1)
    acc = (pred == y_test).float().mean().item()
print(f"testi täpsus: {acc:.1%}")        # umbes 97–99%

Mida uued sõnad tähendavad

  • Epohh: üks täisring üle kõigi treeningandmete.
  • Partii (batch): väike tükk andmeid, siin 32 näidet. Parameetreid uuendatakse pärast iga partiid, mitte ainult epohhi lõpus. See on kiirem ja õpib sageli paremini.
  • DataLoader: jagab andmed partiideks. shuffle=True segab järjekorra igal epohhil.
  • Adam: SGD targem sugulane, mis kohandab sammu iga parameetri jaoks ise. Hea vaikevalik.
  • model.train() ja model.eval(): lülitavad treeningu ja testimise režiimi. Selles mudelis vahet pole, aga Dropouti ja BatchNormiga on. Harjuta need kohe sisse.
32↓ partii 1: 32 rida = 1 sammpartii 251 epohh = 25 partiid = 25 parameetrite uuendustshuffle=True: iga epohhi alguses segatakse read, nii et partiid tulevad iga kord erinevad50 epohhi × 25 partiid = 1250 sammu
Täisnäites on treeningus 800 rida. DataLoader lõikab need 32-realisteks partiideks ja iga partii järel uuendatakse parameetreid.

Ennusta uut punkti

python
new = scaler.transform([[0.5, -0.3]])                 # sama skaleerimine!
new = torch.tensor(new, dtype=torch.float32)

with torch.no_grad():
    probs = torch.softmax(model(new), dim=1)

print(probs)                     # nt tensor([[0.02, 0.98]])
print(probs.argmax(dim=1).item())   # ennustatud klass: 1
Iseseisev ülesanne

Vaheta andmed välja: from sklearn.datasets import load_iris, X, y = load_iris(return_X_y=True). Mida pead mudelis muutma? (Vihje: vaata tunnuste ja klasside arvu, need on 4 ja 3.)

9Levinud vead

Mida näedPõhjusParanda
mat1 and mat2 shapes cannot be multiplied (32x4 and 2x16)Esimese kihi sisendite arv ei võrdu tunnuste arvuga.Esimene kiht peab olema nn.Linear(X.shape[1], …), siin nn.Linear(4, 16).
Viga, kus on Double ja FloatNumPy andmed jäid float64.torch.tensor(X, dtype=torch.float32)
Viga, kus on LongCrossEntropyLoss silt on vale tüüpi või kujuga.Sildid torch.long kujuga (N,), mitte (N, 1).
Loss ei lange või on nanLiiga suur õppesamm, skaleerimata andmed või puudub zero_grad().Vähenda lr, kasuta StandardScalerit, kontrolli 5 sammu.
Täpsus kinni umbes klasside osakaalu juuresViimases kihis on softmax või ReLU, või puudub aktivatsioon kihtide vahel.Viimane kiht on puhas Linear, ReLU ainult kihtide vahel.
Can't call numpy() on Tensor that requires gradTensor on veel arvutusgraafi küljes.t.detach().numpy() või arvuta ploki torch.no_grad() sees.
Mälu kasvab, treening aeglustubKogud loendisse loss-tensoreid koos graafiga.Salvesta loss.item().
Silumise nipp

Kui midagi ei tööta, prindi kujud: print(xb.shape, yb.shape, logits.shape). Pooled vead selguvad sellest ühest reast.

Kontrolli ennast

Kaheksa küsimust. Vali vastus, kohe näed, kas see oli õige ja miks.

Spikker

tensor
torch.tensor(x, dtype=torch.float32)
t.shape, t.dtype
t.reshape(2, -1), t.unsqueeze(1)
t.sum(dim=0), t.mean(dim=1)
a @ b          # maatrikskorrutis
t.item()       # üks arv Pythoni arvuks
t.detach().numpy()
mudel
model = nn.Sequential(
    nn.Linear(F, H), nn.ReLU(),
    nn.Linear(H, K),
)
loss_fn = nn.CrossEntropyLoss()
opt = torch.optim.Adam(
    model.parameters(), lr=0.01)
treening
model.train()
for xb, yb in loader:
    loss = loss_fn(model(xb), yb)
    opt.zero_grad()
    loss.backward()
    opt.step()
hindamine
model.eval()
with torch.no_grad():
    pred = model(X_test).argmax(dim=1)
acc = (pred == y_test).float().mean()

Mis edasi? Kui MLP on selge, on järgmised sammud ülesobitamise vastu Dropout ja varajane peatamine, mudeli salvestamine (torch.save(model.state_dict(), "mudel.pt")) ning piltide jaoks konvolutsioonivõrk (CNN).