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

Maak beeldpatches klaar voor een transformer

Van een expliciet HWC-rooster via gedeelde projectie naar een reeks met CLS en posities.

65 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je ordent HWC-patches, past een gedeelde projectie toe en voegt CLS en posities toe.
  • Je bewaakt pixelvolgorde, kanaalvolgorde, aansluitende vormen en onafhankelijke uitvoer.

Een taalmodel krijgt tokens, maar een foto is een rooster met kleurwaarden. Hoe kun je dezelfde transformerbouwstenen toch op beelden toepassen? Je maakt eerst kleine beeldtegels, zet iedere tegel om naar een vector en bewaart waar die tegel zat.

Denk aan een foto die je in vier kaarten knipt. Iedere kaart houdt zijn eigen pixels; de transformer kan de kaarten later met elkaar vergelijken. Alleen knippen leert nog niets: de gedeelde projectie en de latere lagen moeten tijdens echte training bruikbare patronen leren.

In deze les kies je de gewichten zelf om iedere stap te kunnen narekenen. Het voorbeeld is geen getrainde beeldherkenner. Je bouwt de invoer van een Vision Transformer (ViT); de attention en de uiteindelijke voorspelling volgen in de volgende les.

1. Van rooster naar geordende tegels

Onze invoer heeft drie assen: hoogte H, breedte W en kleurkanalen C. Een grijsbeeld heeft hier expliciet één kanaal; een RGB-beeld heeft er drie. Een vierkante patch heeft zijde P en bevat dus P×P×C getallen.

N=HWP2

N is het aantal patches. Voor een beeld van 4 bij 4 met patches van 2 bij 2 krijg je vier patches, met vier grijswaarden per patch. Hoogte en breedte moeten in ons lescontract deelbaar zijn door de patchmaat; we vullen de rand niet ongemerkt met nullen aan.

image = np.arange(16).reshape(4,4,1)
patches = patchify(image,2)
print(patches[0].tolist())
# [0.0, 1.0, 4.0, 5.0]

Lees binnen een patch eerst de bovenste pixelrij, dan de volgende. Bij kleur komen de kanalen van dezelfde pixel direct achter elkaar. Lees de patches zelf van links naar rechts en vervolgens van boven naar beneden; een andere volgorde verwisselt de betekenis van de latere posities.

print(patches.shape)
print(patches[1].tolist())
# (4, 4)
# [2.0, 3.0, 6.0, 7.0]

Maak patchify met deze volgorde en een onafhankelijke uitvoer. Alleen image.reshape(4,4) gebruiken is een veelgemaakte fout: de eerste rij bevat dan een hele beeldrij in plaats van de linker bovenpatch. Controleer daarom de waarden, niet alleen de vorm.

VERKEN HET ZELFKnippen verandert de invoer, niet de pixels

Verander de patchmaat en kies een patch. Volg dezelfde pixels van het rooster naar één rij en zijn projectie.

[0, 1, 4, 5]

De vaste projectie telt afwisselende kenmerken op; bias [0, 1]. Uitvoer: [4, 7].

Patches
4
Met CLS
5
Getallen per patch
4

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

01
CHECKPOINTKnip en orden de beeldpatches

2. Geef iedere patch dezelfde projectie

De transformer verwacht steeds dezelfde vectorbreedte D. Vermenigvuldig daarom iedere uitgevlakte patchrij x met dezelfde gewichtsmatrix E, en tel de biasvector b op. E heeft P²C rijen en D kolommen; iedere geprojecteerde rij e heeft D getallen.

e=xE+b

Kies D=2. Ons eerste kenmerk telt de eerste en derde patchwaarde op; het tweede telt de tweede en vierde op, plus bias 1. Voor de eerste patch geeft dat 0+4=4 en 1+5+1=7.

weight = np.array([
    [1,0],[0,1],[1,0],[0,1]])
bias = np.array([0,1])
embedded = project_patches(
    patches,weight,bias)
print(embedded[0].tolist())
# [4.0, 7.0]

Maak project_patches voor de hele matrix. Reken met floats vóór de vermenigvuldiging, behoud de invoer en controleer de aansluitende vormen. Deze gedeelde lineaire stap lijkt op een convolutie met stapgrootte P; hij maakt op zichzelf nog geen contact tussen verschillende patches.

02
CHECKPOINTProjecteer met gedeelde gewichten

3. Bewaar een verzamelplek en de positie

Zet vóór de patchrijen een class-token, afgekort CLS. Dit is één extra vector die later informatie uit het hele beeld kan verzamelen. Tel daarna een positietabel op met één rij voor ieder token, inclusief CLS; de tabelbreedte is opnieuw D.

cls = np.array([0,0])
positions = np.array([
    [0,0],[0,0],[1,0],[0,1],[1,1]])
tokens = add_visual_tokens(
    embedded,cls,positions)
print(tokens.shape)
# (5, 2)

CLS start hier op nul, maar is niet hetzelfde als een gemiddelde van de patches. In een getrainde ViT zijn dit leerbare parameters. Onze gekozen positiegetallen maken alleen zichtbaar welke rij wordt aangepast; ze bewijzen niet dat een model ruimtelijk begrip heeft.

print(tokens[0].tolist())
print(tokens[2].tolist())
# [0.0, 0.0]
# [9.0, 11.0]

Maak add_visual_tokens met CLS op rij nul en de opgegeven positietabel. Posities achteraf verwisselen verandert de invoer van het model. Ook hebben kleinere patches meer tokens tot gevolg: volgende keer zie je waarom dat de attentionberekening duurder maakt.

03
CHECKPOINTVoeg CLS en posities toe

Wat neem je mee?

  • De patchvolgorde en kanaalvolgorde horen bij het invoercontract.
  • Alle patches delen één projectie naar dezelfde vectorbreedte.
  • CLS en positierijen maken de tokenreeks klaar voor beeldattention.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-29.
DE VOLGENDE STAPLaat beeldtokens elkaar gebruiken
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
Knip en orden de beeldpatches

Schrijf patchify. Geef een zelfstandige floatmatrix met één uitgevlakte HWC-patch per rij, patches links→rechts en boven→beneden. Behoud fractionele pixels en kanaalvolgorde; patch_size is een positieve Python-int, geen bool, die hoogte en breedte deelt. Gebruik de gegeven arrayhelper voor niet-lege eindige reële H×W×C-invoer tot65536cellen; ongeldige invoer 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
Projecteer met gedeelde gewichten

Schrijf project_patches. Bereken patches@weight+bias als zelfstandige floatmatrix, na controle van twee matrices en één biasvector met aansluitende vormen. Gebruik de gegeven helpers voor gecontroleerde kopieën en eindige berekeningen. Beperk de uitvoervorm vóór vermenigvuldigen tot65536cellen; een ongeldige vorm of float64-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
Voeg CLS en posities toe

Schrijf add_visual_tokens. Plaats de gegeven CLS-vector vóór de patch-embeddings en tel de opgegeven positietabel op, inclusief een positierij voor CLS. Geef een zelfstandige floatmatrix terug. Controleer featurebreedtes, precies N+1positierijen en de gegeven invoer-/uitvoergrenzen; ongeldige 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.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

# Jouw functies

def patchify(image, patch_size):
    image=_mm_array(image,3,'image')
    if type(patch_size) is not int or patch_size<=0:raise ValueError('Positieve gehele patchmaat vereist')
    height,width,channels=image.shape
    if height%patch_size or width%patch_size:raise ValueError('Hoogte en breedte moeten deelbaar zijn')
    patches=[]
    for top in range(0,height,patch_size):
        for left in range(0,width,patch_size):
            patches.append(image[top:top+patch_size,left:left+patch_size,:].reshape(-1))
    return np.stack(patches)

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

def add_visual_tokens(embeddings, cls, positions):
    embeddings=_mm_array(embeddings,2,'embeddings');cls=_mm_array(cls,1,'cls');positions=_mm_array(positions,2,'positions')
    if cls.shape!=(embeddings.shape[1],) or positions.shape!=(embeddings.shape[0]+1,embeddings.shape[1]):
        raise ValueError('Ook het eerste CLS-token krijgt een eigen positierij')
    _mm_shape(embeddings.shape[0]+1,embeddings.shape[1])
    with np.errstate(over='ignore',invalid='ignore'):result=np.vstack([cls,embeddings])+positions
    return _mm_result(result)

# 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')

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