スマホゲーム上海のログインボーナスの結果を画像認識し、同様に確からしいか、独立性の検定をしてみる。



# 目的
[上海というスマホゲーム](https://apps.apple.com/jp/app/%E4%B8%8A%E6%B5%B7-%E3%83%91%E3%82%BA%E3%83%AB%E3%82%B2%E3%83%BC%E3%83%A0/id382379749)にはログインボーナスがあり、８個並んだ配列から１個選ぶと、ダイヤかハートがもらえる。（細かくは記載しないが）ダイヤのほうが価値があるので、ダイヤを当てたい。

![IMG_3883_resize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/3705e9ca-f0b2-638b-61b0-2d665e93b274.png)　⇒TouchToStart⇒　![IMG_3884_resize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/65184737-fc3d-8847-a109-88798ed0c87d.png)　⇒SelectOne⇒　![IMG_3885_resize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/a628a1bd-326e-dde9-1644-a0e8f90a5af2.png)　⇒OK⇒　![IMG_3886_resize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/152b2a59-2e27-a5cb-7db1-806e63506901.png)

これまでの経験上、どうも、右下の端が、よくダイヤ（x15）がでるようなので、選択位置と取得できるマークには、偏りがあると考えた。そこで、選択位置と取得できるマークには関連があるか、独立性の検定で確かめてみたい。
なお、マーク（と数）については、目視確認ではなく、機械学習により分類・認識する。

ゲーム「上海」とは、「積み上げられた麻雀牌の山から、ある一定のルールに従って牌を取り除いていく、「Mahjong solitaire」とも呼ばれるソリティアの一種」[Wikipedia](https://ja.wikipedia.org/wiki/上海_(ゲーム))より引用、ただし、ゲーム内容とこの記事は全く関係がない。

独立性の検定とは、「2つの変数に対する2つの観察（2x2分割表で表される）が互いに独立かどうかを検定する。例えば、「別の地域の人々について、選挙である候補を支持する頻度が違う」かどうかを検定する方法」[Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%82%A4%E4%BA%8C%E4%B9%97%E6%A4%9C%E5%AE%9A#%E7%8B%AC%E7%AB%8B%E6%80%A7%E6%A4%9C%E5%AE%9A)より引用。
今回は、選択位置と取得できるマーク数という２変数の独立性を検定する。

## 補足
検定する上で、適合度の検定なのか、独立性の検定なのか、判断に迷いました。マークの出方が同様に確からしいかを検定するので、適合度の検定があっているように思いましたが、選択した位置という変数もあるので、独立性の検定にしました。参考：[適合度検定](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%82%A4%E4%BA%8C%E4%B9%97%E6%A4%9C%E5%AE%9A#%E9%81%A9%E5%90%88%E5%BA%A6%E6%A4%9C%E5%AE%9A)Wikipediaより

# データの取得
右下の端が、よくダイヤ（x15）がでるということも確かめてみたい（ダイヤを当てたい）ので、まずは、右下の端を選択し続けること５０回。そのスクリーンショットを保存し、データを収集した。
ログインボーナスは１日１回なので、毎日できたとしても５０日かかったことになる。データ分析は、データ収集に時間と手間がかかることをあらためて実感できた。

# 画像認識
５０個であれば、１個１個手作業で集計することもそれほど大変ではないが、技術的興味から、画像認識で選択位置毎のマークを特定したい。マーク（ダイヤ、ハート）だけでなく、その数もあるので、数字認識はCNNで認識する。マークは簡単そうなので、ニューラルネットワークは使用せず、決定木(DecisionTree) で分類する。

## 教師データの切り出し
教師データを切り出す必要があるので、マスターファイルに１ファイルずつ、選択位置とマークを目視確認して記載する。それを読み込むと、以下のようになる。
* 画像左から右にx0,x1,x2,x3、上から下にy0,y1とし（例：右下はy1_x3）、そのラベルを記載する。
* ラベルはダイヤをD、ハートをH、それに続けて数字とした。
* 選択した位置からは牌が消えてブランクとなるので、BLというラベルとした。
* １０画像分のラベルを作成したので、10枚ｘ８個=80個の教師データが作成される。

```python
# 教師データのラベリングしたマスターファイルを作成し読み込む
import pandas as pd
df_master = pd.read_csv('./data/master.csv')
df_master
```
```
filename	y0_x0	y0_x1	y0_x2	y0_x3	y1_x0	y1_x1	y1_x2	y1_x3
0	IMG_2465.PNG	D10	D15	H2	H3	D5	H1	H1	BL
1	IMG_2467.PNG	H3	H1	D15	H2	H1	H1	D10	BL
2	IMG_2475.PNG	D5	H1	H1	H2	D10	H1	H3	BL
3	IMG_2482.PNG	H3	H1	D10	H1	H1	D5	H2	BL
4	IMG_3295.PNG	H3	D15	H2	H1	D5	D10	H1	BL
5	IMG_3299.PNG	H1	H1	D15	H2	H3	D10	H1	BL
6	IMG_3308.PNG	D10	H1	H1	H3	D5	D15	H1	BL
7	IMG_3311.PNG	D10	D5	H1	H3	H1	H2	D15	BL
8	IMG_3316.PNG	H1	H1	H1	H2	H3	D5	D15	BL
9	IMG_3320.PNG	D10	H1	H1	H2	H1	D5	D15	BL
```
選択位置の座標については、マークと数字でそれぞれ設定し、扱いやすいようにデータフレーム化する。８か所の位置についてそれぞれ、X方向とY方向の開始座標と終了座標が取得できるようになった。
```python
# 切り出す座標、サイズを設定（マーク）
mark_x_pos = [164,284,404,524]   # X方向開始座標（４個）
mark_y_pos = [234,394]           # Y方向開始座標（２個）
mark_x_size = 56                 # Xサイズ
mark_y_size = 56                 # Yサイズ

# 切り出す座標、サイズを設定（数字）
number_x_pos = [198,318,438,558] # X方向開始座標（４個）
number_y_pos = [285,445]         # Y方向開始座標（２個）
number_x_size = 32               # Xサイズ
number_y_size = 32               # Yサイズ

# このままでは扱いにくいので、８個の矩形をデータフレーム化する
def get_pos_list(x_pos,y_pos,x_size,y_size):
  pos_list = []
  for y in range(0,2):
    for x in range(0,4):
      start_x = x_pos[x]
      end_x   = start_x+x_size
      start_y = y_pos[y]
      end_y   = start_y+y_size
      pos_list.append([x,y,start_x,end_x,start_y,end_y])
  df_pos_list = pd.DataFrame(pos_list,columns=['x','y','start_x','end_x','start_y','end_y'])
  df_pos_list.set_index(['x','y'],inplace=True)
  return df_pos_list

mark_pos_list = get_pos_list(mark_x_pos,mark_y_pos,mark_x_size,mark_y_size)
number_pos_list = get_pos_list(number_x_pos,number_y_pos,number_x_size,number_y_size)
print(mark_pos_list)
```
```
     start_x  end_x  start_y  end_y
x y                                
0 0      164    220      234    290
1 0      284    340      234    290
2 0      404    460      234    290
3 0      524    580      234    290
0 1      164    220      394    450
1 1      284    340      394    450
2 1      404    460      394    450
3 1      524    580      394    450
```

正しく設定できるか確認するために、選択位置矩形を表示してみる。（マークは赤枠、数字は青枠）
```python
import numpy as np
from PIL import Image, ImageDraw
from matplotlib import pyplot as plt

# １枚読み込んで、表示してみる
filename_png = 'data/' + df_master.iloc[0,0]
img = Image.open(filename_png)
draw = ImageDraw.Draw(img)

# マーク（赤枠）
for index, pos in mark_pos_list.iterrows():
  draw.rectangle((pos.start_x, pos.start_y, pos.end_x, pos.end_y), outline=(255, 0, 0), width =10)
# 数字（青枠）
for index, pos in number_pos_list.iterrows():
  draw.rectangle((pos.start_x, pos.start_y, pos.end_x, pos.end_y), outline=(0, 0, 255), width =10)

np_img = np.asarray(img)
plt.imshow(np_img)
plt.axis('off')
plt.show()
```
![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/9342f83a-8091-960f-e09b-f6ee037d2ddf.png)

## 教師データのラベリング

全体（マーク＋数字）のラベル付け、及び、マークと数字をそれぞれ認識するので、それぞれのラベル付けを行う。
```python
# 全体のラベル
label_list = {'D15':0,'D10':1,'D5':2,'H3':3,'H2':4,'H1':5,'BL':6}
# マークのラベル
mark_label_list = {'D':0,'H':1,'B':2}
# 数字のラベル
number_label_list = {'15':0, '10':1, '5':2, '3':3, '2':4, '1':5,'0':6}

# キーから値を取得
def get_keys_from_value(val,list=label_list):
    return [k for k, v in list.items() if v == val]

# キーのリストを取得
def get_key_list(list):
  return [key for key in list]

# 全体ラベルからマークラベルの取得
def get_mark_label(label):
  return label[0] # ラベルの１文字目(D/H/B)

# 全体ラベルから数字ラベルの取得
def get_number_label(label):
  if label=='BL':
    return '0'
  return label[1:]
```

## 前処理
設定した選択位置の座標毎に教師データを切り出す。画像認識するので、[0,1]に正規化し、RGBの順番を入れ替えて、numpy array として戻すようにする。

```python
import os

# マーク/数字部分の切り出し
def cut_image(img,x,y,pos,label=None):
  img_cut = img.crop((pos.start_x, pos.start_y, pos.end_x, pos.end_y))

  # ファイル保存（デバッグ用）
  if label:
    dirname_out = 'cut/' + label
    if not os.path.isdir(dirname_out):
      os.mkdir(dirname_out)
    filename_out = dirname_out + '/y' + str(y) + '_x' + str(x) + '_' + filename
    img_cut.save(filename_out, quality=100)

  # np array に変換し、[0,255]諧調 を [0,1]諧調に変換
  np_img = np.array(img_cut,dtype=float)
  np_img /= 255

  # 横,縦,RGB を RGB,横,縦 に変換
  size_x = np_img.shape[0]
  size_y = np_img.shape[1]
  image_r = np_img[:,:,0]
  image_r  = image_r.reshape( 1, size_x, size_y)
  image_g = np_img[:,:,1]
  image_g  = image_g.reshape( 1, size_x, size_y)
  image_b = np_img[:,:,2]
  image_b  = image_b.reshape( 1, size_x, size_y)
  return np.vstack([image_r,image_g,image_b])
```
切り出した結果の例を図示する。

【マーク】左からダイヤ、ハート、BL
![y0_x0_IMG_2465.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/150924a3-b6d9-f371-a967-4839d421186f.png)　![y0_x0_IMG_2467.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/35bdf0be-b3fd-aa01-b135-899237f8b7f7.png)　![y1_x2_IMG_3598.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/14cfbdb6-bf76-0127-24c4-66b20e95b91b.png)
【数字】左からx15、x10、x5、x3、x2、x1、BL
![y0_x0_IMG_3380.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/43af2476-edca-5871-856f-979885cc4fdd.png)　![y0_x0_IMG_2465.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/4ae6d429-d507-35fc-8120-03520b93896d.png)　![y0_x0_IMG_2475.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/65fc6e73-fc59-2c43-25a0-a329abbc3ded.png)　![y0_x0_IMG_2467.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/dce8b215-ecbf-9f9e-6d41-f37cef2f2d58.png)　![y0_x0_IMG_3357.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/6dfc72d1-8aa0-2529-27d9-31e1c061e3a8.png)　![y0_x0_IMG_3299.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/6cb25153-559c-e5f7-bd88-618f676f2713.png)　![y1_x2_IMG_3598.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/208543f1-6848-6563-515c-12a6e5196374.png)

同じマークは全く同じ画像（バイナリレベルで）と考えていたが、よく見ると、キラキラした部分がそれぞれ異なり、バイナリレベルでは同じ画像ではない。
![y0_x0_IMG_3463.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/cf1e2abc-c552-814c-c888-c9db922416d9.png)　![y0_x0_IMG_3761.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/b79f2e8d-a55b-ed5c-0ead-289914e61dbb.png)　![y0_x0_IMG_3791.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/fa8e246f-a62b-2cdf-4c25-993a0e6ba891.png)　![y0_x0_IMG_3816.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/649fff24-2526-3b6c-bfd5-7dbf49a038f7.png)　![y0_x0_IMG_3819.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/8e020e11-5cca-8fbc-7e28-0fc0fba57ace.png)

# 教師データの作成

全ファイルを読み込み、教師データを切り出しリスト化する。

```python
# 教師データのリスト（マーク）
mark_train_img_list = []
mark_y_train = []
# 教師データのリスト（数字）
number_train_img_list = []
number_y_train = []

for index, label_info in df_master.iterrows():
  filename = label_info[0]
  filename_png = 'data/' + filename
  img = Image.open(filename_png)

  # マーク
  for (x,y), pos in mark_pos_list.iterrows():
    label = label_info[1+x+y*4]
    mark_label = get_mark_label(label)
    mark_cut_image = cut_image(img,x,y,pos,mark_label)
    mark_train_img_list.append(mark_cut_image)
    mark_y_train.append(mark_label_list[mark_label])
  # 数字
  for (x,y), pos in number_pos_list.iterrows():
    label = label_info[1+x+y*4]
    number_label = get_number_label(label)
    number_cut_image = cut_image(img,x,y,pos,number_label)
    number_train_img_list.append(number_cut_image)
    number_y_train.append(number_label_list[number_label])

mark_train_img_list = np.array(mark_train_img_list)
print(mark_train_img_list.shape)

number_train_img_list = np.array(number_train_img_list)
print(number_train_img_list.shape)

```

```
(80, 3, 56, 56)
(80, 3, 32, 32)
```
教師データは、画像数(10)x８個が教師データとなり合計80個、RGBの3色毎に、マークサイズが56x56、数字サイズが32x32で、ということがわかる。

# マーク認識

## 分類の特徴量

ハートは赤、ダイヤは青、BLは黒なので、全く同じ画像ではないものの、簡単に分類できそうである。
RGB値の平均で分類できそうか、RGBの平均を箱ひげ図で可視化する。
```python
# マークのイメージリストから、説明変数xを取得する
def get_mark_x(img_list):
  # rgbの平均をとる
  mark_mean = [rgb.mean() for img in img_list for rgb in img]
  # numpy array に変換し、画像数xRGBに変形
  np_mark_mean = np.array(mark_mean)
  np_mark_mean = np_mark_mean.reshape(-1,3)
  # 平均
  mark_x_colums=['r_mean','g_mean','b_mean']
  return pd.DataFrame( np_mark_mean, columns=mark_x_colums)

mark_X_train = get_mark_x(mark_train_img_list)

# 各マーク毎に、箱ひげ図を表示

fig = plt.figure(figsize=(15, 3))
index = 1

for i, colums in enumerate(mark_X_train.columns):
  plot_list = list()
  for key, valule in mark_label_list.items():
    mark_X_train_temp = mark_X_train.copy()
    mark_X_train_temp['y'] = mark_y_train
    mark_X_train_temp = mark_X_train_temp[mark_X_train_temp['y']==valule]
    mark_X_train_temp = mark_X_train_temp.iloc[:,i]
    plot_list.append(mark_X_train_temp)
  ax = fig.add_subplot(1, 3, index)
  ax.boxplot(plot_list,labels=mark_label_list.keys())
  ax.set_title(colums)
  index += 1
```
![output2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/26d8db83-9e7e-797a-e28b-d767f8ac7597.png)

左から赤(R)平均、緑(G)平均、青(B)平均のグラフである。
BLは全体的に暗めで簡単に分類可能そうだが、ハートは赤いのでRでダイヤと区別できると思いきや、ダイヤのRも高くRでは区別できない。それでも、GかBでは区別できそうである。

## 学習用と評価用に分割
```python
from sklearn.model_selection import train_test_split
mark_X_tra, mark_X_val, mark_y_tra, mark_y_val = train_test_split(mark_X_train, mark_y_train,train_size=0.7, random_state=0)
print(f'train_size={mark_X_tra.shape[0]},test_size={mark_X_val.shape[0]}')
```
```
train_size=56,test_size=24
```
80個の教師データを、学習データ70%、評価データ30%に分割したので、それぞれ56個、24個に分割できた。

## 決定木による分類

決定木で学習と予測し、認識したマーク数を集計すると、うまく分類できている。（認識率100%）
```python
# 決定木
from sklearn.tree import DecisionTreeClassifier
model_dt = DecisionTreeClassifier(max_depth=3,random_state=0)
model_dt.fit(mark_X_tra, mark_y_tra)
# 予測
y_pred_dt = model_dt.predict(mark_X_val)
# 認識率
result = y_pred_dt==mark_y_val
print(f'認識率={result.mean()*100:.3f}%')

# 認識結果まとめ（マーク）
mark_np_result = np.zeros([3,3],dtype=int)
for predict, label in zip(y_pred_dt,mark_y_val):
  mark_np_result[label][predict] += 1
mark_df_result = pd.DataFrame(mark_np_result,index=mark_label_list.keys(),columns=mark_label_list.keys())
print('認識したマーク（縦:正解、横：予測）')
print(mark_df_result)
```
```
認識率=100.000%
認識したマーク（縦:正解、横：予測）
   D   H  B
D  8   0  0
H  0  12  0
B  0   0  4
```

## 【参考】学習済モデルの可視化
学習済モデルを可視化する。[graphviz でシンプルな分類木を描く](https://qiita.com/bellvine/items/c1bd011c66f9d6349d53)を参考とさせていただきました。
```python
import graphviz
data = tree.export_graphviz(model_dt,
                     feature_names = mark_X_tra.columns,
                     impurity = True,
                     class_names = get_key_list(mark_label_list),
                     filled=False, rounded=True
                     )
graph = graphviz.Source(data)
graph
```
![model_dt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/902e7de2-aee3-7659-88e1-46ed4dfd568c.png)

まず、Greenの平均で、ダイヤを分類し、Blueの平均で、BLとハートに分類していることがわかる。

# 数字認識

RGBの平均では数字は認識できないので、数字はCNNで認識する。

## 学習用と評価用に分割
ここはマークの認識と全く同様。
```python
# 訓練データ、評価データに分割する。
number_X_tra, number_X_val, number_y_tra, number_y_val = train_test_split(number_train_img_list, number_y_train,train_size=0.7, random_state=0)
print(f'train_size={number_X_tra.shape[0]},test_size={number_X_val.shape[0]}')
```
```
train_size=56,test_size=24
```
## CNNによる分類
CNNについては、[PyTorchでCNNを徹底解説](https://qiita.com/mathlive/items/8e1f9a8467fff8dfd03c)を参考とさせて頂きました。
ベースは同じですが、ハイパーパラメータを弄ったり、ドロップアウトを追加したり、いろいろと試して、細かい点は変更しました。

```python
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt    #グラフ出力用module

BATCH_SIZE = 256
WEIGHT_DECAY = 0.005
LEARNING_RATE = 0.1
EPOCH = 100

X_train_torch = torch.tensor(number_X_tra, dtype=torch.float)
y_train_torch = torch.tensor(number_y_tra, dtype=torch.long)
trainset = torch.utils.data.TensorDataset(X_train_torch, y_train_torch)
trainloader  = torch.utils.data.DataLoader(trainset, batch_size=BATCH_SIZE)

X_test_torch = torch.tensor(number_X_val, dtype=torch.float)
y_test_torch = torch.tensor(number_y_val, dtype=torch.long)
testset = torch.utils.data.TensorDataset(X_test_torch, y_test_torch)
testloader  = torch.utils.data.DataLoader(testset, batch_size=BATCH_SIZE)

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool2d(2)
        self.conv1 = nn.Conv2d(3, 16, (5, 5))
        self.conv2 = nn.Conv2d(16, 32, (5, 5))
        self.fc1 = nn.Linear(800, 256)
        self.fc2 = nn.Linear(256, 7)
        self.flatten = nn.Flatten()
        self.drop = nn.Dropout(p=0.5)

    def forward(self, x):
        x = self.conv1(x)
        x = self.relu(x)
        x = self.drop(x)
        x = self.pool(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.drop(x)
        x = self.pool(x)
        x = self.flatten(x)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.drop(x)
        x = self.fc2(x)
        return x

device = torch.device("cpu")
net = Net()
net = net.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=LEARNING_RATE)

train_loss_value=[]      #trainingのlossを保持するlist
train_acc_value=[]       #trainingのaccuracyを保持するlist
test_loss_value=[]       #testのlossを保持するlist
test_acc_value=[]        #testのaccuracyを保持するlist 

for epoch in range(EPOCH):
    print('epoch', epoch+1)    #epoch数の出力
    for (inputs, labels) in trainloader:
        inputs, labels = inputs.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
    sum_loss = 0.0          #lossの合計
    sum_correct = 0         #正解率の合計
    sum_total = 0           #dataの数の合計

    #train dataを使ってテストをする(パラメータ更新がないようになっている)
    for (inputs, labels) in trainloader:
        inputs, labels = inputs.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        sum_loss += loss.item()                            #lossを足していく
        _, predicted = outputs.max(1)                      #出力の最大値の添字(予想位置)を取得
        sum_total += labels.size(0)                        #labelの数を足していくことでデータの総和を取る
        sum_correct += (predicted == labels).sum().item()  #予想位置と実際の正解を比べ,正解している数だけ足す
    print("train mean loss={}, accuracy={}"
            .format(sum_loss*BATCH_SIZE/len(trainloader.dataset), float(sum_correct/sum_total)))  #lossとaccuracy出力
    train_loss_value.append(sum_loss*BATCH_SIZE/len(trainloader.dataset))  #traindataのlossをグラフ描画のためにlistに保持
    train_acc_value.append(float(sum_correct/sum_total))   #traindataのaccuracyをグラフ描画のためにlistに保持

    sum_loss = 0.0
    sum_correct = 0
    sum_total = 0
    net.eval()

    #test dataを使ってテストをする
    for (inputs, labels) in testloader:
        inputs, labels = inputs.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        sum_loss += loss.item()
        _, predicted = outputs.max(1)
        sum_total += labels.size(0)
        sum_correct += (predicted == labels).sum().item()
    print("test  mean loss={}, accuracy={}"
            .format(sum_loss*BATCH_SIZE/len(testloader.dataset), float(sum_correct/sum_total)))
    test_loss_value.append(sum_loss*BATCH_SIZE/len(testloader.dataset))
    test_acc_value.append(float(sum_correct/sum_total))
    net.train()

#以下グラフ描画
plt.plot(range(EPOCH), train_loss_value)
plt.plot(range(EPOCH), test_loss_value, c='#00ff00')
plt.xlim(0, EPOCH)
plt.xlabel('EPOCH')
plt.ylabel('LOSS')
plt.legend(['train loss', 'test loss'])
plt.title('loss')
plt.show()
plt.clf()

plt.plot(range(EPOCH), train_acc_value)
plt.plot(range(EPOCH), test_acc_value, c='#00ff00')
plt.xlim(0, EPOCH)
plt.xlabel('EPOCH')
plt.ylabel('ACCURACY')
plt.legend(['train acc', 'test acc'])
plt.title('accuracy')
plt.show()
```

```
epoch 1
train mean loss=8.184068407331194, accuracy=0.375
test  mean loss=20.446175893147785, accuracy=0.25
epoch 2
train mean loss=8.101869310651507, accuracy=0.375
test  mean loss=20.058897654215496, accuracy=0.25
;（後略）
```
![output3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/b210b0d5-7016-70f9-a2c5-3e8fe6b2723e.png)
![output4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3570565/614e5b97-863c-76ba-b6f9-36c0a37a17e7.png)

順調に誤差が減少し、100エポックの学習を終えると、正解率もほぼ１(100%)となっていることがわかる。
それでは、この学習モデルを使って、評価用データを予測してみる。

```python
# 予測
def predict_number(X):
  X_torch = torch.tensor(X, dtype=torch.float)
  y_pred = net(X_torch)
  _, predicted = torch.max(y_pred.data, 1)
  # numpy array に変換
  normal_list = []
  for i in predicted:
    normal_list.append(int(i))
  return np.array(normal_list)

# 予測
predicted = predict_number(number_X_val)
result = predicted==number_y_val
print(f'認識率={result.mean()*100:.3f}%')

# 認識結果まとめ（数字）
number_np_result = np.zeros([7,7],dtype=int)
for predict, label in zip(predicted,number_y_val):
  number_np_result[label][predict] += 1
number_df_result = pd.DataFrame(number_np_result,index=number_label_list.keys(),columns=number_label_list.keys())
print('認識した数字（縦:正解、横：予測）')
print(number_df_result)
```
```
認識率=100.000%
認識した数字（縦:正解、横：予測）
    15  10  5  3  2  1  0
15   1   0  0  0  0  0  0
10   0   4  0  0  0  0  0
5    0   0  3  0  0  0  0
3    0   0  0  3  0  0  0
2    0   0  0  0  3  0  0
1    0   0  0  0  0  6  0
0    0   0  0  0  0  0  4
```
認識した数字を集計すると、うまく分類できている。（認識率100%）

# 全体（マーク＋数字）の分類
１画面（１ファイル）のマークと数字の認識結果を用いて、以下のように処理する。
* ８個の位置について、それぞれ、マークと数字を認識し、結果を結合する。
* １つはBLなので、他の７個を認識した結果以外のマークと数字をBLにあてがう。
  *  具体的には、
  *  label_one_image というリストに、８個の取得できるラベルを定義しておき、
  *  そのリストから、BL以外で認識したマークのラベルを削除していく。
  *  残ったラベルのマークが、BLの位置のマークである。
  *  と判断する。
* もし、BL以外の７個の認識を間違えていた場合、エラーとしエラーとなった画像ファイル名を出力し、除外する。
* 可能性としては、２個をそれぞれ反対に認識する（例：H1をH3と誤認識し、H3をH1と誤認識する）とつじつまがあうが、これまでの認識率の高さから、可能性は低いとして、このようなケースは考えない。
```python
from PIL import Image
import numpy as np

# １ファイルの処理
# 戻り値は、予測したラベルのリスト
# 0 filename, #1 y0_x0, #2 y0_x1, #3 y0_x2, #4 y0_x3, #5 y1_x0, #6 y1_x1, #7 y1_x2, #8 y1_x3, #9 選択index(1-8)
def predict_1file(filename):
  basename = os.path.basename(filename)
  # 読み込み
  img = Image.open(filename)
  # 予測したラベル
  predict_label = []
  predict_label.append(basename)
  # １毎画像のラベル
  label_one_image = ['D15','D10','D5','H3','H2','H1','H1','H1']
  # テストデータのリスト
  mark_test_img_list = []
  number_test_img_list = []
  # マークの切り出し、認識
  for (x,y), pos in mark_pos_list.iterrows():
    mark_cut_image = cut_image(img,x,y,pos)
    mark_test_img_list.append(mark_cut_image)
  mark_test_img_list = np.array(mark_test_img_list)
  mark_X_test = get_mark_x(mark_test_img_list)
  mark_y_pred_test = model_dt.predict(mark_X_test)
  mark_label = [get_keys_from_value(y,mark_label_list)[0] for y in mark_y_pred_test]
  # 数字の切り出し、認識
  for (x,y), pos in number_pos_list.iterrows():
    number_cut_image = cut_image(img,x,y,pos)
    number_test_img_list.append(number_cut_image)
  number_test_img_list = np.array(number_test_img_list)
  number_X_test = number_test_img_list
  number_y_pred_test = predict_number(number_X_test)
  number_label = [get_keys_from_value(y,number_label_list)[0] for y in number_y_pred_test]
  # ラベル（マーク＋数字）
  for mark, number in zip(mark_label,number_label):
    label = mark+number
    predict_label.append(label)
    if label in label_one_image:
     del label_one_image[label_one_image.index(label)]

  # １画像でうまく認識できた
  if len(label_one_image)==1:
    bk_index = predict_label.index('B0')
    predict_label[bk_index] = label_one_image[0]
    predict_label.append(bk_index)
    return predict_label
  else:
    print('error'+basename)
    print(predict_label)
    return None
```
上記１ファイルの処理関数を使って、取得した全画像５０枚について処理する。

```python
import glob

# 予測したラベルのリストを取得
def get_predict_label_list(dir):
  predict_label_list = []
  # ファイルをループ
  files = glob.glob( dir + '/*.PNG' )
  for file in files:
    # １ファイルの処理
    predict_label = predict_1file(file)
    if predict_label!=None:
      predict_label_list.append(predict_label)
  return predict_label_list

# 予測したラベルのリストを取得
predict_label_list = get_predict_label_list('./Data')
```

エラーが出力されないので、前述の通り、うまく認識できた（１００％）と判断する。

## 位置毎に取得できたマークの集計
位置毎に取得できたマークの集計するが、その際、H1は、１画面から３個取得できるので、1/3として、取得できる確率を全て1/8として合わせておく。
また、選択した（取得した）数と合計数も集計しておく。

```python
# 位置毎のマークの集計
def get_mark_list(predict_label_list):
  #0 filename         #1  y0_x0 #2 y0_x1 #3 y0_x2 #4 y0_x3 #5 y1_x0 #6 y1_x1 #7 y1_x2 #8 y1_x3 #9 選択index(1-8)
  mark_list = np.zeros(9*(len(label_list)-1),dtype=float)
  mark_list = mark_list.reshape( 9 , len(label_list)-1)
  for predict_label in predict_label_list:
    # 位置毎のマーク
    for index in range(1,9):
      mark = predict_label[index]
      mark_list[index-1, label_list[mark]] += 1
    # Getしたマーク（BLの場所）
    bk_index = predict_label[9]
    mark = predict_label[bk_index]
    mark_list[8,label_list[mark]] += 1

  # H1は、３枚出るので、1/3しておく
  mark_list[:,-1] /= 3
  # 合計を計算
  sum_list = mark_list[:-1,:].sum(axis=0)
  mark_list = np.append(mark_list,mark_list[:-1,:].sum(axis=0))
  mark_list = mark_list.reshape(10,6)

  # データフレーム化
  column_list = [key for key in label_list][:-1]
  index_list = ['y'+str(y)+'_x'+str(x) for y in range(0,2)for x in range(0,4)]
  index_list.append('Get')
  index_list.append('Sum')
  df_mark_list = pd.DataFrame(mark_list,index=index_list,columns=column_list)
  return df_mark_list

# 位置毎のマークの集計
df_mark_list = get_mark_list(predict_label_list)
df_mark_list
```
```
  D15  D10  D5  H3  H2  H1
y0_x0  3.0  9.0  8.0  8.0  6.0  5.333333
y0_x1  5.0  1.0  3.0  3.0  4.0  11.333333
y0_x2  7.0  7.0  5.0  3.0  9.0  6.333333
y0_x3  2.0  3.0  2.0  12.0  11.0  6.666667
y1_x0  4.0  2.0  16.0  7.0  2.0  6.333333
y1_x1  2.0  19.0  10.0  4.0  3.0  4.000000
y1_x2  18.0 4.0  1.0  5.0  8.0  4.666667
y1_x3  9.0  5.0  5.0  8.0  7.0  5.333333
Get  9.0  5.0  5.0  8.0  7.0  5.333333
Sum  50.0  50.0  50.0  50.0  50.0  50.000000
```
縦方向はマーク位置と、取得した個数（Get）、合計（Sum）、横軸はマークである。
合計５０回のうち、同様に確からしい場合1/8の確率となるので、期待値は6.25となる。
ダイヤ(x15)は、９個もらえたので、やはり多いようだ。
しかしながら、y1_x2では、その倍の１８個にもなり、かなり偏っているように見える。
独立性の検定をするまでもなく、関連がありそうな・・・


# 独立性の検定
選択位置と取得できるマークには関連があるかを検定したい。同様に確からしい場合どの位置でも1/8の確率となるところ、いかさまくじのようなインチキをしている場合には関連がある、という結果となる。
帰無仮説:「選択位置と取得できるマークには関連がない」
対立仮説:「選択位置と取得できるマークには関連がある」
として、独立性の検定をした。
[PythonとRで独立性の検定（カイ2乗検定・フィッシャーの直接確率検定）](https://qiita.com/code0327/items/376955fe8479407f7982)を参考とさせて頂きました。

```python
import scipy.stats as st

# 独立性の検定
def test_of_independence(df_mark_list,alpha = 0.05):
  # 取得と合計を削除
  df_mark_list_wo_GetSum = df_mark_list[:-2]
  print(df_mark_list_wo_GetSum)

  x2, p, dof, e = st.chi2_contingency(df_mark_list_wo_GetSum,correction=False)
  print(f'p値 　　　= {p}')
  print(f'カイ2乗値 = {x2:.2f}')
  print(f'自由度　  = {dof}')

  print(f'よって、帰無仮説:「選択位置と取得できるマークには関連がない」は、有意水準{alpha}において、',end='')
  if p < alpha:
    print('棄却される')
  else:
    print('採択される')

# 独立性の検定
test_of_independence(df_mark_list)

```

```
        D15   D10    D5    H3    H2         H1
y0_x0   3.0   9.0   8.0   8.0   6.0   5.333333
y0_x1   5.0   1.0   3.0   3.0   4.0  11.333333
y0_x2   7.0   7.0   5.0   3.0   9.0   6.333333
y0_x3   2.0   3.0   2.0  12.0  11.0   6.666667
y1_x0   4.0   2.0  16.0   7.0   2.0   6.333333
y1_x1   2.0  19.0  10.0   4.0   3.0   4.000000
y1_x2  18.0   4.0   1.0   5.0   8.0   4.666667
y1_x3   9.0   5.0   5.0   8.0   7.0   5.333333
p値 　　　= 5.5391302763822504e-11
カイ2乗値 = 118.37
自由度　  = 35
よって、帰無仮説:「選択位置と取得できるマークには関連がない」は、有意水準0.05において、棄却される
```
p値がマイナス１１乗なので、極めて稀で起こる確率である。
有意水準を5%と設定しておいたが、まさにけた違いであり得ない確率である。
ダイヤ(x15)は、９個もらえたので、いかさまというわけではないが、かなり偏った出方であることがわかった。

# 選択位置を変えてもう一度、検定
右下(y1_x3)を選択し続けた結果、その隣(y1_x2)でダイヤ(x15)が多いので、y1_x2を選択することを５０回試してみた。
その結果に対して、同じ処理をしてみるみると・・・
```python
# 予測したラベルのリストを取得
predict_label_list2 = get_predict_label_list('./Data2')
# 位置毎のマークの集計
df_mark_list2 = get_mark_list(predict_label_list2)
# 独立性の検定
test_of_independence(df_mark_list2)
```

```
        D15   D10    D5    H3    H2         H1
y0_x0   5.0   4.0   8.0  12.0   7.0   4.666667
y0_x1   5.0   2.0   2.0   5.0   5.0  10.333333
y0_x2   1.0   5.0   5.0   0.0   9.0  10.000000
y0_x3   7.0   4.0   5.0  13.0  10.0   3.666667
y1_x0   4.0   5.0  18.0   7.0   3.0   4.333333
y1_x1   2.0  22.0   1.0   3.0   4.0   6.000000
y1_x2   6.0   2.0  10.0   8.0   7.0   5.666667
y1_x3  20.0   6.0   1.0   2.0   5.0   5.333333
p値 　　　= 4.118824762977543e-17
カイ2乗値 = 155.41
自由度　  = 35
よって、帰無仮説:「選択位置と取得できるマークには関連がない」は、有意水準0.05において、棄却される
```

今度は、y1_x2の位置ではダイヤ(x15)が6個と平均以下となり、y1_x3の位置でダイヤ(x15)が１９個と突出している。当然、独立性の検定では、帰無仮説は棄却される。

# 結論
選択した位置により、マークの出る確率が変わっている。

予測モデルをうまく作れば、より多くのダイヤ(x15)がもらえそうなので、検討してみようかなとおもっていますが、良い結果がまとまればまた報告します。