4. Attention Mechanisms

Μηχανισμοί Προσοχής και Αυτοπροσοχή σε Νευρωνικά Δίκτυα

Οι μηχανισμοί προσοχής επιτρέπουν στα νευρωνικά δίκτυα να εστιάζουν σε συγκεκριμένα μέρη της εισόδου κατά την παραγωγή κάθε μέρους της εξόδου. Αναθέτουν διαφορετικά βάρη σε διαφορετικές εισόδους, βοηθώντας το μοντέλο να αποφασίσει ποιες είσοδοι είναι πιο σχετικές με την τρέχουσα εργασία. Αυτό είναι κρίσιμο σε εργασίες όπως η μηχανική μετάφραση, όπου η κατανόηση του πλαισίου της ολόκληρης πρότασης είναι απαραίτητη για ακριβή μετάφραση.

Ο στόχος αυτής της τέταρτης φάσης είναι πολύ απλός: Εφαρμόστε μερικούς μηχανισμούς προσοχής. Αυτοί θα είναι πολλές επανειλημμένες στρώσεις που θα καταγράψουν τη σχέση μιας λέξης στο λεξιλόγιο με τους γείτονές της στην τρέχουσα πρόταση που χρησιμοποιείται για την εκπαίδευση του LLM. Χρησιμοποιούνται πολλές στρώσεις γι' αυτό, οπότε πολλοί εκπαιδευόμενοι παράμετροι θα καταγράφουν αυτές τις πληροφορίες.

Κατανόηση Μηχανισμών Προσοχής

Στα παραδοσιακά μοντέλα ακολουθίας προς ακολουθία που χρησιμοποιούνται για τη μετάφραση γλώσσας, το μοντέλο κωδικοποιεί μια ακολουθία εισόδου σε ένα σταθερού μεγέθους διάνυσμα πλαισίου. Ωστόσο, αυτή η προσέγγιση δυσκολεύεται με μεγάλες προτάσεις επειδή το σταθερού μεγέθους διάνυσμα πλαισίου μπορεί να μην καταγράψει όλες τις απαραίτητες πληροφορίες. Οι μηχανισμοί προσοχής αντιμετωπίζουν αυτόν τον περιορισμό επιτρέποντας στο μοντέλο να εξετάσει όλα τα εισερχόμενα tokens κατά την παραγωγή κάθε εξόδου token.

Παράδειγμα: Μηχανική Μετάφραση

Σκεφτείτε να μεταφράσετε την γερμανική πρόταση "Kannst du mir helfen diesen Satz zu übersetzen" στα αγγλικά. Μια λέξη προς λέξη μετάφραση δεν θα παραγάγει μια γραμματικά σωστή αγγλική πρόταση λόγω διαφορών στις γραμματικές δομές μεταξύ των γλωσσών. Ένας μηχανισμός προσοχής επιτρέπει στο μοντέλο να εστιάσει σε σχετικές partes της εισερχόμενης πρότασης κατά την παραγωγή κάθε λέξης της εξόδου, οδηγώντας σε μια πιο ακριβή και συνεκτική μετάφραση.

Εισαγωγή στην Αυτοπροσοχή

Η αυτοπροσοχή, ή ενδοπροσοχή, είναι ένας μηχανισμός όπου η προσοχή εφαρμόζεται εντός μιας μόνο ακολουθίας για να υπολογίσει μια αναπαράσταση αυτής της ακολουθίας. Επιτρέπει σε κάθε token στην ακολουθία να εστιάσει σε όλα τα άλλα tokens, βοηθώντας το μοντέλο να καταγράψει εξαρτήσεις μεταξύ των tokens ανεξαρτήτως της απόστασής τους στην ακολουθία.

Βασικές Έννοιες

  • Tokens: Ατομικά στοιχεία της εισερχόμενης ακολουθίας (π.χ., λέξεις σε μια πρόταση).

  • Ενσωματώσεις: Διανυσματικές αναπαραστάσεις των tokens, που καταγράφουν σημασιολογικές πληροφορίες.

  • Βάρη Προσοχής: Τιμές που καθορίζουν τη σημασία κάθε token σε σχέση με τα άλλα.

Υπολογισμός Βαρών Προσοχής: Ένα Βήμα-Βήμα Παράδειγμα

Ας εξετάσουμε την πρόταση "Hello shiny sun!" και να αναπαραστήσουμε κάθε λέξη με μια 3-διάστατη ενσωμάτωση:

  • Hello: [0.34, 0.22, 0.54]

  • shiny: [0.53, 0.34, 0.98]

  • sun: [0.29, 0.54, 0.93]

Ο στόχος μας είναι να υπολογίσουμε το διάνυσμα πλαισίου για τη λέξη "shiny" χρησιμοποιώντας αυτοπροσοχή.

Βήμα 1: Υπολογισμός Σκορ Προσοχής

Απλώς πολλαπλασιάστε κάθε τιμή διάστασης του query με την αντίστοιχη κάθε token και προσθέστε τα αποτελέσματα. Θα πάρετε 1 τιμή ανά ζεύγος tokens.

Για κάθε λέξη στην πρόταση, υπολογίστε το σκορ προσοχής σε σχέση με το "shiny" υπολογίζοντας το εσωτερικό γινόμενο των ενσωματώσεών τους.

Σκορ Προσοχής μεταξύ "Hello" και "shiny"

Σκορ Προσοχής μεταξύ "shiny" και "shiny"

Σκορ Προσοχής μεταξύ "sun" και "shiny"

Βήμα 2: Κανονικοποίηση Σκορ Προσοχής για Απόκτηση Βαρών Προσοχής

Μην χαθείτε στους μαθηματικούς όρους, ο στόχος αυτής της συνάρτησης είναι απλός, κανονικοποιήστε όλα τα βάρη ώστε να αθροίζουν 1 συνολικά.

Επιπλέον, η συνάρτηση softmax χρησιμοποιείται επειδή τονίζει τις διαφορές λόγω του εκθετικού μέρους, διευκολύνοντας την ανίχνευση χρήσιμων τιμών.

Εφαρμόστε τη συνάρτηση softmax στα σκορ προσοχής για να τα μετατρέψετε σε βάρη προσοχής που αθροίζουν σε 1.

Υπολογίζοντας τις εκθετικές:

Υπολογίζοντας το άθροισμα:

Υπολογίζοντας τα βάρη προσοχής:

Βήμα 3: Υπολογισμός του Διάνυσματος Πλαισίου

Απλώς πάρτε κάθε βάρος προσοχής και πολλαπλασιάστε το με τις σχετικές διαστάσεις token και στη συνέχεια αθροίστε όλες τις διαστάσεις για να πάρετε μόνο 1 διάνυσμα (το διάνυσμα πλαίσιο)

Το διάνυσμα πλαισίου υπολογίζεται ως το ζυγισμένο άθροισμα των ενσωματώσεων όλων των λέξεων, χρησιμοποιώντας τα βάρη προσοχής.

Υπολογίζοντας κάθε συστατικό:

  • Ζυγισμένη Ενσωμάτωση του "Hello":

* **Ζυγισμένη Ενσωμάτωση του "shiny"**:

* **Ζυγισμένη Ενσωμάτωση του "sun"**:

Αθροίζοντας τις ζυγισμένες ενσωματώσεις:

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

Αυτό το διάνυσμα πλαισίου αντιπροσωπεύει την εμπλουτισμένη ενσωμάτωση για τη λέξη "shiny," ενσωματώνοντας πληροφορίες από όλες τις λέξεις στην πρόταση.

Περίληψη της Διαδικασίας

  1. Υπολογίστε Σκορ Προσοχής: Χρησιμοποιήστε το εσωτερικό γινόμενο μεταξύ της ενσωμάτωσης της στοχευμένης λέξης και των ενσωματώσεων όλων των λέξεων στην ακολουθία.

  2. Κανονικοποιήστε τα Σκορ για να Λάβετε Βάρη Προσοχής: Εφαρμόστε τη συνάρτηση softmax στα σκορ προσοχής για να αποκτήσετε βάρη που αθροίζουν σε 1.

  3. Υπολογίστε το Δίχτυ Πλαισίου: Πολλαπλασιάστε την ενσωμάτωση κάθε λέξης με το βάρος προσοχής της και αθροίστε τα αποτελέσματα.

Αυτοπροσοχή με Εκπαιδευόμενα Βάρη

Στην πράξη, οι μηχανισμοί αυτοπροσοχής χρησιμοποιούν εκπαιδευόμενα βάρη για να μάθουν τις καλύτερες αναπαραστάσεις για τα queries, keys και values. Αυτό περιλαμβάνει την εισαγωγή τριών πινάκων βαρών:

Το query είναι τα δεδομένα που χρησιμοποιούνται όπως πριν, ενώ οι πίνακες keys και values είναι απλώς τυχαίοι-εκπαιδευόμενοι πίνακες.

Βήμα 1: Υπολογισμός Queries, Keys και Values

Κάθε token θα έχει τον δικό του πίνακα query, key και value πολλαπλασιάζοντας τις τιμές διάστασης του με τους καθορισμένους πίνακες:

Αυτοί οι πίνακες μετασχηματίζουν τις αρχικές ενσωματώσεις σε έναν νέο χώρο κατάλληλο για τον υπολογισμό της προσοχής.

Παράδειγμα

Υποθέτοντας:

  • Διάσταση εισόδου din=3 (μέγεθος ενσωμάτωσης)

  • Διάσταση εξόδου dout=2 (επιθυμητή διάσταση για queries, keys και values)

Αρχικοποιήστε τους πίνακες βαρών:

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

Υπολογίστε τα ερωτήματα, τα κλειδιά και τις τιμές:

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

Βήμα 2: Υπολογισμός Σ scaled Dot-Product Attention

Υπολογισμός Σκορ Προσοχής

Παρόμοια με το προηγούμενο παράδειγμα, αλλά αυτή τη φορά, αντί να χρησιμοποιούμε τις τιμές των διαστάσεων των tokens, χρησιμοποιούμε τον πίνακα κλειδιών του token (που έχει υπολογιστεί ήδη χρησιμοποιώντας τις διαστάσεις):. Έτσι, για κάθε ερώτημα qi​ και κλειδί kj​:

Κλιμάκωση των Σκορ

Για να αποτρέψουμε τα dot products από το να γίνουν πολύ μεγάλα, τα κλιμακώνουμε με την τετραγωνική ρίζα της διάστασης του κλειδιού dk​:

Το σκορ διαιρείται με την τετραγωνική ρίζα των διαστάσεων γιατί τα dot products μπορεί να γίνουν πολύ μεγάλα και αυτό βοηθά να ρυθμιστούν.

Εφαρμογή Softmax για Απόκτηση Βαρών Προσοχής: Όπως στο αρχικό παράδειγμα, κανονικοποιούμε όλες τις τιμές ώστε να αθροίζουν 1.

Βήμα 3: Υπολογισμός Συγκείμενων Διανυσμάτων

Όπως στο αρχικό παράδειγμα, απλώς αθροίζουμε όλους τους πίνακες τιμών πολλαπλασιάζοντας τον καθένα με το βάρος προσοχής του:

Παράδειγμα Κώδικα

Αρπάζοντας ένα παράδειγμα από https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb μπορείτε να ελέγξετε αυτή την κλάση που υλοποιεί τη λειτουργικότητα self-attendant που συζητήσαμε:

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

Σημειώστε ότι αντί να αρχικοποιήσουμε τους πίνακες με τυχαίες τιμές, χρησιμοποιείται το nn.Linear για να δηλώσουμε όλα τα βάρη ως παραμέτρους προς εκπαίδευση.

Αιτιώδης Προσοχή: Απόκρυψη Μελλοντικών Λέξεων

Για τα LLMs θέλουμε το μοντέλο να εξετάσει μόνο τα tokens που εμφανίζονται πριν από την τρέχουσα θέση προκειμένου να προβλέψει το επόμενο token. Η αιτιώδης προσοχή, γνωστή επίσης ως masked attention, το επιτυγχάνει τροποποιώντας τον μηχανισμό προσοχής για να αποτρέψει την πρόσβαση σε μελλοντικά tokens.

Εφαρμογή Μάσκας Αιτιώδους Προσοχής

Για να εφαρμόσουμε την αιτιώδη προσοχή, εφαρμόζουμε μια μάσκα στους βαθμούς προσοχής πριν από τη λειτουργία softmax έτσι ώστε οι υπόλοιποι να αθροίζουν 1. Αυτή η μάσκα ρυθμίζει τους βαθμούς προσοχής των μελλοντικών tokens σε αρνητική άπειρο, διασφαλίζοντας ότι μετά το softmax, τα βάρη προσοχής τους είναι μηδέν.

Βήματα

  1. Υπολογισμός Βαθμών Προσοχής: Ίδιο με πριν.

  2. Εφαρμογή Μάσκας: Χρησιμοποιήστε έναν ανώτερο τριγωνικό πίνακα γεμάτο με αρνητική άπειρο πάνω από τη διαγώνιο.

mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) * float('-inf')
masked_scores = attention_scores + mask
  1. Εφαρμογή Softmax: Υπολογίστε τα βάρη προσοχής χρησιμοποιώντας τους μασκαρισμένους βαθμούς.

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

Μάσκα πρόσθετων Βαρών Προσοχής με Dropout

Για να αποτρέψουμε την υπερβολική προσαρμογή, μπορούμε να εφαρμόσουμε dropout στα βάρη προσοχής μετά τη λειτουργία softmax. Το dropout τυχαία μηδενίζει ορισμένα από τα βάρη προσοχής κατά τη διάρκεια της εκπαίδευσης.

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

Ένας κανονικός dropout είναι περίπου 10-20%.

Code Example

Code example from 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)

Επέκταση της Μονοκέφαλης Προσοχής σε Πολυκέφαλη Προσοχή

Πολυκέφαλη προσοχή στην πρακτική σημαίνει την εκτέλεση πολλών περιπτώσεων της λειτουργίας αυτοπροσοχής, καθεμία με τα δικά της βάρη, έτσι ώστε να υπολογίζονται διαφορετικοί τελικοί διανύσματα.

Παράδειγμα Κώδικα

Θα μπορούσε να είναι δυνατό να επαναχρησιμοποιηθεί ο προηγούμενος κώδικας και απλώς να προστεθεί μια περιτύλιξη που να τον εκκινεί πολλές φορές, αλλά αυτή είναι μια πιο βελτιστοποιημένη έκδοση από https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb που επεξεργάζεται όλες τις κεφαλές ταυτόχρονα (μειώνοντας τον αριθμό των δαπανηρών βρόχων). Όπως μπορείτε να δείτε στον κώδικα, οι διαστάσεις κάθε token διαιρούνται σε διαφορετικές διαστάσεις ανάλογα με τον αριθμό των κεφαλών. Με αυτόν τον τρόπο, αν το token έχει 8 διαστάσεις και θέλουμε να χρησιμοποιήσουμε 3 κεφαλές, οι διαστάσεις θα διαιρεθούν σε 2 πίνακες των 4 διαστάσεων και κάθε κεφαλή θα χρησιμοποιήσει έναν από αυτούς:

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)

Για μια άλλη συμπαγή και αποδοτική υλοποίηση, μπορείτε να χρησιμοποιήσετε την torch.nn.MultiheadAttention κλάση στο PyTorch.

Σύντομη απάντηση του ChatGPT σχετικά με το γιατί είναι καλύτερο να διαιρούμε τις διαστάσεις των tokens μεταξύ των κεφαλών αντί να έχει κάθε κεφαλή πρόσβαση σε όλες τις διαστάσεις όλων των tokens:

Ενώ η δυνατότητα κάθε κεφαλής να επεξεργάζεται όλες τις διαστάσεις ενσωμάτωσης μπορεί να φαίνεται πλεονεκτική, καθώς κάθε κεφαλή θα έχει πρόσβαση σε όλες τις πληροφορίες, η τυπική πρακτική είναι να διαιρούμε τις διαστάσεις ενσωμάτωσης μεταξύ των κεφαλών. Αυτή η προσέγγιση ισορροπεί την υπολογιστική αποδοτικότητα με την απόδοση του μοντέλου και ενθαρρύνει κάθε κεφαλή να μάθει ποικιλόμορφες αναπαραστάσεις. Επομένως, η διαίρεση των διαστάσεων ενσωμάτωσης προτιμάται γενικά από το να έχει κάθε κεφαλή πρόσβαση σε όλες τις διαστάσεις.

References

Last updated