0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで〇×ゲームのAIを一から作成する その251 ε-greedy法の長期的な性質と桁落ち誤差によるバグの修正

0
Last updated at Posted at 2026-10-25

目次と前回の記事

Python のバージョンとこれまでに作成したモジュール

本記事のプログラムは Python のバージョン 3.13 で実行しています。また、numpy のバージョンは 2.3.5 です。

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
mbtest.py テストに関する関数
util.py ユーティリティ関数の定義
tree.py ゲーム木に関する Node、Mbtree クラスなどの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

今回の記事の内容

前回の記事では、探索に全振りした「完全ランダム」と、活用に全振りした「貪欲法」という、真逆の性質を持つ 2 つの極端なアルゴリズムを多角的な視点からグラフ化しました。その結果、多腕バンディット問題(強化学習)の目的関数を最大化するためには、「探索」と「活用」のどちらか片方に偏るのではなく、両者をいかにバランスよく、そして適切な順番で組み合わせるかが極めて重要であることを明らかにしました。

今回の記事からは、このトレードオフのジレンマを解決するための「真の強化学習アルゴリズム」の解説を行います。今回の記事ではその手始めとして、最もシンプルながら強力な能力を持つ ε-greedy法(イプシロン・グリーディ法)を紹介します。

ε-greedy 法(イプシロン・グリーディ法)

ε-greedy 法(イプシロン・グリーディ法)の考え方は非常に単純で、「毎回、探索と活用のどちらを行うかを、あらかじめ決めておいた特定の確率で決める」というものです。

ε-greedy 法では、各ステップで探索を行う確率を $\boldsymbol{ε}$(イプシロン) という記号で表します。ε は確率なので、必ず 0 以上 1 以下の値を設定します。

ε-greedy 法の方策

具体的には、以下のようなルール(方策)で次に選択するスロットマシンを決めます。

  • $\boldsymbol{ε}$ の確率 で、ランダムにスロットマシンを選択する(探索)
  • $\boldsymbol{1 - ε}$ の確率(残りの確率)で、これまでに一番当たっている(当たりの確率の推定値が最大となる)スロットマシンを選択する(活用)

例えば $ε = 0.1$ と設定した場合は、毎回の選択で 10% の確率で冒険(探索) し、残りの 90% の確率で手堅く稼ぐ(活用) ことになります。

ハイパーパラメータとは何か

機械学習では、この $ε$ のように 「学習を始める前に、人間があらかじめ決めておく設定値」 の事を ハイパーパラメータ と呼びます。

それに対して、学習を進める中で変化していく、「各スロットマシンの当たりの確率の推定値」などを、単に パラメータ と呼んで区別します。

つまり、ε-greedy 法は、人間が設定する $ε$ の値によって、アルゴリズムの性格が大きく変わるのが特徴です。

実際に、$ε = 1$(100 % 探索)とした場合は常に探索を行うという「完全ランダム」なアルゴリズムに、$ε = 0$(0 % 探索)とした場合は常に活用を行うという「貪欲法」に完全に一致します。

そこで、今回の記事では、この $ε$ をいくつかの値に変えてみて、アルゴリズムの性質や動きがどのように変化するかをグラフで可視化することにします。

上記では、「探索」と「活用」を場合分けして説明しましたが、数学的には方策を 1 つの確率の式にまとめることもできます。

スロットマシンが全部で $n$ 台あるとします。

  1. 探索が選ばれたとき(確率 $ε$):
    どのマシンも均等に選ばれるため、1 台あたりの選ばれる確率は $\frac{ε}{n}$ となる
  2. 活用が選ばれたとき(確率 $1 - ε$):
    「これまでの最高の当たり台」だけが $1 - ε$ の確率で選ばれ、それ以外のスロットマシンが選ばれる確率は $0$ となる

この 2 つは独立(同時に起きることはない)しているため、足し算することができます。そのため、最終的に各スロットマシンが選ばれる確率は以下のようになります。

  • これまでの最高の当たり台が選ばれる確率1:$(1 - ε) + \frac{ε}{n}$
  • それ以外のスロットマシンが選ばれる確率:$\frac{ε}{n}$

一見すると少し複雑な式に見えますが、やっていることは「場合分け」と同じです。なお、この記事では直感的なわかりやすさを重視して、以降も「探索」と「活用」に分けた状態で説明を進めていきます。

短期的な性質と長期的な性質

ε-greedy 法の性質は、ステップ数が数百回くらいまでの「短期的な段階(まだデータの蓄積が少ない時期)」と、数千回を超えた「長期的な段階(十分にデータが集まった時期)」とで、その特徴が大きく変わります。

今回の記事では、ε-greedy 法の動きが直感的にも、視覚的にもわかりやすい、「ステップ数が 1 万回の時点(長期的な視点での性質)」 について詳しく説明します。

なお、データが集まりきっていない「短期的な視点での性質」については、次回の記事で詳しく解説します。

Epsilon_Greedy_MAB_Agent クラスの定義

ε-greedy 法で多腕バンディット問題を解く Epsilon_Greedy_MAB_Agent クラスを定義します。前回の記事で作成した貪欲法(Greedy_MAB_Agent)と同様に、共通の土台である MAB_Agent クラス2を継承することで、下記のように簡潔に実装することができます。

  • 7 ~ 10 行目(__init__ メソッド)
    新しく $ε$ を自由に設定できるように、元々の __init__ メソッドを オーバーライド して再定義する
    • 7 行目:$ε$ を受け取るための仮引数 epsilon を追加する
    • 9 行目:受け取った $ε$ の値を後で使えるように、epsilon 属性に代入して記録する
    • 10 行目:グラフのタイトルに、どの $ε$ の設定で実験したかが一目でわかるように $ε$ の値を入れる
  • 12 ~ 21 行目(select_arm メソッド)
    次に選択するスロットマシンを決定するメソッドの定義。13 行目で np.random.uniform() を使って「0.0 以上 1.0 未満のランダムな数値(一様乱数)」を 1 つ発生させる。これが設定した $ε$ よりも小さければ「探索(14 行目)」を、そうでなければ「活用(16 ~ 21 行目)」を行う3
 1  from ai import MAB, MAB_Agent
 2  import numpy as np
 3  
 4  class Epsilon_Greedy_MAB_Agent(MAB_Agent):
 5      NAME = "ε-greedy 法"
 6  
 7      def __init__(self, p, epsilon):
 8          self.mab = MAB(p=p)
 9          self.epsilon = epsilon
10          self.title = f"{self.NAME}(ε={epsilon})"
11  
12      def select_arm(self):
13          if np.random.uniform() < self.epsilon:
14              return np.random.choice(self.mab.num)
15          else:
16              maxcoin = max(self.slot["mean_coin"])
17              bestarms = []
18              for arm in range(self.mab.num):
19                  if maxcoin == self.slot["mean_coin"][arm]:
20                      bestarms.append(arm)           
21              return np.random.choice(bestarms)
行番号のないプログラム
from ai import MAB, MAB_Agent
import numpy as np

class Epsilon_Greedy_MAB_Agent(MAB_Agent):
    NAME = "ε-greedy 法"

    def __init__(self, p, epsilon):
        self.mab = MAB(p=p)
        self.epsilon = epsilon
        self.title = f"{self.NAME}(ε={epsilon})"
        
    def select_arm(self):
        if np.random.uniform() < self.epsilon:
            return np.random.choice(self.mab.num)
        else:
            maxcoin = max(self.slot["mean_coin"])
            bestarms = []
            for arm in range(self.mab.num):
                if maxcoin == self.slot["mean_coin"][arm]:
                    bestarms.append(arm)           
            return np.random.choice(bestarms)

先程のノートで説明した通り、「探索」と「活用」を 1 つの方策(確率の式)にまとめて select_arm メソッドを記述することも不可能ではありません。しかし、その場合は各スロットマシンが選ばれる確率を毎回個別に計算して np.random.choice に確率の重み(引数 p)として渡す必要があり、プログラムがかなり複雑になってしまいます。

そのため、このように if 文を使ってシンプルに場合分けして記述するほうが、プログラムがはるかにわかりやすくなります。

複数エピソードの実行と保存

今回の記事でも、$ε$ の設定値によって ε-greedy 法の性質がどのように変化するかを明らかにするために、大規模なシミュレーションを行います。実験の条件は、これまでの記事とまったく同じ以下の条件を設定します。

  • スロットマシンの台数:3 台
  • 各スロットマシンの当たりの真の確率:[0.3, 0.5, 0.7]

この環境に対して、 $ε$ の値を [0, 0.25, 0.5, 0.75, 1.0] の 5 種類に変化させ、アルゴリズムの挙動にどのような違いが現れるかを検証します。

下記はその処理を行うプログラムです。

  • 1 行目:5 つのエージェントを記録する egreedy_agents を空の list で初期化する
  • 4 ~ 9 行目:繰り返し処理で、ε が [0, 0.25, 0.5, 0.75, 1.0]4 のそれぞれの場合に対して、ステップ数が 1 万、エピソード数が 1,000 の設定で多腕バンディット問題を解く。計算結果を後で再利用できるようにするために5、データを保存するフォルダ("../data/")に "egreedy_agent(e=ε の値).pkl" というファイル名で保存する
1  egreedy_agents = []
2  
3  p = [ 0.3, 0.5, 0.7 ]
4  for i in range(5):
5      epsilon = i / 4
6      egreedy_agent = Epsilon_Greedy_MAB_Agent(p=p, epsilon=epsilon)
7      egreedy_agent.play_episodes(stepnum=10000, episodenum=1000)
8      egreedy_agent.save(f"../data/egreedy_agent(e={epsilon})")
9      egreedy_agents.append(egreedy_agent)
行番号のないプログラム
egreedy_agents = []

p = [ 0.3, 0.5, 0.7 ]
for i in range(5):
    epsilon = i / 4
    egreedy_agent = Epsilon_Greedy_MAB_Agent(p=p, epsilon=epsilon)
    egreedy_agent.play_episodes(stepnum=10000, episodenum=1000)
    egreedy_agent.save(f"../data/egreedy_agent(e={epsilon})")
    egreedy_agents.append(egreedy_agent)

実行結果

100%|██████████| 1000/1000 [05:23<00:00,  3.09it/s]
save completed.
100%|██████████| 1000/1000 [05:14<00:00,  3.18it/s]
save completed.
100%|██████████| 1000/1000 [05:08<00:00,  3.24it/s]
save completed.
100%|██████████| 1000/1000 [04:55<00:00,  3.38it/s]
save completed.
100%|██████████| 1000/1000 [05:04<00:00,  3.29it/s]
save completed.

集計結果のまとめの表示

グラフを表示する前に、前回の記事と同様に、まずは print_summary メソッドを呼び出してそれぞれの実験結果の最終ステップ(9,999 ステップ6)である、長期的な視点での統計データを文字(テキスト)で出力し、設定した $ε$ の値によって数値がどう変化しているかを考察します。

for agent in egreedy_agents:
    agent.print_summary()

実行結果

ε-greedy 法(ε=0.0) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  7008
得られたコインの平均  = 0.701
スロットマシン 0 (p=0.30)
  得られたコインの総数 =     0
  レバーを引いた回数  =     1
  得られたコインの平均 = 0.000
スロットマシン 1 (p=0.50)
  得られたコインの総数 =     0
  レバーを引いた回数  =     2
  得られたコインの平均 = 0.000
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  7008
  レバーを引いた回数  =  9997
  得られたコインの平均 = 0.701
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.556
最終ステップのステップ当たりのコインの標準偏差 = 0.151
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.061
  最終ステップの当たりの確率の推測値の標準偏差 = 0.11499
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.158
  最終ステップの当たりの確率の推測値の標準偏差 = 0.24023
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.336
  最終ステップの当たりの確率の推測値の標準偏差 = 0.35303

ε-greedy 法(ε=0.25) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  6442
得られたコインの平均  = 0.644
スロットマシン 0 (p=0.30)
  得られたコインの総数 =   241
  レバーを引いた回数  =   819
  得られたコインの平均 = 0.294
スロットマシン 1 (p=0.50)
  得られたコインの総数 =   440
  レバーを引いた回数  =   885
  得られたコインの平均 = 0.497
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  5761
  レバーを引いた回数  =  8296
  得られたコインの平均 = 0.694
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.649
最終ステップのステップ当たりのコインの標準偏差 = 0.011
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.300
  最終ステップの当たりの確率の推測値の標準偏差 = 0.02245
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.501
  最終ステップの当たりの確率の推測値の標準偏差 = 0.01746
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.700
  最終ステップの当たりの確率の推測値の標準偏差 = 0.03674

ε-greedy 法(ε=0.5) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  6008
得られたコインの平均  = 0.601
スロットマシン 0 (p=0.30)
  得られたコインの総数 =   490
  レバーを引いた回数  =  1634
  得られたコインの平均 = 0.300
スロットマシン 1 (p=0.50)
  得られたコインの総数 =   830
  レバーを引いた回数  =  1676
  得られたコインの平均 = 0.495
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  4688
  レバーを引いた回数  =  6690
  得られたコインの平均 = 0.701
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.600
最終ステップのステップ当たりのコインの標準偏差 = 0.051
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.300
  最終ステップの当たりの確率の推測値の標準偏差 = 0.02028
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.500
  最終ステップの当たりの確率の推測値の標準偏差 = 0.01176
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.700
  最終ステップの当たりの確率の推測値の標準偏差 = 0.03662

ε-greedy 法(ε=0.75) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  5485
得られたコインの平均  = 0.548
スロットマシン 0 (p=0.30)
  得られたコインの総数 =   737
  レバーを引いた回数  =  2513
  得られたコインの平均 = 0.293
スロットマシン 1 (p=0.50)
  得られたコインの総数 =  1281
  レバーを引いた回数  =  2513
  得られたコインの平均 = 0.510
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  3467
  レバーを引いた回数  =  4974
  得られたコインの平均 = 0.697
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.550
最終ステップのステップ当たりのコインの標準偏差 = 0.037
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.300
  最終ステップの当たりの確率の推測値の標準偏差 = 0.02371
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.500
  最終ステップの当たりの確率の推測値の標準偏差 = 0.01006
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.700
  最終ステップの当たりの確率の推測値の標準偏差 = 0.03699

ε-greedy 法(ε=1.0) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  4971
得られたコインの平均  = 0.497
スロットマシン 0 (p=0.30)
  得られたコインの総数 =  1004
  レバーを引いた回数  =  3406
  得られたコインの平均 = 0.295
スロットマシン 1 (p=0.50)
  得られたコインの総数 =  1659
  レバーを引いた回数  =  3307
  得られたコインの平均 = 0.502
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  2308
  レバーを引いた回数  =  3287
  得られたコインの平均 = 0.702
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.500
最終ステップのステップ当たりのコインの標準偏差 = 0.005
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.300
  最終ステップの当たりの確率の推測値の標準偏差 = 0.02333
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.500
  最終ステップの当たりの確率の推測値の標準偏差 = 0.00868
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.700
  最終ステップの当たりの確率の推測値の標準偏差 = 0.03809

最後のエピソードのデータのまとめと考察

下記は print_summary で表示された、「最後に行われた 1 エピソードの結果」をまとめた表です。$ε = 0$ と $ε = 1$ の場合は、前回の記事で検証した「貪欲法」と「完全ランダム」と全く同じアルゴリズムです。そのため完全ランダムの場合の実験結果は、前回の記事の実験結果とほぼ同じ数値になります。一方、貪欲法の場合は、前回の記事と異なり、最初に「スロット 0」で当たりがでたため、結果が大きく異なります。

ε=0.0
(貪欲法)
ε=0.25 ε=0.5 ε=0.75 ε=1.0
(完全ランダム)
得られたコインの総数 7008 6442 6000 5485 4971
得られたコインの平均 0.701 0.644 0.601 0.548 0.497
コインの総数
(スロット 0)
0 241 490 737 1004
引いた回数
(スロット 0)
1 819 1634 2513 3406
コインの平均
(スロット 0)
0.294 0.300 0.300 0.293 0.295
コインの総数
(スロット 1)
0 448 830 1281 1659
引いた回数
(スロット 1)
2 885 1676 2513 3307
コインの平均
(スロット 1)
0.000 0.497 0.495 0.510 0.502
コインの総数
(スロット 2)
7008 5761 4688 3467 2388
引いた回数
(スロット 2)
9997 8296 6690 4974 3287
コインの平均
(スロット 2)
0.701 0.694 0.701 0.697 0.702

この表を観察すると、ランダムに選択する確率である $ε$ の値を大きくしていくにつれて、各数値が次のように変化していくという性質が見えてきます。

  • 全体のトータルスコア(得られたコインの平均)
    • $ε = 0$ のときの最高の実力 0.7(神の視点での上限)から、$ε = 1.0$ に向かって全体の平均値である 0.5 へと減少していく
  • ハズレ台(スロット 0 と 1)のデータ
    • 引いた回数:0 回付近から、3 台への平等な割り当てである「全体の 1/3(約 3,333 回)」に向かって増加していく
    • コインの総数:回数が増えるのに比例して、0 枚から「真の確率 $\times$ 約3,333回」の枚数に向かって増加していく
    • コインの平均(精度):どの $ε$ であっても、大数の法則が働くだけの十分な回数が引かれるため、それぞれの真の実力(0.3 や 0.5)へ正確に収束している
  • 本命の当たり台(スロット 2)のデータ
    • 引いた回数:ほぼ引きっぱなしの 1 万回から、完全ランダム時の「全体の 1/3(約3,333回)」に向かって減少していく
    • コインの総数:回数が減るのに比例して、「真の確率(0.7)$\times$ 1 万 (= 7,000 枚)」から「0.7 $\times$ 約3,333」に向かって減少していく

データから読み取れる性質の比較と考察

このような結果になった理由について詳しく説明します。

ε-greedy 法は、全体の「$ε$ の確率」で完全にランダムな探索を行います。つまり、1万ステップのエピソードの中では、平均して ステップ数 × $ε$ 回 の探索が行われます。

探索では、3 台のどのスロットマシンも完全に平等に選択されるため、各スロットマシンは最低でも以下の数式で計算できる回数だけ、確実に試行(探索)されることになります。

$$\text{各マシンの最低試行回数} = \frac{\text{総ステップ数} \times ε}{\text{スロットマシンの台数}}$$

この理論式を、今回の「1万ステップ」、「3 台」という実験条件に当てはめると、各スロットマシンが 最低限選ばれるはずの理論値 は次のようになります。

ε の設定値 1台あたりの最低試行回数(理論値)
0 0
0.25 約 833 回
0.5 約 1,667 回
0.75 約 2,500 回
1.0 約 3,333 回

なぜ ε > 0 の場合に「最高の当たり台」を見抜けるのか?

上記の表から、$ε = 0$ の場合を除けば、どの設定であっても すべてのスロットマシンが最低でも 800 回以上は確実に選択される ことがわかります。

これだけたくさんレバーを引けば、大数の法則が強力に働きます。そのため、AI はデータ不足による勘違いを起こすことなく、「スロット 2(確率 0.7)」が最高の当たり台(神台)であることをほぼ確実に見抜けるようになります。

神台が判明した後は、「活用(残りの $1 - ε$ の確率)」が選ばれたステップでは、AI は常に「スロット 2」のレバーだけを勘違いすることなく確実に引き続けることになります。

実際に、先ほどの print_summary のデータ表を見直してみると、ハズレ台である「スロット 0」と「スロット 1」が選ばれた回数は、上記の表の理論値(最低試行回数)とほぼぴったり一致している ことが確認できます。これは、AI がシミュレーションの「かなり早い段階」で「スロット 2」が最高の当たり台であることを見抜き、それ以降の「活用」ではハズレ台を引くという無駄な選択を完全にストップできていた証拠です。

ε = 0(貪欲法)の危険性を再確認する

一方で、一度も探索を行わない $ε = 0$(貪欲法)の結果は 7,008枚 という理論上の最高のスコアを叩き出しています。実際に内訳を見ると、1 万回中 9,997 回も「スロット 2」が選択されていることから、最初から最後までほぼ「スロット 2」だけを引き続けています。

前回の記事で検証した時は、たまたま最初にハズレ台(スロット 0)で当たりが出てしまったために、一生そのハズレ台に縛られる「自滅ループ」が起きていました。しかし今回のエピソードでは、運良く最高の当たり台である「スロット 2」のレバーを最初に引いたときに当たりが出たため、最初から最後まで正解の台を引き続けることに成功したのです。

このように、貪欲法($ε = 0$)は 「たまたま 1 回目の運が良かっただけで最高のスコアになる」 という非常に激しいハイリスクハイリターンのギャンブル的な特性を持っています。

この 1 回きりの偶然の結果だけを見て、「$ε = 0$ の貪欲法が最強のアルゴリズムだ!」と判断してしまうのがいかに危険(早計)であるかが、数学的にも裏付けられました。

先ほどの $ε = 0$ の結果をよく見ると、「スロット 0」が 1 回、「スロット 1」が 2 回選択されています。貪欲法では「最初に選択されたスロットマシンだけを引くのでは?」と思うかもしれませんが、これは以下のような挙動が起きたためです。

  1. いずれかのスロットマシンで当たりが出るまでは、すべてのスロットマシンの当たりの確率の推定値が「0」になる。そのため、活用しか行わない貪欲法でも、3 台の中からランダム選択される
  2. 「スロット 2」で最初に当たりが出るまでのステップで、「スロット 0」が 1 回、「スロット 1」が 2 回選択され、そのいずれの場合でもハズレを引いた7
  3. 一度「スロット 2」で当たりが出ると、「スロット 2」の当たりの推定値だけがプラスになるため、それ以降の残りのすべてにおいて、AIは「スロット 2」だけをひたすら「えこひいき」して引き続けることになる

このようにアルゴリズムの動きを厳密に追うことで、出力された数値の意味を説明することができます。

1,000 エピソードのまとめと考察

1 回きりの偶然の結果(1 エピソード)は、乱数(運・不運)によってデータが大きく変わってしまいます。そこで、運の要素を完全に消し去り、アルゴリズムが持つ「真の実力」を明らかにするために、1,000 回もの実験を重ね合わせた最終ステップ(1 万回目)の集計データを比較してみることにします。。

下記は print_summary が出力した、1,000エピソードの集計結果をまとめた表です。

ε=0.0 ε=0.25 ε=0.5 ε=0.75 ε=1.0
ステップ当たりの
コインの平均
0.556 0.649 0.600 0.550 0.500
当たりの推測値の平均
(スロット 0)
0.061 0.300 0.300 0.300 0.300
当たりの推測値の平均
(スロット 1)
0.158 0.501 0.500 0.500 0.500
当たりの推測値の平均
(スロット 2)
0.336 0.700 0.700 0.700 0.700

実は、これまでのプログラムの集計処理には、計算される標準偏差の値に大きな誤差が生じてしまうというバグ が隠されていることがわかりました。そこで、混乱を防ぐために上記の表からは標準偏差のデータをあえて削除しています。このバグの正体と修正方法については、今回の記事の後半で解説します。

この表から、ランダムに探索する確率である $ε$ の値を大きくしていくにつれて、長期的な視点での実力が次のように変化していく様子が見えてきます。

  • ステップ当たりのコインの平均(全体のスコア)
    $ε = 0$ の 0.556 から、$ε = 0.25$ にした瞬間に 0.649 へと大きく数値が跳ね上がります。しかし、ここをピークにして、今度は完全ランダム($ε = 1$)の 0.500 に向かって右肩下がりに減少していきます。
  • 各スロットの当たりの推測値の平均(実力を見抜く精度)
    思考停止のループが起きる $ε = 0$ の場合だけ、真の確率(0.3 / 0.5 / 0.7)から大幅にハズれた低い数値になっています。しかし、$ε \gt 0$ の場合は、すべてのスロットマシンが本来の確率通りの数値へとほぼ完璧に収束しています。

先ほどの「1 エピソードだけをまとめた表」では、$ε$ が増えるほど獲得コインの平均が綺麗に右肩下がりに減っていました。しかしそれは、$ε = 0.0$ の貪欲法が「たまたま最高の当たり台(スロット 2)を1 発目で引き当てた」からです。

1,000 回も実験をやり直して平均をとると、貪欲法($ε = 0$)の本当の平均スコアは 0.556 という低い値まで低迷してしまいます。なぜなら、3 回に 1 回は最悪のハズレ台を掴んで一生引き続けてしまうからです。貪欲法の性質に対するより詳しい考察は前回の記事を参照して下さい。

長期的な視点での「ステップ当たりのコインの平均」の理論値

今回の実験のように、総ステップ数を 10,000 のような大きな値に設定した場合は、$ε$ をよほど極端に小さな値(たとえば 0.0001 など)に設定しない限り、AI はシミュレーションの「かなり早い段階」で、真の確率が最も高いスロットマシン(スロット 2)を正しく見抜けるようになります。

最高の当たり台が完全に判明した後は、それ以降のステップで得られるコインの枚数は、次の 2 つのモードの組み合わせによって数学的に計算できるようになります。

  1. 探索モード(確率 $ε$):すべての台を均等に引くため、得られるコインの期待値は全スロットマシンの平均確率である $p_{mean} = 0.5$ になる
  2. 活用モード(確率 $1 - ε$):正解の神台だけを引き続けるため、得られるコインの期待値は最高の真の確率である $p_{max} = 0.7$ になる

したがって、この 2 つを足し合わせた下記の式は、データが集まりきった後の 「1 ステップあたりに得られるコインの平均枚数(期待値)」 を表します。

$$ε \times p_{mean} + (1 - ε) \times p_{max}$$

$ε$ が 0 でない限り、どれだけ $ε$ を小さな値に設定したとしても、ステップ数を大きくしていけば、いずれはすべてのスロットマシンが「実力を見抜くのに十分な回数」だけ探索されるようになります。そのため、十分なデータが溜まった長期的な段階では、活用モードのときに常に最高の当たり台(スロット 2)だけを選択できるようになります。

そのため、ステップ数を一定以上に大きくすると、いずれは 1 ステップあたりの平均報酬は常にこの期待値の式で固定されます。これは、全体の試行回数を増やせば増やすほど、「ステップ当たりのコインの平均」が、この数式の値に限りなく近づいていく(収束する) ということを意味しています。

今回の実験の設定値($p_{mean} = 0.5$、$p_{max} = 0.7$)をこの期待値の式に当てはめると、下記のようになります。

$$
\begin{aligned}
& ε \times 0.5 + (1 - ε) \times 0.7 \\
& = 0.5ε + 0.7 - 0.7ε \\
&= 0.7 - 0.2 \times ε
\end{aligned}
$$

この $0.7 - 0.2 \times ε$ という式を使えば、それぞれの $ε$ に対して、ステップ数が十分に大きくなった時点で「1 ステップあたりに得られるコインの平均」が何枚になるかの理論値 を簡単に計算できます。下記は実際にその値を計算してまとめた表です。

$ε$ の設定値 「ステップ当たりのコインの平均」の理論値
0.25 0.65
0.5 0.60
0.75 0.55
1.0 0.50

上記の表の理論値(0.65、0.60、0.55、0.50)は、先ほど print_summary で出力した実際のシミュレーション結果(0.649、0.600、0.550、0.500)と ほぼ完全に一致 しています。プログラムを動かして得られた実験データが、偶然の産物ではなく、数学的な理論値へと綺麗に収束していることが目に見えて実感できたのではないでしょうか。

数式から見えてくる長期的な視点での「究極の作戦」

この数式の形($0.7 - 0.2 \times ε$)をじっくり眺めると、強化学習における非常に重要な事実が明らかになります。

長期的な視点(1万回、10万回と長く遊ぶゲーム)に立った場合は、「$ε$(探索する確率)の値をできる限り小さくして、活用を行う割合($1-ε$)を増やした方が、トータルの獲得コインを最高の $p_{max}$(0.7)へと限界まで近づけることができる」 ということです。

$$ε \times p_{mean} + (1 - ε) \times p_{max}$$

短期的な場合や ε が極小の場合

上記から、例えば $ε = 0.0001$ のように、$ε$ を極限まで小さな値に設定するのが一番賢いのでは?」と思う人がいるかもしれませんが、話はそう単純ではありません。ここに強化学習のもう一つの壁である 「有限の時間(ステップ数)」 というジレンマが立ちはだかります。

詳しくは次回の記事で解説しますが、先ほどの「$ε$ を小さくするほど強い」という理論は、あくまで 「多くのステップ数をかけて、すべての台を完全に調べ尽くした後」 だけで成り立つ話です。例えば、$ε = 0.0001$(1 万分の 1)という極小の値を設定したとします。この場合は、確率を計算すると 1 万ステップの中で 平均して、たったの「1 回」しか探索が行われない ことになります。

3 台のスロットマシンがあるのに、1 回しか探索が行われないということは、探索されなかった 残りの 2 台は一度も選択されず、当たりの確率の推測値が初期値である 0 のまま最後まで変化しない 可能性が極めて高くなります。つまり、ゲームが終わるまでに神台を見抜くチャンスがほぼ訪れません。

そのことを、下記のシミュレーションで実際に検証してみます。

agent = Epsilon_Greedy_MAB_Agent(p, 0.0001)
agent.play_episodes(stepnum=10000, episodenum=1000)
agent.save(f"../data/egreedy_agent(e=0.0001)")
agent.print_summary()

実行結果

100%|██████████| 1000/1000 [04:38<00:00,  3.60it/s]
save completed.
ε-greedy 法(ε=0.0001) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  7007
得られたコインの平均  = 0.701
スロットマシン 0 (p=0.30)
  得られたコインの総数 =     0
  レバーを引いた回数  =     1
  得られたコインの平均 = 0.000
スロットマシン 1 (p=0.50)
  得られたコインの総数 =     0
  レバーを引いた回数  =     0
  得られたコインの平均 = 0.000
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  7007
  レバーを引いた回数  =  9999
  得られたコインの平均 = 0.701
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.566
最終ステップのステップ当たりのコインの標準偏差 = 0.142
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.093
  最終ステップの当たりの確率の推測値の標準偏差 = 0.16272
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.224
  最終ステップの当たりの確率の推測値の標準偏差 = 0.25415
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.386
  最終ステップの当たりの確率の推測値の標準偏差 = 0.34351

下記は先ほどの $ε = 0.0$(貪欲法)のデータと、今回の極小設定($ε = 0.0001$)の 1,000 エピソードの平均値に関するデータまとめた表です。

各エージェントの実力の比較(1,000 回の平均) ε=0.0
(貪欲法)
ε=0.0001
(極小設定)
ステップ当たりのコインの平均 0.556 0.566
当たりの推測値の平均(スロット 0) 0.061 0.093
当たりの推測値の平均(スロット 1) 0.158 0.224
当たりの推測値の平均(スロット 2) 0.336 0.386

先ほどの期待値の計算式($0.7 - 0.2 \times ε$)の通りにいけば、$ε = 0.0001$ のときのスコアは $0.7 - 0.2 \times 0.0001 = 0.69998$(約 0.7 枚)というほぼ最高のハイスコアになるはずです。しかし、実際のシミュレーション結果(実測値)は、理論値(0.699)よりも大幅に低い 0.566 という悲惨なスコアに終わっています。

表の数値をよく見ると、各スロットの実力(推測値の平均)も真の確率(0.3 / 0.5 / 0.7)には程遠く、一切冒険をしなかった $ε = 0.0$ の貪欲法とほとんど変わらない大爆死状態になっていることがわかります。このよう残念な結果になった原因は、「神台を見抜く(探索する)ための時間が圧倒的に足りなかったから」 です。

1 万回というゲームの有限の時間の中で、たった 1 回しか探索しない作戦では、データがまったく集まりません。結果として、データ不足による勘違い(まちぼうけ現象)を軌道修正することができず、1,000 回中何百回かは最初期にたまたま当たったハズレ台を一生引き続けることになります。

この実験から、以下のような、強化学習における重要な教訓が得られます。

長期的なゴールだけを見れば「探索($ε$)を減らして手堅く稼ぐ」のが最強の戦術あるが、時間が有限である以上、探索を減らしすぎるとそもそも正解にたどり着くことすらできずに自滅する。

このように、ε-greedy 法は長期的な視点と短期的な視点では、全く異なる性質 を持ちます。なお、短期的な視点における最適な $ε$ がどのような値になるかについては次回の記事で詳しく説明します。

グラフによる視覚化

ここまでの記事の考察が正しいことを、様々な視点のグラフで視覚化して確認することにします。なお、ε = 0(貪欲法)の場合の結果は運(最初の当たり台)に左右されるため、基本的にはグラフの描画を用いた考察からは外すことにします。

累積報酬の推移の比較

データが集まりきった後の長期的な性質を視覚的に確かめるため、時間の経過とともに獲得コインがどのように積み上がっていくか(累積報酬の推移)を可視化します。

貪欲法($ε = 0$)のグラフを除外するために、スライス表記([1:])を利用しています。list の 1 番以降($ε = 0.25$ 〜 $1.0$)のエージェントだけをループで回すことで、0 番の要素である $ε = 0$ のデータを取り除いて描画することができます。

なお、極端な例である $ε = 0$(貪欲法)と $ε = 1$(完全ランダム)のグラフの詳細な読み解き方については、前回の記事を参照して下さい。

for agent in egreedy_agents[1:]:
    agent.draw_cumulative_reward()

実行結果

 
 

グラフから読み取れる ε-greedy 法の性質

一連のグラフを注意深く観察することで、十分にデータが集まった長期的な視点(9,999 ステップ)において、ε-greedy 法には $ε$ の値が大きくなるにつれて以下のような対照的な特性があることが視覚的にもはっきりと確認できます。

  • 「コインの総数」(水色の実線)の傾きが緩やかになる
    $ε$ の値(探索の確率)が増えるに従って、最終的に手に入るコインの総数を表す水色の線の傾きがどんどん右下がりに緩やかになっていきます。
  • 最高の当たり台「スロット 2」(赤色の点線)の傾きが緩やかになる
    $ε$ が増加すると、あてずっぽうに台を選ぶ「探索」の割合が増えるため、せっかく見つけた最高の当たり台(スロット 2)を「活用」で引く回数が減ってしまいます。その結果、赤色の点線の傾きが緩やかになり、獲得枚数が減っていきます。
  • ハズレ台「スロット 0、1」(オレンジ色と緑色の点線)の傾きが急になる
    $ε$ が増加して探索の回数が増えると、本来なら引きたくないハズレ台(スロット 0 や スロット 1)を無駄に引いてしまう回数がどうしても増加します。その結果、底辺に沈んでいたオレンジ色と緑色の点線の傾きが、$ε$ の増加に比例してどんどん上向きに急になっていきます。

グラフ化によって可視化されたトレードオフの姿

このように複数の $ε$ の値を設定してグラフを並べてみることで、人間が設定するハイパーパラメータである $ε$ の値が、「それぞれのスロットマシンから得られるコインの配分(内訳)」をどのように変化させ、その結果としてトータルのスコアがどう左右されるかを表す下記の性質が一目で明らかになりました。

「探索($ε$)を増やせばハズレ台(オレンジ・緑)の線が浮き上がり、本命(赤)の線が下がる。探索を減らせば本命(赤)の線が神様の上限に近づく」

この直線の傾きの変化が、強化学習における「探索と活用のジレンマ」の正体です。

ステップ当たりの累積報酬の推移の比較

先ほどのトータルのグラフに続いて、貪欲法($ε = 0$)を除いた 4 つのアルゴリズムの本当の実力の推移を見極めるために、「1 ステップ当たりに稼いだコインの平均枚数」が時間の経過(ステップ数)とともにどう変化していったかを可視化します。

貪欲法($ε = 0$)と完全ランダム($ε = 1$)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

for agent in egreedy_agents[1:]:
    agent.draw_cumulative_reward_per_step()

実行結果

 
 

出力されたグラフを並べて観察すると、序盤の激しいブレが引いた後の「後半での水平線の位置」に明快な差が現れます。

$ε$ の値(探索する確率)が大きくなるにつれて、「1 ステップあたりに稼いだコインの平均枚数」のゴール地点が、先ほど数学的に導き出した下記の 理論値の式の通り、神様の上限である 黒線の $p_{max} = 0.7$ から、全員平等のラインである 水色の線の $p_{mean} = 0.5$ に向かって、段階的に減少していく様子が視覚的にもはっきりと確認できます。

$$ε \times p_{mean} + (1 - ε) \times p_{max}$$

事前の探索によって正解の神台(スロット 2)を見抜く能力はどの $ε$ でも完璧ですが、後半で「正解がわかっているのに、あえてあてずっぽうにレバーを引く(探索する)割合」を増やせば増やすほど、手堅く稼げるはずの報酬の水平線が引き下げられていくという仕組みが、この 4 枚のグラフから改めて確認できました。

スロットマシンの選択回数の推移の比較

$ε = 0$(貪欲法)を除いた 4 つのアルゴリズムの最後のエピソードのデータを元に、時間の経過(ステップ数)とともに「AI がそれぞれのスロットマシンを何回選んだか」という累積選択回数の推移を比較します。

貪欲法($ε = 0$)と完全ランダム($ε = 1$)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

for agent in egreedy_agents[1:]:
    agent.draw_selectnum()

実行結果

 
 

4 つのグラフを順番に見比べていくと、$ε$ の値(探索の確率)が増加するにつれて、AI の「えこひいきの度合い(行動の偏り)」が変化していく性質がわかります。

  • 本命の当たり台「スロット 2」(緑色の線)の独走が緩やかになる
    • $ε = 0.25$ のときは、正解の神台(「スロット 2」)を高い確率で引き続けるため、緑色の線が最大値である 10,000 回の上限付近に向かって急角度で独走する
    • しかし、$ε$ の値が大きくなって探索の割合が増えるにつれて、独走する勢いがじわじわと落ち着き、最終的には 3 台を均等に割り当てた「全体の 1/3(約3,333回)」に向かってグラフの右端が減少していく8
  • ハズレ台「スロット 0、1」(水色とオレンジ色の線)の重なりと増加
    • ハズレ台である「スロット 0」と「スロット 1」は、「探索モード」のときにしか選択されない。そのため、どのグラフを見てもオレンジ色と緑色の 2 本の線は 常にぴったりと重なり合って 1 本の線 のようにに見える
    • $ε$(探索する確率)が増えるに従って、2 つのハズレ台を無駄に引いてしまう回数が 0 回付近からスタートしてどんどん増加していく
    • 最終的に探索 100 % の $ε = 1$(完全ランダム)に達すると、ハズレ台の線も「全体の 1/3(約 3,333回)」まで上昇する。その結果、本命の赤色の線と完全に重なり合って 3本のグラフが綺麗な 1 本の直線 に統合される

スロットマシンの当たりの確率の推定値の推移の比較

4 つのアルゴリズムの最後のエピソードデータを元に、時間の経過(ステップ数)に伴って、AI が頭の中で見積もっている「各マシンの当たりの確率の推定値(標本平均)」がどう変化したかを比較します。

貪欲法($ε$ = 0)と完全ランダム($ε$ = 1)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

for agent in egreedy_agents[1:]:
    agent.draw_estimated_odds_of_arm()

実行結果

 
 

一連のグラフを注意深く観察すると、ランダムに探索する確率である $ε$ の値によって、AI が各スロットマシンの「本当の実力」を見抜いていくスピードに、トレードオフの関係が現れていることがわかります。

  • ステップ数の増加による確実な収束(共通の強み)
    どの $ε$ の設定であっても、ステップ数が進むにつれてすべての折れ線グラフの激しい上下のブレが収まり、黒い点線で描かれた「真の当たりの確率(0.3, 0.5, 0.7)」へと綺麗に重なっていきます。これは、前節で計算した通り、$ε \gt 0$ であればすべての台が最低でも数百回以上は確実に試行されるため、大数の法則の数学的な恩恵を全員がしっかりと受けられている証拠です。
  • 当たり台(スロット 2)の収束スピードは、$ε$ が小さいほど「速い」
    $ε$ が小さい設定(0.25 など)のグラフに注目すると、最高の当たり台である「スロット 2」の実力の見積もりは、ほんの初期の数百ステップのうちにブレが完全に収まり、いち早く真の確率(0.7)のラインへと急速に収束していきます。その理由は、早い段階で「スロット 2」が本命だと見抜いた AI が、高い確率で行われる活用モードで「スロット 2」だけを「えこひいき」して猛烈なスピードでデータを集めるからです。しかし、$ε$ が大きくなるにつれて、あてずっぽうに他の台を選ぶようになるため、「スロット 2」のデータが集まる効率が落ち、ブレが収まるスピードは遅くなっていきます。
  • ハズレ台(スロット 0、1)の収束スピードは、$ε$ が大きいほど「速い」
    逆に、ハズレ台の実力を見積もるスピードは完全に真逆の性質を示します。$ε$ が小さいほど、AI はハズレ台を極力引かないように避ける(活用で本命ばかりを引く)ため、ハズレ台の試行回数がなかなか増えず、真の確率(0.3 や 0.5)へ収束するまでに長いステップ数を必要とします。一方で、$ε$ が大きくなるにつれて、ハズレ台であってもお構いなしにランダム引く回数が増えていくため、ハズレ台のデータがたまるスピードが上がって収束が速くなります。完全ランダムである $ε = 1.0$ の場合は、すべての台を完全に均等(確率 1/3)に引き続けるため、本命もハズレ台もまったく同じスピードで(ブレが均等に収まりながら)綺麗に平行して収束していくことになります。

このグラフは、ε-greedy 法が $ε$ の値によってどのように本命をえこひいきしているかを、線のブレのおさまり具合によって可視化しています。

10 エピソードのステップ当たりの累積報酬の推移の比較

ここまでは最後に実施された 1 回きりの偶然の産物である 1 エピソードの結果 をグラフ化しました。ここからは 複数回のエピソードの集計結果をグラフ化することで、偶然に左右されないアルゴリズムの本質的な性質を可視化 して比較することにします。

最初に 10 回分のエピソード(各 1 万ステップ)の「ステップ当たりの獲得コイン」の推移を重ね合わせたグラフを描画します。これにより、それぞれのアルゴリズムが持つ「結果の安定性(リスクの激しさ)」を一目で比較できるようになります。

貪欲法($ε = 0$)と完全ランダム($ε = 1$)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

for agent in egreedy_agents[1:]:
    agent.draw_cumulative_rewards(10)

実行結果

 
 

10 本のエピソード(シミュレーション)を重ねて描画することで、$ε$ の値が「結果の安定性(リスク)」にどのようなグラデーションをもたらすのかが視覚的に明らかになります。

  • 後半に向かうほど「1 本の太い束」へ収束していく(共通の性質)
    どの $ε$ の設定であっても、ステップ数が 10,000 回に近づくにつれて、散らばっていた 10 本の線が綺麗にまとまり、それぞれのアルゴリズムの理論上の値へと収束していきます。これは、十分なステップ数を重ねることでどのスロットマシンも十分な回数だけ選択され、運・不運の要素が薄まって大数の法則がすべてのエピソードで均等に働くからです。
  • $ε$ が小さいほど「前半戦のばらつき」が激しく暴れる(リスクの発生)
    $ε$ が小さい設定(0.25 など)のグラフの左側(序盤)に注目してください。10 本の線が上下に大きくバラつき、激しく暴れている様子が観察できます。これは、探索の確率が低いために「どの台が優秀か」を確信するまでにどうしても長い時間がかかり、エピソード(回)ごとの運によって「正解を見抜くタイミング」が大きく前後してしまうからです。
  • $ε$ が大きくなるほど「スタート直後からブレない」
    逆に $ε$ が大きくなるにつれて、グラフの左端のバラつきは少なくなり、スタート直後から 1 本の細い束のように安定します。積極的に未知の台を調べるため、どのエピソードでもすぐにスロットマシンの実力が見抜かれ、結果の予測がつきやすくなる(不確実性が下がる)という探索特化型の強みがここに現れています。

「ローリスク・ローターン」と「成長の不確実性」のトレードオフ

この 10 本重ね合わせのグラフによって、設定した $ε$ の値が「AI の学習の安定度」にどう影響するかが明らかになりました。

「$ε$ を大きくすれば、最初から最後まで結果がブレずに安定するが、ゴール地点のスコアは低くなる。逆に $ε$ を小さくすれば、最終スコアは高くなるものの、データが溜まる前の序盤では運に激しく左右されて大荒れになる」

この序盤のブレの厚みの違いこそが、データ不足の段階における不確実性の大きさを表しているのです。

ステップ当たりの累積報酬の平均の推移の比較

1,000 エピソードという膨大な実験データを集計した、4 つのアルゴリズムの「実力の真実(期待値の推移)」を検証します。

貪欲法($ε = 0$)と完全ランダム($ε = 1$)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

MAB_Agent.compare_mean_cumulative_reward_by_step(egreedy_agents[1:])

実行結果

1,000 回のエピソード平均を重ね合わせたこの直接対決のグラフを観察すると、ε-greedy法 がこれまでの「成長しない極端なアルゴリズム」の壁をどのように打ち破り、そしてどこで限界を迎えているのかの実態が、視覚的にはっきりとわかるようになります。

  • 最低のベースライン(0.5)の突破(短期的な勝利)
    $ε = 1$ を除くすべてのグラフにおいて、スタート直後から右肩上がりの急激なカーブを描いています。これまでの「完全ランダム」が永遠に超えられなかった最悪のベースライン($p_{mean} = 0.5$)の水平線を、驚異的な勢いで突き破っていく様子が確認できます。これは、適度な探索によってAIが正解の台を素早く見つけ出し、学習の成果を行動へと反映(活用)し始めているためです。
  • 理論値での高止まり(長期的な限界)
    残念ながら、上昇のカーブは 500 ステップ を過ぎたあたりで止まり、それ以降は傾き(成長)が完全にゼロの「綺麗な水平の直線」へと姿を変えてしまいます。この頭打ちになった天井の高さは、先ほど計算した以下の期待値の理論式($ε = 0.25$ では 0.65、$ε = 0.5$ では 0.60、$ε = 0.75$ では 0.55)の数値そのものです。

$$ε \times p_{mean} + (1 - ε) \times p_{max}$$

中盤以降で成長が完全に止まってしまう原因

グラフの線が途中から完全に真横を向いてしまう理由は、「AI の頭の中(推測の精度)は完璧に仕上がっているのに、作戦(方策)そのものが固定化されているから」です。500 ステップを過ぎたあたりから AI は「どの台が一番当たるか」をほぼ 100 % 見抜いています。それにも関わらず、あらかじめ人間に設定された「$ε$ の確率できまぐれに探索する」というルールを馬鹿正直に最後まで守り続けているため、「探索で一定の割合でハズレ台を引く」という無駄な行動を行い続けてしまうのです。

強化学習の理想は、「最初は多めに探索してスロットマシンの実力を全力で見抜き(右肩上がりの成長)、正解がわかった後は探索の割合をできるだけに減らして、神の上限である 0.7 に向かって張り付くこと」 にあります。

ずっと同じ確率で気まぐれに探索し続けてしまうという ε-greedy 法 には、どれだけステップ数を増やしても理論値の水平線の壁に当たって頭打ちになる、という限界があることがこのグラフによって明らかになりました。

ステップ当たりの累積報酬の標準偏差の推移の比較とバグの発見

平均値の推移の比較グラフによって、各アルゴリズムが特定の値で頭打ちになることが実証されました。しかし、平均の折れ線グラフからは、その裏側に隠された「結果のブレ幅(リスク)」の大きさを読み取ることはできません。

そこで、1,000 回 のエピソードデータを集計し、貪欲法($ε = 0$)を加えた 5 つのグラフを同じ画面で重ねて表示することで直接対決させた比較グラフを同時に描画します。

貪欲法($ε = 0$)と完全ランダム($ε = 1$)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

MAB_Agent.compare_std_cumulative_reward_by_step(egreedy_agents)

実行結果

この標準偏差の比較グラフを観察すると、直感的な予想を大きく裏切る異常なデータが描画されていることがわかります。数学の理論(大数の法則)からは、ランダムに台を選ぶ「探索」の割合が増えれば増えるほど、AI がスロットマシンの実力をより早く、高い精度で見抜けるようになるはずです。実力を見抜くスピードが上がれば、エピソード間の不確実性(結果のブレ幅)は小さくなるため、グラフの線は $ε$ の値が大きくなるにつれて低い位置へ沈んでいく必要があります。

しかし、実際のシミュレーションから出力された上記のグラフの最終ステップ(9,999 ステップ目)での標準偏差は、以下のような謎の順番で大きくなっています。

$ε = 0$ の場合 > $ε = 0.5$ の場合 > $ε = 0.25$ の場合 > $ε = 0.75$ の場合 > $ε = 1.0$ の場合

本来なら $ε=0.25$ の方が $ε=0.5$ よりもブレが大きくなるはずなのに、グラフ上では 0.25 と 0.5 の上下関係が完全に逆転してしまっています。この結果は、偶然のブレ(乱数のゆらぎ)としてはあまりにも不自然です。このことから、「シミュレーションプログラムのどこかに、致命的な集計バグが隠れているのではないか?」とい疑惑が浮かび上がります。

信頼区間の可視化によるバグの決定的な証拠

そこで、標準偏差がおかしいという仮説を別の角度から検証するために、下記のプログラムで平均値を中心として、1,000 回のエピソードデータがどれだけの範囲に広がっているかという「信頼区間(不確実性の広がり)」の推移をグラフ化して確認することにします。

for agent in egreedy_agents[1:]:
    agent.draw_ci_of_cumulative_reward_by_step()

実行結果

 
 

出力された 4 つの塗りつぶしのグラフ(信頼区間)を並べて比較すると、バグの存在を示す決定的な異常(証拠)が明らかになりました。

  • $ε = 0.5$ と $ε = 0.75$ で起きている異常な挙動
    本来であれば、ステップ数(試行回数)が増えるにつれて大数の法則が働き、データのブレ幅は急速に細くなっていくはずです。しかし、$ε = 0.5$ と $ε = 0.75$ のグラフでは、平均 ± 3σ)(標準偏差の 3 倍)の範囲を表す半透明な青い帯の上下幅が、ステップ数が進んでもまったく狭まることなく、厚みを維持したまま最後まで水平に伸び続けています。
  • 理論と実態の矛盾
    先程の「スロットマシンの当たりの確率の推定値の推移の比較」のグラフで確認した通り、$ε \gt 0$ の場合ではすべての AI がかなり早い段階で「スロット 2 が神台である」という正解を見抜いていました。正解を見抜いている以上、後半の行動(スロットの選択比率)はどのエピソードでも完全に一定の割合に固定されるため、エピソード間で獲得報酬がこれほど太い幅のまま激しくバラつき続けることは絶対にあり得ません。

これまでに分析した平均値のデータやグラフには不振な点がないことから、1,000 回のエピソードで得られた実験データそのものには間違いはなさそうです。従って、これらの一連のグラフによる分析から、AI の行動そのものが間違っているのではなく、「蓄積された ndarray のデータから標準偏差($σ$)を計算して算出する、プログラム側の処理にに深刻なバグが潜んでいる」という可能性が極めて高いことが確認できました。

桁落ち誤差とは何か:データ型を小さくした代償

このやっかいなバグを引き起こした原因は、前回の記事で行った「ファイルサイズを小さくする工夫」にあります。この工夫によってデータを記録する NumPy のデータ型を 64 ビットの浮動小数点数(float64) から 16 ビットの浮動小数点数(float16)に変更したことで、コンピュータの計算限界による巨大な 「桁落ち誤差」 が発生してしまったのです。

桁落ち誤差 とは、値が非常に近い 2 つの数値を引き算したときに、意味のある数字(有効数字)の桁数が大きく減ってしまう ことで生じる計算上のバグです。

ここで言う 有効数字の桁数 とは、数値の中で「位取り(小数点の位置)を示すだけのゼロを除いた、正しい値であることが保証されている意味のある数字の桁数」のことです。

わかりづらいと思いますのでいくつの具体例を挙げます。

  • 1.234:先頭からすべて意味がある数字なので 4 桁
  • 0.0123:先頭に並ぶ 0 は位取りを示しているだけなので、それらを含めない 3 桁
  • 1.23000:末尾の 0 は「ここまで正確に測定した」ことを明示するためにつけられるので 6 桁

例えば、有効数字が 5 桁ある 1.2345 と 1.2344 という、非常に近い 2 つの数値を引き算してみます。普通に計算すれば、結果は以下のようになります。

$$1.2345 - 1.2344 = 0.0001$$

しかし、先頭に並んだ 0 は有効数字の桁数のカウントから除外されるため、計算結果である 0.0001 の有効数字は たったの 1 桁 に激減してしまいます。

つまり、元々は 5 桁目まで細かく計測した精密なデータが、引き算を 1 回しただけで、上位の 4 桁分の情報が完全に消滅した、有効桁数が 1 桁の大きく精度が劣るデータになってしまったことになります。これが「桁落ち誤差」の正体です。

参考までに Wikipedia の有効数字と桁落ち誤差の項目のリンクを下記に示します。

桁落ち誤差は、あくまで「近い値同士の 引き算」をしたときにだけで発生する現象です。そのため、足し算、掛け算、割り算では発生しません。

足し算と割り算だけで計算される「平均値(累積報酬の平均)」の計算には桁落ちの誤差が一切発生しないため、これまでにグラフ化してきた平均値のデータは正確であり、それらのデータを元に行った考察には全く問題はありません。

分散と標準偏差の計算で発生する桁落ち誤差

分散は「$(各データ - 全データの平均値)^2$」の平均値であり、標準偏差は分散の平方根(ルート)によって計算されます。

ばらつきが非常に小さいデータでは、「ほぼすべてのデータ」が「全データの平均値」に非常に近い値になります。その結果、そのようなデータの 分散や標準偏差 を計算すると、値がほぼ等しい 2 つの数値を引き算 が行われるため、桁落ち誤差が発生 します。

桁落ち誤差は、元のデータの有効桁数が小さいほど、計算結果に大きな悪影響を及ぼします。そのことを実証するために、[1.001, 1.002, 1.004] という 3 つの数値の標準偏差を次の 2 つのデータ型で計算して比較してみることにします。

  • 64 ビット浮動小数点数:10 進数での有効桁数が約 16 桁 9
  • 16 ビット浮動小数点数:10 進数での有効桁数が約 3 桁

64 ビットの浮動小数点数を用いた標準偏差の計算

Python の標準の float 型は 64 ビットの浮動小数点数なので、特別な指定をしなければ自動的にこのデータ型で計算が行われます。

計算の途中でどのような数値が作られているかをはっきりさせるために、「平均値」、「各データと平均の差」、「差の2乗」をそれぞれ変数に代入して出力する実験プログラムを作成しました。最後に、NumPy の np.std を使った結果を表示することで、自前で計算した数値(std)が 100 % 正しいことを答え合わせできるようにしています。

data = [1.001, 1.002, 1.004]
mean = np.mean(data)
mean_minus_data = (mean - data)
mean_minus_data_square = mean_minus_data ** 2
std = np.sqrt(sum(mean_minus_data_square) / 3)
print(f"data: {data}")
print(f"mean: {mean}")
print(f"mean_minus_data: {mean_minus_data}")
print(f"mean_minus_data_square: {mean_minus_data_square}")
print(f"std: {std}")
print(f"np.std: {np.std(data)}")

実行結果

data: [1.001, 1.002, 1.004]
mean: 1.0023333333333333
mean_minus_data: [ 0.00133333  0.00033333 -0.00166667]
mean_minus_data_square: [1.77777778e-06 1.11111111e-07 2.77777778e-06]
std: 0.0012472191289246877
np.std: 0.0012472191289246877

std と np.std が同じ値であることから、正しい計算が行われたことが確認できます。

64 ビットの有効桁数は約 16 桁あるため、平均値(mean)として表示されている 1.0023333333333333 のうち、先頭から 16 個の数字はすべて正確な情報です。

ここから各データを引き算すると、先頭の共通部分(1.00...)の 3 桁分が相殺されて消滅し、結果は 0.00133333... のようになり、引き算によって有効桁数が 3 桁分減ってしまいます(これが桁落ちです)。しかし、元々の有効桁数が「16 桁」もあるため、3 桁減ったとしても、計算結果にはまだ「13 桁」もの非常に高い精度が残されてます。

そのため、桁落ち誤差の悪影響をほとんど受けることなく、最終的な標準偏差(std)まで 10 桁以上もの十分な精度で正しい値を計算することができるのです。

16 ビットの浮動小数点数を用いた標準偏差の計算

次に、同じデータをデータサイズが小さい 16ビットの浮動小数点数(float16)に変換して計算してみます。前回の記事で解説した通り、np.array のキーワード引数に dtype="float16" を指定することで、データを 16 ビットで記録した ndarray を作成できます。

data = np.array([1.001, 1.002, 1.004], dtype="float16")
mean = np.mean(data)
mean_minus_data = (mean - data)
mean_minus_data_square = mean_minus_data ** 2
std = np.sqrt(sum(mean_minus_data_square) / 3)
print(f"data: {data}")
print(f"mean: {mean}")
print(f"mean_minus_data: {mean_minus_data}")
print(f"mean_minus_data_square: {mean_minus_data_square}")
print(f"std: {std}")
print(f"np.std: {np.std(data)}")

実行結果

data: [1.001 1.002 1.004]
mean: 1.001953125
mean_minus_data: [ 0.000977  0.       -0.001953]
mean_minus_data_square: [9.5e-07 0.0e+00 3.8e-06]
std: 0.0012683868408203125
np.std: 0.00138092041015625

std と np.std が先程(0.0012472191289246877)と異なる値になる10ことから、std と np.std の両方で桁落ち誤差による間違った計算が行われていることが確認できます。

16 ビット浮動小数点数の有効桁数は約 3 桁しかありませんが、実行結果から元のデータである [1.001, 1.002, 1.004] は、ギリギリすべての正確な値を表現できています。しかし、問題はここから始まります。

16 ビットの浮動小数点数の有効桁数は約 3 桁ですが、上記の実行結果から 1.001, 1.002, 1.004 はいずれも正確な値を表現できていることが確認できます。

計算された平均値(mean)を見てみると、1.001953125 と出力されています。先ほど 64 ビットで計算した正しい平均値は 1.00233333... でした。つまり、データ型を 16 ビットに減らしたせいで、平均値の段階ですでに 0.0003 近くもズレるという深刻な情報落ち(精度不足) が起きてしまっています。

この平均値のズレが引き金となり、「各データと平均値の引き算(mean_minus_data)」を計算した結果は [ 0.000977, 0., -0.001953] となり、本来あるべき正しい差の数値([ 0.001333, 0.000333, -0.001666])とは異なる、誤差を含んだ数値に変形してしまいます。

そして、その歪んだ差を 2 乗して平方根をとった結果、最終的な標準偏差(np.std)は約 0.00138 と計算されました11。十分な精度を持つ 64 ビットで計算した正しい値(約 0.00124)と比較すると、約 10 % もの深刻な計算誤差 が生じていることがわかります。この「桁落ち誤差」が、今回のシミュレーションした標準偏差のグラフの上下関係がでたらめに逆転し、ブレ幅の帯がまったく縮まらなくなってしまったバグの原因です。

バグの修正方法:メモリを節約しつつ計算精度を高める

16 ビット(float16)の省メモリ(データサイズを小さくできる)という特性を活かしつつ、標準偏差の引き算での「桁落ち」を完全に防ぐ、データサイエンスでも非常によく使われる解決策があります。

NumPy の標準偏差を計算する np.std などの統計関数には、計算処理を行う瞬間だけ一時的にデータ型を変更できる dtype という仮引数が用意されています。たとえば、関数を呼び出す際に dtype="float64" というキーワード引数を記述することで、たとえ NumPy の配列(ndarray)に保存されている元のデータが 16 ビットのデータ型であったとしても、「標準偏差などの計算を行う間だけは、データを高い精度を持つ 64 ビット浮動小数点数(float64)に変換12して計算する」という処理を行ってくれるようになります。

さらに便利なことに、複雑な計算が終わって最終的に出力される標準偏差の集計結果は、元のデータ型に自動的に戻されます。そのため、データサイズが巨大化してファイルに保存した際のファイルが増える心配はありません。

下記は MAB_Agent クラスの中で、標準偏差を計算する play_episodes を修正したプログラムです。14 行目と 16 行目で、標準偏差を計算している np.std の中に キーワード引数 dtype="float64" を加えるだけ で、この深刻なバグを驚くほど簡単に修正することができます。なお、先ほど解説した通り、足し算と割り算しか行わない「平均値の計算(13 行目と 15 行目)」では桁落ち誤差は発生しないので、修正する必要はありません

 1  def play_episodes(self, stepnum, episodenum):
 2      self.stepnum = stepnum
 3      self.episodenum = episodenum
 4      self.mean_coins_by_step = np.zeros((self.episodenum, self.stepnum), dtype="float16")
 5      self.slotratio_by_step = np.zeros((self.mab.num, self.episodenum, self.stepnum), dtype="float16")
 6  
 7      for episode in tqdm(range(self.episodenum)):  
 8          self.play_episode(self.stepnum)
 9          self.mean_coins_by_step[episode, :] = self.mean_coins
10          for arm in range(self.mab.num):
11              self.slotratio_by_step[arm, episode, :] = self.slot["mean_coins"][arm]            
12  
13      self.mean_mean_coins_by_step = np.mean(self.mean_coins_by_step, axis=0)
14      self.std_mean_coins_by_step = np.std(self.mean_coins_by_step, axis=0, dtype="float64")
15      self.mean_estimate_coins_by_step = np.mean(self.slotratio_by_step, axis=1) 
16      self.std_estimate_coins_by_step = np.std(self.slotratio_by_step, axis=1,dtype="float64") 

データの再集計と保存

すでに数分間をかけて実行した 1,000 回のエピソードの膨大なシミュレーションデータ(レバーを引いた全履歴)自体は、現在も JupyterLab のメモリ(egreedy_agents)の中に生きています。そのため、わざわざ面倒なシミュレーションを一からやり直す必要はありません。

下記のプログラムを実行すれば、今回の実験データが持つ std_mean_coins_by_step(報酬の標準偏差)と std_estimate_coins_by_step(推定値の標準偏差)の属性データだけを 64 ビットの精度で計算しなおして上書き修正し し、同じファイルへ安全にバックアップ(上書き保存)することができます。

for agent in egreedy_agents:
    agent.std_mean_coins_by_step = np.std(agent.mean_coins_by_step, axis=0, dtype="float64")
    agent.std_estimate_coins_by_step = np.std(agent.slotratio_by_step, axis=0, dtype="float64")
    agent.save(f"../data/egreedy_agent(e={agent.epsilon})")    
    agent.print_summary()

実行結果

save completed.
ε-greedy 法(ε=0.0) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  7008
得られたコインの平均  = 0.701
スロットマシン 0 (p=0.30)
  得られたコインの総数 =     0
  レバーを引いた回数  =     1
  得られたコインの平均 = 0.000
スロットマシン 1 (p=0.50)
  得られたコインの総数 =     0
  レバーを引いた回数  =     2
  得られたコインの平均 = 0.000
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  7008
  レバーを引いた回数  =  9997
  得られたコインの平均 = 0.701
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.556
最終ステップのステップ当たりのコインの標準偏差 = 0.156
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.061
  最終ステップの当たりの確率の推測値の標準偏差 = 0.12083
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.158
  最終ステップの当たりの確率の推測値の標準偏差 = 0.23264
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.336
  最終ステップの当たりの確率の推測値の標準偏差 = 0.34988

save completed.
ε-greedy 法(ε=0.25) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  6442
得られたコインの平均  = 0.644
スロットマシン 0 (p=0.30)
  得られたコインの総数 =   241
  レバーを引いた回数  =   819
  得られたコインの平均 = 0.294
スロットマシン 1 (p=0.50)
  得られたコインの総数 =   440
  レバーを引いた回数  =   885
  得られたコインの平均 = 0.497
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  5761
  レバーを引いた回数  =  8296
  得られたコインの平均 = 0.694
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.649
最終ステップのステップ当たりのコインの標準偏差 = 0.005
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.300
  最終ステップの当たりの確率の推測値の標準偏差 = 0.01577
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.501
  最終ステップの当たりの確率の推測値の標準偏差 = 0.01750
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.700
  最終ステップの当たりの確率の推測値の標準偏差 = 0.00503

save completed.
ε-greedy 法(ε=0.5) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  6008
得られたコインの平均  = 0.601
スロットマシン 0 (p=0.30)
  得られたコインの総数 =   490
  レバーを引いた回数  =  1634
  得られたコインの平均 = 0.300
スロットマシン 1 (p=0.50)
  得られたコインの総数 =   830
  レバーを引いた回数  =  1676
  得られたコインの平均 = 0.495
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  4688
  レバーを引いた回数  =  6690
  得られたコインの平均 = 0.701
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.600
最終ステップのステップ当たりのコインの標準偏差 = 0.005
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.300
  最終ステップの当たりの確率の推測値の標準偏差 = 0.01154
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.500
  最終ステップの当たりの確率の推測値の標準偏差 = 0.01180
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.700
  最終ステップの当たりの確率の推測値の標準偏差 = 0.00532

save completed.
ε-greedy 法(ε=0.75) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  5485
得られたコインの平均  = 0.548
スロットマシン 0 (p=0.30)
  得られたコインの総数 =   737
  レバーを引いた回数  =  2513
  得られたコインの平均 = 0.293
スロットマシン 1 (p=0.50)
  得られたコインの総数 =  1281
  レバーを引いた回数  =  2513
  得られたコインの平均 = 0.510
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  3467
  レバーを引いた回数  =  4974
  得られたコインの平均 = 0.697
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.550
最終ステップのステップ当たりのコインの標準偏差 = 0.005
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.300
  最終ステップの当たりの確率の推測値の標準偏差 = 0.00914
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.500
  最終ステップの当たりの確率の推測値の標準偏差 = 0.01004
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.700
  最終ステップの当たりの確率の推測値の標準偏差 = 0.00656

save completed.
ε-greedy 法(ε=1.0) の結果のまとめ
============================
総ステップ数 = 10000
最後に行われたエピソードの結果
得られたコインの総数  =  4971
得られたコインの平均  = 0.497
スロットマシン 0 (p=0.30)
  得られたコインの総数 =  1004
  レバーを引いた回数  =  3406
  得られたコインの平均 = 0.295
スロットマシン 1 (p=0.50)
  得られたコインの総数 =  1659
  レバーを引いた回数  =  3307
  得られたコインの平均 = 0.502
スロットマシン 2 (p=0.70)
  得られたコインの総数 =  2308
  レバーを引いた回数  =  3287
  得られたコインの平均 = 0.702
============================
1000 エピソードの結果
最終ステップのステップ当たりのコインの平均   = 0.500
最終ステップのステップ当たりのコインの標準偏差 = 0.005
スロットマシン 0 (p=0.30)
  最終ステップの当たりの確率の推測値の平均   = 0.300
  最終ステップの当たりの確率の推測値の標準偏差 = 0.00792
スロットマシン 1 (p=0.50)
  最終ステップの当たりの確率の推測値の平均   = 0.500
  最終ステップの当たりの確率の推測値の標準偏差 = 0.00872
スロットマシン 2 (p=0.70)
  最終ステップの当たりの確率の推測値の平均   = 0.700
  最終ステップの当たりの確率の推測値の標準偏差 = 0.00809

バグの修正の確認:修正前と修正後のデータの比較

下記は、1,000 回のエピソードを重ね合わせた最終ステップ(1 万回目)の集計データのうち、「標準偏差(データのブレ幅)」 の部分だけを修正前と修正後で並べて比較した表です。各セルの 「上段が修正前の誤差のあるデータ」、「下段が修正後の正しいデータ」 を表しています。なお、計算に引き算を一切使わない「平均値(期待値)」のデータは、変化しないため省略しています。

各スロットマシンの標準偏差
(最終ステップ)
ε=0.0
(貪欲法)
ε=0.25 ε=0.5 ε=0.75 ε=1.0
(完全ランダム)
ステップ当たりのコイン 0.151
0.156
0.011
0.005
0.051
0.005
0.037
0.005
0.005
0.005
当たりの推測値
(スロット 0)
0.115
0.121
0.022
0.016
0.020
0.012
0.024
0.009
0.023
0.008
当たりの推測値
(スロット 1)
0.240
0.233
0.017
0.018
0.012
0.012
0.010
0.010
0.009
0.009
当たりの推測値
(スロット 2)
0.353
0.350
0.037
0.005
0.037
0.005
0.037
0.007
0.038
0.008

表を見比べると、修正前の標準偏差(上段)に、桁落ち誤差による計算誤差が生じていたことがわかります。特に $ε = 0.5$ と $ε = 0.75$ の「ステップ当たりのコイン」の誤差が大きく、先程のブレ幅が太いまま最後まで全く減らない異常なグラフが描画された原因が、桁落ち誤差によるものであることが明らかになりました。

正しいデータから読み解く、強化学習の標準偏差の性質の考察

正しく計算し直したデータ(下段の太字)をベースにして、改めてアルゴリズムの性質を正しく考察してみることにします。

  • $ε > 0$(探索する AI)に共通した「驚異的な安定性」
    今回設定した $ε$ の中で最も小さい $ε = 0.25$ であっても、1 万ステップの中で最低 2,500 回(25 %)は確実に探索が行われます。探索では 3 台のスロットマシンを平等に調べるため、最も不遇なハズレ台であっても「最低で約 833 回($2,500 \div 3$)」はレバーが引かれる計算になります。これだけの回数を試せば、大数の法則が強力に働きます。その結果、$ε = 0.25, 0.5, 0.75, 1.0$ のすべての場合において、AI は 1,000 回中 1,000 回とも 完全に同じ正解(スロット 2)を見抜くことに成功し、その結果がステップ当たりのコインの標準偏差である「0.005」という極めて小さい数値に現れています。
  • ブレ幅が一律で小さな値で安定する理由
    どの $ε \gt 0$ であっても「すべての回で神台を見抜く(データ不足による勘違いをしない)」という状態が作れるため、1,000 回実験をやり直したときのマシンの見積もり結果(分布の散らばり具合)は、どの $ε$ を選んでも約 0.01 付近の非常に小さなブレ幅になります。ただし、$ε$ の値によってスロットマシンごとに選択される回数が異なるため、標準偏差の値は選択された回数が多いスロットマシンほど小さくなります

「探索の確率がわずかでも 0 を超えていれば、長期的な視点ではデータのブレ(リスク)は非常に小さな値までえ込める」という性質が、バグを修正したことで改めて確認できるようになりました。

実は、今回の「桁落ち誤差によるバグ」は、前回の記事(原始モンテカルロ法と貪欲法の直接対決)の集計を行うプログラムでも既に発生していました。そのため、前回の記事に掲載していた標準偏差の数値やグラフにも、一部この誤差が含まれてしまっています。

ただし、誤差を含んだ数値であっても、「完全ランダムはブレない安定型」「貪欲法は一か八かのハイリススク・ハイリターンのギャンブル型」という、前回解説したアルゴリズムの本質的な結論や解説ストーリーのロジックにはまったく影響を与えません。そのため、前回の記事のデータは混乱を防ぐためにあえて修正せず、そのままの形で残しておくことにします。

ステップ当たりの累積報酬の標準偏差の推移の比較

桁落ちバグが修正されたので、真実のデータを使って、改めて 5 つのアルゴリズムの「ステップ当たりの累積報酬の標準偏差の推移」を正しく可視化、比較してみます。

MAB_Agent.compare_std_cumulative_reward_by_step(egreedy_agents)

実行結果

バグが直った正しい実行結果のグラフからは、$ε$ が持つ統計学的な性質がはっきりと読み取れます。

  • 「不確実性(リスク)の完全な消滅」:全員が驚異的な勢いでゼロへと急降下する
    一切探索をしない $ε= 0$(貪欲法)の青い線だけが高い位置に取り残される一方、探索を混ぜた $ε > 0$(0.25〜1.0)のグラフは、スタート直後から驚異的なスピードでグラフの底辺である 「標準偏差がほぼ 0」のラインに向かって急降下し、完全に収束します。
    これは、1,000 回シミュレーションをやり直したとしても、どの回でも AI がハズレ台の勘違いを完璧に克服し、ほぼ 100 % 確実に同じ正解(スロット 2)見抜けている、という絶対的な安定状態(ローリスク)を作れている証拠です
  • 「初期の収束スピード」に現れる微小な差
    じっくり目を凝らしてスタート直後(グラフの左端)の挙動を観察すると、わずかな性能差があることがわかります。ほんの初期の数百ステップの間だけ、オレンジ色の $ε= 0.25$ のグラフが、それ以外の $ε > 0.25$(緑や赤など)のグラフよりも少しだけ上に浮き上がっており、0 へ向かうスピードが僅かに遅れている 様子が確認できます。

グラフの「超序盤」における新たなトレードオフ

長期的な視点で見れば、$ε > 0$ という条件が満たされれば、どんな設定値であってもすべてのスロットマシンが十分に引かれ、大数の法則の強力な恩恵によってリスクはやがて 0 へと収束します。

しかし、このグラフの左端がかすかに物語っているように、データがまだ溜まりきっていない「短期的な視点(ゲームの序盤戦)」に焦点を当てると、「探索の割合($ε$)を少なくすればするほど、正解の実力を見抜いて不確実性を抑え込むスピード(収束の速度)が逆に遅くなってしまうのではないか?」 という、長期的な視点とは真逆のトレードオフ が存在することが推測されます。

つまり、「長く遊ぶなら $ε$ は小さい方が手堅くて強いが、短期決戦を挑む場合は、本当に小さい $ε$ が最強と言えるのだろうか?」という疑問がここで生じるということです。この短期的な視点において、$ε$ がどのような影響を及ぼすかについては、次回の記事でさらに細かくグラフを拡大しながら詳しく解説する予定です。

特定のステップでのステップ当たりの累積報酬のヒストグラムの比較

シミュレーションの進行(0、9、99、999、9,999 ステップ目)に合わせて、1,000 回のエピソードのデータがどのように広がっているかという「分布の形状の変化」を可視化します。

貪欲法($ε = 0$)と完全ランダム($ε = 1$)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

for agent in egreedy_agents:
    agent.draw_histogram_of_cumulative_reward_by_step()

実行結果(9, 999 ステップのグラフは省略しました)

  
  
  
  
  

時間の経過(ステップ数の増加)に伴って、1,000 回分のエピソードデータが描く分布の形状の推移を追うことで、ε-greedy法が持つ統計学的な性質が明らかになります。

  • 「単一の正規分布」への収束(貪欲法の呪縛からの解放)
    前回の記事で観察した貪欲法($ε = 0$)のヒストグラムは、データ不足による勘違い(まちぼうけ現象)が原因で、3つのゴール(0.3, 0.5, 0.7)へと分布がバラバラに分裂してしまっていました。しかし、わずかでも探索する確率を混ぜた $ε > 0$ のアルゴリズムでは、ステップ数が進むにつれてデータが分裂することなく、中央の期待値を中心とした「1 つの綺麗な釣り鐘型の山(正規分布)」へと凝縮されていきます。これは、すべてのエピソードにおいて AI が完璧に神台を見抜くことができるため、中心極限定理の数学的な恩恵を受けることができたからです
  • $ε$ の増加に伴う「山の中心(平均値)」の減少(トレードオフの可視化)
    もう一つの注目すべき点としてはステップ 9999 の最終的な山の「位置」が挙げられます。$ε$ の値を 0.25、0.5、0.75、1.0 と増やしていくにつれて、釣り鐘型の山の中心(頂点)が、徐々に左にずれて減少していきます。この山の中心の移動は、先ほど数式で証明した下記の長期的な期待値の理論式($0.7 - 0.2 \times ε$)によってもたらされたものです。

$$ε \times p_{mean} + (1 - ε) \times p_{max}$$

このヒストグラムの推移を観察することで、ε-greedy 法が貪欲法の最大の弱点であった「3 つの山への分裂(大爆死するリスク)」を完全に克服できることが明らかになりました。

$ε > 0$ であれば、「長期的に見れば AI がハズレ台に縛られて大爆死する」という最悪の不確実性が解消され、山は必ず 1 つにまとまり、人間が設定した $ε$ の理論通りのスコア(山の中心)へと確実に着地します。この絶対的な予測のしやすさ(単一正規分布への収束)は、ε-greedy 法の大きな特徴の一つです。

ステップ当たりの累積報酬の平均からのブレの比較

平均値を中心として、1,000 回のエピソードのデータがどれだけの範囲に広がっているかという「信頼区間(不確実性の広がり)」の推移を視覚化します。データの分布が正規分布に従う場合は、sigmul=3(平均 ± 3σ)の範囲に約 99.7% のデータが収まることになります。このグラフによって、ほとんど(99 % 以上)のデータがどのような範囲に収まるかが直観的に理解できるようになります。

貪欲法($ε = 0$)と完全ランダム($ε = 1$)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

for agent in egreedy_agents[1:]:
    agent.draw_ci_of_cumulative_reward_by_step()

実行結果

 
 

先ほどは $ε=0.5$ と $ε=0.75$ のグラフが「桁落ち誤差」のせいで、極太の帯がそのまま平行に伸び続けるというあり得ないグラフになっていました。しかし、計算精度を 64 ビット(float64)へ修正したことで、グラフが本来あるべき統計学的に正しい収束の形で描画されるようになりました。先程行った数文字の修正(dtype="float64")によって、桁落ちの誤差が完璧に修正されたことが改めて実証されました。

  • 「不確実性の完全な消滅」:すべての帯が一本の線に向かって急速に収束する
    どの $ε$ の設定であっても、ステップ数が進むにつれて半透明の青色の帯(ブレの範囲)が急速に細くなっていきます。最終ステップ付近では、すべてのエピソードにおける獲得報酬のバラつきがほぼ完全に消滅し、それぞれの理論値のラインへと一本の細い線のように収束します。これは、先程のヒストグラムで確認した「3 つの山への分裂リスクの消滅(単一正規分布への凝縮)」が、時間の経過とともに帯が潰れていくことで可視化された結果です。
    • $ε$ の値が大きくなるほど「低い天井」へ収束する
    半透明の帯が収束した後の「最終的な実線の高さ(スコア)」に注目すると、ここでも先ほど証明した期待値の理論式($ε=0.25$ なら 0.65 など)の値通りにグラフが描画されます。$ε$ の値を増やして無駄な探索を多くさせるほど、実線のゴール地点が全体の平均値である 0.5 へ向かって徐々に低くなっていく様子がグラフから直観的に理解できます。

スロットマシンの当たりの確率の推測値の平均からのブレの比較

AI が頭の中で見積もっている「各スロットマシンの当たりの確率の推定値」が、真の確率を中心としてどれだけの範囲に広がっているかという「信頼区間(不確実性の広がり)」の推移を視覚化します。また、具体的なブレ幅(標準偏差)の数値の減り方をはっきりと捉えるために、その推移グラフもあわせて描画しました。

貪欲法($ε = 0$)と完全ランダム($ε = 1$)の場合のこのグラフの詳細な読み解き方については、前回の記事を参照して下さい。

for agent in egreedy_agents[1:]:
    agent.draw_ci_of_estimated_odds_of_arm()
    agent.draw_std_estimated_odds_of_arm()

実行結果

 
 
 
 

桁落ちバグを修正したことで、AI がそれぞれのスロットマシンの「本当の確率」を見抜いていくプロセスのバラつき(標準偏差)が、グラフによって明らかになりました。

  • 最高の当たり台「スロット 2」の急速なブレの収束(えこひいきの証明)
    $ε > 0$ のどのグラフを観察しても、最高の当たり台である「スロット 2」を囲む半透明の帯(ブレの範囲)が、他のスロットに比べて圧倒的なスピードで収束していいく様子が確認できます。これは、AI が「スロット 2 が一番当たる神台だ」と見抜いた瞬間から、活用においてスロット 2 だけを「えこひいきして選択」し始めるからです。さらに、$ε$ の値が小さいエージェント(0.25 など)ほど活用の割合($1 - ε = 0.75$)が大きくなるため、「スロット 2」に割り振られる試行回数が劇的に増加し、ブレ幅の収束のスピードはより一層加速します。
  • ハズレ台「スロット 0、1」の均等な収束(ランダム探索の公平性)
    一方で、ハズレ台である「スロット 0」と「スロット 1」の信頼区間の帯は、お互いにまったく同じ速度で徐々に細くなっていきます。その理由は、探索が進むと AI にとって「スロット 0」と「スロット 1」はどちらも「活用では絶対に選ばないハズレの選択肢」になるからです。これらの台は、$ε$ の確率で発生する「あてずっぽうな探索モード」の時にしかレバーを引かれません。探索モードではすべての台が等確率(1/3)で選ばれるため、ハズレ台でデータが溜まるペースは完全に平等になり、標準偏差の減少速度もぴったり一致するのです。

draw_std_estimated_odds_of_arm には、下記の 2 行目のようにせっかく play_episodes で計算したスロットマシン当たりの確率の推測値の標準偏差を使わずに、ここで改めて計算し直しているというバグがあります。

1  def draw_std_estimated_odds_of_arm(self):        
2      std = np.std(self.slotratio_by_step , axis=1)
3      plt.title(f"{self.title}\nスロットマシンの当たりの確率の推測値の標準偏差({self.episodenum} エピソード)")
4       for arm in range(self.mab.num):
5          plt.plot(std[arm], label=f"arm {arm} (p={self.mab.p[arm]:.2f})")
6  以下略

このバグは、下記のように 2 行目を削除し、上記の 5 行目の std[arm] を self.std_estimate_coins_by_step[arm] に変更することで修正することができます。今回の記事のプログラムでインポートしている ai.py はこのバグを修正済です。

1  def draw_std_estimated_odds_of_arm(self):        
2      plt.title(f"{self.title}\nスロットマシンの当たりの確率の推測値の標準偏差({self.episodenum} エピソード)")
3       for arm in range(self.mab.num):
4          plt.plot(self.std_estimate_coins_by_step[arm], label=f"arm {arm} (p={self.mab.p[arm]:.2f})")
5  以下略

知識の獲得スピードに潜むトレードオフ

このスロットマシンごとの信頼区間の挙動を見比べることで、ε-greedy 法における下記の「学習の偏り」の構造が明らかになりました。

「$ε$ を小さく抑えれば、本命(「スロット 2」)の実力を素早くで見抜いて盤石な稼ぎ体制に入れるが、ハズレ台(「スロット 0、1」)の実力を完全に見抜くのには時間がかかる。逆に $ε$ を大きくすれば、ハズレ台の実力も素早く判明するが、本命を引く回数が減ってブレの収束は少し遅くなる」

今回の記事のまとめ

今回の記事では、あらかじめ設定された特定の確率 $ε$ によって探索と活用の割合をコントロールする「ε-greedy法」のプログラムを実装しました。そして、様々な統計グラフを用いて、長期的な視点(ステップ数が十分に大きい場合)におけるアルゴリズムの性質を多角的に明らかにしてきました。

その結果、わずかでもきまぐれに探索を行う $ε> 0$ の設定であれば、長期的な視点では「1 ステップあたりに稼ぐコインの平均枚数」の理論値が、以下の式で表される値に収束していくことを示しました。

$$ε \times p_{mean} + (1 - ε) \times p_{max}$$

この数式が意味するのは、長期間(1 万ステップなど)じっくりとレバーを引き続けるのであれば、探索の割合を表す $ε$ の値が 0(貪欲法)に近ければ近いほど、最終的に手に入るコインの総数は高くなる という事実です。

しかし、現実の世界には時間の制約があるため、悠長に長い間じっくりとレバーを引き続けることはありません。「多腕バンディット問題(強化学習)」の真の本質は、永遠に近い長い時間ではなく、以下のような 限られた短い期間(ステップ数)の中で、いかに効率よく多くの累積報酬を得られるか という、実戦的なアルゴリズムを求めることにあります。

「もし、ゲームの制限時間が 1 万回ではなく、もっと少ない 100 回や 500 回で終わってしまうとしたら、どのような $ε$ によっ一番多くのコインを稼げるのだろうか?」

そこで、次回の記事では ε-greedy 法の本質である「短期的な視点での ε-greedy 法の性質」 について詳しく考察します。データが溜まりきっていない序盤において、設定値 $ε$ と獲得報酬の間にどのようなトレードオフが発生するのか、そして限られた時間内で最も多くのコインを稼ぎ出す 「最適な $ε$」がどのような値になるのかを、次回の記事でもグラフで視覚化しながら解き明かしていきます。

本記事で入力したプログラム

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
ai.py 本記事で更新した ai_new.py

次回の記事

  1. 最高の当たり台が複数ある場合は、活用の確率($1-ε$)をその台数で均等に割り振ります ↩

  2. MAB_Agent クラスの詳細な仕様については以前の記事を参照して下さい ↩

  3. 探索と活用の処理は Random_MAB_Agent と Greedy_MAB_Agent の select_arm メソッドと同じです ↩

  4. 5 行目では i / 4 を計算することで、0 / 4 =0.0、1 / 4 = 0.25、2 / 4 = 0.5、3 / 4 = 0.75、4 / 4 = 1.0 という、5 種類の $ε$ の設定値を計算しています ↩

  5. 実行結果からわかるように、この規模の計算には数分間の処理時間が発生します ↩

  6. 強化学習の慣例として、ステップ数は 0 から数えるので、1 万回目の最終ステップは 9,999 ステップになります ↩

  7. 「スロット 2」で最初に当たりが出るまでの間に、何回か「スロット 2」が選択されてハズレを引いた可能性がありますが、その回数を print_summary の結果から知ることはできません ↩

  8. グラフを見ると赤色の線の傾き(角度)がどれも同じように見えるかもしれませんが、これは Matplotlib の機能で縦軸(選択回数)の最大値の縮尺(スケール)が自動で変化しているための目の錯覚です。実際の数値としての傾きは $ε$ が増えるほど緩やかになり、右端の最大値は減少しています ↩

  9. コンピューターは内部では 2 進数で数値を表現しているため、10 進数で数値を表現した場合の有効桁数は整数にはなりません。約 16 桁と表記したのはそのためです ↩

  10. std と np.std が若干異なる値を計算する理由は、標準偏差を計算する手順が若干異なるためですが、いずれの場合も桁落ち誤差によって誤差を含んだ値が計算される点に変わりはありません ↩

  11. Mab_Agent クラスでは np.std を用いて標準偏差を計算しているので、ここでは np.std の値で説明しています ↩

  12. このようなデータ型の変換のことを 型キャスト と呼びます。 ↩

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?