文章目录

  • 1、architecture
    • 1.1 特征提取网络
    • 1.2 互相关层
  • 2、training
    • 2.1 图像预处理
    • 2.2 加载训练数据、标签及损失函数
    • 2.3 优化策略
    • 2.4 模型的训练与保存
  • 3、test
    • 3.1 init(初始帧)
    • 3.2 update(后续帧)
      • 正向推导得到response map最大值位置
      • 由response map最大值位置反推目标在原图像的中心位置
    • 3.3 otb、GOT10k结果

  本文对SiamFC代码进行详细分析,并从代码角度阐述理论。具体的算法原理请查看: SiamFC论文,虽然SiamFC是在2016年提出的算法,近几年也在此基础上涌现了大量的优秀算法,但SiamFC作为第一个基于孪生神经网络的跟踪算法属实值得研究,也以此记录下自己的学习路程。复现的 代码基于pytorch,内含注释,方便初学者学习!!
  本文分三部分分析代码: architecture部分主要分析网络的框架结构; training部分主要分析训练的整个流程,包括数据预处理、损失函数、优化策略等; test部分主要分析跟踪流程,给定第一帧图片目标的位置,SiamFC如何在后续帧预测目标位置。

1、architecture

1.1 特征提取网络

  这是从SiamFC论文截取的框架图

  上图中,孪生网络结构中φ\varphiφ 代表的是特征提取网络,∗\ast∗代表互相关运算,上分支对应127x127x3大小的目标模板图像,下分支对应255x255x3大小的搜索图像,上下分支共享网络参数。这里给大家介绍的是带批量规范化(Batch Normation)的Alexnet版本,代码如下:

class AlexNet(nn.Module):output_stride = 8def __init__(self,init_weight=True):super(AlexNet, self).__init__()self.conv1 = nn.Sequential(nn.Conv2d(3, 96, 11, 2),    #[1,3,127,127]->[1,96,59,59]nn.BatchNorm2d(96),nn.ReLU(inplace=True),nn.MaxPool2d(3, 2))         #[1,96,59,59]->[1,96,29,29]self.conv2 = nn.Sequential(nn.Conv2d(96, 256, 5, 1),   #[1,96,29,29]->[1,256,25,25]nn.BatchNorm2d(256),nn.ReLU(inplace=True),nn.MaxPool2d(3, 2))         #[1,256,25,25]->[1,256,12,12]self.conv3 = nn.Sequential(nn.Conv2d(256, 384, 3, 1),  #[1,256,12,12]->[1,384,10,10]nn.BatchNorm2d(384),nn.ReLU(inplace=True))self.conv4 = nn.Sequential(nn.Conv2d(384, 384, 3, 1),  #[1,384,10,10]->[1,384,8,8]nn.BatchNorm2d(384),nn.ReLU(inplace=True))self.conv5 = nn.Sequential(nn.Conv2d(384, 128, 3, 1))   #[1,384,8,8]->[1,128,6,6]#初始化网络参数if init_weight:self._init_weights()def forward(self, x):x = self.conv1(x)x = self.conv2(x)x = self.conv3(x)x = self.conv4(x)x = self.conv5(x)return x#初始化网络参数def _init_weights(self):for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.xavier_uniform_(m.weight, 1) #xavier是参数初始化,它的初始化思想是保持输入和输出方差一致,这样就避免了所有输出值都趋向于0if m.bias is not None:nn.init.constant_(m.bias, 0)     #偏置初始化为0elif isinstance(m, nn.BatchNorm2d):      #在激活函数之前,希望输出值由较好的分布,以便于计算梯度和更新参数,这时用到BatchNorm2d函数nn.init.constant_(m.weight, 1)nn.init.constant_(m.bias, 0)elif isinstance(m, nn.Linear):nn.init.xavier_uniform_(m.weight, 1)if m.bias is not None:nn.init.constant_(m.bias, 0)

如代码所示,首先简单分析几个函数功能:

  1. nn.Conv2d:重点关注前5个参数,in_channels、out_channels、kernel_size、stride和padding(默认为1和0),假设输入feature map大小为fffxfff,则输出feature map的大小是:⌊f−kernelsize+2∗paddingstride+1⌋\lfloor\frac{f-kernel_{size}+2*padding}{stride}+1\quad\rfloor⌊stridef−kernelsize​+2∗padding​+1⌋式中,⌊⌋\lfloor\rfloor⌊⌋表示下取整。
  2. nn.BatchNorm2d:防止梯度爆炸和梯度消失,加快网络学习,输入参数为上一层通道数。
  3. nn.ReLu:对每一层的输出激活处理,增加网络的非线性学习能力。
  4. nn.Maxpool2d:池化下采样,代码中一共有三次下采样(kernel_size=3,stride=2),每次缩小1/2特征图大小。

  代码注释仅以上分支为例,根据上式进行推导举例[1,3,127,127]->[1,96,59,59]->[1,96,29,29]来说,卷积层的in_channels=3,out_channels=96,kernel_size=11,stride=2,padding=0(默认),输入图片大小是127x127,那么输出feature map大小为59=(127-11+2 ⋅\cdot⋅ 0)/2+1;池化层的kernel_size=3,stride=2,(59-3+2 ⋅\cdot⋅ 0)/2+1。依次类推,可以得到最终输入互相关层的feature map大小为[1,128,6,6],同样地,下分支得到的feature map的大小是[1,128,22,22]。
  另外有两点需要注意:因为网络的输入要求是4维,第1维指的是batch_size,比如训练过程中batch_size为(8,16,32等等),代码中假设为1; total_stride=8表示下采样的总步距,代码一共出现3次Maxpool2d,2^3=8,这个参数在后面测试阶段代码分析会详细讲解。
为了验证网络的可行性,验证代码如下:

if __name__ == '__main__':model = AlexNet()z = torch.randn([1, 3, 127, 127])feature_map_z = model(z)

feature_map_z得shape如下:

1.2 互相关层

  原始图像对通过特征提取网络后,得到相应大小的feature map进行互相关运算,即把目标模板的feature map[1,128,6,6]作为卷积核,搜索图像的feature map[1,128,22,22]作为输入,卷积核在搜索图像的feature map上密集地采集子窗口进行相似性的判断,6x6大小的卷积核在22x22特征图上滑动得17x17大小的response map,得分最高的子窗口(对应17x17特征图上的像素点)即为预测目标所在位置。代码如下:

class _corr(nn.Module):def __init__(self):super(_corr, self).__init__()#互相关运算,设batch_size=8def forward(self, z, x):kernel = z #[8,128,6,6]group = z.size(0)  #8input = x.view(-1, group*x.size(1), x.size(2), x.size(3))#输出为[8,1,17,17], 那么反推input[8,128,22,22],kernel[1,1024,6,6] group=128/1024?错误#所以先输出[1,8,17,17],再view变换维度成[8,1,17,17],那么input[1,1024,22,22],kernel[8,128,6,6],group=1024/128=8=batch_sizeresponse_maps = F.conv2d(input, kernel,groups=group)response_maps = response_maps.view(x.size(0),-1,response_maps.size(2), response_maps.size(3))return response_maps

下面是对代码中函数F.conv2dview的讲解

  1. F.conv2d:输入input(batch_size,in_channels,h,w),卷积核kernel(out_channels,in_channels/groups,kh,kw);输入batch_size对图片,要得到batch_size个相同大小的response map,即大小为(batch_size,out_channels=1,17,17)的response map。
  2. view:转换tensor张量的大小,-1表示自动填充,保证tensor元素个数不变;另外,view与reshape不同,view函数只能用于内存中连续存储的tensor,而reshape则没有这个限制条件。

同样,我们对代码进行验证,z的第一维取8或1结果都一样,8代表8个卷积核分别与对应的某个特征图卷积操作,1代表用1个卷积核对每个特征图卷积操作,这两种情况分别对应训练和测试

if __name__ == '__main__':z = torch.randn([8, 128, 6, 6])# z = torch.randn([1,128,6,6])x = torch.randn([8, 128, 22, 22])f = _corr()response_maps = f(z, x)

response_maps的shape如下:

  至此,完成了对网络框架architecture的代码分析!!!接下来是训练和测试代码的分析。

2、training

  根据我在阅读作者源代码后的理解结合自己的思路,给出训练的框图如下。训练过程中,首先要获取训练数据集的所有视频序列(每个视频序列的所有帧),我采用的是GOT-10k数据集训练;获取数据集之后进行图像预处理,对每一个视频序列抽取两帧图像并作数据增强处理(包括裁剪、resize等过程),分别作为目标模板图像和搜索图像;把经过图像处理的所有图像对加载并以batch_size输入网络得到预测输出;建立标签和损失函数,损失函数的输入是预测输出,目标是标签;设置优化策略,梯度下降损失,最终得到网络模型。

#mermaid-svg-D7AkAnP1utIaj1N3 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-D7AkAnP1utIaj1N3 .error-icon{fill:#552222;}#mermaid-svg-D7AkAnP1utIaj1N3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D7AkAnP1utIaj1N3 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-D7AkAnP1utIaj1N3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D7AkAnP1utIaj1N3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D7AkAnP1utIaj1N3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D7AkAnP1utIaj1N3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D7AkAnP1utIaj1N3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D7AkAnP1utIaj1N3 .marker.cross{stroke:#333333;}#mermaid-svg-D7AkAnP1utIaj1N3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D7AkAnP1utIaj1N3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-D7AkAnP1utIaj1N3 .cluster-label text{fill:#333;}#mermaid-svg-D7AkAnP1utIaj1N3 .cluster-label span{color:#333;}#mermaid-svg-D7AkAnP1utIaj1N3 .label text,#mermaid-svg-D7AkAnP1utIaj1N3 span{fill:#333;color:#333;}#mermaid-svg-D7AkAnP1utIaj1N3 .node rect,#mermaid-svg-D7AkAnP1utIaj1N3 .node circle,#mermaid-svg-D7AkAnP1utIaj1N3 .node ellipse,#mermaid-svg-D7AkAnP1utIaj1N3 .node polygon,#mermaid-svg-D7AkAnP1utIaj1N3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D7AkAnP1utIaj1N3 .node .label{text-align:center;}#mermaid-svg-D7AkAnP1utIaj1N3 .node.clickable{cursor:pointer;}#mermaid-svg-D7AkAnP1utIaj1N3 .arrowheadPath{fill:#333333;}#mermaid-svg-D7AkAnP1utIaj1N3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-D7AkAnP1utIaj1N3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-D7AkAnP1utIaj1N3 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-D7AkAnP1utIaj1N3 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-D7AkAnP1utIaj1N3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-D7AkAnP1utIaj1N3 .cluster text{fill:#333;}#mermaid-svg-D7AkAnP1utIaj1N3 .cluster span{color:#333;}#mermaid-svg-D7AkAnP1utIaj1N3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-D7AkAnP1utIaj1N3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

数据集获取及图像预处理
加载训练数据
标签
损失
优化器

2.1 图像预处理

  先贴代码,再分析:

def train(data_dir, net_path=None,save_dir='pretrained'):#从文件中读取图像数据集seq_dataset = GOT10k(data_dir,subset='train',return_meta=False)#定义图像预处理方法transforms = SiamFCTransforms(  exemplar_sz=cfg.exemplar_sz, #127instance_sz=cfg.instance_sz, #255context=cfg.context) #0.5#从读取的数据集每个视频序列配对训练图像并进行预处理,裁剪等train_dataset = GOT10kDataset(seq_dataset,transforms)

data_dir是存放GOT-10k数据集的文件路径,GOT-10k一共有9335个训练视频序列,seq_dataset返回的是所有视频序列的图片路径列表seq_dirs及对应groundtruth列表anno_files及一些其他信息,如下:

  接下来是定义好图像预处理方法,在GOT10kDataset方法中对每个视频序列配对两帧图像,并使用定义好的图像处理方法,接下来直接进入该方法分析代码,GOT10kDataset的代码结构如下图:

class GOT10kDataset(Dataset): #继承了torch.utils.data的Dataset类def __init__(self, seqs, transforms=None,pairs_per_seq=1):def __getitem__(self, index): #通过_sample_pair方法得到索引返回item=(z,x,box_z,box_x),然后经过transforms处理def __len__(self): #返回9335*pairs_per_seq对def _sample_pair(self, indices): #随机挑选两个索引,这里取的间隔不超过T=100def _filter(self, img0, anno, vis_ratios=None): #通过该函数筛选符合条件的有效索引val_indices

这里最重要的方法就是__getitem__,该方法最终返回处理后的图像,在内部首先调用了_sample_pair方法,用于提取两帧有效图片(有效的定义是图片目标的面积和高宽等有约束条件)的索引,在得到这两帧图片和对应groundtruth之后通过定义好的transforms进行处理,transforms是SiamFCTransforms类的实例化对象,该类中主要继承了resize图片大小和各种裁剪方式等,如代码所示:

class SiamFCTransforms(object):def __init__(self, exemplar_sz=127, instance_sz=255, context=0.5):self.exemplar_sz = exemplar_szself.instance_sz = instance_szself.context = context#transforms_z/x是数据增强方法self.transforms_z = Compose([RandomStretch(),     #随机resize图片大小,变化再[1 1.05]之内CenterCrop(instance_sz - 8),  #中心裁剪 裁剪为255-8RandomCrop(instance_sz - 2 * 8),   #随机裁剪  255-8->255-8-8CenterCrop(exemplar_sz),   #中心裁剪 255-8-8->127ToTensor()])                        #图片的数据格式从numpy转换成torch张量形式self.transforms_x = Compose([RandomStretch(),                   #s随机resize图片CenterCrop(instance_sz - 8),      #中心裁剪 裁剪为255-8RandomCrop(instance_sz - 2 * 8),  #随机裁剪 255-8->255-8-8ToTensor()])                      #图片数据格式转化为torch张量def __call__(self, z, x, box_z, box_x): #z,x表示传进来的图像z = self._crop(z, box_z, self.instance_sz)       #对z(x类似)图像 1、box转换(l,t,w,h)->(y,x,h,w),并且数据格式转为float32,得到center[y,x],和target_sz[h,w]x = self._crop(x, box_x, self.instance_sz)       #2、得到size=((h+(h+w)/2)*(w+(h+2)/2))^0.5*255(instance_sz)/127z = self.transforms_z(z)                         #3、进入crop_and_resize:传入z作为图片img,center,size,outsize=255(instance_sz),随机选方式填充,均值填充x = self.transforms_x(x)                         #   以center为中心裁剪一块边长为size大小的正方形框(注意裁剪时的padd边框填充问题),再resize成out_size=255(instance_sz)return z, x

实例化对象后,直接从__call__开始运行代码,首先关注的应该是_crop函数,该函数将原始的两帧图片分别以目标为中心,裁剪一块包含上下文信息的patch,patch的边长定义如下:patchsize=0.5∗(h+(w+h)/2)⋅(w+(w+h)/2)∗255/127patch_{size}=0.5*\sqrt{(h+(w+h)/2)\cdot(w+(w+h)/2)}*255/127patchsize​=0.5∗(h+(w+h)/2)⋅(w+(w+h)/2)​∗255/127式中,w、h分别表示目标的宽和高。在裁剪过程中会出现越界的情况,需要对原始图像边缘填充,填充值固定为图像的RGB均值,填充大小根据图像边缘越界最大值作为填充值,具体实现过程由以下代码完成。

# padding操作#corners表示目标的[ymin,xmin,ymax,xmax]pads = np.concatenate((-corners[:2], corners[2:] - img.shape[:2]))npad = max(0, int(pads.max())) #得到上下左右4个越界值中最大的与0对比,<0代表无越界if npad > 0:img = cv2.copyMakeBorder(img, npad, npad, npad, npad,cv2.BORDER_CONSTANT, value=img_average)

裁剪完成后resize成255x255x3大小的图片,然后就是一系列的裁剪过程作为数据增强,将其中一帧图像裁剪成127x127x3大小作为目标模板图像,然而,对搜索图像进行裁剪最终得到的大小为239,并不是255,所以在训练过程中,得到的搜索图像feature map大小是[128,20,20],response map大小[15,15],但这并不会造成训练的模型对测试产生影响。
  同样,用代码验证了图像预处理的效果,图片展示了目标模板图像z的获取流程。

if __name__ == "__main__":root_dir = 'D:/Dataset/GOT-10k'seq_dataset = GOT10k(root_dir, subset='train')transforms = SiamFCTransforms(exemplar_sz=cfg.exemplar_sz,  # 127instance_sz=cfg.instance_sz,  # 255context=cfg.context)  # 0.5train_dataset = GOT10kDataset(seq_dataset, transforms)train_dataset.__getitem__(1) #返回随机的某个视频序列两帧处理后的图片

2.2 加载训练数据、标签及损失函数

  图像预处理完成后,得到了用与训练的9335对图像,将图像加载批量加载输入网络得到输出结果作为损失函数的input,损失函数的target是制定好的labels。

#加载训练数据集loader_dataset = DataLoader( dataset = train_dataset,batch_size=cfg.batch_size,shuffle=True,num_workers=cfg.num_workers,pin_memory=True,drop_last=True, )#初始化训练网络cuda = torch.cuda.is_available()  #支持GPU为Truedevice = torch.device('cuda:0' if cuda else 'cpu')  #cuda设备号为0model = AlexNet(init_weight=True)corr = _corr()model = model.to(device)corr = corr.to(device)# 设置损失函数和标签logist_loss = BalancedLoss()labels = _create_labels(size=[cfg.batch_size, 1, cfg.response_sz - 2, cfg.response_sz - 2])labels = torch.from_numpy(labels).to(device).float()

首先根据自己设备情况设定好batch_size和num_workers情况,windows下num_workers只能设置为0,然后加载网络并初始化网络参数。本小节主要讲网络输出的labels和损失函数,接下来只是我个人的一些理解,代码与论文理论部分形式不一致,但效果一样。先上图,论文中labels以及损失函数如下图:

然而代码中的labels值却是1和0,损失函数使用的是二值交叉熵损失函数F.binary_cross_entropy_with_logits,如下图推导所示,解释了为什么代码实现部分真正使用的labels值是1和0,而理论部分使用的是1和-1。

lables公式中,在response map距离中心距离小于R/k值得设为1,其他设为0,R值对应原图的以中心为原点的半径,k是total_stride,R/k映射为response map上以中心为原点的半径,利用下面代码的这个_creat_labels方法可以得到标签。

def _create_labels(size):def logistic_labels(x, y, r_pos):# x^2+y^2<4 的位置设为为1,其他为0dist = np.sqrt(x ** 2 + y ** 2)labels = np.where(dist <= r_pos,    #r_os=2np.ones_like(x),  #np.ones_like(x),用1填充xnp.zeros_like(x)) #np.zeros_like(x),用0填充xreturn labels#获取标签的参数n, c, h, w = size  # [8,1,15,15]x = np.arange(w) - (w - 1) / 2  #x=[-7 -6 ....0....6 7]y = np.arange(h) - (h - 1) / 2  #y=[-7 -6 ....0....6 7]x, y = np.meshgrid(x, y)       #建立标签r_pos = cfg.r_pos / cfg.total_stride  # 16/8labels = logistic_labels(x, y, r_pos)#重复batch_size个label,因为网络输出是batch_size张response maplabels = labels.reshape((1, 1, h, w))   #[1,1,15,15]labels = np.tile(labels, (n, c, 1, 1))  #将labels扩展[8,1,15,15]return labels

验证结果如下图,只截取了部分labels,得到的labels对应输入,大小都是[8,1,15,15]

if __name__ == '__main__':labels = _create_labels([8,1,15,15])  #返回的label.shape=(8,1,15,15)

2.3 优化策略

  代码选择的优化器是加入一阶动量的SGD,什么是一阶动量?一阶动量的数学概念是指数加权平均和,可以简单理解为在更新网络参数时保留了一部分前一次参数更新的方向作为缓冲,简而言之就是可以通过积累之前的动量来加速当前梯度下降。原始的SGD更新参数可以用下式表示,θ\thetaθ表示网络参数,lrlrlr表示学习率,dθ(Loss)nowd_\theta(Loss)_{now}dθ​(Loss)now​表示损失函数对参数的求导。vnow=lr∗dθ(Loss)nowv_{now}=lr*d_\theta(Loss)_{now}vnow​=lr∗dθ​(Loss)now​θ=θ−vnow\theta=\theta-v_{now}θ=θ−vnow​使用一阶动量的SGD,仅仅是对vvv进行优化,vnow=momentum∗vlast+(1−momentum)∗lr∗dθ(Loss)nowv_{now}=momentum*v_{last}+(1-momentum)*lr*d_\theta(Loss)_{now}vnow​=momentum∗vlast​+(1−momentum)∗lr∗dθ​(Loss)now​。另外,二阶动量是在学习率lrlrlr上进行内部优化,代码没有使用二阶动量,但是采用了类似二阶动量的方式人工对lrlrlr进行优化,学习率lrlrlr随着训练次数epoch增多而减小,具体值如下公式
lr=initiallr∗gammaepochlr=initial_{lr}*gamma^{epoch}lr=initiallr​∗gammaepoch式中,initiallrinitial_{lr}initiallr​是初始学习率,gammagammagamma是定义的超参,epochepochepoch为训练次数。整个优化器及学习率调整实现代码如下:

#建立优化器,设置指数变化的学习率optimizer = optim.SGD(model.parameters(),lr=cfg.initial_lr,              #初始化的学习率,后续会不断更新weight_decay=cfg.weight_decay,  #λ=5e-4,正则化momentum=cfg.momentum)          #v(now)=dx∗lr+v(last)∗momemtumgamma = np.power(                   #np.power(a,b) 返回a^bcfg.ultimate_lr / cfg.initial_lr,1.0 / cfg.epoch_num)lr_scheduler = ExponentialLR(optimizer, gamma)  #指数形式衰减,lr=initial_lr*(gamma^epoch)

2.4 模型的训练与保存

  一切准备工作就绪后,就开始训练了。代码中设定epoch_num为50次,训练时密切加上model.train(),告诉网络处于训练状态,这样,网络运行时就会利用pytorch的自动求导机制求导;在测试时,改为model.eval(),关闭自动求导。模型训练的步骤如代码所示:

    for epoch in range(start_epoch, cfg.epoch_num+1):model.train()#遍历训练集for it, batch in enumerate(tqdm(loader_dataset)):z = batch[0].to(device,non_blocking=cuda)   # z.shape=([8,3,127,127])x = batch[1].to(device, non_blocking=cuda)  # x.shape=([8,3,239,239])#输入网络后通过损失函数z, x = model(z), model(x)responses = corr(z, x) * cfg.out_reduce  # 返回的是heatmap的响应表15x15  因为x是239x239 [8,1,15,15]loss = logist_loss(responses, labels)#反向传播optimizer.zero_grad() #梯度清0loss.backward()       #反向传播求导optimizer.step()   #更新参数

代码中cfg.out_reduce取0.001,将网络输出缩小1000倍,再与labels值0、1通过损失函数进行梯度下降,倘若缺少这一步,输出将非常大,也就意味这损失函数的input与target相差巨大,这将发生梯度爆炸。下图是乘0.001前后的Loss值情况:
优化过程的重点是反向传播三步,顺序不能乱,首先对之前的梯度清0,再损失反向传播,最后更新网络参数。如果不对梯度清0,刚开始时,累加的梯度可以加速网络训练,但随着训练次数的增长梯度不断累加会造成梯度爆炸。
  最后,要保存训练好的模型进行测试,在保存中,代码不仅仅保存model的参数,并且保留了epoch和optimizer的参数,方便训练中断时,可以使用上次训练好的模型接着训练,把此刻的epoch和optimizer的学习率等参数一同保存,保存路径为save_path。

torch.save({'epoch':epoch,'model':model.state_dict(),'optimizer':optimizer.state_dict()},save_path)

同时,附上模型再次训练的代码:

    if net_path is not None:checkpoint = torch.load(net_path)if 'epoch' in checkpoint:start_epoch = checkpoint['epoch'] + 1model.load_state_dict(checkpoint['model'])optimizer.load_state_dict(checkpoint['optimizer'])

只需要在调用train()函数时传入模型的net_path参数就可以接着训练。
  training部分的代码分析也告一段落!

3、test

  测试时,可以使用多种数据集,GOT-10k-toolkit提供了多种数据集接口,测试的方法都千篇一律,只要自己提供init和update方法便可在数据集上进行测试与性能评估,使用过程中,需要好好理解接口源码,包括测试方式和性能评估方式。在后面,我会给出代码在otb100、otb2013和GOT10k测试集上的性能结果。

3.1 init(初始帧)

  这一步目的是要得到6x6x128的目标模板feature map,在后续跟踪过程中,一直使用它作为卷积核不变,这也是一大SiamFC的缺点所在;init内还要初始化目标的中心所在位置、宽高、汉宁窗等后续跟踪使用。

#传入第一帧的gt和图片,初始化一些参数,计算一些之后搜索区域的中心等等def init(self, img, box):#设置成评估模式,测试模型时一开始要加这个,属于Pytorch,训练模型前,要加self.net.train()self.model.eval()#将原始的目标位置表示[l,t,w,h]->[center_y,center_x,h,w]yxhw = ltwh_to_yxhw(ltwh=box)self.center, self.target_sz = yxhw[:2], yxhw[2:]#创建汉宁窗口update使用self.response_upsz = cfg.response_up * cfg.response_sz  # 16*17=272self.hann_window = creat_hanning_window(size=self.response_upsz)#三种尺度1.0375**(-1,0,1) 三种尺度self.scale_factors = three_scales()# patch边长,两种边长:目标模板图像z_sz和搜索图像x_zscontext = cfg.context * np.sum(self.target_sz)  # 上下文信息(h+w)/2self.z_sz = np.sqrt(np.prod(self.target_sz + context))  # (h+(h+w)/2)*(w+(h+2)/2))^0.5self.x_sz = self.z_sz * cfg.instance_sz / cfg.exemplar_sz  # (h+(h+w)/2)*(w+(h+2)/2))^0.5*255/127#图像的RGB均值,返回(aveR,aveG,aveB)self.avg_color = np.mean(img, axis=(0, 1))#裁剪一块以目标为中心,边长为z_sz大小的patch,然后将其resize成exemplar_sz的大小z = z_to127(img, self.center, self.z_sz, cfg.exemplar_sz, self.avg_color)z = torch.from_numpy(z).to(    #torch.size=([1,3,127,127])self.device).permute(2, 0, 1).unsqueeze(0).float()self.kernel = self.model(z)    #torch.size=([1,128,6,6])

follow代码,先设置model.eval(),原因在training过程中讲了,初始化目标的位置信息self.center和self.target_sz分别代表目标的中心位置和高宽;接着是汉宁窗口和尺度因子,汉宁窗口如下图展示的是17x17大小的,它与response map相乘会突出response map的中心并抑制边缘。窗口大小是17*16=272,而不是response map的大小17,这样做的好处是找到更加精确的位置,相比于在17x17大小的特征图上寻找最大值,272x272大小的特征图分辨率更高,位置更精确,所以在后续跟踪过程中,得到的17x17大小的response map会resize成272x272大小;代码使用的是SiamFC_3s,所以定义了三个不同的尺度;包含上下文信息的patch边长与training不同,训练时是将两张图片以相同的patch裁剪再resize成255x255x3大小,之后在一系列数据增强的过程中将其中一张图片裁剪成127x127x3作为目标模板图像。而test过程中,目标模板图像和搜索图像的patch边长不同,如代码所示,成比例255/127关系;最后就是对init传入的初始帧图片裁剪并输入网络得到跟踪过程使用的卷积核,大小为[1,128,6,6]。

3.2 update(后续帧)

  Update就是tracking过程,传入的参数是后续帧图片,一共可以分为三个阶段:1、经过网络的正向推导得到response map;2、根据response map的最大值反推目标在原始图片中的位置;3、参数更新。本文只讲前两个阶段,参数更新就是在得到新的目标后,更新目标的位置信息和高宽,高宽的更新依据尺度。

 #传入后续帧,然后根据SiamFC跟踪流程返回目标的box坐标def update(self, img):self.model.eval()"""----------------正向推导得到response map最大值位置---------------------"""#三种patch边长 patch*3scalesx = x_to3s255(img,self.center,self.x_sz,self.scale_factors,cfg.instance_sz,self.avg_color)#numpy转为float的torch型张量x = torch.from_numpy(x).to(self.device).permute(0, 3, 1, 2).float()#[3,255,22,22]x = self.model(x)#得到三种尺度下的response mapresponses = self.corr(self.kernel, x) * cfg.out_reduce #[3,1,17,17]responses = responses.squeeze(1).cpu().numpy()   #压缩为[3,17,17]并转为numpy作后续计算处理#将17x17大小的response map->[3,272,272]responses = map_to272(responses,out_size=self.response_upsz)#对尺度变化做出相应惩罚responses[:cfg.scale_num // 2] *= cfg.scale_penalty      #response[0]*(0.9745惩罚项)responses[cfg.scale_num // 2 + 1:] *= cfg.scale_penalty  #response[2]*(0.9745惩罚项)#找到最大值属于哪个response map,并把该response map赋给responsescale_id = np.argmax(np.amax(responses, axis=(1, 2))) #里面求得三个map的最大值 再对三个值求最大值 得到索引response = responses[scale_id]   #[272,272]#一系列数据处理,重点在汉宁窗惩罚response = map_process(response,self.hann_window)loc = np.unravel_index(response.argmax(),response.shape)   #unravel_index该函数可返回索引response.argmax()的元素的坐标,逐行拉伸,返回第几行第几个"""---------------由response map最大值位置反推目标在原图像的位置------------"""disp_in_response = np.array(loc) - (self.response_upsz - 1) / 2  #峰值点相对于response中心的位移disp = disp_in_response / 16disp = disp * 8disp = disp * self.x_sz * self.scale_factors[scale_id] / cfg.instance_szself.center += disp"""---------------参数更新------------"""

正向推导得到response map最大值位置

  代码中可知,三种尺度的具体含义是三种不同patch。在对目标的跟踪过程中,目标的形状大小是会发生变化的,如果不更新目标的大小(h、w),跟踪器的性能将很糟糕,又由于理论上,相邻视频帧的目标形状变化极小,所以使用的不同尺度也都接近于1.0375^(-1,0,1)。通过三种patch得到三张255x255x3大小的图片输入model,再与init的卷积核互相关,得到3张response map,每张map代表一个尺度,并且对第1、3张尺度变化的response map惩罚。惩罚可以理解为置信度因子,首先明确要加入多种不同尺度,假设没有惩罚项,目标存在形状变化效果还好,但当目标形状不发生变化时,会造成极大的干扰;如果加了惩罚项,当目标形状不发生变化时,会对尺度变化的response map乘小于1的数,减小它的值,使得response map的最大值存在尺度为1的response map上,当目标形状变化时,由于我们乘的数较小,如果目标形状确实变化,还是会在尺度变化的response map上得到最大值。简而言之,加入惩罚项后,目标变化时,我们更加相信目标变化了,而排出目标轻微的抖动等(不属于形状变化)。
  选出了response map之后,接下来要得到该response map中最大值的位置。这里,就需要对这张response map加窗处理,代码使用的是汉宁窗,余弦窗的效果几乎一样。为什么要加窗呢?也可以理解为一种置信度,置信的对象是目标发生了大位移,因为在相邻帧中,目标的位置变化较小,所以加窗可以有效抑制跟踪器因为干扰而产生漂移,使跟踪器更加稳定。下图是对response map加窗前后的对比图,可以看出,对response map加窗处理后,能够更加突出目标位置,虽然效果不是很明显。

  在完成上面两步之后,就得到了最大值的索引loc

由response map最大值位置反推目标在原图像的中心位置

  接下来是重点!!下分支的推导过程:

#mermaid-svg-YkkAu4kpjBkJyrb8 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-YkkAu4kpjBkJyrb8 .error-icon{fill:#552222;}#mermaid-svg-YkkAu4kpjBkJyrb8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YkkAu4kpjBkJyrb8 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-YkkAu4kpjBkJyrb8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YkkAu4kpjBkJyrb8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YkkAu4kpjBkJyrb8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YkkAu4kpjBkJyrb8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YkkAu4kpjBkJyrb8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YkkAu4kpjBkJyrb8 .marker.cross{stroke:#333333;}#mermaid-svg-YkkAu4kpjBkJyrb8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YkkAu4kpjBkJyrb8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YkkAu4kpjBkJyrb8 .cluster-label text{fill:#333;}#mermaid-svg-YkkAu4kpjBkJyrb8 .cluster-label span{color:#333;}#mermaid-svg-YkkAu4kpjBkJyrb8 .label text,#mermaid-svg-YkkAu4kpjBkJyrb8 span{fill:#333;color:#333;}#mermaid-svg-YkkAu4kpjBkJyrb8 .node rect,#mermaid-svg-YkkAu4kpjBkJyrb8 .node circle,#mermaid-svg-YkkAu4kpjBkJyrb8 .node ellipse,#mermaid-svg-YkkAu4kpjBkJyrb8 .node polygon,#mermaid-svg-YkkAu4kpjBkJyrb8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YkkAu4kpjBkJyrb8 .node .label{text-align:center;}#mermaid-svg-YkkAu4kpjBkJyrb8 .node.clickable{cursor:pointer;}#mermaid-svg-YkkAu4kpjBkJyrb8 .arrowheadPath{fill:#333333;}#mermaid-svg-YkkAu4kpjBkJyrb8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YkkAu4kpjBkJyrb8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YkkAu4kpjBkJyrb8 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-YkkAu4kpjBkJyrb8 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-YkkAu4kpjBkJyrb8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YkkAu4kpjBkJyrb8 .cluster text{fill:#333;}#mermaid-svg-YkkAu4kpjBkJyrb8 .cluster span{color:#333;}#mermaid-svg-YkkAu4kpjBkJyrb8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YkkAu4kpjBkJyrb8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

patch裁剪和resize
stride=8,padding=0
互相关
resize
原始image
255x255x3搜索图像
特征图128x22x22
response map17x17
response map272x272

首先我们由上一步loc计算得到其相对于272x272response map中心点的位移disp_in_response,接下来一步步反推相对(17x17response map、特征图、搜索图像、原始image)中心点的位移。

  1. response map17x17:disp=disp_in_response/16,resize直接/就行。
  2. 特征图:disp=disp,位移相同,互相关(卷积)过程中没有加入padding所以22->17。
  3. 搜索图像:disp=disp*8,表面上,255/22不等于8,但这与resize不同,网络一系列卷积的总步距total_stride为8,只不过没有padding操作,与上相同。论文也提到过,由于网络没有padding操作,使得网络不满足全卷积性质,所以才会有255->22而不是255->255/8,不过no padding也是SiamFC的一大优势,只有no padding网络才满足空间平移不变性,padding操作会导致训练的时候出现位置偏差。
  4. 原始image:disp=disp*pach_size/255,首先在原始image上裁剪patch并不会引起disp位移发生变化,因为裁剪仅仅是从原始image以中心取一块,图像的分辨率并没有变化。而resize过程是将边长为patch_size的图像resize成255x255x3的搜索图像,倍率变化为255/patch_size,所以disp等于disp乘上patch_size/255。

经过上述过程就得到了目标在原始image中相对于中心的位移,这里的中心指得是上一帧的目标中心,是已知量,也是每次update需要更新的。
  至此,SiamFC代码分析over!

3.3 otb、GOT10k结果

  模型上传在github中,使用GOT10k测试集时,需要将跑出的results提交给here才能查看结果,下面还给出看官方的测试结果,otb结果比源代码提供者的更高。



SiamFC代码分析(architecture、training、test)相关推荐

  1. SiamFC代码配置复现 matlab版本

    原创 SiamFC代码配置复现 2019-04-29 22:18:06 ZZXin_ 阅读数 1603更多 分类专栏: 深度学习 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议 ...

  2. NLP-生成模型-2017-Transformer(二):Transformer各模块代码分析

    一.WordEmbedding层模块(文本嵌入层) Embedding Layer(文本嵌入层)的作用:无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示, 由一维转为多维 ...

  3. Device Tree(三):代码分析

    2019独角兽企业重金招聘Python工程师标准>>> 一.前言 Device Tree总共有三篇,分别是: 1.为何要引入Device Tree,这个机制是用来解决什么问题的?(请 ...

  4. starGAN原理代码分析

    下载: git clone https://github.com/yunjey/StarGAN.git 1 cd StarGAN/ 1 下载celebA训练数据: bash download.sh 1 ...

  5. tensorflow笔记:多层CNN代码分析

    tensorflow笔记系列:  (一) tensorflow笔记:流程,概念和简单代码注释  (二) tensorflow笔记:多层CNN代码分析  (三) tensorflow笔记:多层LSTM代 ...

  6. 模块加载过程代码分析1

    一.概述 模块是作为ELF对象文件存放在文件系统中的,并通过执行insmod程序链接到内核中.对于每个模块,系统都要分配一个包含以下数据结构的内存区. 一个module对象,表示模块名的一个以null ...

  7. kernel 3.10代码分析--KVM相关--虚拟机创建\VCPU创建\虚拟机运行

    分三部分:一是KVM虚拟机创建.二是VCPU创建.三是KVM虚拟机运行 第一部分: 1.基本原理 如之前分析,kvm虚拟机通过对/dev/kvm字符设备的ioctl的System指令KVM_CREAT ...

  8. 开源项目kcws代码分析--基于深度学习的分词技术

    http://blog.csdn.net/pirage/article/details/53424544 分词原理 本小节内容参考待字闺中的两篇博文: 97.5%准确率的深度学习中文分词(字嵌入+Bi ...

  9. crt0.S(_main)代码分析

    crt0,S(_main)代码分析 --- 1. 设置sp寄存器地址 //设置SP栈指针 #if defined(CONFIG_SPL_BUILD) && defined(CONFIG ...

  10. Device Tree(三):代码分析【转】

    转自:http://www.wowotech.net/linux_kenrel/dt-code-analysis.html Device Tree(三):代码分析 作者:linuxer 发布于:201 ...

最新文章

  1. HttpHand和HttpModule的详细解释,包括Asp.Net对Http请求的处理流程。
  2. Android移动开发之【Android实战项目】记一次app开发过程!
  3. 用JS查看修改CSS样式(cssText,attribute('style'),currentStyle,getComputedStyle)
  4. 积木赛尔号机器人_赛尔号:盖亚是如何一步步得到缪斯的青睐?千万别将他看成直男...
  5. 灼灼夏日 - 遥思故乡 - 赤子无相忘
  6. 苹果修复 Gatekeeper 绕过漏洞
  7. python入门经典-入门python有什么好的书籍推荐?
  8. Luogu3613 睡觉困难综合征
  9. Linux入门的基础知识点
  10. 【微信小程序项目】——十分钟开发网络API·天气查询小程序、天气查询,美观简约,简单易上手
  11. JS禁止右键、CTRL+C、查看源文件
  12. CodeForces - 1152 B. Neko Performs Cat Furrier Transform
  13. windows或office原版镜像下载地址及msdn.itellyou上面列的项目的详细解读
  14. 计算机科学与专业大学排名,计算机科学与技术专业大学排名
  15. c语言算兔子野鸡,“上山兔子下山鸡,离水甲鱼不要追”,有什么科学依据?...
  16. react-ative安装expo-linking后运行报错
  17. PAT-A1008(C/C++代码解析)
  18. excel后几位数字自动变成000
  19. Epub格式怎么转换成PDF格式?这个方法轻松解决
  20. windows11+office2021安装教程

热门文章

  1. linux设备开发详解宋宝华,[Linux设备驱动开发详解(第2版)].宋宝华.pdf
  2. Solidity学习教程
  3. 中职计算机基础课堂传统教学,中职计算机课堂教学初探
  4. 什么是缺陷分析?一篇文章带你了解,测试工程师必备技能
  5. php正则表达式 w3c,正则表达式 – 匹配规则 | w3cschool菜鸟教程
  6. RNAcentral 数据库简介
  7. JCEF vs JxBrowser
  8. STM8S103f单片机的开发(1)LED灯的点亮
  9. windows winrar 指令_WINRAR 命令行语法
  10. Multisim14实现逻辑函数的化简与变换