利用神經(jīng)網(wǎng)絡(luò)算法的C#手寫數(shù)字識(shí)別
下載Demo - 2.77 MB (原始地址)
handwritten_character_recognition.zip
下載源碼 - 70.64 KB (原始地址)
nnhandwrittencharreccssource.zip
介紹
這是一篇基于Mike O'Neill 寫的一篇很棒的文章:神經(jīng)網(wǎng)絡(luò)的手寫字符識(shí)別(Neural Network for Recognition of Handwritten Digits)而給出的一個(gè)人工神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)手寫字符識(shí)別的例子。盡管在過去幾年已經(jīng)有許多系統(tǒng)和分類算法被提出,但是手寫識(shí)別任然是模式識(shí)別中的一項(xiàng)挑戰(zhàn)。Mike O'Neill的程序?qū)ο雽W(xué)習(xí)通過神經(jīng)網(wǎng)絡(luò)算法實(shí)現(xiàn)一般手寫識(shí)別的程序員來說是一個(gè)極好的例子,尤其是在神經(jīng)網(wǎng)絡(luò)的卷積部分。那個(gè)程序是用MFC/ C++編寫的,對于不熟悉的人來說有些困難。所以,我決定用C#重新寫一下我的一些程序。我的程序已經(jīng)取得了良好的效果,但還并不優(yōu)秀(在收斂速度,錯(cuò)誤率等方面)。但這次僅僅是程序的基礎(chǔ),目的是幫助理解神經(jīng)網(wǎng)絡(luò),所以它比較混亂,有重構(gòu)的必要。我一直在試把它作為一個(gè)庫的方式重建,那將會(huì)很靈活,很簡單地通過一個(gè)INI文件來改變參數(shù)。希望有一天我能取得預(yù)期的效果。
字符檢測
模式檢測和字符候選檢測是我在程序中必須面對的最重要的問題之一。事實(shí)上,我不僅僅想利用另一種編程語言重新完成Mike的程序,而且我還想識(shí)別文檔圖片中的字符。有一些研究提出了我在互聯(lián)網(wǎng)上發(fā)現(xiàn)的非常好的目標(biāo)檢測算法,但是對于像我這樣的業(yè)余項(xiàng)目來說,它們太復(fù)雜了。在教我女兒繪畫時(shí)發(fā)現(xiàn)的一個(gè)方法解決了這個(gè)問題。當(dāng)然,它仍然有局限性,但在***次測試中就超出了我的預(yù)期。在正常情況下,字符候選檢測分為行檢測,字檢測和字符檢測幾種,分別采用不同的算法。我的做法和這有一點(diǎn)點(diǎn)不同。檢測使用相同的算法:
- 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)
通過改變參數(shù)hStep (水平步進(jìn))和vStep (垂直步進(jìn))可以簡單地檢測行,字或字符。矩形邊界也可以通過更改bTopStart 為true 或false實(shí)現(xiàn)從上到下和從左到右不同方式進(jìn)行檢測。矩形被widthMin 和d限制。我的算法的***優(yōu)點(diǎn)是:它可以檢測不在同一行的字或字符串。
字符候選識(shí)別可以通過以下方法實(shí)現(xiàn):
- 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;
- }
- }
字符識(shí)別
原程序中的卷積神經(jīng)網(wǎng)絡(luò)(CNN)包括輸入層在內(nèi)本質(zhì)上是有五層。卷積體系結(jié)構(gòu)的細(xì)節(jié)已經(jīng)在Mike和Simard博士在他們的文章《應(yīng)用于視覺文件分析的卷積神經(jīng)網(wǎng)絡(luò)的***實(shí)踐》中描述過了。這種卷積網(wǎng)絡(luò)的總體方案是用較高的分辨率去提取簡單的特征,然后以較低的分辨率將它們轉(zhuǎn)換成復(fù)雜的特征。生成較低分辨的最簡單方法是對子層進(jìn)行二倍二次采樣。這反過來又為卷積核的大小提供了參考。核的寬度以一個(gè)單位(奇數(shù)大?。橹行谋贿x定,需要足夠的重疊從而不丟失信息(對于一個(gè)單位3重疊顯得過小),同時(shí)不至于冗余(7重疊將會(huì)過大,5重疊能實(shí)現(xiàn)超過70%的重疊)。因此,在這個(gè)網(wǎng)絡(luò)中我選擇大小為5的卷積核。填充輸入(調(diào)整到更大以實(shí)現(xiàn)特征單元居中在邊界上)并不能顯著提高性能。所以不填充,內(nèi)核大小設(shè)定為5進(jìn)行二次采樣,每個(gè)卷積層將特征尺寸從n減小到(n-3)/2。由于在MNIST的初始輸入的圖像大小為28x28,所以在二次卷積后產(chǎn)生整數(shù)大小的近似值是29x29。經(jīng)過兩層卷積之后,5x5的特征尺寸對于第三層卷積而言太小。Simard博士還強(qiáng)調(diào),如果***層的特征少于五個(gè),則會(huì)降低性能,然而使用超過5個(gè)并不能改善(Mike使用了6個(gè))。類似地,在第二層上,少于50個(gè)特征會(huì)降低性能,而更多(100個(gè)特征)沒有改善。關(guān)于神經(jīng)網(wǎng)絡(luò)的總結(jié)如下:
#0層:是MNIST數(shù)據(jù)庫中手寫字符的灰度圖像,填充到29x29像素。輸入層有29x29 = 841個(gè)神經(jīng)元。
#1層:是一個(gè)具有6個(gè)特征映射的卷積層。從層#1到前一層有13×13×6 = 1014個(gè)神經(jīng)元,(5×5 + 1)×6 = 156個(gè)權(quán)重,以及1014×26 = 26364個(gè)連接。
#2層:是一個(gè)具有五十(50)個(gè)特征映射的卷積層。從#2層到前一層有5x5x50 = 1250個(gè)神經(jīng)元,(5x5 + 1)x6x50 = 7800個(gè)權(quán)重,以及1250x(5x5x6 + 1)= 188750個(gè)連接。
(在Mike的文章中不是有32500個(gè)連接)。
#3層:是一個(gè)100個(gè)單元的完全連接層。有100個(gè)神經(jīng)元,100x(1250 + 1)= 125100權(quán)重,和100x1251 = 125100連接。
#4層:是***的,有10個(gè)神經(jīng)元,10×(100 + 1)= 1010個(gè)權(quán)重,以及10×10 1 = 1010個(gè)連接。
反向傳播
反向傳播是更新每個(gè)層權(quán)重變化的過程,從***一層開始,向前移動(dòng)直到達(dá)到***個(gè)層。
在標(biāo)準(zhǔn)的反向傳播中,每個(gè)權(quán)重根據(jù)以下公式更新:
(1)
其中eta是“學(xué)習(xí)率”,通常是類似0.0005這樣的小數(shù)字,在訓(xùn)練過程中會(huì)逐漸減少。但是,由于收斂速度慢,標(biāo)準(zhǔn)的反向傳播在程序中不需要使用。相反,LeCun博士在他的文章《Efficient BackProp》中提出的稱為“隨機(jī)對角列文伯格-馬夸爾特法(Levenberg-Marquardt)”的二階技術(shù)已得到應(yīng)用,盡管Mike說它與標(biāo)準(zhǔn)的反向傳播并不相同,理論應(yīng)該幫助像我這樣的新人更容易理解代碼。
在Levenberg-Marquardt方法中,rw 計(jì)算如下:
假設(shè)平方代價(jià)函數(shù)是:
那么梯度是:
而Hessian遵循如下規(guī)則:
Hessian矩陣的簡化近似為Jacobian矩陣,它是一個(gè)維數(shù)為N×O的半矩陣。
用于計(jì)算神經(jīng)網(wǎng)絡(luò)中的Hessian矩陣對角線的反向傳播過程是眾所周知的。假設(shè)網(wǎng)絡(luò)中的每一層都有:
(7)
使用Gaus-Neuton近似(刪除包含|'(y))的項(xiàng),我們得到:
(8)
(9)
以及:
隨機(jī)對角列文伯格-馬夸爾特(Levenberg-Marquardt)法
事實(shí)上,使用完整Hessian矩陣信息(Levenberg-Marquardt,Gaus-Newton等)的技術(shù)只能應(yīng)用于以批處理模式訓(xùn)練的非常小的網(wǎng)絡(luò),而不能用于隨機(jī)模式。為了獲得Levenberg- Marquardt算法的隨機(jī)模式,LeCun博士提出了通過關(guān)于每個(gè)參數(shù)的二階導(dǎo)數(shù)的運(yùn)算估計(jì)來計(jì)算Hessian對角線的思想。瞬時(shí)二階導(dǎo)數(shù)可以通過反向傳播獲得,如公式(7,8,9)所示。只要我們利用這些運(yùn)算估計(jì),可以用它們來計(jì)算每個(gè)參數(shù)各自的學(xué)習(xí)率:
其中e是全局學(xué)習(xí)速率,并且
是關(guān)于h ki的對角線二階導(dǎo)數(shù)的運(yùn)算估計(jì)。m是防止h ki在二階導(dǎo)數(shù)較小的情況下(即優(yōu)化在誤差函數(shù)的平坦部分移動(dòng)時(shí))的參數(shù)??梢栽谟?xùn)練集的一個(gè)子集(500隨機(jī)化模式/ 60000訓(xùn)練集的模式)中計(jì)算二階導(dǎo)數(shù)。由于它們變化非常緩慢,所以只需要每隔幾個(gè)周期重新估計(jì)一次。在原來的程序中,對角線Hessian是每個(gè)周期都重新估算的。
這里是C#中的二階導(dǎo)數(shù)計(jì)算函數(shù):
- public void BackpropagateSecondDerivatives(DErrorsList d2Err_wrt_dXn /* in */,
- DErrorsList d2Err_wrt_dXnm1 /* out */)
- {
- // 命名(從NeuralNetwork類繼承)
- // 注意:盡管我們正在處理二階導(dǎo)數(shù)(而不是一階),
- // 但是我們使用幾乎相同的符號,就好像有一階導(dǎo)數(shù)
- // 一樣,否則ASCII的顯示會(huì)令人誤解。 我們添加一
- // 個(gè)“2”而不是兩個(gè)“2”,比如“d2Err_wrt_dXn”,以簡
- // 單地強(qiáng)調(diào)我們使用二階導(dǎo)數(shù)
- //
- // Err是整個(gè)神經(jīng)網(wǎng)絡(luò)的輸出誤差
- // Xn是第n層上的輸出向量
- // Xnm1是前一層的輸出向量
- // Wn是第n層權(quán)重的向量
- // Yn是第n層的激活值,
- // 即,應(yīng)用擠壓功能之前的輸入的加權(quán)和
- // F是擠壓函數(shù):Xn = F(Yn)
- // F'是擠壓函數(shù)的導(dǎo)數(shù)
- // 簡單說,對于F = tanh,則F'(Yn)= 1-Xn ^ 2,即,
- // 可以從輸出中計(jì)算出導(dǎo)數(shù),而不需要知道輸入
- 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
- //////////////////////////////////////////////////
- //
- ///// 設(shè)計(jì) TRADEOFF: REVIEW !!
- //
- // 請注意,此命名的方案與NNLayer :: Backpropagate()
- // 函數(shù)中的推理相同,即從該函數(shù)派生的
- // BackpropagateSecondDerivatives()函數(shù)
- //
- // 我們希望對數(shù)組“d2Err_wrt_dWn”使用STL向量(為了便于編碼)
- // ,這是圖層中當(dāng)前模式的錯(cuò)誤權(quán)重的二階微分。 但是,對于
- // 具有許多權(quán)重的層(例如完全連接的層),也有許多權(quán)重。 分
- // 配大內(nèi)存塊時(shí),STL向量類的分配器非常愚蠢,并導(dǎo)致大量的頁
- // 面錯(cuò)誤,從而導(dǎo)致應(yīng)用程序總體執(zhí)行時(shí)間減慢。
- // 為了解決這個(gè)問題,我嘗試使用一個(gè)普通的C數(shù)組,
- // 并從堆中取出所需的空間,并在函數(shù)結(jié)尾處刪除[]。
- // 但是,這會(huì)導(dǎo)致相同數(shù)量的頁面錯(cuò)誤錯(cuò)誤,并
- // 且不會(huì)提高性能。
- // 所以我試著在棧上分配一個(gè)普通的C數(shù)組(即不是堆)。
- // 當(dāng)然,我不能寫double d2Err_wrt_dWn [m_Weights.size()];
- // 因?yàn)榫幾g器堅(jiān)持一個(gè)編譯時(shí)間為數(shù)組大小的已知恒定值。
- // 為了避免這個(gè)需求,我使用_alloca函數(shù)來分配堆棧上的內(nèi)存。
- // 這樣做的缺點(diǎn)是堆棧使用過多,可能會(huì)出現(xiàn)堆棧溢出問題。
- // 這就是為什么將它命名為“Review”
- double[] d2Err_wrt_dWn = new double[m_Weights.Count];
- for (ii = 0; ii < m_Weights.Count; ++ii)
- {
- d2Err_wrt_dWn[ii] = 0.0;
- }
- // 計(jì)算 d2Err_wrt_dYn = ( F'(Yn) )^2 *
- // dErr_wrt_Xn (其中dErr_wrt_Xn實(shí)際上是二階導(dǎo)數(shù))
- 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);
- }
- // 計(jì)算d2Err_wrt_Wn =(Xnm1)^ 2 * d2Err_wrt_Yn
- // (其中dE2rr_wrt_Yn實(shí)際上是二階導(dǎo)數(shù))
- // 對于這個(gè)層中的每個(gè)神經(jīng)元,通過先前層的連接
- // 列表,并更新相應(yīng)權(quán)重的差分
- 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;
- // 這是隱含的聯(lián)系; 隱含的神經(jīng)元輸出“1”
- }
- else
- {
- output = m_pPrevLayer.m_Neurons[(int)kk].output;
- }
- // ASSERT( (*cit).WeightIndex < d2Err_wrt_dWn.size() );
- // 因?yàn)樵趯2Err_wrt_dWn更改為C風(fēng)格的
- // 數(shù)組之后,size()函數(shù)將不起作用
- d2Err_wrt_dWn[cit.WeightIndex] = d2Err_wrt_dYn[ii] * output * output;
- }
- catch (Exception ex)
- {
- }
- }
- ii++;
- }
- // 計(jì)算d2Err_wrt_Xnm1 =(Wn)^ 2 * d2Err_wrt_dYn
- // (其中d2Err_wrt_dYn是不是***個(gè)二階導(dǎo)數(shù))。
- // 需要d2Err_wrt_Xnm1作為d2Err_wrt_Xn的
- // 二階導(dǎo)數(shù)反向傳播的輸入值
- // 對于下一個(gè)(即先前的空間)層
- // 對于這個(gè)層中的每個(gè)神經(jīng)元
- 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”的
- // 虛偏置神經(jīng)元,因?yàn)槲覀儾荒苷嬗?xùn)練偏置神經(jīng)元
- 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 跟蹤神經(jīng)元迭代器
- }
- double oldValue, newValue;
- // ***,使用dErr_wrt_dW更新對角線的層
- // 神經(jīng)元的權(quán)重。通過設(shè)計(jì),這個(gè)函數(shù)
- // 以及它對許多(約500個(gè)模式)的迭代被
- // 調(diào)用,而單個(gè)線程已經(jīng)鎖定了神經(jīng)網(wǎng)絡(luò),
- // 所以另一個(gè)線程不可能改變Hessian的值。
- // 不過,由于這很容易做到,所以我們使用一
- // 個(gè)原子比較交換操作,這意味著另一個(gè)線程
- // 可能在二階導(dǎo)數(shù)的反向傳播過程中,而且Hessians
- // 可能會(huì)稍微移動(dòng)
- for (jj = 0; jj < m_Weights.Count; ++jj)
- {
- oldValue = m_Weights[jj].diagHessian;
- newValue = oldValue + d2Err_wrt_dWn[jj];
- m_Weights[jj].diagHessian = newValue;
- }
- }
- //////////////////////////////////////////////////////////////////
訓(xùn)練和實(shí)驗(yàn)
盡管MFC / C ++和C#之間存不兼容,但是我的程序與原程序相似。使用MNIST數(shù)據(jù)庫,網(wǎng)絡(luò)在60,000個(gè)訓(xùn)練集模式中執(zhí)行后有291次錯(cuò)誤識(shí)別。這意味著錯(cuò)誤率只有0.485%。然而,在10000個(gè)模式中,有136個(gè)錯(cuò)誤識(shí)別,錯(cuò)誤率為1.36%。結(jié)果并不像基礎(chǔ)測試那么好,但對我來說,用我自己的手寫字符集做實(shí)驗(yàn)已經(jīng)足夠了。首先將輸入的圖像從上到下分為字符組,然后在每組中把字符從左到右進(jìn)行檢測,調(diào)整到29x29像素,然后由神經(jīng)網(wǎng)絡(luò)系統(tǒng)識(shí)別。該方案滿足我的基本要求,我自己的手寫數(shù)字是可以被正確識(shí)別的。在AForge.Net的圖像處理庫中添加了檢測功能,以便使用。但是,因?yàn)樗皇窃谖业臉I(yè)余時(shí)間編程,我相信它有很多的缺陷需要修復(fù)。反向傳播時(shí)間就是一個(gè)例子。每個(gè)周期使用大約3800秒的訓(xùn)練時(shí)間,但是只需要2400秒。(我的電腦使用了英特爾奔騰雙核E6500處理器)。與Mike的程序相比,速度相當(dāng)慢。我也希望能有一個(gè)更好的手寫字符數(shù)據(jù)庫,或者與其他人合作,繼續(xù)我的實(shí)驗(yàn),使用我的算法開發(fā)一個(gè)真正的應(yīng)用程序。
原文鏈接:https://www.codeproject.com/Articles/143059/Neural-Network-for-Recognition-of-Handwritten-Di
作者:Vietdungiitb
【本文是51CTO專欄作者“云加社區(qū)”的原創(chuàng)稿件,轉(zhuǎn)載請通過51CTO聯(lián)系原作者獲取授權(quán)】