现在的位置: 首页 > 综合 > 正文

Deeplearning Toolbox中CNN代码学习

2017年12月25日 ⁄ 综合 ⁄ 共 8352字 ⁄ 字号 评论关闭
    最近在研究CNN的实现,在还没自己动手写之前,先看看现在比较常用的Matlab的Deeplearning Toolbox中的CNN实现。

    在Toolbox中,CNN的实现过程主要包括下面几个文件
    cnntrain.m           //用于训练CNN的m文件
    cnntest.m            //用于预测的m文件
    cnnsetup.m           //设置训练参数的m文件
    cnnnumgradcheck.m    //用于梯度检查的m文件
    cnnff.m              //用于计算前向过程的m文件
    cnnbp.m              //用于计算误差方向传递的m文件
    cnnapplygrads.m      //用于使用梯度下降算法的m文件



    在test_example_CNN.m中,Toolbox的作者使用了MNIST手写字体数据库对CNN部分进行了测试。我将从test_example_CNN.m文件开始,按照使用CNN的过程对CNN的每一部分代码进行学习。



test_example_CNN.m

function test_example_CNN
load mnist_uint8;

% 将样本调整大小,并进行归一化
train_x = double(reshape(train_x',28,28,60000))/255;
test_x = double(reshape(test_x',28,28,10000))/255;
train_y = double(train_y');
test_y = double(test_y');

%% ex1 Train a 6c-2s-12c-2s Convolutional neural network 
%will run 1 epoch in about 200 second and get around 11% error. 
%With 100 epochs you'll get around 1.2% error

rand('state',0)


% CNN的网络结构,这些只是说明卷积层和下采样层,CNN是一个结构数组
cnn.layers = {
    struct('type', 'i') %input layer
    struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer
    struct('type', 's', 'scale', 2) %sub sampling layer
    struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer
    struct('type', 's', 'scale', 2) %subsampling layer
};


opts.alpha = 1;        %BP算法的学习速率
opts.batchsize = 50;   %进行一个训练的样本的大小
opts.numepochs = 5;    %迭代次数


cnn = cnnsetup(cnn, train_x, train_y);        %设置CNN的结构
cnn = cnntrain(cnn, train_x, train_y, opts);  %训练CNN

[er, bad] = cnntest(cnn, test_x, test_y);     %测试CNN

%plot mean squared error
figure; plot(cnn.rL);
assert(er<0.12, 'Too big error');

er                   %显示错误率
save('CNN-10');      %保存神经网络

    从test_example_CNN.m文件可以看出,这个文件主要按照下面的步骤进行:
  1. 将读取训练样本和测试样本;
  2. 将样本恢复为图像,并归一化为0~1;
  3. 设置CNN网络的层数,每一层的结构,学习速率,batch的大小,迭代的次数;
  4. 调用cnnsetup函数,初始化CNN的结构和参数;
  5. 调用cnntrain函数,训练CNN;
  6. 调用cnntest函数,对测试样本进行预测;
  7. 输出错误率。



cnnsetup.m
function net = cnnsetup(net, x, y)
%    assert(~isOctave() || compare_versions(OCTAVE_VERSION, '3.8.0', '>='), ['Octave 3.8.0 or greater is required for CNNs as there is a bug in convolution in previous versions. See http://savannah.gnu.org/bugs/?39314. Your version is ' OCTAVE_VERSION]);
    
    % 每一层输入的特征图的个数
    inputmaps = 1;
    mapsize = size(squeeze(x(:, :, 1)));

    for l = 1 : numel(net.layers)   %  layer
        
        if strcmp(net.layers{l}.type, 's')   %下采样层
            mapsize = mapsize / net.layers{l}.scale;  %修改特征图的大小
            assert(all(floor(mapsize)==mapsize), ['Layer ' num2str(l) ' size must be integer. Actual: ' num2str(mapsize)]);
            for j = 1 : inputmaps
                net.layers{l}.b{j} = 0;  %下采样层的偏移初始化为0,此处并没有安装经典的LeNet-5中设置一个系数参数
            end
        end
        
        if strcmp(net.layers{l}.type, 'c')  %卷积层
            mapsize = mapsize - net.layers{l}.kernelsize + 1;  %卷积层特征图大小
            fan_out = net.layers{l}.outputmaps * net.layers{l}.kernelsize ^ 2;
            for j = 1 : net.layers{l}.outputmaps  %  output map
                fan_in = inputmaps * net.layers{l}.kernelsize ^ 2;
                for i = 1 : inputmaps  %  input map
                    net.layers{l}.k{i}{j} = (rand(net.layers{l}.kernelsize) - 0.5) * 2 * sqrt(6 / (fan_in + fan_out));  %卷积核参数随机初始化,此处为矩阵
                end
                net.layers{l}.b{j} = 0;   %卷积层的偏置初始化为0
            end
            inputmaps = net.layers{l}.outputmaps; %将下一层的输入特征图个数更新为此卷积层的输出特征图个数
        end
    end
    
    % 'onum' is the number of labels, that's why it is calculated using size(y, 1). If you have 20 labels so the output of the network will be 20 neurons.
    % 'fvnum' is the number of output neurons at the last layer, the layer just before the output layer.
    % 'ffb' is the biases of the output neurons.
    % 'ffW' is the weights between the last layer and the output neurons. Note that the last layer is fully connected to the output layer, that's why the size of the weights is (onum * fvnum)
    fvnum = prod(mapsize) * inputmaps;  
    onum = size(y, 1);     %输出层的神经元个数

    net.ffb = zeros(onum, 1);  %初始化为0
    net.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum)); %随机初始化
end
    从cnnsetup.m文件可以看出,CNN结构中每一层的特征图的输入特征图的个数,以及每一层的可训练参数都在cnnsetup函数中进行设置。这里和经典的LeNet-5中使用的CNN结构不同之处在于,对于下采样层,并没有设置可训练的系数参数。





cnntrain.m文件

function net = cnntrain(net, x, y, opts)
% This function is used to train the CNN
    m = size(x, 3);   %训练样本个数,每次迭代都使用所有样本
    numbatches = m / opts.batchsize;  %样本包个数,在一个包的样本进行梯度计算后才更新参数
    
    if rem(numbatches, 1) ~= 0        %样本包个数不是常数时报错
        error('numbatches not integer');
    end
    
    net.rL = [];
    
    for i = 1 : opts.numepochs   %开始进行迭代
        disp(['epoch ' num2str(i) '/' num2str(opts.numepochs)]);
        tic;  %计时开始
        
        kk = randperm(m);
        for l = 1 : numbatches
            
            %随机抽取batchsize个训练样本
            batch_x = x(:, :, kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));
            batch_y = y(:,    kk((l - 1) * opts.batchsize + 1 : l * opts.batchsize));

            net = cnnff(net, batch_x);   %计算CNN的forward的值
            net = cnnbp(net, batch_y);   %计算CNN的梯度?
            net = cnnapplygrads(net, opts);  %使用梯度下降算法训练CNN
            if isempty(net.rL)
                net.rL(1) = net.L;
            end
            net.rL(end + 1) = 0.99 * net.rL(end) + 0.01 * net.L;
        end
        toc;
    end
    
end
   在cnntrain.m文件中,主要是调用了cnnff进行前向计算,然后调用cnnbp函数进行误差计算,最后调用cnnappplygrads函数更新cnn的参数。





cnnff.m文件
function net = cnnff(net, x)
%This function is used to calculateing the forward 
    n = numel(net.layers);
    net.layers{1}.a{1} = x; 
    inputmaps = 1;

    for l = 2 : n   %  for each layer
        
        if strcmp(net.layers{l}.type, 'c')     %卷积层
            %  !!below can probably be handled by insane matrix operations
            for j = 1 : net.layers{l}.outputmaps   %  for each output map
                %  create temp output 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
                    z = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid'); 
                end
                %  add bias, pass through nonlinearity
                net.layers{l}.a{j} = sigm(z + net.layers{l}.b{j});  %上采样层经过Sigmoid函数进行激活
            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
                z = convn(net.layers{l - 1}.a{j}, ones(net.layers{l}.scale) / (net.layers{l}.scale ^ 2), 'valid');   %  !! replace with variable
                net.layers{l}.a{j} = z(1 : net.layers{l}.scale : end, 1 : net.layers{l}.scale : end, :);             % 下采样层没有经过Sigmoid函数激活?
            end
        end
    end

    
    %  concatenate all end layer feature maps into vector
    % 将最后一层特征图展开为列向量的形式
    net.fv = [];
    for j = 1 : numel(net.layers{n}.a)
        sa = size(net.layers{n}.a{j});
        net.fv = [net.fv; reshape(net.layers{n}.a{j}, sa(1) * sa(2), sa(3))];
    end
    
    %  feedforward into output perceptrons
    net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));  % 输出层为普通sigm激活?

end
    在cnnff.m文件中,我们可以看出,对于下采样层,这个实现并没有使用到在cnnsetup中初始化的偏置b,同时,下采样层也没有经过sigmoid函数进行激活。这个与经典的LeNet-5的实现之间有较大区别,同时,在这个实现中,对于卷积层的每一个特征图,都是将前一层中所有的特征图分别进行卷积后全部相加,然后加上一个偏置后,经过Sigmoid函数激活后得到的,并没有经过选择前一层的某些特征图进行卷积的过程。这一点和LeNet-5的实现也有所不同。





cnnbp.m
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
    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});
    fvnum = sa(1) * sa(2);
    for j = 1 : numel(net.layers{n}.a)
        net.layers{n}.d{j} = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum, :), sa(1), sa(2), sa(3));
    end

    % 非输出层的delta计算
    for l = (n - 1) : -1 : 1
        
        % 卷积层的的delta的计算
        if strcmp(net.layers{l}.type, 'c')
            for j = 1 : numel(net.layers{l}.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]) / net.layers{l + 1}.scale ^ 2);
            end
            
         % 下采样层的delta计算   
        elseif strcmp(net.layers{l}.type, 's')
            for i = 1 : numel(net.layers{l}.a)
                z = zeros(size(net.layers{l}.a{1}));
                for j = 1 : numel(net.layers{l + 1}.a)
                     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
    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)
                    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
                net.layers{l}.db{j} = sum(net.layers{l}.d{j}(:)) / size(net.layers{l}.d{j}, 3);
            end
        end
    end
    
    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
    在cnnbp.m文件中,主要计算了各层之间的delta,以及卷积层的参数的梯度,由于在cnnff函数中,没有用到下采样层的偏置,所以,在cnnbp函数中,并没有计算该参数的梯度的过程。
   




cnnapplaygrads.m文件
function net = cnnapplygrads(net, opts)
% 使用梯度下降算法进行参数训练
    for l = 2 : numel(net.layers)
        if strcmp(net.layers{l}.type, 'c')  %只更新卷积层的参数?没有更新下采样层的参数?
            for j = 1 : numel(net.layers{l}.a)
                for ii = 1 : numel(net.layers{l - 1}.a)
                    net.layers{l}.k{ii}{j} = net.layers{l}.k{ii}{j} - opts.alpha * net.layers{l}.dk{ii}{j};
                end
                net.layers{l}.b{j} = net.layers{l}.b{j} - opts.alpha * net.layers{l}.db{j};
            end
        end
    end

    net.ffW = net.ffW - opts.alpha * net.dffW;
    net.ffb = net.ffb - opts.alpha * net.dffb;
end
    在cnnapplaygrads函数中,主要是使用梯度下降算法更新卷积层的参数和输出层的参数,由于在cnnff中并没有使用到下采样层的偏置这个参数,所以在cnnapplaygrads函数中,也没有更新这个参数的过程。






cnntest.m文件

function [er, bad] = cnntest(net, x, y)
    %  feedforward
    net = cnnff(net, x);
    
    % CNN给出的样本的输出的类别,此类别为输出神经元中最大的部分,此部分与传统神经BP神经网络一致
    [~, h] = max(net.o);
    
    % 样本的实际输出类别
    [~, a] = max(y);
    
    % 寻找预测错误的样本
    bad = find(h ~= a);

    er = numel(bad) / size(y, 2);
end

    cnntest函数主要用来对测试样本进行预测,并计算错误率,其实,对于CNN来说,对样本进行预测就是简单地使用cnnff函数计算样本的前向过程,然后找出输出层中值最大的那个神经元的类别,就是样本对应的类别,所以,在cnntest函数中,并没有什么重要的内容。





    至此,Toolbox中cnn部分的代码已经介绍完毕,其实,要想能真正理解cnn的训练过程,还是需要看看《Notes on Convolutional Neural Networks》,里面对于CNN的训练过程的介绍还是相当清楚。
    在看完CNN的代码之后,就可以开始动手修改这些代码了。Enjoy it~


抱歉!评论已关闭.