Active inference in dettaglio
L’Active Inference può essere compresa come un unico ciclo: l’agente mantiene credenze su stati del mondo non direttamente osservabili, usa un modello generativo per prevedere come tali stati evolveranno e quali osservazioni produrranno, confronta le osservazioni reali con quelle attese, aggiorna le proprie credenze e, su una scala più lenta, apprende anche i parametri del modello stesso. In un modello discreto, le due matrici centrali sono \(A\), che collega stati e osservazioni, e \(B\), che descrive le transizioni tra stati condizionate dall’azione.
1. Lo schema fondamentale: osservazioni sopra, stati nascosti sotto
Per rappresentare correttamente l’Active Inference conviene distinguere fin dall’inizio due livelli. Il livello superiore contiene le osservazioni \(o_t\): sono i dati realmente accessibili all’agente. Il livello inferiore contiene invece gli stati nascosti \(s_t\): non sono osservati direttamente, ma sono oggetto di inferenza probabilistica. Per questo nello schema non scriviamo semplicemente \(s_t\), bensì la credenza \(q(s_t)\) dell’agente su quello stato.
1.1 La credenza sullo stato nasce da una prior e da un’osservazione
La credenza sullo stato non compare dal nulla. All’inizio l’agente dispone di una prior sugli stati iniziali,
dove \(D\) esprime ciò che ritiene plausibile prima dell’osservazione iniziale. Quando arriva \(o_0\), la matrice \(A\) fornisce la likelihood \(P(o_0\mid s_0)\) e Bayes produce:
Ai tempi successivi non si riparte da \(D\). La posterior corrente viene propagata nel futuro attraverso \(B\), generando la prior predittiva:
Quando arriva la nuova osservazione reale \(o_{t+1}\), questa prior viene corretta:
1.2 La matrice \(A\): dagli stati alle osservazioni e dalle osservazioni all’inferenza
La matrice di likelihood è:
La sua direzione generativa naturale va dallo stato all’osservazione. Per una distribuzione sugli stati:
Quando invece arriva un dato reale, \(A\) viene utilizzata come likelihood insieme alla prior per applicare Bayes. Non si “inverte \(A\)” algebricamente:
La stessa \(A\) ha dunque due ruoli complementari: generare una previsione sensoriale a partire da una credenza sugli stati e fornire la likelihood necessaria per correggere quella credenza quando arriva un’osservazione reale.
1.3 La matrice \(B\): dinamica e azione
\(B\) descrive come gli stati possono evolvere:
Ogni azione può avere una propria matrice \(B^{(a)}\). La matrice non trasforma uno stato certo in un altro stato certo, ma propaga una distribuzione di credenza:
La freccia orizzontale inferiore rappresenta quindi la domanda: “dato ciò che credo sul presente e data l’azione \(a_t\), quale distribuzione sugli stati futuri dovrei aspettarmi prima di ricevere nuovi dati?”.
1.4 Osservazione prevista e osservazione reale
L’osservazione prevista è una distribuzione:
\(o_{t+1}\) è invece il dato effettivamente prodotto dal mondo. Se l’osservazione è categorica, può essere rappresentata come vettore one-hot. Si può definire intuitivamente:
Nel modello discreto bayesiano canonico, però, questo errore non viene semplicemente sommato ad \(A\) o \(B\). L’osservazione modifica prima la posterior sullo stato; saranno poi le posterior sugli stati a fornire le statistiche sufficienti per l’apprendimento.
1.5 Le quantità che lo schema distingue
| Quantità | Significato | Origine |
|---|---|---|
| \(D=P(s_0)\) | Prior sugli stati iniziali | Credenza prima del primo dato |
| \(q(s_t\mid o_{1:t})\) | Posterior sullo stato corrente | Inferenza bayesiana |
| \(q^{-}(s_{t+1})\) | Prior predittiva sul prossimo stato | \(B^{(a_t)}q(s_t)\) |
| \(\hat q(o_{t+1})\) | Distribuzione dell’osservazione prevista | \(Aq^{-}(s_{t+1})\) |
| \(o_{t+1}\) | Osservazione realmente ricevuta | Ambiente/processo generativo reale |
Il ciclo può quindi essere riassunto come:
Questa architettura sarà la base del resto del saggio. Inferenza, previsione e apprendimento non sono processi separati arbitrariamente: la posterior inferita viene usata da \(B\) per prevedere; \(A\) trasforma gli stati previsti in esiti attesi; il nuovo dato corregge la posterior; infine l’esperienza accumulata modifica i parametri che definiscono \(A\) e \(B\).
2. Il modello generativo
L’agente non osserva direttamente lo stato nascosto \(s_t\). Riceve invece un’osservazione \(o_t\) generata probabilisticamente dallo stato. Un modello generativo discreto minimale può essere scritto come:
Le due distribuzioni sono codificate da:
Le colonne di \(A\) e di ogni \(B^{(a)}\) sono distribuzioni di probabilità e quindi sommano a uno.
3. Inferenza: ricostruire lo stato nascosto
Supponiamo che prima di osservare \(o_t\) l’agente abbia una prior \(q^{-}(s_t)\). Quando arriva l’osservazione reale \(o_t\), applica Bayes:
Se \(o_t\) è una categoria osservata realmente, la forma componente per componente è:
Questa è l’inferenza percettiva: l’osservazione non dice direttamente quale sia lo stato; modifica la probabilità assegnata ai diversi stati.
4. Previsione: propagare le credenze nel futuro
Dopo aver inferito \(q(s_t)\), l’agente considera una possibile azione \(a_t\). La previsione dello stato futuro è:
Questa è ancora una distribuzione sugli stati nascosti. Per sapere quali osservazioni si aspetta di ricevere, l’agente applica \(A\):
Quindi la previsione sensoriale complessiva è:
Questa formula riassume lo schema iniziale: la credenza corrente viene propagata da \(B\), poi trasformata in previsione osservativa da \(A\).
5. Osservazione prevista e osservazione reale
Quando arriva realmente \(o_{t+1}\), si può definire intuitivamente un errore di previsione:
Nel modello discreto bayesiano canonico, tuttavia, non si aggiorna semplicemente \(A\) o \(B\) aggiungendo questo errore. Il dato reale modifica prima di tutto la credenza sullo stato:
La discrepanza tra previsto e osservato si manifesta quindi come uno spostamento:
È questa credenza corretta che fornisce le statistiche necessarie per apprendere il modello.
6. Perché servono i parametri di Dirichlet
Se \(A\) fosse semplicemente una matrice di numeri, sapremmo quali probabilità l’agente sta usando, ma non quanto sia sicuro di esse. Per rappresentare anche la quantità di evidenza accumulata, si introduce una distribuzione di Dirichlet.
Per ogni colonna di \(A\):
I parametri \(a_{ij}\) possono essere interpretati come pseudo-conteggi: quanta evidenza è stata accumulata per l’associazione tra stato \(s_j\) e osservazione \(o_i\). Il valore medio della matrice \(A\) è:
Lo stesso vale per le transizioni:
7. Apprendimento di \(A\)
Supponiamo che al tempo \(t+1\) l’osservazione reale sia rappresentata da un vettore one-hot \(\mathbf o_{t+1}\), e che dopo l’inferenza la credenza sullo stato sia \(\mathbf q_{t+1}\). L’incremento dei conteggi è:
Il prodotto esterno distribuisce l’evidenza in modo frazionario sugli stati possibili. Se l’osservazione reale è:
e la posterior è:
allora:
L’agente aggiunge quindi \(0.9\) unità di evidenza alla coppia “osservazione 1 – stato 1” e \(0.1\) alla coppia “osservazione 1 – stato 2”. Non conta uno stato come certo quando lo stato è in realtà latente.
8. Apprendimento di \(B\)
Per \(B\) occorre stimare quale transizione sia avvenuta. La quantità più corretta è la posterior congiunta sui due stati consecutivi:
Per un singolo passo, usando il modello corrente:
Dopo normalizzazione su tutte le coppie \((k,j)\), l’aggiornamento è:
In una semplificazione mean-field si può approssimare:
da cui:
La versione con \(\xi\) è però preferibile perché conserva la dipendenza tra stato precedente e stato successivo.
9. Inferenza e apprendimento non sono la stessa cosa
| Processo | Che cosa cambia | Scala concettuale |
|---|---|---|
| Inferenza | \(q(s_t)\): credenza sullo stato nascosto corrente | Rapida, a ogni osservazione |
| Previsione | \(q^{-}(s_{t+1})\), \(q(o_{t+1})\) | In avanti, sotto azioni o policy |
| Apprendimento | \(a,b\), quindi \(A,B\) | Accumulo di esperienza nel tempo |
Si può quindi pensare a due cicli annidati. Il ciclo interno usa \(A\) e \(B\) per inferire e prevedere; il ciclo esterno aggiorna i parametri \(a\) e \(b\), che a loro volta modificano lentamente \(A\) e \(B\).
10. Dove entra l’azione: preferenze ed Expected Free Energy
Fin qui abbiamo descritto un filtro bayesiano capace di apprendere. L’Active Inference aggiunge un principio per scegliere le azioni. L’agente considera diverse azioni o policy \(\pi\), predice gli esiti futuri e preferisce le policy con minore Expected Free Energy \(G(\pi)\).
Una forma utile e leggibile, per un singolo passo futuro, è:
Il primo termine misura quanto gli esiti previsti si discostino dalle preferenze dell’agente. Il secondo penalizza stati che produrrebbero osservazioni molto ambigue. In questa forma, minimizzare \(G\) combina comportamento pragmatico e ricerca di osservazioni informative.
Le preferenze possono essere rappresentate da una distribuzione:
dove \(C\) assegna maggiore valore agli esiti desiderati. La distribuzione sulle policy può essere:
con \(\gamma\) che controlla la precisione della scelta: valori elevati rendono più probabile la policy con \(G\) minore.
11. Il ciclo completo
Il ciclo può essere letto così: inferisci lo stato corrente; per ogni azione candidata predici lo stato futuro con \(B\) e l’osservazione futura con \(A\); valuta gli esiti con \(G\); scegli un’azione; ricevi un’osservazione reale; aggiorni la posterior sullo stato; infine accumuli evidenza nei parametri Dirichlet \(a\) e \(b\).
12. Un modello Python minimale ma completo
Il seguente programma implementa un piccolo ambiente con due stati nascosti
(calmo, pericoloso), due osservazioni
(buono, cattivo) e due azioni
(attendi, intervieni). L’agente mantiene distribuzioni di
Dirichlet per \(A\) e \(B\), esegue inferenza bayesiana sugli stati, predice gli esiti,
sceglie l’azione minimizzando una forma semplice di Expected Free Energy e aggiorna
\(a\) e \(b\) dopo ogni nuova osservazione.
import numpy as np
rng = np.random.default_rng(7)
# ------------------------------------------------------------
# Utility matematiche
# ------------------------------------------------------------
def normalize_columns(M):
"""Normalizza ogni colonna come distribuzione di probabilità."""
return M / M.sum(axis=0, keepdims=True)
def softmax(x):
z = x - np.max(x)
e = np.exp(z)
return e / e.sum()
def categorical_sample(p):
return rng.choice(len(p), p=p)
def entropy(p, eps=1e-16):
p = np.clip(p, eps, 1.0)
return -np.sum(p * np.log(p))
def kl_div(q, p, eps=1e-16):
q = np.clip(q, eps, 1.0)
p = np.clip(p, eps, 1.0)
return np.sum(q * (np.log(q) - np.log(p)))
# ------------------------------------------------------------
# "Mondo vero": l'agente non conosce direttamente queste matrici
# ------------------------------------------------------------
# A_true[o, s] = P(o | s)
A_true = np.array([
[0.90, 0.20], # osservazione "buono"
[0.10, 0.80], # osservazione "cattivo"
])
# B_true[a][s_next, s_current] = P(s_next | s_current, a)
B_true = np.array([
# azione 0 = attendi
[[0.90, 0.20],
[0.10, 0.80]],
# azione 1 = intervieni
[[0.85, 0.70],
[0.15, 0.30]],
])
STATE_NAMES = ["calmo", "pericoloso"]
OBS_NAMES = ["buono", "cattivo"]
ACTION_NAMES = ["attendi", "intervieni"]
class Environment:
def __init__(self, initial_state=1):
self.state = initial_state
def observe(self):
return categorical_sample(A_true[:, self.state])
def step(self, action):
self.state = categorical_sample(B_true[action][:, self.state])
observation = self.observe()
return observation
# ------------------------------------------------------------
# Agente Active Inference
# ------------------------------------------------------------
class ActiveInferenceAgent:
def __init__(self, eta_A=1.0, eta_B=1.0, precision=6.0):
# Parametri Dirichlet di A.
# Questi sono pseudo-conteggi, non probabilità.
self.a = np.array([
[4.0, 1.5],
[1.5, 4.0],
])
# Parametri Dirichlet di B per ciascuna azione.
# Il prior incorpora una conoscenza iniziale debole.
self.b = np.array([
[[4.0, 1.5],
[1.5, 4.0]],
[[4.0, 3.0],
[1.5, 2.0]],
])
self.eta_A = eta_A
self.eta_B = eta_B
self.precision = precision
# Preferenza per osservazioni: "buono" è desiderato.
self.pref_o = np.array([0.95, 0.05])
# Prior iniziale sugli stati.
self.q_s = np.array([0.5, 0.5])
self.refresh_model()
def refresh_model(self):
"""Converte i conteggi Dirichlet nelle matrici probabilistiche A e B."""
self.A = normalize_columns(self.a)
self.B = np.array([normalize_columns(self.b[a])
for a in range(len(self.b))])
def infer_initial_state(self, observation):
"""
q(s_0 | o_0) ∝ P(o_0 | s_0) q^-(s_0)
"""
likelihood = self.A[observation, :]
posterior = likelihood * self.q_s
posterior /= posterior.sum()
self.q_s = posterior
return posterior
def predict(self, action):
"""
q^-(s_{t+1}|a) = B^a q(s_t)
q(o_{t+1}|a) = A q^-(s_{t+1}|a)
"""
q_s_next = self.B[action] @ self.q_s
q_o_next = self.A @ q_s_next
return q_s_next, q_o_next
def expected_free_energy(self, action):
"""
G(a) = risk + ambiguity
risk:
KL(q(o|a) || P_pref(o))
ambiguity:
E_q(s|a)[ H(P(o|s)) ]
"""
q_s_next, q_o_next = self.predict(action)
risk = kl_div(q_o_next, self.pref_o)
state_ambiguities = np.array([
entropy(self.A[:, s]) for s in range(self.A.shape[1])
])
ambiguity = np.dot(q_s_next, state_ambiguities)
G = risk + ambiguity
return G
def choose_action(self):
"""
q(a) = softmax(-precision * G(a))
"""
G = np.array([
self.expected_free_energy(a)
for a in range(len(ACTION_NAMES))
])
q_action = softmax(-self.precision * G)
action = categorical_sample(q_action)
return action, G, q_action
def infer_transition(self, observation, action, q_prev):
"""
Posterior congiunta:
xi[k,j] ∝
P(o_{t+1}|s_{t+1}=k)
P(s_{t+1}=k|s_t=j,a_t)
q(s_t=j)
Le righe sono s_{t+1}; le colonne sono s_t.
"""
likelihood_next = self.A[observation, :] # indice k
xi = (
likelihood_next[:, None]
* self.B[action]
* q_prev[None, :]
)
xi /= xi.sum()
# Marginalizzando s_t otteniamo q(s_{t+1})
q_next = xi.sum(axis=1)
return q_next, xi
def learn(self, observation, action, q_prev, q_next, xi):
"""
Apprendimento bayesiano tramite pseudo-conteggi Dirichlet.
A:
a <- a + eta_A * o q_next^T
B:
b[action] <- b[action] + eta_B * xi
"""
o_onehot = np.zeros(self.A.shape[0])
o_onehot[observation] = 1.0
# Aggiornamento dei conteggi di A
self.a += self.eta_A * np.outer(o_onehot, q_next)
# Aggiornamento dei conteggi della sola B usata
self.b[action] += self.eta_B * xi
# Nuove stime probabilistiche
self.refresh_model()
def update_after_observation(self, observation, action):
q_prev = self.q_s.copy()
# Inferenza sul nuovo stato prima dell'apprendimento
q_next, xi = self.infer_transition(
observation=observation,
action=action,
q_prev=q_prev,
)
# Apprendimento di A e B
self.learn(
observation=observation,
action=action,
q_prev=q_prev,
q_next=q_next,
xi=xi,
)
self.q_s = q_next
return q_next, xi
# ------------------------------------------------------------
# Simulazione
# ------------------------------------------------------------
env = Environment(initial_state=1)
agent = ActiveInferenceAgent()
# Prima osservazione
o0 = env.observe()
q0 = agent.infer_initial_state(o0)
print("Osservazione iniziale:", OBS_NAMES[o0])
print("Credenza iniziale q(s):", np.round(q0, 3))
print()
for t in range(20):
# 1. Valuta le azioni mediante Expected Free Energy
action, G, q_action = agent.choose_action()
# 2. Il mondo evolve e genera una nuova osservazione
observation = env.step(action)
# 3. L'agente inferisce il nuovo stato e apprende A e B
q_next, xi = agent.update_after_observation(
observation=observation,
action=action,
)
print(
f"t={t:02d} | "
f"azione={ACTION_NAMES[action]:10s} | "
f"obs={OBS_NAMES[observation]:7s} | "
f"q(s)={np.round(q_next, 3)} | "
f"G={np.round(G, 3)} | "
f"q(a)={np.round(q_action, 3)}"
)
print("\nA appresa:")
print(np.round(agent.A, 3))
print("\nB appresa per 'attendi':")
print(np.round(agent.B[0], 3))
print("\nB appresa per 'intervieni':")
print(np.round(agent.B[1], 3))
13. Relazione tra i blocchi del programma e la teoria
13.1 A_true e B_true: il mondo
Queste matrici appartengono all’ambiente, non all’agente. Servono a generare i dati. L’agente non le consulta direttamente; deve inferire stati e apprendere il proprio modello. Questo separa il “processo generativo reale” dal “modello generativo interno”.
13.2 a e b: memoria dell’esperienza
Le variabili self.a e self.b sono i parametri di Dirichlet.
Non sono probabilità normalizzate, ma pseudo-conteggi. La funzione
refresh_model() normalizza le colonne e produce le stime correnti:
13.3 predict(): lo schema iniziale
Il metodo implementa esattamente:
È la parte puramente predittiva: nessun parametro viene ancora modificato.
13.4 expected_free_energy(): valutare il futuro
Per ogni azione, il programma calcola una previsione sugli esiti e la confronta con le preferenze. Il termine di rischio:
favorisce azioni che rendono probabili osservazioni desiderate. Il termine di ambiguità:
penalizza stati che producono osservazioni poco diagnostiche. L’azione non viene quindi scelta in base a un reward esterno nel senso classico del reinforcement learning, ma in base a credenze predittive e preferenze codificate nel modello.
13.5 choose_action(): dalla free energy alla scelta
Il programma trasforma i valori \(G(a)\) in una distribuzione:
L’azione con Expected Free Energy minore è più probabile, ma la scelta rimane stocastica.
13.6 infer_transition(): inferenza dopo il dato reale
Una volta osservato \(o_{t+1}\), il programma costruisce:
Questa distribuzione congiunta incorpora contemporaneamente il modello sensoriale, la dinamica prevista e la credenza precedente. Sommando sulle colonne:
13.7 learn(): aggiornamento di \(A\) e \(B\)
Per \(A\):
Per \(B\):
Quindi l’osservazione reale non modifica direttamente una singola cella con una regola “errore previsto-reale”. Prima modifica le credenze sugli stati; poi queste credenze determinano quanto credito assegnare alle diverse associazioni stato-osservazione e alle diverse transizioni.
14. Un esempio numerico completo di un singolo passo
Supponiamo:
La previsione dello stato è:
Con:
la previsione osservativa è:
L’agente prevede quindi il primo esito con probabilità \(74.6\%\). Supponiamo però che arrivi realmente il secondo esito:
La posterior diventa:
quindi:
L’osservazione inattesa ha spostato fortemente la credenza verso il secondo stato. A quel punto, per apprendere \(A\):
L’agente accumula dunque soprattutto evidenza che il secondo stato generi la seconda osservazione. Per \(B\), invece, utilizza la posterior congiunta \(\xi\), che assegna credito alle possibili transizioni compatibili con stato precedente, dinamica e nuova osservazione.
15. Che cosa significa, in sintesi, “agire secondo Active Inference”
Un agente di Active Inference non esegue una catena del tipo “stimolo → risposta”. Mantiene una distribuzione di credenza sugli stati nascosti, simula in avanti le conseguenze delle azioni attraverso \(B\), trasforma tali conseguenze in osservazioni previste attraverso \(A\), valuta queste osservazioni rispetto alle proprie preferenze e alla loro informatività, sceglie un’azione, osserva ciò che accade realmente e usa il nuovo dato per correggere sia le credenze sullo stato sia, nel tempo, il modello stesso.
Il punto concettuale più importante è che previsione, inferenza e apprendimento sono distinti ma strettamente collegati. \(A\) e \(B\) vengono usate per prevedere; la differenza tra ciò che era plausibile e ciò che viene osservato modifica \(q(s)\); le posterior sugli stati forniscono poi le statistiche sufficienti con cui aggiornare i parametri di Dirichlet \(a\) e \(b\); questi, normalizzati, producono nuove \(A\) e \(B\), che influenzeranno le previsioni e le azioni future.
Documento in formato HTML con formule MathJax incorporate.
Ponte matematico tra Reinforcement Learning e Active Inference
Un modo particolarmente utile per mettere in relazione Reinforcement Learning e Active Inference consiste nel concentrarsi sul ruolo della ricompensa. Nel Reinforcement Learning l’agente apprende attraverso reward e punishment; nell’Active Inference, invece, le azioni vengono valutate rispetto a una distribuzione di preferenze sugli outcome, indicata con \(P_{\mathrm{pref}}(o)\). Il ponte matematico consiste nel fare in modo che questa distribuzione non sia fissata una volta per tutte, ma venga appresa dall’esperienza di ricompensa.
1. Dalla reward alla distribuzione di preferenza
Supponiamo che ogni osservazione o outcome \(o\) sia associato a una ricompensa \(r(o)\). Possiamo convertire queste ricompense in una distribuzione di probabilità preferenziale mediante una softmax:
\[ \boxed{ P_{\mathrm{pref}}(o) = \frac{\exp(\beta r(o))} {\sum_{o’}\exp(\beta r(o’))} } \]
equivalentemente:
\[ \boxed{ P_{\mathrm{pref}}(o) = \operatorname{softmax}(\beta r(o)) } \]
e quindi:
\[ \ln P_{\mathrm{pref}}(o) = \beta r(o)-\ln Z. \]
A meno della costante di normalizzazione \(\ln Z\), la reward è quindi proporzionale al logaritmo della probabilità preferenziale:
\[ \boxed{ r(o) \propto \ln P_{\mathrm{pref}}(o) } \]
Questa relazione è il punto centrale del ponte fra i due formalismi: ciò che nel Reinforcement Learning compare come reward può essere rappresentato, nell’Active Inference, come log-preferenza.
2. Perché introdurre una variabile C
Nel programma precedente la distribuzione preferenziale era definita direttamente, per esempio:
self.pref_o = np.array([0.95, 0.05])
Questo significa che l’agente parte già sapendo quali outcome preferire. Nel nuovo programma introduciamo invece un vettore di valori:
\[ \boxed{ C(o) } \]
che rappresenta un punteggio appreso di desiderabilità. La distribuzione preferenziale diventa:
\[ \boxed{ P_{\mathrm{pref}}(o) = \operatorname{softmax}(\beta_C C(o)) } \]
Inizialmente poniamo:
\[ C= \begin{pmatrix} 0\\ 0 \end{pmatrix} \]
e quindi:
\[ P_{\mathrm{pref}}(o) = \begin{pmatrix} 0.5\\ 0.5 \end{pmatrix}. \]
L’agente non possiede dunque preferenze iniziali forti: dovrà apprenderle attraverso l’esperienza.
3. Apprendere C mediante reward e punishment
La nuova regola di apprendimento è:
\[ \boxed{ C_{t+1}(o_t) = C_t(o_t)+\eta_C r_t } \]
dove \(\eta_C\) è il learning rate delle preferenze. Se la reward è positiva, il valore di \(C\) associato all’outcome osservato aumenta; se è negativa, diminuisce.
Quindi:
\[ r_t>0 \quad\Rightarrow\quad C(o_t)\uparrow \quad\Rightarrow\quad P_{\mathrm{pref}}(o_t)\uparrow, \]
mentre:
\[ r_t<0 \quad\Rightarrow\quad C(o_t)\downarrow \quad\Rightarrow\quad P_{\mathrm{pref}}(o_t)\downarrow. \]
Il sistema costruisce quindi progressivamente una mappa delle conseguenze desiderabili e indesiderabili.
4. Esempio numerico
Supponiamo:
\[ C= \begin{pmatrix} 0\\ 0 \end{pmatrix}, \qquad \beta_C=2. \]
All’inizio:
\[ P_{\mathrm{pref}} = \begin{pmatrix} 0.5\\ 0.5 \end{pmatrix}. \]
Se viene osservato l’outcome “cattivo” e l’ambiente assegna:
\[ r=-1, \]
con:
\[ \eta_C=0.2, \]
il nuovo valore diventa:
\[ C= \begin{pmatrix} 0\\ -0.2 \end{pmatrix}. \]
Applicando la softmax:
\[ P_{\mathrm{pref}} \approx \begin{pmatrix} 0.599\\ 0.401 \end{pmatrix}. \]
L’outcome penalizzato diventa quindi meno preferito rispetto all’altro.
5. Come entra P_pref nell’Expected Free Energy
Nel nostro modello, per ogni azione \(a\), l’agente predice una distribuzione sugli outcome:
\[ q(o_{t+1}\mid a). \]
Il termine pragmatico dell’Expected Free Energy è:
\[ \boxed{ \mathrm{Risk}(a) = D_{KL} \left[ q(o_{t+1}\mid a) \Vert P_{\mathrm{pref}}(o) \right] } \]
Nel nuovo programma \(P_{\mathrm{pref}}(o)\) non è più costante: cambia man mano che l’agente riceve reward e punishment.
L’Expected Free Energy usata nel programma è:
\[ \boxed{ G(a) = D_{KL} \left[ q(o\mid a) \Vert P_{\mathrm{pref}}(o) \right] + \mathbb E_{q(s\mid a)} \left[ H(P(o\mid s)) \right] } \]
Il primo termine collega direttamente Active Inference e Reinforcement Learning. Il secondo conserva la componente epistemica: l’agente non valuta solo quanto siano desiderabili gli outcome attesi, ma anche quanto siano ambigui o informativi gli stati raggiungibili.
6. Le tre forme di apprendimento
Con questa modifica, il modello possiede tre memorie adattive distinte:
\[ \boxed{ a \rightarrow A } \]
che apprende come gli stati producono osservazioni;
\[ \boxed{ b \rightarrow B } \]
che apprende come gli stati cambiano in funzione dell’azione;
e:
\[ \boxed{ C \rightarrow P_{\mathrm{pref}} } \]
che apprende quali outcome risultano preferibili sulla base delle ricompense ricevute.
In forma compatta:
\[ \boxed{ \begin{aligned} o_t,q(s_t) &\rightarrow a \rightarrow A,\\ q(s_t,s_{t+1}) &\rightarrow b \rightarrow B,\\ r_t &\rightarrow C \rightarrow P_{\mathrm{pref}}. \end{aligned} } \]
7. Il nuovo ciclo agente-ambiente
Il ciclo completo diventa:
\[ q(s_t) \xrightarrow{B} q(s_{t+1}\mid a) \xrightarrow{A} q(o_{t+1}\mid a) \xrightarrow{P_{\mathrm{pref}}} G(a) \rightarrow a_t. \]
Dopo che l’azione viene eseguita:
\[ a_t \rightarrow (o_{t+1},r_{t+1}). \]
L’osservazione aggiorna la credenza sugli stati e il modello:
\[ o_{t+1} \rightarrow q(s_{t+1}) \rightarrow A,B. \]
La reward aggiorna invece le preferenze:
\[ \boxed{ r_{t+1} \rightarrow C \rightarrow P_{\mathrm{pref}}(o). } \]
Il risultato è un modello ibrido in cui la struttura descrittiva del mondo viene appresa in modo bayesiano, mentre il valore degli outcome viene appreso tramite rinforzo.
8. Le nuove funzioni del programma
La prima modifica riguarda l’ambiente. Oltre all’osservazione, ora restituisce anche una reward:
def reward_from_observation(self, observation):
reward_table = np.array([+1.0, -1.0])
return reward_table[observation]
def step(self, action):
self.state = categorical_sample(B_true[action][:, self.state])
observation = self.observe()
reward = self.reward_from_observation(observation)
return observation, reward
La seconda modifica è l’introduzione di \(C\):
self.C = np.zeros(len(OBS_NAMES))
self.eta_C = eta_C
self.beta_C = beta_C
self.pref_o = softmax(self.beta_C * self.C)
La funzione:
def refresh_preferences(self):
self.pref_o = softmax(self.beta_C * self.C)
implementa direttamente:
\[ P_{\mathrm{pref}}(o) = \operatorname{softmax}(\beta_C C). \]
La nuova regola di apprendimento è:
def learn_preferences(self, observation, reward):
self.C[observation] += self.eta_C * reward
self.refresh_preferences()
cioè:
\[ C_{t+1}(o_t) = C_t(o_t)+\eta_Cr_t. \]
Infine, nella funzione che aggiorna l’agente dopo il nuovo dato, si aggiunge:
self.learn_preferences(
observation=observation,
reward=reward,
)
La sequenza complessiva è quindi:
\[ \boxed{ \text{inferenza} \rightarrow \text{apprendimento di }A,B \rightarrow \text{apprendimento di }C \rightarrow \text{nuove preferenze}. } \]
9. Il programma Python completo
import numpy as np
rng = np.random.default_rng(7)
def normalize_columns(M):
return M / M.sum(axis=0, keepdims=True)
def softmax(x):
z = x - np.max(x)
e = np.exp(z)
return e / e.sum()
def categorical_sample(p):
return rng.choice(len(p), p=p)
def entropy(p, eps=1e-16):
p = np.clip(p, eps, 1.0)
return -np.sum(p * np.log(p))
def kl_div(q, p, eps=1e-16):
q = np.clip(q, eps, 1.0)
p = np.clip(p, eps, 1.0)
return np.sum(q * (np.log(q) - np.log(p)))
# ============================================================
# Processo generativo esterno: il mondo vero
# ============================================================
# A_true[o, s] = P(o | s)
A_true = np.array([
[0.90, 0.20], # osservazione 0 = buono
[0.10, 0.80], # osservazione 1 = cattivo
])
# B_true[a][s_next, s_current] = P(s_next | s_current, a)
B_true = np.array([
# azione 0 = attendi
[[0.90, 0.20],
[0.10, 0.80]],
# azione 1 = intervieni
[[0.85, 0.70],
[0.15, 0.30]],
])
STATE_NAMES = ["calmo", "pericoloso"]
OBS_NAMES = ["buono", "cattivo"]
ACTION_NAMES = ["attendi", "intervieni"]
class Environment:
def __init__(self, initial_state=1):
self.state = initial_state
def observe(self):
return categorical_sample(A_true[:, self.state])
def reward_from_observation(self, observation):
"""NOVITÀ: reward esterna associata all'esito osservato."""
reward_table = np.array([+1.0, -1.0])
return reward_table[observation]
def step(self, action):
self.state = categorical_sample(B_true[action][:, self.state])
observation = self.observe()
reward = self.reward_from_observation(observation) # NOVITÀ
return observation, reward # NOVITÀ
# ============================================================
# Agente Active Inference con preferenze apprese
# ============================================================
class ActiveInferenceAgent:
def __init__(
self,
eta_A=1.0,
eta_B=1.0,
eta_C=0.20, # NOVITÀ: learning rate delle preferenze
beta_C=2.0, # NOVITÀ: forza della trasformazione C -> P_pref
precision=6.0,
):
# Parametri Dirichlet per A
self.a = np.array([
[4.0, 1.5],
[1.5, 4.0],
])
# Parametri Dirichlet per B
self.b = np.array([
[[4.0, 1.5],
[1.5, 4.0]],
[[4.0, 3.0],
[1.5, 2.0]],
])
self.eta_A = eta_A
self.eta_B = eta_B
self.precision = precision
# -------------------- NOVITÀ --------------------
# C contiene punteggi di preferenza appresi.
# Inizialmente: nessuna preferenza particolare.
self.C = np.zeros(len(OBS_NAMES))
self.eta_C = eta_C
self.beta_C = beta_C
self.pref_o = softmax(self.beta_C * self.C)
# ------------------------------------------------
# Prior iniziale sugli stati
self.q_s = np.array([0.5, 0.5])
self.refresh_model()
def refresh_model(self):
self.A = normalize_columns(self.a)
self.B = np.array([
normalize_columns(self.b[a])
for a in range(len(self.b))
])
# -------------------- NOVITÀ --------------------
def refresh_preferences(self):
"""
P_pref(o) = softmax(beta_C * C)
"""
self.pref_o = softmax(self.beta_C * self.C)
def learn_preferences(self, observation, reward):
"""
C(o_t) <- C(o_t) + eta_C * reward_t
reward > 0 -> l'esito diventa più preferito
reward < 0 -> l'esito diventa meno preferito
"""
self.C[observation] += self.eta_C * reward
self.refresh_preferences()
# ------------------------------------------------
def infer_initial_state(self, observation):
likelihood = self.A[observation, :]
posterior = likelihood * self.q_s
posterior /= posterior.sum()
self.q_s = posterior
return posterior
def predict(self, action):
"""
q^-(s_{t+1}|a) = B^a q(s_t)
q(o_{t+1}|a) = A q^-(s_{t+1}|a)
"""
q_s_next = self.B[action] @ self.q_s
q_o_next = self.A @ q_s_next
return q_s_next, q_o_next
def expected_free_energy(self, action):
"""
G(a) = risk + ambiguity
risk = KL(q(o|a) || P_pref(o))
ambiguity = E_q(s|a)[H(P(o|s))]
NOVITÀ: P_pref(o) è appresa da reward tramite C.
"""
q_s_next, q_o_next = self.predict(action)
risk = kl_div(q_o_next, self.pref_o)
state_ambiguities = np.array([
entropy(self.A[:, s])
for s in range(self.A.shape[1])
])
ambiguity = np.dot(q_s_next, state_ambiguities)
return risk + ambiguity
def choose_action(self):
G = np.array([
self.expected_free_energy(a)
for a in range(len(ACTION_NAMES))
])
q_action = softmax(-self.precision * G)
action = categorical_sample(q_action)
return action, G, q_action
def infer_transition(self, observation, action, q_prev):
"""
xi[k,j] ∝ P(o_{t+1}|s_{t+1}=k)
P(s_{t+1}=k|s_t=j,a_t)
q(s_t=j)
"""
likelihood_next = self.A[observation, :]
xi = (
likelihood_next[:, None]
* self.B[action]
* q_prev[None, :]
)
xi /= xi.sum()
q_next = xi.sum(axis=1)
return q_next, xi
def learn_model(self, observation, action, q_next, xi):
"""
a <- a + eta_A * o q_next^T
b[action] <- b[action] + eta_B * xi
"""
o_onehot = np.zeros(self.A.shape[0])
o_onehot[observation] = 1.0
self.a += self.eta_A * np.outer(o_onehot, q_next)
self.b[action] += self.eta_B * xi
self.refresh_model()
def update_after_observation(self, observation, reward, action):
"""
1. inferenza sul nuovo stato
2. apprendimento di A e B
3. NOVITÀ: reward -> C -> P_pref(o)
"""
q_prev = self.q_s.copy()
q_next, xi = self.infer_transition(
observation=observation,
action=action,
q_prev=q_prev,
)
self.learn_model(
observation=observation,
action=action,
q_next=q_next,
xi=xi,
)
# NOVITÀ
self.learn_preferences(
observation=observation,
reward=reward,
)
self.q_s = q_next
return q_next, xi
# ============================================================
# Simulazione
# ============================================================
env = Environment(initial_state=1)
agent = ActiveInferenceAgent(
eta_A=1.0,
eta_B=1.0,
eta_C=0.20,
beta_C=2.0,
precision=6.0,
)
# Prima osservazione
o0 = env.observe()
q0 = agent.infer_initial_state(o0)
print("Osservazione iniziale:", OBS_NAMES[o0])
print("Credenza iniziale q(s):", np.round(q0, 3))
print("Preferenze iniziali P_pref(o):", np.round(agent.pref_o, 3))
print()
for t in range(30):
# 1. L'agente valuta le azioni con G(a)
action, G, q_action = agent.choose_action()
# 2. Il mondo restituisce osservazione + reward
observation, reward = env.step(action) # MODIFICATO
pref_before = agent.pref_o.copy()
# 3. Inferenza + apprendimento di A, B e C
q_next, xi = agent.update_after_observation(
observation=observation,
reward=reward, # NOVITÀ
action=action,
)
print(
f"t={t:02d} | "
f"azione={ACTION_NAMES[action]:10s} | "
f"obs={OBS_NAMES[observation]:7s} | "
f"reward={reward:+.1f} | "
f"q(s)={np.round(q_next, 3)} | "
f"G={np.round(G, 3)} | "
f"q(a)={np.round(q_action, 3)} | "
f"P_pref prima={np.round(pref_before, 3)} | "
f"P_pref dopo={np.round(agent.pref_o, 3)}"
)
print("\n================ RISULTATI FINALI ================")
print("\nC appreso:")
print(np.round(agent.C, 3))
print("\nP_pref(o) appresa:")
print(np.round(agent.pref_o, 3))
print("\nA appresa:")
print(np.round(agent.A, 3))
print("\nB appresa per 'attendi':")
print(np.round(agent.B[0], 3))
print("\nB appresa per 'intervieni':")
print(np.round(agent.B[1], 3))
10. Interpretazione finale
Questo programma non trasforma l’Active Inference in Reinforcement Learning. Piuttosto, costruisce un ponte fra i due formalismi. La reward viene usata per apprendere una distribuzione di preferenze, ma l’agente continua a scegliere le azioni tramite Expected Free Energy e continua a rappresentare l’incertezza sugli stati e sulla dinamica del mondo.
La struttura risultante è:
\[ \boxed{ \text{reward} \rightarrow C \rightarrow P_{\mathrm{pref}} \rightarrow G \rightarrow \text{azione} } \]
insieme a:
\[ \boxed{ \text{osservazioni} \rightarrow q(s) \rightarrow A,B } \]
Il primo canale apprende il valore; il secondo apprende il mondo. Il punto di incontro è l’Expected Free Energy, che combina preferenze apprese e struttura probabilistica del modello.
Va infine sottolineato che la regola \(C(o)\leftarrow C(o)+\eta_C r\) è una scelta intenzionalmente semplice e ibrida. Non rappresenta l’unico modo possibile di apprendere le preferenze nell’Active Inference. Una possibile estensione consiste nel sostituire la reward immediata con un errore di Temporal Difference:
\[ \delta_t = r_t+\gamma V(s_{t+1})-V(s_t), \]
così da collegare ulteriormente Active Inference, valore atteso e apprendimento di Bellman.
HumAI.it – Alvise Giubelli