物体检测算法
RCNN
之前的show锚框、nms筛选最合适的锚框的学习,都是基于假设已知样本。对于未知样本,可以通过模型训练的方式,选择出合适的锚框。
rcnn从输入的图像选取若干锚框,并标注类别和边界框,任何用卷积神经网络进行前向传播,以提取其特征。
每次选择的锚框大小是不一样的。
怎么让锚框大小一样,好批量化处理?
问题: 对每张图片的锚框都抽取特征,相当于,一张图片分成了上千张小图片,每张小图片都单独做特征提取,由于很多区域都是有重叠的,会导致计算量太大且重复计算
改进之后:不对每个锚框,从零开始做特征抽取。而是一开始就得到一张全图像的特征图。
卷积的输入是一整张图片,而不是各个锚框。此外,这个网络还会参与训练
cnn之后,得到一张整个图像的特征图。
比如,在原图上通过启发式搜索,选择了2个锚框,将这两个锚框的位置映射到特征图上,提取出属于这两个锚框的特征。经由rol池化,得到了两个形状相同的特征。然后通过全连接层。
启发式搜索算法,就是在状态空间中先对每一条搜索分支进行评估,得到最好的分支,再从这个分支继续搜索从而到达目标,这样可以有效省略大量无谓的搜索路径,大大提高了搜索效率。在启发式搜索中,对分支的评估是非常重要的,启发式搜索算法定义了一个估价函数 f(x),与问题相关的启发式信息都被计算为一定的 f(x) 值,引入到搜索过程中。f(x) = g(x) + h(x),其中 f(x) 是节点 x的估价函数,g(x)是在状态空间中从初始节点到节点 x的实际代价,h(x)是从节点 x到目标节点的最佳路径的估计代价。g(x)是已知的,所以在这里主要是 h(x) 体现了搜索的启发信息,h(x)专业的叫法是启发函数。换句话说,g(x)代表了搜索的广度的优先趋势。但是当 h(x) >> g(x)时,可以忽略g(x),从而提高效率。
就是动态规划的思想
faster rcnn 使用一个区域提议网络,减少提议区域(比如,锚框)的生成数量,并确保目标检测的精确度
步骤1、使用1*1且填充为1的卷积层,转换卷积神经网络的输出,保持输出通道数为c。这样,cnn为图像提取的特征图的每个单元均得到一个长度为c的新特征
步骤2、以特征图的每个像素为中心,生成多个不同比例、宽高比的锚框,并标注
步骤3、使用锚框中心单元长度为c的特征,分别预测该锚框的二元类别(背景还是含目标)和边界框
步骤4、使用非极大值抑制,只得到最相似的边界框。
SSD 单发多框检测
import torch
import d2l
from torch import nn
import d2l.torch
import torchvision
#类别预测层
#使用一个保持宽度和高度不变的卷积层,让输入输出在特征图宽和高上的空间坐标一一对应
def class_predictor(num_inputs,num_anchors_per_pixel,num_class):
return nn.Conv2d(
in_channels=num_inputs,out_channels=num_anchors_per_pixel*(num_class+1),
kernel_size=3,padding=1)
#对于一个坐标(x,y),其通道包括以(x,y)为中心的所有锚框的类别预测
#所以,输出通道数就是每个锚框的所有类别可能数(1是背景)
#边界预测框
def bbox_offset_predictor(num_inputs,num_anchors_per_pixel):
return nn.Conv2d(
in_channels=num_inputs,
out_channels=num_anchors_per_pixel*4,
kernel_size=3,padding=1)
#为每个锚框预测4个偏移量,左上两个,右下两个
#输出通道数是,以每个像素为中心的锚框数*4
#连接多尺度的预测
def forward(x,block):
return block(x)
#例子
Y1 = forward(torch.zeros(size=(2,8,20,20)),class_predictor(8,5,10))
#生成2个不一样的特征图,通道数8,大小20*20
#类别预测8个输入通道数,5个输出通道数(锚框数),类别10种
Y2 = forward(torch.zeros(size=(2,16,10,10)),class_predictor(16,3,10))
Y1.shape,Y2.shape#输出(批量大小,通道数,高,宽)
#由于y1和y2大小不一样,可以知道,两个输出预测没法连接,提高效率
#将张量格式转化成2d。除了一样的批量大小,剩下的几个维度拉到同一维度
def flatten_pred(pred):
return torch.flatten(pred.permute(0,2,3,1),start_dim=1)
#把通道数放到最后,从1号维度开始,拉成一个向量
def concat_preds(preds):
return torch.cat([flatten_pred(pred) for pred in preds],dim=1)
#把向量维度转化格式之后,连接
#构建的高和宽减半块会更改输入通道的数量,并将输入特征图的高度和宽度减半
def down_sample_block(in_channels,out_channels):
#VGG模块设计
blk = []
for _ in range(2):
#卷积、批量归一化、激活
blk.append(nn.Conv2d(in_channels=in_channels,out_channels=out_channels,kernel_size=3,padding=1))
blk.append(nn.BatchNorm2d(num_features=out_channels))
blk.append(nn.ReLU())
in_channels = out_channels
blk.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*blk)
Y3 = forward(torch.zeros(size=(2,10,20,20)),down_sample_block(10,50))
Y3.shape
#基础网络块
#用于从图像中抽取特征,就是3个高宽减半块串联在一起 ,并逐步通道数翻倍
def base_net():
blk = []
num_filters = [3,16,32,64]
for i in range(len(num_filters)-1):
blk.append(down_sample_block(in_channels=num_filters[i],out_channels=num_filters[i+1]))
return nn.Sequential(*blk)
Y4 = forward(torch.zeros(size=(2,3,256,256)),base_net())
Y4.shape
#完整的单发多框检测模型由5个模块组成
def get_block(i):
if i == 0:
block = base_net()#第1块基础网络块
#用于从图像中抽取特征,就是3个高宽减半块串联在一起 ,并逐步通道数翻倍
elif i ==1 :
block = down_sample_block(in_channels=64,out_channels=128)
#通道数从64变128,但是大小减半
elif i == 4:
block = nn.AdaptiveAvgPool2d(output_size=(1,1))
#第5个块,最终压缩成1*1大小
else:
block = down_sample_block(in_channels=128,out_channels=128)
#通道数不变,大小减半
return block
#为每个模块定义前向传播
def block_forward(X,block,size,ratio,block_class_predictor,block_bbox_offset_predictor):
Y = block(X)
#对图像x生成特征图y
anchors = d2l.torch.multibox_prior(data=Y,sizes=size,ratios=ratio)
#对特征图y,生成若干锚框
class_predictor = block_class_predictor(Y)
#每个锚框的类别
bbox_offset_predictor = block_bbox_offset_predictor(Y)
#每个锚框的偏移
return (Y,anchors,class_predictor,bbox_offset_predictor)
class TinySSD(nn.Module):#完整的ssd网络模型
def __init__(self,num_classes):
super(TinySSD,self).__init__()
self.num_classes = num_classes
idx_to_in_channels = [64,128,128,128,128]
for i in range(5):
# 即赋值语句self.blk_i=get_blk(i)
setattr(self,f'block_{i}',get_block(i))
setattr(self,f'block_class_predictor_{i}',
class_predictor(
num_inputs=idx_to_in_channels[i],
num_anchors_per_pixel=num_anchors_per_pixel,
num_class=num_classes))
setattr(self,f'block_bbox_offset_predictor_{i}',
bbox_offset_predictor(
num_inputs=idx_to_in_channels[i],
num_anchors_per_pixel=num_anchors_per_pixel))
def forward(self,X):
anchors,class_predictors,bbox_offset_predictors=[None]*5,[None]*5,[None]*5
for i in range(5):
# getattr(self,'blk_%d'%i)即访问self.blk_i
X,anchors[i],class_predictors[i],bbox_offset_predictors[i] =
block_forward(
X,getattr(self,f'block_{i}'),
sizes[i],ratios[i],
getattr(self,f'block_class_predictor_{i}'),
getattr(self,f'block_bbox_offset_predictor_{i}'))
anchors = torch.cat(anchors,dim=1)
class_predictors = concat_preds(class_predictors)
class_predictors = class_predictors.reshape(
class_predictors.shape[0],-1,self.num_classes+1)
bbox_offset_predictors = concat_preds(bbox_offset_predictors)
return anchors,class_predictors,bbox_offset_predictors
batch_size = 32
train_iter,_ = d2l.torch.load_data_bananas(batch_size)
#香蕉检测数据集中,目标的类别数为1,定义好模型后,需要初始化其参数并定义优化算法。
#定义网络和优化算法
device,net = d2l.torch.try_gpu(0),TinySSD(num_classes=1)
optim = torch.optim.SGD(params=net.parameters(),lr=0.2,weight_decay=5e-4)
#定义损失函数
class_loss = nn.CrossEntropyLoss(reduction='none')
bbox_offset_loss = nn.L1Loss(reduction='none')
#不用l2损失,是因为有可能预测和真实相差很大,l2会让差距更大
def calculate_loss(class_preds,class_labels,bbox_offset_preds,bbox_offset_labels,bbox_mask):
batch_sizes,num_classes = class_preds.shape[0],class_preds.shape[2]
cla_ls = class_loss(class_preds.reshape(-1,num_classes),
class_labels.reshape(-1)).reshape(batch_sizes,-1).mean(dim=1)
bbox_offset_ls = bbox_offset_loss(bbox_offset_preds*bbox_mask,
bbox_offset_labels*bbox_mask).mean(dim=1)
return cla_ls+bbox_offset_ls
#定义分类评价函数、偏移评价函数
def class_eval(class_preds,class_labels):
# 由于类别预测结果放在最后一维,argmax需要指定最后一维。
return float((torch.argmax(class_preds,dim=-1).type(class_labels.dtype) == class_labels).sum())
def offset_eval(bbox_offset_preds,bbox_offset_labels,bbox_mask):
return float((torch.abs((bbox_offset_labels-bbox_offset_preds)*bbox_mask)).sum())
#模型训练
#在模型的前向传播过程中,生成多尺度锚框,并预测其类别和偏移量
#然后根据标签信息y为生成的锚框标注类别和偏移量
#根据预测和真实计算损失
num_epochs,timer = 20,d2l.torch.Timer()
animator = d2l.torch.Animator(xlabel='epoch',xlim=[1,num_epochs],legend=['class error','bbox mae'])
net = net.to(device)
for epoch in range(num_epochs):
# 批量类别损失和,批量样本数
# 批量偏移损失和,批量样本数
accumulator = d2l.torch.Accumulator(4)
net.train()
for X,Y in train_iter:
timer.start()
X,Y = X.to(device),Y.to(device)
optim.zero_grad()
# 生成多尺度的锚框,为每个锚框预测类别和偏移量
anchors,class_preds,bbox_offset_preds = net(X)
# 为每个锚框标注类别和偏移量
bboxes_offset_labels, bboxes_masks, bboxes_class_labels = d2l.torch.multibox_target(anchors,Y)
# 根据类别和偏移量的预测和标注值计算损失函数
loss = calculate_loss(class_preds,bboxes_class_labels,bbox_offset_preds,bboxes_offset_labels,bboxes_masks)
loss.mean().backward()
optim.step()
accumulator.add(class_eval(class_preds,bboxes_class_labels),bboxes_class_labels.numel(),offset_eval(bbox_offset_preds,bboxes_offset_labels,bboxes_masks),bboxes_offset_labels.numel())
class_error,offset_mae = 1-accumulator[0]/accumulator[1],accumulator[2]/accumulator[3]
animator.add(epoch+1,[class_error,offset_mae])
print('class_error:',class_error,'offset_mae:',offset_mae)
print(f'{len(train_iter.dataset)/timer.stop()}examples/s,on ',str(device))
训练结果
YOLO
如果我们让每个框只能识别出一个物体,且要求这个物体必须在这个框之内,那YOLO就变成了很蠢的滑窗法了。
YOLO的聪明之处在于,它只要求这个物体的中心落在这个框框之中。
这意味着,我们不用设计非常非常大的框,因为我们只需要让物体的中心在这个框中就可以了,而不是必须要让整个物体都在这个框中。
具体怎么实现呢?
我们要让这个 个框每个都预测出B个bounding box,这个bounding box有5个量,分别是物体的中心位置(x,y)和它的高(h)和宽(w),以及这次预测的置信度。
每个框框不仅只预测B个bounding box,它还要负责预测这个框框中的物体是什么类别的,这里的类别用one-hot编码表示。
注意,虽然一个框框有多个bounding boxes,但是只能识别出一个物体,因此每个框框需要预测物体的类别,而bounding box不需要。
也就是说,如果我们有个框框,每个框框的bounding boxes个数为B,分类器可以识别出C种不同的物体,那么所有整个ground truth的长度为:
写给小白的YOLO介绍 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/94986199