C++从零实现深度神经网络之四——神经网络的预测和输入输出的解析
在上一篇的结尾提到了神经网络的预测函数predict(),说道predict调用了forward函数并进行了输出的解析,输出我们看起来比较方便的值。
神经网络的预测函数predict()
predict()
函数和predict_one()
函数的区别相信很容易从名字看出来,那就是输入一个样本得到一个输出和输出一组样本得到一组输出的区别,显然predict()
应该是循环调用predict_one()
实现的。所以我们先看一下predict_one()
的代码:
int Net::predict_one(cv::Mat &input)
{
if (input.empty())
{
std::cout << "Input is empty!" << std::endl;
return -1;
}
if (input.rows == (layer[0].rows) && input.cols == 1)
{
layer[0] = input;
farward();
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;
}
}
可以在第二个if语句里面看到最主要的内容就是两行:
farward();
...
...
minMaxLoc(layer_out, NULL, NULL, NULL, &predict_maxLoc, cv::noArray());
分别是前面提到的前向传播和输出解析。
前向传播得到最后一层输出层layer_out,然后从layer_out中提取最大值的位置,最后输出位置的y坐标。
输出的组织方式和解析
之所以这么做,就不得不提一下标签或者叫目标值在这里是以何种形式存在的。以激活函数是sigmoid函数为例,sigmoid函数是把实数映射到[0,1]区间,所以显然最后的输出y:0<=y<=1。如果激活函数是tanh函数,则输出区间是[-1,1]。如果是sigmoid,而且我们要进行手写字体识别的话,需要识别的数字一共有十个:0-9。显然我们的神经网络没有办法输出大于1的值,所以也就不能直观的用0-9几个数字来作为神经网络的实际目标值或者称之为标签。
这里采用的方案是,把输出层设置为一个单列十行的矩阵,标签是几就把第几行的元素设置为1,其余都设为0。由于编程中一般都是从0开始作为第一位的,所以位置与0-9的数字正好一一对应。我们到时候只需要找到输出最大值所在的位置,也就知道了输出是几。
当然上面说的是激活函数是sigmoid的情况。如果是tanh函数呢?那还是是几就把第几位设为1,而其他位置全部设为-1即可。
如果是ReLU函数呢?ReLU函数的至于是0到正无穷。所以我们可以标签是几就把第几位设为几,其他为全设为0。最后都是找到最大值的位置即可。
这些都是需要根据激活函数来定。代码中是调用opencv的minMaxLoc()
函数来寻找矩阵中最大值的位置。
输入的组织方式和读取方法
既然说到了输出的组织方式,那就顺便也提一下输入的组织方式。生成神经网络的时候,每一层都是用一个单列矩阵来表示的。显然第一层输入层就是一个单列矩阵。所以在对数据进行预处理的过程中,这里就是把输入样本和标签一列一列地排列起来,作为矩阵存储。标签矩阵的第一列即是第一列样本的标签。以此类推。
值得一提的是,输入的数值全部归一化到0-1之间。
由于这里的数值都是以float
类型保存的,这种数值的矩阵Mat不能直接保存为图片格式,所以这里我选择了把预处理之后的样本矩阵和标签矩阵保存到xml文档中。在源码中可以找到把原始的csv文件转换成xml文件的代码。在csv2xml.cpp
中。而我转换完成的MNIST的部分数据保存在data文件夹中,可以在Github上找到。
在opencv中xml的读写非常方便,如下代码是写入数据:
string filename = "input_label.xml";
FileStorage fs(filename, FileStorage::WRITE);
fs << "input" << input_normalized;
fs << "target" << target_; // Write cv::Mat
fs.release();
而读取代码的一样简单明了:
cv::FileStorage fs;
fs.open(filename, cv::FileStorage::READ);
cv::Mat input_, target_;
fs["input"] >> input_;
fs["target"] >> target_;
fs.release();
我写了一个函数get_input_label()
从xml文件中从指定的列开始提取一定数目的样本和标签。默认从第0列开始读取,只是上面函数的简单封装:
//Get sample_number samples in XML file,from the start column.
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));
}
至此其实已经可以开始实践,训练神经网络识别手写数字了。只有一部分还没有提到,那就是模型的保存和加载。下一篇将会讲模型的save和load,然后就可以实际开始进行例子的训练了。等不及的小伙伴可以直接去github下载完整的程序开始跑了。
源码链接
未完待续。。。
公众号CVPy,分享OpenCV和Python的实战内容。每一篇都会放出完整的代码。欢迎关注。
cvpy.jpg