计算机视觉PyTorch实现目标检测:SSD

目标检测算法概况

目前目标检测点主流算法分为二种类型

  • 二阶段方法:如R-CNN系算法,其主要思路是先通过启发式方法(selective search)或者CNN网络(RPN)产生一系列稀疏的候选框,然后对这些候选框进行分类与回归two-stage方法的优势是准确度高
  • 单阶段方法:如Yolo和SSD,其主要思路是均匀地在图片的不同位置进行密集抽样,抽样时可以采用不同尺度和长宽比,然后利用CNN提取特征后直接进行分类与回归,整个过程只需要一步,所以其优势是速度快
    计算机视觉PyTorch实现目标检测:SSD_第1张图片

目标检测知识点

目标检测的算法,通常都是对图片上的四个参数做处理:分别是中心点x轴、y轴坐标,框的高和宽

对图片做目标检测,通常是通过卷积,将原始图片,卷积成不同尺寸大小的图片,例如一张cat的图片,通过卷积可以生成如下尺寸的图像。
计算机视觉PyTorch实现目标检测:SSD_第2张图片
通常尺寸越小的越容易检测大的物体,尺寸越大适合检测小的物体。

SSD算法知识点

先验框

先验框功能通常是帮助我们定好常见目标的宽和高,在进行预测的时候,我们可以利用这个已经定好的宽和高处理,可以帮助我们进行预测。

在进行模型训练的时候,通过分割网格,来生成不同的先验框,并先验框对图像进行处理,通过训练调整先验框位置,来正确框出图像中的事物

每一层网格都对应一层先验框对图像中的事物做处理,可以理解为每一层卷积后对图像中的事物做一次先验框框出目标事物,随着更深层的网格训练,先验框也在不断调整位置和大小,使得框出的事物越来越精确

过程大致如下图所示:
计算机视觉PyTorch实现目标检测:SSD_第3张图片
对于检测COCO数据集,它输出的就是一个(13x13,(80+5)*5)的数据,13x13对应图像的网格点,*5表示每个网格点上有五个先验框,每个先验框有85个参数,其中80对应着有80个网格通道数,5分别对应着先验框中心坐标x、中心点坐标y、先验框高h和宽先验框w、置信度(即分类结果属于那种类型的概率)。
计算机视觉PyTorch实现目标检测:SSD_第4张图片
计算机视觉PyTorch实现目标检测:SSD_第5张图片

SSD算法原理

SSD算法是一种单阶段的目标检测算法,通过输入一张图片,神经网络可以预测出对应物体的边框以及对应边框下物体的种类信息。

在图像分类网络构架中,通常使用VGG16算法作为特征提取的骨架网络,而SSD模型与VGG16模型在网络架构有很多相似的,

VGG模型架构
计算机视觉PyTorch实现目标检测:SSD_第6张图片
不同的是

  • SSD的输入大小与VGG不一样,SSD模型采用了不同的输入图像尺寸,常用的模型有SSD300和SSD512,分别代表300x300和512x512对输入图像大小。
  • 在网络架构上SSD采用VGG16作为基础模型,然后在VGG16的基础上新增了卷积层来获得更多的特征图以用于检测。SSD的网络结构如下图所示

SSD架构:

SSD算法网络中,通过输入图像进过VGG16的conv1~conv5计算,并保留conv4,conv5的中间特征输出用于后续的预测。

卷积过程

  1. 输入图像为(300x300x3)
  2. Conv1:经过二次 大小为(3x3x64),步长为1的卷积核,输出为图像为 (300x300x64)。再经过一次(2x2),步长为2的最大池化层,输出图像为(150x150x64)
  3. Conv2:经过二次 大小为(3x3x128),步长为1的卷积核,输出为图像为 (150x150x128)。再经过一次(2x2),步长为2的最大池化层,输出图像为(75x75x128)
  4. Conv3:经过三次 大小为(3x3x256),步长为1的卷积核,输出为图像为 (75x75x256)。再经过一次(2x2),步长为2的最大池化层,输出图像为(38x38x256)
  5. Conv4:经过三次 大小为(3x3x512),步长为1的卷积核,输出为图像为 (38x38x512)。再经过一次(2x2),步长为2的最大池化层,输出图像为(19x19x512)
  6. Conv5:经过三次 大小为(3x3x512),步长为1的卷积核,输出为图像为 (19x19x512)。再经过一次(3x3),步长为1的最大池化层,输出图像为(19x19x512)
  7. 利用卷积代替全连接层:进行了一次(3x3x1024)卷积网络和一次(1x1x1024)卷积网络,分别为fc6和fc7,输出的通道数为1024,因此输出的net为(19,19,1024)。(从这里往前都是VGG的结构)
  8. Conv6:经过一次(1x1x256)步长为1的卷积核,再经过一次(3x3x512),步长为2的卷积核,输出图像为(10x10x512)
  9. Conv7:经过一次(1x1x128)步长为1的卷积核,再经过一次(3x3x256),步长为2的卷积核,输出图像为(5x5x256)
  10. Conv8:经过一次(1x1x128)步长为1的卷积核,再经过一次(3x3x256),步长为2的卷积核,输出图像为(3x3x256)
  11. Conv9:经过一次(1x1x128)步长为1的卷积核,再经过一次(3x3x256),步长为2的卷积核,输出图像为(1x1x256)

回溯到上面的先验框知识点,这里在SSD算法网络中利用Conv4、fc7、Conv6、Conv7、Conv8、Conv9各层生成的特征,增加先验框处理,从Conv4到Conv9实现先验框不断的优化。

先验框操作

  1. 进行一次(1 x 1x(num x 4))卷积处理,其中num表示每个层的先验框数量,其值在Conv4到Conv9中对应为(4、6、6、6、4、4),num x 4 表示对应的卷积核数量
  2. 进行一次(1x1x(num x classes))卷积处理,其中classes对应分类的类别,num x classes表示卷积核数量
  3. 计算对应的先验框

先验框流程

Conv4到Conv9中对应为(4、6、6、6、4、4)
classes为 21

  1. Conv4:输入图像为(38x38x512),经过一次(1x1x(4x4))步长为1的卷积,得到(38x38x16),再经过一次(1x1x(4x21)),步长为1的卷积,得到(38x38x84),最后获得先验框(38x38x16)。
  2. fc7:输入图像为(19x19x1024),经过一次(1x1x(4x6))步长为1的卷积,得到(19x19x24),再经过一次(1x1x(6x21)),步长为1的卷积,得到(19x19x126),最后获得先验框(19x19x24)。
  3. Conv6:输入图像为(10x10x512),经过一次(1x1x(4x6))步长为1的卷积,得到(10x10x24),再经过一次(1x1x(6x21)),步长为1的卷积,得到(10x10x126),最后获得先验框(10x10x24)。
  4. Conv7:输入图像为(5x5x256),经过一次(1x1x(4x6))步长为1的卷积,得到(5x5x24),再经过一次(1x1x(6x21)),步长为1的卷积,得到(5x5x126),最后获得先验框(5x5x24)。
  5. Conv8:输入图像为(3x3x256),经过一次(1x1x(4x4))步长为1的卷积,得到(3x3x16),再经过一次(1x1x(4x21)),步长为1的卷积,得到(3x3x84),最后获得先验框(3x3x16)。
  6. Conv9:输入图像为(1x1x256),经过一次(1x1x(4x4))步长为1的卷积,得到(1x1x16),再经过一次(1x1x(4x21)),步长为1的卷积,得到(1x1x84),最后获得先验框(1x1x16)。

最后根据得到的预测置信度,进行得分排序与非极大抑制筛选(去除多余的边框)

Pytorch实现SSD算法

VGG-16代码实现

这里只实现Conv1~Conv4层代码

class VGG16(nn.Module):
      def __init__(self):
          super(VGG16,self).__init__()
          self.layers=self.make_layers()
      
      def forward(self,x):
          y=self.layers(x)
          return y

      def make_layers(self):
          cfg=[64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512,'M',512, 512, 512]
          layers=[]
          in_channels=3
          for x in cfg:
              if x=='M':
                 layers+=[nn.MaxPool2d(kernel_size=2,stride=2)]
              elif x=='c':
                 layers=[nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
              else:
                 layers+=[nn.Conv2d(in_channels,x,kernel_size=3,padding=1),nn.ReLU(True)]
                 in_channels=x
          return nn.Sequential(*layers)         

SSD模型特征提取代码实现

这里实现Conv5~Conv9代码

class SSD(nn.Module):
      def __init__(self):
          super(SSD,self).__init__()
          
          #导入上面的模块
          self.features=VGG16()
          self.norm4=L2Norm(512,20)

          self.conv5_1=nn.Conv2d(512,512,kernel_size=3,padding=1,dilation=1)
          self.conv5_2=nn.Conv2d(512,512,kernel_size=3,padding=1,dilation=1)
          self.conv5_3=nn.Conv2d(512,512,kernel_size=3,padding=1,dilation=1)

          self.fc6=nn.Conv2d(512,1024,kernel_size=3,padding=6,dilation=6)
          self.fc7=nn.Conv2d(1024,1024,kernel_size=1)
          self.conv6_1=nn.Conv2d(1024,256,kernel_size=1,stride=1)
          self.conv6_2=nn.Conv2d(256,512,kernel_size=3,stride=2,padding=1)
          self.conv7_1=nn.Conv2d(512,128,kernel_size=1, stride=1)
          self.conv7_2=nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1))
          self.conv8_1=nn.Conv2d(256, 128, kernel_size=1, stride=1)
          self.conv8_2=nn.Conv2d(128, 256, kernel_size=3, stride=1)
          self.conv9_1=nn.Conv2d(256, 128, kernel_size=1, stride=1)
          self.conv9_2=nn.Conv2d(128, 256, kernel_size=3, stride=1)

      def forward(self,x):
          hs=[]
          h=self.features(x)
          hs.append(self.norm4(h))

          h=F.max_pool2d(kernel_size=2, stride=2, ceil_mode=True)
          h=F.relu(self.conv5_1(h))
          h=F.relu(self.conv5_2(h))
          h=F.relu(self.conv5_3(h))
          h=F.max_pool2d(h,kernel_size=3, stride=1, padding=1,ceil_mode=True)
          h=F.relu(self.fc6(h))
          h=F.relu(self.fc7(h))
          hs.append(h) #第一个先验框

          h=F.relu(self.conv6_1(h))
          h=F.relu(self.conv6_2(h))
          hs.append(h) #第二个先验框
          
          h=F.relu(self.conv7_1(h))
          h=F.relu(self.conv7_2(h))
          hs.append(h) #第三个先验框

          h=F.relu(self.conv8_1(h))
          h=F.relu(self.conv8_2(h))
          hs.append(h) #第四个先验框

          h=F.relu(self.conv9_1(h))
          h=F.relu(self.conv9_2(h))
          hs.append(h) #第五个先验框

          return hs

class L2Norm(nn.Module):
      def __init__(self,in_features,scale):
          super(L2Norm,self).__init__()
          self.weight=nn.Parameter(torch.Tensor(in_features))
          self.reset_parameters(scale)

      def reset_parameters(self,scale):
          nn.init.constant(self.weight,scale)

      def forward(self,x):
          x=F.normalize(x,dim=1)
          scale=self.weight(None,:,None,None)
          return scale*x            

先验框代码实现(获取特征)

class get_ssd(nn.Module):
      #定义步长
      steps=(8,16,32,64,100,300)
      #定义先验框的基础大小
      box_sizes=(30,60,111,162,213,264,315)
      #定义先验框的高宽比
      aspect_ratios=((2,),(2,3),(2,3),(2,3),(2,),(2,))
      fm_sizes=(38,19,10,5,3,1)
   
      def __init__(self,num_classes):
          super(get_ssd,self).__init()
          self.num_classes=num_classes
          self.num_anchors=(4,6,6,6,4,4)
          self.in_channels=(512,1024,512,256,256,256)
          self.extractor=SSD()
          self.loc_layers=nn.ModuleList()
          self.cls_layers=nn.ModuleList()
          for i in range(len(self.in_channels)):
              self.loc_layers+=[nn.Conv2d(self.in_channels[i],self.num_anchors[i]*4,kernel_size=3,padding=1)]
              self.cls_layers+=[nn.Conv2d(self.in_channels[i],self.num_anchors[i]*self.num_classes,kernel_size=3,padding=1)]
       def forward(self,x):
           loc_preds=[]
           cls_preds=[]
           xs=self.extractor(x)
           for i,x in enumerate(xs):
               loc_pred=self.loc_layers[i][x]
               loc_pred=loc_pred.permute(0,2,3,1).contiguous()
               loc_preds.append(loc_pred.view(loc_pred.size(0),-1,4))
               cls_pred=self.cls_layers[i][x]
               cls_pred=cls_pred.permute(0,2,3,1).contiguous()
               cls_preds.append(cls_pred.view(cls_pred.size(0),-1,self.num_classes))
           los_preds=torch.cat(loc_preds,1)
           cls_preds=torch.cat(cls_preds,1)
           return loc_preds,cls_preds

先验框函数

def default_prior_box():
    mean_layer = []
    for k,f in enumerate(Config.feature_map):
        mean = []
        for i,j in product(range(f),repeat=2):
            f_k = Config.image_size/Config.steps[k]
            cx = (j+0.5)/f_k
            cy = (i+0.5)/f_k

            s_k = Config.sk[k]/Config.image_size
            mean += [cx,cy,s_k,s_k]

            s_k_prime = sqrt(s_k * Config.sk[k+1]/Config.image_size)
            mean += [cx,cy,s_k_prime,s_k_prime]
            for ar in Config.aspect_ratios[k]:
                mean += [cx, cy, s_k * sqrt(ar), s_k/sqrt(ar)]
                mean += [cx, cy, s_k / sqrt(ar), s_k * sqrt(ar)]
        if Config.use_cuda:
            mean = torch.Tensor(mean).cuda().view(Config.feature_map[k], Config.feature_map[k], -1).contiguous()
        else:
            mean = torch.Tensor(mean).view( Config.feature_map[k],Config.feature_map[k],-1).contiguous()
        mean.clamp_(max=1, min=0)
        mean_layer.append(mean)

    return mean_layer

损失函数计算

class LossFun(nn.Module):
    def __init__(self):
        super(LossFun,self).__init__()
    def forward(self, prediction,targets,priors_boxes):
        loc_data , conf_data = prediction
        loc_data = torch.cat([o.view(o.size(0),-1,4) for o in loc_data] ,1)
        conf_data = torch.cat([o.view(o.size(0),-1,21) for o in conf_data],1)
        priors_boxes = torch.cat([o.view(-1,4) for o in priors_boxes],0)
        if Config.use_cuda:
            loc_data = loc_data.cuda()
            conf_data = conf_data.cuda()
            priors_boxes = priors_boxes.cuda()
        # batch_size
        batch_num = loc_data.size(0)
        # default_box数量
        box_num = loc_data.size(1)
        # 存储targets根据每一个prior_box变换后的数据
        target_loc = torch.Tensor(batch_num,box_num,4)
        target_loc.requires_grad_(requires_grad=False)
        # 存储每一个default_box预测的种类
        target_conf = torch.LongTensor(batch_num,box_num)
        target_conf.requires_grad_(requires_grad=False)
        if Config.use_cuda:
            target_loc = target_loc.cuda()
            target_conf = target_conf.cuda()
        # 因为一次batch可能有多个图,每次循环计算出一个图中的box,即8732个box的loc和conf,存放在target_loc和target_conf中
        for batch_id in range(batch_num):
            target_truths = targets[batch_id][:,:-1].data
            target_labels = targets[batch_id][:,-1].data
            if Config.use_cuda:
                target_truths = target_truths.cuda()
                target_labels = target_labels.cuda()
            # 计算box函数,即公式中loc损失函数的计算公式
            utils.match(0.5,target_truths,priors_boxes,target_labels,target_loc,target_conf,batch_id)
        pos = target_conf > 0
        pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
        # 相当于论文中L1损失函数乘xij的操作
        pre_loc_xij = loc_data[pos_idx].view(-1,4)
        tar_loc_xij = target_loc[pos_idx].view(-1,4)
        # 将计算好的loc和预测进行smooth_li损失函数
        loss_loc = F.smooth_l1_loss(pre_loc_xij,tar_loc_xij,size_average=False)

        batch_conf = conf_data.view(-1,21)

        # 参照论文中conf计算方式,求出ci
        loss_c = utils.log_sum_exp(batch_conf) - batch_conf.gather(1, target_conf.view(-1, 1))

        loss_c = loss_c.view(batch_num, -1)
        # 将正样本设定为0
        loss_c[pos] = 0

        # 将剩下的负样本排序,选出目标数量的负样本
        _, loss_idx = loss_c.sort(1, descending=True)
        _, idx_rank = loss_idx.sort(1)

        num_pos = pos.long().sum(1, keepdim=True)
        num_neg = torch.clamp(3*num_pos, max=pos.size(1)-1)

        # 提取出正负样本
        neg = idx_rank < num_neg.expand_as(idx_rank)
        pos_idx = pos.unsqueeze(2).expand_as(conf_data)
        neg_idx = neg.unsqueeze(2).expand_as(conf_data)

        conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, 21)
        targets_weighted = target_conf[(pos+neg).gt(0)]
        loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)

        N = num_pos.data.sum().double()
        loss_l = loss_loc.double()
        loss_c = loss_c.double()
        loss_l /= N
        loss_c /= N
        return loss_l, loss_c

你可能感兴趣的:(深度学习,计算机视觉)