卷积神经网络资料整理

卷积神经网络资料整理

感觉自己接受能力不是很强,从开始接触卷积神经网络到现在已经快一个半月了,总是每天晚上花上两三个小时来学习,现在才慢慢找到了一点点的感觉,想把网上自己看的资料进行整理一下:

学习卷积神经网络的基础知识是要知道一些简单的机器学习算法,什么是人工神经网络,神经网络的BP算法,会简单的使用Python或者MATLAB。

我自己的经历是学习了Coursera上的Andrew Ng的《机器学习》和《用Python玩转数据》,不要光看视频,把练习题也做完才有真正的体验和收获。学习卷积神经网络,特别推荐的是斯坦福大学的CS231n课程Convolutional Neural Networks for Visual Recognition,百度链接是:http://pan.baidu.com/s/1c1W6G72 密码:dwoi 。

下面正式的开始讨论卷积神经网络,建议的学习的代码是matlab DeepLearnToolbox https://github.com/rasmusbergpalm/DeepLearnToolbox

**

第一部分 转载网友的总结(非常好)

**
前言
  从理解卷积神经到实现它,前后花了一个月时间,现在也还有一些地方没有理解透彻,CNN还是有一定难度的,不是看哪个的博客和一两篇论文就明白了,主要还是靠自己去专研,阅读推荐列表在末尾的参考文献。目前实现的CNN在MINIT数据集上效果还不错,但是还有一些bug,因为最近比较忙,先把之前做的总结一下,以后再继续优化。

  卷积神经网络CNN是Deep Learning的一个重要算法,在很多应用上表现出卓越的效果,[1]中对比多重算法在文档字符识别的效果,结论是CNN优于其他所有的算法。CNN在手写体识别取得最好的效果,[2]将CNN应用在基于人脸的性别识别,效果也非常不错。前段时间我用BP神经网络对手机拍照图片的数字进行识别,效果还算不错,接近98%,但在汉字识别上表现不佳,于是想试试卷积神经网络。

1、CNN的整体网络结构
  卷积神经网络是在BP神经网络的改进,与BP类似,都采用了前向传播计算输出值,反向传播调整权重和偏置;CNN与标准的BP最大的不同是:CNN中相邻层之间的神经单元并不是全连接,而是部分连接,也就是某个神经单元的感知区域来自于上层的部分神经单元,而不是像BP那样与所有的神经单元相连接。CNN的有三个重要的思想架构:

  • 局部区域感知
  • 权重共享
  • 空间或时间上的采样
      局部区域感知能够发现数据的一些局部特征,比如图片上的一个角,一段弧,这些基本特征是构成动物视觉的基础[3];而BP中,所有的像素点是一堆混乱的点,相互之间的关系没有被挖掘。

      CNN中每一层的由多个map组成,每个map由多个神经单元组成,同一个map的所有神经单元共用一个卷积核(即权重),卷积核往往代表一个特征,比如某个卷积和代表一段弧,那么把这个卷积核在整个图片上滚一下,卷积值较大的区域就很有可能是一段弧。注意卷积核其实就是权重,我们并不需要单独去计算一个卷积,而是一个固定大小的权重矩阵去图像上匹配时,这个操作与卷积类似,因此我们称为卷积神经网络,实际上,BP也可以看做一种特殊的卷积神经网络,只是这个卷积核就是某层的所有权重,即感知区域是整个图像。权重共享策略减少了需要训练的参数,使得训练出来的模型的泛华能力更强。

      采样的目的主要是混淆特征的具体位置,因为某个特征找出来后,它的具体位置已经不重要了,我们只需要这个特征与其他的相对位置,比如一个“8”,当我们得到了上面一个”o”时,我们不需要知道它在图像的具体位置,只需要知道它下面又是一个“o”我们就可以知道是一个’8’了,因为图片中”8”在图片中偏左或者偏右都不影响我们认识它,这种混淆具体位置的策略能对变形和扭曲的图片进行识别。

      CNN的这三个特点是其对输入数据在空间(主要针对图像数据)上和时间(主要针对时间序列数据,参考TDNN)上的扭曲有很强的鲁棒性。CNN一般采用卷积层与采样层交替设置,即一层卷积层接一层采样层,采样层后接一层卷积…这样卷积层提取出特征,再进行组合形成更抽象的特征,最后形成对图片对象的描述特征,CNN后面还可以跟全连接层,全连接层跟BP一样。下面是一个卷积神经网络的示例:

这里写图片描述

图1(图片来源)

  卷积神经网络的基本思想是这样,但具体实现有多重版本,我参考了matlab的Deep Learning的工具箱DeepLearnToolbox,这里实现的CNN与其他最大的差别是采样层没有权重和偏置,仅仅只对卷积层进行一个采样过程,这个工具箱的测试数据集是MINIST,每张图像是28*28大小,它实现的是下面这样一个CNN:

这里写图片描述

图2

2、网络初始化
  CNN的初始化主要是初始化卷积层和输出层的卷积核(权重)和偏置,DeepLearnToolbox里面对卷积核和权重进行随机初始化,而对偏置进行全0初始化。

3、前向传输计算
  前向计算时,输入层、卷积层、采样层、输出层的计算方式不相同。

  3.1 输入层:输入层没有输入值,只有一个输出向量,这个向量的大小就是图片的大小,即一个28*28矩阵;

  3.2 卷积层:卷积层的输入要么来源于输入层,要么来源于采样层,如上图红色部分。卷积层的每一个map都有一个大小相同的卷积核,Toolbox里面是5*5的卷积核。下面是一个示例,为了简单起见,卷积核大小为2*2,上一层的特征map大小为4*4,用这个卷积在图片上滚一遍,得到一个一个(4-2+1)(4-2+1)=3*3的特征map,卷积核每次移动一步,因此。在Toolbox的实现中,卷积层的一个map与上层的所有map都关联,如上图的S2和C3,即C3共有6*12个卷积核,卷积层的每一个特征map是不同的卷积核在前一层所有map上作卷积并将对应元素累加后加一个偏置,再求sigmod得到的。还有需要注意的是,卷积层的map个数是在网络初始化指定的,而卷积层的map的大小是由卷积核和上一层输入map的大小决定的,假设上一层的map大小是n*n、卷积核的大小是k*k,则该层的map大小是(n-k+1)(n-k+1),比如上图的24*24的map大小24=(28-5+1)。 斯坦福的深度学习教程更加详细的介绍了卷积特征提取的计算过程。
这里写图片描述
  图3

  3.3 采样层(subsampling,Pooling):采样层是对上一层map的一个采样处理,这里的采样方式是对上一层map的相邻小区域进行聚合统计,区域大小为scale*scale,有些实现是取小区域的最大值,而ToolBox里面的实现是采用2*2小区域的均值。注意,卷积的计算窗口是有重叠的,而采用的计算窗口没有重叠,ToolBox里面计算采样也是用卷积(conv2(A,K,’valid’))来实现的,卷积核是2*2,每个元素都是1/4,去掉计算得到的卷积结果中有重叠的部分,即:
这里写图片描述
图4

4、反向传输调整权重
  反向传输过程是CNN最复杂的地方,虽然从宏观上来看基本思想跟BP一样,都是通过最小化残差来调整权重和偏置,但CNN的网络结构并不像BP那样单一,对不同的结构处理方式不一样,而且因为权重共享,使得计算残差变得很困难,很多论文[1][5]和文章[4]都进行了详细的讲述,但我发现还是有一些细节没有讲明白,特别是采样层的残差计算,我会在这里详细讲述。

  4.1输出层的残差

  和BP一样,CNN的输出层的残差与中间层的残差计算方式不同,输出层的残差是输出值与类标值得误差值,而中间各层的残差来源于下一层的残差的加权和。输出层的残差计算如下:

这里写图片描述

公式来源

  这个公式不做解释,可以查看公式来源,看斯坦福的深度学习教程的解释。

  4.2 下一层为采样层(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

  4.3 下一层为卷积层(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

  这里我们采用自己实现的convn(即内部不会对卷积核旋转),并假定上面的矩阵A、B下标都从1开始,那么有:

B11 = A11*K11 + A12*K12 + A13*K13 + A21*K21 + A22*K22 + A23*K23 + A31*K31 + A32*K32 + A33*K33
B12 = A12*K11 + A13*K12 + A14*K13 + A22*K21 + A23*K22 + A24*K23 + A32*K31 + A33*K32 + A34*K33
B13 = A13*K11 + A14*K12 + A15*K13 + A23*K21 + A24*K22 + A25*K23 + A33*K31 + A34*K32 + A35*K33
B21 = A21*K11 + A22*K12 + A23*K13 + A31*K21 + A32*K22 + A33*K23 + A41*K31 + A42*K32 + A43*K33
B22 = A22*K11 + A23*K12 + A24*K13 + A32*K21 + A33*K22 + A34*K23 + A42*K31 + A43*K32 + A44*K33
B23 = A23*K11 + A24*K12 + A25*K13 + A33*K21 + A34*K22 + A35*K23 + A43*K31 + A44*K32 + A45*K33
B31 = A31*K11 + A32*K12 + A33*K13 + A41*K21 + A42*K22 + A43*K23 + A51*K31 + A52*K32 + A53*K33
B32 = A32*K11 + A33*K12 + A34*K13 + A42*K21 + A43*K22 + A44*K23 + A52*K31 + A53*K32 + A54*K33
B33 = A33*K11 + A34*K12 + A35*K13 + A43*K21 + A44*K22 + A45*K23 + A53*K31 + A54*K32 + A55*K33

复制代码
  我们可以得到B矩阵每个单元与哪些卷积核单元和哪些A矩阵的单元之间有关联:

A11 [K11] [B11]
A12 [K12, K11] [B12, B11]
A13 [K13, K12, K11] [B12, B13, B11]
A14 [K13, K12] [B12, B13]
A15 [K13] [B13]
A21 [K21, K11] [B21, B11]
A22 [K22, K21, K12, K11] [B12, B22, B21, B11]
A23 [K23, K22, K21, K13, K12, K11] [B23, B22, B21, B12, B13, B11]
A24 [K23, K22, K13, K12] [B23, B12, B13, B22]
A25 [K23, K13] [B23, B13]
A31 [K31, K21, K11] [B31, B21, B11]
A32 [K32, K31, K22, K21, K12, K11] [B31, B32, B22, B12, B21, B11]
A33 [K33, K32, K31, K23, K22, K21, K13, K12, K11] [B23, B22, B21, B31, B12, B13, B11, B33, B32]
A34 [K33, K32, K23, K22, K13, K12] [B23, B22, B32, B33, B12, B13]
A35 [K33, K23, K13] [B23, B13, B33]
A41 [K31, K21] [B31, B21]
A42 [K32, K31, K22, K21] [B32, B22, B21, B31]
A43 [K33, K32, K31, K23, K22, K21] [B31, B23, B22, B32, B33, B21]
A44 [K33, K32, K23, K22] [B23, B22, B32, B33]
A45 [K33, K23] [B23, B33]
A51 [K31] [B31]
A52 [K32, K31] [B31, B32]
A53 [K33, K32, K31] [B31, B32, B33]
A54 [K33, K32] [B32, B33]
A55 [K33] [B33]

  然后再用matlab的convn(内部会对卷积核进行180度旋转)进行一次convn(B,K,’full’),结合图7,看红色部分,除去0,A11=B’33*K’33=B11*K11,发现A11正好与K11、B11关联对不对;我们再看一个A24=B’34*K’21+B’35*K’22+B’44*K’31+B’45*K’32=B12*K23+B13*K22+B22*K13+B23*K12,发现参与A24计算的卷积核单元与B矩阵单元,正好是前向计算时关联的单元,所以我们可以通过旋转卷积核后进行卷积而得到采样层的残差。

  残差计算出来后,剩下的就是用更新权重和偏置,这和BP是一样的,因此不再细究,有问题欢迎交流。

5、参考文献
[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

转载请注明出处:http://www.cnblogs.com/fengfenggirl

第二部分 MATLAB DeepLearnToolbox代码解释

建议用MATLAB调试的状态来查看数据流的过程。

test_example_CNN.m文件

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

clear all; close all; clc;
% addpath(‘../data’);
addpath(‘../util’);
% addpath(‘../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 = 1; %迭代次数

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’);
%show test error
disp([num2str(er*100) ‘% error’]);

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

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 ’ myOctaveVersion]);
% function net = cnnsetup(net, x, y)
inputmaps = 1;
% B=squeeze(A) 返回和矩阵A相同元素但所有单一维都移除的矩阵B,单一维是满足size(A,dim)=1的维。
% train_x中图像的存放方式是三维的reshape(train_x’,28,28,60000),前面两维表示图像的行与列,
% 第三维就表示有多少个图像。这样squeeze(x(:, :, 1))就相当于取第一个图像样本后,再把第三维
% 移除,就变成了28x28的矩阵,也就是得到一幅图像,再size一下就得到了训练样本图像的行数与列数了
mapsize = size(squeeze(x(:, :, 1)));

% 下面通过传入net这个结构体来逐层构建CNN网络
% n = numel(A)返回数组A中元素个数
% net.layers中有五个struct类型的元素,实际上就表示CNN共有五层,这里范围的是5
for l = 1 : numel(net.layers)   %  layer
    if strcmp(net.layers{l}.type, 's') % 如果这层是 子采样层
        % subsampling层的mapsize,最开始mapsize是每张图的大小28*28
        % 这里除以scale=2,就是pooling之后图的大小,pooling域之间没有重叠,所以pooling后的图像为14*14
        % 注意这里的右边的mapsize保存的都是上一层每张特征map的大小,它会随着循环进行不断更新
        mapsize = floor(mapsize / net.layers{l}.scale); %floor,顾名思义,就是地板,所以是取比它小的整数,即朝负无穷方向取整,如floor(-1.3)=-2; floor(1.3)=1;floor(-1.8)=-2,floor(1.8)=1
        for j = 1 : inputmaps % inputmap就是上一层有多少张特征图
            net.layers{l}.b{j} = 0; % 将偏置初始化为0
        end
    end
    if strcmp(net.layers{l}.type, 'c') % 如果这层是 卷积层
        % 旧的mapsize保存的是上一层的特征map的大小,那么如果卷积核的移动步长是1,那用
        % kernelsize*kernelsize大小的卷积核卷积上一层的特征map后,得到的新的map的大小就是下面这样
        mapsize = mapsize - net.layers{l}.kernelsize + 1;
        % 该层需要学习的参数个数。每张特征map是一个(后层特征图数量)*(用来卷积的patch图的大小)
        % 因为是通过用一个核窗口在上一个特征map层中移动(核窗口每次移动1个像素),遍历上一个特征map
        % 层的每个神经元。核窗口由kernelsize*kernelsize个元素组成,每个元素是一个独立的权值,所以
        % 就有kernelsize*kernelsize个需要学习的权值,再加一个偏置值。另外,由于是权值共享,也就是
        % 说同一个特征map层是用同一个具有相同权值元素的kernelsize*kernelsize的核窗口去感受输入上一
        % 个特征map层的每个神经元得到的,所以同一个特征map,它的权值是一样的,共享的,权值只取决于
        % 核窗口。然后,不同的特征map提取输入上一个特征map层不同的特征,所以采用的核窗口不一样,也
        % 就是权值不一样,所以outputmaps个特征map就有(kernelsize*kernelsize+1)* outputmaps那么多的权值了
        % 但这里fan_out只保存卷积核的权值W,偏置b在下面独立保存
        fan_out = net.layers{l}.outputmaps * net.layers{l}.kernelsize ^ 2;
        for j = 1 : net.layers{l}.outputmaps  %  output map
            % fan_out保存的是对于上一层的一张特征map,我在这一层需要对这一张特征map提取outputmaps种特征,
            % 提取每种特征用到的卷积核不同,所以fan_out保存的是这一层输出新的特征需要学习的参数个数
            % 而,fan_in保存的是,我在这一层,要连接到上一层中所有的特征map,然后用fan_out保存的提取特征
            % 的权值来提取他们的特征。也即是对于每一个当前层特征图,有多少个参数链到前层
            fan_in = inputmaps * net.layers{l}.kernelsize ^ 2;
            for i = 1 : inputmaps  %  input map
                % 随机初始化权值,也就是共有outputmaps个卷积核,对上层的每个特征map,都需要用这么多个卷积核
                % 去卷积提取特征。
                % rand(n)是产生n×n的 0-1之间均匀取值的数值的矩阵,再减去0.5就相当于产生-0.5到0.5之间的随机数
                % 再 *2 就放大到 [-1, 1]。然后再乘以后面那一数,why?
                % 反正就是将卷积核每个元素初始化为[-sqrt(6 / (fan_in + fan_out)), sqrt(6 / (fan_in + fan_out))]
                % 之间的随机数。因为这里是权值共享的,也就是对于一张特征map,所有感受野位置的卷积核都是一样的
                % 所以只需要保存的是 inputmaps * outputmaps 个卷积核。
                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
        % 只有在卷积层的时候才会改变特征map的个数,pooling的时候不会改变个数。这层输出的特征map个数就是
        % 输入到下一层的特征map个数
        inputmaps = net.layers{l}.outputmaps; 
    end
end

% fvnum 是输出层的前面一层的神经元个数。
% 这一层的上一层是经过pooling后的层,包含有inputmaps个特征map。每个特征map的大小是mapsize。
% 所以,该层的神经元个数是 inputmaps * (每个特征map的大小)
% prod: Product of elements.
% For vectors, prod(X) is the product of the elements of X
% 在这里 mapsize = [特征map的行数 特征map的列数],所以prod后就是 特征map的行*列
fvnum = prod(mapsize) * inputmaps;
% onum 是标签的个数,也就是输出层神经元的个数。你要分多少个类,自然就有多少个输出神经元
onum = size(y, 1);

% 这里是最后一层神经网络的设定
% ffb 是输出层每个神经元对应的基biases
net.ffb = zeros(onum, 1);
% ffW 输出层前一层 与 输出层 连接的权值,这两层之间是全连接的
net.ffW = (rand(onum, fvnum) - 0.5) * 2 * sqrt(6 / (onum + fvnum));

end

cnnff.m文件
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

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
% 这里可以参考 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

cnntest.m文件
function [er, bad] = cnntest(net, x, y)
% feedforward
net = cnnff(net, x); % 前向传播得到输出
% [Y,I] = max(X) returns the indices of the maximum values in vector I
[~, h] = max(net.o); % 找到最大的输出对应的标签
[~, a] = max(y); % 找到最大的期望输出对应的索引
bad = find(h ~= a); % 找到他们不相同的个数,也就是错误的次数

er = numel(bad) / size(y, 2); % 计算错误率

end

第三部分 自己理解卷积神经网络的过程

由于我是学机械专业的,所以以前没有接触过“卷积”这个数学概念,加上有一定的抽象成分,开始看了很多的帖子都不是很了解。如果从物理或者数学上无法理解,至少我们需要知道这个卷积过程是怎么样发生的,下图从斯坦福大学的CS231n课程上的截图,希望能够有助于理解:
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

我下一步的练习目标是利用cifar-10训练这个MATLAB代码,并且进行参数修改,提供准确率。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值