Werkplaats / De werkplaats
In jouw eigen tempo
← AI in de wereld
MODULE 01 · LES 02

Scheid modelkwaliteit van verschoven verdelingen

Bereken gelabelde kwaliteitsvensters en histogramverschillen en kies afzonderlijke signalen voor onderzoek.

90 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je berekent accuracy en Brier per rijvenster, inclusief een onvolledige staart.
  • Je onderscheidt Jensen–Shannon-divergentie, wortelafstand en gekozen alarmgrenzen van bewijs over modelkwaliteit.

Een AI-dienst kan snel blijven reageren terwijl de voorspellingen slechter worden. Andersom kan de invoer sterk veranderen terwijl het model nog steeds goed presteert. Monitoring moet daarom verschillende signalen uit elkaar houden.

Denk aan twee meetpanelen. Het ene vergelijkt voorspellingen met echte labels; het andere vergelijkt de verdeling van nieuwe waarden met een referentie. Een waarschuwing op één paneel vertelt nog niet automatisch wat de oorzaak is.

Je bouwt beide panelen en vervolgens een expliciete signaalregel. We kiezen kleine oefengrenzen om het gedrag te onderzoeken. De voorbeelden leveren geen universele alarmdrempel en bewijzen geen statistische significantie.

1. Kwaliteit vraagt echte uitkomsten

Voor accuracy heb je een juist label nodig. De Brier-score gebruikt bovendien de voorspelde kans s_i en het binaire label y_i: een zelfverzekerde verkeerde voorspelling krijgt een grote kwadratische fout. N is het aantal gelabelde rijen in het venster.

B=1N∑i=1N(si−yi)2

Neem labels [0, 1] en kansen [0,1; 0,8]. Beide beslissingen zijn goed bij drempel 0,5; de kwadratische fouten zijn 0,01 en 0,04, gemiddeld 0,025. Een tweede venster mag dus dezelfde accuracy hebben en toch anders gekalibreerde kansen tonen.

quality = window_quality(
    [0,1,1,0,1],
    [.1,.8,.4,.2,.6],window=2)
print([row['accuracy']
       for row in quality['rows']])
print([round(row['brier'],3)
       for row in quality['rows']])
# [1.0, 0.5, 1.0]
# [0.025, 0.2, 0.16]

De vijfde rij blijft een geldig laatste venster. Start en stop verwijzen naar rijposities, met stop exclusief; dit zijn geen kloktijden. Bewaar die grenzen zodat je kunt terugzoeken welke voorbeelden een score bepaalden.

Labels kunnen later binnenkomen of slechts voor een selectie beschikbaar zijn. Vermeld daarom welke gelabelde rijen je meet; verzin geen kwaliteit voor ontbrekende labels. De functie werkt hier met volledig aangeleverde labels.

01
CHECKPOINTMeet vensters met echte labels

2. Een verdeling kan veranderen zonder slechter model

Onderzoek eerst afzonderlijk de scoreverdelingen. Dezelfde twee histogramvakken worden op referentie en nieuwe data toegepast. Hun genormaliseerde vakmassa's noemen we p en q; de middenverdeling heet m.

VERKEN HET ZELFEen andere verdeling en slechtere kwaliteit zijn aparte signalen

Verander de scores of juist de labels. De histogrammen kennen de labels niet; accuracy en Brier hebben ze wel nodig. Dezelfde vakverdeling kan verschillende ruwe scores verbergen.

00,51V1Vak 1: referentie 50%, huidig 50%V2Vak 2: referentie 0%, huidig 0%V3Vak 3: referentie 0%, huidig 0%V4Vak 4: referentie 50%, huidig 50%KansReferentie links · huidig rechts
Interne grenzen horen rechts; 1 hoort bij het laatste vak. Histogramkansen worden per dataset genormaliseerd.
ScorevakReferentieHuidig
V1: [0; 0,25)50%50%
V2: [0,25; 0,5)0%0%
V3: [0,5; 0,75)0%0%
V4: [0,75; 1]50%50%
Accuracy referentie
100%
Accuracy huidig
0%
Brier huidig
0,725
JS-divergentie (ln)
0
Wortelafstand
0
Gelabelde rijvensters, stop exclusief; dit zijn geen tijdvensters.
Rijen [start; stop)AccuracyBrier
[0; 2)0%0,725
[2; 4)0%0,725

Gekozen grenzen overschreden: accuracy ja, Brier ja, verdeling nee. Hier verslechtert de kwaliteit terwijl het histogram gelijk blijft.

Vier vaste onderwijsvoorbeelden, rijvensters en gekozen grenzen: accuracy minimaal 0,8; Brier maximaal 0,25; divergentie maximaal 0,1. Dit zijn onderzoekssignalen, geen universele alarmen, significantietoets of automatische hertraining.

Jensen–Shannon-divergentie J vergelijkt beide verdelingen met dat midden. D staat voor KL-divergentie; we gebruiken natuurlijke logaritmen. Een term met nul massa draagt nul bij, zodat ook lege vakken geldig blijven.

m=p+q2J=D(p∥m)+D(q∥m)2

Bij referentie [0, 0] en nieuwe scores [1, 1] liggen alle massa's in tegengestelde vakken. Het midden heeft in ieder vak massa 0,5. Beide KL-termen zijn ln(2), dus J is ongeveer 0,693.

| Scorevak | p | q | |---|---:|---:| | [0; 0,5) | 1 | 0 | | [0,5; 1] | 0 | 1 |

shift = histogram_shift(
    [0,0],[1,1],[0,.5,1])
print(round(shift['divergence'],3))
print(round(shift['distance'],3))
# 0.693
# 0.833

De wortelafstand is een andere grootheid: onze tweede uitvoer is de wortel van J, zoals de afstand in de SciPy-bron. Geef de divergentie aan de latere signaalfunctie door. Met natuurlijke log blijft J tussen nul en ln(2).

Histogrammen vergeten ook details binnen ieder vak. Twee verschillende verzamelingen kunnen dezelfde vakmassa's hebben. Interne grenzen horen bij het rechtervak, terwijl de uiterste bovengrens in het laatste vak blijft.

same_bins = histogram_shift(
    [.1,.2,.8,.9],
    [.15,.25,.75,.95],[0,.5,1])
print(same_bins['divergence'])
# 0.0
02
CHECKPOINTVergelijk histogrammen zonder kwaliteit te verzinnen

3. Houd signalen gescheiden

Bij dezelfde scores en omgekeerde labels kan de kwaliteit volledig instorten terwijl het scorehistogram gelijk blijft. Omgekeerd kunnen nieuwe perfecte voorspellingen een ander histogram hebben doordat de verhouding tussen klassen verandert. Bekijk dus zowel gelabelde fouten als de verschuiving.

scores = [0,0,1,1]
old = window_quality(
    [0,0,1,1],scores,window=4)
new = window_quality(
    [1,1,0,0],scores,window=4)
print(old['rows'][0]['accuracy'])
print(new['rows'][0]['accuracy'])
print(histogram_shift(
    scores,scores,[0,.5,1])
    ['divergence'])
# 1.0
# 0.0
# 0.0

Maak aparte booleans voor te lage accuracy, te hoge Brier en te hoge divergentie. Needs_review is hun gezamenlijke of: er is iets om te onderzoeken. Gelijkheid aan een gekozen grens telt als toegestaan.

on_edge = monitor_flags(.8,.25,.1)
bad = monitor_flags(0,1,0)
print(on_edge['needs_review'])
print(bad['accuracy'])
print(bad['distribution'])
# False
# True
# False

Een signaal is geen opdracht om automatisch een nieuw model te publiceren. Onderzoek meetfouten, selectie, veranderde omstandigheden en de concrete mislukkingen. Pas daarna kun je een onderbouwde vervolgstap testen.

03
CHECKPOINTHoud kwaliteits- en verdelingssignalen apart

Wat neem je mee?

  • Accuracy en Brier meten kwaliteit met echte labels.
  • Histogramverschil meet een gekozen, grof gemaakte verdeling.
  • Bewaar beide signalen en hun meetcontext bij iedere conclusie.

Hierna bepaal je welke bestanden en meetprofielen bij een lokaal apparaatbudget passen.

DE VOLGENDE STAPVergelijk payload, bestandsidentiteit en apparaatprofielen
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
Meet vensters met echte labels

Schrijf window_quality(labels,scores,window=8,threshold=.5). Gebruik de gegeven vectorvalidator: beide 1D, lengte 1–4096, even lang; labels integer-dtype 0/1, scores reële eindige kansen [0,1]. Window is Python-int 1–256. Threshold is een niet-booleaanse reële scalar [0,1] volgens de gegeven validator. Neem opeenvolgende rijvensters en behoud een onvolledige staart. Prediction is score>=threshold. Geef count,window,threshold,rows; elke rij heeft start (inclusief), stop (exclusief), count, accuracy, brier, positives, predicted_positives, mean_score. Brier=gemiddelde (score-label)**2; accuracy=fractie correcte voorspellingen. Start, stop, count, positives en predicted_positives zijn integerwaarden; accuracy, brier en mean_score floats. Vensters zijn rijen in de aangeleverde volgorde, geen kloktijd. Behoud invoer/RNG; ongeldige invoer geeft ValueError. Gegeven scalarvalidators controleren eerst het type, converteren naar Python-float en controleren daarna eindigheid en bereik van die geconverteerde waarde.

Een hint

Werk het handvoorbeeld na; houd eenheden, grenzen en behoud van de invoer expliciet.

02
Vergelijk histogrammen zonder kwaliteit te verzinnen

Schrijf histogram_shift(reference,current,edges). De gegeven validator verlangt reële eindige 1D-vectoren, amplitudes binnen [-1e6,1e6]; reference/current elk 1–4096 waarden, edges 3–65 strikt oplopende grenzen. Alle waarden moeten binnen [edges[0],edges[-1]] liggen. Interne grenzen horen bij het rechtervak; de laatste bovengrens hoort bij het laatste vak. Geef edges als zelfstandige floatkopie, reference_counts/current_counts als integerarrays van K=len(edges)-1, reference_probabilities/current_probabilities als zelfstandige floatarrays, divergence en distance als floats. Normaliseer ieder histogram met zijn eigen aantal. Met m=(p+q)/2 is divergence=(KL(p||m)+KL(q||m))/2, natuurlijke log; een term met massa nul draagt nul bij. Handhaaf numeriek 0<=divergence<=ln(2); distance=sqrt(divergence). Behoud invoer/RNG; ongeldige invoer geeft ValueError. Dit is histogramverschil, geen bewijs van slechtere kwaliteit of statistische significantie.

Een hint

Werk het handvoorbeeld na; houd eenheden, grenzen en behoud van de invoer expliciet.

03
Houd kwaliteits- en verdelingssignalen apart

Schrijf monitor_flags(accuracy,brier,divergence,min_accuracy=.8,max_brier=.25,max_divergence=.1). Valideer niet-booleaanse reële scalars met de gegeven validator: accuracy/brier/min_accuracy/max_brier binnen [0,1], divergence/max_divergence binnen [0,ln(2)]. Geef booleans in een dict: accuracy=(accuracy<min_accuracy), brier=(brier>max_brier), distribution=(divergence>max_divergence), needs_review=een van die drie waar. Gelijkheid aan de gekozen grens is geen overschrijding. Behoud invoer/RNG; ongeldige invoer geeft ValueError. Dit zijn expliciete oefengrenzen en signalen voor onderzoek, geen universele alarmdrempels of automatische hertraining. Gegeven scalarvalidators controleren eerst het type, converteren naar Python-float en controleren daarna eindigheid en bereik van die geconverteerde waarde.

Een hint

Werk het handvoorbeeld na; houd eenheden, grenzen en behoud van de invoer expliciet.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

import math

# Jouw functies

def window_quality(labels,scores,window=8,threshold=.5):
    labels=_mon_vector(labels,True);scores=_mon_vector(scores)
    if labels.shape!=scores.shape or np.any(labels<0) or np.any(labels>1) or np.any(scores<0) or np.any(scores>1):raise ValueError('Evenveel binaire labels en kansen vereist')
    if type(window) is not int or not 1<=window<=256:raise ValueError('Vensterlengte buiten oefenbereik')
    threshold=_mon_number(threshold,0,1);rows=[]
    for start in range(0,len(labels),window):
        stop=min(start+window,len(labels));target=labels[start:stop];probability=scores[start:stop];prediction=probability>=threshold
        rows.append({'start':start,'stop':stop,'count':stop-start,'accuracy':float(np.mean(prediction==target)),
            'brier':float(np.mean((probability-target)**2)),'positives':int(target.sum()),
            'predicted_positives':int(prediction.sum()),'mean_score':float(probability.mean())})
    return {'count':len(labels),'window':window,'threshold':threshold,'rows':rows}

def histogram_shift(reference,current,edges):
    reference=_mon_vector(reference);current=_mon_vector(current);edges=_mon_vector(edges,maximum=65)
    if len(edges)<3 or np.any(np.diff(edges)<=0):raise ValueError('Twee tot 64 oplopende histogramvakken vereist')
    if min(reference.min(),current.min())<edges[0] or max(reference.max(),current.max())>edges[-1]:raise ValueError('Alle waarden moeten binnen de histogramgrenzen liggen')
    counts=[];bins=len(edges)-1
    for sample in [reference,current]:
        indices=np.minimum(np.searchsorted(edges,sample,side='right')-1,bins-1)
        counts.append(np.bincount(indices,minlength=bins))
    p=counts[0]/len(reference);q=counts[1]/len(current);m=(p+q)/2;total=0.
    for distribution in [p,q]:
        mask=distribution>0
        total+=float(np.sum(distribution[mask]*(np.log(distribution[mask])-np.log(m[mask]))))/2
    divergence=min(math.log(2),max(0.,total))
    return {'edges':edges.copy(),'reference_counts':counts[0],'current_counts':counts[1],
        'reference_probabilities':p,'current_probabilities':q,'divergence':divergence,'distance':math.sqrt(divergence)}

def monitor_flags(accuracy,brier,divergence,min_accuracy=.8,max_brier=.25,max_divergence=.1):
    accuracy=_mon_number(accuracy,0,1);brier=_mon_number(brier,0,1);divergence=_mon_number(divergence,0,math.log(2))
    min_accuracy=_mon_number(min_accuracy,0,1);max_brier=_mon_number(max_brier,0,1);max_divergence=_mon_number(max_divergence,0,math.log(2))
    flags={'accuracy':accuracy<min_accuracy,'brier':brier>max_brier,'distribution':divergence>max_divergence}
    return dict(flags,needs_review=any(flags.values()))

# Gegeven helpers

def _mon_vector(value,integer=False,maximum=4096):
    raw=np.asarray(value)
    if raw.ndim!=1 or not 1<=len(raw)<=maximum or raw.dtype.kind not in ('iu' if integer else 'iuf'):raise ValueError('Begrensde reële vector vereist')
    if not np.isfinite(raw).all() or np.any(np.abs(raw.astype(float))>1e6):raise ValueError('Eindige oefenwaarden vereist')
    return raw.astype(int if integer else float,copy=True)

def _mon_number(value,low,high):
    if isinstance(value,(bool,np.bool_)) or not isinstance(value,(int,float,np.integer,np.floating)):raise ValueError('Begrensd scalair getal vereist')
    try:number=float(value)
    except (OverflowError,ValueError,TypeError):raise ValueError('Begrensd scalair getal vereist') from None
    if not math.isfinite(number) or not low<=number<=high:raise ValueError('Begrensd scalair getal vereist')
    return number

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