メインコンテンツまでスキップ

REINFORCEアルゴリズムとは?

REINFORCEアルゴリズム
(画像は、Geminiで作成されたものです)

REINFORCEアルゴリズムの概要

これまで「Q学習」や「DQN」といった、行動の価値(Q値)を予測する価値ベース(Value-based) の強化学習アルゴリズムを見てきました。価値ベースの手法は「すべての行動のQ値を計算し、その中から最もQ値が高い行動を選ぶ(argmax)」というアプローチをとります。

しかし、このアプローチには致命的な弱点があります。それは、連続的な行動空間(例: 自動運転のハンドルの角度)や、非常に選択肢が多い環境(例: 言語モデルにおける数万〜十数万の単語の語彙)では、「すべての行動のQ値を計算して比較する」という処理自体が膨大になりすぎて実質的に計算不可能になってしまうという点です。

そこで登場するのが、すべての行動の価値を調べるのではなく、方策(行動をとる確率分布)そのものを直接最適化する方策ベース(Policy-based)の手法です。本記事では、その原点であるアルゴリズムREINFORCEについて解説します。

REINFORCEの処理概要

REINFORCE(モンテカルロ方策勾配法)は、ニューラルネットワークを用いて「ある状態 ss で特定の行動 aa をとる確率 πθ(as)\pi_\theta(a|s)」を出力し、それが良い結果をもたらしたならその確率を上げ、悪い結果なら下げるという直感的なアプローチをとります。

方策勾配定理 (Policy Gradient Theorem) とその導出

方策ベースの目標は、「得られる総報酬の期待値」を最大化するように、ネットワークのパラメータ θ\theta を更新することです。 いきなり最終的な数式を見ると難しく見えますが、順を追って考えると非常に直感的です。

1. 目的関数の定義(何を最大化したいか?)

まず、ある方策 πθ\pi_\theta に従ってゲームを最初から最後までプレイしたときの「状態と行動の連続(軌跡)」を τ\tau(タウ)とし、そのプレイで得られた最終的な総報酬を R(τ)R(\tau) とします。 目的関数 J(θ)J(\theta) は、方策 πθ\pi_\theta によって得られる総報酬の期待値(平均スコア) として定義されます。

J(θ)=Eτπθ[R(τ)]=τP(τθ)R(τ)J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} [R(\tau)] = \sum_{\tau} P(\tau|\theta) R(\tau)

(※連続値の場合は和の代わりに積分になります) ここで P(τθ)P(\tau|\theta) は、パラメータ θ\theta の方策に従ったときに、その特定のプレイ履歴 τ\tau が発生する確率です。

2. パラメータの更新方向(勾配)を求める

スコアの期待値 J(θ)J(\theta) を最大化したいので、これを θ\theta で微分(勾配を計算)します。報酬 R(τ)R(\tau) 自体はパラメータ θ\theta に依存しない(ただのゲームの得点)ため、微分は確率 P(τθ)P(\tau|\theta) にのみかかります。

θJ(θ)=τθP(τθ)R(τ)\nabla_\theta J(\theta) = \sum_{\tau} \nabla_\theta P(\tau|\theta) R(\tau)

しかし、このままでは計算ができません。私たちが知りたいのは期待値(実際にプレイして得られたデータの平均)として計算できる形です。

3. 「対数微分のトリック(Log-derivative trick)」の活用

ここで、高校数学で習う合成関数の微分法則 logf(x)=f(x)f(x)\nabla \log f(x) = \frac{\nabla f(x)}{f(x)} (つまり f(x)=f(x)logf(x)\nabla f(x) = f(x) \nabla \log f(x))を利用します。これを先ほどの式に当てはめます。

θJ(θ)=τP(τθ)θlogP(τθ)R(τ)\nabla_\theta J(\theta) = \sum_{\tau} P(\tau|\theta) \nabla_\theta \log P(\tau|\theta) R(\tau)

式の中に再び P(τθ)P(\tau|\theta) (確率)が現れました。確率を掛けてすべてのパターンを足し合わせることは、すなわち期待値を取ることと同じです。これにより、数式が「実際にプレイした結果(τπθ\tau \sim \pi_\theta)から計算できる期待値」の形に変わります!(τP(τθ)Eτπθ[]\sum_{\tau} P(\tau|\theta) \cdot \to \mathbb{E}_{\tau \sim \pi_\theta} [\cdot]

θJ(θ)=Eτπθ[θlogP(τθ)R(τ)]\nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \nabla_\theta \log P(\tau|\theta) R(\tau) \right]

4. 軌跡の確率をステップごとに分解する

軌跡の確率 P(τθ)P(\tau|\theta) は、各ステップでの「方策による行動確率」と「環境の遷移確率(初期状態 P(s0)P(s_0) や状態遷移 P(st+1st,at)P(s_{t+1}|s_t, a_t))」の掛け算で表されます。

P(τθ)=P(s0)t=0Tπθ(atst)P(st+1st,at)P(\tau|\theta) = P(s_0) \prod_{t=0}^{T} \pi_\theta(a_t|s_t) P(s_{t+1}|s_t, a_t)

対数(log)をとると、掛け算はすべて足し算に変わります。 さらに、「環境の遷移確率(P(s0)P(s_0) および P(st+1st,at)P(s_{t+1}|s_t, a_t))」は私たちのパラメータ θ\theta には依存しない(ゲームのルール自体は変えられない)ため、θ\theta で微分するとすべてゼロになって消えます。 残るのは「自分の方策がその行動を選んだ確率」の対数微分だけになります。

θlogP(τθ)=t=0Tθlogπθ(atst)\nabla_\theta \log P(\tau|\theta) = \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t|s_t)

5. 最終的な方策勾配定理

さらに因果関係(過去の行動は未来の報酬にしか影響しない)を考慮し、R(τ)R(\tau) を「時刻 tt 以降の割引報酬和 GtG_tGt=k=tTγktrkG_t = \sum_{k=t}^{T} \gamma^{k-t} r_k)」に置き換えます。これにより、最終的な方策勾配定理の数式が導出されます。

θJ(θ)=Eτπθ[t=0Tθlogπθ(atst)Gt]\nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t|s_t) G_t \right]
モンテカルロ法との関係

エピソードを最後までやり切って実際の収益 GtG_t を計算し、学習の評価指標として用いるこのアプローチは、まさにモンテカルロ法の考え方そのものです。REINFORCEが別名「モンテカルロ方策勾配法」と呼ばれる理由はここにあります。基礎となる収益の計算やモンテカルロ推定の仕組みを復習したい方は、モンテカルロ法(Monte Carlo Method)とは?の記事もあわせてご覧ください。

【数式の意味(直感的な理解)】 この式は非常にシンプルで強力なメッセージを持っています。

  • θlogπθ(atst)\nabla_\theta \log \pi_\theta(a_t|s_t) は、「その行動をとる確率を上げるためのパラメータの更新方向」を指します。
  • GtG_t は、その行動をとった結果として将来得られた報酬です。

つまり、「結果(GtG_t)が良かった行動は、その確率を上げる方向にパラメータを大きく更新し、結果が悪かった(マイナスだった)行動は、その確率を下げる」 という、極めて直感的な学習の仕組みが数式として証明されているのです。

アルゴリズムの流れ

  1. 現在の方策 πθ\pi_\theta に従ってエピソードを最後まで実行し、状態・行動・報酬の軌跡(Trajectory)を収集する。
  2. 各ステップ tt における割引報酬和 GtG_t を計算する。
  3. θlogπθ(atst)×Gt\nabla_\theta \log \pi_\theta(a_t|s_t) \times G_t を計算し、勾配上昇(Gradient Ascent)によってパラメータ θ\theta を更新する。

REINFORCEの実装(概念的なシンプルな実装)

以下では、REINFORCEのシンプルなPyTorch実装を示します。

方策ネットワークとREINFORCEの更新ステップ関数の定義

ここでは、ニューラルネットワークを用いて「状態から各行動の選択確率を出力する」ための 方策ネットワーク(PolicyNetwork と、集めたプレイ結果(対数確率と報酬)をもとに方策勾配定理の数式に従ってパラメータを更新する 学習用関数(update_policy を定義します。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.distributions import Categorical

# 方策ネットワークの定義
class PolicyNetwork(nn.Module):
def __init__(self, state_dim, action_dim):
super(PolicyNetwork, self).__init__()
self.fc = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, action_dim),
nn.Softmax(dim=-1) # 行動の確率分布を出力
)

def forward(self, x):
return self.fc(x)

# REINFORCEの更新ステップ
def update_policy(policy_net, optimizer, log_probs, rewards, gamma=0.99):
discounted_rewards = []
R = 0
# 後ろから割引報酬和 G_t を計算
for r in reversed(rewards):
R = r + gamma * R
discounted_rewards.insert(0, R)

discounted_rewards = torch.tensor(discounted_rewards)
# ベースラインの導入(平均0、分散1に正規化することで分散の爆発を抑え、学習を安定化させる)
if len(discounted_rewards) > 1:
discounted_rewards = (discounted_rewards - discounted_rewards.mean()) / (discounted_rewards.std() + 1e-9)

policy_loss = []
for log_prob, G_t in zip(log_probs, discounted_rewards):
# 勾配上昇を行うため、マイナスをつけて最小化問題にする
policy_loss.append(-log_prob * G_t)

optimizer.zero_grad()
loss = torch.stack(policy_loss).sum()
loss.backward()
optimizer.step()

上記のコードは、REINFORCEの根幹となる「方策ネットワーク」と「パラメータ更新処理」の2つから構成されています。

  1. 方策ネットワーク(PolicyNetwork: 状態を入力として受け取り、最後に nn.Softmax(dim=-1) を適用することで、合計が1(100%)になる「行動の確率分布」を出力します。
  2. 割引報酬和の計算(update_policyの前半): エピソードの最後から逆向きにループ(reversed(rewards))を回すことで、各時刻における割引報酬和 GtG_t を効率よく計算しています。
  3. ベースラインの導入による学習の安定化: コード内の discounted_rewards = (discounted_rewards - discounted_rewards.mean()) ... の部分は、計算された報酬和を平均0・分散1に正規化する処理です。REINFORCEは報酬をそのまま使うと「分散の爆発」により学習が不安定になるため、このように報酬を正規化(ベースラインを導入)し、「平均より良かったか、悪かったか」で評価することで学習の安定性を劇的に向上させています。

コードと数式の対応関係

上記のコードは、前述の「方策勾配定理」の数式をそのままPyTorchで表現したものです。

  1. logπθ(atst)\log \pi_\theta(a_t|s_t)(対数確率): 次項のプレイ用コードで登場しますが、PyTorchの m.log_prob(action) を使うことで即座に計算でき、引数として渡される log_probs リストに蓄積されている想定です。
  2. GtG_t(割引報酬和): for r in reversed(rewards): のループを用いて、エピソードの最後から逆順にたどることで、各時刻 ttGtG_t を効率的に計算しています。
  3. θlogπθ(atst)×Gt\nabla_\theta \log \pi_\theta(a_t|s_t) \times G_t(勾配の計算と更新): PyTorchの最適化機能(Optimizer)はデフォルトで勾配降下法(最小化) を行うため、数式の「勾配上昇(最大化)」を行うにはマイナス符号をつける必要があります。それが policy_loss.append(-log_prob * G_t) の部分です。これを全て足し合わせて loss.backward() を呼ぶことで、方策勾配定理通りのパラメータ更新が実行されます。

CartPoleでの性能評価

実際に gymnasium ライブラリの CartPole-v1 環境を用いて、REINFORCEアルゴリズムでエージェントを学習させる完全なコードを紹介します。

import gymnasium as gym

def train_reinforce():
env = gym.make('CartPole-v1')
# 前のセルで定義したネットワークを利用
policy_net = PolicyNetwork(env.observation_space.shape[0], env.action_space.n)
optimizer = optim.Adam(policy_net.parameters(), lr=1e-2)

for episode in range(500):
state, _ = env.reset()
log_probs = []
rewards = []

# 1エピソードのプレイ
for t in range(1000):
state_tensor = torch.FloatTensor(state).unsqueeze(0)
probs = policy_net(state_tensor)
m = Categorical(probs)
action = m.sample() # 確率分布に従って行動をサンプリング

# 選んだ行動の対数確率を保存
log_probs.append(m.log_prob(action))

# 環境を1ステップ進める (gymnasiumのAPIに対応)
state, reward, terminated, truncated, _ = env.step(action.item())
rewards.append(reward)

if terminated or truncated:
break

# 先ほど定義した関数を呼び出してパラメータを更新
update_policy(policy_net, optimizer, log_probs, rewards)

# 進捗の表示
if (episode+1) % 50 == 0:
print(f"Episode {episode+1}\tScore: {sum(rewards)}")

env.close()

# 学習の実行
train_reinforce()

コードの解説(数式との対応)

この関数では、1エピソード(ゲームの開始から終了まで)をプレイし、その結果を使って方策を更新するサイクルを繰り返します。

  • probs = policy_net(state_tensor) で状態を入力し、行動の確率分布を取得します。
  • m = Categorical(probs)m.sample() を使って、出力された確率に従って行動を選びます。確率が高い行動ほど選ばれやすくなります。
  • log_probs.append(m.log_prob(action)) で、選ばれた行動の「対数確率」を計算しリストに保存しておきます。ここでの m.log_prob(action) が、方策勾配定理の数式における logπθ(atst)\log \pi_\theta(a_t|s_t) に直接対応しています。
  • エピソードが終了(terminated または truncated)したら、保存しておいた対数確率と報酬のリストを update_policy に渡します。そこで割引報酬和 GtG_t と掛け合わされ、数式通りの勾配計算(θlogπθ(atst)×Gt\nabla_\theta \log \pi_\theta(a_t|s_t) \times G_t)が行われて方策ネットワークが一気に更新されます。

上記のコードを実行すると以下の結果が得られます(環境によって多少異なります)。

実行結果

Episode 50 Score: 65.0
Episode 100 Score: 113.0
Episode 150 Score: 500.0
Episode 200 Score: 12.0
Episode 250 Score: 123.0
Episode 300 Score: 500.0
Episode 350 Score: 500.0
Episode 400 Score: 500.0
Episode 450 Score: 13.0
Episode 500 Score: 17.0

実行結果からの考察

上記の出力から、以下の重要なポイントが読み取れます。

  1. 学習の進行と適応: 序盤(Episode 50〜100)はスコアが低迷していますが、エピソードを重ねるごとに方策が改善され、Episode 150や300〜400ではCartPoleの最大スコアである500点に到達しています。REINFORCEの直感的な仕組みでも、確実に環境を学習できていることがわかります。
  2. 学習の極端な不安定さ: 一方で、一度500点を達成したにもかかわらず、Episode 200や450〜500で突然スコアが10点台に急落しています。良い方策を見つけた後でも、一度運の悪い結果(偶然の失敗など)を強く学習してしまうと、方策が一気に崩壊してしまうのです。これが次項で解説する「分散の大きさ」に起因する、REINFORCEの致命的な弱点を示す典型的な挙動です。

Stable Baselines3での実装について

DQNの解説記事などでは、実践的なライブラリとして「Stable Baselines3 (SB3)」による実装例を紹介してきましたが、実は Stable Baselines3にはREINFORCE(Vanilla Policy Gradient)は実装されていません。

その理由は次項で解説する通り、REINFORCEが抱える「分散の爆発」という致命的な弱点を解消したA2CPPOといった上位互換のアクター・クリティック(Actor-Critic)系アルゴリズムが実用上の標準となっており、あえてREINFORCEを採用するメリットがないためです。そのため、実務で方策ベースの手法を用いる場合は、次にご紹介するA2CやPPOを利用することになります。

REINFORCEの限界(最大の弱点)

REINFORCEは方策勾配法の中でも最もシンプルで直感的ですが、いくつかの致命的な弱点があります。

1. 分散の爆発(モンテカルロ推定の限界)

方策の評価にエピソードの最後までプレイした「総収益(モンテカルロ推定)」を使っているためです。同じ状態から同じ行動をとっても、その後の展開によって最終的な総収益 GtG_t は毎回大きくブレるため、勾配の分散が極めて大きくなり、学習が著しく不安定になります。

実際に、全く同じ初期状態から同じ最初の一手をとった場合でも、その後の展開によって総報酬がどれほどブレるか(分散するか)を確認するコードを以下に示します。

import gymnasium as gym
import numpy as np

def demonstrate_variance():
env = gym.make('CartPole-v1')
returns = []

# 100回、全く同じ初期状態からシミュレーションを行う
for _ in range(100):
# seedを固定して、CartPoleの初期状態を完全に同一にする
state, _ = env.reset(seed=42)

# 最初の行動を「右に移動(1)」で完全に固定する
state, reward, terminated, truncated, _ = env.step(1)

G_t = reward
gamma = 0.99
discount = gamma

# その後はランダムに行動し続ける(未学習の方策を想定)
while not (terminated or truncated):
action = env.action_space.sample()
_, r, terminated, truncated, _ = env.step(action)
G_t += discount * r
discount *= gamma

returns.append(G_t)

returns = np.array(returns)
print(f"報酬和の平均: {returns.mean():.2f}")
print(f"報酬和の標準偏差(ブレの大きさ): {returns.std():.2f}")
print(f"最大報酬: {returns.max():.2f}, 最小報酬: {returns.min():.2f}")

env.close()

# 実行
demonstrate_variance()

このコードでは、全く同じ初期状態(env.reset(seed=42) でシードを固定)から開始し、最初の一手も「右に移動」で固定(env.step(1))しています。つまり、ここでのエージェントの「状態」と「選んだ行動」は完全に同一の条件です。 その後は env.action_space.sample() を用いてランダムに行動し続け、エピソード終了までの割引報酬和(G_t)を計算するシミュレーションを100回繰り返しています。

実行結果

報酬和の平均: 20.15
報酬和の標準偏差(ブレの大きさ): 8.78
最大報酬: 53.41, 最小報酬: 10.47

実行結果からの考察

この結果から、「全く同じ初期状態」から「全く同じ最初の一手」を取ったにもかかわらず、最終的な報酬和は 約10〜53 まで大きくばらついていることがわかります。 標準偏差(8.78)が平均(20.15)の半分近くにも達しており、これは「その行動自体が良かったかどうか」よりも「その後のランダムな展開(運)」によって最終スコアが大きく左右されてしまうことを意味しています。

このように、1回のプレイ結果(モンテカルロ推定)をそのまま使って方策を更新しようとすると、たまたま運が良かっただけの行動を過大評価したり、運が悪かっただけの良い行動を過小評価したりしてしまい、学習が正しく進みません。これが方策勾配法における最大の課題である「分散の爆発」です。

この問題を根本から解決するために、後の研究で「価値ネットワーク(Critic)」を導入し、安定したベースラインを引く手法(A2C / Actor-Critic)が誕生しました。

2. オンポリシー(On-policy)によるサンプル効率の悪さ

強化学習アルゴリズムは、データを収集する方策と学習する方策の関係性によって分類されます。

  • オフポリシー(Off-policy): Q学習やDQNのように、過去の古い方策で集めたデータ(経験再生バッファ)を再利用して学習できる手法。
  • オンポリシー(On-policy): REINFORCEのように、「今まさに学習中の最新の方策」が直接収集したデータでしか学習できない手法。

REINFORCEはオンポリシー手法であるため、1回学習に使ったデータ(エピソード)は直ちに捨てなければならず、DQNのように過去の経験を何度も再利用(Experience Replay)することができません。そのため、膨大なデータ収集が必要となり、非常にサンプル効率が悪いという問題があります。

まとめ

  • REINFORCEは、状態から行動確率を直接出力する方策ベースの強化学習の原点です。
  • 良い結果をもたらした行動の確率を上げるように、対数確率勾配を使ってパラメータを更新します。
  • エピソード全体の報酬和に基づくため、勾配の分散が大きく学習が不安定になりやすいという課題があり、これが次の A2CPPO の発展に直結します。

本記事の文章・構成の一部に生成AIを使用しています。