DatasetveDataLoaderile 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_dictile 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.
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())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]
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.
- 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örev | Son katman | Kayıp | Hedefler |
|---|---|---|---|
| Regresyon | nn.Linear(h, 1) | nn.MSELoss | float32 sayılar |
| İkili sınıflandırma | nn.Linear(h, 1) | nn.BCEWithLogitsLoss | 0,0 ya da 1,0 (float32) |
| K sınıflı sınıflandırma | nn.Linear(h, K) | nn.CrossEntropyLoss | sınıf numaraları 0…K−1 (int64) |
- 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.
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Çözümü gizle
L = −2 + 2,2413 = 0,2413.
b) Hatalar −0,5 ve 0,5, kareleri 0,25 ve 0,25; ortalama 0,25.
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))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.
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)tensor([-16.]) tensor([2.6000], requires_grad=True) None
- 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.
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)800 200 25 torch.Size([32, 2]) torch.Size([32]) torch.int64
int64'tür, çünkü CrossEntropyLoss sınıf numaraları bekler. Test verisi karıştırılmaz.- 1Gradyanları sıfırla
optimizer.zero_grad()— önceki adımın gradyanlarını temizle. - 2İleri geçiş
logits = model(xb)— yığın için tahminler. - 3Kayıp
loss = loss_fn(logits, yb)— tek bir skaler. - 4Geri geçiş
loss.backward()— tüm parametrelerin gradyanları. - 5Adım
optimizer.step()— parametreleri güncelle.
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}')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
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):
- argmax(zᵢ)i. örnek için en büyük logitin sınıfı; yani tahmin
- [ … ]koşul doğruysa 1, değilse 0
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}')train accuracy: 0.974 test accuracy: 0.975
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Çözümü gizle
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.
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}')['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,DataLoaderonları 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 veint64etiketlerleCrossEntropyLosskullanı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()vetorch.no_grad()ile yapılır; doğruluk = doğru tahminler / N. - Model
state_dictile kaydedilir ve aynı mimariyeload_state_dictile yüklenir.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.