İçeriğe geç

Kitaplar / Yöntem ile Makine Öğrenmesi / Bölüm 4 — Derin öğrenme

Bölüm 4 — Derin öğrenme

Şimdiye kadarki modellerde kuralı biz tasarladık (bir eşik, bir mesafe, bir ağaç). Sinir ağlarında ise yalnızca yapıyı kurarız; milyonlarca küçük ayarı (ağırlığı) veri belirler. Bu bölümde YZ biriminin iki özelliğini kullanacağız: tensörler (sayı tabloları) ve otomatik türev. kayıp.GeriYay çağrısı, kaybın her ağırlığa göre türevini kendisi hesaplar. Optimize edici (Adam) de her ağırlığı kaybı azaltacak yöne biraz kaydırır. Beş programın hepsi bu döngüyü kullanır:

Çıktı
opt.SıfırlaGrad          { eski türevleri sil }
kayıp := ...             { ileri hesap: tahmin et, hatayı ölç }
kayıp.GeriYay            { geri yayılım: her ağırlığın türevi }
opt.Adım                 { ağırlıkları güncelle }

16. Çok katmanlı algılayıcı (sinir ağı)

Fikir. Bir nöron girdilerinin ağırlıklı toplamını alır ve sonucu doğrusal olmayan bir fonksiyondan (aktivasyon) geçirir. Nöronlar katmanlara dizilir; her katmanın çıktısı bir sonrakinin girdisidir. Lojistik regresyon tek bir nörondur ve yalnızca düz bir sınır çizebilir. Araya gizli bir katman eklenince ağ çember gibi eğri sınırlar da öğrenebilir.

h=ReLU(xW1+b1),ŷ=σ(hW2+b2)h = \mathrm{ReLU}(x W_1 + b_1), \qquad \hat{y} = \sigma(h W_2 + b_2)

Yöntem
program ÇokKatmanlıAlgılayıcı
{ Çok katmanlı algılayıcı (MLP): katman katman dizilmiş "nöronlar".
  Her katman  çıktı = aktivasyon(girdi @ W + b)  hesaplar. Doğrusal olmayan aktivasyon (ReLU, tanh)
  sayesinde ağ eğri karar sınırları öğrenebilir. Ağırlıklar geri yayılım + Adam ile güncellenir.
  Görev: bir noktanın yarıçapı 1 olan çemberin içinde mi dışında mı olduğunu öğrenmek. }
kullan YZ

değişken
  noktalar, etiket, deneme: liste
  X, Y, tahmin: tensör
  model, opt: herhangi
  geçmiş: liste
  i, isabet: tamsayı
  px, py: ondalık

başla
  Tohum(7)
  noktalar := []; etiket := []
  1'den 200'e kadar her i için
  başla
    px := Rastgele(4000) / 1000 - 2       { [-2, 2) aralığında }
    py := Rastgele(4000) / 1000 - 2
    noktalar.Ekle([px, py])
    eğer px * px + py * py < 1 ise etiket.Ekle([1]) değilse etiket.Ekle([0])
  bitir
  X := Tensör(noktalar)
  Y := Tensör(etiket)

  { 2 girdi → 16 gizli nöron (ReLU) → 1 çıktı (sigmoid: "içeride olma olasılığı") }
  model := Ardışık([Yoğun(2, 16, 'relu'), Yoğun(16, 1, 'sigmoid')])
  opt := Adam(model.Parametreler, 0.05)
  geçmiş := Eğit(model, X, Y, 'ikiliÇaprazEntropi', opt, 400, 100)

  tahmin := model.Tahmin(X)
  isabet := 0
  0'dan 199'a kadar her i için
    eğer (tahmin[i, 0] > 0.5) = (etiket[i][0] = 1) ise isabet := isabet + 1
  yaz('Eğitim doğruluğu: %' + isabet / 2, 1)
  yaz('Parametre sayısı: ' + model.ParametreSayısı, 1)

  yaz('Yeni noktalar:', 1)
  deneme := [[0, 0], [0.5, 0.5], [0.9, 0], [1.2, 0], [1.5, 1.5]]
  tahmin := model.Tahmin(Tensör(deneme))
  0'dan 4'e kadar her i için
    yaz(Biçimle('  {}  içeride olma olasılığı {:.2}', deneme[i], tahmin[i, 0]), 1)
bitir.

Programın çıktısı şöyledir:

Çıktı
  Epok     1/400   kayıp = 0.564626
  Epok   100/400   kayıp = 0.021568
  Epok   200/400   kayıp = 0.006376
  Epok   300/400   kayıp = 0.003043
  Epok   400/400   kayıp = 0.001746
Eğitim doğruluğu: %100.0
Parametre sayısı: 65
Yeni noktalar:
  [0, 0]  içeride olma olasılığı 1.00
  [0.5, 0.5]  içeride olma olasılığı 1.00
  [0.9, 0]  içeride olma olasılığı 0.31
  [1.2, 0]  içeride olma olasılığı 0.00
  [1.5, 1.5]  içeride olma olasılığı 0.00

Ne öğrendik. 65 ağırlıklı küçük bir ağ, kimse ona çember formülünü söylemeden eğitim noktalarının hepsini doğru sınıflandırdı. Ama [0.9, 0] noktasına dikkat etmekte yarar vardır: nokta aslında içeridedir, ağ ise %31 der. Eğitim doğruluğunun %100 olması modelin kusursuz olduğu anlamına gelmez. Sınıra yakın bölgede az örnek vardı ve ağ sınırı biraz içeriden çizdi. Gerçek başarıyı her zaman ayrı tutulmuş bir test kümesiyle ölçmek gerekir.

Alıştırma. Gizli katmanı Yoğun(2, 2, 'relu') yapınız. İki nöron bir çember çizmeye yetiyor mu? Sonra 200 yerine 1000 nokta kullanıp [0.9, 0] tahmininin nasıl değiştiğine bakınız.

17. Evrişimli sinir ağı (CNN)

Fikir. Bir resimde “kenar” nerede olursa olsun kenardır. CNN bu yüzden her yerde aynı küçük filtreyi kullanır: filtre resmin üzerinde kaydırılır ve her konumda altındaki yamayla çarpılıp toplanır. Sonuç bir öznitelik haritasıdır: filtrenin aradığı desen nerede varsa orada büyük değer çıkar. Havuzlama bu haritayı küçültür. Gerçek ağlarda filtreler elle seçilmez, veriden öğrenilir.

(R*F)[i,j]=∑a∑bR[i+a,j+b]⋅F[a,b](R * F)[i, j] = \sum_a \sum_b R[i + a,\, j + b] \cdot F[a, b]

Yöntem
program EvrişimliAğ
{ Evrişimli sinir ağı (CNN) iki temel işlem üzerine kurulur:
  1) Evrişim: küçük bir filtre (çekirdek) görüntünün üzerinde kaydırılır; her konumda filtre ile altındaki
     yama çarpılıp toplanır. Aynı filtre her yerde kullanıldığı için desen nerede olursa olsun bulunur.
  2) Havuzlama: harita küçük bloklara bölünür, her bloktan en büyük değer alınır.
  Bölüm A elle seçilmiş bir kenar filtresini uygular. Bölüm B bir filtreyi veriden öğrenir:
  yamaları bir matrisin satırlarına dizersek evrişim tek bir matris çarpımına dönüşür (yama @ filtre). }
kullan YZ

değişken
  resim, harita, havuz, filtre: ondalık dizisi dizisi
  sinyal, hedef, yama: ondalık dizisi
  yamalar: liste
  P, H, w: tensör
  opt: herhangi
  kayıp: tensör
  i, adım: tamsayı

fonksiyon Evrişim(R, F: ondalık dizisi dizisi): ondalık dizisi dizisi
değişken i, j, a, b: tamsayı; satır: ondalık dizisi; t: ondalık
başla
  sonuç := []
  0'dan Uzunluk(R) - Uzunluk(F)'e kadar her i için
  başla
    satır := []
    0'dan Uzunluk(R[0]) - Uzunluk(F[0])'a kadar her j için
    başla
      t := 0
      0'dan Uzunluk(F) - 1'e kadar her a için
        0'dan Uzunluk(F[0]) - 1'e kadar her b için
          t := t + R[i + a][j + b] * F[a][b]
      satır.Ekle(t)
    bitir
    sonuç.Ekle(satır)
  bitir
bitir

fonksiyon Göster(M: ondalık dizisi dizisi)
değişken s: ondalık dizisi
başla
  M içindeki her s için
    yaz('  ' + Birleştir(Eşle(s, fonksiyon(v: ondalık) => Biçimle('{:4.0}', v)), ' '), 1)
bitir

başla
  yaz('--- A) Hazır filtreyle dikey kenar bulma ---', 1)
  { 6×6 gri tonlu resim: solu koyu (0), sağı açık (9) }
  resim := []
  1'den 6'ya kadar her i için resim.Ekle([0, 0, 9, 9, 9, 9])
  filtre := [[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]]     { soldan sağa parlaklık artışına tepki verir }
  harita := Evrişim(resim, filtre)
  yaz('Öznitelik haritası (4×4):', 1); Göster(harita)
  { 2×2 en büyük değer havuzlaması }
  havuz := []
  0'dan 1'e kadar her i için
    havuz.Ekle([Maks([harita[2*i][0], harita[2*i][1], harita[2*i+1][0], harita[2*i+1][1]]),
                Maks([harita[2*i][2], harita[2*i][3], harita[2*i+1][2], harita[2*i+1][3]])])
  yaz('Havuzlamadan sonra (2×2):', 1); Göster(havuz)

  yaz('', 1)
  yaz('--- B) Filtreyi veriden öğrenmek ---', 1)
  Tohum(3)
  sinyal := []
  1'den 30'a kadar her i için sinyal.Ekle(Rastgele(10))
  { Gizli kural: hedef = sinyalin [1, -2, 1] filtresiyle evrişimi. Ağ bu filtreyi bilmiyor. }
  hedef := []; yamalar := []
  0'dan Uzunluk(sinyal) - 3'e kadar her i için
  başla
    yama := [sinyal[i], sinyal[i + 1], sinyal[i + 2]]
    yamalar.Ekle(yama)
    hedef.Ekle(1 * yama[0] - 2 * yama[1] + 1 * yama[2])
  bitir
  P := Tensör(yamalar)                        { 28 × 3: her satır bir yama }
  H := Tensör(hedef)
  w := RastgeleTensör(3, boş, 0.5, doğru)     { öğrenilecek filtre }
  opt := Adam([w], 0.1)
  1'den 300'e kadar her adım için
  başla
    opt.SıfırlaGrad
    kayıp := OKH(P @ w, H)
    kayıp.GeriYay
    opt.Adım
    eğer adım = 1 veya adım mod 100 = 0 ise
      yaz(Biçimle('Adım {:3}: kayıp {:9.4}  filtre ({:.3}, {:.3}, {:.3})', adım, kayıp.Değer, w[0], w[1], w[2]), 1)
  bitir
bitir.

Programın çıktısı şöyledir:

Çıktı
--- A) Hazır filtreyle dikey kenar bulma ---
Öznitelik haritası (4×4):
    27   27    0    0
    27   27    0    0
    27   27    0    0
    27   27    0    0
Havuzlamadan sonra (2×2):
    27    0
    27    0

--- B) Filtreyi veriden öğrenmek ---
Adım   1: kayıp   98.3248  filtre (0.147, 0.525, -0.366)
Adım 100: kayıp    0.0003  filtre (0.999, -1.997, 0.998)
Adım 200: kayıp    0.0000  filtre (1.000, -2.000, 1.000)
Adım 300: kayıp    0.0000  filtre (1.000, -2.000, 1.000)

Ne öğrendik. A bölümünde filtre, koyudan açığa geçilen sütunlarda 27, düz bölgelerde 0 verdi; havuzlamadan sonra bile “kenar solda” bilgisi korunuyor. B bölümü CNN’lerin asıl gücünü gösteriyor: rastgele başlayan filtre, yalnızca girdi–çıktı örneklerine bakarak gizli kuralı [1, -2, 1] tam olarak buldu. Gerçek bir CNN aynı şeyi yüzlerce filtre ve birkaç katmanla yapar. İlk katmanlar kenarları, sonrakiler gözler ve tekerlekler gibi daha karmaşık desenleri öğrenir.

Alıştırma. A bölümünde filtreyi yatay kenar bulacak biçimde ([[-1, -1, -1], [0, 0, 0], [1, 1, 1]]) değiştiriniz. Bu resimde ne çıkar, neden? Sonra üst yarısı koyu, alt yarısı açık bir resim kurup tekrar deneyiniz.

18. Yinelemeli sinir ağı (RNN)

Fikir. Cümleler, ses ve fiyat grafikleri birer dizidir; anlamları sıraya bağlıdır. RNN diziyi eleman eleman okur ve her adımda bir “gizli durum” günceller. Bu durum, ağın o ana kadar okuduklarından aklında tuttuğu özettir. Her adımda aynı ağırlıklar kullanılır, bu yüzden ağ her uzunlukta diziyi okuyabilir.

ht=tanh(xtWx+ht−1Wh+b),ŷ=hTWc+bch_t = \tanh(x_t W_x + h_{t-1} W_h + b), \qquad \hat{y} = h_T W_c + b_c

Yöntem
program YinelemeliAğ
{ Yinelemeli sinir ağı (RNN): bir diziyi eleman eleman okur ve bir "gizli durum" h taşır.
  Her adımda   h ← tanh(x_t · Wx + h @ Wh + b)   hesaplanır; h, o ana kadar okunanların özetidir.
  Dizinin sonunda çıktı h'den üretilir:   ŷ = h @ Wç + bç.
  Görev: 5 sayılık bir diziyi oku, sonunda İLK sayıyı söyle. Bunu yapabilmesi için ağın
  ilk sayıyı 4 adım boyunca gizli durumunda saklamayı öğrenmesi gerekir. }
kullan YZ

sabit
  N: tamsayı = 64          { örnek sayısı }
  UZUNLUK: tamsayı = 5     { dizi uzunluğu }
  GİZLİ: tamsayı = 8       { gizli durum boyutu }

değişken
  diziler: ondalık dizisi dizisi
  sütun: liste
  Xt: liste                       { Xt[t]: tüm örneklerin t. öğesi, N × 1 tensör }
  hedef: tensör
  Wx, Wh, b, Wç, bç: tensör
  opt: herhangi
  kayıp, tahmin: tensör
  i, t, adım: tamsayı

fonksiyon İleri(girdiler: liste; n: tamsayı): tensör
değişken h: tensör; t: tamsayı
başla
  h := Sıfırlar(n, GİZLİ)
  0'dan UZUNLUK - 1'e kadar her t için
    h := (girdiler[t] @ Wx + h @ Wh + b).Tanh
  dön h @ Wç + bç
bitir

fonksiyon Sütunlar(D: ondalık dizisi dizisi): liste
değişken t: tamsayı
başla
  sonuç := []
  0'dan UZUNLUK - 1'e kadar her t için
    sonuç.Ekle(Tensör(Eşle(D, fonksiyon(d: ondalık dizisi) => [d[t]])))
bitir

başla
  Tohum(11)
  diziler := []
  1'den N'ye kadar her i için
  başla
    sütun := []
    1'den UZUNLUK'a kadar her t için sütun.Ekle(Rastgele(200) / 100 - 1)   { [-1, 1) }
    diziler.Ekle(sütun)
  bitir
  Xt := Sütunlar(diziler)
  hedef := Tensör(Eşle(diziler, fonksiyon(d: ondalık dizisi) => [d[0]]))

  Wx := RastgeleTensör(1, GİZLİ, 0.5, doğru)
  Wh := RastgeleTensör(GİZLİ, GİZLİ, 0.3, doğru)
  b  := Sıfırlar(GİZLİ, boş, doğru)
  Wç := RastgeleTensör(GİZLİ, 1, 0.5, doğru)
  bç := Sıfırlar(1, boş, doğru)
  opt := Adam([Wx, Wh, b, Wç, bç], 0.02)

  1'den 600'e kadar her adım için
  başla
    opt.SıfırlaGrad
    kayıp := OKH(İleri(Xt, N), hedef)
    kayıp.GeriYay          { zaman içinde geri yayılım: hata 5 adımın hepsine geri dağıtılır }
    opt.Adım
    eğer adım = 1 veya adım mod 150 = 0 ise
      yaz(Biçimle('Adım {:3}: kayıp {:.4}', adım, kayıp.Değer), 1)
  bitir

  yaz('Hiç görülmemiş diziler:', 1)
  diziler := [[0.8, -0.3, 0.5, -0.9, 0.1], [-0.6, 0.9, 0.9, 0.2, -0.4], [0.1, 0.7, -0.8, 0.6, 0.9]]
  tahmin := İleri(Sütunlar(diziler), 3)
  0'dan 2'ye kadar her i için
    yaz(Biçimle('  {}  →  ağın cevabı {:.2}  (doğrusu {})', diziler[i], tahmin[i, 0], diziler[i][0]), 1)
bitir.

Programın çıktısı şöyledir:

Çıktı
Adım   1: kayıp 0.2896
Adım 150: kayıp 0.0004
Adım 300: kayıp 0.0012
Adım 450: kayıp 0.0006
Adım 600: kayıp 0.0001
Hiç görülmemiş diziler:
  [0.8, -0.3, 0.5, -0.9, 0.1]  →  ağın cevabı 0.79  (doğrusu 0.8)
  [-0.6, 0.9, 0.9, 0.2, -0.4]  →  ağın cevabı -0.59  (doğrusu -0.6)
  [0.1, 0.7, -0.8, 0.6, 0.9]  →  ağın cevabı 0.11  (doğrusu 0.1)

Ne öğrendik. Ağ ilk sayıyı dört adım boyunca “hafızasında” taşımayı öğrendi ve hiç görmediği dizilerde de doğru cevap verdi. Kaybın 300. adımda kısa süre yükselmesi normaldir: Adam bazen bir vadiden taşar, sonra geri döner. Düz RNN’lerin bilinen zayıflığı uzun dizilerdir. Hata geriye doğru yayılırken her adımda küçülür (kaybolan gradyan) ve ağ uzak geçmişi unutur. LSTM ve GRU hücreleri bu sorun için geliştirildi.

Alıştırma. UZUNLUK sabitini 15 yapınız. Kayıp aynı hızla düşüyor mu? Sonra görevi “dizinin toplamını söyle” olarak değiştiriniz; hangisi RNN için daha kolay, neden?

19. Otokodlayıcı

Fikir. Otokodlayıcı girdiyi çıktıda yeniden üretmeyi öğrenir; hedef girdinin kendisi olduğu için etiket gerekmez. İşin püf noktası ortadaki dar boğazdır. Kodlayıcı girdiyi birkaç sayılık bir koda sıkıştırır, çözücü bu koddan girdiyi geri kurar. Boğazdan sığmak için ağ verinin özünü bulmak zorundadır. Bu yüzden otokodlayıcılar sıkıştırmada, gürültü temizlemede ve “bu örnek tuhaf” demek gereken anomali tespitinde kullanılır.

z=f(x),x̂=g(z),minf,gL(x,x̂)z = f(x), \qquad \hat{x} = g(z), \qquad \min_{f,\,g} \; L(x, \hat{x})

Yöntem
program Otokodlayıcı
{ Otokodlayıcı: girdiyi kendisine kopyalamayı öğrenen bir ağ — ama ortada dar bir boğaz vardır.
  Kodlayıcı girdiyi küçük bir koda sıkıştırır, çözücü koddan girdiyi geri kurar.
  Etiket gerekmez: hedef girdinin kendisidir. Boğazdan geçebilmek için ağ verinin özünü yakalamak zorundadır.
  Klasik deney: 8 farklı "tek-sıcak" vektörü 3 sayıya sıkıştırmak (8 = 2³, yani ikili kod yeterli). }
kullan YZ

değişken
  X, kod, geri: tensör
  kodlayıcı, çözücü, opt: herhangi
  kayıp: tensör
  i, adım, isabet: tamsayı

başla
  Tohum(21)
  X := BirSıcak([0, 1, 2, 3, 4, 5, 6, 7], 8)       { 8 × 8 birim matris }
  kodlayıcı := Ardışık([Yoğun(8, 3, 'sigmoid')])
  çözücü := Ardışık([Yoğun(3, 8)])
  opt := Adam(kodlayıcı.Parametreler + çözücü.Parametreler, 0.1)
  1'den 1500'e kadar her adım için
  başla
    opt.SıfırlaGrad
    kayıp := ÇaprazEntropi(çözücü.İleri(kodlayıcı.İleri(X)), X)   { ÇaprazEntropi softmaksı kendisi uygular }
    kayıp.GeriYay
    opt.Adım
    eğer adım = 1 veya adım mod 500 = 0 ise
      yaz(Biçimle('Adım {:4}: yeniden kurma kaybı {:.4}', adım, kayıp.Değer), 1)
  bitir

  kod := kodlayıcı.İleri(X)
  geri := Softmaks(çözücü.İleri(kod))
  yaz('Girdi  →  3 sayılık kod        →  geri kurulan', 1)
  isabet := 0
  0'dan 7'ye kadar her i için
  başla
    yaz(Biçimle('  {}    →  ({:.2}, {:.2}, {:.2})  →  {}', Birleştir(Eşle(Aralık(8), fonksiyon(k: tamsayı) => eğer k = i ise '1' değilse '0'), ''),
      kod[i, 0], kod[i, 1], kod[i, 2], ArgMaks(geri)[i]), 1)
    eğer ArgMaks(geri)[i] = i ise isabet := isabet + 1
  bitir
  yaz('Doğru geri kurulan: ' + isabet + ' / 8', 1)
bitir.

Programın çıktısı şöyledir:

Çıktı
Adım    1: yeniden kurma kaybı 2.1617
Adım  500: yeniden kurma kaybı 0.0050
Adım 1000: yeniden kurma kaybı 0.0015
Adım 1500: yeniden kurma kaybı 0.0007
Girdi  →  3 sayılık kod        →  geri kurulan
  10000000    →  (0.01, 0.35, 0.34)  →  0
  01000000    →  (0.01, 1.00, 1.00)  →  1
  00100000    →  (0.00, 0.00, 1.00)  →  2
  00010000    →  (0.01, 1.00, 0.00)  →  3
  00001000    →  (1.00, 0.01, 0.99)  →  4
  00000100    →  (0.99, 0.99, 0.01)  →  5
  00000010    →  (0.99, 0.99, 0.98)  →  6
  00000001    →  (0.89, 0.01, 0.01)  →  7
Doğru geri kurulan: 8 / 8

Ne öğrendik. Ağ, kimse ona öğretmeden 3 bitlik ikili koda çok benzeyen bir sıkıştırma icat etti: kodların çoğu 0 ve 1’lerden oluşuyor. Sekizinci köşe (0, 0, 0) yerine ilk vektör için (0,01, 0,35, 0,34) gibi bir ara nokta seçti; çözücü bunu da ayırt edebildiği için bir sorun yoktur. Bu örnekte kodlayıcı ve çözücü iki ayrı Ardışık modeldir. Böylece eğitimden sonra kodlayıcıyı tek başına kullanıp her girdinin kodunu alabiliyoruz.

Alıştırma. Boğazı 2 nöron yapınız (Yoğun(8, 2, 'sigmoid') ve Yoğun(2, 8)). 8 vektörün hepsi yine geri kurulabiliyor mu? Kodlar nasıl yerleşiyor?

20. Transformer ve dikkat mekanizması

Fikir. RNN bir cümleyi sırayla okur ve uzak kelimeleri unutabilir. Dikkat mekanizmasında ise her kelime cümledeki bütün kelimelere aynı anda bakar ve hangisine ne kadar önem vereceğine kendisi karar verir. Her kelime üç vektör üretir: sorgu (“ne arıyorum?”), anahtar (“bende ne var?”) ve değer (“ne aktarırım?”). Sorgusu bir anahtara ne kadar benzerse o kelimenin değerinden o kadar çok pay alır. ChatGPT ve Claude gibi dil modellerinin temelindeki Transformer, bu işlemi katman katman yığarak kurulur.

Dikkat(Q,K,V)=softmaks(QK⊤d)V\mathrm{Dikkat}(Q, K, V) = \mathrm{softmaks}\!\left(\frac{Q K^\top}{\sqrt{d}}\right) V

Yöntem
program DikkatMekanizması
{ Transformer'ların kalbi: ölçeklenmiş nokta çarpımı dikkati.
  Her kelime üç vektör üretir: Sorgu (Q: "ne arıyorum?"), Anahtar (K: "bende ne var?"), Değer (V: "ne aktarırım?").
     ağırlıklar = softmaks(Q Kᵀ / √d)      çıktı = ağırlıklar @ V
  Bir kelimenin sorgusu hangi kelimenin anahtarına benziyorsa, o kelimenin değerinden daha çok pay alır.
  Böylece her kelime cümlenin geri kalanından bağlamını toplar.
  Bölüm A: elle seçilmiş küçük vektörlerle "o" zamirinin kime baktığını görelim.
  Bölüm B: Q ve K izdüşümlerini veriden öğrenerek dikkatin bir görevi çözmesini sağlayalım. }
kullan YZ, Matematik

değişken
  kelimeler: metin dizisi
  E, Wq, Wk, Q, K, V, A: tensör
  i, adım: tamsayı
  kayıp: tensör
  opt: herhangi
  girdi, hedef, çıkış: tensör

fonksiyon Dikkat(Q, K: tensör): tensör
başla
  dön Softmaks(Q @ K.T / Kök(K.Şekil[1]))
bitir

başla
  yaz('--- A) "kedi sütü içti çünkü o acıkmıştı" ---', 1)
  kelimeler := ['kedi', 'sütü', 'içti', 'çünkü', 'o', 'acıkmıştı']
  { 4 boyutlu temsiller: [canlı mı, nesne mi, eylem mi, zamir mi] }
  E := Tensör([[1.0, 0.0, 0.0, 0.0],
               [0.0, 1.0, 0.0, 0.0],
               [0.0, 0.2, 1.0, 0.0],
               [0.0, 0.0, 0.3, 0.0],
               [0.3, 0.0, 0.0, 1.0],
               [0.4, 0.0, 1.0, 0.0]])
  { Elle kurulmuş izdüşümler: zamir özelliği olan kelimeler "canlı" bir şey arasın }
  Wq := Tensör([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [3, 0, 0, 0]])
  Wk := Tensör([[3, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]])
  Q := E @ Wq
  K := E @ Wk
  A := Dikkat(Q, K)
  yaz('Dikkat ağırlıkları (satır: bakan kelime, sütun: bakılan kelime)', 1)
  yaz('            ' + Birleştir(Eşle(kelimeler, fonksiyon(k: metin) => Biçimle('{:10}', k)), ''), 1)
  0'dan 5'e kadar her i için
    yaz(Biçimle('{:11} ', kelimeler[i]) + Birleştir(Eşle(Aralık(6), fonksiyon(k: tamsayı) => Biçimle('{:10.2}', A[i, k])), ''), 1)
  yaz('"o" en çok şuna bakıyor: ' + kelimeler[ArgMaks(A.Satır(4))], 1)

  yaz('', 1)
  yaz('--- B) Dikkati öğrenmek: "her konum bir sonraki konuma baksın" ---', 1)
  { 4 konum; her konumun temsili kendi konumunun tek-sıcak vektörü. Hedef: her konum,
    bir sonraki konumun değerini getirsin (sonuncu başa dönsün). Wq ve Wk rastgele başlar. }
  Tohum(8)
  girdi := BirSıcak([0, 1, 2, 3], 4)
  { V'nin her sütunu ayrı bir eğitim örneği (aynı konumlar, farklı değerler).
    Tek örnekle ağ "hileli" karışımlar bulabilir; çeşitli örnekler gerçek kuralı zorlar. }
  V := Tensör([[5, 2, 0], [1, 8, 4], [7, 3, 9], [3, 6, 1]])
  hedef := Tensör([[1, 8, 4], [7, 3, 9], [3, 6, 1], [5, 2, 0]])
  Wq := RastgeleTensör(4, 4, 0.5, doğru)
  Wk := RastgeleTensör(4, 4, 0.5, doğru)
  opt := Adam([Wq, Wk], 0.1)
  1'den 300'e kadar her adım için
  başla
    opt.SıfırlaGrad
    çıkış := Dikkat(girdi @ Wq, girdi @ Wk) @ V
    kayıp := OKH(çıkış, hedef)
    kayıp.GeriYay
    opt.Adım
    eğer adım = 1 veya adım mod 100 = 0 ise
      yaz(Biçimle('Adım {:3}: kayıp {:.4}  1. örneğin çıktıları {}', adım, kayıp.Değer,
        Eşle(Aralık(4), fonksiyon(k: tamsayı) => Yuvarla(çıkış[k, 0], 2))), 1)
  bitir
  A := Dikkat(girdi @ Wq, girdi @ Wk)
  yaz('1. örneğin hedefi [1, 7, 3, 5]. Öğrenilen dikkat ağırlıkları:', 1)
  0'dan 3'e kadar her i için
    yaz(Biçimle('  konum {} → ', i) + Birleştir(Eşle(Aralık(4), fonksiyon(k: tamsayı) => Biçimle('{:6.2}', A[i, k])), ''), 1)
  { Öğrenilen şey değerler değil bakılacak yer: yepyeni değerlerle de çalışır }
  V := Tensör([[10.0], [20.0], [30.0], [40.0]])
  çıkış := A @ V
  yaz('Yeni değerler [10, 20, 30, 40] → ' + Eşle(Aralık(4), fonksiyon(k: tamsayı) => Yuvarla(çıkış[k, 0], 1)), 1)
bitir.

Programın çıktısı şöyledir:

Çıktı
--- A) "kedi sütü içti çünkü o acıkmıştı" ---
Dikkat ağırlıkları (satır: bakan kelime, sütun: bakılan kelime)
                  kedi      sütü      içti     çünkü         o acıkmıştı
       kedi       0.17      0.17      0.17      0.17      0.17      0.17
       sütü       0.17      0.17      0.17      0.17      0.17      0.17
       içti       0.13      0.13      0.22      0.16      0.13      0.22
      çünkü       0.16      0.16      0.18      0.16      0.16      0.18
          o       0.87      0.01      0.01      0.01      0.04      0.06
  acıkmıştı       0.13      0.13      0.22      0.16      0.13      0.22
"o" en çok şuna bakıyor: kedi

--- B) Dikkati öğrenmek: "her konum bir sonraki konuma baksın" ---
Adım   1: kayıp 7.1553  1. örneğin çıktıları [3.73, 4.06, 3.93, 3.99]
Adım 100: kayıp 0.0001  1. örneğin çıktıları [1.0, 7.0, 3.0, 5.0]
Adım 200: kayıp 0.0000  1. örneğin çıktıları [1.0, 7.0, 3.0, 5.0]
Adım 300: kayıp 0.0000  1. örneğin çıktıları [1.0, 7.0, 3.0, 5.0]
1. örneğin hedefi [1, 7, 3, 5]. Öğrenilen dikkat ağırlıkları:
  konum 0 →   0.00  1.00  0.00  0.00
  konum 1 →   0.00  0.00  1.00  0.00
  konum 2 →   0.01  0.01  0.00  0.99
  konum 3 →   1.00  0.00  0.00  0.00
Yeni değerler [10, 20, 30, 40] → [20.0, 30.0, 39.7, 10.0]

Ne öğrendik. A bölümünde “o” zamiri ağırlığının %87’sini “kedi”ye verdi, yani kimin acıktığını bağlamdan çıkardı. Sorgusu boş olan kelimeler (kedi, sütü) herkese eşit (1/6 ≈ 0,17) baktı. B bölümünde ağ, rastgele başlayan Wq ve Wk’den “bir sonrakine bak” desenini öğrendi. Öğrenilen şey değerler değil nereye bakılacağı olduğu için desen yepyeni değerlerle de çalışıyor. Programdaki yoruma da dikkat etmekte yarar vardır: tek bir örnekle eğitilen ağ, hedefi tutturan ama genellemeyen “hileli” karışımlar bulabilir. V’yi tek sütuna indirip deneyerek bunu kendiniz görebilirsiniz. Gerçek bir Transformer bu işlemi birden fazla “kafa” ile paralel yapar, araya Yoğun katmanlar koyar ve onlarca kez üst üste yığar.

Alıştırma. B bölümünde hedefi “bir önceki konuma bak” olarak değiştiriniz (hedef satırlarını bir aşağı kaydırınız). Ardından V’yi tek sütun [[5], [1], [7], [3]] yapıp eğitiniz. Öğrenilen ağırlıklar ve yeni değerlerle sonuç nasıl değişiyor?