じゃんけんに勝てるAIは作れるか

#最初に
機械学習においてこのジャンルはよくサザエさんのじゃんけん予測の方法やYOLOによる物体検出による高速あと出しじゃんけんの方法がメジャーだが、正攻法の学習はあまりない気がする。じゃんけんの機械学習を自分の条件付けの仮定の下で考えてみたい。

#1.完全ランダムな手には勝てない
もし、対戦相手の出す手がグーが1/3、チョキが1/3、パーが1/3としてこれに勝ち越す事はできるだろうか。
これはもう100%運勝負になってしまうので、戦略で勝ち越す事は不可能である。
逆に言えば全体的に出す手は均等でも、ある条件下において出す手に偏りがあれば勝率の高くできる可能性はある。

#2.コンピュータ同士の対戦による強化学習は無駄である
ゲームの強化学習ではコンピュータ同士で対戦をさせて、優秀なAIを選択するケースがあります。
じゃんけん勝負でこれをやると、おそらく堂々巡りに陥ると思われる。
グーが多いAIに対してパーが多いAIが有利であり、パーが多いAIに対してチョキが多いAIが有利であり、チョキが多いAIに対してグーが多いAIが有利だからである。
ある特定の二個のAIを対戦させたところで真の優劣はじゃんけんゲームの場合では決定できません。

#3.人間におけるポリシーを仮定する
今、人間が出す手は1個前のじゃんけんの勝敗に依存すると仮定する。
例えば1個前の人間の出した手がグーの時、その勝負結果に依存してグーから手を変えるかどうか判断するでしょう。この時の手を変える確率に関して何らかの偏りがあると仮定する。
（例えば現実のじゃんけんでは勝敗があいこの次の手は同じ手を出す確率は低いらしい）
一方、2個前や3個前のじゃんけんの勝敗は出す手に影響を与えないと仮定する。

#4.相手はパーティーゲームの司会者ではない
パーティーゲームの司会者が出すじゃんけんは一方的であり、こちらの手との勝敗を考慮して次の手を変更してきたりはしません。従って本項の内容はサザエさんに通用する戦略ではない。

#5.誰かに特化したいわけではない
特定のポリシーに対してだけ強いのではなく様々なポリシーに対して対応できるようになりたい。

#6.勝負の前に事前の結果が与えられているとする
入力として100回の勝負結果が与えられているとする。この100回の結果は自分がランダムに出した手と相手（人間）のポリシーによって生成されたものである。

この条件はどこか多腕バンディット問題を想起させるが、あまり関係ない。

#7.相手の次の手ではなくポリシーを予測したい
相手の次の手を予測するためには現在の条件(1個前の手と勝敗)と相手ポリシーを組み合わせたものを予測する必要がある。
実はこの条件を抽出するモデルを作成するのは意外と難しい。従って相手の次の手ではなく相手のポリシーのみを予測するモデルにしたい。

#8.相手が出す最も確率の高い手を予測する
要するに引き分けの扱いである。負けなければ引き分けでもいいやというケースと引き分けよりも勝利を優先するケースである。前者は勝敗率を優先し、後者は勝利数を優先する。今回は後者を仮定する。

#モデル
入力$(100,3,2)$、出力$(9,3)$のCNNモデル
入力は100回の偏ったポリシーを仮定した人間の手とAIの手(二通り)の出す手の履歴（グー、チョキ、パーの三通り×100回）。
出力は人間の偏ったポリシーで前回の人間の手×AIの手の計9通りにおける次回の各グー、チョキ、パーの出す確率。グー、チョキ、パーの出す確率の合計値は'softmax'で1にする。
畳み込みを用いることで隣り合うデータ(一個前の出した手と勝敗)から特徴量抽出を行うようにしたい。

```python:
    inputs = Input(shape=(100,3,2))

    x = Conv2D(32, (3,3), padding='same')(inputs)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = Conv2D(64, (3,3), padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = Flatten()(x)
    y = Dense(27)(x)
    y = Reshape((9, 3), input_shape=(27,))(y)
    y = Activation('softmax')(y)
    
    return Model(inputs, y)
```

```python:
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 100, 3, 2)         0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 100, 3, 32)        608
_________________________________________________________________
batch_normalization_1 (Batch (None, 100, 3, 32)        128
_________________________________________________________________
activation_1 (Activation)    (None, 100, 3, 32)        0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 100, 3, 64)        18496
_________________________________________________________________
batch_normalization_2 (Batch (None, 100, 3, 64)        256
_________________________________________________________________
activation_2 (Activation)    (None, 100, 3, 64)        0
_________________________________________________________________
flatten_1 (Flatten)          (None, 19200)             0
_________________________________________________________________
dense_1 (Dense)              (None, 27)                518427
_________________________________________________________________
reshape_1 (Reshape)          (None, 9, 3)              0
_________________________________________________________________
activation_3 (Activation)    (None, 9, 3)              0
=================================================================
Total params: 537,915
Trainable params: 537,723
Non-trainable params: 192
_________________________________________________________________
```
#結果
学習を進めた結果[acc]の値は上昇した。
![Figure_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299928/bd9e2bd4-4a02-ba6e-f40d-cb938cc5c249.png)

正解のポリシーと予測したポリシーを比較すると全てにおいて最も相手の出す確率の高い手を予測出来ていた。この結果からじゃんけん100回の入力結果から偏りのあるポリシーを予測出来ているようである。

```python:
test_Y =                                        #相手のポリシー
 [[0.3 0.2 0.5]
 [0.2 0.5 0.3]
 [0.2 0.5 0.3]
 [0.3 0.2 0.5]
 [0.3 0.2 0.5]
 [0.3 0.2 0.5]
 [0.5 0.2 0.3]
 [0.2 0.3 0.5]
 [0.5 0.3 0.2]]
predict_Y =                                     #予測ポリシー 
 [[0.31976774 0.2903652  0.389867  ]            …正解
 [0.27517384 0.40205565 0.32277042]             …正解
 [0.26223266 0.41418174 0.32358557]             …正解
 [0.33693022 0.2662295  0.39684036]             …正解
 [0.3045507  0.31321627 0.3822327 ]             …正解
 [0.3195449  0.2623827  0.41807258]             …正解
 [0.41370207 0.254595   0.331703  ]             …正解
 [0.31436297 0.28560433 0.4000329 ]             …正解
 [0.3956554  0.3243742  0.27997023]]            …正解
```
#機械学習でやる意味はない？
しかし、正直言ってこのくらいならそもそも機械学習でやる意味はない。
相手のポリシーが偏っていて一つ前の手と勝敗に依存することが分かってれば、その条件で事前結果をカウントしてやればポリシーを推定するアルゴリズムは機械学習でなくとも作成できるはずである。
もっと相手ポリシーを決める複雑な条件（例えば相手が次の手を決めるまでの時間とか相手の性格や年齢とか）が入力データに与えられたならば、機械学習でなければ解くのが難しくなるかもしれない。

#コード：

```python:janken.py
# coding: cp932

import numpy as np
import keras
from keras.utils import np_utils
from keras.layers import Input
from keras.layers.convolutional import Conv2D
from keras.layers.normalization import BatchNormalization 
from keras.layers.core import Dense, Activation, Flatten
from keras.models import Model
from keras.layers import Concatenate, Reshape
from keras.callbacks import LearningRateScheduler
import itertools

def choice_way(rand, choice):

    if rand < choice[0]:
        return 0
    elif (1 - rand) < choice[2]:
        return 2
    else:
        return 1

def play_random(human_policy, n):

    AI_policy = [1/3, 1/3, 1/3]
    human_init = [1/3, 1/3, 1/3]

    AI_history = []
    human_history = []

    for i in range(n):
        rand = np.random.random(2)
        AI_choice = AI_policy
        
        if i == 0:
            human_choice = human_init
        else:
            human_choice = human_policy[AI_history[-1], human_history[-1]]

        AI_history.append(choice_way(rand[0], AI_choice))
        human_history.append(choice_way(rand[1], human_choice))
    
    AI_history = np_utils.to_categorical(AI_history, num_classes=3)
    human_history = np_utils.to_categorical(human_history, num_classes=3)
    
    one_hot_x = []
    one_hot_x.append(AI_history)
    one_hot_x.append(human_history)
    one_hot_x = np.array(one_hot_x).transpose((1,2,0))
    one_hot_x = np.reshape(one_hot_x, (n,3,2))

    one_hot_y = np.reshape(human_policy, (9,3))

    return one_hot_x, one_hot_y

class DataGenerator(object):
    def make_data(self, batch_size=64, play_num=100):
        while True:
            X_data = []
            Y_data = []

            for i in range(batch_size):

                human_policy = np.tile([0.5,0.3,0.2], (3,3,1))
                
                for i, j in itertools.product(range(3), range(3)):
                    human_policy[i,j] = np.roll(human_policy[i,j], np.random.randint(0,3))
                    if np.random.randint(0,2) == 0:
                        human_policy[i,j] = human_policy[i,j,::-1]

                X_one, Y_one = play_random(human_policy, play_num)
                X_data.append(X_one)
                Y_data.append(Y_one)
            yield np.asarray(X_data), np.asarray(Y_data)

def make_model():
    inputs = Input(shape=(100,3,2))

    x = Conv2D(32, (3,3), padding='same')(inputs)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = Conv2D(64, (3,3), padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    x = Flatten()(x)
    y = Dense(27)(x)
    y = Reshape((9, 3), input_shape=(27,))(y)
    y = Activation('softmax')(y)
    
    return Model(inputs, y)


train_gen = DataGenerator()
test_gen = DataGenerator()

model = make_model()
model.summary()
model.compile(loss='categorical_crossentropy',
              optimizer='Adam', metrics=['acc'])


callbacks = []

base_lr = 1e-3
lr_decay_rate = 1 / 3
lr_steps = 3
epochs = 100
batch_size = 256
callbacks.append(keras.callbacks.LearningRateScheduler(lambda ep: float(base_lr * lr_decay_rate ** (ep * lr_steps // epochs))))

history = model.fit_generator(train_gen.make_data(batch_size=batch_size, play_num=100), steps_per_epoch=10000//batch_size,
                              validation_data=test_gen.make_data(batch_size=batch_size, play_num=100), validation_steps=10000//batch_size,
                              epochs=epochs, callbacks=callbacks)


import matplotlib.pyplot as plt

plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.plot(history.history['acc'], label='acc')
plt.plot(history.history['val_acc'], label='val_acc')
plt.grid()
plt.legend()
plt.show()

test_policy = np.tile([0.5,0.3,0.2], (3,3,1))

for i, j in itertools.product(range(3), range(3)):
    test_policy[i,j] = np.roll(test_policy[i,j], np.random.randint(0,3))
    if np.random.randint(0,2) == 0:
        test_policy[i,j] = test_policy[i,j,::-1]
test_X = []
for i in range(256):
    test_X_one, test_Y = play_random(test_policy, 100)
    test_X.append(test_X_one)

test_X = np.asarray(test_X)
predict_Y = model.predict(test_X)

print('test_Y =\n', test_Y)
print('predict_Y =\n', np.average(predict_Y, axis=0))
```