【论文笔记】R-CNN系列之代码实现
代码源码
前情回顾:【论文笔记】R-CNN系列之论文理解
整体架构
由三部分组成
(1)提取特征的卷积网络extractor
(2)输入特征获得建议框rois的rpn网络
(3)传入rois和特征图,获得分类结果和回归结果的分类网络classifier
伪代码:
class FasterRCNN(nn.Module):
def __init__(self, ...):
super(FasterRCNN, self).__init__()
# 构建特征提取网络
self.extractor, classifier = decom_vgg16(...)
# 构建建议框网络
self.rpn = RegionProposalNetwork(...)
# 构建分类器网络
self.head = VGG16RoIHead(..., classifier=classifier) def forward(self, x, scale=1.):
# 计算输入图片的大小
img_size = x.shape[2:]
# 利用主干网络提取特征
base_feature = self.extractor.forward(x)
# 获得建议框
_, _, rois, roi_indices, _ = self.rpn.forward(base_feature, img_size, scale)
# 获得classifier的分类结果和回归结果
roi_cls_locs, roi_scores = self.head.forward(base_feature, rois, roi_indices, img_size)
return roi_cls_locs, roi_scores, rois, roi_indices def freeze_bn(self):
for m in self.modules():
if isinstance(m, nn.BatchNorm2d):
m.eval()
1.卷积网络提取特征
1.1 关于VGG16
代码
import torch
import torch.nn as nn cfg = [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M']
'''
假设输入图像为(600, 600, 3),随着cfg的循环,特征层变化如下:
600,600,3 -> 600,600,64 -> 600,600,64 -> 300,300,64 -> 300,300,128 -> 300,300,128 -> 150,150,128 -> 150,150,256 -> 150,150,256 -> 150,150,256
-> 75,75,256 -> 75,75,512 -> 75,75,512 -> 75,75,512 -> 37,37,512 -> 37,37,512 -> 37,37,512 -> 37,37,512
到cfg结束,我们获得了一个37,37,512的特征层
'''
class VGG16(nn.Module):
def __init__(self):
super(VGG16, self).__init__() self.feature = make_layers(cfg, batch_norm=False)
self.avgpool = nn.AdaptiveAvgPool2d((7, 7)) #不管cfg結束后特征层大小是多少,统一变成7*7
self.classifier = nn.Sequential(
nn.Linear(7 * 7 * 512, out_features=4096, bias=False),
nn.ReLU(inplace=True), # 原地操作更加节省内存
nn.Linear(in_features=4096, out_features=4096, bias=False),
nn.ReLU(inplace=True),
nn.Linear(in_features=4096, out_features=1000, bias=False),
) def forward(self, x):
x = self.feature(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x def make_layers(cfg, batch_norm=False):
input_channels = 3
layers = []
for v in cfg:
if v == 'M':
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
else:
conv2d = nn.Conv2d(in_channels=input_channels, out_channels=v, kernel_size=3, stride=1, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)] # layers.append()不能加list
input_channels = v
return nn.Sequential(*layers) input = torch.randn(1, 3, 600, 600)
model = VGG16()
output = model(input)
print(output.shape)
1.2 调整VGG16
- 利用VGG16的特征提取部分作为Faster R-CNN的特征提取网络
- 利用后半部分的全连接网络作为Faster R-CNN的分类网络
def decom_vgg16(pretrained = False):
model = VGG16()
feature = list(model.feature) #转化为list
classifier = list(model.classifier)
del classifier[-1] # 删去最后的全连接层,如果有dropout层也删掉
feature = nn.Sequential(*feature) # 再转为Sequential
classifier = nn.Sequential(*classifier)
return feature,classifier
打印一下classifier :
2. RPN生成区域提议
图中的intermediate layer可以用3*3的卷积实现,cls layer和reg layer都可以用1*1的卷积实现。
self.conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
self.score = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0)
self.loc = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0)
cls layer后接softmax层
reg layer后接筛选层
2.1 anchor
(1)计算出3*3滑动窗口中心点对应原始图像上的中心点
特征图上所有的点,对应原图上固定间隔的点
import matplotlib.pyplot as plt
import numpy as np if __name__ == "__main__":
width = 38
height = 38
feat_stride = 16
x = np.arange(0, width * feat_stride, feat_stride)
y = np.arange(0, height * feat_stride, feat_stride)
X, Y = np.meshgrid(x, y)
plt.plot(X, Y,
color='limegreen', # 设置颜色为limegreen
marker='.', # 设置点类型为圆点
linestyle='') # 设置线型为空,也即没有线连接点
plt.grid(True)
plt.show()
原图 特征图
(2)生成anchor box
首先在原图(0,0)位置生成一组(9个)anchor box,然后在原图上滑动(stride=16),得到(38*38*9个)anchor box
# 生成基础的9个先验框
def generate_anchor_base(base_size=16, ratios=[0.5, 1, 2], anchor_scales=[8, 16, 32]):
anchor_base = np.zeros((len(ratios) * len(anchor_scales), 4), dtype=np.float32)
for i in range(len(ratios)):
for j in range(len(anchor_scales)):
h = base_size * anchor_scales[j] * np.sqrt(ratios[i])
w = base_size * anchor_scales[j] * np.sqrt(1. / ratios[i]) index = i * len(anchor_scales) + j
anchor_base[index, 0] = - h / 2. #框的四个参数分别是左上的(x,y)右下的(x,y)
anchor_base[index, 1] = - w / 2.
anchor_base[index, 2] = h / 2.
anchor_base[index, 3] = w / 2.
return anchor_base # 对基础先验框移动对应到所有特征点上(特征点在特征图上是相连的,但对应到原图上间隔feat_stride)
def _enumerate_shifted_anchor(anchor_base, feat_stride, height, width):
# 计算网格中心点
shift_x = np.arange(0, width * feat_stride, feat_stride)
shift_y = np.arange(0, height * feat_stride, feat_stride)
shift_x, shift_y =np.meshgrid(shift_x, shift_y)
shift = np.stack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel(),), axis=1)
# 每个网格点上的9个先验框
A = anchor_base.shape[0]
K = shift.shape[0]
anchor = anchor_base.reshape((1, A, 4)) + shift.reshape((K, 1, 4))#应为anchor_base的四个参数都是坐标,所以都要平移
anchor = anchor.reshape((K * A, 4)).astype(np.float32)# 所有的先验框
return anchor # 38*38*9个
(3)获得预测框
根据rpn得到的k个(dx,dy,dw,dh),以及上一步得到的k个anchor box的位置(Px,Py,Pw,Ph),可以计算得到预测框的G(Gx,Gy,Gw,Gh)
def loc2bbox(src_bbox, loc):
if src_bbox.size()[0] == 0:
return torch.zeros((0, 4), dtype=loc.dtype)
# src_bbox就是anchor,四个参数为左上角的坐标和右下角的,所以要转换得到(Px,Py,Pw,Ph)
src_width = torch.unsqueeze(src_bbox[:, 2] - src_bbox[:, 0], -1)
src_height = torch.unsqueeze(src_bbox[:, 3] - src_bbox[:, 1], -1)
src_ctr_x = torch.unsqueeze(src_bbox[:, 0], -1) + 0.5 * src_width
src_ctr_y = torch.unsqueeze(src_bbox[:, 1], -1) + 0.5 * src_height
# rpn得到的回归参数
dx = loc[:, 0::4]
dy = loc[:, 1::4]
dw = loc[:, 2::4]
dh = loc[:, 3::4]
# 计算得到预测框的G(Gx,Gy,Gw,Gh)
ctr_x = dx * src_width + src_ctr_x
ctr_y = dy * src_height + src_ctr_y
w = torch.exp(dw) * src_width
h = torch.exp(dh) * src_height
# dst_bbox又是左上角右下角格式
dst_bbox = torch.zeros_like(loc)
dst_bbox[:, 0::4] = ctr_x - 0.5 * w
dst_bbox[:, 1::4] = ctr_y - 0.5 * h
dst_bbox[:, 2::4] = ctr_x + 0.5 * w
dst_bbox[:, 3::4] = ctr_y + 0.5 * h return dst_bbox
(4)筛选anchor
# 建议框筛选 (1)出界的不要
roi[:, [0, 2]] = torch.clamp(roi[:, [0, 2]], min=0, max=img_size[1])
roi[:, [1, 3]] = torch.clamp(roi[:, [1, 3]], min=0, max=img_size[0])
# 建议框筛选(2)小于16的不要
min_size = self.min_size * scale
keep = torch.where(((roi[:, 2] - roi[:, 0]) >= min_size) & ((roi[:, 3] - roi[:, 1]) >= min_size))[0]
roi = roi[keep, :]
score = score[keep]
# 建议框筛选(3)按概率选出前n_pre_nms个
order = torch.argsort(score, descending=True)
if n_pre_nms > 0:
order = order[:n_pre_nms]
roi = roi[order, :]
score = score[order]
# 建议框筛选(4)利用nms选出n_post_nms个
keep = nms(roi, score, self.nms_iou)
keep = keep[:n_post_nms]
roi = roi[keep]
3.分类网络
分类网络主要分为三部分
(1)ROI pooling layer:将rois转换为固定大小
(2)classifier:VGG16的后半部分全连接层
(3)cls layer 和 loc layer:两个全连接层
伪代码:
class VGG16RoIHead(nn.Module):
def __init__(self, n_class, roi_size, spatial_scale, classifier):
super(VGG16RoIHead, self).__init__()
self.roi = RoIPool((roi_size, roi_size), spatial_scale)
self.classifier = classifier
self.cls_loc = nn.Linear(4096, n_class * 4)
self.score = nn.Linear(4096, n_class)
def forward(self, x, rois, roi_indices, img_size):
...
# (1) ROI pooling layer
pool = self.roi(x, indices_and_rois)
pool = pool.view(pool.size(0), -1)
# (2) classifier layer
fc7 = self.classifier(pool)
# (3) cls layer 和 loc layer
roi_cls_locs = self.cls_loc(fc7)
roi_scores = self.score(fc7)
roi_cls_locs = roi_cls_locs.view(n, -1, roi_cls_locs.size(1))
roi_scores = roi_scores.view(n, -1, roi_scores.size(1))
return roi_cls_locs, roi_scores
3. 1 ROI pooling layer
将区域提议对应的feature map转换成小的固定大小(H*W)的feature map,不限制输入map的尺寸。
ROI pooling layer的输入有两项:
(1)提取特征的网络的最后一个feature map;
(2)一个表示图片中所有ROI的N*5的矩阵,其中N表示 ROI的数目,5表示图像index和坐标参数(x,y,h,w) 。坐标的参考系不是针对feature map这张图的,而是针对原图的。
...
self.roi = RoIPool((roi_size, roi_size), spatial_scale)
...
pool = self.roi(x, indices_and_rois)# 利用建议框对公用特征层进行截取
(roi_size, roi_size):执行裁剪后的输出大小(int或Tuple[int,int]),如(高度、宽度)
spatial_scale:将输入坐标映射到框坐标的比例因子。默认值:1.0。
4. 训练
分为几步:
(1)获取公用特征层
(2)利用rpn网络获得调整参数、得分、建议框、先验框
(3)计算损失
4.1 计算建议框网络rpn的回归损失和分类损失
(0)rpn后我们得到rpn_locs, rpn_scores, rois, roi_indices, anchor,其中anchor是先验框,roi是预测框。
(1)为每个先验框anchor找到对应的最大IOU的真实框
def _calc_ious(self, anchor, bbox):
# 获得的ious的shape为[num_anchors, num_gt]
ious = bbox_iou(anchor, bbox)
if len(bbox)==0:
return np.zeros(len(anchor), np.int32), np.zeros(len(anchor)), np.zeros(len(bbox))
# 获得每一个先验框最对应的真实框 [num_anchors, ]
argmax_ious = ious.argmax(axis=1)
# 找出每一个先验框最对应的真实框的iou [num_anchors, ]
max_ious = np.max(ious, axis=1)
# 获得每一个真实框最对应的先验框 [num_gt, ]
gt_argmax_ious = ious.argmax(axis=0)
# 保证每一个真实框都存在对应的先验框
for i in range(len(gt_argmax_ious)):
argmax_ious[gt_argmax_ious[i]] = i
return argmax_ious, max_ious, gt_argmax_ious
# argmax_ious为每个先验框对应的最大的真实框的序号 [num_anchors, ]
# max_ious为每个真实框对应的最大的真实框的iou [num_anchors, ]
# gt_argmax_ious为每一个真实框对应的最大的先验框的序号 [num_gt, ]
(2)采样256个anchor计算损失函数,正负样本比为1:1,如果正样本少于128个,用负样本填充。每个ground truth至少对应一个先验框。
我们分配正标签前景给两类anchor:
1)与某个ground truth有最高的IoU重叠的anchor(也许不到0.7)
2)与任意ground truth有大于0.7的IoU交叠的anchor。
我们分配负标签(背景)给与所有ground truth的IoU比率都低于0.3的anchor。
(3)根据每一个先验框最对应的真实框,得到256个真实框bbox的loc gt_rpn_loc =(dx,dy,dw,dh)
(4)由rpn网络得到的rpn_loc,rpn_score和真实框得到的gt_rpn_loc,gt_rpn_label计算损失
rpn_loc_loss = self._fast_rcnn_loc_loss(rpn_loc, gt_rpn_loc, gt_rpn_label, self.rpn_sigma)
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_label, ignore_index=-1)
4.2 计算Classifier网络的回归损失和分类损失
(0)rpn后我们得到rpn_locs, rpn_scores, rois, roi_indices, anchor,其中anchor是先验框,roi是预测框。
(1)获得每一个建议框roi最对应的真实框
(2)采样n_sample个建议框roi计算损失函数
满足建议框和真实框重合程度大于neg_iou_thresh_high的作为正样本
将正样本的数量限制在self.pos_roi_per_image以内满足建议框和真实框重合程度小于neg_iou_thresh_high大于neg_iou_thresh_low作为负样本
将正样本的数量和负样本的数量的总和固定成self.n_sample
(3)得到真实框bbox的loc gt_roi_loc =(dx,dy,dw,dh)
(4)由Classifier网络得到的roi_loc,roi_score,和真实框得到的gt_roi_loc,gt_roi_label计算损失
roi_loc_loss = self._fast_rcnn_loc_loss(roi_loc, gt_roi_loc, gt_roi_label.data, self.roi_sigma)
roi_cls_loss = nn.CrossEntropyLoss(roi_score[0], gt_roi_label)
Tip:
上图为4个真实框ground truth,在rpn层中,我们将roi筛选至n_post_nms个(600个)。
在rpn计算的是先验框anchor和真实框bbox的IOU,分类层用的是建议框roi和真实框bbox的IOU
5. 评价指标
5.1计算交并比IOU
def bbox_iou(bbox_a, bbox_b):
if bbox_a.shape[1] != 4 or bbox_b.shape[1] != 4:
print(bbox_a, bbox_b)
raise IndexError
tl = np.maximum(bbox_a[:, None, :2], bbox_b[:, :2])
br = np.minimum(bbox_a[:, None, 2:], bbox_b[:, 2:])
area_i = np.prod(br - tl, axis=2) * (tl < br).all(axis=2)
area_a = np.prod(bbox_a[:, 2:] - bbox_a[:, :2], axis=1)
area_b = np.prod(bbox_b[:, 2:] - bbox_b[:, :2], axis=1)
return area_i / (area_a[:, None] + area_b - area_i)
5.2 计算AP
(1)获得预测结果
- 图片送入网络得到预测结果 roi_cls_locs, roi_scores, rois等
- 对建议框进行解码,获得预测框 [ top, left, bottom, right, score, predicted_class ]
- 预测结果写入txt
(2)获得真实框
- 从"VOC2007/Annotations/"+image_id+".xml"中找到xmin,ymin,xmax,ymax
- 写入txt
(3)按照置信度对预测框排序
假设一共有7张图片,绿色框是GT(15个),红色框是预测框(24个)并带有置信度。
现在假设IOU=30%,按照置信度排序得到下表。
其中TP表示预测正确、FP表示预测错误、acc TP表示从头到该位置累计正确个数、precision表示从头到该位置的精确率、recall表示从头到该位置的召回率。
下图表示的就是从头到尾,依次加入新的样本时,P和R的变化情况:
代码:
- 按照置信度排序
bounding_boxes.sort(key=lambda x:float(x['confidence']), reverse=True)
- 遍历bounding_boxes,计算IOU,大于min_overlap为TP,不然为FP
- 计算precision和recall
for idx, val in enumerate(tp):
rec[idx] = float(tp[idx])/np.maximum(gt_counter_per_class[class_name], 1) # rec=tp/(tp+fn)
# gt_counter_per_class 计算每个类有多少个gt,gt的个数=tp+fn
for idx, val in enumerate(tp):
prec[idx] = float(tp[idx]) / np.maximum((fp[idx] + tp[idx]), 1) # prec=tp/(tp+fp)
(4)使精度单调下降,计算PR曲线的面积
蓝色线就是前面那张PR图,红色虚线的纵坐标是单调减小的,每次减小到右侧蓝线的最高点。APall就是红色虚线下方的面积。
def voc_ap(rec, prec):
rec.insert(0, 0.0) # insert 0.0 at begining of list
rec.append(1.0) # insert 1.0 at end of list
mrec = rec[:]
prec.insert(0, 0.0) # insert 0.0 at begining of list
prec.append(0.0) # insert 0.0 at end of list
mpre = prec[:]
# 1. 这一部分使精度单调下降
i_list = []
for i in range(1, len(mrec)):
if mrec[i] != mrec[i-1]:
i_list.append(i) # if it was matlab would be i + 1
# 2. 平均精度(AP)是曲线下的面积
ap = 0.0
for i in i_list:
ap += ((mrec[i]-mrec[i-1])*mpre[i])
return ap, mrec, mpre
5.3 mAP指标
如果是多类别目标检测任务,就要使用mean AP(mAP),其定义为:
即,对所有的类别进行AP的计算,然后取均值。
AP50指的是IOU的值取50%,AP70同理。
AP@50:5:95指的是IOU的值从50%取到95%,步长为5%,然后算在在这些IOU下的AP的均值。
参考文献:
1. 目标检测01:常用评价指标(AP、AP50、AP@50:5:95、mAP)
3. Pytorch 搭建自己的Faster-RCNN目标检测平台(视频)
4. 深度学习小技巧-mAP精度概念详解与计算绘制(视频)
【论文笔记】R-CNN系列之代码实现的更多相关文章
- 论文笔记:CNN经典结构2(WideResNet,FractalNet,DenseNet,ResNeXt,DPN,SENet)
前言 在论文笔记:CNN经典结构1中主要讲了2012-2015年的一些经典CNN结构.本文主要讲解2016-2017年的一些经典CNN结构. CIFAR和SVHN上,DenseNet-BC优于ResN ...
- 论文笔记:CNN经典结构1(AlexNet,ZFNet,OverFeat,VGG,GoogleNet,ResNet)
前言 本文主要介绍2012-2015年的一些经典CNN结构,从AlexNet,ZFNet,OverFeat到VGG,GoogleNetv1-v4,ResNetv1-v2. 在论文笔记:CNN经典结构2 ...
- 【论文笔记】CNN for NLP
什么是Convolutional Neural Network(卷积神经网络)? 最早应该是LeCun(1998)年论文提出,其结果如下:运用于手写数字识别.详细就不介绍,可参考zouxy09的专栏, ...
- Deep Learning论文笔记之(四)CNN卷积神经网络推导和实现(转)
Deep Learning论文笔记之(四)CNN卷积神经网络推导和实现 zouxy09@qq.com http://blog.csdn.net/zouxy09 自己平时看了一些论文, ...
- 论文笔记系列-Auto-DeepLab:Hierarchical Neural Architecture Search for Semantic Image Segmentation
Pytorch实现代码:https://github.com/MenghaoGuo/AutoDeeplab 创新点 cell-level and network-level search 以往的NAS ...
- Person Re-identification 系列论文笔记(一):Scalable Person Re-identification: A Benchmark
打算整理一个关于Person Re-identification的系列论文笔记,主要记录近年CNN快速发展中的部分有亮点和借鉴意义的论文. 论文笔记流程采用contributions->algo ...
- 【论文笔记系列】AutoML:A Survey of State-of-the-art (下)
[论文笔记系列]AutoML:A Survey of State-of-the-art (上) 上一篇文章介绍了Data preparation,Feature Engineering,Model S ...
- 论文笔记系列-Neural Network Search :A Survey
论文笔记系列-Neural Network Search :A Survey 论文 笔记 NAS automl survey review reinforcement learning Bayesia ...
- Multimodal —— 看图说话(Image Caption)任务的论文笔记(一)评价指标和NIC模型
看图说话(Image Caption)任务是结合CV和NLP两个领域的一种比较综合的任务,Image Caption模型的输入是一幅图像,输出是对该幅图像进行描述的一段文字.这项任务要求模型可以识别图 ...
- Deep Reinforcement Learning for Visual Object Tracking in Videos 论文笔记
Deep Reinforcement Learning for Visual Object Tracking in Videos 论文笔记 arXiv 摘要:本文提出了一种 DRL 算法进行单目标跟踪 ...
随机推荐
- ef 查询生成语句的几种方式
前言 整理一下ef 如何查看生成sql 语句的,现在有ef core 了,统一整理一下. 正文 方式如下: 数据库监听 这是一种推荐方式,因为调试和代码分开,不会有影响. 然后连接: 然后可以进行一些 ...
- JS isPrototypeOf 和hasOwnProperty 还有in的区别
isPrototypeOf 和hasOwnProperty 的区别 isPrototypeOf 是判断原生链上是否有该对象. 1.isPrototypeOf isPrototypeOf是用来判断指定对 ...
- Mysql安装和远程登录--Centos7
在Centos7中使用的包管理工具是yum,当然使用包管理工具安装也是最方便的. 本文操作内容需要在root用户下,否则有些步骤无法成功执行. 系统环境信息展示 安装 MySQL 提供的 RPM wg ...
- 实训篇-Html-frameset框架集
frameset.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...
- stmp 501 5.1.3 Invalid Address 无效的邮件地址
stmp 501 5.1.3 Invalid Address 无效的邮件地址 一般来说就是要确认邮箱地址是不是对的 还有一种可能的情况是使用的邮件服务器仅支持对内邮件,没有对外邮件的发送权限
- PolarDB-X 源码解读:事务的一生
简介: 本文将主要解读 PolarDB-X 中事务部分的相关代码,着重解读事务的一生在计算节点(CN)中的关键代码:从开始.执行.到最后提交这一整个生命周期. 概述 本文将主要解读 PolarDB-X ...
- SchedulerX 如何帮助用户解决分布式任务调度难题?
简介:本文分别对任务调度平台的资源定义.可视化管控能力.分布式批处理能力进行了简述,并基于 SchedulerX 的能力结合实际业务场景提供了一些基础参考案例.希望通过上述内容能让大家方便地熟悉任务 ...
- FileStream 的 FlushAsync 方法在 .NET Framework 与 .NET Core 行为的不同
本文记录 FileStream 的 FlushAsync 方法在 .NET Framework 与 .NET Core 行为的不同 在使用 HID 设备进行 IO 通讯时,可以采用 FileStrea ...
- 特权同学笔记-《边练边学》-在QP里调用modelsim的步骤
在QP里调用Modelsim需要先设置仿真参数和工具路径. 在QP调用modelsim的步骤 1. 在QP里建立工程,代码,分析综合:2. 添加testbench代码,processing-start ...
- vue监听watch
export default { watch:{ showArea(val,_val){ console.log('showAre ...