İçeriğe geç
Educora
Üniversite25 dk40 / 42

PyTorch: eğitim döngüsü

Dataset ve DataLoader, kayıp fonksiyonları, SGD ve Adam optimizasyon araçları, tam eğitim döngüsü, eval modu ve doğruluk, modeli state_dict ile kaydetme; sentetik veride küçük bir sınıflandırıcı eğiterek.

Kendini test et
Bu derste öğreneceklerin
  • Dataset ve DataLoader ile veriyi yığınlara bölmek
  • Göreve uygun bir kayıp fonksiyonu ve optimizasyon aracı seçmek
  • Tam eğitim döngüsünü yazmak ve modeli doğrulama/test verisinde değerlendirmek
  • Modeli state_dict ile kaydetmek ve geri yüklemek

Artık tüm parçalar elimizde: tensörler, autograd ve nn.Module. Şimdi onları uygulayıcıların her gün yazdığı standart tarifte birleştirelim: veri → DataLoader → model → kayıp → optimizasyon aracı → eğitim döngüsü → değerlendirme → kaydetme. İlginçtir ki bu iskelet, küçük bir sınıflandırıcı için de milyarlarca parametreli bir dil modeli için de neredeyse aynıdır; yalnızca verinin ve modelin boyutu değişir.

Dataset ve DataLoader

Dataset, bir örnek listesi gibi davranan bir nesnedir: __len__ örnek sayısını, __getitem__(i) ise i. (x, y) çiftini döndürür. Verin zaten tensörlerdeyse hazır TensorDataset yeterlidir; dosyalardan (görüntüler, metinler) okumak için kendi sınıfını yazarsın. DataLoader ise Dataset'ten yığınlar oluşturur, istenirse her epokta örnekleri karıştırır (shuffle=True) ve num_workers ile veriyi paralel yükleyebilir.

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())
Beklenen çıktı
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 örnek, yığın boyutu 4: iki tam yığın ve 2 örnekli son bir yığın. DataLoader tek tek örnekleri kendisi tensörlere birleştirir.
Tanım
Yığın, adım (iterasyon) ve epok

Yığın (batch), bir güncelleme için birlikte işlenen örnek grubudur. Adım ya da iterasyon, bir yığın üzerinde yapılan tek bir parametre güncellemesidir. Epok, her eğitim örneğinin bir kez “görüldüğü” tam geçiştir. Eğitim genellikle onlarca epok sürer ve her epok birçok adımdan oluşur.

steps per epoch = ⌈N / B⌉ updates = epochs · ⌈N / B⌉steps per epoch = ⌈N / B⌉ updates = epochs · ⌈N / B⌉
burada:
  • Neğitim kümesindeki örnek sayısı
  • Byığın boyutu (batch size)
  • ⌈ ⌉yukarı yuvarlama: son eksik yığın da bir adımdır

Kayıp fonksiyonları ve optimizasyon araçları

GörevSon katmanKayıpHedefler
Regresyonnn.Linear(h, 1)nn.MSELossfloat32 sayılar
İkili sınıflandırmann.Linear(h, 1)nn.BCEWithLogitsLoss0,0 ya da 1,0 (float32)
K sınıflı sınıflandırmann.Linear(h, K)nn.CrossEntropyLosssınıf numaraları 0…K−1 (int64)
L = −zₜ + ln ∑ⱼ₌₁ᴷ eᶻʲ
burada:
  • zmodelin ham logitleri
  • tdoğru sınıfın numarası

nn.CrossEntropyLoss, log-softmax ile çapraz entropiyi birleştirir: −ln softmax(z)ₜ = −zₜ + ln ∑ eᶻʲ. Bir yığın için kayıpların ortalaması alınır.

Örnek 1: logitlerden kayıp

a) Logitler z = (2; 0,5; −1), doğru sınıf 0. CrossEntropyLoss kaçtır?
b) Tahminler (2,5; 0), gerçek değerler (3; −0,5). MSELoss kaçtır?

Çözümü göster
a) e² + √e + e⁻¹ ≈ 7,389 + 1,649 + 0,368 = 9,406; ln 9,406 ≈ 2,2413.
L = −2 + 2,2413 = 0,2413.
b) Hatalar −0,5 ve 0,5, kareleri 0,25 ve 0,25; ortalama 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))
Beklenen çıktı
0.2413
0.25

Optimizasyon aracı (optimizer), parametreleri gradyanlarına göre günceller. En basiti torch.optim.SGD'dir: step() metodu θ ← θ − η · g formülünü uygular. Aşağıdaki kod, ilk dersteki “bir adım” örneğini (x = 2, y = 6, w = 1, η = 0,1) tekrarlar ve aynı w = 2,6 sonucunu bulur. zero_grad() ise gradyanları None'a sıfırlar.

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)
Beklenen çıktı
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 adımındaki gradyan
  • mₜ, vₜgradyanın ve karesinin kayan ortalamaları (momentum ve ölçek)
  • m̂ₜ, v̂ₜbaşlangıç sapması düzeltilmiş değerler: mₜ/(1 − β₁ᵗ), vₜ/(1 − β₂ᵗ)
  • β₁, β₂, ε, ηPyTorch'ta varsayılan 0,9; 0,999; 10⁻⁸; 0,001

Adam optimizasyon aracı. Her parametre kendi adım boyunu alır: gradyanı sürekli büyük olan parametre temkinli, küçük olan daha cesurca güncellenir. Momentumlu SGD ise v ← μ·v + g, θ ← θ − η·v formülüyle çalışır.

Eğitim döngüsü

Şimdi gerçek bir göreve geçelim. İç içe geçmiş iki yarım ay (“two moons”) biçiminde noktalar üretiyoruz: her sınıfta 500 nokta var ve bunlar düz bir çizgiyle ayrılamaz; yani doğrusal olmayan bir model gerekir. Veriyi 800 eğitim ve 200 test örneğine bölüyoruz.

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)
Beklenen çıktı
800 200 25
torch.Size([32, 2]) torch.Size([32]) torch.int64
800 / 32 = epok başına 25 adım. Etiketler int64'tür, çünkü CrossEntropyLoss sınıf numaraları bekler. Test verisi karıştırılmaz.
  1. 1
    Gradyanları sıfırla

    optimizer.zero_grad() — önceki adımın gradyanlarını temizle.

  2. 2
    İleri geçiş

    logits = model(xb) — yığın için tahminler.

  3. 3
    Kayıp

    loss = loss_fn(logits, yb) — tek bir skaler.

  4. 4
    Geri geçiş

    loss.backward() — tüm parametrelerin gradyanları.

  5. 5
    Adım

    optimizer.step() — parametreleri güncelle.

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}')
Beklenen çıktı
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
Önceki kodun devamıdır. Çıktı PyTorch 2.14 ile CPU'da alındı; başka bir sürümde ya da GPU'da sayılar biraz farklı olabilir.

Kayıp ilk epoklarda hızla düşer, sonra 0,07–0,09 civarında dalgalanır. 20. epoktaki küçük artış normaldir: mini yığınlar rastgele olduğu için adımlar gürültülüdür. loss.item() * len(xb) çarpımlarını toplayıp N'ye bölmek, epokun tam ortalama kaybını verir (son yığın daha küçük olsa bile).

Yığın boyutu B de bir hiperparametredir. Büyük bir yığın gradyanı daha doğru tahmin eder ve GPU'yu daha verimli kullanır ama daha çok bellek ister ve epok başına daha az güncelleme verir. Küçük bir yığın gürültülüdür, ancak bu gürültü bazen genellemeye yardımcı olur. Uygulamada 32 ile 256 arasındaki değerlerle başlanır; yığını iki katına çıkardığında öğrenme oranını da genellikle biraz artırabilirsin.

Gerçek projelerde döngüye iki öğe daha eklenir. Öğrenme oranı zamanlayıcısı (scheduler) η'yı zamanla azaltır: örneğin torch.optim.lr_scheduler.CosineAnnealingLR; her epokun sonunda scheduler.step() çağrılır. Erken durdurma (early stopping) her epoktan sonra doğrulama kaybını ölçer, en iyi modeli saklar ve kayıp birkaç epok boyunca iyileşmezse eğitimi durdurur; aşırı öğrenmeye karşı en basit savunmadır.

Değerlendirme: eval modu ve doğruluk

Değerlendirmede iki şey önemlidir. model.eval() bazı katmanların davranışını değiştirir: Dropout kapatılır, BatchNorm ise eğitim sırasında toplanan istatistikleri kullanır. torch.no_grad() graf kurmayı durdurur. Bunlar farklı işlerdir ve genellikle ikisi birlikte gerekir. Sınıflandırmada temel ölçüt doğruluktur (accuracy):

accuracy = (1/N) · ∑ᵢ [argmax(zᵢ) = yᵢ]accuracy = (1/N) · ∑ᵢ [argmax(zᵢ) = yᵢ]
burada:
  • argmax(zᵢ)i. örnek için en büyük logitin sınıfı; yani tahmin
  • [ … ]koşul doğruysa 1, değilse 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}')
Beklenen çıktı
train accuracy: 0.974
test accuracy:  0.975
Eğitim ve test doğruluğu birbirine yakın; aşırı öğrenme yok. Kalan ≈ %2,5 hata, gürültü yüzünden iki ayın üst üste bindiği yerlerdedir.
Örnek 2: adımları ve doğruluğu hesapla

a) 800 örnek ve 64'lük yığın boyutuyla bir epokta kaç adım vardır? 20 epokta kaç güncelleme yapılır?
b) 200 test örneğinden 195'i doğru sınıflandırıldı. Doğruluk kaçtır?

Çözümü göster
a) 800 / 64 = 12,5 ⇒ ⌈12,5⌉ = 13 adım (son yığında 32 örnek var). 20 · 13 = 260 güncelleme.
b) 195 / 200 = 0,975, yani %97,5; tam olarak modelimizin sonucu.

Modeli kaydetmek ve yüklemek

model.state_dict(), “parametre adı → tensör” sözlüğüdür. Önerilen yol, nesnenin tamamını değil bu sözlüğü kaydetmektir. Yüklemek için aynı mimaride bir modeli yeniden oluşturur ve load_state_dict çağırırsın; map_location, GPU'da kaydedilmiş bir dosyayı CPU'da açmanı sağlar.

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}')
Beklenen çıktı
['0.weight', '0.bias', '2.weight', '2.bias', '4.weight', '4.bias']
restored test accuracy: 0.975

Uzun bir eğitimi durdurup sonra sürdürmek için bir checkpoint kaydedilir: torch.save({'epoch': epoch, 'model': model.state_dict(), 'optimizer': optimizer.state_dict()}, 'ckpt.pt'). Adam'ın momentum istatistikleri de optimizasyon aracının state_dict'indedir; onlar olmadan sürdürmek “soğuk başlangıç” olur.

Önemli noktalar

  • Dataset örnekleri verir, DataLoader onları yığınlara böler ve karıştırır; bir epokta ⌈N / B⌉ adım vardır.
  • Regresyon MSELoss, K sınıflı sınıflandırma ise logitler ve int64 etiketlerle CrossEntropyLoss kullanır.
  • Döngü: zero_grad → ileri geçiş → kayıp → backward → step; yeni bir görevde Adam (lr = 0,001) iyi bir başlangıçtır.
  • Değerlendirme model.eval() ve torch.no_grad() ile yapılır; doğruluk = doğru tahminler / N.
  • Model state_dict ile kaydedilir ve aynı mimariye load_state_dict ile yüklenir.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Bir eğitim adımının doğru sırası hangisidir?