1段階目標検出モデルYoLoシリーズ(1):YoLoV3の詳細説明とコード実装

目次

 1. YoLoV3のネットワーク構造 

1.1 バックボーン:Darknet-53

1.2 機能ピラミッドの構築

1.3 ヨローヘッド

2. yolov3モデルの予測結果のデコード

2.1 先験的なフレーム

2.2 検出フレームのデコード

2.3 信頼デコード

2.4 カテゴリのデコード

 3. yolov3 モデルのトレーニング戦略と損失関数


 1. YoLoV3のネットワーク構造 

        YoLoV3モデルのネットワーク構造は下図に大まかに示されており、主にバックボーンネットワークで画像特徴を抽出する部分、特徴ピラミッドFPNを構築して特徴融合を実現する部分、YoLoヘッドを利用して予測結果を取得する部分で構成されています

1.1 バックボーン:Darknet-53

        YoLoV3 モデルは、画像の特徴を抽出するバックボーン ネットワークとして Darknet-53 を使用します。複数の残差モジュールが Darknet-53 ネットワークにスタックされ、隣接する残差モジュール間には kernel-size=3×3、stride=2 がありますレイヤーは主にダウンサンプルに使用されます。Darknet-53 ネットワークは、最初は画像分類タスクに適用され、下の図に示すように、画像の特徴を抽出するための 52 の畳み込み層と、画像分類のための最後の全結合層を含む、合計 53 の層があります。使用される入力画像のサイズは例として 416×416 です (Darknet-53 ネットワークには 5 つのダウンサンプリング操作があり、特徴マップのサイズは各ダウンサンプリング後の元のサイズ 1/2)。さらに、yolov3 のバックボーン ネットワークは、画像の特徴を抽出するために Darknet-53 の前にある 52 の畳み込み層のみを使用することに注意してください。

        Darknet-53 ネットワークの残差モジュールの入力特徴マップと出力特徴マップは、畳み込みカーネル サイズ1×1 および 3×3で 2 つの畳み込みを経て、Resnet ネットワーク残差と同様の Residual 残差接続方法を採用します。接続方法は基本的に同じです。つまり、次の図に示すように、残差モジュールの入力特徴マップと 2 つの畳み込み後の入力特徴マップを追加します。

         さらに、下の図に示すように、Darknet-53 ネットワークの畳み込みモジュール= conv2d+bn+leaky relu です。

        Darknet-53 ネットワーク コードは次のように実装されます。

import math
from collections import OrderedDict

import torch.nn as nn


# ---------------------------------------------------------------------#
#   残差结构
#   利用一个1x1卷积下降通道数,然后利用一个3x3卷积提取特征并且上升通道数
#   最后接上一个残差边
# ---------------------------------------------------------------------#
class BasicBlock(nn.Module):
    def __init__(self, inplanes, planes):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(inplanes, planes[0], kernel_size=1, stride=1, padding=0, bias=False)
        self.bn1 = nn.BatchNorm2d(planes[0])
        self.relu1 = nn.LeakyReLU(0.1)

        self.conv2 = nn.Conv2d(planes[0], planes[1], kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes[1])
        self.relu2 = nn.LeakyReLU(0.1)

    def forward(self, x):
        residual = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu1(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu2(out)

        out += residual
        return out


class DarkNet(nn.Module):
    def __init__(self, layers):
        super(DarkNet, self).__init__()
        self.inplanes = 32
        # 416,416,3 -> 416,416,32
        self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(self.inplanes)
        self.relu1 = nn.LeakyReLU(0.1)

        # 416,416,32 -> 208,208,64
        self.layer1 = self._make_layer([32, 64], layers[0])
        # 208,208,64 -> 104,104,128
        self.layer2 = self._make_layer([64, 128], layers[1])
        # 104,104,128 -> 52,52,256
        self.layer3 = self._make_layer([128, 256], layers[2])
        # 52,52,256 -> 26,26,512
        self.layer4 = self._make_layer([256, 512], layers[3])
        # 26,26,512 -> 13,13,1024
        self.layer5 = self._make_layer([512, 1024], layers[4])

        self.layers_out_filters = [64, 128, 256, 512, 1024]

        # 进行权值初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()

    # ---------------------------------------------------------------------#
    #   在每一个layer里面,首先利用一个步长为2的3x3卷积进行下采样
    #   然后进行残差结构的堆叠
    # ---------------------------------------------------------------------#
    def _make_layer(self, planes, blocks):
        layers = []
        # 下采样,步长为2,卷积核大小为3
        layers.append(("ds_conv", nn.Conv2d(self.inplanes, planes[1], kernel_size=3, stride=2, padding=1, bias=False)))
        layers.append(("ds_bn", nn.BatchNorm2d(planes[1])))
        layers.append(("ds_relu", nn.LeakyReLU(0.1)))
        # 加入残差结构
        self.inplanes = planes[1]
        for i in range(0, blocks):
            layers.append(("residual_{}".format(i), BasicBlock(self.inplanes, planes)))
        return nn.Sequential(OrderedDict(layers))

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu1(x)

        x = self.layer1(x)
        x = self.layer2(x)
        out3 = self.layer3(x)
        out4 = self.layer4(out3)
        out5 = self.layer5(out4)

        return out3, out4, out5


def darknet53():
    model = DarkNet([1, 2, 8, 8, 4])
    return model

1.2 機能ピラミッドの構築

       yolov3 が機能ピラミッドを構築する方法を説明する前に、機能ピラミッド構造を簡単に紹介しましょう。特徴ピラミッド構造は、論文「物体検出のための特徴ピラミッド ネットワーク」で最初に提案されました。解決すべき主な問題は、マルチスケールの変化に対処する際の目標検出が不十分であることです。現在、多くのネットワークは、目標に対して 1 つの深い特徴だけを使用しています(たとえば、Faster R-CNN は、後続のオブジェクト分類とバウンディング ボックス回帰に 4 倍のダウンサンプリング畳み込み層を使用します) ですが、これには明らかな欠陥があります。つまり、小さなオブジェクト自体のピクセル情報が少なく、ダウンサンプリングが行われないということです。オブジェクトのサイズに明らかな違いがある場合の検出問題に対処するには、画像ピラミッドを使用してマルチスケール変化を強調するのが古典的な方法ですが、これでは膨大な量の変化が生じます。計算の。そこで本論文では、物体検出におけるマルチスケール変化問題を最小限の計算量で処理できる、下図のような特徴ピラミッドのネットワーク構造を提案する。

ただし、本稿では、低解像度の特徴マップは、アップサンプリング後の高解像度の特徴マップとの特徴融合に使用されます(つまり、特徴マップ上の対応する特徴値が追加され、 ResNet で初めて 実際、より一般的に使用されるもう 1 つの特徴融合メソッドは concat メソッドです (つまり、DenseNet で初めて登場したチャネル数の結合)。yolov3 に組み込まれた特徴ピラミッドは、concat メソッドを使用して達成ます。機能の融合

        Darknet-53 が入力画像の特徴を抽出した後、抽出された特徴レイヤーから 3 つの特徴レイヤーが選択され、異なるレベルの特徴の効果的な融合を実現する特徴ピラミッドが構築されます。これら 3 つの特徴レイヤーは、Darknet-53 の異なる位置に配置されます。 53 ネットワークであり、その形状は、下図に示すように、それぞれ (52,52,256)、(26,26,512)、(13,13,1024)です。

これら 3 つの効果的なフィーチャ レイヤーを使用してフィーチャ ピラミッド FPN を構築するプロセスは次のとおりです。

① 13×13×1024 特徴層に対して5 回の畳み込み演算を実行した後、最初の拡張特徴層 13×13×512が取得され、この拡張特徴層は UmSampling2d によってアップサンプリングされ、26×26×512 特徴層と結合されます。チャネル番号。これにより特徴融合が実現され、融合後に得られる新しい特徴層の形状は (26,26,768) になります

②形状 (26,26,768) の新しいフィーチャ レイヤーに対して5 回の畳み込み演算を実行した後、 2 番目の拡張フィーチャ レイヤー 26×26×256が取得され、この拡張フィーチャ レイヤーは UmSampling2d でアップサンプリングされ、52x52x512 フィーチャ レイヤーでチャネリングされます。 、特徴融合を実現するために、融合後に得られる新しい特徴レイヤーの形状は (52,52,384) です

③形状 (52,52,384) の新しいフィーチャ レイヤーに対して5 回の畳み込み演算を実行した後、3 番目の拡張フィーチャ レイヤー 52×52×128が得られます

注: 5 つの畳み込み演算の順序は 1×1、3×3、1×1、3×3、1×1 です。このうち 1×1 畳み込みは主にチャネル数を削減するために使用され、3×3 は主にチャネル数を削減するために使用されます。畳み込みは主に使用されます。画像の特徴をさらに抽出し、チャネル数を増やすために使用されます。

1.3 ヨローヘッド

       上記の特徴ピラミッドを構築することにより、3 つの強化された特徴レイヤーが得られました。これら 3 つの強化された特徴レイヤーの形状は (13,13,512)、(26,26,256)、(52,52,128) であり、これら 3 つの特徴レイヤーを強化します。それぞれYolo Headに渡され、モデルの予測結果が取得されます。Yolo Head は本質的に 3x3 コンボリューションと 1x1 コンボリューションです。VOC データセット (20 種類のターゲット) を例にとると、これら 3 つの拡張フィーチャ レイヤーは YoLo Head に入力され、最初に3x3 コンボリューションを通じて取得されます (13 、 13、1024) 、(26, 26, 512)、(52, 52, 256) フィーチャー マップを作成し、(13, 13, 75)、(26, 26, 75)、( 52,52,75) として 3 つの形状を取得します。 75 はデータ セット内のターゲット カテゴリの総数に関連します。75 =3×(20+1+4)3 は出力特徴マップ上の各特徴点に 3 つの予測ボックスがあることを意味し、20 はデータがset には 20 種類のオブジェクトが含まれます。1 は予測フレームにオブジェクトが含まれるかどうかを表し、4 は予測フレームの調整パラメータ、つまり予測フレームの中心点の座標パラメータ x_offset と y_offset、高さ h と幅を表します予測フレームの w。

        YoLoV3 ネットワーク構造の完全なコード実装は次のとおりです。

from collections import OrderedDict

import torch
import torch.nn as nn

from nets.darknet import darknet53


def conv2d(filter_in, filter_out, kernel_size):
    pad = (kernel_size - 1) // 2 if kernel_size else 0
    return nn.Sequential(OrderedDict([
        ("conv", nn.Conv2d(filter_in, filter_out, kernel_size=kernel_size, stride=1, padding=pad, bias=False)),
        ("bn", nn.BatchNorm2d(filter_out)),
        ("relu", nn.LeakyReLU(0.1)),
    ]))


# ------------------------------------------------------------------------#
#   make_last_layers里面一共有七个卷积,前五个用于提取特征。
#   后两个用于获得yolo网络的预测结果
# ------------------------------------------------------------------------#
def make_last_layers(filters_list, in_filters, out_filter):
    m = nn.Sequential(
        conv2d(in_filters, filters_list[0], 1),
        conv2d(filters_list[0], filters_list[1], 3),
        conv2d(filters_list[1], filters_list[0], 1),
        conv2d(filters_list[0], filters_list[1], 3),
        conv2d(filters_list[1], filters_list[0], 1),
        conv2d(filters_list[0], filters_list[1], 3),
        nn.Conv2d(filters_list[1], out_filter, kernel_size=1, stride=1, padding=0, bias=True)
    )
    return m


class YoloBody(nn.Module):
    def __init__(self, anchors_mask, num_classes, pretrained=False):
        super(YoloBody, self).__init__()
        # ---------------------------------------------------#
        #   生成darknet53的主干模型
        #   获得三个有效特征层,他们的shape分别是:
        #   52,52,256
        #   26,26,512
        #   13,13,1024
        # ---------------------------------------------------#
        self.backbone = darknet53()
        if pretrained:
            self.backbone.load_state_dict(torch.load("model_data/darknet53_backbone_weights.pth"))

        # ---------------------------------------------------#
        #   out_filters : [64, 128, 256, 512, 1024]
        # ---------------------------------------------------#
        out_filters = self.backbone.layers_out_filters

        # ------------------------------------------------------------------------#
        #   计算yolo_head的输出通道数,对于voc数据集而言
        #   final_out_filter0 = final_out_filter1 = final_out_filter2 = 75
        # ------------------------------------------------------------------------#
        self.last_layer0 = make_last_layers([512, 1024], out_filters[-1], len(anchors_mask[0]) * (num_classes + 5))

        self.last_layer1_conv = conv2d(512, 256, 1)
        self.last_layer1_upsample = nn.Upsample(scale_factor=2, mode='nearest')
        self.last_layer1 = make_last_layers([256, 512], out_filters[-2] + 256, len(anchors_mask[1]) * (num_classes + 5))

        self.last_layer2_conv = conv2d(256, 128, 1)
        self.last_layer2_upsample = nn.Upsample(scale_factor=2, mode='nearest')
        self.last_layer2 = make_last_layers([128, 256], out_filters[-3] + 128, len(anchors_mask[2]) * (num_classes + 5))

    def forward(self, x):
        # ---------------------------------------------------#
        #   获得三个有效特征层,他们的shape分别是:
        #   52,52,256;26,26,512;13,13,1024
        # ---------------------------------------------------#
        x2, x1, x0 = self.backbone(x)

        # ---------------------------------------------------#
        #   第一个特征层
        #   out0 = (batch_size,255,13,13)
        # ---------------------------------------------------#
        # 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512 -> 13,13,1024 -> 13,13,512
        out0_branch = self.last_layer0[:5](x0)
        out0 = self.last_layer0[5:](out0_branch)

        # 13,13,512 -> 13,13,256 -> 26,26,256
        x1_in = self.last_layer1_conv(out0_branch)
        x1_in = self.last_layer1_upsample(x1_in)

        # 26,26,256 + 26,26,512 -> 26,26,768
        x1_in = torch.cat([x1_in, x1], 1)
        # ---------------------------------------------------#
        #   第二个特征层
        #   out1 = (batch_size,255,26,26)
        # ---------------------------------------------------#
        # 26,26,768 -> 26,26,256 -> 26,26,512 -> 26,26,256 -> 26,26,512 -> 26,26,256
        out1_branch = self.last_layer1[:5](x1_in)
        out1 = self.last_layer1[5:](out1_branch)

        # 26,26,256 -> 26,26,128 -> 52,52,128
        x2_in = self.last_layer2_conv(out1_branch)
        x2_in = self.last_layer2_upsample(x2_in)

        # 52,52,128 + 52,52,256 -> 52,52,384
        x2_in = torch.cat([x2_in, x2], 1)
        # ---------------------------------------------------#
        #   第一个特征层
        #   out3 = (batch_size,255,52,52)
        # ---------------------------------------------------#
        # 52,52,384 -> 52,52,128 -> 52,52,256 -> 52,52,128 -> 52,52,256 -> 52,52,128
        out2 = self.last_layer2(x2_in)
        return out0, out1, out2

2. yolov3モデルの予測結果のデコード

        VOC データセット(合計 20 種類のターゲット)の画像サイズを416×416×3と仮定し、それを yolov3 ネットワークに入力すると、出力される 3 つの異なるスケールの特徴マップは3 つの異なる予測結果を表し、その予測結果は形状はそれぞれ13 × 13 × 75、26 × 26 × 75、52 × 52 × 75であり、小規模な特徴マップは大きなターゲットを予測し、大規模な特徴マップは小さなターゲットを予測しますここで、各予測結果が入力画像をどのように変換するかを簡単に理解します. 13 × 13 × 75 のサイズの出力特徴マップを例にとると、元の入力画像を 13 × 13 グリッドに分割することと等価です、元の入力画像上のすべての 32 × 32 ピクセル ポイントが、yolov3 ネットワークによって出力特徴マップにマッピングされ、特徴点になります (13 × 13 出力特徴マップは入力画像の 32 倍のダウンサンプリングに相当するため)。そして、各出力特徴マップ上の各特徴点には、アスペクト比の異なる 3 種類の事前フレームがあり、これらの事前フレームの h と w は、ネットワークのトレーニング前の過去の経験に基づいて事前に設定され、後でネットワークに渡されます。トレーニングでは前のフレームのパラメータを調整します。yolov3 ネットワークの予測結果には、オブジェクトを含む検出フレームの信頼度、検出フレームの調整パラメータ x、y、w、h、およびタイプの信頼度が含まれます。オブジェクトの 3×(1+4+20)=75。これが、3 つの出力特徴マップのチャネル数が 75 である理由です。検出情報の解読方法については、後で詳しく説明します。

2.1 先験的なフレーム

        以前のフレームには、(10×13)、(16×30)、(33×23)、(30×61)、(62×45)、(59×119)、(116×90)の 9 つのサイズがあります。 、(156 × 198)、(373 × 326)、順序は w × h、スケール 13 × 13 の出力特徴マップは、(116,90)、(156,198)、(373,326) の 3 種類に対応します。幅と高さのアスペクト比のアプリオリ フレーム、スケール 26×26 の出力特徴マップは、(30×61)、(62×45)、(59×119) の 3 種類のアスペクト比に対応します。 52×52 のスケールの事前フレーム 出力特徴マップは、これら 3 つのアスペクト比の (10×13)、(16×30)、(33×23) の事前ボックスに対応します。これら 9 つのサイズの前のフレームは入力画像に関連しており、コードが実装されるときは出力特徴マップ上で操作されることが多いため、変換に注意する必要があることに注意してください。前のフレームは検出にのみ関係します。これはフレームの w と h に関係しますが、x と y には関係しません。

2.2 検出フレームのデコード

        前のフレームと出力特徴マップを使用すると、検出フレームは次の式でデコードできます。

b_{x}=\sigma (t_{x})+c_{x}

b_{y}=\sigma (t_{y})+c_{y}

b_{w}=p_{w}e^{t_{w}}

b_{h}=p_{h}e^{t_{h}}

このうち、b_{x}による}、はb_{w}b_{h}検出フレームをデコードした後の中心点座標、幅、高さの4つのパラメータを表し、 、 、 は、t_{x}yolov3モデルの出力特徴マップ(予測結果)の4つのパラメータを表しt_{y}は、長方形枠の左上隅の格子点 座標 (実際のコード実装では、長方形枠の左上隅の格子点は、事前に前のフレームの中心点、つまり特徴量として使用されます)出力特徴マップの点)、 は、長方形フレームの左上隅のグリッド点を基準とした検出フレームの中心点の座標を表します。 オフセット σ はシグモイド活性化関数であり、 は幅と高さ表します下図に示すように、点線は前のフレーム、青は検出枠、赤は前述の長方形の枠、および検出枠の中心点を表します。調整範囲は以下の範囲内に制限されます。長方形の範囲。t_{w}t_{h}c_{x}c_{y}\シグマ(t_{x})\シグマ (t_{y})p_{w}p_{h}

         検出フレームのデコード プロセスをより明確に理解していただくために、引き続き下図を参照してください。13×13 の出力特徴マップを例にとります。下図の青い点は、出力特徴マップの特徴点 下図の左側の画像は、中心点としてマークされた特徴点に対応する 3 つの前のフレームに基づいています 下図の右の画像は、取得された調整された検出フレームですyolov3の予測結果、前フレームの中心点の位置、幅、高さに応じて上記4つの式により調整されています。

2.3 信頼デコード

        オブジェクトの検出の信頼度は Yolo 設計において非常に重要であり、アルゴリズムの精度と再現率に関係します。信頼度は出力 25 次元の固定ビットを占め、シグモイド関数で復号化できます。復号後の数値範囲は [0, 1] で、検出フレーム内に物体が存在する確率を表します。 。

2.4 カテゴリのデコード

        VOC データ セットには 20 のカテゴリがあるため、カテゴリの数は 25 次元出力の 20 次元を占めます。各次元はカテゴリの信頼度を表します。シグモイド アクティベーション関数は、Yolov2 のソフトマックスを置き換えるために使用され、相互排除カテゴリはキャンセルされます。これにより、ネットワークがより柔軟になります。3 つの異なるスケールの出力特徴マップは、合計 13 × 13 × 3 + 26 × 26 × 3 + 52 × 52 × 3 = 10647 個のボックスと、対応するカテゴリおよび信頼レベルをデコードできます。これらの 10647 個のボックスは、トレーニングと推論中に使用されます。使用法が異なります:

① トレーニング中に、10647 個のボックスすべてがラベリング関数に送信され、ラベリングと損失関数の計算の次のステップが実行されます。

②推論の際、信頼度閾値を選択し、低閾値ボックスをフィルタリングして除外し、nms(非最大値抑制)を通過させて最終予測結果全体を出力します。

 3. yolov3 モデルのトレーニング戦略と損失関数

        ニューラル ネットワーク モデルが損失を計算するためにトレーニングされるとき、それは実際には予測結果と実際のラベルの比較になります。ほとんどのモデルは MSE、MAE、クロスエントロピーなどを直接計算できますが、yolov3 のトレーニング戦略はモデルは比較的複雑なので、次の手順を実行する必要があります。

① 学習中に生成された 10647 個のボックスにラベルを付けます (ボックスには、肯定的な例、否定的な例、無視された例の 3 種類のラベルが付いています)。

        正の例: 任意のグラウンド トゥルースを取得し、10647 個のボックスすべてを使用して IOU を計算します。最大の IOU を持つ予測ボックスが正の例です (グラウンド トゥルースを使用した計算後の最大の IOU を持つ検出ボックスですが、IOU はしきい値より小さいです) 、これはまだ肯定的な例です)。また、予測ボックスは 1 つの Ground Truth にのみ割り当てることができます。たとえば、最初のグラウンド トゥルースはすでに正の検出ボックスと一致しており、次のグラウンド トゥルースは残りの 10647 個のボックスの中で最大の IOU を持つ検出ボックスを正の例として検索し、グラウンド トゥルースの順序は無視できます。正の例では、信頼性損失、検出ボックス損失、カテゴリ損失が生成されます予測ボックスは、対応する Ground Truth ボックス ラベルです。カテゴリ ラベルは 1 に対応し、残りは 0、信頼度ラベルは 1 です。

        負の例: これは正の例ではありませんが、すべての Ground Truth を含む IOU がしきい値より小さいため、負の例となります。負の例では、信頼ラベル 0 を持つ信頼損失のみが生成されます。

        サンプルを無視する: 正の例を除き、グラウンド トゥルースを含む IOU がしきい値より大きい場合、それは無視されるサンプルです。サンプルを無視しても損失は発生しません無視例が定義されている理由は、Yolov3 ではマルチスケールの特徴マップが使用されており、異なるスケールの特徴マップ間で重複する検出部分が存在するためです。特徴マップ 1 の 3 番目のボックスの IOU は 0.98 です。このとき、特徴マップ 2 の最初のボックスとグラウンド トゥルース間の IOU は 0.95 であり、グラウンド トゥルースも検出されます。信頼水準が強制的にラベル付けされた場合この時点で 0 では、オンライン学習の効果は十分ではありません。

②損失を計算します。検出フレーム x、y、w、h の損失は、MSE を損失関数として使用し、(Faster R-CNN からの) スムーズ L1 損失を損失関数として使用することもできます。スムーズ L1 は、トレーニングをよりスムーズにすることができます。論文では、ネットワーク トレーニングに GIOU 損失を使用するものもあります。信頼度ラベルとカテゴリ ラベルが 0、1 のバイナリ分類であるため、クロス エントロピーが損失関数として使用されます。

おすすめ

転載: blog.csdn.net/Mike_honor/article/details/126379701