您的位置:首页 > 其它

从DeepLearnToolbox-master看CNN

2015-12-05 23:21 309 查看
卷积神经网络
揭开卷积神经网络神秘的面纱,发现CNN也不过如此,就像对普通NN一样,第一步了解网络结构,第二步了解节点计算方法,第三步反向调节误差。就可以完全认识这个模型了。从网上看的大部分资料感觉很少有能够说清楚的,CNN确实原本也是一个比较难说明白的模型,所以从大牛的代码来看CNN会更清晰。
一、 CNN结构



图一
这个网络结构举了这样一个列子,输入的map是一个28*28的矩阵,输入层有1个map,到了第一层的时候可以设定有多少个map,图中第一层有6个map。从输入层到第一层的每一个map是通过一种类似卷积的运算求出来的(这种运算在下面小节卷积神经网络节点的计算方法会介绍),因而第一层一般就叫做C1。在第一层到第二层的时候是通过一种可以称为采样的运算得来的(这种运算方式也在后面讲述),故第二层一般称为s2。如上面图所示C1层有6个map,而后面的s2层也会有6个map,并且C1层和s1层的map是一一对应的关系,也就是s1的每个map只与C1层的一个map有关。然后从s2层到第三层又是通过类似卷积的运算得出来的,所以第三层就可以称为C3层。就这样网络一遍一遍的重复卷积运算和采样运算前向的发展。在上面的例子中看到最后一层变成了12个4*4的map。在最后一层这12个4*4的map到输出层还有一个权值。因为做的是模式识别,输出层每个节点输出的结果就是一个数值了而不是矩阵。
二、CNN每层计算方式
1、卷积运算
前面说到输入层到C1层和S2层到C3层都用到了类似于卷积的运算。这种运算方式如下



图二
这个地方的卷积核就类似于普通NN中的权值,而上一层的一个输出map Mj就是普通NN中的第j个节点的输出。看图一中举的例子,因为输入层只有一个map,而C1层有6个map,所以就要有1*6个卷积核了,而在s2层到C3层,s2有6个map,C3有12个map,所以这两层中间就要有6*12个卷积核。而C3层中第j个map到底应该怎么算的,就是s2中的每一个map与对应的卷积核做卷积运算,然后累加求和,再加上偏置,在带入激励函数sigmord函数得出来的。下面看一下工具箱的代码是不是这样子



图三
因为卷积运算和采样运算方式不同,所以第一行首先判断l层是卷积层还是采样层,如果是卷积层的话就计算l层net.layer{l}.outputmaps个map的值,所以就有了第二行代码(不算注释)的for循环,在第四行到第六行就是通过for循环计算前一层(即l-1层)inputmaps个输入map——net.Layers{l-1}.a{i}与对应的卷积核net.Layers{l}.k{i}{j}做卷积运算,然后累加求和。在第七行的部分,将累加求和所得的z与对应的偏置net.Layers{l}.b{j}相加然后带入sigmord函数,结果付给l层的map——net.Layers{l}.a{i}。有点说一下,在第五行中出现的convn函数就是卷积函数,做的运算就是图二中所示的运算。
2、重采样层的采样
重采样层的计算方式非常简单,通过看图三中倒数第四行、倒数第三行的代码可以看到,如果要求采样曾第j个map,只需要拿着前一层的map——net.Layers{l-1}.a{i}与一个卷积核做卷积运算,这个卷积核是ones(net.layers{l}.scale)/(
net.layers{l}.scale),在图一举的例子中采样层scale都为2,那么这个矩阵就是一个[1,1;1,1]/4=[1/4,1/4;1/4,1/4]这样的一个矩阵了。net.Layers{l-1}.a{i}和ones(net.layers{l}.scale)/(
net.layers{l}.scale)做完卷积运算后赋给z,再把z中的一部分提取出来,通过z(1 : net.layers{l}.scale : end, 1 :net.layers{l}.scale : end, :)可以看出把1 : net.layers{l}.scale: end的行和列提取了出来。这样整个一个过程可以通过图四来表示:



图四
在这里就知道了网络每层中的map到底应该怎样计算了。但是在第一部分CNN结构中说到,最后一层是有12个4*4的map,而我们最后的输出是一个行向量,比如要识别0-9十个手写字符,输出的结果应该是一个10维的向量,从12个4*4的map怎么到一个10维的向量呢?看下面的代码:



在最后一层有12个map,所以numel(net.layers{n}.a)为12,net.fv
= [net.fv; X];就是将X堆叠到net.fv上,代码中的X为reshape(net.layers{n}.a{j},
sa(1) * sa(2), sa(3)),reshape后的net.layers{n}.a{j}把一个4*4*M的矩阵转变成了一个16*M的矩阵,这个M就是本次训练样本数量:M=opts.batchsize,在图三种的代码也看到了net.layers{n}.a{j}是一个有三个维度的矩阵,因为写代码的时候必定会一次opts.batchsize个样本全部计算出来,而不是一个一个来的,这样代码就会简练好多。总之就是把原来的12个4*4的map先一个map一个map的平铺为1*16的向量,再把向量连接起来变成一个1*(16*12=192)的向量,这192个值作为输出层的输入分别乘以对应的权值:net.ffW
* net.fv加上偏置,repmat(net.ffb, 1, size(net.fv, 2)(这个地方还是因为一次要计算M=opts.batchsize个样本,所以要把偏置堆叠起来)。到这个地方这个网络每一步骤是怎么计算的已经完全熟知了。
这一部分粘贴的代码全部在cnnff.m文件中。
三、误差反向传播
这一部分粘贴的代码全在cnnbp.m文件中。在cnntrain函数中主要有下面三行代码net = cnnff(net,
batch_x);net = cnnbp(net, batch_y);net = cnnapplygrads(net, opts);cnnff函数就是前向计算每层map和最后输出的值,cnnbp函数进行误差反向调节,这个函数算是最关键的部分了,这两个函数理解之后整个CNN如何运行就一目了然了,最后一个函数cnnapplygrads是对权值和阈值进行更新。



Net.L是对损失函数loss function的定义,net.o是这一组opts.batchsize个样本通过网络的输出,所以net.o就是一个二维的矩阵,net.e
= net.o - y;对应的输出和理想输出的差就是误差。损失函数定义为net.L = 1/2* sum(net.e(:) .^ 2) /size(net.e, 2);,举个例子,假如CNN最终分为两类,每一组数据输入之后输出的是一个2维的向量,假设每次opts.batchsize=3组数据改动一次权值,则某次的net.e可能是这样一个矩阵:e=[1,2;3,4;5,6]。



通过matlab的截图可以看出损失函数net.L的计算方式完全正确。
可以知道





图五
图五中标注的net.fvd、net.od、net.e都是net.L对相应位置的偏导数,公式如下:



求得net.fvd之后为了实现误差的反向传播,还需要把net.fvd还原成图一中最后一层矩阵的形式:



这一部分是对下面代码的逆过程



通过net.layers{n}.d{j} = reshape(net.fvd(((j - 1) * fvnum + 1) : j * fvnum,:), sa(1), sa(2), sa(3));可以看出是吧net.fvd赋给了net.layers{n}.d{j}。这时候相当于知道了最后一层s层每个map中的每个像素点的偏导数,再看从第n层为s层偏导数的时候,如何求取n-1层C层的偏导数。返回第二部分的图四,可以看出s层某个像素点的值是C中对应的一个大小为net.layers{n}.scale*net.layers{n}.scale的矩阵求取均值得到的。在图五中就是c(1)(1)*0.25+c(1)(2)*0.25+c(2)(1)*0.25+c(2)(2)*0.25=s(1),已经知道损失函数对s(1)的偏导数,所以很容易求出损失函数对c(1)(1)、c(1)(2)、c(2)(1)、c(2)(2)的偏导数。只需要让偏导数再乘以0.25即可,对应的代码如下:(expand(net.layers{l
+ 1}.d{j}, [net.layers{l +1}.scale net.layers{l + 1}.scale 1]) / net.layers{l + 1}.scale ^ 2,(expand(net.layers{l + 1}.d{j},[net.layers{l + 1}.scale net.layers{l + 1}.scale 1])是先把4*4的map扩展成8*8的map,最后除以net.layers{l
+ 1}.scale ^ 2=4,此部分代码较长,在cnnbp函数文件中的第26行代码。(expand(net.layers{l + 1}.d{j}, [net.layers{l + 1}.scale net.layers{l
+1}.scale 1]) / net.layers{l + 1}.scale ^ 2这部分算出来的是损失函数对c层输出后的结果求取的偏导数,那么损失函数对c层输入值求偏导数就是: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}.scalenet.layers{l + 1}.scale 1]) / net.layers{l + 1}.scale ^ 2);(cnnbp函数文件中的第26行代码),接下来就是一直到l层c层的偏导数net.layers{l}.d{j}如何求取l-1层s层的偏导数了。回顾卷积运算冲采样层的某个map表示为矩阵S,S(i,j)都对下一层的卷积层中某一个map用矩阵C表示的哪些位置有影响,观察卷积运算可以发现(常数下面都用小写的c代表,k代表n*n的卷积核)
S(I,j)k(n,n)+c=C(i-n+1,j-n+1)
S(I,j)k(n,n-1)+c=C(i-n+1,j-(n-1)+1)
.
.
.
S(I,j)k(n,1)+c=C(i-n+1,j-1+1)
.
.
.
S(I,j)k(1,1)+c=C(i-1+1,j-1+1)
记C’为损失函数对卷积层求取偏导数,所以损失函数对S(I,j)求取偏导数为:



前面之所以加上求和公式,是因为前面的列出的只是S(i,j)对下一层的卷积层中某一个map用矩阵C表示的哪些位置有影响,所以要把S(i,j)对下一层的卷积层中所有的map的影响都加进去。看函数中的代码:



第一层的for循环相当于求和公式,里面的convn(net.layers{l + 1}.d{j},rot180(net.layers{l + 1}.k{i}{j}), 'full');其实就是C(i-n+1,j-n+1)* k(n,n)+
C(i-n+1,j-(n-1)+1)*k(n,n-1)+⋯+C(i-n+1,j-1+1)* k(n,1)+⋯+C(i-1+1,j-1+1)*
k(1,1)这一部分。到这里整个网络的误差如何从输出层传到最后一层,又如何从采样层反向传播给卷积层,从卷积层反向传播到采样层。
最后一步,看这几行代码,前面讲的那么多全部是损失函数对每层中每个map的每个节点求偏导数,而我们实际改变的是卷积核的值。既然求出了损失函数对每层中每个map的每个节点的偏导数,那么损失函数对每层中卷积核的偏导数也就近了。



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);这一步就不解释了吧,只要是看过bp算法的基本上都知道在求得了损失函数对所有节点的偏导数之后怎么求取对权值的偏导数了,在简单的bp中只不过是简单的乘法运算,在这里是卷积运算而已。

四、修正权值
到了修正权值其实也就到了结尾了cnnapplygrads函数也仅仅是在调节权值和阈值而已,代码如下:



附件:
有的人可能没有这工具箱,为了方便大家解读,把工具箱的这些函数的代码也附带上吧。
测试文件的代码:
clear all;close all; clc;
load('data1.2.mat')
t_y =t_y(1:3,:);
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= 0.5;
opts.batchsize= 8;
opts.numepochs= 100;

cnn =cnnsetup(cnn, t_x, t_y);
cnn =cnntrain(cnn, t_x, t_y, opts);

[er, bad] =cnntest(cnn, t_x, t_y);

Cnnsetup函数代码:里面的代码就是对卷积核、阈值、map的大小、scale等的初始化,代码比较简单,耐心看都能看懂。
functionnet = cnnsetup(net, x, y)
assert(~isOctave() ||compare_versions(OCTAVE_VERSION, '3.8.0', '>='), ['Octave 3.8.0 or greateris required for CNNs as there is a bug in convolution in previous versions. Seehttp://savannah.gnu.org/bugs/?39314.
Your version is ' myOctaveVersion]);
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 beinteger. Actual: ' num2str(mapsize)]);
for j = 1 : inputmaps
net.layers{l}.b{j} = 0;
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;

end
inputmaps =net.layers{l}.outputmaps;
end
end
% 'onum' is the number of labels, that'swhy it is calculated using size(y, 1). If you have 20 labels so the output ofthe network will be 20 neurons.
% 'fvnum' is the number of output neuronsat the last layer, the layer just before the output layer.
% 'ffb' is the biases of the outputneurons.
% 'ffW' is the weights between the lastlayer and the output neurons. Note that the last layer is fully connected tothe 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);
net.ffW = (rand(onum, fvnum) - 0.5) * 2 *sqrt(6 / (onum + fvnum));
end

cnntrain函数代码:
functionnet = cnntrain(net, x, y, opts)
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
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);
net = cnnbp(net, batch_y);
net = cnnapplygrads(net, opts);
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

cnnff函数代码:
functionnet = cnnff(net, x)
n = numel(net.layers);
net.layers{1}.a{1} = x;
inputmaps = 1;

for l = 2 : n % foreach 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 % foreach 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});
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, :);
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)));

end

cnnbp函数代码:
functionnet = 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

for l = (n - 1) : -1 : 1
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
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

cnnapplygrads函数代码:
functionnet = 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

cnntest函数代码:
function[er, bad] = cnntest(net, x, y)
% feedforward
net = cnnff(net, x);
[~, h] = max(net.o);
[~, a] = max(y);
bad = find(h ~= a);

er = numel(bad) / size(y, 2);
end
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: