Matlab:LeNet
LeNet是一个经典的卷积神经网络,其中结构的主要的关键部分是卷积层(Convolutional Layer)、池化层(Pooling Layer)、激活函数(Activation Function)、全连层(Full Connection Layer)、输出层(output layer)。
本博文实现的是一个简单的结构,主要包含:一个卷积层,一个池化层,一个全连接层以及以1个输出层。
卷积核
卷积层的关键是卷积核,简单的基本实现就是随机的生成。
Matlab 代码如下:
init_kernel.m:
%======================
% 卷积核的初始化
% 卷积核大小为:[K_m,K_n,K_num]
%======================
function [kernel] = init_kernel(K_m,K_n,K_num)
%基本的均匀分布随机数
kernel = rand(K_m,K_n,K_num);
end
激活函数
激活函数分为很多种,经典LeNet中采用的是sigmoid函数:
其图像为:
AF.m:
%======================
% 激活函数
% 输入为上一层神经元的输出x
%======================
function AF_res = AF(x)
%sigmoid 函数
y = 1./(1+exp(-x);
end
池化层
池化是一种下采样的过程,只要的作用是降低特征的数量,只要的方法有最大池化和平均池化。
pooling.m:
%======================
% 池化层 pooling layer
% 输入为特征图数据data、池化模板pool、池化方式option
%option :'maxpooling',最大池化方式;
% 'averagepooling',平均池化方式。
%======================
function op_PL = pooling(data,POOL,option)
[D_m,D_n,K_num] = size(data);%获取输入数据的尺寸
[PL_m,PL_n,PL_z] = size(POOL);%获取池子的尺寸
%选择平均池化方式
if strcmp(option,'averagepooling')
%忽略了边缘,可以选择padding
for m = 1 : D_m/PL_m
for n =1: D_n/PL_n
block = data((m-1)*PL_m+1:m*PL_m,(n-1)*PL_n+1:n*PL_n,:) .* repmat(POOL,1,1,K_num);
op_PL(m,n,:) = sum(sum(block))/(PL_m*PL_n);
end
end
end
end
卷积层
卷积层的作用是提取特征,利用卷积核和数据集做卷积。
COV.m:
%======================
% 卷积层 convolutional layer
% 输入为特征图数据train_data、卷积核w_COV(kernel)
% 输出结果:op_conv
%======================
function op_COV = COV(train_data,kernel,b_COV)
[K_m,K_n,K_num] = size(kernel);%获取卷积核的尺寸
[D_m,D_n] = size(train_data);%获取输入数据的尺寸
train_data=repmat(train_data,1,1,K_num);
for i=1:(D_m - K_m + 1)
for j=1:(D_n - K_n + 1)
block = train_data(i:i+K_m-1,j:j+K_n-1,:).* kernel;
op_COV(i,j,:) = sum(sum(block));
end
end
op_COV= AF(op_COV+repmat(reshape(b_COV,1,1,[]),size(op_COV(:,:,1))));
end
全连接层
全连接层直接连接池化层,该层将前一层的输出转化为一个特征向量,输入具有分类器作用的输出层进行分类。
FC.m:
%======================
% 全连接层 (集成了激活函数在内)
% 输入为上一层的输出,输出为一个一维列向量
% 输出参量说明:ip_FC:全连接层神经元的输入(wx+b)
% op_FC:全连接层的输出(y=f(ip_FC))
%======================
function [op_FC,ip_FC]= FC(op_PL,w_FC,b_FC)
%内积的实现方式
K_num=size(op_PL,3);
n_FCxKnum=size(w_FC,3);
n_FC = n_FCxKnum/K_num;
for i = 1:n_FC%100
block = op_PL .* w_FC(:,:,(i-1)*K_num+1:i*K_num);
op_FC(i,1)=sum(sum(sum(block)));
end
ip_FC = op_FC+b_FC;
op_FC = AF(ip_FC);
%可以考虑卷积的实现方式
end
参数更新
对于模型的参数,主要为卷积核的参数、全连接层的参数、池化层的参数及其相关部分对应的偏置参数(并不是所有的都有偏置)。比照基本的神经网络参数,上述的参数都可以看成是基本神经网络中的参数 (W,b) 。
针对该模型构建相应的损失函数后,对损失函数的极小化就变成了一个无约束的优化问题。可以使用梯度下降法来进行参数的更新的基本范式为:
假设所定义的损失函数为 E(W,b) 引入梯度后可以得到更新方法为:
反向传播
基本的模型搭建完成后的,训练的时候所做的就是完成模型参数的更新。由于存在多层的网络结构,因此无法直接对中间的隐层利用损失来进行参数更新。卷积神经网络也是神经网络的一种,当然也可以使用传统过的反向传播算法来进行训练。 可以利用损失从顶层到底层的反向传播来进行参数的估计。(约定:小写字母—标量,加粗小写字母—向量,大写字母—矩阵)
假设在模型中:
输入样本为
x
,其标签为
t
;
对于层
l
,用
其中 f(⋅) 为激活函数, b(j)l 为第 l 层第
对于网络的最后一层第
k
层——输出层,现在定义损失函数:
为了极小化损失函数,通过梯度下降来进行推导:
在上式子中,根据之前的定义,很容易得到:
那么则有:
另有,下一层所有结点的输入都与前一层的每个结点输出有关,因此损失函数可以认为是下一层的每个神经元结点输入的函数。那么:
此处定义节点的灵敏度为误差对输入的变化率,即:
那么第 l 层第
结合灵敏度的定义,则有:
上式两边同时乘上 f′(u(j)l) ,则有
注意到上式中表达的是前后两层的灵敏度关系,而对于最后一层,也就是输出层来说,并不存在后续的一层,因此并不满足上式。但输出层的输出是直接和误差联系的,因此可以用损失函数的定义来直接求取偏导数。那么:
至此,损失函数对各参数的梯度为:
上述的推到都是建立在单个节点的基础上,对于各层所有节点,采用矩阵的方式表示,则上述公式可以写成:
其中运算符 ∘ 表示矩阵或者向量中的对应元素相乘。
常见的几个激活函数的导数为:
根据上述公式,可以得到各层参数的更新公式为:
CNN反向传播中的特殊问题
1)经典的池化层中并没有激活函数
针对这种没有激活函数的神经元节点,可以令其激活函数为
f(x)=x
,即激活函数的输出值等于输入值,此时激活函数的导数恒为1。
2)池化层在前向传播的过程中,进行的一定的数据压缩,那么在进行反向传播的过程中与经典的反向传播算法有些许区别。
在池化的抽象描述过程中,上一层与池化层并不存在连接。但我们可以假设这种连接是存在的,只是对应的参数权重是不会更新的,且这种连接根据选择的池化方法不同也会不同。这种连接我称之为虚拟连接。
- 对于average池化,可以假设存在这样的连接:上层中的每个池化区域对应一个池化层神经元单元且该区域内的神经元都只和该池化层中的神经元单元相连,且每条连结的权重都是为 1PoolingSize2 (池化区域连接权重和为1)。
- 对于max池化,可以假设存在这样的连接:上层中的每个池化区域对应一个池化层神经元单元,该区域内只有神经元输出最大的神经元才和对应池化层中的神经元单元相连且这条连结的权重都是为1(即每个池化区域只有一条权重为1的连接或者说所有连接中只有一条存在非0权重而其他连接权重为0,区域内所有连接权重和为1)。 值得注意的是,这条连接并不是固定的,随着训练的进行其连接的对象会发生变化,也就是说网络拓扑是在发生变化的,和dropout是否有相似的作用? 。
由反向传播的中参数更新公式可以得知池化层的误差灵敏度:
基于之前所述的 虚拟连接,在做反向传播时就需要将 δl 的还原为池化前的大小。如果池化方法是MAX,则将 δl 中所属各个池化区域的值放在前向传播算算法得到最大值的位置;如果是Average池化,则将 δl 的所属各个池化区域的值取平均后放在还原后的池化区域中。上述过程称之为上采样(upsampling)。 该上采样过程和基于虚拟连接的传统bp参数更新过程是否一致?有待讨论
例如:假设一个池化层中的神经元的误差敏感度为 δkl=1 ,其对应的池化区域大小为 2×2 ,经过上采样后的结果为:
CNN_updateweight.m:
%=========参数更新=============
%
% 输入参数说明:
% eta:学习率
% class:当前样本所属的类别
% class_num:类别总数目 10
% op_OP:模型对当前样本的输出,列向量[10,1]
% w_OP:softmax层,即输出层的权重[10,100]
% op_FC:全连接层的输出,列向量[100,1]
% ip_FC:全连接层的神经元的输入(wx+b)
% w_FC:全连接层连接权重[12,12,20x100]
% b_FC:全连接层偏置权重[100,1]
% op_PL:池化层输出[12,12,20]
% PL_m:池化尺寸1
% PL_n:池化尺寸2
% op_COV:卷积层的输出(本实验中未使用)
% ip_COV:卷积操作结果
% w_COV:卷积层的连接权重,即卷积核参数
% b_COV:卷积层的偏置权重
% op_ORG:原始图像的输出
%
%输出参数说明:
% w_OP:输出层连接权重
% b_OP:输出层偏置权重(本处未设置)
% w_FC:全连接层连接权重
% b_FC:全连接层偏置权重
% w_COV:全连接层连接权重
% b_COV:全连接层偏置权重
%======================
function [w_OP,w_FC,b_FC,w_COV,b_COV]=CNN_updateweight(eta,class,class_num,op_OP,w_OP,...
op_FC,w_FC,b_FC,op_PL,PL_m,PL_n,op_COV,w_COV,b_COV,op_ORG)
% 完成参数更新,权值和卷积核
%% 将输入进来的参数转化为适合计算的列向量
w_FC_temp=w_FC;
b_FC_temp=b_FC;
w_OP_temp=w_OP;
w_COV_temp=w_COV;
b_COV_temp=b_COV;
%% 更新输出层参数w_OP,此处输出层没有添加偏置参数b_OP
% Error计算
label=zeros(class_num,1);
label(class+1,1)=1;%期望的输出结果[10,1]
% ip_OP=w_OP*op_FC;%输出层的输入[10,1]=[10,100][100,1]
delta_OP=(op_OP-label) .* (op_OP - op_OP.^2);%输出层的灵敏度[10,1]
delta_w_OP=delta_OP*op_FC';%输出连接权重的变化量[10,100]=[10,1][100,1]’
w_OP_new=w_OP_temp-eta*delta_w_OP;%输出连接权重更新[10,100]
%% 更新全连接层参数b_FC和w_FC
delta_FC = (w_OP'*delta_OP) .* (op_FC .*(1 - op_FC));%全连接层的灵敏度[100,1]=([100,10][10,1])
%delta_w_FC = delta_FC * op_PL';%全连接层的变化量[12,12,20x100]=[100,1][12,12,20]
K_num = size(op_PL,3);
n_FC=(size(w_FC,3)/K_num);
for i=1:n_FC%100
delta_w_FC(:,:,(i-1)*K_num+1:i*K_num)=delta_FC(i,1) * op_PL;%[12 12 20x100]=[1][12 12]
end
delta_b_FC = delta_FC;%[100 1]
w_FC_new=w_FC_temp-eta*delta_w_FC;%全连接层连接权重更新
b_FC_new=b_FC_temp-eta*delta_b_FC;%全连接层偏置权重更新
%% 更新池化层的参数(本实验中没有)
%delta_PL = (w_FC'*delta_FC).*(1);%本实验中池化层的激活函数的导数为1,没有偏置项
%[12,12,20] = [12,12,20x100]’[100,1]
delta_PL =0;
for m=1:size(delta_FC,1)
delta_PL=delta_PL+w_FC(:,:,(m-1)*20+1:m*20)*delta_FC(m,1);
end
%% 更新卷积层参数(W,b)
%%需要对池化层的误差敏感度进行上采样
%delta_conv = upsample(delta_PL).* (ip_COV .* (1- ip_COV))(此处采用average池化)
for i=1:size(delta_PL,3)%20
delta_COV(:,:,i) = kron(delta_PL(:,:,i),ones(PL_m,PL_n)/(PL_m*PL_n)) .* (op_COV(:,:,i).*(1- op_COV(:,:,i));%[24 24 20]=[24 24 20].* [24 24 20]
delta_w_COV(:,:,i) = rot90(conv2(op_ORG,rot90(delta_COV(:,:,i),2),'valid'),2);%卷积核的变化量 [5 5 20]=cov([28 28] [24 24 20])
end
w_COV_new=w_COV_temp-eta*delta_w_COV;%卷积核更新 ;[5 5 20]
delta_b_COV = reshape(sum(sum(delta_COV)),[],1);%卷积层的偏置量变化量[20 1]
b_COV_new =b_COV_temp-eta*delta_b_COV;%卷积层偏置权重更新 ;
%% 网络权值更新
w_FC=w_FC_new;
b_FC=b_FC_new;
w_OP=w_OP_new;
w_COV=w_COV_new;
b_COV=b_COV_new;
end
训练
卷积神经网络的训练有SGD、BGD和min-batch GD。本处使用的是SGD的参数更新方法。
LeNet.m:
function LeNet()
%%% LetNet Matlab 简单实现 version_1
%%% author:吕爽
%%% data:2017.11.2
%===========卷积神经网络结构===========
% 输入层:输入数据data[D_m,D_n]
% 卷积层:卷积核kernal[K_m,K_n,K_num](w_COV),偏置b_COV,sigmoid激活函数
% 池化层:max池化,pool[p_m,p_n],无偏置,无激活函数
% 全连接层:偏置b_FC[n_FC,1],权值w_FC[ , ,n_FC x K_num]
% 输出层:softmax激活函数,class_num个种类,无偏置
%=======================================
%%---------------初始化参数---------------------
D_m=28;D_n=28;%数据集大小参数
%卷积
K_m=5;K_n=5;K_num = 20;%卷积核的相关参数设置(尺寸及个数)
b_COV=randn(K_num,1);%初始化卷积层偏置[20 1]
w_COV=init_kernel(K_m,K_n,K_num);%初始化卷积核[5 5 20]
%池化
PL_m=2;PL_n = 2;%池化模板大小
POOL=ones(PL_m,PL_n)/(PL_m*PL_n);%初始化池化模板(默认摸板为平均池化)
%全连接
n_FC = 100;%全连接层100个神经元
b_FC=randn(n_FC,1);%初始化全连接层的偏置[100 1]
w_FC=randn((D_m-K_m+1)/PL_m,(D_n-K_n+1)/PL_n,K_num * n_FC);%初始化全连接层的权重[12 12 20x100]
%类别
class_num = 10;%共10类,输出层的神经元个数
w_OP=randn(class_num,n_FC);%初始输出层的权重[10 100]
%学习速率
eta = 0.05;
%------------------训练--------------------------
X=strcat('开始训练...');disp(X);
for iter =1:100 %epco 训练循环迭代的次数
train_precious = 0;%准确率
train_T_cnt = 0;%正确分类
train_F_cnt = 0;%错误分类
for m = 0:1 %每类训练数据的数量
for n = 0:class_num-1 %训练样本的种类
X=strcat('Epco. ',num2str(iter),'== Image:',num2str(n),'_',num2str(m),'.bmp ');disp(X);
%读取数据
image_name =strcat('train_image\',num2str(n),'_',num2str(m),'.bmp');
train_data = double(imread(image_name));
%预处理
train_data = pre_process(train_data);
%前向传播
%卷积层
op_COV = COV(train_data,w_COV,b_COV);%w_COV存储得到的K_num个卷积核
%池化层
op_PL= pooling(op_COV,POOL,'averagepooling');
%全连接层
op_FC = FC(op_PL,w_FC,b_FC);
%输出层
op_OP = OP(w_OP,op_FC);
%参数更新
[w_OP,w_FC,b_FC,w_COV,b_COV]=CNN_updateweight(eta,n,class_num,op_OP,w_OP,op_FC,w_FC,b_FC,op_PL,PL_m,PL_n,op_COV,w_COV,b_COV,train_data);
%统计分类正误
[train_p,train_classify]=max(op_OP);
if train_classify == n+1
train_T_cnt = train_T_cnt+1;
else
train_F_cnt = train_F_cnt+1;
end
end
end
%训练误差
train_precious = train_T_cnt/(train_T_cnt+train_F_cnt);%准确率
if mod(iter,5)==0
X=strcat('Epco. ',num2str(iter),'测试误差为:',num2str(1-train_precious));
disp(X);
train_results(1,iter) = train_precious;
end
end
save parameters.mat w_OP w_FC b_FC w_COV b_COV;
%=========================验证=======================
disp('网络训练完成,开始检验......');
valid_precious = 0;%准确率
valid_T_cnt = 0;
valid_F_cnt = 0;
for m = 700:799 %每类验证数据的数量(10)
for n = 0:(class_num-1) %验证样本的种类(10)
%读取数据
image_name =strcat('train_image\',num2str(n),'_',num2str(m),'.bmp');
train_data = double(imread(image_name));
%预处理
train_data = pre_process(train_data);
%前向传播
%卷积层
op_COV = COV(train_data,w_COV,b_COV);%w_COV存储得到的K_num个卷积核
%池化层
op_PL= pooling(op_COV,POOL,'averagepooling');
%全连接层
op_FC = FC(op_PL,w_FC,b_FC);
%输出层
op_OP = OP(w_OP,op_FC);
%t统计正确率
[p,classify]=max(op_OP);
if classify == n+1
valid_T_cnt = valid_T_cnt+1;
else
valid_F_cnt = valid_F_cnt+1;
end
fprintf('真实数字为%d 网络标记为%d 概率值为%d \n',n,classify-1,p);
end
end
valid_precious = valid_T_cnt/(valid_T_cnt+valid_F_cnt);%准确率
if mod(iter,1)==0
X=strcat('Epco. ',num2str(iter),'测试误差为:',num2str(1-valid_precious));disp(X);
valid_results(1,iter) = valid_precious;
end
end
每类训练图片300张,验证图片10张,训练30次,正确率85%,matlab代码运行结果如图显示:
References:
[1]卷积神经网络全面解析
[2]Deep learning:五十一(CNN的反向求导及练习)
[3]卷积神经网络(CNN)反向传播算法
[4]卷积神经网络CNN原理——结合实例matlab实现