Introduzione: La Rivoluzione dei Transformer
I Transformer, introdotti nel paper "Attention Is All You Need" (2017), hanno rivoluzionato l'AI. Da GPT a BERT, da DALL-E a Stable Diffusion, da Claude a Gemini: tutti si basano sull'architettura Transformer. Il segreto del loro successo e il meccanismo di self-attention, che permette di catturare dipendenze a lungo raggio senza le limitazioni delle reti ricorrenti.
Cosa Imparerai
- Query, Key, Value: le tre proiezioni lineari
- Scaled Dot-Product Attention: la formula centrale
- perchè dividere per la radice di d
- Multi-Head Attention: prospettive multiple
- Positional Encoding: aggiungere ordine senza ricorrenza
- Implementazione step-by-step in NumPy
Proiezioni Q, K, V: Tre Prospettive sui Dati
Il meccanismo di attention opera su tre trasformazioni lineari dell'input \\mathbf{X} \\in \\mathbb{R}^{n \\times d_{\\text{model}}}:
dove \\mathbf{W}^Q, \\mathbf{W}^K \\in \\mathbb{R}^{d_{\\text{model}} \\times d_k} e \\mathbf{W}^V \\in \\mathbb{R}^{d_{\\text{model}} \\times d_v}.
Intuizione:
- Query (\\mathbf{Q}): "cosa sto cercando?" - la domanda che ogni token pone
- Key (\\mathbf{K}): "cosa offro?" - l'etichetta di ogni token
- Value (\\mathbf{V}): "qual e il mio contenuto?" - l'informazione effettiva
Il meccanismo di attention calcola la similarità tra ogni Query e tutte le Key, usa queste similarità come pesi, e combina i Value corrispondenti.
Scaled Dot-Product Attention
La formula centrale dei Transformer:
Passo per passo:
- \\mathbf{Q} \\mathbf{K}^T \\in \\mathbb{R}^{n \\times n}: matrice di similarità tra tutti i token (prodotto scalare)
- Divisione per \\sqrt{d_k}: scaling per stabilità numerica
- Softmax per riga: converte i punteggi in pesi (probabilità) che sommano a 1
- Moltiplicazione per \\mathbf{V}: media pesata dei Value
perchè il Fattore di Scaling?
Senza \\sqrt{d_k}, il prodotto scalare cresce con la dimensione dei vettori. Se q e k hanno componenti i.i.d. con media 0 e varianza 1, allora:
Per valori grandi di d_k, il prodotto scalare avrebbe valori molto grandi o molto piccoli, portando la softmax in regioni di saturazione (gradienti quasi zero). Dividendo per \\sqrt{d_k}, la varianza torna a 1.
import numpy as np
def softmax(x, axis=-1):
exp_x = np.exp(x - np.max(x, axis=axis, keepdims=True))
return exp_x / np.sum(exp_x, axis=axis, keepdims=True)
def scaled_dot_product_attention(Q, K, V, mask=None):
"""Scaled Dot-Product Attention."""
d_k = Q.shape[-1]
# 1. Calcola punteggi di similarità
scores = Q @ K.transpose(0, 2, 1) if Q.ndim == 3 else Q @ K.T
scores = scores / np.sqrt(d_k)
# 2. Applica mask (opzionale, per decoder)
if mask is not None:
scores = np.where(mask == 0, -1e9, scores)
# 3. Softmax per ottenere pesi di attenzione
attention_weights = softmax(scores)
# 4. Media pesata dei Value
if Q.ndim == 3:
output = attention_weights @ V
else:
output = attention_weights @ V
return output, attention_weights
# Esempio: sequenza di 4 token, dimensione 8
np.random.seed(42)
seq_len, d_model, d_k = 4, 8, 8
X = np.random.randn(seq_len, d_model)
# Matrici di proiezione
W_Q = np.random.randn(d_model, d_k) * 0.1
W_K = np.random.randn(d_model, d_k) * 0.1
W_V = np.random.randn(d_model, d_k) * 0.1
# Proiezioni
Q = X @ W_Q
K = X @ W_K
V = X @ W_V
# Attention
output, weights = scaled_dot_product_attention(Q, K, V)
print(f"Input shape: {X.shape}")
print(f"Attention weights:\n{np.round(weights, 3)}")
print(f"Output shape: {output.shape}")
Multi-Head Attention: Prospettive Multiple
Invece di un singolo meccanismo di attention, i Transformer usano h teste (heads) in parallelo, ognuna con le proprie matrici di proiezione:
Ogni testa ha dimensione d_k = d_{\\text{model}} / h. Con 8 teste e d_{\\text{model}} = 512, ogni testa opera in uno spazio di 64 dimensioni.
perchè più teste? Ogni testa può catturare un tipo diverso di relazione: una testa potrebbe focalizzarsi sulla sintassi, un'altra sulla semantica, un'altra sulla co-riferenza.
import numpy as np
def multi_head_attention(X, n_heads, d_model):
"""Multi-Head Attention implementazione semplificata."""
d_k = d_model // n_heads
seq_len = X.shape[0]
# Proiezioni per ogni testa
heads_output = []
all_weights = []
for h in range(n_heads):
W_Q = np.random.randn(d_model, d_k) * 0.1
W_K = np.random.randn(d_model, d_k) * 0.1
W_V = np.random.randn(d_model, d_k) * 0.1
Q = X @ W_Q
K = X @ W_K
V = X @ W_V
# Scaled dot-product attention
scores = Q @ K.T / np.sqrt(d_k)
weights = softmax(scores)
head_out = weights @ V
heads_output.append(head_out)
all_weights.append(weights)
# Concatena le teste
concat = np.concatenate(heads_output, axis=-1) # (seq_len, d_model)
# Proiezione output
W_O = np.random.randn(d_model, d_model) * 0.1
output = concat @ W_O
return output, all_weights
np.random.seed(42)
seq_len, d_model, n_heads = 6, 16, 4
X = np.random.randn(seq_len, d_model)
output, weights = multi_head_attention(X, n_heads, d_model)
print(f"Input: {X.shape}, Output: {output.shape}")
print(f"Numero teste: {n_heads}, d_k per testa: {d_model // n_heads}")
Positional Encoding: Ordine Senza Ricorrenza
L'attention e permutation-invariant: non distingue l'ordine dei token. Per aggiungere informazione sulla posizione, si usa il positional encoding basato su funzioni sinusoidali:
perchè seno e coseno? perchè PE_{pos+k} può essere espresso come trasformazione lineare di PE_{pos}, permettendo al modello di imparare facilmente a "guardare" a distanze relative fisse.
import numpy as np
def positional_encoding(max_len, d_model):
"""Sinusoidal Positional Encoding."""
PE = np.zeros((max_len, d_model))
position = np.arange(max_len)[:, np.newaxis]
div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
PE[:, 0::2] = np.sin(position * div_term) # Indici pari: seno
PE[:, 1::2] = np.cos(position * div_term) # Indici dispari: coseno
return PE
# Genera positional encoding
max_len, d_model = 100, 64
PE = positional_encoding(max_len, d_model)
print(f"PE shape: {PE.shape}")
print(f"PE[0, :8]: {np.round(PE[0, :8], 4)}")
print(f"PE[1, :8]: {np.round(PE[1, :8], 4)}")
# L'embedding finale e: token_embedding + positional_encoding
token_embedding = np.random.randn(10, d_model) # 10 token
input_with_position = token_embedding + PE[:10]
print(f"\nEmbedding + PE shape: {input_with_position.shape}")
Layer Normalization e Residual Connections
Ogni sub-layer del Transformer usa residual connections e layer normalization:
La layer normalization normalizza lungo la dimensione delle feature:
dove \\mu e \\sigma sono calcolati per ciascun campione, e \\gamma, \\beta sono parametri apprendibili.
Riepilogo
Punti Chiave da Ricordare
- Attention: \\text{softmax}(QK^T / \\sqrt{d_k}) V - similarità pesata tra token
- Scaling \\sqrt{d_k}: previene saturazione della softmax
- Multi-head: h teste in parallelo catturano relazioni diverse
- Positional Encoding: seno/coseno aggiungono ordine sequenziale
- Residual + LayerNorm: stabilizzano il training di reti profonde
- L'intera architettura e composta di operazioni di algebra lineare e softmax
Nel Prossimo Articolo: esploreremo la data augmentation e le tecniche di generazione dati sintetici. SMOTE, Mixup, augmentation per immagini e testo, e quando l'augmentation aiuta davvero.







