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

Kies labels zonder een nieuwe head te trainen

Maak promptensembles en onderscheid aangeboden labels, softmaxscherpte en twee retrievalrichtingen.

65 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je bouwt een promptensemble en maakt voorspellingen binnen aangeboden labels.
  • Je onderscheidt kansscherpte van juistheid en meet retrieval in twee richtingen.

Je wilt een foto indelen als fiets, hond of boom zonder een nieuwe classificatiehead voor precies die drie labels te trainen. Met een gedeelde beeld-/tekstruimte kun je de beeldvector vergelijken met teksten die de klassen beschrijven. Dit heet zero-shotclassificatie: de taaklabels worden bij gebruik aangeboden.

Denk aan een meetlat met verschillende gelabelde richtingen. De beeldvector kiest de richting die het meest overeenkomt met zijn eigen richting. De kwaliteit hangt af van de eerder geleerde ruimte én van de beschrijvingen waarmee je de labels vertegenwoordigt.

Wij gebruiken opnieuw zelfgekozen vectoren. De rekensommen laten het mechanisme zien, maar bewijzen geen beeldherkenning of generalisatie. Bij een echt model zijn voorafgaande training, de gebruikte data en de mogelijke vertekeningen onderdeel van de beoordeling.

1. Geef een klasse meer dan één beschrijving

Een label zoals bank kan een meubel of financiële instelling bedoelen. Ook zonder dubbelzinnigheid kan de formulering een tekstencoder beïnvloeden. Een promptensemble maakt per klasse één vertegenwoordiger uit meerdere beschrijvingen, bijvoorbeeld foto van een fiets en een fiets in beeld.

Normaliseer eerst iedere tekstvector afzonderlijk. Middel daarna de vectoren van dezelfde klasse en normaliseer het gemiddelde opnieuw. Anders kan een toevallig lange tekstvector bijna alleen de richting van de klasse bepalen.

prompts = [
    [[100,0],[0,1]],
    [[0,4],[0,8]]]
classes = prompt_ensemble(prompts)
print(classes.round(3).tolist())
# [[0.707, 0.707], [0.0, 1.0]]

De eerste klasse heeft twee loodrechte richtingen. Na normalisatie zijn dat [1,0] en [0,1], met gemiddelde [0,5;0,5]; opnieuw normaliseren geeft ongeveer [0,707;0,707]. De waarde 100 krijgt zo geen extra stemgewicht ten opzichte van de waarde 1.

Maak prompt_ensemble voor de vorm klassen×prompts×kenmerken. Alle klassen hebben in dit lescontract evenveel prompts. Een nulvector of een precies wegvallend gemiddelde geeft ValueError: tegengestelde beschrijvingen leveren hier geen bruikbare klassenrichting.

01
CHECKPOINTMaak één richting per klasse

2. Kies uit de aangeboden labels

Vergelijk iedere beeldvector met alle klassenrichtingen via dezelfde genormaliseerde inproducten en temperatuur als in de vorige les. Normaliseer vervolgens iedere score-rij met softmax. Elke rij hoort bij één beeld en moet over zijn aangeboden klassen optellen tot één.

images = [[1,0],[0,1]]
labels = [[1,0],[0,1],[-1,0]]
answer = zero_shot_predict(
    images,labels,1.0)
print(answer['prediction'].tolist())
# [0, 1]

Maak zero_shot_predict met probabilities en prediction als uitvoervelden. De kansmatrix heeft één rij per beeld en één kolom per label; prediction bevat per beeld de index van de hoogste score. Bij een exacte gelijke hoogste score kiezen we de eerste index, zoals np.argmax.

print(answer['probabilities']
      .round(3).tolist())
# [[0.665, 0.245, 0.09],
#  [0.212, 0.576, 0.212]]

Voor het eerste beeld zijn de ongeschaalde scores 1, 0 en min 1. Exponentiëren geeft ongeveer 2,718;1;0,368, en delen door hun som geeft de eerste kansrij. Een softmax langs de beeldas zou een andere vraag beantwoorden en is voor deze classificatie verkeerd.

VERKEN HET ZELFEen winnaar blijft bestaan als het juiste label ontbreekt

Het beeld heeft richting A: [1, 0]. Verwijder die richting uit de aangeboden labels en maak de verdeling scherper met een lagere temperatuur.

  1. richting A66,524%
  2. richting B24,473%
  3. tegenover A9,003%
Gekozen label
richting A
Hoogste softmaxwaarde
66,524%

Richting A is beschikbaar en wint binnen deze aangeboden labels.

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

02
CHECKPOINTKies uit aangeboden labels

3. Een scherpe kans is geen zekerheid over de wereld

Softmax verdeelt altijd alle kans over de aangeboden labels, ook als geen label goed past. Een foto van een boot krijgt nog steeds een winnaar tussen fiets, hond en boom. Je hebt aparte evaluatie en eventueel een afwijsregel nodig; een hoge score bewijst op zichzelf geen juiste of complete categorie.

Temperatuur laat dit scherp zien. Gebruik dezelfde beelden en labels met een kleinere temperatuur: de index van de winnaar blijft gelijk, terwijl de hoogste softmaxwaarde veel groter kan worden. Het model heeft daarbij geen nieuwe informatie over het beeld gekregen.

sharp = zero_shot_predict(
    images,labels,0.1)
print(sharp['prediction'].tolist())
print(sharp['probabilities'].max()>0.99)
# [0, 1]
# True

Voor een batch met bekende beeld-tekstparen kun je ook retrieval meten: welk percentage beelden kiest de tekst op dezelfde index, en omgekeerd? Beide richtingen gebruiken dezelfde scoretabel, maar kunnen verschillende percentages halen. Hun noemers zijn de aantallen zoekopdrachten, niet het aantal scorecellen.

quality = paired_retrieval_accuracy(
    [[3,2],[4,1]])
print(quality)
# {'image_to_text': 0.5,
#  'text_to_image': 0.0}

Hier kiezen beide beelden tekst nul, dus één van twee beeldzoekopdrachten is juist. Tekst nul kiest beeld één en tekst één kiest beeld nul: nul van twee tekstzoekopdrachten is juist. Maak paired_retrieval_accuracy voor deze vierkante tabel met de bekende koppelingen op de diagonaal.

03
CHECKPOINTMeet retrieval in twee richtingen

Wat neem je mee?

  • Een promptensemble normaliseert vóór en na het middelen.
  • Zero-shot kiest binnen de aangeboden labels, niet binnen alle mogelijke objecten.
  • Meet retrieval in beide richtingen en beoordeel kansscherpte apart van juistheid.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-29.
DE VOLGENDE STAPVerzwak signaal en voeg gecontroleerde ruis toe
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
Maak één richting per klasse

Schrijf prompt_ensemble. Normaliseer iedere promptvector, middel daarna per klasse en normaliseer opnieuw. Invoer is een niet-lege eindige reële array klassen×prompts×kenmerken tot65536cellen; uitvoer is een zelfstandige floatmatrix. Gebruik de gegeven normalize_rows-helper. Nulvectoren, een precies nulgemiddelde en ongeldige invoer 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
Kies uit aangeboden labels

Schrijf zero_shot_predict. Gebruik de gegeven cross_modal_logits en normaliseer met softmax langs de klasse-as. Geef probabilities als zelfstandige floatmatrix en prediction als zelfstandige integerarray met één argmax per beeld; kies bij gelijke hoogste scores de eerste index. Aantallen beelden en labels mogen verschillen. De gegeven input-, temperatuur- en uitvoergrenzen blijven gelden; 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
Meet retrieval in twee richtingen

Schrijf paired_retrieval_accuracy. Geef image_to_text als het gemiddelde van correcte rij-argmaxen en text_to_image als het gemiddelde van correcte kolom-argmaxen, met de bekende koppelingen op de diagonaal. Geef twee Python-floats; gelijke hoogste scores kiezen de eerste index. Een niet-lege vierkante eindige reële matrix tot65536cellen vereist, anders 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 prompt_ensemble(prompt_features):
    features=_mm_array(prompt_features,3,'prompt_features')
    classes,prompts,dimension=features.shape
    normalized=normalize_rows(features.reshape(classes*prompts,dimension)).reshape(features.shape)
    return normalize_rows(normalized.mean(axis=1))

def zero_shot_predict(image_features, class_features, temperature):
    logits=cross_modal_logits(image_features,class_features,temperature)
    probabilities=_mm_softmax(logits)
    return {'probabilities':probabilities,'prediction':np.argmax(logits,axis=1)}

def paired_retrieval_accuracy(similarity):
    similarity=_mm_array(similarity,2,'similarity')
    if similarity.shape[0]!=similarity.shape[1]:raise ValueError('De bekende batchkoppeling ligt op de diagonaal')
    return {'image_to_text':float(np.mean(np.argmax(similarity,axis=1)==np.arange(len(similarity)))),
            'text_to_image':float(np.mean(np.argmax(similarity,axis=0)==np.arange(len(similarity))))}

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

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

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

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