- Bir görüntüyü (N, C, H, W) tensörü olarak göstermek ve normalleştirmek
- Evrişimi elle hesaplamak, çıktı boyutunu ve parametre sayısını bulmak
- MNIST için küçük bir CNN kurmak ve şekilleri katman katman izlemek
- Transfer öğrenmeyi açıklamak ve önceden eğitilmiş bir modeli uyarlamak
Telefonunun kamerası yüzleri tanır, doktorlara yardım eden programlar röntgen görüntülerindeki değişiklikleri bulur, arabalar trafik işaretlerini okur. Bunların çoğunun arkasında evrişimli sinir ağları (CNN) vardır. Önceki derste 224 × 224 renkli bir görüntü için tam bağlantılı bir katmanın 150 milyondan fazla parametre gerektirdiğini gördük. CNN'ler iki basit gözlemden yararlanır: görüntüdeki anlamlı desenler yereldir (kenarlar, köşeler, dokular) ve aynı desen resmin herhangi bir yerinde görünebilir. Bu yüzden küçük bir filtre tüm görüntü üzerinde “kaydırılır” ve ağırlıkları her yerde paylaşılır.
Tensör olarak görüntüler
Gri tonlamalı 28 × 28 bir görüntü (1, 28, 28) şeklinde bir tensördür: bir kanal, 28 satır, 28 sütun. Renkli bir görüntünün üç kanalı vardır; kırmızı, yeşil, mavi: (3, H, W). Yığınlar için PyTorch (N, C, H, W) düzenini kullanır. Pikseller genellikle 0–255 arası tam sayılardır; transforms.ToTensor() onları [0; 1] aralığına getirir, ardından her kanal normalleştirilir.
- x[0; 1] aralığındaki piksel değeri
- μ, σkanalın eğitim kümesindeki ortalaması ve standart sapması (MNIST için 0,1307 ve 0,3081)
Normalleştirmeden sonra girdiler kabaca sıfır etrafında ve birim ölçektedir; bu, gradyan inişini hızlandırır.
Evrişim işlemi
Filtre (çekirdek, kernel), örneğin 3 × 3 boyutunda küçük bir ağırlık matrisidir. Görüntünün üzerinde kayar ve her konumda altındaki piksellerle eleman bazında çarpılıp toplanır. Sonuçta bir öznitelik haritası (feature map) elde edilir: filtrenin “aradığı” desen nerede varsa orada büyük değerler olur. Bir evrişim katmanında birkaç filtre bulunur ve her biri kendi çıktı kanalını üretir; her filtre tüm girdi kanallarını kapsar.
- Xgirdi görüntüsü (ya da önceki katmanın haritası)
- KK × K boyutunda filtre; öğrenilen ağırlıklar
- bfiltrenin sapması
- Y[i, j]öznitelik haritasının (i, j) elemanı
Tek kanal için 2B evrişim. Birden çok girdi kanalı olduğunda toplam kanallar üzerinden de alınır.
Aşağıdaki kod evrişimi NumPy ile sıfırdan hesaplar. Görüntünün sol yarısı karanlık (0), sağ yarısı parlaktır (9); filtre ise soldan sağa artan parlaklığa tepki verir. Kodu çalıştır ve sonucu kendin kontrol et:
import numpy as np
image = np.array([
[0, 0, 0, 9, 9, 9],
[0, 0, 0, 9, 9, 9],
[0, 0, 0, 9, 9, 9],
[0, 0, 0, 9, 9, 9],
[0, 0, 0, 9, 9, 9],
])
kernel = np.array([
[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1],
])
K = kernel.shape[0]
H, W = image.shape
out = np.zeros((H - K + 1, W - K + 1), dtype=int)
for i in range(out.shape[0]):
for j in range(out.shape[1]):
out[i, j] = np.sum(image[i:i + K, j:j + K] * kernel)
print(out)▸ Beklenen çıktı
[[ 0 27 27 0] [ 0 27 27 0] [ 0 27 27 0]]
Eğitilmiş CNN'lerde ilk katmanların filtreleri genellikle kenarlara ve renk geçişlerine, orta katmanlar dokulara ve parçalara (gözler, tekerlekler), son katmanlar ise bütün nesnelere tepki verir. Her evrişim ve havuzlama katmanından sonra bir nöronun “gördüğü” alan, yani alıcı alanı (receptive field) büyür.
- Wgirdinin genişliği (ya da yüksekliği), piksel
- Kçekirdek boyutu
- Pdolgu (padding): kenarlara eklenen sıfır pikseller
- Sadım (stride): filtrenin kaç piksel kaydığı
- Oçıktı genişliği; ⌊ ⌋ aşağı yuvarlamadır
Çıktı boyutu formülü; yükseklik için de aynısı geçerlidir. Havuzlamada genellikle P = 0 ve S = K alınır.
- Cin, Coutgirdi ve çıktı kanallarının sayısı
Bir evrişim katmanının parametre sayısı görüntü boyutuna bağlı değildir; ağırlıklar tüm konumlarda paylaşılır.
28 × 28 bir girdi için çıktı boyutunu bul: a) K = 3, P = 1, S = 1; b) K = 5, P = 0, S = 1; c) K = 3, P = 1, S = 2. d) 16 filtreli Conv2d(1, 16, 3) katmanında kaç parametre vardır?
Çözümü gösterÇözümü gizle
b) (28 − 5 + 0) / 1 + 1 = 24.
c) ⌊(28 − 3 + 2) / 2⌋ + 1 = ⌊13,5⌋ + 1 = 14; boyut yarıya iner.
d) (3 · 3 · 1 + 1) · 16 = 160. Karşılaştırma için: 28 × 28 bir görüntüyü 16 × 28 × 28 bir çıktıya bağlayan tam bağlantılı bir katmanda 784 · 12.544 ≈ 9,8 milyon ağırlık olurdu!
import torch
from torch import nn
torch.manual_seed(42)
x = torch.randn(8, 1, 28, 28)
conv = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=3, padding=1)
print(conv(x).shape)
print(nn.Conv2d(1, 16, kernel_size=5)(x).shape)
print(nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1)(x).shape)
print(nn.MaxPool2d(kernel_size=2)(conv(x)).shape)
print(conv.weight.shape, sum(p.numel() for p in conv.parameters()))torch.Size([8, 16, 28, 28]) torch.Size([8, 16, 24, 24]) torch.Size([8, 16, 14, 14]) torch.Size([8, 16, 14, 14]) torch.Size([16, 1, 3, 3]) 160
Havuzlama ve 28 × 28 görüntüler için bir CNN
Maksimum havuzlama (max pooling), her pencereden (genellikle 2 × 2) yalnızca en büyük değeri tutar. Haritanın boyutunu yarıya indirir, hesaplamayı ucuzlatır, küçük kaymalara karşı dayanıklılık katar ve hiç parametresi yoktur. Ortalama havuzlama (average pooling) ise ortalamayı alır.
4 × 4 haritaya 2 × 2 maksimum ve ortalama havuzlama uygula (S = 2):
1 3 2 1
4 6 5 0
7 2 1 9
3 8 4 2
Çözümü gösterÇözümü gizle
Maksimum: [[6, 5], [8, 9]].
Ortalama: [[3,5; 2], [5; 4]].
Çıktı 2 × 2'dir: O = ⌊(4 − 2) / 2⌋ + 1 = 2.
Şimdi MNIST için küçük bir CNN kuralım: iki “evrişim → ReLU → maksimum havuzlama” bloğu öznitelikleri çıkarır, sonda ise Flatten ve tam bağlantılı bir katman 10 rakam için logitler üretir. Her katmandan sonra şekli yazdırıyoruz; bir CNN'de hata ayıklamanın en güvenilir yolu budur.
import torch
from torch import nn
class SmallCNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
)
self.classifier = nn.Sequential(nn.Flatten(), nn.Linear(32 * 7 * 7, 10))
def forward(self, x):
return self.classifier(self.features(x))
torch.manual_seed(42)
model = SmallCNN()
x = torch.randn(64, 1, 28, 28)
for layer in model.features:
x = layer(x)
print(f'{type(layer).__name__:9s} {tuple(x.shape)}')
print(model(torch.randn(64, 1, 28, 28)).shape)
print(sum(p.numel() for p in model.parameters()))Conv2d (64, 16, 28, 28) ReLU (64, 16, 28, 28) MaxPool2d (64, 16, 14, 14) Conv2d (64, 32, 14, 14) ReLU (64, 32, 14, 14) MaxPool2d (64, 32, 7, 7) torch.Size([64, 10]) 20490
SmallCNN'in şekillerini izle ve parametrelerini elle say. 4. dersteki MLP ile (101.770 parametre) karşılaştır.
Çözümü gösterÇözümü gizle
conv1: (3 · 3 · 1 + 1) · 16 = 160.
conv2: (3 · 3 · 16 + 1) · 32 = 4.640.
fc: (1568 + 1) · 10 = 15.690.
Toplam: 20.490; MLP'den yaklaşık 5 kat az ama görüntülerin yapısından yararlandığı için MNIST'te genellikle daha isabetlidir.
torchvision ile MNIST
torchvision paketi (pip install torchvision) hazır veri kümeleri (MNIST, CIFAR-10, ImageNet biçimi), görüntü dönüşümleri (transforms) ve önceden eğitilmiş modeller sunar. MNIST, el yazısı rakamlardan oluşan 70.000 görüntüden oluşur: 60.000 eğitim ve 10.000 test, her biri 28 × 28 gri tonlamalı. download=True dosyaları ilk seferde data klasörüne indirir, sonraki çalıştırmalarda diskten okur.
import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
torch.manual_seed(42)
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)),
])
train_set = datasets.MNIST('data', train=True, download=True, transform=transform)
test_set = datasets.MNIST('data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_set, batch_size=64, shuffle=True)
test_loader = DataLoader(test_set, batch_size=1000)
images, labels = next(iter(train_loader))
print(len(train_set), len(test_set))
print(images.shape, labels.shape)60000 10000 torch.Size([64, 1, 28, 28]) torch.Size([64])
model = SmallCNN().to(device) kullan. Bu kadar küçük bir CNN birkaç epokta test kümesinde genellikle yaklaşık %98–99 doğruluğa ulaşır.Transfer öğrenme
Büyük bir CNN'i sıfırdan eğitmek için milyonlarca görüntü ve güçlü GPU'lar gerekir. Ancak ImageNet'te (1000 sınıf, bir milyondan fazla eğitim görüntüsü) eğitilmiş bir ağın ilk katmanlarının öğrendiği kenarlar, dokular ve şekiller neredeyse her görüntü görevine yarar. Transfer öğrenme şöyle çalışır: önceden eğitilmiş bir modeli al, katmanlarını dondur, son sınıflandırma katmanını kendi sınıflarına uygun yenisiyle değiştir ve yalnızca onu eğit.
import torch
from torch import nn
from torchvision import models
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
for p in model.parameters():
p.requires_grad = False
print(model.fc)
model.fc = nn.Linear(model.fc.in_features, 3)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(trainable, total)Linear(in_features=512, out_features=1000, bias=True) 1539 11178051
Yeni katmanda (512 + 1) · 3 = 1.539 parametre vardır. Bu kadar az parametre birkaç yüz görüntüyle, CPU'da bile hızla eğitilir. Daha çok veri varsa ikinci aşamada son birkaç katmanı da “çözüp” tüm ağı çok küçük bir öğrenme oranıyla ince ayarlayabilirsin (fine-tuning). Optimizasyon aracına yalnızca eğitilebilir parametreleri ver: torch.optim.Adam(model.fc.parameters(), lr=1e-3).
Önemli noktalar
- PyTorch'ta görüntüler (N, C, H, W) tensörleridir; girdiler (x − μ) / σ ile normalleştirilir.
- Evrişim küçük bir filtreyi görüntü üzerinde kaydırır; paylaşılan ağırlıklar sayesinde parametre az olur: (K·K·Cin + 1)·Cout.
- Çıktı boyutu: O = ⌊(W − K + 2P) / S⌋ + 1; P = 1 ile 3 × 3 filtre boyutu korur, S = 2 ya da 2 × 2 havuzlama yarıya indirir.
- Tipik bir CNN: [Conv → ReLU → Pool] × n → Flatten → Linear; şekilleri her katmandan sonra yazdır.
- Transfer öğrenme: önceden eğitilmiş modeli dondur, son katmanı değiştir ve yalnızca onu eğit.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.