Alexnetに対応する
Keyword:DICOM,Chainer,Alexnet
前章Pyゼミ3.04ではMNISTのネットワークをDICOM画像に適用できるようにしました。
この章ではだ表的なDeep LearningのネットワークのAlexnetに対応したプログラムを作成します。
Alexnetとは
画像認識のコンペティションであるILSVRC(ImageNet Large Scale Visual Recognition Challenge)の2012年の大会で他者を圧倒する認識率をこのAlexnetが出しました。
Alexnetの構造は5層の畳み込み層(CNN, Constitutional Neural Network)と3層の全結合層(FC,Full Connection)からなっています。
特徴としては以下の点が挙げらます。
- ReLU活性化関数
- マルチGPU(Graphics Processing Unit)での学習
- Data augmentation(データ拡張)
- Dropout
論文[1]より。Alexnetの構成を示す。
一般的に活性関数はシグモイド関数を用いていました。
この関数は大きな入力値に対して出力が1に飽和してしまいますが,ReLU関数は線形変換により大きな入力に対しては大きな値を返ます。
この特徴によりAlexnetは学習が早く進むといわれています。
Alexnetを紹介する論文[1]では2つのGPUを使い,畳み込み演算を同時並行で行い,全結合層で出力を統合して結果を出力しています。
データ拡張はデータにランダムな移動や回転,あるいは反転やノイズなどを加えることでデータの多様性を増します。
これにより未知のデータに対する良い結果を出す汎化性能の向上が期待されるといわれています。
Dropoutは訓練時にランダムに半分のノードしか使わない手法です。
Dropoutは訓練時に過学習を防ぐために用いられています。
訓練プログラム(dicomAlexnetTrain.py)の概要
前章Pyゼミ3.04で作成したdicomNNtrain.pyのネットワークモデルを定義したMyModelクラスをこの章ではAlenxnetクラスに置き換えただけです。
したがって,ネットワークモデルを定義したクラスと入力チャンネル数(n_channel = 1)とモデルの設定(model = Alexnet(nlbl))以外は前回と変わっていません。
前章のプログラムの作成は苦労したかもしれませんが,今後新しいネットワークの設計と定義は,ネットワーク定義のクラスを新しく作るだけです。
インターネット上の様々なネットワークを試してみることができます。
Alexnetクラス
ini関数
ini関数にはネットワークの定義が記述されています。この関数の引数は2つです。
- n_label:出力するラベル数です。
- n_channel:入力データ(画像)のチャンネル数です。CT画像はモノクログレイなので1をディフォルトで設定しています。
実際のネットワークの定義ではChainerのLinkの畳み込みニューラルネットワークを構成するConvolution2Dを用いています。
この関数の引数は次のようになります。
Convolution2D( 入力チャンネル数,
出力チャンネル数,
フィルタサイズ,
ストライドサイズ,
パッドサイズ )
- 入力チャンネル数:前ネットワークからの入力のチャンネル数
- 出力チャンネル数:現ネットワークから出力されるチャンネル数
- フィルタサイズ:畳み込みフィルタのサイズ
- ストライドサイズ:フィルタ処理から次のフィルタ処理までの移動量
- パッドサイズ:入力の辺縁のパディング処理のサイズ
はじめの畳み込み関数を見てみます。
入力チャンネル数はn_channelでここでは最初に初期化した1になります。
出力チャンネル数は96チャンネルです。
チャンネル数とは,フィルタの数と考えます。
このフィルタの重みを最適化するるように訓練が行われます。
論文の図では,2つのGPUに分散して処理しているため,48チャンネルが2つのGPUで処理されています。
畳み込みフィルタのサイズは11で,11×11のサイズのフィルタを意味しています。
ストライドは4で4画素おきに処理が行われます。
conv1 = L.Convolution2D(n_channel, 96, 11, stride=4),
2つめの畳み込み層は前層conv1の値を受け取るため,
入力チャンネル数は96になります。
出力チャンネル数は256です。
フィルタのサイズは5(5x5)です。
パッドサイズは2で,入力データの周囲に2画素パディング(埋め込み)して5×5のフィルタ処理が無駄なくできるようにしています。
conv2 = L.Convolution2D(96, 256, 5, pad=2),
以下,同様に入力出力チャンネル数,フィルタサイズとパッドサイズが設定されています。
conv3 = L.Convolution2D(256, 384, 3, pad=1),
conv4 = L.Convolution2D(384, 384, 3, pad=1),
conv5 = L.Convolution2D(384, 256, 3, pad=1),
この畳み込み層の最終出力は256チャンネルになります。
次に全結合層です。
同じくChainerのLinkの全結合の処理を行うLinearについて説明します。
Linear(入力サイズ,出力サイズ)
- 入力サイズ:入力ベクトルの次元数
- 出力サイズ:出力ベクトルの次元数
それでは全結合のネットワークfc6を見てみます。
入力ベクトルの次元はNoneになっています。ネットワーク定義のこの段階で次元数は未定です。
しかし,最初のデータが順伝播する際に次元数は初期化されます。
そして,出力ベクトルの次元数は4096になります。
fc6 = L.Linear(None, 4096),
以下同様にしてfc7,fc8が定義され,最終的にn_labelの次元数のベクトルが出力されます。
fc7 = L.Linear(4096, 4096),
fc8 = L.Linear(4096, n_label),
call関数
次にcall関数です。
先にini関数で定義したネットワークを順伝播する形態に接続を記述します。
はじめにChainerのFunctionのmax_pooling_2d関数が使われています。
max_pooling_2d( 入力変数,
プーリングウィンドウサイズ,
ストライドサイズ )
- 入力変数:ChanerのVariable型の変数,実際はベクトルになります。
- プーリングウィンドウサイズ:k×kの画素中の最大値をとるのがmax_poolingです。このkの値を設定します。
- ストライドサイズ:プーリング処理を何画素おきに行うのか設定します。
次にlocal_response_normalizationです。
これは,局所近傍のチャンネルの正規化を行います。チャンネルによっては大きな値ばかりになったり,その逆の場合などを正規化して補正します。
local_response_normalization( 入力変数 )
- 入力変数:ChanerのVariable型の変数,実際はベクトルになります。
最後に,reluです。reluは活性化関数でRectified Linear Unit functionの略で,次式で表されます。
f(x)=max(0,x)
入力x(Variable型)と0と比較して大きな値を返します。つまり,xが0未満では0を,0以上ではxの値を返す関数です。
活性化関数によく利用されるSigmoid関数は大きな入力に対して最大1を返しますが,relu関数は入力xに比例した値が返されます。
それでは3つの関数が理解できたところで,入力xについての出力hを見てみましょう。
h = F.max_pooling_2d(F.local_response_normalization(
F.relu(self.conv1(x))), 3, stride=2)
最初に入力xは畳み込みニューラルネットワークのself.conv1に与えらえます。
self.conv1の出力はF.reluの活性化関数に与えられます。
活性化関数F.reluの出力はF.local_response_normalizationに与えられ,局所正規化されます。
正規化されたチャンネルは F.max_pooling_2dに与えられ,この時のプーリングサイズは3ⅹ3で,ストライドが2で処理が行われます。
各注目する3x3の画素中の最大値がプーリング値となり,出力hを得ます。
次は前ネットワークの出力hをself.conv2の入力にして,同じように活性化関数relu, 局所正規化とマックスプーリング処理を行って出力hを得ます。
h = F.max_pooling_2d(F.local_response_normalization(
F.relu(self.conv2(h))), 3, stride=2)
前ネットワークの出力hをself.conv3の入力にして,活性化関数を介して出力hを得たものを次のネットワークself.conv4の入力になり,同様に出力hを得ています。
h = F.relu(self.conv3(h))
h = F.relu(self.conv4(h))
次に最後の畳み込みニューラルネットself.conv5に前ネットワークの出力hを与えて,新たな出力hを得ています。
h = F.max_pooling_2d(F.relu(self.conv5(h)), 2, stride=2)
次のネットワークからは全結合のself.conv6~8の処理を行います。
ここではChainerのFunctionの中のdropoutが使われています。
Dropoutは学習時に重みの半分をないものとして学習を行います。
これにより過学習を予防する効果があるといわれています。
h = F.dropout(F.relu(self.fc6(h)))
h = F.dropout(F.relu(self.fc7(h)))
前ネットワークの出力hをself.conv6に与え,活性化関数を介した後,dropoutで学習して出力を得ています。
同様にself.conv7にその出力を与えて新たな出力hを得ています。
最後に全結合のself.conv8に前出力hを与えて得た新たな出力をcall関数は返しています。
return self.fc8(h)
このようにcall関数はネットワークの定義(conv1~conv8)を使って順伝播を記述しています。
プログラムを入力して実行してみよう
プログラム全体の構成はPyゼミ3.04と同じです。
Pyゼミ3.04の「プログラムのポイント」に記述されているように,訓練データファイル(訓練画像ファイル名とラベル番号のリスト)が存在することが前提です。
参考までにPyゼミ3.04のMyModelクラスをソース中に残してあります。
Pyゼミ3.04から変更されている点は以下の通りです。
- MyModelクラスの代わりにAlenxnetクラスに置き換わっています。
- 入力データのチャンネル数を1に設定しています(n_channel = 1)。
- ClassifyImageクラス内でmodelをAlexnetに設定しています(model = Alexnet(nlbl))。
dicomAlexnetTrain.py
import os, sys import numpy as np import cv2 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 import pydicom def constractTrainData(fname , resz): image,label = [], [] for i, line in enumerate(open(fname, 'r')): data = line.split(",") # 画像ファイル名と正解データに分割 print("Train",i, data[0], "Label#",data[1]) img = readDicom2png(data[0], "tmp.png") if resz > 0: # 画像のリサイズ img = cv2.resize(img,(resz, resz)) img = img.astype(np.float32) img = (img - 128)/128 # 画素値0-255を-1.0から+1.0に変換 image.append([img]) t = np.array(int(data[1]), dtype=np.int32) label.append(t) train = tuple_dataset.TupleDataset(image, label) return train def readDicom2png(dcmfnm, tmpfnm): ds = pydicom.read_file(dcmfnm) wc = ds.WindowCenter ww = ds.WindowWidth img = ds.pixel_array max = wc + ww / 2 min = wc - ww / 2 img = 255 * (img - min)/(max - min) img[img > 255] = 255 img[img < 0 ] = 0 img = img.astype(np.uint8) cv2.imwrite(tmpfnm, img) return img def getLabelName(fname): lblnm = [] for line in open(fname, 'r'): data = line.split(",") lblnm.append(data[1].strip()) #lblnm[int(data[0])]= data[1] print("LabelName:",lblnm) return len(lblnm), lblnm """ class MyModel(Chain): # MNISTで使ったネットワーク(参考) def __init__(self, nlbl): super(MyModel, self).__init__( l1 = L.Linear(784,100), l2 = L.Linear(100,100), l3 = L.Linear(100,nlbl), ) def __call__(self, x): # 順伝播計算 h1 = F.relu(self.l1(x)) # 活性化関数にReLuを定義 h2 = F.relu(self.l2(h1)) return self.l3(h2) """ class Alexnet(Chain): ### AlexNet ### def __init__(self, n_label, n_channel=1): super(Alexnet, self).__init__( conv1 = L.Convolution2D(n_channel, 96, 11, stride=4), conv2 = L.Convolution2D(96, 256, 5, pad=2), conv3 = L.Convolution2D(256, 384, 3, pad=1), conv4 = L.Convolution2D(384, 384, 3, pad=1), conv5 = L.Convolution2D(384, 256, 3, pad=1), fc6 = L.Linear(None, 4096), fc7 = L.Linear(4096, 4096), fc8 = L.Linear(4096, n_label), ) def __call__(self, x): h = F.max_pooling_2d(F.local_response_normalization( F.relu(self.conv1(x))), 3, stride=2) h = F.max_pooling_2d(F.local_response_normalization( F.relu(self.conv2(h))), 3, stride=2) h = F.relu(self.conv3(h)) h = F.relu(self.conv4(h)) h = F.max_pooling_2d(F.relu(self.conv5(h)), 2, stride=2) h = F.dropout(F.relu(self.fc6(h))) h = F.dropout(F.relu(self.fc7(h))) return self.fc8(h) class ClassifyImage(): def __init__(self, mdlnm, nlbl): model = Alexnet(nlbl) self.model = L.Classifier(model,lossfun=F.softmax_cross_entropy) self.opt = optimizers.Adam() self.opt.setup(self.model) self.modelName = mdlnm def doTrain(self, nLbl, train, batch_size, n_epoch, modelName): iterator = iterators.SerialIterator(train, batch_size, shuffle=True) updater = training.StandardUpdater(iterator, self.opt) trainer = training.Trainer(updater, (n_epoch, 'epoch'), out='result') trainer.extend(extensions.LogReport()) trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy'])) trainer.extend(extensions.ProgressBar()) trainer.run() print("学習モデル",self.modelName,"を保存します... . .") chainer.serializers.save_hdf5( self.modelName, self.model ) print("\t... . .学習モデルを保存しました") if __name__=='__main__': trainfnm = "./TrainTestList/TrainData.0.3.csv" labelfnm = "./TrainTestList/labelName.csv" resize = 64 # 画像リサイズ modelnm = "Alexnet.0.3.hdf5" # モデル保存名 batch_size = 10 # バッチサイズ n_epoch = 10 # エポック数 nLbl, lblName = getLabelName(labelfnm) train = constractTrainData(trainfnm, resize) print("\nTrain data information\n") print("画像データ数:",len(train)) print("画像/ラベル:",len(train[0]),"\n画像Chanel数:",len(train[0][0])) print("高さ:",len(train[0][0][0]),"\n幅 :",len(train[0][0][0][0])) ci = ClassifyImage(modelnm, nLbl) ci.doTrain(nLbl, train, batch_size, n_epoch, modelnm)
実行結果
プログラムを実行すると,ラベル名のリスト(配列)が表示されます。
インデックス番号がラベル番号になります。
ラベル番号0は’Lung’,ラベル番号3は’Head’になります。
次に142枚の訓練画像がラベル番号と伴に読み込まれます。
画像サイズを64x64にリサイズしています。
学習が始まると,損失(main/loss)は減少し,精度(main/accuracy)は徐々に高くなり,10回目には0.985714になっています。
最終的に学習したパラメータはファイルAlexnet.0.3.hdf5に保存されます。
$ python␣dicomAlexnetTrain.py⏎
LabelName: ['Lung', 'Abdomen', 'Chest', 'Head']
Train 0 ../dcmdir2/Lung/W5305890 Label# 0
Train 1 ../dcmdir2/Lung/K5303593 Label# 0
Train 2 ../dcmdir2/Lung/R5305640 Label# 0
Train 3 ../dcmdir2/Lung/T5304000 Label# 0
Train 4 ../dcmdir2/Lung/A5304343 Label# 0
Train 5 ../dcmdir2/Lung/D5304468 Label# 0
・・・・・
Train 136 ../dcmdir2/Head/K5221390 Label# 3
Train 137 ../dcmdir2/Head/D5221046 Label# 3
Train 138 ../dcmdir2/Head/F5221140 Label# 3
Train 139 ../dcmdir2/Head/G5221187 Label# 3
Train 140 ../dcmdir2/Head/Q5221671 Label# 3
Train 141 ../dcmdir2/Head/E5221093 Label# 3
Train data information
画像データ数: 142
画像/ラベル: 2
画像Chanel数: 1
高さ: 64
幅 : 64
epoch main/loss main/accuracy
1 1.75138 0.42
2 0.786748 0.657143
3 0.685697 0.685714
4 0.589711 0.778571
5 0.306332 0.907143
6 0.329208 0.94
7 0.0920463 0.992857
8 0.0648474 0.978571
9 0.251898 0.935714
10 0.046363 0.985714
学習モデル Alexnet.0.3.hdf5 を保存します... . .
... . .学習モデルを保存しました
テストプログラム(dicomAlexnetTest.py)の概要
このテストプログラムも基本的にPyゼミ3.04のdicomNNtest.pyと同じですが,
Alexnetクラスに置き換わっていること,
入力データ(画像)のチャンネル数が1であること,
ClassfyImageクラス内でmodelにAlexnetを設定していることが異なっています。
プログラムを入力して実行してみよう
Pyゼミ3.04のdicomNNtest.pyと異なる点を修正するだけで実行できます。
- Alexnetクラスの追加
- Alexnetクラスのini関数の引数にn_channel = 1の追加
- model = Alexnet(nlbl)へ変更
dicomAlexnetTest.py
import os, sys import numpy as np import cv2 import chainer import chainer.links as L import chainer.functions as F from chainer import optimizers, Chain, Variable, initializers from chainer.training import extensions import pydicom def constructTestData(fname , resz): test = [] for i, line in enumerate(open(fname, 'r')): data = line.split(",") print("Test",i, data[0], "Label#",data[1]) img = readDicom2png(data[0], "tmp.png") if resz > 0: img = cv2.resize(img,(resz, resz)) img = img.astype(np.float32) img = (img - 128)/128 test.append([[img], int(data[1]), data[0]]) return test def readDicom2png(dcmfnm, tmpfnm): ds = pydicom.read_file(dcmfnm) wc = ds.WindowCenter ww = ds.WindowWidth img = ds.pixel_array max = wc + ww / 2 min = wc - ww / 2 img = 255 * (img - min)/(max - min) img[img > 255] = 255 img[img < 0 ] = 0 img = img.astype(np.uint8) cv2.imwrite(tmpfnm, img) return img def getLabelName(fname): lblnm = [] for line in open(fname, 'r'): data = line.split(",") lblnm.append(data[1].strip()) print("LabelName:",lblnm) return len(lblnm), lblnm class Alexnet(Chain): ### AlexNet ### def __init__(self, n_label, n_channel=1): super(Alexnet, self).__init__( conv1 = L.Convolution2D(n_channel, 96, 11, stride=4), conv2 = L.Convolution2D(96, 256, 5, pad=2), conv3 = L.Convolution2D(256, 384, 3, pad=1), conv4 = L.Convolution2D(384, 384, 3, pad=1), conv5 = L.Convolution2D(384, 256, 3, pad=1), fc6 = L.Linear(None, 4096), fc7 = L.Linear(4096, 4096), fc8 = L.Linear(4096, n_label), ) def __call__(self, x): h = F.max_pooling_2d(F.local_response_normalization( F.relu(self.conv1(x))), 3, stride=2) h = F.max_pooling_2d(F.local_response_normalization( F.relu(self.conv2(h))), 3, stride=2) h = F.relu(self.conv3(h)) h = F.relu(self.conv4(h)) h = F.max_pooling_2d(F.relu(self.conv5(h)), 2, stride=2) h = F.dropout(F.relu(self.fc6(h))) h = F.dropout(F.relu(self.fc7(h))) return self.fc8(h) class ClassifyImage: def __init__(self, modelname, nlbl): model = Alexnet(nlbl) self.model = L.Classifier(model) print("modelファイル ",modelname," を読み込みます...") chainer.serializers.load_hdf5( modelname, self.model ) print("...ファイルを読み込みました") def predict(self, testimg): img = np.array([testimg]) #print(">img>>",type(img),len(img),len(img[0]),len(img[0][0]),len(img[0][0][0])) x = Variable(img) y = self.model.predictor(x) y = F.softmax(y) answer = y.data answer = np.argmax(answer, axis=1) return answer[0], y[0].data if __name__=='__main__': testfnm = "./TrainTestList/TestData.0.3.csv" labelfnm = "./TrainTestList/labelName.csv" resize = 64 # 画像リサイズ modelnm = "Alexnet.0.3.hdf5" # モデル保存名 nLbl, lblName = getLabelName(labelfnm) lblCnt = np.zeros(nLbl) # 各ラベルの正答数を計数 lblAll = np.zeros(nLbl) # 各ラベルの総数を計数 data = constructTestData(testfnm, resize) print("data:",len(data),len(data[0])) print("img :",len(data[0][0]), len(data[0][0][0]),len(data[0][0][0][0])) ci = ClassifyImage(modelnm, nLbl) # モデルの読み込み ok = 0 np.set_printoptions(formatter={'float': '{: 0.5f}'.format}) for i, line in enumerate(data): testimg, ans, fnm = line prediction, softmax = ci.predict(testimg) # 推論 print("\n#",i,"Softmax:",softmax) result = "File:"+fnm+"\tAns:"+lblName[ans]+"→ Predict:"+lblName[prediction] lblAll[ans] += 1 # ラベルがansの数の計数 if prediction == ans: # 推論結果と答えが一致したら print(result + "\tOK") ok += 1 lblCnt[prediction] += 1 else: print(result + "\tNG") print("\n平均正答率:",ok/len(data)) for i in range(nLbl): print(str(i), lblName[i],"\t",lblCnt[i]/lblAll[i])
実行結果
実行するとはじめにラベル名が表示されます。
その後,テスト画像とラベル番号が読み込まれます。
73枚のテスト画像が読み込まれた後,dicomAlexnetTrain.pyで保存した学習モデルのファイルAlexnet.0.3.hdf5を読み込みます。
各画像についてテストを行います。
Softmax:の後には各ラベルに対する推測結果が4次元ベクトルで表されます。
ベクトルの最も大きな値のインデックス番号が推測したラベル番号になります。
また,教師データのAnsと推測結果のPredictが一致しているとOK表示され,異なっている場合はNGが表示されます。
最後に平均正答率と,各ラベルの正答率が表示されます。
$ python␣dicomAlexnetTest.py⏎ LabelName: ['Lung', 'Abdomen', 'Chest', 'Head'] Test 0 ../dcmdir2/Lung/E5306265 Label# 0 Test 1 ../dcmdir2/Lung/M5305390 Label# 0 Test 2 ../dcmdir2/Lung/C5304421 Label# 0 Test 3 ../dcmdir2/Lung/O5303765 Label# 0 Test 4 ../dcmdir2/Lung/P5305546 Label# 0 Test 5 ../dcmdir2/Lung/Q5305593 Label# 0 ・・・・・ Test 68 ../dcmdir2/Head/B5220953 Label# 3 Test 69 ../dcmdir2/Head/P5221625 Label# 3 Test 70 ../dcmdir2/Head/L5221421 Label# 3 Test 71 ../dcmdir2/Head/W5222046 Label# 3 Test 72 ../dcmdir2/Head/X5222093 Label# 3 data: 73 3 img : 1 64 64 modelファイル Alexnet.0.3.hdf5 を読み込みます... ...ファイルを読み込みました # 0 Softmax: [ 1.00000 0.00000 0.00000 0.00000] File:../dcmdir2/Lung/E5306265 Ans:Lung→ Predict:Lung OK # 1 Softmax: [ 1.00000 0.00000 0.00000 0.00000] File:../dcmdir2/Lung/M5305390 Ans:Lung→ Predict:Lung OK # 2 Softmax: [ 1.00000 0.00000 0.00000 0.00000] File:../dcmdir2/Lung/C5304421 Ans:Lung→ Predict:Lung OK # 3 Softmax: [ 1.00000 0.00000 0.00000 0.00000] File:../dcmdir2/Lung/O5303765 Ans:Lung→ Predict:Lung OK・・・・・# 70 Softmax: [ 0.00000 0.00000 0.00000 1.00000] File:../dcmdir2/Head/L5221421 Ans:Head→ Predict:Head OK # 71 Softmax: [ 0.00002 0.00000 0.00000 0.99998] File:../dcmdir2/Head/W5222046 Ans:Head→ Predict:Head OK # 72 Softmax: [ 0.00000 0.00000 0.00000 1.00000] File:../dcmdir2/Head/X5222093 Ans:Head→ Predict:Head OK 平均正答率: 1.0 0 Lung 1.0 1 Abdomen 1.0 2 Chest 1.0 3 Head 1.0
10回の学習で得たモデルを使って,テストでは良い正答率が出ています。
これは同一人物のデータなので当然の結果です。
いろいろなDICOM画像データで試してみるとよいでしょう。
まとめ
Pyゼミ3.05ではニューラルネットワークを記述したクラスを交換することで簡単にAlexnetに対応することができました。
このようにニューラルネットワークのみを変更することで様々なDeep Neural Networkに発展させることができます。
また,ネットワーク定義においてチャンネル数を変えたり,畳み込み層を増やしたりいろいろと自分オリジナルのネットワークを作ることができます。
0 件のコメント:
コメントを投稿