YOLO-V4目标检测学习
基础知识
特征图的通道数
Softmax函数
Sigmoid函数
ReLU(Rectified Linear Unit)函数
Batch Normalization(批量归一化)
方差(Variance)
均值(Mean)
下采样(Downsampling)
CBL
先验框和模型
-
+
首页
先验框和模型
来源:[计算机视觉:目标检测 先验框和模型](https://zhuanlan.zhihu.com/p/337479794 "计算机视觉:目标检测 先验框和模型") 1 先验框 1.1 什么是先验框 1.2 设置不同尺度的先验框 1.3 先验框和特征图的对应 1.4 生成先验框的代码 2 模型 2.1 模型的主干网络 2.2 检测头(类别头和回归头)的设计 1 先验框(锚框) 1.1 什么是先验框 简单来讲,先验框就是帮助我们定好了常见目标的宽和高,以及大致的位置,在进行预测的时候,我们可以利用这个已经定好的宽和高处理,帮助我们进行预测。我们要遍历图片上每一个可能的目标框,再对这些框进行分类和微调,就可以完成目标检测任务。 1.2 设置不同尺度的先验框 通常,为了覆盖更多可能的情况,在图中的同一个位置,我们会设置几个不同尺度的先验框。这里所说的不同尺度,不单单指大小(scale),还有宽高比(aspect ratio),如下面的示意图所示: ![](/media/202408/2024-08-08_235854_9038410.7372112166722313.png) 可以看到,通过设置不同的尺度的先验框,就有更高的概率出现对于目标物体有良好匹配度的先验框。 1.3 先验框和特征图的对应 除了不同尺度,我们肯定要将先验框铺洒在图片中不同位置上面。 但是遍历原图每个像素,设置的先验框就太多了,完全没必要。一个224x224的图片,假设每个位置设置3个不同尺寸的先验框,那么就有224x224x3=150528个,但是如果我们不去遍历原图,而是去遍历原图下采样得到的feature map呢?以vgg16的backbone为例,下采样了5次,得到7x7的feature map,那就只需要得到7x7x3=147个先验,这样的设置大大减少了先验框的数量,同时也能覆盖大多数情况。 ![](/media/202408/2024-08-08_235935_9800310.9932457622427783.png) 因此,我们就将先验框的设置位置与特征图建立一一对应的关系。而且,通过建立这种映射关系,我们可以通过特征图,直接一次性的输出所有先验框的类别信息以及坐标信息,而不是想前面一直描述的那样,每个候选框都去独立的进行一次分类的预测,这样太慢了。 1.4 先验框生成的代码 model.py 脚本下有一个 tiny_detector 类,是本章节介绍的目标检测网络的定义函数,其内部实现了一个 create_prior_boxes 函数,该函数便是用来生成先验框的。 """ 设置细节介绍: 1. 离散程度 fmap_dims = 7: VGG16最后的特征图尺寸为 7*7 2. 在上面的举例中我们是假设了三种尺寸的先验框,然后遍历坐标。在先验框生成过程中,先验框的尺寸是提前设置好的, 本教程为特征图上每一个cell定义了共9种不同大小和形状的候选框(3种尺度*3种长宽比=9) 生成过程: 0. cx, cy表示中心点坐标 1. 遍历特征图上每一个cell,i+0.5是为了从坐标点移动至cell中心,/fmap_dims目的是将坐标在特征图上归一化 2. 这个时候我们已经可以在每个cell上各生成一个框了,但是这个不是我们需要的,我们称之为base_prior_bbox基准框。 3. 根据我们在每个cell上得到的长宽比1:1的基准框,结合我们设置的3种尺度obj_scales和3种长宽比aspect_ratios就得到了每个cell的9个先验框。 4. 最终结果保存在prior_boxes中并返回。 需要注意的是,这个时候我们的到的先验框是针对特征图的尺寸并归一化的,因此要映射到原图计算IOU或者展示,需要: img_prior_boxes = prior_boxes * 图像尺寸 """ def create_prior_boxes(): """ Create the 441 prior (default) boxes for the network, as described in the tutorial. VGG16最后的特征图尺寸为 7*7 我们为特征图上每一个cell定义了共9种不同大小和形状的候选框(3种尺度*3种长宽比=9) 因此总的候选框个数 = 7 * 7 * 9 = 441 :return: prior boxes in center-size coordinates, a tensor of dimensions (441, 4) """ fmap_dims = 7 obj_scales = [0.2, 0.4, 0.6] aspect_ratios = [1., 2., 0.5] prior_boxes = [] for i in range(fmap_dims): for j in range(fmap_dims): cx = (j + 0.5) / fmap_dims cy = (i + 0.5) / fmap_dims for obj_scale in obj_scales: for ratio in aspect_ratios: prior_boxes.append([cx, cy, obj_scale * sqrt(ratio), obj_scale / sqrt(ratio)]) prior_boxes = torch.FloatTensor(prior_boxes).to(device) # (441, 4) prior_boxes.clamp_(0, 1) # (441, 4) return prior_boxes * 先验框越界问题其实是非常容易出现的,越靠近四周的位置的先验框越容易越界,那么这个问题怎么处理呢?这里我们一般用图片尺寸将越界的先验框进行截断,比如某个先验框左上角坐标是(-5, -9),那么就截断为(0,0),某个先验框右下角坐标是(324,134),当我们的图片大小为(224,224)时,就将其截断为(224,134)。对应于代码中是这行,prior_boxes.clamp_(0, 1),由于进行了归一化,所以使用0-1进行截断。 2 模型 2.1 模型主干网络 VGG16 为了使结构简单易懂,我们使用VGG16作为backbone,即完全采用vgg16的结构作为特征提取模块,只是去掉fc6和fc7两个全连接层。如图所示: ![](/media/202408/2024-08-09_000006_1128860.7448400446834913.png) 对于网络的输入尺寸的确定,由于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 VGG16 self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1) # stride = 1, by default self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1) self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # 224->112 self.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->56 self.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->28 self.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->14 self.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 ImageNet self.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 out def 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.vgg16 We copy these parameters into our network. It's straightforward for conv1 to conv5. """ # Current state of base state_dict = self.state_dict() param_names = list(state_dict.keys()) # Pretrained VGG base pretrained_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 model for 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框,如图所示: ![](/media/202408/2024-08-09_000037_0064390.7381397646675931.png) 对于每个anchor,我们需要预测两类信息,一个是这个anchor的类别信息,一个是物体的边界框信息。 ![](/media/202408/2024-08-09_000056_7706220.7958915779511212.png) 在我们的实验中,类别信息由21类别的得分组成(VOC数据集的20个类别 + 一个背景类),模型最终会选择预测得分最高的类作为边界框对象的类别。 而边界框信息是指,我们大致知道了当前anchor中包含一个物体的情况下,如何对anchor进行微调,使得最终能够准确预测出物体的bbox。 这两种预测我们分别成为分类头和回归头,那么分类头预测和回归头预测是怎么得到的? 其实我们只需在7x7的feature map后,接上两个3x3的卷积层,即可分别完成分类和回归的预测。 2.2 检测头(分类头和回归头)设计 按照前面的介绍,对于输出7x7的feature map上的每个先验框我们想预测: 1)边界框的一组21类分数,其中包括VOC的20类和一个背景类。 2)边界框编码后的偏移量 。 为了得到我们想预测的类别和偏移量,我们需要在feature map后分别接上两个卷积层: 1)一个分类预测的卷积层采用3x3卷积核padding和stride都为1,每个 anchor 需要分配 21 个卷积核代表 21 个不同类别,每个位置有9个anchor,因此需要 21x9 个卷积核。 2)一个定位预测卷积层,每个位置使用3x3卷积核padding和stride都为1,每个 anchor 需要分配 4 个卷积核代表 4 个定位值,每个位置有9个anchor,因此需要 4x9 个卷积核。 我们直观的看这些卷积上的输出为: ![](/media/202408/2024-08-09_000118_1763830.05129057001633219.png) 分类头和回归头结构的定义,由 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' parameters self.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 boxes c_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 map return 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) 这个过程的可视化如下图所示。 ![](/media/202408/2024-08-09_000135_1002270.2378921445079406.png) 参考 动手学CV Chapter 3 目标检测 [动手学CV-Pytorch](https://datawhalechina.github.io/dive-into-cv-pytorch/#/?id=dive-into-cv-pytorch "动手学CV-Pytorch") Dive into Deep Learning - Computer Vision 13.3-13.7 [Dive into Deep Learning 0.15.1 documentation](https://d2l.ai/chapter_computer-vision/index.html "Dive into Deep Learning 0.15.1 documentation")
admin
2024年8月9日 00:07
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码