一、overfeat模型
二、RCNN
1. 算法流程
- 候选区域生成:
- 使用选择性搜索(Selective Search)的方法找出图片中可能存在目标的侯选区域
- CNN网络提取特征:
- 选取预训练卷积神经网网络(AlexNet或VGG)用于进行特征提取。
- 目标分类:
- 训练支持向量机(SVM)来辨别目标物体和背景,对每个类别,都要训练一个二元SVM。
- 目标定位:
- 训练一个线性回归模型,为每个辨识到的物体生成更精确的边界框。
2. 候选区域生成
在选择性搜索(SelectiveSearch,SS)中,使用语义分割的方法,它将颜色、边界、纹理等信息作为合并条件,采用多尺度的综合方法,将图像在像素级上划分出一系列的区域,这些区域要远远少于传统的滑动窗口的穷举法产生的候选区域。 SelectiveSearch在一张图片上提取出来约2000个侯选区域,需要注意的是这些候选区域的长宽不固定。而使用CNN提取候选区域的特征向量,需要接受固定长度的输入,所以需要对候选区域做一些尺寸上的修改。
3. CNN网络提取特征
采用预训练模型(AlexNet或VGG)在生成的候选区域上进行特征提取,将提取好的特征保存在磁盘中,用于后续步骤的分类和回归。
1.全连接层的输入数据的尺寸是固定的,因此在将候选区域送入CNN网络中时,需进行裁剪或变形为固定的尺寸,在进行特征提取。 2.预训练模型在ImageNet数据集上获得,最后的全连接层是1000,在这里我们需要将其改为N+1(N为目标类别的数目,例如VOC数据集中N=20,coco数据集中N=80,1是加一个背景)后,进行微调即可。 3.利用微调后的CNN网络,提取每一个候选区域的特征,获取一个4096维的特征,一幅图像就是2000x4096维特征存储到磁盘中。
4. 目标分类(SVM)
假设我们要检测猫狗两个类别,那我们需要训练猫和狗两个不同类别的SVM分类器,然后使用训练好的分类器对一幅图像中2000个候选区域的特征向量分别判断一次,这样得出[2000, 2]的得分矩阵,如下图所示: 对于N个类别的检测任务,需要训练N(目标类别数目)个SVM分类器,对候选区域的特征向量(4096维)进行二分类,判断其是某一类别的目标,还是背景来完成目标分类。
5. 目标定位
6. 算法总结
- 训练阶段多,训练耗时:
- 微调CNN网络+训练SVM+训练边框回归器。
- 预测速度慢:
- 使用GPU, VGG16模型处理一张图像需要47s。
- 占用磁盘空间大:
- 5000张图像产生几百G的特征文件。
- 数据的形状变化:
- 候选区域要经过缩放来固定大小,无法保证目标的不变形
三、Fast-RCNN
- 考虑到R-CNN存在的问题,2015年提出了一个改善模型:Fast R-CNN。 相比于R-CNN, Fast R-CNN主要在以下三个方面进行了改进:
- 1、提高训练和预测的速度
- R-CNN首先从测试图中提取2000个候选区域,然后将这2000个候选区域分别输入到预训练好的CNN中提取特征。由于候选区域有大量的重叠,这种提取特征的方法,就会重复的计算重叠区域的特征- 。在Fast-RCNN中,将整张图输入到CNN中提取特征,将候选区域映射到特征图上,这样就避免了对图像区域进行重复处理,提高效率减少时间。
- 2、不需要额外的空间保存CNN网络提取的特征向量
- RCNN中需要将提取到的特征保存下来,用于为每个类训练单独的SVM分类器和边框回归器。在Fast-RCNN中,将类别判断和边框回归统一使用CNN实现,不需要在额外的空间存储特征。
- 3、不在直接对候选区域进行缩放
- RCNN中需要对候选区域进行缩放送入CNN中进行特征提取,在Fast-RCNN中使用ROIpooling的方法进行尺寸的调整。
- 1、提高训练和预测的速度
1. 算法流程
1.1 候选区域生成
与RCNN中一样,不再赘述
1.2 CNN网络特征提取
与RCNN中一样,使用预训练模型进行特征提取
1.3 ROI Pooling
只对感兴趣的特征进行pooling
1.4 目标分类和回归
原网络的最后一个全连接层替换为两个同级层:K+1个类别的SoftMax分类层和边框的回归层
2. 模型训练
3. 模型预测
- fastRCNN的工作流程描述如下:
- 输入图像;
- 图像被送入到卷积网络进行特征提取,将通过选择性搜索获取的候选区域映射到特征图中;
- 在特征图上Rol中应用RoIPooling,获取尺寸相同的特征向量;
- 将这些区域传递到全连接的网络中进行分类和回归,得到目标检测的结果。
4. 模型总结
- Fast R-CNN是对R-CNN模型的一种改进:
- CNN网络不再对每个候选区域进行特征提取,而是直接对整张图像进行出路,这样减少了很多重复计算。
- 用ROI pooling进行特征的尺寸变换,来满足FC全连接层对输入数据尺度的要求。
- 将目标的回归和分类统一在一个网络中,使用FC+softmax进行目标分类,使用FC Layer进行目标框的回归。 在Fast R-CNN中使用的目标检测识别网络,在速度和精度上都有了不错的结果。不足的是,其候选区域提取方法耗时较长,而且和目标检测网络是分离的,并不是端到端的,在201- 6年又提出了Faster-RCNN模型用于目标检测,在接下来的课程中我们着重介绍Faster-RCNN网络的原理与实现。
四、Faster-RCNN
在R-CNN和Fast RCNN的基础上,在2016年提出了Faster RCNN网络模型,在结构上,Faster RCNN已经将候选区域的生成,特征提取,目标分类及目标框的回归都整合在了一个网络中,综合性能有较大提高,在检测速度方面尤为明显。接下来我们给大家详细介绍fasterRCNN网络模型。网络基本结构如下图所示:
1. 网络工作流程
- 1、特征提取:将整个图像缩放至固定的大小输入到CNN网络中进行特征提取,得到特征图。
- 2、候选区域提取:输入特征图,使用区域生成网络RPN,产生一些列的候选区域
- 3、ROIPooling: 与Fast RCNN网络中一样,使用最大池化固定候选区域的尺寸,送入后续网络中进行处理
- 4、目标分类和回归:与Fast RCNN网络中一样,使用两个同级层:K+1个类别的SoftMax分类层和边框的回归层,来完成目标的分类和回归。
- Faster R-CNN的流程与Fast R-CNN的区别不是很大,重要的改进是使用RPN网络来替代选择性搜索获取候选区域,所以我们可以将Faster R-CNN网络看做RPN和Fast R-CNN网络的结合。
1.1 数据加载
# 导入相关工具包
# 获取VOC数据使用
from detection.datasets import pascal_voc
# 绘图
import matplotlib.pyplot as plt
import numpy as np
# 模型构建
from detection.models.detectors import faster_rcnn
import tensorflow as tf
# 图像展示
import visualize
# 实例化voc数据集的类,获取送入网络中的一张图片
pascal = pascal_voc.pascal_voc("train")
# image:送入网络中的数据,imagemeta:图像的元信息
image,imagemeta,bbox,label = pascal[218]
# 图像的均值和标准差
img_mean = (122.7717, 115.9465, 102.9801)
img_std = (1., 1., 1.)
# RGB图像(反标准化操作,获取原图像)
rgd_image= np.round(image+img_mean).astype(np.uint8)
# 获取原始图像
from detection.datasets.utils import get_original_image
ori_img = get_original_image(image[0],imagemeta[0],img_mean)
# 展示原图像和送入网络中图像
rgd_image= np.round(image+img_mean).astype(np.uint8)
fig,axes=plt.subplots(nrows=1,ncols=2,figsize=(10,8),dpi=100)
axes[0].imshow(ori_img.astype('uint8'))
axes[0].set_title("原图像")
axes[1].imshow(rgd_image[0])
axes[1].set_title("送入网络中的图像")
plt.show()
1.2 模型加载
# coco数据集的class,共80个类别:人,自行车,火车,。。。
classes = ['bg', 'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']
# 实例化模型
model = faster_rcnn.FasterRCNN(num_classes=len(classes))
model((image,imagemeta,bbox,label),training=True)
# 加载训练好的weights
model.load_weights("weights/faster_rcnn.h5")
1.3 模型预测过程
# RPN获取候选区域:输入图像和对应的元信息,输出是候选的位置信息
proposals = model.simple_test_rpn(image[0],imagemeta[0])
# 绘制在图像上(将proposal绘制在图像上)
visualize.draw_boxes(rgd_image[0],boxes=proposals[:,:4]*1216)
plt.show()
# rcnn进行预测,得到的是原图像的检测结果:
# 输入:要检测的送入网络中的图像,图像的元信息,RPN产生的候选区域
# 输出:目标检测结果:检测框(相对于原图像),类别,置信度
res = model.simple_test_bboxes(image[0],imagemeta[0],proposals)
# 将检测结果绘制在图像上
visualize.display_instances(ori_img,res['rois'],res['class_ids'],classes,res['scores'])
plt.show()
2. 模型结构详解
- Backbone:Backbone由CNN卷积神经网络构成,常用的是VGG和resnet,用来提取图像中的特征,获取图像的特征图。该特征图被共享用于后续RPN层生成候选区域和ROIPooli- ng层中。
- RPN网络:RPN网络用于生成候选区域,用于后续的目标检测。
- Roi Pooling: 该部分收集图像的特征图和RPN网络提取的候选区域位置,综合信息后获取固定尺寸的特征,送入后续全连接层判定目标类别和确定目标位置。
- 目标分类与回归: 该部分利用ROIpooling输出特征向量计算候选区域的类别,并通过回归获得检测框最终的精确位置。
2.1 backbone(骨架网络)
# 使用backbone获取特征图
C2,C3,C4,C5 = model.backbone(image,training=False)
# FPN网络融合:C2,C3,C4,C5是resnet提取的特征结果
P2,P3,P4,P5,P6 = model.neck([C2,C3,C4,C5],training=False)
2.2 RPN网络
经典的检测方法生成检测框都非常耗时,如overfeat中使用滑动窗口生成检测框;或如R-CNN使用选择性搜索方法生成检测框。而Faster RCNN则抛弃了传统的滑动窗口和选择性搜索的方法,直接使用RPN生成候选区域,能极大提升检测速度。
- RPN网络的主要流程是:
- 1、生成一系列的固定参考框anchors,覆盖图像的任意位置,然后送入后续网络中进行分类和回归
- 2、分类分支:通过softmax分类判断anchor中是否包含目标
- 3、回归分支:计算目标框对于anchors的偏移量,以获得精确的候选区域
- 4、最后的Proposal层则负责综合含有目标的anchors和对应bbox回归偏移量获取候选区域,同时剔除太小和超出边界的候选区域。
2.2.1 anchors
# 产生anchor:输入图像元信息即可,输出anchor对应于原图的坐标值
anchors,valid_flags = model.rpn_head.generator.generate_pyramid_anchors(imagemeta)
# 绘制在图像上(将前10000个anchor绘制在图像上)
visualize.draw_boxes(rgd_image[0],boxes=anchors[:10000,:4])
plt.show()
2.2.2 RPN分类
2.2.3 RPN回归
# RPN网络的输入:FPN网络获取的特征图
rpn_feature_maps = [P2,P3,P4,P5,P6]
# RPN网络预测,返回:logits送入softmax之前的分数,包含目标的概率,对框的修正结果
rpn_class_logits,rpn_probs,rpn_deltas = model.rpn_head(rpn_feature_maps,training = False)
# 获取分类结果中包含目标的概率值
rpn_probs_tmp = rpn_probs[0,:,1]
# 获取前100个较高的anchor
limit = 100
ix = tf.nn.top_k(rpn_probs_tmp,k=limit).indices[::-1]
# 获取对应的anchor绘制图像上,那这些anchor就有很大概率生成候选区域
visualize.draw_boxes(rgd_image[0],tf.gather(anchors,ix).numpy())
2.2.4 Proposal层
# 获取候选区域
proposals_list = model.rpn_head.get_proposals(rpn_probs,rpn_deltas,imagemeta)
# 绘制在图像上(将proposal绘制在图像上)
visualize.draw_boxes(rgd_image[0],boxes=proposals_list[0].numpy()[:,:4]*1216)
plt.show()
2.3 ROIPooling
# ROI Pooling层实现:输入是候选区域,特征图,图像的元信息
pool_region_list = model.roi_align((proposals_list,rcnn_feature_maps,imagemeta),training = False)
2.4 目标分类与回归
# RCNN网络的预测:输入是ROIPooling层的特征,输出:类别的score,类别的概率值,回归结果
rcnn_class_logits,rcnn_class_probs,rcnn_deltas_list = model.bbox_head(pool_region_list,training=False)
# 获取预测结果:输入:rcnn返回的分类和回归结果,候选区域,图像元信息,输出:目标检测结果
detection_list = model.bbox_head.get_bboxes(rcnn_class_probs,rcnn_deltas_list,proposals_list,imagemeta)
# 绘制在图像上
visualize.draw_boxes(rgd_image[0],boxes=detection_list[0][:,:4])
plt.show()
3. FasterRCNN的训练
- 整个训练过程分为四步:
- 第一步:RPN网络的训练,使用ImageNet预训练的模型初始化,并端到端微调用于区域建议任务。
- 第二步:利用第一步的RPN生成的建议框,由Fast R-CNN训练一个单独的检测网络,这个检测网络同样是由ImageNet预训练的模型初始化的,这时候两个网络还没有共享卷积层。
- 第三步:用检测网络初始化RPN训练,但是固定共享的卷积层,并且只微调RPN独有的层,现在两个网络共享卷积层了。
- 第四步:保持共享的卷积层固定,微调Fast R-CNN的fc层。这样,两个网络共享相同的卷积层,构成一个统一的网络。
3.1 RPN网络的训练
RPN网络的作用从众多的anchors中提取包含目标的,并且经过回归调整的候选区域。为了训练RPN,给每个anchor分配是否包含目标的标签,也就是正负样本的标记,然后进行训练。
# 获取对应的目标值:输入:要设置正负样本的anchors,anchor在有效区域的标识,样本标记的bbox及类别label;输出:rpn的分类目标值,RPN的回归目标值
rpn_target_matchs,rpn_target_deltas = model.rpn_head.anchor_target.build_targets(anchors,valid_flags,bbox,label)
# 属于正样本的anchors,与GT交并比较大的anchor,目标值设为1
positive_anchors = tf.gather(anchors,tf.where(tf.equal(rpn_target_matchs,1))[:,1])
# 正样本的个数:一共使用29个属于正样本的anchor
TensorShape([29, 4])
接下来,我们看下负样本的结果,负样本的目标值是-1,负样本的个数是227,与29个正样本一共是256个anchor参与网络训练,其余的不参与网络训练。
# 负样本
negtivate_anchors = tf.gather(anchors,tf.where(tf.equal(rpn_target_matchs,-1))[:,1])
# negtivate_anchors.shape
TensorShape([227, 4])
损失函数计算是将网络预测结果和真实值进行比较,获取两者之间的差别。损失函数由两部分组成:分类和回归
# RPN网络的损失函数
# 输入:rpn的分类结果rpn_class_logits,rpn的回归结果,bbox标注框,label是目标累呗,imagemera图像元信息
# 输出:分类损失和回归损失
rpn_class_loss, rpn_bbox_loss = model.rpn_head.loss(
rpn_class_logits, rpn_deltas, bbox, label, imagemeta)
# 分类损失:rpn_bbox_loss
<tf.Tensor: shape=(), dtype=float32, numpy=0.20614956>
# 回归损失:rpn_class_loss
<tf.Tensor: shape=(), dtype=float32, numpy=0.034301624>
接下来使用梯度下降算法对网络进行训练就可以了
3.2 FastRCNN网络的训练
使用RPN网络收集到的候选区域和imageNet预训练的卷积网络提取的特征对检测的FastRCNN网络进行训练。
# fastRCNN的正负样本设置
# 输入:RPN网络生成的候选区域,bbox是标记框,label是目标类别
# 输出:参与训练的候选区域rois_list,候选区域分类的目标值rcnn_target_matchs_list,回归的目标值rcnn_target_deltas_list
rois_list, rcnn_target_matchs_list, rcnn_target_deltas_list = \
model.bbox_target.build_targets(
proposals_list,bbox, label, imagemeta)
# 获取正样本:
positive_proposal = tf.gather(rois_list[0], tf.where(
tf.not_equal(rcnn_target_matchs_list, 0))[:, 1])
# positive_proposal.shape
TensorShape([64, 4])
# 显示
visualize.draw_boxes(rgd_image[0],positive_proposal.numpy()*1216)
plt.show()
同样我们也可以获取负样本(背景),并绘制在图像上:
# 负样本
negtivate_proposal = tf.gather(rois_list[0], tf.where(
tf.equal(rcnn_target_matchs_list, 0))[:, 1])
# negtivate_proposal.shape
TensorShape([192, 4])
# 显示
visualize.draw_boxes(rgd_image[0],negtivate_proposal.numpy()*1216)
plt.show()
损失函数
# 将参与网络训练的候选区域rois_list送入到ROIpooling层中进行维度固定
pooled_regions_list = model.roi_align(
(rois_list, rcnn_feature_maps, imagemeta), training=True)
# 送入网络中进行预测,得到预测结果
rcnn_class_logits_list, rcnn_probs_list, rcnn_deltas_list = \
model.bbox_head(pooled_regions_list, training=True)
# 计算损失函数:分类和回归
# 输入:网络的预测结果和目标值
rcnn_class_loss, rcnn_bbox_loss = model.bbox_head.loss(
rcnn_class_logits_list, rcnn_deltas_list,
rcnn_target_matchs_list, rcnn_target_deltas_list)
# 分类损失rcnn_class_loss
<tf.Tensor: shape=(), dtype=float32, numpy=0.56958425>
# 回归损失rcnn_bbox_loss
<tf.Tensor: shape=(), dtype=float32, numpy=0.28708345>
3.3 共享卷积训练
用fastRCNN检测网络初始化RPN训练,但是固定共享的卷积层,并且只微调RPN独有的层,现在两个网络共享卷积层了,接下来保持共享的卷积层固定,微调Fast R-CNN的fc层。这样,RPN网络和Fast R-CNN网络共享相同的卷积层,构成一个统一的网络。
Faster R-CNN还有一种端到端的训练方式,可以一次完成训练,将RPN loss与Fast RCNN loss相加,然后进行梯度下降优化,更新参数。
4. 端到端的训练
- 前面已经介绍了网络模型架构和预测结果,在网络预测前我们需要对网络进行训练,接下来使用端到端的方式进行模型训练,基本步骤是:
- 加载数据集:我们在这里使用VOC数据集,所以需要加载VOC数据集
- 模型实例化:加载faster RCNN模型
- 模型训练:计算损失函数,使用反向传播算法对模型进行训练
4.1 数据加载
# 加载数据集
train_dataset = pascal_voc.pascal_voc('train')
# 数据的类别: train_dataset.classes
['background',
'person',
'aeroplane',
'bicycle',
'bird',
'boat',
'bottle',
'bus',
'car',
'cat',
'chair',
'cow',
'diningtable',
'dog',
'horse',
'motorbike',
'pottedplant',
'sheep',
'sofa',
'train',
'tvmonitor']
# 数据类别数量:21
num_classes = len(train_dataset.classes)
4.2 模型实例化
# 指定数据集中类别个数
model = faster_rcnn.FasterRCNN(num_classes=num_classes)
4.3 模型训练
# 1.定义优化器
optimizer = tf.keras.optimizers.SGD(1e-3, momentum=0.9, nesterov=True)
# 模型优化
loss_his = []
# 2.设置epoch,进行遍历获取batch数据送入网络中进行预测
for epoch in range(7):
# 获取索引
indices = np.arange(train_dataset.num_gtlabels)
# 打乱
np.random.shuffle(indices)
# 迭代次数
iter = np.round(train_dataset.num_gtlabels/train_dataset.batch_size).astype(np.uint8)
for idx in range(iter):
# 获取batch数据索引
idx = indices[idx]
# 获取batch_size
batch_image,batch_metas,batch_bboxes,batch_label = train_dataset[idx]
# 3.模型训练,计算损失函数,使用反向传播更新参数
# 3.1 定义作用域
with tf.GradientTape() as tape:
# 3.2 计算损失函数
rpn_class_loss,rpn_bbox_loss,rcnn_class_loss,rcnn_bbox_loss = model((batch_image,batch_metas,batch_bboxes,batch_label),training=True)
# 总损失
loss = rpn_class_loss+rpn_bbox_loss+rcnn_class_loss+rcnn_bbox_loss
# 3.3 计算梯度
grads = tape.gradient(loss,model.trainable_variables)
# 3.4 更新参数值
optimizer.apply_gradients(zip(grads,model.trainable_variables))
print("epoch:%d,batch:%d,loss:%f"%(epoch+1,idx,loss))
loss_his.append(loss)
# 绘制损失函数变化的曲线
plt.plot(range(len(loss_his)),[loss.numpy() for loss in loss_his])
plt.grid()