マイブログ リスト

医療言語処理講座

2019年1月10日木曜日

Pyゼミ3.01 手書き認識のニューラルネットワーク

手書き認識のニューラルネットワークを試してみる


Keyword: MNIST, Neural Network, Chainer


だれもが通る道,MNISTの手書き数字の分類


誰もが簡単に取り組めるので,さまざまなサンプルが教科書やネットワーク上にあるので,既に経験したことがあるかもしれません。
手書き数字の分類のニューラルネットワークのプログラムを試しながら,医用画像の分類問題(Pyゼミ3.02)に進んでいきます。
もうすでに試した人には,復習です。

MNIST(Mixed National Institute of Standards and Technology database)は手書き数字画像を集めたデータベースで,訓練用に6万枚,テスト用に1万枚の手書き数字画像が用意されています。
このデータセットを使って誰もが簡単にニューラルネットワークの実験ができます。


プログラムの概要


ChainerではMNISTのデータを読み込みためのメソッドを用意しています。
    train, test = datasets.get_mnist( ndim = 3 )
訓練用データとテスト用データがtrainとtestにそれぞれリスト形式で代入さています。

読み込まれたデータ数を確認するのにprint文で表示させてみると,
    print( "train:", len( train ), "test:", len( test ) )
60000と10000と表示されます。

結構なデータ数なのでPCの性能によっては学習に時間がかかるかもしれません。
訓練用データを少し減らして実験するとよいです。
    train = train[ : 600] #先頭から600個の画像を訓練に使う
    test  = test[ : 100]  #先頭から100個の画像をテストに使う


ネットワークのモデルを作成する

MyModelクラス内でネットワークの定義を行います。
このネットワークは入力層(ノード数784), 2つの隠れ層(ノード数100)と出力層(ノード数10)の単純なネットワークです。

init関数内(イニシャライザ)で各ネットワークを定義し,重みなどのパラメータの初期化を行います。


  • l1は28×28画素(=784画素)の手書き数字画像を入力ノード数にもち,隠れ層(ノード数100)につなげるネットワークを定義しています。
  • l2はノード数100の隠れ層から次のノード数100の隠れ層につなげるネットワークを定義しています。
  • l3は前の隠れ層からの出力を10個のノード数(数字0から9の正解ラベルを学習する)を持つ出力層に接続するネットワークを定義しています。


call関数は順伝搬計算の手順を定義しています。

  • 入力xをネットワークl1に与え,活性化関数ReLuを介して隠れ層の値h1を得る。
  • 同様にh1をネットワークl2に与えて,h2を得る。
  • 最後にh2をネットワークl3に与えた出力値(ベクトル)を返却する。



モデルの作成

先のネットワークの定義などMyModelクラスを呼び,モデルmodelを作成します。
    model = MyModel( )
作成したモデルにおいて損失や精度を計算するためのClassifier関数を設定します。
このとき,損失関数の計算にSoftmax_Cross_Entropyを設定します。
    model = L.Classifier( model, lossfun = F.softmax_cross_entropy )

次に,学習の最適化アルゴリズムAdamをoptimizerに設定して,作成したmodelにセットアップします。
    optimizer = optimizers.Adam( )
    optimizer.setup( model )
最適化アルゴリズムにはAdamの他にSGDなど様々なアルゴリズムが用意されています。


学習パラメータの設定と学習の実行

訓練データtrainを読み込むSerialIteratorに与えます。
引数にミニバッチサイズを50とデータセットのランダム化(shuffle)を有効にます。
shuffle = Trueにすることにより,毎エポックごとにシャッフルされてバッチデータが作成される。
    iterator = iterators.SerialIterator( train, 50, shuffle = True )
iteratorに対して,optimaizerを設定して更新処理updaterを生成します。
    updater = training.StandardUpdater( iterator, optimizer )
訓練のエポック数を設定してtrainerを生成する。ここでエポック数は10回です。
    trainer = training.Trainer( updater, ( 10, 'epoch' ) )
学習の進捗をモニタするためにextendを使って進捗バーを表示するように設定します。
    trainer.extend( extensions.ProgressBar( ) )
これらの一連の記述はお決まりです。
バッチサイズやエポック数を変えて実験することができます。

最後に生成したtrainerの実行を行います。
    trainer.run()

このあと進捗バーが現れ,学習が行われます。
学習が終了すると,テスト用データを用いて推定を行います。
最終的に正答率を表示します。


プログラムを入力して実行してみよう

説明はこのくらいにしてプログラムを入力して実行してみよしょう。

mnistNN.py

import numpy as np
import chainer
from chainer import Function, report, training, utils, Variable
from chainer import datasets, iterators, optimizers, serializers
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
from chainer.datasets import tuple_dataset
from chainer.training import extensions

#訓練データとテストデータの取得
train, test = datasets.get_mnist(ndim=3) 
train = train[:6000]
test  = test[:1000]
print("train:", len(train), "test:", len(test))

class MyModel(Chain):                   #ネットワークモデルを定義する
    def __init__(self):                 #ネットワークの定義
        super(MyModel, self).__init__(
            l1=L.Linear(784,100),       #全結合ネットワーク
            l2=L.Linear(100,100),            
            l3=L.Linear(100,10),
        )
        
    def __call__(self, x):              #順伝播計算
        h1 = F.relu(self.l1(x))         #活性化関数にReLuを定義
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)
        
model = MyModel()                       #ネットワークモデルを生成する
model = L.Classifier(model, lossfun=F.softmax_cross_entropy)
optimizer = optimizers.Adam()           #最適化アルゴリズムを設定する
optimizer.setup(model)                  #モデルを最適化アルゴリズムにセットアップ

iterator = iterators.SerialIterator(train, 50, shuffle = True) #ミニバッチの設定
updater = training.StandardUpdater(iterator, optimizer) #更新処理の設定
trainer = training.Trainer(updater, (10, 'epoch'))      #訓練(エポック数)の設定
trainer.extend(extensions.ProgressBar())                #進捗表示の設定

trainer.run()                           #訓練実行

#訓練結果からtestデータを用いて推定を行う
ok = 0
n  = 0
for i in range(len(test)):              #テストデータ分繰り返す
    x = Variable(np.array([ test[i][0] ], dtype=np.float32)) #入力データ
    t = test[i][1]                      #正解データ
    y = model.predictor(x)              #テストデータから推定結果を計算
    out = F.softmax(y)
    ans = np.argmax(out.data)           #推定結果からラベルを得る
    n += 1
    
    np.set_printoptions(formatter={'float': '{: 0.2f}'.format})
    print('\n#',i)
    print('Infer :',out.data)           #推定結果を表示
    print('Sprvs :','  ','----->'*(t),t)#教師データの表示
    if (ans == t):
        ok += 1    
        print('\tOK',ok/n)

        
print('平均正答率:', ok/len(test))


実行すると進捗バーが現れる。
エポック数,1秒間の繰り返し数や残り時間などの進捗状態が表示される。

$ python␣mnistNN.py⏎
train: 60000 test: 10000
     total [#################...............................] 37.50%
this epoch [###################################.............] 75.00%
      4500 iter, 3 epoch / 10 epoch
    406.63 iters/sec. Estimated time to finish: 0:00:18.444094



学習が終了すると,テスト用データで評価が表示されます。
各テスト用データ(#番号)について推定結果Inferのベクトルが表示されます。
この値が最も大きな値の要素がラベルの推定結果です。
その下の行に正解ラベルSprvsが----->のあとに表示されます。
一致していればOKの正答率は上がります。


    ・・・・・・・・・・・・

# 9995
Infer : [[ 0.00  0.00  1.00  0.00  0.00  0.00  0.00  0.00  0.00  0.00]]
Sprvs :    ----->-----> 2
 OK 0.9770908363345339

# 9996
Infer : [[ 0.00  0.00  0.00  1.00  0.00  0.00  0.00  0.00  0.00  0.00]]
Sprvs :    ----->----->-----> 3
 OK 0.9770931279383815

# 9997
Infer : [[ 0.00  0.00  0.00  0.00  1.00  0.00  0.00  0.00  0.00  0.00]]
Sprvs :    ----->----->----->-----> 4
 OK 0.9770954190838168

# 9998
Infer : [[ 0.00  0.00  0.00  0.00  0.00  1.00  0.00  0.00  0.00  0.00]]
Sprvs :    ----->----->----->----->-----> 5
 OK 0.9770977097709771

# 9999
Infer : [[ 0.00  0.00  0.00  0.00  0.00  0.00  1.00  0.00  0.00  0.00]]
Sprvs :    ----->----->----->----->----->-----> 6
 OK 0.9771
正答率: 0.9771


最終的な正答率は0.9771でした。
非常に単純なネットワークであるが,良い結果が得られています。


プログラムの解説

インポート

Chainerを利用するのに必要なライブラリとnumpyをインポートします。

手書き数字データの準備

ChanerではMNISTのデータを読み込むメソッドが用意されています。
訓練データ6万件,テストデータ1万件がtrain,testそれぞれに格納されます。
引数のndim=3は数値画像の読み込み形式(1,28,28)の3次元を指定しています。
    train, test = datasets.get_mnist(ndim=3) 

ネットワークモデルを定義するクラス

先に説明したように,init関数でネットワークを定義します。
call関数は入力xから順伝播計算を定義します。

モデルの生成と最適化アルゴリズムの設定

以下の4行はお決まりの文です。
   model = MyModel()                       #ネットワークモデルを生成する
   model = L.Classifier(model, lossfun = F.softmax_cross_entropy)
   optimizer = optimizers.Adam()     #最適化アルゴリズムを設定する
   optimizer.setup(model)         #モデルを最適化アルゴリズムにセットアップ

学習パラメータの設定と実行

訓練データtrainを与え,バッチサイズ50で,シャッフルでバッチデータのランダム化を設定しています。
学習の最適化にoptimizerにAdamを設定しています。
実際の学習時にはこのStandardUpdaterにより重みの更新が行われます。
訓練回数を10エポックに設定しています。
学習の進捗をバー表示する設定を行い,最後にtrainerを実行します。


以上が学習のためのプログラムです。
学習が終了時には初期化されたl1,l2とl3の重みが更新され最適な重みになっています。
訓練結果の重みを利用して以下にtestのテストデータで正答率を求めてみましょう。

テストデータを用いいて手書き数字の推定を行う

推定に用いたテスト用データの数と推定結果が正解であった場合の数をそれぞれ変数nとokに代入するため初期化します。
次にfor文にてtestのデータ数分繰り返します。

入力xと正解tを準備し,推定結果を得る

まず,入力xと教師tを準備すします。
i番目の入力の手書き数字画像はtest[i][0]に格納されています。
これを入力xとしてmodelに与えるためにChainerのVariable関数を使って,32ビットの浮動小数点型のVariable変数に変換します。
    x = Variable( np.array( [ test[ i ][ 0 ] ], dtype = np.float32 ) )
次に,i番目の正解データはtest[i][1]に格納されています。
これが正解tとなるため,そのまま代入します。
    t = test[ i ][ 1 ] 
データの準備ができたので,modelのfwdInfer関数を使って入力データxの推定を行います。
    out = model.fwdInfer( x )
推定結果outはラベル0〜9に対する確率で,10次元のベクトルです。
このベクトルから最大値のラベルを求めて,推定結果ansを得ます。
    ans = np.argmax( out.data )
変数ansには0~9までのいずれかの値が推定結果として格納されています。

推定結果と正答率の表示

浮動小数点表示を小数点以下2桁で表示するようにフォーマットの定義を行います。
    np.set_printoptions(formatter={'float': '{: 0.2f}'.format})
データ番号を表示します。'\n'は改行を表す拡張表記です。
    print( '\n#', i )
次に,推定結果のベクトルを表示します。
    print( 'Infer :', out.data )          
その下に,正解ラベルを表示します。
正解ラベルtの数分,矢印を表示し,最後に正解ラベルtの値を表示します。
    print( 'Sprvs :', '  ', '----->'*( t ), t )
もし,推定結果と正解ラベルが一致していれば,ベクトルの最大要素の下に正解ラベルが表示されるはずです。
最後に,if文で推定ラベルansと正解ラベルtを比較して一致したら変数okをインクリメントして,正答率を表示します。



課 題


今回,手書き数字画像のデータベースMNISTを使ってニューラルネットワークの学習と推定の実験を行った。
DICOM画像を読み込んでどのようにネットワークに与えて,学習と推定を行うか考えてみよう。


おわりに

MNISTはみな経験済みかもしれません。
これ以降,このプログラムをDICOM画像に発展させ,また畳み込みニューラルネットワークCNNに対応していきます。

0 件のコメント:

コメントを投稿