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

Breng beeld en tekst in dezelfde richting

Normaliseer featurevectoren, vergelijk modaliteiten en bereken het symmetrische contrastieve verlies.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je normaliseert beeld-/tekstfeatures en berekent paarscores en symmetrisch contrastief verlies.
  • Je onderscheidt schaal, richting, temperatuur en beide zoekrichtingen bij bekende paren.

Je hebt een foto en wilt de juiste beschrijving vinden tussen andere teksten. Een beeldvector en tekstvector kun je niet zomaar vergelijken als hun encoders verschillende ruimtes gebruiken. Contrastief leren bouwt een gedeelde vergelijkingsruimte waarin bekende paren bij elkaar moeten passen.

Denk aan twee kaartenbakken met dezelfde onderwerpen maar andere sorteercodes. De beeldencoder en tekstencoder leveren eerst hun eigen kenmerken; projecties vertalen beide naar één vectorruimte. Daarna kun je meten welke richting bij welke richting past.

CLIP gebruikt dit idee voor beelden en tekst. Wij kiezen kleine featurevectoren om de vergelijking en verliesfunctie na te rekenen; deze vectoren zijn niet door een beeld- of tekstmodel geleerd. Je implementeert drie onderdelen die bij echte training achter de encoders staan.

1. Vergelijk richting in plaats van lengte

Een vector met grote getallen hoeft niet inhoudelijk beter te passen. Deel iedere featurevector daarom door zijn lengte, de L2-norm. De resulterende eenheidsvector behoudt de richting, met lengte één.

u=f‖f‖2

De vector [3,4] heeft lengte vijf en wordt [0,6;0,8]. Bij [30,40] is de richting precies hetzelfde. Na normalisatie kunnen we het inproduct gebruiken als cosinusovereenkomst: één voor dezelfde richting, nul voor loodrechte richtingen en min één voor tegengestelde richtingen.

unit = normalize_rows([[3,4],[30,40]])
print(unit.round(3).tolist())
# [[0.6, 0.8], [0.6, 0.8]]

Maak normalize_rows voor een matrix met één vector per rij. Een nulvector heeft geen richting en geeft volgens ons lescontract ValueError. De gegeven vormhelpers bewaken eindige reële invoer; voorkom overloop door eerst iedere rij door zijn grootste absolute waarde te delen en pas daarna de lengte te berekenen.

01
CHECKPOINTVergelijk stabiele richtingen

2. Maak een tabel van mogelijke paren

Laat u de genormaliseerde beeldvector zijn en v de genormaliseerde tekstvector. De score S van beeld i en tekst j is hun inproduct gedeeld door een positieve temperatuur τ. Beide modaliteiten moeten dezelfde featurebreedte hebben, maar mogen verschillende aantallen voorbeelden bevatten.

Sij=ui·vjτ

Met twee loodrechte richtingen wordt de cosinustabel de identiteitsmatrix. Kies τ=0,5: delen verdubbelt de scores op de diagonaal naar twee. Een kleinere temperatuur maakt verschillen bij latere softmax scherper; het verandert de onderlinge rangorde niet.

images = [[3,0],[0,7]]
texts = [[2,0],[0,8]]
scores = cross_modal_logits(
    images,texts,0.5)
print(scores.tolist())
# [[2.0, 0.0], [0.0, 2.0]]

Maak cross_modal_logits met normalisatie van beide invoeren. Gebruik onze expliciete temperatuur als deler; verwar hem niet met de leerbare log-schaalparameter uit het CLIP-paper. Deze oefenfunctie accepteert temperatuur 0,0001 tot en met 100 en begrenst ook de afgeleide scorematrix.

VERKEN HET ZELFGoede richtingen moeten ook bij het juiste paar horen

Beeld A en tekst A horen bij elkaar, net als B en B. Verwissel de tekstvectoren zonder de doelkoppeling op de diagonaal aan te passen.

Scores: beeldrijen × tekstkolommen
BeeldTekst ATekst B
A20
B02
Symmetrisch verlies
0,127
Juiste indexparen
100%

De gemarkeerde diagonaal blijft het trainingsdoel. Een kleiner verlies betekent hier dat meer kans bij die bekende koppelingen terechtkomt.

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

02
CHECKPOINTVergelijk alle beeld-tekstparen

3. Leer in beide zoekrichtingen

Stel dat iedere beeldrij bij de tekst met hetzelfde indexnummer hoort. In een batch van B paren zijn de diagonale combinaties positief; de andere combinaties tellen hier als negatief. Softmax over iedere rij verdeelt kansen over teksten, terwijl softmax over iedere kolom kansen over beelden verdeelt.

Neem het gemiddelde van de cross-entropy in beide richtingen. Voor iedere positieve combinatie neem je min de logaritme van zijn kans, en daarna het gemiddelde over alle paren en beide richtingen. Een verlies van nul zou alle kans op de bekende juiste koppelingen leggen.

loss = symmetric_contrastive_loss(
    scores)
print(round(loss,3))
# 0.127

In onze tabel is de juiste kans ongeveer 0,881 en is het verlies ongeveer 0,127. Verwissel je de teksten zonder de indexkoppeling aan te passen, dan staat de hoogste score bij het verkeerde paar. Het verlies stijgt daardoor, ook al blijven alle vectorlengtes gelijk.

swapped = cross_modal_logits(
    images,texts[::-1],0.5)
bad_loss = symmetric_contrastive_loss(
    swapped)
print(round(bad_loss,3))
# 2.127

Maak symmetric_contrastive_loss met numeriek stabiele log-som-exponentberekeningen, bijvoorbeeld np.logaddexp.reduce. De invoer is vierkant omdat dit lescontract een bekende één-op-één-batchkoppeling gebruikt. In echte datasets kunnen meerdere beschrijvingen bij hetzelfde beeld passen; zulke combinaties als negatief behandelen is een beperking van dit eenvoudige verlies.

03
CHECKPOINTBeoordeel beide zoekrichtingen

Wat neem je mee?

  • Normalisatie verwijdert lengteverschillen vóór de paarvergelijking.
  • Temperatuur schaalt overeenkomsten en beïnvloedt de scherpte van softmax.
  • Het symmetrische verlies beoordeelt zowel beeld-naar-tekst als tekst-naar-beeld.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-29.
DE VOLGENDE STAPKies labels zonder een nieuwe head te trainen
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
Vergelijk stabiele richtingen

Schrijf normalize_rows. Normaliseer elke rij van een niet-lege eindige reële featurematrix naar L2-lengte1, met behoud van richting en zonder invoermutatie. Geef een zelfstandige floatmatrix; gebruik schaling zodat ook heel grote en subnormale waarden goed werken. Nulrijen, ongeldige vormen/types en overschrijding van de gegeven65536-invoercelgrens geven 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
Vergelijk alle beeld-tekstparen

Schrijf cross_modal_logits. Normaliseer beeld- en tekstfeatures via normalize_rows, controleer gelijke embeddingbreedte en geef image@text.T/temperature als zelfstandige floatmatrix. Temperatuur is een Python-int/float0.0001–100, geen bool. Gebruik de gegeven helpers; ook de uitvoermatrix mag maximaal65536cellen bevatten en wordt vóór matmul begrensd. 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.

03
Beoordeel beide zoekrichtingen

Schrijf symmetric_contrastive_loss. Geef een eindige Python-float voor het gemiddelde van de row- en column-cross-entropy met de diagonaal als positieve koppeling. Gebruik stabiele log-som-exponentberekeningen zonder kansclipping; ook kleine positieve verliezen blijven behouden. Logits zijn een niet-lege vierkante matrix met maximaal64rijen en absolute waarden tot1e6. 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.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

# Jouw functies

def normalize_rows(features):
    features=_mm_array(features,2,'features');scale=np.max(np.abs(features),axis=1,keepdims=True)
    if np.any(scale==0):raise ValueError('Een nulvector heeft geen cosinusrichting')
    scaled=features/scale
    return scaled/np.sqrt(np.sum(scaled*scaled,axis=1,keepdims=True))

def cross_modal_logits(image_features, text_features, temperature):
    image=normalize_rows(image_features);text=normalize_rows(text_features);temperature=_mm_temperature(temperature)
    if image.shape[1]!=text.shape[1]:raise ValueError('Beide modaliteiten vereisen dezelfde embeddingdimensie')
    _mm_shape(image.shape[0],text.shape[0])
    return (image@text.T)/temperature

def symmetric_contrastive_loss(logits):
    logits=_mm_array(logits,2,'logits')
    if logits.shape[0]!=logits.shape[1] or logits.shape[0]>64 or np.abs(logits).max()>1000000:
        raise ValueError('Een vierkante batch tot64met begrensde logits vereist')
    losses=[]
    for scores in [logits,logits.T]:
        relative=scores-np.diag(scores)[:,None]
        losses.append(np.mean(np.logaddexp.reduce(relative,axis=1)))
    return float(np.mean(losses))

# 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_shape(*shape):
    cells=1
    for dimension in shape:cells*=dimension
    if cells>65536:raise ValueError('Ook een afgeleide array mag maximaal65536cellen bevatten')

def _mm_temperature(value):
    if type(value) not in (int,float) or not 0.0001<=value<=100 or not np.isfinite(value):
        raise ValueError('Temperatuur tussen0.0001en100 vereist')
    return float(value)

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