ニューラル ネットワーク アルゴリズムを使用した C# での手書き数字認識

ニューラル ネットワーク アルゴリズムを使用した C# での手書き数字認識

デモをダウンロード - 2.77 MB (元のアドレス)

手書き文字認識.zip

ソースコードをダウンロード - 70.64 KB (元のアドレス)

nn手書き文字ソース.zip

導入

これは、Mike O'Neill による素晴らしい記事「手書き数字の認識のためのニューラル ネットワーク」に基づいて、人工ニューラル ネットワークを使用して手書き数字の認識を実装する例です。過去数年間に多くのシステムや分類アルゴリズムが提案されてきましたが、手書き認識はパターン認識において依然として困難なタスクです。 Mike O'Neill のプログラムは、ニューラル ネットワーク アルゴリズム、特にニューラル ネットワークの畳み込み部分による一般的な手書き認識について学習したいプログラマーにとって優れた例です。このプログラムは MFC/C++ で書かれているので、慣れていない人にとっては少し難しいです。そこで、いくつかのプログラムを C# で書き直すことにしました。私のプログラムは良い結果を達成しましたが、まだ優れているわけではありません (収束速度、エラー率などの点で)。しかし、これはプログラムの基本に過ぎず、目的はニューラル ネットワークの理解を助けることなので、少し乱雑で再構築する必要があります。私はこれをライブラリとして再構築しようとしています。これにより、INI ファイルを通じてパラメータを柔軟かつ簡単に変更できるようになります。いつか望み通りの結果が達成できることを願っています。

文字検出

パターン検出と文字候補検出は、私のプログラムで直面しなければならない最も重要な問題の 1 つです。実際、私は Mike のプログラムを別のプログラミング言語で再実装したいだけでなく、文書画像内の文字を認識したいと思っています。インターネットで見つけた非常に優れた物体検出アルゴリズムを提案する研究がいくつかありますが、私のような趣味のプロジェクトには複雑すぎます。娘に絵を教えているときに発見した方法がこの問題を解決しました。もちろん、まだ制限はありますが、最初のテストでは期待を上回る結果となりました。通常、文字候補の検出は、行検出、単語検出、文字検出に分かれており、それぞれ異なるアルゴリズムが使用されます。私のアプローチはこれとは少し異なります。検出には同じアルゴリズムが使用されます:

  1. 公共 静的矩形 GetPatternRectangeBoundary
  2. (ビットマップ オリジナル、 int colorIndex、 int hStep、 int vStep、bool bTopStart)

同様に:

  1. 公共 静的リスト<Rectangle> パターンRectangeBoundaryList
  2. (ビットマップオリジナル、 int colorIndex、 int hStep、 int vStep、
  3. bool bTopStart、 int widthMin、 int heightMin)

パラメータ hStep (水平ステップ) と vStep (垂直ステップ) を変更することで、行、単語、または文字を簡単に検出できます。 bTopStart を true または false に変更することで、長方形の境界を上から下、左から右に検出することもできます。長方形は widthMin と d によって制約されます。私のアルゴリズムの最大の利点は、同じ行にない単語や文字列を検出できることです。

文字候補の認識は次のように実現できます。

  1. パブリックvoid パターン認識スレッド(ビットマップ ビットマップ)
  2. {
  3. _originalBitmap = ビットマップ;
  4. _rowList == null場合
  5. {
  6. _rowList = AForge.Imaging.Image.パターン矩形境界リスト
  7. (_originalBitmap、255、30、1、 true 、5、5);
  8. _irowインデックス = 0;
  9.  
  10. }
  11. foreach(_rowList内の四角形 rowRect )
  12. {
  13. _currentRow = AForge.Imaging.ImageResize.ImageCrop
  14. (_originalBitmap、行Rect);
  15. _iwordIndex == 0 の場合
  16. {
  17. _currentWordsList = AForge.Imaging.Image.PatternRectangeBoundaryList
  18. (_currentRow、255、20、10、 false 、5、5);
  19. }
  20.  
  21. foreach ( _currentWordsList内のRectangle wordRect)
  22. {
  23. _currentWord = AForge.Imaging.ImageResize.ImageCrop
  24. (_currentRow、wordRect);
  25. _iwordインデックス++;
  26. _icharIndex == 0 の場合
  27. {
  28. _現在の文字リスト =
  29. AForge.Imaging.Image.PatternRectangeBoundaryList
  30. (_currentWord, 255, 1, 1, false , 5, 5);
  31. }
  32.  
  33. foreach ( _currentCharsList内の四角形 charRect)
  34. {
  35. _currentChar = AForge.Imaging.ImageResize.ImageCrop
  36. (_currentWord、charRect);
  37. _icharインデックス++;
  38. ビットマップ bmptemp = AForge.Imaging.ImageResize.FixedSize
  39. (_currentChar, 21, 21);
  40. bmptemp = AForge.Imaging.Image.CreateColorPad
  41. (bmptemp、色.白、4、4);
  42. bmptemp = AForge.Imaging.Image.CreateIndexedGrayScaleBitmap
  43. (bmptemp);
  44. byte[] graybytes = AForge.Imaging.Image.GrayscaletoBytes(bmptemp);
  45. パターン認識スレッド(graybytes);
  46. m_bitmaps.Add (bmptemp) ;
  47. }
  48. 文字列 s = " \n" ;
  49. _form.Invoke(_form._DelegateAddObject、新しいオブジェクト[] { 1、s });
  50. _icharIndex ==_ currentCharsList.Count場合
  51. {
  52. _icharインデックス =0;
  53. }
  54. }
  55. If(_iwordIndex==__ currentWordsList.Count )の場合
  56. {
  57. _iwordインデックス=0;
  58. }
  59. }

文字認識

元のプログラムの畳み込みニューラル ネットワーク (CNN) には、基本的に入力層を含めて 5 つの層があります。畳み込みアーキテクチャの詳細については、Mike と Simard 博士の論文「ビジュアル ドキュメント分析のための畳み込みニューラル ネットワークの最新技術」で説明されています。この畳み込みネットワークの全体的なスキームは、単純な特徴を高解像度で抽出し、それをより低い解像度の複雑な特徴に変換することです。より低い解像度を生成する最も簡単な方法は、サブレイヤーを 2 倍にサブサンプリングすることです。これは、畳み込みカーネルのサイズの参照を提供します。カーネルの幅は、1 つのユニット (奇数サイズ) を中心として選択され、情報が失われない程度のオーバーラップ (1 つのユニットに対して 3 のオーバーラップは小さすぎます) があり、冗長になりすぎません (7 のオーバーラップは大きすぎます。5 のオーバーラップでは 70% を超えるオーバーラップが実現されます)。したがって、このネットワークでは畳み込みカーネルのサイズとして 5 を選択します。入力をパディングする(特徴セルが境界の中央にくるように入力を大きくする)と、パフォーマンスは大幅に向上しません。したがって、パディングは使用されず、サブサンプリングのためにカーネル サイズは 5 に設定され、各畳み込み層は特徴サイズを n から (n-3)/2 に縮小します。 MNIST の初期入力画像サイズは 28x28 なので、2 回目の畳み込み後に生成される整数サイズはおよそ 29x29 になります。 2 層の畳み込みの後、5x5 の特徴サイズは 3 層目の畳み込みには小さすぎます。 Simard 博士はまた、最初のレイヤーの機能が 5 つ未満の場合、パフォーマンスが低下し、5 つを超えてもパフォーマンスは向上しないことを強調しました (Mike は 6 つを使用しました)。同様に、2 番目のレイヤーでは、50 個未満の機能ではパフォーマンスが低下しましたが、それ以上 (100 個) の機能では改善は見られませんでした。ニューラルネットワークの概要は次のとおりです。

レイヤー #0: MNIST データベースからの手書き文字のグレースケール画像。29 x 29 ピクセルにパディングされています。入力層には 29x29 = 841 個のニューロンがあります。

レイヤー #1: 6 つの特徴マップを持つ畳み込みレイヤーです。ニューロンは 13×13×6 = 1014 個、重みは (5×5+1)×6 = 156 個、レイヤー #1 から前のレイヤーへの接続は 1014×26 = 26364 個あります。

レイヤー 2: 50 個の特徴マップを持つ畳み込みレイヤーです。ニューロンは 5x5x50 = 1250 個、重みは (5x5 + 1)x6x50 = 7800 個、レイヤー #2 から前のレイヤーへの接続は 1250x(5x5x6 + 1) = 188750 個あります。

(Mike の記事にある 32500 の接続ではありません)。

レイヤー #3: 100 ユニットを持つ完全接続レイヤーです。ニューロンは 100 個、重みは 100 x (1250 + 1) = 125100 個、接続は 100 x 1251 = 125100 個あります。

レイヤー #4: 最も大きく、10 個のニューロン、10×(100+1)=1010 個の重み、10×101=1010 個の接続を備えています。

バックプロパゲーション

バックプロパゲーションは、最後のレイヤーから始めて最後のレイヤーに到達するまで前進し、各レイヤーでの重みの変化を更新するプロセスです。

標準的なバックプロパゲーションでは、各重みは次の式に従って更新されます。

(1)

ここで、eta は「学習率」であり、通常は 0.0005 のような小さな数値で、トレーニング中に徐々に減少します。ただし、収束が遅いため、このプログラムでは標準的なバックプロパゲーションは使用されません。代わりに、LeCun 博士が論文「Efficient BackProp」で提案した「Randomized Diagonal Levenberg-Marquardt」と呼ばれる 2 次手法が適用されます。Mike は、これは標準的なバックプロパゲーションと同じではないと述べていますが、この理論は私のような初心者がコードをより簡単に理解するのに役立つはずです。

Levenberg-Marquardt 法では、rw は次のように計算されます。

二乗コスト関数が次の通りであると仮定します。

勾配は次のようになります。

ヘッセン人は以下の規則に従います。

ヘッセ行列の簡略化された近似はヤコビ行列であり、これは N×O 次元の半行列です。

ニューラル ネットワークでヘッセ行列の対角成分を計算するためのバックプロパゲーション手順はよく知られています。ネットワーク内の各層には次のものがあると仮定します。

(7)

ガウス-ニュートン近似(|'(y)を含む項を削除)を使用すると、次の式が得られます。

(8)

(9)

同様に:

ランダム対角レーベンバーグ・マルカート法

実際、完全なヘッセ行列情報を使用する手法 (Levenberg-Marquardt、Gaus-Newton など) は、ランダム モードではなくバッチ モードでトレーニングされた非常に小さなネットワークにのみ適用できます。レベンバーグ・マルカートアルゴリズムのランダムパターンを得るために、ルカン博士は、各パラメータに関する2次導関数の推定値を操作してヘッセ行列の対角線を計算するというアイデアを提案しました。瞬間的な2次微分は、式(7,8,9)に示すように、逆伝播法によって得ることができる。これらの運用推定値があれば、それを使用して各パラメータの個別の学習率を計算できます。

ここでeはグローバル学習率であり、

は、h ki に関する対角2次微分の演算推定値です。 m は、2 次導関数が小さい場合 (つまり、最適化がエラー関数の平坦な部分で行われる場合) に hki がドリフトするのを防ぐパラメーターです。 2 次導関数は、トレーニング セットのサブセット (トレーニング セットの 500 個のランダム パターン / 60000 個のパターン) で計算できます。これらは非常にゆっくりと変化するため、数サイクルごとに再推定するだけで済みます。元のプログラムでは、対角ヘッセ行列はサイクルごとに再推定されます。

以下は C# の 2 次導関数計算関数です。

  1. パブリックvoid BackpropagateSecondDerivatives(DErrorsList d2Err_wrt_dXn /* in */,
  2. DErrorsList d2Err_wrt_dXnm1 /*出力*/)
  3. {
  4. // 名前 (NeuralNetwork クラスから継承)
  5. // 注意: ここでは2次微分(1次微分ではない)を扱っていますが、
  6. // しかし、1階微分があるのとほぼ同じ表記法を使用します
  7. // 同じです。そうでないと、ASCII 表示が誤解を招くことになります。 我々は
  8. // 簡略化のため、"d2Err_wrt_dXn" のように 2 つの "2" の代わりに "2" を使用します。
  9. // 2次微分を使うことを強調するだけです
  10. //
  11. // Errはニューラルネットワーク全体の出力誤差です
  12. // Xn は n 番目の層の出力ベクトルです
  13. // Xnm1は前の層の出力ベクトルです
  14. // Wn は n 番目の層の重みのベクトルです
  15. // Ynはn番目の層の活性化値です。
  16. // つまり、squeeze関数を適用する前の入力の加重合計です
  17. // F は圧縮関数です: Xn = F(Yn)
  18. // F'はスクイーズ関数の導関数である
  19. // 簡単に言うと、F = tanh の場合、F'(Yn) = 1-Xn^2 となり、
  20. // 入力を知らなくても出力から導関数を計算できる
  21.   
  22. 整数ii,jj;
  23. uint kk;
  24. intインデックス;
  25. ダブル 出力;
  26. ダブルdTemp;
  27.   
  28. var d2Err_wrt_dYn = 新しい DErrorsList( m_Neurons.Count );
  29. //
  30. // std::vector< double > d2Err_wrt_dWn( m_Weights.size ( ), 0.0 );
  31. //ゼロ初期化することが重要です
  32. //////////////////////////////////////////////////
  33. //
  34. ///// デザインのトレードオフ:レビュー!!
  35. //
  36. // この命名規則はNNLayer::Backpropagate()と同一であることに注意してください
  37. // 関数と同じ推論、つまりこの関数から派生した関数
  38. // BackpropagateSecondDerivatives() 関数
  39. //
  40. // 配列 "d2Err_wrt_dWn" には STL ベクトルを使用します (コーディングを簡単にするため)
  41. // は、レイヤー内の現在のモードのエラー重みの 2 次微分です。 しかし、
  42. // 多くの重みを持つレイヤー (完全に接続されたレイヤーなど) には、多くの重みもあります。 ポイント
  43. // 大きなメモリブロックを割り当てる場合、STLベクトルクラスアロケータは非常に愚かで、多くのページエラーを引き起こします。
  44. // 表面的なエラーが発生し、アプリケーション全体の実行時間が遅くなります。
  45.   
  46. // この問題を解決するために、通常のC配列を使用しようとしました。
  47. // ヒープから必要なスペースを取得し、関数の最後で [] を削除します。
  48. // ただし、これによりページフォールトエラーの数は同じになり、
  49. // パフォーマンスは向上しません。
  50.   
  51. // そこで、スタック (つまりヒープではない) に通常の C 配列を割り当ててみました。
  52. // もちろん、 double d2Err_wrt_dWn[m_Weights.size ( )];と書くことはできません。
  53. // コンパイラは、配列サイズに対してコンパイル時の既知の定数値を要求するためです。
  54. // この必要性を回避するために、_alloca 関数を使用してスタック上にメモリを割り当てます。
  55. // これを実行すると、スタックが過剰に使用され、スタック オーバーフローの問題が発生する可能性があるという欠点があります。
  56. // それが「レビュー」という名前が付けられた理由です
  57.  
  58.   
  59. ダブル[] d2Err_wrt_dWn = 新しいダブル[ m_Weights.Count ];
  60. (ii = 0; ii < m_Weights.Count ; ++ii)の場合
  61. {
  62. d2Err_wrt_dWn[ii] = 0.0;
  63. }
  64. // 計算 d2Err_wrt_dYn = ( F'(Yn) )^2 *
  65. // dErr_wrt_Xn (ここで、dErr_wrt_Xn は実際には 2 次導関数です)
  66.   
  67. (ii = 0; ii < m_Neurons.Count ; ++ii)の場合
  68. {
  69. 出力= m_Neurons[ii] .output ;
  70. dTemp = m_sigmoid.DSIGMOID(出力);
  71. d2Err_wrt_dYn.Add (d2Err_wrt_dXn[ii] * dTemp * dTemp) ;
  72. }
  73. // d2Err_wrt_Wn = (Xnm1)^2 * d2Err_wrt_Yn を計算します
  74. // (ここで、dE2rr_wrt_Yn は実際には 2 次導関数です)
  75. // この層の各ニューロンを前の層を介して接続します
  76. // リストし、対応する重みの差を更新します
  77.   
  78. ii = 0;
  79. foreach (NNNeuron nit in m_Neurons)
  80. {
  81. foreach (NNConnection は nit.m_Connectionsあります)
  82. {
  83. 試す
  84. {
  85. kk = (uint)cit.NeuronIndex;
  86. (kk == 0xffffffff)の場合
  87. {
  88. 出力= 1.0;
  89. // これは暗黙的な接続です。暗黙的なニューロンは「1」を出力します。
  90. }
  91. それ以外 
  92. {
  93. 出力= m_pPrevLayer.m_Neurons[( int )kk] .output ;
  94. }
  95.   
  96. // ASSERT( (*cit).WeightIndex < d2Err_wrt_dWn.size () );
  97. // d2Err_wrt_dWnをCスタイルに変更すると
  98. // 配列の後では、 size ()関数は機能しません
  99.  
  100. d2Err_wrt_dWn[cit.WeightIndex] = d2Err_wrt_dYn[ii] *出力*出力;
  101. }
  102. catch (例外例)
  103. {
  104.   
  105. }
  106. }
  107.   
  108. ii++;
  109. }
  110. // d2Err_wrt_Xnm1 = (Wn)^2 * d2Err_wrt_dYn を計算します
  111. // (ここで、d2Err_wrt_dYn は最初の 2 次導関数です)。
  112. // d2Err_wrt_Xn として d2Err_wrt_Xnm1 が必要です
  113. // 2次微分の逆伝播の入力値
  114. // 次の(つまり前のスペース)レイヤーの場合
  115. // この層の各ニューロンについて
  116.   
  117. ii = 0;
  118. foreach (NNNeuron nit in m_Neurons)
  119. {
  120. foreach (NNConnection は nit.m_Connectionsあります)
  121. {
  122. 試す
  123. {
  124. ニューロンインデックス
  125. (kk != 0xffffffff) の場合
  126. {
  127. // 定数出力「1」を表す ULONG_MAX は除外します。
  128. // 仮想バイアスニューロン。バイアスニューロンを実際に訓練することはできないため
  129.   
  130. nIndex = ( int )kk;
  131. dTemp = m_Weights[( int )cit.WeightIndex].value;
  132. d2Err_wrt_dXnm1[nIndex] += d2Err_wrt_dYn[ii] * dTemp * dTemp;
  133. }
  134. }
  135. catch (例外例)
  136. {
  137. 戻る;
  138. }
  139. }
  140.   
  141. ii++; // ii はニューロン反復子を追跡します
  142. }
  143. 古い値と新しい値を二重に保持します
  144.   
  145. //***、dErr_wrt_dW を使用して対角レイヤーを更新します
  146. // ニューロンの重み。設計上、この機能は
  147. // そして、多くの(約500パターン)にわたる反復は
  148. // 単一のスレッドがニューラルネットワークをロックしている間に呼び出し、
  149. // したがって、別のスレッドが Hessian の値を変更することはできません。
  150. // しかし、これは簡単に実行できるので、
  151. // アトミックな比較と交換の操作。つまり、別のスレッドが
  152. // おそらく2次導関数の逆伝播中、およびヘッセ行列
  153. // 少し動くかもしれない
  154.   
  155. ( jj = 0; jj < m_Weights.Count ; ++jj)の場合
  156. {
  157. 古い値 = m_Weights[jj].diagHessian;
  158. 新しい値 = 古い値 + d2Err_wrt_dWn[jj];
  159. m_Weights[jj].diagHessian = 新しい値;
  160. }
  161. }
  162. //////////////////////////////////////////////////////////////////

トレーニングと実験

MFC/C++ と C# の間には互換性がないにもかかわらず、私のプログラムは元のプログラムと似ています。 MNIST データベースを使用して、ネットワークは 60,000 のトレーニング セット パターンのうち 291 件の誤認を実行しました。つまり、エラー率はわずか 0.485% です。しかし、10,000 パターンのうち 136 個が誤認され、エラー率は 1.36% でした。結果は基本テストほど良くはありませんが、自分の手書き文字セットで実験するには十分です。入力画像はまず上から下に向かって文字グループに分割され、次に各グループ内の文字が左から右に向かって検出され、29x29 ピクセルにサイズ変更されて、ニューラル ネットワーク システムによって認識されます。このソリューションは私の基本的な要件を満たしており、手書きの数字も正しく認識できます。使いやすさを向上させるために、AForge.Net の画像処理ライブラリに検出機能を追加しました。しかし、これは余暇にプログラミングしただけなので、修正が必要なバグがたくさんあると思います。時間の経過によるバックプロパゲーションがその一例です。各エポックでは約 3800 秒のトレーニング時間がかかりますが、実際には 2400 秒しかかかりません。 (私のコンピューターは Intel Pentium Dual-Core E6500 プロセッサを使用しています)。マイクのプログラムと比べると、かなり遅いです。また、手書き文字のより優れたデータベースを作成したり、他の人と協力して実験を継続したり、自分のアルゴリズムを使用して実際のアプリケーションを開発したりしたいと考えています。

オリジナルリンク: https://www.codeproject.com/Articles/143059/Neural-Network-for-Recognition-of-Handwritten-Di

著者: Vietdungiitb

[この記事は51CTOコラムニスト「雲家コミュニティ」によるオリジナル記事です。転載の許可を得るには51CTOを通じて原作者に連絡してください]

この著者の他の記事を読むにはここをクリックしてください

<<:  2018年に人工知能がビジネスに及ぼす10のインパクト

>>:  ブロックチェーン投資の10大リスクポイント。これらを理解していないなら投資しないでください!

ブログ    
ブログ    
ブログ    
ブログ    

推薦する

「段階的に考える」だけでは不十分です。モデルを「より多くのステップで考える」ようにすれば、より有用になります。

今日では、大規模言語モデル (LLM) とその高度なヒント戦略の出現により、特に古典的な NLP タ...

AIを慎重に導入するためのベストプラクティス

人工知能を正しく使用するために、いくつかの提案があります。人工知能を実際に使用する際にこれらの提案を...

AIが予測分析アプリケーションに与える影響

人工知能 (AI) を使用した予測分析により、企業は過去のデータに基づいて将来の結果を予測し、運用効...

コンテストを利用して学習を促進し、エコシステムを共同で構築し、人工知能を普及させましょう。

[元記事は51CTO.comより] 2021年7月12日、上海紫竹コートヤードホテルで、神府改革革...

TikTokが米メディアにアルゴリズムの原則を導入:まずは8つの人気動画を使ってユーザーを理解する

人気の短編動画アプリ「TikTok」(Douyinの海外版)は、主にアルゴリズムのおかげで、世界中で...

ディープラーニングの仕組み: 今日の AI を支えるニューラル ネットワークの内部を覗いてみよう

[[428985]] [51CTO.com クイック翻訳]今日の人工知能の繁栄は、人工ニューラルネッ...

ヘルスケア業界における人工知能と機械学習の応用

[[414016]]ヘルスケア業界における人工知能と機械学習の役割を理解するには、ヘルスケア業界にお...

宇宙全体が巨大なニューラルネットワークなのだろうか?科学者はこう説明する

[[385301]]核となる考え方は、次のように簡単にまとめることができます。ニューラル ネットワー...

CPP アルゴリズム問題のための共通コンテナ技術

[[413003]]アルゴリズムの問​​題を解決するときに CPP でよく使用されるコンテナ テクニ...

生成AIとクラウドの相互利益を探る

近年、生成 AI とクラウドの融合に関心が集まっているのには理由があります。人工知能 (AI) とク...

AppleのApp Storeランキングアルゴリズムが明らかに

ルールを研究し、ランキングの計算方法を大まかに推定した人もいます。今日のランキング = 今日のダウン...

チューリング賞受賞者ジョン・ヘネシー氏:データと機械学習は世界をより良い場所にする

5月26日、チューリング賞受賞者で米国工学アカデミー会員のジョン・ヘネシー氏が、2021年中国国際ビ...

GPU + 生成AIが時空間データ分析の改善に貢献

翻訳者|朱 仙中レビュー | Chonglou導入携帯電話、気候センサー、金融市場取引、車両や輸送コ...

かつては世界トップ50のロボット技術企業の一つだったスターロボット企業がまた一つ倒産した。

この記事はAI新メディアQuantum Bit(公開アカウントID:QbitAI)より許可を得て転載...