C++實現神經網路之四—神經網路的預測和輸入輸出的解析

本文首發於公眾號【CVPy】:

元旦贈書 | C++實現神經網路之四-神經網路的預測和輸入輸出的解析mp.weixin.qq.com

在上一篇的結尾提到了神經網路的預測函數predict(),說道predict調用了forward函數並進行了輸出的解析,輸出我們看起來比較方便的值。

神經網路的預測函數predict()

predict()函數和predict_one()函數的區別相信很容易從名字看出來,那就是輸入一個樣本得到一個輸出和輸出一組樣本得到一組輸出的區別,顯然predict()應該是循環調用predict_one()實現的。所以我們先看一下predict_one()的代碼:

int Net::predict_one(cv::Mat &input)n {n if (input.empty())n {n std::cout << "Input is empty!" << std::endl;n return -1;n }nn if (input.rows == (layer[0].rows) && input.cols == 1)n {n layer[0] = input;n forward();nn cv::Mat layer_out = layer[layer.size() - 1];n cv::Point predict_maxLoc;nn minMaxLoc(layer_out, NULL, NULL, NULL, &predict_maxLoc, cv::noArray());n return predict_maxLoc.y;n }n elsen {n std::cout << "Please give one sample alone and ensure input.rows = layer[0].rows" << std::endl;n return -1;n }n }n

可以在第二個if語句裡面看到最主要的內容就是兩行:

forward();n ...n ...n minMaxLoc(layer_out, NULL, NULL, NULL, &predict_maxLoc, cv::noArray());n

分別是前面提到的前向傳播和輸出解析。

前向傳播得到最後一層輸出層layerout,然後從layerout中提取最大值的位置,最後輸出位置的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";n FileStorage fs(filename, FileStorage::WRITE);n fs << "input" << input_normalized;n fs << "target" << target_; // Write cv::Matn fs.release();n

而讀取代碼的一樣簡單明了:

cv::FileStorage fs;n fs.open(filename, cv::FileStorage::READ);n cv::Mat input_, target_;n fs["input"] >> input_;n fs["target"] >> target_;n fs.release();n

我寫了一個函數get_input_label()從xml文件中從指定的列開始提取一定數目的樣本和標籤。默認從第0列開始讀取,只是上面函數的簡單封裝:

//Get sample_number samples in XML file,from the start column. n void get_input_label(std::string filename, cv::Mat& input, cv::Mat& label, int sample_num, int start)n {n cv::FileStorage fs;n fs.open(filename, cv::FileStorage::READ);n cv::Mat input_, target_;n fs["input"] >> input_;n fs["target"] >> target_;n fs.release();n input = input_(cv::Rect(start, 0, sample_num, input_.rows));n label = target_(cv::Rect(start, 0, sample_num, target_.rows));n }n

至此其實已經可以開始實踐,訓練神經網路識別手寫數字了。只有一部分還沒有提到,那就是模型的保存和載入。下一篇將會講模型的save和load,然後就可以實際開始進行例子的訓練了。等不及的小夥伴可以直接去github下載完整的程序開始跑了。

源碼鏈接

神經網路源碼的Github鏈接:

LiuXiaolong19920720/simple_netgithub.com圖標
推薦閱讀:

《C++ Primer》讀書筆記-第十一章 03 關聯容器操作
當刷題遇見吐槽和涼席椅墊,完美!
15個C++項目列表
說說 C++ 的 Concept
[譯] C++中帶狀態元編程黑科技(二):實現常量表達式計數器

TAG:OpenCV | 神经网络 | C |