StudyAI上MatConvNet框架学习笔记之3:mnist实例代码分析

StudyAI上MatConvNet框架学习笔记之3:mnist实例代码分析

最近在StudyAI上学习了matconvnet框架讲解的课程,算是对这个框架有一个基本的了解,记性不好,将一些课程笔记记录下。

1 文件分析

在matconvnet框架中提供了mnist数据库识别的例程,相关的文件主要在框架工程目录->examples->mnist文件夹下,主要代码文件有: 
. cnn_mnist.m文件:主要是训练网络准备数据等 
. cnn_mnist_experiments.m:主要是正向传播预测的实例 
. cnn_mnist_init.m:主要是搭建网络的函数,初始化网络,设置网络参数

文件路径截图如下: 
这里写图片描述

搭建一个网络并训练主要分成这几步骤:

—设置网络结构并初始化相关参数(比如网络层数、每层的属性、每层的参数、每个层的尺寸等); 
—设置数据输入和中间结果存储的路径,并读取样本数据转化成网络可以用的格式; 
—将初始化网络和读取样本的数据代码综合调用训练网络的函数;测试正向传播预测

2 代码分析

2.1 cnn_mnist_init.m文件代码

function net = cnn_mnist_init(varargin)
% 初始化CNN网络结构
% CNN_MNIST_LENET Initialize a CNN similar for MNIST

%默认参数设置
%开启批量归一化操作选项
%是否开启批量归一化的操作
opts.batchNormalization = true ;
%默认使用simpleNN网络封装器
opts.networkType = 'simplenn' ;
%根据外部参数修改参数默认值
opts = vl_argparse(opts, varargin) ;

%设置随机数发生器
rng('default');
rng(0) ;


% 设置网络结构
% f用于缩小随机初始化的conv层权值参数
f=1/100 ;
net.layers = {} ;

%卷积层参数:
% type -- 该层的类型
% weights -- 权重和偏置参数
% stride -- 步长参数
% pad -- 卷积图像边缘填充参数
% pad = 0 --图像块上的扩展宽度为0,即是不做任何扩展
net.layers{end+1} = struct('type', 'conv', ...
                           'weights', {{f*randn(5,5,1,20, 'single'), zeros(1, 20, 'single')}}, ...
                           'stride', 1, ...
                           'pad', 0) ;

%池化层参数:
% type -- 该层的类型
% method -- 池化的类型,这里为最大池化
% pool -- 为池化模板的大小
% stride -- 步长参数
% pad -- 图像边缘填充参数
net.layers{end+1} = struct('type', 'pool', ...
                           'method', 'max', ...
                           'pool', [2 2], ...
                           'stride', 2, ...
                           'pad', 0) ;
% 卷积层                       
net.layers{end+1} = struct('type', 'conv', ...
                           'weights', {{f*randn(5,5,20,50, 'single'),zeros(1,50,'single')}}, ...
                           'stride', 1, ...
                           'pad', 0) ;
%最大池化层                       
net.layers{end+1} = struct('type', 'pool', ...
                           'method', 'max', ...
                           'pool', [2 2], ...
                           'stride', 2, ...
                           'pad', 0) ;
% 卷积层                       
net.layers{end+1} = struct('type', 'conv', ...
                           'weights', {{f*randn(4,4,50,500, 'single'),  zeros(1,500,'single')}}, ...
                           'stride', 1, ...
                           'pad', 0) ;
% relu激活层,即使用relu为激活函数激活层                       
net.layers{end+1} = struct('type', 'relu') ;
%卷积层
net.layers{end+1} = struct('type', 'conv', ...
                           'weights', {{f*randn(1,1,500,10, 'single'), zeros(1,10,'single')}}, ...
                           'stride', 1, ...
                           'pad', 0) ;                       
% softmax层                      
net.layers{end+1} = struct('type', 'softmaxloss') ;

% optionally switch to batch normalization
% 根据是否需要归一化在网络中插入相应的层
% 如果需要批量归一化则需要在相应的位置插入层
if opts.batchNormalization
  net = insertBnorm(net, 1) ;
  net = insertBnorm(net, 4) ;
  net = insertBnorm(net, 7) ;
end

% Meta parameters
% 设置网络的一些参数

% 设置输入图像的尺寸, 输入尺寸为28*28
net.meta.inputSize = [28 28 1] ;
% 设置学习率
net.meta.trainOpts.learningRate = 0.001 ;
% 训练回合次数
net.meta.trainOpts.numEpochs = 20 ;
% 一个批次传递的样本数量
% 即每次积累多少样本的误差进行一次梯度下降更新参数
net.meta.trainOpts.batchSize = 100 ;

% Fill in defaul values
% 填充一些默认的值
% 在网络中添加一些默认的属性值
net = vl_simplenn_tidy(net) ;

% Switch to DagNN if requested
% 如果网络是simplenn类型不做任何处理
% 如果网络是dagnn,需要做一些设置
switch lower(opts.networkType)
  case 'simplenn'
    % done
  case 'dagnn'
    net = dagnn.DagNN.fromSimpleNN(net, 'canonicalNames', true) ;
    net.addLayer('top1err', dagnn.Loss('loss', 'classerror'), ...
      {'prediction', 'label'}, 'error') ;
    net.addLayer('top5err', dagnn.Loss('loss', 'topkerror', ...
      'opts', {'topk', 5}), {'prediction', 'label'}, 'top5err') ;
  otherwise
    assert(false) ;
end

% --------------------------------------------------------------------
% 在net的第L和L+1层中间插入Bnorm层的实现函数,用于需要批量归一化的时候
function net = insertBnorm(net, l)
% --------------------------------------------------------------------
assert(isfield(net.layers{l}, 'weights'));
ndim = size(net.layers{l}.weights{1}, 4);
layer = struct('type', 'bnorm', ...
               'weights', {{ones(ndim, 1, 'single'), zeros(ndim, 1, 'single')}}, ...
               'learningRate', [1 1 0.05], ...
               'weightDecay', [0 0]) ;
net.layers{l}.weights{2} = [] ;  % eliminate bias in previous conv layer
net.layers = horzcat(net.layers(1:l), layer, net.layers(l+1:end)) ;

 

在网络结构的设置中需要注意:

. 1、所有的卷积核都是三维的,前两维度是卷积核的大小,第三维是卷积核的维度,当前卷基层卷积核的维度是上一层卷积层结果图像的深度。 
. 2、当前卷积层卷积后结果图像的维度和当前卷积层卷积核的个数一致,比如当前卷积核个数为10,那么经过该层卷积层后图像深度为10。 
. 3、每个卷积核和上卷积层图像做卷积时,卷积核的一维对应上层卷积图像的一维,这里相当于多个二维卷积后的结果相加得到最后当前卷积层的卷积结果。 
. 4、最后一层输出要与最后需要识别或者分类的类别数一致。比如做一个四分类的网络,最后一层的输出是2维的。 
.5、 网络的设置是门大学问,大部分是参考网上一些大牛设计的网络,因为自己设计的网络很难保证收敛性和稳定性。

eg:图像输入为21*21*3,那么第1个卷积层卷积核第三维必须为3,如果该层共有这样的卷积核10个,那么经过当前卷积层后特征图像的深度就是10,就相当于有10个波段的图像数据;卷积相当于每个卷积核的一维与输入图的一维做二维卷积再累加;其他层的作用不改变图像的深度信息,下一个卷积层的维度则必须为10,个数的多少决定了该卷积层后特征图像的深度。

2.2 cnn_mnist.m文件代码

function [net, info] = cnn_mnist(varargin)
%CNN_MNIST  Demonstrates MatConvNet on MNIST

%将..\\..\\matlab\\vl_setupnn.m文件导入到当前workspace中
% 运行 vl_setupnn.m 文件
% vl_setupnn.m文件功能:将matconvnet框架工具箱添加到MATLAB的path中
run(fullfile(fileparts(mfilename('fullpath')),...
  '..', '..', 'matlab', 'vl_setupnn.m')) ;

%opt为可选参数,所有的参数
opts.batchNormalization = false ;       %是否批量处理,批量归一化
opts.network = [] ;
opts.networkType = 'simplenn' ;     %网络的模型,只有两种sinplenn或者dagnn
[opts, varargin] = vl_argparse(opts, varargin) ;  %根据外部参数修改参数默认值

%整个代码段构建实验路径
sfx = opts.networkType ;
if opts.batchNormalization, sfx = [sfx '-bnorm'] ; end

%配置实验结果存放路径: data\mnist-baseline-simplenn文件夹
%vl_rootnn为找到框架根目录
opts.expDir = fullfile(vl_rootnn, 'data', ['mnist-baseline-' sfx]) ;
[opts, varargin] = vl_argparse(opts, varargin) ;

%mnist原始数据所在的文件夹路径: matconvnet-1.0-beta24\data\mnist
opts.dataDir = fullfile(vl_rootnn, 'data', 'mnist') ;

%配置可以直接用的数据的路径
%不会直接用原始数据进行跑,会进行预处理,去除均值等
% imdb.mat中存储的就是可以直接用的数据,h*w*c*n这种形式的数据
% 设置网络可用数据的存储路径
%imdb结构体存放的路径: data\mnist-baseline-simplenn\imdb.mat
opts.imdbPath = fullfile(opts.expDir, 'imdb.mat');

opts.train = struct() ;

%解析参数,将传进来的参数赋值给网络参数
opts = vl_argparse(opts, varargin) ;

%默认使用cpu跑程序
%是否使用GPU编译程序
if ~isfield(opts.train, 'gpus'), opts.train.gpus = []; end;

% --------------------------------------------------------------------
%                                                         Prepare data
% --------------------------------------------------------------------


%判断网络持否已经初始化好
if isempty(opts.network)
    %如果没有初始化好一个网络,程序自己初始化一个网络
    % cnn_mnist_init函数 -- 初始化一个cnn网络
    % 根据前面的一些参数初始化一个网路
  net = cnn_mnist_init('batchNormalization', opts.batchNormalization, ...
    'networkType', opts.networkType) ;
else
    %已经初始化好一个网络
  net = opts.network ;
  opts.network = [] ;
end

%读取网络可使用的数据imdb
if exist(opts.imdbPath, 'file')
    %加载数据
  imdb = load(opts.imdbPath) ;
else
    % 从minst数据集中获取数据,减去图像均值,存放到Imdb结构体中
    % 即将原始数据转化成网络可以使用的数据格式,同时做一些预处理
    % 并将可以直接用的数据存储起来
  imdb = getMnistImdb(opts) ;
  mkdir(opts.expDir) ;
  save(opts.imdbPath, '-struct', 'imdb') ;
end

%数据集的lable类别
% 数据的类别, 1到10
net.meta.classes.name = arrayfun(@(x)sprintf('%d',x),1:10,'UniformOutput',false) ;

% --------------------------------------------------------------------
%                                                                Train
% --------------------------------------------------------------------

%根据网络类型选择不同的训练函数
% @为函数句柄
switch opts.networkType
  case 'simplenn', trainfn = @cnn_train ;
  case 'dagnn', trainfn = @cnn_train_dag ;
end


%训练网络
% getBatch每次获取一个batch的图输入
[net, info] = trainfn(net, imdb, getBatch(opts), ...
                'expDir', opts.expDir, ...
                 net.meta.trainOpts, ...     
                  opts.train, ...  
                 'val', find(imdb.images.set == 3)) ;   %选择验证机标签


%%每次获取一个batch的输入数据                 
% --------------------------------------------------------------------
function fn = getBatch(opts)
% --------------------------------------------------------------------
switch lower(opts.networkType)
  case 'simplenn'
    fn = @(x,y) getSimpleNNBatch(x,y) ;
  case 'dagnn'
    bopts = struct('numGpus', numel(opts.train.gpus)) ;
    fn = @(x,y) getDagNNBatch(bopts,x,y) ;
end

% --------------------------------------------------------------------
function [images, labels] = getSimpleNNBatch(imdb, batch)
% --------------------------------------------------------------------
images = imdb.images.data(:,:,:,batch) ;
labels = imdb.images.labels(1,batch) ;

% --------------------------------------------------------------------
function inputs = getDagNNBatch(opts, imdb, batch)
% --------------------------------------------------------------------
images = imdb.images.data(:,:,:,batch) ;
labels = imdb.images.labels(1,batch) ;
if opts.numGpus > 0
  images = gpuArray(images) ;
end
inputs = {'input', images, 'label', labels} ;



% --------------------------------------------------------------------
function imdb = getMnistImdb(opts)
% --------------------------------------------------------------------
% 从原始数据中生成和获取我们网络可以使用的imdb输入数据文件
% Preapre the imdb structure, returns image data with mean image subtracted

%原始输入数据
files = {'train-images-idx3-ubyte', ...
         'train-labels-idx1-ubyte', ...
         't10k-images-idx3-ubyte', ...
         't10k-labels-idx1-ubyte'} ;

if ~exist(opts.dataDir, 'dir')
  mkdir(opts.dataDir) ;
end

for i=1:4
% 读取files中的第一个文件,训练图像包含6万张尺寸为28*28的图像
  if ~exist(fullfile(opts.dataDir, files{i}), 'file')
    url = sprintf('http://yann.lecun.com/exdb/mnist/%s.gz',files{i}) ;
    fprintf('downloading %s\n', url) ;
    gunzip(url, opts.dataDir) ;
  end
end

%读取files中的第一个文件,训练图像集包含6万张尺寸为28*28的图像
f=fopen(fullfile(opts.dataDir, 'train-images-idx3-ubyte'),'r') ;
x1=fread(f,inf,'uint8');
fclose(f) ;
%调整成矩阵,60000张图像调整成一个矩阵
x1=permute(reshape(x1(17:end),28,28,60e3),[2 1 3]) ;

%读取files中的第二个文件
f=fopen(fullfile(opts.dataDir, 't10k-images-idx3-ubyte'),'r') ;
x2=fread(f,inf,'uint8');
fclose(f) ;
x2=permute(reshape(x2(17:end),28,28,10e3),[2 1 3]) ;

%读取files中的第三个文件--训练图像的标签
f=fopen(fullfile(opts.dataDir, 'train-labels-idx1-ubyte'),'r') ;
y1=fread(f,inf,'uint8');
fclose(f) ;
y1=double(y1(9:end)')+1 ;

%读取files中的第四个文件 -- 测试图像集的标签
f=fopen(fullfile(opts.dataDir, 't10k-labels-idx1-ubyte'),'r') ;
y2=fread(f,inf,'uint8');
fclose(f) ;
y2=double(y2(9:end)')+1 ;

% 用于表示样本集合中训练集和测试集的集和
% set中元素==1为训练集,==3为测试集
set = [ones(1,numel(y1)) 3*ones(1,numel(y2))];

% 将x1的训练集和x2的测试集在第三维拼接起来,构成完成的数据集,从uint8转成single
data = single(reshape(cat(3, x1, x2),28,28,1,[]));

%求出所有的训练集中所有图像的均值图像
dataMean = mean(data(:,:,:,set == 1), 4);

%bsxfun函数将minus算子应用到data的每个元素上,对所有的图像减去均值
data = bsxfun(@minus, data, dataMean) ;


%生成网络可以直接使用的输入数据类型
% 填充imdb结构体构造数据集
imdb.images.data = data ;      %存储所有的数据
imdb.images.data_mean = dataMean;   %存储所有训练图像的均值图像
imdb.images.labels = cat(2, y1, y2) ;  %将训练集和测试集标签的第二维拼接起来
imdb.images.set = set ;
imdb.meta.sets = {'train', 'val', 'test'} ; %set == 训练;==2验证;==3测试
imdb.meta.classes = arrayfun(@(x)sprintf('%d',x),0:9,'uniformoutput',false) ;

在读取数据中需要注意:

. 1、我们自己采集的样本或者网上下载的数据集是不能直接用的,必须转换成网络训练所需的数据接口的形式,即上文中的imdb数据形式。 
. 2、在所有的真正用于训练的图像需要求均值图像,再对所有的图像数据(包括验证和测试集)减去该均值图像。 
. 3、给数据打set标签时,需要注意要和训练函数处的一致,比如在imdb中将测试集set为3,那么在train函数中,也要一致对应。 
. 4、最后输入网络的数据格式要和网络需要的一致,后面自己准备数据的时候可以参照这个例子,比如imdb文件有哪些结构,一些内部变量的设置和结构都要保持一致。 
. 5、注意cat函数的使用,主要用于矩阵的拼接,前面的数字代表在第几维度拼接。

注意:后面自己采集数据读取数据构建imdb输入数据时,可以参照这里的一些函数和写法,写一个用于读取自己样本图像数据转化成imdb文件的函数。

posted @ 2018-03-31 19:33  菜鸡一枚  阅读(644)  评论(0)    收藏  举报