Ana içeriğe geç

Orta8 dk

Attention'ın Python'la adım adım kurulumu

Önkoşul:Self-attention mekaniği (Q, K, V)

Kanca

  1. derste attention’ın adımlarını TEK TEK, ayrı ayrı kod hücrelerinde gördük. Bu ders bunları TEK bir fonksiyonda toplayıp, sonucunu PyTorch’un KENDİ yerleşik fonksiyonuyla KARŞILAŞTIRACAĞIZ — ELLE yazdığımız kod DOĞRU mu?

Sezgi

Gerçek kütüphanelerin (PyTorch, Transformers, …) İÇİNDE, self-attention özünde SADECE birkaç satır: nokta çarpım, ölçekleme, softmax, ağırlıklı toplam. Karmaşık görünen bir mekanizma, aslında KÜÇÜK bir fonksiyona sığıyor — ve bunu KENDİMİZ yazıp SAYISAL olarak doğrulayabiliriz (6. dersteki “elle RNN vs nn.RNN” doğrulamasıyla AYNI yöntem).

Mekanizma

Yeniden kullanılabilir dikkat() fonksiyonu

# 10. derste attention'ın adımlarını TEK TEK gördük. Şimdi bunları TEK, YENİDEN
# KULLANILABİLİR bir fonksiyonda toplayalım -- gerçek kütüphanelerin (PyTorch,
# Transformers) İÇİNDE de, özünde, AYNI birkaç satır var.
def dikkat(Q, K, V, maske=None):
    d_k = Q.shape[-1]
    puanlar = (Q @ K.transpose(-2, -1)) / (d_k ** 0.5)
    if maske is not None:
        puanlar = puanlar.masked_fill(maske == 0, float("-inf"))
    agirliklar = F.softmax(puanlar, dim=-1)
    cikti = agirliklar @ V
    return cikti, agirliklar

N, D = 5, 8
Q = torch.randn(N, D)
K = torch.randn(N, D)
V = torch.randn(N, D)

cikti, agirliklar = dikkat(Q, K, V)
print(f"Fonksiyon: {N} token x {D} boyut girdisiyle çağrıldı.")
print(f"Çıktı şekli: {tuple(cikti.shape)}, dikkat matrisi şekli: {tuple(agirliklar.shape)}")
print(f"Her satırın toplamı 1 mi? {torch.allclose(agirliklar.sum(-1), torch.ones(N))}")
Fonksiyon: 5 token x 8 boyut girdisiyle çağrıldı.
Çıktı şekli: (5, 8), dikkat matrisi şekli: (5, 5)
Her satırın toplamı 1 mi? True

Şimdi bu fonksiyonu, PyTorch’un torch.nn.functional.scaled_dot_product_attention’ıyla KARŞILAŞTIRALIM:

Sayısal doğrulama

# Bu ELLE yazdığımız fonksiyon, PyTorch'un YERLEŞİK scaled_dot_product_attention'ı
# ile AYNI sonucu üretiyor mu? (6. dersteki "elle RNN vs nn.RNN" doğrulamasıyla AYNI mantık.)
cikti_elle, agirliklar_elle = dikkat(Q, K, V)
cikti_pytorch = F.scaled_dot_product_attention(Q, K, V)

fark = (cikti_elle - cikti_pytorch).abs().max().item()
print(f"\nElle yazılan fonksiyon ile torch.nn.functional.scaled_dot_product_attention arasındaki")
print(f"MAKSİMUM fark: {fark:.2e}")
print(f"torch.allclose ile eşleşiyor mu? {torch.allclose(cikti_elle, cikti_pytorch, atol=1e-6)}")

Elle yazılan fonksiyon ile torch.nn.functional.scaled_dot_product_attention arasındaki
MAKSİMUM fark: 2.38e-07
torch.allclose ile eşleşiyor mu? True

Fark 2.4e-07 — kayan nokta (floating point) yuvarlama hassasiyetinin İÇİNDE, yani PRATİKTE SIFIR. Elle yazdığımız fonksiyon, kütüphanenin yaptığının AYNISINI yapıyor.

Matematik

Nedensel (causal) maske
MaskeliPuanij={puanijjij>i\text{MaskeliPuan}_{ij} = \begin{cases} \text{puan}_{ij} & j \le i \\ -\infty & j > i \end{cases}

-\infty‘a ayarlanan bir puan, softmax’tan geçtikten sonra TAM OLARAK 00 ağırlık üretir (e=0e^{-\infty} = 0) — yani o token’a HİÇ dikkat edilmez. Bu basit numara, bir modelin GELECEĞİ görmesini engellemenin standart yoludur ve torch.tril (alt üçgen matris) ile tek satırda üretilir.

Kod

Neden böyle bir maskeye ihtiyaç duyarız? Bir dil modeli, bir sonraki kelimeyi TAHMİN ederken, o kelimeyi (ve sonrasını) ZATEN GÖRMÜŞ olmamalı — yoksa “tahmin” değil, “kopyalama” yapar (17-18. ders, GPT’nin temeli).

Nedensel maske: geleceği gizle

# Bir GERÇEK kullanım: bazı token'ların BİRBİRİNİ görmesini ENGELLEMEK isteyebiliriz --
# örn. bir dil modeli, GELECEKTEKİ kelimeyi GÖREMEMELİ (17-18. ders, GPT'nin temeli).
# Buna "nedensel maske" (causal mask) denir: her token, sadece KENDİSİNDEN ÖNCEKİLERİ görebilir.
nedensel_maske = torch.tril(torch.ones(N, N))
print(f"\nNedensel maske (1=görebilir, 0=göremez):")
for satir in nedensel_maske.tolist():
    print(" ", [int(v) for v in satir])

_, agirliklar_maskeli = dikkat(Q, K, V, maske=nedensel_maske)
print(f"\nMaskesiz durumda token 0, token 4'e bakma ağırlığı: {agirliklar[0, 4].item():.4f}")
print(f"Maskeli durumda   token 0, token 4'e bakma ağırlığı: {agirliklar_maskeli[0, 4].item():.4f}")
print("Token 0, kendisinden SONRA gelen token 4'ü artık HİÇ 'göremiyor' -- ağırlık TAM SIFIR.")
print(f"\nMaskeli token 4'ün ağırlıkları (TÜM geçmişi görebilir): {[round(v,3) for v in agirliklar_maskeli[4].tolist()]}")
print(f"Maskesiz token 4'ün ağırlıkları (karşılaştırma için):    {[round(v,3) for v in agirliklar[4].tolist()]}")

Nedensel maske (1=görebilir, 0=göremez):
  [1, 0, 0, 0, 0]
  [1, 1, 0, 0, 0]
  [1, 1, 1, 0, 0]
  [1, 1, 1, 1, 0]
  [1, 1, 1, 1, 1]

Maskesiz durumda token 0, token 4'e bakma ağırlığı: 0.2560
Maskeli durumda   token 0, token 4'e bakma ağırlığı: 0.0000
Token 0, kendisinden SONRA gelen token 4'ü artık HİÇ 'göremiyor' -- ağırlık TAM SIFIR.

Maskeli token 4'ün ağırlıkları (TÜM geçmişi görebilir): [0.062, 0.156, 0.247, 0.494, 0.041]
Maskesiz token 4'ün ağırlıkları (karşılaştırma için):    [0.062, 0.156, 0.247, 0.494, 0.041]
Maskesiz ve nedensel maskeli self-attention matrislerini yan yana gösteren iki ısı haritası; maskeli versiyonda üst üçgen tamamen sıfır (boş).
Solda: maskesiz self-attention, her token her token'ı görebilir. Sağda: nedensel maske sonrası, üst üçgen (gelecek) tamamen sıfırlanmış.

İlk token (0), 4. token’a bakma ağırlığını 0.256’dan 0.000’a düşürüyor — artık kendisinden SONRAKİ hiçbir şeyi “göremiyor”. Son token (4) ise zaten TÜM geçmişi görebildiği için ağırlıkları DEĞİŞMİYOR.

Nerede işe yarar

Bu iki fikir — yeniden kullanılabilir bir fonksiyon ve sayısal doğrulama — ML mühendisliğinin GÜNLÜK pratiği:

  • Gerçek kütüphaneler bu fonksiyonu SizinLE AYNI şekilde yazmaz — GPU’ya özel, bellek-verimli (flash attention gibi) versiyonlar kullanırlar — ama MATEMATİKSEL sonuç AYNIDIR, doğruladığımız gibi.
  • Maskeleme, SADECE nedensel (causal) modellerde gerekli DEĞİL — dolgu (padding) token’larını gizlemek için de AYNI mekanizma kullanılır (kısa cümleleri uzatmak için eklenen “boş” token’lar).
  • Bu fonksiyon, 12. derste “çoklu başlık” (multi-head) için TEKRAR TEKRAR çağrılacak — her başlık, AYNI dikkat() fonksiyonunu FARKLI Q/K/V projeksiyonlarıyla çalıştırır.

Bu 2 hatayı yaparsın:

  1. Maskelemeyi softmax’tan SONRA (ağırlıkları sıfırlayarak) yapmak — YANLIŞ, çünkü kalan ağırlıklar 1’e TOPLANMAZ; maskeleme softmax’tan ÖNCE, -\infty ile yapılmalı.
  2. “Sayısal doğrulama” adımını gereksiz bir formalite sanmak — ELLE yazılan kodda TEK bir işaret hatası (örn. Q @ K.T yerine K @ Q.T), sonucu SESSİZCE yanlış üretir; torch.allclose gibi bir kontrol olmadan bu fark edilmeyebilir.

Kendini test et

1. Elle yazılan dikkat() fonksiyonu ile PyTorch'un scaled_dot_product_attention'ı arasındaki 2.4e-07'lik fark NEYİ gösteriyor?
  1. Elle yazılan kodda bir hata olduğunu
  2. İki fonksiyonun MATEMATİKSEL olarak AYNI işlemi yaptığını -- kalan fark, kayan nokta (floating point) yuvarlama hassasiyetinin içinde, yani pratikte sıfır (doğru cevap)
  3. PyTorch'un daha yavaş olduğunu
  4. Sonuçların karşılaştırılamayacağını

Neden: torch.allclose(..., atol=1e-6) True döndürdü -- bu, iki hesaplamanın (6. dersteki 'elle RNN vs nn.RNN' doğrulamasıyla AYNI standartta) matematiksel olarak eşdeğer olduğunu KANITLAR.

2. Nedensel (causal) maske NEDEN softmax'tan ÖNCE, ağırlıkları SIFIRLAMAK yerine puanları -∞ yaparak uygulanır?
  1. Hesaplama hızını artırmak için
  2. Softmax sonrası sıfırlama, kalan ağırlıkların TOPLAMINI 1'den küçük bırakır (geçerli bir olasılık dağılımı OLMAZ); -∞, softmax İÇİNDE otomatik olarak TAM 0 ağırlığa dönüşür ve kalanlar hâlâ 1'e toplanır (doğru cevap)
  3. İkisi arasında matematiksel bir fark yoktur
  4. Bellek tasarrufu için

Neden: maskeleme-fikri bloğunda masked_fill ile puanlar -∞ yapılıyor, SONRA softmax uygulanıyor -- bu sırayla, maskeli olmayan ağırlıklar hâlâ toplamda 1 olan geçerli bir dağılım oluşturuyor.

3. Notebook'ta token 0'ın token 4'e (gelecekteki bir kelimeye) bakma ağırlığı maskeleme sonrası 0.256'dan 0.000'a düşerken, token 4'ün ağırlıkları HİÇ DEĞİŞMİYOR. Bunun nedeni ne?
  1. Bu bir hesaplama hatasıdır
  2. Nedensel maskede her token SADECE kendisinden ÖNCEKİLERİ (ve kendisini) görebilir; token 4 dizinin SON elemanı olduğu için zaten TÜM geçmişi (0-4 arası her şeyi) görebiliyordu, maske onun için hiçbir şeyi engellemiyor (doğru cevap)
  3. Token 4'ün ağırlıkları rastgele sabitlenmiştir
  4. Maske sadece ilk token için çalışır

Neden: torch.tril ile üretilen maskenin son satırı TAMAMEN 1'lerden oluşur (nedensel_maske bloğunda görüldüğü gibi) -- yani son token için HİÇBİR pozisyon maskelenmez, bu yüzden ağırlıkları maskesiz duruma özdeş kalır.

Özet

Özet

  • Self-attention'ın tüm adımları (Q·Kᵀ, ölçekleme, softmax, ağırlıklı toplam) TEK bir yeniden kullanılabilir fonksiyona sığar.
  • Elle yazılan fonksiyon, torch.nn.functional.scaled_dot_product_attention ile 2.4e-07 farkla (pratikte sıfır) eşleşiyor -- sayısal doğrulama, ML mühendisliğinin standart pratiği.
  • Nedensel (causal) maske, puanları softmax ÖNCESİ -∞ yaparak bir token'ın GELECEĞİ görmesini engeller.
  • torch.tril ile üretilen alt üçgen matris, "sadece geçmişi gör" kuralını TEK satırda kodlar.
  • Bu dikkat() fonksiyonu, 12. derste multi-head attention'ın YAPI TAŞI olacak.
Sonraki adım: Multi-head attention →