mk-toolブログ

エンジニアと家のことをごちゃごちゃと書いてます

【機械学習】手書き文字の学習を行う

機械学習に関して初めての記事です。いろいろ説明することがあると思いますが、いろいろすっ飛ばして手書き文字の学習のサンプルコードを読み解いていきます。

まずはコードの作成に関して必要なライブラリのインポートに関してです。
初記事なのでできる限り掘り下げていきましょう。

%matplotlib inline
import matplotlib.pyplot as plt
import time
import six
import numpy as np
from sklearn.datasets import fetch_mldata
import chainer
from chainer import cuda, Function, gradient_check, Variable, optimizers, serializers, utils
import chainer.functions as F
import chainer.links as L

まず、
「%matplotlib inline」に関して、これは
「import matplotlib.pyplot as pet」はmatplotlibを使用するために導入。matplotlibとはグラフを描画するために使用。
「import time」は学習をさせる際に実行時間等の検索を行うために使用するために導入。
「import six」はpython2系と3系の互換性を保つために導入。
「import numpy as np」はbumpyを使用するための宣言。numpyは行列演算等で重宝するライブラリ。
「from sklearn.datasets import fetch_mldata」は、MNISTのデータを取得してきています。データ自体を自分の手でダウンロードすることなくても、このライブラリを使用してWeb上からデータを取得することができます。
ちなみにMNISTは「Mixed National Institute of Standards and Technology database」の略。
ホームページを見てみたけどデータは、手書き文字しかないのかな。
「import chainer」はchainerを使用しますよという宣言。
「from chainer import cuda, Function, gradient_check, Variable, optimizers, serializers, utils」cuda
「import chainer.functions as F」
「import chainer.links as L」

plt.style.use('ggplot')

この記述に関して,matplotlibを使用してグラフを描画するけど、既に定義されているいい感じのグラフを使います、という宣言。
個人で大きさや、背景なども定義できるらしいけど、それは今回扱うことでなはいのでパス。
気になる方はこの記事をどうぞ。

batchsize = 100
n_epoch = 10
n_units = 1000

mnist = fetch_mldata('MNIST original')
mnist.data = mnist.data.astype(np.float32)
mnist.data /= 255

mnist.target = mnist.target.astype(np.int32)

batchsizeとは、今回画像データは70000枚あるが、それらを特定の枚数に区切って自分の定義する計算機に入れる際に使用する変数である。
今回は値は100なので、100枚区切りで計算機に入れる。
n_epochは学習を何回繰り返すかということである。この回数は、70000枚の画像データを計算機に入れることを特定の回数だけ行う、という値である。なぜこのようなことをするかというと、

{ \displaystyle\textbf{u}} = { \displaystyle\textbf{W}}{ \displaystyle\textbf{x}} + { \displaystyle\textbf{b}}

という計算機に対して画像データを読み込ませていくわけだが、画像1枚目からxxxxx枚めに至るまでの間に、重み{ \displaystyle\textbf{W}}やバイアス{ \displaystyle\textbf{b}}は学習されていくことで更新されていく。仮に現在がn_epochの値が1であるとすると、1枚めの画像が計算機に入れた際は適切な学習を計算機にさせることができたと言えるだろうか。答えはnoである。なぜならば、重みとバイアスの初期値は任意の値が取られており、epochが1の際の1枚目の画像が計算機に与える影響とepochが8の際の1枚目の画像が計算機に与える影響は同じとは言えないからだ。今回は十分な学習をさせることができているかはわからないが、とりあえず、そのような背景があり何度も計算をさせ直すということを覚えておいていただきたい。

「mnist = fetch_mldata('MNIST original')」という箇所に関して、fetch_mldata()関数でWebからデータの取得を行います。

「mnist.data = mnist.data.astype(np.float32)」これは推測でしかないが「mnist.data」で60000枚の画像を784次元のベクトルデータを浮動小数型に型変換を行っているものだと思う。
また、その下では「mnist.data /= 255」という処理で自信を255で除算していることを考えると、各ピクセルには始めは0〜255までの情報が含まれており、それらを0から1までの値を取るように処理がなされたものと考えられる。

「mnist.target = mnist.target.astype(np.int32)」これは教師データを表現しており、0から9までのラベルデータを保管している。

N = 60000
x_train, x_test = np.split(mnist.data, [N])
y_train, y_test = np.split(mnist.target, [N])
N_test = y_test.size

Nという変数は学習データであり、70000枚のうちの60000万枚を学習データとして使用するということになる。
変数x_trainには学習データとして60000枚の画像情報を、変数x_testにはテストデータとして10000枚の画像を格納する。
splitの使用方法は分からなかったのでこの記事からどういったことをしているのかを確認した。
「N_test = y_test.size」という部分では、テストデータのラベルの総数。x_test.sizeでも同じだと思うけどね。

class MLPModel(chainer.Chain):
    def __init__(self, n_in, n_units, n_out):
        super(MLPModel, self).__init__(
            l1 = L.Linear(n_in, n_units),
            l2 = L.Linear(n_units, n_units),
            l3 = L.Linear(n_units, n_out),
        )
    
    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)

いよいよ、ニューラルネットワーク(別名MultiLayer Perceptron)の設計?宣言。
「__init__」関数では各層の定義を行っている。ここではL1〜L3までの3層がで異議されていますね。
「l1 = L.Linear(n_in, n_units)」は「l1 = L.Linear(748, 1000)」
「l2 = L.Linear(n_units, n_units)」は「l2 = L.Linear(1000, 1000)」
「l3 = L.Linear(n_units, n_out)」は「l3 = L.Linear(1000, 10)」になるものと思われますね。

使用する活性化関数は正規化線形関数(Rectified Linear Unit function、Reluと略されている)を使用。
活性化関数は他にはシグモイド関数tanh関数などがあげられる。

net = MLPModel(784, n_units, 10)

ここでニューラルネットワークインスタンス化(っていうのかな)をしていますね。

model = L.Classifier(net)

損失関数を計算したり、予測の精度を評価するために、Classifier Chainを定義する。

#(既存のライブラリ)
class Classifier(Chain):
    def __init__(self, predictor):
        super(Classifier, self).__init__(predictor=predictor)

    def __call__(self, x, t):
        y = self.predictor(x)
        self.loss = F.softmax_cross_entropy(y, t)
    self.accuracy = F.accuracy(y, t)
    return self.loss

このClassifierクラスは精度と損失を計算し、損失値を返す。 softmax_cross_entropy() は与えられた正解ラベルと予測との損失値を計算する。Classifierインスタンスには任意の予測用Linkを設定できる。

同様のクラスがchainer.links.Classifierとして実装されている。 なので、上記のサンプルではなく、すでに定義されているClassifierを使うこととする。

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

オプティマイザとして今回はAdamを採用。他のオプティマイザとしてはGD(確率的勾配降下法)、AdaGradが存在する。
setup()メソッドは与えられたLinkに対する最適化の準備をする。

for epoch in range(1, n_epoch + 1):
    print("epoch", epoch)
    
    perm = np.random.permutation(N)
    sum_accuracy = 0
    sum_loss = 0
    start = time.time()
    for i in range(0, N, batchsize):
        x = Variable(x_train[perm[i : i + batchsize]])
        t = Variable(y_train[perm[i : i + batchsize]])
        
        optimizer.update(model, x, t)
        
        sum_loss += float(model.loss.data) * len(t.data)
        sum_accuracy += float(model.accuracy.data) * len(t.data)
    
    end = time.time()
    elapsed_time = end - start
    throughput = N / elapsed_time
    print('train mean loss={}, accuracy={}, throughput={} images/sec'.format(
        sum_loss / N, sum_accuracy / N, throughput))
    
    # 照合
    sum_accuracy = 0
    sum_loss = 0
    for i in range(0, N_test, batchsize):
        x = Variable(x_test[i : i + batchsize], volatile='on')
        t = Variable(y_test[i : i + batchsize], volatile='on')
        loss = model(x, t)
        sum_loss += float(loss.data) + len(t.data)
        sum_accuracy += float(model.accuracy.data) * len(t.data)
        
    print('test mean loss={}, accuracy={}'.format(
        sum_loss / N_test, sum_accuracy / N_test))
    
print('save the model')
serializers.save_npz('mlp.model', model)
print('save the optimizer')
serializers.save_npz('mlp.state', optimizer)

「perm = np.random.permutation(N)」の部分では、手書き文字の画像データをシャッフルするという操作を行っている。
その理由は、用意する画像データが1枚目〜10000枚目までは「1」の画像で、10001枚目〜20000枚目までは「2」、
と行っていき、60000枚目〜70000万枚目までは「9」といった偏ったデータを計算機に入れてしまうと、偏ったデータを出力してしまうようになる。
例えるしたら、1時間目には数学1を行い、2時間目には古文を行い、、、と行っていき6時間目に物理を行ったのちに
放課後に全教科のテストを行わせると最後に学習した物理のイメージが強いために、数学1のテストでも線形関数は?と聞かれた際に
「y = ax」と答えるのではなく「v = v0 + at」という等速直線運動の式を答えてしまうようなものである。

「sum_accuracy = 0」と「sum_loss = 0」という部分に関しては、英字の通り
学習により正しい答えをすることができた割合と、誤った答えをしてしまった割合の変数の宣言と初期化である。

「start = time.time()」で計算時間の計測を開始している。

「optimizer.update(model, x, t)」ではオプティマイザ
により「重み」と「バイアス」の値を更新している。




参考にしたサイト
chainer_mnist-simple
【機械学習】ディープラーニング フレームワークChainerを試しながら解説してみる。 - Qiita
MNIST For ML Beginners
多層パーセプトロンでMNISTの手書き数字認識 - 人工知能に関する断創録
Chainerチュートリアル の和訳【Chainerの紹介と多層パーセプトロン】 - 俺とプログラミング
Chainerによる多層パーセプトロンの実装 - 人工知能に関する断創録