动手学CV-目标检测入门教程4:模型结构

3.4 模型结构

本文来自开源组织 DataWhale 🐳 CV小组创作的目标检测入门教程。

对应开源项目 《动手学CV-Pytorch》 的第3章的内容,教程中涉及的代码也可以在项目中找到,后续会持续更新更多的优质内容,欢迎⭐️。

如果使用我们教程的内容或图片,请在文章醒目位置注明我们的github主页链接:https://github.com/datawhalechina/dive-into-cv-pytorch

本章教程所介绍的网络,后面我们称其为Tiny_Detector,是为了本教程特意设计的网络,而并不是某个经典的目标检测网络。如果一定要溯源的话,由于代码是由一个外国的开源SSD教程改编而来,因此很多细节上也更接近SSD网络,可以认为是一个简化后的版本,目的是帮助大家更好的入门。

那么下面,我们就开始介绍Tiny_Detector的模型结构

3.4.1 VGG16作为backbone

为了使结构简单易懂,我们使用VGG16作为backbone,即完全采用vgg16的结构作为特征提取模块,只是去掉fc6和fc7两个全连接层。如图3-17所示:

在这里插入图片描述

图3-17 Tiny-Detector的backbone

对于网络的输入尺寸的确定,由于vgg16的ImageNet预训练模型是使用224x224尺寸训练的,因此我们的网络输入也固定为224x224,和预训练模型尺度保持一致可以更好的发挥其作用。通常来说,这样的网络输入大小,对于检测网络来说还是偏小,在完整的进行完本章的学习后,不妨尝试下将输入尺度扩大,看看会不会带来更好的效果。

特征提取模块对应代码模块在model.py中的VGGBase类进行了定义:

class VGGBase(nn.Module):                                                                                                                                         """VGG base convolutions to produce feature maps.完全采用vgg16的结构作为特征提取模块,丢掉fc6和fc7两个全连接层。因为vgg16的ImageNet预训练模型是使用224×224尺寸训练的,因此我们的网络输入也固定为224×224"""def __init__(self):super(VGGBase, self).__init__()# Standard convolutional layers in VGG16self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)  # stride = 1, by defaultself.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)    # 224->112self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)    # 112->56self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)    # 56->28self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)    # 28->14self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)self.pool5 = nn.MaxPool2d(kernel_size=2, stride=2)    # 14->7# Load pretrained weights on ImageNetself.load_pretrained_layers()def forward(self, image):"""Forward propagation.:param image: images, a tensor of dimensions (N, 3, 224, 224):return: feature maps pool5"""out = F.relu(self.conv1_1(image))  # (N, 64, 224, 224)out = F.relu(self.conv1_2(out))  # (N, 64, 224, 224)out = self.pool1(out)  # (N, 64, 112, 112)out = F.relu(self.conv2_1(out))  # (N, 128, 112, 112)out = F.relu(self.conv2_2(out))  # (N, 128, 112, 112)out = self.pool2(out)  # (N, 128, 56, 56)out = F.relu(self.conv3_1(out))  # (N, 256, 56, 56)out = F.relu(self.conv3_2(out))  # (N, 256, 56, 56)out = F.relu(self.conv3_3(out))  # (N, 256, 56, 56)out = self.pool3(out)  # (N, 256, 28, 28)out = F.relu(self.conv4_1(out))  # (N, 512, 28, 28)out = F.relu(self.conv4_2(out))  # (N, 512, 28, 28)out = F.relu(self.conv4_3(out))  # (N, 512, 28, 28)out = self.pool4(out)  # (N, 512, 14, 14)out = F.relu(self.conv5_1(out))  # (N, 512, 14, 14)out = F.relu(self.conv5_2(out))  # (N, 512, 14, 14)out = F.relu(self.conv5_3(out))  # (N, 512, 14, 14)out = self.pool5(out)  # (N, 512, 7, 7)# return 7*7 feature map                                                                                                                                  return outdef load_pretrained_layers(self):"""we use a VGG-16 pretrained on the ImageNet task as the base network.There's one available in PyTorch, see https://pytorch.org/docs/stable/torchvision/models.html#torchvision.models.vgg16We copy these parameters into our network. It's straightforward for conv1 to conv5."""# Current state of basestate_dict = self.state_dict()param_names = list(state_dict.keys())# Pretrained VGG basepretrained_state_dict = torchvision.models.vgg16(pretrained=True).state_dict()pretrained_param_names = list(pretrained_state_dict.keys())# Transfer conv. parameters from pretrained model to current modelfor i, param in enumerate(param_names):  state_dict[param] = pretrained_state_dict[pretrained_param_names[i]]self.load_state_dict(state_dict)print("\nLoaded base model.\n")

因此,我们的Tiny_Detector特征提取层输出的是7x7的feature map,下面我们要在feature_map上设置对应的先验框,或者说anchor。

关于先验框的概念,上节已经做了介绍,在本实验中,anchor的配置如下:

  • 将原图均匀分成7x7个cell
  • 设置3种不同的尺度:0.2, 0.4, 0.6
  • 设置3种不同的长宽比:1:1, 1:2, 2:1

因此,我们对这 7x7 的 feature map 设置了对应的7x7x9个anchor框,其中每一个cell有9个anchor框,如图3-18所示:

在这里插入图片描述
在这里插入图片描述
图3-18(a)原图,(b)中的9种颜色框代表的是9个anchor框

对于每个anchor,我们需要预测两类信息,一个是这个anchor的类别信息,一个是物体的边界框信息。如图3-19:

在我们的实验中,类别信息由21类别的得分组成(VOC数据集的20个类别 + 一个背景类),模型最终会选择预测得分最高的类作为边界框对象的类别。

而边界框信息是指,我们大致知道了当前anchor中包含一个物体的情况下,如何对anchor进行微调,使得最终能够准确预测出物体的bbox。

在这里插入图片描述

图3-19 Tiny-Detector的输出示例

这两种预测我们分别称为分类头和回归头,那么分类头预测和回归头预测是怎么得到的?

其实我们只需在7x7的feature map后,接上两个3x3的卷积层,即可分别完成分类和回归的预测。

下面我们就对分类头和回归头的更多细节进行介绍。

3.4.2 分类头和回归头

3.4.2.1 边界框的编解码

Tiny_Detector并不是直接预测目标框,而是回归对于anchor要进行多大程度的调整,才能更准确的预测出边界框的位置。那么我们的目标就是需要找一种方法来量化计算这个偏差。

对于一只狗的目标边界框和先验框的示例如下图3-21所示:

在这里插入图片描述

图3-21 目标框和预测框示例

我们的模型要预测anchor与目标框的偏移,并且这个偏移会进行某种形式的归一化,这个过程我们称为边界框的编码。

这里我们使用的是与SSD完全一致的编码方法,具体公示表达如下:

gcx=cx−c^xw^g_{cx}=\frac{c_x-\hat{c}_x}{\hat{w}}gcx=w^cxc^x

gcy=cy−c^yh^g_{cy}=\frac{c_y-\hat{c}_y}{\hat{h}}gcy=h^cyc^y

gw=log(ww^)g_w=log(\frac{w}{\hat{w}})gw=log(w^w)

gh=log(hh^)g_h=log(\frac{h}{\hat{h}})gh=log(h^h)

模型预测并输出的是这个编码后的偏移量(gcx,gcy,gw,ghg_{cx},g_{cy},g_w,g_hgcx,gcy,gw,gh),最终只要再依照公式反向进行解码,就可以得到预测的目标框的信息。

目标框编码与解码的实现位于utils.py中,代码如下:

def cxcy_to_gcxgcy(cxcy, priors_cxcy):""" Encode bounding boxes (that are in center-size form) w.r.t. the corresponding prior boxes (that are in center-size form).For the center coordinates, find the offset with respect to the prior box, and scale by the size of the prior box.For the size coordinates, scale by the size of the prior box, and convert to the log-space.In the model, we are predicting bounding box coordinates in this encoded form.:param cxcy: bounding boxes in center-size coordinates, a tensor of size (n_priors, 4):param priors_cxcy: prior boxes with respect to which the encoding must be performed, a tensor of size (n_priors, 4):return: encoded bounding boxes, a tensor of size (n_priors, 4)"""# The 10 and 5 below are referred to as 'variances' in the original SSD Caffe repo, completely empirical# They are for some sort of numerical conditioning, for 'scaling the localization gradient'# See https://github.com/weiliu89/caffe/issues/155return torch.cat([(cxcy[:, :2] - priors_cxcy[:, :2]) / (priors_cxcy[:, 2:] / 10),  # g_c_x, g_c_ytorch.log(cxcy[:, 2:] / priors_cxcy[:, 2:]) * 5], 1)  # g_w, g_hdef gcxgcy_to_cxcy(gcxgcy, priors_cxcy):""" Decode bounding box coordinates predicted by the model, since they are encoded in the form mentioned above.They are decoded into center-size coordinates.This is the inverse of the function above.:param gcxgcy: encoded bounding boxes, i.e. output of the model, a tensor of size (n_priors, 4):param priors_cxcy: prior boxes with respect to which the encoding is defined, a tensor of size (n_priors, 4):return: decoded bounding boxes in center-size form, a tensor of size (n_priors, 4)"""return torch.cat([gcxgcy[:, :2] * priors_cxcy[:, 2:] / 10 + priors_cxcy[:, :2],  # c_x, c_ytorch.exp(gcxgcy[:, 2:] / 5) * priors_cxcy[:, 2:]], 1)  # w, h

3.4.2.2 分类头与回归头预测

按照前面的介绍,对于输出7x7的feature map上的每个先验框我们想预测:

1)边界框的一组21类分数,其中包括VOC的20类和一个背景类。

2)边界框编码后的偏移量(gcx,gcy,gw,ghg_{cx},g_{cy},g_w,g_hgcx,gcy,gw,gh)。

为了得到我们想预测的类别和偏移量,我们需要在feature map后分别接上两个卷积层:

1)一个分类预测的卷积层采用3x3卷积核padding和stride都为1,每个anchor需要分配21个卷积核,每个位置有9个anchor,因此需要21x9个卷积核。

2)一个定位预测卷积层,每个位置使用3x3卷积核padding和stride都为1,每个anchor需要分配4个卷积核,因此需要4x9个卷积核。

我们直观的看看这些卷积上的输出,见下图3-22:

在这里插入图片描述

图 3-22 Tiny-Detector输出示例

这个回归头和分类头的输出分别用蓝色和黄色表示。其feature map的大小7x7保持不变。我们真正关心的是第三维度通道数,把其具体的展开可以看到如下图3-23所示:

在这里插入图片描述

图3-23 每个cell中9个anchor预测编码偏移量

也就是说,最终回归头的输出有36个通道,其中每4个值就对应了一个anchor的编码后偏移量的预测,这样的4个值的预测共有9组,因此通道数是36。

分类头可以用同样的方式理解,如下图3-24所示:

在这里插入图片描述

图3-24 每个cell中9个anchor预测分类得分

分类头和回归头结构的定义,由 model.py 中的 PredictionConvolutions 类实现,代码如下:

class PredictionConvolutions(nn.Module):""" Convolutions to predict class scores and bounding boxes using feature maps.The bounding boxes (locations) are predicted as encoded offsets w.r.t each of the 441 prior (default) boxes.See 'cxcy_to_gcxgcy' in utils.py for the encoding definition.这里预测坐标的编码方式完全遵循的SSD的定义The class scores represent the scores of each object class in each of the 441 bounding boxes located.A high score for 'background' = no object."""def __init__(self, n_classes):""" :param n_classes: number of different types of objects"""super(PredictionConvolutions, self).__init__()self.n_classes = n_classes# Number of prior-boxes we are considering per position in the feature map# 9 prior-boxes implies we use 9 different aspect ratios, etc.n_boxes = 9 # Localization prediction convolutions (predict offsets w.r.t prior-boxes)self.loc_conv = nn.Conv2d(512, n_boxes * 4, kernel_size=3, padding=1)# Class prediction convolutions (predict classes in localization boxes)self.cl_conv = nn.Conv2d(512, n_boxes * n_classes, kernel_size=3, padding=1)# Initialize convolutions' parametersself.init_conv2d()def init_conv2d(self):"""Initialize convolution parameters."""for c in self.children():if isinstance(c, nn.Conv2d):nn.init.xavier_uniform_(c.weight)nn.init.constant_(c.bias, 0.)def forward(self, pool5_feats):"""Forward propagation.:param pool5_feats: conv4_3 feature map, a tensor of dimensions (N, 512, 7, 7):return: 441 locations and class scores (i.e. w.r.t each prior box) for each image"""batch_size = pool5_feats.size(0)# Predict localization boxes' bounds (as offsets w.r.t prior-boxes)l_conv = self.loc_conv(pool5_feats)  # (N, n_boxes * 4, 7, 7)l_conv = l_conv.permute(0, 2, 3, 1).contiguous()  # (N, 7, 7, n_boxes * 4), to match prior-box order (after .view())# (.contiguous() ensures it is stored in a contiguous chunk of memory, needed for .view() below)locs = l_conv.view(batch_size, -1, 4)  # (N, 441, 4), there are a total 441 boxes on this feature map# Predict classes in localization boxesc_conv = self.cl_conv(pool5_feats)  # (N, n_boxes * n_classes, 7, 7)c_conv = c_conv.permute(0, 2, 3, 1).contiguous()  # (N, 7, 7, n_boxes * n_classes), to match prior-box order (after .view())classes_scores = c_conv.view(batch_size, -1, self.n_classes)  # (N, 441, n_classes), there are a total 441 boxes on this feature mapreturn locs, classes_scores

按照上面的介绍,我们的模型输出的shape应该为:

  • 分类头 batch_size x 7 x 7 x 189
  • 回归头 batch_size x 7 x 7 x 36

但是为了方便后面的处理,我们肯定更希望每个anchor的预测独自成一维,也就是:

  • 分类头 batch_size x 441 x 21
  • 回归头 batch_size x 441 x 4

441是因为我们的模型定义了总共441=7x7x9个先验框,这个转换对应了这两行代码:

locs = l_conv.view(batch_size, -1, 4)

classes_scores = c_conv.view(batch_size, -1, self.n_classes)

这个过程的可视化如图3-25所示。

在这里插入图片描述

图3-25 pool5层输出分类和回归结果

3.4.3 小结

到此,模型前向推理相关的内容就已经都预测完毕了。我们已经了解了模型的结构,以及模型最终会输出什么结果。

下一小节,我们将会学习和模型训练相关的内容,看看如何通过定义损失函数和一些相关的训练技巧,来让模型向着正确的方向学习,从而预测出我们想要的结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/499444.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PID控制器开发笔记之十二:模糊PID控制器的实现

在现实控制中,被控系统并非是线性时不变的,往往需要动态调整PID的参数,而模糊控制正好能够满足这一需求,所以在接下来的这一节我们将讨论模糊PID控制器的相关问题。模糊PID控制器是将模糊算法与PID控制参数的自整定相结合的一种控…

动手学CV-目标检测入门教程5:损失函数

3.5 损失函数 本文来自开源组织 DataWhale 🐳 CV小组创作的目标检测入门教程。 对应开源项目 《动手学CV-Pytorch》 的第3章的内容,教程中涉及的代码也可以在项目中找到,后续会持续更新更多的优质内容,欢迎⭐️。 如果使用我们…

Modbus协议栈开发笔记之四:Modbus TCP Client开发

这一次我们封装Modbus TCP Client应用。同样的我们也不是做具体的应用,而是实现TCP客户端的基本功能。我们将TCP客户端的功能封装为函数,以便在开发具体应用时调用。 对于TCP客户端我们主要实现的功能有两个:其一是生成访问TCP服务器的命令&…

动手学CV-目标检测入门教程6:训练与测试

3.6、训练与测试 本文来自开源组织 DataWhale 🐳 CV小组创作的目标检测入门教程。 对应开源项目 《动手学CV-Pytorch》 的第3章的内容,教程中涉及的代码也可以在项目中找到,后续会持续更新更多的优质内容,欢迎⭐️。 如果使用我…

PC软件开发技术之一:在WinCC中通过VBS操作SQL Server2005

在项目中需要在一定条件满足时,保存一些数据到数据库中,并可根据条件查询。考虑到WinCC6.2以后采用的就是SQL Server2005数据库,所以直接利用该数据库即可,通过SQL Server Management Studio(SSMS)可以创建…

K 近邻算法(KNN)与KD 树实现

KD树节点 /// <summary>/// &#xff2b;&#xff24;树节点/// /2016/4/1安晟添加/// </summary>[Serializable]public class KDTreeNode{/// <summary>/// 获取或设置节点的空间坐标/// </summary>public double[] Position { get; set; }/// <…

PC软件开发技术之二:用C#开发基于自动化接口的OPC客户端

OPC全称是Object Linking and Embedding&#xff08;OLE&#xff09; for Process Control&#xff0c;它的出现为基于Windows的应用程序和现场过程控制应用建立了桥梁。OPC作为一整套接口、属性和方法的协议标准集&#xff0c;与具体的开发语言没有关系。 1、OPC客户端接口方…

标记符控制的分水岭算法原理及matlab实现

-------------------------------------------------------------------------------------------------------------------- 附录A 教程【3】给出的matlab源码&#xff0c;附详细注释 function [ ] MarkerControlled_Watershed_tutorial( ) %标记符控制的分水岭算法教程 …

PC软件开发技术之三:C#操作SQLite数据库

我们在开发应用是经常会需要用到一些数据的存储&#xff0c;存储的方式有多种&#xff0c;使用数据库是一种比较受大家欢迎的方式。但是对于一些小型的应用&#xff0c;如一些移动APP&#xff0c;通常的数据库过于庞大&#xff0c;而轻便的SQLite则能解决这一问题。不但操作方便…

自动搜索数据增强方法分享——fast-autoaugment

前言 简短的介绍下分享fast-autoaugment的原因 毫无疑问数据增强对于训练CNN非常有效&#xff0c;大家也在不断发明新的数据增强方法 拿到一份数据集&#xff0c;我们凭借之前的经验组合不同的增强方法形成一个数据增强策略&#xff0c;通常可以得到一个还不错的baseline。但…

SSD之硬的不能再硬的硬核解析

本文是对经典论文 SSD: Single Shot MultiBox Detector 的解析&#xff0c;耗时3周完成&#xff0c;万字长文&#xff0c;可能是你能看到的最硬核的SSD教程了&#xff0c;如果想一遍搞懂SSD&#xff0c;那就耐心读下去吧~ 一句话总结SSD效果就是&#xff1a;比YOLO快一点且准很…

C语言学习及应用笔记之五:C语言typedef关键字及其使用

在C语言中有一个typedef关键字&#xff0c;其用来定义用户自定义类型。当然&#xff0c;并不是真的创造了一种数据类型&#xff0c;而是给已有的或者符合型的以及复杂的数据类型取一个我们自己更容易理解的别名。总之&#xff0c;可以使用typedef关键字定义一个我们自己的类型名…

Modbus协议栈开发笔记之五:Modbus RTU Slave开发

Modbus在串行链路上分为Slave和Master&#xff0c;这一节我们就来开发Slave。对于Modbus RTU从站来说&#xff0c;需要实现的功能其实与Modbus TCP的服务器端是一样的。其操作过程也是一样的。首先接收到主站的访问命令&#xff0c;对该命令报文进行解析&#xff0c;这里我们也…

Modbus协议栈开发笔记之六:Modbus RTU Master开发

这一节我们来封装最后一种应用&#xff08;Modbus RTU Master应用&#xff09;&#xff0c;RTU主站的开发与TCP客户端的开发是一致的。同样的我们也不是做具体的应用&#xff0c;而是实现RTU主站的基本功能。我们将RTU主站的功能封装为函数&#xff0c;以便在开发具体应用时调用…

PID控制器开发笔记之十三:单神经元PID控制器的实现

神经网络是模拟人脑思维方式的数学模型。神经网络是智能控制的一个重要分支&#xff0c;人们针对控制过程提供了各种实现方式&#xff0c;在本节我们主要讨论一下采用单神经元实现PID控制器的方式。 1、单神经元的基本原理 单神经元作为构成神经网络的基本单位&#xff0c;具…

基于STM32L476的锂电池SOC检测

便携式设备由于使用需求而配备了锂电池&#xff0c;但使用过程中需要掌握电源的状态才能保证设备正常运行。而且在电池充放电的过程中&#xff0c;监控电池的充放电状态也是保证设备安全的需要。 1、硬件设计 电池SOC检测是一个难题&#xff0c;有很多的模型和检测电路。但对…

C语言学习及应用笔记之六:C语言extern关键字及其使用

在C语言中&#xff0c;修饰符extern用在变量或者函数的声明前&#xff0c;用来以标识变量或者函数的定义在别的文件中&#xff0c;提示编译器遇到此变量或者函数时&#xff0c;在其它文件中寻找其定义。extern关键字的用法有几种&#xff0c;我们下面对其进行说明。 1、extern…

C语言学习及应用笔记之七:C语言中的回调函数及使用方式

我们在使用C语言实现相对复杂的软件开发时&#xff0c;经常会碰到使用回调函数的问题。但是回调函数的理解和使用却不是一件简单的事&#xff0c;在本篇我们根据我们个人的理解和应用经验对回调函数做简要的分析。 1、什么是回调函数 既然谈到了回调函数&#xff0c;首先我们…

STM32与SHT1X温湿度传感器通讯

在这次项目开发中应用到了SHT1X温湿度传感器&#xff0c;该系列有SHT10、SHT11和SHT15&#xff0c;属于Sersirion温湿度传感器家族中的贴片封装系列。包括一个电容性聚合体测湿敏感元件、一个用能隙材料制成的测温元件&#xff0c;传感器内部有一个精度高达14为位的A/D转换器。…

STM32与MS5837压力传感器的I2C通讯

MS5837压力传感器是一种可用于电路板上&#xff0c;适用于检测10-1200mbar压力范围的传感器&#xff0c;灵敏度非常高&#xff0c;理论上能够检测到0.01mbar的压力变化&#xff0c;实际使用过程中测试并无明显的变化。 MS5837采用I2C总线通讯&#xff0c;与STM32的MCU可以实现…