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

Bekijk individuele responslijnen vóór het gemiddelde

Varieer één feature bij vaste rijcontexten, centreer per eigen anchor en vergelijk uniforme met expliciet gewogen achtergrondgemiddelden.

90 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je bouwt ICE-lijnen door één feature te variëren en de overige context per rij vast te houden.
  • Je centreert per eigen anchor en onderscheidt afzonderlijke lijnen, een PDP-achtergrondgemiddelde en een causale verklaring.

Een gemiddeld modelverloop kan verschillende situaties verbergen. Misschien verandert de voorspelde bezorgtijd sterk met afstand bij grote orders, maar nauwelijks bij kleine. Hoe zie je dat zonder die verschillen direct weg te middelen?

Denk aan een rij met vaste context. Je schuift alleen de afstand langs een meetlat en vraagt hetzelfde model telkens opnieuw om een score. Doe dat voor meerdere contexten en je krijgt afzonderlijke lijnen voordat je een gemiddelde tekent.

Onze twee features en uitkomsten zijn genormaliseerde oefenscores. Het vaste model gebruikt twee gewichten en een interactie; er worden geen parameters opnieuw geleerd. Alle rijen zijn verzonnen.

1. Houd per rij de overige context vast

Een individuele responslijn (Individual Conditional Expectation, ICE) varieert één feature en houdt de andere per achtergrondrij vast. Conditional betekent hier niet dat je automatisch nieuwe rijen trekt uit de populatie die bij die afstand hoort. Je vraagt het gegeven model om voorspellingen bij de gemaakte combinaties.

VERKEN HET ZELFBekijk de lijnen vóór je ze middelt

Varieer één feature bij vaste rijcontexten. Vergelijk afzonderlijke responslijnen, veranderingen vanaf een eigen anchor en hun achtergrondgemiddelde.

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

De interactie maakt de eerste lijn vlak en de tweede stijgend. Eén gemiddelde verbergt dat verschil.

Oorspronkelijke modelrespons; er is geen anchor afgetrokken. Het uniforme gemiddelde is het gewone PDP van deze achtergrond.

Modelrespons-2,604,6-101Rij 1: -2; -2; -2Rij 2: 0; 2; 4Achtergrondgemiddelde: -1; 0; 1Gridwaarde feature 0
  • Rij 1: gewicht 0,5
  • Rij 2: gewicht 0,5
  • ● ━Achtergrondgemiddelde
Respons per gridpunt
GridRij 1Rij 2Gem.
-1-20-1
0-220
1-241
Welke achtergrond blijft per rij vast?
  • Rij 1: oorspronkelijke x₀ 0, x₁ -1. Feature 0 wordt door elk gridpunt vervangen; de andere blijft gelijk.
  • Rij 2: oorspronkelijke x₀ 1, x₁ 1. Feature 0 wordt door elk gridpunt vervangen; de andere blijft gelijk.

Een gemiddelde kan verschillende hellingen verbergen. Centreren trekt per lijn haar eigen anchor af. Gewichten kiezen een achtergrond; zij laten de andere feature niet automatisch met het grid meeveranderen en maken de curve geen causale verklaring.

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 model f, de gekozen feature j, een gridwaarde z en de overige featurewaarden van rij i x_C^(i). Elke lijn bestaat uit f bij z en die vaste context. Het grid geeft de horizontale volgorde; i blijft dezelfde achtergrondrij.

ICEi(z)=f(z,xC(i))

Kies een model met gewicht één voor de eerste feature, twee voor de tweede en een interactie van één, zonder bias. De eerste feature heet x_0 en de tweede x_1. Hun product laat het effect van de eerste afhangen van de tweede.

f(x0,x1)=x0+2x1+x0x1

Varieer de eerste feature van min één via nul naar één. Bij de tweede feature min één is de voorspelling steeds min twee. Bij de tweede feature één wordt zij nul, twee en vier: de interactie maakt de hellingen verschillend.

model = {'weights':[1.,2.],
         'bias':0.,'interaction':1.}
data = [[0.,-1.],[1.,1.]]
grid = [-1.,0.,1.]
lines = ice_values(model,data,0,grid)
for row in lines:
    print(row.tolist())
# [-2.0, -2.0, -2.0]
# [0.0, 2.0, 4.0]

De oorspronkelijke x0 van iedere rij wordt bij iedere gridwaarde vervangen; x1 blijft vast. Een waarde kopiëren naar de verkeerde kolom of rijen verwisselen zou dus een andere vraag beantwoorden. Er zijn geen labels nodig om dit modelgedrag uit te rekenen.

01
CHECKPOINTTeken per achtergrondrij een eigen responslijn

2. Vergelijk veranderingen vanaf een eigen startpunt

Verschillende beginhoogtes kunnen het vergelijken van lijnen lastig maken. Centreren trekt daarom per rij haar eigen voorspelling bij één gekozen anchorpunt af. De helling blijft behouden; je trekt niet een gemiddelde over alle rijen af.

Noem de anchor-index a en de bijbehorende gridwaarde z_a. De gecentreerde lijn C_i begint bij nul op dat punt. Standaard is a nul, dus het eerste gridpunt.

Ci(z)=ICEi(z)−ICEi(za)

Onze eerste lijn verschuift van min twee naar nul op alle punten. De tweede start al op nul en blijft nul, twee, vier. Het verschil in verandering blijft dus zichtbaar nadat de beginhoogtes zijn gelijkgezet.

centered = center_ice(lines)
for row in centered:
    print(row.tolist())
# [0.0, 0.0, 0.0]
# [0.0, 2.0, 4.0]
02
CHECKPOINTVergelijk iedere lijn met haar eigen anchor

3. Middel over een expliciete achtergrond

Een gedeeltelijke-afhankelijkheidscurve (Partial Dependence Plot, PDP) middelt de voorspellingen over de gekozen achtergrondrijen. Noem hun aantal N. Met gelijke gewichten neem je bij elk gridpunt het gemiddelde van de ICE-waarden op diezelfde positie.

PD(z)=1N∑i=1NICEi(z)

Het gemiddelde van min twee en nul is min één; daarna krijg je nul en één. Die enkele lijn verbergt dat één context helemaal niet verandert terwijl de andere sterk stijgt. Bekijk daarom de afzonderlijke lijnen naast het gemiddelde.

average = mean_response(lines)
print(average['response'].tolist())
print(average['weights'].tolist())
# [-1.0, 0.0, 1.0]
# [0.5, 0.5]

Je kunt ook vooraf publieke, niet-negatieve achtergrondgewichten w_i kiezen. De functie normaliseert ze tot α_i met som één; dat verandert de gekozen referentieverdeling, niet het model. Een zwaarder gewogen context telt meer mee in de samenvatting.

αi=wi∑k=1Nwk

Geef de eerste context gewicht 0,25 en de tweede 0,75. Bij de eerste gridwaarde levert de eerste lijn min twee maal een kwart: min een half; de tweede draagt nul bij. De andere twee gewogen gemiddelden zijn één en 2,5.

weighted = mean_response(
    lines,[.25,.75])
print(weighted['response'].tolist())
# [-0.5, 1.0, 2.5]
03
CHECKPOINTMiddel over een expliciete achtergrondverdeling

Waar de uitleg ophoudt

Bij samenhangende kenmerken kunnen gridcombinaties buiten de waargenomen gegevens liggen. De curve toetst niet of zulke combinaties realistisch zijn. Een verandering in de voorspelling bewijst bovendien niet wat een echte ingreep in de wereld zou veroorzaken.

De functies accepteren ook aangeleverde curvematrices; zij bewijzen de herkomst daarvan niet. Een PDP is hier een gemiddelde over een expliciete achtergrond, geen automatische conditionele populatieverwachting. Achtergrondkeuze, modelkwaliteit en individuele verschillen blijven onderdeel van de interpretatie.

Wat je nu kunt

  • Je varieert één feature terwijl de overige context per rij vastblijft.
  • Je centreert op een eigen anchor zonder verschillende hellingen weg te poetsen.
  • Je onderscheidt afzonderlijke lijnen, hun achtergrondgemiddelde en een causale verklaring.

Hierna kijk je naar alignment en veiligheid: welke doelen sturen het systeem en hoe controleer je gewenst gedrag?

DE VOLGENDE STAPOnderscheid een beloningsproxy van het bedoelde doel
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
Teken per achtergrondrij een eigen responslijn

Schrijf ice_values(model,data,feature,grid). Gebruik de gegeven _ice_model, _ice_data, _ice_feature, _ice_grid en _ice_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 1–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. Grid is een 1D-vector met 2–33 naar float geconverteerde, strikt stijgende punten binnen -10–10. Geef een zelfstandige floatarray met vorm N×G. Kolom k bevat modelvoorspellingen wanneer je alleen de gekozen feature op grid[k] zet; de andere feature blijft per rij gelijk. Het oorspronkelijke gekozen feature wordt bij elk punt vervangen. Behoud alle invoerwaarden, vormen, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays en containers. Ongeldige invoer geeft ValueError. Geen labels, training, automatische conditionele populatieverwachting of causale conclusie.

Een hint

Loop over het grid, kopieer de oorspronkelijke rijen en verander steeds dezelfde feature.

02
Vergelijk iedere lijn met haar eigen anchor

Schrijf center_ice(curves,anchor=0). Gebruik de gegeven _ice_curves. Curves is na conversie een eindige numerieke matrix met 1–128 rijen en 2–33 kolommen. De rijen zijn de afzonderlijke lijnen, de kolommen dezelfde geordende gridposities. De geconverteerde curvewaarden liggen 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. Anchor is exact Python-int binnen 0–G-1, geen bool/NumPy-int. Geef een zelfstandige floatarray met dezelfde vorm: trek van iedere rij haar eigen waarde in kolom anchor af. De anchor-kolom wordt nul. Andere hellingen blijven behouden; de uitvoer kan binnen -2e4–2e4 vallen. Behoud alle invoerwaarden, vormen, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays en containers. Ongeldige invoer geeft ValueError. De aangeleverde curves hoeven geen bewezen modelherkomst te hebben.

Een hint

Behoud de anchorwaarden als N×1-kolom en trek die per rij af.

03
Middel over een expliciete achtergrondverdeling

Schrijf mean_response(curves,weights=None). Gebruik de gegeven _ice_curves en _ice_weights. Curves is na conversie een eindige numerieke matrix met 1–128 rijen en 2–33 kolommen. De rijen zijn de afzonderlijke lijnen, de kolommen dezelfde geordende gridposities. De geconverteerde curvewaarden liggen binnen -2e4–2e4; iedere geldige center_ice-uitvoer past dus ook. 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. Weights is None voor uniforme gewichten, of een 1D-vector met N eindige naar float geconverteerde gewichten binnen 0–1 en strikt positieve som. Normaliseer de gewichten naar som één. Geef een dict met response (zelfstandige floatarray vorm G, gewogen som per gridpunt), weights (zelfstandige genormaliseerde floatarray vorm N), rows en points (Python-ints N en G). Behoud alle invoerwaarden, vormen, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays en containers. Ongeldige invoer geeft ValueError. Aangeleverde curvematrices hoeven geen modelprovenance te bewijzen. Uniform is het gewone PDP-gemiddelde; publieke vaste gewichten kiezen een andere achtergrond, geen automatische conditionele verwachting of causaliteit.

Een hint

Normaliseer eerst de gewichten en middel dan over de rijen, niet over de gridkolommen.

Vastgelopen? Bekijk de voorbeeldoplossing
import math

import numpy as np

# Jouw functies

def ice_values(model, data, feature, grid):
    model = _ice_model(model)
    values = _ice_data(data)
    feature = _ice_feature(feature)
    grid = _ice_grid(grid)
    result = np.empty((len(values), len(grid)), dtype=float)
    for index, point in enumerate(grid):
        changed = values.copy()
        changed[:, feature] = point
        result[:, index] = _ice_predict(model, changed)
    return result

def center_ice(curves, anchor=0):
    values = _ice_curves(curves)
    if type(anchor) is not int or not 0 <= anchor < values.shape[1]:
        raise ValueError('Geldige gehele anchor-index vereist')
    return values - values[:, anchor:anchor + 1]

def mean_response(curves, weights=None):
    values = _ice_curves(curves, 20000.0)
    weights = _ice_weights(weights, len(values))
    return {'response': weights @ values, 'weights': weights.copy(), 'rows': len(values), 'points': values.shape[1]}

# Gegeven helpers

def _ice_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 _ice_vector(value, size=None, bound=10000.0, minimum=1, maximum=128):
    raw = np.asarray(value)
    if raw.ndim != 1 or not minimum <= len(raw) <= maximum or raw.dtype.kind not in 'iuf' or (not np.isfinite(raw).all()):
        raise ValueError('Begrensde eindige reële vector 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 _ice_data(value):
    raw = np.asarray(value)
    if raw.ndim != 2 or not 1 <= raw.shape[0] <= 128 or raw.shape[1] != 2 or (raw.dtype.kind not in 'iuf') or (not np.isfinite(raw).all()):
        raise ValueError('Een 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 _ice_model(model):
    if type(model) is not dict or set(model) != {'weights', 'bias', 'interaction'}:
        raise ValueError('Vast tweefeaturemodel met weights/bias/interaction vereist')
    return {'weights': _ice_vector(model['weights'], 2, 10), 'bias': _ice_number(model['bias'], -10, 10), 'interaction': _ice_number(model['interaction'], -10, 10)}

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

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

def _ice_grid(grid):
    grid = _ice_vector(grid, bound=10, minimum=2, maximum=33)
    if np.any(np.diff(grid) <= 0):
        raise ValueError('Strikt stijgend geconverteerd grid vereist')
    return grid

def _ice_curves(value, bound=10000.0):
    raw = np.asarray(value)
    if raw.ndim != 2 or not 1 <= raw.shape[0] <= 128 or (not 2 <= raw.shape[1] <= 33) or (raw.dtype.kind not in 'iuf') or (not np.isfinite(raw).all()):
        raise ValueError('Een tot 128 eindige curven met 2–33 punten vereist')
    values = raw.astype(float, copy=True)
    if not np.isfinite(values).all() or np.any(np.abs(values) > bound):
        raise ValueError('Begrensde geconverteerde curvewaarden vereist')
    return values

def _ice_weights(value, count):
    values = np.ones(count, dtype=float) if value is None else _ice_vector(value, count, 1.0)
    if np.any(values < 0) or float(np.sum(values)) <= 0:
        raise ValueError('Niet-negatieve gewichten met positieve som vereist')
    return values / np.sum(values)

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