2
1
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

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

Posted at

目的

上海というスマホゲームにはログインボーナスがあり、8個並んだ配列から1個選ぶと、ダイヤかハートがもらえる。(細かくは記載しないが)ダイヤのほうが価値があるので、ダイヤを当てたい。

IMG_3883_resize.png ⇒TouchToStart⇒ IMG_3884_resize.png ⇒SelectOne⇒ IMG_3885_resize.png ⇒OK⇒ IMG_3886_resize.png

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

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

独立性の検定とは、「2つの変数に対する2つの観察(2x2分割表で表される)が互いに独立かどうかを検定する。例えば、「別の地域の人々について、選挙である候補を支持する頻度が違う」かどうかを検定する方法」Wikipediaより引用。
今回は、選択位置と取得できるマーク数という2変数の独立性を検定する。

補足

検定する上で、適合度の検定なのか、独立性の検定なのか、判断に迷いました。マークの出方が同様に確からしいかを検定するので、適合度の検定があっているように思いましたが、選択した位置という変数もあるので、独立性の検定にしました。参考:適合度検定Wikipediaより

データの取得

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

画像認識

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

教師データの切り出し

教師データを切り出す必要があるので、マスターファイルに1ファイルずつ、選択位置とマークを目視確認して記載する。それを読み込むと、以下のようになる。

  • 画像左から右にx0,x1,x2,x3、上から下にy0,y1とし(例:右下はy1_x3)、そのラベルを記載する。
  • ラベルはダイヤをD、ハートをH、それに続けて数字とした。
  • 選択した位置からは牌が消えてブランクとなるので、BLというラベルとした。
  • 10画像分のラベルを作成したので、10枚x8個=80個の教師データが作成される。
# 教師データのラベリングしたマスターファイルを作成し読み込む
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

選択位置の座標については、マークと数字でそれぞれ設定し、扱いやすいようにデータフレーム化する。8か所の位置についてそれぞれ、X方向とY方向の開始座標と終了座標が取得できるようになった。

# 切り出す座標、サイズを設定(マーク)
mark_x_pos = [164,284,404,524]   # X方向開始座標(4個)
mark_y_pos = [234,394]           # Y方向開始座標(2個)
mark_x_size = 56                 # Xサイズ
mark_y_size = 56                 # Yサイズ

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

# このままでは扱いにくいので、8個の矩形をデータフレーム化する
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

正しく設定できるか確認するために、選択位置矩形を表示してみる。(マークは赤枠、数字は青枠)

import numpy as np
from PIL import Image, ImageDraw
from matplotlib import pyplot as plt

# 1枚読み込んで、表示してみる
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

教師データのラベリング

全体(マーク+数字)のラベル付け、及び、マークと数字をそれぞれ認識するので、それぞれのラベル付けを行う。

# 全体のラベル
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] # ラベルの1文字目(D/H/B)

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

前処理

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

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 y0_x0_IMG_2467.PNG y1_x2_IMG_3598.PNG
【数字】左からx15、x10、x5、x3、x2、x1、BL
y0_x0_IMG_3380.PNG y0_x0_IMG_2465.PNG y0_x0_IMG_2475.PNG y0_x0_IMG_2467.PNG y0_x0_IMG_3357.PNG y0_x0_IMG_3299.PNG y1_x2_IMG_3598.PNG

同じマークは全く同じ画像(バイナリレベルで)と考えていたが、よく見ると、キラキラした部分がそれぞれ異なり、バイナリレベルでは同じ画像ではない。
y0_x0_IMG_3463.PNG y0_x0_IMG_3761.PNG y0_x0_IMG_3791.PNG y0_x0_IMG_3816.PNG y0_x0_IMG_3819.PNG

教師データの作成

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

# 教師データのリスト(マーク)
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)x8個が教師データとなり合計80個、RGBの3色毎に、マークサイズが56x56、数字サイズが32x32で、ということがわかる。

マーク認識

分類の特徴量

ハートは赤、ダイヤは青、BLは黒なので、全く同じ画像ではないものの、簡単に分類できそうである。
RGB値の平均で分類できそうか、RGBの平均を箱ひげ図で可視化する。

# マークのイメージリストから、説明変数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

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

学習用と評価用に分割

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%)

# 決定木
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 でシンプルな分類木を描くを参考とさせていただきました。

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

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

数字認識

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

学習用と評価用に分割

ここはマークの認識と全く同様。

# 訓練データ、評価データに分割する。
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を徹底解説を参考とさせて頂きました。
ベースは同じですが、ハイパーパラメータを弄ったり、ドロップアウトを追加したり、いろいろと試して、細かい点は変更しました。

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
output4.png

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

# 予測
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%)

全体(マーク+数字)の分類

1画面(1ファイル)のマークと数字の認識結果を用いて、以下のように処理する。

  • 8個の位置について、それぞれ、マークと数字を認識し、結果を結合する。
  • 1つはBLなので、他の7個を認識した結果以外のマークと数字をBLにあてがう。
    • 具体的には、
    • label_one_image というリストに、8個の取得できるラベルを定義しておき、
    • そのリストから、BL以外で認識したマークのラベルを削除していく。
    • 残ったラベルのマークが、BLの位置のマークである。
    • と判断する。
  • もし、BL以外の7個の認識を間違えていた場合、エラーとしエラーとなった画像ファイル名を出力し、除外する。
  • 可能性としては、2個をそれぞれ反対に認識する(例:H1をH3と誤認識し、H3をH1と誤認識する)とつじつまがあうが、これまでの認識率の高さから、可能性は低いとして、このようなケースは考えない。
from PIL import Image
import numpy as np

# 1ファイルの処理
# 戻り値は、予測したラベルのリスト
# 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)
  # 1毎画像のラベル
  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)]

  # 1画像でうまく認識できた
  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

上記1ファイルの処理関数を使って、取得した全画像50枚について処理する。

import glob

# 予測したラベルのリストを取得
def get_predict_label_list(dir):
  predict_label_list = []
  # ファイルをループ
  files = glob.glob( dir + '/*.PNG' )
  for file in files:
    # 1ファイルの処理
    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')

エラーが出力されないので、前述の通り、うまく認識できた(100%)と判断する。

位置毎に取得できたマークの集計

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

# 位置毎のマークの集計
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は、3枚出るので、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)、横軸はマークである。
合計50回のうち、同様に確からしい場合1/8の確率となるので、期待値は6.25となる。
ダイヤ(x15)は、9個もらえたので、やはり多いようだ。
しかしながら、y1_x2では、その倍の18個にもなり、かなり偏っているように見える。
独立性の検定をするまでもなく、関連がありそうな・・・

独立性の検定

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

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値がマイナス11乗なので、極めて稀で起こる確率である。
有意水準を5%と設定しておいたが、まさにけた違いであり得ない確率である。
ダイヤ(x15)は、9個もらえたので、いかさまというわけではないが、かなり偏った出方であることがわかった。

選択位置を変えてもう一度、検定

右下(y1_x3)を選択し続けた結果、その隣(y1_x2)でダイヤ(x15)が多いので、y1_x2を選択することを50回試してみた。
その結果に対して、同じ処理をしてみるみると・・・

# 予測したラベルのリストを取得
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)が19個と突出している。当然、独立性の検定では、帰無仮説は棄却される。

結論

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

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

2
1
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
2
1