ローリングコンバットピッチなう!

AIとか仮想化とかペーパークラフトとか

MNISTで学習させたニューラルネットワークに自分の手書き文字を認識させてみる

前回「chainer用のMNISTのデータの中身を確認する」

rc30-popo.hatenablog.com

で、chainerを使ってMNISTのデータ(28x28dot 手書き数字 60,000個)を学習させてみました。コード自体は以下のAI研究所さんのブログのものをほぼ丸コピーです。

機械学習用ライブラリ「Chainer」を使ったディープラーニング
https://ai-kenkyujo.com/2017/09/18/chainer-deeplearning/

で、学習した結果をMNISTに付属するテスト用データで検証すると正解率97%とか簡単に出ます。そうすると今度は自分で描いた文字を識別させてみたくなります。

ペンタブとか高尚なものは持っていないので、お絵かきツールにマウスで0〜9の数値を描きます。28x28dotに描くのはちょっと辛いので、100x100とか200x200くらいの大きさの正方形キャンバスに背景色黒、前景色白で描いてpngかjpgで保存します。

こんな感じで、ファイル名の先頭1文字目からラベルが読める様にしてあります。

で、これをMNISTで学習したニューラルネットワークに食わせるコードを書いたのですが、6割くらいしか正解しない。中間層の総数を増やしたり、ユニット数を増やしたりしましたがあまり結果には変化ありません。

うーん、ということでMNISTの学習用データの一部を自分が描いたデータに差し替えて学習させる事にしました。元の学習用コードがバッチサイズ1000なので、1000個データを追加すれば良いのですが、それは辛いので50個(0〜9を5セット)描いて、MNISTの学習用データの最後の50個は使わずに自分が描いたデータで学習させます。
学習用データのフォルダを作り、そこに0a.png,0b.png,...9a.png,9b.png..という具合にファイル名の先頭1文字目が0〜9のラベルを示す様にして以下のコードを動かしました。

# -*- Coding: utf-8 -*-

# OS
import os
# OpenCV
import cv2
# Numpy
import numpy as np
# Chainer
import chainer
import chainer.links as L
import chainer.functions as F
from chainer import Chain,optimizers,Variable
# 学習結果ファイル保存用serializerをimport
from chainer import serializers

train, test = chainer.datasets.get_mnist()

train_len = len(train)

# 自分が描いた学習用データの保存フォルダ
mydata_dir = './mydata/'
mydata_files = os.listdir(mydata_dir)
mydata_len = len(mydata_files)


x_train = []
x_test  = []
t_train = []
t_test  = []

train_cnt = 0
# 学習用データ60,000-自分の手描きデータ数をまずlistに登録
train_max = train_len - mydata_len
for line in train:
    x_train.append(line[0])
    t_train.append(int(line[-1]))
    train_cnt += 1
    if train_cnt == train_max:
        break
# 同じlistに自分が描いた学習用データを登録
for test_file in mydata_files:
    if test_file.endswith('.png'):
        image = cv2.imread(mydata_dir + test_file)
        image = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
        image_resized = cv2.resize(image,(28,28),interpolation=cv2.INTER_AREA)        
        image_gs = image_resized.flatten()
        image_gs = np.array(image_gs,dtype=np.float32)
        image_gs /= 255
        x_train.append(image_gs)
        t_train.append(int(test_file[0]))

for line in test:
    x_test.append(line[0])
    t_test.append(int(line[-1]))

x_train = np.array(x_train,dtype=np.float32)
x_test = np.array(x_test,dtype=np.float32)
t_train = np.array(t_train,dtype=np.int32)
t_test = np.array(t_test,dtype=np.int32)

# DNN class
class DNN(Chain):
    def __init__(self):
        super(DNN, self).__init__(
            l1 = L.Linear(784,100),
            l2 = L.Linear(100,100),
            l3 = L.Linear(100,10)
        )
    def forward(self,x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        h3 = self.l3(h2)
        return h3

# Create DNN class instance
model = DNN()

# Set optimizer
optimizer = optimizers.Adam()
optimizer.setup(model)

# Number of epochs
n_epoch = 30

# batch size
batch_size = 1000

# Execute Training

for epoch in range(n_epoch):
    sum_loss = 0
    perm = np.random.permutation(60000)
    for i in range(0, 60000, batch_size):
        x = Variable(x_train[perm[i:i+batch_size]])
        t = Variable(t_train[perm[i:i+batch_size]])
        y = model.forward(x)
        model.cleargrads()
        loss = F.softmax_cross_entropy(y, t)
        loss.backward()
        optimizer.update()
        sum_loss += loss.data*batch_size

    print("epoch: {}, mean loss: {}".format(epoch, sum_loss/60000))

# 学習結果をファイルに保存
serializers.save_npz("mnist_model_custom.npz", model)

# Execute Test
cnt = 0
for i in range(10000):
    x = Variable(np.array([x_test[i]], dtype=np.float32))
    t = t_test[i]
    y = model.forward(x)
    y = np.argmax(y.data[0])
    if t == y:
        cnt += 1

# Display Result
print(cnt)
print("accuracy: {}".format(cnt/(10000)))

これを動かして自作学習データを含めて学習させます。epoch数はオリジナルの10では不足な感じだったので30に増やしました。

さて、この学習結果を別のフォルダに並べた自作テスト用データで検証します。
ファイル名の命名規則は学習用データと同じで更に20個(0〜9を2セット)描いてみました。

検証用コードはこんな感じ。テスト用データ20個しか無いのでラベルとの自動照合は省いています。(毎回手動で正解率計算するの面倒なので、ちょっとコードを改良しました)

# -*- Coding: utf-8 -*-

# OS
import os
# OpenCV
import cv2

# Matplotlib
import matplotlib.pyplot as plt

# Numpy
import numpy as np
# Chainer
import chainer
import chainer.links as L
import chainer.functions as F
from chainer import Chain,optimizers,Variable
from chainer import serializers

# DNN class
class DNN(Chain):
    def __init__(self):
        super(DNN, self).__init__(
            l1 = L.Linear(784,100),
            l2 = L.Linear(100,100),
            l3 = L.Linear(100,10)
        )
    def forward(self,x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        h3 = self.l3(h2)
        return h3

# Create DNN class instance
model = DNN()

# Load trainned model
model_data_file = './mnist_model_custom.npz'
serializers.load_npz(model_data_file, model)

# Start test
mydata_dir = './mydata2/'

mydata_files = os.listdir(mydata_dir)
mydata_files.sort()

#plt.gray()
testcnt = 0
testok  = 0
for test_file in mydata_files:
    if test_file.endswith('.png'):
        testcnt += 1
        x_test_data = []
        label = int(test_file[0])
        image = cv2.imread(mydata_dir + test_file)
        image = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
        image_resized = cv2.resize(image,(28,28),interpolation=cv2.INTER_AREA)
#        plt.imshow(image_resized)
#        plt.pause(0.5)
        image_gs = image_resized.flatten()
        x_test_data.append(image_gs)

        x_test = np.array([image_gs],dtype=np.float32)
        x_test /= 255

        x = Variable(np.array([x_test[0]],dtype=np.float32))
        y = model.forward(x)
        y = np.argmax(y.data[0])
        if y == label:
            testok += 1
            test_res_str = 'OK'
        else:
            test_res_str = 'NG'

        print(' test file = %s ,label = %d,test result = %d,%s' % (test_file,label,y,test_res_str))

# Display result
print('OK %d,NG %d,Total %d' % (testok,(testcnt - testok),testcnt))
print('accuracy: {}'.format(testok/testcnt))

動かした結果は以下の通りで、正解率85%でした。

(py36) toy@toy-VGN-NR52 ~/chainer_test/mnist $ python mnist_mydata_test_custom.py
/home/toy/anaconda3/envs/py36/lib/python3.6/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
  from ._conv import register_converters as _register_converters
 test file = 0f.png ,label = 0,test result = 0,OK
 test file = 0g.png ,label = 0,test result = 0,OK
 test file = 1f.png ,label = 1,test result = 1,OK
 test file = 1g.png ,label = 1,test result = 1,OK
 test file = 2f.png ,label = 2,test result = 2,OK
 test file = 2g.png ,label = 2,test result = 2,OK
 test file = 3f.png ,label = 3,test result = 3,OK
 test file = 3g.png ,label = 3,test result = 3,OK
 test file = 4f.png ,label = 4,test result = 4,OK
 test file = 4g.png ,label = 4,test result = 4,OK
 test file = 5f.png ,label = 5,test result = 5,OK
 test file = 5g.png ,label = 5,test result = 5,OK
 test file = 6f.png ,label = 6,test result = 0,NG
 test file = 6g.png ,label = 6,test result = 6,OK
 test file = 7f.png ,label = 7,test result = 3,NG
 test file = 7g.png ,label = 7,test result = 8,NG
 test file = 8f.png ,label = 8,test result = 8,OK
 test file = 8g.png ,label = 8,test result = 8,OK
 test file = 9f.png ,label = 9,test result = 9,OK
 test file = 9g.png ,label = 9,test result = 9,OK
OK 17,NG 3,Total 20
accuracy: 0.85

テストに使ったデータはこんな感じです。学習用データはこれとは別に0〜9を5セット描いています。

マウス描きなので元々安定して同じ字形は描けないのと、意図的に線の太さを変えたりしています。
人間が見れば間違えはしないと思うのですが、この程度の学習量だとまだ間違えます。学習用データ60,000点と聞くと多そうにも見えますが、0〜9の各数字毎には6,000セットづつなので、実用に使うのは学習データ量が全然足りないということでしょうか。
でも、自分で作ったデータを学習させて結果が改善するのは面白いです。

2020/01/25追記:
学習量は足りないのはおそらく事実なのですが、MNISTデータの学習だけで、自分の手書き文字の認識率が6割程度なのは過学習の影響も大きい様です。
下記エントリで再検証しています。
rc30-popo.hatenablog.com