PyTorchのObjectDetectionフレームワーク「MMDetection」を使って独自モデルを学習する

公式リリースからしばらく経過したPyTorchですが、最近は便利な周辺ライブラリが揃い始めました。
ObjectDetection用のライブラリもちらほら出てきています。
PyTorch用のObjectDetectionライブラリといえばDetectron2が有名ですね。
GitHub - facebookresearch/detectron2: Detectron2 is FAIR's next-generation research platform for object detection and segmentation.

ところが最近arxivに登場するObjectDetectionモデルはMMDetectionというフレームワークの上に実装されているものが多いです。
arxiv.org

上記文献内の表(下記)によると、学習スピードも速く、inferenceもそこそこ速く、
性能もオリジナルをほぼ再現しているといえます。
f:id:Yoneken1:20191208230650p:plain

何よりカスタマイズ性が高いというところがポイントです。
DeepLearningの研究が盛んな中国で開発されているということもあり、
これから流行っていく可能性を秘めています。

早速使ってみる

どのくらいカスタマイズしやすいか試してみました。
今回は、FCOSにBiFPNをくっつけてみたいと思います。

FCOSはRetinaNetの改良版の様な手法で、Detection HeadにCenternessと呼ばれる部分を追加して性能を向上したものです。
詳しくは論文をご参照ください。
arxiv.org

BiFPNはもはや説明不要かと思いますが、EfficientDetで提案されたFPNの上位版です。
arxiv.org

FCOSはMMDetectionに既に入っていますので、今回はFCOSをベースに、BiFPNを組み込んでいきます。
組込み方法はとても簡単で、MMDetectionのGETTING_STARTED.mdに従うだけです。
github.com

手順1: BiFPNの実装

BiFPNは以下に既にそれっぽいものがあったのでお借りしました。
github.com
2019/12/08時点では、なぜかextraモジュールがなく、階層が足りなかったため、適当に追加しました。

MMDetectionでは、FPNなどのモジュールをNECKというカテゴリとしています。
そこで、次のようなおまじないを書いて、NECKに登録します。

from ..registry import NECKS
@NECKS.register_module
class BIFPN(nn.Module):
 ・・・

基本的な変更点はこれだけです。
とても簡単です。

後は初期化とかを適当に追加しました。
最終的なソースは以下の様になりました。
https://github.com/yoneken1/pytorch_bifpn_for_mmdetection/blob/master/bifpn.py
注:この実装はBiFPNのオリジナルを再現してません。ご使用の際は自己責任でお願いします。

手順2:modulesに追加

上記で作成したpythonファイルを、mmdet/models/necksに配置します。
また以下の様に__init__.pyを修正して、BiFPNをインポートします。

from .bifpn import BIFPN
__all__ = ['FPN', 'BFP', 'HRFPN', 'BIFPN']
手順3:configファイルを修正する

MMDetectionはconfigファイルに使いたいモジュールの設定などを書いておき、
実行時にconfigファイルを読み込むことで、
自動的にモデルを構築して学習や評価を実行してくれます。

今回はfcosのconfigファイルを少し書き換えてBiFPNを読み込むようにします。
設定はとても簡単で、以下の様にneckにBIFPNを指定し、その他パラメータを調整するだけです。

    neck=dict(
        type='BIFPN',
        in_channels=[256, 512, 1024, 2048],
        out_channels=256,
        start_level=1,
        stack=2,
        add_extra_convs=False,
        extra_convs_on_inputs=False,
        num_outs=5,
        relu_before_extra_convs=False),

BiFPNは入力解像度が128の倍数である必要があるため、
今回は以下の様な感じで、Resizeの設定を変えました。

 dict(type='Resize', img_scale=(768, 768), keep_ratio=False),

あとはGPUの数やメモリに合わせてバッチサイズと学習率を調整します。

学習の実行

以下の様なコマンドで学習がスタートします。

python ./tools/train.py configs/fcos/fcos_r50_caffe_bifpn_gn_1x_1gpu.py --work_dir 'models/fcos' --gpus 1

評価なども同様です。
とてもシンプルでわかりやすいですね。

おわりに

とてもお手軽に独自モデルを構築することができました。
ObjectDetectionは実装が複雑になりがちで、
自分で実装するととても複雑なコードが出来上がってしまう事が多々あるかと思います。
私自身も以前FasterRCNNをスクラッチ実装しようとして、全く論文値が再現せず、
1か月くらい苦しんだのち、結局著者実装を使うことにしたという苦い経験があります。
(その後何とか再現までこぎつけましたが・・・)
これからObjectDetectionの研究をしてみたい方で、baselineを手早く作りたい人、
さくさくアイディアを試していきたい人にはお勧めのフレームワークかと思います。
ぜひお試しください。

おまけ

上記モデルは鋭意学習中ですので、結果が出たら追記したいと思います。

追記

上記モデルですが、12epoch学習させたところ、mAPが0.31と非常に低い値になりました。
実は元の実装は結構バグがあり、それを直したつもりなのですが、まだ再現に至っていないようです。
この結果は結構悔しいので、いつかリベンジしようと思っています。

学習が爆速と噂のTTFNetをGoogle Colaboratoryで動かしてみた

TTFNetとは

2019年9月頭ころにarxivに公開されたObject Detectionのモデルで、学習がとても速いのが特徴です。
論文タイトル:Training-Time-Friendly Network for Real-Time Object Detection

既に実装が公開されています。
github.com

所謂anchor freeのモデルでCenterNet (Object as point)をベースとしています。
そのため、推論時の実行速度が速い事も売りの一つです。
f:id:Yoneken1:20191025142858p:plain

TTFNetの実力

以下論文の表を抜粋。
f:id:Yoneken1:20191025134514p:plain
TT(h)のところが学習のトータル時間です。
この表によると、一番早いやつでms-cocoの学習がたったの1.8hで終わるようです。

Google Colaboratoryで試してみる

論文中では8枚のGTX1080tiを使って学習しているようですが、
無料で誰でも使えるGoogle Colaboratoryではどの位のスピードになるのでしょうか?
実際に試してみました。

TTFNetのインストール
%cd /content/
!git clone https://github.com/ZJULearning/ttfnet.git

%cd /content/ttfnet
!pip install -v -e .

特に問題なくすんなりとインストールできました。

ms-cocoの準備

学習にはms-cocoのデータが必要なので、これを取ってきて配置します。

%cd /content/
!wget http://images.cocodataset.org/annotations/annotations_trainval2017.zip
!unzip -q -n '/content/annotations_trainval2017.zip'

%cd /content/
!wget http://images.cocodataset.org/zips/val2017.zip
!unzip -q -n '/content/val2017.zip'

%cd /content/
!wget http://images.cocodataset.org/zips/train2017.zip
!unzip -q -n '/content/train2017.zip'

!mkdir -p data/coco
!mv /content/train2017 /content/ttfnet/data/coco/
!mv /content/val2017 /content/ttfnet/data/coco/
!mv /content/annotations /content/ttfnet/data/coco/annotations

この作業は結構時間がかかります。

Google Driveへの接続

モデルの保存先としてGoogle Driveを利用します。

from google.colab import drive
drive.mount('/content/gdrive')

上記を実行するとリンクと入力枠が表示されるので、リンクをクリックして表示された画面で認証を行い、発行されたチケットを入力する事でGoogle Driveがマウントされます。

学習の実行

今回は一番軽量なResNet18の学習スケジュール短い版を学習させてみます。
また使えるGPUは1枚しかないので、gpus=1を指定します。

%cd /content/ttfnet
!mkdir '/content/gdrive/My Drive/ttfnet_models/ttfnet_r18'
!python ./tools/train.py configs/ttfnet/ttfnet_r18_1x.py --work_dir '/content/gdrive/My Drive/ttfnet_models/ttfnet_r18' --validate --autoscale-lr --gpus=1

以下の様な実行結果が出力されます。

/content/ttfnet
2019-10-25 04:21:28,109 - INFO - Distributed training: False
2019-10-25 04:21:28,253 - INFO - load model from: modelzoo://resnet18
2019-10-25 04:21:28,388 - WARNING - The model and loaded state dict do not match exactly

unexpected key in source state_dict: fc.weight, fc.bias

loading annotations into memory...
Done (t=17.55s)
creating index...
index created!
2019-10-25 04:21:49,935 - INFO - Start running, host: root@436d36ee658c, work_dir: /content/gdrive/My Drive/ttfnet_models/ttfnet_r18
2019-10-25 04:21:49,936 - INFO - workflow: [('train', 1)], max: 12 epochs
2019-10-25 04:23:05,022 - INFO - Epoch [1][50/7330]	lr: 0.00056, eta: 1 day, 12:40:12, time: 1.502, data_time: 0.042, memory: 9804, losses/ttfnet_loss_heatmap: 4.6783, losses/ttfnet_loss_wh: 5.0018, loss: 9.6801
2019-10-25 04:24:08,402 - INFO - Epoch [1][100/7330]	lr: 0.00072, eta: 1 day, 9:47:34, time: 1.268, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 4.5887, losses/ttfnet_loss_wh: 4.9279, loss: 9.5166
2019-10-25 04:25:12,307 - INFO - Epoch [1][150/7330]	lr: 0.00088, eta: 1 day, 8:54:25, time: 1.278, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 4.2175, losses/ttfnet_loss_wh: 4.1373, loss: 8.3549
2019-10-25 04:26:15,777 - INFO - Epoch [1][200/7330]	lr: 0.00104, eta: 1 day, 8:24:09, time: 1.269, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 4.1055, losses/ttfnet_loss_wh: 3.9063, loss: 8.0118
2019-10-25 04:27:19,726 - INFO - Epoch [1][250/7330]	lr: 0.00120, eta: 1 day, 8:08:22, time: 1.279, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.9817, losses/ttfnet_loss_wh: 3.6191, loss: 7.6008
2019-10-25 04:28:23,286 - INFO - Epoch [1][300/7330]	lr: 0.00136, eta: 1 day, 7:55:35, time: 1.271, data_time: 0.024, memory: 9804, losses/ttfnet_loss_heatmap: 3.9224, losses/ttfnet_loss_wh: 3.3858, loss: 7.3082
2019-10-25 04:29:27,332 - INFO - Epoch [1][350/7330]	lr: 0.00152, eta: 1 day, 7:48:11, time: 1.281, data_time: 0.026, memory: 9804, losses/ttfnet_loss_heatmap: 3.9698, losses/ttfnet_loss_wh: 3.2906, loss: 7.2604
2019-10-25 04:30:31,479 - INFO - Epoch [1][400/7330]	lr: 0.00168, eta: 1 day, 7:42:44, time: 1.283, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.8613, losses/ttfnet_loss_wh: 3.1832, loss: 7.0445
2019-10-25 04:31:35,502 - INFO - Epoch [1][450/7330]	lr: 0.00184, eta: 1 day, 7:37:51, time: 1.280, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.8858, losses/ttfnet_loss_wh: 3.1491, loss: 7.0349
2019-10-25 04:32:39,829 - INFO - Epoch [1][500/7330]	lr: 0.00200, eta: 1 day, 7:34:37, time: 1.287, data_time: 0.026, memory: 9804, losses/ttfnet_loss_heatmap: 3.7939, losses/ttfnet_loss_wh: 2.9790, loss: 6.7729
2019-10-25 04:33:43,913 - INFO - Epoch [1][550/7330]	lr: 0.00200, eta: 1 day, 7:31:09, time: 1.282, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.7948, losses/ttfnet_loss_wh: 2.8774, loss: 6.6722
2019-10-25 04:34:47,898 - INFO - Epoch [1][600/7330]	lr: 0.00200, eta: 1 day, 7:27:49, time: 1.280, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.7696, losses/ttfnet_loss_wh: 2.7948, loss: 6.5644
2019-10-25 04:35:52,258 - INFO - Epoch [1][650/7330]	lr: 0.00200, eta: 1 day, 7:25:41, time: 1.287, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.7014, losses/ttfnet_loss_wh: 2.7241, loss: 6.4255
2019-10-25 04:36:56,718 - INFO - Epoch [1][700/7330]	lr: 0.00200, eta: 1 day, 7:23:55, time: 1.289, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.6163, losses/ttfnet_loss_wh: 2.6678, loss: 6.2842
2019-10-25 04:38:01,054 - INFO - Epoch [1][750/7330]	lr: 0.00200, eta: 1 day, 7:22:00, time: 1.287, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.6610, losses/ttfnet_loss_wh: 2.6458, loss: 6.3068
2019-10-25 04:39:05,227 - INFO - Epoch [1][800/7330]	lr: 0.00200, eta: 1 day, 7:19:53, time: 1.283, data_time: 0.025, memory: 9804, losses/ttfnet_loss_heatmap: 3.6560, losses/ttfnet_loss_wh: 2.6244, loss: 6.2804
・・・

おおよそ1日と8時間で学習が完了するようです。
Google Colaboratoryは最大で12時間までしか連続使用できないので、12時間以内に学習が終わってくれればベストだったのですが、
それでもこの環境で30時間強で学習が終わるのはなかなか良いです。

あとはPCの電源が落ちないようにして、ブラウザにオートリフレッシュを仕掛けて、
12時間に1回ランタイムを再起動すればOKです。

おわりに

学習が速い事にどれ位の価値があるのか、個人的には色々と疑問ではあるのですが、
非常に短時間で結果を出さないといけないとき(ちょっとした味見とか、締め切り直前のレポート対策とか)なんかに
使ってみるのはいかがでしょうか?

追記

デフォルトの設定では、4epochに1回しかmodelファイルが保存されないようです。
ttfnet_r18_1x.pyのcheckpoint_config = dict(interval=4)のところを編集し、
保存頻度を上げた方が、ランタイム再起動時の無駄が減ります。
またついでにimgs_per_gpuやlrを調整すれば、もう少し早く学習が完了すると思います。
modelファイルの保存時にsymlinkの作成でエラーで落ちてしまう場合がある様です。
これについては、mmcvのrunner.pyを編集して、mmcv.symlinkの所をコメントアウトすると解決します。

NVIDIA DALIを使ってPyTorchのDataIOを高速化する

はじめに

学習にとても時間のかかるDeepLearningですが、
計算している部分よりも、データの前処理などに時間がかかっているということはよくあります。
少しでも学習を早くするために実装レベルでいろいろな工夫がありますが、
このエントリーではNVIDIA DALIを使ってPyTorchのDataIOを高速化した際のメモを紹介します。

最初に結論

PyTorchのDataLoaderをうまく組み合わせるべし

DALIとは?

NVIDIAが開発したライブラリで、データの前処理(augmentationなど)をGPU側に回すことが可能となります。
またメジャーなフレームワークとの連携用APIを提供しているため、
簡単に試すことができます。

以下などで例が紹介されています。
xvideos.hatenablog.com

DALIをPyTorchで使うには?

公式に丁寧なサンプルがあります。
https://github.com/NVIDIA/DALI/blob/master/docs/examples/pytorch/pytorch-external_input.ipynb

基本的にはこれに従うだけですが、
このままじゃ状況によってちょっと遅くなる時があります。
 
 

まずはDataLoaderそのままの場合

ms-cocoのデータセットを使った場合以下の様な実装になります。

class CocoDataset(Dataset):

    def __init__(self, dataType='val2017'):

        annFile='/content/annotations/instances_{}.json'.format(dataType)
        self.coco=COCO(annFile)
        self.ids = list(self.coco.imgToAnns.keys())
        self.imgs = self.coco.loadImgs(self.ids)
            
    def __len__(self):
        return len(self.ids)

    def __getitem__(self, idx):
      
        img_path = os.path.join('/content/val2017', self.imgs[idx]['file_name'])
        time.sleep(DATA_LOAD_TIME) #for simulation of very slow file IO
        img = cv2.imread(img_path)
        img = cv2.resize(img, (512,512))
        return img

def collate_fn(batch):
    imgs = [x for x in batch]
    return imgs

coco_dataset = CocoDataset()
coco_dataloader = DataLoader(coco_dataset, num_workers=8, batch_size=BATCH_SIZE, collate_fn=collate_fn)

途中

time.sleep(DATA_LOAD_TIME) #for simulation of very slow file IO

という怪しい部分がありますが、これは画像サイズがもっと大きいなど不幸なことが起きた時に
データ読み込みに10msくらいかかる場合をシミュレートしています。

これを使って速度をはかりました。

data_iter = iter(coco_dataloader)

start = time.time()
for _ in range(100):
  images = next(data_iter)
  time.sleep(LEARNING_TIME) #for simulation of DeepLearning
end = time.time()
print('total_time : %s' % ( str(end-start) ))

total_time : 11.102628707885742
(今回の設定ではDATA_LOAD_TIME=0.01 LEARNING_TIME=0.1)

10秒はtime.sleep(LEARNING_TIME)に取られているので、実質IOのところは1.1秒くらいです。
このままでも十分早いですね。
pytorchのDataLoaderはいい感じにパイプライン処理してくれるため、
学習の処理が行われている裏でこっそりデータを準備してくれます。
そのため、データIOの時間が少なく見えるようになっています。
 
 

DALI公式のサンプルに従う

DALIではデータIO処理を行うpipelineと、pipelineにデータを供給するiteratorを定義して、
DALIGenericIteratorに渡して制御してもらいます。

class ExternalInputIterator(object):
    def __init__(self, batch_size, dataType='val2017'):

        annFile='/content/annotations/instances_{}.json'.format(dataType)
        self.coco=COCO(annFile)
        self.ids = list(self.coco.imgToAnns.keys())
        self.imgs = self.coco.loadImgs(self.ids)

        self.batch_size = batch_size
        self.data_set_len = len(self.imgs) 
        self.n = len(self.imgs)

    def __iter__(self):
        self.i = 0
        return self

    def __next__(self):
        batch = []

        if self.i >= self.n:
            raise StopIteration

        for _ in range(self.batch_size):
            img_path = os.path.join('/content/val2017', self.imgs[self.i]['file_name'])
            f = open(img_path, 'rb')
            time.sleep(DATA_LOAD_TIME) #for simulation of very slow file IO
            batch.append(np.frombuffer(f.read(), dtype = np.uint8))
            self.i = (self.i + 1) % self.n
        return batch

    @property
    def size(self,):
        return self.data_set_len

    next = __next__

class ExternalSourcePipeline(Pipeline):
    def __init__(self, batch_size, num_threads, device_id, external_data):
        super(ExternalSourcePipeline, self).__init__(batch_size,
                                      num_threads,
                                      device_id,
                                      seed=12)
        self.input = ops.ExternalSource()
        self.decode = ops.ImageDecoder(device = "mixed", output_type = types.RGB)
        self.res = ops.Resize(device="gpu", resize_x=512, resize_y=512)
        self.external_data = external_data
        self.iterator = iter(self.external_data)

    def define_graph(self):
        self.jpegs = self.input()
        images = self.decode(self.jpegs)
        images = self.res(images)
        return images

    def iter_setup(self):
        try:
            images = self.iterator.next()
            self.feed_input(self.jpegs, images)
        except StopIteration:
            self.iterator = iter(self.external_data)
            raise StopIteration

from nvidia.dali.plugin.pytorch import DALIGenericIterator

eii = ExternalInputIterator(batch_size=BATCH_SIZE)
pipe = ExternalSourcePipeline(batch_size=BATCH_SIZE, num_threads=2, device_id = 0,
                              external_data = eii)
pii = DALIGenericIterator(pipe, ['image'], size=100*BATCH_SIZE, last_batch_padded=True, fill_last_batch=False)

start = time.time()
for i, data in enumerate(pii):
    images = data[0]["image"]
    time.sleep(LEARNING_TIME) #for simulation of DeepLearning
end = time.time()

print('total_time : %s' % ( str(end-start) ))

公式の実装ほぼそのままです。
結果は
total_time : 14.16463017463684
遅くなった!?
 
 

何が問題なのか

DALIを使うとデコード処理やそのあとのResizeなど確かに高速に動きます。
しかし、pytorchのDataLoaderが持っていたパイプライン機能はありません。
そのため、最初のデータ読み込みに時間がかかってしまう場合、逆に遅くなってしまいます。
データはSSDなど処理の速いデバイスに保存し、TFRecordなどで固めて読み込めばこの問題は解決しますが、
そうも言ってられない場合もあるでしょう。
そこで、下記の様に実装を変更しました。
 
 

PyTorch DataLoader × DALI

まず最初にデータの読み込みだけ行うpytorchのDataLoaderを用意します。

class CocoDatasetForDALI(Dataset):

    def __init__(self, dataType='val2017'):

        annFile='/content/annotations/instances_{}.json'.format(dataType)
        self.coco=COCO(annFile)
        self.ids = list(self.coco.imgToAnns.keys())
        self.imgs = self.coco.loadImgs(self.ids)
            
    def __len__(self):
        return len(self.ids)

    def __getitem__(self, idx):
      
        img_path = os.path.join('/content/val2017', self.imgs[idx]['file_name'])
        f = open(img_path, 'rb')
        time.sleep(DATA_LOAD_TIME) #for simulation of very slow file IO
        img = np.frombuffer(f.read(), dtype = np.uint8)
        return img

次にpipline内でpytorchのdataloaderからデータを取得するようにします。

class ExternalSourcePipelineForPytorch(Pipeline):
    def __init__(self, batch_size, num_threads, device_id, external_data):
        super(ExternalSourcePipelineForPytorch, self).__init__(batch_size,
                                      num_threads,
                                      device_id,
                                      seed=12)
        self.input = ops.ExternalSource()
        self.decode = ops.ImageDecoder(device = "mixed", output_type = types.RGB)
        self.res = ops.Resize(device="gpu", resize_x=512, resize_y=512)
        self.external_data = external_data
        self.iterator = iter(self.external_data)

    def define_graph(self):
        self.jpegs = self.input()
        images = self.decode(self.jpegs)
        images = self.res(images)
        return images

    def iter_setup(self):
        try:
            images = next(self.iterator)
            self.feed_input(self.jpegs, images)
        except StopIteration:
            self.iterator = iter(self.external_data)
            raise StopIteration

これを使って速度をはかってみます。

coco_dataset_for_dali = CocoDatasetForDALI()
coco_dataloader_for_dali = DataLoader(coco_dataset_for_dali, num_workers=8, batch_size=BATCH_SIZE, collate_fn=collate_fn)

pipe = ExternalSourcePipelineForPytorch(batch_size=BATCH_SIZE, num_threads=2, device_id = 0,
                              external_data = coco_dataloader_for_dali)
pii = DALIGenericIterator(pipe, ['image'], size=100*BATCH_SIZE, last_batch_padded=True, fill_last_batch=False)

start = time.time()
for i, data in enumerate(pii):
    images = data[0]["image"]
    time.sleep(LEARNING_TIME) #for simulation of DeepLearning
end = time.time()

print('total_time : %s' % ( str(end-start) ))

total_time : 10.300386428833008

速くなった!
実質データIOにかかっているのは0.3秒ということになりますので、
元の3~4倍くらいには早くなります。
data augmentationをもっとリッチにした場合などを考えると、結構使えるんではないでしょうか?
 
 

おわりに

pytorchにDALIを組み合わせる場合、データ読み込みに時間がかかるような環境の場合は、
pytorchのDataLoaderをうまく使うと良さそうです。
折角DALIにしたのに遅くなったという方は、ぜひ試してみてください。

Google Colaboratoryで確認した際のソースを下記に公開しています。
良ければお試しください。
https://github.com/yoneken1/colab_pytorch_sample/blob/master/pytorch_dali.ipynb

TensorflowのBatchNormalizationは難しい

はじめに

久しぶりにTensorflowをいじっていて、BatchNormalizationの挙動を確認した際の備忘録です。
実際の挙動確認や理論的なお話は下記によくまとまっています。
qiita.com
qiita.com

本ブログでは、ダメな実装について紹介したいと思います。

ダメな例

色々調べていると、正しく動く例はたくさん出てくるのですが、
当然といえば当然なのですが、ダメな例は殆ど紹介されていません。
早速ですが、正しく動作しない例を紹介します。

Keras API + カスタムEstimator

import tensorflow as tf

#define original model
def model_fn(features, labels, mode):
  
  training = (mode == tf.estimator.ModeKeys.TRAIN)
  x = tf.keras.layers.BatchNormalization()(features,training=training)
  y = tf.keras.layers.Dense(1)(x)
  predictions = {
      "prob": y,
  }
  
  if mode == tf.estimator.ModeKeys.PREDICT:
    return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

  loss = tf.reduce_mean(tf.losses.mean_squared_error(labels,y))

  if mode == tf.estimator.ModeKeys.TRAIN:
      optimizer = tf.train.AdamOptimizer(learning_rate=0.001)
     
      # set batch normalization parameters to train
      update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
      with tf.control_dependencies(update_ops):
        train_op = optimizer.minimize(loss, global_step=tf.train.get_global_step())

      return tf.estimator.EstimatorSpec(
          mode=tf.estimator.ModeKeys.TRAIN,
          loss=loss,
          train_op=train_op)
    
  return tf.estimator.EstimatorSpec(
      mode=mode, loss=loss)
  
# create estimator
estimator = tf.estimator.Estimator(
    model_fn=model_fn)

input_fn = lambda:(tf.constant([[0], [1], [2], [3]], dtype=tf.float32),tf.constant([[0], [-1], [-2], [-3]], dtype=tf.float32))

estimator.train(input_fn,steps=5000)
result = estimator.evaluate(input_fn,steps=4)

この例は、最新のKeras APIであるtf.keras.layers.BatchNormalizationを使っています。
そしてCustomEstimatorを作って学習しています。
実際に動かしてみると、evaluateのlossがtrainとはかけ離れた値になります。
どうやら、BatchNormalizationが学習されていないようです。
問題の部分は下記です。

      update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
      with tf.control_dependencies(update_ops):
        train_op = optimizer.minimize(loss, global_step=tf.train.get_global_step())

この部分で、BatchNormalizationのパラメータを学習するように指定しているつもりなのですが、
実はできていないようです。
そして、正しく指定する方法がよくわかりませんでした。
ついつい、上記の様な実装をしてしまいがちですが、避けたほうが良いようです。

正しく動作する例

1. tf.layers.BatchNormalziation + カスタムEstimator

BatchNormalizationをkeras API ではなく、layersから持ってきます。
最初の実装の該当部分を以下に変えるだけでOKです。

 x = tf.layers.BatchNormalization()(features,training=training)

この場合は with tf.get_collection(tf.GraphKeys.UPDATE_OPS)によって
BatchNormalizationのパラメータが正しく指定されます。

2. tf.keras.layers.BatchNormaliztion + model_to_estimator

EstimatorのCustomを諦めれば簡単に実装できます。

import tensorflow as tf

inputs = tf.keras.Input(shape=(1,))
x = tf.keras.layers.BatchNormalization()(inputs)
outputs = tf.keras.layers.Dense(1)(x)

# create Keras Model
model = tf.keras.Model(inputs=inputs, outputs=outputs)
model.compile(optimizer='adam',
              loss='mse',
              metrics=['accuracy'])

# convert to estimator
estimator = tf.keras.estimator.model_to_estimator(model)

input_fn = lambda:(tf.constant([[0], [1], [2], [3]], dtype=tf.float32),tf.constant([[0], [-1], [-2], [-3]], dtype=tf.float32))

estimator.train(input_fn,steps=5000)
result = estimator.evaluate(input_fn,steps=4)
print(result)

おそらく公式の推奨はこれかと思われます。
記述量も短くてわかりやすいですね。

3. (おまけ) Estimatorを使わない

import tensorflow as tf
import numpy as np

inputs = tf.placeholder(shape=[None,1], dtype=tf.float32)
labels = tf.placeholder(shape=[None,1], dtype=tf.float32)
training = tf.placeholder(shape=[], dtype=tf.bool)

with tf.variable_scope('batch_normalization_test'):
  with tf.name_scope('for_train'):
    BN1 =  tf.layers.BatchNormalization(name="bn1")
    x = BN1(inputs,training =training)
    x = tf.layers.Dense(units=1,name="dense1")(x)
    BN2 =  tf.layers.BatchNormalization(name="bn2")
    x = BN2(x,training =training)
    output1 = tf.layers.Dense(units=1,name="dense2")(x)

with tf.variable_scope('batch_normalization_test', reuse=True):
  with tf.name_scope('for_test'):
    BN1 =  tf.layers.BatchNormalization(name="bn1")
    x = BN1(inputs,training =training)
    x = tf.layers.Dense(units=1,name="dense1")(x)
    BN2 =  tf.layers.BatchNormalization(name="bn2")
    x = BN2(x,training =training)
    output2 = tf.layers.Dense(units=1,name="dense2")(x)

loss = tf.reduce_mean(tf.losses.mean_squared_error(labels,output1))
optimizer = tf.train.AdamOptimizer(learning_rate=0.001)
update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
grads = optimizer.compute_gradients(loss)
with tf.control_dependencies(update_ops):
  train_op = optimizer.apply_gradients(grads)

sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)

train_x = np.array([[-3.],[-2.],[-1.],[0.]])
train_y = np.array([[0.], [1.], [2.], [3.]])

for _ in range(5000):
  sess.run((loss,train_op,output1,output2),{inputs:train_x,labels:train_y,training:True})
#  print(sess.run(BN1.weights))
#  print(sess.run(BN2.weights))

print(sess.run((loss,output1,output2),{inputs:train_x,labels:train_y,training:True}))
print(sess.run((output1),{inputs:train_x,training:False}))
print(sess.run((output2),{inputs:train_x,training:False}))

途中いろいろ余計な部分がありますが、上記のような古いスタイルで問題なく動きます。
Keras API と Estimatorを諦めた方が分かりやすくなるのは気のせいでしょうか。

おわりに

Keras APIのBatchNormalizationを使うときは注意が必要です。
ドキュメントではあまり触れられていませんし、本家Issueでも投げやりな対応です。
(TF2.0が来るから?)
少し試す分にはKeras APIは便利ですが、
研究用途でがっつり使いたい場合は避けた方がよいかもしれません。

Google Colaboratoryで確認した際のソースを下記に公開しています。
良ければお試しください。
https://github.com/yoneken1/colab_tensorflow/blob/master/tensorflow_batch_normalization.ipynb

Google Colaboratory + pytorch で SIGNATEコンペ参加(開発編3)

引き続きGoogle Colaboratory + pytorch で SIGNATEの
AIエッジコンテスト(オブジェクト検出)に参加するお話です。
前回までで、モデル、Loss関数など学習に必要な部分を揃えました。
後は実際にオブジェクト検出を行い、正解と比較して評価を行う部分を作れば完成です。

オブジェクト検出処理

今回オブジェクト検出処理用にpredictという関数を用意しました。
この関数ではネットワークにより計算されたlocとclassを矩形に変換し、
リストにして出力します。
ただし、batchsizeは1を前提としています。

def predict(image, score_th=0.7, nms_th=0.3, class_agnostic=False):
  
  image = image.to(device)
  pred_loc,pred_cls,anchor = net(image)
  anchor = anchor.to(device)
  
  loc_normalize_std = torch.tensor([0.1,0.1,0.2,0.2]).to(device)
  
  pred_cls = torch.nn.functional.softmax(pred_cls,2)
  pred_loc_batch = pred_loc[0]

  detect_box_list = []
  detect_score_list = []
  detect_label_list = []
  
  num_class = pred_cls.size(2) - 1
  
  _classes = ('Car','Truck','Pedestrian',
                   'Bicycle','Signal','Signs')
  
  for class_idx in range(num_class):
    if(class_agnostic):
      pred_cls_loc = pred_loc_batch.detach()
    else:
      pred_cls_loc = pred_loc_batch[:,class_idx*4:(class_idx+1)*4].detach()
    pred_cls_loc = pred_cls_loc * loc_normalize_std
    if _classes[class_idx] in size_expansion:
      sw = 1./size_expansion[_classes[class_idx]][0]
      sh = 1./size_expansion[_classes[class_idx]][1]
      pred_cls_loc[:,2:] = pred_cls_loc[:,2:] * torch.tensor([sw, sh]).to(device)

    pred_box = loc2box(pred_cls_loc.detach(),anchor.detach())

    detect_idx = (pred_cls[0][:,class_idx+1] >= score_th) 
    detect_box = pred_box[detect_idx]
    detect_score = pred_cls[0][detect_idx,class_idx+1]
    if(detect_box.size(0) > 0):
      detect_box[:,0::2].clamp_(min=0,max=image.size(3)-1)
      detect_box[:,1::2].clamp_(min=0,max=image.size(2)-1)

      keep = box_nms(detect_box,detect_score,threshold=nms_th)
      
      detect_box_list.append(detect_box[keep])
      detect_score_list.append(detect_score[keep])
      detect_label_list.append(torch.Tensor(keep.size(0)).fill_(class_idx).to(device))

  if(len(detect_box_list)):
    detect_box_list = torch.cat(detect_box_list,0)
    detect_score_list = torch.cat(detect_score_list,0)
    detect_label_list = torch.cat(detect_label_list,0)
    
  return detect_box_list, detect_score_list, detect_label_list

そしてこの関数で得られた結果を、今回のコンペで評価できる形に変換します。

result_data = OrderedDict()
_classes = ('Car','Truck','Pedestrian',
                   'Bicycle','Signal','Signs')
_, order = torch.sort(detect_score_list, dim=0, descending=True)
  
order_detect_box_list = ((detect_box_list * scale + 0.5).int())[order]
order_detect_label_list = detect_label_list[order]

for i in range(order.size(0)):
    idx = order[i]
    label_name = _classes[int(detect_label_list[idx])]
    if(not (label_name in result_data)):
      result_data[label_name] = []
    if(len(result_data[label_name]) < 100):
      x1 = out_detect_box_list[idx,0]
      y1 = out_detect_box_list[idx,1]
      x2 = out_detect_box_list[idx,2]
      y2 = out_detect_box_list[idx,3]
      result_data[label_name].append([x1,y1,x2,y2])

mAPの評価関数等はSIGNATEであらかじめ用意されていたものをそのまま使いました。

以上で学習されたモデルを使って、評価を行うことができました。
実際に出来上がったソースを使って一旦投稿してみたところ、
当然ですが、最下位に近い成績でした。。。
mAP 0.07位だったと思います。
ただ、この時点でコンペに参加するという目標は達成できました。

オブジェクト検出処理の改善

さて、上にあげた検出処理ですが2つの点で問題があり、検出処理に5秒/1フレームほどかかってしまい、
学習の大半がTESTの時間になってしまうという事態になりました。
そこで、少しだけ処理速度の改善を行ったので紹介いたします。

処理結果の変換処理

上で上げたソースでは、for文を使ってデータ形式を変換していますが、
pythonのfor文はとても遅く、ボトルネックとなっていました。
そこで、以下の様に変更し、pytorchにできるだけ計算を頑張ってもらうようにし
処理速度を上げました。

  result_data = OrderedDict()
  _classes = ('Car','Truck','Pedestrian',
                   'Bicycle','Signal','Signs')
  _, order = torch.sort(detect_score_list, dim=0, descending=True)
  
  order_detect_box_list = ((detect_box_list * scale + 0.5).int())[order]
  order_detect_label_list = detect_label_list[order]
  for label_idx, label_name in enumerate(_classes):
    label_ids = (order_detect_label_list == label_idx)
    label_box_list = order_detect_box_list[label_ids][:100].cpu().numpy().tolist()
    if len(label_box_list) > 0:
      result_data[label_name] = label_box_list
Non Maximum Suppression

上のソースではさらっと無視していましたが、
重なって検出された矩形たちをまとめる処理としてNon Maximum Suppressionを使っています。
ここでは、以下の様なCPUベースの処理を使っています。
この実装はpytorchのSSDやRetinanetの実装などにあったものをそのままお借りしています。

def box_nms(bboxes, scores, threshold=0.3, mode='union'):
    '''Non maximum suppression.
    Args:
      bboxes: (tensor) bounding boxes, sized [N,4].
      scores: (tensor) bbox scores, sized [N,].
      threshold: (float) overlap threshold.
      mode: (str) 'union' or 'min'.
    Returns:
      keep: (tensor) selected indices.
    Reference:
      https://github.com/rbgirshick/py-faster-rcnn/blob/master/lib/nms/py_cpu_nms.py
    '''
    x1 = bboxes[:,0]
    y1 = bboxes[:,1]
    x2 = bboxes[:,2]
    y2 = bboxes[:,3]

    areas = (x2-x1+1) * (y2-y1+1)
    _, order = scores.sort(0, descending=True)

    keep = []
    while order.numel() > 0:
        i = order[0]
        keep.append(i)
        
        if order.numel() == 1:
            break

        xx1 = x1[order[1:]].clamp(min=x1[i])
        yy1 = y1[order[1:]].clamp(min=y1[i])
        xx2 = x2[order[1:]].clamp(max=x2[i])
        yy2 = y2[order[1:]].clamp(max=y2[i])

        w = (xx2-xx1+1).clamp(min=0)
        h = (yy2-yy1+1).clamp(min=0)
        inter = w*h

        if mode == 'union':
            ovr = inter / (areas[i] + areas[order[1:]] - inter)
        elif mode == 'min':
            ovr = inter / areas[order[1:]].clamp(max=areas[i])
        else:
            raise TypeError('Unknown nms mode: %s.' % mode)

        ids = (ovr<=threshold).nonzero().view(-1)
        if ids.numel() == 0:
            break
        order = order[ids+1]
    return torch.LongTensor(keep)

今回はGoogle Colaboratoryで処理を行うので、CUDA周りの設定はいろいろ面倒と考え、
これでよいだろうと思っていたのですが、やはり無視できない遅さでしたので、
cupyベースのNMSを使うことにしました。
ソースは以下からお借りしました。
github.com
nmsの部分をあらかじめGoogle Driveの中に仕込んで置き、
以下を実行することで、ビルドを行います。

#setup nms_cupy
!mkdir -p model/utils/
!cp -r '/content/gdrive/My Drive/colab_pytorch_detection/nms' /content/model/utils/
%cd /content/model/utils/nms/
!python build.py build_ext --inplace
%cd /content

build.pyは元のままだとビルドできないので、
numpy周りの参照関係を修正しておきます。

そして、cupy用のbox_nms関数を用意します。

import cupy as cp
from model.utils.nms import non_maximum_suppression

def box_nms_cupy(bboxes, scores, threshold=0.3):
  boxes_np = bboxes.detach().cpu().numpy()
  prob_np = scores.detach().cpu().numpy()
  
  keep = non_maximum_suppression(
                  cp.array(boxes_np), threshold, prob_np)
  keep = torch.tensor(cp.asnumpy(keep)).long()
  return keep

これを最初のソースのbox_nmsの代わりに使えばOKです。

以上2点を直すことで、処理速度が5fpsくらい出るようになり、
待ち時間を大幅に減らすことができました。

長くなったので今回はここまで。
次は学習処理について書こうと思います。

Google Colaboratory + pytorch で SIGNATEコンペ参加(開発編2)

引き続きGoogle Colaboratory + pytorch で SIGNATEの
AIエッジコンテスト(オブジェクト検出)に参加するお話です。

開発項目おさらい

前回のエントリーで、開発項目を幾つかあげました。
それぞれ紹介するつもりでしたが、あまりGoogle Colaboratoryと関係ない一般的なものについては
省略することにしました。
代わりにGoogle Colaboratoryの機能を使ってGitHubにnotebookをアップロードしましたので、
そのリンクを紹介いたします。

  • Box変換系関数群 、学習データ選択関数

https://github.com/yoneken1/colab_pytorch_detection/blob/master/roi_util.ipynb

  • Anchor

https://github.com/yoneken1/colab_pytorch_detection/blob/master/Anchor.ipynb

  • Dataset

後程紹介します。

  • Loss

https://github.com/yoneken1/colab_pytorch_detection/blob/master/Loss.ipynb

  • Model

前回紹介済み。

次のエントリーで紹介します。

Dataset

pytorchではDatasetクラスを継承してデータ読み込み・変換部を作り、
作成したDatasetオブジェクトをDatalodarに渡すことで、
効率よく読み込んでくれるようになります。
Data Loading and Processing Tutorial — PyTorch Tutorials 1.0.0.dev20190104 documentation
今回はチュートリアルに従って、Signateコンペ用のデータセットにアクセスするクラスを作成します。

Google Driveのマウント

おさらいになりますが、Google ColaboratoryからGoogle Driveにアクセスするために、
ランタイムに接続後、以下を実行する必要があります。

from google.colab import drive
drive.mount('/content/gdrive')

データの準備

必要なデータはGoogle Driveにすべてあげておきます。
今回は下記の様なデータ構造にしてデータを置いています。

/content/gdrive/My Drive/colab_pytorch_detection/data
 /annotations
  /dtc_train_annotations
   train_00000.json
   ・・・
  train.txt
  val.txt
 /dtc_train_images_res
   train_00000.jpg
   ・・・
 /dtc_test_images_res
   test_00000.jpg
   ・・・

dtc_train_annotationsはコンペサイトから落としてきたものを解凍したものです。
dtc_train_images_resとdtc_test_images_resは元データを縦横1/2に縮小したものです。

本コンペでは、validationデータが無かったので、trainデータを9:1の割合でtrainとvalに分けました。
分けたファイルのリストがtrain.txtおよびval.txtです。
それぞれ、以下の様なソースで簡単に作れます。
折角なので、pytorchで作ってみました。

def create_trainval_list():
  
  ROOT_DIR='/content/gdrive/My Drive/colab_pytorch_detection/data'
  ANNO_DIR = os.path.join(ROOT_DIR,'annotations','dtc_train_annotations')
  TRAIN_FILE = os.path.join(ROOT_DIR,'annotations','train.txt')
  VAL_FILE = os.path.join(ROOT_DIR,'annotations','val.txt')
  TRAIN_RATIO = 0.9

  anno = os.listdir(ANNO_DIR)
  anno = sorted(anno)
  
  ids = torch.randperm(len(anno))
  print(ids)
  train_num = int(len(anno) * TRAIN_RATIO)

  train_ids = ids[:train_num]
  val_ids = ids[train_num:]
  
  print(train_ids.size())
  print(val_ids.size())

  with open(TRAIN_FILE, 'w') as f:
    for i in train_ids:
      f.write(anno[i]+'\n')
      
  with open(VAL_FILE, 'w') as f:
    for i in val_ids:
      f.write(anno[i]+'\n')
  
create_trainval_list()

Datasetクラス

冗長な部分があって少々格好悪いですが、以下の様なソースになりました。
targetによってtrain,val,trainval,testを切り替えます。

class SignateDataset(Dataset):

    def __init__(self, root_dir='/content/gdrive/My Drive/colab_pytorch_detection/data', target='trainval', transform=None):
        self.root_dir = root_dir
        self.transform = transform
        self.target = target
        
        self.annotation_dir = os.path.join(root_dir,'annotations','dtc_train_annotations')
        if target=='train':
          self.img_dir = os.path.join(root_dir,'dtc_train_images_res')
          read_file = os.path.join(root_dir,'annotations','train.txt')
          self.anno_list = []
          with open(read_file,'r') as f:
            self.anno_list = [fname.strip() for fname in f]
            self.anno_list = sorted(self.anno_list)
          self.img_list = [os.path.splitext(fname)[0]+".jpg" for fname in self.anno_list]
          

        elif target=='val':
          self.img_dir = os.path.join(root_dir,'dtc_train_images_res')
          read_file = os.path.join(root_dir,'annotations','val.txt')
          self.anno_list = []
          with open(read_file,'r') as f:
            self.anno_list = [fname.strip() for fname in f]
            self.anno_list = sorted(self.anno_list)
          self.img_list = [os.path.splitext(fname)[0]+".jpg" for fname in self.anno_list]

        elif target=='test':
          self.img_dir = os.path.join(root_dir,'dtc_test_images_res')
          self.anno_list = None        
          self.img_list =  sorted(os.listdir(self.img_dir))
          
        else:
          self.img_dir = os.path.join(root_dir,'dtc_train_images_res')
          self.anno_list = sorted(os.listdir(self.annotation_dir))
          self.img_list =  sorted(os.listdir(self.img_dir))
          
        
        self._classes = ('Car','Truck','Pedestrian',
                   'Bicycle','Signal','Signs')
#        self._classes = ('Car','Bus','Truck','SVehicle','Pedestrian','Motorbike',
#                   'Bicycle','Train','Signal','Signs')
        
        self.boxes_list = [None] * len(self.img_list)
        self.labels_list = [None] * len(self.img_list)
        self.scale = 0.5
            
    def __len__(self):
        return len(self.img_list)

    def __getitem__(self, idx):
      
      if((self.boxes_list[idx] is None) and (self.anno_list is not None)):
        anno_file_path = os.path.join(self.annotation_dir,self.anno_list[idx])
        with open(anno_file_path) as f:
          data = json.load(f)
          boxes = []
          lbls = []
          if 'labels' in data:
            labels = data['labels']
            for label in labels:
              if 'box2d' in label:
                if label['category'] in self._classes:
                  x1 = float(label['box2d']['x1']) * self.scale
                  x2 = float(label['box2d']['x2']) * self.scale
                  y1 = float(label['box2d']['y1']) * self.scale
                  y2 = float(label['box2d']['y2']) * self.scale
                  category=self._classes.index(label['category'])
                  boxes.append(torch.tensor([x1,y1,x2,y2]).float())
                  lbls.append(torch.tensor([category]).long())

          self.boxes_list[idx] = torch.stack(boxes)
          self.labels_list[idx] = torch.stack(lbls)

      
      img_path = os.path.join(self.img_dir, self.img_list[idx])
      image = io.imread(img_path)
         
      sample = {'image': image, 'boxes': self.boxes_list[idx], 'labels':self.labels_list[idx]}

      if self.transform:
          sample = self.transform(sample)
      
      return sample
    
    def collate_fn(self, batch):
      imgs = [x['image'] for x in batch]
      boxes = [x['boxes'] for x in batch]
      labels = [x['labels'] for x in batch]
      
      return {'image': torch.stack(imgs), 'boxes': boxes, 'labels':labels}

ポイントは、__getitem__の中でファイルを読み込んでいる部分です。
普段Datasetクラスを作るときは、__init__内でデータを展開してしまう事が多いのですが、
Google Driveへのアクセス速度が非常にネックになり、
学習を開始するまでに延々待たされてしまいます。
そこで今回は__getitem__内で1ファイルずつ読み込むことにしました。
こうすることで、pytorchが勝手にパイプライン化してくれるため、
学習処理をしている間に並列してファイルを読み込みを行うことができ、
時間短縮になります。

datasetの使用例は以下の様になります。

dataset = SignateDataset(transform=train_transform,target='train')
datalodar = DataLoader(dataset, batch_size=BATCH_SIZE,
                        shuffle=True, num_workers=8,collate_fn=dataset.collate_fn)

for i_batch, sample_batched in enumerate(datalodar ):
      
      image = sample_batched['image']
      boxes = sample_batched['boxes']
      labels = sample_batched['labels']
      # 学習処理とか

Datasetについてはここまで。
後は評価スクリプトと学習スクリプトを書けば、一旦完成です。

Google Colaboratory + pytorch で SIGNATEコンペ参加(開発編1)

引き続きGoogle Colaboratory + pytorch で SIGNATEのAIエッジコンテストに参加するお話です。

どんなオブジェクト検出アルゴリズムを使うか

オブジェクト検出といえば古くはHaar+AdaboostやHoG+SVMなど
ハンドメイド系の特徴量を使うものが基本でしたが、
近年ではDNNベースのものが主流となっています。
今回はDNNベースのアルゴリズムを超シンプルにしたものからスタートしていきます。

DNNベースのアルゴリズムといえば、Faster-RCNNやSSD、YOLOなどが有名です。
下記の記事あたりによくまとまっています。
qiita.com
qiita.com

今回は家で子育てしながら開発するので、コード量が多いモデルは時間的に厳しいです。
そのため、いわゆるSingle Stageタイプのモデルを作ることにしました。
Single StageタイプのモデルはSSDやYOLOが有名ですが、
個人的にはFaster-RCNNのRPN(Region Proposal Network)が最もイメージしやすいと思います。
簡単に言うと、画像全体に確認用の矩形(Anchor)をばらまいて、
それぞれの矩形に物体があるかないかをチェックして物体を検出する方法です。
RPNについては下記の記事の解説が分かりやすいと思います。
nonbiri-tereka.hatenablog.com

開発項目

pytorchに標準搭載のものについてはそのまま利用し、
学習に必要な関数などをそろえていきます。

  • Box変換系関数群
  • Anchor
  • Dataset
  • 学習データ選択関数
  • Loss
  • Model
  • 評価スクリプト

Model

今回作るモデルは、おおよそ下の図の様な構成です。

f:id:Yoneken1:20181230005143p:plain
モデルのイメージ
青い箱がConvolution(+活性化など)のイメージです。

これをpytorchで書くと、ソースコードは以下の通りです。(importなどは省略)

class GCDet(nn.Module):
  def __init__(self, num_classes):
    super(GCDet, self).__init__()
    self.num_classes = num_classes
    base_model = squeezenet1_1(pretrained=True)
    self.features = base_model.features
    self.anchor_gen = Anchor(scale_ratios = [1/2., 1., 1.5])
    
    for p in self.features[0].parameters():
          p.requires_grad = False 
    for p in self.features[3].parameters():
          p.requires_grad = False 
        
    num_anchors = self.anchor_gen.num_anchors
        
    self.conv1 = nn.Conv2d(512, 512, 3, 1, 1)
    self.relu1 = nn.ReLU()
    self.loc = nn.Conv2d(512, num_anchors * 4 * num_classes, 1, 1, 0)
    self.cls = nn.Conv2d(512, num_anchors * (num_classes+1), 1, 1, 0)
    
    nn.init.kaiming_normal_(self.conv1.weight, mode='fan_out', nonlinearity='relu')
    self.conv1.bias.data.zero_()
    self.loc.weight.data.normal_(0.0, 0.001)
    self.loc.bias.data.zero_()
    self.cls.weight.data.normal_(0.0, 0.01)
    self.cls.bias.data.zero_()
    
  def forward(self,x):
    fm = self.features(x)
    h = self.relu1(self.conv1(fm))
    pred_loc = self.loc(h)
    pred_cls = self.cls(h)
    pred_loc = pred_loc.permute(0, 2, 3, 1).contiguous().view(pred_loc.size(0), -1, 4 * self.num_classes)
    pred_cls = pred_cls.permute(0, 2, 3, 1).contiguous().view(pred_cls.size(0), -1, self.num_classes+1)
    
    fm_size = fm.size()
    anchor = self.anchor_gen.get_anchor_boxes([fm_size])
    
    return pred_loc,pred_cls,anchor

GLDetというのはGoogleColaboratoryDetectorの略です。

今回は一旦特徴抽出用ネットワークにSqueezeNetを持ってきました。
これは学習を早くして、デバッグを効率よく行うためです。

Anchorというクラスが出てきますが、後程実装を紹介します。
物体があるかどうかチェックするための矩形リストのイメージです。

require_grad=Falseで、前半のConvolutionの学習を止めています。

    pred_loc = pred_loc.permute(0, 2, 3, 1).contiguous().view(pred_loc.size(0), -1, 4 * self.num_classes)
    pred_cls = pred_cls.permute(0, 2, 3, 1).contiguous().view(pred_cls.size(0), -1, self.num_classes+1)

の部分が若干ややこしいですが、
各convolutionのチャンネルに入っているデータを一番後ろにもってきて、
各Anchorの出現順に縦に並べているイメージです。
locやclsはそれぞれ検出対象の位置とクラス(人や車など)を意味しています。

以上でオブジェクト検出用のモデルは完成です。

長くなったので今回はここまで。