TD学習(Temporal Difference Learning)の理論と実装

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で収束します。

  1. ステップサイズの条件(Robbins-Monro条件): $$ \sum_{n=0}^{\infty} \alpha_n(s) = \infty, \quad \sum_{n=0}^{\infty} \alpha_n^2(s) < \infty $$ 第1条件は全ての状態に到達するのに十分な更新量を保証し、第2条件は更新量が最終的に0に収束することを保証します。

  2. 全ての状態の無限回訪問: 方策 $\pi$ のもとで全ての状態が無限回訪問される。

  3. マルコフ性: 環境が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()

この実験により、以下の重要な結果が確認できます。

  1. TD(0)はMC法より速く収束する: 少ないエピソード数でTD(0)の方が真の価値関数に近い推定値を得る
  2. TD($\lambda$)の$\lambda$の効果: 中間的な$\lambda$(0.4〜0.8程度)が最も良い性能を示すことが多い
  3. 学習率$\alpha$への感度: $\lambda$が大きいほど適切な$\alpha$の範囲が狭くなる
  4. バッチ学習での比較: 同じデータに対して、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 TDTD($\lambda$)により、TD(0)とMC法の間を連続的に補間できる
  • 適格度トレースを用いた後方ビューは、前方ビュー($\lambda$-リターン)と等価であり、オンライン実装を可能にする

次のステップとして、以下の記事も参考にしてください。