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

Evrişimli sinir ağları (CNN)

Tensör olarak görüntüler (C × H × W), evrişim ve çıktı boyutu formülü, filtreler, havuzlama, şekillerin adım adım izlendiği 28 × 28 görüntüler için küçük bir CNN, torchvision ile MNIST ve transfer öğrenme.

Kendini test et
Bu derste öğreneceklerin
  • 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′ = (x − μ) / σx′ = (x − μ) / σ
burada:
  • 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.

Y[i, j] = ∑ₘ ∑ₙ X[i + m, j + n] · K[m, n] + b
burada:
  • 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:

Python
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]]
27 değerleri tam olarak dikey kenarın bulunduğu yerde: filtre bir kenar dedektörüdür. CNN'de bu tür filtreler elle yazılmaz; gradyan inişi onları öğrenir.

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.

O = ⌊(W − K + 2P) / S⌋ + 1O = ⌊(W − K + 2P) / S⌋ + 1
burada:
  • 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.

params = (K · K · Cin + 1) · Cout
burada:
  • 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.

Örnek 1: çıktı boyutlarını hesapla

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
a) (28 − 3 + 2) / 1 + 1 = 28; boyut korunur.
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!
Python
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()))
Beklenen çıktı
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
PyTorch, Örnek 1'in cevaplarını doğrular. Filtre ağırlıklarının şekli (Cout, Cin, K, K)'dır.

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.

Örnek 2: elle havuzlama

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
Pencereler: sol üst {1, 3, 4, 6}, sağ üst {2, 1, 5, 0}, sol alt {7, 2, 3, 8}, sağ alt {1, 9, 4, 2}.
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.

Python
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()))
Beklenen çıktı
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
Örnek 3: şekiller ve parametreler

SmallCNN'in şekillerini izle ve parametrelerini elle say. 4. dersteki MLP ile (101.770 parametre) karşılaştır.

Çözümü göster
Şekiller: (1, 28, 28) → evrişim (P = 1): (16, 28, 28) → havuzlama: (16, 14, 14) → evrişim: (32, 14, 14) → havuzlama: (32, 7, 7) → flatten: 32 · 7 · 7 = 1568.
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.

Python
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)
Beklenen çıktı
60000 10000
torch.Size([64, 1, 28, 28]) torch.Size([64])
İnternet bağlantısı gerekir (yalnızca ilk seferde). Eğitim döngüsü önceki dersteki ile aynıdır; sadece 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.

Python
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)
Beklenen çıktı
Linear(in_features=512, out_features=1000, bias=True)
1539 11178051
İlk çalıştırmada ImageNet ağırlıkları (yaklaşık 45 MB) indirilir. 3 sınıflı bir görevde (örneğin bir bitkinin üç yaprak hastalığı) 11,18 milyon parametreden yalnızca 1.539'u eğitilir.

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.

1 / 10
Girdi 32 × 32, K = 5, P = 0, S = 1. Çıktı boyutu nedir?