c++从零实现神经网络
C++從零實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)(收藏版:兩萬(wàn)字長(zhǎng)文)
AI科技大本營(yíng)?2018-01-09 00:00:00??12280??收藏?75
?
作者 | 冰不語(yǔ)
本文轉(zhuǎn)自微信公眾號(hào)CVPy
?
?
01
Net類的設(shè)計(jì)與神經(jīng)網(wǎng)絡(luò)初始化
?
閑言少敘,直接開始
?
既然是要用C++來(lái)實(shí)現(xiàn),那么我們自然而然的想到設(shè)計(jì)一個(gè)神經(jīng)網(wǎng)絡(luò)類來(lái)表示神經(jīng)網(wǎng)絡(luò),這里我稱之為Net類。由于這個(gè)類名太過(guò)普遍,很有可能跟其他人寫的程序沖突,所以我的所有程序都包含在namespace liu中,由此不難想到我姓劉。在之前的博客反向傳播算法資源整理中,我列舉了幾個(gè)比較不錯(cuò)的資源。對(duì)于理論不熟悉而且學(xué)習(xí)精神的同學(xué)可以出門左轉(zhuǎn)去看看這篇文章的資源。這里假設(shè)讀者對(duì)于神經(jīng)網(wǎng)絡(luò)的基本理論有一定的了解。
?
?
神經(jīng)網(wǎng)絡(luò)的要素
?
在真正開始coding之前還是有必要交代一下神經(jīng)網(wǎng)絡(luò)基礎(chǔ),其實(shí)也就是設(shè)計(jì)類和寫程序的思路。簡(jiǎn)而言之,神經(jīng)網(wǎng)絡(luò)的包含幾大要素:
?
-
神經(jīng)元節(jié)點(diǎn)
-
層(layer)
-
權(quán)值(weights)
-
偏置項(xiàng)(bias)
?
神經(jīng)網(wǎng)絡(luò)的兩大計(jì)算過(guò)程分別是前向傳播和反向傳播過(guò)程。每層的前向傳播分別包含加權(quán)求和(卷積?)的線性運(yùn)算和激活函數(shù)的非線性運(yùn)算。反向傳播主要是用BP算法更新權(quán)值。 雖然里面還有很多細(xì)節(jié),但是對(duì)于作為第一篇的本文來(lái)說(shuō),以上內(nèi)容足夠了。
?
Net類的設(shè)計(jì)
?
Net類——基于Mat
?
神經(jīng)網(wǎng)絡(luò)中的計(jì)算幾乎都可以用矩陣計(jì)算的形式表示,這也是我用OpenCV的Mat類的原因之一,它提供了非常完善的、充分優(yōu)化過(guò)的各種矩陣運(yùn)算方法;另一個(gè)原因是我最熟悉的庫(kù)就是OpenCV......有很多比較好的庫(kù)和框架在實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)的時(shí)候會(huì)用很多類來(lái)表示不同的部分。比如Blob類表示數(shù)據(jù),Layer類表示各種層,Optimizer類來(lái)表示各種優(yōu)化算法。但是這里沒那么復(fù)雜,主要還是能力有限,只用一個(gè)Net類表示神經(jīng)網(wǎng)絡(luò)。
?
還是直接讓程序說(shuō)話,Net類包含在Net.h中,大致如下。
?
#ifndef NET_H
#define NET_H
#endif // NET_H
#pragma once
#include <iostream>
#include<opencv2\core\core.hpp>
#include<opencv2\highgui\highgui.hpp>
//#include<iomanip>
#include"Function.h"
namespace liu
{
? ? class Net
? ? {
? ? public:
? ? ? ? std::vector<int> layer_neuron_num;
? ? ? ? std::vector<cv::Mat> layer;
? ? ? ? std::vector<cv::Mat> weights;
? ? ? ? std::vector<cv::Mat> bias;
? ? public:
? ? ? ? Net() {};
? ? ? ? ~Net() {};
? ? ? ? //Initialize net:genetate weights matrices、layer matrices and bias matrices
? ? ? ? // bias default all zero
? ? ? ? void initNet(std::vector<int> layer_neuron_num_);
? ? ? ? //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();
? ? };
}
?
說(shuō)明:以上不是Net類的完整形態(tài),只是對(duì)應(yīng)于本文內(nèi)容的一個(gè)簡(jiǎn)化版,簡(jiǎn)化之后看起來(lái)會(huì)更加清晰明了。
?
成員變量與成員函數(shù)
?
成員變量與成員函數(shù)
?
現(xiàn)在Net類只有四個(gè)成員變量,分別是:
?
-
每一層神經(jīng)元數(shù)目(layer_neuron_num)
-
層(layer)
-
權(quán)值矩陣(weights)
-
偏置項(xiàng)(bias)
?
權(quán)值用矩陣表示就不用說(shuō)了,需要說(shuō)明的是,為了計(jì)算方便,這里每一層和偏置項(xiàng)也用Mat表示,每一層和偏置都用一個(gè)單列矩陣來(lái)表示。
?
Net類的成員函數(shù)除了默認(rèn)的構(gòu)造函數(shù)和析構(gòu)函數(shù),還有:
?
-
initNet():用來(lái)初始化神經(jīng)網(wǎng)絡(luò)
-
initWeights():初始化權(quán)值矩陣,調(diào)用initWeight()函數(shù)
-
initBias():初始化偏置項(xiàng)
-
forward():執(zhí)行前向運(yùn)算,包括線性運(yùn)算和非線性激活,同時(shí)計(jì)算誤差
-
backward():執(zhí)行反向傳播,調(diào)用updateWeights()函數(shù)更新權(quán)值。
?
這些函數(shù)已經(jīng)是神經(jīng)網(wǎng)絡(luò)程序核心中的核心。剩下的內(nèi)容就是慢慢實(shí)現(xiàn)了,實(shí)現(xiàn)的時(shí)候需要什么添加什么,逢山開路,遇河架橋。
?
神經(jīng)網(wǎng)絡(luò)初始化
?
initNet()函數(shù)
?
先說(shuō)一下initNet()函數(shù),這個(gè)函數(shù)只接受一個(gè)參數(shù)——每一層神經(jīng)元數(shù)目,然后借此初始化神經(jīng)網(wǎng)絡(luò)。這里所謂初始化神經(jīng)網(wǎng)絡(luò)的含義是:生成每一層的矩陣、每一個(gè)權(quán)值矩陣和每一個(gè)偏置矩陣。聽起來(lái)很簡(jiǎn)單,其實(shí)也很簡(jiǎn)單。
?
實(shí)現(xiàn)代碼在Net.cpp中。
?
這里生成各種矩陣沒啥難點(diǎn),唯一需要留心的是權(quán)值矩陣的行數(shù)和列數(shù)的確定。值得一提的是這里把權(quán)值默認(rèn)全設(shè)為0。
?
?
//Initialize net
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;
}
?
權(quán)值初始化
?
initWeight()函數(shù)
?
權(quán)值初始化函數(shù)initWeights()調(diào)用initWeight()函數(shù),其實(shí)就是初始化一個(gè)和多個(gè)的區(qū)別。
?
偏置初始化是給所有的偏置賦相同的值。這里用Scalar對(duì)象來(lái)給矩陣賦值。
?
//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對(duì)象來(lái)給矩陣賦值。
?
//Initialise the bias matrices.
void Net::initBias(cv::Scalar& bias_)
{
? ? ?for (int i = 0; i < bias.size(); i++)
? ? ?{
? ? ? ? ? bias[i] = bias_;
? ? ? }
}
?
至此,神經(jīng)網(wǎng)絡(luò)需要初始化的部分已經(jīng)全部初始化完成了。
?
初始化測(cè)試
?
我們可以用下面的代碼來(lái)初始化一個(gè)神經(jīng)網(wǎng)絡(luò),雖然沒有什么功能,但是至少可以測(cè)試下現(xiàn)在的代碼是否有BUG:
?
#include"../include/Net.h"
//<opencv2\opencv.hpp>
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;
}
?
親測(cè)沒有問題。
?
本文先到這里,前向傳播和反向傳播放在下一篇內(nèi)容里面。
?
源碼:所有的代碼都已經(jīng)托管在Github上面,感興趣的可以去下載查看:https://github.com/LiuXiaolong19920720/simple_net。
?
?
02
前向傳播與反向傳播
?
前言
?
前一篇文章C++實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)之壹—Net類的設(shè)計(jì)和神經(jīng)網(wǎng)絡(luò)的初始化中,大部分還是比較簡(jiǎn)單的。因?yàn)樽钪匾虑榫褪巧筛鞣N矩陣并初始化。神經(jīng)網(wǎng)絡(luò)中的重點(diǎn)和核心就是本文的內(nèi)容——前向和反向傳播兩大計(jì)算過(guò)程。每層的前向傳播分別包含加權(quán)求和(卷積?)的線性運(yùn)算和激活函數(shù)的非線性運(yùn)算。反向傳播主要是用BP算法更新權(quán)值。本文也分為兩部分介紹。
?
前向過(guò)程
?
前向過(guò)程簡(jiǎn)介
?
如前所述,前向過(guò)程分為線性運(yùn)算和非線性運(yùn)算兩部分。相對(duì)來(lái)說(shuō)比較簡(jiǎn)單。
線型運(yùn)算可以用Y = WX+b來(lái)表示,其中X是輸入樣本,這里即是第N層的單列矩陣,W是權(quán)值矩陣,Y是加權(quán)求和之后的結(jié)果矩陣,大小與N+1層的單列矩陣相同。b是偏置,默認(rèn)初始化全部為0。不難推知(鬼知道我推了多久!),W的大小是(N+1).rows * N.rows。正如上一篇中生成weights矩陣的代碼實(shí)現(xiàn)一樣:
?
weights[i].create(layer[i + 1].rows, layer[i].rows, CV_32FC1);?
?
非線性運(yùn)算可以用O=f(Y)來(lái)表示。Y就是上面得到的Y。O就是第N+1層的輸出。f就是我們一直說(shuō)的激活函數(shù)。激活函數(shù)一般都是非線性函數(shù)。它存在的價(jià)值就是給神經(jīng)網(wǎng)絡(luò)提供非線性建模能力。激活函數(shù)的種類有很多,比如sigmoid函數(shù),tanh函數(shù),ReLU函數(shù)等。各種函數(shù)的優(yōu)缺點(diǎn)可以參考更為專業(yè)的論文和其他更為專業(yè)的資料。
?
我們可以先來(lái)看一下前向函數(shù)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循環(huán)里面的兩句就分別是上面說(shuō)的線型運(yùn)算和激活函數(shù)的非線性運(yùn)算。
激活函數(shù)activationFunction()里面實(shí)現(xiàn)了不同種類的激活函數(shù),可以通過(guò)第二個(gè)參數(shù)來(lái)選取用哪一種。代碼如下:
?
//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;
}
?
各個(gè)函數(shù)更為細(xì)節(jié)的部分在Function.h和Function.cpp文件中。在此略去不表,感興趣的請(qǐng)君移步Github。
?
需要再次提醒的是,上一篇博客中給出的Net類是精簡(jiǎn)過(guò)的,下面可能會(huì)出現(xiàn)一些上一篇Net類里沒有出現(xiàn)過(guò)的成員變量。完整的Net類的定義還是在Github里。
?
反向傳播過(guò)程
?
反向傳播
?
反向傳播原理是鏈?zhǔn)角髮?dǎo)法則,其實(shí)就是我們高數(shù)中學(xué)的復(fù)合函數(shù)求導(dǎo)法則。這只是在推導(dǎo)公式的時(shí)候用的到。具體的推導(dǎo)過(guò)程我推薦看看下面這一篇教程,用圖示的方法,把前向傳播和反向傳播表現(xiàn)的清晰明了,強(qiáng)烈推薦!
?
Principles of training multi-layer neural network using backpropagation
?
一會(huì)將從這一篇文章中截取一張圖來(lái)說(shuō)明權(quán)值更新的代碼。在此之前,還是先看一下反向傳播函數(shù)backward()的代碼是什么樣的:
?
//Forward
void Net::backward()
{
? ? calcLoss(layer[layer.size() - 1], target, output_error, loss);
? ? deltaError();
? ? updateWeights();
}
?
可以看到主要是是三行代碼,也就是調(diào)用了三個(gè)函數(shù):
?
-
第一個(gè)函數(shù)calcLoss()計(jì)算輸出誤差和目標(biāo)函數(shù),所有輸出誤差平方和的均值作為需要最小化的目標(biāo)函數(shù)。
-
第二個(gè)函數(shù)deltaError()計(jì)算delta誤差,也就是下圖中delta1*df()那部分。
-
第三個(gè)函數(shù)updateWeights()更新權(quán)值,也就是用下圖中的公式更新權(quán)值。
?
下面是從前面強(qiáng)烈推薦的文章中截的一張圖:
就看下updateWeights()函數(shù)的代碼:
?
?
//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;
? ? ?}
}
?
核心的兩行代碼應(yīng)該還是能比較清晰反映上圖中的那個(gè)權(quán)值更新的公式的。圖中公式里的eta常被稱作學(xué)習(xí)率。訓(xùn)練神經(jīng)網(wǎng)絡(luò)調(diào)參的時(shí)候經(jīng)常要調(diào)節(jié)這貨。
?
計(jì)算輸出誤差和delta誤差的部分純粹是數(shù)學(xué)運(yùn)算,乏善可陳。但是把代碼貼在下面吧。
?
calcLoss()函數(shù)在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]);
? ? ? ? ? ?}
? ? ? ?}
}
?
注意:需要注意的就是計(jì)算的時(shí)候輸出層和隱藏層的計(jì)算公式是不一樣的。另一個(gè)需要注意的就是......難道大家沒覺得本系列文章的代碼看起來(lái)非常友好嗎至此,神經(jīng)網(wǎng)絡(luò)最核心的部分已經(jīng)實(shí)現(xiàn)完畢。剩下的就是想想該如何訓(xùn)練了。這個(gè)時(shí)候你如果愿意的話仍然可以寫一個(gè)小程序進(jìn)行幾次前向傳播和反向傳播。還是那句話,鬼知道我在能進(jìn)行傳播之前到底花了多長(zhǎng)時(shí)間調(diào)試!
?
源碼鏈接:https://github.com/LiuXiaolong19920720/simple_net
?
?
03
神經(jīng)網(wǎng)絡(luò)的訓(xùn)練和測(cè)試
?
前言
?
在之前的文章中我們已經(jīng)實(shí)現(xiàn)了Net類的設(shè)計(jì)和前向傳播和反向傳播的過(guò)程。可以說(shuō)神經(jīng)網(wǎng)絡(luò)的核心的部分已經(jīng)完成。接下來(lái)就是應(yīng)用層面了。
?
要想利用神經(jīng)網(wǎng)絡(luò)解決實(shí)際的問題,比如說(shuō)進(jìn)行手寫數(shù)字的識(shí)別,需要用神經(jīng)網(wǎng)絡(luò)對(duì)樣本進(jìn)行迭代訓(xùn)練,訓(xùn)練完成之后,訓(xùn)練得到的模型是好是壞,我們需要對(duì)之進(jìn)行測(cè)試。這正是我們現(xiàn)在需要實(shí)現(xiàn)的部分的內(nèi)容。
?
完善后的Net類
?
需要知道的是現(xiàn)在的Net類已經(jīng)相對(duì)完善了,為了實(shí)現(xiàn)接下來(lái)的功能,不論是成員變量還是成員函數(shù)都變得更加的豐富。現(xiàn)在的Net類看起來(lái)是下面的樣子:
?
class Net
{
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<cv::Mat> layer;
? ? ?std::vector<cv::Mat> weights;
? ? ?std::vector<cv::Mat> bias;
? ? ?std::vector<cv::Mat> delta_err;
?
? ? ?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<int> layer_neuron_num_);
?
? ? ?//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();
};
?
可以看到已經(jīng)有了訓(xùn)練的函數(shù)train()、測(cè)試的函數(shù)test(),還有實(shí)際應(yīng)用訓(xùn)練好的模型的predict()函數(shù),以及保存和加載模型的函數(shù)save()和load()。大部分成員變量和成員函數(shù)應(yīng)該還是能夠通過(guò)名字就能夠知道其功能的。
?
訓(xùn)練
?
訓(xùn)練函數(shù)train()
?
本文重點(diǎn)說(shuō)的是訓(xùn)練函數(shù)train()和測(cè)試函數(shù)test()。這兩個(gè)函數(shù)接受輸入(input)和標(biāo)簽(或稱為目標(biāo)值target)作為輸入?yún)?shù)。其中訓(xùn)練函數(shù)還要接受一個(gè)閾值作為迭代終止條件,最后一個(gè)函數(shù)可以暫時(shí)忽略不計(jì),那是選擇要不要把loss值實(shí)時(shí)畫出來(lái)的標(biāo)識(shí)。
?
訓(xùn)練的過(guò)程如下:
?
1. 接受一個(gè)樣本(即一個(gè)單列矩陣)作為輸入,也即神經(jīng)網(wǎng)絡(luò)的第一層;
2. 進(jìn)行前向傳播,也即forward()函數(shù)做的事情。然后計(jì)算loss;
3. 如果loss值小于設(shè)定的閾值loss_threshold,則進(jìn)行反向傳播更新閾值;
4. 重復(fù)以上過(guò)程直到loss小于等于設(shè)定的閾值。
?
train函數(shù)的實(shí)現(xiàn)如下:
?
//Train,use loss_threshold
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;
? ? ? }
}
?
這里考慮到了用單個(gè)樣本和多個(gè)樣本迭代訓(xùn)練兩種情況。而且還有另一種不用loss閾值作為迭代終止條件,而是用正確率的train()函數(shù),內(nèi)容大致相同,此處略去不表。
?
在經(jīng)過(guò)train()函數(shù)的訓(xùn)練之后,就可以得到一個(gè)模型了。所謂模型,可以簡(jiǎn)單的認(rèn)為就是權(quán)值矩陣。簡(jiǎn)單的說(shuō),可以把神經(jīng)網(wǎng)絡(luò)當(dāng)成一個(gè)超級(jí)函數(shù)組合,我們姑且認(rèn)為這個(gè)超級(jí)函數(shù)就是y = f(x) = ax +b。那么權(quán)值就是a和b。反向傳播的過(guò)程是把a和b當(dāng)成自變量來(lái)處理的,不斷調(diào)整以得到最優(yōu)值或逼近最優(yōu)值。在完成反向傳播之后,訓(xùn)練得到了參數(shù)a和b的最優(yōu)值,是一個(gè)固定值了。這時(shí)自變量又變回了x。我們希望a、b最優(yōu)值作為已知參數(shù)的情況下,對(duì)于我們的輸入樣本x,通過(guò)神經(jīng)網(wǎng)絡(luò)計(jì)算得到的結(jié)果y,與實(shí)際結(jié)果相符合是大概率事件。
?
測(cè)試
?
測(cè)試函數(shù)test()
?
test()函數(shù)的作用就是用一組訓(xùn)練時(shí)沒用到的樣本,對(duì)訓(xùn)練得到的模型進(jìn)行測(cè)試,把通過(guò)這個(gè)模型得到的結(jié)果與實(shí)際想要的結(jié)果進(jìn)行比較,看正確來(lái)說(shuō)到底是多少,我們希望正確率越多越好。
?
test()的步驟大致如下幾步:
?
1. 用一組樣本逐個(gè)輸入神經(jīng)網(wǎng)絡(luò);
2. 通過(guò)前向傳播得到一個(gè)輸出值;
3. 比較實(shí)際輸出與理想輸出,計(jì)算正確率。
?
test()函數(shù)的實(shí)現(xiàn)如下:
?
//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;
? ? ? }
}
?
這里在進(jìn)行前向傳播的時(shí)候不是直接調(diào)用forward()函數(shù),而是調(diào)用了predict_one()函數(shù),predict函數(shù)的作用是給定一個(gè)輸入,給出想要的輸出值。其中包含了對(duì)forward()函數(shù)的調(diào)用。還有就是對(duì)于神經(jīng)網(wǎng)絡(luò)的輸出進(jìn)行解析,轉(zhuǎn)換成看起來(lái)比較方便的數(shù)值。
?
這一篇的內(nèi)容已經(jīng)夠多了,我決定把對(duì)于predict部分的解釋放到下一篇。
?
源碼鏈接:https://github.com/LiuXiaolong19920720/simple_net
?
?
04
神經(jīng)網(wǎng)絡(luò)的預(yù)測(cè)和輸入輸出解析
?
神經(jīng)網(wǎng)絡(luò)的預(yù)測(cè)
?
預(yù)測(cè)函數(shù)predict()
?
在上一篇的結(jié)尾提到了神經(jīng)網(wǎng)絡(luò)的預(yù)測(cè)函數(shù)predict(),說(shuō)道predict調(diào)用了forward函數(shù)并進(jìn)行了輸出的解析,輸出我們看起來(lái)比較方便的值。
?
predict()函數(shù)和predict_one()函數(shù)的區(qū)別相信很容易從名字看出來(lái),那就是輸入一個(gè)樣本得到一個(gè)輸出和輸出一組樣本得到一組輸出的區(qū)別,顯然predict()應(yīng)該是循環(huán)調(diào)用predict_one()實(shí)現(xiàn)的。所以我們先看一下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;
? ? ? ? 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;
? ? ? ?}
}
?
可以在第二個(gè)if語(yǔ)句里面看到最主要的內(nèi)容就是兩行:分別是前面提到的前向傳播和輸出解析。
?
forward();
...
...
minMaxLoc(layer_out, NULL, NULL, NULL, &predict_maxLoc, cv::noArray());
?
前向傳播得到最后一層輸出層layer_out,然后從layer_out中提取最大值的位置,最后輸出位置的y坐標(biāo)。
?
輸出的組織方式和解析
?
之所以這么做,就不得不提一下標(biāo)簽或者叫目標(biāo)值在這里是以何種形式存在的。以激活函數(shù)是sigmoid函數(shù)為例,sigmoid函數(shù)是把實(shí)數(shù)映射到[0,1]區(qū)間,所以顯然最后的輸出y:0<=y<=1。如果激活函數(shù)是tanh函數(shù),則輸出區(qū)間是[-1,1]。如果是sigmoid,而且我們要進(jìn)行手寫字體識(shí)別的話,需要識(shí)別的數(shù)字一共有十個(gè):0-9。顯然我們的神經(jīng)網(wǎng)絡(luò)沒有辦法輸出大于1的值,所以也就不能直觀的用0-9幾個(gè)數(shù)字來(lái)作為神經(jīng)網(wǎng)絡(luò)的實(shí)際目標(biāo)值或者稱之為標(biāo)簽。
?
這里采用的方案是,把輸出層設(shè)置為一個(gè)單列十行的矩陣,標(biāo)簽是幾就把第幾行的元素設(shè)置為1,其余都設(shè)為0。由于編程中一般都是從0開始作為第一位的,所以位置與0-9的數(shù)字正好一一對(duì)應(yīng)。我們到時(shí)候只需要找到輸出最大值所在的位置,也就知道了輸出是幾。
?
當(dāng)然上面說(shuō)的是激活函數(shù)是sigmoid的情況。如果是tanh函數(shù)呢?那還是是幾就把第幾位設(shè)為1,而其他位置全部設(shè)為-1即可。
?
如果是ReLU函數(shù)呢?ReLU函數(shù)的至于是0到正無(wú)窮。所以我們可以標(biāo)簽是幾就把第幾位設(shè)為幾,其他為全設(shè)為0。最后都是找到最大值的位置即可。
?
這些都是需要根據(jù)激活函數(shù)來(lái)定。代碼中是調(diào)用opencv的minMaxLoc()函數(shù)來(lái)尋找矩陣中最大值的位置。
?
輸入的組織方式和讀取方法
?
既然說(shuō)到了輸出的組織方式,那就順便也提一下輸入的組織方式。生成神經(jīng)網(wǎng)絡(luò)的時(shí)候,每一層都是用一個(gè)單列矩陣來(lái)表示的。顯然第一層輸入層就是一個(gè)單列矩陣。所以在對(duì)數(shù)據(jù)進(jìn)行預(yù)處理的過(guò)程中,這里就是把輸入樣本和標(biāo)簽一列一列地排列起來(lái),作為矩陣存儲(chǔ)。標(biāo)簽矩陣的第一列即是第一列樣本的標(biāo)簽。以此類推。
?
值得一提的是,輸入的數(shù)值全部歸一化到0-1之間。
?
由于這里的數(shù)值都是以float類型保存的,這種數(shù)值的矩陣Mat不能直接保存為圖片格式,所以這里我選擇了把預(yù)處理之后的樣本矩陣和標(biāo)簽矩陣保存到xml文檔中。在源碼中可以找到把原始的csv文件轉(zhuǎn)換成xml文件的代碼。在csv2xml.cpp中。而我轉(zhuǎn)換完成的MNIST的部分?jǐn)?shù)據(jù)保存在data文件夾中,可以在Github上找到。
?
在opencv中xml的讀寫非常方便,如下代碼是寫入數(shù)據(jù):
?
string filename = "input_label.xml";
FileStorage fs(filename, FileStorage::WRITE);
fs << "input" << input_normalized;
fs << "target" << target_; // Write cv::Mat
fs.release();
?
而讀取代碼的一樣簡(jiǎn)單明了:
?
cv::FileStorage fs;
fs.open(filename, cv::FileStorage::READ);
cv::Mat input_, target_;
fs["input"] >> input_;
fs["target"] >> target_;
fs.release();
?
讀取樣本和標(biāo)簽
?
我寫了一個(gè)函數(shù)get_input_label()從xml文件中從指定的列開始提取一定數(shù)目的樣本和標(biāo)簽。默認(rèn)從第0列開始讀取,只是上面函數(shù)的簡(jiǎn)單封裝:
?
//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));
}
?
至此其實(shí)已經(jīng)可以開始實(shí)踐,訓(xùn)練神經(jīng)網(wǎng)絡(luò)識(shí)別手寫數(shù)字了。只有一部分還沒有提到,那就是模型的保存和加載。下一篇將會(huì)講模型的save和load,然后就可以實(shí)際開始進(jìn)行例子的訓(xùn)練了。等不及的小伙伴可以直接去github下載完整的程序開始跑了。
?
源碼鏈接:https://github.com/LiuXiaolong19920720/simple_net
?
?
05
模型的保存和加載及實(shí)時(shí)畫出輸出曲線
?
模型的保存和加載
?
在我們完成對(duì)神經(jīng)網(wǎng)絡(luò)的訓(xùn)練之后,一般要把模型保存起來(lái)。不然每次使用模型之前都需要先訓(xùn)練模型,對(duì)于data hungry的神經(jīng)網(wǎng)絡(luò)來(lái)說(shuō),視數(shù)據(jù)多寡和精度要求高低,訓(xùn)練一次的時(shí)間從幾分鐘到數(shù)百個(gè)小時(shí)不等,這是任何人都耗不起的。把訓(xùn)練好的模型保存下來(lái),當(dāng)需要使用它的時(shí)候,只需要加載就行了。
現(xiàn)在需要考慮的一個(gè)問題是,保存模型的時(shí)候,我們到底要保存哪些東西?
?
之前有提到,可以簡(jiǎn)單的認(rèn)為權(quán)值矩陣就是所謂模型。所以權(quán)值矩陣一定要保存。除此之外呢?不能忘記的一點(diǎn)是,我們保存模型是為了加載后能使用模型。顯然要求加載模型之后,輸入一個(gè)或一組樣本就能開始前向運(yùn)算和反向傳播。這也就是說(shuō),之前實(shí)現(xiàn)的時(shí)候,forward()之前需要的,這里也都需要,只是權(quán)值不是隨意初始化了,而是用訓(xùn)練好的權(quán)值矩陣代替。基于以上考慮,最終決定要保存的內(nèi)容如下4個(gè):
?
1. layer_neuron_num,各層神經(jīng)元數(shù)目,這是生成神經(jīng)網(wǎng)絡(luò)需要的唯一參數(shù)。
2. weights,神經(jīng)網(wǎng)絡(luò)初始化之后需要用訓(xùn)練好的權(quán)值矩陣去初始化權(quán)值。
3. activation_function,使用神經(jīng)網(wǎng)絡(luò)的過(guò)程其實(shí)就是前向計(jì)算的過(guò)程,顯然需要知道激活函數(shù)是什么。
4. learning_rate,如果要在現(xiàn)有模型的基礎(chǔ)上繼續(xù)訓(xùn)練以得到更好的模型,更新權(quán)值的時(shí)候需要用到這個(gè)函數(shù)。
?
再?zèng)Q定了需要保存的內(nèi)容之后,接下來(lái)就是實(shí)現(xiàn)了,仍然是保存為xml格式,上一篇已經(jīng)提到了保存和加載xml是多么的方便:
?
//Save model;
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();
}
?
?
實(shí)時(shí)畫出輸出曲線
?
實(shí)時(shí)畫曲線
?
有時(shí)候我們?yōu)榱擞幸粋€(gè)直觀的觀察,我們希望能夠是實(shí)時(shí)的用一個(gè)曲線來(lái)表示輸出誤差。但是沒有找到滿意的程序可用,于是自己就寫了一個(gè)非常簡(jiǎn)單的函數(shù),用來(lái)實(shí)時(shí)輸出訓(xùn)練時(shí)的loss。理想的輸出大概像下面這樣:
?
?
為什么說(shuō)是理想的輸出呢,因?yàn)橐话銇?lái)說(shuō)誤差很小,可能曲線直接就是從左下角開始的,上面一大片都沒有用到。不過(guò)已經(jīng)能夠看出loss的大致走向了。
?
這個(gè)函數(shù)的實(shí)現(xiàn)其實(shí)就是先畫倆個(gè)作為坐標(biāo)用的直線,然后把相鄰點(diǎn)用直線連接起來(lái):
?
//Draw loss curve
void draw_curve(cv::Mat& board, std::vector<double> points)
{
? ? 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);
}
?
至此,神經(jīng)網(wǎng)絡(luò)已經(jīng)實(shí)現(xiàn)完成了。完整的代碼可以在Github上找到。下一步,就是要用編寫的神經(jīng)網(wǎng)絡(luò),用實(shí)際樣本開始訓(xùn)練了。下一篇,用MNIST數(shù)據(jù)訓(xùn)練神經(jīng)網(wǎng)絡(luò)。
?
源碼鏈接:https://github.com/LiuXiaolong19920720/simple_net
?
?
06
實(shí)戰(zhàn)手寫數(shù)字識(shí)別
?
之前的五篇博客講述的內(nèi)容應(yīng)該覆蓋了如何編寫神經(jīng)網(wǎng)絡(luò)的大部分內(nèi)容,在經(jīng)過(guò)之前的一系列努力之后,終于可以開始實(shí)戰(zhàn)了。試試寫出來(lái)的神經(jīng)網(wǎng)絡(luò)怎么樣吧。
?
數(shù)據(jù)準(zhǔn)備
?
MNIST數(shù)據(jù)集
?
有人說(shuō)MNIST手寫數(shù)字識(shí)別是機(jī)器學(xué)習(xí)領(lǐng)域的Hello World,所以我這一次也是從手寫字體識(shí)別開始。我是從Kaggle找的手寫數(shù)字識(shí)別的數(shù)據(jù)集。數(shù)據(jù)已經(jīng)被保存為csv格式,相對(duì)比較方便讀取。
?
數(shù)據(jù)集包含了數(shù)字0-9是個(gè)數(shù)字的灰度圖。但是這個(gè)灰度圖是展開過(guò)的。展開之前都是28x28的圖像,展開后成為1x784的一行。csv文件中,每一行有785個(gè)元素,第一個(gè)元素是數(shù)字標(biāo)簽,后面的784個(gè)元素分別排列著展開后的184個(gè)像素。看起來(lái)像下面這樣:
?
?
也許你已經(jīng)看到了第一列0-9的標(biāo)簽,但是會(huì)疑惑為啥像素值全是0,那是因?yàn)檫@里能顯示出來(lái)的,甚至不足28x28圖像的一行。而數(shù)字一般應(yīng)該在圖像中心位置,所以邊緣位置當(dāng)然是啥也沒有,往后滑動(dòng)就能看到非零像素值了。像下面這樣:
?
?
這里需要注意到的是,像素值的范圍是0-255。一般在數(shù)據(jù)預(yù)處理階段都會(huì)歸一化,全部除以255,把值轉(zhuǎn)換到0-1之間。
?
csv文件中包含42000個(gè)樣本,這么多樣本,對(duì)于我七年前買的4000元級(jí)別的破筆記本來(lái)說(shuō),單單是讀取一次都得半天,更不要提拿這么多樣本去迭代訓(xùn)練了,簡(jiǎn)直是噩夢(mèng)(兼論一個(gè)苦逼的學(xué)生幾年能掙到換電腦的錢!)。所以我只是提取了前1000個(gè)樣本,然后把歸一化后的樣本和標(biāo)簽都保存到一個(gè)xml文件中。在前面的一篇博客中已經(jīng)提到了輸入輸出的組織形式,偷懶直接復(fù)制了。
?
既然說(shuō)到了輸出的組織方式,那就順便也提一句輸入的組織方式。生成神經(jīng)網(wǎng)絡(luò)的時(shí)候,每一層都是用一個(gè)單列矩陣來(lái)表示的。顯然第一層輸入層就是一個(gè)單列矩陣。所以在對(duì)數(shù)據(jù)進(jìn)行預(yù)處理的過(guò)程中,我就是把輸入樣本和標(biāo)簽一列一列地排列起來(lái),作為矩陣存儲(chǔ)。標(biāo)簽矩陣的第一列即是第一列樣本的標(biāo)簽。以此類推。
?
把輸出層設(shè)置為一個(gè)單列十行的矩陣,標(biāo)簽是幾就把第幾行的元素設(shè)置為1,其余都設(shè)為0。由于編程中一般都是從0開始作為第一位的,所以位置與0-9的數(shù)字正好一一對(duì)應(yīng)。我們到時(shí)候只需要找到輸出最大值所在的位置,也就知道了輸出是幾。”
?
這里只是重復(fù)一下,這一部分的代碼在csv2xml.cpp中:
?
#include<opencv2\opencv.hpp>
#include<iostream>
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<float>(label_num, i) = 1.;
? ? ? ? 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<double>(i, j) >= 1.)
? ? ? ? ? ? //{
? ? ? ? ? ? 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;
}
?
這是我最近用ReLU的時(shí)候的代碼,標(biāo)簽是幾就把第幾位設(shè)為幾,其他為全設(shè)為0。最后都是找到最大值的位置即可。
?
在代碼中Mat digit的作用是,檢驗(yàn)下轉(zhuǎn)換后的矩陣和標(biāo)簽是否對(duì)應(yīng)正確這里是把col(3),也就是第四個(gè)樣本從一行重新變成28x28的圖像,看上面的第一張圖的第一列可以看到,第四個(gè)樣本的標(biāo)簽是4。那么它轉(zhuǎn)換回來(lái)的圖像時(shí)什么樣呢?是下面這樣:
?
?
這里也證明了為啥第一張圖看起來(lái)像素全是0。邊緣全黑能不是0嗎?
?
然后在使用的時(shí)候用前面提到過(guò)的get_input_label()獲取一定數(shù)目的樣本和標(biāo)簽。
?
實(shí)戰(zhàn)數(shù)字識(shí)別
?
實(shí)戰(zhàn)
?
沒想到前面數(shù)據(jù)處理說(shuō)了那么多。。
?
廢話少說(shuō),直接說(shuō)訓(xùn)練的過(guò)程:
?
1. 給定每層的神經(jīng)元數(shù)目,初始化神經(jīng)網(wǎng)絡(luò)和權(quán)值矩陣
2. 從inputlabel1000.xml文件中取前800個(gè)樣本作為訓(xùn)練樣本,后200作為測(cè)試樣本。
3. 這是神經(jīng)網(wǎng)絡(luò)的一些參數(shù):訓(xùn)練時(shí)候的終止條件,學(xué)習(xí)率,激活函數(shù)類型
4. 前800樣本訓(xùn)練神經(jīng)網(wǎng)絡(luò),直到滿足loss小于閾值loss_threshold,停止。
5. 后200樣本測(cè)試神經(jīng)網(wǎng)絡(luò),輸出正確率。
6. 保存訓(xùn)練得到的模型。
?
以sigmoid為激活函數(shù)的訓(xùn)練代碼如下:
?
#include"../include/Net.h"
//<opencv2\opencv.hpp>
?
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;
?
}
?
對(duì)比前面說(shuō)的六個(gè)過(guò)程,代碼應(yīng)該是很清晰的了。參數(shù)output_interval是間隔幾次迭代輸出一次,這設(shè)置為迭代兩次輸出一次。
?
如果按照上面的參數(shù)來(lái)訓(xùn)練,正確率是0.855:
?
?
在只有800個(gè)樣本的情況下,這個(gè)正確率我認(rèn)為還是可以接受的。
?
如果要直接使用訓(xùn)練好的樣本,那就更加簡(jiǎn)單了:
?
//Get test samples and the label is 0--1
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;
?
如果激活函數(shù)是tanh函數(shù),由于tanh函數(shù)的值域是[-1,1],所以在訓(xùn)練的時(shí)候要把標(biāo)簽矩陣稍作改動(dòng),需要改動(dòng)的地方如下:
?
//Set loss threshold,learning rate and activation function
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;
?
這里不光改了標(biāo)簽,還有幾個(gè)參數(shù)也是需要改以下的,學(xué)習(xí)率比sigmoid的時(shí)候要小一個(gè)量級(jí),效果會(huì)比較好。這樣訓(xùn)練出來(lái)的正確率大概在0.88左右,也是可以接受的。
?
?
源碼鏈接:https://github.com/LiuXiaolong19920720/simple_net
?
?
?
熱文精選
?
AI領(lǐng)域真正最最最最最稀缺的人才是……會(huì)解牛的那個(gè)庖丁
2018年了,但愿你還有被剝削的價(jià)值!因?yàn)锳I失業(yè)潮真的開始了...
2018 年了,該不該下定決心轉(zhuǎn)型AI呢?
不用數(shù)學(xué)也能講清貝葉斯理論的馬爾可夫鏈蒙特卡洛方法?這篇文章做到了
盤點(diǎn)深度學(xué)習(xí)一年來(lái)在文本、語(yǔ)音和視覺等方向的進(jìn)展,看強(qiáng)化學(xué)習(xí)如何無(wú)往而不利
先搞懂這八大基礎(chǔ)概念,再談機(jī)器學(xué)習(xí)入門!
這三個(gè)普通程序員,幾個(gè)月就成功轉(zhuǎn)型AI,他們的經(jīng)驗(yàn)是...
干貨 | AI 工程師必讀,從實(shí)踐的角度解析一名合格的AI工程師是怎樣煉成的
AI校招程序員最高薪酬曝光!騰訊80萬(wàn)年薪領(lǐng)跑,還送北京戶口
詳解 | 如何用Python實(shí)現(xiàn)機(jī)器學(xué)習(xí)算法
?
總結(jié)
以上是生活随笔為你收集整理的c++从零实现神经网络的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2016a迈特莱布
- 下一篇: doxygen注释规范示例(C++)