1. 程式人生 > >C++從零實現深度神經網路之六——實戰手寫數字識別(sigmoid和tanh)

C++從零實現深度神經網路之六——實戰手寫數字識別(sigmoid和tanh)

本文由@星沉閣冰不語出品,轉載請註明作者和出處。

之前的五篇部落格講述的內容應該覆蓋瞭如何編寫神經網路的大部分內容,在經過之前的一系列努力之後,終於可以開始實戰了。試試寫出來的神經網路怎麼樣吧。

一、資料準備

有人說MNIST手寫數字識別是機器學習領域的Hello World,所以我這一次也是從手寫字型識別開始。我是從Kaggle找的手寫數字識別的資料集。資料已經被儲存為csv格式,相對比較方便讀取。

資料集包含了數字0-9是個數字的灰度圖。但是這個灰度圖是展開過的。展開之前都是28x28的影象,展開後成為1x784的一行。csv檔案中,每一行有785個元素,第一個元素是數字標籤,後面的784個元素分別排列著展開後的184個畫素。看起來像下面這樣:


也許你已經看到了第一列0-9的標籤,但是會疑惑為啥畫素值全是0,那是因為這裡能顯示出來的,甚至不足28x28影象的一行。而數字一般應該在影象中心位置,所以邊緣位置當然是啥也沒有,往後滑動就能看到非零畫素值了。像下面這樣:


這裡需要注意到的是,畫素值的範圍是0-255。一般在資料預處理階段都會歸一化,全部除以255,把值轉換到0-1之間。

csv檔案中包含42000個樣本,這麼多樣本,對於我六年前買的4000元級別的破筆記本來說,單單是讀取一次都得半天,更不要提拿這麼多樣本去迭代訓練了,簡直是噩夢(兼論一個苦逼的學生幾年能掙到換電腦的錢!)所以我只是提取了前1000個樣本,然後把歸一化後的樣本和標籤都儲存到一個xml檔案中。在前面的一篇部落格中已經提到了輸入輸出的組織形式,偷懶直接複製了:

“既然說到了輸出的組織方式,那就順便也提一句輸入的組織方式。生成神經網路的時候,每一層都是用一個單列矩陣來表示的。顯然第一層輸入層就是一個單列矩陣。所以在對資料進行預處理的過程中,我就是把輸入樣本和標籤一列一列地排列起來,作為矩陣儲存。標籤矩陣的第一列即是第一列樣本的標籤。以此類推。”

把輸出層設定為一個單列十行的矩陣,標籤是幾就把第幾行的元素設定為1,其餘都設為0。由於程式設計中一般都是從0開始作為第一位的,所以位置與0-9的數字正好一一對應。我們到時候只需要找到輸出最大值所在的位置,也就知道了輸出是幾。這裡只是重複一下,這一部分的程式碼在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的時候的程式碼,標籤是幾就把第幾位設為幾,其他為全設為0。最後都是找到最大值的位置即可。

在程式碼中Mat digit的作用是,檢驗下轉換後的矩陣和標籤是否對應正確這裡是把col(3),也就是第四個樣本從一行重新變成28x28的影象,看上面的第一張圖的第一列可以看到,第四個樣本的標籤是4。那麼它轉換回來的影象時什麼樣呢?是下面這樣:


這裡也證明了為啥第一張圖看起來畫素全是0。邊緣全黑能不是0嗎?

然後在使用的時候用前面提到過的get_input_label()獲取一定數目的樣本和標籤。

二、實戰數字識別

沒想到前面資料處理說了那麼多。。。。

廢話少說,直接說訓練的過程:

1.給定每層的神經元數目,初始化神經網路和權值矩陣

2.從input_label_1000.xml檔案中取前800個樣本作為訓練樣本,後200作為測試樣本。

3.這是神經網路的一些引數:訓練時候的終止條件,學習率,啟用函式型別

4.前800樣本訓練神經網路,直到滿足loss小於閾值loss_threshold,停止。

5.後200樣本測試神經網路,輸出正確率。

6.儲存訓練得到的模型。

以sigmoid為啟用函式的訓練程式碼如下:

#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;

}

對比前面說的六個過程,程式碼應該是很清晰的了。引數output_interval是間隔幾次迭代輸出一次,這設定為迭代兩次輸出一次。

如果按照上面的引數來訓練,正確率是0.855:


在只有800個樣本的情況下,這個正確率我認為還是可以接受的。

如果要直接使用訓練好的樣本,那就更加簡單了:

	//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;

如果啟用函式是tanh函式,由於tanh函式的值域是[-1,1],所以在訓練的時候要把標籤矩陣稍作改動,需要改動的地方如下:

	//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;

這裡不光改了標籤,還有幾個引數也是需要改以下的,學習率比sigmoid的時候要小一個量級,效果會比較好。這樣訓練出來的正確率大概在0.88左右,也是可以接受的。


最後,程式碼和資料可以在Github下載。