clear all; close all; clc;
addpath('../data');
addpath('../util');
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');
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
};
cnn = cnnsetup(cnn, train_x, train_y);
opts.alpha = 1;
opts.batchsize = 50;
opts.numepochs = 10;
cnn = cnntrain(cnn, train_x, train_y, opts);
[er, bad] = cnntest(cnn, test_x, test_y);
plot(cnn.rL);
disp([num2str(er*100) '% error']);
上面是执行整个卷积神经网络的demo起点,主要注意cnn.layers的设置,在创建卷积神经网络时,需要根据相关的参数进行一些计算。然后是opts结构体的一些参数,alpha记录权值更新的状态,而numepochs表示进行十次迭代,battchsize表示每一批训练的数据包含50个。具体参数的作用可以参照http://www.math.duke.edu/~jvb/papers/cnn_tutorial.pdf文档的解释。
function net = cnnsetup(net, x, y)
inputmaps = 1;
mapsize = size(squeeze(x(:, :, 1)));
for l = 1 : numel(net.layers)
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;
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
net.layers{l}.k{i}{j} = (rand(net.layers{l}.kernelsize) - 0.5) * 2 * sqrt(6 / (fan_in + fan_out));%init the weight of each map connection
end
net.layers{l}.b{j} = 0;
end
inputmaps = net.layers{l}.outputmaps;
end
end
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
inputmap用于计数输入map。对于输入层inputmap等于1,mapsize存放的则是输入的映射的大小,输入层的输入就是训练数据x的尺寸大小。根据层次是属于卷基层还是下采样层进行参数的计算。如果属于下采样层,则需要根据上一层的输入除以本层的scale就可以得到本层输出映射的大小,至于输出映射的个数就等于上一层卷积输入的映射数目。另外还需要根据上一层输入的映射设置偏移b等于0。对于卷积层则不相同,首先卷基层的输出映射是人为定义的,其次卷积层除掉偏移之外还需要一个卷积权重,这个权重不仅依赖与输出映射,还依赖于输入映射。首先计算的是mapsize的大小,根据输出层大小以及卷积核的大小和卷积平移的步长来计算的,这里平移步长为1。首先计算fan_out和fan_in用作归一化处理。下面的双重循环就是计算权重,每一个输入映射都对应outputmaps个输出映射,至于每一个输出组的偏移是可以共享的。最后计算的是输出层的权重和偏移。最后输出层将最后得到的输出映射以及映射大小转化为一个向量,向量的大小为fvnum。然后根据输入的标签得到最终所需要的标签特征的向量大小为onum。因此最后的偏移是onum个,而w的权值是onum*fvnum。因为卷积神经网络本质上是一个分类器,所以onum实际代表的是类标签的数目,而fvnum则表示根据卷积神经网络得到的一个x的特征,经过分类处理将x判断为onum个标签中的某一个。
function net = 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
总体的训练过程没什么可说的,主要是根据传递进来的数据随机生成批次训练的数据,然后调用下一层次的前向传播和反向传播等等操作。最后 根据新得到的损失值来更新原来的损失值。
function net = cnnff(net, x)
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')
for j = 1 : net.layers{l}.outputmaps % for each 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
z = z + convn(net.layers{l - 1}.a{i}, net.layers{l}.k{i}{j}, 'valid');
end
net.layers{l}.a{j} = sigm(z + net.layers{l}.b{j});
end
inputmaps = net.layers{l}.outputmaps;
elseif strcmp(net.layers{l}.type, 's')
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
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
net.o = sigm(net.ffW * net.fv + repmat(net.ffb, 1, size(net.fv, 2)));
end
首先函数根据传递进来的参数得到层次的数目,然后将输入的训练样本整个传递给输入层,设置初始的输入映射为1,然后根据所属的层训练样本。在卷积层中,根据上一层的输出得到本层的输入,然后根据本层卷积数目以及步长计算本层的输出的大小Z,并将z初始化为0.这里需要注意的是z为三维矩阵,所以在下面的convn卷积函数中可以一次性处理一批50个数据,并且由于参数为valid,所以得到的矩阵为size(a)-size(k)+1。最后根据激活函数计算本层的输出。如果是在下采样层,则直接进行卷积,然后根据结果进行下采样,不过这里的convn最后一个参数应该是same。输出层则直接将参数转化为以为数组,最后sa(3)实际上是一批训练样本的数目。最后根据激活函数计算输出的类别,net.o的大小为(onum*sa(3))。
function net = cnnbp(net, y)
n = numel(net.layers);
net.e = net.o - y;
net.L = 1/2* sum(net.e(:) .^ 2) / size(net.e, 2);
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
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
根据已有的参数计算出误差net.e,并且统计得到最小的误差——也就是损失函数net.L。那么根据求导法则不难求出net.od,其中维1/2(X^2)外加sigmoid函数的求导公式就可以推导出来。注意这里是对偏移b求导,所以得到的是传播误差,也就是note中所说的delta。然后根据note上面的传播公式,可以得到输出层的上一层的传播函数,当上一层是卷积层则需要进行传播。传播公式为note里面的公式4。公式4的数学推导过程如下:
delta E/u
-------- = --------
delta1 E/u1
根据上述公式进行数学推导就可以得到note中的公式4.
在第一个循环中将输出层的灵敏度也就是delta进行分解,并往上层传递得到输出层上一层的输出的delta。假设第l-1层是卷积层,则l层为下采样层,所以对delta的传递需要分为两次处理:首先,将下层的delta进行一个上采样,这个在expand函数里面执行;第二,根据delta传递公式往上层传递delta值,其中对本层的求导公式就是最前面的那一部分。如果l-1层是下采样曾,则其l层为卷积层。正好卷积层的输入等于下采样层的输出;所以传递过程仅仅是卷积层的互相关操作就可以了。由于自相关是一个卷积过程,所以需要有一个累加。到这里BP操作基本完成,从整个过程中可以看到,误差会从输出层一直传递到上层的输入层,这就是深度神经网络相对于原来的神经网络的优势,误差不会在BP过程中丢失。
在计算得到误差delta之后需要更新参数,由于在下采样层没有参数,所以下采样曾不需要计算参数,而仅仅只需要计算卷积层的参数。根据计算梯度的公式可以得到卷积核和偏移的求导方式。最后更新输出层权值和偏移的数值。
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
最后一个函数就是权值的更新,通过上面的分析,没什么难度。上面就是整个CNN卷积神经网络的训练过程。