Rakamları tanı

Zorluk: Zor

sinir ağlarıproje

Bitirme projesinde el yazısı rakamları tanıyan bir çok katmanlı algılayıcıyı (multilayer perceptron, MLP) NumPy ile sıfırdan kuracak ve eğiteceksin. Şunları kendin yazacaksın:

  • ileri geçişi (forward pass),
  • geri geçişi (backward pass),
  • eğitim döngüsünü.

Gereken her parçayı şu derslerde gördün:

train_and_predict(X_train, y_train, X_test) fonksiyonunu yaz. Veri, 8×8 piksellik el yazısı rakam görüntülerinden oluşur.

Her görüntü 64 özniteliğe (feature) düzleştirilir. Her feature bir pikselin koyuluğudur ve 0 ile 16 arasında bir değer alır. Fonksiyona üç dizi (array) gelir:

  • X_train: eğitim kümesi (training set), şekli (shape) (2500, 64) olan bir float array’i.
  • y_train: eğitim örneklerinin etiketleri (label), shape’i (2500,). Her değer rakamın kendisidir, yani 0 ile 9 arasında bir tam sayı.
  • X_test: test kümesi (test set), shape’i (800, 64). Etiketleri sana verilmez.

Fonksiyon MLP’yi training set’te eğitir. Sonra X_test’teki her örnek (sample) için tahmin ettiği rakamı döndürür: (800,) shape’inde, tam sayılardan oluşan bir NumPy array’i.

Tahminlerin en az %90’ı doğru olmalı. Doğruluk (accuracy), doğru tahminlerin oranıdır:

accuracy=1N∑i=1N1[y^i=yi]\text{accuracy} = \frac{1}{N}\sum_{i=1}^{N} \mathbf{1}[\hat{y}_i = y_i]

Köşeli parantezin içindeki koşul doğruysa 1[⋅]\mathbf{1}[\cdot] değeri 1, değilse 0 olur. Yani formül doğru tahminlerin oranını verir. N=800N = 800 test örneği için accuracy en az 0,900{,}90 olmalı.

Test set başka kişilerin el yazısından gelir. Bu yüzden ağın hiç görmediği yazılarda ne kadar başarılı olduğunu, yani genelleme (generalization) gücünü ölçer.

Pikseller ham değerleriyle, 0 ile 16 arasında gelir. Öznitelik ölçeklemeyi (feature scaling) sen yaparsın.

Ham değerlerle eğitim çoğu zaman kararsızlaşır. Bu yüzden ölçekleme de işin bir parçasıdır.

Veri kümesi (dataset), UCI’nin eğitim dosyasından seçilen 2500 ve test dosyasından seçilen 800 örnektir. Eğitimde her rakamdan 250, testte 80 örnek vardır. Değerler olduğu gibi bırakılmıştır.

Veri kaynağı: Alpaydin, E. & Kaynak, C. (1998), Optical Recognition of Handwritten Digits, UCI Machine Learning Repository, https://doi.org/10.24432/C50P49. Lisans: CC BY 4.0.

Örnekler

Girdi: n_per_class = 250, seed = 0
Çıktı: array([9, 0, 6, ..., 1, 4, 7], shape=(100,))

İpucu 1

X_train’i 16’ya bölmek bütün değerleri 0–1 aralığına getirir. Piksellerin en büyük değeri 16’dır. X_test’e de aynı dönüşümü uygula.

Piksel başına ortalama ve standart sapmayla standartlaştırmak (standardization) istersen:

  1. Ortalamayı ve standart sapmayı yalnızca training set’ten hesapla.
  2. Test’e aynı değerlerle uygula.

Training set’te değeri hep 0 olan pikseller var (standart sapması 0). Standart sapmaya küçük bir sayı eklemeden bölersen NaN çıkar.

Etiketleri one-hot satırlara çevir. Doğru sınıfın yerinde 1, diğerlerinde 0 olur. Gradyanı bu biçimde yazmak kolaydır.

İpucu 2

Önce ağı kur:

  • İki yoğun katman (dense layer): 64 girişten bir gizli katmana (hidden layer), oradan 10 çıkışa.
  • Ağırlıkların (weight) shape’leri W1 için (64, h), W2 için (h, 10). hh gizli nöron sayısıdır.
  • Hidden layer’da ReLU (negatif değerleri sıfırlayan aktivasyon fonksiyonu) kullan.
  • Son layer 10 ham skor (logits) üretsin.
  • Ağırlıkları He ilklendirmesiyle (initialization) rastgele başlat. Varyansı 2/din2 / d_{\text{in}} olsun (standart sapma 2/din\sqrt{2 / d_{\text{in}}}).
  • Sapmaları (bias) sıfırla.

Bir mini yığın (mini-batch) için sırayla şunları yap:

  1. Forward pass’i hesapla.
  2. Logit’lerden softmax (ham skorları olasılığa çeviren fonksiyon) hesapla. Üstelini almadan önce her satırdan o satırın en büyüğünü çıkar.
  3. Backward pass’i hesapla.
  4. Parametreleri güncelle.

Softmax ile çapraz entropinin (cross-entropy) logit’lere göre gradyanı (P−Y)/B(P - Y) / B‘dir. BB, mini-batch’teki örnek sayısıdır.

Her dense layer’da şunlar gerekir. Burada Z=AW+bZ = AW + \mathbf{b} ve AA layer’ın girdisidir:

dW=A⊤dZ,db=∑idZi,dA=dZ W⊤dW = A^\top dZ, \qquad d\mathbf{b} = \sum_i dZ_i, \qquad dA = dZ\,W^\top

dAdA, bir önceki layer’a giden gradyandır. ReLU’nun geri geçişinde hidden layer’ın dAdA‘sını yalnızca Z>0Z > 0 olan yerlerde bırak. dZdZ budur.

İpucu 3

Her epoch’ta şunları yap:

  1. Örnekleri karıştır.
  2. Mini-batch’lere böl.
  3. Her mini-batch’te yukarıdaki adımları çalıştır.

Başlangıç için şu değerler yeter:

  • 64 gizli nöron.
  • Öğrenme oranı (learning rate) 0,10{,}1.
  • Mini-batch boyutu 32.
  • 30 epoch.

Rastgeleliği rng = np.random.default_rng(0) gibi sabit bir tohumla (seed) ver. Böylece sonuç her çalıştırmada aynı çıkar.

Tahmin için:

  1. X_test’i aynı ölçeklemeyle forward pass’ten geçir.
  2. Her satırda en büyük logit’in indeksini (index) bul: argmax(axis=1).

Takılırsan epoch başına kayba (loss) bak. Düşmüyorsa ya da NaN oluyorsa şunları sırayla kontrol et:

  1. Girdilerin ölçeği.
  2. Gradyanlar.

Gradyanlardan şüphe edersen Gradyan kontrolü problemindeki sayısal gradyanı küçük bir ağda dene.

Gereksinimler

  • Dönüş değeri int türünde bir NumPy array’idir. Shape’i (len(X_test),), yani (800,). Her örnek için bir sınıf numarası verir. Testler olasılık matrisi, one-hot matris (her satırda bir 1, gerisi 0), float array’i ya da liste kabul etmez. argmax zaten tam sayı verir.

  • Her tahmin 0 ile 9 arasında bir tam sayı olmalı.

  • Test set’te accuracy en az %90 olmalı.

  • Pikseller ham gelir, ölçeklemeyi sen yaparsın. X_train ve X_test 0 ile 16 arasındaki değerleri olduğu gibi içerir.

  • Eğitim kısa sürmeli. Testlerin tamamı 60 saniye içinde bitmeli. 2500 örnek ve birkaç düzine epoch (dataset’in tamamı üzerinden bir tur) bunun için fazlasıyla yeter.

  • Test durumundaki veri gerçek rakamlar değil, küçük bir oyuncak veridir (toy dataset). Veri şöyle üretilir:

    1. c numaralı sınıfın 6*c ile 6*c + 5 arasındaki 6 pikseli 16 değerini alır.
    2. Her piksele −3-3 ile +3+3 arası gürültü eklenir.
    3. Sonuç 0 ile 16 arasına kırpılır.

    n_per_class her sınıftan kaç eğitim örneği olduğunu, seed gürültüyü belirler. Bu veride tahminlerin etiketlerle birebir aynı çıkması beklenir. %90 koşulu ise gerçek rakamlarda sınanır.

  • Bir test daha, etiketi iki piksel bloğunun XOR’u olan bir oyuncak veri kullanır:

    • 0–5 ve 6–11 numaralı piksellerin her bloğu ya hep yüksek (16) ya hep düşüktür (0).
    • Aynı gürültü eklenir.
    • Blokların yalnızca biri yüksekse etiket 1 olur. İkisi de aynıysa etiket 0 olur.

    Bu veriyi doğrusal bir model ayıramaz ve yaklaşık %60’ta kalır. Bir MLP’den en az %95 beklenir.

Bu proje, ayrı ayrı çalıştığın parçaların birleşimidir. Parçalar sırayla şöyledir:

  1. Forward pass her örnek için 10 logit üretir.
  2. Softmax logit’leri olasılığa çevirir.
  3. Cross-entropy, doğru rakama verilen olasılığın eksi logaritmasını loss olarak alır.
  4. Backward pass, zincir kuralını (chain rule) layer’lar boyunca geriye uygular ve her ağırlığın gradyanını bulur.
  5. Mini-batch’lerle çalışan stokastik gradyan inişi (stochastic gradient descent, SGD) ağırlıkları bu gradyanın tersi yönünde küçük adımlarla günceller.

Softmax ile cross-entropy birleşince logit’lere göre gradyan, örnek başına yalnızca P−YP - Y olur. İpucundaki sadeliğin sebebi budur.

Accuracy’yi eğitim verisinde değil, ağın hiç görmediği örneklerde ölçeriz. Eğitimde yüksek, testte düşük bir accuracy aşırı öğrenmenin (overfitting) işaretidir. Ağ örnekleri ezberlemiş, genelleştirememiştir.

Burada test set başka kişilerin el yazısından geldiği için bu gerçek bir generalization sınavıdır. Bu veride beklenen accuracy şöyledir:

  • Küçük bir MLP yaklaşık %95 accuracy’ye ulaşır. 8×8’lik görüntüler kaba bir özet olduğundan %100 beklemek gerçekçi değildir.
  • Hidden layer’ı olmayan bir softmax sınıflandırıcı da %94 civarına çıkar. 8×8’lik rakamlar neredeyse doğrusal olarak ayrılır.

Görev yine de bir MLP ister. Amaç en yüksek accuracy’yi almak değil, forward pass’i, backward pass’i ve eğitim döngüsünü hidden layer’lı bir ağ üzerinde kendin kurmaktır.

Hidden layer’ın asıl farkı doğrusal olarak ayrılmayan verilerde görünür. Testlerden biri bu yüzden böyle küçük bir oyuncak veri kullanır.

Girdi ölçeği eğitimin kararlılığını belirler. 0–16 aralığındaki ham pikseller hem ilk layer’ın çıkışını hem gradyanları büyütür.

Aynı learning rate ile loss patlayabilir. Pikselleri 16’ya bölmek ya da standartlaştırmak bu sorunu önler.

Daha fazlası için Bir MLP eğitmek dersine bak.

Pikselleri ölçekle, ağı eğit ve en büyük logit’in indeksini döndür.

  1. Pikselleri ölçekle.
  2. İki layer’ı He ilklendirmesiyle kur.
  3. Her mini-batch’te forward pass’i, gradyanı ve güncellemeyi yap.
  4. Test örneklerini aynı ağdan geçir.
  5. En büyük logit’in indeksini döndür.
import numpy as npdef train_and_predict(X_train: np.ndarray, y_train: np.ndarray, X_test: np.ndarray) -> np.ndarray:    rng = np.random.default_rng(0)    X = X_train / 16.0  # pikselleri 0-1 aralığına getir    n, d = X.shape    hidden, k = 64, 10    Y = np.eye(k)[y_train]  # etiketler one-hot    # He ilklendirmesi; bias'lar sıfır    W1 = rng.normal(0.0, np.sqrt(2.0 / d), (d, hidden))    b1 = np.zeros(hidden)    W2 = rng.normal(0.0, np.sqrt(2.0 / hidden), (hidden, k))    b2 = np.zeros(k)    lr, epochs, batch_size = 0.1, 30, 32    for _ in range(epochs):        order = rng.permutation(n)  # her epoch'ta örnekleri karıştır        for start in range(0, n, batch_size):            idx = order[start:start + batch_size]            xb, yb = X[idx], Y[idx]            # ileri geçiş            Z1 = xb @ W1 + b1            A1 = np.maximum(Z1, 0.0)            logits = A1 @ W2 + b2            probs = np.exp(logits - logits.max(axis=1, keepdims=True))            probs /= probs.sum(axis=1, keepdims=True)            # geri geçiş: softmax ile çapraz entropinin gradyanı (probs - Y) / batch            dZ2 = (probs - yb) / len(idx)            dW2 = A1.T @ dZ2            db2 = dZ2.sum(axis=0)            dZ1 = (dZ2 @ W2.T) * (Z1 > 0)            dW1 = xb.T @ dZ1            db1 = dZ1.sum(axis=0)            # güncelleme            W1 -= lr * dW1            b1 -= lr * db1            W2 -= lr * dW2            b2 -= lr * db2    logits = np.maximum((X_test / 16.0) @ W1 + b1, 0.0) @ W2 + b2    return logits.argmax(axis=1)

Sık yapılan hata, pikselleri ölçeklememektir. Yukarıdaki kodda X_train / 16.0 ve X_test / 16.0 kaldırılıp aynı learning rate 0,10{,}1 ile beş farklı seed denendi:

GirdiAccuracy, beş seed
Ölçeksiz%10 ile %96 arasında
Ölçeklenmişhepsinde %94’ün üstünde

Büyük girdiler büyük gradyanlar demektir. Eğitim kimi seed’lerde patlar, kimilerinde yürür.

Bu problemde bir sorun mu var?

solution.py
import numpy as npdef train_and_predict(X_train: np.ndarray, y_train: np.ndarray, X_test: np.ndarray) -> np.ndarray:    """MLP'yi X_train ile eğitir; X_test'teki her örnek için bir rakam (sınıf numarası) döndürür."""    # Kodunu buraya yaz.    return np.zeros(len(X_test), dtype=int)

İçindekiler

Geri bildirim

Tür