Werkplaats / De werkplaats
In jouw eigen tempo
← Multimodaal & generatief
MODULE 01 · LES 02

Laat beeldtokens elkaar gebruiken

Bereken ongemaskeerde beeldattention, lees CLS uit en tel de scorecellen bij meer resolutie.

65 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je berekent één ongemaskeerde beeldattentionhead en leest een CLS-classificatiescore uit.
  • Je onderscheidt querybreedte, tokenaantal en exacte scorekosten inclusief CLS.

Vier losse beeldtegels vertellen nog niet hoe onderdelen bij elkaar horen. Een gele vlek kan een stukje zon, trui of verkeersbord zijn. Beeldattention laat een patch informatie uit andere patches gebruiken, zodat een volgende laag meer context heeft.

Denk aan kaarten op tafel met een extra verzamelkaart. Iedere kaart stelt een vraag, vergelijkt die met de andere kaarten en maakt een gewogen mengsel van hun informatie. De verzamelkaart is het class-token, CLS, dat in de vorige les vóór de patches kwam.

We onderzoeken één zelfgekozen attentionhead. Dit is een rekenvoorbeeld zonder training, residualverbindingen, normalisatielagen of MLP; het volledige ViT bevat die onderdelen wel. Gebruik de transformerlessen als je wilt reconstrueren hoe deze stap in een volledig blok past.

1. Laat het hele beeld meedoen

De tokenmatrix Z heeft T rijen en D invoerkenmerken. Vermenigvuldig Z met drie gewichtsmatrices om queries Q, keys K en values V te krijgen. Queries en keys delen hun breedte d; values mogen een andere breedte hebben.

A=softmax(QKTd)

Iedere rij van A verdeelt gewicht over de keys. Daarna is de uitvoer Y gelijk aan AV: dezelfde gewichten mengen ieder valuekenmerk. Deel door de wortel van de query-/keybreedte, niet door de invoerbreedte D of het aantal tokens T.

Neem twee tokens met waarden 1 en 2, query- en keygewicht 1 en valuegewicht 3. De eerste query krijgt scores 1 en 2; na softmax wordt dat ongeveer 0,269 en 0,731. De values zijn 3 en 6, dus het eerste mengsel is ongeveer 5,193.

tokens = np.array([[1.],[2.]])
step = visual_attention(
    tokens,[[1]],[[1]],[[3]])
print(step['weights'].round(3).tolist())
# [[0.269, 0.731], [0.119, 0.881]]
print(step['context'].round(3).tolist())
# [[5.193], [5.642]]

Maak visual_attention met weights en context als uitvoervelden. Alle tokens mogen hier alle tokens gebruiken; een causaal driehoeksmasker uit een taalmodel zou informatie onnodig afsluiten. Een hoog attentiongewicht blijft een rekengewicht, geen bewijs dat deze patch de volledige voorspelling verklaart.

VERKEN HET ZELFEen andere vraag verandert het mengsel

Twee tokens hebben waarden 1 en 2. Keys blijven 1 en 2, values 3 en 6. Verander alleen de queryprojectie.

  1. Key 026,894%
  2. Key 173,106%
Gewogen uitvoer
5,193
Som van gewichten
1

Beide keys blijven bereikbaar. Dit is ongemaskeerde beeldattention, geen woordvoorspelling.

Tokens inclusief CLS
5
Aandachtsscorecellen
25

Patchmaat 2 blijft gelijk. Dit telt scores in één laag; de telling is geen meting van tijd of FLOPs.

Handgekozen rekenvoorbeelden. De getallen komen uit de eigen NumPy-functies; er is geen getrainde beeld- of tekstencoder.

01
CHECKPOINTMeng beeldtokens zonder causaal masker

2. Lees de verzamelkaart uit

CLS is rij nul, zowel vóór als na deze mengstap. Een classificatiehead projecteert die uitvoerrij naar een score per klasse en telt een bias op. Het zijn ruwe scores, ook wel logits; ze hoeven niet positief te zijn of op te tellen tot één.

scores = classify_visual_tokens(
    step['context'],[[1,-1]],[0,1])
print(scores.round(3).tolist())
# [5.193, -4.193]

Maak classify_visual_tokens voor deze lineaire head. Neem uitsluitend rij nul en geef een zelfstandige scorevector terug. De hoogste score is in dit voorbeeld index nul, maar zonder getrainde parameters en afgesproken klassennaam zegt die index niets over de inhoud van een foto.

Gemiddeld poolen is een mogelijke andere architectuurkeuze, maar het is een ander contract. Stilzwijgend alle tokenrijen middelen verandert hier de uitkomst. Controleer ook dat head en bias bij de featurebreedte en het aantal klassen passen.

02
CHECKPOINTLees de CLS-uitvoer uit

3. Meer resolutie vraagt meer vergelijkingen

Elke head vergelijkt ieder token met ieder token. Voor h heads bevat de scorematrix daarom hT² getallen. We tellen hier scorecellen in één laag; dit is geen FLOP-telling, totale modelgrootte of gemeten laadtijd.

small = visual_attention_cost(4,4,2)
large = visual_attention_cost(8,8,2)
print(small)
print(large)
# {'tokens': 5, 'scores': 25}
# {'tokens': 17, 'scores': 289}

De patchmaat blijft 2. Verdubbel je hoogte én breedte, dan stijgt het aantal patches van vier naar zestien; met CLS zijn dat vijf en zeventien tokens. Hun scoreaantallen verschillen een factor 11,56, dus bij dit kleine voorbeeld niet exact zestien.

print(large['scores']/small['scores'])
# 11.56

Maak visual_attention_cost met tokens en scores, inclusief CLS en het opgegeven aantal heads. Bij grote aantallen patches nadert de resolutiefactor zestien; CLS veroorzaakt de afwijking in kleine voorbeelden. Zo kun je een ontwerptrade-off narekenen voordat je een model kiest.

03
CHECKPOINTTel tokens en aandachtsscores

Wat neem je mee?

  • Beeldattention is hier ongemaskeerd en normaliseert langs keys.
  • De classificatiehead leest het bijgewerkte CLS-token uit.
  • Kleinere patches leveren meer tokens en veel meer onderlinge scores op.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-29.
DE VOLGENDE STAPBreng beeld en tekst in dezelfde richting
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Meng beeldtokens zonder causaal masker

Schrijf visual_attention. Maak één ongemaskeerde attentionhead met Q=tokens@wq,K=tokens@wk,V=tokens@wv. Queries en keys delen hun breedte d; values mogen een andere breedte hebben. Deel Q@K.T door sqrt(d), normaliseer per rij over keys en geef weights/context terug als zelfstandige floatmatrices. Maximaal65tokens, passende vormen, alle input/intermediate/outputarrays maximaal65536cellen. Gebruik de gegeven helpers; ongeldige vorm of niet-eindige berekening geeft ValueError. Behoud alle invoer en geef onafhankelijke uitvoer terug.

Een hint

Controleer eerst de vormen en reken het kleine voorbeeld uit de uitleg na. Gebruik de zichtbare gegeven helpers.

02
Lees de CLS-uitvoer uit

Schrijf classify_visual_tokens. Geef tokens[0]@head+bias als zelfstandige floatvector met ruwe klassescores. Controleer een niet-lege tokenmatrix, aansluitende head en bias, en de gegeven array-/eindigheidsgrenzen. Gebruik niet het gemiddelde over tokens. Ongeldige invoer/vorm of overloop geeft ValueError. Behoud alle invoer en geef onafhankelijke uitvoer terug.

Een hint

Controleer eerst de vormen en reken het kleine voorbeeld uit de uitleg na. Gebruik de zichtbare gegeven helpers.

03
Tel tokens en aandachtsscores

Schrijf visual_attention_cost. Geef tokens=(height//patch_size)*(width//patch_size)+1 en scores=heads*tokens*tokens als Python-ints in een dictionary. Height,width,patch_size zijn Python-ints1–4096, heads1–64; bools zijn ongeldig en de patchmaat moet beide afmetingen delen. Ongeldige invoer geeft ValueError. Dit telt scorecellen, geen FLOPs of gemeten tijd. Behoud alle invoer en geef onafhankelijke uitvoer terug.

Een hint

Controleer eerst de vormen en reken het kleine voorbeeld uit de uitleg na. Gebruik de zichtbare gegeven helpers.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

# Jouw functies

def visual_attention(tokens, wq, wk, wv):
    tokens=_mm_array(tokens,2,'tokens');wq=_mm_array(wq,2,'wq');wk=_mm_array(wk,2,'wk');wv=_mm_array(wv,2,'wv')
    if tokens.shape[0]>65 or any(weight.shape[0]!=tokens.shape[1] for weight in [wq,wk,wv]) or wq.shape[1]!=wk.shape[1]:
        raise ValueError('Een kleine head met maximaal65tokens en passende query/keyprojecties vereist')
    for weight in [wq,wk,wv]:_mm_shape(tokens.shape[0],weight.shape[1])
    _mm_shape(tokens.shape[0],tokens.shape[0])
    with np.errstate(over='ignore',invalid='ignore'):
        query=tokens@wq;key=tokens@wk;value=tokens@wv
        scores=_mm_result((query@key.T)/np.sqrt(query.shape[1]))
        weights=_mm_softmax(scores);context=_mm_result(weights@value)
    return {'weights':weights,'context':context}

def classify_visual_tokens(tokens, head, bias):
    tokens=_mm_array(tokens,2,'tokens');head=_mm_array(head,2,'head');bias=_mm_array(bias,1,'bias')
    if head.shape[0]!=tokens.shape[1] or bias.shape!=(head.shape[1],):raise ValueError('Classificatievormen sluiten niet aan')
    with np.errstate(over='ignore',invalid='ignore'):result=tokens[0]@head+bias
    return _mm_result(result)

def visual_attention_cost(height, width, patch_size, heads=1):
    if any(type(value) is not int or not 1<=value<=4096 for value in [height,width,patch_size]) or type(heads) is not int or not 1<=heads<=64:
        raise ValueError('Positieve gehele afmetingen tot4096en1–64heads vereist')
    if height%patch_size or width%patch_size:raise ValueError('Afmetingen moeten deelbaar zijn door de patchmaat')
    tokens=(height//patch_size)*(width//patch_size)+1
    return {'tokens':tokens,'scores':heads*tokens*tokens}

# Gegeven helpers — behouden gecontroleerde kopieën en begrensde arrays

def _mm_array(value, ndim, name):
    try:
        raw=np.asarray(value)
        if raw.dtype.kind not in 'iuf' or raw.ndim!=ndim or not raw.size or raw.size>65536:
            raise ValueError(name+': eindige niet-lege reële array met de afgesproken vorm vereist')
        array=raw.astype(np.float64,copy=True)
    except (TypeError,OverflowError) as error:
        raise ValueError(name+': numerieke array vereist') from error
    if not np.isfinite(array).all():raise ValueError(name+': alleen eindige waarden')
    return array

def _mm_result(value):
    if not np.isfinite(value).all():raise ValueError('De berekening valt buiten het eindige float64-bereik')
    return value

def _mm_shape(*shape):
    cells=1
    for dimension in shape:cells*=dimension
    if cells>65536:raise ValueError('Ook een afgeleide array mag maximaal65536cellen bevatten')

def _mm_softmax(scores):
    shifted=scores-scores.max(axis=1,keepdims=True)
    weights=np.exp(shifted)
    return weights/weights.sum(axis=1,keepdims=True)

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.