关闭

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

2514人阅读 评论(0) 收藏 举报
分类:

       反向传输过程是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 = 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

在这里插上一段代码,原githupDeep Learningtoolbox代码位置:https://github.com/rasmusbergpalm/DeepLearnToolbox


CNN卷积神经网络代码理解,下载地址

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); % 网络层数

    %  error
    net.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 delta
    if strcmp(net.layers{n}.type, 'c')         %  only conv layers has sigm function
        net.fvd = net.fvd .* (net.fv .* (1 - net.fv));
    end

    %  reshape feature vector deltas into output map style
    sa = 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 : 1
        if 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;
				% end
				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]) / net.layers{l + 1}.scale ^ 2);
            end
        elseif 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');
                end
                net.layers{l}.d{i} = z;
            end
        end
    end

    %%  calc gradients
	% 这里与 Notes on Convolutional Neural Networks 中不同,这里的 子采样 层没有参数,也没有
	% 激活函数,所以在子采样层是没有需要求解的参数的
    for l = 2 : n
        if 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);
            end
        end
    end
	% 最后一层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);
    end
end




[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



1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:17371次
    • 积分:270
    • 等级:
    • 排名:千里之外
    • 原创:8篇
    • 转载:9篇
    • 译文:0篇
    • 评论:1条
    文章分类
    最新评论
  • Caffe学习:Layers

    nzzfsw: 请问楼主的caffe包是从哪里下载的?我的caffe里缺少vision_layers.hpp等文件啊...