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,
- le decisioni si dichiarano con
? :: decisione. - le regole si dichiarano come
? :: decisione :- condizioni. - le utilità si dichiarano con
utility(atomo, valore).
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.