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

Bewaar bekende scenario’s en kies binnen een expliciete grens

Houd nut en schade apart, behoud nulgewichtscenario’s en filter op maximale bekende schade voordat je op nut rangschikt.

90 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je vergelijkt gemiddelde en slechtste bekende schade zonder scenario’s met gewicht nul te verbergen.
  • Je kiest alleen geschikte kandidaten binnen een expliciete grens en bewaart geen-keuze als betekenisvolle uitkomst.

Een assistent handelt gewone verzoeken snel af. Bij één uitzonderlijk verzoek kan dezelfde aanpak echter veel schade opleveren. Een fraai gemiddeld resultaat helpt weinig als dat ene scenario buiten beeld blijft.

Denk aan een proefrit met verschillende wegdelen. Een gemiddelde verbergt een gevaarlijke bocht wanneer rechte stukken zwaar meetellen. Je wilt de losse situaties zien voordat je beslist welke aanpak verantwoord lijkt.

In deze oefening staan nut en mogelijke schade in twee afzonderlijke tabellen. Beide bevatten verzonnen scores tussen nul en één. Het zijn geen gemeten kansen, medische beoordelingen of voorspellingen over een echt systeem.

1. Bewaar de losse scenario’s

De eerste rij is een routineverzoek en de tweede een uitzondering. Actie nul voert direct uit; actie één controleert eerst. Direct uitvoeren heeft nut 0,9 in beide rijen, maar schade 0,05 bij routine en 0,8 bij de uitzondering.

Controleren heeft telkens nut 0,6 en schade 0,1. Een hogere nutscore is beter; een hogere schadewaarde is ongunstiger. Die twee richtingen houd je zichtbaar apart.

utility = [[.9,.6],[.9,.6]]
harm = [[.05,.1],[.8,.1]]
report = scenario_profile(
    utility,harm,[0,0],[1.,0.])
print(report['mean_harm'])
print(report['worst_harm'])
print(report['worst_contexts'])
# 0.05
# 0.8
# [1]

Het tweede scenario krijgt in dit gemiddelde gewicht nul. Toch blijft zijn schadewaarde 0,8 aanwezig en is zijn rij-index één het slechtste bekende geval. De gewichten beschrijven hier de gemiddelde achtergrond; zij bepalen niet welke gedeclareerde scenario’s je controleert.

VERKEN HET ZELFEen grens vóór het nut, met ruimte voor geen keuze

Vergelijk vaste kandidaten op dezelfde bekende scenario’s. De achtergrond verandert gemiddelden; de hoogste schade blijft over alle opgegeven scenario’s berekend.

De gecombineerde kandidaat kan de routine direct afhandelen en de uitzondering controleren.

Kandidaat p2 gekozen

Eerst maximale schade ≤ 0,2, daarna hoogste gemiddelde nut. Geschikt in inputvolgorde: p1, p2.

Groen is gemiddeld nut, oranje is hoogste bekende schade. De gestreepte lijn markeert de grens; een hoger nut kan een te hoge schade niet goedmaken.

p0boven grens

Acties per context: 0, 0. Slechtste context: 1.

Nut
0,9
Gem. schade
0,05
Max. schade
0,8
00,51Nut0,9p0: Nut 0,9Max0,8p0: Max 0,8
Geselecteerde scores blijven per scenario zichtbaar
ContextNutSchadeGewicht
00,90,051
10,90,80

p1binnen grens

Acties per context: 1, 1. Slechtste contexten: 0, 1.

Nut
0,6
Gem. schade
0,1
Max. schade
0,1
00,51Nut0,6p1: Nut 0,6Max0,1p1: Max 0,1
Geselecteerde scores blijven per scenario zichtbaar
ContextNutSchadeGewicht
00,60,11
10,60,10

p2gekozen

Acties per context: 0, 1. Slechtste context: 1.

Nut
0,9
Gem. schade
0,05
Max. schade
0,1
00,51Nut0,9p2: Nut 0,9Max0,1p2: Max 0,1
Geselecteerde scores blijven per scenario zichtbaar
ContextNutSchadeGewicht
00,90,051
10,60,10

Gelijkheid aan de schadegrens telt mee. Bij gelijk nut volgen lagere maximale schade, lagere gemiddelde schade en de lexicografisch kleinste id. Een nieuw, niet opgegeven scenario kan andere gevolgen hebben.

Nut en schade zijn verzonnen scores, geen gemeten kansen. De grens geldt alleen voor de gedeclareerde scenario’s en kandidaten. Herkomst van rapporten en veiligheid in onbekende situaties zijn hiermee niet bewezen.

Noem het nut u_i, de schade h_i en de genormaliseerde achtergrondgewichten α_i. Je berekent twee gemiddelden, maar bewaart daarnaast de hoogste schade H over alle bekende scenario’s. De hoogste schade hoeft geen vaak voorkomend geval te zijn.

U=∑iαiuiB=∑iαihiH=maxihi
01
CHECKPOINTBewaar gemiddelde en slechtste bekende schade

2. Vergelijk vaste kandidaatgedragingen

Een beleid (policy) geeft in deze tabel voor iedere context één actie op. Je vergelijkt drie vaste kandidaten: altijd direct uitvoeren, altijd controleren en routine direct afhandelen terwijl je de uitzondering controleert. Er wordt niets getraind of uitgevoerd buiten deze tabellen.

policies = [[0,0],[1,1],[0,1]]
rows = compare_candidates(
    utility,harm,policies,[1.,0.])
for row in rows:
    print(row['id'],
          row['mean_utility'],
          row['worst_harm'])
# p0 0.9 0.8
# p1 0.6 0.1
# p2 0.9 0.1

Kandidaat p0 en p2 hebben hetzelfde gemiddelde nut 0,9. Hun slechtste bekende schade verschilt sterk: 0,8 tegenover 0,1. Alleen op het gemiddelde nut rangschikken zou dat belangrijke onderscheid missen.

02
CHECKPOINTBeoordeel een klein vast stel kandidaatgedragingen

3. Leg een grens vast vóór de nutsrangschikking

Kies expliciet welke hoogste schadewaarde je in deze oefening toestaat. Noem die grens τ. Een kandidaat komt alleen in aanmerking wanneer zijn slechtste bekende schade H hoogstens τ is; gelijkheid telt mee.

E={k:Hk≤τ}

Met grens 0,2 valt p0 af. Van p1 en p2 kies je daarna het hoogste gemiddelde nut: p2 wint met 0,9. Bij gelijke nutsscores volgt eerst lagere slechtste schade, daarna lagere gemiddelde schade en ten slotte de lexicografisch kleinste id.

choice = choose_guarded(rows,.2)
print(choice['chosen_id'])
print(choice['eligible_ids'])
# p2
# ['p1', 'p2']
03
CHECKPOINTFilter op de grens vóór je nut vergelijkt

Je filtert dus vóór de nutsvergelijking. Eerst de nuttigste kandidaat kiezen en hem achteraf goedpraten met een gemiddelde is een andere beslisregel. De grens en de verzameling scenario’s blijven zichtbare ontwerpkeuzes.

Geen keuze is een betekenisvolle uitkomst

Verlaag de grens naar nul. Geen kandidaat voldoet, dus de functie geeft geen gekozen id: None. Bij grens 0,1 voldoen p1 en p2 precies; de gelijkheidsgrens sluit ze niet uit.

strict = choose_guarded(rows,0.)
boundary = choose_guarded(rows,.1)
print(strict['chosen_id'])
print(boundary['chosen_id'])
# None
# p2

None betekent dat je onder deze afspraken geen automatische keuze hebt. Je kunt het verzoek laten beoordelen, extra gegevens verzamelen of de kandidaatgedragingen verbeteren. Een ontbrekende keuze vervangen door de nuttigste afgewezen kandidaat zou de afgesproken grens opheffen.

Waar deze beoordeling ophoudt

De maximumcontrole omvat alleen de opgegeven scenario’s. Een nieuw type verzoek of veranderende omgeving kan andere gevolgen hebben. Geen schade boven de grens in deze tabel bewijst daarom geen algemene veiligheid; beperkte bevoegdheden, toezicht en nieuwe evaluaties blijven nodig.

De selectiefunctie accepteert ook aangeleverde kandidaatrapporten. Zij controleert hun structuur en gedeclareerde getallen, maar bewijst niet dat iemand de tabellen werkelijk heeft doorgerekend. Wie de schadecriteria bepaalt en welke mensen vertegenwoordigd zijn, blijft bovendien een bestuurlijke vraag.

Wat je nu kunt

  • Je bewaart nut, gemiddelde schade en slechtste bekende schade afzonderlijk.
  • Je past een expliciete grens toe voordat je op nut rangschikt.
  • Je herkent geen-keuze en beperkt conclusies tot de gedeclareerde scenario’s.

Hierna verbind je zulke ontwerpkeuzes aan verantwoordelijkheid, documentatie en toezicht.

DE VOLGENDE STAPCombineer beoordeelde risico’s en behoud open vragen
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
Bewaar gemiddelde en slechtste bekende schade

Schrijf scenario_profile(utility,harm,actions,weights=None). Gebruik _safe_tables, _safe_actions, _safe_weights en _safe_mean. Utility/harm hebben dezelfde vorm en bounds als de scoretabellen. Scoretabelvorm is N×A met 1–64 contexten en 2–8 acties; alle geconverteerde waarden binnen 0–1. Arrays worden via np.asarray beoordeeld: dtype i/u/f, geen pure bool/object/complex; gemengde bool/numerieke reeksen kunnen numeriek worden. Controleer eindigheid en bounds ook na veilige floatconversie. Actions is na np.asarray een 1D-vector met N gehele indices 0 tot A-1, dtype i/u; pure bool/float/object ongeldig, gemengde bool/int kan integer worden. Weights is None voor uniform of een 1D-vector met N eindige geconverteerde waarden binnen 0–1 en positieve som. Normaliseer naar som één. Alle gedeclareerde contexten blijven in de losse uitvoer aanwezig, ook bij gewicht nul. Geef een dict met utility/harm/weights (zelfstandige floatarrays vorm N met geselecteerde nut-/schadescores en genormaliseerde gewichten), mean_utility/mean_harm/worst_harm (Python-floats), en worst_contexts (zelfstandige lijst van Python-ints, alle scenario-indices met maximale geselecteerde schade in oplopende volgorde). Worst_harm is het maximum over alle N contexten, ook bij gewicht nul. Gebruik de gegeven helper voor het gewogen floatgemiddelde; wegens afronding begrenst die het op minimum/maximum van de gemeten vector. Behoud invoerwaarden, vormen, volgorde, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays/containers; ongeldige invoer geeft ValueError. Alleen de opgegeven bekende scenario’s, geen gemeten schadekans of algemene veiligheid.

Een hint

Selecteer nut en schade apart; gebruik gewichten voor gemiddelden en alle rijen voor het maximum.

02
Beoordeel een klein vast stel kandidaatgedragingen

Schrijf compare_candidates(utility,harm,policies,weights=None). Gebruik _safe_tables, _safe_actions, _safe_weights en _safe_mean. Scoretabelvorm is N×A met 1–64 contexten en 2–8 acties; alle geconverteerde waarden binnen 0–1. Utility/harm hebben dezelfde vorm. Arrays worden via np.asarray beoordeeld: dtype i/u/f, geen pure bool/object/complex; gemengde bool/numerieke reeksen kunnen numeriek worden. Controleer eindigheid en bounds ook na veilige floatconversie. Weights is None voor uniform of een 1D-vector met N eindige geconverteerde waarden binnen 0–1 en positieve som. Normaliseer naar som één. Alle gedeclareerde contexten blijven in de losse uitvoer aanwezig, ook bij gewicht nul. Policies is een integer-matrix M×N met 1–16 kandidaten; dtype i/u, indices 0 tot A-1; pure bool/float/object ongeldig. Dubbele beleidsrijen zijn toegestaan. Geef een zelfstandige lijst van M zelfstandige dicts, in inputvolgorde. Iedere dict heeft precies id (p0,p1,...), actions (zelfstandige integerarray vorm N), mean_utility/mean_harm/worst_harm (Python-floats) en worst_contexts (zelfstandige lijst van alle maximale schade-indices als Python-ints, oplopend). De gemiddelden wegen de geselecteerde nut-/schadevector; het maximum telt alle N scenario’s mee. Gebruik de gegeven helper voor het gewogen floatgemiddelde; wegens afronding begrenst die het op minimum/maximum van de gemeten vector. Behoud invoerwaarden, vormen, volgorde, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays/containers; ongeldige invoer geeft ValueError. Vergelijk vaste kandidaten zonder training of nieuwe scenario’s te verzinnen.

Een hint

Reken iedere kandidaat op dezelfde twee tabellen en achtergrond door.

03
Filter op de grens vóór je nut vergelijkt

Schrijf choose_guarded(candidate_rows,max_harm=.2). Gebruik _safe_candidates en _safe_number. Candidate_rows is exact list/tuple met 1–16 exacte dicts, precies id/actions/mean_utility/mean_harm/worst_harm/worst_contexts. Id is een unieke exacte Python-str p0 tot p15, zonder voorloopnullen. Actions is een 1D-integervector dtype i/u met 1–64 indices 0–7, dezelfde lengte N in alle kandidaten. Worst_contexts is een 1D-integervector dtype i/u met 1–N unieke, strikt stijgende indices binnen 0–N-1. Pure bool/float/object-arrays ongeldig; gemengde bool/int kan integer converteren. Scalars zijn Python-/NumPy-int/float zonder bool; converteer veilig naar Python-float en controleer daarna eindigheid en bereik 0–1. De drie rapportscalars en max_harm vallen binnen 0–1; mean_harm<=worst_harm. De samenvattingen hoeven geen reconstrueerbare of echte tabelherkomst te bewijzen. Filter worst_harm<=max_harm (gelijkheid telt mee). Kies daarna hoogste mean_utility, dan laagste worst_harm, dan laagste mean_harm, dan lexicografisch kleinste id. Geef een dict met max_harm (Python-float), chosen_id (str of None), eligible_ids (zelfstandige lijst in inputvolgorde) en rejected (zelfstandige dict van ieder afgewezen id naar above-limit). Behoud invoerwaarden, vormen, volgorde, readonlyflags en de globale Python-/NumPy-RNG. Geef zelfstandige uitvoerarrays/containers; ongeldige invoer geeft ValueError. Geen keuze is geldig als niemand voldoet; gebruik dan geen afgewezen fallback. Geen algemeen optimaal of veilig systeem claimen.

Een hint

Maak eerst de geschikte set; rangschik alleen die kandidaten en bewaar None als de set leeg is.

Vastgelopen? Bekijk de voorbeeldoplossing
import math
import re
import numpy as np

# Jouw functies

def scenario_profile(utility, harm, actions, weights=None):
    utility, harm = _safe_tables(utility, harm)
    actions = _safe_actions(actions, len(utility), utility.shape[1])
    weights = _safe_weights(weights, len(utility))
    rows = np.arange(len(utility))
    selected_utility = utility[rows, actions].copy()
    selected_harm = harm[rows, actions].copy()
    worst = float(np.max(selected_harm))
    return {'utility': selected_utility, 'harm': selected_harm, 'weights': weights.copy(), 'mean_utility': _safe_mean(selected_utility, weights), 'mean_harm': _safe_mean(selected_harm, weights), 'worst_harm': worst, 'worst_contexts': [int(index) for index, value in enumerate(selected_harm) if value == worst]}

def compare_candidates(utility, harm, policies, weights=None):
    utility, harm = _safe_tables(utility, harm)
    policies = _safe_actions(policies, len(utility), utility.shape[1], True)
    weights = _safe_weights(weights, len(utility))
    rows = np.arange(len(utility))
    result = []
    for index, actions in enumerate(policies):
        chosen_utility = utility[rows, actions]
        chosen_harm = harm[rows, actions]
        worst = float(np.max(chosen_harm))
        result.append({'id': 'p' + str(index), 'actions': actions.copy(), 'mean_utility': _safe_mean(chosen_utility, weights), 'mean_harm': _safe_mean(chosen_harm, weights), 'worst_harm': worst, 'worst_contexts': [int(row) for row, value in enumerate(chosen_harm) if value == worst]})
    return result

def choose_guarded(candidate_rows, max_harm=0.2):
    rows = _safe_candidates(candidate_rows)
    limit = _safe_number(max_harm)
    eligible = [row for row in rows if row['worst_harm'] <= limit]
    chosen = min(eligible, key=lambda row: (-row['mean_utility'], row['worst_harm'], row['mean_harm'], row['id'])) if eligible else None
    return {'max_harm': limit, 'chosen_id': None if chosen is None else chosen['id'], 'eligible_ids': [row['id'] for row in eligible], 'rejected': {row['id']: 'above-limit' for row in rows if row['worst_harm'] > limit}}

# Gegeven helpers

def _safe_tables(utility, harm):
    result = []
    for value in [utility, harm]:
        raw = np.asarray(value)
        if raw.ndim != 2 or not 1 <= raw.shape[0] <= 64 or (not 2 <= raw.shape[1] <= 8) or (raw.dtype.kind not in 'iuf') or (not np.isfinite(raw).all()):
            raise ValueError('Kleine eindige numerieke scenariotabellen vereist')
        with np.errstate(over='ignore', invalid='ignore'):
            values = raw.astype(float, copy=True)
        if not np.isfinite(values).all() or np.any(values < 0) or np.any(values > 1):
            raise ValueError('Geconverteerde nut- en schadewaarden binnen 0–1 vereist')
        result.append(values)
    if result[0].shape != result[1].shape:
        raise ValueError('Gelijke tabelvormen vereist')
    return result

def _safe_actions(value, rows, options, matrix=False):
    raw = np.asarray(value)
    valid = raw.ndim == 2 and 1 <= raw.shape[0] <= 16 and (raw.shape[1] == rows) if matrix else raw.ndim == 1 and len(raw) == rows
    if not valid or raw.dtype.kind not in 'iu' or np.any(raw < 0) or np.any(raw >= options):
        raise ValueError('Geldige gehele acties per scenario vereist')
    return raw.astype(np.int64, copy=True)

def _safe_weights(value, rows):
    if value is None:
        return np.ones(rows, dtype=float) / rows
    raw = np.asarray(value)
    if raw.ndim != 1 or len(raw) != rows or raw.dtype.kind not in 'iuf' or (not np.isfinite(raw).all()):
        raise ValueError('Een eindig numeriek gewicht per scenario vereist')
    with np.errstate(over='ignore', invalid='ignore'):
        weights = raw.astype(float, copy=True)
    if not np.isfinite(weights).all() or np.any(weights < 0) or np.any(weights > 1) or (float(np.sum(weights)) <= 0):
        raise ValueError('Geconverteerde gewichten binnen 0–1 met positieve som vereist')
    return weights / np.sum(weights)

def _safe_mean(values, weights):
    return float(np.clip(weights @ values, float(np.min(values)), float(np.max(values))))

def _safe_number(value):
    if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, float, np.integer, np.floating)):
        raise ValueError('Eindige numerieke fractie vereist')
    try:
        value = float(value)
    except (OverflowError, ValueError, TypeError):
        raise ValueError('Eindige numerieke fractie vereist') from None
    if not math.isfinite(value) or not 0 <= value <= 1:
        raise ValueError('Geconverteerde fractie binnen 0–1 vereist')
    return value

def _safe_candidates(candidate_rows):
    if type(candidate_rows) not in (list, tuple) or not 1 <= len(candidate_rows) <= 16:
        raise ValueError('Een tot zestien kandidaatrapporten vereist')
    result = []
    seen = set()
    count = None
    for row in candidate_rows:
        if type(row) is not dict or set(row) != {'id', 'actions', 'mean_utility', 'mean_harm', 'worst_harm', 'worst_contexts'}:
            raise ValueError('Precies de gedeclareerde kandidaatvelden vereist')
        name = row['id']
        if type(name) is not str or not re.fullmatch('p(?:[0-9]|1[0-5])', name) or name in seen:
            raise ValueError('Unieke id p0 tot p15 vereist')
        seen.add(name)
        raw = np.asarray(row['actions'])
        if raw.ndim != 1 or not 1 <= len(raw) <= 64:
            raise ValueError('Een tot 64 scenarioacties vereist')
        actions = _safe_actions(raw, len(raw), 8)
        if count is not None and len(actions) != count:
            raise ValueError('Iedere kandidaat heeft evenveel scenarioacties')
        count = len(actions)
        utility = _safe_number(row['mean_utility'])
        harm = _safe_number(row['mean_harm'])
        worst = _safe_number(row['worst_harm'])
        contexts = np.asarray(row['worst_contexts'])
        if contexts.ndim != 1 or not 1 <= len(contexts) <= count or contexts.dtype.kind not in 'iu' or np.any(contexts < 0) or np.any(contexts >= count):
            raise ValueError('Geldige slechtste scenario-indices vereist')
        contexts = contexts.astype(np.int64, copy=True)
        if np.any(np.diff(contexts) <= 0) or harm > worst:
            raise ValueError('Geordende unieke indices en gemiddelde schade hoogstens het maximum vereist')
        result.append({'id': name, 'actions': actions, 'mean_utility': utility, 'mean_harm': harm, 'worst_harm': worst, 'worst_contexts': [int(index) for index in contexts]})
    return result

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