github资源地址:[Release-x86/x64]
上一篇:轻量级C++神经网络应用库CreativeLus:3、复杂函数逼近。案例:多输入混合逼近。
下一篇:轻量级C++神经网络应用库CreativeLus:5、ResNet残差网咯。案例:(cifar-100)图片分类。
案例4:CNN网络,实现(MNIST)手写数字识别
本案例的任务
经过前面几章的介绍,对CL基本功能应有所了解。CL致力于通过简单的代码,易懂的逻辑,通用化的功能达到快速应用的目的。之前的几个案例,演示功能性居多,实用性不强,但今天,我们就用CL来重现一个经典的CNN案例,用来解决实际应用问题:单色手写数字图片的识别。
- 本章将通过简单代码构建一个经典CNN网络LeNet-5,并介绍以下核心功能:
- 方法一:通过结构定义对象的API,构建卷积神经网络的方法;
- 方法二:通过脚本定义对象,构建卷积神经网络方法;
卷积网络LeNet-5介绍
关于什么是卷积神经网络?相关基础知识,在此不再论述,CSDN 和百度都有很多的介绍。今天我们通过构建经典的cnn网络LeNet-5来实现Mnist数据集的训练和手写数字图片的识别。特别说明:本章代码中,对原LeNet-5逻辑有一些小修改,取消了原C3、C5层的链接标记。经测试,该修改对模型的训练及识别能力并无实际影响,若一定要使用,可采用在C3、C5层设置Dropout方案替代。
- 为了礼貌起见,放两张图,供小伙伴们回忆一下cnn网络和LeNet-5。(图片源于网络)
准备工作
1、Mnist数据集的获取和预处理
首先我们下载好本例所用手写数据训练数据集和测试数据集[Mnist:http://yann.lecun.com/exdb/mnist/],他的官网是这样介绍它的:
- MNIST手写数字数据库(可从本页获得)有60000个示例的训练集和10000个示例的测试集。它是NIST提供的更大集合的子集。数字已被规格化,并集中在一个固定大小的图像中。
- 它是一个很好的数据库,供那些想尝试在实际数据上学习技术和模式识别方法,同时在预处理和格式化方面花费最少精力的人使用。
之后使用代码做一下预处理,生成CL样本对集合对象,代码很简单,如下:
#include "CreativeLus.h"
#include "CreativeLusExTools.h"
string pathdatasrc = ("D:\\Documents\\Desktop\\nndata\\mnist\\");
// 第一步: 由Mnist数据预处理,生成模型训练集和预测集---------------------------------------------
BpnnSamSets trainSets, testSets;
if (!trainSets.readFromFile((pathTag + "cnnTrainData.txt").c_str())) {
StdDataHelper::readDataOfMnist(
(pathdatasrc + "train-images.idx3-ubyte").c_str(),
(pathdatasrc + "train-labels.idx1-ubyte").c_str(),
trainSets,
-1, //表示训练输入数据的值区间最小为-1
1, //表示训练输入数据的值区间最大为-1
-0.8, //表示训练输入数据对应的分类标签的值区间最小为-0.8
0.8, //表示训练输入数据对应的分类标签的值区间最大为0.8
2 //对样本输入数据做padding=2的数据扩充,使得28x28的原始数据变为32x32
);
trainSets.writeToFile((pathTag + "cnnTrainData.txt").c_str());
}
if (!testSets.readFromFile((pathTag + "cnnTestData.txt").c_str())) {
StdDataHelper::readDataOfMnist(
(pathdatasrc + "t10k-images.idx3-ubyte").c_str(),
(pathdatasrc + "t10k-labels.idx1-ubyte").c_str(),
testSets, -1, 1, -0.8, 0.8, 2
);
testSets.writeToFile((pathTag + "cnnTestData.txt").c_str());
}
- 其中静态
StdDataHelper::readDataOfMnist()
方法,是包含在头文件"CreativeLusExTools.h"
中,实现快速处理Mnist原始数据,并转换成BpnnSamSets数据集的函数,源代码也可根据需要自行调整。 - 关于LeNet-5网络实现,可参考【卷积神经网络(CNN)的简单实现(MNIST)】,不过要做好思想准备,这是一种纯C基于过程的实现方案,很难修改,很难扩充,代码灵活度不大。光是要构造出网络,代码就麻烦到让人吐血,算法过程代码更是不好理解,对于初学者很不友好。
2、卷积网络组装
基于以上前车之鉴,CL应该有简单明了的建模手段,因此,“8”行代码构建CNN网络LeNet-5的方式就诞生了:(关于 BpnnStructScript
对象使用,详见完整测试代码部分)
//代码片段
BpnnStructScript scp = //构造生成脚本
{
{{0,SCP_Conv,{5,5,6},wi[0],bi,transfunc},}, //标准卷积层
{{0,SCP_Pool,{2,2},{}, {},-1,WC_Average},}, //池化层,均值池化
{{0,SCP_ConvSep,{1,1},wi[2],bi, transfunc},},//分割卷积层
{{0,SCP_Conv,{5,5,16},wi[3],bi,transfunc},}, //标准卷积层
{{0,SCP_Pool,{2,2},{}, {},-1,WC_Average},}, //池化层,均值池化
{{0,SCP_ConvSep,{1,1},wi[5],bi,transfunc},}, //分割卷积层
{{0,SCP_Conv,{5,5,120},wi[6],bi,transfunc},},//标准卷积层,本层链接了map=1x1的分割卷积层,即等价于一个全连接层
{{0,SCP_Fc,{10},wi[7],bi, transfunc},}, //10个输出神经元的全连接层
};
关于如何做到的?CL是基于对象建模的,按[案例3]介绍的结构定义方式,可以构建任意的自定义的网络,加上些许封装,即可实现通过快速的脚本对象定义,生成复杂的卷积网络(后续还有更复杂的VGG,ResNet等网络,原理均同),这在后续技术附录中介绍。
3、手写数字图片准备
- 接下来准备10张32x32尺寸的手写数字图片,从0到9,均采用单色bitmap格式(windows位图格式)。
- 处理图片,采用头文件
"CreativeLusExTools.h"
中定义的CLBmpHelper::readbmp()
静态方法即可(不需要用OpenCV库来处理这么麻烦了)。关于CLBmpHelper::readbmp()
可自行查看源码,按需修改。
- 读取的数据时记得做一下必要的值域转换:
//代码片段
CLBmpHelper::readbmp(str.c_str(), bmpData);
data.resize(bmpData.size());
for (size_t j = 0, sj = bmpData.size(); j < sj; j++){
//将[0,255]的之间的byte值映射转换到[-1,1]的范围内,至于为什么,请自行思考。
data[j] = bmpData[j] / 255.0 * (1 - (-1)) + (-1);
}
完整测试代码
#include <stdio.h>
#include <string>
#include <vector>
#include <map>
#include "CreativeLus.h"
#include "CreativeLusExTools.h"
using namespace cl;
int main() {
printf("\n\n//案例4:cnn卷积神经网络,实现(Mnist)手写数字识别\n");
string pathTag = ("D:\\Documents\\Desktop\\example_04_cnn_mnist\\");
string pathdatasrc = ("D:\\Documents\\Desktop\\nndata\\mnist\\"); //原始Mnist数据目录
string pathdatabmp = ("D:\\Documents\\Desktop\\nndata\\bmp32x32_0_9\\"); //手写数字图片文件目录
// 第一步: 由Mnist数据预处理,生成模型训练集和预测集---------------------------------------------
BpnnSamSets trainSets, testSets;
if (!trainSets.readFromFile((pathTag + "cnnTrainData.txt").c_str())) {
StdDataHelper::readDataOfMnist(
(pathdatasrc + "train-images.idx3-ubyte").c_str(),
(pathdatasrc + "train-labels.idx1-ubyte").c_str(),
trainSets, -1, 1, -0.8, 0.8, 2
);
trainSets.writeToFile((pathTag + "cnnTrainData.txt").c_str());
}
if (!testSets.readFromFile((pathTag + "cnnTestData.txt").c_str())) {
StdDataHelper::readDataOfMnist(
(pathdatasrc + "t10k-images.idx3-ubyte").c_str(),
(pathdatasrc + "t10k-labels.idx1-ubyte").c_str(),
testSets, -1, 1, -0.8, 0.8, 2
);
testSets.writeToFile((pathTag + "cnnTestData.txt").c_str());
}
Bpnn bp;
if (bp.readBpnnFormFile((pathTag + "cnn.txt").c_str()) == false) { //读取已有文件,用于预测,没有就从头开始
// 第二步:自定义卷积网络结构------------------------------
EBP_TF transfunc = TF_Tanh; //激活函数选Tanh
BpnnStructDef mod;
#define UseScript 1
#if UseScript < 1
// 方式一:BpnnStructDef内置API方式,生成卷积网络定义,该方法相比于脚本法,会麻烦些,不过脚本构造法也是封装了BpnnStructDef对象方法实现的。
UINT dp = 1, wt = 32, ht = 32;
mod.addOneConvolution(0, 0, wt, ht, { 1,5,5,6 },
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (25.0 + 150.0))),Float(sqrt(6.0 / (25.0 + 150.0))) },
{ IT_Const,0 }, transfunc);
mod.addOnePooling(1, 0, wt, ht, { 6,2,2 }, WC_Average);
mod.addOneConvolutionSeparable(2, 0, wt, ht, { 6,1,1 },
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (4.0 + 1.0))),Float(sqrt(6.0 / (4.0 + 1.0))) },
{ IT_Const,0 }, transfunc);
mod.addOneConvolution(3, 0, wt, ht, { 6,5,5,16 },
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (150.0 + 400.0))),Float(sqrt(6.0 / (150.0 + 400.0))) },
{ IT_Const,0 }, transfunc);
mod.addOnePooling(4, 0, wt, ht, { 16,2,2 }, WC_Average);
mod.addOneConvolutionSeparable(5, 0, wt, ht, { 16,1,1 },
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (4.0 + 1.0))),Float(sqrt(6.0 / (4.0 + 1.0))) },
{ IT_Const,0 }, transfunc);
mod.addOneConvolution(6, 0, wt, ht, { 16,5,5,120 },
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (400.0 + 3000.0))),Float(sqrt(6.0 / (400.0 + 3000.0))) },
{ IT_Const,0 }, transfunc);
mod.addOneFullConnect(7, 0, wt * ht * 120, 10,
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (120.0 + 10.0))),Float(sqrt(6.0 / (120.0 + 10.0))) },
{ IT_Const,0 }, transfunc);
#else
// 方式二:用脚本BpnnStructScript,定义卷积网络
vector<WiInitDef> wi = { //权值初始化设置定义
//WiInitDef表示描述了权值将如何被初始化:
//IT_Uniform表示采用 均值随机分布初始化,
//均值分布范围:[-1.0 * sqrt(6.0 / (25.0 + 150.0)) , sqrt(6.0 / (25.0 + 150.0))] 之间。
//至于为什么这样设,其中知识涉及到模型调参,是个复杂的过程,在此不做论述。但调参却很重要,
//大部分模型训练效果不好都是初值设定不合理造成的。可自行尝试调整为{IT_Uniform,-1.0,1.0}试试效果。
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (25.0 + 150.0))),Float(sqrt(6.0 / (25.0 + 150.0))) },
{},
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (4.0 + 1.0))),Float(sqrt(6.0 / (4.0 + 1.0))) },
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (150.0 + 400.0))),Float(sqrt(6.0 / (150.0 + 400.0))) },
{},
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (4.0 + 1.0))),Float(sqrt(6.0 / (4.0 + 1.0))) },
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (400.0 + 3000.0))),Float(sqrt(6.0 / (400.0 + 3000.0))) },
{ IT_Uniform,Float(-1.0 * sqrt(6.0 / (120.0 + 10.0))),Float(sqrt(6.0 / (120.0 + 10.0))) },
};
BiInitDef bi = { IT_Const,0 }; //阈值初始化设置定义:IT_Const表示以常量0初始化
InputFilterMap ifm = { //脚本输入map信息描述
0, //表示输入数据,对齐的标号。此处,表示从输入样本对的输入向量0位置处开始读取数据
1, //输入图片的数据通道为1
32, //输入图片的数据宽度为32
32 //输入图片的数据高度为32
};
BpnnStructScript scp = //构造生成脚本
{
//第一层:第0分组:标准卷积层,卷积核输入通道数由InputFilterMap决定(本例为1),
//感受野5x5,6个卷积核,移动step=1,用wi[0]描述的初始化方案进行对应的卷积核的权值做初始化,
//用bi描述方案初始化各个卷积核阈值,该层激活函数选Tanh。(该层效果等价于LeNet-5网络的C1层)
{{0,SCP_Conv,{5,5,6},wi[0],bi,transfunc},},
//第二层:第0分组:池化层,感受野2x2,移动step=2,无权置初始化,无阈值初始化,传递函数为-1
//表示默认(池化层永远是以PureLin线性函数作为激活函数的,并且权值固定为1),
//权值链接处理方式取Average即均值模式
{{0,SCP_Pool,{2,2},{}, {},-1,WC_Average},},
//第三层:第0分组:单通道卷积层,感受野1x1,移动step=1,wi[2]权置初始化,bi阈值初始化,
//传递函数Tanh。(单通道卷积层,即输入通道永远为1,逐层并排的卷积核扫描的方式。)
//说明:第二层和第三层组合起来,效果等价于LeNet-5网络的S2层
{{0,SCP_ConvSep,{1,1},wi[2],bi, transfunc},},
//第四层:第0分组:标准卷积层,卷积核感受野5x5,共16个卷积核(卷积核的输入通道数由上层
//输出map数自动确定无需人为干预指定),扫描移动step=1。(该层效果等价于LeNet-5网络的C3层)
{{0,SCP_Conv,{5,5,16},wi[3],bi,transfunc},},
//第五层:第0分组:均值池化层,意义同上
{{0,SCP_Pool,{2,2},{}, {},-1,WC_Average},},
//第六层:第0分组:单通道卷积层,意义同上
//说明:第五层和第六层组合起来,效果等价于LeNet-5网络的S4层
{{0,SCP_ConvSep,{1,1},wi[5],bi,transfunc},},
//第七层:第0分组:标准卷积层,意义同上。(该层效果等价于LeNet-5网络的C5层)
{{0,SCP_Conv,{5,5,120},wi[6],bi,transfunc},},
//第八层:第0分组:全连接层,输出维度10(即10个神经元),wi[7]权值初始化,bi阈值初始化,
//激活函数Tanh。(该层效果等价于LeNet-5网络的output层)
{{0,SCP_Fc,{10},wi[7],b。i, transfunc},},
};
mod.addScript( //很重要
scp, //将构造好的脚本,选入结构定义对象中
ifm, //放在脚本描述前面的输入层的描述
true //脚本构造过程中,数据输出显示到控制台
);
#endif
mod.writeToFile((pathTag + "cnnStructDef.txt").c_str()); //输出保存结构定义
//将结构定义对象选入bp模型中
bp.setStructure(mod);
//设置必须要的训练和预测数据集
bp.setSampSets(trainSets);
bp.setCorrectRateEvaluationModel(
0.985, //正确率目标设为98.5%
&testSets,
0,
false,
CRT_MaxValuePosMatch //采用最大值出现的位置评价标准。(技术原因,详见,[案例2]的附录2)
);
Bpnn::CallBackExample monit;
BPNN_CALLBACK_MAKE(pMonit, Bpnn::CallBackExample::print);
//生成网络模型(必须的)
bp.buildNet(pMonit, &monit);
// 第三步:开始训练网络---------------------------------------------
bp.setParam(1.01, 0.0, 0.8); //学习率1.01(此处有意为之,让模型演示自调整学习率的过程)
bp.openGraphFlag(true);
bp.setMultiThreadSupport(true); //打开多线程
bp.setMaxTimes(trainSets.size());
CLTick tick, tick2;
bool rt = false; Int epoch = 0;
while (!rt) {
rt = bp.train(0, 0, 0, pMonit, &monit);
printf(("Epoch %d:总耗时:%g秒,本次:%g秒,正确率 = %.2f %%, Er = %g \n\n"), ++epoch, tick.getSpendTime(), tick2.getSpendTime(true),
bp.getSavedCorrectRate() * 100.0, bp.getEr());
bp.showGraphParam();
//bp.showGraphNetStruct(true, 800, 1); //注意:由于卷积网络的神经元很多,网络结构图是无法显示的
};
if (rt) {
//将训练完成的模型输出保存
bp.writeBpnnToFile((pathTag + "cnn.txt").c_str());
}
bp.exportGraphEr((pathTag + "cnnEr.bmp").c_str());
bp.exportGraphCorrectRate((pathTag + "cnnCr.bmp").c_str());
bp.detachExtend();
}
// 第四步:手写图片带入模型做识别测试---------------------------------------------
std::vector<int> target{ 0,1,2,3,4,5,6,7,8,9 };
VLB bmpData;
VLF data; VLF neuron_output;
for (auto& i : target) {
std::string str = pathdatabmp + std::to_string(i) + ".bmp"; //图片路径
CLBmpHelper::readbmp(str.c_str(), bmpData); //读取bmp图片数据
data.resize(bmpData.size());
for (size_t j = 0, sj = bmpData.size(); j < sj; j++)
data[j] = bmpData[j] / 255.0 * (1 - (-1)) + (-1); //数据从[0,255]向[-1,1]转换
// 模型预测(此处演示采用的是内置数据区做为预测缓存,区别于[案例2]和[案例3]的独立数据区)
bp.predict(data, &neuron_output); /
int ret = -1;
Float _max;
#define listMaxI( var , lst , lstSize ,index ) \
(var) = (lst)[(index) = 0];\
for (size_t _ise231=1,_si = (lstSize);_ise231 < _si;_ise231++)\
if( (var) < (lst)[_ise231]) (var) = (lst)[(index) = _ise231];
#define vectorMaxI( var , lst ,index ) listMaxI(var,lst,(lst).size(),index)
//找到预测结果向量,最大值出现的位置,与数据分类索引值位置对比,若匹配说明预测分类正确
vectorMaxI(_max, neuron_output, ret);
fprintf(stdout, "\n模型预测数字是: %d , 正确数字是: %d . %s \noutput =[ ", ret, i, ret != i ? " <<<<<<<预测错误!!!>>>>>>" : " 预测正确");
Float somaxall = 0;
for (Uint i = 0; i < bp.outputDimension(); i++)
{
cout << std::setprecision(2) << neuron_output[i] << ", ";
somaxall += exp(neuron_output[i] );
}
cout << " ]\nSoftmax=[ ";
for (Uint i = 0; i < bp.outputDimension(); i++)
{
cout << exp(neuron_output[i] ) / somaxall * 100.0 << "%, ";
}
cout << " ]\n\n";
}
return system("pause");
}
结果输出
//案例4:cnn卷积神经网络,实现(Mnist)手写数字识别
//脚本输出
BpnnStructDef create by script: input= [ 0 , 1 x 32 x 32 ]
Layer= 0:
-> set= 0: upLink= 0, range= [ 0 - 4703 ], size= (4704), map= ( 28 x 28 x 6 ), SCP_Conv
Layer= 1:
-> set= 0: upLink= 0, range= [ 0 - 1175 ], size= (1176), map= ( 14 x 14 x 6 ), SCP_Pool
Layer= 2:
-> set= 0: upLink= 0, range= [ 0 - 1175 ], size= (1176), map= ( 14 x 14 x 6 ), SCP_ConvSep
Layer= 3:
-> set= 0: upLink= 0, range= [ 0 - 1599 ], size= (1600), map= ( 10 x 10 x 16 ), SCP_Conv
Layer= 4:
-> set= 0: upLink= 0, range= [ 0 - 399 ], size= (400), map= ( 5 x 5 x 16 ), SCP_Pool
Layer= 5:
-> set= 0: upLink= 0, range= [ 0 - 399 ], size= (400), map= ( 5 x 5 x 16 ), SCP_ConvSep
Layer= 6:
-> set= 0: upLink= 0, range= [ 0 - 119 ], size= (120), map= ( 1 x 1 x 120 ), SCP_Conv
Layer= 7:
-> set= 0: upLink= 0, range= [ 0 - 9 ], size= (10), map= ( 1 x 1 x 10 ), SCP_Fc
Script end.
Net construct completed. Neurons: 9586, layers: 8.
//自调节输出
[Waring]: Times= 1'st.The model automatically adjust learning rate ( 1.01 -> 0.233056 ) and retry!
[Waring]: Times= 2'st.The model automatically adjust learning rate ( 0.233056 -> 0.0572928 ) and retry!
[Waring]: Times= 3'st.The model automatically adjust learning rate ( 0.0572928 -> 0.00897293 ) and retry!
//训练过程
Net training epoch completed.
Epoch 1:总耗时:92.6971秒,本次:92.6971秒,正确率 = 97.19 %, Er = 0.0198421
Net training epoch completed.
Epoch 2:总耗时:201.192秒,本次:108.495秒,正确率 = 97.95 %, Er = 0.0201395
Net training epoch completed.
Epoch 3:总耗时:289.006秒,本次:87.8145秒,正确率 = 98.35 %, Er = 0.023344
Net training epoch completed.
Epoch 4:总耗时:431.617秒,本次:142.611秒,正确率 = 98.44 %, Er = 0.0194745
Net training epoch completed with achieve accuracy. CorrectRate(98.53%) >= TagCorrectRate(98.50%)
Epoch 5:总耗时:685.648秒,本次:254.031秒,正确率 = 98.53 %, Er = 0.0151698
//模型预测
模型预测数字是: 0 , 正确数字是: 0 . 预测正确
output =[ -0.22, -0.79, -0.69, -0.72, -0.77, -0.84, -0.44, -0.86, -0.74, -0.84, ]
Softmax=[ 16%, 8.9%, 9.8%, 9.5%, 9%, 8.4%, 13%, 8.3%, 9.3%, 8.4%, ]
模型预测数字是: 1 , 正确数字是: 1 . 预测正确
output =[ -0.58, 0.59, -0.93, -0.87, -0.88, -0.54, -0.74, -0.67, -0.94, -0.79, ]
Softmax=[ 9.3%, 30%, 6.6%, 7%, 6.9%, 9.7%, 8%, 8.5%, 6.5%, 7.6%, ]
模型预测数字是: 2 , 正确数字是: 2 . 预测正确
output =[ -0.7, -0.48, 0.21, -0.5, -0.96, -0.9, -0.89, -0.78, -0.61, -0.48, ]
Softmax=[ 8.6%, 11%, 21%, 11%, 6.6%, 7.1%, 7.1%, 8%, 9.4%, 11%, ]
模型预测数字是: 3 , 正确数字是: 3 . 预测正确
output =[ -0.83, -0.83, -0.88, 0.86, -0.82, -0.83, -0.88, -0.81, -0.86, -0.88, ]
Softmax=[ 7%, 7%, 6.7%, 38%, 7%, 7%, 6.7%, 7.1%, 6.8%, 6.7%, ]
模型预测数字是: 4 , 正确数字是: 4 . 预测正确
output =[ -0.88, -0.79, -0.81, -0.91, 0.63, -0.75, -0.65, -0.7, -0.77, -0.87, ]
Softmax=[ 6.9%, 7.6%, 7.5%, 6.7%, 32%, 7.9%, 8.8%, 8.3%, 7.8%, 7%, ]
模型预测数字是: 5 , 正确数字是: 5 . 预测正确
output =[ -0.85, -0.83, -0.77, -0.81, -0.76, 0.81, -0.77, -0.79, -0.84, -0.82, ]
Softmax=[ 6.8%, 7%, 7.4%, 7.1%, 7.4%, 36%, 7.4%, 7.2%, 6.9%, 7%, ]
模型预测数字是: 6 , 正确数字是: 6 . 预测正确
output =[ -0.91, -0.85, -0.86, -0.83, -0.87, -0.73, 0.88, -0.77, -0.77, -0.84, ]
Softmax=[ 6.3%, 6.7%, 6.7%, 6.9%, 6.6%, 7.6%, 38%, 7.3%, 7.3%, 6.8%, ]
模型预测数字是: 7 , 正确数字是: 7 . 预测正确
output =[ -0.75, -0.29, -0.94, -0.61, -0.75, -0.85, -0.83, 0.36, -0.82, -0.83, ]
Softmax=[ 8.2%, 13%, 6.7%, 9.4%, 8.1%, 7.4%, 7.5%, 25%, 7.6%, 7.5%, ]
模型预测数字是: 8 , 正确数字是: 8 . 预测正确
output =[ -0.85, -0.82, -0.83, -0.73, -0.88, -0.81, -0.76, -0.76, 0.77, -0.78, ]
Softmax=[ 6.9%, 7.1%, 7.1%, 7.8%, 6.7%, 7.2%, 7.5%, 7.5%, 35%, 7.4%, ]
模型预测数字是: 9 , 正确数字是: 9 . 预测正确
output =[ -0.87, -0.84, -0.68, -0.81, -0.8, -0.92, -0.84, -0.25, -0.64, -0.21, ]
Softmax=[ 8.1%, 8.3%, 9.8%, 8.6%, 8.7%, 7.6%, 8.3%, 15%, 10%, 16%, ]
请按任意键继续. . .
- 正确率曲线:
-
误差曲线:(始终未收敛,原因,详见附录2)
-
控台输出结果:
-
模型预测效果还是很不错的。可调整参数,增加手写图片,尝试其他的训练方法等,再观察模型表现。
github资源地址:[Release-x86/x64]
上一篇:轻量级C++神经网络应用库CreativeLus:3、复杂函数逼近。案例:多输入混合逼近。
下一篇:轻量级C++神经网络应用库CreativeLus:5、ResNet残差网咯。案例:(cifar-100)图片分类。
附录:几个发散性思维问题
- 1、因为使用了Tanh作为激活函数,其值域在[-1,1],所以我们将原始数据映射到值域区间[-1,1],否则正确率永远无法收敛。
- 2、虽然模型正确率收敛,但误差曲线却始终混乱并不收敛,原因是:采用了【位置型评价】后,模型除了关心主维度(即最大值出现的位置的维度)数据之外,并未对其他维度数据过多关心,甚至放弃了其他维度的数据的逼近和拟合(事实上模型预测,并不需要关心这些数据是否接近真实值,模型只关心出现极值的维度的位置,即可实现主分类判断,解决主分类问题)。而误差计算采用的了均方差算法,对每一个输出维度都做考量,当预测正确出现时,非主要维度(即非极值维度)的实际值可能与真实值相去甚远,所以均方误差始终很大,得不到收敛。
- 可以在模型第2、第6层设置Dropout方式,代替LeNet-5在C3和C5层的链接矩阵效果。经过测试该正则化手段效果并不好。
//代码片段
//每一100次训练步幅,更新一次dropout网络。设定dropout层为第2、第6层,dropout剪除率15%
bp.setDropout(trainSets.size() / 600, { DRP(2,0.15),DRP(6,0.15) });
bp.train()