您好,登錄后才能下訂單哦!
這篇文章主要講解了“Pytorch搭建YoloV5目標檢測平臺實現的方法”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Pytorch搭建YoloV5目標檢測平臺實現的方法”吧!
在學習YoloV5之前,我們需要對YoloV5所作的工作有一定的了解,這有助于我們后面去了解網絡的細節。
和之前版本的Yolo類似,整個YoloV5可以依然可以分為三個部分,分別是Backbone,FPN以及Yolo Head。
Backbone可以被稱作YoloV5的主干特征提取網絡,根據它的結構以及之前Yolo主干的叫法,我一般叫它CSPDarknet,輸入的圖片首先會在CSPDarknet里面進行特征提取,提取到的特征可以被稱作特征層,是輸入圖片的特征集合。在主干部分,我們獲取了三個特征層進行下一步網絡的構建,這三個特征層我稱它為有效特征層。
FPN可以被稱作YoloV5的加強特征提取網絡,在主干部分獲得的三個有效特征層會在這一部分進行特征融合,特征融合的目的是結合不同尺度的特征信息。在FPN部分,已經獲得的有效特征層被用于繼續提取特征。在YoloV5里依然使用到了Panet的結構,我們不僅會對特征進行上采樣實現特征融合,還會對特征再次進行下采樣實現特征融合。
Yolo Head是YoloV5的分類器與回歸器,通過CSPDarknet和FPN,我們已經可以獲得三個加強過的有效特征層。每一個特征層都有寬、高和通道數,此時我們可以將特征圖看作一個又一個特征點的集合,每一個特征點都有通道數個特征。Yolo Head實際上所做的工作就是對特征點進行判斷,判斷特征點是否有物體與其對應。與以前版本的Yolo一樣,YoloV5所用的解耦頭是一起的,也就是分類和回歸在一個1X1卷積里實現。
因此,整個YoloV5網絡所作的工作就是 特征提取-特征加強-預測特征點對應的物體情況。
1、主干網絡Backbone介紹
YoloV5所使用的主干特征提取網絡為CSPDarknet,它具有五個重要特點:
1、使用了殘差網絡Residual,CSPDarknet中的殘差卷積可以分為兩個部分,主干部分是一次1X1的卷積和一次3X3的卷積;殘差邊部分不做任何處理,直接將主干的輸入與輸出結合。
整個YoloV5的主干部分都由殘差卷積構成:
class Bottleneck(nn.Module): # Standard bottleneck def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion super(Bottleneck, self).__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_, c2, 3, 1, g=g) self.add = shortcut and c1 == c2 def forward(self, x): return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
殘差網絡的特點是容易優化,并且能夠通過增加相當的深度來提高準確率。其內部的殘差塊使用了跳躍連接,緩解了在深度神經網絡中增加深度帶來的梯度消失問題。
2、使用CSPnet網絡結構,CSPnet結構并不算復雜,就是將原來的殘差塊的堆疊進行了一個拆分,拆成左右兩部分:
主干部分繼續進行原來的殘差塊的堆疊;
另一部分則像一個殘差邊一樣,經過少量處理直接連接到最后。
因此可以認為CSP中存在一個大的殘差邊。
class C3(nn.Module): # CSP Bottleneck with 3 convolutions def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion super(C3, self).__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1) self.cv3 = Conv(2 * c_, c2, 1) # act=FReLU(c2) self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]) # self.m = nn.Sequential(*[CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)]) def forward(self, x): return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))
3、使用了Focus網絡結構,這個網絡結構是在YoloV5里面使用到比較有趣的網絡結構,具體操作是在一張圖片中每隔一個像素拿到一個值,這個時候獲得了四個獨立的特征層,然后將四個獨立的特征層進行堆疊,此時寬高信息就集中到了通道信息,輸入通道擴充了四倍。拼接起來的特征層相對于原先的三通道變成了十二個通道,下圖很好的展示了Focus結構,一看就能明白。
class Focus(nn.Module): def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super(Focus, self).__init__() self.conv = Conv(c1 * 4, c2, k, s, p, g, act) def forward(self, x): return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))
4、使用了SiLU激活函數,SiLU是Sigmoid和ReLU的改進版。SiLU具備無上界有下界、平滑、非單調的特性。SiLU在深層模型上的效果優于 ReLU。可以看做是平滑的ReLU激活函數。
class SiLU(nn.Module): @staticmethod def forward(x): return x * torch.sigmoid(x)
5、使用了SPP結構,通過不同池化核大小的最大池化進行特征提取,提高網絡的感受野。在YoloV4中,SPP是用在FPN里面的,在YoloV5中,SPP模塊被用在了主干特征提取網絡中。
class SPP(nn.Module): # Spatial pyramid pooling layer used in YOLOv3-SPP def __init__(self, c1, c2, k=(5, 9, 13)): super(SPP, self).__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1) self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k]) def forward(self, x): x = self.cv1(x) return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))
整個主干實現代碼為:
import torch import torch.nn as nn class SiLU(nn.Module): @staticmethod def forward(x): return x * torch.sigmoid(x) def autopad(k, p=None): if p is None: p = k // 2 if isinstance(k, int) else [x // 2 for x in k] return p class Focus(nn.Module): def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super(Focus, self).__init__() self.conv = Conv(c1 * 4, c2, k, s, p, g, act) def forward(self, x): return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1)) class Conv(nn.Module): def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): super(Conv, self).__init__() self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False) self.bn = nn.BatchNorm2d(c2, eps=0.001, momentum=0.03) self.act = SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity()) def forward(self, x): return self.act(self.bn(self.conv(x))) def fuseforward(self, x): return self.act(self.conv(x)) class Bottleneck(nn.Module): # Standard bottleneck def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion super(Bottleneck, self).__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_, c2, 3, 1, g=g) self.add = shortcut and c1 == c2 def forward(self, x): return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x)) class C3(nn.Module): # CSP Bottleneck with 3 convolutions def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion super(C3, self).__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1) self.cv3 = Conv(2 * c_, c2, 1) # act=FReLU(c2) self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]) # self.m = nn.Sequential(*[CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)]) def forward(self, x): return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1)) class SPP(nn.Module): # Spatial pyramid pooling layer used in YOLOv3-SPP def __init__(self, c1, c2, k=(5, 9, 13)): super(SPP, self).__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1) self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k]) def forward(self, x): x = self.cv1(x) return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1)) class CSPDarknet(nn.Module): def __init__(self, base_channels, base_depth): super().__init__() #-----------------------------------------------# # 輸入圖片是640, 640, 3 # 初始的基本通道是64 #-----------------------------------------------# #-----------------------------------------------# # 利用focus網絡結構進行特征提取 # 640, 640, 3 -> 320, 320, 12 -> 320, 320, 64 #-----------------------------------------------# self.stem = Focus(3, base_channels, k=3) #-----------------------------------------------# # 完成卷積之后,320, 320, 64 -> 160, 160, 128 # 完成CSPlayer之后,160, 160, 128 -> 160, 160, 128 #-----------------------------------------------# self.dark2 = nn.Sequential( Conv(base_channels, base_channels * 2, 3, 2), C3(base_channels * 2, base_channels * 2, base_depth), ) #-----------------------------------------------# # 完成卷積之后,160, 160, 128 -> 80, 80, 256 # 完成CSPlayer之后,80, 80, 256 -> 80, 80, 256 #-----------------------------------------------# self.dark3 = nn.Sequential( Conv(base_channels * 2, base_channels * 4, 3, 2), C3(base_channels * 4, base_channels * 4, base_depth * 3), ) #-----------------------------------------------# # 完成卷積之后,80, 80, 256 -> 40, 40, 512 # 完成CSPlayer之后,40, 40, 512 -> 40, 40, 512 #-----------------------------------------------# self.dark4 = nn.Sequential( Conv(base_channels * 4, base_channels * 8, 3, 2), C3(base_channels * 8, base_channels * 8, base_depth * 3), ) #-----------------------------------------------# # 完成卷積之后,40, 40, 512 -> 20, 20, 1024 # 完成SPP之后,20, 20, 1024 -> 20, 20, 1024 # 完成CSPlayer之后,20, 20, 1024 -> 20, 20, 1024 #-----------------------------------------------# self.dark5 = nn.Sequential( Conv(base_channels * 8, base_channels * 16, 3, 2), SPP(base_channels * 16, base_channels * 16), C3(base_channels * 16, base_channels * 16, base_depth, shortcut=False), ) def forward(self, x): x = self.stem(x) x = self.dark2(x) #-----------------------------------------------# # dark3的輸出為80, 80, 256,是一個有效特征層 #-----------------------------------------------# x = self.dark3(x) feat1 = x #-----------------------------------------------# # dark4的輸出為40, 40, 512,是一個有效特征層 #-----------------------------------------------# x = self.dark4(x) feat2 = x #-----------------------------------------------# # dark5的輸出為20, 20, 1024,是一個有效特征層 #-----------------------------------------------# x = self.dark5(x) feat3 = x return feat1, feat2, feat3
在特征利用部分,YoloV5提取多特征層進行目標檢測,一共提取三個特征層。
三個特征層位于主干部分CSPdarknet的不同位置,分別位于中間層,中下層,底層,當輸入為(640,640,3)的時候,三個特征層的shape分別為feat1=(80,80,256)、feat2=(40,40,512)、feat3=(20,20,1024)。
在獲得三個有效特征層后,我們利用這三個有效特征層進行FPN層的構建,構建方式為:
feat3=(20,20,1024)的特征層進行1次1X1卷積調整通道后獲得P5,P5進行上采樣UmSampling2d后與feat2=(40,40,512)特征層進行結合,然后使用CSPLayer進行特征提取獲得P5_upsample,此時獲得的特征層為(40,40,512)。
P5_upsample=(40,40,512)的特征層進行1次1X1卷積調整通道后獲得P4,P4進行上采樣UmSampling2d后與feat1=(80,80,256)特征層進行結合,然后使用CSPLayer進行特征提取P3_out,此時獲得的特征層為(80,80,256)。
P3_out=(80,80,256)的特征層進行一次3x3卷積進行下采樣,下采樣后與P4堆疊,然后使用CSPLayer進行特征提取P4_out,此時獲得的特征層為(40,40,512)。
P4_out=(40,40,512)的特征層進行一次3x3卷積進行下采樣,下采樣后與P5堆疊,然后使用CSPLayer進行特征提取P5_out,此時獲得的特征層為(20,20,1024)。
特征金字塔可以將不同shape的特征層進行特征融合,有利于提取出更好的特征。
import torch import torch.nn as nn from nets.CSPdarknet import CSPDarknet, C3, Conv #---------------------------------------------------# # yolo_body #---------------------------------------------------# class YoloBody(nn.Module): def __init__(self, anchors_mask, num_classes, phi): super(YoloBody, self).__init__() depth_dict = {'s' : 0.33, 'm' : 0.67, 'l' : 1.00, 'x' : 1.33,} width_dict = {'s' : 0.50, 'm' : 0.75, 'l' : 1.00, 'x' : 1.25,} dep_mul, wid_mul = depth_dict[phi], width_dict[phi] base_channels = int(wid_mul * 64) # 64 base_depth = max(round(dep_mul * 3), 1) # 3 #-----------------------------------------------# # 輸入圖片是640, 640, 3 # 初始的基本通道是64 #-----------------------------------------------# #---------------------------------------------------# # 生成CSPdarknet53的主干模型 # 獲得三個有效特征層,他們的shape分別是: # 80,80,256 # 40,40,512 # 20,20,1024 #---------------------------------------------------# self.backbone = CSPDarknet(base_channels, base_depth) self.upsample = nn.Upsample(scale_factor=2, mode="nearest") self.conv_for_feat3 = Conv(base_channels * 16, base_channels * 8, 1, 1) self.conv3_for_upsample1 = C3(base_channels * 16, base_channels * 8, base_depth, shortcut=False) self.conv_for_feat2 = Conv(base_channels * 8, base_channels * 4, 1, 1) self.conv3_for_upsample2 = C3(base_channels * 8, base_channels * 4, base_depth, shortcut=False) self.down_sample1 = Conv(base_channels * 4, base_channels * 4, 3, 2) self.conv3_for_downsample1 = C3(base_channels * 8, base_channels * 8, base_depth, shortcut=False) self.down_sample2 = Conv(base_channels * 8, base_channels * 8, 3, 2) self.conv3_for_downsample2 = C3(base_channels * 16, base_channels * 16, base_depth, shortcut=False) self.yolo_head_P3 = nn.Conv2d(base_channels * 4, len(anchors_mask[2]) * (5 + num_classes), 1) self.yolo_head_P4 = nn.Conv2d(base_channels * 8, len(anchors_mask[1]) * (5 + num_classes), 1) self.yolo_head_P5 = nn.Conv2d(base_channels * 16, len(anchors_mask[0]) * (5 + num_classes), 1) def forward(self, x): # backbone feat1, feat2, feat3 = self.backbone(x) P5 = self.conv_for_feat3(feat3) P5_upsample = self.upsample(P5) P4 = torch.cat([P5_upsample, feat2], 1) P4 = self.conv3_for_upsample1(P4) P4 = self.conv_for_feat2(P4) P4_upsample = self.upsample(P4) P3 = torch.cat([P4_upsample, feat1], 1) P3 = self.conv3_for_upsample2(P3) P3_downsample = self.down_sample1(P3) P4 = torch.cat([P3_downsample, P4], 1) P4 = self.conv3_for_downsample1(P4) P4_downsample = self.down_sample2(P4) P5 = torch.cat([P4_downsample, P5], 1) P5 = self.conv3_for_downsample2(P5) #---------------------------------------------------# # 第三個特征層 # y3=(batch_size,75,80,80) #---------------------------------------------------# out2 = self.yolo_head_P3(P3) #---------------------------------------------------# # 第二個特征層 # y2=(batch_size,75,40,40) #---------------------------------------------------# out1 = self.yolo_head_P4(P4) #---------------------------------------------------# # 第一個特征層 # y1=(batch_size,75,20,20) #---------------------------------------------------# out0 = self.yolo_head_P5(P5) return out0, out1, out2
3、利用Yolo Head獲得預測結果
利用FPN特征金字塔,我們可以獲得三個加強特征,這三個加強特征的shape分別為(20,20,1024)、(40,40,512)、(80,80,256),然后我們利用這三個shape的特征層傳入Yolo Head獲得預測結果。
對于每一個特征層,我們可以獲得利用一個卷積調整通道數,最終的通道數和需要區分的種類個數相關,在YoloV5里,每一個特征層上每一個特征點存在3個先驗框。
如果使用的是voc訓練集,類則為20種,最后的維度應該為75 = 3x25,三個特征層的shape為(20,20,75),(40,40,75),(80,80,75)。
最后的75可以拆分成3個25,對應3個先驗框的25個參數,25可以拆分成4+1+20。
前4個參數用于判斷每一個特征點的回歸參數,回歸參數調整后可以獲得預測框;
第5個參數用于判斷每一個特征點是否包含物體;
最后20個參數用于判斷每一個特征點所包含的物體種類。
如果使用的是coco訓練集,類則為80種,最后的維度應該為255 = 3x85,三個特征層的shape為(20,20,255),(40,40,255),(80,80,255)
最后的255可以拆分成3個85,對應3個先驗框的85個參數,85可以拆分成4+1+80。
前4個參數用于判斷每一個特征點的回歸參數,回歸參數調整后可以獲得預測框;
第5個參數用于判斷每一個特征點是否包含物體;
最后80個參數用于判斷每一個特征點所包含的物體種類。
實現代碼如下:
import torch import torch.nn as nn from nets.CSPdarknet import CSPDarknet, C3, Conv #---------------------------------------------------# # yolo_body #---------------------------------------------------# class YoloBody(nn.Module): def __init__(self, anchors_mask, num_classes, phi): super(YoloBody, self).__init__() depth_dict = {'s' : 0.33, 'm' : 0.67, 'l' : 1.00, 'x' : 1.33,} width_dict = {'s' : 0.50, 'm' : 0.75, 'l' : 1.00, 'x' : 1.25,} dep_mul, wid_mul = depth_dict[phi], width_dict[phi] base_channels = int(wid_mul * 64) # 64 base_depth = max(round(dep_mul * 3), 1) # 3 #-----------------------------------------------# # 輸入圖片是640, 640, 3 # 初始的基本通道是64 #-----------------------------------------------# #---------------------------------------------------# # 生成CSPdarknet53的主干模型 # 獲得三個有效特征層,他們的shape分別是: # 80,80,256 # 40,40,512 # 20,20,1024 #---------------------------------------------------# self.backbone = CSPDarknet(base_channels, base_depth) self.upsample = nn.Upsample(scale_factor=2, mode="nearest") self.conv_for_feat3 = Conv(base_channels * 16, base_channels * 8, 1, 1) self.conv3_for_upsample1 = C3(base_channels * 16, base_channels * 8, base_depth, shortcut=False) self.conv_for_feat2 = Conv(base_channels * 8, base_channels * 4, 1, 1) self.conv3_for_upsample2 = C3(base_channels * 8, base_channels * 4, base_depth, shortcut=False) self.down_sample1 = Conv(base_channels * 4, base_channels * 4, 3, 2) self.conv3_for_downsample1 = C3(base_channels * 8, base_channels * 8, base_depth, shortcut=False) self.down_sample2 = Conv(base_channels * 8, base_channels * 8, 3, 2) self.conv3_for_downsample2 = C3(base_channels * 16, base_channels * 16, base_depth, shortcut=False) self.yolo_head_P3 = nn.Conv2d(base_channels * 4, len(anchors_mask[2]) * (5 + num_classes), 1) self.yolo_head_P4 = nn.Conv2d(base_channels * 8, len(anchors_mask[1]) * (5 + num_classes), 1) self.yolo_head_P5 = nn.Conv2d(base_channels * 16, len(anchors_mask[0]) * (5 + num_classes), 1) def forward(self, x): # backbone feat1, feat2, feat3 = self.backbone(x) P5 = self.conv_for_feat3(feat3) P5_upsample = self.upsample(P5) P4 = torch.cat([P5_upsample, feat2], 1) P4 = self.conv3_for_upsample1(P4) P4 = self.conv_for_feat2(P4) P4_upsample = self.upsample(P4) P3 = torch.cat([P4_upsample, feat1], 1) P3 = self.conv3_for_upsample2(P3) P3_downsample = self.down_sample1(P3) P4 = torch.cat([P3_downsample, P4], 1) P4 = self.conv3_for_downsample1(P4) P4_downsample = self.down_sample2(P4) P5 = torch.cat([P4_downsample, P5], 1) P5 = self.conv3_for_downsample2(P5) #---------------------------------------------------# # 第三個特征層 # y3=(batch_size,75,80,80) #---------------------------------------------------# out2 = self.yolo_head_P3(P3) #---------------------------------------------------# # 第二個特征層 # y2=(batch_size,75,40,40) #---------------------------------------------------# out1 = self.yolo_head_P4(P4) #---------------------------------------------------# # 第一個特征層 # y1=(batch_size,75,20,20) #---------------------------------------------------# out0 = self.yolo_head_P5(P5) return out0, out1, out2
由第二步我們可以獲得三個特征層的預測結果,shape分別為(N,20,20,255),(N,40,40,255),(N,80,80,255)的數據。
但是這個預測結果并不對應著最終的預測框在圖片上的位置,還需要解碼才可以完成。在YoloV5里,每一個特征層上每一個特征點存在3個先驗框。
每個特征層最后的255可以拆分成3個85,對應3個先驗框的85個參數,我們先將其reshape一下,其結果為(N,20,20,3,85),(N,40.40,3,85),(N,80,80,3,85)。
其中的85可以拆分成4+1+80。
前4個參數用于判斷每一個特征點的回歸參數,回歸參數調整后可以獲得預測框;
第5個參數用于判斷每一個特征點是否包含物體;
最后80個參數用于判斷每一個特征點所包含的物體種類。
以(N,20,20,3,85)這個特征層為例,該特征層相當于將圖像劃分成20x20個特征點,如果某個特征點落在物體的對應框內,就用于預測該物體。
如圖所示,藍色的點為20x20的特征點,此時我們對左圖黑色點的三個先驗框進行解碼操作演示:
1、進行中心預測點的計算,利用Regression預測結果前兩個序號的內容對特征點的三個先驗框中心坐標進行偏移,偏移后是右圖紅色的三個點;
2、進行預測框寬高的計算,利用Regression預測結果后兩個序號的內容求指數后獲得預測框的寬高;
3、此時獲得的預測框就可以繪制在圖片上了。
除去這樣的解碼操作,還有非極大抑制的操作需要進行,防止同一種類的框的堆積。
def decode_box(self, inputs): outputs = [] for i, input in enumerate(inputs): #-----------------------------------------------# # 輸入的input一共有三個,他們的shape分別是 # batch_size, 255, 20, 20 # batch_size, 255, 40, 40 # batch_size, 255, 80, 80 #-----------------------------------------------# batch_size = input.size(0) input_height = input.size(2) input_width = input.size(3) #-----------------------------------------------# # 輸入為416x416時 # stride_h = stride_w = 32、16、8 #-----------------------------------------------# stride_h = self.input_shape[0] / input_height stride_w = self.input_shape[1] / input_width #-------------------------------------------------# # 此時獲得的scaled_anchors大小是相對于特征層的 #-------------------------------------------------# scaled_anchors = [(anchor_width / stride_w, anchor_height / stride_h) for anchor_width, anchor_height in self.anchors[self.anchors_mask[i]]] #-----------------------------------------------# # 輸入的input一共有三個,他們的shape分別是 # batch_size, 3, 20, 20, 85 # batch_size, 3, 40, 40, 85 # batch_size, 3, 80, 80, 85 #-----------------------------------------------# prediction = input.view(batch_size, len(self.anchors_mask[i]), self.bbox_attrs, input_height, input_width).permute(0, 1, 3, 4, 2).contiguous() #-----------------------------------------------# # 先驗框的中心位置的調整參數 #-----------------------------------------------# x = torch.sigmoid(prediction[..., 0]) y = torch.sigmoid(prediction[..., 1]) #-----------------------------------------------# # 先驗框的寬高調整參數 #-----------------------------------------------# w = torch.sigmoid(prediction[..., 2]) h = torch.sigmoid(prediction[..., 3]) #-----------------------------------------------# # 獲得置信度,是否有物體 #-----------------------------------------------# conf = torch.sigmoid(prediction[..., 4]) #-----------------------------------------------# # 種類置信度 #-----------------------------------------------# pred_cls = torch.sigmoid(prediction[..., 5:]) FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor #----------------------------------------------------------# # 生成網格,先驗框中心,網格左上角 # batch_size,3,20,20 #----------------------------------------------------------# grid_x = torch.linspace(0, input_width - 1, input_width).repeat(input_height, 1).repeat( batch_size * len(self.anchors_mask[i]), 1, 1).view(x.shape).type(FloatTensor) grid_y = torch.linspace(0, input_height - 1, input_height).repeat(input_width, 1).t().repeat( batch_size * len(self.anchors_mask[i]), 1, 1).view(y.shape).type(FloatTensor) #----------------------------------------------------------# # 按照網格格式生成先驗框的寬高 # batch_size,3,20,20 #----------------------------------------------------------# anchor_w = FloatTensor(scaled_anchors).index_select(1, LongTensor([0])) anchor_h = FloatTensor(scaled_anchors).index_select(1, LongTensor([1])) anchor_w = anchor_w.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(w.shape) anchor_h = anchor_h.repeat(batch_size, 1).repeat(1, 1, input_height * input_width).view(h.shape) #----------------------------------------------------------# # 利用預測結果對先驗框進行調整 # 首先調整先驗框的中心,從先驗框中心向右下角偏移 # 再調整先驗框的寬高。 #----------------------------------------------------------# pred_boxes = FloatTensor(prediction[..., :4].shape) pred_boxes[..., 0] = x.data * 2. - 0.5 + grid_x pred_boxes[..., 1] = y.data * 2. - 0.5 + grid_y pred_boxes[..., 2] = (w.data * 2) ** 2 * anchor_w pred_boxes[..., 3] = (h.data * 2) ** 2 * anchor_h #----------------------------------------------------------# # 將輸出結果歸一化成小數的形式 #----------------------------------------------------------# _scale = torch.Tensor([input_width, input_height, input_width, input_height]).type(FloatTensor) output = torch.cat((pred_boxes.view(batch_size, -1, 4) / _scale, conf.view(batch_size, -1, 1), pred_cls.view(batch_size, -1, self.num_classes)), -1) outputs.append(output.data) return outputs
得到最終的預測結果后還要進行得分排序與非極大抑制篩選。
得分篩選就是篩選出得分滿足confidence置信度的預測框。
非極大抑制就是篩選出一定區域內屬于同一種類得分最大的框。
得分篩選與非極大抑制的過程可以概括如下:
1、找出該圖片中得分大于門限函數的框。在進行重合框篩選前就進行得分的篩選可以大幅度減少框的數量。
2、對種類進行循環,非極大抑制的作用是篩選出一定區域內屬于同一種類得分最大的框,對種類進行循環可以幫助我們對每一個類分別進行非極大抑制。
3、根據得分對該種類進行從大到小排序。
4、每次取出得分最大的框,計算其與其它所有預測框的重合程度,重合程度過大的則剔除。
得分篩選與非極大抑制后的結果就可以用于繪制預測框了。
下圖是經過非極大抑制的。
下圖是未經過非極大抑制的。
實現代碼為:
def non_max_suppression(self, prediction, num_classes, input_shape, image_shape, letterbox_image, conf_thres=0.5, nms_thres=0.4): #----------------------------------------------------------# # 將預測結果的格式轉換成左上角右下角的格式。 # prediction [batch_size, num_anchors, 85] #----------------------------------------------------------# box_corner = prediction.new(prediction.shape) box_corner[:, :, 0] = prediction[:, :, 0] - prediction[:, :, 2] / 2 box_corner[:, :, 1] = prediction[:, :, 1] - prediction[:, :, 3] / 2 box_corner[:, :, 2] = prediction[:, :, 0] + prediction[:, :, 2] / 2 box_corner[:, :, 3] = prediction[:, :, 1] + prediction[:, :, 3] / 2 prediction[:, :, :4] = box_corner[:, :, :4] output = [None for _ in range(len(prediction))] for i, image_pred in enumerate(prediction): #----------------------------------------------------------# # 對種類預測部分取max。 # class_conf [num_anchors, 1] 種類置信度 # class_pred [num_anchors, 1] 種類 #----------------------------------------------------------# class_conf, class_pred = torch.max(image_pred[:, 5:5 + num_classes], 1, keepdim=True) #----------------------------------------------------------# # 利用置信度進行第一輪篩選 #----------------------------------------------------------# conf_mask = (image_pred[:, 4] * class_conf[:, 0] >= conf_thres).squeeze() #----------------------------------------------------------# # 根據置信度進行預測結果的篩選 #----------------------------------------------------------# image_pred = image_pred[conf_mask] class_conf = class_conf[conf_mask] class_pred = class_pred[conf_mask] if not image_pred.size(0): continue #-------------------------------------------------------------------------# # detections [num_anchors, 7] # 7的內容為:x1, y1, x2, y2, obj_conf, class_conf, class_pred #-------------------------------------------------------------------------# detections = torch.cat((image_pred[:, :5], class_conf.float(), class_pred.float()), 1) #------------------------------------------# # 獲得預測結果中包含的所有種類 #------------------------------------------# unique_labels = detections[:, -1].cpu().unique() if prediction.is_cuda: unique_labels = unique_labels.cuda() detections = detections.cuda() for c in unique_labels: #------------------------------------------# # 獲得某一類得分篩選后全部的預測結果 #------------------------------------------# detections_class = detections[detections[:, -1] == c] #------------------------------------------# # 使用官方自帶的非極大抑制會速度更快一些! #------------------------------------------# keep = nms( detections_class[:, :4], detections_class[:, 4] * detections_class[:, 5], nms_thres ) max_detections = detections_class[keep] # # 按照存在物體的置信度排序 # _, conf_sort_index = torch.sort(detections_class[:, 4]*detections_class[:, 5], descending=True) # detections_class = detections_class[conf_sort_index] # # 進行非極大抑制 # max_detections = [] # while detections_class.size(0): # # 取出這一類置信度最高的,一步一步往下判斷,判斷重合程度是否大于nms_thres,如果是則去除掉 # max_detections.append(detections_class[0].unsqueeze(0)) # if len(detections_class) == 1: # break # ious = bbox_iou(max_detections[-1], detections_class[1:]) # detections_class = detections_class[1:][ious < nms_thres] # # 堆疊 # max_detections = torch.cat(max_detections).data # Add max detections to outputs output[i] = max_detections if output[i] is None else torch.cat((output[i], max_detections)) if output[i] is not None: output[i] = output[i].cpu().numpy() box_xy, box_wh = (output[i][:, 0:2] + output[i][:, 2:4])/2, output[i][:, 2:4] - output[i][:, 0:2] output[i][:, :4] = self.yolo_correct_boxes(box_xy, box_wh, input_shape, image_shape, letterbox_image) return output
計算loss實際上是網絡的預測結果和網絡的真實結果的對比。
和網絡的預測結果一樣,網絡的損失也由三個部分組成,分別是Reg部分、Obj部分、Cls部分。Reg部分是特征點的回歸參數判斷、Obj部分是特征點是否包含物體判斷、Cls部分是特征點包含的物體的種類。
在YoloV5中,訓練時正樣本的匹配過程可以分為兩部分。
a、匹配先驗框。
b、匹配特征點。
所謂正樣本匹配,就是尋找哪些先驗框被認為有對應的真實框,并且負責這個真實框的預測。
在YoloV5網絡中,一共設計了9個不同大小的先驗框。每個輸出的特征層對應3個先驗框。
對于任何一個真實框gt,YoloV5不再使用iou進行正樣本的匹配,而是直接采用高寬比進行匹配,即使用真實框和9個不同大小的先驗框計算寬高比。
如果真實框與某個先驗框的寬高比例大于設定閾值,則說明該真實框和該先驗框匹配度不夠,將該先驗框認為是負樣本。
比如此時有一個真實框,它的寬高為[200, 200],是一個正方形。YoloV5默認設置的9個先驗框為[10,13], [16,30], [33,23], [30,61], [62,45], [59,119], [116,90], [156,198], [373,326]。設定閾值門限為4。
此時我們需要計算該真實框和9個先驗框的寬高比例。比較寬高時存在兩個情況,一個是真實框的寬高比先驗框大,一個是先驗框的寬高比真實框大。因此我們需要同時計算:真實框的寬高/先驗框的寬高;先驗框的寬高/真實框的寬高。然后在這其中選取最大值。
下個列表就是比較結果,這是一個shape為[9, 4]的矩陣,9代表9個先驗框,4代表真實框的寬高/先驗框的寬高;先驗框的寬高/真實框的寬高。
[[20. 15.38461538 0.05 0.065 ] [12.5 6.66666667 0.08 0.15 ] [ 6.06060606 8.69565217 0.165 0.115 ] [ 6.66666667 3.27868852 0.15 0.305 ] [ 3.22580645 4.44444444 0.31 0.225 ] [ 3.38983051 1.68067227 0.295 0.595 ] [ 1.72413793 2.22222222 0.58 0.45 ] [ 1.28205128 1.01010101 0.78 0.99 ] [ 0.53619303 0.61349693 1.865 1.63 ]]
然后對每個先驗框的比較結果取最大值。獲得下述矩陣:
[20. 12.5 8.69565217 6.66666667 4.44444444 3.38983051 2.22222222 1.28205128 1.865 ]
之后我們判斷,哪些先驗框的比較結果的值小于門限。可以知道[59,119], [116,90], [156,198], [373,326]四個先驗框均滿足需求。
[116,90], [156,198], [373,326]屬于20,20的特征層。
[59,119]屬于40,40的特征層。
此時我們已經可以判斷哪些大小的先驗框可用于該真實框的預測。
在過去的Yolo系列中,每個真實框由其中心點所在的網格內的左上角特征點來負責預測。
對于被選中的特征層,首先計算真實框落在哪個網格內,此時該網格左上角特征點便是一個負責預測的特征點。
同時利用四舍五入規則,找出最近的兩個網格,將這三個網格都認為是負責預測該真實框的。
紅色點表示該真實框的中心,除了當前所處的網格外,其2個最近的鄰域網格也被選中。從這里就可以發現預測框的XY軸偏移部分的取值范圍不再是0-1,而是0.5-1.5。
找到對應特征點后,對應特征點在a中被選中的先驗框負責該真實框的預測。
由第一部分可知,YoloV5的損失由三個部分組成:
1、Reg部分,由第2部分可知道每個真實框對應的先驗框,獲取到每個框對應的先驗框后,取出該先驗框對應的預測框,利用真實框和預測框計算CIOU損失,作為Reg部分的Loss組成。
2、Obj部分,由第2部分可知道每個真實框對應的先驗框,所有真實框對應的先驗框都是正樣本,剩余的先驗框均為負樣本,根據正負樣本和特征點的是否包含物體的預測結果計算交叉熵損失,作為Obj部分的Loss組成。
3、Cls部分,由第三部分可知道每個真實框對應的先驗框,獲取到每個框對應的先驗框后,取出該先驗框的種類預測結果,根據真實框的種類和先驗框的種類預測結果計算交叉熵損失,作為Cls部分的Loss組成。
import torch import torch.nn as nn import math import numpy as np class YOLOLoss(nn.Module): def __init__(self, anchors, num_classes, input_shape, cuda, anchors_mask = [[6,7,8], [3,4,5], [0,1,2]], label_smoothing = 0): super(YOLOLoss, self).__init__() #-----------------------------------------------------------# # 13x13的特征層對應的anchor是[142, 110],[192, 243],[459, 401] # 26x26的特征層對應的anchor是[36, 75],[76, 55],[72, 146] # 52x52的特征層對應的anchor是[12, 16],[19, 36],[40, 28] #-----------------------------------------------------------# self.anchors = anchors self.num_classes = num_classes self.bbox_attrs = 5 + num_classes self.input_shape = input_shape self.anchors_mask = anchors_mask self.label_smoothing = label_smoothing self.threshold = 4 self.balance = [0.4, 1.0, 4] self.box_ratio = 5 self.cls_ratio = 0.5 self.obj_ratio = 1 self.cuda = cuda def clip_by_tensor(self, t, t_min, t_max): t = t.float() result = (t >= t_min).float() * t + (t < t_min).float() * t_min result = (result <= t_max).float() * result + (result > t_max).float() * t_max return result def MSELoss(self, pred, target): return torch.pow(pred - target, 2) def BCELoss(self, pred, target): epsilon = 1e-7 pred = self.clip_by_tensor(pred, epsilon, 1.0 - epsilon) output = - target * torch.log(pred) - (1.0 - target) * torch.log(1.0 - pred) return output def box_giou(self, b1, b2): """ 輸入為: ---------- b1: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh b2: tensor, shape=(batch, feat_w, feat_h, anchor_num, 4), xywh 返回為: ------- giou: tensor, shape=(batch, feat_w, feat_h, anchor_num, 1) """ #----------------------------------------------------# # 求出預測框左上角右下角 #----------------------------------------------------# b1_xy = b1[..., :2] b1_wh = b1[..., 2:4] b1_wh_half = b1_wh/2. b1_mins = b1_xy - b1_wh_half b1_maxes = b1_xy + b1_wh_half #----------------------------------------------------# # 求出真實框左上角右下角 #----------------------------------------------------# b2_xy = b2[..., :2] b2_wh = b2[..., 2:4] b2_wh_half = b2_wh/2. b2_mins = b2_xy - b2_wh_half b2_maxes = b2_xy + b2_wh_half #----------------------------------------------------# # 求真實框和預測框所有的iou #----------------------------------------------------# intersect_mins = torch.max(b1_mins, b2_mins) intersect_maxes = torch.min(b1_maxes, b2_maxes) intersect_wh = torch.max(intersect_maxes - intersect_mins, torch.zeros_like(intersect_maxes)) intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1] b1_area = b1_wh[..., 0] * b1_wh[..., 1] b2_area = b2_wh[..., 0] * b2_wh[..., 1] union_area = b1_area + b2_area - intersect_area iou = intersect_area / union_area #----------------------------------------------------# # 找到包裹兩個框的最小框的左上角和右下角 #----------------------------------------------------# enclose_mins = torch.min(b1_mins, b2_mins) enclose_maxes = torch.max(b1_maxes, b2_maxes) enclose_wh = torch.max(enclose_maxes - enclose_mins, torch.zeros_like(intersect_maxes)) #----------------------------------------------------# # 計算對角線距離 #----------------------------------------------------# enclose_area = enclose_wh[..., 0] * enclose_wh[..., 1] giou = iou - (enclose_area - union_area) / enclose_area return giou #---------------------------------------------------# # 平滑標簽 #---------------------------------------------------# def smooth_labels(self, y_true, label_smoothing, num_classes): return y_true * (1.0 - label_smoothing) + label_smoothing / num_classes def forward(self, l, input, targets=None): #----------------------------------------------------# # l 代表使用的是第幾個有效特征層 # input的shape為 bs, 3*(5+num_classes), 13, 13 # bs, 3*(5+num_classes), 26, 26 # bs, 3*(5+num_classes), 52, 52 # targets 真實框的標簽情況 [batch_size, num_gt, 5] #----------------------------------------------------# #--------------------------------# # 獲得圖片數量,特征層的高和寬 #--------------------------------# bs = input.size(0) in_h = input.size(2) in_w = input.size(3) #-----------------------------------------------------------------------# # 計算步長 # 每一個特征點對應原來的圖片上多少個像素點 # # 如果特征層為13x13的話,一個特征點就對應原來的圖片上的32個像素點 # 如果特征層為26x26的話,一個特征點就對應原來的圖片上的16個像素點 # 如果特征層為52x52的話,一個特征點就對應原來的圖片上的8個像素點 # stride_h = stride_w = 32、16、8 #-----------------------------------------------------------------------# stride_h = self.input_shape[0] / in_h stride_w = self.input_shape[1] / in_w #-------------------------------------------------# # 此時獲得的scaled_anchors大小是相對于特征層的 #-------------------------------------------------# scaled_anchors = [(a_w / stride_w, a_h / stride_h) for a_w, a_h in self.anchors] #-----------------------------------------------# # 輸入的input一共有三個,他們的shape分別是 # bs, 3 * (5+num_classes), 13, 13 => bs, 3, 5 + num_classes, 13, 13 => batch_size, 3, 13, 13, 5 + num_classes # batch_size, 3, 13, 13, 5 + num_classes # batch_size, 3, 26, 26, 5 + num_classes # batch_size, 3, 52, 52, 5 + num_classes #-----------------------------------------------# prediction = input.view(bs, len(self.anchors_mask[l]), self.bbox_attrs, in_h, in_w).permute(0, 1, 3, 4, 2).contiguous() #-----------------------------------------------# # 先驗框的中心位置的調整參數 #-----------------------------------------------# x = torch.sigmoid(prediction[..., 0]) y = torch.sigmoid(prediction[..., 1]) #-----------------------------------------------# # 先驗框的寬高調整參數 #-----------------------------------------------# w = torch.sigmoid(prediction[..., 2]) h = torch.sigmoid(prediction[..., 3]) #-----------------------------------------------# # 獲得置信度,是否有物體 #-----------------------------------------------# conf = torch.sigmoid(prediction[..., 4]) #-----------------------------------------------# # 種類置信度 #-----------------------------------------------# pred_cls = torch.sigmoid(prediction[..., 5:]) #-----------------------------------------------# # 獲得網絡應該有的預測結果 #-----------------------------------------------# y_true, noobj_mask, box_loss_scale = self.get_target(l, targets, scaled_anchors, in_h, in_w) #---------------------------------------------------------------# # 將預測結果進行解碼,判斷預測結果和真實值的重合程度 # 如果重合程度過大則忽略,因為這些特征點屬于預測比較準確的特征點 # 作為負樣本不合適 #----------------------------------------------------------------# pred_boxes = self.get_pred_boxes(l, x, y, h, w, targets, scaled_anchors, in_h, in_w) if self.cuda: y_true = y_true.cuda() noobj_mask = noobj_mask.cuda() box_loss_scale = box_loss_scale.cuda() #-----------------------------------------------------------# # reshape_y_true[...,2:3]和reshape_y_true[...,3:4] # 表示真實框的寬高,二者均在0-1之間 # 真實框越大,比重越小,小框的比重更大。 #-----------------------------------------------------------# box_loss_scale = 2 - box_loss_scale #---------------------------------------------------------------# # 計算預測結果和真實結果的giou #----------------------------------------------------------------# giou = self.box_giou(pred_boxes[y_true[..., 4] == 1], y_true[..., :4][y_true[..., 4] == 1]) loss_loc = torch.sum((1 - giou) * box_loss_scale[y_true[..., 4] == 1]) #-----------------------------------------------------------# # 計算置信度的loss #-----------------------------------------------------------# loss_conf = torch.sum(self.BCELoss(conf[y_true[..., 4] == 1], giou.detach().clamp(0))) + \ torch.sum(self.BCELoss(conf, y_true[..., 4]) * noobj_mask) loss_cls = torch.sum(self.BCELoss(pred_cls[y_true[..., 4] == 1], self.smooth_labels(y_true[..., 5:][y_true[..., 4] == 1], self.label_smoothing, self.num_classes))) loss = loss_loc * self.box_ratio + loss_conf * self.balance[l] * self.obj_ratio + loss_cls * self.cls_ratio num_pos = torch.sum(y_true[..., 4]) num_pos = torch.max(num_pos, torch.ones_like(num_pos)) return loss, num_pos def get_near_points(self, x, y, i, j): sub_x = x - i sub_y = y - j if sub_x > 0.5 and sub_y > 0.5: return [[0, 0], [1, 0], [0, 1]] elif sub_x < 0.5 and sub_y > 0.5: return [[0, 0], [-1, 0], [0, 1]] elif sub_x < 0.5 and sub_y < 0.5: return [[0, 0], [-1, 0], [0, -1]] else: return [[0, 0], [1, 0], [0, -1]] def get_target(self, l, targets, anchors, in_h, in_w): #-----------------------------------------------------# # 計算一共有多少張圖片 #-----------------------------------------------------# bs = len(targets) #-----------------------------------------------------# # 用于選取哪些先驗框不包含物體 #-----------------------------------------------------# noobj_mask = torch.ones(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False) #-----------------------------------------------------# # 讓網絡更加去關注小目標 #-----------------------------------------------------# box_loss_scale = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False) #-----------------------------------------------------# # anchors_best_ratio #-----------------------------------------------------# box_best_ratio = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, requires_grad = False) #-----------------------------------------------------# # batch_size, 3, 13, 13, 5 + num_classes #-----------------------------------------------------# y_true = torch.zeros(bs, len(self.anchors_mask[l]), in_h, in_w, self.bbox_attrs, requires_grad = False) for b in range(bs): if len(targets[b])==0: continue batch_target = torch.zeros_like(targets[b]) #-------------------------------------------------------# # 計算出正樣本在特征層上的中心點 #-------------------------------------------------------# batch_target[:, [0,2]] = targets[b][:, [0,2]] * in_w batch_target[:, [1,3]] = targets[b][:, [1,3]] * in_h batch_target[:, 4] = targets[b][:, 4] batch_target = batch_target.cpu() #-------------------------------------------------------# # batch_target : num_true_box, 4 # anchors : 9, 2 # # ratios_of_gt_anchors : num_true_box, 9, 2 # ratios_of_anchors_gt : num_true_box, 9, 2 # # ratios : num_true_box, 9, 4 # max_ratios : num_true_box, 9 #-------------------------------------------------------# ratios_of_gt_anchors = torch.unsqueeze(batch_target[:, 2:4], 1) / torch.unsqueeze(torch.FloatTensor(anchors), 0) ratios_of_anchors_gt = torch.unsqueeze(torch.FloatTensor(anchors), 0) / torch.unsqueeze(batch_target[:, 2:4], 1) ratios = torch.cat([ratios_of_gt_anchors, ratios_of_anchors_gt], dim = -1) max_ratios, _ = torch.max(ratios, dim = -1) for t, ratio in enumerate(max_ratios): #-------------------------------------------------------# # ratio : 9 #-------------------------------------------------------# over_threshold = ratio < self.threshold over_threshold[torch.argmin(ratio)] = True for k, mask in enumerate(self.anchors_mask[l]): if not over_threshold[mask]: continue #----------------------------------------# # 獲得真實框屬于哪個網格點 #----------------------------------------# i = torch.floor(batch_target[t, 0]).long() j = torch.floor(batch_target[t, 1]).long() offsets = self.get_near_points(batch_target[t, 0], batch_target[t, 1], i, j) for offset in offsets: local_i = i + offset[0] local_j = j + offset[1] if local_i >= in_w or local_i < 0 or local_j >= in_h or local_j < 0: continue if box_best_ratio[b, k, local_j, local_i] != 0: if box_best_ratio[b, k, local_j, local_i] > ratio[mask]: y_true[b, k, local_j, local_i, :] = 0 else: continue #----------------------------------------# # 取出真實框的種類 #----------------------------------------# c = batch_target[t, 4].long() #----------------------------------------# # noobj_mask代表無目標的特征點 #----------------------------------------# noobj_mask[b, k, local_j, local_i] = 0 #----------------------------------------# # tx、ty代表中心調整參數的真實值 #----------------------------------------# y_true[b, k, local_j, local_i, 0] = batch_target[t, 0] y_true[b, k, local_j, local_i, 1] = batch_target[t, 1] y_true[b, k, local_j, local_i, 2] = batch_target[t, 2] y_true[b, k, local_j, local_i, 3] = batch_target[t, 3] y_true[b, k, local_j, local_i, 4] = 1 y_true[b, k, local_j, local_i, c + 5] = 1 #----------------------------------------# # 用于獲得xywh的比例 # 大目標loss權重小,小目標loss權重大 #----------------------------------------# box_loss_scale[b, k, local_j, local_i] = batch_target[t, 2] * batch_target[t, 3] / in_w / in_h #----------------------------------------# # 獲得當前先驗框最好的比例 #----------------------------------------# box_best_ratio[b, k, local_j, local_i] = ratio[mask] return y_true, noobj_mask, box_loss_scale def get_pred_boxes(self, l, x, y, h, w, targets, scaled_anchors, in_h, in_w): #-----------------------------------------------------# # 計算一共有多少張圖片 #-----------------------------------------------------# bs = len(targets) FloatTensor = torch.cuda.FloatTensor if x.is_cuda else torch.FloatTensor LongTensor = torch.cuda.LongTensor if x.is_cuda else torch.LongTensor #-----------------------------------------------------# # 生成網格,先驗框中心,網格左上角 #-----------------------------------------------------# grid_x = torch.linspace(0, in_w - 1, in_w).repeat(in_h, 1).repeat( int(bs * len(self.anchors_mask[l])), 1, 1).view(x.shape).type(FloatTensor) grid_y = torch.linspace(0, in_h - 1, in_h).repeat(in_w, 1).t().repeat( int(bs * len(self.anchors_mask[l])), 1, 1).view(y.shape).type(FloatTensor) # 生成先驗框的寬高 scaled_anchors_l = np.array(scaled_anchors)[self.anchors_mask[l]] anchor_w = FloatTensor(scaled_anchors_l).index_select(1, LongTensor([0])) anchor_h = FloatTensor(scaled_anchors_l).index_select(1, LongTensor([1])) anchor_w = anchor_w.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(w.shape) anchor_h = anchor_h.repeat(bs, 1).repeat(1, 1, in_h * in_w).view(h.shape) #-------------------------------------------------------# # 計算調整后的先驗框中心與寬高 #-------------------------------------------------------# pred_boxes_x = torch.unsqueeze(x * 2. - 0.5 + grid_x, -1) pred_boxes_y = torch.unsqueeze(y * 2. - 0.5 + grid_y, -1) pred_boxes_w = torch.unsqueeze((w * 2) ** 2 * anchor_w, -1) pred_boxes_h = torch.unsqueeze((h * 2) ** 2 * anchor_h, -1) pred_boxes = torch.cat([pred_boxes_x, pred_boxes_y, pred_boxes_w, pred_boxes_h], dim = -1) return pred_boxes
首先前往Github下載對應的倉庫,下載完后利用解壓軟件解壓,之后用編程軟件打開文件夾。
注意打開的根目錄必須正確,否則相對目錄不正確的情況下,代碼將無法運行。
一定要注意打開后的根目錄是文件存放的目錄。
本文使用VOC格式進行訓練,訓練前需要自己制作好數據集,如果沒有自己的數據集,可以通過Github連接下載VOC12+07的數據集嘗試下。
訓練前將標簽文件放在VOCdevkit文件夾下的VOC2007文件夾下的Annotation中。
訓練前將圖片文件放在VOCdevkit文件夾下的VOC2007文件夾下的JPEGImages中。
此時數據集的擺放已經結束。
在完成數據集的擺放之后,我們需要對數據集進行下一步的處理,目的是獲得訓練用的2007_train.txt以及2007_val.txt,需要用到根目錄下的voc_annotation.py。
voc_annotation.py里面有一些參數需要設置。
分別是annotation_mode、classes_path、trainval_percent、train_percent、VOCdevkit_path,第一次訓練可以僅修改classes_path
''' annotation_mode用于指定該文件運行時計算的內容 annotation_mode為0代表整個標簽處理過程,包括獲得VOCdevkit/VOC2007/ImageSets里面的txt以及訓練用的2007_train.txt、2007_val.txt annotation_mode為1代表獲得VOCdevkit/VOC2007/ImageSets里面的txt annotation_mode為2代表獲得訓練用的2007_train.txt、2007_val.txt ''' annotation_mode = 0 ''' 必須要修改,用于生成2007_train.txt、2007_val.txt的目標信息 與訓練和預測所用的classes_path一致即可 如果生成的2007_train.txt里面沒有目標信息 那么就是因為classes沒有設定正確 僅在annotation_mode為0和2的時候有效 ''' classes_path = 'model_data/voc_classes.txt' ''' trainval_percent用于指定(訓練集+驗證集)與測試集的比例,默認情況下 (訓練集+驗證集):測試集 = 9:1 train_percent用于指定(訓練集+驗證集)中訓練集與驗證集的比例,默認情況下 訓練集:驗證集 = 9:1 僅在annotation_mode為0和1的時候有效 ''' trainval_percent = 0.9 train_percent = 0.9 ''' 指向VOC數據集所在的文件夾 默認指向根目錄下的VOC數據集 ''' VOCdevkit_path = 'VOCdevkit'
classes_path用于指向檢測類別所對應的txt,以voc數據集為例,我們用的txt為:
訓練自己的數據集時,可以自己建立一個cls_classes.txt,里面寫自己所需要區分的類別。
通過voc_annotation.py我們已經生成了2007_train.txt以及2007_val.txt,此時我們可以開始訓練了。
訓練的參數較多,大家可以在下載庫后仔細看注釋,其中最重要的部分依然是train.py里的classes_path。
classes_path用于指向檢測類別所對應的txt,這個txt和voc_annotation.py里面的txt一樣!訓練自己的數據集必須要修改!
修改完classes_path后就可以運行train.py開始訓練了,在訓練多個epoch后,權值會生成在logs文件夾中。
其它參數的作用如下:
#-------------------------------# # 是否使用Cuda # 沒有GPU可以設置成False #-------------------------------# Cuda = True #--------------------------------------------------------# # 訓練前一定要修改classes_path,使其對應自己的數據集 #--------------------------------------------------------# classes_path = 'model_data/voc_classes.txt' #---------------------------------------------------------------------# # anchors_path代表先驗框對應的txt文件,一般不修改。 # anchors_mask用于幫助代碼找到對應的先驗框,一般不修改。 #---------------------------------------------------------------------# anchors_path = 'model_data/yolo_anchors.txt' anchors_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]] #----------------------------------------------------------------------------------------------------------------------------# # 權值文件的下載請看README,可以通過網盤下載。模型的 預訓練權重 對不同數據集是通用的,因為特征是通用的。 # 模型的 預訓練權重 比較重要的部分是 主干特征提取網絡的權值部分,用于進行特征提取。 # 預訓練權重對于99%的情況都必須要用,不用的話主干部分的權值太過隨機,特征提取效果不明顯,網絡訓練的結果也不會好 # # 如果訓練過程中存在中斷訓練的操作,可以將model_path設置成logs文件夾下的權值文件,將已經訓練了一部分的權值再次載入。 # 同時修改下方的 凍結階段 或者 解凍階段 的參數,來保證模型epoch的連續性。 # # 當model_path = ''的時候不加載整個模型的權值。 # # 此處使用的是整個模型的權重,因此是在train.py進行加載的。 # 如果想要讓模型從0開始訓練,則設置model_path = '',下面的Freeze_Train = Fasle,此時從0開始訓練,且沒有凍結主干的過程。 # 一般來講,從0開始訓練效果會很差,因為權值太過隨機,特征提取效果不明顯。 # # 網絡一般不從0開始訓練,至少會使用主干部分的權值,有些論文提到可以不用預訓練,主要原因是他們 數據集較大 且 調參能力優秀。 # 如果一定要訓練網絡的主干部分,可以了解imagenet數據集,首先訓練分類模型,分類模型的 主干部分 和該模型通用,基于此進行訓練。 #----------------------------------------------------------------------------------------------------------------------------# model_path = 'model_data/yolov5_s.pth' #------------------------------------------------------# # 輸入的shape大小,一定要是32的倍數 #------------------------------------------------------# input_shape = [640, 640] #------------------------------------------------------# # 所使用的YoloV5的版本。s、m、l、x #------------------------------------------------------# phi = 's' #------------------------------------------------------# # Yolov4的tricks應用 # mosaic 馬賽克數據增強 True or False # 實際測試時mosaic數據增強并不穩定,所以默認為False # Cosine_lr 余弦退火學習率 True or False # label_smoothing 標簽平滑 0.01以下一般 如0.01、0.005 #------------------------------------------------------# mosaic = False Cosine_lr = False label_smoothing = 0 #----------------------------------------------------# # 訓練分為兩個階段,分別是凍結階段和解凍階段。 # 顯存不足與數據集大小無關,提示顯存不足請調小batch_size。 # 受到BatchNorm層影響,batch_size最小為2,不能為1。 #----------------------------------------------------# #----------------------------------------------------# # 凍結階段訓練參數 # 此時模型的主干被凍結了,特征提取網絡不發生改變 # 占用的顯存較小,僅對網絡進行微調 #----------------------------------------------------# Init_Epoch = 0 Freeze_Epoch = 50 Freeze_batch_size = 16 Freeze_lr = 1e-3 #----------------------------------------------------# # 解凍階段訓練參數 # 此時模型的主干不被凍結了,特征提取網絡會發生改變 # 占用的顯存較大,網絡所有的參數都會發生改變 #----------------------------------------------------# UnFreeze_Epoch = 100 Unfreeze_batch_size = 8 Unfreeze_lr = 1e-4 #------------------------------------------------------# # 是否進行凍結訓練,默認先凍結主干訓練后解凍訓練。 #------------------------------------------------------# Freeze_Train = True #------------------------------------------------------# # 用于設置是否使用多線程讀取數據 # 開啟后會加快數據讀取速度,但是會占用更多內存 # 內存較小的電腦可以設置為2或者0 #------------------------------------------------------# num_workers = 4 #----------------------------------------------------# # 獲得圖片路徑和標簽 #----------------------------------------------------# train_annotation_path = '2007_train.txt' val_annotation_path = '2007_val.txt'
訓練結果預測需要用到兩個文件,分別是yolo.py和predict.py。
我們首先需要去yolo.py里面修改model_path以及classes_path,這兩個參數必須要修改。
model_path指向訓練好的權值文件,在logs文件夾里。
classes_path指向檢測類別所對應的txt。
完成修改后就可以運行predict.py進行檢測了。運行后輸入圖片路徑即可檢測。
感謝各位的閱讀,以上就是“Pytorch搭建YoloV5目標檢測平臺實現的方法”的內容了,經過本文的學習后,相信大家對Pytorch搭建YoloV5目標檢測平臺實現的方法這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。