YOLO V3详解(一):网络结构介绍
YOLO V3详解(二):输出介绍
YOLO V3详解(三):损失介绍
YOLO V3详解(四):进行目标检测
DarkNet53: YOLO v3中的Backbone
在学习Yolo v3时,不知道有没有小伙伴对它的输出感觉很疑惑。以20类分类任务为例:为什么输出的是13*13*75、26*26*75以及52*52*75的。这些最后是如何计算损失函数以及与论文中说的偏移、先验框又有什么关系呢?
对于这些问题,统一在这篇文章中对其进行介绍。
如果对YOLO v3了解不深入的建议先阅读这篇文章:YOLO V3详解(一):网络结构介绍。
在YOLO V3中,输出信息需要包括 t x t_x tx、 t y t_y ty、 t w t_w tw、 t h t_h th、置信度以及各个类别的概率。然而,在YOLO V3中,我们用的不是一个先验框。对于每一种尺度的特征提取(无论是13*13,还是52*52),每种尺度都使用了三种先验框。因此,最终的输出的个数应为:(5+类别数)*3。因此,对于20类的任务,最终输出通道为75。对于80类的任务,最终输出通道为255。
对于上面所说的输出 t x t_x tx、 t y t_y ty、 t w t_w tw、 t h t_h th、置信度以及各个类别的概率,可以通过对YOLO v3网络得到的输出进行处理得到。具体处理代码如下所示:
# input为输出部分 这里以batch_size, 75, 13, 13 为例
batch_size = input.size(0)
# self.anchors_mask[i] 指的是先验框的id 这里指的是[6,7,8] 即self.anchors_mask[i] = [6,7,8]
self.out_num = input.size(1)/len(self.anchors_mask[i])
input_height = input.size(2)
input_width = input.size(3)
#这里是为了将input 变换为[batch_size,3,13,13,25]的数据
prediction = input.view(batch_size, len(self.anchors_mask[i]),
self.out_num, input_height, input_width).permute(0, 1, 3, 4, 2).contiguous()
# 第一个维度为tx,第二个维度为ty
x = prediction[..., 0]
y = prediction[..., 1]
# 分别得到tw 和 th 以及置信度
w = prediction[..., 2]
h = prediction[..., 3]
conf = torch.sigmoid(prediction[..., 4])
#得到各个类别的概率
pred_cls = prediction[..., 5:]
虽然上面得到了 t x t_x tx、 t y t_y ty、 t w t_w tw、 t h t_h th、置信度以及各个类别的概率, 但是如何得到 c x c_x cx、 c y c_y cy、 p w p_w pw、 p h p_h ph的数值?
上文得到的 t x t_x tx、 t y t_y ty等内容均为一个(batch_size, 3, 13, 13)的数组。以batch_size=1为例, t x t_x tx指的是相对于中心点的偏移,如何得到中心点的坐标 c x c_x cx、 c y c_y cy呢?这里就考虑到了作者论文中所说的:以每个像素点为中心点绘制先验框。因此,这里的 t x t_x tx 指的是相对于这个像素点的偏移,而这个像素点指的是左下角的点。
如上图,左下角的点为(0,0),此时 c x c_x cx = 0、 c y c_y cy = 0。因此,我们需要创建一个形如 t x t_x tx、 t y t_y ty 的矩阵来得到像素点的具体位置。得到的矩阵如下所示:
tensor([[[[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
...
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12]],
[[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
...
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12]],
[[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
...
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12],
[0., 1., 2., 3., 4. ... 12]]]]) (batch_size, 3, 13, 13)
同样地,先验框的宽高的倍数 p w p_w pw、 p h p_h ph 也是同样生成这样大小的数据。
需要注意的是:在偏移时,其各个通道的数值是一致的。而对于宽高的倍数而言,其是不同的,三个通道分别表示三个不同的先验框。
最后,以Pytorch实现的代码来实现上述所有的内容。如果对内容有疑惑的可以与代码相结合来进行研究。
import torch
import numpy as np
class DecodeBox():
def __init__(self, anchors, num_classes, input_shape, anchors_mask = [[6,7,8], [3,4,5], [0,1,2]]):
super(DecodeBox, self).__init__()
self.anchors = anchors
self.num_classes = num_classes # int 20
self.bbox_attrs = 5 + num_classes # int 25
self.input_shape = input_shape # (416, 416) 元组
#-----------------------------------------------------------#
# 13x13的特征层对应的anchor是[116,90],[156,198],[373,326]
# 26x26的特征层对应的anchor是[30,61],[62,45],[59,119]
# 52x52的特征层对应的anchor是[10,13],[16,30],[33,23]
#-----------------------------------------------------------#
self.anchors_mask = anchors_mask
# ----------------------------------------------#
# 得到out0、out1、out2不同尺度下每个网格点上的的预测情况(预测框位置、类别概率、置信度分数)
# ----------------------------------------------#
def decode_box(self, inputs): # input一共有三组数据,out0,out1,out2
outputs = []
for i, input in enumerate(inputs): # 一次只能对一个特征层的输出进行解码操作
# -----------------------------------------------#
# 输入的input一共有三个,他们的shape分别是 针对voc数据集
# batch_size, 75, 13, 13 batch_size, channels, weight, height
# batch_size, 75, 26, 26
# batch_size, 75, 52, 52
# -----------------------------------------------#
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 # 输出特征图和resize之后的原图上对应步长,映射回去的操作
stride_w = self.input_shape[1] / input_width
#-------------------------------------------------#
# 把先验框的尺寸调整成特征层的大小形式,用来对应两者宽和高
# 此时获得的scaled_anchors大小是相对于特征层的,anchors是大数据kmeans聚类经验所得
# out0越小,stride越大,用来检测大目标
#-------------------------------------------------#
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, 13, 13, 25
# batch_size, 3, 26, 26, 25
# batch_size, 3, 52, 52, 25
# batch_size,3*(5+num_classes),13,13 -> batch_size,3,5+num_classes,13,13 -> batch_size, 3, 13, 13, 25
#-----------------------------------------------#
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 shape: torch.size([batch_size,3,13,13])
# y shape: torch.size([batch_size,3,13,13])
#-----------------------------------------------#
x = torch.sigmoid(prediction[..., 0]) # sigmoid可以把输出值固定到0~1之间
y = torch.sigmoid(prediction[..., 1]) # 先验框中心点的调整只能在其右下角的网格里面
#-----------------------------------------------#
# 先验框的宽高调整参数
#-----------------------------------------------#
w = prediction[..., 2]
h = 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
#----------------------------------------------------------#
# 生成网格,先验框中心=网格左上角
# grid_x shape:torch.size([batch_size,3,13,13])
# grid_y shape:torch.size([batch_size,3,13,13])
# 关于该行代码解读,详细参考本文第3节
#----------------------------------------------------------#
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,13,13
# 关于该行代码解读,详细参考本文第4节
#----------------------------------------------------------#
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 + grid_x
pred_boxes[..., 1] = y.data + grid_y
pred_boxes[..., 2] = torch.exp(w.data) * anchor_w
pred_boxes[..., 3] = torch.exp(h.data) * 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 # 得到out0、out1、out2不同尺度下每个网格点上的的预测情况(预测框位置、类别概率、置信度分数)
if __name__ == '__main__':
anchors = [10.0, 13.0, 16.0, 30.0, 33.0, 23.0, 30.0, 61.0, 62.0, 45.0, 59.0, 119.0, 116.0, 90.0, 156.0, 198.0, 373.0, 326.0]
# anchors: ndarray:(9, 2)
anchors = np.array(anchors).reshape(-1,2)
num_classes = 20 # voc类别个数
anchors_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]]
input_shape = [416,416]
bbox_util = DecodeBox(anchors, num_classes, (input_shape[0], input_shape[1]), anchors_mask)
# ---------------------------------------------------------#
# 将图像输入网络当中进行预测!
# ---------------------------------------------------------#
net = YoloBody(anchors_mask, num_classes) # 此地YoloBody可见https://blog.csdn.net/qq_36758270/article/details/130117432?spm=1001.2014.3001.5502
outputs = net(images) # 此地images表示输入图片,outputs为三个输出out0, out1, out2
outputs = bbox_util.decode_box(outputs) # 得到out0、out1、out2不同尺度下每个网格点上的预测情况(预测框位置、类别概率、置信度分数)
最后,补充两个小例子来分别介绍生成网格代码、生成先验框的宽高代码。
先验框中心=网格左上角,下面这行代码到底如何理解呢?
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)
以宽为5,高为5, batch_size为1为例,详细解读见下方代码及输出。
import torch
if __name__ == "__main__":
input_width = 5
input_height = 5
batch_size = 1
anchors_mask = [[6,7,8], [3,4,5], [0,1,2]]
a = torch.linspace(0, input_width - 1, input_width) # torch.linspace左闭右闭
print(a) # 输出一个张量列表
"""
tensor([0., 1., 2., 3., 4.])
"""
b = a.repeat(input_height, 1)
print(b)
"""
tensor([[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]])
"""
c = b.repeat(batch_size * 3, 1, 1) # len(anchors_mask[i]) = 3
print(c)
"""
tensor([[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]],
[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]],
[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]]])
"""
d = c.view(batch_size, 3, input_height, input_width) # 对已知的进行reshape
print(d)
"""
tensor([[[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]],
[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]],
[[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.],
[0., 1., 2., 3., 4.]]]])
"""
e = d.type(FloatTensor) # 数据类型
按照网格格式生成先验框的宽高,其代码如下:
#----------------------------------------------------------#
# 按照网格格式生成先验框的宽高
# batch_size,3,13,13
#----------------------------------------------------------#
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)
对于上面这四行代码,我们以最小特征层为例,详细理解:
import torch
if __name__ == "__main__":
#-----------------------------------------------------------------------------#
# 把先验框的尺寸调整成特征层的大小形式,用来对应两者宽和高
# 此时获得的scaled_anchors大小是相对于特征层的,anchors是大数据kmeans聚类经验所得
# out0越小,stride越大,用来检测大目标
# 此以最小特征层为例,batch_size, 75, 13, 13
#-----------------------------------------------------------------------------#
scaled_anchors = [(3.625,2.8125), (4.875,6.1875), (11.65625, 10.1875)]
x_is_cuda = False # x.is_cuda = False,表示没用cuda
FloatTensor = torch.cuda.FloatTensor if x_is_cuda else torch.FloatTensor
LongTensor = torch.cuda.LongTensor if x_is_cuda else torch.LongTensor
# ------------------------------#
# 解读第 1 行anchor_w
# ------------------------------#
a = LongTensor([0])
print(a) # tensor([0])
b = FloatTensor(scaled_anchors)
print(b) # 保留的小数点位数变了
"""
tensor([[ 3.6250, 2.8125],
[ 4.8750, 6.1875],
[11.6562, 10.1875]])
"""
# ----------------------------------------------------------#
# tensor.index_select(dim, index)
# dim :表示要查找的维度,对于二维,0代表行,1代表列
# index:表示要索引的序列,是一个tensor对象
# a = tensor([0]),表示要索引的为宽
# a = tensor([1]),表示要索引的为高
# ----------------------------------------------------------#
anchor_w = b.index_select(1, a)
print(anchor_w) # anchor_w shape: torch.size([3,1])
"""
tensor([[ 3.6250],
[ 4.8750],
[11.6562]])
"""
# ------------------------------#
# 解读第 2 行anchor_h
# 类似上面
# ------------------------------#
anchor_h = b.index_select(1, LongTensor([1]))
"""
tensor([[ 2.8125],
[ 6.1875],
[10.1875]])
"""
# ----------------------------------------------------#
# 解读第 3 行anchor_w
# w.shape 和 h.shape: torch.size([1,3,13,13])
# ----------------------------------------------------#
batch_size = 1 # 以batch_size=1为例
input_height = 13 # 最小特征层输出,宽高均为13
input_width = 13
# ------------------------------------#
# tensor.repeat(dim1,dim2,...)
# 复制多个tensor
# ------------------------------------#
c = anchor_w.repeat(batch_size, 1)
print(c)
"""
tensor([[ 3.6250],
[ 4.8750],
[11.6562]])
若batch_size = 2, c 的结果:
tensor([[ 3.6250],
[ 4.8750],
[11.6562],
[ 3.6250],
[ 4.8750],
[11.6562]])
毕竟有几张图片,先验框的宽,参数个数就应该有几倍,每张图片都有
"""
d = c.repeat(1, 1, input_height * input_width)
print(d.shape) # torch.Size([1, 3, 169])
# ---------------------------------------------------#
# 每个像素点,都有三个先验框,每个先验框,都有宽
# 有点各用各的,的感觉
# ---------------------------------------------------#
anchor_w = d.view(1,3,13,13)
print(anchor_w.shape) # torch.Size([1, 3, 13, 13]),先验框的宽就都生成了,高类似
虽然上述解决了部分问题,得到了先验框、置信度以及各个类别的概率。但是,关于损失如何计算、先验框数值的计算还未介绍。这些内容会在后续进行介绍。