C++从零实现神经网络, 史上最全 2W 字长文收藏版
点击“开发者技术前线”,选择“星标🔝” 13:21 在看|星标|留言, 真爱
前言
元旦期间,CVPy连载神经网络系列的同时举行送书活动,不料大家对于书籍的热情高涨,以至于大部分人只顾参加送书活动而忽略了文章的主内容。而且考虑到元旦假期期间的分散发布,可能导致一部分人未能看到全部内容。故而把系列六篇整合之后发布,方便感兴趣的人查阅收藏。
神经网络的要素 在真正开始coding之前还是有必要交代一下神经网络基础,其实也就是设计类和写程序的思路。简而言之,神经网络的包含几大要素: 神经元节点
层(layer)
权值(weights)
偏置项(bias)
Net类——基于Mat 神经网络中的计算几乎都可以用矩阵计算的形式表示,这也是我用OpenCV的Mat类的原因之一,它提供了非常完善的、充分优化过的各种矩阵运算方法;另一个原因是我最熟悉的库就是OpenCV......有很多比较好的库和框架在实现神经网络的时候会用很多类来表示不同的部分。比如Blob类表示数据,Layer类表示各种层,Optimizer类来表示各种优化算法。但是这里没那么复杂,主要还是能力有限,只用一个Net类表示神经网络。 还是直接让程序说话,Net类包含在Net.h中,大致如下。 #ifndef NET_H
#define NET_H
#endif // NET_H
#pragma once
#include
#include
#include
//#include
#include"Function.h"
namespace liu
{
class Net
{
public:
std:: vector< int> layer_neuron_num;
std:: vector
std:: vector
std:: vector
public:
Net() {};
~Net() {};
//Initialize net:genetate weights matrices、layer matrices and bias matrices
// bias default all zero
void initNet(std::vector
//Initialise the weights matrices.
void initWeights(int type = 0, double a = 0., double b = 0.1);
//Initialise the bias matrices.
void initBias(cv::Scalar& bias);
//Forward
void forward();
//Forward
void backward();
protected:
//initialise the weight matrix.if type =0,Gaussian.else uniform.
void initWeight(cv::Mat &dst, int type, double a, double b);
//Activation function
cv:: Mat activationFunction(cv::Mat &x, std::string func_type);
//Compute delta error
void deltaError();
//Update weights
void updateWeights();
};
}
说明
以上不是Net类的完整形态,只是对应于本文内容的一个简化版,简化之后看起来会更加清晰明了。 成员变量与成员函数
现在Net类只有四个成员变量,分别是:
每一层神经元数目(layer_neuron_num)
层(layer)
权值矩阵(weights)
偏置项(bias)
initNet():用来初始化神经网络
initWeights():初始化权值矩阵,调用initWeight()函数
initBias():初始化偏置项
forward():执行前向运算,包括线性运算和非线性激活,同时计算误差
backward():执行反向传播,调用updateWeights()函数更新权值。
void Net::initNet( std:: vector< int> layer_neuron_num_)
{
layer_neuron_num = layer_neuron_num_;
//Generate every layer.
layer.resize(layer_neuron_num.size());
for ( int i = 0; i < layer.size(); i++)
{
layer[i].create(layer_neuron_num[i], 1, CV_32FC1);
}
std:: cout << "Generate layers, successfully!" << std:: endl;
//Generate every weights matrix and bias
weights.resize(layer.size() - 1);
bias.resize(layer.size() - 1);
for ( int i = 0; i < (layer.size() - 1); ++i)
{
weights[i].create(layer[i + 1].rows, layer[i].rows, CV_32FC1);
//bias[i].create(layer[i + 1].rows, 1, CV_32FC1);
bias[i] = cv::Mat::zeros(layer[i + 1].rows, 1, CV_32FC1);
}
std:: cout << "Generate weights matrices and bias, successfully!" << std:: endl;
std:: cout << "Initialise Net, done!" << std:: endl;
} 权值初始化 initWeight()函数 权值初始化函数initWeights()调用initWeight()函数,其实就是初始化一个和多个的区别。 偏置初始化是给所有的偏置赋相同的值。这里用Scalar对象来给矩阵赋值。 //initialise the weights matrix.if type =0,Gaussian.else uniform.
void Net::initWeight(cv::Mat &dst, int type, double a, double b)
{
if (type == 0)
{
randn(dst, a, b);
}
else
{
randu(dst, a, b);
}
}
//initialise the weights matrix.
void Net::initWeights( int type, double a, double b)
{
//Initialise weights cv::Matrices and bias
for ( int i = 0; i < weights.size(); ++i)
{
initWeight(weights[i], 0, 0., 0.1);
}
}
偏置初始化是给所有的偏置赋相同的值。这里用Scalar对象来给矩阵赋值。 //Initialise the bias matrices.
void Net::initBias(cv::Scalar& bias_)
{
for ( int i = 0; i < bias.size(); i++)
{
bias[i] = bias_;
}
}
至此,神经网络需要初始化的部分已经全部初始化完成了。 初始化测试 我们可以用下面的代码来初始化一个神经网络,虽然没有什么功能,但是至少可以测试下现在的代码是否有BUG: #include"../include/Net.h"
//
using namespace std;
using namespace cv;
using namespace liu;
int main(int argc, char *argv[])
{
//Set neuron number of every layer
vector< int> layer_neuron_num = { 784, 100, 10 };
// Initialise Net and weights
Net net;
net.initNet(layer_neuron_num);
net.initWeights( 0, 0., 0.01);
net.initBias(Scalar( 0.05));
getchar();
return 0;
}
亲测没有问题。 本文先到这里,前向传播和反向传播放在下一篇内容里面。 源码 源码链接 所有的代码都已经托管在Github上面,感兴趣的可以去下载查看。欢迎提意见。 公众号后台回复“神经网络”可得源码链接地址。
前向过程 前向过程简介 如前所述,前向过程分为线性运算和非线性运算两部分。相对来说比较简单。 线型运算可以用
Y = WX+b 来表示,其中X是输入样本,这里即是第N层的单列矩阵,W是权值矩阵,Y是加权求和之后的结果矩阵,大小与N+1层的单列矩阵相同。b是偏置,默认初始化全部为0。不难推知 (鬼知道我推了多久!) ,W的大小是 (N+1).rows * N.rows 。正如上一篇中生成weights矩阵的代码实现一样: weights[i].create(layer[i + 1].rows, layer[i].rows, CV_32FC1); 非线性运算可以用
O=f(Y) 来表示。Y就是上面得到的Y。O就是第N+1层的输出。f就是我们一直说的激活函数。激活函数一般都是非线性函数。它存在的价值就是给神经网络提供非线性建模能力。激活函数的种类有很多,比如sigmoid函数,tanh函数,ReLU函数等。各种函数的优缺点可以参考更为专业的论文和其他更为专业的资料。 我们可以先来看一下前向函数forward()的代码: //Forward void Net::forward()
{
for ( int i = 0; i < layer_neuron_num.size() - 1; ++i)
{
cv::Mat product = weights[i] * layer[i] + bias[i];
layer[i + 1] = activationFunction(product, activation_function);
}
} for循环里面的两句就分别是上面说的线型运算和激活函数的非线性运算。 激活函数
activationFunction() 里面实现了不同种类的激活函数,可以通过第二个参数来选取用哪一种。代码如下: //Activation function cv::Mat Net::activationFunction(cv::Mat &x, std:: string func_type)
{
activation_function = func_type;
cv::Mat fx;
if (func_type == "sigmoid")
{
fx = sigmoid(x);
}
if (func_type == "tanh")
{
fx = tanh(x);
}
if (func_type == "ReLU")
{
fx = ReLU(x);
}
return fx;
} 各个函数更为细节的部分在
Function.h 和 Function.cpp 文件中。在此略去不表,感兴趣的请君移步 Github 。 需要再次提醒的是,上一篇博客中给出的Net类是精简过的,下面可能会出现一些上一篇Net类里没有出现过的成员变量。完整的Net类的定义还是在 Github 里。 反向传播过程 反向传播 反向传播原理是链式求导法则,其实就是我们高数中学的复合函数求导法则。这只是在推导公式的时候用的到。具体的推导过程我推荐看看下面这一篇教程,用图示的方法,把前向传播和反向传播表现的清晰明了,强烈推荐! Principles of training multi-layer neural network using backpropagation。 一会将从这一篇文章中截取一张图来说明权值更新的代码。在此之前,还是先看一下反向传播函数backward()的代码是什么样的: //Forward void Net::backward()
{
calcLoss(layer[layer.size() - 1], target, output_error, loss);
deltaError();
updateWeights();
} 可以看到主要是是三行代码,也就是调用了三个函数:
第一个函数
calcLoss()计算输出误差和目标函数,所有输出误差平方和的均值作为需要最小化的目标函数。第二个函数
deltaError()计算delta误差,也就是下图中delta1*df()那部分。第三个函数
updateWeights()更新权值,也就是用下图中的公式更新权值。
就看下updateWeights()函数的代码: //Update weights void Net::updateWeights()
{
for ( int i = 0; i < weights.size(); ++i)
{
cv::Mat delta_weights = learning_rate * (delta_err[i] * layer[i].t());
weights[i] = weights[i] + delta_weights;
}
} 核心的两行代码应该还是能比较清晰反映上图中的那个权值更新的公式的。图中公式里的eta常被称作学习率。训练神经网络调参的时候经常要调节这货。 计算输出误差和delta误差的部分纯粹是数学运算,乏善可陈。但是把代码贴在下面吧。
calcLoss() 函数在 Function.cpp 文件中: //Objective function void calcLoss(cv::Mat &output, cv::Mat &target, cv::Mat &output_error, float &loss)
{
if (target.empty())
{
std:: cout << "Can't find the target cv::Matrix" << std:: endl;
return;
}
output_error = target - output;
cv::Mat err_sqrare;
pow(output_error, 2., err_sqrare);
cv::Scalar err_sqr_sum = sum(err_sqrare);
loss = err_sqr_sum[ 0] / ( float)(output.rows);
}
deltaError() 在 Net.cpp 中: //Compute delta error void Net::deltaError()
{
delta_err.resize(layer.size() - 1);
for ( int i = delta_err.size() - 1; i >= 0; i--)
{
delta_err[i].create(layer[i + 1].size(), layer[i + 1].type());
//cv::Mat dx = layer[i+1].mul(1 - layer[i+1]);
cv::Mat dx = derivativeFunction(layer[i + 1], activation_function);
//Output layer delta error
if (i == delta_err.size() - 1)
{
delta_err[i] = dx.mul(output_error);
}
else //Hidden layer delta error
{
cv::Mat weight = weights[i];
cv::Mat weight_t = weights[i].t();
cv::Mat delta_err_1 = delta_err[i];
delta_err[i] = dx.mul((weights[i + 1]).t() * delta_err[i + 1]);
}
}
} 注意 需要注意的就是计算的时候输出层和隐藏层的计算公式是不一样的。 另一个需要注意的就是......难道大家没觉得本系列文章的代码看起来非常友好吗 至此,神经网络最核心的部分已经实现完毕。剩下的就是想想该如何训练了。这个时候你如果愿意的话仍然可以写一个小程序进行几次前向传播和反向传播。还是那句话,鬼知道我在能进行传播之前到底花了多长时间调试! 源码链接 所有的代码都已经托管在Github上面,感兴趣的可以去下载查看。欢迎提意见。 公众号后台回复“神经网络”可得源码链接地址。
{
public:
//Integer vector specifying the number of neurons in each layer including the input and output layers.
std:: vector< int> layer_neuron_num;
std:: string activation_function = "sigmoid";
double learning_rate;
double accuracy = 0.;
std:: vector< double> loss_vec;
float fine_tune_factor = 1.01;
protected:
std:: vector
std:: vector
std:: vector
std:: vector
cv::Mat output_error;
cv::Mat target;
float loss;
public:
Net() {};
~Net() {};
//Initialize net:genetate weights matrices、layer matrices and bias matrices
// bias default all zero
void initNet(std::vector
//Initialise the weights matrices.
void initWeights(int type = 0, double a = 0., double b = 0.1);
//Initialise the bias matrices.
void initBias(cv::Scalar& bias);
//Forward
void forward();
//Forward
void backward();
//Train,use loss_threshold
void train(cv::Mat input, cv::Mat target_, float loss_threshold, bool draw_loss_curve = false); //Test
void test(cv::Mat &input, cv::Mat &target_);
//Predict,just one sample
int predict_one(cv::Mat &input);
//Predict,more than one samples
std:: vector< int> predict(cv::Mat &input);
//Save model;
void save(std::string filename);
//Load model;
void load(std::string filename);
protected:
//initialise the weight matrix.if type =0,Gaussian.else uniform.
void initWeight(cv::Mat &dst, int type, double a, double b);
//Activation function
cv:: Mat activationFunction(cv::Mat &x, std::string func_type);
//Compute delta error
void deltaError();
//Update weights
void updateWeights();
}; 可以看到已经有了训练的函数train()、测试的函数test(),还有实际应用训练好的模型的predict()函数,以及保存和加载模型的函数save()和load()。大部分成员变量和成员函数应该还是能够通过名字就能够知道其功能的。 训练 训练函数train() 本文重点说的是训练函数train()和测试函数test()。这两个函数接受输入(input)和标签(或称为目标值target)作为输入参数。其中训练函数还要接受一个阈值作为迭代终止条件,最后一个函数可以暂时忽略不计,那是选择要不要把loss值实时画出来的标识。 训练的过程如下:
接受一个样本(即一个单列矩阵)作为输入,也即神经网络的第一层;
进行前向传播,也即forward()函数做的事情。然后计算loss;
如果loss值小于设定的阈值loss_threshold,则进行反向传播更新阈值;
重复以上过程直到loss小于等于设定的阈值。
void Net::train(cv::Mat input, cv::Mat target_, float loss_threshold, bool draw_loss_curve)
{
if (input.empty())
{
std:: cout << "Input is empty!" << std:: endl;
return;
}
std:: cout << "Train,begain!" << std:: endl;
cv::Mat sample;
if (input.rows == (layer[ 0].rows) && input.cols == 1)
{
target = target_;
sample = input;
layer[ 0] = sample;
forward();
//backward();
int num_of_train = 0;
while (loss > loss_threshold)
{
backward();
forward();
num_of_train++;
if (num_of_train % 500 == 0)
{
std:: cout << "Train " << num_of_train << " times" << std:: endl;
std:: cout << "Loss: " << loss << std:: endl;
}
}
std:: cout << std:: endl << "Train " << num_of_train << " times" << std:: endl;
std:: cout << "Loss: " << loss << std:: endl;
std:: cout << "Train sucessfully!" << std:: endl;
}
else if (input.rows == (layer[ 0].rows) && input.cols > 1)
{
double batch_loss = loss_threshold + 0.01;
int epoch = 0;
while (batch_loss > loss_threshold)
{
batch_loss = 0.;
for ( int i = 0; i < input.cols; ++i)
{
target = target_.col(i);
sample = input.col(i);
layer[ 0] = sample;
farward();
backward();
batch_loss += loss;
}
loss_vec.push_back(batch_loss);
if (loss_vec.size() >= 2 && draw_loss_curve)
{
draw_curve(board, loss_vec);
}
epoch++;
if (epoch % output_interval == 0)
{
std:: cout << "Number of epoch: " << epoch << std:: endl;
std:: cout << "Loss sum: " << batch_loss << std:: endl;
}
if (epoch % 100 == 0)
{
learning_rate *= fine_tune_factor;
}
}
std:: cout << std:: endl << "Number of epoch: " << epoch << std:: endl;
std:: cout << "Loss sum: " << batch_loss << std:: endl;
std:: cout << "Train sucessfully!" << std:: endl;
}
else
{
std:: cout << "Rows of input don't cv::Match the number of input!" << std:: endl;
}
} 这里考虑到了用单个样本和多个样本迭代训练两种情况。而且还有另一种不用loss阈值作为迭代终止条件,而是用正确率的train()函数,内容大致相同,此处略去不表。 在经过train()函数的训练之后,就可以得到一个模型了。所谓模型,可以简单的认为就是权值矩阵。简单的说,可以把神经网络当成一个超级函数组合,我们姑且认为这个超级函数就是y = f(x) = ax +b。那么权值就是a和b。反向传播的过程是把a和b当成自变量来处理的,不断调整以得到最优值或逼近最优值。在完成反向传播之后,训练得到了参数a和b的最优值,是一个固定值了。这时自变量又变回了x。我们希望a、b最优值作为已知参数的情况下,对于我们的输入样本x,通过神经网络计算得到的结果y,与实际结果相符合是大概率事件。 测试 测试函数test() test()函数的作用就是用一组训练时没用到的样本,对训练得到的模型进行测试,把通过这个模型得到的结果与实际想要的结果进行比较,看正确来说到底是多少,我们希望正确率越多越好。 test()的步骤大致如下几步:
用一组样本逐个输入神经网络;
通过前向传播得到一个输出值;
比较实际输出与理想输出,计算正确率。
void Net::test(cv::Mat &input, cv::Mat &target_)
{
if (input.empty())
{
std:: cout << "Input is empty!" << std:: endl;
return;
}
std:: cout << std:: endl << "Predict,begain!" << std:: endl;
if (input.rows == (layer[ 0].rows) && input.cols == 1)
{
int predict_number = predict_one(input);
cv::Point target_maxLoc;
minMaxLoc(target_, NULL, NULL, NULL, &target_maxLoc, cv::noArray());
int target_number = target_maxLoc.y;
std:: cout << "Predict: " << predict_number << std:: endl;
std:: cout << "Target: " << target_number << std:: endl;
std:: cout << "Loss: " << loss << std:: endl;
}
else if (input.rows == (layer[ 0].rows) && input.cols > 1)
{
double loss_sum = 0;
int right_num = 0;
cv::Mat sample;
for ( int i = 0; i < input.cols; ++i)
{
sample = input.col(i);
int predict_number = predict_one(sample);
loss_sum += loss;
target = target_.col(i);
cv::Point target_maxLoc;
minMaxLoc(target, NULL, NULL, NULL, &target_maxLoc, cv::noArray());
int target_number = target_maxLoc.y;
std:: cout << "Test sample: " << i << " " << "Predict: " << predict_number << std:: endl;
std:: cout << "Test sample: " << i << " " << "Target: " << target_number << std:: endl << std:: endl;
if (predict_number == target_number)
{
right_num++;
}
}
accuracy = ( double)right_num / input.cols;
std:: cout << "Loss sum: " << loss_sum << std:: endl;
std:: cout << "accuracy: " << accuracy << std:: endl;
}
else
{
std:: cout << "Rows of input don't cv::Match the number of input!" << std:: endl;
return;
}
} 这里在进行前向传播的时候不是直接调用forward()函数,而是调用了predict_one()函数,predict函数的作用是给定一个输入,给出想要的输出值。其中包含了对forward()函数的调用。还有就是对于神经网络的输出进行解析,转换成看起来比较方便的数值。 这一篇的内容已经够多了,我决定把对于predict部分的解释放到下一篇。
源码链接 所有的代码都已经托管在Github上面,感兴趣的可以去下载查看。欢迎提意见。 公众号后台回复“神经网络”可得源码链接地址。
predict() 函数和 predict_one() 函数的区别相信很容易从名字看出来,那就是输入一个样本得到一个输出和输出一组样本得到一组输出的区别,显然 predict() 应该是循环调用 predict_one() 实现的。所以我们先看一下 predict_one() 的代码:
{
if (input.empty())
{
std:: cout << "Input is empty!" << std:: endl;
return -1;
}
if (input.rows == (layer[ 0].rows) && input.cols == 1)
{
layer[ 0] = input;
forward();
cv::Mat layer_out = layer[layer.size() - 1];
cv::Point predict_maxLoc;
minMaxLoc(layer_out, NULL, NULL, NULL, &predict_maxLoc, cv::noArray());
return predict_maxLoc.y;
}
else
{
std:: cout << "Please give one sample alone and ensure input.rows = layer[0].rows" << std:: endl;
return -1;
}
}
...
...
minMaxLoc(layer_out, NULL, NULL, NULL, &predict_maxLoc, cv::noArray());
minMaxLoc() 函数来寻找矩阵中最大值的位置。
输入的组织方式和读取方法
输入的组织方式和读取方法 既然说到了输出的组织方式,那就顺便也提一下输入的组织方式。生成神经网络的时候,每一层都是用一个单列矩阵来表示的。显然第一层输入层就是一个单列矩阵。所以在对数据进行预处理的过程中,这里就是把输入样本和标签一列一列地排列起来,作为矩阵存储。标签矩阵的第一列即是第一列样本的标签。以此类推。 值得一提的是,输入的数值全部归一化到0-1之间。 由于这里的数值都是以 float 类型保存的,这种数值的矩阵Mat不能直接保存为图片格式,所以这里我选择了把预处理之后的样本矩阵和标签矩阵保存到xml文档中。在源码中可以找到把原始的csv文件转换成xml文件的代码。在 csv2xml.cpp 中。而我转换完成的MNIST的部分数据保存在data文件夹中,可以在Github上找到。 在opencv中xml的读写非常方便,如下代码是写入数据:
FileStorage fs(filename, FileStorage::WRITE);
fs << "input" << input_normalized;
fs << "target" << target_; // Write cv::Mat
fs.release();
fs.open(filename, cv::FileStorage::READ);
cv::Mat input_, target_;
fs[ "input"] >> input_;
fs[ "target"] >> target_;
fs.release();
get_input_label() 从xml文件中从指定的列开始提取一定数目的样本和标签。默认从第0列开始读取,只是上面函数的简单封装:
void get_input_label(std::string filename, cv::Mat& input, cv::Mat& label, int sample_num, int start)
{
cv::FileStorage fs;
fs.open(filename, cv::FileStorage::READ);
cv::Mat input_, target_;
fs[ "input"] >> input_;
fs[ "target"] >> target_;
fs.release();
input = input_(cv::Rect(start, 0, sample_num, input_.rows));
label = target_(cv::Rect(start, 0, sample_num, target_.rows));
}
源码链接 所有的代码都已经托管在Github上面,感兴趣的可以去下载查看。欢迎提意见。 公众号后台回复“神经网络”可得源码链接地址。
layer_neuron_num,各层神经元数目,这是生成神经网络需要的唯一参数。weights,神经网络初始化之后需要用训练好的权值矩阵去初始化权值。activation_function,使用神经网络的过程其实就是前向计算的过程,显然需要知道激活函数是什么。learning_rate,如果要在现有模型的基础上继续训练以得到更好的模型,更新权值的时候需要用到这个函数。
xml 格式,上一篇已经提到了保存和加载 xml 是多么的方便:
void Net::save( std:: string filename)
{
cv:: FileStorage model(filename, cv::FileStorage::WRITE);
model << "layer_neuron_num" << layer_neuron_num;
model << "learning_rate" << learning_rate;
model << "activation_function" << activation_function;
for ( int i = 0; i < weights.size(); i++)
{
std:: string weight_name = "weight_" + std::to_string(i);
model << weight_name << weights[i];
}
model.release();
}
//Load model;
void Net::load( std:: string filename)
{
cv::FileStorage fs;
fs.open(filename, cv::FileStorage::READ);
cv::Mat input_, target_;
fs[ "layer_neuron_num"] >> layer_neuron_num;
initNet(layer_neuron_num);
for ( int i = 0; i < weights.size(); i++)
{
std:: string weight_name = "weight_" + std::to_string(i);
fs[weight_name] >> weights[i];
}
fs[ "learning_rate"] >> learning_rate;
fs[ "activation_function"] >> activation_function;
fs.release();
}

void draw_curve(cv::Mat& board, std::vector
{
cv:: Mat board_(620, 1000, CV_8UC3, cv::Scalar::all(200));
board = board_;
cv::line(board, cv::Point( 0, 550), cv::Point( 1000, 550), cv::Scalar( 0, 0, 0), 2);
cv::line(board, cv::Point( 50, 0), cv::Point( 50, 1000), cv::Scalar( 0, 0, 0), 2);
for ( size_t i = 0; i < points.size() - 1; i++)
{
cv:: Point pt1(50 + i * 2, (int)(548 - points[i]));
cv:: Point pt2(50 + i * 2 + 1, (int)(548 - points[i + 1]));
cv::line(board, pt1, pt2, cv::Scalar( 0, 0, 255), 2);
if (i >= 1000)
{
return;
}
}
cv::imshow( "Loss", board);
cv::waitKey( 10);
}
源码链接 所有的代码都已经托管在Github上面,感兴趣的可以去下载查看。欢迎提意见。 公众号后台回复“神经网络”可得源码链接地址。

这里需要注意到的是,像素值的范围是0-255。 一般在数据预处理阶段都会归一化,全部除以255,把值转换到0-1之间。 csv文件中包含42000个样本,这么多样本,对于我七年前买的4000元级别的破笔记本来说,单单是读取一次都得半天,更不要提拿这么多样本去迭代训练了,简直是噩梦(兼论一个苦逼的学生几年能挣到换电脑的钱!)。所以我只是提取了前1000个样本,然后把归一化后的样本和标签都保存到一个xml文件中。在前面的一篇博客中已经提到了输入输出的组织形式,偷懒直接复制了。 既然说到了输出的组织方式,那就顺便也提一句输入的组织方式。生成神经网络的时候,每一层都是用一个单列矩阵来表示的。显然第一层输入层就是一个单列矩阵。所以在对数据进行预处理的过程中,我就是把输入样本和标签一列一列地排列起来,作为矩阵存储。标签矩阵的第一列即是第一列样本的标签。以此类推。 把输出层设置为一个单列十行的矩阵,标签是几就把第几行的元素设置为1,其余都设为0。由于编程中一般都是从0开始作为第一位的,所以位置与0-9的数字正好一一对应。我们到时候只需要找到输出最大值所在的位置,也就知道了输出是几。”这里只是重复一下,这一部分的代码在
csv2xml.cpp中:
#include
using namespace std;
using namespace cv;
//int csv2xml()
int main()
{
CvMLData mlData;
mlData.read_csv( "train.csv"); //读取csv文件
Mat data = cv::Mat(mlData.get_values(), true);
cout << "Data have been read successfully!" << endl;
//Mat double_data;
//data.convertTo(double_data, CV_64F);
Mat input_ = data(Rect( 1, 1, 784, data.rows - 1)).t();
Mat label_ = data(Rect( 0, 1, 1, data.rows - 1));
Mat target_(10, input_.cols, CV_32F, Scalar::all(0.));
Mat digit(28, 28, CV_32FC1);
Mat col_0 = input_.col( 3);
float label0 = label_.at< float>( 3, 0);
cout << label0;
for ( int i = 0; i < 28; i++)
{
for ( int j = 0; j < 28; j++)
{
digit.at< float>(i, j) = col_0.at< float>(i * 28 + j);
}
}
for ( int i = 0; i < label_.rows; ++i)
{
float label_num = label_.at< float>(i, 0);
//target_.at
target_.at< float>(label_num, i) = label_num;
}
Mat input_normalized(input_.size(), input_.type());
for ( int i = 0; i < input_.rows; ++i)
{
for ( int j = 0; j < input_.cols; ++j)
{
//if (input_.at
//{
input_normalized.at< float>(i, j) = input_.at< float>(i, j) / 255.;
//}
}
}
string filename = "input_label_0-9.xml";
FileStorage fs(filename, FileStorage::WRITE);
fs << "input" << input_normalized;
fs << "target" << target_; // Write cv::Mat
fs.release();
Mat input_1000 = input_normalized(Rect( 0, 0, 10000, input_normalized.rows));
Mat target_1000 = target_(Rect( 0, 0, 10000, target_.rows));
string filename2 = "input_label_0-9_10000.xml";
FileStorage fs2(filename2, FileStorage::WRITE);
fs2 << "input" << input_1000;
fs2 << "target" << target_1000; // Write cv::Mat
fs2.release();
return 0;
}
Mat digit 的作用是,检验下转换后的矩阵和标签是否对应正确这里是把col(3),也就是第四个样本从一行重新变成28x28的图像,看上面的第一张图的第一列可以看到,第四个样本的标签是4。那么它转换回来的图像时什么样呢?是下面这样:
这里也证明了为啥第一张图看起来像素全是0。边缘全黑能不是0吗? 然后在使用的时候用前面提到过的get_input_label()获取一定数目的样本和标签。
实战数字识别
实战 没想到前面数据处理说了那么多。。。。 废话少说,直接说训练的过程: 给定每层的神经元数目,初始化神经网络和权值矩阵
从inputlabel1000.xml文件中取前800个样本作为训练样本,后200作为测试样本。
这是神经网络的一些参数:训练时候的终止条件,学习率,激活函数类型
前800样本训练神经网络,直到满足loss小于阈值loss_threshold,停止。
后200样本测试神经网络,输出正确率。
保存训练得到的模型。
//
using namespace std;
using namespace cv;
using namespace liu;
int main(int argc, char *argv[])
{
//Set neuron number of every layer
vector< int> layer_neuron_num = { 784, 100, 10 };
// Initialise Net and weights
Net net;
net.initNet(layer_neuron_num);
net.initWeights( 0, 0., 0.01);
net.initBias(Scalar( 0.5));
//Get test samples and test samples
Mat input, label, test_input, test_label;
int sample_number = 800;
get_input_label( "data/input_label_1000.xml", input, label, sample_number);
get_input_label( "data/input_label_1000.xml", test_input, test_label, 200, 800);
//Set loss threshold,learning rate and activation function
float loss_threshold = 0.5;
net.learning_rate = 0.3;
net.output_interval = 2;
net.activation_function = "sigmoid";
//Train,and draw the loss curve(cause the last parameter is ture) and test the trained net
net.train(input, label, loss_threshold, true);
net.test(test_input, test_label);
//Save the model
net.save( "models/model_sigmoid_800_200.xml");
getchar();
return 0;
}

Mat test_input, test_label;
int sample_number = 200;
int start_position = 800;
get_input_label( "data/input_label_1000.xml", test_input, test_label, sample_number, start_position);
//Load the trained net and test.
Net net;
net.load( "models/model_sigmoid_800_200.xml");
net.test(test_input, test_label);
getchar();
return 0;
float loss_threshold = 0.2;
net.learning_rate = 0.02;
net.output_interval = 2;
net.activation_function = "tanh";
//convert label from 0---1 to -1---1,cause tanh function range is [-1,1]
label = 2 * label - 1;
test_label = 2 * test_label - 1;
源码链接 所有的代码都已经托管在Github上面,感兴趣的可以去下载查看。欢迎提意见。 公众号后台回复“神经网络”可得源码链接地址。 END
「在看」大家一起看
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
