4. Dikkat Mekanizmaları

Reading time: 11 minutes

Dikkat Mekanizmaları ve Sinir Ağlarındaki Kendine Dikkat

Dikkat mekanizmaları, sinir ağlarının her çıktı parçasını oluştururken girdi verisinin belirli kısımlarına odaklanmasını sağlar. Farklı girdilere farklı ağırlıklar atayarak, modelin mevcut görevle en ilgili girdileri belirlemesine yardımcı olur. Bu, makine çevirisi gibi görevlerde, tüm cümlenin bağlamını anlamanın doğru çeviri için gerekli olduğu durumlarda kritik öneme sahiptir.

tip

Bu dördüncü aşamanın amacı çok basit: Bazı dikkat mekanizmaları uygulamak. Bunlar, LLM'yi eğitmek için kullanılan mevcut cümledeki bir kelimenin komşularıyla olan ilişkisini yakalayacak çok sayıda tekrarlanan katman olacak.
Bunun için çok sayıda katman kullanılır, bu nedenle çok sayıda eğitilebilir parametre bu bilgiyi yakalayacaktır.

Dikkat Mekanizmalarını Anlamak

Dil çevirisi için kullanılan geleneksel sıralı modellemelerde, model bir girdi dizisini sabit boyutlu bir bağlam vektörüne kodlar. Ancak, bu yaklaşım uzun cümlelerle başa çıkmakta zorlanır çünkü sabit boyutlu bağlam vektörü gerekli tüm bilgileri yakalayamayabilir. Dikkat mekanizmaları, modelin her çıktı token'ını oluştururken tüm girdi token'larını dikkate almasına olanak tanıyarak bu sınırlamayı aşar.

Örnek: Makine Çevirisi

Almanca "Kannst du mir helfen diesen Satz zu übersetzen" cümlesini İngilizceye çevirmeyi düşünün. Kelime kelime çeviri, diller arasındaki dilbilgisel yapı farklılıkları nedeniyle gramer açısından doğru bir İngilizce cümle üretmez. Bir dikkat mekanizması, modelin çıktı cümlesinin her kelimesini oluştururken girdi cümlesinin ilgili kısımlarına odaklanmasını sağlar ve bu da daha doğru ve tutarlı bir çeviri ile sonuçlanır.

Kendine Dikkate Giriş

Kendine dikkat, ya da intra-dikkat, dikkat mekanizmasının tek bir dizide uygulanarak o dizinin bir temsilini hesapladığı bir mekanizmadır. Bu, dizideki her token'ın diğer tüm token'lara dikkat etmesine olanak tanır ve modelin token'lar arasındaki bağımlılıkları, dizideki mesafelerine bakılmaksızın yakalamasına yardımcı olur.

Temel Kavramlar

  • Token'lar: Girdi dizisinin bireysel elemanları (örneğin, bir cümledeki kelimeler).
  • Gömme: Token'ların vektör temsilleri, anlamsal bilgiyi yakalar.
  • Dikkat Ağırlıkları: Her token'ın diğerlerine göre önemini belirleyen değerler.

Dikkat Ağırlıklarını Hesaplama: Adım Adım Bir Örnek

"Hello shiny sun!" cümlesini ele alalım ve her kelimeyi 3 boyutlu bir gömme ile temsil edelim:

  • Hello: [0.34, 0.22, 0.54]
  • shiny: [0.53, 0.34, 0.98]
  • sun: [0.29, 0.54, 0.93]

Amacımız, shiny kelimesi için kendine dikkat kullanarak bağlam vektörünü hesaplamaktır.

Adım 1: Dikkat Puanlarını Hesapla

tip

Sadece sorgunun her boyut değerini ilgili token'ınki ile çarpın ve sonuçları toplayın. Her token çifti için 1 değer elde edersiniz.

Cümledeki her kelime için, shiny ile ilgili dikkat puanını, gömmelerinin nokta çarpımını hesaplayarak hesaplayın.

"Hello" ile "shiny" arasındaki Dikkat Puanı

"shiny" ile "shiny" arasındaki Dikkat Puanı

"sun" ile "shiny" arasındaki Dikkat Puanı

Adım 2: Dikkat Puanlarını Normalleştirerek Dikkat Ağırlıklarını Elde Et

tip

Matematik terimlerinde kaybolmayın, bu fonksiyonun amacı basit, tüm ağırlıkları normalleştirin ki toplamları 1 olsun.
Ayrıca, softmax fonksiyonu, üstel kısım nedeniyle farklılıkları vurguladığı için, yararlı değerleri tespit etmeyi kolaylaştırır.

Dikkat puanlarına softmax fonksiyonunu uygulayarak, toplamı 1 olan dikkat ağırlıklarına dönüştürün.

Üstel değerleri hesaplama:

Toplamı hesaplama:

Dikkat ağırlıklarını hesaplama:

Adım 3: Bağlam Vektörünü Hesapla

tip

Her dikkat ağırlığını alın ve ilgili token boyutlarıyla çarpın, ardından tüm boyutları toplayarak sadece 1 vektör (bağlam vektörü) elde edin.

Bağlam vektörü, tüm kelimelerin gömmelerinin ağırlıklı toplamı olarak hesaplanır, dikkat ağırlıklarını kullanarak.

Her bileşeni hesaplama:

  • "Hello" için Ağırlıklı Gömme:
  • "shiny" için Ağırlıklı Gömme:
  • "sun" için Ağırlıklı Gömme:

Ağırlıklı gömmeleri toplama:

bağlam vektörü=[0.0779+0.2156+0.1057, 0.0504+0.1382+0.1972, 0.1237+0.3983+0.3390]=[0.3992,0.3858,0.8610]

Bu bağlam vektörü, "shiny" kelimesi için zenginleştirilmiş gömme temsilini, cümledeki tüm kelimelerden gelen bilgileri içerecek şekilde temsil eder.

Sürecin Özeti

  1. Dikkat Puanlarını Hesapla: Hedef kelimenin gömmesi ile dizideki tüm kelimelerin gömmeleri arasındaki nokta çarpımını kullanın.
  2. Ağırlıkları Elde Etmek için Puanları Normalleştir: Dikkat puanlarına softmax fonksiyonunu uygulayarak toplamı 1 olan ağırlıklar elde edin.
  3. Bağlam Vektörünü Hesapla: Her kelimenin gömmesini dikkat ağırlığı ile çarpın ve sonuçları toplayın.

Eğitilebilir Ağırlıklarla Kendine Dikkat

Pratikte, kendine dikkat mekanizmaları, sorgular, anahtarlar ve değerler için en iyi temsilleri öğrenmek amacıyla eğitilebilir ağırlıklar kullanır. Bu, üç ağırlık matrisinin tanıtılmasını içerir:

Sorgu, daha önce olduğu gibi kullanılacak veridir, anahtarlar ve değerler matrisleri ise sadece rastgele eğitilebilir matrislerdir.

Adım 1: Sorguları, Anahtarları ve Değerleri Hesapla

Her token, tanımlı matrislerle boyut değerlerini çarparak kendi sorgu, anahtar ve değer matrisine sahip olacaktır:

Bu matrisler, orijinal gömmeleri dikkat hesaplamaları için uygun yeni bir alana dönüştürür.

Örnek

Varsayalım ki:

  • Girdi boyutu din=3 (gömme boyutu)
  • Çıktı boyutu dout=2 (sorgular, anahtarlar ve değerler için istenen boyut)

Ağırlık matrislerini başlatın:

python
import torch.nn as nn

d_in = 3
d_out = 2

W_query = nn.Parameter(torch.rand(d_in, d_out))
W_key = nn.Parameter(torch.rand(d_in, d_out))
W_value = nn.Parameter(torch.rand(d_in, d_out))

Sorguları, anahtarları ve değerleri hesaplayın:

python
queries = torch.matmul(inputs, W_query)
keys = torch.matmul(inputs, W_key)
values = torch.matmul(inputs, W_value)

Adım 2: Ölçeklenmiş Nokta-Ürün Dikkatini Hesapla

Dikkat Puanlarını Hesapla

Önceki örneğe benzer, ancak bu sefer, token'ların boyutlarının değerlerini kullanmak yerine, token'ın anahtar matrisini (zaten boyutlar kullanılarak hesaplanmış) kullanıyoruz:. Yani, her sorgu qi​ ve anahtar kj​ için:

Puanları Ölçekle

Nokta ürünlerinin çok büyük olmasını önlemek için, bunları anahtar boyutunun karekökü dk​ ile ölçeklendir:

tip

Puan, boyutların karekökü ile bölünür çünkü nokta ürünleri çok büyük hale gelebilir ve bu, onları düzenlemeye yardımcı olur.

Dikkat Ağırlıklarını Elde Etmek İçin Softmax Uygula: İlk örnekte olduğu gibi, tüm değerleri normalize et, böylece toplamları 1 olur.

Adım 3: Bağlam Vektörlerini Hesapla

İlk örnekte olduğu gibi, sadece tüm değer matrislerini topla ve her birini dikkat ağırlığı ile çarp:

Kod Örneği

https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb adresinden bir örnek alarak, bahsettiğimiz kendine dikkat işlevselliğini uygulayan bu sınıfı kontrol edebilirsiniz:

python
import torch

inputs = torch.tensor(
[[0.43, 0.15, 0.89], # Your     (x^1)
[0.55, 0.87, 0.66], # journey  (x^2)
[0.57, 0.85, 0.64], # starts   (x^3)
[0.22, 0.58, 0.33], # with     (x^4)
[0.77, 0.25, 0.10], # one      (x^5)
[0.05, 0.80, 0.55]] # step     (x^6)
)

import torch.nn as nn
class SelfAttention_v2(nn.Module):

def __init__(self, d_in, d_out, qkv_bias=False):
super().__init__()
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key   = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)

def forward(self, x):
keys = self.W_key(x)
queries = self.W_query(x)
values = self.W_value(x)

attn_scores = queries @ keys.T
attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)

context_vec = attn_weights @ values
return context_vec

d_in=3
d_out=2
torch.manual_seed(789)
sa_v2 = SelfAttention_v2(d_in, d_out)
print(sa_v2(inputs))

tip

Matrisleri rastgele değerlerle başlatmak yerine, tüm ağırlıkları eğitilecek parametreler olarak işaretlemek için nn.Linear kullanıldığını unutmayın.

Nedensel Dikkat: Gelecek Kelimeleri Gizleme

LLM'ler için modelin, bir sonraki token'ı tahmin etmek amacıyla mevcut pozisyondan önceki token'ları dikkate almasını istiyoruz. Nedensel dikkat, aynı zamanda maskelenmiş dikkat olarak da bilinir, dikkat mekanizmasını değiştirerek gelecekteki token'lara erişimi engelleyerek bunu başarır.

Nedensel Dikkat Maskesi Uygulama

Nedensel dikkati uygulamak için, dikkat puanlarına softmax işlemi öncesinde bir maske uygularız, böylece kalanlar hala 1'e toplamış olur. Bu maske, gelecekteki token'ların dikkat puanlarını negatif sonsuzluğa ayarlayarak, softmax'tan sonra dikkat ağırlıklarının sıfır olmasını sağlar.

Adımlar

  1. Dikkat Puanlarını Hesapla: Önceki gibi.
  2. Maske Uygula: Diyagonalın üstünde negatif sonsuzlukla doldurulmuş bir üst üçgen matris kullanın.
python
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) * float('-inf')
masked_scores = attention_scores + mask
  1. Softmax Uygula: Maskelenmiş puanları kullanarak dikkat ağırlıklarını hesaplayın.
python
attention_weights = torch.softmax(masked_scores, dim=-1)

Ek Dikkat Ağırlıklarını Dropout ile Maskeleme

Aşırı uyum sağlamayı önlemek için, softmax işleminden sonra dikkat ağırlıklarına dropout uygulayabiliriz. Dropout, eğitim sırasında dikkat ağırlıklarının bazılarını rastgele sıfırlar.

python
dropout = nn.Dropout(p=0.5)
attention_weights = dropout(attention_weights)

Bir normal dropout yaklaşık %10-20'dir.

Kod Örneği

Kod örneği https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb:

python
import torch
import torch.nn as nn

inputs = torch.tensor(
[[0.43, 0.15, 0.89], # Your     (x^1)
[0.55, 0.87, 0.66], # journey  (x^2)
[0.57, 0.85, 0.64], # starts   (x^3)
[0.22, 0.58, 0.33], # with     (x^4)
[0.77, 0.25, 0.10], # one      (x^5)
[0.05, 0.80, 0.55]] # step     (x^6)
)

batch = torch.stack((inputs, inputs), dim=0)
print(batch.shape)

class CausalAttention(nn.Module):

def __init__(self, d_in, d_out, context_length,
dropout, qkv_bias=False):
super().__init__()
self.d_out = d_out
self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key   = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
self.dropout = nn.Dropout(dropout)
self.register_buffer('mask', torch.triu(torch.ones(context_length, context_length), diagonal=1)) # New

def forward(self, x):
b, num_tokens, d_in = x.shape
# b is the num of batches
# num_tokens is the number of tokens per batch
# d_in is the dimensions er token

keys = self.W_key(x) # This generates the keys of the tokens
queries = self.W_query(x)
values = self.W_value(x)

attn_scores = queries @ keys.transpose(1, 2) # Moves the third dimension to the second one and the second one to the third one to be able to multiply
attn_scores.masked_fill_(  # New, _ ops are in-place
self.mask.bool()[:num_tokens, :num_tokens], -torch.inf)  # `:num_tokens` to account for cases where the number of tokens in the batch is smaller than the supported context_size
attn_weights = torch.softmax(
attn_scores / keys.shape[-1]**0.5, dim=-1
)
attn_weights = self.dropout(attn_weights)

context_vec = attn_weights @ values
return context_vec

torch.manual_seed(123)

context_length = batch.shape[1]
d_in = 3
d_out = 2
ca = CausalAttention(d_in, d_out, context_length, 0.0)

context_vecs = ca(batch)

print(context_vecs)
print("context_vecs.shape:", context_vecs.shape)

Tek Başlı Dikkati Çok Başlı Dikkate Genişletme

Çok başlı dikkat, pratikte kendi ağırlıkları olan birden fazla örneğin kendine dikkat fonksiyonunu çalıştırmasından oluşur, böylece farklı son vektörler hesaplanır.

Kod Örneği

Önceki kodu yeniden kullanmak ve sadece birkaç kez çalıştıran bir sarmalayıcı eklemek mümkün olabilir, ancak bu, tüm başları aynı anda işleyen https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb adresinden daha optimize edilmiş bir versiyondur (pahalı for döngülerinin sayısını azaltır). Kodda görebileceğiniz gibi, her bir token'ın boyutları baş sayısına göre farklı boyutlara bölünmüştür. Bu şekilde, eğer token 8 boyuta sahipse ve 3 baş kullanmak istiyorsak, boyutlar 4 boyuttan oluşan 2 diziye bölünecek ve her baş bunlardan birini kullanacaktır:

python
class MultiHeadAttention(nn.Module):
def __init__(self, d_in, d_out, context_length, dropout, num_heads, qkv_bias=False):
super().__init__()
assert (d_out % num_heads == 0), \
"d_out must be divisible by num_heads"

self.d_out = d_out
self.num_heads = num_heads
self.head_dim = d_out // num_heads # Reduce the projection dim to match desired output dim

self.W_query = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_key = nn.Linear(d_in, d_out, bias=qkv_bias)
self.W_value = nn.Linear(d_in, d_out, bias=qkv_bias)
self.out_proj = nn.Linear(d_out, d_out)  # Linear layer to combine head outputs
self.dropout = nn.Dropout(dropout)
self.register_buffer(
"mask",
torch.triu(torch.ones(context_length, context_length),
diagonal=1)
)

def forward(self, x):
b, num_tokens, d_in = x.shape
# b is the num of batches
# num_tokens is the number of tokens per batch
# d_in is the dimensions er token

keys = self.W_key(x) # Shape: (b, num_tokens, d_out)
queries = self.W_query(x)
values = self.W_value(x)

# We implicitly split the matrix by adding a `num_heads` dimension
# Unroll last dim: (b, num_tokens, d_out) -> (b, num_tokens, num_heads, head_dim)
keys = keys.view(b, num_tokens, self.num_heads, self.head_dim)
values = values.view(b, num_tokens, self.num_heads, self.head_dim)
queries = queries.view(b, num_tokens, self.num_heads, self.head_dim)

# Transpose: (b, num_tokens, num_heads, head_dim) -> (b, num_heads, num_tokens, head_dim)
keys = keys.transpose(1, 2)
queries = queries.transpose(1, 2)
values = values.transpose(1, 2)

# Compute scaled dot-product attention (aka self-attention) with a causal mask
attn_scores = queries @ keys.transpose(2, 3)  # Dot product for each head

# Original mask truncated to the number of tokens and converted to boolean
mask_bool = self.mask.bool()[:num_tokens, :num_tokens]

# Use the mask to fill attention scores
attn_scores.masked_fill_(mask_bool, -torch.inf)

attn_weights = torch.softmax(attn_scores / keys.shape[-1]**0.5, dim=-1)
attn_weights = self.dropout(attn_weights)

# Shape: (b, num_tokens, num_heads, head_dim)
context_vec = (attn_weights @ values).transpose(1, 2)

# Combine heads, where self.d_out = self.num_heads * self.head_dim
context_vec = context_vec.contiguous().view(b, num_tokens, self.d_out)
context_vec = self.out_proj(context_vec) # optional projection

return context_vec

torch.manual_seed(123)

batch_size, context_length, d_in = batch.shape
d_out = 2
mha = MultiHeadAttention(d_in, d_out, context_length, 0.0, num_heads=2)

context_vecs = mha(batch)

print(context_vecs)
print("context_vecs.shape:", context_vecs.shape)

Başka bir kompakt ve verimli uygulama için PyTorch'taki torch.nn.MultiheadAttention sınıfını kullanabilirsiniz.

tip

ChatGPT'nin, her başın tüm token'ların tüm boyutlarını kontrol etmesi yerine token'ların boyutlarını başlar arasında bölmenin neden daha iyi olduğuna dair kısa yanıtı:

Her başın tüm gömme boyutlarını işlemesine izin vermek, her başın tam bilgilere erişimi olacağı için avantajlı gibi görünse de, standart uygulama gömme boyutlarını başlar arasında bölmektir. Bu yaklaşım, hesaplama verimliliği ile model performansını dengeler ve her başın çeşitli temsilleri öğrenmesini teşvik eder. Bu nedenle, gömme boyutlarını bölmek, her başın tüm boyutları kontrol etmesinden genellikle tercih edilir.

References