本文简要介绍了如何用MATLAB搭建一个可自定义层数的深度神经网络,并以MNIST为例进行网络训练、验证、误差可视化展示。不仅限制在该手写数字库,所搭建的DNN也可以用于其他数据。读者需要对MATLAB的矩阵操作、反向传播算法有所了解。
MATLAB:version>=7.0.0.19920 (R14)。本人所使用的MATLAB版本较低,对于任何不低于该版本的MATLAB,代码都是可以运行的。
MNIST:手写数字库。也可以使用其他数据。
你需要了解MNIST的存储格式,并将MNIST原始格式转换为mat格式。鉴于网上已有很多博客介绍了如何将MNIST转换为mat格式,本节直接贴上MATLAB代码loadMNIST。该函数参考了互联网上现有代码,包括多个来源,未指明具体作者。如有冒犯,请您见谅。
function [Train, Label] = loadMNIST(train_file, label_file, force)
% MNIST数据读取与保存.
% train_file = 'data/train-images.idx3-ubyte';
% label_file = 'data/train-labels.idx1-ubyte';
% 返回时将矩阵转置,即矩阵的每一列是一个结果.
% 如果第3个参数输入force为true,则强制从原始文件读取数据.
% 该函数参考了互联网上现有代码,包括多个来源,未指明具体作者.
if nargin < 3
force = false;
end
% MATLAB7.0
VERSION = datenum(version('-date'));
r2013a = datenum('Jan 01, 2013');
if ~exist('train-images.mat', 'file') || force
FID = fopen(train_file, 'rb');
if FID == -1
Train = [];
Label = [];
fprintf('File [%s] does not exist.\n', train_file);
return
end
magic = fread(FID, 1, 'int32', 0, 'ieee-be');
if VERSION > r2013a
assert(magic == 2051, ['Bad magic number in ', train_file, '']);
end
numImages = fread(FID, 1, 'int32', 0, 'ieee-be');
numRows = fread(FID, 1, 'int32', 0, 'ieee-be');
numCols = fread(FID, 1, 'int32', 0, 'ieee-be');
Train = fread(FID, inf, 'unsigned char');
Train = reshape(Train, numCols, numRows, numImages);
Train = permute(Train,[2 1 3]);
fclose(FID);
% Reshape to #pixels x #examples
Train = reshape(Train, size(Train, 1) * size(Train, 2), size(Train, 3));
% Convert to double and rescale to [0,1]
% [参看这里](https://blog.csdn.net/weixin_41503009/article/details/83420189)
Train = double(Train) * (0.999 / 255) + 0.001;
if ~force
save('train-images.mat', 'Train');
end
else
% 已存在mat格式文件,则直接加载
load('train-images.mat');
end
if ~exist('train-labels.mat', 'file') || force
FID = fopen(label_file,'r');
if FID == -1
Train = [];
Label = [];
fprintf('File [%s] does not exist.\n', label_file);
return
end
magic=readint32(FID);
NumberofImages=readint32(FID);
if VERSION > r2013a
assert(magic == 2049, ['Bad magic number in ', train_file, '']);
assert(NumberofImages == size(Train, 2));
end
Label = zeros(NumberofImages,10);
for i = 1:NumberofImages
temp = fread(FID,1);
Label(i,temp+1) = 1;
end
fclose(FID);
Label = Label';
Label(Label==0) = 0.001;
if ~force
save('train-labels.mat','Label');
end
else
% 已存在mat格式文件,则直接加载
load('train-labels.mat');
end
end
function [getdata]=readint32(FID)
data = [];
for i = 1:4
f=fread(FID,1);
data = strcat(data,num2str(dec2base(f,2,8)));
end
getdata = bin2dec(data);
end
MATLAB代码所在目录的下面应该包含data目录和test目录,data目录存放的是训练用的数据”train-images-idx3-ubyte.gz”和”train-labels-idx1-ubyte.gz”,test目录存放的是测试用的数据”t10k-images-idx3-ubyte.gz”和”t10k-labels-idx1-ubyte.gz”。运行代码之前请将这4个文件解压到它们所在目录,否则程序会提示你没有解压。
编号 | 文件名 | 功能 | 类型 |
---|---|---|---|
1 | s = Accuracy(DNN, Test, Tag) | 计算神经网络DNN在测试集Test上的准确率.Tag是测试集的标签. | 函数 |
2 | m = Grad(m) | 激活层的梯度:由激活函数决定。 | 函数 |
3 | [p, s] = Identify(file) | 对给定的图像文件进行识别.数字为白底黑字. | 函数 |
4 | [Train, Label] = loadMNIST(train_file, label_file) | MNIST数据读取与保存. | 函数 |
5 | DNN = LoadNN(file) | 从指定目录加载现有神经网络. | 函数 |
6 | m = reLU(m) | 激活函数:reLU,S函数或其他,视具体情况而定。 | 函数 |
7 | Loss = SaveResult(DNN, loss, errs, iter, n) | 更新DNN网络权重和残差,并且绘制Loss曲线. | 函数 |
8 | DNN = TrainDNN(Train, Label, Test, Tag, hidden, alpha) | 在给定的数据集上训练神经网络. | 函数 |
9 | [DNN, state] = TrainRecovery(n) | 恢复之前的结果,接着进行训练;或者加载现有神经网络. | 函数 |
10 | trainMNIST | 在MNIST数据集上进行DNN训练、验证、误差可视化。 | 脚本/main/入口 |
本项目一共包含10个m文件,其中9个是function类型,第10个文件类型为script,可视为本项目的主函数或项目入口。在这10个文件当中,TrainDNN是核心训练代码,包括注释也仅仅88行。
function DNN = TrainDNN(Train, Label, Test, Tag, hidden, alpha)
% 在给定的数据集上训练神经网络.
% Train: 给定数据集,每一列代表一个input.
% Label: 数据集归类标签,每一列代表一个output.
% Test: 给定测试集,每一列代表一个input.
% Tag: 测试集归类标签,每一列代表一个output.
% hidden: vector,中间各层神经元个数.
% alpha: 初始学习率.
% DNN: cell数组,依次存放A1, A2, A3, ...和 E, Loss.
% 袁沅祥,2019-7
%% 各层神经元数量
sx = size(Train, 1); %输入层神经元个数
n = hidden; %隐藏层神经元个数
sy = size(Label, 1); %输出层神经元个数
q = length(n) + 1; %权重矩阵的个数
%% 初始值
if nargin < 4
alpha = 1e-2; % 初始学习率
end
iter = 1000; % 单次最大迭代次数
[DNN, state] = TrainRecovery([sx, n, sy]);% 恢复训练
start = size(DNN{end}, 2); % 上一次迭代次数
if state
fprintf('DNN:迭代[%g]次,精度%g.\n', start, DNN{end}(3, end));
return
end
fprintf('从第[%g]步开始迭代.\n', start);
p = alpha * 0.99^start;
lr = p * 0.99.^(0:iter); % 学习率随迭代次数衰减
%profile on;
%profile clear;
%% 开始迭代
num = size(Train, 2);
% 第一行存放误差,第二、三行存放准确率
errs = zeros(3, iter);
count = 0; EarlyStopping = 3; %DNN早停条件
queue = cell(EarlyStopping+1, 1); %存放最近几次DNN网络
for i = 1:iter
tic;
alpha = lr(i);
% 总误差
total = zeros(sy, num);
for k = 1 : num % 遍历元素
% 前向传播
X = cell(1, q+1);
X{1} = Train(:, k); % input
for p = 1:q
X{p+1} = reLU(DNN{p} * [1; X{p}]);
end
err = X{q+1} - Label(:, k); % error
total(:, k) = err;
Store = DNN;
% BP-误差反向传播
for p = 1:q
err = (DNN{end-p}(:, 2:end)' * err) .* Grad(X{q+2-p});
Store{end-1-p} = DNN{end-1-p} - alpha * err * [1; X{q+1-p}]';
end
DNN = Store;
end
queue = circshift(queue, 1);
queue{1} = DNN;
e = mean(sqrt(sum(total.*total)));
s = Accuracy(DNN, Train, Label);
t = Accuracy(DNN, Test, Tag);
best = max(errs(3, 1:i)); % 前i-1次最好的结果
errs(1, i) = e; errs(2, i) = s; errs(3, i) = t;
if t <= best
count = count + 1;
if count == EarlyStopping
DNN = queue{end};
Loss = SaveResult(DNN, DNN{end}, errs, i-EarlyStopping, 1);
return
end
else
count = 0;
end
% 保存权重
if t >= 0
Loss = SaveResult(DNN, DNN{end}, errs, i, 10);
end
fprintf('%g err=%g lr=%g acc=%g %g use %gs\n',i+start,e,alpha,s,t,toc);
end
%profile viewer;
简单:只使用MATLAB的矩阵操作和cell,就实现了可定义层数的DNN。
早停策略:该DNN训练代码加入了早停策略,以防止过拟合。
可视化:每训练一步都会将当前的误差、训练集精度、验证集精度用图形展示。
支持DNN恢复训练:当DNN训练比较漫长的时候,可先训练一定的步数(建议为10的倍数),后续启动trainMNIST时会恢复之前的训练。
MATLAB-DNN完整的代码在本人的github,如有需要,敬请移步。