根据官方实际代码,更加详细一点的网络结构如下图所示,可以看出,与SiamFC的网络结构类似,CFNet也包含两个分支——z和x,其中z分支对应目标物体模板,可以理解为目标在第 帧之内所有帧的模板数据加权融合(利用学习率来进行,与KCF算法类似),x分支对应目标物体搜索图像,它是目标周围的一大片区域,用于在区域内利用滑动窗口进行搜索,从而确定目标的真正位置。
论文定义的join_cf_window层,其主要作用是对模板图像进行加窗,抑制边缘部分从而尽量减轻因样本循环移位带来的边缘失真(边界效应),join_cf_window层的定义位于make_net.m源码中:
join = dagnn.DagNN();
% Apply window before correlation filter.
join.addLayer('cf_window', MulConst(), ...
{'in1'}, {'cf_example'}, {'window'});
p = join.getParamIndex('window');
join.params(p).value = single(make_window(in_sz, join_opts.window));
join.params(p).learningRate = join_opts.window_lr;
其中MulConst是作者自己定义的计算函数,其具体定义位于src/util目录下的MulConst.m和mul_const.m文件中。
在mul_const.m文件中,关于前向传播时,其实现细节为:
y = bsxfun(@times, x, h);
varargout = {y};
其中,x表示join_cf_window层的输入数据,h表示窗,可以看出加窗的过程实质是矩阵元素级乘法,这一点与KCF算法相同。
这里需要注意的是:窗口h的定义位于src/training目录下的make_window.m源码文件中,如下所示:
function h = make_window(sz, type)
% sz = [m1, m2]
switch type
case ''
h = ones(sz(1:2));
case 'cos'
h = bsxfun(@times, reshape(hann(sz(1)), [sz(1), 1]), ...
reshape(hann(sz(2)), [1, sz(2)]));
otherwise
error(sprintf('unknown window: ''%s''', type));
end
end
在mul_const.m文件中,利用join_cf_window层进行反向传播时,其实现细节为:
der_x = bsxfun(@times, der_y, h);
der_h = sum(sum(der_y .* x, 3), 4);
varargout = {der_x, der_h};
这里面有一个细节需要注意:作者在设计该层的方向传播时,用的也是矩阵元素级乘法,与正向传播的计算模式相同,并没有求偏导的过程。个人推测这里面的原因是:join_cf_window层的计算任务是对数据进行加窗,这样的任务对正向和反向传播是对等的,因此反向传播计算方式与正向传播类似。
join_cf层是论文中非常关键的一层,文章的核心内容几乎全体现在这一层里面了。该层的定义位于make_net.m源码中:
join.addLayer('cf', ...
CorrFilter('lambda', join_opts.lambda, 'bias', join_opts.bias), ...
{'cf_example'}, cf_outputs, {'cf_target'});
其中CorrFilter是作者自己定义的函数,它的具体定义位于src/util目录下的CorrFilter.m和corr_filter.m文件中,两者的关系是:CorrFilter.m调用corr_filter.m。首先分析CorrFilter.m,其关键代码为:
function outputs = forward(obj, inputs, params)
assert(numel(inputs) == 1, 'one input is needed');
assert(numel(params) == 1, 'one param is needed');
args = {'lambda', obj.lambda};
if obj.bias
[outputs{1}, outputs{2}] = corr_filter_bias(...
inputs{1}, params{1}, [], [], args{:});
else
outputs{1} = corr_filter(inputs{1}, params{1}, [], args{:});
end
end
function [derInputs, derParams] = backward(obj, inputs, params, derOutputs)
assert(numel(inputs) == 1, 'one input is needed');
assert(numel(params) == 1, 'one param is needed');
args = {'lambda', obj.lambda};
if obj.bias
assert(numel(derOutputs) == 2, 'expect two gradients');
[derInputs{1}, derParams{1}] = corr_filter_bias(...
inputs{1}, params{1}, derOutputs{1}, derOutputs{2}, args{:});
else
assert(numel(derOutputs) == 1, 'expect one gradient');
[derInputs{1}, derParams{1}] = corr_filter(...
inputs{1}, params{1}, derOutputs{1}, args{:});
end
end
从上述代码可以看出,CorrFilter.m源码文件中定义了join_cf层的前向传播函数(对应tracking过程)和反向传播函数(对应training过程),其中判断分支if obj.bias无需理会,因为代码默认配置的该参数值为false。前向传播和反向传播的详细实现均位于src/util目录下的corr_filter.m文件中,现将该源代码完整贴出:
function varargout = corr_filter(x, y, der_w, varargin)
opts.lambda = nan;
opts = vl_argparse(opts, varargin);
% x is [m1, m2, p, b]
% y is [m1, m2]
% der_w is same size as x
sz = size_min_ndims(x, 4);
n = prod(sz(1:2));
y_f = fft2(y);
x_f = fft2(x);
% k = 1/n sum_i x_i corr x_i + lambda delta
assert(~isnan(opts.lambda), 'lambda must be specified');
k_f = 1/n*sum(conj(x_f).*x_f, 3) + opts.lambda;
% a must satisfy n (k conv a) = y
% The signal a contains a weight per example (shift)
a_f = 1/n*bsxfun(@times, y_f, 1./k_f);
if isempty(der_w)
% Use same weight a for all channels i.
% w[i] = a corr x[i]
w_f = bsxfun(@times, conj(a_f), x_f);
% w = ifft2(w_f, 'symmetric');
w = real(ifft2(w_f));
varargout = {w};
else
der_w_f = fft2(der_w);
% a, x -> w
% w[i] = a corr x[i]
% dw[i] = da corr x[i] + a corr dx[i]
% F dw[i] = conj(F da) .* F x[i] + conj(F a) .* F dx[i]
% = sum_i = sum_i
% = sum_i
% = + sum_i
der_a_f = sum(x_f .* conj(der_w_f), 3);
der_x_f = bsxfun(@times, a_f, der_w_f);
% k, y -> a
% k conv a = 1/n y
% dk conv a + k conv da = 1/n dy
% dk_f .* a_f + k_f .* da_f = 1/n dy_f
% =
% =
% = <1/n der_a_f .* conj(k_f^-1), dy_f> + <-der_a_f .* conj(k_f^-1 .* a_f), dk_f>
% = +
der_y_f = 1/n*sum(der_a_f .* conj(1 ./ k_f), 4); % accumulate gradients over batch
der_y = real(ifft2(der_y_f));
der_k_f = -der_a_f .* conj(a_f ./ k_f);
% x -> k
% k = 1/n sum_i x_i corr x_i + lambda delta
% dk = 1/n sum_i {dx[i] corr x[i] + x[i] corr dx[i]}
% F dk = 1/n sum_i {conj(F dx[i]) .* F x[i] + conj(F x[i]) .* F dx[i]}
% =
% = sum_i
% = sum_i + <1/n F der_k .* F x[i], F dx[i]>
% = sum_i
% = sum_i
% = sum_i
der_x_f = der_x_f + 2/n*bsxfun(@times, real(der_k_f), x_f);
% der_x = ifft2(der_x_f, 'symmetric');
der_x = real(ifft2(der_x_f));
varargout = {der_x, der_y};
end
end
上述代码即为论文join_cf层的核心实现部分,函数内部通过isempty(der w)判断来分别实现正向传播和反向传播过程,首先分析正向传播过程,其对应于is empty(der w)=true的分支中,在该if判断之前及其内部,下面四行代码最为关键:
k_f = 1/n*sum(conj(x_f).*x_f, 3) + opts.lambda;
a_f = 1/n*bsxfun(@times, y_f, 1./k_f);
w_f = bsxfun(@times, conj(a_f), x_f);
w = real(ifft2(w_f));
varargout = {w};
上述前三行代码对应论文原文中的公式7(a)-7(c),如下所示:
其中表示目标物体模板(在代码实现中它是一个融合了多帧图像的移动平均值,因此叫做模板),符号表示矩阵元素级乘法,表示正则项系数,表示单位矩阵,表示目标物体自相关,表示期望的响应,表示期望响应与自相关的关联,是join_cf层最终的输出,它表示求解得到的滤波器。
现在分析join_cf层的反向传播。由于论文将相关滤波作为一个layer嵌入到网络中并且还需要进行end-to-end训练,因此有必要为join_cf层设计反向传播函数。同样地,在corr_filter.m文件中,反向传播最为关键的核心代码如下所示:
der_a_f = sum(x_f .* conj(der_w_f), 3);
der_y_f = 1/n*sum(der_a_f .* conj(1 ./ k_f), 4); % accumulate gradients over batch
der_k_f = -der_a_f .* conj(a_f ./ k_f);
der_x_f = der_x_f + 2/n*bsxfun(@times, real(der_k_f), x_f);
varargout = {der_x, der_y};
上述四行代码对应论文原文中的公式10,如下所示:
反向传播的最终输出,是变量der_x和der_y。
关于join_cf层的正向传播和反向传播的详细推导过程,可以参考本人的另一篇博客文章:CFNet视频目标跟踪推导笔记
join_crop_z层的主要作用是对滤波器进行裁切。个人推测作者设计这一层的原因:只有进行了裁切,保留滤波器的中央核心区域,才能进行Siamese网络最终的匹配模式进行跟踪。
join_crop_z层的最初定义位于make_net.m源码中:
join.addLayer('crop_z', ...
CropMargin('margin', 16), ...
cf_outputs, xcorr_inputs{1});
其中CropMargin是作者自己定义的函数,其内部包含了正向传播函数和反向传播函数。
首先看正向传播代码,位于src/util/CropMargin.m源码文件中,如下所示:
function outputs = forward(obj, inputs, params)
assert(numel(inputs) == 1);
assert(numel(params) == 0);
x = inputs{1};
sz = size_min_ndims(x, 4);
p = obj.margin;
y = x(1+p:end-p, 1+p:end-p, :, :);
outputs = {y};
end
从上述代码可以看出,变量p即为需要裁掉的边缘大小,起裁切作用是语句y = x(1+p:end-p, 1+p:end-p, :, :);,由于网络中的数据是四维的,裁切只针对第一维和第二维,也就是平面视觉部分,因此后面两个参数都没有进行配置。
join_crop_z层的反向传播代码如下所示:
function [derInputs, derParams] = backward(obj, inputs, params, derOutputs)
assert(numel(inputs) == 1);
assert(numel(params) == 0);
assert(numel(derOutputs) == 1);
x = inputs{1};
dldy = derOutputs{1};
if isa(x, 'gpuArray')
dldx = gpuArray(zeros(size(x), classUnderlying(x)));
else
dldx = zeros(size(x), class(x));
end
p = obj.margin;
dldx(1+p:end-p, 1+p:end-p, :, :) = dldy;
derInputs = {dldx};
derParams = {};
end
从上述代码可以看出,在进行反向传播时,其最终要求得的变量derInputs相当于对变量dldy的周围进行0元素填充,因此该处的反向传播代码也不涉及求偏导。
join_xcorr层的主要作用是通过滑动窗口方式进行匹配,并形成response map用于最后的目标判别,它的最初定义位于src/training目录下的make_net.m源码文件的make_join_corr_filt函数中,如下所示:
join.addLayer('xcorr', XCorr('bias', join_opts.bias), ...
xcorr_inputs, {'out'});
其中XCorr是作者自己定义的函数,它位于src/util/XCorr.m源码文件中,下面将分析该层的正向传播和反向传播实现。
在src/util/XCorr.m源码文件中,join_xcorr层的前向传播代码如下所示:
function outputs = forward(obj, inputs, params)
if obj.bias
assert(numel(inputs) == 3, 'three inputs are needed');
else
assert(numel(inputs) == 2, 'two inputs are needed');
end
if obj.bias
outputs{1} = cross_corr(inputs{1:3});
else
outputs{1} = cross_corr(inputs{1:2}, []);
end
end
由于在CFNet源码中,参数obj.bias的值默认被赋为false,因此只需要关注代码outputs{1} = cross_corr(inputs{1:2}, []);即可。在这一行代码中,调用了cross_corr(z, x, c, der_y)函数,调用时传递的参数是inputs{1:2},它们的含义如下:
该函数位于src/util/cross_corr.m源码文件中,源码内包含了前向传播和反向传播的具体实现逻辑,其中前向传播部分如下所示:
z_sz = size_min_ndims(z, 4);
x_sz = size_min_ndims(x, 4);
r_sz = [x_sz(1:2) - z_sz(1:2) + 1, 1, x_sz(4)];
x_ = reshape(x, [x_sz(1:2), prod(x_sz(3:4)), 1]);
r_ = vl_nnconv(x_, z, []);
assert(isequal(size_min_ndims(r_, 4), [r_sz(1:2), r_sz(4), 1]));
r = reshape(r_, r_sz);
y = r;
varargout = {y};
从上述源码可以看出,滑动窗口卷积的核心代码是r_ = vl_nnconv(x_, z, []);作者调用matconvnet里面的函数实现了这一过程。
在src/util/XCorr.m源码文件中,join_xcorr层的反向传播代码如下所示:
function [derInputs, derParams] = backward(obj, inputs, params, derOutputs)
if obj.bias
assert(numel(inputs) == 3, 'three inputs are needed');
else
assert(numel(inputs) == 2, 'two inputs are needed');
end
assert(numel(derOutputs) == 1, 'only one gradient should be flowing in this layer (dldy)');
if obj.bias
[derInputs{1:3}] = cross_corr(inputs{1:3}, derOutputs{1});
else
[derInputs{1:2}] = cross_corr(inputs{1:2}, [], derOutputs{1});
end
derParams = {};
end
前文已经提及,参数obj.bias的值默认被赋为false,因此在进行反向传播时重点关注代码[derInputs{1:2}] = cross_corr(inputs{1:2}, [], derOutputs{1});即可,其传入的参数中inputs{1}对应分支z的输出(目标模板),inputs{2}对应分支x的输出(搜索图像),函数cross_corr位于src/util/cross_corr.m源码文件中,其中反向传播部分如下所示:
z_sz = size_min_ndims(z, 4);
x_sz = size_min_ndims(x, 4);
r_sz = [x_sz(1:2) - z_sz(1:2) + 1, 1, x_sz(4)];
x_ = reshape(x, [x_sz(1:2), prod(x_sz(3:4)), 1]);
der_r = der_y;
der_r_ = reshape(der_r, [r_sz(1:2), r_sz(4), 1]);
[der_x_, der_z] = vl_nnconv(x_, z, [], der_r_);
der_x = reshape(der_x_, x_sz);
varargout = {der_z, der_x};
从上述源码可以看出,作者通过调用vl_nnconv函数来实现join_xcorr层中变量z和x的偏导数计算。
fin_adjust层的主要作用是对join_xcorr层计算出来的response map矩阵进行校准,形成更加规范的response map,其实现主要通过MatConvNet来进行,其代码如下所示:
final.layers = {};
final.layers{end+1} = struct(...
'type', 'conv', 'name', 'adjust', ...
'weights', {{single(1), single(-0.5)}}, ...
'learningRate', [1, 2], ...
'weightDecay', [0 0], ...
'opts', {convOpts});
CFNet论文在SiamFC跟踪算法的基础上,将相关滤波引入,形成一个独立的网络层参与end-to-end的训练,在网络结构设计上具有自己的特色,了解这些设计思想和方法,对我们的学习和工作具有较强的实用价值。
更多内容,欢迎扫码关注“视觉边疆”微信订阅号