Ga naar hoofdinhoud

Multi-head attention in Transformers begrijpen

Leer wat multi-head attention is, hoe self-attention werkt binnen transformers en waarom deze mechanismen essentieel zijn voor LLM’s zoals GPT-5 en VLM’s zoals CLIP, met simpele voorbeelden, diagrammen en code.
· 12 min lezen

Verken met AI

Introductie tot het self-attention-mechanisme

Het attention-mechanisme, of specifieker het self-attention-mechanisme, werd voor het eerst geïntroduceerd in de beroemde  “Attention is All You Need“-paper van Vaswani et al. uit 2017. 

Dit mechanisme voedt het Transformer-model, dat op zijn beurt enkele van de krachtigste modellen aandrijft die we hebben, zoals LLM’s en VLM’s. 

Dus ja, het is best belangrijk!

Maar hier is het punt: veel mensen (en ik was er ook één!) denken dat attention in 2017 is uitgevonden.

Dat is niet zo.

Het oorspronkelijke attention-mechanisme werd voor het eerst geïntroduceerd in 2015, toen Bahdanau et al. het introduceerden in de context van machinevertaling. Hun idee was om de decoder bij elke decodeerstap op verschillende delen van de invoerzins te laten focussen, in plaats van te vertrouwen op één vaste contextvector.

Die kleine verschuiving maakte een enorm verschil — en plantte het zaadje voor wat self-attention zou worden en uiteindelijk multi-head attention.

Maar je hebt nu misschien een vraag:

“Wat betekent attention eigenlijk?”

Ik introduceer dit concept met twee simpele zinnen:

  • “De kat zat op de mat.” → Ons model zou aandacht kunnen richten van zat naar zowel kat als mat.
  • “Ze dronk haar thee omdat ze dorstig was.” → Aandacht vanaf dronk wijst naar zowel thee als dorstig, en legt zo oorzaak en handeling met elkaar in verband.

Uit deze voorbeelden wordt duidelijk wat attention probeert te doen. Het draait om focus: het model helpen beslissen welke andere woorden relevant zijn voor het woord dat het verwerkt, vaak op basis van semantische betekenis of grammaticale relaties.

Belangrijk om te weten is dat alle tokens worden gebruikt bij het berekenen van de attention-gewichten en -scores (later meer), niet slechts enkele specifieke tokens, zoals te zien is in het bovenstaande diagram. Dit betekent ook dat er tussen sommige woorden sterkere aandacht is dan tussen andere, weergegeven door de dikte van de pijlen.

Dit is waarom transformers zo goed zijn in taalbegrip; ze gebruiken attention om elk woord dynamisch te relateren aan elk ander woord.

Tussenstappen

We weten nu wat attention is, maar nog niet hoe de interne verwerking verloopt.

Hier komen Queries, Keys en Values (vaak afgekort als Q, K en V) om de hoek kijken. Voor de rest van dit artikel houden we vast aan onze voorbeeldzin: “the cat sat on the mat”. 

Allereerst wordt een zin die de Transformer in gaat, meteen getokenized door een tokenizer, waarbij Byte Pair Encoding een bekende tokenizer is. 

Je kunt tokenization zien als het opdelen van tekst in kleinere eenheden. Omdat alle woorden in onze zin vrij kort zijn, kunnen we in dit voorbeeld aannemen dat elke eenheid gelijk is aan een woord. Visueel zou de zin na de tokenizer er zo uitzien:

Let op dat zelfs de interpunctie (punt) is omgezet in een token. Wederom nemen we voor de eenvoud aan dat de punt een eigen, afzonderlijk token is. 

Na tokenization gebruiken we een tokenizer-vocabulaire om elk token te converteren naar zijn bijbehorende Token ID. De tokenizer-vocabulaire kun je zien als een database/tabel met alle tokens die het model kan herkennen en hun respectieve Token ID.

Vervolgens worden deze unieke token-ID’s via een embeddingmatrix aan hun respectieve token-embeddings gekoppeld. Belangrijk is dat al deze token-embeddings vectoren van dezelfde dimensie zijn; in ons voorbeeld nemen we 512 aan.

De laatste stap is het toevoegen van positional embeddings zodat het model positie-informatie van elk token heeft. De twee gangbare methoden zijn:

  1. Sinusoïdale encodering (gebruikt in de originele Transformer)
  2. Aangeleerde positie-embeddings (gebruikt in BERT, GPT)

De onderliggende mechaniek van deze positional embeddings valt buiten de scope van deze tutorial. Wat we nu hoeven te weten is dat we onze inputvectoren verkrijgen via deze formule:

Inputvector = Token-embedding + Positional embedding

Queries, keys en values

We zijn nu eindelijk klaar om Q-, K-, V-vectoren te introduceren. Voor elk woord in de input maken we drie vectoren aan:

  • Query (Q) – dit is waar het woord naar zoekt
  • Key (K) – dit is wat het woord biedt aan anderen
  • Value (V) – dit is de daadwerkelijke informatie die het woord bevat

Dit zijn leerbare transformaties en ook hun respectieve matrices. Wiskundig schrijven we deze transformaties als volgt:

Hieruit kan elk token worden weergegeven door drie nieuwe vectoren. Ik heb onderstaand diagram gemaakt om dit proces te visualiseren:

Scaled dot-product berekenen 

In de “Attention is all you need”-paper werd een wiskundige formule gegeven die laat zien hoe de nieuwe vectoren worden gebruikt om de attention-gewichten te berekenen. Laten we dit stap voor stap doornemen, ervan uitgaande dat we de attention-gewichten berekenen wanneer we bij “sat” zijn:

Formula presented in the "Attention is all you need" paper.

Om te beginnen vergelijken we de Queryvector van “sat” met de Keyvectoren van elk ander woord — inclusief zichzelf. Het resultaat is een set similariteitsscores (dit zijn scalars) die aangeven hoe relevant elk woord is voor “sat”. Deze staan formeler bekend als attentionscores.

Deze scores worden vervolgens geschaald via sqrt(d_k) om te voorkomen dat de scores te groot worden en om gradiënten te stabiliseren tijdens training via backpropagation. Deze geschaalde scores gaan daarna door een softmax, waardoor ze worden omgezet in attention-gewichten. 

De softmax geeft ons een waarschijnlijkheid die het model vertelt hoeveel gewicht aan elk woord moet worden gegeven, en daardoor zijn de attention-gewichten voor een bepaald token samen gelijk aan 1. 

Tot slot nemen we een gewogen som van de valuevectoren met behulp van deze attention-gewichten. 

Dit levert een nieuwe vector op die “sat” in context weergeeft, verrijkt door de informatie van alle tokens, maar vooral van cat, mat en andere relevante tokens.

Dit proces gebeurt voor elk token in de zin. Elk token kijkt naar elk ander token, en zelfs naar zichzelf. Daarom noemen we het self-attention.

Merk ook op dat we de nieuwe verrijkte vector voor één token hebben berekend, maar in de praktijk doen we dit voor veel, zo niet alle, tokens tegelijk, omdat matrixvermenigvuldiging extreem efficiënt is. 

Dat kan omdat de architectuur volledig paralleliseerbaar is. In tegenstelling tot recurrente neurale netwerken (RNN’s) die sequentieel verwerken, laat dit mechanisme Transformers alle tokens in één keer verwerken, wat ze snel en krachtig maakt.

Om dit te visualiseren, bekijk het bovenstaande diagram. Je ziet dat de inputmatrix X wordt getransformeerd naar de respectieve Query-, Key- en Value-matrices, en dat vervolgens de scaled dot-product attention-formule wordt toegepast om de volledige verrijkte embeddingvectoren van alle tokens in onze input te verkrijgen - A.

Scaled dot-product attention implementeren in PyTorch

Laten we nu in PyTorch coderen wat we zojuist hebben besproken. Verrassend genoeg is het vrij eenvoudig:

import torchimport torch.nn.functional as Fdef scaled_dot_product_attention(Q, K, V):    d_k = Q.size(-1)  # the dimension of the key vectors    scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))    attn_weights = F.softmax(scores, dim=-1)    output = torch.matmul(attn_weights, V)    return output, attn_weights

Door een simpele functie te implementeren, kunnen we de bovenstaande uitleg repliceren! Dit zijn de belangrijkste punten:

  • Q, K en V zijn tensors met vorm [batch_size, seq_len, dim]
  • De output is een nieuwe set vectoren waarin elk token wordt weergegeven in de context van de volledige sequentie
  • Attentionscores zijn de ruwe, ongenormaliseerde similariteiten
  • Attention-gewichten zijn de genormaliseerde (via softmax) waarden die focussen en optellen tot 1

Verschillende soorten attention

Er is echter één belangrijk detail dat ik nog niet heb genoemd. Er is niet slechts één soort attention in de Transformer - er zijn er eigenlijk drie:

  1. Encoder self-attention
  2. Decoder self-attention
  3. Encoder–decoder (cross) attention

Elk van deze speelt een iets andere rol, dus laten we ze kort doornemen.

Encoder self-attention

Dit is wat we tot nu toe hebben verkend: in de encoder kijkt elk token naar elk ander token in dezelfde inputsequentie, zoals in het voorbeeld “the cat sat on the mat.”. 

Belangrijk om te onthouden is dat er geen beperking is, omdat de volledige input vooraf bekend is, waardoor het model diepe contextuele representaties van elk token kan opbouwen, waarin het begrijpt wat ervoor en erna komt. 

Dit gebeurt dus in de encoder. 

Decoder self-attention

Hier wordt het net iets lastiger. Omdat we nu in de decoder zitten, willen we niet dat elk token de toekomst kan zien, want dat zou valsspelen zijn tijdens tekstgeneratie.

Neem nog een voorbeeld: “The dog barked very loudly.” Wanneer het model “barked” genereert (ervan uitgaande dat elk woord een token is voor de eenvoud), mag het niet weten dat het volgende token “very” is, anders leert het model niet om nieuwe tokens te genereren. 

Om dit op te lossen, passen we een causal mask toe (ook wel een driehoeksmaskeer genoemd) die ervoor zorgt dat elk token alleen naar zichzelf en de voorgaande tokens kan kijken, nooit naar de volgende.

Diagram shows a Causal mask being applied to a 5 token input

Uit het bovenstaande diagram blijkt dat de toekomstige attentionscores worden verwijderd door ze te maskeren, zodat het model leert om nieuwe, nauwkeurige en zinvolle tokens te genereren.

Encoder-decoder (cross) attention

Zodra de decoder is begonnen met genereren, heeft hij nog steeds informatie nodig van de oorspronkelijke input (waarop wordt gereageerd). Dit is waar cross-attention om de hoek komt kijken.

Hierbij kijkt de decoder naar de output van de encoder. Terwijl decoder self-attention gemaskeerd is, is cross-attention dat niet, en dus mag de decoder alle encoder-tokens in één keer bekijken.

Toen ik dit concept leerde, vond ik het vertaaloordeel erg verhelderend. Stel dat je “Bonjour tout le monde” naar het Engels vertaalt, dan moet de decoder toch naar alle Franse woorden kunnen kijken terwijl hij elk Engels woord kiest? 

Dat is precies wat cross-attention doet. We gaan hier echter niet dieper op in, omdat het niet nodig is voor de scope van dit artikel.

De noodzaak van multi-head attention

We begrijpen nu hoe self-attention werkt. Maar nu heb je misschien deze vraag:

Als we al attention hebben, waarom hebben we dan meerdere heads nodig?

Hiervoor gebruik ik graag de zaklamp-analogie, die mij hielp bij mijn intuïtie toen ik dit voor het eerst leerde.

Zie één attention-head als één zaklamp die probeert de belangrijke delen van een zin te verlichten. Maar een zin kent meerdere verschillende soorten relaties om te begrijpen: onderwerp-werkwoord, object-voornaamwoord, bijvoeglijk naamwoord-zelfstandig naamwoord, langetermijnafhankelijkheden, enzovoort.

Zou het niet beter zijn als we meer van deze relaties konden vastleggen, in plaats van telkens maar één?

De oplossing is dus om het model meerdere zaklampen te geven, die elk op verschillende delen van de zin focussen. Dat is multi-head attention.

In plaats van één set Q, K, V te berekenen en één enkele attentionberekening uit te voeren, splitsen we de embeddings in kleinere delen en draaien we meerdere onafhankelijke attention-heads parallel. 

Illustration showing how an Input Embedding is split evenly across the Multiple Heads

Zoals te zien is in het bovenstaande diagram, is de input-embedding (voor één token in dit eenvoudige geval) gelijkmatig opgesplitst over de n verschillende heads. 

In ons voorbeeld nemen we aan dat de dimensie 512 was en het aantal heads 8, dus verwerkt elke attention-head (512/8=64) dimensies in parallel, waarbij geprobeerd wordt verschillende relaties vast te leggen. 

Om je intuïtie verder te vormen over welke soorten relaties (ik zeg mogelijk, omdat we nog steeds niet volledig begrijpen naar welke relatie elke head kijkt) deze heads verwerken, lopen we door een simpel voorbeeld:

Neem de zin:

“The quick brown fox jumps over the lazy dog.”

  • Eén head zou kunnen focussen op bijvoeglijk naamwoord–zelfstandig naamwoord-paren: “quick → fox”, “lazy → dog”
  • Een andere head zou kunnen focussen op onderwerp–werkwoord: “fox → jumps”
  • En weer een andere head kan langeafstandsrelaties vastleggen: “jumps → over → dog”

Belangrijk om te benadrukken is dat al deze heads gelijktijdig draaien, en dat hun outputs worden geconcateneerd en terug geprojecteerd naar de oorspronkelijke embeddinggrootte (in ons geval 512). 

Meerdere heads visualiseren via code

Om onze kennis van Multi-Head Attention helemaal te verankeren, gebruiken we de HuggingFace-bibliotheek om te zien hoe het mechanisme werkt. 

!pip install transformers bertviz torch

Voer eerst de bovenstaande cel uit om alle relevante pakketten te downloaden.

from transformers import BertTokenizer, BertModelfrom bertviz import head_viewimport torchmodel_name = 'bert-base-uncased' # We will be using the Bert modelmodel = BertModel.from_pretrained(model_name, output_attentions=True)tokenizer = BertTokenizer.from_pretrained(model_name)model.eval() # As we are not training, we have set the model to evaluation mode so no updates are made# This is our input sentencesentence = "The quick brown fox jumps over the lazy dog."# Using the tokenizer to convert the sentence into tokensinputs = tokenizer(sentence, return_tensors='pt')input_ids = inputs['input_ids']# Forward pass with attentionswith torch.no_grad():    outputs = model(**inputs)    attentions = outputs.attentions  # Tuple of (num_layers, batch, num_heads, seq_len, seq_len)# Here we decode the tokens tokens = tokenizer.convert_ids_to_tokens(input_ids[0])head_view(attentions, tokens) # Visualization

De output die we krijgen, zie je hieronder. Merk op dat er 12 verschillende kleuren zijn, wat betekent dat dit model 12 heads heeft. De dikte van de lijnen correleert ook met de grootte van de attentionscores tussen de tokens. 

Layer 1's Multi-Head Attention Visualization of our example

Multi-head attention implementeren: best practices

In deze sectie schrijven we de pseudocode voor het multi-head attention-mechanisme. Laten we eerst aannemen dat onze input X de vorm [batch, seq_len, d_model] heeft. 

Vervolgens kunnen we verder met de pseudocode: 

Q = X @ W_Q # Calculating the Query valuesK = X @ W_K # Calculating the Key valuesV = X @ W_V# Calculating the Value valuesQ_i, K_i, V_i = split_into_heads(Q, K, V, num_heads) # Now we split the Q,K,V into multiple different heads (by evenly splitting the dimensions)# Here we compute scaled dot-product attention for each headscores = (Q_i @ K_i.T) / sqrt(head_dim)scores += maskA_i = softmax(scores)A_i = dropout(A_i)Z_i = A_i @ V_iZ = concat_heads(Z_i) # We have to concatenate all of the Heads# This is our final linear projection matrixoutput = Z @ W_Ooutput = dropout(output)
Dit alles zou je inmiddels bekend moeten voorkomen, behalve misschien de laatste lineaire projectiematrix. 

Laten we dat snel doornemen. 

Nadat alle afzonderlijke attention-heads hun werk hebben gedaan, worden hun outputs weer samengevoegd tot één vector. Maar deze gecombineerde output moet nog worden teruggebracht naar de originele modeldimensie en hier komt de laatste lineaire projectiematrix om de hoek kijken. Die map de geconcateneerde output terug naar dezelfde grootte als de input-embedding, zodat die kan worden doorgegeven aan de volgende laag in de Transformer.

Laten we ook enkele optimalisaties en best practices bespreken:

  1. Parallelisatie: omdat elke head onafhankelijk is, kunnen we ze allemaal parallel berekenen.
  2. Matrixvermenigvuldiging: we batchen alles met lineaire-algebra-operaties (bijv. matmul), die sterk geoptimaliseerd zijn.
  3. FlashAttention (gevorderd): dit is een nieuwere techniek die low-level CUDA-trucs gebruikt om het geheugenverbruik te verminderen en attention te versnellen — vooral bij lange sequenties.

Qua best practices, wanneer we de attentionlaag coderen, zijn er 3 hoofd-hyperparameters om naar te kijken:

  1. num_heads: hoeveel attention-heads je parallel draait; zorg dat dit getal netjes deelt in d_model.
  2. head_dim: de dimensie van elke head.
  3. dropout_rate: om overfitting te voorkomen.

Conclusie

We hebben veel behandeld in dit artikel. We begonnen met een simpel idee: attention gaat over focus. Daarna bekeken we hoe Transformers deze focus gebruiken door te berekenen hoe elk woord zich verhoudt tot elk ander woord met Queries, Keys en Values.

We zagen hoe self-attention (en kort andere vormen zoals causale en cross-attention) elk token een rijke, contextbewuste representatie geeft. En toen gingen we een stap verder met multi-head attention, waarbij we het model in plaats van één “zaklamp” meerdere geven, die elk tegelijkertijd verschillende patronen in de zin vastleggen.

Dit is wat Transformers hun echte kracht geeft. Ze lezen niet alleen; ze begrijpen de onderliggende structuur, betekenis en relaties binnen hele sequenties. 

We hebben in dit artikel één aspect van Transformers behandeld, maar er valt nog veel meer te leren. Bekijk daarom deze volledige, gedetailleerde cursus: Transformer Models with PyTorch Course. 

Als je direct met toepassingen aan de slag wilt, raad ik ook het Developing Multi-Input Models For OCR-project of de cursus Working with HuggingFace aan. Lees je liever artikelen? Check dan deze intuïtieve Complete gids voor het bouwen van een Transformer-model met PyTorch en mijn andere gids over Vision Transformers. 

Multi-Head Attention: veelgestelde vragen

Hoe verbetert multi-head attention de prestaties van Transformers?

Door meerdere kleinere attention-heads parallel te laten draaien, kan het model verschillende patronen (zoals syntaxis, positioneel, enz.) tegelijk vastleggen, wat de nauwkeurigheid en stabiliteit verbetert.

Wat doen queries, keys en values?

Simpel gezegd: queries vragen “waar ben ik naar op zoek?”, keys zeggen “wat bied ik aan?”, en values dragen de informatie die met attention-gewichten wordt gemengd.

Single-head vs. multi-head — wat is het verschil?

Single-head gebruikt één projectie, terwijl multi-head de modeldimensie opdeelt in h (het aantal heads) subruimtes, onafhankelijk aandacht berekent in elk, en vervolgens concateneert en terug projecteert.

Hoeveel heads moeten we gebruiken?

Dat bepalen wij! Je moet er alleen op letten dat de embeddingdimensie deelbaar is door h - het aantal heads. Veelgebruikte keuzes zijn veelvouden van 2, maar onthoud dat meer heads de rekentijd en het geheugenverbruik verhogen.

Waar wordt multi-head attention gebruikt?

Het is het kernbouwblok van moderne foundation-modellen, zoals in NLP (vertaling, QA, samenvatting), visie (ViT) en spraak (ASR, Conformers).

Vaibhav Mehra

Auteur

Vaibhav Mehra

Onderwerpen

Gerelateerd