手書き数字画像MNISTから医療画像DICOMへ発展させよう
Keyword:MNIST,DICOM,Neural Network,Chainer
前回のPyゼミ3.01にて,手書き数字画像を用いて,ニューラルネットワークの学習を行ないました。
前回のニューラルネットワークをDICOM画像に発展させることを考えてみます。
MNISTがどのようなデータ構造をもった訓練画像なのか調べてみる
対話モードで,Chainerのdatasetsをimportして,訓練データ,テストデータをそれぞれtrainとtestに読み込んでみます。
>>> from chainer import datasets >>> from chainer.datasets import tuple_dataset >>> >>> train, test = datasets.get_mnist(ndim = 3)次に,訓練データの数をlenメソッドを使って調べてみます。
>>> len(train) 60000
確かに6万件のデータ(手書き数字画像)が読み込まれていることがわかります。
それでは,最初の0番めのデータについて,同じくいくつ格納されているのかlenメソッドで調べてみます。
>>> len(train[0]) 2
2つです。
つまり,train[0][0]とtrain[0][1]の2つです。
さらにtrain[0][0]について,調べてみます。
>>> len(train[0][0]) 1
1つです。
train[0][0][0]は1つということです。
さらに調べてみると,
>>> len(train[0][0][0]) 28
28と表示されました。
これはMNISTの手書き数字の画素サイズです。
さらに調べると。
>>> len(train[0][0][0][0]) 28
また,28と表示されました。
少しまとめてみましょう。
最初の60000と表示された。これはデータ数(画像と教師ラベル)を表しています。
つまり,train[0]からtrain[59999]のデータがあることを示しています。
先頭の0番目のデータについて注目してみると
この中には2つの情報があるようです。
ひとつめの情報(tarin[0][0])を表示してみましょう。
>>> train[0][0] array([[[ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , ・・・略・・・ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ]]], dtype=float32)
かなり大きなリストのデータです。多分画像じゃないかって想像がつきます。
ふたつめの情報(train[0][1])を表示してみましょう。
>>> train[0][1]5
次に,画像の方をさらに見てみると,len(train[0][0])は1だったので,train[0][0][0]しかない。
これを表示してみると,
>>> train[0][0][0] array([[ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , ・・・略・・・ 0. , 0. , 0. ], [ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ]], dtype=float32)
画像かどうか表示してみましょう。OpenCVで簡単に見ることができます。
(OpneCVのインストールはPyゼミ1.02参照)
(OpneCVのインストールはPyゼミ1.02参照)
>>> import cv2 >>> >>> cv2.imshow('image', (train[0][0][0])) >>> cv2.waitKey(0)
教師ラベルが5であったように,確かに5の手書き数字です。
少し補足するとtrain[0][0][0]は28×28画素のモノクロの画像1枚(1プレーン,1チャンネル)なので,len( train[0][0] )は1を出力しました。
もし,カラー画像の場合,len( train[0][0] )は3を出力します。
つまり,RGBそれぞれがtrain[0][0][0],train[0][0][1],train[0][0][2]に格納されています。
まとめると
訓練画像データは5次元のデータで,ラベルデータは2次元である。1次元:データの順序番号を表す。(0~データ数-1)
2次元:画像またはラベルを表す。0のとき画像,1のときラベル
3次元:画像プレーン番号(モノクロは0の1つのみ,カラーは0,1または2の3つ)
4次元:画像の行数(縦の画素数,画像の高さ)
5次元:画像の列数(横の画素数,画像の幅)
画像の画素x,yにアクセスするには次のようになります。
train[データ番号][0][プレーン番号][画像の行(y)][画像の列(x)]
たとえば,10番めの画像のx=11,y=12の画素を得るには次のように書きます。
pix = train[9][0][0][12][11]
ラベルについては下記のようにアクセスできます。
tarin[データ番号][1]
したがって,画像とそのラベルが与えられたら5次元の訓練データを作成するプログラムを作成すれば良いことになります。
DICOM画像から訓練データを作成する
DICOM画像(このサンプルは個人の画像なのでこの実験以外では使用しないでください)から訓練データを作成する仕様としては以下のように考えます。- DICOM画像の入ったフォルダを指定して,その中の画像を訓練データとする。
- ラベル名はファイル名(Brain01, Chest02など)から抽出し,それにラベル番号を自動で(0から)付与する(サンプル画像のファイル名の命名がラベル名となっている)。
- DICOMファイル名,ラベル番号とラベル名のCSVファイルを作成する。
- 上記,CSVファイルから訓練データを作成する関数を作る
プログラムの概要
constractTrainData関数
次の2つの引数を受け取とります。- CSVファイル名(fname) :DICOMファイル名,ラベル番号とラベル名のCSVファイル名を指定する。
- リサイズ値(resz):画像を任意のサイズに拡大または縮小する画素数を指定する。
プログラムははじめに画像とラベルを格納するリストを初期化します。
image = [ ]
label = [ ]
次にfor文にて,CSVファイルから1行づつ読み込みlineに代入し,読み込んだ回数を0から変数n代入します。
変数lineの値はカンマ区切りのデータで,data(リスト)に分割して格納します。
data[0]にはDICOMファイル名が
data[1]にはラベル番号が
data[2]にはラベル名が格納されます。
readDicom2png関数(2.03参照)を用いてウィンドウニングした画像をimgに代入します。
このとき,imgの画素値は0から255の8ビットの値をとります。
もし,リサイズの値reszが0より大きければリサイズしますが,0の場合はリサイズしません。
画像imgは浮動小数点32ビット(np.float32)に変換し,さらに0から255の値を-1.0から1.0の値に変換します。
img = (img - 128)/128
そして,imageのリストにappendメソッドを用いてimgを追加します。
image.append( [ img] )
次にラベル番号は整数32ビット(int32)に変換して,labelのリストに追加します。
t = np.array( int( data[1] ), dtype = np.int32 )
label.append( t )
最後に,画像imageとラベルlabelのリストをTupleDatasetメソッドを使って,統合してtrainデータを作成します。
train = tuple_dataset.TupleDataset( image, label )
TupleDatasetは複数の同じ長さのデータセットをひとつのtuple型のデータセットにまとめるメソッドです。
プログラムを入力して実行してみよう
makeDicomTrain.py
import os, sys import numpy as np import cv2 import chainer from chainer.datasets import tuple_dataset import pydicom def constractTrainData( fname , resz ): image = [] label = [] for n, line in enumerate(open(fname, 'r')): data = line.split(",") # 画像ファイル名と正解データに分割 print(n," File Name:", 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 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 if __name__=='__main__': dirname = "../dcmdir1/" fname = "trainlist.csv" resize = 200 lblnum = 0 lblname = {} csvstr = "" dlist = os.listdir(dirname) print(dlist) for nm in dlist: bsnm = nm[:-2] #ファイル名の最後の2文字(数字)を取り除く if not bsnm in lblname: lblname[bsnm] = lblnum lblnum += 1 csvstr += dirname+nm+","+str(lblname[bsnm])+","+bsnm+"\n" f = open(fname, 'w') f.write(csvstr) f.close() #画像データ,教師データの構築 train = constractTrainData(fname, resize) print("\ntrain\n") print("画像データ数 :",len(train)) print("画像/ラベル :",len(train[0])) print("画像チャンネル数:",len(train[0][0])) print("画像の高さ :",len(train[0][0][0])) print("画像の幅 :",len(train[0][0][0][0])) cv2.imshow("image",(train[0][0][0])) cv2.waitKey(0) print("\nLabel Number:",train[0][1])
実行結果
makeDicomTrain.pyを実行すると,指定したフォルダ(../dcmdir1/)内のファイルを表示します。8枚のDICOM画像のファイル名とラベル番号が表示されます。
最後に訓練データの構成が表示されます。
画像をリサイズしたので画像の高さと幅が200となっています。
$ python makeDicomTrain.py ['Abdomen01', 'Chest02', 'Chest01', 'Brain01', 'Abdomen03', 'Abdomen02', 'Brain02', 'Chest03'] 0 File Name: ../dcmdir1/Abdomen01 Label: 0 1 File Name: ../dcmdir1/Chest02 Label: 1 2 File Name: ../dcmdir1/Chest01 Label: 1 3 File Name: ../dcmdir1/Brain01 Label: 2 4 File Name: ../dcmdir1/Abdomen03 Label: 0 5 File Name: ../dcmdir1/Abdomen02 Label: 0 6 File Name: ../dcmdir1/Brain02 Label: 2 7 File Name: ../dcmdir1/Chest03 Label: 1 train 画像データ数 : 8 画像/ラベル : 2 画像チャンネル数: 1 画像の高さ : 200 画像の幅 : 200 Label Number : 0
trainlist.csvの出力
CSVファイルにDICOMファイル名,ラベル番号とラベル名が記録されてます。確認画像のPNG出力と表示
tmp.pngファイルがreadDicom2png関数から出力されています。mnistNN.pyにconstractTrainData関数を実装してみる
実装方法は以下の要領で行います。
- mnistNN.pyをコピーしてdicomNN.pyに変更します。
- makeDicomTrain.pyのconstractTrainData関数,readDicom2png関数をdicomNN.pyにコピーします。
- mnistの入力部分は削除し,makeDicomTrain.pyのデータ入力部分を上記2つの関数のあとにコピーします。
- os, sys, cv2とpydicomのインポートを追加します。
- 今回,testデータはないので,trainデータの2つを使って実験をしてみましょう。
下記プログラムを参照してください。
dicomNN.py
import os, sys import cv2 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 import pydicom def constractTrainData( fname , resz ): image = [] label = [] for n, line in enumerate(open(fname, 'r')): data = line.split(",") # 画像ファイル名と正解データに分割 print(n," File Name:", 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 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 #訓練データとテストデータの取得 dirname = "../dcmdir1/" fname = "trainlist.csv" resize = 28 lblnum = 0 lblname = {} csvstr = "" dlist = os.listdir(dirname) print(dlist) for nm in dlist: bsnm = nm[:-2] #最後の2文字を取り除く if not bsnm in lblname: lblname[bsnm] = lblnum lblnum += 1 csvstr += dirname+nm+","+str(lblname[bsnm])+","+bsnm+"\n" f = open(fname, 'w') f.write(csvstr) f.close() #画像データ,教師データの構築 train = constractTrainData(fname, resize) print("\ntrain\n") print("画像データ数 :",len(train)) print("画像/ラベル :",len(train[0])) print("画像チャンネル数:",len(train[0][0])) print("画像の高さ :",len(train[0][0][0])) print("画像の幅 :",len(train[0][0][0][0])) class MyModel(Chain): #ネットワークモデルを定義する def __init__(self, nLabel): #ネットワークの定義 super(MyModel, self).__init__( l1=L.Linear(784,100), #全結合ネットワーク l2=L.Linear(100,100), l3=L.Linear(100,nLabel), ) def __call__(self, x): #順伝播計算 h1 = F.relu(self.l1(x)) #活性化関数にReLuを定義 h2 = F.relu(self.l2(h1)) return self.l3(h2) model = MyModel(lblnum) #ネットワークモデルを生成する 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=train[:2] #試しに2つのデータについて #訓練結果から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 :',' ','----->'*(int(t)),t)#教師データの表示 if (ans == t): ok += 1 print('\tOK',ok/n) print('平均正答率:', ok/len(test))
実行結果
mnistの手書きデータのサイズに合わせるために,DICOM画像を28×28にリサイズしています。テストは未知のデータですべきですが,最初の2つの画像データでテストをしています。
今回はMNISTからDICOM画像への応用が成功しました。
$ python dicomNN.py ['Abdomen01', 'Chest02', 'Chest01', 'Brain01', 'Abdomen03', 'Abdomen02', 'Brain02', 'Chest03'] 0 File Name: ../dcmdir1/Abdomen01 Label: 0 1 File Name: ../dcmdir1/Chest02 Label: 1 2 File Name: ../dcmdir1/Chest01 Label: 1 3 File Name: ../dcmdir1/Brain01 Label: 2 4 File Name: ../dcmdir1/Abdomen03 Label: 0 5 File Name: ../dcmdir1/Abdomen02 Label: 0 6 File Name: ../dcmdir1/Brain02 Label: 2 7 File Name: ../dcmdir1/Chest03 Label: 1 train 画像データ数 : 8 画像/ラベル : 2 画像チャンネル数: 1 画像の高さ : 28 画像の幅 : 28 # 0 Infer : [[ 0.67 0.22 0.11]] Sprvs : 0 OK 1.0 # 1 Infer : [[ 0.31 0.60 0.09]] Sprvs : -----> 1 OK 1.0 平均正答率: 1.0
おわりに
MNISTはとても有名なデータセットなので,インターネットで調べると実はたくさん情報を得ることができます。しかし,今回は解析的にデータ構造を追いかけることをしました。
こうした手法は,将来未知のデータセットに出会ったときに役に立つと思います。
今回訓練用データを作成するconstrauctTrain関数はこれからも使います。
また,画像ファイル名とラベル番号をCSVファイルに保存する考え方は,これからも訓練やテストのために使用します。
0 件のコメント:
コメントを投稿