物体检测中的困难样本挖掘,之前有一个名词叫做hard negative mining困难负样本挖掘
先讨论什么是on-line hard example mining(OHEM):例如一个背景,被很大概率预测为前景了,那么这个背景就是困难样本;对于一个前景(ground truth classes为foreground),网络模型预测它很大的概率值为背景,则这个前景也是困难样本,也就是说,在线困难忘本挖掘是不区分正负样本的(但是在代码实现中,由于正样本个数较少,通常保留所有的正样本,然后按照1:3的比例采样出3倍数量的负样本),它希望更多的训练困难样本,而不是简单样本,具体的程序实现在 torchcv中的ssdloss.py中有所体现。
在SSD和FPNSSD编码器中,都是将IOU_max大于0.5的anchor设置为正样本,小于0.5的设置为负样本,
classification target为 -1 ingore 0 negative examples 1 2 3 ~ num_classes positive examples
然后计算SSDLoss时,也是先计算出所有正样本anchor boxes和所有负样本anchor boxes的classification loss,但是size_average参数设置为False,然后从所有的正负样本中找出所有的困难样本。
ingore(classification target=-1)的classification loss=0
然后取出所有的正样本的loss,并记录当前batch size图像中positive examples总数,再对所有的负样本的classification loss进行排序,取出前3*(num positive examples)个负样本,这是因为负样本的个数要比正样本多出很多。
也就是说,SSD训练过程中的正负样本比例是1:3,只对负样本进行了困难样本挖掘,不对正样本进行困难样本挖掘,这样做的主要原因在于,设置IOU_max>0.5做为正样本,或者说在众多的anchor boxes中,正样本的个数很少,大多数都是负样本,这样会带来严重的类别不平衡问题,故而设定固定比例,取出较为困难(体现在classification loss大)的负样本,计算classification loss。在每次训练过程中的困难负样本都不一样,故而称之为在线困难样本挖掘(因为每次模型参数不一样,negative examples的classification loss排序不同)
class SSDLoss(nn.Module):#损失函数 def __init__(self, num_classes): super(SSDLoss, self).__init__() self.num_classes = num_classes#类别总数,对于VOC数据集而言,是21类 def _hard_negative_mining(self, cls_loss, pos): '''Return negative indices that is 3x the number as postive indices. Args: cls_loss: (tensor) cross entroy loss between cls_preds and cls_targets, sized [N,#anchors]. 分类损失值 pos: (tensor) positive class mask, sized [N,#anchors]. Return: (tensor) negative indices, sized [N,#anchors]. ''' cls_loss = cls_loss * (pos.float() - 1) #对于正样本,损失值为0,得到对于负样本计算出的损失值,损失值越大的负样本,cls_loss值越小 #正样本损失值 0 #负样本损失值=之前的负样本损失值*(-1) #这是因为_hard_negative_mining只返回所有的负样本classification loss #从所有的负样本中采样出前(3*num_positive)个负样本的loss #这些负样本的classification loss最大,是困难的负样本 _, idx = cls_loss.sort(1) # sort by negative losses ''' cls_loss: [N,#anchors] 正样本的损失值为0,对于负样本,损失值越大,cls_loss越小 tensor.sort方法返回sort之后的按升序排列的tensor和对应的indices 对每一行,遍历所有的列,则得到的每一行按照升序排列,即对于每个input images,得到其按照升序排列的分类损失idx idx同样是[N,#anchors].的tensor,其中的每一行的值范围为 [0,1,2,……,8732] 表示当前input image 的所有anchors的负样本的分类损失 由大到小的索引排序 ''' _, rank = idx.sort(1) # [N,#anchors] num_neg = 3*pos.sum(1) # [N,] #num_neg为长度为batch size 的tensor,其中的每个元素表示3*当前input image中的正样本个数 neg = rank < num_neg[:,None] # [N,#anchors] neg中的数值为1或者0 如果是hard negative examples,则对应位置处的值为1 ''' 对于当前batch size张图像中的每一张(每一张图像中的正样本不同) 找到是当前图像中正样本数量3倍的负样本,并且固定数量的负样本是通过在线困难样本挖掘得到的 这主要是为了解决计算分类损失函数时样本不均衡的问题,因为比如说SSD300这种模型中8732个default boxes 中的正样本数量很少(与ground truth 的overlap大于0.5,在box_coder.encode函数中设置) 为了保证在同一张图像中的正负样本比例在1:3,故而使用在线困难样本挖掘(在线指的是在训练过程中,这意味着 在每次训练过程中,每次挖掘到的困难负样本可能是不同的,要根据网络模型预测的输出值决定) 算法如下: 首先取出所有的负样本,对于当前batch_size*#anchors ,对于每一行(每张训练图像)的分类损失值进行排序 按照当前图像中正样本的数量的3倍取出loss值排在前面的负样本) 负样本的分类损失值计算:np.log(p) 小 p小,就是说对于负样本预测为背景类的概率值小,就是预测为前景的概率值大 这些是很容易被分类错的负样本,被称为困难负样本,这些样本的loss值很大,对于网络模型的参数更新非常有效 而那些很容易就能被分类正确的负样本对于最终权值更新效果不大,故而舍弃 ''' return neg def forward(self, loc_preds, loc_targets, cls_preds, cls_targets): '''Compute loss between (loc_preds, loc_targets) and (cls_preds, cls_targets). 计算分类损失和回归损失 Args: loc_preds: (tensor) predicted locations, sized [N, #anchors, 4]. 对于当前batch size的图像所预测出来的localization N=batch_size,#anchors表示default boxes的数量 loc_targets: (tensor) encoded target locations, sized [N, #anchors, 4]. cls_preds: (tensor) predicted class confidences, sized [N, #anchors, #classes]. 对于当前batch size的图像所预测出来的classification N=batch_size,#anchors表示default boxe数量,#classes表示数据集类别总数 cls_targets: (tensor) encoded target labels, sized [N, #anchors]. batch_size行,#anchors列,第i行第j列的元素表示对于第i个训练样本图像,SSD预测出来的第j个default boxes的GT类别标号(一个int类型整数) loss: (tensor) loss = SmoothL1Loss(loc_preds, loc_targets) + CrossEntropyLoss(cls_preds, cls_targets). 位置回归损失 交叉熵分类损失 ''' pos = cls_targets > 0 # [N,#anchors] pos中的数值是 0 1 ''' cls_targets是经过编码之后的classification ground truth 表示与ground truth bounding boxes的IOU值最大或者大于一定的阈值的anchor boxes则会被认为是正样本,为1 负样本为-1 在encoder阶段, ''' batch_size = pos.size(0)#每个batch 中包含多少张训练图片 num_pos = pos.sum().item()#对pos 2-Dtensor求和,得到当前batch size的训练图片中共有多少个anchor boxes为正样本 #当前batch size 数量的输入图像中,positive examples(这里的正样本指的是default boxes而不是一整张图像)的数量 #=============================================================== # loc_loss = SmoothL1Loss(pos_loc_preds, pos_loc_targets) #=============================================================== mask = pos.unsqueeze(2).expand_as(loc_preds) # [N,#anchors,4] loc_loss = F.smooth_l1_loss(loc_preds[mask], loc_targets[mask], size_average=False)#只对正样本进行回归损失的计算 #mask是# [N,#anchors,4]的3-dimension tensor,扩展的第2维度与之前的数值相同,即对于正样例(batch size中的第i幅图片中的第j个anchors) #mask[i,j,:]=1,如果为负样本则mask[i,j,:]=0 #mask作下标则表示其中元素值为1的下标,即所有的正样本所在的下标(4) #=============================================================== # cls_loss = CrossEntropyLoss(cls_preds, cls_targets) #=============================================================== cls_loss = F.cross_entropy(cls_preds.view(-1,self.num_classes), \ cls_targets.view(-1), reduce=False) # [N*#anchors*num_classes,] ''' cls_preds:[N,#anchors,num_classes] view cls_preds:[(N*#anchors),num_classes] cls_targets:[N*#anchors,] 计算多分类的交叉损失函数是cross_entropy,reduce参数为false,则返回值cls_loss维度为(N*#anchors) 分别给出了这一个batch size中每张图像所有anchor boxes的分类损失值得 ''' cls_loss = cls_loss.view(batch_size, -1)#cls_loss:[N,#anchors] cls_loss[cls_targets<0] = 0 # set ignored loss to 0 现将所有负样本的分类损失变成0,这是为了使用hard negative mining算法挑选出困难负样本 neg = self._hard_negative_mining(cls_loss, pos) # [N,#anchors] cls_loss = cls_loss[pos|neg].sum() ''' 正样本具有分类损失和回归损失,SSD中的正样本包括最大的IOU和IOU值大于0.5的region proposal 一般的负样本没有分类损失,也没有回归损失 hard negative examples具有分类损失,不具有回归损失 实际上训练时采用的正负样本是所有的正样本和所有的hard negative examples, ''' print('loc_loss: %.3f | cls_loss: %.3f' % (loc_loss.item()/num_pos, cls_loss.item()/num_pos), end=' | ') loss = (loc_loss+cls_loss)/num_pos return loss
这里想先记录一下,老板说在物体检测或者特定物体检测的问题中,数据增强中的尺度变换非常重要,可能直接对于最后的结果带来3%的提升,其次是随机旋转-10~10度,可能带来 1%的提升,最后是常规的水平方向翻转的操作。