歡迎你們前往雲+社區,獲取更多騰訊海量技術實踐乾貨哦~css
這是一篇基於Mike O'Neill 寫的一篇很棒的文章:神經網絡的手寫字符識別(Neural Network for Recognition of Handwritten Digits)而給出的一我的工神經網絡實現手寫字符識別的例子。儘管在過去幾年已經有許多系統和分類算法被提出,可是手寫識別任然是模式識別中的一項挑戰。Mike O'Neill的程序對想學習經過神經網絡算法實現通常手寫識別的程序員來講是一個極好的例子,尤爲是在神經網絡的卷積部分。那個程序是用MFC/ C++編寫的,對於不熟悉的人來講有些困難。因此,我決定用C#從新寫一下個人一些程序。個人程序已經取得了良好的效果,但還並不優秀(在收斂速度,錯誤率等方面)。但此次僅僅是程序的基礎,目的是幫助理解神經網絡,因此它比較混亂,有重構的必要。我一直在試把它做爲一個庫的方式重建,那將會很靈活,很簡單地經過一個INI文件來改變參數。但願有一天我能取得預期的效果。html
模式檢測和字符候選檢測是我在程序中必須面對的最重要的問題之一。事實上,我不只僅想利用另外一種編程語言從新完成Mike的程序,並且我還想識別文檔圖片中的字符。有一些研究提出了我在互聯網上發現的很是好的目標檢測算法,可是對於像我這樣的業餘項目來講,它們太複雜了。在教我女兒繪畫時發現的一個方法解決了這個問題。固然,它仍然有侷限性,但在第一次測試中就超出了個人預期。在正常狀況下,字符候選檢測分爲行檢測,字檢測和字符檢測幾種,分別採用不一樣的算法。個人作法和這有一點點不一樣。檢測使用相同的算法:git
public static Rectangle GetPatternRectangeBoundary (Bitmap original,int colorIndex, int hStep, int vStep, bool bTopStart)
以及:程序員
public static List<Rectangle> PatternRectangeBoundaryList (Bitmap original, int colorIndex, int hStep, int vStep, bool bTopStart,int widthMin,int heightMin)
經過改變參數hStep
(水平步進)和vStep
(垂直步進)能夠簡單地檢測行,字或字符。矩形邊界也能夠經過更改bTopStart
爲true
或false
實現從上到下和從左到右不一樣方式進行檢測。矩形被widthMin
和d限制。個人算法的最大優勢是:它能夠檢測不在同一行的字或字符串。web
public void PatternRecognitionThread(Bitmap bitmap) { _originalBitmap = bitmap; if (_rowList == null) { _rowList = AForge.Imaging.Image.PatternRectangeBoundaryList (_originalBitmap,255, 30, 1, true, 5, 5); _irowIndex = 0; } foreach(Rectangle rowRect in _rowList) { _currentRow = AForge.Imaging.ImageResize.ImageCrop (_originalBitmap, rowRect); if (_iwordIndex == 0) { _currentWordsList = AForge.Imaging.Image.PatternRectangeBoundaryList (_currentRow, 255, 20, 10, false, 5, 5); } foreach (Rectangle wordRect in _currentWordsList) { _currentWord = AForge.Imaging.ImageResize.ImageCrop (_currentRow, wordRect); _iwordIndex++; if (_icharIndex == 0) { _currentCharsList = AForge.Imaging.Image.PatternRectangeBoundaryList (_currentWord, 255, 1, 1, false, 5, 5); } foreach (Rectangle charRect in _currentCharsList) { _currentChar = AForge.Imaging.ImageResize.ImageCrop (_currentWord, charRect); _icharIndex++; Bitmap bmptemp = AForge.Imaging.ImageResize.FixedSize (_currentChar, 21, 21); bmptemp = AForge.Imaging.Image.CreateColorPad (bmptemp,Color.White, 4, 4); bmptemp = AForge.Imaging.Image.CreateIndexedGrayScaleBitmap (bmptemp); byte[] graybytes = AForge.Imaging.Image.GrayscaletoBytes(bmptemp); PatternRecognitionThread(graybytes); m_bitmaps.Add(bmptemp); } string s = " \n"; _form.Invoke(_form._DelegateAddObject, new Object[] { 1, s }); If(_icharIndex ==_currentCharsList.Count) { _icharIndex =0; } } If(_iwordIndex==__currentWordsList.Count) { _iwordIndex=0; } }
原程序中的卷積神經網絡(CNN)包括輸入層在內本質上是有五層。卷積體系結構的細節已經在Mike和Simard博士在他們的文章《應用於視覺文件分析的卷積神經網絡的最佳實踐》中描述過了。這種卷積網絡的整體方案是用較高的分辨率去提取簡單的特徵,而後以較低的分辨率將它們轉換成複雜的特徵。生成較低分辨的最簡單方法是對子層進行二倍二次採樣。這反過來又爲卷積核的大小提供了參考。核的寬度以一個單位(奇數大小)爲中心被選定,須要足夠的重疊從而不丟失信息(對於一個單位3重疊顯得太小),同時不至於冗餘(7重疊將會過大,5重疊能實現超過70%的重疊)。所以,在這個網絡中我選擇大小爲5的卷積核。填充輸入(調整到更大以實現特徵單元居中在邊界上)並不能顯着提升性能。因此不填充,內核大小設定爲5進行二次採樣,每一個卷積層將特徵尺寸從n減少到(n-3)/2。因爲在MNIST的初始輸入的圖像大小爲28x28,因此在二次卷積後產生整數大小的近似值是29x29。通過兩層卷積以後,5x5的特徵尺寸對於第三層卷積而言過小。Simard博士還強調,若是第一層的特徵少於五個,則會下降性能,然而使用超過5個並不能改善(Mike使用了6個)。相似地,在第二層上,少於50個特徵會下降性能,而更多(100個特徵)沒有改善。關於神經網絡的總結以下:算法
#0層:是MNIST數據庫中手寫字符的灰度圖像,填充到29x29像素。輸入層有29x29 = 841個神經元。數據庫
#1層:是一個具備6個特徵映射的卷積層。從層#1到前一層有13×13×6 = 1014個神經元,(5×5 + 1)×6 = 156個權重,以及1014×26 = 26364個鏈接。編程
#2層:是一個具備五十(50)個特徵映射的卷積層。從#2層到前一層有5x5x50 = 1250個神經元,(5x5 + 1)x6x50 = 7800個權重,以及1250x(5x5x6 + 1)= 188750個鏈接。(在Mike的文章中不是有32500個鏈接)。數組
#3層:是一個100個單元的徹底鏈接層。有100個神經元,100x(1250 + 1)= 125100權重,和100x1251 = 125100鏈接。markdown
#4層:是最後的,有10個神經元,10×(100 + 1)= 1010個權重,以及10×10 1 = 1010個鏈接。
反向傳播是更新每一個層權重變化的過程,從最後一層開始,向前移動直到達到第一個層。
在標準的反向傳播中,每一個權重根據如下公式更新:
其中eta是「學習率」,一般是相似0.0005這樣的小數字,在訓練過程當中會逐漸減小。可是,因爲收斂速度慢,標準的反向傳播在程序中不須要使用。相反,LeCun博士在他的文章《Efficient BackProp》中提出的稱爲「隨機對角列文伯格-馬夸爾特法(Levenberg-Marquardt)」的二階技術已獲得應用,儘管Mike說它與標準的反向傳播並不相同,理論應該幫助像我這樣的新人更容易理解代碼。
在Levenberg-Marquardt方法中,rw
計算以下:
假設平方代價函數是:
那麼梯度是:
而Hessian遵循以下規則:
Hessian矩陣的簡化近似爲Jacobian矩陣,它是一個維數爲N×O的半矩陣。
用於計算神經網絡中的Hessian矩陣對角線的反向傳播過程是衆所周知的。假設網絡中的每一層都有:
使用Gaus-Neuton近似(刪除包含|'(y))的項,咱們獲得:
以及:
事實上,使用完整Hessian矩陣信息(Levenberg-Marquardt,Gaus-Newton等)的技術只能應用於以批處理模式訓練的很是小的網絡,而不能用於隨機模式。爲了得到Levenberg- Marquardt算法的隨機模式,LeCun博士提出了經過關於每一個參數的二階導數的運算估計來計算Hessian對角線的思想。瞬時二階導數能夠經過反向傳播得到,如公式(7,8,9)所示。只要咱們利用這些運算估計,能夠用它們來計算每一個參數各自的學習率:
其中e是全局學習速率,而且
是關於h ki的對角線二階導數的運算估計。m是防止h ki在二階導數較小的狀況下(即優化在偏差函數的平坦部分移動時)的參數。能夠在訓練集的一個子集(500隨機化模式/ 60000訓練集的模式)中計算二階導數。因爲它們變化很是緩慢,因此只須要每隔幾個週期從新估計一次。在原來的程序中,對角線Hessian是每一個週期都從新估算的。
這裏是C#中的二階導數計算函數:
public void BackpropagateSecondDerivatives(DErrorsList d2Err_wrt_dXn /* in */, DErrorsList d2Err_wrt_dXnm1 /* out */) { // 命名(從NeuralNetwork類繼承) // 注意:儘管咱們正在處理二階導數(而不是一階), // 可是咱們使用幾乎相同的符號,就好像有一階導數 // 同樣,不然ASCII的顯示會使人誤解。 咱們添加一 // 個「2」而不是兩個「2」,好比「d2Err_wrt_dXn」,以簡 // 單地強調咱們使用二階導數 // // Err是整個神經網絡的輸出偏差 // Xn是第n層上的輸出向量 // Xnm1是前一層的輸出向量 // Wn是第n層權重的向量 // Yn是第n層的激活值, // 即,應用擠壓功能以前的輸入的加權和 // F是擠壓函數:Xn = F(Yn) // F'是擠壓函數的導數 // 簡單說,對於F = tanh,則F'(Yn)= 1-Xn ^ 2,即, // 能夠從輸出中計算出導數,而不須要知道輸入 int ii, jj; uint kk; int nIndex; double output; double dTemp; var d2Err_wrt_dYn = new DErrorsList(m_Neurons.Count); // // std::vector< double > d2Err_wrt_dWn( m_Weights.size(), 0.0 ); // important to initialize to zero ////////////////////////////////////////////////// // ///// 設計 TRADEOFF: REVIEW !! // // 請注意,此命名的方案與NNLayer :: Backpropagate() // 函數中的推理相同,即從該函數派生的 // BackpropagateSecondDerivatives()函數 // // 咱們但願對數組「d2Err_wrt_dWn」使用STL向量(爲了便於編碼) // ,這是圖層中當前模式的錯誤權重的二階微分。 可是,對於 // 具備許多權重的層(例如徹底鏈接的層),也有許多權重。 分 // 配大內存塊時,STL向量類的分配器很是愚蠢,並致使大量的頁 // 面錯誤,從而致使應用程序整體執行時間減慢。 // 爲了解決這個問題,我嘗試使用一個普通的C數組, // 並從堆中取出所需的空間,並在函數結尾處刪除[]。 // 可是,這會致使相同數量的頁面錯誤錯誤,並 // 且不會提升性能。 // 因此我試着在棧上分配一個普通的C數組(即不是堆)。 // 固然,我不能寫double d2Err_wrt_dWn [m_Weights.size()]; // 由於編譯器堅持一個編譯時間爲數組大小的已知恆定值。 // 爲了不這個需求,我使用_alloca函數來分配堆棧上的內存。 // 這樣作的缺點是堆棧使用過多,可能會出現堆棧溢出問題。 // 這就是爲何將它命名爲「Review」 double[] d2Err_wrt_dWn = new double[m_Weights.Count]; for (ii = 0; ii < m_Weights.Count; ++ii) { d2Err_wrt_dWn[ii] = 0.0; } // 計算 d2Err_wrt_dYn = ( F'(Yn) )^2 * // dErr_wrt_Xn (其中dErr_wrt_Xn其實是二階導數) for (ii = 0; ii < m_Neurons.Count; ++ii) { output = m_Neurons[ii].output; dTemp = m_sigmoid.DSIGMOID(output); d2Err_wrt_dYn.Add(d2Err_wrt_dXn[ii] * dTemp * dTemp); } // 計算d2Err_wrt_Wn =(Xnm1)^ 2 * d2Err_wrt_Yn // (其中dE2rr_wrt_Yn其實是二階導數) // 對於這個層中的每一個神經元,經過先前層的鏈接 // 列表,並更新相應權重的差分 ii = 0; foreach (NNNeuron nit in m_Neurons) { foreach (NNConnection cit in nit.m_Connections) { try { kk = (uint)cit.NeuronIndex; if (kk == 0xffffffff) { output = 1.0; // 這是隱含的聯繫; 隱含的神經元輸出「1」 } else { output = m_pPrevLayer.m_Neurons[(int)kk].output; } // ASSERT( (*cit).WeightIndex < d2Err_wrt_dWn.size() ); // 由於在將d2Err_wrt_dWn更改成C風格的 // 數組以後,size()函數將不起做用 d2Err_wrt_dWn[cit.WeightIndex] = d2Err_wrt_dYn[ii] * output * output; } catch (Exception ex) { } } ii++; } // 計算d2Err_wrt_Xnm1 =(Wn)^ 2 * d2Err_wrt_dYn // (其中d2Err_wrt_dYn是否是第一個二階導數)。 // 須要d2Err_wrt_Xnm1做爲d2Err_wrt_Xn的 // 二階導數反向傳播的輸入值 // 對於下一個(即先前的空間)層 // 對於這個層中的每一個神經元 ii = 0; foreach (NNNeuron nit in m_Neurons) { foreach (NNConnection cit in nit.m_Connections) { try { kk = cit.NeuronIndex; if (kk != 0xffffffff) { // 咱們排除了ULONG_MAX,它表示具備恆定輸出「1」的 // 虛偏置神經元,由於咱們不能正真訓練偏置神經元 nIndex = (int)kk; dTemp = m_Weights[(int)cit.WeightIndex].value; d2Err_wrt_dXnm1[nIndex] += d2Err_wrt_dYn[ii] * dTemp * dTemp; } } catch (Exception ex) { return; } } ii++; // ii 跟蹤神經元迭代器 } double oldValue, newValue; // 最後,使用dErr_wrt_dW更新對角線的層 // 神經元的權重。經過設計,這個函數 // 以及它對許多(約500個模式)的迭代被 // 調用,而單個線程已經鎖定了神經網絡, // 因此另外一個線程不可能改變Hessian的值。 // 不過,因爲這很容易作到,因此咱們使用一 // 個原子比較交換操做,這意味着另外一個線程 // 可能在二階導數的反向傳播過程當中,並且Hessians // 可能會稍微移動 for (jj = 0; jj < m_Weights.Count; ++jj) { oldValue = m_Weights[jj].diagHessian; newValue = oldValue + d2Err_wrt_dWn[jj]; m_Weights[jj].diagHessian = newValue; } } //////////////////////////////////////////////////////////////////
儘管MFC / C ++和C#之間存不兼容,可是個人程序與原程序類似。使用MNIST數據庫,網絡在60,000個訓練集模式中執行後有291次錯誤識別。這意味着錯誤率只有0.485%。然而,在10000個模式中,有136個錯誤識別,錯誤率爲1.36%。結果並不像基礎測試那麼好,但對我來講,用我本身的手寫字符集作實驗已經足夠了。首先將輸入的圖像從上到下分爲字符組,而後在每組中把字符從左到右進行檢測,調整到29x29像素,而後由神經網絡系統識別。該方案知足個人基本要求,我本身的手寫數字是能夠被正確識別的。在AForge.Net的圖像處理庫中添加了檢測功能,以便使用。可是,由於它只是在個人業餘時間編程,我相信它有不少的缺陷須要修復。反向傳播時間就是一個例子。每一個週期使用大約3800秒的訓練時間,可是隻須要2400秒。(個人電腦使用了英特爾奔騰雙核E6500處理器)。與Mike的程序相比,速度至關慢。我也但願能有一個更好的手寫字符數據庫,或者與其餘人合做,繼續個人實驗,使用個人算法開發一個真正的應用程序。