矩形の左上のy座標, 矩形の左上のx座標, 矩形の右下のy座標, 矩形の右下のx座標]のような形式で定義されることが多く,クラスは物体の種類ごとに割り振られたID(以下クラスラベル)で表されることが多いようです.例えば,RBCは0,WBCは1,Plateletは2といったように,対象とする物体に1対1対応した非負整数が割り当てられるのが一般的です.

!pip install chainercv # ChainerCVのインストール
import chainer
import cupy
import chainercv
import matplotlib
chainer.print_runtime_info()
print('ChainerCV:', chainercv.__version__)
print('matplotlib:', matplotlib.__version__)
!if [ ! -d BCCD_Dataset ]; then git clone https://github.com/Shenggan/BCCD_Dataset.git; fiBCCD_Datasetディレクトリ以下のファイル構成を見てみましょう.このデータセットは,以下のようなファイル構成で配布されています.train.txtにリストアップされた画像を学習に,val.txtにリストアップされた画像を検証(学習中に汎化性能を大雑把に調べるために使うデータセットスプリット)に,test.txtにリストアップされた画像を学習終了後の最終的な性能評価に用います._get_annotationsメソッドをオーバーライドして,今回使用するデータセットを読み込み可能にします.変更が必要な行は1行だけです.こちらから該当するコード(_get_annotationsメソッドの部分)をコピーしてきて,以下の変更を行い,VOCBboxDatasetを継承するBCCDDatasetクラスのメソッドとして追加してみましょう.
(以下はdiff形式とよばれ,-でははじまる行を削除し,+で始まる行を追加するという意味です)- label.append(voc_utils.voc_bbox_label_names.index(name))
+ label.append(bccd_labels.index(name))

import os
import xml.etree.ElementTree as ET
import numpy as np
from chainercv.datasets import VOCBboxDataset
bccd_labels = ('rbc', 'wbc', 'platelets')
class BCCDDataset(VOCBboxDataset):
def _get_annotations(self, i):
id_ = self.ids[i]
# Pascal VOC形式のアノテーションデータは,XML形式で配布されています
anno = ET.parse(
os.path.join(self.data_dir, 'Annotations', id_ + '.xml'))
# XMLを読み込んで,bboxの座標・大きさ,bboxごとのクラスラベルなどの
# 情報を取り出し,リストに追加していきます
bbox = []
label = []
difficult = []
for obj in anno.findall('object'):
bndbox_anno = obj.find('bndbox')
# bboxの座標値が0-originになるように1を引いています
# subtract 1 to make pixel indexes 0-based
bbox.append([
int(bndbox_anno.find(tag).text) - 1
for tag in ('ymin', 'xmin', 'ymax', 'xmax')])
name = obj.find('name').text.lower().strip()
label.append(bccd_labels.index(name))
bbox = np.stack(bbox).astype(np.float32)
label = np.stack(label).astype(np.int32)
# オリジナルのPascal VOCには,difficultという
# 属性が画像ごとに真偽値で与えられていますが,今回は用いません
# (今回のデータセットでは全画像がdifficult = 0に設定されているため)
# When `use_difficult==False`, all elements in `difficult` are False.
difficult = np.array(difficult, dtype=np.bool)
return bbox, label, difficult
train_dataset = BCCDDataset('BCCD_Dataset/BCCD', 'train')
valid_dataset = BCCDDataset('BCCD_Dataset/BCCD', 'val')
test_dataset = BCCDDataset('BCCD_Dataset/BCCD', 'test')
print('Number of images in "train" dataset:', len(train_dataset))
print('Number of images in "valid" dataset:', len(valid_dataset))
print('Number of images in "test" dataset:', len(test_dataset))train_datasetの1つ目のデータにアクセスしてみましょう.
first_datum = train_dataset[0]train_datasetはVOCBboxDatasetを継承したBCCDDatasetクラスのオブジェクトでした.そのため,上でオーバーライドした_get_annotationsメソッド以外は,VOCBboxDatasetクラスが提供する機能を継承しているはずです.どのような機能が提供されているのか,VOCBboxDatasetクラスのドキュメントを見て確認してみましょう:VOCBboxDataset| name | shape | dtype | format |
|---|---|---|---|
| img | (3,H,W) | float32 | RGB, [0,255] |
| bbox | (R,4) | float32 | (ymin,xmin,ymax,xmax) |
| label | (R,) | int32 | [0,#fg_class−1] |
| difficult (optional)* | (R,) | bool | – |
return_difficult = True のときのみ有効return_difficultオプションを明示的にTrueと指定していないので,デフォルト値のFalseが使われています.そのため上の表の最後の行にあるdifficultという要素は返ってきません.(画像データ, 正解のbboxリスト, 各bboxごとのクラス)という3つの配列となっています.
len(first_datum)
print(first_datum[0].shape, first_datum[0].dtype)(3=チャンネル数, H=高さ, W=幅)という形になっており,またデータ型はfloat32になっています.上の表にあったとおりでした.ではbboxはどのような形式になっているのでしょうか.中身と,そのshapeを表示して見てみます.
print(first_datum[1])
print(first_datum[1].shape)(y_min, x_min, y_max, x_max)という4つの数字で表されています.この4つの数字はbboxの左上と右下の画像座標値(画像平面上の位置)を表しています.
print(first_datum[2])
print(first_datum[2].shape)first_datum[1])に順番に対応しており,それぞれのbboxがどのクラスに属する物体か(0: RBC, 1: WBC, 2: Platelet)を表しています.
%matplotlib inline
from chainercv.visualizations import vis_bbox
img, bbox, label = train_dataset[0]
ax = vis_bbox(img, bbox, label, label_names=bccd_labels)
ax.set_axis_off()
ax.figure.tight_layout()


chainercv.links.SSD300 というクラスは,縦横が300ピクセルの画像を入力にとるSSDのモデルを表していて,デフォルトで特徴抽出器にはVGG16という16層のネットワーク構造が用いられます.alpha と k をコンストラクタで受け取っています.alpha は,位置の予測に対する誤差とクラスの予測に対する誤差それぞれの間の重み付けを行う係数です.k は hard negative mining のためのパラメータです.学習時,一つの正解bounding boxに対して,モデルは最低一つの近しい(positiveな)予測と,多くの間違った(negativeな)予測を出力します.この多くの間違った予測をconfidence score(モデルがどの程度確信を持ってその予測を出力しているかを表す値)によってソートした上で,上から positive : negative が 1:k になるように negative サンプルを選択し,ロスの計算に使用します.このバランスを決めているのが k というパラメータで,上記論文中では とされているため,ここでもデフォルトで3を使っています.forward メソッドでは,入力画像と正解の位置・ラベルのリストを受け取って,実際にロスの計算を行っています.物体検出は,物体のlocalization(位置の予測)とclassification(種類(=クラス)の予測)の二つの問題を同時に解きますが,SSDでは,localization lossとclassification lossを別々に計算します.
import chainer
from chainercv.links import SSD300
from chainercv.links.model.ssd import multibox_loss
class MultiboxTrainChain(chainer.Chain):
def __init__(self, model, alpha=1, k=3):
super(MultiboxTrainChain, self).__init__()
with self.init_scope():
self.model = model
self.alpha = alpha
self.k = k
def forward(self, imgs, gt_mb_locs, gt_mb_labels):
mb_locs, mb_confs = self.model(imgs)
loc_loss, conf_loss = multibox_loss(
mb_locs, mb_confs, gt_mb_locs, gt_mb_labels, self.k)
loss = loc_loss * self.alpha + conf_loss
chainer.reporter.report(
{'loss': loss, 'loss/loc': loc_loss, 'loss/conf': conf_loss},
self)
return loss
model = SSD300(n_fg_class=len(bccd_labels), pretrained_model='imagenet')
train_chain = MultiboxTrainChain(model)__call__メソッド内に記述されている5つとなります.例えば画像の意味を大きくかえない範囲で色を変えたり,水平方向に反転させたり,拡大,縮小したりします.それらの際には正解ラベルも適切に変換する必要があることに注意してください.例えば、水平方向に反転させる場合は,正解ラベルも水平方向に反転させたものを正解とします.また,画像の一部分をマスクし、隠すことも有効な手法です.これにより認識の際,一つの情報だけに依存せず様々な情報に基づいて認識できるようになります.
import copy
import numpy as np
from chainercv import transforms
from chainercv.links.model.ssd import random_crop_with_bbox_constraints
from chainercv.links.model.ssd import random_distort
from chainercv.links.model.ssd import resize_with_random_interpolation
class Transform(object):
def __init__(self, coder, size, mean):
# to send cpu, make a copy
self.coder = copy.copy(coder)
self.coder.to_cpu()
self.size = size
self.mean = mean
def __call__(self, in_data):
# There are five data augmentation steps
# 1. Color augmentation
# 2. Random expansion
# 3. Random cropping
# 4. Resizing with random interpolation
# 5. Random horizontal flipping
img, bbox, label = in_data
# 1. Color augmentation
img = random_distort(img)
# 2. Random expansion
if np.random.randint(2):
img, param = transforms.random_expand(
img, fill=self.mean, return_param=True)
bbox = transforms.translate_bbox(
bbox, y_offset=param['y_offset'], x_offset=param['x_offset'])
# 3. Random cropping
img, param = random_crop_with_bbox_constraints(
img, bbox, return_param=True)
bbox, param = transforms.crop_bbox(
bbox, y_slice=param['y_slice'], x_slice=param['x_slice'],
allow_outside_center=False, return_param=True)
label = label[param['index']]
# 4. Resizing with random interpolatation
_, H, W = img.shape
img = resize_with_random_interpolation(img, (self.size, self.size))
bbox = transforms.resize_bbox(bbox, (H, W), (self.size, self.size))
# 5. Random horizontal flipping
img, params = transforms.random_flip(
img, x_random=True, return_param=True)
bbox = transforms.flip_bbox(
bbox, (self.size, self.size), x_flip=params['x_flip'])
# Preparation for SSD network
img -= self.mean
mb_loc, mb_label = self.coder.encode(bbox, label)
return img, mb_loc, mb_labelTransformDatasetを使って,直前に定義した変換Transformをデータ毎に適用するようにします.
from chainer.datasets import TransformDataset
from chainer.optimizer_hooks import WeightDecay
from chainer import serializers
from chainer import training
from chainer.training import extensions
from chainer.training import triggers
from chainercv.extensions import DetectionVOCEvaluator
from chainercv.links.model.ssd import GradientScaling
chainer.cuda.set_max_workspace_size(1024 * 1024 * 1024)
chainer.config.autotune = True
batchsize = 32
gpu_id = 0
out = 'results'
initial_lr = 0.001
training_epoch = 300
log_interval = 10, 'epoch'
lr_decay_rate = 0.1
lr_decay_timing = [200, 250]Transformクラスで定義した変換処理にて変換されます.
transformed_train_dataset = TransformDataset(train_dataset, Transform(model.coder, model.insize, model.mean))
train_iter = chainer.iterators.MultiprocessIterator(transformed_train_dataset, batchsize)
valid_iter = chainer.iterators.SerialIterator(valid_dataset, batchsize, repeat=False, shuffle=False)update_ruleに対してフックを設定します.また,バイアスパラメータの場合にはweight decayは行わず,バイアスパラメータ以外のパラメータに対してはweight decayを行うように設定しています.これらは学習の安定化などのためにしばしば用いられるテクニックです.
optimizer = chainer.optimizers.MomentumSGD()
optimizer.setup(train_chain)
for param in train_chain.params():
if param.name == 'b':
param.update_rule.add_hook(GradientScaling(2))
else:
param.update_rule.add_hook(WeightDecay(0.0005))StandardUpdaterを用いました.CPUもしくはシングルGPUを用いて学習を行う際には,このUpdaterを使います.
updater = training.updaters.StandardUpdater(
train_iter, optimizer, device=gpu_id)
trainer = training.Trainer(
updater,
(training_epoch, 'epoch'), out)ManualScheduleTriggerという新しい減衰のタイミングの指定方法が使われています.これはシンプルに,[200, 250]などのようにそのExtentionを起動したいタイミングを表す数字が並んだリストと,その単位(ここではepoch)を渡すと,指定されたタイミングのみでそのExtensionが発動するというものです.以下のコードでは,lr_decay_timingに上で[200, 250]を代入していますので,200エポックと250エポックの時点でExponentialShiftが発動し,学習率をlr_decay_rate倍,つまり上で設定したように,倍するというものになっています.
trainer.extend(
extensions.ExponentialShift('lr', lr_decay_rate, init=initial_lr),
trigger=triggers.ManualScheduleTrigger(lr_decay_timing, 'epoch'))DetectionVOCEvaluatorというExtensionは,渡されたイテレータ(ここではvalidation datasetに対して作成したval_iterというイテレータ)を使って,各クラスごとのAPや全体のmAPを学習中に計算してくれます.ここでもこのExtensionを利用します.
trainer.extend(
DetectionVOCEvaluator(
valid_iter, model, use_07_metric=False,
label_names=bccd_labels),
trigger=(1, 'epoch'))
trainer.extend(extensions.LogReport(trigger=log_interval))
trainer.extend(extensions.observe_lr(), trigger=log_interval)
trainer.extend(extensions.PrintReport(
['epoch', 'iteration', 'lr',
'main/loss', 'main/loss/loc', 'main/loss/conf',
'validation/main/map', 'elapsed_time']),
trigger=log_interval)
if extensions.PlotReport.available():
trainer.extend(
extensions.PlotReport(
['main/loss', 'main/loss/loc', 'main/loss/conf'],
'epoch', file_name='loss.png'))
trainer.extend(
extensions.PlotReport(
['validation/main/map'],
'epoch', file_name='accuracy.png'))
trainer.extend(extensions.snapshot(
filename='snapshot_epoch_{.updater.epoch}.npz'), trigger=(10, 'epoch'))
trainer.run()
!wget https://github.com/japan-medical-ai/medical-ai-course-materials/releases/download/v0.1/detection_snapshot_epoch_290.npzdetection_snapshot_ 100%[===================>] 171.33M 23.7MB/s in 11s
2018-12-16 13:36:55 (16.1 MB/s) - ‘detection_snapshot_epoch_290.npz’ saved [179653491/179653491]
detection_snapshot_epoch_250.npzというファイルを先程作成したTrainerオブジェクトに読み込んでみましょう.
chainer.serializers.load_npz('detection_snapshot_epoch_290.npz', trainer)
trainer.run()