4. Μηχανισμοί Προσοχής
Reading time: 13 minutes
Μηχανισμοί Προσοχής και Αυτοπροσοχή σε Νευρωνικά Δίκτυα
Οι μηχανισμοί προσοχής επιτρέπουν στα νευρωνικά δίκτυα να εστιάζουν σε συγκεκριμένα μέρη της εισόδου κατά την παραγωγή κάθε μέρους της εξόδου. Αναθέτουν διαφορετικά βάρη σε διαφορετικές εισόδους, βοηθώντας το μοντέλο να αποφασίσει ποιες είσοδοι είναι πιο σχετικές με την τρέχουσα εργασία. Αυτό είναι κρίσιμο σε εργασίες όπως η μηχανική μετάφραση, όπου η κατανόηση του πλαισίου της ολόκληρης πρότασης είναι απαραίτητη για ακριβή μετάφραση.
tip
Ο στόχος αυτής της τέταρτης φάσης είναι πολύ απλός: Εφαρμόστε μερικούς μηχανισμούς προσοχής. Αυτοί θα είναι πολλαπλά επαναλαμβανόμενα επίπεδα που θα καταγράφουν τη σχέση μιας λέξης στο λεξιλόγιο με τους γείτονές της στην τρέχουσα πρόταση που χρησιμοποιείται για την εκπαίδευση του 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: Υπολογισμός Σκορ Προσοχής
tip
Απλά πολλαπλασιάστε κάθε τιμή διάστασης του query με την αντίστοιχη κάθε token και προσθέστε τα αποτελέσματα. Θα πάρετε 1 τιμή ανά ζεύγος tokens.
Για κάθε λέξη στην πρόταση, υπολογίστε το σκορ προσοχής σε σχέση με το "shiny" υπολογίζοντας το εσωτερικό γινόμενο των ενσωματώσεών τους.
Σκορ Προσοχής μεταξύ "Hello" και "shiny"
 (1) (1).png)
Σκορ Προσοχής μεταξύ "shiny" και "shiny"
 (1) (1) (1) (1) (1) (1) (1).png)
Σκορ Προσοχής μεταξύ "sun" και "shiny"
 (1) (1) (1) (1).png)
Βήμα 2: Κανονικοποίηση Σκορ Προσοχής για Απόκτηση Βαρών Προσοχής
tip
Μην χαθείτε στους μαθηματικούς όρους, ο στόχος αυτής της συνάρτησης είναι απλός, κανονικοποιήστε όλα τα βάρη ώστε να αθροίζουν 1 συνολικά.
Επιπλέον, η συνάρτηση softmax χρησιμοποιείται επειδή τονίζει τις διαφορές λόγω του εκθετικού μέρους, διευκολύνοντας την ανίχνευση χρήσιμων τιμών.
Εφαρμόστε τη συνάρτηση softmax στα σκορ προσοχής για να τα μετατρέψετε σε βάρη προσοχής που αθροίζουν σε 1.
 (1) (1) (1) (1).png)
Υπολογίζοντας τις εκθετικές:
 (1) (1) (1).png)
Υπολογίζοντας το άθροισμα:
 (1) (1).png)
Υπολογίζοντας τα βάρη προσοχής:
 (1) (1).png)
Βήμα 3: Υπολογισμός του Διάνυσματος Πλαισίου
tip
Απλά πάρτε κάθε βάρος προσοχής και πολλαπλασιάστε το με τις σχετικές διαστάσεις token και στη συνέχεια αθροίστε όλες τις διαστάσεις για να πάρετε μόνο 1 διάνυσμα (το διάνυσμα πλαισίου)
Το διάνυσμα πλαισίου υπολογίζεται ως το ζυγισμένο άθροισμα των ενσωματώσεων όλων των λέξεων, χρησιμοποιώντας τα βάρη προσοχής.
.png)
Υπολογίζοντας κάθε συστατικό:
- Ζυγισμένη Ενσωμάτωση του "Hello":
 (1) (1).png)
- Ζυγισμένη Ενσωμάτωση του "shiny":
 (1) (1).png)
- Ζυγισμένη Ενσωμάτηση του "sun":
 (1) (1).png)
Αθροίζοντας τις ζυγισμένες ενσωματώσεις:
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," ενσωματώνοντας πληροφορίες από όλες τις λέξεις στην πρόταση.
Περίληψη της Διαδικασίας
- Υπολογίστε τα Σκορ Προσοχής: Χρησιμοποιήστε το εσωτερικό γινόμενο μεταξύ της ενσωμάτωσης της στοχευμένης λέξης και των ενσωματώσεων όλων των λέξεων στην ακολουθία.
- Κανονικοποιήστε τα Σκορ για να Λάβετε Βάρη Προσοχής: Εφαρμόστε τη συνάρτηση softmax στα σκορ προσοχής για να αποκτήσετε βάρη που αθροίζουν σε 1.
- Υπολογίστε το Δίπλωμα Πλαισίου: Πολλαπλασιάστε την ενσωμάτωση κάθε λέξης με το βάρος προσοχής της και αθροίστε τα αποτελέσματα.
Αυτοπροσοχή με Εκπαιδεύσιμα Βάρη
Στην πράξη, οι μηχανισμοί αυτοπροσοχής χρησιμοποιούν εκπαιδεύσιμα βάρη για να μάθουν τις καλύτερες αναπαραστάσεις για queries, keys και values. Αυτό περιλαμβάνει την εισαγωγή τριών πινάκων βαρών:
 (1) (1).png)
Το query είναι τα δεδομένα που χρησιμοποιούνται όπως πριν, ενώ οι πίνακες keys και values είναι απλώς τυχαίοι-εκπαιδεύσιμοι πίνακες.
Βήμα 1: Υπολογισμός Queries, Keys και Values
Κάθε token θα έχει τον δικό του πίνακα query, key και value πολλαπλασιάζοντας τις τιμές διάστασης του με τους καθορισμένους πίνακες:
.png)
Αυτοί οι πίνακες μετασχηματίζουν τις αρχικές ενσωματώσεις σε έναν νέο χώρο κατάλληλο για τον υπολογισμό της προσοχής.
Παράδειγμα
Υποθέτοντας:
- Διάσταση εισόδου
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: Υπολογισμός Κλιμακωτής Προσοχής Σημείου
Υπολογισμός Σκορ Προσοχής
Παρόμοια με το προηγούμενο παράδειγμα, αλλά αυτή τη φορά, αντί να χρησιμοποιούμε τις τιμές των διαστάσεων των tokens, χρησιμοποιούμε τον πίνακα κλειδιών του token (που έχει υπολογιστεί ήδη χρησιμοποιώντας τις διαστάσεις):. Έτσι, για κάθε ερώτημα qi
και κλειδί kj
:
.png)
Κλιμάκωση των Σκορ
Για να αποτρέψουμε τα εσωτερικά προϊόντα να γίνουν πολύ μεγάλα, τα κλιμακώνουμε με την τετραγωνική ρίζα της διάστασης του κλειδιού dk
:
.png)
tip
Το σκορ διαιρείται με την τετραγωνική ρίζα των διαστάσεων επειδή τα εσωτερικά προϊόντα μπορεί να γίνουν πολύ μεγάλα και αυτό βοηθά στη ρύθμισή τους.
Εφαρμογή Softmax για Απόκτηση Βαρών Προσοχής: Όπως στο αρχικό παράδειγμα, κανονικοποιούμε όλες τις τιμές ώστε να αθροίζουν 1.
.png)
Βήμα 3: Υπολογισμός Συγκείμενων Διανυσμάτων
Όπως στο αρχικό παράδειγμα, απλώς αθροίζουμε όλους τους πίνακες τιμών πολλαπλασιάζοντας τον καθένα με το βάρος προσοχής του:
.png)
Παράδειγμα Κώδικα
Αρπάζοντας ένα παράδειγμα από https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01_main-chapter-code/ch03.ipynb μπορείτε να ελέγξετε αυτή την κλάση που υλοποιεί τη λειτουργικότητα αυτοπροσοχής που συζητήσαμε:
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
Σημειώστε ότι αντί να αρχικοποιήσουμε τους πίνακες με τυχαίες τιμές, χρησιμοποιείται το nn.Linear
για να σημάνει όλα τα βάρη ως παραμέτρους προς εκπαίδευση.
Causal Attention: Απόκρυψη Μελλοντικών Λέξεων
Για τα LLMs θέλουμε το μοντέλο να εξετάζει μόνο τα tokens που εμφανίζονται πριν από την τρέχουσα θέση προκειμένου να προβλέψει το επόμενο token. Causal attention, γνωστό και ως masked attention, επιτυγχάνει αυτό τροποποιώντας τον μηχανισμό προσοχής για να αποτρέψει την πρόσβαση σε μελλοντικά tokens.
Εφαρμογή Μάσκας Causal Attention
Για να υλοποιήσουμε την causal attention, εφαρμόζουμε μια μάσκα στους βαθμούς προσοχής πριν από τη λειτουργία softmax ώστε οι υπόλοιποι να αθροίζουν 1. Αυτή η μάσκα ορίζει τους βαθμούς προσοχής των μελλοντικών tokens σε αρνητική άπειρο, διασφαλίζοντας ότι μετά το softmax, τα βάρη προσοχής τους είναι μηδέν.
Βήματα
- Υπολογισμός Βαθμών Προσοχής: Ίδιο με πριν.
- Εφαρμογή Μάσκας: Χρησιμοποιήστε έναν ανώτερο τριγωνικό πίνακα γεμάτο με αρνητική άπειρο πάνω από τη διαγώνιο.
mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1) * float('-inf')
masked_scores = attention_scores + mask
- Εφαρμογή 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 που επεξεργάζεται όλες τις κεφαλές ταυτόχρονα (μειώνοντας τον αριθμό των δαπανηρών βρόχων for). Όπως μπορείτε να δείτε στον κώδικα, οι διαστάσεις κάθε 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.
tip
Σύντομη απάντηση του ChatGPT σχετικά με το γιατί είναι καλύτερο να διαιρούμε τις διαστάσεις των tokens μεταξύ των heads αντί να έχει κάθε head πρόσβαση σε όλες τις διαστάσεις όλων των tokens:
Ενώ η δυνατότητα σε κάθε head να επεξεργάζεται όλες τις διαστάσεις embedding μπορεί να φαίνεται πλεονεκτική επειδή κάθε head θα έχει πρόσβαση σε όλες τις πληροφορίες, η τυπική πρακτική είναι να διαιρούμε τις διαστάσεις embedding μεταξύ των heads. Αυτή η προσέγγιση ισορροπεί την υπολογιστική αποδοτικότητα με την απόδοση του μοντέλου και ενθαρρύνει κάθε head να μάθει ποικιλόμορφες αναπαραστάσεις. Επομένως, η διαίρεση των διαστάσεων embedding προτιμάται γενικά από το να έχει κάθε head πρόσβαση σε όλες τις διαστάσεις.