TD学習(Temporal Difference Learning)は、モンテカルロ法(MC法)と動的計画法(DP)の長所を組み合わせた、強化学習の中核的な手法です。MC法のようにモデルフリーでありながら、DPのように各ステップで即座に学習を更新できます。エピソードの終了を待たずに学習できるため、連続的なタスクや長いエピソードを持つ問題に特に有効です。
TD学習のキーアイデアはブートストラップです。MC法が実際のリターン $G_t$ を用いて価値関数を更新するのに対し、TD学習は現在の価値関数の推定値 $V(S_{t+1})$ を使って更新目標を構成します。これにより、1ステップの経験だけで即座に学習できるようになります。
本記事の内容
- TD(0)の更新則の導出(MCとDPのハイブリッド)
- TD誤差 $\delta_t$ の定義と意味
- MC法とTD法の比較(バイアス-分散トレードオフ)
- TD(0)の収束証明の概要(確率的近似理論)
- n-step TDとTD($\lambda$)
- 適格度トレース(eligibility traces)
- 前方ビューと後方ビューの等価性
- Pythonでランダムウォーク問題でのMC vs TD比較実験
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
TD(0)の更新則の導出
MC法の更新則の復習
MC法の方策評価では、エピソード完了後に得られた実際のリターン $G_t$ を用いて価値関数を更新します。
$$ V(S_t) \leftarrow V(S_t) + \alpha \left[ G_t – V(S_t) \right] $$
ここで $\alpha$ は学習率(ステップサイズ)です。これは $G_t$ を目標値とし、現在の推定値 $V(S_t)$ を目標に向かって少しだけ修正する更新です。
$G_t$ を展開すると
$$ G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \cdots = R_{t+1} + \gamma G_{t+1} $$
MC法は $G_t$ 全体、すなわちエピソード終了までの全ての報酬が確定するまで待つ必要があります。
DPの更新則の復習
DP(方策評価)の更新では、ベルマン期待方程式を直接用います。
$$ V(S_t) \leftarrow \sum_{a} \pi(a|S_t) \sum_{s’} P(s’|S_t, a) \left[ R(S_t, a, s’) + \gamma V(s’) \right] $$
DPは環境モデル $P(s’|s,a)$ と $R(s,a,s’)$ を必要とします。
TD(0)の導出
TD(0)はMCとDPのハイブリッドです。MC法の更新則において、実際のリターン $G_t$ の代わりに1ステップ先の推定リターンを使います。
$G_t$ を1ステップだけ展開すると
$$ G_t = R_{t+1} + \gamma G_{t+1} $$
ここで $G_{t+1}$ を $V(S_{t+1})$(現在の推定値)で置き換えます。
$$ G_t \approx R_{t+1} + \gamma V(S_{t+1}) $$
この近似値を TD目標 (TD target) と呼びます。
$$ \text{TD目標} = R_{t+1} + \gamma V(S_{t+1}) $$
MC法の更新則で $G_t$ をTD目標に置き換えると、TD(0)の更新則が得られます。
$$ \begin{equation} \boxed{V(S_t) \leftarrow V(S_t) + \alpha \left[ R_{t+1} + \gamma V(S_{t+1}) – V(S_t) \right]} \end{equation} $$
この更新は、$S_t$ で行動を取り、報酬 $R_{t+1}$ を受け取って $S_{t+1}$ に遷移した直後に実行できます。エピソードの終了を待つ必要はありません。
ブートストラップ
TD(0)の更新則で $V(S_{t+1})$ を使うことをブートストラップ(bootstrap)と呼びます。「自分自身の推定値を使って推定値を更新する」という意味です。
| 手法 | 更新目標 | ブートストラップ | モデルフリー |
|---|---|---|---|
| DP | $\sum_{s’} P(s’|s,a)[R + \gamma V(s’)]$ | する | しない |
| MC | $G_t$ | しない | する |
| TD | $R_{t+1} + \gamma V(S_{t+1})$ | する | する |
TD法はブートストラップを行いつつモデルフリーであるという、DPとMCの両方の利点を併せ持っています。
TD誤差
定義
TD誤差(temporal difference error)$\delta_t$ は次のように定義されます。
$$ \begin{equation} \delta_t = R_{t+1} + \gamma V(S_{t+1}) – V(S_t) \end{equation} $$
TD(0)の更新則は $\delta_t$ を用いて
$$ V(S_t) \leftarrow V(S_t) + \alpha \, \delta_t $$
と書けます。
TD誤差の意味
TD誤差は「現在の予測 $V(S_t)$ がどれだけ間違っていたか」を表す指標です。
- $\delta_t > 0$: 実際に得られた報酬 $R_{t+1}$ + 次の状態の価値 $\gamma V(S_{t+1})$ が予測 $V(S_t)$ より良い。値をプラス方向に修正。
- $\delta_t < 0$: 予測が楽観的すぎた。値をマイナス方向に修正。
- $\delta_t = 0$: 予測と一致。更新なし。
TD誤差とベルマン方程式の関係
$V = V^{\pi}$ のとき(真の価値関数のとき)、TD誤差の期待値は0になります。
$$ \begin{align} \mathbb{E}_{\pi}[\delta_t \mid S_t = s] &= \mathbb{E}_{\pi}[R_{t+1} + \gamma V^{\pi}(S_{t+1}) – V^{\pi}(S_t) \mid S_t = s] \\ &= \mathbb{E}_{\pi}[R_{t+1} + \gamma V^{\pi}(S_{t+1}) \mid S_t = s] – V^{\pi}(s) \end{align} $$
ベルマン期待方程式より
$$ V^{\pi}(s) = \mathbb{E}_{\pi}[R_{t+1} + \gamma V^{\pi}(S_{t+1}) \mid S_t = s] $$
なので
$$ \mathbb{E}_{\pi}[\delta_t \mid S_t = s] = V^{\pi}(s) – V^{\pi}(s) = 0 $$
つまり、TD誤差の期待値が0になることと、ベルマン方程式が満たされることは等価です。
MC法のリターンとTD誤差の関係
実際のリターン $G_t$ はTD誤差の総和として表現できます。これは重要な関係です。
$$ \begin{align} G_t &= R_{t+1} + \gamma G_{t+1} \\ &= R_{t+1} + \gamma V(S_{t+1}) + \gamma G_{t+1} – \gamma V(S_{t+1}) \\ &= \delta_t + V(S_t) + \gamma (G_{t+1} – V(S_{t+1})) \end{align} $$
再帰的に展開すると($V$ が更新されないと仮定して)
$$ \begin{align} G_t – V(S_t) &= \delta_t + \gamma (G_{t+1} – V(S_{t+1})) \\ &= \delta_t + \gamma \delta_{t+1} + \gamma^2 (G_{t+2} – V(S_{t+2})) \\ &= \delta_t + \gamma \delta_{t+1} + \gamma^2 \delta_{t+2} + \cdots \\ &= \sum_{k=0}^{T-t-1} \gamma^k \delta_{t+k} \end{align} $$
最後の行で終端状態では $G_T – V(S_T) = 0$ であることを用いました。
$$ \begin{equation} \boxed{G_t – V(S_t) = \sum_{k=0}^{T-t-1} \gamma^k \delta_{t+k}} \end{equation} $$
MC法の更新量 $G_t – V(S_t)$ は、TD誤差の割引和に等しいのです。
MC法とTD法の比較
バイアス-分散トレードオフ
MC法とTD法の根本的な違いは、更新目標のバイアスと分散のトレードオフにあります。
MC法の更新目標: $G_t$
$$ G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \cdots $$
- バイアス: 不偏($\mathbb{E}_{\pi}[G_t \mid S_t = s] = V^{\pi}(s)$)
- 分散: 高い(エピソード内の全ての確率的な遷移と報酬の影響を受ける)
TD(0)の更新目標: $R_{t+1} + \gamma V(S_{t+1})$
- バイアス: あり($V(S_{t+1})$ が真の価値でない限りバイアスを持つ)
- 分散: 低い(1ステップ先の報酬と遷移のみに依存)
これをまとめると
| 特性 | MC法 | TD(0) |
|---|---|---|
| バイアス | なし | あり(推定値 $V$ が正確でない限り) |
| 分散 | 高い(全ステップの変動) | 低い(1ステップの変動のみ) |
| 更新タイミング | エピソード完了後 | 各ステップごと |
| エピソード終了 | 必要 | 不要 |
| マルコフ性の利用 | しない | する(ブートストラップ経由) |
バイアスの定量的分析
TD目標のバイアスを定量的に分析しましょう。
$$ \begin{align} \mathbb{E}_{\pi}[R_{t+1} + \gamma V(S_{t+1}) \mid S_t = s] &= \sum_a \pi(a|s) \sum_{s’} P(s’|s,a) \left[ R(s,a,s’) + \gamma V(s’) \right] \end{align} $$
真の価値関数 $V = V^{\pi}$ の場合
$$ \mathbb{E}_{\pi}[R_{t+1} + \gamma V^{\pi}(S_{t+1}) \mid S_t = s] = V^{\pi}(s) \quad (\text{ベルマン方程式}) $$
しかし、$V \neq V^{\pi}$ の場合
$$ \mathbb{E}_{\pi}[R_{t+1} + \gamma V(S_{t+1}) \mid S_t = s] \neq V^{\pi}(s) $$
バイアスの大きさは $|V(s’) – V^{\pi}(s’)|$ に依存します。学習が進むにつれて $V \to V^{\pi}$ となるため、バイアスは漸近的に消失します。
マルコフ性との関係
TD法はマルコフ性を暗黙に利用しています。$V(S_{t+1})$ で将来のリターンを近似する(ブートストラップする)ことは、「$S_{t+1}$ から先の情報は $S_{t+1}$ の値に集約されている」というマルコフ性を前提としています。
環境が完全にマルコフ的であればTD法は効率的ですが、部分観測環境(状態がマルコフ性を満たさない場合)ではMC法の方が頑健な場合があります。
TD(0)の収束証明の概要
確率的近似理論(Robbins-Monro)
TD(0)の収束は、確率的近似理論(Stochastic Approximation Theory)によって保証されます。
Robbins-Monro定理の一般的な形は、以下の更新則
$$ \theta_{n+1} = \theta_n + \alpha_n [f(\theta_n) + w_n] $$
が $f(\theta^{*}) = 0$ の解 $\theta^{*}$ に収束するための条件を与えます。ここで $w_n$ はノイズ項です。
TD(0)への適用
TD(0)の更新則を状態 $s$ について書くと
$$ V_{n+1}(s) = V_n(s) + \alpha_n(s) \delta_t $$
ここで $\delta_t = R_{t+1} + \gamma V_n(S_{t+1}) – V_n(S_t)$ です。
期待値を取ると
$$ \mathbb{E}[\delta_t \mid S_t = s] = \sum_a \pi(a|s) \sum_{s’} P(s’|s,a) [R(s,a,s’) + \gamma V_n(s’)] – V_n(s) $$
これはベルマン作用素 $\mathcal{T}^{\pi}$ を用いて $(\mathcal{T}^{\pi} V_n)(s) – V_n(s)$ と書けます。
収束条件
以下の条件のもとで、TD(0)は $V^{\pi}$ に確率1で収束します。
-
ステップサイズの条件(Robbins-Monro条件): $$ \sum_{n=0}^{\infty} \alpha_n(s) = \infty, \quad \sum_{n=0}^{\infty} \alpha_n^2(s) < \infty $$ 第1条件は全ての状態に到達するのに十分な更新量を保証し、第2条件は更新量が最終的に0に収束することを保証します。
-
全ての状態の無限回訪問: 方策 $\pi$ のもとで全ての状態が無限回訪問される。
-
マルコフ性: 環境がMDPとしてモデル化できる。
収束の直感的理解
TD(0)がなぜ収束するかの直感は以下の通りです。
$V = V^{\pi}$ のとき $\mathbb{E}[\delta_t \mid S_t = s] = 0$ であり、更新の期待値は0(不動点)。$V \neq V^{\pi}$ のとき、ベルマン作用素の縮小性により $(\mathcal{T}^{\pi} V)(s) – V(s)$ は $V$ を $V^{\pi}$ に近づける方向を向きます。ステップサイズが適切に減衰すれば、確率的な揺らぎに打ち勝って不動点 $V^{\pi}$ に収束します。
n-step TD
定義
TD(0)は1ステップ先の推定値を使いますが、これを $n$ ステップに一般化したのが n-step TD です。
$n$-step リターン $G_t^{(n)}$ を次のように定義します。
$$ G_t^{(n)} = R_{t+1} + \gamma R_{t+2} + \cdots + \gamma^{n-1} R_{t+n} + \gamma^n V(S_{t+n}) $$
$$ = \sum_{k=0}^{n-1} \gamma^k R_{t+k+1} + \gamma^n V(S_{t+n}) $$
更新則は
$$ V(S_t) \leftarrow V(S_t) + \alpha \left[ G_t^{(n)} – V(S_t) \right] $$
特殊ケース:
- $n = 1$: TD(0) — $G_t^{(1)} = R_{t+1} + \gamma V(S_{t+1})$
- $n = \infty$: MC法 — $G_t^{(\infty)} = G_t$(実際のリターン)
$n$ を大きくするとMC法に近づき(低バイアス、高分散)、小さくするとTD(0)に近づきます(高バイアス、低分散)。
TD($\lambda$)と適格度トレース
前方ビュー: TD($\lambda$)
n-step TDの各 $n$ についてリターンを計算する代わりに、全ての $n$ のリターンを加重平均したものが TD($\lambda$) です。
$\lambda$-リターン $G_t^{\lambda}$ を次のように定義します。
$$ \begin{equation} G_t^{\lambda} = (1 – \lambda) \sum_{n=1}^{T-t-1} \lambda^{n-1} G_t^{(n)} + \lambda^{T-t-1} G_t \end{equation} $$
ここで $\lambda \in [0, 1]$ はパラメータです。
重み $(1-\lambda)\lambda^{n-1}$ は幾何的に減少する確率分布で、正規化条件
$$ (1-\lambda) \sum_{n=1}^{\infty} \lambda^{n-1} = (1-\lambda) \cdot \frac{1}{1-\lambda} = 1 $$
を満たします。
特殊ケース:
- $\lambda = 0$: $G_t^{0} = G_t^{(1)} = R_{t+1} + \gamma V(S_{t+1})$(TD(0))
- $\lambda = 1$: $G_t^{1} = G_t$(MC法)
$\lambda$ を調整することで、TD(0)とMC法の間を連続的に補間できます。
後方ビュー: 適格度トレース
前方ビューは将来のリターンを見る必要があり、オンラインでの実装が難しいです。適格度トレース(eligibility traces)を用いた後方ビューは、同等の更新を各ステップで実行可能にします。
適格度トレース $e_t(s)$ は各状態に対して定義されるベクトルです。
$$ \begin{equation} e_t(s) = \begin{cases} \gamma \lambda \, e_{t-1}(s) & \text{if } s \neq S_t \\ \gamma \lambda \, e_{t-1}(s) + 1 & \text{if } s = S_t \end{cases} \end{equation} $$
初期値は $e_0(s) = 0$ (全状態)です。
適格度トレースの直感は「最近訪問した状態ほど、現在のTD誤差に対する責任が大きい」というものです。状態 $s$ を訪問するとトレースが1増加し、時間経過とともに $\gamma \lambda$ で減衰していきます。
TD($\lambda$)の更新則(後方ビュー)
各ステップ $t$ で、全ての状態 $s$ に対して同時に更新します。
$$ \begin{equation} V(s) \leftarrow V(s) + \alpha \, \delta_t \, e_t(s), \quad \forall s \in \mathcal{S} \end{equation} $$
ここで $\delta_t = R_{t+1} + \gamma V(S_{t+1}) – V(S_t)$ はTD誤差です。
TD誤差 $\delta_t$ が適格度トレース $e_t(s)$ に比例して各状態に分配されます。最近訪問した状態は大きく更新され、過去に訪問した状態は $\gamma\lambda$ で減衰した分だけ更新されます。
前方ビューと後方ビューの等価性
定理: オフラインの場合(エピソード完了後に一括更新する場合)、前方ビュー($\lambda$-リターンを用いた更新)と後方ビュー(適格度トレースを用いた更新)は等価である。
証明の概略:
各状態 $s$ に対する総更新量を比較します。
後方ビューの総更新量:
$$ \Delta V_{\text{backward}}(s) = \alpha \sum_{t=0}^{T-1} \delta_t \, e_t(s) $$
適格度トレースを展開すると
$$ e_t(s) = \sum_{k=0}^{t} (\gamma \lambda)^{t-k} \mathbf{1}(S_k = s) $$
ここで $\mathbf{1}(S_k = s)$ は状態 $s$ を時刻 $k$ で訪問したとき1、そうでなければ0です。
代入して整理すると
$$ \begin{align} \sum_{t=0}^{T-1} \delta_t \, e_t(s) &= \sum_{t=0}^{T-1} \delta_t \sum_{k=0}^{t} (\gamma \lambda)^{t-k} \mathbf{1}(S_k = s) \\ &= \sum_{k=0}^{T-1} \mathbf{1}(S_k = s) \sum_{t=k}^{T-1} (\gamma \lambda)^{t-k} \delta_t \end{align} $$
2行目で和の順序を交換しました。
前方ビューの総更新量:
状態 $s$ を時刻 $k$ で訪問したとき
$$ \Delta V_{\text{forward}}(s) = \alpha \sum_{k: S_k = s} \left[ G_k^{\lambda} – V(S_k) \right] $$
先に示した関係 $G_t – V(S_t) = \sum_{j=0}^{T-t-1} \gamma^j \delta_{t+j}$ を $\lambda$-リターンに拡張すると
$$ G_k^{\lambda} – V(S_k) = \sum_{t=k}^{T-1} (\gamma \lambda)^{t-k} \delta_t $$
が成り立ちます(この導出は $\lambda$-リターンの定義と幾何級数の性質から得られます)。
したがって
$$ \Delta V_{\text{forward}}(s) = \alpha \sum_{k=0}^{T-1} \mathbf{1}(S_k = s) \sum_{t=k}^{T-1} (\gamma \lambda)^{t-k} \delta_t = \Delta V_{\text{backward}}(s) $$
前方ビューと後方ビューの総更新量が一致するので、両者は等価です。 $\square$
Pythonでの実装: ランダムウォーク問題
ランダムウォーク環境
5状態のランダムウォーク問題は、MC法とTD法を比較するための古典的なベンチマークです。
[T_L] -- A -- B -- C -- D -- E -- [T_R]
s1 s2 s3 s4 s5
- 状態: $\{A, B, C, D, E\}$(5状態)
- 行動: 等確率で左右に移動
- 左端 $T_L$ に到達: 報酬0で終了
- 右端 $T_R$ に到達: 報酬1で終了
- それ以外: 報酬0
真の価値関数は $V^{\pi} = (\frac{1}{6}, \frac{2}{6}, \frac{3}{6}, \frac{4}{6}, \frac{5}{6})$ です。
import numpy as np
import matplotlib.pyplot as plt
class RandomWalk:
"""5状態のランダムウォーク環境"""
def __init__(self, n_states=5):
self.n_states = n_states
# 状態: 0, 1, ..., n_states-1 (内部状態)
# 終端状態: -1 (左端), n_states (右端)
self.start_state = n_states // 2 # 中央から開始
def reset(self):
"""中央状態からスタート"""
self.state = self.start_state
return self.state
def step(self):
"""等確率で左右に移動"""
if np.random.random() < 0.5:
self.state -= 1 # 左に移動
else:
self.state += 1 # 右に移動
# 終端判定
if self.state < 0:
return self.state, 0, True # 左端: 報酬0
elif self.state >= self.n_states:
return self.state, 1, True # 右端: 報酬1
else:
return self.state, 0, False # 継続: 報酬0
def true_values(self):
"""真の価値関数(解析解)"""
return np.array([(i + 1) / (self.n_states + 1) for i in range(self.n_states)])
# 環境の確認
env = RandomWalk(n_states=5)
print(f"状態数: {env.n_states}")
print(f"開始状態: {env.start_state}")
print(f"真の価値関数: {env.true_values()}")
TD(0)の実装
import numpy as np
import matplotlib.pyplot as plt
class RandomWalk:
def __init__(self, n_states=5):
self.n_states = n_states
self.start_state = n_states // 2
def reset(self):
self.state = self.start_state
return self.state
def step(self):
if np.random.random() < 0.5:
self.state -= 1
else:
self.state += 1
if self.state < 0:
return self.state, 0, True
elif self.state >= self.n_states:
return self.state, 1, True
else:
return self.state, 0, False
def true_values(self):
return np.array([(i + 1) / (self.n_states + 1) for i in range(self.n_states)])
def td0_evaluation(env, alpha=0.1, num_episodes=100, gamma=1.0):
"""TD(0) 方策評価"""
V = np.full(env.n_states, 0.5) # 初期値0.5
rms_history = []
true_V = env.true_values()
for episode in range(num_episodes):
state = env.reset()
while True:
next_state, reward, done = env.step()
if done:
# 終端状態の価値は0
V[state] += alpha * (reward + gamma * 0 - V[state])
break
else:
V[state] += alpha * (reward + gamma * V[next_state] - V[state])
state = next_state
# RMSEの記録
rms = np.sqrt(np.mean((V - true_V) ** 2))
rms_history.append(rms)
return V, rms_history
def mc_evaluation(env, alpha=0.1, num_episodes=100, gamma=1.0):
"""定数α MC方策評価"""
V = np.full(env.n_states, 0.5) # 初期値0.5
rms_history = []
true_V = env.true_values()
for episode in range(num_episodes):
# エピソード生成
states = []
rewards = []
state = env.reset()
while True:
states.append(state)
next_state, reward, done = env.step()
rewards.append(reward)
if done:
break
state = next_state
# リターンの計算と更新(逆順)
G = 0
for t in range(len(states) - 1, -1, -1):
G = gamma * G + rewards[t]
V[states[t]] += alpha * (G - V[states[t]])
# RMSEの記録
rms = np.sqrt(np.mean((V - true_V) ** 2))
rms_history.append(rms)
return V, rms_history
# --- MC vs TD の比較実験 ---
env = RandomWalk(n_states=5)
true_V = env.true_values()
# 実験1: TD(0)の学習過程(異なるエピソード数)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ax = axes[0]
state_labels = ['A', 'B', 'C', 'D', 'E']
# 異なるエピソード数でTD(0)を実行
for n_ep in [1, 10, 100]:
V_td, _ = td0_evaluation(env, alpha=0.1, num_episodes=n_ep)
ax.plot(state_labels, V_td, 'o-', label=f'TD(0), {n_ep} episodes', linewidth=2, markersize=8)
ax.plot(state_labels, true_V, 'k--', label='True $V^{\\pi}$', linewidth=2, markersize=8)
ax.set_xlabel('State', fontsize=12)
ax.set_ylabel('$V(s)$', fontsize=12)
ax.set_title('TD(0) Learning Progress', fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# 実験2: MC vs TD の収束速度比較
ax = axes[1]
# 複数回の実験で平均を取る
num_runs = 100
num_episodes = 100
alphas_td = [0.05, 0.1, 0.15]
alphas_mc = [0.01, 0.02, 0.04]
for alpha in alphas_td:
rms_all = np.zeros(num_episodes)
for run in range(num_runs):
_, rms_history = td0_evaluation(env, alpha=alpha, num_episodes=num_episodes)
rms_all += np.array(rms_history)
rms_all /= num_runs
ax.plot(rms_all, label=f'TD(0), α={alpha}', linewidth=2)
for alpha in alphas_mc:
rms_all = np.zeros(num_episodes)
for run in range(num_runs):
_, rms_history = mc_evaluation(env, alpha=alpha, num_episodes=num_episodes)
rms_all += np.array(rms_history)
rms_all /= num_runs
ax.plot(rms_all, '--', label=f'MC, α={alpha}', linewidth=2)
ax.set_xlabel('Episodes', fontsize=12)
ax.set_ylabel('RMS Error', fontsize=12)
ax.set_title('MC vs TD(0): Convergence Speed', fontsize=13)
ax.legend(fontsize=9, loc='upper right')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
TD($\lambda$)の実装(適格度トレース付き)
import numpy as np
import matplotlib.pyplot as plt
# RandomWalk クラスは上で定義済みとする
def td_lambda_evaluation(env, alpha=0.1, lam=0.8, num_episodes=100, gamma=1.0):
"""TD(λ) 方策評価(後方ビュー、適格度トレース)"""
V = np.full(env.n_states, 0.5)
rms_history = []
true_V = env.true_values()
for episode in range(num_episodes):
state = env.reset()
# 適格度トレースの初期化
e = np.zeros(env.n_states)
while True:
next_state, reward, done = env.step()
if done:
# TD誤差(終端状態の価値は0)
delta = reward + gamma * 0 - V[state]
else:
delta = reward + gamma * V[next_state] - V[state]
# 適格度トレースの更新
e *= gamma * lam
e[state] += 1 # 訪問した状態のトレースを増加
# 全状態の価値関数を更新
V += alpha * delta * e
if done:
break
state = next_state
rms = np.sqrt(np.mean((V - true_V) ** 2))
rms_history.append(rms)
return V, rms_history
# --- 異なるλの比較実験 ---
env = RandomWalk(n_states=5)
true_V = env.true_values()
num_runs = 100
num_episodes = 100
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# (1) 異なるλでの収束速度
ax = axes[0]
lambda_values = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
colors = plt.cm.viridis(np.linspace(0, 1, len(lambda_values)))
for lam, color in zip(lambda_values, colors):
rms_all = np.zeros(num_episodes)
for run in range(num_runs):
_, rms_history = td_lambda_evaluation(
env, alpha=0.1, lam=lam, num_episodes=num_episodes
)
rms_all += np.array(rms_history)
rms_all /= num_runs
ax.plot(rms_all, label=f'λ={lam}', linewidth=2, color=color)
ax.set_xlabel('Episodes', fontsize=12)
ax.set_ylabel('RMS Error', fontsize=12)
ax.set_title('TD(λ): Effect of λ on Convergence', fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# (2) 最適αとλの関係
ax = axes[1]
alphas = np.arange(0.01, 0.5, 0.02)
lambdas = [0.0, 0.4, 0.8, 0.95, 1.0]
colors2 = plt.cm.plasma(np.linspace(0, 1, len(lambdas)))
for lam, color in zip(lambdas, colors2):
avg_rms = []
for alpha in alphas:
rms_final = 0
valid_runs = 0
for run in range(50):
_, rms_history = td_lambda_evaluation(
env, alpha=alpha, lam=lam, num_episodes=50
)
# 最後の10エピソードの平均RMS
rms_final += np.mean(rms_history[-10:])
valid_runs += 1
avg_rms.append(rms_final / valid_runs)
ax.plot(alphas, avg_rms, label=f'λ={lam}', linewidth=2, color=color)
ax.set_xlabel('α (step size)', fontsize=12)
ax.set_ylabel('Average RMS Error\n(last 10 episodes)', fontsize=12)
ax.set_title('TD(λ): Sensitivity to α', fontsize=13)
ax.legend(fontsize=10)
ax.set_ylim(0, 0.6)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
バッチ学習での比較
MC法とTD法の本質的な違いを示すために、バッチ学習(同じデータを繰り返し使って収束するまで学習)での比較を行います。
import numpy as np
import matplotlib.pyplot as plt
# RandomWalk クラスは上で定義済みとする
def batch_td0(env, episodes_data, alpha=0.01, gamma=1.0, max_iter=1000, theta=1e-6):
"""バッチTD(0): 同じデータを繰り返し使って収束するまで学習"""
V = np.full(env.n_states, 0.5)
for iteration in range(max_iter):
V_old = V.copy()
for episode in episodes_data:
for t in range(len(episode) - 1):
state, reward = episode[t]
next_state = episode[t + 1][0]
if next_state < 0 or next_state >= env.n_states:
# 終端状態
V[state] += alpha * (reward + gamma * 0 - V[state])
else:
V[state] += alpha * (reward + gamma * V[next_state] - V[state])
# 最後のステップ
last_state, last_reward = episode[-1]
if 0 <= last_state < env.n_states:
V[last_state] += alpha * (last_reward - V[last_state])
if np.max(np.abs(V - V_old)) < theta:
break
return V
def batch_mc(env, episodes_data, alpha=0.01, gamma=1.0, max_iter=1000, theta=1e-6):
"""バッチMC: 同じデータを繰り返し使って収束するまで学習"""
V = np.full(env.n_states, 0.5)
for iteration in range(max_iter):
V_old = V.copy()
for episode in episodes_data:
# リターンの計算
states = [s for s, r in episode if 0 <= s < env.n_states]
rewards = [r for s, r in episode]
G = 0
for t in range(len(episode) - 1, -1, -1):
state, reward = episode[t]
G = gamma * G + reward
if 0 <= state < env.n_states:
V[state] += alpha * (G - V[state])
if np.max(np.abs(V - V_old)) < theta:
break
return V
def generate_episodes_data(env, num_episodes):
"""エピソードデータの生成"""
episodes = []
for _ in range(num_episodes):
episode = []
state = env.reset()
while True:
next_state, reward, done = env.step()
episode.append((state, reward))
if done:
episode.append((next_state, 0))
break
state = next_state
episodes.append(episode)
return episodes
# --- バッチ学習での比較 ---
env = RandomWalk(n_states=5)
true_V = env.true_values()
num_runs = 100
batch_sizes = [1, 5, 10, 20, 50, 100]
fig, ax = plt.subplots(figsize=(10, 6))
rms_td_batch = []
rms_mc_batch = []
for n_episodes in batch_sizes:
rms_td = 0
rms_mc = 0
for run in range(num_runs):
# 同じデータでMCとTDを比較
episodes_data = generate_episodes_data(env, n_episodes)
V_td = batch_td0(env, episodes_data, alpha=0.001, max_iter=500)
V_mc = batch_mc(env, episodes_data, alpha=0.001, max_iter=500)
rms_td += np.sqrt(np.mean((V_td - true_V) ** 2))
rms_mc += np.sqrt(np.mean((V_mc - true_V) ** 2))
rms_td_batch.append(rms_td / num_runs)
rms_mc_batch.append(rms_mc / num_runs)
ax.plot(batch_sizes, rms_td_batch, 'o-', label='Batch TD(0)', linewidth=2, markersize=8)
ax.plot(batch_sizes, rms_mc_batch, 's-', label='Batch MC', linewidth=2, markersize=8)
ax.set_xlabel('Number of Episodes in Batch', fontsize=12)
ax.set_ylabel('RMS Error', fontsize=12)
ax.set_title('Batch TD(0) vs Batch MC', fontsize=14)
ax.legend(fontsize=12)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
この実験により、以下の重要な結果が確認できます。
- TD(0)はMC法より速く収束する: 少ないエピソード数でTD(0)の方が真の価値関数に近い推定値を得る
- TD($\lambda$)の$\lambda$の効果: 中間的な$\lambda$(0.4〜0.8程度)が最も良い性能を示すことが多い
- 学習率$\alpha$への感度: $\lambda$が大きいほど適切な$\alpha$の範囲が狭くなる
- バッチ学習での比較: 同じデータに対して、TD(0)はMC法より低いRMSEを達成する。これはTD(0)がマルコフ性を活用しているためである
まとめ
本記事では、TD学習の理論と実装について解説しました。
- TD(0)の更新則 $V(S_t) \leftarrow V(S_t) + \alpha[R_{t+1} + \gamma V(S_{t+1}) – V(S_t)]$ はMCとDPのハイブリッドであり、ブートストラップによりステップごとに学習できる
- TD誤差 $\delta_t = R_{t+1} + \gamma V(S_{t+1}) – V(S_t)$ は予測の誤差を表し、真の価値関数では期待値が0になる
- MCは不偏だが高分散、TDはバイアスありだが低分散であり、バイアス-分散トレードオフの関係にある
- TD(0)の収束は確率的近似理論(Robbins-Monro条件)により保証される
- n-step TDとTD($\lambda$)により、TD(0)とMC法の間を連続的に補間できる
- 適格度トレースを用いた後方ビューは、前方ビュー($\lambda$-リターン)と等価であり、オンライン実装を可能にする
次のステップとして、以下の記事も参考にしてください。