00 Tensör Dünyası
PyTorch tensörü, NumPy array'inin autograd ve GPU desteğiyle donatılmış halidir.
NumPy, bilimsel hesaplamanın temel taşıdır ve çok boyutlu dizi işlemleri için son derece güçlüdür. Ancak derin öğrenme için kritik olan iki özelliği yoktur: GPU üzerinde hesaplama ve otomatik türev (autograd). PyTorch Tensor, bu iki özelliği ndarray benzeri bir API ile sunar. Bir tensör oluşturulduğunda, hesaplama grafiği otomatik olarak izlenebilir ve istenen herhangi bir değere göre gradyan hesaplanabilir. Bu, modelin parametrelerini güncellemek için gereken türevleri elle hesaplamaktan kurtarır.
Tensörlerin dtype'ı hesaplama hassasiyetini ve bellek kullanımını belirler. torch.float32 standart eğitim tipidir; her eleman 4 byte kaplar. torch.float16 ve torch.bfloat16 mixed precision eğitimde kullanılır, belleği yarıya indirir. torch.int64 token indeksleri ve sınıf etiketleri için tercih edilir. Yanlış dtype sessiz hataya yol açabilir; örneğin integer tensörü ile float tensörü toplanmaya çalışıldığında PyTorch hata verir.
Şekil işlemleri derin öğrenmede sürekli kullanılır. reshape ve view aynı veriyi farklı boyutlarda gösterir ancak view contiguous bellek gerektirir. transpose ve permute eksenleri değiştirir. squeeze boyutu 1 olan eksenleri kaldırır, unsqueeze yeni bir boyut ekler. Bu işlemler verinin kopyasını oluşturmaz; yalnızca bellek görünümünü değiştirir.
import torch
import numpy as np
# ─── Tensör oluşturma ────────────────────────────────────────
a = torch.zeros(3, 4) # sıfır tensörü
b = torch.ones(2, 3, dtype=torch.float32)
c = torch.randn(4, 8) # N(0,1) dağılımı
d = torch.arange(0, 10, step=2) # [0, 2, 4, 6, 8]
e = torch.linspace(0.0, 1.0, steps=5) # [0, 0.25, 0.5, 0.75, 1.0]
f = torch.tensor([[1, 2], [3, 4]]) # Python listesinden
# ─── NumPy ↔ Tensor dönüşümü (bellek paylaşımı) ─────────────
arr = np.random.randn(4, 4)
t = torch.from_numpy(arr) # kopyalamaz, aynı bellek
arr2 = t.numpy() # geri dönüşüm (CPU'da çalışır)
# ─── Dtype işlemleri ─────────────────────────────────────────
x = torch.randn(4, dtype=torch.float32)
x_half = x.half() # float16
x_bfloat = x.bfloat16() # bfloat16 (LLM eğitiminde yaygın)
x_int = x.long() # int64
# ─── Şekil işlemleri ─────────────────────────────────────────
x = torch.randn(2, 3, 4)
print(f"Şekil : {x.shape}") # [2, 3, 4]
print(f"Toplam eleman : {x.numel()}") # 24
y = x.reshape(6, 4) # (6, 4)
z = x.transpose(0, 2) # (4, 3, 2)
p = x.permute(2, 0, 1) # (4, 2, 3)
# squeeze / unsqueeze
v = torch.randn(1, 8, 1)
print(f"Squeeze : {v.squeeze().shape}") # (8,)
print(f"Unsqueeze dim1 : {v.unsqueeze(1).shape}") # (1, 1, 8, 1)
# Matris çarpımı — üç eşdeğer yol
A = torch.randn(4, 8)
B = torch.randn(8, 6)
C1 = torch.mm(A, B) # 2D matris çarpımı
C2 = torch.matmul(A, B) # genel — broadcasting destekler
C3 = A @ B # @ operatörü
print(f"Matmul sonucu: {C1.shape}") # (4, 6)
view yalnızca contiguous tensörlerde çalışır. transpose veya permute sonrasında tensör contiguous olmayabilir; bu durumda .contiguous().view(...) ya da doğrudan .reshape(...) kullanın.
01 Autograd — Otomatik Türev
PyTorch'un autograd motoru, ileri hesaplama sırasında bir hesaplama grafiği inşa eder ve backward() çağrısıyla bu grafik üzerinden gradyanları otomatik hesaplar.
Hesaplama grafiği (computational graph), her tensör işlemini bir düğüm olarak temsil eder. requires_grad=True olan bir tensörle yapılan her işlem, bu grafiğe otomatik olarak eklenir. loss.backward() çağrıldığında PyTorch, zincir kuralı (chain rule) ile bu grafik üzerinden geriye doğru türevler hesaplar. Her yaprak tensörün .grad attribute'u, loss'un o tensöre göre kısmi türevini içerir.
Gradyan akışını kontrol etmek için iki önemli araç vardır. torch.no_grad() context manager, bu blok içindeki tüm işlemlerin hesaplama grafına eklenmesini engeller; inference ve validation sırasında bellek ve hesaplama tasarrufu sağlar. .detach() metodu bir tensörü hesaplama grafından koparır; gradyanın bu tensör üzerinden akmamasını sağlar, özellikle GAN eğitimi veya feature extractor dondurmada kullanılır.
Autograd'ın nasıl çalıştığını anlamak hata ayıklamada kritiktir. Eğer bir loss'un .grad_fn attribute'u None ise, hesaplama grafiği oluşmamış demektir ve backward() gradyan üretmez. Bu durum genellikle NumPy arayüzüne geçiş yapıp geri dönerken veya detach() yanlış kullanıldığında oluşur.
import torch
# ─── Basit quadratic: f(x) = 3x² + 2x + 1, f'(x) = 6x + 2 ─
x = torch.tensor(2.0, requires_grad=True)
y = 3 * x**2 + 2 * x + 1
y.backward()
print(f"x=2 için f'(x) = {x.grad}") # 6*2 + 2 = 14.0
# ─── Çok değişkenli örnek ────────────────────────────────────
a = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
b = torch.tensor([4.0, 5.0, 6.0], requires_grad=True)
loss = (a * b).sum() # loss = a·b = 4 + 10 + 18 = 32
loss.backward()
print(f"∂loss/∂a = {a.grad}") # b değerleri: [4, 5, 6]
print(f"∂loss/∂b = {b.grad}") # a değerleri: [1, 2, 3]
# ─── no_grad — inference modu ────────────────────────────────
w = torch.randn(4, 4, requires_grad=True)
with torch.no_grad():
out = w * 2 # hesaplama grafiği oluşmaz
print(f"requires_grad: {out.requires_grad}") # False
# ─── detach — gradyan akışını kes ───────────────────────────
encoder_out = torch.randn(4, 64, requires_grad=True)
frozen = encoder_out.detach() # encoder'dan gradyan akmaz
loss2 = (frozen * torch.randn(4, 64)).sum()
loss2.backward()
print(f"encoder_out.grad: {encoder_out.grad}") # None — gradyan akmadı
# ─── Manuel gradient vs autograd karşılaştırması ────────────
# f(x) = sigmoid(x), f'(x) = sigmoid(x) * (1 - sigmoid(x))
x = torch.tensor(0.5, requires_grad=True)
sig = torch.sigmoid(x)
sig.backward()
auto_grad = x.grad.item()
manual_grad = sig.item() * (1 - sig.item())
print(f"Autograd : {auto_grad:.6f}") # 0.235004
print(f"Manuel : {manual_grad:.6f}") # 0.235004
Optimizer adımından önce optimizer.zero_grad() çağrılmazsa gradyanlar birikir. Bu bazen kasıtlı (gradient accumulation) ama genellikle hatadır. Modern kodda optimizer.zero_grad(set_to_none=True) kullanmak daha verimlidir çünkü .grad tensor'larını serbest bırakır.
02 nn.Module — Katman Tanımı
nn.Module, PyTorch'ta tüm katman ve modellerin temel sınıfıdır; parametre yönetimi, alt modül takibi ve cihaz transferi otomatik sağlanır.
nn.Module'den türetilen her sınıf iki metod tanımlamalıdır: __init__ ve forward. __init__ içinde kullanılacak alt katmanlar ve parametreler tanımlanır; PyTorch bunları otomatik olarak kaydeder. forward ileri hesaplama mantığını barındırır. Modeli bir fonksiyon gibi çağırdığınızda (model(x)), Python aslında __call__'u çalıştırır; bu da hook'ları tetikler ve ardından forward'ı çağırır.
Parameter ve buffer ayrımı önemlidir. nn.Parameter, gradyanı hesaplanan ve optimizer tarafından güncellenen bir tensördür. register_buffer ile kaydedilen tensörler ise gradyan tutmaz ancak modelin durumunun parçasıdır (state_dict'e dahil olur) ve .to(device) ile cihaz değiştirildiğinde otomatik taşınırlar. Positional encoding tabloları tipik olarak buffer olarak kaydedilir.
model.parameters() tüm öğrenilecek parametreleri döner ve optimizer'a verilir. model.named_parameters() isimlerle birlikte döner; hangi katmanın ne kadar parametre içerdiğini incelemek veya belirli katmanları dondurmak için kullanılır. model.state_dict() hem parametreleri hem buffer'ları içeren tam model durumunu döner.
import torch
import torch.nn as nn
class CustomLinear(nn.Module):
"""Manuel Linear katman — nn.Linear'ı taklit eder."""
def __init__(self, in_features: int, out_features: int):
super().__init__()
# nn.Parameter: gradyan hesaplanır, optimizer günceller
self.weight = nn.Parameter(torch.randn(out_features, in_features) * 0.02)
self.bias = nn.Parameter(torch.zeros(out_features))
def forward(self, x):
return x @ self.weight.T + self.bias
class SmallMLP(nn.Module):
"""3 katmanlı MLP + buffer örneği."""
def __init__(self, in_dim, hidden, out_dim):
super().__init__()
self.fc1 = nn.Linear(in_dim, hidden)
self.fc2 = nn.Linear(hidden, hidden)
self.fc3 = nn.Linear(hidden, out_dim)
self.act = nn.GELU()
self.drop = nn.Dropout(0.1)
# Buffer: gradyan yok, state_dict'te var, .to(device) ile taşınır
self.register_buffer('running_mean', torch.zeros(hidden))
def forward(self, x):
x = self.act(self.fc1(x))
x = self.drop(x)
x = self.act(self.fc2(x))
x = self.fc3(x)
return x
# ─── Parametre inceleme ──────────────────────────────────────
model = SmallMLP(64, 256, 10)
print("=== Parametre Tablosu ===")
total = 0
for name, param in model.named_parameters():
print(f" {name:30s} {str(param.shape):20s} {param.numel():>8,}")
total += param.numel()
print(f"Toplam: {total:,}")
# ─── Belirli katmanları dondurma ─────────────────────────────
for param in model.fc1.parameters():
param.requires_grad = False # fc1 donduruldu
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"Eğitilebilir parametre: {trainable:,}")
# ─── state_dict inceleme ─────────────────────────────────────
sd = model.state_dict()
for k, v in sd.items():
print(f" {k:35s} {str(v.shape)}")
03 Aktivasyon Fonksiyonları
Aktivasyon fonksiyonları doğrusal dönüşümlere doğrusal olmayan kabiliyetler ekler; doğru seçim modelin öğrenme hızını ve kapasitesini etkiler.
Aktivasyon fonksiyonu olmadan, kaç katman eklenirse eklensin sinir ağı tek bir lineer dönüşüme indirgenir. ReLU (Rectified Linear Unit), max(0, x) formülüyle negatif değerleri sıfırlar. Hesaplama açısından son derece ucuzdur ve derin ağları pratik hale getiren aktivasyondur. Ancak "dying ReLU" problemi vardır: eğer bir nöronun ağırlıkları öyle güncellenir ki her giriş için negatif değer üretiyorsa, bu nöronun gradyanı sürekli sıfır olur ve bir daha güncellenmez.
GELU (Gaussian Error Linear Unit), modern transformer modellerinin tercihidir. x · Φ(x) formülündedir (Φ standart normal CDF). ReLU'dan farklı olarak sıfırda yumuşak bir geçiş yapar; negatif değerleri sıfırlamak yerine küçük negatif bir değer geçirir. Bu yumuşak gate davranışı öğrenmeyi zenginleştirir. Sigmoid ve Tanh doyum (saturation) problemi nedeniyle modern derin ağlarda gizli katmanlarda artık kullanılmaz; sigmoid yalnızca binary çıktı katmanlarında, tanh bazı RNN/LSTM hücrelerinde yer alır.
Softmax, çok sınıflı sınıflandırmada çıktıları olasılık dağılımına dönüştürür. softmax(x_i) = exp(x_i) / Σ exp(x_j). Sayısal stabilite için pratikte F.log_softmax + F.nll_loss yerine F.cross_entropy kullanılır çünkü ikincisi log-sum-exp trick ile daha stabil hesaplama yapar.
import torch
import torch.nn as nn
import torch.nn.functional as F
x = torch.linspace(-3.0, 3.0, steps=7)
print(f"Giriş: {x.numpy().round(2)}")
# ReLU: max(0, x)
relu = F.relu(x)
print(f"ReLU : {relu.numpy().round(3)}")
# Leaky ReLU: negatifler için küçük eğim
leaky = F.leaky_relu(x, negative_slope=0.01)
print(f"Leaky ReLU : {leaky.numpy().round(3)}")
# GELU: smooth gate
gelu = F.gelu(x)
print(f"GELU : {gelu.numpy().round(3)}")
# Sigmoid: çıktı [0,1]
sigmoid = torch.sigmoid(x)
print(f"Sigmoid : {sigmoid.numpy().round(3)}")
# Tanh: çıktı [-1,1]
tanh = torch.tanh(x)
print(f"Tanh : {tanh.numpy().round(3)}")
# Softmax: olasılık dağılımı (son boyut üzerinde)
logits = torch.tensor([2.0, 1.0, 0.5])
probs = F.softmax(logits, dim=0)
print(f"Softmax : {probs.numpy().round(4)}") # toplamı 1.0
# ─── Dying ReLU demonstrasyonu ───────────────────────────────
class DyingReLUDemo(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(4, 4)
# Tüm ağırlıkları negatif yap — nöronları öldür
nn.init.constant_(self.fc.weight, -1.0)
nn.init.constant_(self.fc.bias, -1.0)
def forward(self, x):
return F.relu(self.fc(x))
demo = DyingReLUDemo()
x_pos = torch.rand(8, 4)
out = demo(x_pos)
print(f"Ölmüş ReLU çıktısı (hepsi 0): {out.unique()}") # tensor([0.])
| Fonksiyon | Aralık | Avantaj | Dezavantaj | Kullanım |
|---|---|---|---|---|
| ReLU | [0, ∞) | Hızlı, basit | Dying ReLU | CNN, genel MLP |
| Leaky ReLU | (-∞, ∞) | Dying ReLU yok | Negatif eğim ayarı | GAN discriminator |
| GELU | ~(-0.17, ∞) | Yumuşak, stabil | Hesaplama ağır | Transformer, BERT, GPT |
| Sigmoid | (0, 1) | Olasılık çıktısı | Vanishing gradient | Binary çıktı katmanı |
| Tanh | (-1, 1) | Sıfır merkezli | Doyum sorunu | LSTM hücre durumu |
| Softmax | (0, 1), Σ=1 | Olasılık dağılımı | Büyük logit'lerde overflow | Çıktı katmanı (çok sınıf) |
04 Loss Fonksiyonları
Loss fonksiyonu, modelin tahminleri ile gerçek değerler arasındaki farkı ölçer; eğitim sinyalinin tamamı buradan gelir.
CrossEntropyLoss, çok sınıflı sınıflandırmanın standart loss fonksiyonudur. PyTorch'un nn.CrossEntropyLoss uygulaması raw logit'leri girdi olarak bekler ve dahili olarak log-softmax ile negatif log-likelihood hesaplar. Bu birleşik implementasyon sayısal açıdan daha stabil ve daha hızlıdır. Giriş boyutu (N, C) veya (N, C, *) formatındadır; C sınıf sayısıdır.
MSELoss (Mean Squared Error), regresyon görevlerinin standart loss fonksiyonudur. L = (1/N) Σ (y_pred - y_true)². Gürültülü veya aykırı değer içeren verilerde MAELoss (L1Loss) daha sağlamdır çünkü hata karesi almak büyük sapmaları aşırı cezalandırır. Hybrid çözüm olarak HuberLoss (SmoothL1Loss) küçük hatalarda L2, büyük hatalarda L1 gibi davranır.
BCELoss (Binary Cross Entropy) ikili sınıflandırma içindir ve sigmoid çıktı bekler. BCEWithLogitsLoss daha tercih edilen versiyondur: hem sigmoid hem de binary cross entropy'yi birleştirerek sayısal stabilite sağlar. Custom loss fonksiyonu tanımlamak için nn.Module'den türeten bir sınıf yazılır; autograd otomatik türev hesaplar.
import torch
import torch.nn as nn
import torch.nn.functional as F
# ─── CrossEntropyLoss — sınıflandırma ───────────────────────
criterion_ce = nn.CrossEntropyLoss(reduction='mean')
logits = torch.randn(8, 10) # 8 örnek, 10 sınıf
targets = torch.randint(0, 10, (8,))
loss_ce = criterion_ce(logits, targets)
print(f"CrossEntropy: {loss_ce.item():.4f}")
# Label smoothing — overfitting'i önler
criterion_smooth = nn.CrossEntropyLoss(label_smoothing=0.1)
loss_smooth = criterion_smooth(logits, targets)
print(f"Smooth CE : {loss_smooth.item():.4f}")
# ─── MSELoss — regresyon ─────────────────────────────────────
mse = nn.MSELoss()
mae = nn.L1Loss()
huber = nn.HuberLoss(delta=1.0)
pred = torch.randn(16)
target = torch.randn(16)
print(f"MSE : {mse(pred, target).item():.4f}")
print(f"MAE : {mae(pred, target).item():.4f}")
print(f"Huber: {huber(pred, target).item():.4f}")
# ─── BCEWithLogitsLoss — binary sınıflandırma ───────────────
bce = nn.BCEWithLogitsLoss()
bin_logits = torch.randn(16)
bin_targets = torch.randint(0, 2, (16,)).float()
print(f"BCE : {bce(bin_logits, bin_targets).item():.4f}")
# ─── Custom loss: Focal Loss ─────────────────────────────────
class FocalLoss(nn.Module):
"""Dengesiz sınıflar için — kolay örnekleri küçültür."""
def __init__(self, alpha: float = 1.0, gamma: float = 2.0):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, logits, targets):
ce_loss = F.cross_entropy(logits, targets, reduction='none')
pt = torch.exp(-ce_loss) # p(doğru sınıf)
focal = self.alpha * ((1 - pt) ** self.gamma) * ce_loss
return focal.mean()
fl = FocalLoss(gamma=2.0)
print(f"Focal: {fl(logits, targets).item():.4f}")
reduction parametresi 'mean' (batch ortalaması), 'sum' (toplam) veya 'none' (her örnek ayrı) alabilir. Gradient accumulation'da öğrenme hızını sabit tutmak için batch boyutuna bağlı olarak 'mean' tercih edilir.
05 Optimizerlar
Optimizer, loss gradyanlarını kullanarak model parametrelerini günceller; seçilen algoritma ve hiperparametreler eğitim hızını ve nihai performansı doğrudan etkiler.
SGD (Stochastic Gradient Descent), en temel optimizerdir: θ = θ - lr · ∇θ. momentum parametresi geçmiş gradyanların ağırlıklı ortalamasını kullanarak güncellemeleri pürüzsüzleştirir ve yerel minimumları aşmaya yardımcı olur. weight_decay L2 regularizasyon ekler. SGD derin öğrenmede hâlâ rekabetçidir; özellikle görüntü sınıflandırma benchmark'larında iyi ayarlanmış SGD çoğu zaman Adam'ı geçer.
Adam (Adaptive Moment Estimation), her parametre için ayrı öğrenme hızı uyarlaması yapan bir optimizerdir. Birinci moment (gradyanların ortalaması) ve ikinci moment (gradyanların karesinin ortalaması) tutulur. Adam genellikle varsayılan hiperparametrelerle (lr=1e-3, β₁=0.9, β₂=0.999) iyi çalışır ve az hiperparametre ayarı gerektirdiğinden araştırma ve prototipleme için tercih edilir. AdamW, Adam'ın weight decay implementasyonunu düzelten versiyonudur — transformer modellerinin neredeyse tamamı AdamW kullanır.
Learning rate scheduler, eğitim boyunca öğrenme hızını dinamik olarak değiştirir. CosineAnnealingLR öğrenme hızını kosinüs eğrisiyle kademeli olarak sıfıra indirir ve genellikle en iyi sonucu verir. ReduceLROnPlateau validation loss iyileşmediğinde öğrenme hızını düşürür; bu adaptif yaklaşım kaç epoch'ta ne kadar düşürüleceğini belirlemek zor olduğunda kullanışlıdır. Transformer modelleri genellikle lineer warm-up ardından cosine decay kullanır.
import torch
import torch.nn as nn
import torch.optim as optim
# ─── Model ───────────────────────────────────────────────────
model = nn.Sequential(nn.Linear(64, 128), nn.GELU(), nn.Linear(128, 10))
# ─── SGD + momentum ──────────────────────────────────────────
sgd = optim.SGD(
model.parameters(),
lr=0.01, momentum=0.9, weight_decay=1e-4
)
# ─── Adam ────────────────────────────────────────────────────
adam = optim.Adam(
model.parameters(),
lr=1e-3, betas=(0.9, 0.999), eps=1e-8, weight_decay=0.0
)
# ─── AdamW (transformer modelleri için standart) ─────────────
# Weight decay sadece ağırlıklara uygulanır (bias ve norm parametrelerine değil)
no_decay = ['bias', 'LayerNorm.weight', 'norm']
param_groups = [
{'params': [p for n, p in model.named_parameters()
if not any(nd in n for nd in no_decay)],
'weight_decay': 0.01},
{'params': [p for n, p in model.named_parameters()
if any(nd in n for nd in no_decay)],
'weight_decay': 0.0},
]
adamw = optim.AdamW(param_groups, lr=3e-4)
# ─── Scheduler'lar ───────────────────────────────────────────
num_epochs = 50
# StepLR: her N epoch'ta lr'yi gamma ile çarp
step_sched = optim.lr_scheduler.StepLR(adamw, step_size=10, gamma=0.5)
# CosineAnnealingLR: kosinüs eğrisiyle T_max epoch sonra lr→lr_min
cos_sched = optim.lr_scheduler.CosineAnnealingLR(
adamw, T_max=num_epochs, eta_min=1e-6
)
# ReduceLROnPlateau: metrik iyileşmediğinde lr düşür
plateau_sched = optim.lr_scheduler.ReduceLROnPlateau(
adamw, mode='min', factor=0.5, patience=5, verbose=True
)
# ─── Lineer warm-up + cosine decay ──────────────────────────
def get_lr_with_warmup(step, warmup_steps, total_steps, base_lr):
if step < warmup_steps:
return base_lr * step / warmup_steps
progress = (step - warmup_steps) / (total_steps - warmup_steps)
import math
return base_lr * 0.5 * (1.0 + math.cos(math.pi * progress))
# LambdaLR ile sarma
total_steps = 1000
warmup_steps = 100
lambda_sched = optim.lr_scheduler.LambdaLR(
adamw,
lambda step: get_lr_with_warmup(step, warmup_steps, total_steps, 1.0)
)
# ─── Optimizer loop örneği ───────────────────────────────────
x = torch.randn(16, 64)
y = torch.randint(0, 10, (16,))
criterion = nn.CrossEntropyLoss()
for step in range(3):
adamw.zero_grad(set_to_none=True)
loss = criterion(model(x), y)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # gradient clipping
adamw.step()
lambda_sched.step()
print(f"Step {step}: loss={loss.item():.4f}, lr={lambda_sched.get_last_lr()[0]:.6f}")
Gradient clipping (nn.utils.clip_grad_norm_) transformer eğitiminde önemlidir. Norm eşiği genellikle 1.0 seçilir. Clipping olmadan büyük gradyanlar parametre güncellemelerini patlatabilir; bu özellikle eğitimin ilk epoch'larında kritiktir.
06 DataLoader ve Dataset
PyTorch'un veri boru hattı soyutlaması, veri yükleme, ön işleme ve batch oluşturma işlemlerini eğitim döngüsünden ayırarak yönetilebilir ve verimli kılar.
torch.utils.data.Dataset, özel veri setlerini tanımlamak için kullanılan soyut sınıftır. İki metod uygulanmalıdır: __len__ veri setinin boyutunu döner, __getitem__ verilen indeksteki örneği döner. Bu yapı sayesinde DataLoader, paralel veri yükleme ve batch oluşturmayı otomatik halleder. Büyük veri setleri için tüm veriyi belleğe yüklemek yerine __getitem__ içinde tek örnekler okunabilir.
DataLoader, Dataset'i bir döngüde kolayca kullanmak için wrapper sağlar. batch_size kaç örneğin gruplanacağını belirler. shuffle=True her epoch başında veri karıştırır; bu, modelin belirli bir sırayı ezberlemesini engeller. num_workers paralel CPU işçisi sayısını belirler; disk I/O darboğazını azaltır. pin_memory=True, GPU transferini hızlandırmak için verileri kilitlenmiş belleğe yükler.
torchvision.transforms görüntü veri setleri için ön işleme pipeline'ı sağlar. Compose ile birden fazla dönüşüm zincirlenir. Normalize, RandomCrop, RandomHorizontalFlip gibi dönüşümler veri zenginleştirme (data augmentation) için kullanılır. NLP için tokenizasyon ve padding işlemleri DataLoader içinde collate_fn ile özelleştirilebilir.
import torch
from torch.utils.data import Dataset, DataLoader
import torch.nn as nn
# ─── Özel Dataset tanımı ─────────────────────────────────────
class SyntheticClassificationDataset(Dataset):
"""Sentetik 2 sınıflı veri seti."""
def __init__(self, n_samples: int = 1000, n_features: int = 20):
self.n_samples = n_samples
# Tüm veriyi belleğe yükle (küçük veri setleri için uygun)
self.X = torch.randn(n_samples, n_features)
self.y = torch.randint(0, 2, (n_samples,))
def __len__(self):
return self.n_samples
def __getitem__(self, idx):
return self.X[idx], self.y[idx] # (özellik vektörü, etiket)
# ─── NLP için custom Dataset ─────────────────────────────────
class TextDataset(Dataset):
"""Token ID dizilerini batch'e dönüştüren dataset."""
def __init__(self, token_ids, block_size: int = 128):
self.data = token_ids
self.block_size = block_size
def __len__(self):
return len(self.data) - self.block_size
def __getitem__(self, idx):
chunk = self.data[idx : idx + self.block_size + 1]
x = chunk[:-1] # giriş: ilk block_size token
y = chunk[1:] # hedef: kaydırılmış dizi
return x, y
# ─── DataLoader kurulumu ─────────────────────────────────────
dataset = SyntheticClassificationDataset(n_samples=5000)
train_size = int(0.8 * len(dataset))
val_size = len(dataset) - train_size
train_ds, val_ds = torch.utils.data.random_split(dataset, [train_size, val_size])
train_loader = DataLoader(
train_ds,
batch_size = 64,
shuffle = True,
num_workers = 0, # Windows için 0 önerilir; Linux'ta 4-8
pin_memory = False, # GPU varsa True yap
drop_last = True, # son küçük batch'i at (batch norm için önemli)
)
val_loader = DataLoader(val_ds, batch_size=128, shuffle=False)
# ─── Döngü örneği ────────────────────────────────────────────
for batch_idx, (X_batch, y_batch) in enumerate(train_loader):
print(f"Batch {batch_idx}: X={X_batch.shape}, y={y_batch.shape}")
if batch_idx >= 2:
break
# ─── Custom collate_fn (değişken uzunluk için) ───────────────
def collate_variable_length(batch):
"""Farklı uzunluktaki sekansları padding ile hizala."""
seqs, labels = zip(*batch)
lengths = [s.size(0) for s in seqs]
max_len = max(lengths)
padded = torch.zeros(len(seqs), max_len, dtype=torch.long)
for i, (seq, l) in enumerate(zip(seqs, lengths)):
padded[i, :l] = seq
return padded, torch.tensor(labels)
print(f"Eğitim batch sayısı : {len(train_loader)}")
print(f"Doğrulama batch sayısı : {len(val_loader)}")
07 Eğitim Döngüsü
Standart eğitim döngüsü zero_grad, forward pass, loss hesaplama, backward ve optimizer adımından oluşur; train/eval modları dropout ve batch norm davranışını değiştirir.
model.train() ve model.eval() modları kritik öneme sahiptir. Train modunda nn.Dropout nöronları rastgele kapatır ve nn.BatchNorm batch istatistiklerini kullanır. Eval modunda Dropout devre dışı kalır ve BatchNorm running statistics kullanır. Bu iki modu doğru yönetmemek hem train hem de validation metriklerini bozar. Validation döngüsü her zaman torch.no_grad() içinde çalıştırılmalıdır; aksi takdirde gereksiz hesaplama grafı birikir ve bellek dolabilir.
Tipik bir epoch döngüsü şu adımlardan oluşur: DataLoader'dan bir batch al, optimizer.zero_grad() ile önceki gradyanları temizle, modelden forward pass yap, loss hesapla, loss.backward() ile gradyanları hesapla, gradient clipping uygula, optimizer.step() ile parametreleri güncelle, scheduler adımını at. Her adım sırayla gerçekleşmeli; özellikle zero_grad'ın backward'dan önce gelmesi unutulmamalıdır.
Metrik takibi için PyTorch, Python'un yerleşik araçlarıyla çalışır. Her batch'te loss değerleri biriktirilir ve epoch sonunda ortalama alınır. Karmaşık metrikler (F1, ROC-AUC gibi) için torchmetrics kütüphanesi uygundur. Eğitim ilerlemesini izlemek için tqdm kullanımı sık görülür; WandB veya TensorBoard ile metrikler görselleştirilebilir.
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
# ─── Model, veri, optimizer ──────────────────────────────────
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = nn.Sequential(
nn.Linear(32, 128), nn.GELU(), nn.Dropout(0.1),
nn.Linear(128, 64), nn.GELU(), nn.Dropout(0.1),
nn.Linear(64, 5),
).to(device)
X = torch.randn(2000, 32)
y = torch.randint(0, 5, (2000,))
ds = TensorDataset(X, y)
train_ds, val_ds = torch.utils.data.random_split(ds, [1600, 400])
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=128)
optimizer = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)
criterion = nn.CrossEntropyLoss()
def train_epoch(model, loader, optimizer, criterion, device):
model.train() # dropout açık
total_loss, correct, total = 0.0, 0, 0
for X_b, y_b in loader:
X_b, y_b = X_b.to(device), y_b.to(device)
optimizer.zero_grad(set_to_none=True) # gradyanları sıfırla
logits = model(X_b) # forward pass
loss = criterion(logits, y_b) # loss hesapla
loss.backward() # gradyanları hesapla
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step() # parametreleri güncelle
total_loss += loss.item() * X_b.size(0)
preds = logits.argmax(dim=1)
correct += (preds == y_b).sum().item()
total += X_b.size(0)
return total_loss / total, correct / total
def val_epoch(model, loader, criterion, device):
model.eval() # dropout kapalı
total_loss, correct, total = 0.0, 0, 0
with torch.no_grad(): # gradyan grafiği oluşturma
for X_b, y_b in loader:
X_b, y_b = X_b.to(device), y_b.to(device)
logits = model(X_b)
loss = criterion(logits, y_b)
total_loss += loss.item() * X_b.size(0)
preds = logits.argmax(dim=1)
correct += (preds == y_b).sum().item()
total += X_b.size(0)
return total_loss / total, correct / total
# ─── Tam eğitim döngüsü ──────────────────────────────────────
best_val_loss = float('inf')
for epoch in range(1, 6):
tr_loss, tr_acc = train_epoch(model, train_loader, optimizer, criterion, device)
vl_loss, vl_acc = val_epoch(model, val_loader, criterion, device)
scheduler.step()
print(f"Epoch {epoch:02d} | "
f"Train loss: {tr_loss:.4f} acc: {tr_acc:.3f} | "
f"Val loss: {vl_loss:.4f} acc: {vl_acc:.3f}")
if vl_loss < best_val_loss:
best_val_loss = vl_loss
torch.save(model.state_dict(), 'best_model.pt')
print(f" ✓ En iyi model kaydedildi (val_loss={vl_loss:.4f})")
Gradient accumulation, küçük GPU belleğiyle büyük batch boyutu simüle etmek için kullanılır. zero_grad'ı her N adımda bir çağırın ve optimizer.step'i de o adımda yapın. Effective batch size = batch_size × accumulation_steps olur.
08 GPU ve CUDA
GPU hızlandırma, derin öğrenme modellerinin pratik eğitimini mümkün kılar; device-agnostic kod yazmak modeli hem CPU hem GPU'da sorunsuz çalıştırır.
PyTorch'ta bir tensör veya modeli GPU'ya taşımanın yolu .to(device) metodudur. device = 'cuda' if torch.cuda.is_available() else 'cpu' kalıbı, GPU yoksa otomatik CPU'ya düşer ve kodu taşınabilir kılar. Modeli GPU'ya taşıdıktan sonra, modele verilecek tüm tensörlerin de aynı cihazda olması gerekir; aksi takdirde runtime hatası alınır. .to(device) hem modeller hem tensörler üzerinde çalışır.
GPU bellek yönetimi üretim kodunda kritik önem taşır. Her forward pass, hesaplama grafiği için bellek kullanır. torch.no_grad() ile inference yapılmazsa validation sırasında VRAM dolar. torch.cuda.empty_cache() Python garbage collector'ın serbest bıraktığı ama CUDA'nın henüz iade etmediği belleği geri alır; ancak bu fonksiyon gerçek bir bellek sızıntısını çözmez. VRAM kullanımını torch.cuda.memory_summary() ile izleyebilirsiniz.
Mixed precision training, modelin ağırlıklarını float16/bfloat16 ile depolayıp kritik hesaplamaları float32'de yaparak hem hız hem bellek tasarrufu sağlar. PyTorch'un torch.cuda.amp.autocast context manager'ı hangi işlemlerin hangi precisionda çalışacağını otomatik seçer. GradScaler ise float16'nın küçük değerlerde yaşadığı underflow problemini ölçekleme ile çözer.
import torch
import torch.nn as nn
import torch.optim as optim
from torch.cuda.amp import autocast, GradScaler
# ─── Device-agnostic kod ─────────────────────────────────────
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Cihaz: {device}")
if device.type == 'cuda':
print(f"GPU : {torch.cuda.get_device_name(0)}")
print(f"VRAM : {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
# ─── Model ve tensörleri cihaza taşı ────────────────────────
model = nn.Sequential(
nn.Linear(256, 512), nn.GELU(),
nn.Linear(512, 256), nn.GELU(),
nn.Linear(256, 10),
).to(device)
X = torch.randn(64, 256).to(device)
y = torch.randint(0, 10, (64,)).to(device)
# ─── Mixed precision eğitim ──────────────────────────────────
optimizer = optim.AdamW(model.parameters(), lr=3e-4)
scaler = GradScaler(enabled=(device.type == 'cuda'))
criterion = nn.CrossEntropyLoss()
for step in range(3):
optimizer.zero_grad(set_to_none=True)
with autocast(device_type=device.type, dtype=torch.float16):
logits = model(X)
loss = criterion(logits, y)
scaler.scale(loss).backward() # ölçeklenmiş gradyan
scaler.unscale_(optimizer) # clipping öncesi unscale
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
scaler.step(optimizer) # NaN/Inf yoksa güncelle
scaler.update() # ölçek faktörünü güncelle
print(f"Step {step}: loss={loss.item():.4f}")
# ─── VRAM kullanım bilgisi ───────────────────────────────────
if device.type == 'cuda':
allocated = torch.cuda.memory_allocated() / 1e6
reserved = torch.cuda.memory_reserved() / 1e6
print(f"Kullanılan VRAM : {allocated:.1f} MB")
print(f"Rezerve VRAM : {reserved:.1f} MB")
torch.cuda.empty_cache() # önbelleği temizle
| Precision | Boyut | Hız | Doğruluk | Kullanım |
|---|---|---|---|---|
| float32 | 4 byte | Referans | Tam | Küçük modeller, CPU |
| float16 | 2 byte | ~2× | Küçük hata riski | GPU eğitimi + GradScaler |
| bfloat16 | 2 byte | ~2× | float32'ye yakın | A100/H100, LLM eğitimi |
| int8 | 1 byte | ~4× | Kayıplı | Quantized inference |
09 Model Kaydetme ve Yükleme
state_dict tabanlı kaydetme, modelin mimariden bağımsız parametrelerini taşınabilir biçimde saklar ve eğitimi kaldığı yerden sürdürmeyi mümkün kılar.
PyTorch'ta model kaydetmenin iki yolu vardır. Birincisi, tüm model nesnesini pickle ile kaydetmek: torch.save(model, 'model.pt'). Bu yaklaşım kullanımı kolaydır ancak model sınıfının tanımlı olmasını gerektirir ve kayıt ile yükleme arasında kod değişirse (katman ismi, yapı) yükleme başarısız olur. İkincisi ve her zaman tercih edilen yaklaşım state_dict kaydetmektir: yalnızca parametre değerleri saklanır, mimari bilgisi saklanmaz. Bu format taşınabilirdir ve PyTorch sürüm geçişlerinde sorun çıkarmaz.
Checkpoint, yalnızca model ağırlıklarını değil, aynı zamanda optimizer durumunu, epoch numarasını ve en iyi validation loss değerini de saklar. Bu sayede eğitim kesildiğinde kaldığı yerden devam edilebilir. Optimizer state'i (momentum tamponları, Adam'ın ikinci moment kümülatif değerleri) yüklenmezse eğitim sağlıklı devam etmez; optimizer yeni başlamış gibi davranır ve loss geçici olarak artabilir.
HuggingFace ekosistemindeki modeller model.save_pretrained('klasör/') ve AutoModel.from_pretrained('klasör/') kullanır. Bu format, PyTorch state_dict'ini pytorch_model.bin veya model.safetensors dosyasına, model konfigürasyonunu ise config.json dosyasına kaydeder. Fine-tuning sonrası modeli HuggingFace formatında kaydetmek, tekrar yüklemeyi ve paylaşmayı kolaylaştırır.
import torch
import torch.nn as nn
import torch.optim as optim
import os
# ─── Model tanımı ────────────────────────────────────────────
class SimpleMLP(nn.Module):
def __init__(self, in_dim, hidden, out_dim):
super().__init__()
self.layers = nn.Sequential(
nn.Linear(in_dim, hidden), nn.GELU(),
nn.Linear(hidden, out_dim)
)
def forward(self, x): return self.layers(x)
model = SimpleMLP(64, 128, 10)
optimizer = optim.AdamW(model.parameters(), lr=3e-4)
# ─── Yalnızca state_dict kaydet ──────────────────────────────
torch.save(model.state_dict(), 'weights_only.pt')
# Yükleme: önce aynı mimariyi oluştur, sonra ağırlıkları yükle
model2 = SimpleMLP(64, 128, 10)
model2.load_state_dict(torch.load('weights_only.pt', map_location='cpu'))
model2.eval()
# ─── Tam checkpoint ──────────────────────────────────────────
def save_checkpoint(model, optimizer, epoch, val_loss, path):
torch.save({
'epoch' : epoch,
'model_state_dict': model.state_dict(),
'optim_state_dict': optimizer.state_dict(),
'val_loss' : val_loss,
}, path)
print(f"Checkpoint kaydedildi: {path}")
def load_checkpoint(model, optimizer, path, device='cpu'):
ckpt = torch.load(path, map_location=device)
model.load_state_dict(ckpt['model_state_dict'])
optimizer.load_state_dict(ckpt['optim_state_dict'])
epoch = ckpt['epoch']
val_loss = ckpt['val_loss']
print(f"Checkpoint yüklendi: epoch={epoch}, val_loss={val_loss:.4f}")
return epoch, val_loss
# Kaydet
save_checkpoint(model, optimizer, epoch=10, val_loss=0.412,
path='checkpoint_epoch10.pt')
# Kaldığı yerden devam et
model3 = SimpleMLP(64, 128, 10)
optim3 = optim.AdamW(model3.parameters(), lr=3e-4)
start_ep, best_loss = load_checkpoint(model3, optim3, 'checkpoint_epoch10.pt')
# ─── Checkpoint varlığını kontrol ederek resume ──────────────
CKPT_PATH = 'training_checkpoint.pt'
start_epoch = 0
if os.path.exists(CKPT_PATH):
start_epoch, _ = load_checkpoint(model, optimizer, CKPT_PATH)
print(f"Eğitim epoch {start_epoch+1}'den devam ediyor")
else:
print("Eğitim baştan başlıyor")
NUM_EPOCHS = 20
X = torch.randn(32, 64)
y = torch.randint(0, 10, (32,))
criterion = nn.CrossEntropyLoss()
for epoch in range(start_epoch, min(start_epoch + 3, NUM_EPOCHS)):
model.train()
optimizer.zero_grad(set_to_none=True)
loss = criterion(model(X), y)
loss.backward()
optimizer.step()
save_checkpoint(model, optimizer, epoch, loss.item(), CKPT_PATH)
print(f"Epoch {epoch}: loss={loss.item():.4f}")
torch.load pickle kullanır ve güvenilmez kaynaklardan yükleme güvenlik riski taşır. PyTorch 2.0+ ile gelen weights_only=True parametresi yalnızca tensör verilerini yükler ve zararlı kod çalıştırılmasını engeller. Üretimde her zaman torch.load(path, weights_only=True) kullanın.
01 Eğitim döngüsü → her epoch sonunda checkpoint kaydet 02 Kesinti / crash → en son checkpoint'ten devam et 03 En iyi val_loss → best_model.pt olarak kaydet 04 Eğitim bitti → best_model.pt yükle → test / deploy