這是CVPR17的一篇文章。項目地址
該論文主要基於SiameseFC網絡的改進,主要加入了CF層,並推導了前向和反向公式,也是端到端的網絡,使得網絡在淺層特徵下也能有不錯的性能,是整體網絡輕量化,速度也更快。下面的分析將結合具體的代碼,有不對的地方歡迎討論~
1. 網絡結構
基礎及背景這裏就不說了,直接來看模型。
爲什麼要加這個CF層:
結合傳統的DCF進行對比,DCF就是線性核的KCF,DCF分爲訓練和檢測兩步,訓練時用最小二乘法解一個嶺迴歸問題把CF模板w求出來,檢測時w和搜索域的patch卷積就得到了response,所以CFNet的原版SiameseFC沒有訓練直接把目標和搜索域做相關,比較naive,所以CFNet把DCF的一套都整合到了網絡中,是一個端到端的模型,由於加入了最小二乘的思想所以淺層也能有不錯的性能,但是這樣做的問題是把CF的邊界問題也帶到網絡中去了,所以作者又加了crop層僅保留中間的一部分,這就把邊界效應降低了。
CF層怎麼設計:
這裏首先要會推導嶺迴歸的最小二乘解w,然後把計算w的過程拆成能用網絡表示的3步如下:
不要忘了DCF的重要性質樣本x是循環樣本,這樣可以利用頻域加速求解,卷積變成了點乘,如下:
這樣就可以表示成一個網絡流的形式如下圖:
2. 構建基礎特徵網絡
CFNet的兩個支流的提特徵部分是完全一樣的,這也是Siamese的意思,訓練也是隻要訓一支就好了,就是共享參數,這裏使用了AlexNet的五層卷積結構,代碼中解釋如下:
以第一層爲例,包括卷積,normalization和pooling操作
net = add_block(net, opts, '1', 11, 11, ...
opts.num_in(i), opts.num_out(i), ...
opts.conv_stride(i), 0, 1) ;
net = add_norm(net, opts, '1') ;
net.layers{end+1} = struct('type', 'pool', 'name', 'pool1', ...
'method', 'max', ...
'pool', [3 3], ...
'stride', opts.pool_stride(i), ...
'pad', 0) ;
(11,11)是濾波器大小,pooling的stride爲2,且只有前兩層有pooling,卷積層stride除了第一層是2,其餘都是1;
卷積層輸入輸出通道數表示如下,至於輸出爲什麼是輸入的兩倍這是因爲AlexNet網絡是用2臺GPU跑的,計算量分到了兩臺GPU上。
層數 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
輸入 | 3 | 48 | 256 | 192 | 192 |
輸出 | 96 | 256 | 384 | 384 | 256 |
3. 構建CF層
在構建CF層之前作者加了加窗層,目的和傳統一樣,抑制邊界效應。
join.addLayer('cf_window', MulConst(), ...
{'in1'}, {'cf_example'}, {'window'});
這裏的MulConst函數是作者設計的,MulConst中前傳反傳的核心代碼在mul_const函數中如下:
function varargout = mul_const(x, h, der_y)
% x is [m1, m2, p, b]
% h is [m1, m2]
% der_y is same size as x
if nargin < 3
der_y = [];
end
if isempty(der_y)
y = bsxfun(@times, x, h);
varargout = {y};
else
else
der_x = bsxfun(@times, der_y, h);
der_h = sum(sum(der_y .* x, 3), 4);
varargout = {der_x, der_h};
end
end
反傳的時候的誤差是用前一層的誤差和窗點乘,再按通道和batch求和。
然後加入CF層
join.addLayer('cf', ...
CorrFilter('lambda', join_opts.lambda, ...
'bias', join_opts.bias), ...
{'cf_example'}, cf_outputs, {'cf_target'});
看看具體CF函數是如何構建的,代碼在corr_filter.m中,按照流程圖前向代碼如下:
y_f = fft2(y);
x_f = fft2(x);
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));
y是target高斯label預先定義好的,所以前向過程僅輸入一個變量x,輸出w,過程完全符合流程圖。
那麼反向傳播如何呢,反傳增加一個輸入der_w,即w的誤差,前傳的前面4行都一樣除了最後兩行,看看代碼如何寫的
der_w_f = fft2(der_w);
der_a_f = sum(x_f .* conj(der_w_f), 3);
der_x_f = bsxfun(@times, a_f, der_w_f);
der_y_f = 1/n*sum(der_a_f .* conj(1 ./ k_f), 4);
der_y = real(ifft2(der_y_f));
der_y = real(ifft2(der_y_f));
der_x_f = der_x_f + 2/n*bsxfun(@times, real(der_k_f), x_f);
der_x = real(ifft2(der_x_f));
最後得到了需要的輸入誤差der_x和der_y,式子論文中給出了推導結果
至於如何推導的看看論文的補充材料應該可以理解。
4. 構建Crop層
CF層後緊接了一個Crop層還是爲了緩解邊界效應。來看看前向如何實現
x = inputs{1};
sz = size_min_ndims(x, 4);
p = obj.margin;
y = x(1+p:end-p, 1+p:end-p, :, :);
outputs = {y};
目的很簡單,僅僅取出中間目標的部分。反向如下:
x = inputs{1};
dldy = derOutputs{1};
dldx = zeros(size(x), class(x));
p = obj.margin;
dldx(1+p:end-p, 1+p:end-p, :, :) = dldy;
derInputs = {dldx};
也很簡單,就是把中間目標的誤差直接作爲輸入誤差,其他部分置0。
5. 構建相關層
這裏已經是最後一層了,將上面crop得到的模板特徵和搜索域上提取的特徵作相關,即滑窗點乘,最後得到響應圖,對應最大值的位置就是目標的相對位置了。相關層在代碼中代號“xcorr”,來看看具體代碼,先是前傳部分
y = vl_nnconv(x, z, []);
可以看到核心就是直接用卷積操作,代碼做了一些額外的處理,如有無bias的處理,這裏就不貼了。反傳的時候輸入y的誤差der_y,
[der_x_, der_z] = vl_nnconv(x_, z, [], der_y);
可以看到核心仍然是卷積層代碼,只是把卷積核換成了模板特徵x。
6. 實驗結果
最後看看CFNet的實驗結果
可以看到速度相比baseline有了不錯的提升,因爲CF層的引入提升了特徵的判別力所以淺層就可以獲得不錯的精度。歡迎與我討論~