Buckets:
| """Numerical verification of the theory claims (1-3) on Taxi-v3 / CliffWalking. | |
| Claim 1 (Lemma 2): |R - Rhat| <= 2*(extrinsic + intrinsic). | |
| Claim 2 (Theorem 1): extrinsic <= Ls*H*W1(p0d,p0) + H^2*Ls*(Lp+1)*W1(pd,p). | |
| Claim 3 (Theorem 2): intrinsic <= L_PI*H*sqrt(2 log|A|+2) + sum Rademacher + 3H^2*sqrt(log(4H/d)/(2T)). | |
| We use the L1 (one-hot) metric on discrete states: d(s,s') = ||e_s - e_{s'}||_1 = 2*1[s!=s']. | |
| Under this metric, W1 between two distributions over states equals ||p - q||_1. | |
| """ | |
| import numpy as np | |
| from scipy.stats import wasserstein_distance | |
| import sys, os | |
| sys.path.insert(0, "/root/Rationality") | |
| from src.env.taxi import build_experiment_finite_horizon as build_taxi, get_base_P as taxi_P | |
| from src.utils.utils import finite_horizon_optimal_Qh, stochastic_policy_from_Qh, greedy_policy_from_Qh | |
| # ----------------------------------------------------------- | |
| # Build a Taxi experiment with a clear training->deployment shift | |
| # training: eps_train slip; deployment: eps_infer slip (different) | |
| # initial-state shift: alpha_d0 mixes d0_train and d0_hard | |
| # ----------------------------------------------------------- | |
| np.random.seed(0) | |
| eps_train, eps_infer, alpha_d0, H = 0.25, 0.0, 0.5, 200 | |
| exp = build_taxi(eps_infer=eps_infer, eps_train=eps_train, alpha_d0=alpha_d0, | |
| max_steps_per_ep=H, seed=0, tau_star=1e-6) | |
| nS, nA = exp.nS, exp.nA | |
| Q_inf = exp.Q_star_infer_h # Q^{*,dagger}_h (deployment optimal) | |
| Q_tr = exp.Q_star_train_h # Q^*_h (training optimal) | |
| P_inf = exp.P_infer # p^dagger | |
| P_tr = exp.P_train # p | |
| d0_inf = exp.d0_inf # p0^dagger | |
| d0_tr = exp.d0_train # p0 | |
| pi_star = exp.pi_star_h # softmax optimal policy in deployment | |
| print(f"Taxi: nS={nS} nA={nA} H={H} eps_train={eps_train} eps_infer={eps_infer} alpha_d0={alpha_d0}") | |
| print(f" d0 shift W1 = {np.abs(d0_inf-d0_tr).sum():.4f}") | |
| # ---- helpers: one-hot L1 metric; W1 = ||p-q||_1 on this metric ---- | |
| def w1_states(p, q): | |
| return float(np.abs(p - q).sum()) | |
| def w1_kernels(P_a, P_b): | |
| """sup_{s,a} W1(p^a(.|s,a), p^b(.|s,a)) under one-hot L1 metric.""" | |
| m = 0.0 | |
| for s in range(nS): | |
| for a in range(nA): | |
| pa = np.zeros(nS); pb = np.zeros(nS) | |
| for (p, s2, r, done) in P_a[s][a]: pa[s2] += p | |
| for (p, s2, r, done) in P_b[s][a]: pb[s2] += p | |
| m = max(m, w1_states(pa, pb)) | |
| return m | |
| W1_d0 = w1_states(d0_inf, d0_tr) | |
| W1_p = w1_kernels(P_inf, P_tr) | |
| print(f" W1(p0dagger,p0) = {W1_d0:.4f}") | |
| print(f" W1(pdagger,p) = {W1_p:.4f}") | |
| # ---- Lipschitz constant Ls of value function (one-hot L1 metric) ---- | |
| # value function V_h(s) = max_a Q_h(s,a). d(s,s')=2 for s!=s'. | |
| # Ls = sup_{s!=s'} |V(s)-V(s')| / 2 | |
| def lipschitz_V(Qh): | |
| Ls = 0.0 | |
| for h in range(Qh.shape[0]): | |
| V = Qh[h].max(axis=1) | |
| rng = V.max() - V.min() | |
| # sup over distinct states is at most range / 2; compute exactly | |
| for s in range(nS): | |
| for s2 in range(s+1, nS): | |
| Ls = max(Ls, abs(V[s]-V[s2]) / 2.0) | |
| return Ls | |
| Ls = lipschitz_V(Q_inf) | |
| print(f" Ls (Lipschitz of value, one-hot L1) = {Ls:.4f}") | |
| # ---- Lipschitz constant Lp: transition-kernel -> induced state distribution ---- | |
| # Assumption 3: W1(D^{pi,dagger}_{h+1}, D^pi_{h+1}) <= W1(p0d,p0) + h*Lp*W1(pd,p) | |
| # Estimate Lp by simulating a fixed policy's state distribution divergence growth. | |
| def induced_state_dist(P, d0, policy_h, nS, nA, H): | |
| """D^pi_h for a time-indexed stochastic policy (H, nS, nA).""" | |
| D = np.zeros((H+1, nS)) | |
| D[0] = d0 / d0.sum() | |
| for h in range(H): | |
| # next-state distribution | |
| nxt = np.zeros(nS) | |
| for s in range(nS): | |
| for a in range(nA): | |
| pa = np.zeros(nS) | |
| for (p, s2, r, done) in P[s][a]: | |
| pa[s2] += p | |
| nxt += D[h, s] * policy_h[h, s, a] * pa | |
| D[h+1] = nxt | |
| return D | |
| # use a fixed greedy policy (greedy on deployment Q) for both kernels | |
| pi_greedy = np.zeros((H, nS, nA)) | |
| for h in range(H): | |
| g = greedy_policy_from_Qh(Q_inf[h][None])[0] | |
| for s in range(nS): | |
| pi_greedy[h, s, g[s]] = 1.0 | |
| D_inf = induced_state_dist(P_inf, d0_inf, pi_greedy, nS, nA, H) | |
| D_tr = induced_state_dist(P_tr, d0_tr, pi_greedy, nS, nA, H) | |
| divs = np.array([w1_states(D_inf[h], D_tr[h]) for h in range(H+1)]) | |
| # estimate Lp from per-step growth: div_{h+1} <= div_h + Lp*W1_p (roughly, after d0 term) | |
| # growth rate = (div_{h+1} - div_h)/W1_p | |
| if W1_p > 1e-12: | |
| growths = (divs[1:] - divs[:-1]) / W1_p | |
| Lp = float(np.abs(growths).max()) | |
| else: | |
| Lp = 0.0 | |
| print(f" Lp (empirical per-step divergence growth / W1_p) = {Lp:.4f}") | |
| # ----------------------------------------------------------- | |
| # Claim 1: Lemma 2 decomposition | |
| # R(pi) = sum_h E_{D^{pi,dagger}_h} [Q^dagger(s, a_circ) - Q^dagger(s, a_pi)] | |
| # Rhat(pi) = (1/T) sum_t sum_h [Q(s_h^t, a_circ) - Q(s_h^t, a_pi)] | |
| # We use the optimal policy as pi (a_circ = a_pi = optimal => both zero), so instead | |
| # use a *sub-optimal* fixed policy a_pi to get nonzero quantities and check the bound. | |
| # ----------------------------------------------------------- | |
| # choose a_pi as a uniformly-random-but-fixed action per state (same across h) | |
| rng = np.random.default_rng(1) | |
| a_pi = rng.integers(0, nA, size=nS) # fixed suboptimal action per state | |
| a_circ = greedy_policy_from_Qh(Q_inf)[0] # perfectly-rational action per state (greedy deployment) | |
| # R(pi): expected rational value risk in deployment | |
| R = 0.0 | |
| for h in range(H): | |
| g = Q_inf[h, np.arange(nS), a_circ] - Q_inf[h, np.arange(nS), a_pi] | |
| R += float(D_inf[h] @ g) | |
| # Extrinsic component (per Lemma 2 inner term): | |
| # sup_pi | E_{D^pi,dagger_h} Q^dagger(s,a_pi) - E_{D^pi_h} Q(s,a_pi) | | |
| # We evaluate for our fixed a_pi: | |
| extrinsic = 0.0 | |
| for h in range(H): | |
| ed = float(D_inf[h] @ Q_inf[h, np.arange(nS), a_pi]) | |
| et = float(D_tr[h] @ Q_tr[h, np.arange(nS), a_pi]) | |
| extrinsic += abs(ed - et) | |
| # Intrinsic component: | E_{D^pi_h} Q(s,a_pi) - (1/T) sum_t Q(s_h^t, a_pi) | | |
| # Simulate T training episodes and average. | |
| T = 200 | |
| def sample_episode(P, d0, H, seed): | |
| r = np.random.default_rng(seed) | |
| s = int(r.choice(nS, p=d0/d0.sum())) | |
| states = [s] | |
| for h in range(H): | |
| a = int(a_pi[s]) | |
| trans = P[s][a] | |
| ps = np.array([t[0] for t in trans]); ps = ps/ps.sum() | |
| idx = int(r.choice(len(trans), p=ps)) | |
| _, s2, rew, done = trans[idx] | |
| s = int(s2) | |
| states.append(s) | |
| if done: break | |
| return states | |
| emp_terms = [] | |
| for t in range(T): | |
| st = sample_episode(P_tr, d0_tr, H, 1000+t) | |
| acc = 0.0 | |
| for h in range(min(H, len(st)-1)): | |
| s = st[h] | |
| acc += float(Q_tr[h, s, a_circ[s]] - Q_tr[h, s, a_pi[s]]) | |
| emp_terms.append(acc) | |
| Rhat = float(np.mean(emp_terms)) # empirical rational value risk (avg over T episodes) | |
| # intrinsic = | E_{D^pi_h} Q(s,a_pi) - (1/T)sum_t Q(s_h^t, a_pi) | with a_circ vs a_pi | |
| # (matching Definition: expected uses a_circ-a_pi; empirical uses a_circ-a_pi) | |
| exp_train_risk = 0.0 | |
| for h in range(H): | |
| exp_train_risk += float(D_tr[h] @ (Q_tr[h, np.arange(nS), a_circ] - Q_tr[h, np.arange(nS), a_pi])) | |
| intrinsic = abs(exp_train_risk - Rhat) | |
| gap = abs(R - Rhat) | |
| print("\n=== Claim 1: Lemma 2 decomposition ===") | |
| print(f" R(pi) [deployment expected rational risk] = {R:.4f}") | |
| print(f" Rhat(pi) [empirical rational risk, T={T}] = {Rhat:.4f}") | |
| print(f" |R - Rhat| = {gap:.4f}") | |
| print(f" 2*(extrinsic + intrinsic) = {2*(extrinsic+intrinsic):.4f}") | |
| print(f" extrinsic (ERG) = {extrinsic:.4f}") | |
| print(f" intrinsic (IRG) = {intrinsic:.4f}") | |
| print(f" Lemma 2 holds: {gap <= 2*(extrinsic+intrinsic) + 1e-9}") | |
| # ----------------------------------------------------------- | |
| # Claim 2: Theorem 1 extrinsic bound | |
| # extrinsic <= Ls*H*W1(p0d,p0) + H^2*Ls*(Lp+1)*W1(pd,p) | |
| # (Note: Theorem 1 sums over h with (H-h) factor; the closed form is as above.) | |
| # ----------------------------------------------------------- | |
| bound1 = Ls*H*W1_d0 + (H**2)*Ls*(Lp+1)*W1_p | |
| print("\n=== Claim 2: Theorem 1 extrinsic bound ===") | |
| print(f" extrinsic ERG = {extrinsic:.4f}") | |
| print(f" Ls*H*W1(p0d,p0) = {Ls*H*W1_d0:.4f}") | |
| print(f" H^2*Ls*(Lp+1)*W1(pd,p) = {(H**2)*Ls*(Lp+1)*W1_p:.4f}") | |
| print(f" Theorem 1 bound = {bound1:.4f}") | |
| print(f" Theorem 1 holds: {extrinsic <= bound1 + 1e-9}") | |
| # ----------------------------------------------------------- | |
| # Claim 3: Theorem 2 intrinsic bound | |
| # intrinsic <= L_PI*H*sqrt(2 log|A|+2) + sum_h Rhat_h(QPi) + 3H^2*sqrt(log(4H/d)/(2T)) | |
| # We estimate empirical Rademacher complexity of the value-function class Q_Pi. | |
| # Q_Pi: class of Q_h(s,a) functions realized by the DQN MLP. We approximate it with | |
| # the empirical Rademacher complexity of a linear function class over one-hot states | |
| # (a tractable proxy that captures scale), plus check the concentration term directly. | |
| # ----------------------------------------------------------- | |
| delta = 0.05 | |
| # Concentration term | |
| concentration = 3 * (H**2) * np.sqrt(np.log(4*H/delta) / (2*T)) | |
| # Empirical Rademacher complexity of value class: estimate via the realized Q^*_h(s,a_pi) | |
| # values on the sampled training states (Monte Carlo with Rademacher signs). | |
| rademacher_sum = 0.0 | |
| for h in range(H): | |
| # gather state h across episodes | |
| sh = [] | |
| for t in range(T): | |
| st = sample_episode(P_tr, d0_tr, H, 1000+t) | |
| if h < len(st)-1: sh.append(st[h]) | |
| sh = np.array(sh) | |
| vals = Q_tr[h, sh, a_pi[sh]] # f(s) = Q_h(s, a_pi(s)) for our fixed policy | |
| # empirical Rademacher complexity: E_sigma sup |(1/T) sum sigma_t f(s_t)| | |
| # for a singleton-ish class, this ~ std of vals / sqrt(T) | |
| nR = 50 | |
| rs = [] | |
| for _ in range(nR): | |
| sigma = np.sign(np.random.default_rng().standard_normal(len(sh))) | |
| rs.append(abs((sigma*vals).mean())) | |
| rademacher_sum += np.mean(rs) | |
| L_PI = Ls # proxy: Lipschitz of policy->state distribution mapping ~ value Lipschitz | |
| policy_term = L_PI * H * np.sqrt(2*np.log(nA) + 2) | |
| bound2 = policy_term + rademacher_sum + concentration | |
| print("\n=== Claim 3: Theorem 2 intrinsic bound ===") | |
| print(f" intrinsic IRG = {intrinsic:.4f}") | |
| print(f" L_PI*H*sqrt(2 log|A|+2) = {policy_term:.4f}") | |
| print(f" sum Rademacher (proxy) = {rademacher_sum:.4f}") | |
| print(f" 3H^2*sqrt(log(4H/d)/(2T)) = {concentration:.4f} (T={T}, delta={delta})") | |
| print(f" Theorem 2 bound = {bound2:.4f}") | |
| print(f" Theorem 2 holds (proxy): {intrinsic <= bound2 + 1e-6}") | |
| # ---- Show the 1/sqrt(T) concentration decay by varying T ---- | |
| print("\n Concentration term 3H^2*sqrt(log(4H/d)/(2T)) vs T:") | |
| for Tt in [50, 100, 200, 500, 1000, 2000]: | |
| print(f" T={Tt:5d}: {3*H**2*np.sqrt(np.log(4*H/delta)/(2*Tt)):.4f}") | |
Xet Storage Details
- Size:
- 10.7 kB
- Xet hash:
- a1b3a75724f2db748c23f80ed027223b8f8202b621cf9f072bdad64e569c16be
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.