前言:上次講了YOLOv3中的模型構建,從頭到尾理了一遍從cfg讀取到模型整個構建的過程。其中模型構建中最重要的YOLOLayer還沒有梳理,本文將從代碼的角度理解YOLOLayer的構建與實現。
1. Grid創建
YOLOv3是一個單階段的目標檢測器,將目標划分為不同的grid,每個grid分配3個anchor作為先驗框來進行匹配。首先讀一下代碼中關於grid創建的部分。
首先了解一下pytorch中的API:torch.mershgrid
舉一個簡單的例子就比較清楚了:
Python 3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> a = torch.arange(3)
>>> b = torch.arange(5)
>>> x,y = torch.meshgrid(a,b)
>>> a
tensor([0, 1, 2])
>>> b
tensor([0, 1, 2, 3, 4])
>>> x
tensor([[0, 0, 0, 0, 0],
[1, 1, 1, 1, 1],
[2, 2, 2, 2, 2]])
>>> y
tensor([[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4]])
>>>
單純看輸入輸出,可能不是很明白,列舉一個例子:
>>> for i in range(3):
... for j in range(4):
... print("(", x[i,j], "," ,y[i,j],")")
...
( tensor(0) , tensor(0) )
( tensor(0) , tensor(1) )
( tensor(0) , tensor(2) )
( tensor(0) , tensor(3) )
( tensor(1) , tensor(0) )
( tensor(1) , tensor(1) )
( tensor(1) , tensor(2) )
( tensor(1) , tensor(3) )
( tensor(2) , tensor(0) )
( tensor(2) , tensor(1) )
( tensor(2) , tensor(2) )
( tensor(2) , tensor(3) )
>>> torch.stack((x,y),2)
tensor([[[0, 0],
[0, 1],
[0, 2],
[0, 3],
[0, 4]],
[[1, 0],
[1, 1],
[1, 2],
[1, 3],
[1, 4]],
[[2, 0],
[2, 1],
[2, 2],
[2, 3],
[2, 4]]])
>>>
現在就比較清楚了,划分了3×4的網格,通過遍歷得到的x和y就能遍歷全部格子。
下面是yolov3中提供的代碼(需要注意的是這是針對某一層YOLOLayer,而不是所有的YOLOLayer):
def create_grids(self,
img_size=416,
ng=(13, 13),
device='cpu',
type=torch.float32):
nx, ny = ng # 網格尺寸
self.img_size = max(img_size)
#下采樣倍數為32
self.stride = self.img_size / max(ng)
# 划分網格,構建相對左上角的偏移量
yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
# 通過以上例子很容易理解
self.grid_xy = torch.stack((xv, yv), 2).to(device).type(type).view(
(1, 1, ny, nx, 2))
# 處理anchor,將其除以下采樣倍數
self.anchor_vec = self.anchors.to(device) / self.stride
self.anchor_wh = self.anchor_vec.view(1, self.na, 1, 1,
2).to(device).type(type)
self.ng = torch.Tensor(ng).to(device)
self.nx = nx
self.ny = ny
2. YOLOLayer
在之前的文章中講過,YOLO層前一層卷積層的filter個數具有特殊的要求,計算方法為:
如下圖所示:
訓練過程:
YOLOLayer的作用就是對上一個卷積層得到的張量進行處理,具體可以看training過程涉及的代碼(暫時不關心ONNX部分的代碼):
class YOLOLayer(nn.Module):
def __init__(self, anchors, nc, img_size, yolo_index, arc):
super(YOLOLayer, self).__init__()
self.anchors = torch.Tensor(anchors)
self.na = len(anchors) # 該YOLOLayer分配給每個grid的anchor的個數
self.nc = nc # 類別個數
self.no = nc + 5 # 每個格子對應輸出的維度 class + 5 中5代表x,y,w,h,conf
self.nx = 0 # 初始化x方向上的格子數量
self.ny = 0 # 初始化y方向上的格子數量
self.arc = arc
if ONNX_EXPORT: # grids must be computed in __init__
stride = [32, 16, 8][yolo_index] # stride of this layer
nx = int(img_size[1] / stride) # number x grid points
ny = int(img_size[0] / stride) # number y grid points
create_grids(self, img_size, (nx, ny))
def forward(self, p, img_size, var=None):
'''
onnx代表開放式神經網絡交換
pytorch中的模型都可以導出或轉換為標准ONNX格式
在模型采用ONNX格式后,即可在各種平台和設備上運行
在這里ONNX代表規范化的推理過程
'''
if ONNX_EXPORT:
bs = 1 # batch size
else:
bs, _, ny, nx = p.shape # bs, 255, 13, 13
if (self.nx, self.ny) != (nx, ny):
create_grids(self, img_size, (nx, ny), p.device, p.dtype)
# p.view(bs, 255, 13, 13) -- > (bs, 3, 13, 13, 85)
# (bs, anchors, grid, grid, classes + xywh)
p = p.view(bs, self.na, self.no, self.ny,
self.nx).permute(0, 1, 3, 4, 2).contiguous()
if self.training:
return p
在理解以上代碼的時候,需要理解每一個通道所代表的意義,原先的P是由上一層卷積得到的feature map, 形狀為(以80個類別、輸入416、下采樣32倍為例):【batch size, anchor×(80+5), 13, 13】,在訓練的過程中,將feature map通過張量操作轉化的形狀為:【batch size, anchor, 13, 13, 85】。
測試過程:
# p的形狀目前為:【bs, anchor_num, gridx,gridy,xywhc+class】
else: # 測試推理過程
# s = 1.5 # scale_xy (pxy = pxy * s - (s - 1) / 2)
io = p.clone() # 測試過程輸出就是io
io[..., :2] = torch.sigmoid(io[..., :2]) + self.grid_xy # xy
# grid_xy是左上角再加上偏移量io[...:2]代表xy偏移
io[..., 2:4] = torch.exp(
io[..., 2:4]) * self.anchor_wh # wh yolo method
# io[..., 2:4] = ((torch.sigmoid(io[..., 2:4]) * 2) ** 3) * self.anchor_wh
# wh power method
io[..., :4] *= self.stride
if 'default' in self.arc: # seperate obj and cls
torch.sigmoid_(io[..., 4])
elif 'BCE' in self.arc: # unified BCE (80 classes)
torch.sigmoid_(io[..., 5:])
io[..., 4] = 1
elif 'CE' in self.arc: # unified CE (1 background + 80 classes)
io[..., 4:] = F.softmax(io[..., 4:], dim=4)
io[..., 4] = 1
if self.nc == 1:
io[..., 5] = 1
# single-class model https://github.com/ultralytics/yolov3/issues/235
# reshape from [1, 3, 13, 13, 85] to [1, 507, 85]
return io.view(bs, -1, self.no), p
理解以上內容是需要對應以下公式:
xy部分:
\(c_x, c_y\)代表的是格子的左上角坐標;\(t_x, t_y\)代表的是網絡預測的結果;\(\sigma\)代表sigmoid激活函數。對應代碼理解:
io[..., :2] = torch.sigmoid(io[..., :2]) + self.grid_xy # xy
# grid_xy是左上角再加上偏移量io[...:2]代表xy偏移
wh部分:
\(p_w, p_h\)代表的是anchor先驗框在feature map上對應的大小。\(t_w, t_h\)代表的是網絡學習得到的縮放系數。對應代碼理解:
# wh yolo method
io[..., 2:4] = torch.exp(io[..., 2:4]) * self.anchor_wh
class部分:
在類別部分,提供了幾種方法,根據arc參數來進行不同模式的選擇。以CE(crossEntropy)為例:
#io: (bs, anchors, grid, grid, xywh+classes)
io[..., 4:] = F.softmax(io[..., 4:], dim=4)# 使用softmax
io[..., 4] = 1
3. 參考資料
pytorch的官方API