您的位置:首页 > 理论基础 > 计算机网络

CNN卷积神经网络--反向传播(4,代码理解)

2016-08-30 12:02 543 查看
       反向传输过程是CNN最复杂的地方,虽然从宏观上来看基本思想跟BP一样,都是通过最小化残差来调整权重和偏置,但CNN的网络结构并不像BP那样单一,对不同的结构处理方式不一样,而且因为权重共享,使得计算残差变得很困难,很多论文[1][5]和文章[4]都进行了详细的讲述,但我发现还是有一些细节没有讲明白,特别是采样层的残差计算,我会在这里详细讲述。  输出层的残差  和BP一样,CNN的输出层的残差与中间层的残差计算方式不同,输出层的残差是输出值与类标值得误差值,而中间各层的残差来源于下一层的残差的加权和。输出层的残差计算如下:公式来源  这个公式不做解释,可以查看公式来源,看斯坦福的深度学习教程的解释。  下一层为采样层(subsampling)的卷积层的残差  当一个卷积层L的下一层(L+1)为采样层,并假设我们已经计算得到了采样层的残差,现在计算该卷积层的残差。从最上面的网络结构图我们知道,采样层(L+1)的map大小是卷积层L的1/(scale*scale),ToolBox里面,scale取2,但这两层的map个数是一样的,卷积层L的某个map中的4个单元与L+1层对应map的一个单元关联,可以对采样层的残差与一个scale*scale的全1矩阵进行克罗内克积进行扩充,使得采样层的残差的维度与上一层的输出map的维度一致,Toolbox的代码如下,其中d表示残差,a表示输出值:
net.layers{l}.d{j} = net.layers{l}.a{j} .* (1 - net.layers{l}.a{j}) .* expand(net.layers{l + 1}.d{j}, [net.layers{l + 1}.scale net.layers{l + 1}.scale 1])
  扩展过程:图5  利用卷积计算卷积层的残差:图6   下一层为卷积层(subsampling)的采样层的残差  当某个采样层L的下一层是卷积层(L+1),并假设我们已经计算出L+1层的残差,现在计算L层的残差。采样层到卷积层直接的连接是有权重和偏置参数的,因此不像卷积层到采样层那样简单。现再假设L层第j个map Mj与L+1层的M2j关联,按照BP的原理,L层的残差Dj是L+1层残差D2j的加权和,但是这里的困难在于,我们很难理清M2j的那些单元通过哪些权重与Mj的哪些单元关联,Toolbox里面还是采用卷积(稍作变形)巧妙的解决了这个问题,其代码为:
convn(net.layers{l + 1}.d{j}, rot180(net.layers{l + 1}.k{i}{j}), 'full');
rot180表示对矩阵进行180度旋转(可通过行对称交换和列对称交换完成),为什么这里要对卷积核进行旋转,答案是:通过这个旋转,'full'模式下得卷积的正好抓住了前向传输计算上层map单元与卷积和及当期层map的关联关系,需要注意的是matlab的内置函数convn在计算卷积前,会对卷积核进行一次旋转,因此我们之前的所有卷积的计算都对卷积核进行了旋转:
a = 1 1 1 1 1 1 1 1 1 k = 1 2 3 4 5 6 7 8 9 >> convn(a,k,'full') ans = 1 3 6 5 3 5 12 21 16 9 12 27 45 33 18 11 24 39 28 15 7 15 24 17 9
   convn在计算前还会对待卷积矩阵进行0扩展,如果卷积核为k*k,待卷积矩阵为n*n,需要以n*n原矩阵为中心扩展到(n+2(k-1))*(n+2(k-1)),所有上面convn(a,k,'full')的计算过程如下:图7
实际上convn内部是否旋转对网络训练没有影响,只要内部保持一致(即都要么旋转,要么都不旋转),所有我的卷积实现里面没有对卷积核旋转。如果在convn计算前,先对卷积核旋转180度,然后convn内部又对其旋转180度,相当于卷积核没有变。
  为了描述清楚对卷积核旋转180与卷积层的残差的卷积所关联的权重与单元,正是前向计算所关联的权重与单元,我们选一个稍微大一点的卷积核,即假设卷积层采用用3*3的卷积核,其上一层采样层的输出map的大小是5*5,那么前向传输由采样层得到卷积层的过程如下:图8在这里插上一段代码,原githup的DeepLearning的toolbox代码位置:https://github.com/rasmusbergpalm/DeepLearnToolboxCNN卷积神经网络代码理解,下载地址 http://download.csdn.net/detail/qq295456059/9277657
function net = cnnff(net, x)
n = numel(net.layers); % 层数
net.layers{1}.a{1} = x; % 网络的第一层就是输入,但这里的输入包含了多个训练图像
inputmaps = 1; % 输入层只有一个特征map,也就是原始的输入图像

for l = 2 : n   %  for each layer
if strcmp(net.layers{l}.type, 'c') % 卷积层
%  !!below can probably be handled by insane matrix operations
% 对每一个输入map,或者说我们需要用outputmaps个不同的卷积核去卷积图像
for j = 1 : net.layers{l}.outputmaps   %  for each output map
%  create temp output map
% 对上一层的每一张特征map,卷积后的特征map的大小就是
% (输入map宽 - 卷积核的宽 + 1)* (输入map高 - 卷积核高 + 1)
% 对于这里的层,因为每层都包含多张特征map,对应的索引保存在每层map的第三维
% 所以,这里的z保存的就是该层中所有的特征map了
z = zeros(size(net.layers{l - 1}.a{1}) - [net.layers{l}.kernelsize - 1 net.layers{l}.kernelsize - 1 0]);
for i = 1 : inputmaps   %  for each input map
%  convolve with corresponding kernel and add to temp output map
% 将上一层的每一个特征map(也就是这层的输入map)与该层的卷积核进行卷积
% 然后将对上一层特征map的所有结果加起来。也就是说,当前层的一张特征map,是
% 用一种卷积核去卷积上一层中所有的特征map,然后所有特征map对应位置的卷积值的和
% 另外,有些论文或者实际应用中,并不是与全部的特征map链接的,有可能只与其中的某几个连接
z = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid');
end
%  add bias, pass through nonlinearity
% 加上对应位置的基b,然后再用sigmoid函数算出特征map中每个位置的激活值,作为该层输出特征map
net.layers{l}.a{j} = sigm(z + net.layers{l}.b{j});
end
%  set number of input maps to this layers number of outputmaps
inputmaps = net.layers{l}.outputmaps;
elseif strcmp(net.layers{l}.type, 's') % 下采样层
%  downsample
for j = 1 : inputmaps
%  !! replace with variable
% 例如我们要在scale=2的域上面执行mean pooling,那么可以卷积大小为2*2,每个元素都是1/4的卷积核
z = convn(net.layers{l - 1}.a{j}, ones(net.layers{l}.scale) / (net.layers{l}.scale ^ 2), 'valid');
% 因为convn函数的默认卷积步长为1,而pooling操作的域是没有重叠的,所以对于上面的卷积结果
% 最终pooling的结果需要从上面得到的卷积结果中以scale=2为步长,跳着把mean pooling的值读出来
net.layers{l}.a{j} = z(1 : net.layers{l}.scale : end, 1 : net.layers{l}.scale : end, :);
end
end
end

%  concatenate all end layer feature maps into vector
% 把最后一层得到的特征map拉成一条向量,作为最终提取到的特征向量
net.fv = [];
for j = 1 : numel(net.layers{n}.a) % 最后一层的特征map的个数
sa = size(net.layers{n}.a{j}); % 第j个特征map的大小
% 将所有的特征map拉成一条列向量。还有一维就是对应的样本索引。每个样本一列,每列为对应的特征向量
net.fv = [net.fv; reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3))];
end
%  feedforward into output perceptrons
% 计算网络的最终输出值。sigmoid(W*X + b),注意是同时计算了batchsize个样本的输出值
net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));

end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
<pre name="code" class="plain">function net = cnnbp(net, y)n = numel(net.layers); % 网络层数%  errornet.e = net.o - y;%  loss function% 代价函数是 均方误差net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2);%%  backprop deltas% 这里可以参考 UFLDL 的 反向传导算法 的说明% 输出层的 灵敏度 或者 残差net.od = net.e .* (net.o .* (1 - net.o));   %  output delta% 残差 反向传播回 前一层net.fvd = (net.ffW' * net.od);              %  feature vector deltaif strcmp(net.layers{n}.type, 'c')         %  only conv layers has sigm functionnet.fvd = net.fvd .* (net.fv .* (1 - net.fv));end%  reshape feature vector deltas into output map stylesa = size(net.layers{n}.a{1}); % 最后一层特征map的大小。这里的最后一层都是指输出层的前一层fvnum = sa(1) * sa(2); % 因为是将最后一层特征map拉成一条向量,所以对于一个样本来说,特征维数是这样for j = 1 : numel(net.layers{n}.a) % 最后一层的特征map的个数% 在fvd里面保存的是所有样本的特征向量(在cnnff.m函数中用特征map拉成的),所以这里需要重新% 变换回来特征map的形式。d 保存的是 delta,也就是 灵敏度 或者 残差net.layers{n}.d{j} = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3));end% 对于 输出层前面的层(与输出层计算残差的方式不同)for l = (n - 1) : -1 : 1if strcmp(net.layers{l}.type, 'c')for j = 1 : numel(net.layers{l}.a) % 该层特征map的个数% net.layers{l}.d{j} 保存的是 第l层 的 第j个 map 的 灵敏度map。 也就是每个神经元节点的delta的值% expand的操作相当于对l+1层的灵敏度map进行上采样。然后前面的操作相当于对该层的输入a进行sigmoid求导% 这条公式请参考 Notes on Convolutional Neural Networks% for k = 1:size(net.layers{l + 1}.d{j}, 3)% net.layers{l}.d{j}(:,:,k) = net.layers{l}.a{j}(:,:,k) .* (1 - net.layers{l}.a{j}(:,:,k)) .*  kron(net.layers{l + 1}.d{j}(:,:,k), ones(net.layers{l + 1}.scale)) / net.layers{l + 1}.scale ^ 2;% endnet.layers{l}.d{j} = net.layers{l}.a{j} .* (1 - net.layers{l}.a{j}) .* (expand(net.layers{l + 1}.d{j}, [net.layers{l + 1}.scale net.layers{l + 1}.scale 1]) / net.layers{l + 1}.scale ^ 2);endelseif strcmp(net.layers{l}.type, 's')for i = 1 : numel(net.layers{l}.a) % 第l层特征map的个数z = zeros(size(net.layers{l}.a{1}));for j = 1 : numel(net.layers{l + 1}.a) % 第l+1层特征map的个数z = z + convn(net.layers{l + 1}.d{j}, rot180(net.layers{l + 1}.k{i}{j}), 'full');endnet.layers{l}.d{i} = z;endendend%%  calc gradients% 这里与 Notes on Convolutional Neural Networks 中不同,这里的 子采样 层没有参数,也没有% 激活函数,所以在子采样层是没有需要求解的参数的for l = 2 : nif strcmp(net.layers{l}.type, 'c')for j = 1 : numel(net.layers{l}.a)for i = 1 : numel(net.layers{l - 1}.a)% dk 保存的是 误差对卷积核 的导数net.layers{l}.dk{i}{j} = convn(flipall(net.layers{l - 1}.a{i}), net.layers{l}.d{j}, 'valid') / size(net.layers{l}.d{j}, 3);end% db 保存的是 误差对于bias基 的导数net.layers{l}.db{j} = sum(net.layers{l}.d{j}(:)) / size(net.layers{l}.d{j}, 3);endendend% 最后一层perceptron的gradient的计算net.dffW = net.od * (net.fv)' / size(net.od, 2);net.dffb = mean(net.od, 2);function X = rot180(X)X = flipdim(flipdim(X, 1), 2);endend
[1].YANN LECUN. Gradient-Based Learning Applied to Document Recognition.[2].Shan Sung LIEW. Gender classification: A convolutional neural network approach.[3] D. H. Hubel and T. N. Wiesel, “Receptive fields, binocular interaction teraction,and functional architecture in the cat’s visual cortex,”[4] tornadomeet. http://www.cnblogs.com/tornadomeet/p/3468450.html.[5] Jake Bouvrie. Notes on Convolutional Neural Networks.[6] C++实现的详细介绍. http://www.codeproject.com/Articles/16650/Neural-Network-for-Recognition-of-Handwritten-Digi [7] matlab DeepLearnToolbox https://github.com/rasmusbergpalm/DeepLearnToolbox
                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: