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

Meet permutatiebelang voor één vast model

Vergelijk baselineverlies en verlies na één kolomverstoring, behoud negatieve effecten en beperk conclusies bij samenhangende features.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je herschikt één feature met expliciete indexvolgorden terwijl model en labels vastblijven.
  • Je berekent verliesverschillen en beschrijvende spreiding en onderscheidt modelafhankelijk belang van causaliteit.

Een bezorgtijdmodel gebruikt afstand en ordergrootte. Welk kenmerk gebruikt het model werkelijk bij zijn voorspellingen? Een groot gewicht of een overtuigende uitleg vertelt nog niet wat er met zijn kwaliteit gebeurt wanneer informatie uit een kolom wordt verstoord.

Denk aan een spreadsheet waarin je één kolom door elkaar schudt. De labels blijven bij hun oorspronkelijke rijen; de andere kolommen ook. Als het vaste model daarna slechter voorspelt, gebruikte het kennelijk informatie uit die kolom in deze gegevens.

Je werkt met volledig verzonnen, genormaliseerde oefenscores. Negatieve waarden betekenen hier onder het gekozen midden, geen negatieve kilometers. Alle modelparameters blijven tijdens de vergelijking vast.

1. Meet eerst de oorspronkelijke fout

Permutatiebelang (permutation importance) is afhankelijk van een gekozen kwaliteitsmaat. Hier gebruik je gemiddelde kwadratische fout (mean squared error, MSE): groter is slechter. Controleer de baseline voordat je verschillen gaat interpreteren; een slecht model kan bruikbare kenmerken ongebruikt laten.

Noem de labels y_i, de voorspellingen ŷ_i en het aantal rijen n. Per rij kwadrateer je het verschil en daarna middel je. De eenheid is de gekwadrateerde oefenscore, geen percentage.

L=1n∑i=1n(y^i−yi)2

Bij labels nul en twee en voorspellingen één en nul zijn de verschillen één en min twee. Hun kwadraten zijn één en vier, dus het gemiddelde is 2,5. Dat is de berekening die je code moet bewaren.

print(squared_loss([0.,2.],[1.,0.]))
# 2.5
01
CHECKPOINTMeet gemiddelde kwadratische fout

2. Verander precies één informatiebron

Onze drie rijen liggen op een diagonaal: beide kenmerken hebben telkens dezelfde waarde. Een indexvolgorde 2, 1, 0 verplaatst de laatste waarde naar de eerste rij. Alleen de gekozen kolom verandert; je hersorteert dus niet de volledige tabel of de labels mee.

data = [
 [-1.,-1.],
 [0.,0.],
 [1.,1.]]
order = [2,1,0]
changed = permute_feature(data,0,order)
for row in changed:
    print(row.tolist())
# [1.0, -1.0]
# [0.0, 0.0]
# [-1.0, 1.0]

De nieuwe combinaties liggen niet allemaal op de oorspronkelijke diagonaal. Dat is een belangrijke beperking bij samenhangende kenmerken: je kunt het model ongewone combinaties aanbieden. De uitkomst zegt iets over deze ingreep in dit model, niet automatisch over een oorzaak in de echte wereld.

02
CHECKPOINTHerschik één kolom en behoud de rijcontext

3. Vergelijk verlies met dezelfde vaste referentie

Het gegeven tweefeaturemodel combineert een bias, twee gewichten en een mogelijke interactie. Kies hier gewicht één voor de eerste kolom en nul voor de tweede, zonder bias of interactie. Met labels min één, nul en één voorspelt dit model de oorspronkelijke drie rijen precies.

VERKEN HET ZELFWelke informatie gebruikt dit vaste model?

Herschik één kolom, behoud de labels en vergelijk met hetzelfde baselineverlies. Kies ook een model waarbij verstoring juist helpt.

Vast model: gewichten 1 / 0, bias 0, interactie 0.

De tweede kolom voorspelt dezelfde labels, maar dit vaste model gebruikt haar niet.

Baselineverlies
0
Gemiddeld verschil
1,333
Populatiespreiding
1,333
01,3332,667Basis0Baseline: MSE 0P12,667Permutatie 1: MSE 2,667P20Permutatie 2: MSE 0Gemiddelde kwadratische fout

Verlies na verstoring min baseline: 1,333 gemiddeld. In deze vaste vergelijking stijgt het gemiddelde verlies.

Nieuwe kolom volgt rij-indices 2, 1, 0 (vanaf nul). Labels blijven bij hun rij. Verschil voor deze volgorde: 2,667.

Voor → na herschikking van alleen feature 0
Rijx₀x₁LabelVoorsp.
0-1↓1-1↓-1-1-1↓1
10↓00↓000↓0
21↓-11↓111↓-1

Dit zijn expliciet opgegeven volgorden, geen nieuwe willekeurige trekkingen. De populatiespreiding beschrijft deze herhalingen en is geen betrouwbaarheidsinterval. Correlatie kan ongewone combinaties maken; een afgeronde nul bewijst geen exact nul-effect of waardeloos kenmerk.

Alle modellen en rijen zijn verzonnen en blijven vast. Deze uitleg beschrijft modelgedrag bij de gekozen gegevens en ingreep; zij bewijst geen causaliteit, dataherkomst of geldigheid buiten deze oefening.

Noem het baselineverlies L_0 en het verlies na permutatie r L_r. Het verschil E_r is L_r min L_0; het gemiddelde over R opgegeven volgorden is het getoonde belang. Bij een hogere-is-beter score zou je juist een scoreafname meten, dus houd de tekenconventie expliciet.

Er=Lr−L0E¯=1R∑r=1REr

Omkeren geeft voorspellingen één, nul en min één: de kwadratische fouten zijn vier, nul en vier. Het verlies wordt 8/3; de onveranderde volgorde geeft nul. Over die twee vaste volgorden zijn gemiddelde verschil en populatiestandaardafwijking beide 4/3.

model = {'weights':[1.,0.],
         'bias':0.,'interaction':0.}
labels = [-1.,0.,1.]
orders = [order,[0,1,2]]
effect = permutation_effect(
    model,data,labels,0,orders)
print(effect['baseline_loss'])
print(round(effect['mean_change'],3))
print(round(effect['spread'],3))
# 0.0
# 1.333
# 1.333

De spreiding is de wortel van de gemiddelde gekwadrateerde afwijking van dat gemiddelde. Zij beschrijft deze opgegeven volgorden; twee gekozen voorbeelden zijn geen betrouwbaarheidsinterval of bewijs van onafhankelijke randomtrekkingen. Herhaalde volgorden mogen in de invoer voorkomen.

s=1R∑r=1R(Er−E¯)2
03
CHECKPOINTVergelijk een vast model met zijn eigen baseline

Wat vertelt een nul of een negatief verschil?

Draai het eerste gewicht om. Het model voorspelt dan oorspronkelijk precies de verkeerde richting; de omgekeerde kolom helpt het toevallig. Het verliesverschil is negatief en moet zichtbaar blijven.

bad = dict(model,weights=[-1.,0.])
negative = permutation_effect(
    bad,data,labels,0,[order])
unused = permutation_effect(
    model,data,labels,1,[order])
print(round(negative['mean_change'],3))
print(unused['mean_change'])
# -2.667
# 0.0

De tweede kolom is volledig voorspellend, maar dit vaste model gebruikt haar niet. Nul belang maakt dat kenmerk dus niet intrinsiek waardeloos. Bij vrijwel nul verschillen kan bovendien gewone float64-afronding een kleine verandering tot nul afronden.

Wat je nu kunt

  • Je kiest een verliesmaat en controleert de baseline op vastgehouden gegevens.
  • Je verstoort één kolom terwijl labels en model gelijk blijven.
  • Je bewaart negatieve effecten en begrenst conclusies bij correlatie en afronding.

Hierna bekijk je responslijnen: wat doet hetzelfde model als je één feature langs een grid schuift?

DE VOLGENDE STAPBekijk individuele responslijnen vóór het gemiddelde
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 gemiddelde kwadratische fout

Schrijf squared_loss(actual,predicted). Gebruik de gegeven _imp_vector. Beide invoeren zijn even lange 1D-vectoren met 1–128 waarden, naar float geconverteerd binnen -1e4–1e4. Numerieke arrays worden met np.asarray beoordeeld: dtype i/u/f, geen pure bool/object/complex. Gemengde bool/int kan naar integer converteren. Controleer ook de naar float geconverteerde waarden op eindigheid en bounds. Geef een Python-float: het gemiddelde van de gekwadrateerde verschillen. Deel door N, niet door N-1; behoud de labelschaal. Behoud alle invoerwaarden, vormen, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays en containers. Ongeldige invoer geeft ValueError.

Een hint

Kwadrateer eerst per rij en neem dan het gemiddelde.

02
Herschik één kolom en behoud de rijcontext

Schrijf permute_feature(data,feature,order). Gebruik de gegeven _imp_matrix, _imp_feature en _imp_orders. Data heeft na conversie vorm N×2 en alle featurewaarden binnen -10–10. N ligt binnen 2–128. Numerieke arrays worden met np.asarray beoordeeld: dtype i/u/f, geen pure bool/object/complex. Gemengde bool/int kan naar integer converteren. Controleer ook de naar float geconverteerde waarden op eindigheid en bounds. Feature is exact Python-int 0 of 1, geen bool/NumPy-int. Order(s) heeft dtype i/u, geen pure bool of float. Iedere volgorde bevat precies alle gehele indices 0 tot N-1, elk eenmaal; duplicaten binnen één volgorde zijn ongeldig. Verschillende herhalingen mogen dezelfde volgorde hebben. Order is één 1D-vector met N indices. Geef een zelfstandige floatarray met dezelfde vorm, waarbij alleen result[:,feature]=data[order,feature]. De andere kolom en de rijvolgorde blijven gelijk. Behoud alle invoerwaarden, vormen, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays en containers. Ongeldige invoer geeft ValueError. De functie toetst niet of nieuwe combinaties realistisch zijn.

Een hint

Kopieer de tabel en vervang alleen de gekozen kolom met de opgegeven indexvolgorde.

03
Vergelijk een vast model met zijn eigen baseline

Schrijf permutation_effect(model,data,labels,feature,orders). Gebruik de gegeven _imp_model, _imp_matrix, _imp_vector, _imp_feature, _imp_orders en _imp_predict. Model is exact een dict met weights/bias/interaction. Weights is een numerieke vector met twee naar float geconverteerde waarden binnen -10–10. Bias/interaction zijn eindige Python-/NumPy-int/float, geen bool, na veilige floatconversie binnen -10–10. Het vaste model is bias+w0*x0+w1*x1+interaction*x0*x1. Data heeft na conversie vorm N×2 en alle featurewaarden binnen -10–10. N ligt binnen 2–128; labels is een 1D-vector van N waarden binnen -1e4–1e4. Numerieke arrays worden met np.asarray beoordeeld: dtype i/u/f, geen pure bool/object/complex. Gemengde bool/int kan naar integer converteren. Controleer ook de naar float geconverteerde waarden op eindigheid en bounds. Feature is exact Python-int 0 of 1, geen bool/NumPy-int. Order(s) heeft dtype i/u, geen pure bool of float. Iedere volgorde bevat precies alle gehele indices 0 tot N-1, elk eenmaal; duplicaten binnen één volgorde zijn ongeldig. Verschillende herhalingen mogen dezelfde volgorde hebben. Orders is een 2D-matrix R×N met 1–16 herhalingen. Bereken baseline-MSE op de oorspronkelijke rijen en voor iedere volgorde MSE nadat alleen de gekozen kolom is herschikt. Geef een dict met baseline_loss (Python-float), permuted_losses en changes (zelfstandige floatarrays vorm R), mean_change en spread (Python-floats). Changes=permuted_losses-baseline_loss; mean_change is hun gemiddelde; spread is de populatiestandaardafwijking (ddof=0). Bewaar ook negatieve verschillen. Behoud alle invoerwaarden, vormen, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays en containers. Ongeldige invoer geeft ValueError. Geen training of nieuwe RNG-trekkingen. Deze beschrijvende spreiding is geen CI; dataherkomst/onafhankelijkheid, intrinsiek featurebelang of causaliteit wordt niet bewezen. Gewone float64-cancelatie rond nul blijft mogelijk. Gebruik vaste heldout-data en één vast model voor de vergelijking.

Een hint

Reken iedere permutatie vanuit de oorspronkelijke tabel; vergelijk telkens met hetzelfde baselineverlies.

Vastgelopen? Bekijk de voorbeeldoplossing
import math

import numpy as np

# Jouw functies

def squared_loss(actual, predicted):
    actual = _imp_vector(actual)
    predicted = _imp_vector(predicted, len(actual))
    difference = actual - predicted
    return float(np.mean(difference * difference))

def permute_feature(data, feature, order):
    values = _imp_matrix(data)
    feature = _imp_feature(feature)
    order = _imp_orders(order, len(values))
    result = values.copy()
    result[:, feature] = values[order, feature]
    return result

def permutation_effect(model, data, labels, feature, orders):
    model = _imp_model(model)
    values = _imp_matrix(data)
    labels = _imp_vector(labels, len(values))
    feature = _imp_feature(feature)
    orders = _imp_orders(orders, len(values), True)
    baseline = float(np.mean((_imp_predict(model, values) - labels) ** 2))
    losses = []
    for order in orders:
        permuted = values.copy()
        permuted[:, feature] = values[order, feature]
        losses.append(float(np.mean((_imp_predict(model, permuted) - labels) ** 2)))
    losses = np.asarray(losses, dtype=float)
    changes = losses - baseline
    return {'baseline_loss': baseline, 'permuted_losses': losses, 'changes': changes, 'mean_change': float(np.mean(changes)), 'spread': float(np.std(changes, ddof=0))}

# Gegeven helpers

def _imp_number(value, low, high):
    if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, float, np.integer, np.floating)):
        raise ValueError('Eindige begrensde scalar vereist')
    try:
        value = float(value)
    except (OverflowError, ValueError, TypeError):
        raise ValueError('Eindige begrensde scalar vereist') from None
    if not math.isfinite(value) or not low <= value <= high:
        raise ValueError('Eindige begrensde scalar vereist')
    return value

def _imp_vector(value, size=None, bound=10000.0):
    raw = np.asarray(value)
    if raw.ndim != 1 or not 1 <= len(raw) <= 128 or raw.dtype.kind not in 'iuf' or (not np.isfinite(raw).all()):
        raise ValueError('Een tot 128 eindige reële waarden vereist')
    values = raw.astype(float, copy=True)
    if not np.isfinite(values).all() or np.any(np.abs(values) > bound) or (size is not None and len(values) != size):
        raise ValueError('Evenlange begrensde vector vereist')
    return values

def _imp_matrix(value):
    raw = np.asarray(value)
    if raw.ndim != 2 or not 2 <= raw.shape[0] <= 128 or raw.shape[1] != 2 or (raw.dtype.kind not in 'iuf') or (not np.isfinite(raw).all()):
        raise ValueError('Twee tot 128 rijen met twee eindige features vereist')
    values = raw.astype(float, copy=True)
    if not np.isfinite(values).all() or np.any(np.abs(values) > 10):
        raise ValueError('Geconverteerde features binnen -10–10 vereist')
    return values

def _imp_feature(value):
    if type(value) is not int or value not in (0, 1):
        raise ValueError('Feature-index 0 of 1 als Python-int vereist')
    return value

def _imp_orders(value, count, matrix=False):
    raw = np.asarray(value)
    if raw.dtype.kind not in 'iu':
        raise ValueError('Gehele permutatie-indices vereist')
    if matrix:
        if raw.ndim != 2 or not 1 <= raw.shape[0] <= 16 or raw.shape[1] != count:
            raise ValueError('Een tot zestien volledige permutaties vereist')
        rows = raw
    else:
        if raw.ndim != 1 or len(raw) != count:
            raise ValueError('Een volledige permutatie vereist')
        rows = raw.reshape(1, -1)
    if np.any(rows < 0) or np.any(rows >= count) or any((not np.array_equal(np.sort(row), np.arange(count)) for row in rows)):
        raise ValueError('Iedere rij moet precies alle rij-indices bevatten')
    return raw.astype(np.int64, copy=True)

def _imp_model(model):
    if type(model) is not dict or set(model) != {'weights', 'bias', 'interaction'}:
        raise ValueError('Een vast toy-model met weights/bias/interaction vereist')
    weights = _imp_vector(model['weights'], 2, 10)
    return {'weights': weights, 'bias': _imp_number(model['bias'], -10, 10), 'interaction': _imp_number(model['interaction'], -10, 10)}

def _imp_predict(model, data):
    return model['bias'] + model['weights'][0] * data[:, 0] + model['weights'][1] * data[:, 1] + model['interaction'] * data[:, 0] * data[:, 1]

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