スロットマシンで稼ぐためには？（多腕バンディット問題入門）

#　はじめに
こんにちは。株式会社エイアイ・フィールドでプロダクト開発をしている久保です。この記事は[エイアイ・フィールドアドベントカレンダー2021](https://qiita.com/advent-calendar/2021/aifield)の初日の記事です。この記事では、多腕バンディット問題と簡単なアルゴリズムの紹介をします。

#　多腕バンディット問題とは

今、あなたはカジノに来ています。このカジノでは専用のコインを買い、そのコインをつかってスロットマシンで遊びます。スロットマシンからは当たれば現金が1万円出てくるものとします。（スロットマシンから出たコインではスロットマシンは起動できないものとします）。

さて、さっそくあなたはコインを100枚入手しました。

<img width="250" alt="coin_case.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30977/1505e8e7-3464-3033-ab89-5b3b329ead51.png">

このコインをつかって、スロットマシンに挑みます。このカジノには3台のスロットマシンがあり、それぞれ当たる確率が異なることがわかっています。

<img width="150" alt="slot.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30977/36d84823-3194-d86c-ac7e-97bee9179b76.png"><img width="150" alt="slot.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30977/36d84823-3194-d86c-ac7e-97bee9179b76.png"><img width="150" alt="slot.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30977/36d84823-3194-d86c-ac7e-97bee9179b76.png">

さて、どのようにプレーをすれば最終的な獲得金額を最大化できるでしょうか？

このように、選択肢の候補から1つを選び、選んだ選択肢からは確率に従って報酬が得られ、それ以外の選択肢からは情報も得られない。それをN回繰り返すときに報酬を最大化する問題を**多腕バンディット問題**とよびます。

# 素朴な方法

獲得金額を最大化するためのもっともよいやり方は、3つのスロットマシンの中で最も当たる確率の高いものにすべてのコインを使うことです。しかし、最初の段階では、どのスロットマシンが最も当たる確率が高いのかは分かりません。なので、ある程度調査(**探索**)が必要になります。
例えば、100枚のうち、最初の30枚を調査に使い。その中で最も有望そうなマシンに残りの70枚を使って(**知識利用**して)みましょう。

以下に多少汚いですが、sample codeを置きます。

```python
import random

class PlayBoard:
  def __init__(self, n_machines=3, n_coins=100):
    self.reset_machines(n_machines)
    self.reset_history(n_coins)

  def reset_machines(self, n_machines):
    self.n_machines = n_machines
    self.machine_mu =  [random.random() for _ in range(self.n_machines)]

  def set_machines(self, machine_mu):
    self.n_machines = len(machine_mu)
    self.machine_mu = machine_mu

  def reset_history(self, n_coins):
    self.n_coins = 100
    self.history = []
    self.reward = 0

  def play(self, arm):
    if self.n_coins < 1:
      return
    self.n_coins -= 1
    record = {'arm': arm, 'result': None}
    if random.random() < self.machine_mu[arm]:
      record['result'] = 1
      self.reward += 1
    else:
      record['result'] = 0
    self.history.append(record)

  def get_summary(self):
    ret = [{'arm': idx, 'n_choice': 0, 'n_reward': 0} for idx in range(self.n_machines)]
    for r in self.history:
      ret[r['arm']]['n_choice'] += 1
      ret[r['arm']]['n_reward'] += r['result']
    for r_ in ret:
      r_['est'] = r_['n_reward'] / r_['n_choice'] if r_['n_choice'] else 0
    return ret
    
def epsilon_greedy(pb, eps = 0.3):
  n_exploration = int(eps * pb.n_coins)
  n_exploitation = pb.n_coins - n_exploration
  for idx in range(n_exploration):
    pb.play(idx % pb.n_machines)
  max_arm = max(pb.get_summary(), key=lambda x: x['est'])['arm']
  for _ in range(n_exploitation):
    pb.play(max_arm)
  return pb.reward
```

`PlayBoard` は状況設定を表すクラスで、`reset_machines`, `set_machines`はスロットマシンをresetのほうはランダムで、setのほうは任意で用意できます。`reset_history`では履歴とコインのリセットを、`play`ではマシンを選んで遊ぶことを行います。最後の`get_summary`は現在までにどのマシンをどれだけ選んだか、そのマシンから報酬を得られたのは何回か、報酬を得られる経験的な割合はいくらかをまとめて出します。

`epsilon_greedy`は先ほど紹介した素朴なアルゴリズムで、全体のうち`eps`分だけ探索をして、最も有望そうなマシンに残り全部を使います。

これらのコードを用いて、何回も実験を行い。最終的にどのように報酬が得られるか見てみることにしましょう。

```python
pb = PlayBoard()
pb.set_machines([0.3, 0.5, 0.8])
```

今回は、マシンとして確率0.3, 0.5, 0.8で報酬を返すものを用意しました。なので、最も良いマシンを選び続けたときの報酬の期待値は80万円となります。

```python
results = []
for _ in range(100):
  pb.reset_history(100)
  results.append(epsilon_greedy(pb))
```
このようなコードで100回ほど実験をしてみました。

```python
import pandas as pd
df_ = pd.DataFrame(results)
df_.hist()
```
結果
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30977/ce6fc862-2bf5-f25c-8c1c-21b441a384de.png)

となり、平均70.9の報酬がもらえました。まだ、50のところに山がありそうで、0.5の報酬に引っ張られたように見受けられます。
探索が足りなかったかもしれないので、`eps`を0.4まで上げてみましょう。

```python
results = []
for _ in range(100):
  pb.reset_history(100)
  results.append(epsilon_greedy(pb, 0.4))

df_ = pd.DataFrame(results)
df_.hist()
```
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30977/8a32c527-7fe3-71f4-ce11-4a1a62c2e5a3.png)

結果、このようになりました。50へ引っ張られているものは減りましたが、探索を増やした分期待報酬も減ってしまい。平均では68.81となっています。

# ほかの方法
このバンディット問題ですが、上に紹介した素朴なアルゴリズムは**epsilon greedy**法と呼ばれています。ほかのアルゴリズムも考案されており、**UCB法**や**UBT法**、また問題を強化学習と考え**Q-Learning**で解く方法などがあります。この記事が長くなってしまったので、これらについては次回以降の記事でできれば紹介したいと思います。

# おわりに

今回は多腕バンディット問題の紹介とその中でも最も素朴なアルゴリズムであるepsilon greedy法について紹介しました。多腕バンディット問題はこのようなスロットマシンの問題だけでなく、もう少し問題を拡張することによって**広告配信**や**推薦**、囲碁などの**ゲーム木探索**などにも応用されているます。また、問題の形式を少し変えることによって、デザインにおける**A/Bテスト**の世界ともつながっていきます。
興味がわいた方は、[本多さんの本](https://www.amazon.co.jp/dp/B07566CMDC/)などを読んでみるとよいかもしれません。もちろん次回以降にも他のアルゴリズムや拡張問題について書いていくつもりですので、ぜひご期待ください。