## Meccanismi di Attenzione e Auto-Attenzione nelle Reti Neurali
I meccanismi di attenzione consentono alle reti neurali di **concentrarsi su parti specifiche dell'input durante la generazione di ciascuna parte dell'output**. Assegnano pesi diversi a diversi input, aiutando il modello a decidere quali input sono più rilevanti per il compito in questione. Questo è cruciale in compiti come la traduzione automatica, dove comprendere il contesto dell'intera frase è necessario per una traduzione accurata.
{% hint style="success" %}
L'obiettivo di questa quarta fase è molto semplice: **Applicare alcuni meccanismi di attenzione**. Questi saranno molti **strati ripetuti** che andranno a **catturare la relazione di una parola nel vocabolario con i suoi vicini nella frase attuale utilizzata per addestrare il LLM**.\
Vengono utilizzati molti strati per questo, quindi molti parametri addestrabili andranno a catturare queste informazioni.
{% endhint %}
### Comprendere i Meccanismi di Attenzione
Nei modelli tradizionali sequenza-a-sequenza utilizzati per la traduzione linguistica, il modello codifica una sequenza di input in un vettore di contesto di dimensione fissa. Tuttavia, questo approccio ha difficoltà con frasi lunghe perché il vettore di contesto di dimensione fissa potrebbe non catturare tutte le informazioni necessarie. I meccanismi di attenzione affrontano questa limitazione consentendo al modello di considerare tutti i token di input durante la generazione di ciascun token di output.
#### Esempio: Traduzione Automatica
Considera la traduzione della frase tedesca "Kannst du mir helfen diesen Satz zu übersetzen" in inglese. Una traduzione parola per parola non produrrebbe una frase inglese grammaticalmente corretta a causa delle differenze nelle strutture grammaticali tra le lingue. Un meccanismo di attenzione consente al modello di concentrarsi su parti rilevanti della frase di input durante la generazione di ciascuna parola della frase di output, portando a una traduzione più accurata e coerente.
### Introduzione all'Auto-Attenzione
L'auto-attenzione, o intra-attention, è un meccanismo in cui l'attenzione viene applicata all'interno di una singola sequenza per calcolare una rappresentazione di quella sequenza. Consente a ciascun token nella sequenza di prestare attenzione a tutti gli altri token, aiutando il modello a catturare le dipendenze tra i token indipendentemente dalla loro distanza nella sequenza.
#### Concetti Chiave
* **Token**: Elementi individuali della sequenza di input (ad es., parole in una frase).
* **Embeddings**: Rappresentazioni vettoriali dei token, che catturano informazioni semantiche.
* **Pesi di Attenzione**: Valori che determinano l'importanza di ciascun token rispetto agli altri.
### Calcolo dei Pesi di Attenzione: Un Esempio Passo-Passo
Consideriamo la frase **"Hello shiny sun!"** e rappresentiamo ciascuna parola con un embedding a 3 dimensioni:
* **Hello**: `[0.34, 0.22, 0.54]`
* **shiny**: `[0.53, 0.34, 0.98]`
* **sun**: `[0.29, 0.54, 0.93]`
Il nostro obiettivo è calcolare il **vettore di contesto** per la parola **"shiny"** utilizzando l'auto-attenzione.
#### Passo 1: Calcolare i Punteggi di Attenzione
{% hint style="success" %}
Basta moltiplicare ciascun valore dimensionale della query con quello pertinente di ciascun token e sommare i risultati. Ottieni 1 valore per coppia di token.
{% endhint %}
Per ciascuna parola nella frase, calcola il **punteggio di attenzione** rispetto a "shiny" calcolando il prodotto scalare dei loro embeddings.
Basta prendere ciascun peso di attenzione e moltiplicarlo per le dimensioni del token correlato e poi sommare tutte le dimensioni per ottenere un solo vettore (il vettore di contesto) 
{% endhint %}
Il **vettore di contesto** è calcolato come la somma pesata degli embeddings di tutte le parole, utilizzando i pesi di attenzione.
**Questo vettore di contesto rappresenta l'embedding arricchito per la parola "shiny", incorporando informazioni da tutte le parole nella frase.**
### Riepilogo del Processo
1.**Calcolare i Punteggi di Attenzione**: Utilizzare il prodotto scalare tra l'embedding della parola target e gli embeddings di tutte le parole nella sequenza.
2.**Normalizzare i Punteggi per Ottenere i Pesi di Attenzione**: Applicare la funzione softmax ai punteggi di attenzione per ottenere pesi che sommano a 1.
3.**Calcolare il Vettore di Contesto**: Moltiplicare l'embedding di ciascuna parola per il suo peso di attenzione e sommare i risultati.
## Auto-Attenzione con Pesi Addestrabili
In pratica, i meccanismi di auto-attenzione utilizzano **pesi addestrabili** per apprendere le migliori rappresentazioni per query, chiavi e valori. Questo comporta l'introduzione di tre matrici di peso:
* Dimensione di input `din=3` (dimensione dell'embedding)
* Dimensione di output `dout=2` (dimensione desiderata per query, chiavi e valori)
Inizializza le matrici di peso:
```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))
```
Calcola le query, le chiavi e i valori:
```python
queries = torch.matmul(inputs, W_query)
keys = torch.matmul(inputs, W_key)
values = torch.matmul(inputs, W_value)
```
#### Step 2: Calcola l'Attenzione a Prodotto Scalato
**Calcola i Punteggi di Attenzione**
Simile all'esempio precedente, ma questa volta, invece di utilizzare i valori delle dimensioni dei token, utilizziamo la matrice chiave del token (già calcolata utilizzando le dimensioni):. Quindi, per ogni query `qi` e chiave `kj`:
Prendendo un esempio da [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01\_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01\_main-chapter-code/ch03.ipynb) puoi controllare questa classe che implementa la funzionalità di auto-attention di cui abbiamo parlato:
Nota che invece di inizializzare le matrici con valori casuali, `nn.Linear` viene utilizzato per contrassegnare tutti i pesi come parametri da addestrare.
{% endhint %}
## Attenzione Causale: Nascondere Parole Future
Per i LLM vogliamo che il modello consideri solo i token che appaiono prima della posizione attuale per **prevedere il prossimo token**. **L'attenzione causale**, nota anche come **attenzione mascherata**, raggiunge questo obiettivo modificando il meccanismo di attenzione per impedire l'accesso ai token futuri.
Per implementare l'attenzione causale, applichiamo una maschera ai punteggi di attenzione **prima dell'operazione softmax** in modo che quelli rimanenti sommino ancora 1. Questa maschera imposta i punteggi di attenzione dei token futuri a meno infinito, garantendo che dopo il softmax, i loro pesi di attenzione siano zero.
Per **prevenire l'overfitting**, possiamo applicare **dropout** ai pesi di attenzione dopo l'operazione softmax. Il dropout **annulla casualmente alcuni dei pesi di attenzione** durante l'addestramento.
Code example from [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01\_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01\_main-chapter-code/ch03.ipynb):
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)
**L'attenzione a testa multipla** in termini pratici consiste nell'eseguire **più istanze** della funzione di auto-attenzione, ognuna con **i propri pesi**, in modo che vengano calcolati vettori finali diversi.
Potrebbe essere possibile riutilizzare il codice precedente e aggiungere solo un wrapper che lo lancia più volte, ma questa è una versione più ottimizzata da [https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01\_main-chapter-code/ch03.ipynb](https://github.com/rasbt/LLMs-from-scratch/blob/main/ch03/01\_main-chapter-code/ch03.ipynb) che elabora tutte le teste contemporaneamente (riducendo il numero di costose iterazioni for). Come puoi vedere nel codice, le dimensioni di ciascun token sono suddivise in diverse dimensioni in base al numero di teste. In questo modo, se un token ha 8 dimensioni e vogliamo utilizzare 3 teste, le dimensioni saranno suddivise in 2 array di 4 dimensioni e ciascuna testa utilizzerà uno di essi:
Per un'altra implementazione compatta ed efficiente, puoi utilizzare la [`torch.nn.MultiheadAttention`](https://pytorch.org/docs/stable/generated/torch.nn.MultiheadAttention.html) classe in PyTorch.
{% hint style="success" %}
Risposta breve di ChatGPT sul perché sia meglio dividere le dimensioni dei token tra le teste invece di far controllare a ciascuna testa tutte le dimensioni di tutti i token:
Sebbene consentire a ciascuna testa di elaborare tutte le dimensioni di embedding possa sembrare vantaggioso perché ogni testa avrebbe accesso a tutte le informazioni, la pratica standard è **dividere le dimensioni di embedding tra le teste**. Questo approccio bilancia l'efficienza computazionale con le prestazioni del modello e incoraggia ciascuna testa a imparare rappresentazioni diverse. Pertanto, dividere le dimensioni di embedding è generalmente preferito rispetto a far controllare a ciascuna testa tutte le dimensioni.