4. Attention Mechanisms

Aandagmeganismes en Self-Aandag in Neurale Netwerke

Aandagmeganismes laat neurale netwerke toe om op spesifieke dele van die invoer te fokus wanneer hulle elke deel van die uitvoer genereer. Hulle ken verskillende gewigte aan verskillende invoere toe, wat die model help om te besluit watter invoere die relevantste is vir die taak wat voorlê. Dit is van kardinale belang in take soos masjienvertaling, waar die begrip van die konteks van die hele sin noodsaaklik is vir akkurate vertaling.

Die doel van hierdie vierde fase is baie eenvoudig: Pas 'n paar aandagmeganismes toe. Hierdie gaan baie herhalende lae wees wat die verhouding van 'n woord in die woordeskat met sy bure in die huidige sin wat gebruik word om die LLM te train, vasvang. Daar word baie lae hiervoor gebruik, so 'n groot aantal opleibare parameters gaan hierdie inligting vasvang.

Verstaan Aandagmeganismes

In tradisionele volgorde-tot-volgorde modelle wat vir taalvertaling gebruik word, kodeer die model 'n invoervolgorde in 'n vaste-grootte konteksvektor. Hierdie benadering sukkel egter met lang sinne omdat die vaste-grootte konteksvektor dalk nie al die nodige inligting vasvang nie. Aandagmeganismes spreek hierdie beperking aan deur die model toe te laat om al die invoertokens in ag te neem wanneer dit elke uitvoertoken genereer.

Voorbeeld: Masjienvertaling

Oorweeg om die Duitse sin "Kannst du mir helfen diesen Satz zu übersetzen" in Engels te vertaal. 'n Woord-vir-woord vertaling sou nie 'n grammatikaal korrekte Engelse sin lewer nie weens verskille in grammatikaal strukture tussen tale. 'n Aandagmeganisme stel die model in staat om op relevante dele van die invoersin te fokus wanneer dit elke woord van die uitvoersin genereer, wat lei tot 'n meer akkurate en samehangende vertaling.

Inleiding tot Self-Aandag

Self-aandag, of intra-aandag, is 'n meganisme waar aandag binne 'n enkele volgorde toegepas word om 'n voorstelling van daardie volgorde te bereken. Dit laat elke token in die volgorde toe om op al die ander tokens te let, wat die model help om afhanklikhede tussen tokens vas te vang ongeag hul afstand in die volgorde.

Sleutelkonsepte

  • Tokens: Individuele elemente van die invoervolgorde (bv. woorde in 'n sin).

  • Inbedings: Vektorvoorstellings van tokens, wat semantiese inligting vasvang.

  • Aandaggewigte: Waardes wat die belangrikheid van elke token relatief tot ander bepaal.

Berekening van Aandaggewigte: 'n Stap-vir-Stap Voorbeeld

Kom ons oorweeg die sin "Hello shiny sun!" en verteenwoordig elke woord met 'n 3-dimensionele inbeding:

  • Hello: [0.34, 0.22, 0.54]

  • shiny: [0.53, 0.34, 0.98]

  • sun: [0.29, 0.54, 0.93]

Ons doel is om die konteksvektor vir die woord "shiny" te bereken met behulp van self-aandag.

Stap 1: Bereken Aandagpunte

Vermenigvuldig net elke dimensiewaarde van die navraag met die relevante een van elke token en voeg die resultate bymekaar. Jy kry 1 waarde per paar tokens.

Vir elke woord in die sin, bereken die aandagpunt ten opsigte van "shiny" deur die dotproduk van hul inbedings te bereken.

Aandagpunt tussen "Hello" en "shiny"

Aandagpunt tussen "shiny" en "shiny"

Aandagpunt tussen "sun" en "shiny"

Stap 2: Normaliseer Aandagpunte om Aandaggewigte te Verkry

Moet nie in die wiskundige terme verlore gaan nie, die doel van hierdie funksie is eenvoudig, normaliseer al die gewigte sodat hulle in totaal 1 optel.

Boonop word die softmax funksie gebruik omdat dit verskille beklemtoon weens die eksponensiële deel, wat dit makliker maak om nuttige waardes te identifiseer.

Pas die softmax funksie toe op die aandagpunte om hulle in aandaggewigte te omskep wat tot 1 optel.

Berekening van die eksponensiale:

Berekening van die som:

Berekening van aandaggewigte:

Stap 3: Bereken die Konteksvektor

Neem net elke aandaggewig en vermenigvuldig dit met die verwante token dimensies en som dan al die dimensies om net 1 vektor (die konteksvektor) te kry.

Die konteksvektor word bereken as die gewigte som van die inbedings van al die woorde, met behulp van die aandaggewigte.

Berekening van elke komponent:

  • Gewigte Inbeding van "Hello":

* **Gewigte Inbeding van "shiny"**:

* **Gewigte Inbeding van "sun"**:

Som die gewigte inbedings:

konteksvektor=[0.0779+0.2156+0.1057, 0.0504+0.1382+0.1972, 0.1237+0.3983+0.3390]=[0.3992,0.3858,0.8610]

Hierdie konteksvektor verteenwoordig die verrykte inbeding vir die woord "shiny," wat inligting van al die woorde in die sin inkorporeer.

Samevatting van die Proses

  1. Bereken Aandagpunte: Gebruik die dotproduk tussen die inbeding van die teikenwoord en die inbedings van al die woorde in die volgorde.

  2. Normaliseer Punte om Aandaggewigte te Verkry: Pas die softmax funksie toe op die aandagpunte om gewigte te verkry wat tot 1 optel.

  3. Bereken Konteksvektor: Vermenigvuldig elke woord se inbeding met sy aandaggewig en som die resultate.

Self-Aandag met Opleibare Gewigte

In praktyk gebruik self-aandagmeganismes opleibare gewigte om die beste voorstellings vir navrae, sleutels en waardes te leer. Dit behels die bekendstelling van drie gewig matrikse:

Die navraag is die data om soos voorheen te gebruik, terwyl die sleutels en waardes matrikse bloot ewekansige-opleibare matrikse is.

Stap 1: Bereken Navrae, Sleutels, en Waardes

Elke token sal sy eie navraag, sleutel en waarde matriks hê deur sy dimensiewaarde met die gedefinieerde matrikse te vermenigvuldig:

Hierdie matrikse transformeer die oorspronklike inbedings in 'n nuwe ruimte wat geskik is vir die berekening van aandag.

Voorbeeld

Aannemend:

  • Invoerdimensie din=3 (inbeding grootte)

  • Uitvoerdimensie dout=2 (gewens dimensie vir navrae, sleutels, en waardes)

Inisialiseer die gewig matrikse:

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))

Bereken vrae, sleutels en waardes:

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

Stap 2: Bereken Geskaalde Dot-Produk Aandag

Bereken Aandag Punte

Soos in die vorige voorbeeld, maar hierdie keer, in plaas daarvan om die waardes van die dimensies van die tokens te gebruik, gebruik ons die sleutel matriks van die token (wat reeds bereken is met behulp van die dimensies):. So, vir elke navraag qi​ en sleutel kj​:

Skaal die Punte

Om te voorkom dat die dot produkte te groot word, skaal hulle met die vierkantswortel van die sleutel dimensie dk​:

Die punt word gedeel deur die vierkantswortel van die dimensies omdat dot produkte baie groot kan word en dit help om hulle te reguleer.

Pas Softmax toe om Aandag Gewigte te Verkry: Soos in die aanvanklike voorbeeld, normaliseer al die waardes sodat hulle 1 som.

Stap 3: Bereken Konteks Vektore

Soos in die aanvanklike voorbeeld, som net al die waardes matriks op deur elkeen met sy aandag gewig te vermenigvuldig:

Kode Voorbeeld

Gryp 'n voorbeeld van https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb jy kan hierdie klas kyk wat die self-aandag funksionaliteit implementeer waaroor ons gepraat het:

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))

Let daarop dat in plaas daarvan om die matriks met ewekansige waardes te initialiseer, nn.Linear gebruik word om al die gewigte as parameters te merk om te train.

Causale Aandag: Toekomstige Woorde Versteek

Vir LLMs wil ons hê die model moet slegs die tokens oorweeg wat voor die huidige posisie verskyn om die volgende token te voorspel. Causale aandag, ook bekend as gemaskeerde aandag, bereik dit deur die aandagmeganisme te wysig om toegang tot toekomstige tokens te verhoed.

Toepassing van 'n Causale Aandagmasker

Om causale aandag te implementeer, pas ons 'n masker toe op die aandag punte voor die softmax operasie sodat die oorblywende eenhede steeds 1 sal optel. Hierdie masker stel die aandag punte van toekomstige tokens op negatiewe oneindigheid, wat verseker dat na die softmax, hul aandag gewigte nul is.

Stappe

  1. Bereken Aandag Punten: Dieselfde as voorheen.

  2. Pas Masker Toe: Gebruik 'n boonste driehoekige matriks wat met negatiewe oneindigheid bo die diagonaal gevul is.

mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) * float('-inf')
masked_scores = attention_scores + mask
  1. Pas Softmax Toe: Bereken aandag gewigte met behulp van die gemaskeerde punte.

attention_weights = torch.softmax(masked_scores, dim=-1)

Maskering van Bykomende Aandag Gewigte met Dropout

Om oorpassing te voorkom, kan ons dropout toepas op die aandag gewigte na die softmax operasie. Dropout maak sommige van die aandag gewigte ewekansig nul tydens opleiding.

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

'n Gereelde dropout is ongeveer 10-20%.

Code Voorbeeld

Code voorbeeld van https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb:

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)

Om Enkelkop Aandag uit te brei na Meerkop Aandag

Meerkop aandag bestaan in praktiese terme uit die uitvoering van meerdere instansies van die self-aandag funksie, elk met hulle eie gewigte, sodat verskillende finale vektore bereken kan word.

Kode Voorbeeld

Dit kan moontlik wees om die vorige kode te hergebruik en net 'n omhulsel toe te voeg wat dit verskeie kere begin, maar dit is 'n meer geoptimaliseerde weergawe van https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb wat al die koppe terselfdertyd verwerk (wat die aantal duur vir-lusse verminder). Soos jy in die kode kan sien, word die dimensies van elke token in verskillende dimensies verdeel volgens die aantal koppe. Op hierdie manier, as 'n token 8 dimensies het en ons wil 3 koppe gebruik, sal die dimensies in 2 arrays van 4 dimensies verdeel word en elke kop sal een daarvan gebruik:

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)

Vir 'n ander kompakte en doeltreffende implementering kan jy die torch.nn.MultiheadAttention klas in PyTorch gebruik.

Kort antwoord van ChatGPT oor hoekom dit beter is om dimensies van tokens onder die koppe te verdeel in plaas daarvan om elke kop al die dimensies van al die tokens te laat nagaan:

Terwyl dit mag voorkom asof dit voordelig is om elke kop al die inbed dimensies te laat verwerk omdat elke kop toegang tot die volle inligting sou hê, is die standaard praktyk om die inbed dimensies onder die koppe te verdeel. Hierdie benadering balanseer rekenaar doeltreffendheid met model prestasie en moedig elke kop aan om diverse voorstellings te leer. Daarom is dit oor die algemeen verkieslik om die inbed dimensies te verdeel eerder as om elke kop al die dimensies te laat nagaan.

References

Last updated