基於libtorch的yolov5目標檢測網絡實現(2)——網絡結構實現


“yolov5是yolo系列目標檢測框架的v5版本,本系列文章我們將一步步來解析該框架的原理,並使用libtorch來一步步將其實現——從數據集准備,到網絡結構實現,接着到損失函數實現,再到訓練代碼實現,最后到模型驗證。

上篇文章中我們已經講了COCO數據集的json標簽文件的解析:

基於libtorch的yolov5目標檢測網絡實現——COCO數據集json標簽文件解析

本文我們主要講yolov5網絡的結構與實現。

01

yolov5網絡目標檢測的基本思想

yolov5網絡輸入640*640的三通道圖像,也即3*640*640的矩陣數據,如果原圖像尺寸不是640*640,則通過填充或縮放將其尺寸變成640*640尺寸之后再輸入網絡。

通常把640*640的圖像划分成N*N(通常為80*80,或40*40,或20*20)的網格區域。網絡的輸出端則輸出所有網格區域的預測信息,每個網格的預測信息包括目標的分類概率、置信度,以及包圍檢測目標的方框的中心坐標、長、寬。其中分類概率表征網格區域所預測目標的分類信息,置信度表征網格區域中存在檢測目標的概率(也即置信度越高表示該網格區域越有可能存在檢測目標),方框的中心坐標、長、寬信息則表示網格所預測目標的具體大小和位置。

因此,在解析網絡輸出時,我們根據每個網格的置信度是否超過設定閾值來判斷該網格區域是否有檢測目標(並非每個網格區域都存在檢測目標),根據分類概率來判斷該網格所預測目標的分類,根據方框信息來確定目標的位置和大小。

由於檢測目標有大有小、尺度不一,通常使用不同的網格尺寸同時對640*640圖像進行區域划分,小尺寸網格負責檢測小目標,大尺寸網格負責檢測大目標,比如yolov5網絡將640*640圖像分別划分為80*80-->40*40-->20*20的網格,對應的網格尺寸分別為8*8-->16*16-->32*32,不同尺寸網格負責預測目標的尺寸分別為小-->中-->大。

此外,每一個網格會預測3個不同尺寸和位置的檢測目標(這3個目標的尺寸屬同一級別,位置都在同一網格內,但具體尺寸和坐標位置則不相同),因此:

  • 80*80網格輸出3*80*80個小尺寸檢測目標的信息

  • 40*40網格輸出3*40*40個中尺寸檢測目標的信息

  • 20*20網格輸出3*20*20個大尺寸檢測目標的信息

02

yolov5整體網絡結構

如下圖所示,yolov5的網絡結構主要由輸入端、Backbone、Neck、Head這4大部分組成(其中nc為分類總數,比如COCO數據集總共有80個分類,那么nc=80)。

其中各部分的作用為:

  • 輸入端:對輸入圖像進行填充或縮放,使其尺寸變成3*640*640,並對圖像數據進行歸一化,轉換為0~1之間的浮點數;

  • Backbone:提取圖像特征;

  • Neck:Neck在Backbone和Head之間,其作用為對Backbone輸出的圖像特征信息進行進一步提取、融合;

  • Head:對Neck輸出的特征信息進行分類、定位,從而輸出檢測目標的分類概率、置信度、方框等信息。

以上是從大組件的角度來對網絡結構進行解析,如果更加細分,我們會發現整個網絡又主要由Focus、CBL、CSP1_n、SPP、CSP2_n、Upsample和Conv這幾個小組件搭建而成(圖中的cat為張量的拼接操作,前文已經講過,此處不再重復)。因此主要把這幾個小組件實現之后,那么整個網絡就容易實現了。在以下章節中我們將分別講解這幾個組件的結構與實現。

03

CBL結構與實現

CBL組件由1個卷積層(Conv層)、1個Batch normalize層(BN層),和1個LeakyRelu激活函數層構成,如下圖所示:

這里的LeakyRelu激活函數是后人在Relu函數的基礎上新提出的,我們對比一下它們的計算公式:

兩者的函數曲線分別為:

可以看到兩種激活函數的主要區別在於輸入小於0的情況,此情況下Relu直接輸出0,而LeakyRelu則輸出固定系數與輸入信號的乘積,因此LeakyRelu解決了Relu在輸入小於0時梯度死亡的問題。

CBL代碼實現:

#define NONE -1


//自動計算padding參數,當輸入的padding參數為-1時,則取卷積核尺寸的一半作為padding參數
int autopad(int k, int p)
{
  if (p == NONE)
    return k / 2;
  else
    return p;
}


struct CBL :torch::nn::Module
{


  bool _act;   //_act參數控制輸出是否經過LeakyRelu激活


  CBL(int c1, int c2, int k = 1, int s = 1, int p = NONE, int g = 1, bool act = true, float e = 1.0)
  {


    int _c1 = (int)(c1*e + 0.5);
    int _c2 = (int)(c2*e + 0.5);
    _act = act;


    conv1 = register_module("conv1", torch::nn::Conv2d(torch::nn::Conv2dOptions(_c1, _c2, k).stride(s).padding(autopad(k, p)).groups(g).bias(false)));


    c1b = register_module("c1b", torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(_c2).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)));
  }


  torch::Tensor forward(torch::Tensor input)
  {
    namespace F = torch::nn::functional;


    auto x = conv1->forward(input);


    x = c1b->forward(x);


    if (_act)
    {
      x = torch::nn::LeakyReLU(torch::nn::LeakyReLUOptions().negative_slope(1e-2).inplace(true))->forward(x);
    }
      


    return x;
  }


  torch::nn::Conv2d conv1{ nullptr };
  torch::nn::BatchNorm2d c1b{ nullptr };


};

04

Focus結構與實現

Focus主要是對輸入的[3, 640, 640]數據以2的間隔在第2維度、第3維度上面進行slice切片,然后將切片之后的數據在第1維度上面拼接,如下圖所示:

所以Focus相當於對每個通道圖像進行2倍下采樣,然后再對下采樣圖像進行拼接的操作,最后將拼接之后的數據輸入CBL進行處理。

代碼實現:

struct Focus :torch::nn::Module
{


  Focus(int c1, int c2, int k = 3, int s = 1, int p = 1, int g = 1, bool act = true, float e = 1.0)
  {
    int _c2 = (int)(c2*e + 0.5);
    cbl_s = register_module("cbl_s", torch::nn::Sequential(CBL(c1 * 4, _c2, k, s, p, g, act)));
  }




  torch::Tensor forward(torch::Tensor input)
{


    auto x = torch::cat({ input.index({ "...", Slice({0, None, 2}), Slice({0, None, 2}) }),
                          input.index({ "...", Slice({1, None, 2}), Slice({0, None, 2}) }),
                          input.index({ "...", Slice({0, None, 2}), Slice({1, None, 2}) }),
                          input.index({ "...", Slice({1, None, 2}), Slice({1, None, 2}) }) }, 1);


    x = cbl_s->forward(x);


    return x;
  }




  torch::nn::Sequential cbl_s{ nullptr };
};

05

SPP結構與實現

SPP是空間金字塔池化的縮寫,SPP模塊的精髓在於使用不同尺寸的池化窗口分別對數據進行池化操作,然后再對不同池化窗口的池化結果進行拼接。主要有以下兩個作用:

  • 解決對輸入圖像進行裁剪、縮放導致的圖像失真問題;

  • 通過不同尺度的池化、融合,有利於在檢測目標大小差異較大情況下的目標檢測。

SPP的主要構成如下圖所示:

代碼實現:

struct SPP :torch::nn::Module
{
  int _k1, _k2, _k3;


  SPP(int c1, int c2, int k1 = 5, int k2 = 9, int k3 = 13, float e = 1.0)
  {
    int _c1 = (int)(c1*e + 0.5);
    int _c2 = (int)(c2*e + 0.5);
    int _c = _c1 / 2;
    _k1 = k1;
    _k2 = k2;
    _k3 = k3;


    cbl_before = register_module("cbl_before", torch::nn::Sequential(CBL(_c1, _c, 1, 1)));
    cbl_after = register_module("cbl_after", torch::nn::Sequential(CBL(_c * 4, _c2, 1, 1)));
  }




  torch::Tensor forward(torch::Tensor input)
{


    namespace F = torch::nn::functional;
    //CBL輸出,相當於1*1池化
    auto x0 = cbl_before->forward(input);   //CBL
    //5*5池化
    auto x1 = F::max_pool2d(x0, F::MaxPool2dFuncOptions(_k1).stride(1).padding(_k1 / 2));
    //9*9池化
    auto x2 = F::max_pool2d(x0, F::MaxPool2dFuncOptions(_k2).stride(1).padding(_k2 / 2));
    //13*13池化
    auto x3 = F::max_pool2d(x0, F::MaxPool2dFuncOptions(_k3).stride(1).padding(_k3 / 2));
    //將1*1、5*5、9*9、13*13的池化結果進行拼接
    auto x_cat = torch::cat({ x0, x1, x2, x3 }, 1);
    //將拼接結果再輸入CBL
    x_cat = cbl_after->forward(x_cat);


    return x_cat;
  }


  torch::nn::Sequential cbl_before{ nullptr };
  torch::nn::Sequential cbl_after{ nullptr };
};

05

CSP1_n和CSP2_n結構與實現

講CSP1_n和CSP2_n的結構之前,讓我們先來回顧一下殘差模塊,之前我們在Resnet34網絡中已經講過殘差模塊:

基於libtorch的Resnet34殘差網絡實現——Cifar-10分類

殘差模塊的主要特點是把輸入信號加到輸出信號中,如下圖所示:

我們把CSP1_n中的殘差模塊稱為ResUnit_n模塊,ResUnit_n模塊中包含了2的倍數個CBL模塊,如下圖所示:

而CSP1_n中又包含了n個ResUnit_n模塊,比如CSP1_1有1個ResUnit_n,CSP1_3有3個ResUnit_n:

CSP2_n與CSP1_n模塊的結構大同小異,唯一的區別在於ResUnit_n殘差模塊:CSP1_n中的ResUnit_n模塊把輸入信號疊加到輸出,而CSP2_n中的ResUnit_n模塊則不把輸入信號迭代到輸出。如下圖所示:

因此在實現代碼的時候,我們可以給ResUnit_n模塊增加一個參數,用於控制是否把輸入信號疊加到輸出上:該參數為true則疊加,為false則不疊加。這樣一來,CSP2_n與CSP1_n模塊就可以使用同一段代碼來實現了,區別在於傳入的參數是true還是false:

//殘差模塊代碼
//參數res_flag控制是否把輸入信號疊加到輸出上
struct ResUnit_n :torch::nn::Module
{


  bool _shortcut;
  bool _res_flag;


  ResUnit_n(int c1, int c2, int n, bool res_flag = true)
  {
    _shortcut = (c1 == c2) ? true : false;
    _res_flag = res_flag;


    res_n = register_module("res_n", torch::nn::Sequential());


    for (int i = 0; i < n; i++)
    {
      res_n->push_back(CBL(c1, c1, 1, 1, 0));
      res_n->push_back(CBL(c1, c2, 3, 1, 1));
    }


  }


  torch::Tensor forward(torch::Tensor input)
{
    Tensor x;


    if (_shortcut && _res_flag)
      x = input + res_n->forward(input);
    else
      x = res_n->forward(input);


    return x;
  }


  torch::nn::Sequential res_n{ nullptr };
};


//CSP1_n與CSP2_n模塊代碼,res_flag為true是CSP1_n,res_flag為false是CSP2_n
struct CSP1_n :torch::nn::Module
{


  CSP1_n(int c1, int c2, int k = 1, int s = 1, int p = NONE, int g = 1, bool act = true, bool res_flag = true, int n = 1, float e0 = 1.0, float e1 = 1.0)
  {
    int _n = (int)(n*e0 + 0.5);
    int _c1 = (int)(c1*e1 + 0.5);
    int _c2 = (int)(c2*e1 + 0.5);
    int _c = _c2 / 2;


    up = register_module("up", torch::nn::Sequential(
      CBL(_c1, _c, k, s, autopad(k, p), g, act),
      ResUnit_n(_c, _c, _n, res_flag)
      //torch::nn::Conv2d(torch::nn::Conv2dOptions(_c, _c, 1).stride(1).padding(0).bias(false))
    ));


    bottom = register_module("bottom", torch::nn::Conv2d(torch::nn::Conv2dOptions(_c1, _c, 1).stride(1).padding(0)));


    tie = register_module("tie", torch::nn::Sequential(
      torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(_c * 2).eps(1e-5).momentum(0.1).affine(true).track_running_stats(true)),
      torch::nn::LeakyReLU(torch::nn::LeakyReLUOptions().negative_slope(1e-2).inplace(true)),
      torch::nn::Conv2d(torch::nn::Conv2dOptions(_c * 2, _c2, 1).stride(1).padding(0).bias(false))
    ));
  }


  torch::Tensor forward(torch::Tensor input)
{
    auto x1 = up->forward(input);
    auto x2 = bottom->forward(input);
    auto total = torch::cat({ x1, x2 }, 1);


    auto x = tie->forward(total);


    return x;
  }


  torch::nn::Sequential up{ nullptr };
  torch::nn::Conv2d bottom{ nullptr };
  torch::nn::Sequential tie{ nullptr };
};

06

Upsample結構與實現

Upsample模塊就是一個向上采樣的操作,相當於采用插值的方式把圖像放大了,比如本來圖像是32*32大小,經過向上采樣之后則變成64*64大小。

代碼實現:

struct Up_Sample :torch::nn::Module
{
  Up_Sample()
  {
    //torch::kNearest表示使用最鄰近插值的方式向上采樣
    up_sample = register_module("up_sample", torch::nn::Upsample(torch::nn::UpsampleOptions().scale_factor(std::vector<double>({ 2, 2 })).mode(torch::kNearest)));
  }


  torch::Tensor forward(torch::Tensor input)
{
    auto x = up_sample->forward(input);


    return x;
  }


  torch::nn::Upsample up_sample{ nullptr };


};

07

整體網絡結構與實現

以上講整體結構時,我們說到yolov5網絡大致可以分為輸入端、Backbone、Neck、Head這4大部分。我們實現代碼的時候,就不這么看了,我們根據輸出信號的分支,把輸出信號標記為out1~out7,然后以out1~out7信號的計算為導向來實現代碼,如下圖所示:

首先我們把out1~out7信號的計算分解步驟:

  • Input-->Focus-->CBL-->CSP1_1-->CBL-->CSP1_3-->out1

  • out1-->CBL-->CSP1_3-->out2

  • out2-->CBL-->SPP-->CSP2_1-->CBL-->out3

  • out3-->upsample-->upsample(out3)-->cat(upsample(out3, out2))-->CSP2_1-->CBL-->out4

  • out4-->upsample-->upsample(out4)-->cat(upsample(out4, out1))-->CSP2_1-->out5

  • out5-->CBL-->out5'-->cat(out5', out4)-->CSP2_1-->out6

  • out6-->CBL-->out6'-->cat(out6', out3)-->CSP2_1-->out7

最后,再把out5、out6、out7分別經過一個卷積層處理,分別得到80*80、40*40、20*20網格的預測輸出:

  • out5-->Conv-->batch_size*80*80*255輸出

  • out6-->Conv-->batch_size*40*40*255輸出

  • out7-->Conv-->batch_size*20*20*255輸出

注意到輸出數據的最后一個維度都是255,這個255是怎么來的呢?COCO數據集總共有80個分類,因此輸出包含80個分類概率,加上預測目標的方框信息(中心x坐標、中心y坐標、長、寬),再加上一個置信度,所以每個預測目標總共有4+1+80=85個數據,再加上每個網格預測3個目標,所以85再乘以3得到255個數據。

為了方便后續的數據分析,我們把輸出的預測數據轉換成以下維度:

代碼實現:

struct yolov5 :torch::nn::Module
{
  int _nc;  
  //nc為分類總數,gd為控制網絡深度參數,gw為控制網絡寬度參數
  yolov5(int nc = 80, float gd = 0.33, float gw = 0.5)
  {
    _nc = nc;
    
    out1 = register_module("out1", torch::nn::Sequential(
      Focus(3, 64, 3, 1, 1, 1, true, gw),
      CBL(64, 128, 3, 2, 1, 1, true, gw),
      CSP1_n(128, 128, 1, 1, NONE, 1, true, true, 3, gd, gw),
      CBL(128, 256, 3, 2, 1, 1, true, gw),
      CSP1_n(256, 256, 1, 1, NONE, 1, true, true, 9, gd, gw)
    ));


    out2 = register_module("out2", torch::nn::Sequential(
      CBL(256, 512, 3, 2, 1, 1, true, gw),
      CSP1_n(512, 512, 1, 1, NONE, 1, true, true, 9, gd, gw)
    ));


    out3 = register_module("out3", torch::nn::Sequential(
      CBL(512, 1024, 3, 2, 1, 1, true, gw),
      SPP(1024, 1024, 5, 9, 13, gw),
      CSP1_n(1024, 1024, 1, 1, NONE, 1, true, false, 3, gd, gw),
      CBL(1024, 512, 1, 1, 0, 1, true, gw)
    ));
    
    upsample1 = register_module("upsample1", torch::nn::Sequential(
      Up_Sample()
    ));


    


    out4 = register_module("out4", torch::nn::Sequential(
      CSP1_n(1024, 512, 1, 1, 0, 1, true, false, 3, gd, gw),
      CBL(512, 256, 1, 1, 0, 1, true, gw)
    ));


    upsample2 = register_module("upsample2", torch::nn::Sequential(
      Up_Sample()
    ));


    out5 = register_module("out5", torch::nn::Sequential(
      CSP1_n(512, 256, 1, 1, NONE, 1, true, false, 3, gd, gw)
    ));


    pan_middle = register_module("pan_middle ", torch::nn::Sequential(
      CBL(256, 256, 3, 2, 1, 1, true, gw)
    ));


    out6 = register_module("out6 ", torch::nn::Sequential(
      CSP1_n(512, 512, 1, 1, NONE, 1, true, false, 3, gd, gw)
    ));


    pan_small = register_module("pan_small", torch::nn::Sequential(
      CBL(512, 512, 3, 2, 1, 1, true, gw)
    ));


    out7 = register_module("out7", torch::nn::Sequential(
      CSP1_n(1024, 1024, 1, 1, NONE, 1, true, false, 3, gd, gw)
    ));


    int _big = (int)(gw * 256 + 0.5);
    int _middle = (int)(gw * 512 + 0.5);
    int _small = (int)(gw * 1024 + 0.5);


    out_big = register_module("out_big", torch::nn::Sequential(
      torch::nn::Conv2d(torch::nn::Conv2dOptions(_big, 3 * (5 + nc), 1).stride(1).padding(0))
    ));


    out_middle = register_module("out_middle", torch::nn::Sequential(
      torch::nn::Conv2d(torch::nn::Conv2dOptions(_middle, 3 * (5 + nc), 1).stride(1).padding(0))
    ));


    out_small = register_module("out_small", torch::nn::Sequential(
      torch::nn::Conv2d(torch::nn::Conv2dOptions(_small, 3 * (5 + nc), 1).stride(1).padding(0))
    ));
  }


  //{N, channels, height, width}
  torch::Tensor forward(torch::Tensor input)
  {


    auto out_1 = out1->forward(input);
    auto out_2 = out2->forward(out_1);
    auto out_3 = out3->forward(out_2);


    auto out_4_in = upsample1->forward(out_3);
    out_4_in = torch::cat({ out_4_in , out_2 }, 1);
    auto out_4 = out4->forward(out_4_in);


    auto out_5_in = upsample2->forward(out_4);
    out_5_in = torch::cat({ out_5_in , out_1 }, 1);
    auto out_5 = out5->forward(out_5_in);


    auto out_6_in = pan_middle->forward(out_5);
    out_6_in = torch::cat({ out_6_in , out_4 }, 1);
    auto out_6 = out6->forward(out_6_in);


    auto out_7_in = pan_small->forward(out_6);
    out_7_in = torch::cat({ out_7_in , out_3 }, 1);
    auto out_7 = out7->forward(out_7_in);


    out_5 = out_big->forward(out_5);   //batch_size*80*80*255
    out_6 = out_middle->forward(out_6);  //batch_size*40*40*255
    out_7 = out_small->forward(out_7);  //batch_size*20*20*255


    out_5 = out_5.view({ out_5.sizes()[0], 3, _nc + 5, out_5.sizes()[2], out_5.sizes()[3] }).permute({ 0, 1, 3, 4, 2 }).contiguous();  //[batch_size, 3, 80, 80, 85]
    out_6 = out_6.view({ out_6.sizes()[0], 3, _nc + 5, out_6.sizes()[2], out_6.sizes()[3] }).permute({ 0, 1, 3, 4, 2 }).contiguous();  //[batch_size, 3, 40, 40, 85]
    out_7 = out_7.view({ out_7.sizes()[0], 3, _nc + 5, out_7.sizes()[2], out_7.sizes()[3] }).permute({ 0, 1, 3, 4, 2 }).contiguous();  //[batch_size, 3, 20, 20, 85]


    out_5 = out_5.view({ out_5.sizes()[0], 3 * out_5.sizes()[2] * out_5.sizes()[3], _nc + 5 }).contiguous();  //[batch_size, 3*80*80, 85]
    out_6 = out_6.view({ out_6.sizes()[0], 3 * out_6.sizes()[2] * out_6.sizes()[3], _nc + 5 }).contiguous();  //[batch_size, 3*40*40, 85]
    out_7 = out_7.view({ out_7.sizes()[0], 3 * out_7.sizes()[2] * out_7.sizes()[3], _nc + 5 }).contiguous();  //[batch_size, 3*20*20, 85]


    //[batch_size, 25200, 85]張量,其中框的個數為25200=(80*80+40*40+20*20)*3,85=[x, y, w, h] + 置信度得分[4] + 分類概率結果[5:84]
    auto out_cat = torch::cat({ out_5, out_6, out_7 }, 1);


    out_cat = torch::sigmoid(out_cat);


    return out_cat;
  }


  torch::nn::Sequential out1{ nullptr };
  torch::nn::Sequential out2{ nullptr };
  torch::nn::Sequential out3{ nullptr };
  torch::nn::Sequential upsample1{ nullptr };
  torch::nn::Sequential out4{ nullptr };
  torch::nn::Sequential upsample2{ nullptr };
  torch::nn::Sequential out5{ nullptr };
  torch::nn::Sequential pan_middle{ nullptr };
  torch::nn::Sequential out6{ nullptr };
  torch::nn::Sequential pan_small{ nullptr };
  torch::nn::Sequential out7{ nullptr };
  torch::nn::Sequential out_big{ nullptr };
  torch::nn::Sequential out_middle{ nullptr };
  torch::nn::Sequential out_small{ nullptr };
};

08

yolov5網絡的參數配置

yolov5有yolov5s、yolov5m、yolov5l、yolov5x這4種不同的參數配置,不同配置的區別無非就是深度和寬度的區別。我們注意到以上代碼實現中,有gd、gw這兩個參數,就是用來配置yolov5的網絡深度、寬度的。

以上4種參數配置所對應的gd、gw值列出如下,gd值越大,網絡深度越深,gw值越大,網絡寬度越寬。


gd
gw
yolov5s 0.33
0.5
yolov5m 0.67
0.75
yolov5l 1.0
1.0
yolov5x 1.33
1.25

好了本文我們就講到這里吧,下一篇文章我們繼續~

歡迎掃碼關注本微信公眾號,接下來會不定時更新更加精彩的內容,敬請期待~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM