Testo (dalle slide). Un robot si muove in una griglia con 4 colonne e 3 righe. Le celle sono indicate con (colonna, riga), partendo da (1,1) in basso a sinistra. La cella (2,2) è un muro. Le celle (4,3) e (4,2) sono terminali: entrare in (4,3) ha ricompensa +1, entrare in (4,2) ha ricompensa −1. Ogni altra transizione ha ricompensa r=−0,04. L'azione voluta (Su, Giù, Sinistra, Destra) riesce con probabilità 0,8; con probabilità 0,1 per parte il robot si muove perpendicolarmente. Un urto contro un muro o il bordo lascia il robot fermo. Fattore di sconto γ=1.
(a) Calcolare a mano U1 e U2 di value iteration partendo da U0=0. (b) Scrivere il programma, trovare le utilità finali e il numero di iterazioni. (c) Ricavare la politica ottima e verificare la scelta in (1,1). (d) Che cosa cambia con r=−2 e con r=−0,01?
Soluzione
Regole di calcolo. Per ogni stato non terminale s e azione a:
Qk(s,a)=s′∑P(s′∣s,a)[R(s,a,s′)+γUk(s′)],Uk+1(s)=amaxQk(s,a),
con ricompensa +1 o −1 quando s′ è un terminale (e Uk(terminale)=0: la ricompensa si incassa entrando, poi il gioco finisce) e r+Uk(s′) altrimenti. Un esito che urta un muro o il bordo ha s′=s.
(a) Iterazioni a mano
Iterazione 1 (U0=0): Q0(s,a) è solo la ricompensa attesa del passo. Quasi ovunque =r=−0,04. Fanno eccezione le celle da cui si può entrare in un terminale:
(3,3), Destra: 0,8⋅(+1)+0,1(r+0)+0,1(r+0)=0,8−0,008=0,792 (Su e Giù non entrano nel +1 con probabilità 0,8, la deviazione a destra sì ma con 0,1: valgono meno, per esempio Su: 0,1⋅1+0,9⋅r=0,064);
(3,2), Su: 0,8(r+0)+0,1(r+0)+0,1⋅(−1)=−0,04⋅0,9−0,1=−0,136; Giù: 0,8r+0,1r+0,1(−1) è uguale, e Sinistra (0,8 muro: resta; 0,1 su; 0,1 giù) vale −0,04. Il massimo è −0,04 (Sinistra, che non rischia il −1);
(4,1) e tutte le altre: −0,04.
Quindi U1(3,3)=0,792 e tutti gli altri stati non terminali valgono −0,04.
(3,2), Su: 0,8(r+U1(3,3))+0,1(r+U1(3,2)) (sinistra: muro) +0,1⋅(−1)=0,6016−0,008−0,1=0,4936;
tutte le altre celle hanno vicini non ancora "informati" (valgono −0,04) e la miglior azione non tocca i terminali: U2=−0,04+(−0,04)=−0,08.
col. 1
col. 2
col. 3
col. 4
riga 3, U1
−0,04
−0,04
0,792
terminale
riga 2, U1
−0,04
muro
−0,04
terminale
riga 1, U1
−0,04
−0,04
−0,04
−0,04
riga 3, U2
−0,08
0,5856
0,8672
terminale
riga 2, U2
−0,08
muro
0,4936
terminale
riga 1, U2
−0,08
−0,08
−0,08
−0,08
(b) Il programma
python
import numpy as np
MURI = {(2, 2)}
TERMINALI = {(4, 3): +1.0, (4, 2): -1.0} # ricompensa per l'ingresso
STATI = [(x, y) for y in (1, 2, 3) for x in (1, 2, 3, 4) if (x, y) notin MURI]
IDX = {s: i for i, s inenumerate(STATI)}
AZIONI = {"Su": (0, 1), "Giu": (0, -1), "Sx": (-1, 0), "Dx": (1, 0)}
LATERALI = {"Su": ("Sx", "Dx"), "Giu": ("Sx", "Dx"), "Sx": ("Su", "Giu"), "Dx": ("Su", "Giu")}
NOMI = list(AZIONI)
defpasso(s, a):
t = (s[0] + AZIONI[a][0], s[1] + AZIONI[a][1])
return s if (t in MURI ornot (1 <= t[0] <= 4and1 <= t[1] <= 3)) else t
defcostruisci_mdp(r):
n = len(STATI)
P = np.zeros((4, n, n)); R = np.zeros((n, 4)) # P[a,s,s'], R[s,a] = ricompensa attesafor ai, a inenumerate(NOMI):
for s in STATI:
if s in TERMINALI:
P[ai, IDX[s], IDX[s]] = 1# terminale: assorbente, nessuna ricompensacontinuefor b, p in ((a, .8), (LATERALI[a][0], .1), (LATERALI[a][1], .1)):
t = passo(s, b)
P[ai, IDX[s], IDX[t]] += p
R[IDX[s], ai] += p * TERMINALI.get(t, r) # +1 / -1 se entra in un terminale, altrimenti rreturn P, R
defstampa(V):
for y in (3, 2, 1):
print(" ".join(" muro "if (x, y) in MURI elsef"{V[IDX[(x, y)]]:7.4f}"for x in (1, 2, 3, 4)))
defvalue_iteration(P, R, gamma, tol=1e-10):
V = np.zeros(P.shape[1]); storia = [V]
whileTrue:
Q = R.T + gamma * P @ V
V_new = Q.max(axis=0); storia.append(V_new)
if np.abs(V_new - V).max() < tol:
return V_new, Q.argmax(axis=0), storia
V = V_new
P, R = costruisci_mdp(-0.04)
V, pol, storia = value_iteration(P, R, 1.0)
print("iterazioni:", len(storia) - 1)
for k in (1, 2):
print("U_%d" % k); stampa(storia[k])
Output atteso (i terminali compaiono con 0 nelle tabelle di Uk, perché la loro ricompensa è incassata nell'ingresso):
Gli stessi valori che si sono ricavati a mano. Dopo 40 iterazioni (tolleranza 10−10; con γ=1 la soglia ε(1−γ)/γ non si può usare) le utilità sono quelle delle slide:
col. 1
col. 2
col. 3
col. 4
riga 3
0,8516
0,9078
0,9578
+1
riga 2
0,8016
muro
0,7003
−1
riga 1
0,7453
0,6953
0,6514
0,4279
(c) Politica
La politica è π∗(s)=argmaxaQ(s,a). In (1,1), con U(1,2)=0,8016, U(2,1)=0,6953, U(1,1)=0,7453:
Su: 0,8⋅0,7616+0,1⋅0,6553+0,1⋅0,7053=0,7453;
Sinistra: 0,9⋅0,7053+0,1⋅0,7616=0,7109;
Giù: 0,9⋅0,7053+0,1⋅0,6553=0,7003;
Destra: 0,8⋅0,6553+0,1⋅0,7616+0,1⋅0,7053=0,6709.
Vince Su, e 0,7453=U(1,1) (equazione di Bellman soddisfatta). Politica completa:
col. 1
col. 2
col. 3
col. 4
riga 3
Dx
Dx
Dx
terminale
riga 2
Su
muro
Su
terminale
riga 1
Su
Sx
Sx
Sx
È una politica prudente: dalle celle in basso (2,1), (3,1), (4,1) si va a sinistra e si sale lungo la colonna 1 (il giro lungo), perché la strada breve passa da (3,2), che confina con il −1: una deviazione a destra costerebbe 0,1⋅(−1). Nel codice la politica si legge con NOMI[pol[IDX[(x, y)]]].
(d) Altri valori di r
Cambiando solo costruisci_mdp(r):
r=−2 (33 iterazioni): ogni passo costa moltissimo, conviene finire subito. Riga 3: Dx Dx Dx; riga 2: Su, muro, Dx; riga 1: Dx Dx Dx e Su in (4,1): dall'angolo in basso a destra si va dritti nel −1, perché restare in gioco costa più di −1. È la politica "sotto −1,6497" delle slide.
r=−0,01 (200 iterazioni: con un costo così basso l'informazione si stabilizza lentamente): il costo del passo è basso, si evita il −1 a ogni costo. Riga 3: Dx Dx Dx; riga 2: Su, muro, Sx; riga 1: Su, Sx, Sx e Giù in (4,1): in (3,2) si va a sinistra (urtando il muro) per non rischiare lo scivolamento in (4,2), e in (4,1) si urta il bordo inferiore (Giù). È la politica "tra −0,0274 e 0" delle slide.
Con r=−0,6 (intervallo −0,7311<r<−0,4526 delle slide) si ottiene la politica Dx Dx Dx / Su, muro, Su / Su, Dx, Su, Su, uguale a quella del corso.
Confermato per i tre valori con lo stesso programma.
Setup. Griglia 4×3, muro in (2,2), terminali (4,3) con +1 e (4,2) con −1, altrimenti r=−0,04; azione voluta con 0,8, laterali 0,1+0,1; urto = si resta; γ=1. Aggiornamento: Uk+1(s)=maxa∑s′P(s′∣s,a)[R+γUk(s′)], simultaneo, con U(terminale)=0 e ricompensa incassata all'ingresso.
Passaggi.
U1(3,3)=0,8⋅1+0,1(−0,04)+0,1(−0,04)=0,792; gli altri stati valgono −0,04.
U2(3,3)=0,8+0,1⋅0,752+0,1⋅(−0,08)=0,8672.
U2(2,3)=0,8⋅0,752+0,2⋅(−0,08)=0,5856.
U2(3,2)=0,8⋅0,752+0,1⋅(−0,08)+0,1⋅(−1)=0,4936.
Le altre celle: −0,08.
Dopo 40 iterazioni (tolleranza 10−10): riga 3 0,8516;0,9078;0,9578; riga 2 0,8016 e 0,7003; riga 1 0,7453;0,6953;0,6514;0,4279.
Politica. In (1,1): Su 0,7453 (massimo), Sinistra 0,7109, Giù 0,7003, Destra 0,6709. Politica completa: riga 3 Dx Dx Dx; riga 2 Su, muro, Su; riga 1 Su, Sx, Sx, Sx.
Altri r.r=−2: da (4,1) Su dritto nel −1, riga 1 Dx Dx Dx; r=−0,01: (3,2) Sx, (4,1) Giù, riga 1 Su Sx Sx.
Costruzione di P e R. Per ogni stato non terminale e azione a si considerano tre direzioni: a con 0,8 e le due laterali con 0,1; passo(s, b) restituisce la cella di arrivo (se muro o bordo, resta in s). P[a,s,s'] accumula le probabilità; R[s,a] accumula p⋅(±1) se l'arrivo è un terminale, altrimenti p⋅r. I terminali sono assorbenti (P[a,t,t]=1, ricompensa nulla). Stati: 11 non-muro, di cui 2 terminali.
Tabella finale.(1,3)0,8516, (2,3)0,9078, (3,3)0,9578, (1,2)0,8016, (3,2)0,7003, (1,1)0,7453, (2,1)0,6953, (3,1)0,6514, (4,1)0,4279. Con γ=1 non si può usare la soglia ε(1−γ)/γ: si usa una tolleranza sul massimo delle differenze (10−10).
Altri r:r=−2 (33 iterazioni) riga 1 Dx Dx Dx Su; r=−0,6 riga 2 Su, muro, Su e riga 1 Su, Dx, Su, Su; r=−0,01 (200 iterazioni) riga 2 Su, muro, Sx e riga 1 Su, Sx, Sx, Giù.
python
V, pol, storia = value_iteration(*costruisci_mdp(-0.04), 1.0)
Errori tipici: aggiornare in sequenza; contare un urto come "resta senza costo"; usare U(terminale)=±1e la ricompensa ±1 insieme (si conta due volte).