Pynote

Python、機械学習、画像処理について

Keras 実装で学ぶ YOLOv3 - その1 YOLOv3 ネットワークの概要とその実装

ネットワークの構造

YOLOv3 ネットワーク


  • ベースネットワークには Darknet-53 を使用する。
  • Fully Convolutional Network である。
  • 75層の畳み込み層、アップサンプリング層及び Shortcut Connection で構成される。
  • 各畳み込み層は畳み込み、Batch Normalization、活性化関数 Leaky ReLU の順番で構成される。
  • プーリングの代わりにストライド2の畳み込み層を使用する。

Darknet-53 ネットワーク


  • プーリング目的の畳み込み層と複数の Residual Block を繰り返す構造となっている。
  • 53 層の畳み込み層で構成される。(Darknet-53 の由来)

各 Residual Block は ResNet と同様のものである。

Darkent-53 は特徴抽出器 (Body) 及び分類器 (Head) から成るが、このうち、Body の部分を特徴抽出器として使用する。
Body 部分の畳み込み層の数は52層である。

ネットワークの実装

keras-yolo3 の yolo3/model.py をベースに YOLOv3 ネットワークを作成する実装について解説する。

必要なモジュールを import する。

from functools import reduce

import numpy as np
import tensorflow as tf
from keras.layers import (Add, BatchNormalization, Concatenate, Conv2D, Input,
                          LeakyReLU, MaxPooling2D, UpSampling2D, ZeroPadding2D)
from keras.models import Model
from keras.regularizers import l2

utils.compose() について

Keras の Functional API を使用する場合、複数の層を結合するときは以下の書き方をする。

# layer1 -> layer2 -> layer3
layer1 = Layer(パラメータ)
layer2 = Layer(パラメータ)(layer1)
layer3 = Layer(パラメータ)(layer2)

# これは以下と同値
layer3 = Layer(パラメータ)(Layer(パラメータ)(Layer(パラメータ)))

compose() は引数に結合する順番で Layer クラスを渡すと、結合したモデルを返してくれるヘルパー関数である。
仕組みについては Python - 関数の合成 を参照。

output = compose(layer1, layer2, layer3)
# output = layer3(layer2(layer1)) を返す。

1つの畳み込み層

DarknetConv2D_BN_Leaky() は、畳み込み層、Batch Normalization、活性化関数 Leaky ReLU の順番で結合し、1つの畳み込みを作成する関数である。

def DarknetConv2D(*args, **kwargs):
    # L2正則化 5e-4
    darknet_conv_kwargs = {'kernel_regularizer': l2(5e-4)}
    # ストライド2の場合はプーリング目的の畳み込みなので、
    # 入出力サイズを同じにする目的のパディングは行わない。
    darknet_conv_kwargs['padding'] = 'valid' if kwargs.get('strides') == (2, 2) else 'same'
    darknet_conv_kwargs.update(kwargs)
    return Conv2D(*args, **darknet_conv_kwargs)

def DarknetConv2D_BN_Leaky(*args, **kwargs):
    # batch normalization を使用する場合、畳み込み層の bias は不要である。
    no_bias_kwargs = {'use_bias': False}
    no_bias_kwargs.update(kwargs)
    return compose(
        DarknetConv2D(*args, **no_bias_kwargs),
        BatchNormalization(),
        LeakyReLU(alpha=0.1))

Darknet-53 の Body 部分

darknet_body() は、Darknet-53 のうち、Body 部分を作成する関数である。

def resblock_body(x, num_filters, num_blocks):
    # プーリング目的で畳み込み層を利用するためのパディング
    x = ZeroPadding2D(((1, 0), (1, 0)))(x)

    # プーリング目的の畳み込み層
    x = DarknetConv2D_BN_Leaky(num_filters, (3, 3), strides=(2, 2))(x)

    # num_blocks 個の residual block を作成する。
    for i in range(num_blocks):
        y = compose(
            DarknetConv2D_BN_Leaky(num_filters // 2, (1, 1)),
            DarknetConv2D_BN_Leaky(num_filters, (3, 3)))(x)
        x = Add()([x, y])
    return x

def darknet_body(x):
    x = DarknetConv2D_BN_Leaky(32, (3, 3))(x)
    x = resblock_body(x, 64, 1)
    x = resblock_body(x, 128, 2)
    x = resblock_body(x, 256, 8)
    x = resblock_body(x, 512, 8)
    x = resblock_body(x, 1024, 4)
    return x

追加の畳み込み層

Darkent-53 の後に続く A、B、C (記事冒頭の図を参照) の部分である。
yolo_body() は YOLOv3 全体を作成する関数である。

def make_last_layers(x, num_filters, out_filters):
    # Aの部分を作成する。
    x = compose(
        DarknetConv2D_BN_Leaky(num_filters, (1, 1)),
        DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),
        DarknetConv2D_BN_Leaky(num_filters, (1, 1)),
        DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),
        DarknetConv2D_BN_Leaky(num_filters, (1, 1)))(x)
    # Bの部分を作成する。
    y = compose(
        DarknetConv2D_BN_Leaky(num_filters * 2, (3, 3)),
        DarknetConv2D(out_filters, (1, 1)))(x)
    return x, y

def yolo_body(inputs, num_anchors, num_classes):
    darknet = Model(inputs, darknet_body(inputs))
    # 1個目の A 及び B の部分を作成する。
    x, y1 = make_last_layers(darknet.output, 512, num_anchors * (num_classes + 5))

    # 1個目の C の部分を作成する。
    x = compose(
        DarknetConv2D_BN_Leaky(256, (1, 1)),
        UpSampling2D(2))(x)
    x = Concatenate()([x, darknet.layers[152].output])

    # 2個目の A 及び B の部分を作成する。
    x, y2 = make_last_layers(x, 256, num_anchors * (num_classes + 5))

    # 2個目の C の部分を作成する。
    x = compose(
        DarknetConv2D_BN_Leaky(128, (1, 1)),
        UpSampling2D(2))(x)
    x = Concatenate()([x, darknet.layers[92].output])

    # 3個目の A 及び B の部分を作成する。
    x, y3 = make_last_layers(x, 128, num_anchors * (num_classes + 5))

    # 3個の B の出力を YOLOv3 の出力とする。
    return Model(inputs, [y1, y2, y3])