Məzmuna keç
Educora
Universitet25 dəq40 / 42

PyTorch: modelin öyrədilməsi dövrü

Dataset və DataLoader, itki funksiyaları, SGD və Adam optimallaşdırıcıları, tam öyrətmə dövrü, eval rejimi və dəqiqlik, modelin state_dict ilə saxlanması — sintetik verilənlərdə kiçik təsnifatçının öyrədilməsi ilə.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • Dataset və DataLoader ilə verilənləri paketlərə bölmək
  • Tapşırığa uyğun itki funksiyası və optimallaşdırıcı seçmək
  • Tam öyrətmə dövrünü yazmaq və modeli validasiya/test verilənlərində qiymətləndirmək
  • Modeli state_dict ilə saxlamaq və yenidən yükləmək

Artıq bütün hissələr əlimizdədir: tenzorlar, autograd və nn.Module. İndi onları peşəkarların hər gün yazdığı standart resept şəklində birləşdirək: verilənlər → DataLoader → model → itki → optimallaşdırıcı → öyrətmə dövrü → qiymətləndirmə → saxlama. Maraqlıdır ki, bu skelet kiçik təsnifatçı üçün də, milyardlarla parametrli dil modeli üçün də demək olar ki, eynidir — yalnız verilənlərin və modelin ölçüsü dəyişir.

Dataset və DataLoader

Dataset nümunələrin siyahısı kimi davranan obyektdir: __len__ nümunələrin sayını, __getitem__(i) isə i-ci (x, y) cütünü qaytarır. Verilənlər artıq tenzordadırsa, hazır TensorDataset kifayətdir; fayllardan (şəkillər, mətnlər) oxumaq üçün öz sinfini yazırsan. DataLoader isə Dataset-dən paketlər düzəldir, lazım olsa hər epoxada nümunələri qarışdırır (shuffle=True) və num_workers ilə yükləməni paralel apara bilir.

Python
import torch
from torch.utils.data import Dataset, DataLoader

class SquaresDataset(Dataset):
    def __init__(self, n):
        self.x = torch.arange(n, dtype=torch.float32)

    def __len__(self):
        return len(self.x)

    def __getitem__(self, i):
        return self.x[i], self.x[i] ** 2

ds = SquaresDataset(10)
print(len(ds), ds[3])
loader = DataLoader(ds, batch_size=4)
for xb, yb in loader:
    print(xb.tolist(), yb.tolist())
Gözlənilən nəticə
10 (tensor(3.), tensor(9.))
[0.0, 1.0, 2.0, 3.0] [0.0, 1.0, 4.0, 9.0]
[4.0, 5.0, 6.0, 7.0] [16.0, 25.0, 36.0, 49.0]
[8.0, 9.0] [64.0, 81.0]
10 nümunə, paket ölçüsü 4: iki tam paket və 2 nümunəlik son paket. DataLoader ayrı-ayrı nümunələri özü tenzorlara birləşdirir.
Tərif
Paket, addım (iterasiya) və epoxa

Paket — bir yeniləmədə birlikdə emal olunan nümunələr qrupudur. Addım və ya iterasiya — bir paket üzrə bir parametr yeniləməsidir. Epoxa — öyrədici dəstin bütün nümunələrinin bir dəfə «görülməsidir». Öyrətmə adətən onlarla epoxa davam edir və hər epoxa çoxlu addımdan ibarətdir.

steps per epoch = ⌈N / B⌉ updates = epochs · ⌈N / B⌉steps per epoch = ⌈N / B⌉ updates = epochs · ⌈N / B⌉
burada:
  • Nöyrədici dəstdəki nümunələrin sayı
  • Bpaketin ölçüsü (batch size)
  • ⌈ ⌉yuxarı yuvarlaqlaşdırma: son natamam paket də bir addımdır

İtki funksiyaları və optimallaşdırıcılar

TapşırıqSon qatİtkiHədəflər
Reqressiyann.Linear(h, 1)nn.MSELossfloat32 ədədlər
İkili təsnifatnn.Linear(h, 1)nn.BCEWithLogitsLoss0,0 və ya 1,0 (float32)
K sinifli təsnifatnn.Linear(h, K)nn.CrossEntropyLosssinif nömrələri 0…K−1 (int64)
L = −zₜ + ln ∑ⱼ₌₁ᴷ eᶻʲ
burada:
  • zmodelin xam logitləri
  • tdüzgün sinfin nömrəsi

nn.CrossEntropyLoss log-softmax ilə çarpaz entropiyanı birləşdirir: −ln softmax(z)ₜ = −zₜ + ln ∑ eᶻʲ. Paket üçün itkilərin orta qiyməti götürülür.

Nümunə 1: itkini logitlərdən hesabla

a) Logitlər z = (2; 0,5; −1), düzgün sinif 0. CrossEntropyLoss nədir?
b) Proqnozlar (2,5; 0), həqiqi qiymətlər (3; −0,5). MSELoss nədir?

Həllini göstər
a) e² + √e + e⁻¹ ≈ 7,389 + 1,649 + 0,368 = 9,406; ln 9,406 ≈ 2,2413.
L = −2 + 2,2413 = 0,2413.
b) Səhvlər −0,5 və 0,5, kvadratları 0,25 və 0,25; orta qiymət 0,25.
Python
import torch
from torch import nn

logits = torch.tensor([[2.0, 0.5, -1.0]])
target = torch.tensor([0])
print(round(nn.CrossEntropyLoss()(logits, target).item(), 4))
pred = torch.tensor([2.5, 0.0])
true = torch.tensor([3.0, -0.5])
print(round(nn.MSELoss()(pred, true).item(), 4))
Gözlənilən nəticə
0.2413
0.25

Optimallaşdırıcı qradiyentlərə əsasən parametrləri yeniləyir. Ən sadəsi torch.optim.SGD-dir: onun step() metodu θ ← θ − η · g düsturunu tətbiq edir. Aşağıdakı kod birinci dərsdəki «bir addım» nümunəsini (x = 2, y = 6, w = 1, η = 0,1) təkrarlayır və eyni w = 2,6 cavabını alır. zero_grad() isə qradiyentləri None-a sıfırlayır.

Python
import torch

w = torch.tensor([1.0], requires_grad=True)
optimizer = torch.optim.SGD([w], lr=0.1)
loss = ((2 * w - 6) ** 2).sum()
loss.backward()
print(w.grad)
optimizer.step()
print(w)
optimizer.zero_grad()
print(w.grad)
Gözlənilən nəticə
tensor([-16.])
tensor([2.6000], requires_grad=True)
None
mₜ = β₁·mₜ₋₁ + (1 − β₁)·gₜ vₜ = β₂·vₜ₋₁ + (1 − β₂)·gₜ² θₜ = θₜ₋₁ − η · m̂ₜ / (√v̂ₜ + ε)mₜ = β₁·mₜ₋₁ + (1 − β₁)·gₜ vₜ = β₂·vₜ₋₁ + (1 − β₂)·gₜ² θₜ = θₜ₋₁ − η · m̂ₜ / (√v̂ₜ + ε)
burada:
  • gₜt addımındakı qradiyent
  • mₜ, vₜqradiyentin və onun kvadratının sürüşən orta qiymətləri (impuls və miqyas)
  • m̂ₜ, v̂ₜbaşlanğıc sürüşməsi düzəldilmiş qiymətlər: mₜ/(1 − β₁ᵗ), vₜ/(1 − β₂ᵗ)
  • β₁, β₂, ε, ηPyTorch-da susmaya görə 0,9; 0,999; 10⁻⁸; 0,001

Adam optimallaşdırıcısı. Hər parametr öz addım ölçüsünü alır: qradiyenti daim böyük olan parametr ehtiyatla, kiçik olan isə daha cəsarətlə yenilənir. SGD-nin impulslu (momentum) variantı isə v ← μ·v + g, θ ← θ − η·v düsturu ilə işləyir.

Öyrətmə dövrü

İndi əsl tapşırığa keçək. İki «aypara» (two moons) formasında nöqtələr yaradırıq: hər sinif 500 nöqtədir və onları düz xətlə ayırmaq mümkün deyil — deməli, qeyri-xətti model lazımdır. Verilənləri 800 öyrədici və 200 test nümunəsinə bölürük.

Python
import math
import torch
from torch import nn
from torch.utils.data import TensorDataset, DataLoader, random_split

torch.manual_seed(42)

def make_moons(n, noise=0.2):
    t = torch.rand(n) * math.pi
    upper = torch.stack([torch.cos(t), torch.sin(t)], dim=1)
    lower = torch.stack([1 - torch.cos(t), 0.5 - torch.sin(t)], dim=1)
    X = torch.cat([upper, lower]) + noise * torch.randn(2 * n, 2)
    y = torch.cat([torch.zeros(n), torch.ones(n)]).long()
    return X, y

X, y = make_moons(500)
train_set, test_set = random_split(TensorDataset(X, y), [800, 200])
train_loader = DataLoader(train_set, batch_size=32, shuffle=True)
test_loader = DataLoader(test_set, batch_size=100)
xb, yb = next(iter(train_loader))
print(len(train_set), len(test_set), len(train_loader))
print(xb.shape, yb.shape, yb.dtype)
Gözlənilən nəticə
800 200 25
torch.Size([32, 2]) torch.Size([32]) torch.int64
800 / 32 = 25 addım bir epoxadır. Nişanlar int64-dür, çünkü CrossEntropyLoss sinif nömrələri gözləyir. Test verilənləri qarışdırılmır.
  1. 1
    Qradiyentləri sıfırla

    optimizer.zero_grad() — əvvəlki addımın qradiyentlərini təmizlə.

  2. 2
    İrəli keçid

    logits = model(xb) — paket üçün proqnozlar.

  3. 3
    İtki

    loss = loss_fn(logits, yb) — bir skalyar.

  4. 4
    Geri keçid

    loss.backward() — bütün parametrlərin qradiyentləri.

  5. 5
    Addım

    optimizer.step() — parametrləri yenilə.

Python
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = nn.Sequential(
    nn.Linear(2, 16), nn.ReLU(),
    nn.Linear(16, 16), nn.ReLU(),
    nn.Linear(16, 2),
).to(device)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

for epoch in range(1, 21):
    model.train()
    total = 0.0
    for xb, yb in train_loader:
        xb, yb = xb.to(device), yb.to(device)
        optimizer.zero_grad()
        loss = loss_fn(model(xb), yb)
        loss.backward()
        optimizer.step()
        total += loss.item() * len(xb)
    if epoch == 1 or epoch % 5 == 0:
        print(f'epoch {epoch:2d}  loss {total / len(train_set):.4f}')
Gözlənilən nəticə
epoch  1  loss 0.4741
epoch  5  loss 0.1200
epoch 10  loss 0.0864
epoch 15  loss 0.0715
epoch 20  loss 0.0823
Əvvəlki kodun davamıdır. Nəticə PyTorch 2.14 ilə CPU-da alınıb; başqa versiyada və ya GPU-da rəqəmlər bir qədər fərqlənə bilər.

İtki ilk epoxalarda sürətlə düşür, sonra 0,07–0,09 ətrafında tərəddüd edir. 20-ci epoxadakı kiçik artım normaldır: mini-paketlər təsadüfi olduğundan addımlar küylüdür. loss.item() * len(xb) hasillərini toplayıb N-ə bölməklə epoxanın dəqiq orta itkisini alırıq (son paket kiçik olsa belə).

Paket ölçüsü B də hiperparametrdir. Böyük paket qradiyenti daha dəqiq qiymətləndirir və GPU-nu daha səmərəli yükləyir, amma daha çox yaddaş tələb edir və bir epoxada daha az yeniləmə verir. Kiçik paket isə küylüdür, lakin bu küy bəzən ümumiləşdirməyə kömək edir. Praktikada 32–256 arası qiymətlərlə başlanır; paketi 2 dəfə böyüdəndə adətən öyrənmə sürətini də bir qədər artırmaq olar.

Real layihələrdə dövrə daha iki element əlavə olunur. Öyrənmə sürətinin cədvəli (scheduler) η-nı zamanla azaldır: məsələn, torch.optim.lr_scheduler.CosineAnnealingLR, hər epoxanın sonunda scheduler.step() çağırılır. Erkən dayandırma (early stopping) isə hər epoxadan sonra validasiya itkisini ölçür, ən yaxşı modeli saxlayır və itki bir neçə epoxa ərzində yaxşılaşmayanda öyrətməni dayandırır — bu, həddən artıq uyğunlaşmaya qarşı ən sadə müdafiədir.

Qiymətləndirmə: eval rejimi və dəqiqlik

Qiymətləndirmədə iki şey vacibdir. model.eval() bəzi qatların davranışını dəyişir: Dropout söndürülür, BatchNorm isə öyrətmə zamanı toplanmış statistikalardan istifadə edir. torch.no_grad() isə qraf qurmağı dayandırır. Bunlar fərqli işlərdir və adətən ikisi birlikdə lazımdır. Təsnifatda əsas metrika dəqiqlikdir:

accuracy = (1/N) · ∑ᵢ [argmax(zᵢ) = yᵢ]accuracy = (1/N) · ∑ᵢ [argmax(zᵢ) = yᵢ]
burada:
  • argmax(zᵢ)i-ci nümunə üçün ən böyük logitin sinfi — proqnoz
  • [ … ]şərt doğrudursa 1, əks halda 0
Python
def accuracy(model, loader):
    model.eval()
    correct = 0
    with torch.no_grad():
        for xb, yb in loader:
            xb, yb = xb.to(device), yb.to(device)
            correct += (model(xb).argmax(dim=1) == yb).sum().item()
    return correct / len(loader.dataset)

print(f'train accuracy: {accuracy(model, train_loader):.3f}')
print(f'test accuracy:  {accuracy(model, test_loader):.3f}')
Gözlənilən nəticə
train accuracy: 0.974
test accuracy:  0.975
Öyrədici və test dəqiqliyi yaxındır — həddən artıq uyğunlaşma yoxdur. Qalan ≈ 2,5% səhv küy səbəbindən iki ayparanın üst-üstə düşdüyü yerlərdədir.
Nümunə 2: addımları və dəqiqliyi hesabla

a) 800 nümunə və paket ölçüsü 64 olanda bir epoxada neçə addım var? 20 epoxada neçə yeniləmə olur?
b) 200 test nümunəsindən 195-i düzgün təsnif olunub. Dəqiqlik nədir?

Həllini göstər
a) 800 / 64 = 12,5 ⇒ ⌈12,5⌉ = 13 addım (sonuncu paketdə 32 nümunə). 20 · 13 = 260 yeniləmə.
b) 195 / 200 = 0,975, yəni 97,5% — məhz bizim modelin nəticəsi.

Modelin saxlanması və yüklənməsi

model.state_dict() — «parametrin adı → tenzor» lüğətidir. Tövsiyə olunan üsul bütün obyekti yox, məhz bu lüğəti saxlamaqdır. Yükləmək üçün eyni arxitekturalı modeli yenidən yaradır, load_state_dict çağırırsan; map_location GPU-da saxlanmış faylı CPU-da açmağa imkan verir.

Python
torch.save(model.state_dict(), 'moons.pt')
restored = nn.Sequential(
    nn.Linear(2, 16), nn.ReLU(),
    nn.Linear(16, 16), nn.ReLU(),
    nn.Linear(16, 2),
).to(device)
restored.load_state_dict(torch.load('moons.pt', map_location=device))
print(list(restored.state_dict().keys()))
print(f'restored test accuracy: {accuracy(restored, test_loader):.3f}')
Gözlənilən nəticə
['0.weight', '0.bias', '2.weight', '2.bias', '4.weight', '4.bias']
restored test accuracy: 0.975

Uzun öyrətməni yarımçıq dayandırıb sonra davam etdirmək üçün checkpoint saxlanır: torch.save({'epoch': epoch, 'model': model.state_dict(), 'optimizer': optimizer.state_dict()}, 'ckpt.pt'). Adam-ın impuls statistikaları da optimallaşdırıcının state_dict-indədir — onsuz davam etdirmə «soyuq başlanğıc» olar.

Əsas fikirlər

  • Dataset nümunələri verir, DataLoader onları paketlərə bölür və qarışdırır; bir epoxada ⌈N / B⌉ addım olur.
  • Reqressiya — MSELoss, K sinifli təsnifat — logitlər və int64 nişanlarla CrossEntropyLoss.
  • Dövr: zero_grad → irəli keçid → itki → backward → step; yeni tapşırıqda Adam (lr = 0,001) yaxşı başlanğıcdır.
  • Qiymətləndirmə model.eval() və torch.no_grad() ilə aparılır; dəqiqlik = düzgün proqnozlar / N.
  • Model state_dict ilə saxlanılır və eyni arxitekturaya load_state_dict ilə yüklənir.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
Öyrətmə addımının düzgün ardıcıllığı hansıdır?