手書き数字の画像認識 深層学習モデルの実装例
本チュートリアルでは【練習問題】手書き数字の画像認識コンペの簡単な深層学習モデルの実装例を示します.
本コンペは0~9の手書き数字が写った画像に対して0~9の数字を割り当てる問題です. 学習用として与えられる画像データは60,000枚で
評価用として与えられる画像データは10,000枚です. 評価指標は正解率(Accuracy)です.
7層の比較的浅いモデル(LeNet[1])に対して実験した結果、ほぼ99%の精度を出すことが可能であることがわかりました。
分析環境は以下のようなものを想定しています。
- OS: Windows 10 Pro
- 言語: Python==3.6.3
- ライブラリ
- pandas==0.23.0
- numpy==1.14.3
- Pillow==5.1.0
- chainer==4.0.0
- cupy==4.0.0
- GPU: Quadro M1200
はじめに¶
まずは画像データ(train.zip, test.zip)とメタデータ(train_master.tsv)をダウンロードし, 画像データを解凍します.
下記のようにそれぞれ自身で設定したディレクトリに置きます.
import os
_train_images_path = os.path.join('..', 'data', 'image','mnist','open','train')
_test_images_path = os.path.join('..', 'data', 'image', 'mnist','open','test')
_train_meta_path = os.path.join('..','data','image','mnist','open','train_master.tsv')
学習用画像データと数字の対応表を確認してみます.
import pandas as pd
train_master = pd.read_csv(_train_meta_path, sep='\t')
train_master.head()
実際に画像を確認してみます.
from PIL import Image
image = Image.open(os.path.join(_train_images_path, 'train_0.jpg'))
print('width, height:', image.size)
image
image = Image.open(os.path.join(_train_images_path, 'train_1.jpg'))
print('width, height:', image.size)
image
image = Image.open(os.path.join(_train_images_path, 'train_2.jpg'))
print('width, height:', image.size)
image
image = Image.open(os.path.join(_train_images_path, 'train_3.jpg'))
print('width, height:', image.size)
image
image = Image.open(os.path.join(_train_images_path, 'train_4.jpg'))
print('width, height:', image.size)
image
それぞれ画像の大きさは横×縦が28×28で, グレースケール画像であることが確認できました.
それぞれの数字で特徴が出ています. この例だと4と9が若干似ています.
学習用画像データにおけるそれぞれの数字の分布をみてみます.
train_master['category_id'].value_counts().sort_index()
ほぼ均等ですが, 1が多めで5が少なめでした.
手法¶
次に実際にモデリングを行い, 評価用画像データに対して予測をできるようにします.
今回は7層の比較的浅い畳み込みニューラルネットワークモデル(LeNet[1])によりモデリングを試みます.
実装はchainerを用います.
まず60,000枚の学習用画像データを読み込みます. その際, あらかじめデータの数値の範囲が[0, 1]となるように正規化しておきます.
chainerではモデルに学習データを渡す際は(サンプル数, チャンネル数, 縦, 横)の形でなければならないので, そのように変形しておきます.
import numpy as np
num_train = len(train_master)
image_size = (28,28)
train_X = np.zeros((num_train,)+(1,)+image_size)
train_Y = np.zeros((num_train,))
for data in train_master.iterrows():
image = Image.open(os.path.join(_train_images_path, data[1]['file_name']))
image_array = np.array(image).reshape((1,)+image_size)
train_X[data[0],:] = image_array/255.
train_Y[data[0]] = data[1]['category_id']
train_X = train_X.astype(np.float32)
train_Y = train_Y.astype(np.int32)
print(train_X.shape)
print(train_Y.shape)
畳み込みニューラルネットワークモデルLeNetを構築する抽象クラスを定義します.
活性化関数はtanh, relu, sigmoidを指定できるようにします. もともとはtanhです.
from chainer import Chain
import chainer.links as L
class LeNet(Chain):
def __init__(self, out_size=10, act_func = ''):
super(LeNet, self).__init__(
conv1 = L.Convolution2D(None, 6, 5, stride=1),
conv2 = L.Convolution2D(None, 16, 5, stride=1),
fc3 = L.Linear(None, 120),
fc4 = L.Linear(None, 84),
fc5 = L.Linear(None, out_size))
self.train = True
if act_func == 'sigmoid':
print('activation function is', act_func)
self.act_func = F.sigmoid
elif act_func == 'relu':
print('activation function is', act_func)
self.act_func = F.relu
else:
print('activation function is', 'tanh')
self.act_func = F.tanh
def __call__(self, x):
with chainer.using_config('enable_backprop', self.train):
h = F.max_pooling_2d(self.act_func(self.conv1(x)), 2, stride=2)
h = F.max_pooling_2d(self.act_func(self.conv2(x)), 2, stride=2)
h = self.act_func(self.fc3(h))
h = self.act_func(self.fc4(h))
y = self.fc5(h)
return y
学習データとモデルオブジェクトを渡してモデルを学習させる関数を定義します.
import chainer
import chainer.functions as F
import chainer.cuda as cuda
from chainer import Chain, optimizers, serializers
from time import clock
def train_model(model, train, model_name, val = False, batchsize = 128, init_lr = 0.01, num_epochs = 50, gpu = True):
"""
train: (np.array, np.array)
val: (np.array, np.array)
"""
X_train, Y_train = train
if val:
X_train, X_val = np.split(X_train,[54000])
Y_train, Y_val = np.split(Y_train,[54000])
n_val = len(X_val)
n_train = len(X_train)
optimizer = optimizers.NesterovAG(lr=init_lr)
optimizer.setup(model)
if gpu:
model.to_gpu()
update_start = clock()
for epoch in range(num_epochs):
print('-'*20, 'epoch:', epoch+1, '-'*20)
# training
count = 0
num_samples = 0
train_loss = 0
train_acc = 0
train_start = clock()
print('training...')
for t in range(0, n_train, batchsize):
model.cleargrads()
minibatch, labels = X_train[t:t+batchsize], Y_train[t:t+batchsize]
if gpu:
minibatch = cuda.to_gpu(minibatch)
labels = cuda.to_gpu(labels)
y = model(minibatch)
loss = F.softmax_cross_entropy(y, labels)
loss.backward()
optimizer.update()
y_pred = F.softmax(y)
preds = cuda.to_cpu(y_pred.data).argmax(axis=1)
labels = cuda.to_cpu(labels)
train_loss += float(loss.data)*len(minibatch)
train_acc += (labels==preds).sum()
count += 1
num_samples += len(minibatch)
print('train_acc:', round(train_acc/num_samples, 4), 'train_loss:', round(train_loss/num_samples, 4))
print('Took', clock() - train_start, 'seconds.')
index = np.random.permutation(np.arange(n_train))
X_train = X_train[index]
Y_train = Y_train[index]
if val:
# validation
val_acc = 0
val_loss = 0
num_samples = 0
count = 0
model.train = False
val_start = clock()
print('\n')
print('validating...')
for v in range(0, n_val, batchsize):
minibatch, labels = X_val[v:v+batchsize], Y_val[v:v+batchsize]
if gpu:
minibatch = cuda.to_gpu(minibatch)
labels = cuda.to_gpu(labels)
y = model(minibatch)
loss = F.softmax_cross_entropy(y, labels)
y_pred = F.softmax(y)
preds = cuda.to_cpu(y_pred.data).argmax(axis=1)
val_loss += float(loss.data)*len(minibatch)
labels = cuda.to_cpu(labels)
val_acc += (labels==preds).sum()
count+=1
num_samples += len(minibatch)
val_acc /= num_samples
print('val_acc:', round(val_acc, 4), 'val_loss:', round(val_loss/num_samples, 4))
print('Took', clock()-val_start, 'seconds.')
model.train = True
serializers.save_npz(model_name+'.npz', model)
print('\nTook', clock()-update_start, 'seconds.')
LeNetモデルを学習させてみます. まずは活性化関数はデフォルトで設定しておきます.
lenet = LeNet()
train = train_X, train_Y
train_model(lenet, train, 'lenet')
活性化関数がreluのモデルを学習させてみます.
lenet_relu = LeNet(act_func='relu')
train_model(lenet_relu, train, 'lenet_relu')
評価用画像データに対して予測値を出力する関数を実装します.
def predict(model, X_test, batchsize = 256, gpu=True):
n_test = len(X_test)
predictions = np.array([])
model.train = False
pred_start = clock()
print('\n')
print('predicting...')
for t in range(0, n_test, batchsize):
minibatch = X_test[t:t+batchsize]
if gpu:
minibatch = cuda.to_gpu(minibatch)
y = model(minibatch)
y_pred = F.softmax(y)
preds = cuda.to_cpu(y_pred.data).argmax(axis=1)
predictions = np.concatenate((predictions, preds))
print('Took', clock()-pred_start, 'seconds.')
return predictions
評価用画像データを読み込みます. 学習用画像データと同様に[0, 1]に正規化しておきます.
image_size = (28,28)
test_files = os.listdir(_test_images_path)
num_test = len(test_files)
test_X = np.zeros((num_test,)+(1,)+image_size)
for i, file_name in enumerate(os.listdir(_test_images_path)):
image = Image.open(os.path.join(_test_images_path, file_name))
image_array = np.array(image).reshape((1,)+image_size)
test_X[i,:] = image_array/255.
test_X = test_X.astype(np.float32)
LeNet(tanh)に対する予測値を出力します.
predictions_lenet = predict(lenet, test_X)
LeNet(relu)に対する予測値を出力します.
predictions_lenet_relu = predict(lenet_relu, test_X)
pd.DataFrame({'0':test_files, '1':predictions_lenet.astype(np.int)}).to_csv('lenet.tsv', sep='\t', index=None, header=None)
pd.DataFrame({'0':test_files, '1':predictions_lenet_relu.astype(np.int)}).to_csv('lenet_relu.tsv', sep='\t', index=None, header=None)
評価¶
lenet.tsv(LeNet(tanh))を投稿すると, 0.9896ほどで,
lenet_relu.tsv(LeNet(relu))を投稿すると, 0.9898ほど
となります.
まとめ¶
今回は比較的浅いネットワークモデルであるLeNetを活性化関数を変えつつ適用してみて, ほぼ99%の正解率を出すことがわかりました.
LeNetは初期の深層学習モデルで、その後様々な新しい深層学習モデルが提案されているので, 色々試してみると精度に違いが見えると思います.
その他色々工夫をしてさらなる改善を目指してみてください.
参考文献¶
[1] Y. LeCun, L. Bottou, Y. Bengio and P. Haffner: Gradient-Based Learning Applied to Document Recognition, Proceedings of the IEEE, 86(11):2278-2324, November 1998