Opencv人工神經網路實現字母與數字識別流程
人工神經網路簡介
人工神經網路(Artificial Neural Network,ANN)簡稱神經網路(NN),是基於生物學中神經網路的基本原理,在理解和抽象了人腦結構和外界刺激響應機制後,以網路拓撲知識為理論基礎,模擬人腦的神經系統對複雜資訊的處理機制的一種數學模型。神經網路是一種運算模型,由大量的節點(或稱神經元)之間相互聯接構成。每個節點代表一種特定的輸出函式,稱為啟用函式(activation function)。每兩個節點間的連線都代表一個對於通過該連線訊號的加權值,稱之為權重(weight),神經網路就是通過這種方式來模擬人類的記憶。網路的輸出則取決於網路的結構、網路的連線方式、權重和啟用函式。
人工神經元/神經網路模型
左邊幾個灰底圓中所標字母w代表浮點數,稱為權重(weight,或權值,權數)。進入人工神經細胞的每一個input(輸入)都與一個權重w相聯絡,正是這些權重將決定神經網路的整體活躍性。你現在暫時可以設想所有這些權重都被設定到了-1和1之間的一個隨機小數。因為權重可正可負,故能對與它關聯的輸入施加不同的影響,如果權重為正,就會有激發(excitory)作用,權重為負,則會有抑制(inhibitory)作用。當輸入訊號進入神經細胞時,它們的值將與它們對應的權重相乘,作為圖中大圓的輸入。大圓的‘核’是一個函式,叫激勵函式(activation
function),它把所有這些新的、經過權重調整後的輸入全部加起來,形成單個的激勵值(activation value)。激勵值也是一浮點數,且同樣可正可負。然後,再根據激勵值來產生函式的輸出也即神經細胞的輸出:如果激勵值超過某個閥值(作為例子我們假設閥值為1.0),就會產生一個值為1的訊號輸出;如果激勵值小於閥值1.0,則輸出一個0。這是人工神經細胞激勵函式的一種最簡單的型別。在這裡,從激勵值產生輸出值是一個階躍函式.
字元特徵提取
在深度學習(將特徵提取作為訓練的一部分)這個概念引入之前,一般在準備分類器進行識別之前都需要進行特徵提取。因為一幅影象包含的內容太多,有些資訊能區分差異性,而有些資訊卻代表了共性。所以我們要進行適當的特徵提取把它們之間的差異性特徵提取出來。
這裡面我們計算二種簡單的字元特徵:梯度分佈特徵、灰度統計特徵。這兩個特徵只是配合本篇文章來說明神經網路的普遍用法,實際中進行字元識別需要考慮的字元特徵遠遠要比這複雜,還包括相似字特徵的選取等,也由於工作上的原因,這一部分並不深入的介紹。
1,首先是梯度分佈特徵,該特徵計算影象水平方向和豎直方向的梯度影象,然後通過給梯度影象分劃不同的區域,進行梯度影象每個區域亮度值的統計,以下是演算法步驟:
<1>將字元由RGB轉化為灰度,然後將影象歸一化到16*8。
<2>定義soble水平檢測運算元: 和豎直方向梯度檢測運算元 。
<3>對影象分別用 和 進行影象濾波得到 和 ,下圖分別代表原影象、 和 。
<4>對濾波後的影象,計算影象總的畫素和,然後劃分4*2的網路,計算每個網格內的畫素值的總和。
<5>將每個網路內總灰度值佔整個影象的百分比統計在一起寫入一個向量,將兩個方向各自得到的向量並在一起,組成特徵向量。
2,第二個特徵非常簡單,只需要將影象歸一化到特定的大小,然後將影象每個點的灰度值作為特徵即可。
<1>將影象由RGB影象轉換為灰度影象;
<2>將影象歸一化大小為 ,並將影象展開為一行,組成特徵向量。
Sample Code (以下程式碼使用的是 Opencv 3.0環境)
float sumMatValue(const Mat & image){ float sumValue = 0; int r = image.rows; int c = image.cols; if(image.isContinuous()){ c = r*c; r = 1; } for(int i = 0; i < r; i++){ const uchar *linePtr = image.ptr<uchar>(i); for (int j = 0; j < c; j++){ sumValue += linePtr[j]; } } return sumValue; } void calcGradientFeat(Mat & imgSrc, vector<float> & feat){ Mat image; cvtColor(imgSrc, image, CV_BGR2GRAY); resize(image, image, Size(8,16)); float mask[3][3] = {{1,2,1},{0,0,0},{-1,-2,-1}}; Mat y_mask = Mat(3,3, CV_32F, mask) / 8; Mat x_mask = y_mask.t(); // 轉置 Mat sobelX, sobelY; filter2D(image, sobelX, CV_32F, x_mask); filter2D(image, sobelY, CV_32F, y_mask); sobelX = abs(sobelX); sobelY = abs(sobelY); float totleValueX = sumMatValue(sobelX); float totleValueY = sumMatValue(sobelY); for(int i = 0; i < image.rows; i = i +4) { for( int j = 0; j < image.cols; j = j + 4) { Mat subImageX = sobelX(Rect(j, i, 4, 4)); feat.push_back(sumMatValue(subImageX) / totleValueX); Mat subImageY= sobelY(Rect(j, i, 4, 4)); feat.push_back(sumMatValue(subImageY) / totleValueY); } } Mat img2; resize(image, img2, Size(4,8)); int r = img2.rows; int c = img2.cols; if(img2.isContinuous()){ c = r*c; r = 1; } for(int i = 0; i < r; i++){ const uchar *linePtr = img2.ptr<uchar>(i); for (int j = 0; j < c; j++){ feat.push_back(linePtr[j]); } } }
Opencv的神經網路
CvANN_MLP是OpenCV中提供的一個神經網路的類,正如它的名字一樣(multi-layer perceptrons),它是一個多層感知網路,它有一個輸入層,一個輸出層以及1或多個隱藏層。
建立一個網路
Ptr<StatModel> buildMLPClassifier(Mat & input , Mat & output){ Ptr<ANN_MLP> model; //train classifier; int layer_sz[] = {input.cols, 100 , output.cols}; int nlayers = (int)(sizeof(layer_sz)/ sizeof(layer_sz[0])); Mat layer_sizes(1,nlayers,CV_32S, layer_sz); int method; double method_param; int max_iter; if(1){ method = ANN_MLP::BACKPROP; method_param = 0.0001; max_iter = 1000; }else{ method = ANN_MLP::RPROP; method_param = 0.1; max_iter = 1000; } Ptr<TrainData> tData = TrainData::create(input,ROW_SAMPLE,output); model = ANN_MLP::create(); model->setLayerSizes(layer_sizes); model->setActivationFunction(ANN_MLP::SIGMOID_SYM, 0, 0); model->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER+TermCriteria::EPS, max_iter, FLT_EPSILON)); //setIterCondition(max_iter, 0); model->setTrainMethod(method, method_param); model->train(tData); model->save("mlp1.xml"); return model; } }layerSizes:一個整型的陣列,這裡面用Mat儲存。它是一個1*N的Mat,N代表神經網路的層數,第 列的值表示第 層的結點數。這裡需要注意的是,在建立這個Mat時,一定要是整型的,uchar和float型都會報錯。
比如我們要建立一個3層的神經網路,其中第一層結點數為 ,第二層結點數為 ,第三層結點數為 ,則layerSizes可以採用如下定義:
1 Mat layerSizes=(Mat_<int>(1,3)<<x1,x2,x3);
或者用一個數組來初始化:
1 int ar[]={x1,x2,x3}; 2 Mat layerSizes(1,3,CV_32S,ar);
activateFunc:這個引數用於指定啟用函式,不熟悉的可以去看我部落格裡的這篇文章《神經網路:感知器與梯度下降》,一般情況下我們用SIGMOID函式就可以了,當然你也可以選擇正切函式或高斯函式作為啟用函式。OpenCV裡提供了三種啟用函式,線性函式(CvANN_MLP::IDENTITY)、sigmoid函式(CvANN_MLP::SIGMOID_SYM)和高斯啟用函式(CvANN_MLP::GAUSSIAN)。
後面兩個引數則是SIGMOID啟用函式中的兩個引數 和 ,預設情況下會都被設定為1。
網路引數設定
神經網路訓練引數的型別存放在CvANN_MLP_TrainParams這個類裡,它提供了一個預設的建構函式,我們可以直接呼叫,也可以一項一項去設。
1 CvANN_MLP_TrainParams::CvANN_MLP_TrainParams() 2 { 3 term_crit = cvTermCriteria( CV_TERMCRIT_ITER + CV_TERMCRIT_EPS, 1000, 0.01 ); 4 train_method = RPROP; 5 bp_dw_scale = bp_moment_scale = 0.1; 6 rp_dw0 = 0.1; rp_dw_plus = 1.2; rp_dw_minus = 0.5; 7 rp_dw_min = FLT_EPSILON; rp_dw_max = 50.; 8 }
它的引數大概包括以下幾項。
term_crit:終止條件,它包括了兩項,迭代次數(CV_TERMCRIT_ITER)和誤差最小值(CV_TERMCRIT_EPS),一旦有一個達到條件就終止訓練。
train_method:訓練方法,OpenCV裡提供了兩個方法一個是很經典的反向傳播演算法BACKPROP,另一個是彈性反饋演算法RPROP,對第二種訓練方法,沒有仔細去研究過,這裡我們運用第一種方法。
剩下就是關於每種訓練方法的相關引數,針對於反向傳播法,主要是兩個引數,一個是權值更新率bp_dw_scale和權值更新衝量bp_moment_scale。這兩個量一般情況設定為0.1就行了;太小了網路收斂速度會很慢,太大了可能會讓網路越過最小值點。
我們一般先運用它的預設建構函式,然後根據需要再修改相應的引數就可以了。如下面程式碼所示,我們將迭代次數改為了5000次。
1 CvANN_MLP_TRainParams param; 2 param.term_crit=cvTermCriteria(CV_TerMCrIT_ITER+CV_TERMCRIT_EPS,5000,0.01);
inputs:輸入矩陣。它儲存了所有訓練樣本的特徵。假設所有樣本總數為nSamples,而我們提取的特徵維數為ndims,則inputs是一個 的矩陣,我們可以這樣建立它。
1 Mat inputs(nSamples,ndims,CV_32FC1); //CV_32FC1說明它儲存的資料是float型的。
我們需要將我們的訓練集,經過特徵提取把得到的特徵向量儲存在inputs中,每個樣本的特徵佔一行。
outputs:輸出矩陣。我們實際在訓練中,我們知道每個樣本所屬的種類,假設一共有nClass類。那麼我們將outputs設定為一個nSample行nClass列的矩陣,每一行表示一個樣本的預期輸出結果,該樣本所屬的那類對應的列設定為1,其他都為0。比如我們需要識別0-9這10個數字,則總的類數為10類,那麼樣本數字“3”的預期輸出為[0,0,1,0,0,0,0,0,0,0];
sampleWeights:一個在使用RPROP方法訓練時才需要的資料,所以這裡我們不設定,直接設定為Mat()即可。
sampleIdx:相當於一個遮罩,它指定哪些行的資料參與訓練。如果設定為Mat(),則所有行都參與。
params:這個在剛才已經說過了,是訓練相關的引數。
flag:它提供了3個可選項引數,用來指定資料處理的方式,我們可以用邏輯符號去組合它們。UPDATE_WEIGHTS指定用一定的演算法去初始化權值矩陣而不是用隨機的方法。NO_INPUT_SCALE和NO_OUTPUT_SCALE分別用於禁止輸入與輸出矩陣的歸一化。
一切都準備好後,直接開始訓練吧!
識別
Ptr<StatModel> model = buildMLPClassifier(input, output); //Ptr<StatModel> model = loadMLPClassifiler(); float response = model->predict(test, test1); cout<<"response = "<<response<<endl; for(int i = 0; i < test1.size(); i++) { cout<<"test1 = "<<test1[i]<<endl; }識別返回的response 就是預測的值,test1 裡面存放的是每個字母的可能概率
完整程式碼:
int main() { Mat image; vector<float>feats; vector<float>test,test1; string path = "code/python_image_learn/identfying_code_recognize/charSamples/"; int num = 0; int classfilternum = 34; int modlenum = 30; for(int i = 0 ; i < classfilternum ; i++){ for(int j = 0; j < modlenum; j++){ ostringstream oss; oss<<path<<i<<"/"<<j<<".png"; //cout<<oss.str()<<endl; image=imread(oss.str()); calcGradientFeat(image, feats); num++; if(i == 10 && j == 10){ ostringstream oss; oss<<path<<i<<"/"<<(j+1)<<".png"; //cout<<oss.str()<<endl; image=imread(oss.str()); calcGradientFeat(image, test); } } } Mat input, output; input = Mat(classfilternum*modlenum, 48, CV_32F); output = Mat(classfilternum*modlenum, classfilternum, CV_32F, Scalar(0)); int r = input.rows; int c = input.cols; if(input.isContinuous()){ c = r*c; r = 1; } for(int i = 0; i < r; i++){ float *linePtr = input.ptr<float>(i); for (int j = 0; j < c; j++){ linePtr[j] = feats[c*i + j]; } } for(int i = 0; i < output.rows; i++){ float *lineoutput = output.ptr<float>(i); lineoutput[i/modlenum] = 1; } //if( Ptr<StatModel> model = buildMLPClassifier(input, output); //Ptr<StatModel> model = loadMLPClassifiler(); float response = model->predict(test, test1); cout<<"response = "<<response<<endl; for(int i = 0; i < test1.size(); i++) { cout<<"test1 = "<<test1[i]<<endl; } //cout<<input<<endl; //cout<<"rows = "<<input.rows<<"col = "<<input.cols<<endl; //cout<<output<<endl; // waitKey(); //等待按鍵 return 0; }
字元樣本的下載
連結:http://pan.baidu.com/s/1pLPeZkZ 密碼:26eb