Cybersecurity & cyberwarfare<p><b>Intelligenza Artificiale: Implementazione del meccanismo dell’attenzione in Python</b></p><p>Il meccanismo di attenzione è spesso associato all’architettura dei <strong>transformers</strong>, ma era già stato utilizzato nelle <strong>RNN (reti ricorrenti)</strong>.</p><p>Nei task di traduzione automatica (ad esempio, inglese-italiano), quando si vuole prevedere la parola italiana successiva, è necessario che<em> il modello si concentri, o presti attenzione, sulle parole inglesi più importanti nell’input, utili per ottenere una buona traduzione.</em></p><p>Non entrerò nei dettagli delle RNN, ma<strong> l’attenzione ha aiutato questi modelli a mitigare il problema vanishing gradient,</strong> e a catturare più dipendenze a lungo raggio tra le parole.</p><p>A un certo punto, abbiamo capito che l’unica cosa importante era il meccanismo di attenzione e che l’intera architettura RNN era superflua. Quindi, <a href="https://arxiv.org/abs/1706.03762" rel="nofollow noopener" target="_blank">Attention is All You Need!</a><br> </p><p><strong>Self-Attention nei Transformers</strong></p><p><br>L’attenzione classica indica dove le parole della sequenza in output devono porre attenzione rispetto alle parole della sequenza di input. È importante in task del tipo sequence-to-sequence come la traduzione automatica.</p><p>La self-attention è un tipo specifico di attenzione. Opera tra due elementi qualsiasi della stessa sequenza. Fornisce informazioni su quanto siano “correlate” le parole nella stessa frase.</p><p>Per un dato token (o parola) in una sequenza, la self-attention genera un elenco di pesi di attenzione corrispondenti a tutti gli altri token della sequenza. Questo processo viene applicato a ogni token della frase, ottenendo una matrice di pesi di attenzione (come nella figura).</p><p>Questa è l’idea generale, in pratica le cose sono un po’ più complicate perché vogliamo aggiungere molti parametri/pesi nell nostra rete, in modo che il modella abbia più capacità di apprendimento.<br> </p><p><strong>Le rappresentazioni K, V, Q</strong></p><p><br>L’input del nostro modello è una frase come “mi chiamo <a href="https://www.linkedin.com/in/marcello-politi/" rel="nofollow noopener" target="_blank">Marcello Politi</a>”. Con il processo di tokenizzazione, una frase viene convertita in un elenco di numeri come [2, 6, 8, 3, 1].</p><p>Prima di passare la frase al transformer, dobbiamo creare una rappresentazione densa per ogni token.</p><p>Come creare questa rappresentazione? Moltiplichiamo ogni token per una matrice. La matrice viene appresa durante l’addestramento.</p><p>Aggiungiamo ora un po’ di complessità.</p><p>Per ogni token, creiamo 3 vettori invece di uno, che chiamiamo vettori: chiave (K), valore (V) e domanda (Q). (Vedremo più avanti come creare questi 3 vettori).</p><p>Concettualmente questi 3 token hanno un significato particolare:</p><ul><li>La chiave del vettore rappresenta l’informazione principale catturata dal token.</li><li>Il valore del vettore cattura l’informazione completa di un token.</li><li>Il vettore query, è una domanda sulla rilevanza del token per il task corrente.</li></ul><p>L’idea è che ci concentriamo su un particolare token <em>i</em> e vogliamo chiedere qual è l’importanza degli altri token della frase rispetto al token <em>i</em> che stiamo prendendo in considerazione.</p><p>Ciò significa che prendiamo il vettore <em>q_i</em> (poniamo una domanda relativa a <em>i</em>) per il token <em>i</em>, e facciamo alcune operazioni matematiche con tutti gli altri token <em>k_j</em> (<em>j!=</em>i). È come se ci chiedessimo a prima vista quali sono gli altri token della sequenza che sembrano davvero importanti per capire il significato del token <em>i</em>.</p><p>Ma qual’è questa operazione magica?</p><p>Dobbiamo moltiplicare (dot-product) il vettore della query per i vettori delle chiavi e dividere per un fattore di normalizzazione. Questo viene fatto per ogni token <em>k_j</em>.</p><p>In questo modo, otteniamo uno scroe per ogni coppia (<em>q_i, k_j</em>). Trasformiamo questi score in una distribuzione di probabilità applicandovi un’operazione di softmax. Bene, ora abbiamo ottenuto i pesi di attenzione!</p><p>Con i pesi di attenzione, sappiamo qual è l’importanza di ogni token <em>k_j</em> per indistinguere il token <em>i</em>. Quindi ora moltiplichiamo il vettore di valore <em>v_j</em> associato a ogni token per il suo peso e sommiamo i vettori. In questo modo otteniamo il vettore finale <strong>context-aware</strong> del <em>token_i</em>.</p><p>Se stiamo calcolando il vettore denso contestuale del <em>token_1</em>, calcoliamo:</p><p><em>z1 = a11v1 + a12v2 + … + a15*v5</em></p><p>Dove <em>a1j</em> sono i pesi di attenzione del computer e <em>v_j</em> sono i vettori di valori.</p><p>Fatto! Quasi…</p><p>Non ho spiegato come abbiamo ottenuto i vettori k, v e q di ciascun token. Dobbiamo definire alcune matrici w_k, w_v e w_q in modo che quando moltiplichiamo:</p><ul><li>token * w_k -> k</li><li>token * w_q -> q</li><li>token * w_v -> v</li></ul><p>Queste tre matrici sono inizializzate in modo casuale e vengono apprese durante l’addestramento; questo è il motivo per cui abbiamo molti parametri nei modelli moderni come gli LLM.<br> </p><p><strong>Multi-Head Self-Attention (MHSA) nei Transformers </strong></p><p><br>Siamo sicuri che il precedente meccanismo di self-attention sia in grado di catturare tutte le relazioni importanti tra i token (parole) e di creare vettori densi di quei token che abbiano davvero senso?</p><p>In realtà potrebbe non funzionare sempre perfettamente. E se, per mitigare l’errore, si rieseguisse l’intera operazione due volte con nuove matrici w_q, w_k e w_v e si unissero in qualche modo i due vettori densi ottenuti? In questo modo forse una self-attention è riuscita a cogliere qualche relazione e l’altra è riuscita a cogliere qualche altra relazione.</p><p>Ebbene, questo è ciò che accade esattamente in MHSA. Il caso appena discusso contiene due head (teste), perché ha due insiemi di matrici w_q, w_k e w_v. Possiamo avere anche più head: 4, 8, 16, ecc.</p><p>L’unica cosa complicata è che tutte queste teste vengono gestite in parallelo, elaborandole tutte nello stesso calcolo utilizzando i tensori.</p><p>Il modo in cui uniamo i vettori densi di ogni head è semplice, li concateniamo (quindi la dimensione di ogni vettore deve essere più piccola, in modo che quando li concateniamo otteniamo la dimensione originale che volevamo) e passiamo il vettore ottenuto attraverso un’altra matrice imparabile w_o.<br> </p><p><strong>Hands-on</strong></p><p>Supponiamo di avere una frase. Dopo la tokenizzazione, ogni token (o parola) corrisponde a un indice (numero):</p><p>tokenized_sentence = torch.tensor([<br> 2, #my<br> 6, #name<br> 8, #is<br> 3, #marcello<br> 1 #politi<br>])<br>tokenized_sentence</p><p>Prima di passare la frase nel transformer, dobbiamo creare una rappresentazione densa per ciascun token.</p><p>Come creare questa rappresentazione? Moltiplichiamo ogni token per una matrice. Questa matrice viene appresa durante l’addestramento.</p><p>Costruiamo questa matrice, chiamata matrice di embedding.</p><p>torch.manual_seed(0) # set a fixed seed for reproducibility<br>embed = torch.nn.Embedding(10, 16)</p><p>Se moltiplichiamo la nostra frase tokenizzata con la matrice di embedding, otteniamo una rappresentazione densa di dimensione 16 per ogni token</p><p>sentence_embed = embed(tokenized_sentence).detach()<br>sentence_embed</p><p>Per utilizzare il meccanismo di attenzione dobbiamo creare 3 nuove matrici w_q, w_k e w_v. Moltiplicando un token di ingresso per w_q otteniamo il vettore q. Lo stesso vale per w_k e w_v.</p><p>d = sentence_embed.shape[1] # let's base our matrix on a shape (16,16)</p><p>w_key = torch.rand(d,d)<br>w_query = torch.rand(d,d)<br>w_value = torch.rand(d,d)</p><p><strong>Calcolo dei pesi di attenzione</strong></p><p><br>Calcoliamo ora i pesi di attenzione solo per il primo token della frase.</p><p>token1_embed = sentence_embed</p><p>[0]<a href="https://poliverso.org/search?tag=compute" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>compute</span></a> the tre vector associated to token1 vector : q,k,v<br>key_1 = w_key.matmul(token1_embed)<br>query_1 = w_query.matmul(token1_embed)<br>value_1 = w_value.matmul(token1_embed)</p><p>print("key vector for token1: \n", key_1)<br>print("query vector for token1: \n", query_1)<br>print("value vector for token1: \n", value_1)</p><p>Dobbiamo moltiplicare il vettore query associato al token1 (query_1) con tutte le chiavi degli altri vettori.</p><p>Quindi ora dobbiamo calcolare tutte le chiavi (chiave_2, chiave_2, chiave_4, chiave_5). Ma aspettate, possiamo calcolarle tutte in una sola volta moltiplicando sentence_embed per la matrice w_k.</p><p>keys = sentence_embed.matmul(w_key.T)<br>keys[0] <a href="https://poliverso.org/search?tag=contains" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>contains</span></a> the key vector of the first token and so on</p><p>Facciamo la stessa cosa con i valori</p><p>values = sentence_embed.matmul(w_value.T)<br>values[0] <a href="https://poliverso.org/search?tag=contains" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>contains</span></a> the value vector of the first token and so on</p><p>Calcoliamo la prima parte della formula adesso.</p><p>import torch.nn.functional as F</p><p># the following are the attention weights of the first tokens to all the others<br>a1 = F.softmax(query_1.matmul(keys.T)/d**0.5, dim = 0)<br>a1</p><p>Con i pesi di attenzione sappiamo qual è l’importanza di ciascun token. Quindi ora moltiplichiamo il vettore di valori associato a ogni token per il suo peso.</p><p>Per ottenere il vettore finale del token_1 che includa anche il contesto.</p><p>z1 = a1.matmul(values)<br>z1</p><p>Allo stesso modo, possiamo calcolare i vettori densi consapevoli del contesto di tutti gli altri token. Ora stiamo utilizzando sempre le stesse matrici w_k, w_q, w_v. Diciamo che usiamo una sola head.</p><p>Ma possiamo avere più triplette di matrici, quindi una multi-heads. Ecco perché si chiama multi-head attention.</p><p>I vettori densi di un token in ingresso, dati in input a ciascuna head, vengono poi concatenati e trasformati linearmente per ottenere il vettore denso finale.</p><p>import torch<br>import torch.nn as nn<br>import torch.nn.functional as F</p><p>torch.manual_seed(0) #</p><p># Tokenized sentence (same as yours)<br>tokenized_sentence = torch.tensor([2, 6, 8, 3, 1]) # [my, name, is, marcello, politi]</p><p># Embedding layer: vocab size = 10, embedding dim = 16<br>embed = nn.Embedding(10, 16)<br>sentence_embed = embed(tokenized_sentence).detach() # Shape: [5, 16] (seq_len, embed_dim)</p><p>d = sentence_embed.shape[1] # embed dimension 16<br>h = 4 # Number of heads<br>d_k = d // h # Dimension per head (16 / 4 = 4)</p><p># Define weight matrices for each head<br>w_query = torch.rand(h, d, d_k) # Shape: [4, 16, 4] (one d x d_k matrix per head)<br>w_key = torch.rand(h, d, d_k) # Shape: [4, 16, 4]<br>w_value = torch.rand(h, d, d_k) # Shape: [4, 16, 4]<br>w_output = torch.rand(d, d) # Final linear layer: [16, 16]</p><p># Compute Q, K, V for all tokens and all heads<br># sentence_embed: [5, 16] -> Q: [4, 5, 4] (h, seq_len, d_k)<br>queries = torch.einsum('sd,hde->hse', sentence_embed, w_query) # h heads, seq_len tokens, d dim<br>keys = torch.einsum('sd,hde->hse', sentence_embed, w_key) # h heads, seq_len tokens, d dim<br>values = torch.einsum('sd,hde->hse', sentence_embed, w_value) # h heads, seq_len tokens, d dim</p><p># Compute attention scores<br>scores = torch.einsum('hse,hek->hsk', queries, keys.transpose(-2, -1)) / (d_k ** 0.5) # [4, 5, 5]<br>attention_weights = F.softmax(scores, dim=-1) # [4, 5, 5]</p><p># Apply attention weights<br>head_outputs = torch.einsum('hij,hjk->hik', attention_weights, values) # [4, 5, 4]<br>head_outputs.shape</p><p># Concatenate heads<br>concat_heads = head_outputs.permute(1, 0, 2).reshape(sentence_embed.shape[0], -1) # [5, 16]<br>concat_heads.shape</p><p>multihead_output = concat_heads.matmul(w_output) # [5, 16] @ [16, 16] -> [5, 16]<br>print("Multi-head attention output for token1:\n", multihead_output[0])</p><p><strong>Conclusioni</strong></p><p><br>In questo post ho implementato una versione semplice del meccanismo di attenzione. Questo non è il modo in cui viene realmente implementato nei framework moderni, ma il mio scopo è quello di fornire alcuni spunti per permettere a chiunque di capire come funziona. Nei prossimi articoli analizzerò l’intera implementazione di un’architettura transformer.</p><p>L'articolo <a href="https://www.redhotcyber.com/post/implementazione-del-meccanismo-dellattenzione-in-python/" rel="nofollow noopener" target="_blank">Intelligenza Artificiale: Implementazione del meccanismo dell’attenzione in Python</a> proviene da <a href="https://www.redhotcyber.com/feed" rel="nofollow noopener" target="_blank">il blog della sicurezza informatica</a>.</p>