強化学習

目次

概要

行動して、報酬から学ぶ

強化学習では、正解ラベルを直接与えず、環境との相互作用から方策を学びます。状態、行動、報酬、遷移という枠組みで問題を表現し、長期的な累積報酬を最大化する行動を探します。

要点

強化学習は「その場の正解」を学ぶのではなく、「長期的に何が良いか」を試行錯誤から学ぶ枠組みです。

この章で重視すること

  • MDPの基本を理解する
  • value-basedとpolicy-basedの違いを整理する
  • 探索と活用のトレードオフを押さえる

基本概念

  • state
  • action
  • reward
  • policy
  • value function
  • discount factor

強化学習では、モデルが正解ラベルを直接与えられるのではなく、行動の結果として得られる報酬から学びます。そのため、問題設定の作り方が学習結果を大きく左右します。

flowchart LR Agent["agent"] Action["action"] Env["environment"] State["state"] Reward["reward"] Agent --> Action --> Env Env --> State --> Agent Env --> Reward --> Agent

このループでは、状態が十分に観測できるか、行動が安全に試せるか、報酬が本当に目的を表しているかが重要です。報酬がずれていると、agentは仕様の抜け道を最適化します。

MDP

強化学習の多くはMarkov Decision Process(MDP) として定式化されます。状態 s、行動 a、報酬 r、遷移確率、割引率を使い、将来の累積報酬を最大化する方策を学びます。

flowchart LR S["state"] --> A["action"] A --> E["environment"] E --> R["reward"] E --> S2["next state"]

Markov性とは、次の状態が過去全体ではなく現在状態と行動から決まる、という仮定です。

探索と活用

すでに良いと分かっている行動を選ぶのが活用、まだ十分知らない行動を試すのが探索です。探索しなければより良い行動を見つけられず、探索しすぎると短期性能が下がります。

代表的な方法:

  • epsilon-greedy
  • softmax exploration
  • upper confidence bound
  • entropy regularization

典型アルゴリズム

  • Q-learning
  • DQN
  • policy gradient
  • actor-critic

Value-based

value-basedな方法では、状態や行動の価値を推定し、価値が高い行動を選びます。Q-learningやDQNが代表です。

Policy-based

policy-basedな方法では、方策そのものを直接学習します。連続行動や確率的方策と相性がよい一方、学習が不安定になることがあります。

Actor-Critic

actorは方策を、criticは価値を学習します。両者を組み合わせることで、方策勾配の学習を安定させやすくします。

報酬設計

報酬設計は強化学習で最も難しい部分の1つです。報酬が不適切だと、モデルは期待しない近道を学びます。これをreward hackingと呼ぶことがあります。

良い報酬は、最終目的に近く、過度に細かい誘導をしすぎず、安全制約を無視しないものです。

報酬は「望ましい結果」を数値にしたものですが、数値化した瞬間に抜け道が生まれます。たとえばゲームでスコアだけを報酬にすると、進行せずに安全な場所で点を稼ぐ戦略を学ぶかもしれません。推薦でクリックだけを報酬にすると、長期満足度より刺激的な項目を選びやすくなります。

報酬設計で確認すること:

  • 短期報酬と長期報酬が衝突しないか
  • 禁止行動を罰則だけで表そうとしていないか
  • 人間が見て望ましい行動になっているか
  • 報酬を得る抜け道がないか
  • 安全制約を別に置く必要がないか

強化学習の失敗はアルゴリズムだけでなく、環境と報酬の設計から起きます。

難しさ

  • サンプル効率が悪い
  • 報酬設計が難しい
  • オフライン評価が難しい
  • 実世界では安全性制約が大きい

Offline RL

実環境で試行錯誤できない場合、過去ログから方策を学ぶoffline RLが使われます。ただし、ログにない行動の結果を推定するのは難しく、分布外行動への過信が問題になります。

Offline RLでは、行動を実際に試せないため、データ収集方策の偏りが問題になります。過去のシステムが選ばなかった行動については、良いか悪いかを判断する材料が少ないからです。

問題 意味 対策
distribution shift 学習データにない行動を選ぶ conservativeな方策、制約
selection bias 過去方策が選んだ行動に偏る logging policyの記録
counterfactual evaluation 試していない行動を評価できない off-policy evaluation
safety 実環境で失敗できない simulation、人間承認、段階配備

実務では、Offline RLをいきなり本番制御に使うのではなく、まず候補方策の比較や意思決定支援に使い、安全な範囲で段階的に広げる方が現実的です。

応用領域

  • ロボティクス
  • ゲーム
  • 推薦
  • 広告入札
  • 資源割り当て
  • 制御

LLMとの関係

LLMの学習では、人間のフィードバックやpreference dataを使って応答を調整することがあります。厳密な意味での古典的な強化学習と同じではない部分もありますが、報酬モデル、方策更新、安全性制約という考え方は接続しています。

価値関数とベルマン方程式

強化学習では、目先の報酬だけでなく将来の報酬も含めて行動を評価します。その中心にあるのが価値関数です。

  • V(s) 状態 s から始めたときの期待累積報酬
  • Q(s, a) 状態 s で行動 a を選んだときの期待累積報酬

ベルマン方程式は「今得る報酬」と「次状態の価値」を再帰的につなぎます。この見方により、長い意思決定問題を局所的な更新として扱えるようになります。

Q(s, a) = r + gamma * max_a' Q(s', a')

この式は直感的には、良い行動とは「今の報酬が高く、次に良い状態へ行ける行動」です。

On-policyとoff-policy

強化学習のアルゴリズムは、データを集める方策と、学習したい方策が同じかどうかで整理できます。

種類 説明
on-policy 現在の方策で集めた経験から、その方策を改善する SARSA、PPO
off-policy 別の方策や過去ログから、目標方策を学ぶ Q-learning、DQN

off-policyは過去ログを使いやすい一方、ログにない行動の結果を過信しやすい問題があります。推薦や広告のような実サービスでは、探索のコストと安全性が重要になります。

実環境での安全性

ゲームやシミュレータでは失敗しても再試行できますが、現実のロボット、医療、金融、交通ではそうはいきません。実環境の強化学習では、次を先に考える必要があります。

  • 危険な行動を探索させない制約
  • シミュレータと現実の差
  • offline評価の信頼性
  • 人間の介入と停止
  • 既存ルールベースとの併用

強化学習は強力ですが、実務では「完全自律で学習させる」より、「限られた範囲で最適化する」使い方が現実的なことが多いです。

モデルベースとモデルフリー

強化学習は、環境の遷移モデルを学ぶかどうかでも分けられます。

種類 考え方 強み 難しさ
model-free 経験から価値や方策を直接学ぶ 実装しやすい サンプル効率が悪い
model-based 環境のモデルを学び計画に使う 少ない経験を活用しやすい モデル誤差が方策を壊す

ロボティクスや制御では、シミュレータや物理モデルを活用できると学習効率が上がります。ただし、シミュレータで良くても現実でうまくいくとは限りません。

具体的な強化学習アルゴリズムの比較

Q-Learning: オフポリシー価値反復

Q-learningは、状態行動対(s, a)に対するQ値を段階的に更新します。

アルゴリズム(tabular版):

for each episode:
  state = environment.reset()
  while not done:
    action = epsilon_greedy(Q, state)
    reward, next_state = environment.step(action)
    Q[state, action] += alpha * (
      reward + gamma * max(Q[next_state, :]) - Q[state, action]
    )
    state = next_state

パラメータの役割:

  • alpha: 学習率。大きいほど新しい経験を重視
  • gamma: 割引率。0.99など、将来報酬の重視度
  • epsilon: ε-greedyの探索率。0.1なら10%確率

Q-learningの強み:

  • オフポリシーのため、過去ログからでも学習可能
  • 収束が保証される
  • 実装が比較的シンプル

弱み:

  • 状態空間が大きいとスケールしない
  • 観測不完全な環境では仮定が成り立たない

DQN: Deep Q-Network

Q-learningを深いニューラルネットワークで拡張。Atariゲームなどで高次元状態に対応。

主な工夫:

  1. Experience Replay: バッファに保存し、ランダムサンプルで学習
  2. Target Network: 更新用に別のネットワークを使用
  3. ε-decay: 最初は探索、段階的に活用へ移行

DQNの実績:

  • Atari 57ゲーム中49ゲームで人間以上(Nature 2015)
  • 安定した学習には大量の計算資源が必要

Policy Gradient と REINFORCE

方策πをニューラルネットワークで直接パラメータ化。

REINFORCE アルゴリズム:

θ ← random initialization
for each episode:
    trajectory = collect_episode(π_θ)
    for t in trajectory:
        g_t = sum(gamma^k * r_{t+k} for k)
        ∇_θ J(θ) += ∇_θ log π_θ(a_t|s_t) * g_t
    θ ← θ + α * ∇_θ J(θ)

特徴:

  • オンポリシー(現在の方策で集めたデータ)
  • 連続行動に自然に対応
  • 学習が遅く、分散が大きい

強化学習アルゴリズムの実装詳細

Value-Based Methods: Q-Learning と DQN

Q-Learning(Tabular):

class QLearningAgent:
    def __init__(self, n_states, n_actions, learning_rate=0.1, discount_factor=0.99, epsilon=0.1):
        self.q_table = np.zeros((n_states, n_actions))
        self.lr = learning_rate
        self.gamma = discount_factor
        self.epsilon = epsilon
    
    def select_action(self, state):
        # ε-greedy:確率εで探索、1-εで最適行動
        if np.random.random() < self.epsilon:
            return np.random.randint(0, self.q_table.shape[1])  # 探索
        else:
            return np.argmax(self.q_table[state])  # 利用
    
    def update(self, state, action, reward, next_state, done):
        # Bellman 更新
        if done:
            target = reward
        else:
            target = reward + self.gamma * np.max(self.q_table[next_state])
        
        td_error = target - self.q_table[state, action]
        self.q_table[state, action] += self.lr * td_error

Deep Q-Network (DQN):

import torch
import torch.nn as nn
import torch.optim as optim
from collections import deque
import random

class DQNAgent:
    def __init__(self, state_size, action_size, learning_rate=0.001):
        self.state_size = state_size
        self.action_size = action_size
        
        # Q-network と Target network
        self.q_network = self._build_network(state_size, action_size)
        self.target_network = self._build_network(state_size, action_size)
        self.target_network.load_state_dict(self.q_network.state_dict())
        
        self.optimizer = optim.Adam(self.q_network.parameters(), lr=learning_rate)
        
        # Experience replay buffer
        self.memory = deque(maxlen=100000)
        self.gamma = 0.99
        self.epsilon = 1.0
        self.epsilon_min = 0.01
        self.epsilon_decay = 0.995
    
    def _build_network(self, state_size, action_size):
        return nn.Sequential(
            nn.Linear(state_size, 128),
            nn.ReLU(),
            nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, action_size)
        )
    
    def remember(self, state, action, reward, next_state, done):
        self.memory.append((state, action, reward, next_state, done))
    
    def replay(self, batch_size):
        if len(self.memory) < batch_size:
            return
        
        batch = random.sample(self.memory, batch_size)
        states = torch.tensor([exp[0] for exp in batch], dtype=torch.float32)
        actions = torch.tensor([exp[1] for exp in batch], dtype=torch.long)
        rewards = torch.tensor([exp[2] for exp in batch], dtype=torch.float32)
        next_states = torch.tensor([exp[3] for exp in batch], dtype=torch.float32)
        dones = torch.tensor([exp[4] for exp in batch], dtype=torch.float32)
        
        # Q-learning update
        q_values = self.q_network(states)
        q_values_next = self.target_network(next_states).detach()
        
        targets = rewards + self.gamma * q_values_next.max(dim=1)[0] * (1 - dones)
        
        loss = nn.MSELoss()(q_values.gather(1, actions.unsqueeze(1)), targets.unsqueeze(1))
        
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
        
        # ε-decay
        if self.epsilon > self.epsilon_min:
            self.epsilon *= self.epsilon_decay
    
    def update_target_network(self):
        """Target network を更新(定期的に呼び出し)"""
        self.target_network.load_state_dict(self.q_network.state_dict())

Policy-Based Methods: Policy Gradient

REINFORCE(モンテカルロ ポリシーグラディエント):

class REINFORCEAgent:
    def __init__(self, state_size, action_size, learning_rate=0.001):
        self.policy_network = nn.Sequential(
            nn.Linear(state_size, 128),
            nn.ReLU(),
            nn.Linear(128, action_size),
            nn.Softmax(dim=-1)
        )
        self.optimizer = optim.Adam(self.policy_network.parameters(), lr=learning_rate)
        self.gamma = 0.99
    
    def select_action(self, state):
        state = torch.tensor(state, dtype=torch.float32)
        action_probs = self.policy_network(state)
        action = torch.multinomial(action_probs, 1).item()
        return action, action_probs
    
    def update(self, episode_states, episode_actions, episode_rewards):
        """
        エピソード終了後に実行
        REINFORCE: Σ log π(a|s) * G_t
        """
        returns = []
        g = 0
        
        # リターンを逆順に計算
        for reward in reversed(episode_rewards):
            g = reward + self.gamma * g
            returns.insert(0, g)
        
        returns = torch.tensor(returns, dtype=torch.float32)
        returns = (returns - returns.mean()) / (returns.std() + 1e-8)  # 正規化
        
        loss = 0
        for state, action, g in zip(episode_states, episode_actions, returns):
            state_tensor = torch.tensor(state, dtype=torch.float32)
            action_probs = self.policy_network(state_tensor)
            loss -= torch.log(action_probs[action]) * g  # 負の log probability
        
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

Actor-Critic:

class ActorCriticAgent:
    def __init__(self, state_size, action_size, learning_rate=0.001):
        # Actor: ポリシー
        self.actor = nn.Sequential(
            nn.Linear(state_size, 128),
            nn.ReLU(),
            nn.Linear(128, action_size),
            nn.Softmax(dim=-1)
        )
        
        # Critic: 価値推定
        self.critic = nn.Sequential(
            nn.Linear(state_size, 128),
            nn.ReLU(),
            nn.Linear(128, 1)
        )
        
        self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=learning_rate)
        self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=learning_rate)
        self.gamma = 0.99
    
    def select_action(self, state):
        state = torch.tensor(state, dtype=torch.float32)
        action_probs = self.actor(state)
        action = torch.multinomial(action_probs, 1).item()
        return action, action_probs
    
    def update(self, state, action, reward, next_state, done):
        state = torch.tensor(state, dtype=torch.float32)
        next_state = torch.tensor(next_state, dtype=torch.float32)
        
        # Critic update: 価値関数の精度を改善
        v_current = self.critic(state)
        v_next = self.critic(next_state).detach()
        
        if done:
            target = reward
        else:
            target = reward + self.gamma * v_next
        
        # Temporal Difference (TD) error
        td_error = target - v_current.squeeze()
        
        critic_loss = td_error.pow(2)
        self.critic_optimizer.zero_grad()
        critic_loss.backward()
        self.critic_optimizer.step()
        
        # Actor update: ポリシーを改善(TD error で重み付け)
        action_probs = self.actor(state)
        actor_loss = -torch.log(action_probs[action]) * td_error.detach()
        
        self.actor_optimizer.zero_grad()
        actor_loss.backward()
        self.actor_optimizer.step()

高度なアルゴリズム

PPO (Proximal Policy Optimization)

class PPOAgent:
    def __init__(self, state_size, action_size, learning_rate=0.001, clip_ratio=0.2):
        self.actor = nn.Sequential(
            nn.Linear(state_size, 128),
            nn.ReLU(),
            nn.Linear(128, action_size),
            nn.Softmax(dim=-1)
        )
        
        self.critic = nn.Sequential(
            nn.Linear(state_size, 128),
            nn.ReLU(),
            nn.Linear(128, 1)
        )
        
        self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=learning_rate)
        self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=learning_rate)
        
        self.clip_ratio = clip_ratio  # ポリシーの変更幅を制限
        self.gamma = 0.99
        self.gae_lambda = 0.95  # Generalized Advantage Estimation
    
    def compute_gae(self, rewards, values, next_value, dones):
        """Generalized Advantage Estimation"""
        advantages = []
        gae = 0
        
        for i in reversed(range(len(rewards))):
            if i == len(rewards) - 1:
                next_val = next_value
            else:
                next_val = values[i + 1]
            
            delta = rewards[i] + self.gamma * next_val * (1 - dones[i]) - values[i]
            gae = delta + self.gamma * self.gae_lambda * (1 - dones[i]) * gae
            advantages.insert(0, gae)
        
        return advantages
    
    def update(self, states, actions, old_probs, rewards, values, next_value, dones, epochs=5):
        # Advantage 計算
        advantages = self.compute_gae(rewards, values.detach().numpy(), next_value.detach().item(), dones)
        advantages = torch.tensor(advantages, dtype=torch.float32)
        advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
        
        returns = advantages + values.detach()
        
        # 複数エポック更新
        for epoch in range(epochs):
            # Actor update (Clipped Surrogate)
            new_probs = self.actor(states)
            new_action_probs = new_probs.gather(1, actions.unsqueeze(1)).squeeze()
            
            ratio = new_action_probs / (old_probs + 1e-8)
            surr1 = ratio * advantages
            surr2 = torch.clamp(ratio, 1 - self.clip_ratio, 1 + self.clip_ratio) * advantages
            
            actor_loss = -torch.min(surr1, surr2).mean()
            
            self.actor_optimizer.zero_grad()
            actor_loss.backward()
            self.actor_optimizer.step()
            
            # Critic update
            new_values = self.critic(states)
            critic_loss = nn.MSELoss()(new_values.squeeze(), returns)
            
            self.critic_optimizer.zero_grad()
            critic_loss.backward()
            self.critic_optimizer.step()

SAC (Soft Actor-Critic)

エントロピー正則化で探索と利用のバランスを自動的に制御。

class SACAgent:
    def __init__(self, state_size, action_size, learning_rate=0.0003):
        self.actor = nn.Sequential(
            nn.Linear(state_size, 256),
            nn.ReLU(),
            nn.Linear(256, action_size),
            nn.Tanh()  # [-1, 1] 出力
        )
        
        self.q1_network = self._build_q_network(state_size, action_size)
        self.q2_network = self._build_q_network(state_size, action_size)
        self.target_q1 = self._build_q_network(state_size, action_size)
        self.target_q2 = self._build_q_network(state_size, action_size)
        
        self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=learning_rate)
        self.q1_optimizer = optim.Adam(self.q1_network.parameters(), lr=learning_rate)
        self.q2_optimizer = optim.Adam(self.q2_network.parameters(), lr=learning_rate)
        
        # 自動エントロピー調整
        self.target_entropy = -action_size  # 目標エントロピー
        self.log_alpha = torch.tensor(0.0, requires_grad=True)
        self.alpha_optimizer = optim.Adam([self.log_alpha], lr=learning_rate)
    
    def _build_q_network(self, state_size, action_size):
        return nn.Sequential(
            nn.Linear(state_size + action_size, 256),
            nn.ReLU(),
            nn.Linear(256, 256),
            nn.ReLU(),
            nn.Linear(256, 1)
        )
    
    def select_action(self, state, deterministic=False):
        state = torch.tensor(state, dtype=torch.float32)
        action = self.actor(state)
        
        if deterministic:
            return action.detach().numpy()
        else:
            # 確率的に行動を選択(実装では log_std を追加)
            return action.detach().numpy()
    
    def update(self, state, action, reward, next_state, done):
        state = torch.tensor(state, dtype=torch.float32)
        action = torch.tensor(action, dtype=torch.float32)
        next_state = torch.tensor(next_state, dtype=torch.float32)
        
        # Q-network update
        with torch.no_grad():
            next_action = self.actor(next_state)
            target_q_value = min(
                self.target_q1(torch.cat([next_state, next_action], dim=-1)),
                self.target_q2(torch.cat([next_state, next_action], dim=-1))
            )
            target = reward + (1 - done) * 0.99 * target_q_value
        
        q1_loss = nn.MSELoss()(self.q1_network(torch.cat([state, action], dim=-1)), target)
        self.q1_optimizer.zero_grad()
        q1_loss.backward()
        self.q1_optimizer.step()
        
        # Actor update with entropy regularization
        alpha = torch.exp(self.log_alpha)
        new_action = self.actor(state)
        new_q_value = min(
            self.q1_network(torch.cat([state, new_action], dim=-1)),
            self.q2_network(torch.cat([state, new_action], dim=-1))
        )
        
        actor_loss = -(new_q_value - alpha * torch.tensor(0.0)).mean()  # 簡略化
        
        self.actor_optimizer.zero_grad()
        actor_loss.backward()
        self.actor_optimizer.step()

探索と利用のトレードオフ

ε-Greedy vs Boltzmann Exploration

# ε-Greedy
def epsilon_greedy(q_values, epsilon):
    if np.random.random() < epsilon:
        return np.random.randint(len(q_values))
    else:
        return np.argmax(q_values)

# Boltzmann Exploration(温度を使用)
def boltzmann_exploration(q_values, temperature):
    """
    Temperature が高い:全行動がほぼ等確率(探索的)
    Temperature が低い:最高価値行動を強く選ぶ(利用的)
    """
    scaled_q = q_values / temperature
    exp_q = np.exp(scaled_q - np.max(scaled_q))  # 数値安定性
    probs = exp_q / exp_q.sum()
    return np.random.choice(len(q_values), p=probs)

# Upper Confidence Bound (UCB)
def ucb_select(q_values, counts, total_count, c=1.0):
    """
    Q(a) + c * sqrt(ln(N) / N(a))
    
    未試行の行動にボーナスを付与
    """
    return np.argmax(q_values + c * np.sqrt(np.log(total_count) / (counts + 1e-8)))

環境の設定と評価

Gymnasium(OpenAI Gym の後継)の使用

import gymnasium as gym

# 環境作成
env = gym.make('CartPole-v1', render_mode='human')
state, info = env.reset()

# トレーニングループ
episode_reward = 0
for step in range(500):
    action = agent.select_action(state)
    next_state, reward, terminated, truncated, info = env.step(action)
    done = terminated or truncated
    
    agent.update(state, action, reward, next_state, done)
    episode_reward += reward
    
    if done:
        break
    
    state = next_state

env.close()

性能評価メトリクス

class RLEvaluator:
    def __init__(self, env_name, n_episodes=100):
        self.env = gym.make(env_name)
        self.n_episodes = n_episodes
    
    def evaluate(self, agent, render=False):
        total_rewards = []
        
        for episode in range(self.n_episodes):
            state, _ = self.env.reset()
            episode_reward = 0
            
            while True:
                action = agent.select_action(state)
                state, reward, terminated, truncated, _ = self.env.step(action)
                episode_reward += reward
                
                if render and episode < 5:
                    self.env.render()
                
                if terminated or truncated:
                    break
            
            total_rewards.append(episode_reward)
        
        return {
            'mean': np.mean(total_rewards),
            'std': np.std(total_rewards),
            'min': np.min(total_rewards),
            'max': np.max(total_rewards)
        }

Actor-Critic: 価値と方策を組み合わせ

ActorはREINFORCEのような方策を、Criticは価値関数を学習。

代表的なアルゴリズム:

  • A3C: 複数ワーカーで並列採集、非同期更新
  • PPO: clipping を使った安定化
  • TRPO: 信頼域を厳密に保証

PPO: 実務で広く使用

PPO (Proximal Policy Optimization) は、方策更新を「信頼域内」に制限。

利点:

  • TRPOより実装が簡単で、計算効率が高い
  • ハイパーパラメータ調整に対してロバスト

PPO採用例:

  • ChatGPT の RLHF で使用
  • ロボティクス(Boston DynamicsのSpot)
  • ゲームAI(OpenAI Five)

報酬設計の実践的な罠と回避策

Reward Hacking の例と対策

例1: スコアのみ報酬

環境: ゲーム
報酬: スコア +1
結果: 安全ゾーンで無限ループ
対策: (スコア, 進行度, エネルギー)の複合報酬

例2: クリック率のみ報酬(推薦システム)

環境: ニュース推薦
報酬: クリック数
結果: 扇動的なニュースを勧める傾向
対策: 滞在時間、離脱率、長期満足度を組み込む

報酬設計のチェックリスト

  1. 短期と長期の衝突確認

    • シミュレータで最速報酬最大化戦略を手動で考える
    • 実ビジネスで望ましいか確認
  2. 単位テスト的検証

    assert reward_function(good) > reward_function(bad)
    assert reward_function(shortcut) <= reward_function(intended)
    
  3. 複数シード・複数初期化の学習

    • 運が悪いシードでも同じ方策を学ぶか検証
  4. Adversarial テスト

    • 報酬ハッキング方法をブレインストーミング
    • 実装前に罠を排除

実環境適用の段階的戦略

Phase 1: シミュレーション & 静止評価

  • Gymnasium環境でアルゴリズムを検証
  • offline evaluation(過去ログで評価)
  • 報酬設計とアルゴリズム選択の確認

Phase 2: Shadow 運用

  • 本番システムと並行してRL方策を動作
  • ログには記録するが、ユーザーへ適用しない
  • オフライン評価の精度確認

Phase 3: Limited Rollout

  • 小規模ユーザー(1-5%)へ段階的適用
  • A/Bテスト: 既存ルールベースと比較
  • 安全性メトリクス監視

Phase 4: Gradual Expansion & Monitor

  • ユーザーベースを段階的に拡大
  • リアルタイムメトリクス監視
  • 異常検知と自動ロールバック機能

評価の難しさ

強化学習では、通常の教師あり学習のように固定test setで簡単に評価できません。方策が変わると訪れる状態の分布も変わるためです。

見るべき観点:

  • 平均報酬だけでなく分散も見る
  • seedを変えて複数回評価する
  • 安全制約違反の回数を見る
  • 学習曲線を見る
  • baseline方策と比較する
  • offline評価を過信しない

実務では、いきなり本番方策へ切り替えるのではなく、shadow評価や制限付きrolloutを使います。

RL vs ルールベースとの使い分け

観点 RL ルールベース
実装時間 長い(シム構築、調整) 短い
説明可能性 低い 高い
パフォーマンス 高い(十分な学習後) エキスパート依存
保守性 ルール追加に弱い 新規則追加が容易
適用時間 数ヶ月~年 数週間
失敗時コスト 高い 低い

推奨: ルールベース + 人間判断でプロトタイプ → パフォーマンスプラトーでRL導入の混合アプローチ。

実務導入の判断

強化学習を使う前に、まず「本当に逐次意思決定の問題か」を確認します。単発の分類やランキングで済むなら、教師あり学習やルールベースの方が安全で説明しやすいことがあります。強化学習が向くのは、現在の行動が将来の状態や報酬に影響し、短期最適化だけでは良い結果にならない場合です。

導入判断では、次を先に確認します。

観点 確認すること
環境 行動の結果を観測できるか
報酬 代理指標が本当に目的を表すか
安全性 悪い行動を試す余地をどこまで許せるか
評価 本番投入前に方策を比較できるか
運用 方策が変わった理由を追跡できるか

推薦や広告では、クリックだけを報酬にすると短期的な刺激に寄りやすくなります。ロボティクスでは、シミュレータでの探索と現実での安全制約を分ける必要があります。制御では、既存の制御理論や安全なfallback方策を残したうえで、学習部分を限定する方が現実的です。

RL アルゴリズムの詳細

Q-Learning と Deep Q-Network(DQN)

Q(s, a) = 状態 s で行動 a を取った時の期待返報。ベルマン方程式: Q(s,a) = r + gamma * max(Q(s’,a’))。

Q-Learning: オフポリシー学習。探索ポリシー(epsilon-greedy)で学習データ収集、目標ポリシー(greedy)で更新。

DQN: Q 値関数をニューラルネットワークで近似。Experience Replay でミニバッチ学習。Target Network(遅延更新)で安定化。

Policy Gradient と Actor-Critic

Policy Gradient: ポリシー π(a|s) を直接学習。勾配上昇でポリシー改善。Advantage function で分散削減。

Actor-Critic: Actor(ポリシー)と Critic(価値関数)の組み合わせ。Critic で advantage 推定。

PPO(Proximal Policy Optimization)

Trust region で 大幅な ポリシー更新を防止。Clipped surrogate objective で安定化。シンプルで安定、多くの環境で最先端性能。

Model-Based RL(計画)

環境モデル(遷移確率、報酬)を学習して計画。Sample efficiency が高い。

MCTS(Monte Carlo Tree Search): AlphaGo で採用。木探索で最適行動を見つける。

https://gymnasium.farama.org/)

  • [Spinning Up in D

報酬設計と Reward Shaping

Sparse reward: ターミナルステートでのみ報酬。学習が遅い(exploration efficiency 低)。

Dense reward: 各ステップで報酬。学習高速化。ただし suboptimal local optimum risk。

Reward shaping: 中間目標で報酬追加。例:物体認識タスクで中間段階での精度向上に報酬。

Exploration vs Exploitation

Epsilon-greedy: 確率 ε で random action、1-ε で best action。

Upper Confidence Bound(UCB): 不確実性高い行動を優先。乐观主義。

Thompson sampling: 確率的に best action を推定。Bayesian approach。

Value iteration と Policy iteration

Value iteration: V(s) を反復更新。収束が遅い。

Policy iteration: ポリシーを直接改善。計算コスト高い(ポリシー評価ステップ)。

Generalized Policy Iteration: Value と Policy を交互更新。

Off-policy と On-policy

On-policy: 探索ポリシーで学習(REINFORCE)。Safe ただし sample inefficient。

Off-policy: 探索ポリシー(behavior)と学習ポリシー(target)を分離(Q-learning)。Sample efficient。重点度サンプリング で補正。

実装フレームワーク

OpenAI Gym

RL 環境の標準インターフェース。CartPole から Atari games まで。

env.reset() → env.step(action) → (observation, reward, done, info)

RLlib(Ray Tune)

分散強化学習。PPO、DQN、QMIX など多数アルゴリズム。自動 hyperparameter tuning。

Google Colab で数行で分散学習実行。

TensorFlow Agents

TensorFlow ベース。Network design の flexibility。

Actor-Critic、DQN、など実装例豊富。

AlphaGo / AlphaZero の構成

MCTS + Neural Network。Policy Network(行動選択)と Value Network(局面評価)。

Self-play で強化。

eep RL](https://spinningup.openai.com/en/latest/)

まとめ

強化学習は、逐次意思決定を学ぶための重要な枠組みです。実運用には難しさも多いですが、ロボティクス、推薦、制御などで独自の価値を持ちます。

参考文献

講義・記事

書籍

解説・補助

  • [Gymnasium Documentation](