!pip install chainercv
import chainer
import cupy
import chainercv
import matplotlib
chainer.print_runtime_info()
print('ChainerCV:', chainercv.__version__)
print('matplotlib:', matplotlib.__version__)
!if [ ! -d train ]; then curl -L -O https://github.com/mitmul/chainer-handson/releases/download/SegmentationDataset/train.zip && unzip train.zip && rm -rf train.zip; fi
!if [ ! -d val ]; then curl -L -O https://github.com/mitmul/chainer-handson/releases/download/SegmentationDataset/val.zip && unzip val.zip && rm -rf val.zip; fi
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
# PILライブラリで画像を読み込む
img = np.asarray(Image.open('train/image/000.png'))
label = np.asarray(Image.open('train/label/000.png'))
# matplotlibライブラリを使って2つの画像を並べて表示
fig, axes = plt.subplots(1, 2)
axes[0].set_axis_off()
axes[0].imshow(img, cmap='gray')
axes[1].set_axis_off()
axes[1].imshow(label, cmap='gray')
plt.show()
Trainer
が用意されています.これを用いて,左心室であるかそれ以外かの2クラスにすべてのピクセルを分類するSemantic Segmentationタスクに取り組みます.Trainer
を使って学習を行う際にユーザがする必要がある準備について再度復習しましょう.chainer.Chain
クラスを継承して書く)chainer.optimizers
以下にある最適化手法から選ぶ)Updater
オブジェクトの準備(Iterator
とOptimizer
をとり,実際の学習部分(パラメータアップデート)を行うもの)Trainer
オブジェクトの作成(学習ループの管理)Trainer
に含まれるコンポーネントは,以下のような関係になっています.Updater
は,Iterator
からDataset
にあるデータを指定したバッチサイズ数だけ取り出し,Model
に与えて目的関数の値を計算し,Optimizer
によってパラメータを更新する,という一連の作業(これが1 iterationになります)を隠蔽しています.Trainer
はExtension
という拡張機能を使うことができ,指定したタイミング(毎iterationや,毎epoch)でログを取る,目的関数の値や精度のプロットを描画して保存,などを自動的に行うことができます.Trainer
オブジェクトを作成し,trainer.run()
のようにして学習を開始することになります.Trainer
を使わず,自分で学習ループを記述することもできますが,今回はTrainer
を使用することを前提とします.自分で学習ループを記述する方法を知りたい場合は4章を参照してください)ImageDataset
は,画像ファイルへのファイルパスのリストを渡して初期化してやると,そのパスにある画像を学習時にディスクから読み込み,それを返してくれるようなデータセットクラスです.TupleDataset
は,複数のデータセットオブジェクトを渡して初期化すると,それらから同じインデックスを持つデータをタプルに束ねて返してくれるようなデータセットオブジェクトを作成するクラスです.(Pythonのzip
と同様です.)ImageDataset
オブジェクトを作成します.以下のセルを実行してください.import glob
from chainer import datasets
def create_dataset(img_filenames, label_filenames):
img = datasets.ImageDataset(img_filenames)
img = datasets.TransformDataset(img, lambda x: x / 255.) # 0-1に正規化
label = datasets.ImageDataset(label_filenames, dtype=np.int32)
dataset = datasets.TupleDataset(img, label)
return dataset
img_filenames
と,正解ラベル画像(0 or 1の画素値を持つ二値画像)のファイルパスのリストlabel_filenames
を与えて,2つのデータセットオブジェクトをTupleDataset
で束ねて返すものになっています.img
は入力画像のデータセットですが,まるで入力画像が入ったリストのように振る舞い,img[i]
はi
番目の画像を返します([i]
でアクセスしたときに初めてディスクから画像が読み込まれます).label
も同様に,ラベル画像のリストのように振る舞います.これらをTupleDataset
で束ねて作ったdataset
は,dataset[i]
でアクセスすると(img[i], label[i])
というタプル(値の2つ以上の集まり)を返すものになります.(これはimg
とlabel
が同じ長さのリストの場合,zip(img, label)
の結果と同じです.)ImageDataset
で作った入力データセットを元にTransformDataset
という新しいデータセットを作っています.TransformDataset
は,第1引数に与えられたデータセットにアクセスする際に第2引数に与えた関数を適用してから返すようにできるクラスで,任意の関数を与えてデータを変換させる処理をはさむことができます.ここでは,変換を行う関数をlambda
関数を使って与え,単純に値域をに変換するだけの処理を行っています.この他,例えば内部で乱数によって様々な変換(画像の場合,ランダムに左右反転を行ったり,ランダムな角度で回転をしたり,などがよく行われます)を施す関数を引数として渡すことでData augmentationを簡単に実装することができます.create_dataset
関数を使って学習用・検証用それぞれのデータセットオブジェクトを作成しましょう.下のセルを実行してください.def create_datasets():
# Python標準のglobを使ってMRI画像ファイル名/ラベル画像ファイル名の一覧を取得
train_img_filenames = sorted(glob.glob('train/image/*.png'))
train_label_filenames = sorted(glob.glob('train/label/*.png'))
# リストを渡して,データセットオブジェクト train を作成
train = create_dataset(train_img_filenames, train_label_filenames)
# 同様のことをvalidationデータに対しても行う
val_img_filenames = sorted(glob.glob('val/image/*.png'))
val_label_filenames = sorted(glob.glob('val/label/*.png'))
val = create_dataset(val_img_filenames, val_label_filenames)
return train, val
create_datasets()
では,まずPython標準に備わっているglob
を使って,.png
の拡張子を持つ画像ファイルを指定したディレクトリ以下から探してきて,ファイルパスが格納されたリストを作ります.次に,入力画像とラベル画像のファイルリストが同じインデックスで対応したデータをそれぞれ指すように,sorted
を使ってファイル名をソートしています(glob
関数で列挙されるファイルリストは必ずしもソートされているとは限りません).そのあと,それらのファイル名リストを先程のcreate_dataset
関数に渡して,データセットオブジェクトを作成しています.同様のことを検証用の画像ファイルに対しても行い,train
とval
2つのデータセットオブジェクトを作成して返します.train, val = create_datasets()
print('Dataset size:\n\ttrain:\t{}\n\tvalid:\t{}'.format(len(train), len(val)))
len()
を使っていくつのデータが含まれているかを知ることができます.import chainer
import chainer.functions as F
import chainer.links as L
class MultiLayerPerceptron(chainer.Chain):
def __init__(self, out_h, out_w):
super().__init__()
with self.init_scope():
self.l1 = L.Linear(None, 100)
self.l2 = L.Linear(100, 100)
self.l3 = L.Linear(100, out_h * out_w)
self.out_h = out_h
self.out_w = out_w
def forward(self, x):
h = F.relu(self.l1(x))
h = F.relu(self.l2(h))
h = self.l3(h)
n = x.shape[0]
return h.reshape((n, 1, self.out_h, self.out_w))
Trainer
オブジェクトを作成して返してくれるcreate_trainer
関数を定義しましょう.各引数の定義は以下の通りです‥-1
にするとCPU,>=0
の場合はそのIDを持つGPUfrom chainer import iterators
from chainer import training
from chainer import optimizers
from chainer.training import extensions
def create_trainer(batchsize, train, val, stop, device=-1):
# 先程定義したモデルを使用
model = MultiLayerPerceptron(out_h=256, out_w=256)
# ピクセルごとの二値分類なので,目的関数にSigmoid cross entropyを,
# 精度をはかる関数としてBinary accuracyを指定しています
train_model = L.Classifier(
model, lossfun=F.sigmoid_cross_entropy, accfun=F.binary_accuracy)
# 最適化手法にAdamを使います
optimizer = optimizers.Adam()
optimizer.setup(train_model)
# データセットから,指定したバッチサイズ数のデータ点をまとめて取り出して返すイテレータを定義します
train_iter = iterators.MultiprocessIterator(train, batchsize)
val_iter = iterators.MultiprocessIterator(val, batchsize, repeat=False, shuffle=False)
# イテレータからデータを引き出し,モデルに渡して,目的関数の値を計算し,backwardしてパラメータを更新,
# までの一連の処理を行う updater を定義します
updater = training.StandardUpdater(train_iter, optimizer, device=device)
# 様々な付加機能をExtensionとして与えられるTrainerを使います
trainer = training.trainer.Trainer(updater, stop)
logging_attributes = [
'epoch', 'main/loss', 'main/accuracy', 'val/main/loss', 'val/main/accuracy']
trainer.extend(extensions.LogReport(logging_attributes))
trainer.extend(extensions.PrintReport(logging_attributes))
trainer.extend(extensions.PlotReport(['main/loss', 'val/main/loss'], 'epoch', file_name='loss.png'))
trainer.extend(extensions.PlotReport(['main/accuracy', 'val/main/accuracy'], 'epoch', file_name='accuracy.png'))
trainer.extend(extensions.Evaluator(val_iter, optimizer.target, device=device), name='val')
return trainer
LogReport
)やその標準出力への表示(PrintReport
),目的関数の値や精度のプロットの自動作成(PlotReport
),指定したタイミングおきにvalidationデータで評価(Evaluator
),などをしてくれる拡張機能です.Extension
の一覧から,使い方やできることを調べることができます: Trainer extensionstrainer
からrun()関数を呼び出すだけです.%%time
trainer = create_trainer(64, train, val, (20, 'epoch'), device=0)
trainer.run()
PrintReport
というExtensionが出力したログの情報です.現在のエポック数,目的関数の値,精度(学習データセットに対してのものはmain/loss
, main/accuracy
,検証データセットに対してのものはval/main/loss
, val/main/accuracy
)が表示されています.PlotReport
拡張が出力したグラフを見てみましょう.学習が終了したら,以下の2つのセルを実行してみてください.from IPython.display import Image
Image('result/loss.png')
Image('result/accuracy.png')
out
という引数で指定された場所に画像として保存されています.これは逐次更新されているので,実際には学習の途中でもその時点でのプロットを確認することができます.学習の進み具合を視覚的に確認するのに便利です.from chainer import cuda
from chainercv import evaluations
def evaluate(trainer, val, device=-1):
# Trainerオブジェクトから学習済みモデルを取り出す
model = trainer.updater.get_optimizer('main').target.predictor
# validationデータ全部に対して予測を行う
preds = []
for img, label in val:
img = cuda.to_gpu(img[np.newaxis], device)
pred = model(img)
pred = cuda.to_cpu(pred.data[0, 0] > 0)
preds.append((pred, label[0]))
pred_labels, gt_labels = zip(*preds)
# 評価をして結果を表示
evals = evaluations.eval_semantic_segmentation(pred_labels, gt_labels)
print('Pixel Accuracy:', evals['pixel_accuracy'])
print('mIoU:', evals['miou'])
evaluate(trainer, val, device=0)
PrintReport
が表示した val/main/accuracy と同じ値になっています.学習中に"accuracy"として表示していたものは,Pixel Accuracyと同じものでした.こちらは,とても高い値を示しています.最大値が1であるので0.98というのは高い数値です.miou
)が思ったより低いことが分かります.なぜでしょうか.def show_predicts(trainer, val, device=-1, n_sample=3):
# Trainerオブジェクトから学習済みモデルを取り出す
model = trainer.updater.get_optimizer('main').target.predictor
for i in range(n_sample):
img, label = val[i]
img = cuda.to_gpu(img, device)
pred = model(img[np.newaxis])
pred = cuda.to_cpu(pred.data[0, 0] > 0)
fig, axes = plt.subplots(1, 2)
axes[0].set_axis_off()
axes[0].imshow(pred, cmap='gray')
axes[1].set_axis_off()
axes[1].imshow(label[0], cmap='gray')
plt.show()
show_predicts(trainer, val, device=0)
mIoU
は今回のような画像中の予測対象領域の割合が少ない場合に有効な指標となります.mIoU
を改善するかに取り組んでみましょう.Convolution2D
とDeconvolution2D
の2つだけです.それぞれ,カーネルサイズ(ksize
),ストライド(stride
),パディング(pad
)を指定することができます.これらがどのように出力を変化させるかを,まずはまとめてみましょう.Convolution2D
というLinkは,一般的な畳込みレイヤの実装です.Convolutionがどのようなレイヤかは前章で説明しました.畳み込み層のパラメータを設定する際には,以下の点を知っておくと便利です.Deconvolution2D
は,歴史的な経緯からその名とは異なり数学的な意味でのdeconvolutionではありません.実際に適用している操作からTransposed convolutionや,Backward convolutionとよばれることもあります.Deconvolution2Dとはフィルタの適用の仕方はConvolutionと同じですが入力特徴マップの値を飛び飛びに配置するなどの処理が入る部分が異なる処理のことです.Deconvolution2D
レイヤのパラメータを設定する際には,以下の点を知っておくと便利です.None
を与えておくと,実行時に自動的に決定してくれます.from chainer import reporter
from chainer import cuda
from chainercv import evaluations
class FullyConvolutionalNetwork(chainer.Chain):
def __init__(self, out_h, out_w, n_class=1):
super().__init__()
with self.init_scope():
# L.Convolution2D(in_ch, out_ch, ksize, stride, pad)
# in_chは省略することができるので,
# L.Convolution2D(out_ch, ksize, stride, pad)
# とかくこともできます.
self.conv1 = L.Convolution2D(None, FIXME_1, ksize=5, stride=2, pad=2)
self.conv2 = L.Convolution2D(None, FIXME_2, ksize=5, stride=2, pad=2)
self.conv3 = L.Convolution2D(None, FIXME_3, ksize=3, stride=1, pad=1)
self.conv4 = L.Convolution2D(None, FIXME_4, ksize=3, stride=1, pad=1)
self.conv5 = L.Convolution2D(None, FIXME_5, ksize=1, stride=1, pad=0)
# L.Deconvolution2D(in_ch, out_ch, ksize, stride, pad)
# in_chは省略することができるので,
# L.Deconvolution2D(out_ch, ksize, stride, pad)
# と書くこともできます.
self.deconv6 = L.Deconvolution2D(None, n_class, ksize=32, stride=16, pad=8)
self.out_h = out_h
self.out_w = out_w
def forward(self, x):
h = F.relu(self.conv1(x))
h = F.max_pooling_2d(h, 2, 2)
h = F.relu(self.conv2(h))
h = F.max_pooling_2d(h, 2, 2)
h = F.relu(self.conv3(h))
h = F.relu(self.conv4(h))
h = self.conv5(h)
h = self.deconv6(h)
return h.reshape(x.shape[0], 1, h.shape[2], h.shape[3])
print(FullyConvolutionalNetwork(256, 256)(np.zeros((1, 1, 256, 256), dtype=np.float32)).shape[2:])
class PixelwiseSigmoidClassifier(chainer.Chain):
def __init__(self, predictor):
super().__init__()
with self.init_scope():
# 学習対象のモデルをpredictorとして保持しておく
self.predictor = predictor
def __call__(self, x, t):
# 学習対象のモデルでまず推論を行う
y = self.predictor(x)
# 2クラス分類の誤差を計算
loss = F.sigmoid_cross_entropy(y, t)
# 予測結果(0~1の連続値を持つグレースケール画像)を二値化し,
# ChainerCVのeval_semantic_segmentation関数に正解ラベルと
# 共に渡して各種スコアを計算
y, t = cuda.to_cpu(F.sigmoid(y).data), cuda.to_cpu(t)
y = np.asarray(y > 0.5, dtype=np.int32)
y, t = y[:, 0, ...], t[:, 0, ...]
evals = evaluations.eval_semantic_segmentation(y, t)
# 学習中のログに出力
reporter.report({'loss': loss,
'miou': evals['miou'],
'pa': evals['pixel_accuracy']}, self)
return loss
L.Classifier
というオブジェクトに渡した上でOptimizerに渡していました.Chainerが用意しているこのL.Classifier
は,内部で目的関数の値だけでなくAccuracyも計算し,reporter.report
に辞書を渡す形でLogReport
などのExtensionが補足できるように値の報告を行います.
しかし,L.Classifier
はmIoUの計算をしてくれません.L.Classifier
を自前のPixelwiseSigmoidClassifier
に置き換え,自分で実際の目的関数となるF.sigmoid_cross_entropy
の計算を書きつつ,予測(上記コード中のy
)に対してPixel AccuracyとmIoUの両方を計算して,報告するようにします.__call__
自体は目的関数の値(スカラ)を返すことが期待されているので,F.sigmoid_cross_entropy
の返り値であるloss
だけをreturn
しています.def create_trainer(batchsize, train, val, stop, device=-1, log_trigger=(1, 'epoch')):
model = FullyConvolutionalNetwork(out_h=256, out_w=256)
train_model = PixelwiseSigmoidClassifier(model)
optimizer = optimizers.Adam(eps=1e-05)
optimizer.setup(train_model)
train_iter = iterators.MultiprocessIterator(train, batchsize)
val_iter = iterators.MultiprocessIterator(val, batchsize, repeat=False, shuffle=False)
updater = training.StandardUpdater(train_iter, optimizer, device=device)
trainer = training.trainer.Trainer(updater, stop, out='result_fcn')
logging_attributes = [
'epoch', 'main/loss', 'main/miou', 'main/pa',
'val/main/loss', 'val/main/miou', 'val/main/pa']
trainer.extend(extensions.LogReport(logging_attributes), trigger=log_trigger)
trainer.extend(extensions.PrintReport(logging_attributes), trigger=log_trigger)
trainer.extend(extensions.PlotReport(['main/loss', 'val/main/loss'], 'epoch', file_name='loss.png'))
trainer.extend(extensions.PlotReport(['main/miou', 'val/main/miou'], 'epoch', file_name='miou.png'))
trainer.extend(extensions.PlotReport(['main/pa', 'val/main/pa'], 'epoch', file_name='pa.png'))
trainer.extend(extensions.Evaluator(val_iter, train_model, device=device), name='val')
trainer.extend(extensions.dump_graph('main/loss'))
return trainer
LogReport
や標準出力にログを指定項目を出力するPrintReport
,またグラフを出力するPlotReport
拡張でloss
とaccuracy
(ここではpa
=Pixel Accuracy)だけでなくmiou
も出力しているところです.%%time
trainer = create_trainer(128, train, val, (200, 'epoch'), device=0, log_trigger=(10, 'epoch'))
trainer.run()
PrintReport
が出力した経過の値を見る限り,mIoUが少なくとも0.90近くまで到達していることがわかります.PlotReport
拡張が出力したグラフを見てみましょう.下記の3つのセルを実行してください.from IPython.display import Image
print('Loss')
Image('result_fcn/loss.png')
print('mean IoU')
Image('result_fcn/miou.png')
print('Pixel Accuracy')
Image('result_fcn/pa.png')
evaluate(trainer, val, device=0)
show_predicts(trainer, val, device=0, )