Active inference in dettaglio

Active inference in dettaglio

Condividi con i tuoi amici...

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.

OSSERVAZIONI STATI NASCOSTI / CREDENZE osservazione reale oₜ osservazione prevista q̂(oₜ₊₁) osservazione reale oₜ₊₁ posterior corrente q(sₜ | o₁:ₜ) prior predittiva q⁻(sₜ₊₁) nuova posterior q(sₜ₊₁ | o₁:ₜ₊₁) A likelihood + prior → Bayes B(aₜ) A predizione A likelihood + q⁻ → Bayes correzione previsto ↔ reale D = P(s₀): prior iniziale
Schema a due livelli. In alto compaiono i dati osservabili; in basso le distribuzioni di credenza sugli stati latenti. \(A\) collega verticalmente stati e osservazioni; \(B^{(a_t)}\) propaga orizzontalmente le credenze sugli stati nel tempo.

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,

\[ \boxed{P(s_0)=D,} \]

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:

\[ \boxed{ q(s_0\mid o_0) = \frac{P(o_0\mid s_0)D(s_0)} {\sum_s P(o_0\mid s)D(s)}. } \]

Ai tempi successivi non si riparte da \(D\). La posterior corrente viene propagata nel futuro attraverso \(B\), generando la prior predittiva:

\[ \boxed{ q^{-}(s_{t+1}) = \sum_{s_t} P(s_{t+1}\mid s_t,a_t)\, q(s_t\mid o_{1:t}). } \]

Quando arriva la nuova osservazione reale \(o_{t+1}\), questa prior viene corretta:

\[ \boxed{ q(s_{t+1}\mid o_{1:t+1},a_t) \propto P(o_{t+1}\mid s_{t+1})q^{-}(s_{t+1}). } \]

1.2 La matrice \(A\): dagli stati alle osservazioni e dalle osservazioni all’inferenza

La matrice di likelihood è:

\[ \boxed{A_{ij}=P(o=i\mid s=j).} \]

La sua direzione generativa naturale va dallo stato all’osservazione. Per una distribuzione sugli stati:

\[ \boxed{\hat{\mathbf o}_{t+1}=A\mathbf q^{-}_{t+1}.} \]

Quando invece arriva un dato reale, \(A\) viene utilizzata come likelihood insieme alla prior per applicare Bayes. Non si “inverte \(A\)” algebricamente:

\[ \boxed{ P(s\mid o) = \frac{P(o\mid s)P(s)} {\sum_{s’}P(o\mid s’)P(s’)}. } \]

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:

\[ \boxed{ B^{(a)}_{kj} = P(s_{t+1}=k\mid s_t=j,a_t=a). } \]

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:

\[ \boxed{\mathbf q^{-}_{t+1}=B^{(a_t)}\mathbf q_t.} \]

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:

\[ \boxed{ \hat q(o_{t+1}) = A B^{(a_t)}q(s_t). } \]

\(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:

\[ \varepsilon_o = \mathbf o_{t+1}-\hat{\mathbf o}_{t+1}. \]

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àSignificatoOrigine
\(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:

\[ \boxed{ q(s_t\mid o_{1:t}) \xrightarrow{B^{(a_t)}} q^{-}(s_{t+1}) \xrightarrow{A} \hat q(o_{t+1}), } \] \[ \boxed{ q^{-}(s_{t+1})+o_{t+1} \xrightarrow{A,\;\mathrm{Bayes}} q(s_{t+1}\mid o_{1:t+1}). } \]

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:

\[ p(o_t,s_t\mid s_{t-1},a_{t-1}) = p(o_t\mid s_t)\, p(s_t\mid s_{t-1},a_{t-1}). \]

Le due distribuzioni sono codificate da:

\[ A_{ij}=P(o_t=i\mid s_t=j) \] \[ B^{(a)}_{kj}=P(s_{t+1}=k\mid s_t=j,a_t=a). \]

Le colonne di \(A\) e di ogni \(B^{(a)}\) sono distribuzioni di probabilità e quindi sommano a uno.

Interpretazione. \(A\) risponde alla domanda: “se il mondo è nello stato \(s\), che cosa dovrei osservare?”. \(B\) risponde invece: “se credo di essere nello stato \(s_t\) e compio l’azione \(a_t\), in quale stato potrei trovarmi al passo successivo?”.

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:

\[ q(s_t\mid o_t) \propto p(o_t\mid s_t)\,q^{-}(s_t). \]

Se \(o_t\) è una categoria osservata realmente, la forma componente per componente è:

\[ q_{t,j} = \frac{ A_{o_t,j}\,q^{-}_{t,j} }{ \sum_\ell A_{o_t,\ell}\,q^{-}_{t,\ell} }. \]

Questa è l’inferenza percettiva: l’osservazione non dice direttamente quale sia lo stato; modifica la probabilità assegnata ai diversi stati.

Esempio. Supponiamo due stati, “calmo” e “pericoloso”, e due osservazioni, “segnale buono” e “segnale cattivo”. Se \[ A= \begin{pmatrix} 0.9 & 0.2\\ 0.1 & 0.8 \end{pmatrix} \] e la prior è \(q^{-}(s_t)=(0.5,0.5)^T\), osservando “segnale buono” si ottiene: \[ q(s_t\mid o_t=\text{buono}) \propto (0.9,0.2)^T\odot(0.5,0.5)^T =(0.45,0.10)^T. \] Dopo normalizzazione: \[ q(s_t)\approx(0.818,0.182)^T. \] L’agente non “sa” che il mondo è calmo: crede che sia calmo con probabilità circa \(81.8\%\).

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 è:

\[ \boxed{ q^{-}(s_{t+1}\mid a_t) = B^{(a_t)}q(s_t). } \]

Questa è ancora una distribuzione sugli stati nascosti. Per sapere quali osservazioni si aspetta di ricevere, l’agente applica \(A\):

\[ \boxed{ q(o_{t+1}\mid a_t) = A\,q^{-}(s_{t+1}\mid a_t). } \]

Quindi la previsione sensoriale complessiva è:

\[ \boxed{ \hat{\mathbf o}_{t+1} = A B^{(a_t)}\mathbf q_t. } \]

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:

\[ \varepsilon_o = \mathbf o_{t+1}-\hat{\mathbf o}_{t+1}. \]

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:

\[ q(s_{t+1}\mid o_{t+1}) \propto p(o_{t+1}\mid s_{t+1})q^{-}(s_{t+1}). \]

La discrepanza tra previsto e osservato si manifesta quindi come uno spostamento:

\[ q^{-}(s_{t+1}) \longrightarrow q(s_{t+1}\mid o_{t+1}). \]

È 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\):

\[ A_{\cdot j}\sim\mathrm{Dir}(a_{\cdot j}). \]

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\) è:

\[ \boxed{ A_{ij} = \mathbb E[A_{ij}] = \frac{a_{ij}}{\sum_r a_{rj}}. } \]
Se per un certo stato abbiamo conteggi \[ a_{\cdot j}= \begin{pmatrix} 8\\2 \end{pmatrix}, \] allora \[ A_{\cdot j}= \begin{pmatrix} 0.8\\0.2 \end{pmatrix}. \] Se invece i conteggi fossero \[ a_{\cdot j}= \begin{pmatrix} 800\\200 \end{pmatrix}, \] la probabilità media sarebbe ancora \(0.8/0.2\), ma l’evidenza complessiva sarebbe molto maggiore. La Dirichlet distingue quindi “stessa probabilità stimata” da “stessa certezza”.

Lo stesso vale per le transizioni:

\[ B^{(a)}_{\cdot j}\sim\mathrm{Dir}(b^{(a)}_{\cdot j}), \qquad B^{(a)}_{kj} = \frac{b^{(a)}_{kj}} {\sum_r b^{(a)}_{rj}}. \]

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 è:

\[ \boxed{ a^{new} = a^{old} + \eta_A\, \mathbf o_{t+1}\mathbf q_{t+1}^{T}. } \]

Il prodotto esterno distribuisce l’evidenza in modo frazionario sugli stati possibili. Se l’osservazione reale è:

\[ \mathbf o= \begin{pmatrix}1\\0\end{pmatrix} \]

e la posterior è:

\[ \mathbf q(s)= \begin{pmatrix}0.9\\0.1\end{pmatrix}, \]

allora:

\[ \mathbf o\mathbf q^T = \begin{pmatrix} 0.9&0.1\\ 0&0 \end{pmatrix}. \]

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:

\[ \xi_{kj,t} = q(s_{t+1}=k,s_t=j\mid o_{1:t+1},a_t). \]

Per un singolo passo, usando il modello corrente:

\[ \boxed{ \xi_{kj,t} \propto p(o_{t+1}\mid s_{t+1}=k)\, B^{(a_t)}_{kj}\, q(s_t=j). } \]

Dopo normalizzazione su tutte le coppie \((k,j)\), l’aggiornamento è:

\[ \boxed{ b^{(a_t),new}_{kj} = b^{(a_t),old}_{kj} + \eta_B\,\xi_{kj,t}. } \]

In una semplificazione mean-field si può approssimare:

\[ \xi_{kj,t}\approx q(s_{t+1}=k)\,q(s_t=j), \]

da cui:

\[ b^{(a_t),new} \approx b^{(a_t),old} + \eta_B\, \mathbf q_{t+1}\mathbf q_t^T. \]

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

ProcessoChe cosa cambiaScala 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, è:

\[ \boxed{ G(\pi) = D_{\mathrm{KL}}\!\left( q(o_{t+1}\mid\pi) \,\|\,P_{\mathrm{pref}}(o_{t+1}) \right) + \mathbb E_{q(s_{t+1}\mid\pi)} \left[ H\!\left(P(o_{t+1}\mid s_{t+1})\right) \right]. } \]

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:

\[ P_{\mathrm{pref}}(o) = \mathrm{softmax}(C), \]

dove \(C\) assegna maggiore valore agli esiti desiderati. La distribuzione sulle policy può essere:

\[ \boxed{ q(\pi) = \mathrm{softmax}(-\gamma G(\pi)), } \]

con \(\gamma\) che controlla la precisione della scelta: valori elevati rendono più probabile la policy con \(G\) minore.

11. Il ciclo completo

q(sₜ) posterior corrente B(a) dinamica q⁻(sₜ₊₁|a) stato previsto A q⁻ esito previsto G(a) valuta l’azione aₜ azione eseguita oₜ₊₁ dato reale inferenza nuova q(sₜ₊₁) a ← a + o qᵀ apprendimento di A b ← b + ξ apprendimento di B

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:

\[ A_{ij}=\frac{a_{ij}}{\sum_r a_{rj}}, \qquad B^{(a)}_{kj}=\frac{b^{(a)}_{kj}}{\sum_r b^{(a)}_{rj}}. \]

13.3 predict(): lo schema iniziale

Il metodo implementa esattamente:

\[ q^{-}(s_{t+1}|a)=B^{(a)}q(s_t), \] \[ q(o_{t+1}|a)=Aq^{-}(s_{t+1}|a). \]

È 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:

\[ D_{KL}\left(q(o|a)\|P_{\mathrm{pref}}(o)\right) \]

favorisce azioni che rendono probabili osservazioni desiderate. Il termine di ambiguità:

\[ \mathbb E_{q(s|a)}[H(P(o|s))] \]

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:

\[ q(a)=\mathrm{softmax}(-\gamma G(a)). \]

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:

\[ \xi_{kj} \propto A_{o_{t+1},k} B^{(a_t)}_{kj} q_t(j). \]

Questa distribuzione congiunta incorpora contemporaneamente il modello sensoriale, la dinamica prevista e la credenza precedente. Sommando sulle colonne:

\[ q(s_{t+1}=k)=\sum_j\xi_{kj}. \]

13.7 learn(): aggiornamento di \(A\) e \(B\)

Per \(A\):

\[ a\leftarrow a+\eta_A\,o_{t+1}q_{t+1}^T. \]

Per \(B\):

\[ b^{(a_t)} \leftarrow b^{(a_t)}+\eta_B\,\xi_t. \]

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:

\[ q(s_t)= \begin{pmatrix}0.8\\0.2\end{pmatrix}, \qquad B^{(a_t)}= \begin{pmatrix} 0.9&0.3\\ 0.1&0.7 \end{pmatrix}. \]

La previsione dello stato è:

\[ q^{-}(s_{t+1}) = B^{(a_t)}q(s_t) = \begin{pmatrix} 0.78\\ 0.22 \end{pmatrix}. \]

Con:

\[ A= \begin{pmatrix} 0.9&0.2\\ 0.1&0.8 \end{pmatrix}, \]

la previsione osservativa è:

\[ \hat o_{t+1} = Aq^{-}(s_{t+1}) = \begin{pmatrix} 0.746\\ 0.254 \end{pmatrix}. \]

L’agente prevede quindi il primo esito con probabilità \(74.6\%\). Supponiamo però che arrivi realmente il secondo esito:

\[ o_{t+1}= \begin{pmatrix}0\\1\end{pmatrix}. \]

La posterior diventa:

\[ q(s_{t+1}) \propto \begin{pmatrix} 0.1\\0.8 \end{pmatrix} \odot \begin{pmatrix} 0.78\\0.22 \end{pmatrix} = \begin{pmatrix} 0.078\\0.176 \end{pmatrix}, \]

quindi:

\[ q(s_{t+1}) \approx \begin{pmatrix} 0.307\\ 0.693 \end{pmatrix}. \]

L’osservazione inattesa ha spostato fortemente la credenza verso il secondo stato. A quel punto, per apprendere \(A\):

\[ \Delta a = o_{t+1}q(s_{t+1})^T = \begin{pmatrix} 0&0\\ 0.307&0.693 \end{pmatrix}. \]

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.

\[ \boxed{ \text{inferire} \rightarrow \text{prevedere} \rightarrow \text{valutare} \rightarrow \text{agire} \rightarrow \text{osservare} \rightarrow \text{aggiornare} \rightarrow \text{apprendere}. } \]

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.

Nota metodologica. Il programma è volutamente minimale e didattico. Le implementazioni complete di Active Inference possono includere più fattori di stato, più modalità osservative, policy multi-step, prior sulle policy, precisioni apprese, aggiornamenti variationali iterativi, learning rates o forgetting, e decomposizioni più sofisticate dell’Expected Free Energy. Il modello qui presentato conserva però la struttura matematica essenziale necessaria per vedere nello stesso ciclo inferenza, previsione, selezione dell’azione e apprendimento bayesiano di \(A\) e \(B\).

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