Decisioni e utilità (DTProbLog)


DTProbLog, dove DT sta per decision-theoretic, è l'estensione di ProbLog per il ragionamento su decisioni e utilità. L'obiettivo qui non è solo calcolare la probabilità di un evento, ma scegliere la strategia che massimizza l'utilità attesa .
In DTProbLog, dove non ci sono né queries né evidence,

Il sistema trova la combinazione di decisioni che massimizza il guadagno atteso.

Un esempio introduttivo classico per capire la sintassi di base riguarda il problema: fuori può piovere o tirare vento e occorre decidere se portare un ombrello o un impermeabile, valutando costi e benefici.
Occorrerà usare il modulo dtproblog e potremo servirci di una function come la seguente.

from problog.tasks.dtproblog import dtproblog from problog.program import PrologString def run_DT(program_string): decisions, score, statistics = dtproblog(PrologString(program_string)) return {'decisions: ':decisions, 'score: ':round(score,2), 'statistics: ':statistics }
Così
prog = (r""" % Fatti probabilistici (l'incertezza del mondo) 0.3::rain. 0.5::wind. % Fatti decisionali (le scelte che possiamo fare) ?::umbrella. ?::raincoat. % Regole che determinano i risultati delle scelte broken_umbrella :- umbrella, rain, wind. % Ombrello rotto se piove e c'è vento dry :- rain, raincoat. % Asciutti se piove e abbiamo l'impermeabile dry :- rain, umbrella, not broken_umbrella. % Asciutti se piove, abbiamo l'ombrello e non si rompe dry :- not(rain). % Asciutti se non piove % Utilità (ricompense o costi per gli eventi) utility(broken_umbrella, -40). % Malus se l'ombrello si rompe utility(raincoat, -20). % Costo dell'impermeabile utility(umbrella, -2). % Costo dell'ombrello utility(dry, 60). % Ricompensa per essere rimasti asciutti """) run_DT(prog)
{'decisions: ': {umbrella: 1, raincoat: 0}, 'score: ': 43.0, 'statistics: ': {'eval': 4}}
Il motore di DTProbLog valuta tutte le combinazioni di decisioni, considera le probabilità di pioggia e vento, e calcola l'utilità attesa per ogni strategia. La strategia ottimale risulta essere quella con l'utilità attesa più alta.
Questo esempio mostra come l'inferenza combini la logica delle regole con le probabilità e i valori numerici.

Un approccio che mostra più nel dettaglio il meccanismo è il seguente.

import re from itertools import product from problog import get_evaluatable from problog.logic import Term UTILITY_RE = re.compile(r'utility\(\s*([^,]+?)\s*,\s*([-\d.]+)\s*\)') def run_DT(program_string, verbose=True): """ Risolve un problema DTProbLog enumerando tutte le combinazioni di decisioni e calcolando l'utilità attesa per ciascuna. Args: program_string (str): Programma DTProbLog con fatti decisionali (?::) e utility/2. verbose (bool): Se True, stampa i dettagli di ogni combinazione. Returns: tuple: (strategia_ottimale, valore_atteso) """ decisions, utility_info, base_lines = [], {}, [] for raw_line in program_string.splitlines(): line = raw_line.split('%', 1)[0].strip() # rimuove eventuali commenti inline if not line: continue if line.startswith('?::') and line.endswith('.'): name = line[3:-1].strip() decisions.append(name) base_lines.append(f'0.0::{name}.') # placeholder: falso di default elif line.startswith('utility('): match = UTILITY_RE.match(line) if match: utility_info[match.group(1)] = float(match.group(2)) else: base_lines.append(line) if not decisions: print("Nessuna decisione trovata nel programma.") return {}, 0.0 base_program = "\n".join(base_lines) query_block = "\n".join(f"query({atom})." for atom in utility_info) results = {} if verbose: print("Analisi di tutte le combinazioni di decisioni:") print("-" * 60) for combo in product([True, False], repeat=len(decisions)): decision_dict = dict(zip(decisions, combo)) facts = "\n".join(f"{name}." for name, val in decision_dict.items() if val) program = PrologString(f"{base_program}\n{facts}\n{query_block}") query_results = get_evaluatable().create_from(program).evaluate() probs = {atom: query_results.get(Term(atom), 0.0) for atom in utility_info} expected_utility = sum(probs[atom] * value for atom, value in utility_info.items()) key = ", ".join(f"{k}={v}" for k, v in decision_dict.items()) results[key] = (decision_dict, expected_utility) if verbose: print(f"{key}:") for atom, value in utility_info.items(): print(f" P({atom}) = {probs[atom]:.3f} (contributo: {probs[atom] * value:.2f})") print(f" Utilità attesa = {expected_utility:.2f}\n") best_key = max(results, key=lambda k: results[k][1]) best_strategy, best_value = results[best_key] print("-" * 60) print(f"🏆 DECISIONE OTTIMALE: {best_key}") print(f" Utilità attesa massima: {best_value:.2f}")
così
run_DT(prog)
Analisi di tutte le combinazioni di decisioni: ------------------------------------------------------------ umbrella=True, raincoat=True: P(broken_umbrella) = 0.150 (contributo: -6.00) P(raincoat) = 1.000 (contributo: -20.00) P(umbrella) = 1.000 (contributo: -2.00) P(dry) = 1.000 (contributo: 60.00) Utilità attesa = 32.00 umbrella=True, raincoat=False: P(broken_umbrella) = 0.150 (contributo: -6.00) P(raincoat) = 0.000 (contributo: -0.00) P(umbrella) = 1.000 (contributo: -2.00) P(dry) = 0.850 (contributo: 51.00) Utilità attesa = 43.00 umbrella=False, raincoat=True: P(broken_umbrella) = 0.000 (contributo: -0.00) P(raincoat) = 1.000 (contributo: -20.00) P(umbrella) = 0.000 (contributo: -0.00) P(dry) = 1.000 (contributo: 60.00) Utilità attesa = 40.00 umbrella=False, raincoat=False: P(broken_umbrella) = 0.000 (contributo: -0.00) P(raincoat) = 0.000 (contributo: -0.00) P(umbrella) = 0.000 (contributo: -0.00) P(dry) = 0.700 (contributo: 42.00) Utilità attesa = 42.00 ------------------------------------------------------------ 🏆 DECISIONE OTTIMALE: umbrella=True, raincoat=False Utilità attesa massima: 43.00

L'esempio seguente è più complesso e mostra un problema decisionale su un grafo sociale. L'obiettivo: decidere a quali persone di una rete inviare materiale marketing per massimizzare il profitto, sapendo che gli acquisti si diffondono per influenza tra amici.

run_DT(""" % Decisions ?::marketed(P) :- person(P). utility(buys(P), 8) :- person(P). utility(marketed(P), -2) :- person(P). % Probabilistic facts 0.2 :: buy_from_marketing(_). 0.3 :: buy_from_trust(_,_). % Background knowledge person(bernd). person(ingo). person(theo). person(angelika). trusts(X,Y) :- trusts_directed(X,Y). trusts(X,Y) :- trusts_directed(Y,X). trusts_directed(bernd,ingo). trusts_directed(ingo,theo). trusts_directed(theo,angelika). buys(X) :- marketed(X), buy_from_marketing(X). buys(X) :- trusts(X,Y), buy_from_trust(X,Y), buys(Y). """)
{'decisions: ': {marketed(bernd): 0, marketed(ingo): 1, marketed(theo): 1, marketed(angelika): 0}, 'score: ': 1.16, 'statistics: ': {'eval': 16}}
Questo esempio mostra la potenza delle regole ricorsive. La definizione di buys(X) crea una cascata di acquisti all'interno della rete. L'inferenza di DTProbLog è complessa perché deve gestire le dipendenze tra le decisioni (marketing su più persone) e le interazioni probabilistiche tra gli acquisti . Trovare la strategia ottimale (es. contattare martijn, angelika, theo, ingo) richiede di esplorare un enorme spazio di possibilità, che DTProbLog risolve con diagrammi di decisione . Inoltre, esiste una versione con tabling (:- problog_table buys/1.) per gestire i cicli nel grafo e velocizzare l'inferenza esatta .

Un esempio più avanzato mostra come DTProbLog possa modellare dilemmi morali con azioni incerte. Il problema del carrello ferroviario è un esperimento mentale di filosofia etica formulato nel 1967 dalla filosofa Philippa Ruth Foot. Esso pone di fronte a un dilemma morale in cui un carrello senza freni sta per investire cinque persone legate ai binari; l'osservatore può azionare una leva per deviarlo su un binario secondario, salvando i cinque ma uccidendo un'altra persona sul secondario.
Nell'esempio consideriamo una variante del problema del carrello: Caio per fermare un treno in corsa e salvare così le cinque persone legate ai binari deve decidere se spingere un uomo sopra un interruttore o restare a guardare. L'esito dipende da eventi probabilistici (es. l'uomo cade sopra l'interruttore, sopravvive) e ha conseguenze con diverse utilità (vite salvate, vendetta).

run_DT(r""" % ----------------------------------------------------------------- % 1. FATTI PROBABILISTICI (INCERTEZZA DEL MONDO) % ----------------------------------------------------------------- 0.7::on_switch. % Probabilità che l'uomo cada sull'interruttore 0.8::man_dies. % Probabilità che, spinto, l'uomo muoia 0.1::revenge. % Probabilità che i suoi familiari si vendichino % ----------------------------------------------------------------- % 2. DECISIONI (mutualmente esclusive) % ----------------------------------------------------------------- ?::watch. % Stare a guardare ?::shove. % Spingere :- watch, shove. % Non possono essere entrambe vere :-\+ watch, \+ shove. % ma almeno una si % ----------------------------------------------------------------- % 3. REGOLE PER GLI ESITI % ----------------------------------------------------------------- % Cinque persone salvate se Caio spinge l'uomo che cade sull'interruttore % (indipendentemente dal fatto che l'uomo muoia o sopravviva) five_saved :- shove, on_switch. % L'uomo sopravvive se Caio non spinge o spinge e l'uomo non muore man_survives :- not(shove). man_survives :- shove, \+ man_dies. % Vendetta se l'uomo non sopravvive e la famiglia si vendica revenge_occurs :- \+ man_survives, revenge. % ----------------------------------------------------------------- % 4. UTILITÀ (PREFERENZE DI CAIO) % ----------------------------------------------------------------- utility(five_saved, 500). % +500 per aver salvato 5 persone utility(man_dies, -100). % -100 se Caio causa la morte utility(man_survives, 100). % +100 se l'uomo sopravvive utility(revenge_occurs, -500). % -500 per la vendetta utility(watch, 1). % stando a guardare non si corrono rischi utility(shove, -20). % -20 costo psicologico dello spingere """)
{'decisions: ': {shove: 1, watch: 0}, 'score: ': 230.0, 'statistics: ': {'eval': 4}}
La complessità sta nel combinare più eventi probabilistici interconnessi per calcolare l'utilità attesa di azioni molto diverse (es. guardare vs. spingere). L'inferenza valuta come le probabilità di diversi esiti influenzano la scelta finale, mostrando che un agente razionale può scegliere di "non agire" se il rischio di conseguenze negative (es. vendetta) è troppo alto.