4. Aandag Meganismes
Reading time: 13 minutes
Aandag Meganismes en Self-Aandag in Neurale Netwerke
Aandag meganismes 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 mees relevant 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.
tip
Die doel van hierdie vierde fase is baie eenvoudig: Pas 'n paar aandag meganismes toe. Hierdie gaan baie herhaalde 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.
'n Groot aantal lae word hiervoor gebruik, so 'n groot aantal leerbare parameters gaan hierdie inligting vasvang.
Verstaan Aandag Meganismes
In tradisionele volgorde-tot-volgorde modelle wat vir taalvertaling gebruik word, kodeer die model 'n invoer volgorde 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. Aandag meganismes spreek hierdie beperking aan deur die model toe te laat om al die invoer tokens in ag te neem wanneer dit elke uitvoer token 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 Aandag meganisme stel die model in staat om op relevante dele van die invoer sin te fokus wanneer dit elke woord van die uitvoer sin 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 invoer volgorde (bv. woorde in 'n sin).
- Embeddings: Vektor voorstellings van tokens, wat semantiese inligting vasvang.
- Aandag Gewigte: Waardes wat die belangrikheid van elke token relatief tot ander bepaal.
Berekening van Aandag Gewigte: 'n Stap-vir-Stap Voorbeeld
Kom ons oorweeg die sin "Hello shiny sun!" en verteenwoordig elke woord met 'n 3-dimensionele embedding:
- 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 Aandag Punte
tip
Vermy om in die wiskundige terme te verlore te gaan, die doel van hierdie funksie is eenvoudig, normaliseer al die gewigte sodat hulle in totaal 1 optel.
Boonop, softmax funksie word gebruik omdat dit verskille beklemtoon as gevolg van die eksponensiële deel, wat dit makliker maak om nuttige waardes te identifiseer.
Vir elke woord in die sin, bereken die aandag punt ten opsigte van "shiny" deur die dot produk van hul embeddings te bereken.
Aandag Punt tussen "Hello" en "shiny"
Aandag Punt tussen "shiny" en "shiny"
Aandag Punt tussen "sun" en "shiny"
Stap 2: Normaliseer Aandag Punte om Aandag Gewigte te Verkry
tip
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, softmax funksie word gebruik omdat dit verskille beklemtoon as gevolg van die eksponensiële deel, wat dit makliker maak om nuttige waardes te identifiseer.
Pas die softmax funksie toe op die aandag punte om hulle in aandag gewigte te omskep wat tot 1 optel.
Berekening van die eksponensiale:
Berekening van die som:
Berekening van aandag gewigte:
Stap 3: Bereken die Konteksvektor
tip
Kry net elke aandag gewig 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 embeddings van al die woorde, met behulp van die aandag gewigte.
Berekening van elke komponent:
- Gewigte Embedding van "Hello":
- Gewigte Embedding van "shiny":
- Gewigte Embedding van "sun":
Som die gewigte embeddings:
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 embedding vir die woord "shiny," wat inligting van al die woorde in die sin inkorporeer.
Samevatting van die Proses
- Bereken Aandag Punte: Gebruik die dot produk tussen die embedding van die teikenwoord en die embeddings van al die woorde in die volgorde.
- Normaliseer Punte om Aandag Gewigte te Verkry: Pas die softmax funksie toe op die aandag punte om gewigte te verkry wat tot 1 optel.
- Bereken Konteksvektor: Vermenigvuldig elke woord se embedding met sy aandag gewig en som die resultate.
Self-Aandag met Leerbare Gewigte
In praktyk gebruik self-aandag meganismes leerbare gewigte om die beste voorstellings vir vrae, sleutels, en waardes te leer. Dit behels die bekendstelling van drie gewig matrikse:
Die vraag is die data om soos voorheen te gebruik, terwyl die sleutels en waardes matrikse bloot ewekansige-leerbare matrikse is.
Stap 1: Bereken Vrae, Sleutels, en Waardes
Elke token sal sy eie vraag, sleutel en waarde matriks hê deur sy dimensiewaarde met die gedefinieerde matrikse te vermenigvuldig:
Hierdie matrikse transformeer die oorspronklike embeddings in 'n nuwe ruimte wat geskik is vir die berekening van aandag.
Voorbeeld
Aannemend:
- Invoer dimensie
din=3
(embedding grootte) - Uitvoer dimensie
dout=2
(gewens dimensie vir vrae, 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
:
tip
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))
note
Let daarop dat in plaas van 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ê dat die model slegs die tokens moet 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 punte steeds 1 sal som. Hierdie masker stel die aandag punte van toekomstige tokens op negatiewe oneindigheid, wat verseker dat na die softmax, hul aandag gewigte nul is.
Stappe
- Bereken Aandag Punten: Dieselfde as voorheen.
- 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
- 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 gelyktydig 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 3 koppe wil 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.
tip
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 inbedingsdimensies te laat verwerk omdat elke kop toegang tot die volle inligting sou hê, is die standaardpraktyk om die inbedingsdimensies onder die koppe te verdeel. Hierdie benadering balanseer rekenkundige doeltreffendheid met modelprestasie en moedig elke kop aan om diverse voorstellings te leer. Daarom is dit oor die algemeen verkieslik om die inbedingsdimensies te verdeel eerder as om elke kop al die dimensies te laat nagaan.