经过几天连续的开坑和读源代码,对 mmdetection 的配置流程了解的差不多了。考虑一个实例应用,尝试着将 FGSM 攻击算法的制作的对抗样本植入目标检测中,企图增加网络的鲁棒性,也就是一个自定义输出处理 Pipeline 的实际流程。之后会尝试自定义损失函数,支持简单的对抗训练,如 MART 算法等 1  。
 
对抗攻击之植入 mmdetection 众所周知,攻击算法基于原始样本制作一种含有梯度的扰动信息,将扰动信息叠加至原始样本,就得到了对抗样本。因此,对抗样本应该添加在数据处理的 pipeline 中,而不是网络层。首先实现最简单的对抗训练:
\begin{equation} \min_\theta \biggl( \max \Bigl( l \bigl(T_\theta(x’), y_i \bigl) \Bigr) \biggr) \end{equation}
内部的对抗样本 $x’$ 企图最大化模型的分类误差,外部的训练企图最小化对抗样本带来的误差。查看训练数据的 pipeline,我们发现是在最后一步转为 tensor,而攻击算法是在tensor上进行操作的。所以,对抗攻击应该加在最后一步之后或之前。我们查看返回 tensor 的源代码。此部分代码在 mmdet/datasets/pipelines 目录下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def  __call__ (self, results ):    """Call function to transform and format common fields in results.      Args:         results (dict): Result dict contains the data to convert.     Returns:         dict: The result dict contains the data that is formatted with \             default bundle.     """     if  'img'  in  results:         img = results['img' ]                  results = self._add_default_meta_keys(results)         if  len (img.shape) < 3 :             img = np.expand_dims(img, -1 )         img = np.ascontiguousarray(img.transpose(2 , 0 , 1 ))         results['img' ] = DC(to_tensor(img), stack=True )     for  key in  ['proposals' , 'gt_bboxes' , 'gt_bboxes_ignore' , 'gt_labels' ]:         if  key not  in  results:             continue          results[key] = DC(to_tensor(results[key]))     if  'gt_masks'  in  results:         results['gt_masks' ] = DC(results['gt_masks' ], cpu_only=True )     if  'gt_semantic_seg'  in  results:         results['gt_semantic_seg' ] = DC(             to_tensor(results['gt_semantic_seg' ][None , ...]), stack=True )     return  results 
 
而对抗攻击只关注图像与标签,所以,只关注图像与标签,而这部分一目了然。所以,开始写自己的对抗样本:
黑盒攻击,基于 resnet18 制作攻击样本,插入到 pipeline 中 
我暂时的想法是只攻击目标区域,所以筛选出目标区域的位置与标签,按照梯度上升的方向制作对抗样本。 
 
自定义 backbone 注意这步不做也行 。可以直接模改原有的 backbone,只是不推荐。而为了方便调试,我们使用自己定义的网络,网络只打印输入观察数据是否修改成功。因此,自定义的网络如下,并放在 mmdet/models/backbones/mybackbone下,
1 2 3 4 5 6 7 8 9 10 import  torch.nn as  nnfrom  ..builder import  BACKBONES@BACKBONES.register_module() class  mynet (nn.Module):    def  __init__ (self ):         pass      def  forward (self, x ):         print (x) 
 
而后为了导入模块,在 mmdet/models/backbones/__init__.py 中添加 from .mybackbone import mynet。为了适配 faster rcnn 的其它参数,我这里的 backbone 直接使用了 ResNet 里面的内容,只是打印了 x,这里是为了方便观察结果。这里需要注意,那些 super 初始化也要改,防止重名,因为不具备普适性,所以这里就不演示了。而后配置文件中指定以下就可以自由使用,我的配置文件是 adv_faster_rcnn.py:
1 2 3 4 5 6 7 8 9 model = dict (     ...     backbone=dict (         type ='mynet' ,         arg1=xxx,         arg2=xxx),     ... runner = dict (type ='EpochBasedRunner' , max_epochs=1 ) 
 
而后执行一下,成功的看到输入被打印了出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from  mmdet.apis import  init_detector, inference_detectorconfig_file = 'mmdetection/configs/faster_rcnn/adv_faster_rcnn.py'  checkpoint_file = 'mmdetection/checkpoints/faster_rcnn_r50_fpn_1x_coco_20200130-047c8118.pth'  model = init_detector(config=config_file,                       checkpoint=checkpoint_file,                       device='cuda:0' ) img = 'a.png'  result = inference_detector(model=model, imgs=img) 
 
对抗样本代码思想 首先在 pipeline 文件夹下创建自己的 my_pipeline,而后定义自己的处理流程,比如我就看看处理的数据都有哪些:
1 2 3 4 5 6 7 8 9 10 from  mmdet.datasets import  PIPELINES@PIPELINES.register_module() class  MyTransform :    def  __call__ (self, results ):         print (type (results))         for  key in  results:             print (key, type (results[key]))         return  results 
 
最后在 __init__.py 中导入自己的东西,from .my_pipeline import MyTransform。综上发现,在 DefaultFormatBundle 和 Collect 处理后,数据类型不是我能驾驭的常见数据类型: mmcv.parallel.data_container.DataContainer。
所以选择在 Normalize 后加入对抗攻击,因为常见的攻击算法也都是在标准化之后,此外那里还是 ndarray 等常见类型,我能把握住这些基本的类似那个。Normalize 后的数据类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 img_prefix      <class  'str' > seg_prefix      <class  'NoneType' > proposal_file   <class  'NoneType' > bbox_fields     <class  'list' > mask_fields     <class  'list' > seg_fields      <class  'list' > filename        <class  'str' > ori_filename    <class  'str' > img             <class  'numpy.ndarray' > img_shape       <class  'tuple' > ori_shape       <class  'tuple' > img_fields      <class  'list' > gt_bboxes       <class  'numpy.ndarray' > gt_bboxes_ignore        <class  'numpy.ndarray' > gt_labels       <class  'numpy.ndarray' > flip            <class  'bool' > flip_direction  <class  'NoneType' > img_norm_cfg    <class  'dict' > 
 
我利用的信息只有 img,gt_labels 和 gt_bboxes。其实我也不知道以上字段是啥,源码和文档的信息很少,所以我只能自己都打印了一下。所以此时的任务就是,按照 gt_bboxes,截取 img,根据 gt_labels,制作对抗样本。但对抗样本返回的是 tensor,所以最后要转回到 numpy,在覆盖原来的数据。而 PGD、CW 等攻击算法过程会很慢,所以选用攻击强度大且快捷的 FGSM 单步算法。
而为了防止对抗样本带来的过大负担,所以添加了两个额外参数 $p_1$ 和 $p_2$。
如果概率小于 $p_1$,在目标区域叠加高斯噪音 
如果概率在 $p_1$ 到 $p_2$ 之间,在目标区域制作对抗样本 
如果概率大于 $p_2$,使用原图,不做任何处理 
 
综上,此时的配置文件应该为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 adv_para = dict (mu=0 , std=0.1 , epsilon=0.1 , pro1=0.3 , pro2=0.6 , adv='fgsm' ) train_pipeline = [     dict (type ='LoadImageFromFile' ),     dict (type ='LoadAnnotations' , with_bbox=True ),     dict (type ='Resize' , img_scale=(1333 , 800 ), keep_ratio=True ),     dict (type ='RandomFlip' , flip_ratio=0.0001 ),     dict (type ='Normalize' , **img_norm_cfg_trian),          dict (type ='advTransform' , **adv_para),     dict (type ='Pad' , size_divisor=32 ),     dict (type ='DefaultFormatBundle' ),     dict (type ='Collect' , keys=['img' , 'gt_bboxes' , 'gt_labels' ]), ] 
 
开始制作 为了使代码易于维护和扩展,我尽力使额外添加的程序符合设计模式,对扩展开放,对修改封闭,针对接口编程。
在 mmdet/datasets/pipelines/ 目录下增加 adv_example pipeline,生成对抗样本。 
至于攻击算法,依据设计模式,应使用额外的类来实现。位于 datasets/ 目录下,命名为 attack 文件夹。 
 
对抗样本 pipeline 这里有几个点需要注意下:
通过获得的 results,图像的维度是:高、宽、通道 
bboxes 中的信息是 x1,x2,y1,y2  
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 from  mmdet.datasets import  PIPELINESfrom  mmdet.datasets import  attackimport  randomimport  numpy as  np@PIPELINES.register_module() class  advTransform :    def  __init__ (self,                   mu=0 ,                  std=0.1 ,                  epsilon=0.1 ,                  pro1=0.3 ,                  pro2=0.6 ,                  adv='fgsm'  ):                 if  isinstance (mu, int ) or  isinstance (mu, float ):             self.mu = mu         else :             self.mu = 0          if  isinstance (mu, int ) or  isinstance (mu, float ):             self.std = std         else :             self.std = 0.1                   if  isinstance (mu, int ) or  isinstance (mu, float ):             self.epsilon = epsilon         else :             self.epsilon = 0.5                   if  isinstance (pro1, float ):             self.pro1 = pro1         else :             self.pro1 = 0.3          if  isinstance (pro2, float ):             self.pro2 = pro2         else :             self.pro2 = 0.6                   if  isinstance (adv, str ):             self.adv = adv         else :             self.adv = 'fgsm'                                              assert  0  < pro1 < pro2 < 1          self.pro1 = pro1         self.pro2 = pro2         self.rand_ = random.random()          def  __call__ (self, results ):                                    if  self.rand_ > self.pro2:             return  results                  elif  self.rand_ < self.pro1:                          bboxes = results['gt_bboxes' ]             img = results['img' ]             for  box in  bboxes:                 box = box.tolist()                 box = [int (i) for  i in  box]                 x1, y1, x2, y2 = box[0 ], box[1 ], box[2 ], box[3 ]                 noise = np.random.normal(                     self.mu, self.std, size=(y2 - y1, x2 - x1, 3 ))                 img[y1:y2, x1:x2] += noise             results['img' ] = img             return  results                  else :                                       labels = results['gt_labels' ]             bboxes = results['gt_bboxes' ]             img = results['img' ]                          img = attack.fgsm.fgsm_attack(img, bboxes, labels, self.epsilon)             results['img' ] = img             return  results 
 
攻击算法 攻击算法位于 datasets/attack/ 文件夹下。此外,为了导入包,需要添加 __init__.py,并按以下格式添加内容:
1 2 3 from  .fgsm import  fgsm_attack__all__ = ['fgsm_attack' ] 
 
同样这里也有一些需要注意的地方:
针对接口编程,只需要给攻击算法提供图像、目标区域、标签和扰动值,至于内部自己如何实现,不重要 
攻击算法返回的就是图像,内部如何实现,不重要,减少两个模块的耦合 
 
fgsm.py 的内容为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 def  fgsm_attack (img, bboxes, labels, epsilon ):         import  torch     import  torch.nn.functional as  F     model = torch.hub.load(         'pytorch/vision:v0.9.0' , 'resnet18' , pretrained=True )     model.eval ()          tmp_img = torch.from_numpy(img).clone().unsqueeze(dim=0 ).transpose(         1 , 3 ).transpose(2 , 3 )          for  box, target in  zip (bboxes, labels):                  box = box.tolist()         box = [int (i) for  i in  box]                  x1, y1, x2, y2 = box[0 ], box[1 ], box[2 ], box[3 ]                  input_ = tmp_img[:, :, y1:y2, x1:x2].clone()         input_.requires_grad = True          label = torch.tensor([int (target)])         output = model(input_)         loss = F.nll_loss(output, label)                  loss.backward()         sign_data = input_.grad.data.sign()                  perturbed_image = input_.detach() + epsilon * sign_data                  tmp_img[:, :, y1:y2, x1:x2] += perturbed_image          tmp_img = tmp_img.squeeze().detach().cpu().numpy().transpose(1 , 2 , 0 )     return  tmp_img 
 
踩坑记录 
也许你看过一些对抗攻击的算法,知道最后的对抗样本应该 torch.clamp 到 $[0,1]$。可那是论文里用的玩具数据集才会做的事情,那些数据集知道均值和方差。现实世界的真实数据,又怎么会知道均值和方差,怎么去标准化到 $[0,1]$ 之间呢? 
我遇到了一个佷头疼的 bug,是多线程导致的反向传播异常,大概错误信息是:terminate called after throwing an instance of ‘c10::Error what():  CUDA error: initialization error。网上翻阅了无数 bug issue,才找到一篇有用的,也不知道那些 github 仓库的作者咋想的,问题还没解决就 close 掉,冲业绩还是图仓库 bug 少?这里把每个 GPU 的线程数量设为 0 就可以了 2   。而至于写多线程下的梯度反向传播,我貌似还没这个本事。但是,我服务器上没发现有这个 bug,应该是版本问题。  
 
程序 官方的程序如何使用,我的就怎么使用,毕竟是直接 fork 过来的,从安装一步步来就行。此外,为了方便使用,我把配置文件放到了 configs/faster_rcnn/faster_rcnn_r50_fpn_1x_coco.py 文件中,可以进去参观下。
https://github.com/muyuuuu/mmdetection 
此外,我在 colab 也创建了一份能用的,在不会用就真没救了。
https://github.com/muyuuuu/open-mmlab-colab/blob/main/Detection/mmdet_adv.ipynb 
reference