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

Q学習(Q-Learning)とは?

Q学習のコンセプト (画像はGeminiで作成)

Q学習(Q-Learning)の概要

Q学習(Q-Learning)は、1989年にChristopher Watkinsらによって提唱された、強化学習(Reinforcement Learning: RL)における最も基礎的で有名なアルゴリズムの一つです。 この記事では、Deep Q-Network(DQN)のような高度な深層強化学習アルゴリズムを学ぶ前の基礎として、 Q学習の基本的な仕組み、ベルマン方程式の詳細な解説、そして「Cart Pole(倒立振子)」を用いた具体的なPythonコード(可視化を含む)について解説します。

強化学習の基本パイプライン

Q学習の詳細に入る前に、強化学習の基本的な枠組みをおさらいしましょう。

強化学習の環境は、以下のような要素で構成されます。

  • エージェント(Agent): 環境内で行動を起こし、学習を行う主体です。
  • 環境(Environment): エージェントが相互作用する世界やシミュレーションのことです。
  • エージェントは環境から状態(State, StS_t を受け取ります。
  • その状態に基づいて行動(Action, AtA_t を決定し、環境に働きかけます。
  • 環境は行動に応じて状態を変化させ、エージェントに報酬(Reward, Rt+1R_{t+1}次の状態(Next State, St+1S_{t+1} を返します。

エージェントの目的は、時間の経過とともに得られる報酬の合計(長期的な収益)を最大化するような「方策(Policy)」を学習することです。また、強化学習では「探索(Exploration)」と「利用(Exploitation)」のバランスを最適に保つことが重要になります。

事前知識

強化学習の基本的な枠組みや用語についてさらに詳しく知りたい方、事前知識に不安がある方は、先に 強化学習超入門 の記事をお読みいただくことをおすすめします。

ベルマン方程式とQ関数(詳細解説)

強化学習において、エージェントが将来どれくらいの報酬を得られるかを示す指標を収益(Return, GtG_t と呼びます。

Gt=Rt+1+γ(1)Rt+2+γ(2)Rt+3+=k=0Tγ(k)Rt+k+1G_t = R_{t+1} + \gamma^{(1)} R_{t+2} + \gamma^{(2)} R_{t+3} + \dots = \sum_{k=0}^{T} \gamma^{(k)} R_{t+k+1}

ここで、γ\gamma(ガンマ)は 割引率(Discount Factor) と呼ばれ、0γ10 \le \gamma \le 1 の値を取ります。これは経済学の考え方を借りたもので、遠い未来の報酬よりも目先の報酬を高く評価するためのパラメータであり、同時に数学的な収束を保証する役割も果たします。

価値関数(Value Function)と無限期間問題の回避

現実の環境は確率的(Stochastic)であるため、収益 GtG_t の期待値をとったものを状態価値関数(Value Function, VtV_t と定義します。

Vt=E[Gtst=s]=E[k=0Tγ(k)Rt+k+1st=s]V_t = E[G_t \vert{} s_t = s] = E \left[ \sum_{k=0}^{T} \gamma^{(k)} R_{t+k+1} \vert{} s_t = s \right]

割引率による発散の回避(数値例)

もしゲームが無限に続くタスク(TT \to \infty)の場合、割引率が γ=1\gamma = 1 だと、毎ステップ +1 の報酬を得続けた場合の収益が 1+1+1+=1 + 1 + 1 + \dots = \infty と無限大に発散してしまい、状態の良し悪しを比較できなくなります。 しかし、γ=0.9\gamma = 0.9 のように 11 未満に設定すると、収益は 1+0.9+0.92+0.93+1 + 0.9 + 0.9^2 + 0.9^3 + \dots という無限等比級数になります。高校数学の等比級数の和の公式 a1r\frac{a}{1 - r} を当てはめると、110.9=10\frac{1}{1 - 0.9} = 10 となり、未来永劫の報酬を足し合わせても「10」という有限の値に綺麗に収束します。割引率には、このような数学的な破綻を防ぐ重要な役割があります。

しかし、この期待値の式をそのまま計算しようとすると、未来永劫にわたるすべての状態遷移と報酬を追跡しなければならない「無限期間問題(Infinite horizon problem)」に陥ってしまいます。これを解決するのがベルマン方程式(Bellman Equation) です。1950年代にRichard Bellmanらによって提案されたこの手法は、収益を「即時報酬」と「割引された未来の価値」に再帰的に分解します。

Vt=Rt+1+γVt+1V_t = R_{t+1} + \gamma V_{t+1}

この再帰的な表現によって、無限に続く問題を解可能な部分問題に分割することができます。

Q関数(状態行動価値関数)

ベルマン方程式の考え方を状態だけでなく、「ある状態で特定の行動をとった場合」に拡張したものがQ関数(Q-function) です。Qは「Quality(品質)」を表します。

Qt=E[Gtst=s,at=a]Q_t = E[G_t \vert{} s_t = s, a_t = a]

状態 VV と違い、Q関数は状態と行動の両方に条件付けられているため、エージェントの方策を評価し、具体的な行動を選択することが容易になります。

Q学習のメカニズム:TD学習とQテーブル

Q学習は、このQ関数を表形式(Qテーブル)で保存し、更新していくアルゴリズムです。動的計画法(Dynamic Programming)のように環境の完全なモデル(遷移確率など)を必要としないモデルフリー(Model-free) な手法であり、1ステップごとに学習を行う TD(Temporal-Difference)学習 の一種です。

TD学習について

Q学習のベースとなっている「1ステップごとに予測を更新する」仕組みについてより深く学びたい方は、TD学習(時間的差分学習)とは?の記事をあわせてご覧ください。TD誤差やベルマン方程式に基づく価値更新の概念を、Pythonによるスクラッチ実装とともに詳しく解説しています。

Q学習の更新式は以下のようになります。

Q(st,at)Q(st,at)+α[rt+γmaxaQ(st+1,a)Q(st,at)]Q(s_t, a_t) \leftarrow Q(s_t, a_t) + \alpha \left[ r_{t} + \gamma \max_{a} Q(s_{t+1}, a) - Q(s_t, a_t) \right]

  • α\alpha : 学習率(Learning Rate)
  • rt+γmaxaQ(st+1,a)r_{t} + \gamma \max_{a} Q(s_{t+1}, a) : 推定された未来の価値(TDターゲット)。
  • Q(st,at)Q(s_t, a_t) : 現在のQ値(Old Q-value)。
  • 大カッコの中身全体がTD誤差(TD error) と呼ばれます。
「未来の最大価値」の具体的なイメージ

数式の中にある maxaQ(st+1,a)\max_{a} Q(s_{t+1}, a) は、「次の状態 st+1s_{t+1} に移動したあと、そこから最も賢い行動(最大のQ値を持つ行動)を選んだ場合に得られるであろう将来の価値」を意味しています。

  • グリッドワールド(迷路)の例: 現在地から「一歩右に動く」という行動をとった結果、次のマス(st+1s_{t+1} が「ゴールのすぐ隣のマス」だったとします。まだゴールに到達していませんが、次のマスにおいては「さらに右へ動く(ゴールに入る)」という行動のQ値が非常に高く設定されているはずです(maxa\max_a)。そのため、現在のマスのQ値も「次へ進めば高得点が確定している」という理由で高く更新されます。
  • Cart Pole(倒立振子)の例: 現在カートを「右に押す」という行動をとった結果、次の状態(st+1s_{t+1} が「ポールが垂直にピンと立って安定している状態」になったとします。この次状態からは、一番安全な行動(例えば少し左に押して微調整するなど)を選んだときの価値 maxaQ(st+1,a)\max_a Q(s_{t+1}, a) が高いはずです。したがって、「そもそも現在カートを右に押した行動は正解だった」として現在のQ値が引き上げられます。

つまり、「次の場所から一番良い選択をした場合のポテンシャル」を逆算し、現在の行動の評価(Q値)に組み込んでいるのがこの数式の核心(コア)です。

エージェントはQテーブルを用いて、特定の状態で取るべき行動のQ値を保存します。そして、観測された報酬と推定された将来の価値に基づいてQ値を更新します。

探索と利用(ϵ\epsilon-greedy法)

強化学習には探索と利用のトレードオフ(Exploration-Exploitation Trade-off) という最大のジレンマが存在します。

  • 利用(Exploitation): 今知っている知識の中で、一番良いと思われる行動をとること。
  • 探索(Exploration): まだ試していない行動をとり、新しい知識を得ること。

例えば、お気に入りのおいしいレストランに行く(利用)か、まだ行ったことのない新しいレストランを開拓する(探索)か、という状況と同じです。最初から「利用」ばかりしていると、もっと良い選択肢を見逃してしまうため、このバランスを最適に保つことが学習成功の鍵となります。

Q学習では、このジレンマを解決するために ϵ\epsilon-greedy法(イプシロン・グリーディ法) を用います。

  • ϵ\epsilon(小さな値、例: 0.1)の確率であえてランダムな行動を選び、新しい可能性を探索します。
  • 残りの 1ϵ1 - \epsilon の確率で、現在のQテーブルの中で最大のQ値を持つ行動を選び、既存の知識を利用します。

Q学習の実装:Cart Poleの学習と可視化

理論を理解したところで、Pythonと gymnasium を用いてCart PoleのQ学習を実装してみましょう。 Cart Poleの環境において、カートの位置や速度などの「状態」は連続値(Continuous states)を取ります。しかし、Q学習は有限サイズのテーブル(Finite-size tables)を脳として使用するため、連続的な状態や行動を持つ環境で直接学習することはできません。 そこで、連続値をいくつかの「ビン(箱)」に分割して離散化(Discretize)する処理を行います。

1. ライブラリのインストール

強化学習環境である gymnasium、数値計算用の numpy、そして可視化のための matplotlib をインストールします。

!pip install gymnasium numpy matplotlib

(※Google Colab等の環境で実行する場合は、セルの先頭に ! をつけて実行してください。)

2. 学習コードの実装

以下のコードは、連続値の状態を離散化し、Qテーブルを用いてCart Poleを学習させるスクリプトです。
カートの位置や速度などの4次元の連続データをそれぞれ6個の「ビン(区間)」に分割し、6×6×6×6=12966 \times 6 \times 6 \times 6 = 1296 個の離散的な状態として扱います。

import gymnasium as gym
import numpy as np
import matplotlib.pyplot as plt
import math

# 環境の初期化
env = gym.make('CartPole-v1')

# ハイパーパラメータの設定
EPISODES = 1000
LEARNING_RATE = 0.1 # α
DISCOUNT = 0.99 # γ
EPSILON = 1.0 # 探索率の初期値
EPSILON_DECAY = 0.995
EPSILON_MIN = 0.01

# 連続状態を離散化するためのビンの数(各次元6分割)
NUM_BINS = [6, 6, 6, 6]

# 環境の各状態(カート位置、カート速度、ポール角度、ポール角速度)の最小値・最大値
upper_bounds = [env.observation_space.high[0], 0.5, env.observation_space.high[2], math.radians(50)]
lower_bounds = [env.observation_space.low[0], -0.5, env.observation_space.low[2], -math.radians(50)]

# Qテーブルの初期化(状態の各次元のビン数 × 行動数)
q_table = np.random.uniform(low=-1, high=1, size=(NUM_BINS + [env.action_space.n]))

# 状態を離散化(インデックス化)する関数
def discretize_state(state):
discrete_state = []
for i in range(len(state)):
# 最小値・最大値の範囲内にクリップ
val = np.clip(state[i], lower_bounds[i], upper_bounds[i])
# 範囲を0〜1に正規化し、ビン数に掛けてインデックスを取得
ratio = (val - lower_bounds[i]) / (upper_bounds[i] - lower_bounds[i])
bin_idx = int(round((NUM_BINS[i] - 1) * ratio))
discrete_state.append(bin_idx)
return tuple(discrete_state)

# 学習ループ
scores = []
for episode in range(EPISODES):
state, _ = env.reset()
discrete_state = discretize_state(state)
done = False
score = 0

while not done:
# epsilon-greedy法による行動選択
if np.random.random() < EPSILON:
action = env.action_space.sample() # 探索
else:
action = np.argmax(q_table[discrete_state]) # 利用

next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
next_discrete_state = discretize_state(next_state)

# Q値の更新(ベルマン方程式に基づく更新式)
old_q_value = q_table[discrete_state + (action,)]
next_max = np.max(q_table[next_discrete_state])

# 新しいQ値の計算
new_q_value = old_q_value + LEARNING_RATE * (reward + DISCOUNT * next_max - old_q_value)
q_table[discrete_state + (action,)] = new_q_value

discrete_state = next_discrete_state
score += reward

# epsilonの減衰
if EPSILON > EPSILON_MIN:
EPSILON *= EPSILON_DECAY

scores.append(score)
if (episode + 1) % 100 == 0:
print(f"Episode {episode + 1}: Average Score (last 100): {np.mean(scores[-100:])}, EPSILON:{EPSILON}")

print("学習完了!")
env.close()

【コードのポイント】

  • 状態の離散化(discretize_state: gymnasium から返される連続値のリストを、Qテーブルのインデックスとして使えるように整数のタプルに変換しています。
  • 行動選択: np.random.random() < EPSILON の場合はランダムに行動し(探索)、それ以外は np.argmax(q_table[discrete_state]) で現在の最大Q値の行動(利用)を選びます。
  • Q値の更新: 先ほど解説したベルマン方程式に基づくTD学習の更新式 old_q_value + LEARNING_RATE * (...) をそのままコードに落とし込んでいます。

学習コードの実行結果

Episode 100: Average Score (last 100): 19.45, EPSILON:0.6057704364907278
Episode 200: Average Score (last 100): 20.15, EPSILON:0.3669578217261671
Episode 300: Average Score (last 100): 20.26, EPSILON:0.22229219984074702
Episode 400: Average Score (last 100): 20.6, EPSILON:0.1346580429260134
Episode 500: Average Score (last 100): 21.6, EPSILON:0.08157186144027828
Episode 600: Average Score (last 100): 23.86, EPSILON:0.0494138221100385
Episode 700: Average Score (last 100): 25.16, EPSILON:0.029933432588273214
Episode 800: Average Score (last 100): 30.13, EPSILON:0.018132788524664028
Episode 900: Average Score (last 100): 35.29, EPSILON:0.01098430721937979
Episode 1000: Average Score (last 100): 32.28, EPSILON:0.00998645168764533
学習完了!

エピソードが進むごとに ϵ\epsilon(探索率)が減衰し、エージェントが徐々に「利用(学習した知識の活用)」にシフトしていることがわかります。それに伴い、直近100エピソードの平均スコアも上昇していきます。

3. 可視化(エージェントの動きを見る)

学習が終わったQテーブルを用いて、実際にCart Poleがどのようにバランスを取るのかをアニメーション(レンダリング)で確認するコードです。 ここではGoogle Colabなどのクラウド環境でも動作するように、画像を配列として取得し、Matplotlibを使ってアニメーションとして表示します。

from matplotlib import animation
from IPython.display import HTML

# 可視化用の環境を再作成(Colab用にrgb_arrayを指定)
env = gym.make('CartPole-v1', render_mode='rgb_array')

state, _ = env.reset()
discrete_state = discretize_state(state)
done = False
total_reward = 0
frames = [] # 動画用のアニメーションフレームを保存するリスト

print("--- 学習済みエージェントのテスト開始 ---")
while not done:
# 現在の環境の描画データを配列として取得し、リストに追加
frames.append(env.render())

# テスト時は探索(epsilon)を行わず、学習済みのQテーブルから最適な行動を常に選ぶ
action = np.argmax(q_table[discrete_state])
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated

discrete_state = discretize_state(next_state)
total_reward += reward

print(f"テスト完了: 合計スコア = {total_reward}")
env.close()

# アニメーションを作成し、Colab上に表示する
print("アニメーションを生成中...")
fig = plt.figure(figsize=(frames[0].shape[1] / 72.0, frames[0].shape[0] / 72.0), dpi=72)
ax = fig.add_subplot(111)
ax.axis('off')

patch = ax.imshow(frames[0])

def animate(i):
patch.set_data(frames[i])

anim = animation.FuncAnimation(plt.gcf(), animate, frames=len(frames), interval=50)
plt.close(fig)

HTML(anim.to_jshtml())

このコードでは、学習が完了した q_table を使用して、エージェントが自律的にバランスを取る様子をシミュレーションし、アニメーションとして描画しています。 注目すべき点は、学習時と異なり epsilon-greedy 法によるランダムな「探索」を一切行わず、常に np.argmax(q_table[discrete_state]) によって「利用(もっともQ値が高い行動の選択)」を行っている点です。

レンダリングモード(render_mode)の使い分けについて

本記事の実装コードでは、Google Colabなどのブラウザ・クラウド環境でもエラーなく動作し、その場でアニメーションを確認できるように、render_mode='rgb_array'matplotlib を組み合わせた動画再生方式を採用しています。

もし、ご自身のローカルPC(GUIが利用できる環境)で直接ポップアップウィンドウを開いてリアルタイム描画を確認したい場合は、以下のように変更して実行することも可能です。

  • 変更点1: 環境作成時に render_mode='human' を指定する。 env = gym.make('CartPole-v1', render_mode='human')
  • 変更点2: 各ステップでの frames.append(env.render()) や、Matplotlibによるアニメーション生成・再生用のコードをすべて削除する。

可視化コードの実行結果

--- 学習済みエージェントのテスト開始 ---
テスト完了: 合計スコア = 34.0
アニメーションを生成中...

可視化コードの実行結果

上記の処理を実行すると、Jupyter NotebookやColab上でインラインの動画プレーヤーが表示され、エージェントが左右に動いてポールを落とさないようバランスを取る様子がアニメーションとして確認できます。

Qテーブルの限界とDQNへの橋渡し

上記のコードで見たように、Q学習はシンプルでありながら強力なアルゴリズムです。しかし、状態を離散化したことにお気づきでしょうか。

状態空間や行動空間が巨大な問題(例えば、Atariのゲーム画面のような高次元のピクセルデータ)では、すべての状態を網羅する有限サイズのテーブルを作成することは不可能です。この問題は「次元の呪い」と呼ばれます。

この限界を突破するために、Qテーブルという「表」の代わりに、「ニューラルネットワーク(NN)」を関数近似器として用いたのがDeep Q-Network (DQN)です。DQNの登場により、強化学習は連続的な状態空間や複雑な画像入力を持つタスクでも、人間を超えるパフォーマンスを発揮できるようになりました。

ここまでのQ学習の基礎とベルマン方程式の仕組みを理解していれば、DQNにおけるニューラルネットワークの損失関数が「なぜあの形(TD誤差の最小化)をしているのか」がはっきりと理解できるはずです。

まとめ

この記事では、強化学習の根幹をなす「Q学習」の基本的な仕組みとベルマン方程式の意味について解説し、Pythonを用いたCart Poleの実装を行いました。

  • 強化学習の目的 は、累積報酬(収益)を最大化する方策を見つけることです。
  • ベルマン方程式 は、無限に続く未来の価値の計算を、再帰的な部分問題(即時報酬+割引された未来の価値)に落とし込みます。
  • Q学習 は、各状態での行動の価値(Q値)をテーブルに記録し、経験を通じてTD誤差を修正しながら学習を進めるモデルフリーな手法です。
  • 連続的な状態空間では 離散化(ビン分割) が必要になります。

Q学習のアルゴリズムは非常にシンプルですが、これをニューラルネットワークに置き換えた「DQN」などの深層強化学習(Deep RL)を理解するための重要な土台となります。

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