ize:medium;!DOCTYPE html> AI talk with me: C Language, C99 MNIST Neural Network Implementation

 ニューラルネットワークの "Hello, world!" MNIST を C99
 C Language で フルスクラッチ実装する。

 
 多くのライブラリーが提供されているので、簡単に高認識率の環境を構築できるのは有難いのですが、
 初心者や、中級者にとっては、全てがブラックボックス化されており、またコードが公開されていても、
 あまりに複雑で見通しが悪く中々本質が分かりにくい。
 
 簡単な実装(300行程度)を、外部ライブラリを使用しないで実装することで、初心者でも理解し易い
 実際に動作するサンプルを使って、わからないところは生成AIに説明してもらいながら、自己学習を
 進められるように用意した。
 
 習うより、慣れろ! の精神で、トライしてもらえればと思う。
 
 良い結果が得られることを、祈りながら。
 
 
 追記するなら、
 記憶の実態も含め、AGIの実装形態で最も可能性の高い構造が、超分散型パーセプトロン(UDP)かも知れない。
 何処かの誰かが UDPの実装をすれば、現状とは異なる面白い未来がある様にも思える。
 (注)ここでのAGIは以下の様に定義する
  ・集められた情報の中から、共通性(ルール・定理)を自動的に見つけ出す或いは定義する。
  ・自己生成したるルールや定理、仮定を証明する、具体的な方法を論理的に構築する。
  ・自己生成したプランに従い、検証を行いその結果を保存再利用する機能を有する
 

一般的認識だと、この規模の正解率は、90% 前後とされているようです。

このプログラムの MNIST のような手書き数字画像分類に対する正解率(accuracy) を予測すると、
おおよそ 85〜92%の範囲 になる可能性があります。
ただし、使用する画像データセットの内容や量に大きく依存します。以下にその予測の論拠を説明します。


正解率予測:85〜92%

この推定は、小規模な1層のMLP(多層パーセプトロン)でのMNIST分類タスクにおける実績から得られています。


論拠の詳細:

1. ニューラルネットワーク構造

▶ この構成は典型的なシンプルなMLPです。
学習用画像がMNIST相当なら、この構成で 88〜92%は比較的容易に到達可能です(文献・実測から確認済み)。

文献による正解率(MLP構造でのMNIST分類)

LeCun et al. (1998) – "Gradient-Based Learning Applied to Document Recognition"


Hinton's Lecture Notes(UofT, Coursera Deep Learning)


補足情報 Evaluation error rate: 1.25% (125/10000)

>mnist_base.exe | tee log_base.text

>head -30 log_base.text
Epoch 1, Loss: 0.3209, Accuracy: 0.9107
Epoch 2, Loss: 0.1654, Accuracy: 0.9523
Epoch 3, Loss: 0.1228, Accuracy: 0.9647
Epoch 4, Loss: 0.0991, Accuracy: 0.9718
Epoch 5, Loss: 0.0832, Accuracy: 0.9768
Epoch 6, Loss: 0.0719, Accuracy: 0.9796
Epoch 7, Loss: 0.0631, Accuracy: 0.9822
Epoch 8, Loss: 0.0560, Accuracy: 0.9845
Epoch 9, Loss: 0.0503, Accuracy: 0.9863
Epoch 10, Loss: 0.0455, Accuracy: 0.9878
Epoch 11, Loss: 0.0413, Accuracy: 0.9888
Epoch 12, Loss: 0.0377, Accuracy: 0.9898
Epoch 13, Loss: 0.0345, Accuracy: 0.9911
Epoch 14, Loss: 0.0316, Accuracy: 0.9921
Epoch 15, Loss: 0.0291, Accuracy: 0.9930
Epoch 16, Loss: 0.0267, Accuracy: 0.9940
Epoch 17, Loss: 0.0247, Accuracy: 0.9946
Epoch 18, Loss: 0.0227, Accuracy: 0.9954
Epoch 19, Loss: 0.0211, Accuracy: 0.9959
Epoch 20, Loss: 0.0195, Accuracy: 0.9964
TEST_00000.7.png 7   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7: 99.99%  8:  0.00%  9:  0.00%
TEST_00001.2.png 2   0:  0.00%  1:  0.00%  2:100.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_00002.1.png 1   0:  0.00%  1: 99.86%  2:  0.01%  3:  0.00%  4:  0.08%  5:  0.00%  6:  0.00%  7:  0.01%  8:  0.04%  9:  0.00%
TEST_00003.0.png 0   0:100.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_00004.4.png 4   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4: 99.95%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.04%
TEST_00005.1.png 1   0:  0.00%  1: 99.96%  2:  0.00%  3:  0.00%  4:  0.01%  5:  0.00%  6:  0.00%  7:  0.02%  8:  0.00%  9:  0.00%
TEST_00006.4.png 4   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4: 99.94%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.01%  9:  0.05%
TEST_00007.9.png 9   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.02%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9: 99.97%
TEST_00008.5.png 5   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5: 99.87%  6:  0.12%  7:  0.00%  8:  0.01%  9:  0.00%
TEST_00009.9.png 9   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  1.48%  5:  0.00%  6:  0.00%  7:  0.02%  8:  0.00%  9: 98.50%

>tail -10 log_base.text
TEST_09990.7.png 7   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:100.00%  8:  0.00%  9:  0.00%
TEST_09991.8.png 8   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:100.00%  9:  0.00%
TEST_09992.9.png 9   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.62%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9: 99.38%
TEST_09993.0.png 0   0:100.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09994.1.png 1   0:  0.00%  1: 99.98%  2:  0.01%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.01%  9:  0.00%
TEST_09995.2.png 2   0:  0.00%  1:  0.00%  2:100.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09996.3.png 3   0:  0.00%  1:  0.00%  2:  0.00%  3: 99.99%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.01%
TEST_09997.4.png 4   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:100.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09998.5.png 5   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5: 99.99%  6:  0.00%  7:  0.00%  8:  0.01%  9:  0.00%
TEST_09999.6.png 6   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:100.00%  7:  0.00%  8:  0.00%  9:  0.00%
>
>cat log_base.text | grep -v -e "0.png 0" -e "1.png 1" -e "2.png 2" -e "3.png 3" -e "4.png 4" -e "5.png 5" -e "6.png 6" -e "7.png 7"
-e "7.png 7" -e "8.png 8" -e "9.png 9" | grep -e "TEST_" | wc -l
217

誤った認識例
>cat log_base.text | grep -v  -e "0.png 0" -e "1.png 1" -e "2.png 2" -e "3.png 3" -e "4.png 4" -e "5.png 5"
-e "6.png 6" -e "7.png 7" -e "7.png 7" -e "8.png 8" -e "9.png 9" | grep -e "TEST_" | head

TEST_00217.6.png 5   0:  0.01%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5: 73.79%  6: 26.18%  7:  0.00%  8:  0.01%  9:  0.00%
TEST_00247.4.png 2   0:  0.00%  1:  0.01%  2: 96.87%  3:  0.00%  4:  0.69%  5:  0.00%  6:  2.42%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_00259.6.png 0   0: 64.94%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.02%  6: 35.04%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_00321.2.png 7   0:  0.00%  1:  0.00%  2: 10.54%  3:  0.86%  4:  0.00%  5:  0.00%  6:  0.00%  7: 87.84%  8:  0.71%  9:  0.04%
TEST_00340.5.png 3   0:  0.00%  1:  0.01%  2:  0.00%  3: 80.18%  4:  0.00%  5: 19.81%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_00445.6.png 0   0: 96.03%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  3.97%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_00449.3.png 5   0:  0.00%  1:  0.00%  2:  0.00%  3: 30.27%  4:  0.00%  5: 69.73%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_00495.8.png 2   0:  2.30%  1:  0.00%  2: 90.02%  3:  0.01%  4:  0.01%  5:  0.00%  6:  0.00%  7:  0.00%  8:  7.65%  9:  0.00%
TEST_00582.8.png 2   0:  0.00%  1:  1.44%  2: 95.53%  3:  1.74%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  1.29%  9:  0.00%
TEST_00646.2.png 6   0:  0.22%  1:  0.92%  2: 20.23%  3:  0.00%  4:  7.64%  5:  0.02%  6: 70.97%  7:  0.00%  8:  0.00%  9:  0.00%


学習+評価のトータル時間ですが、必要最小限の実装なのですが、かなり時間がかかります。

$ time ./mnist_base
    :
TEST_09992.9.png 9   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.45%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9: 99.55%
TEST_09993.0.png 0   0:100.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09994.1.png 1   0:  0.00%  1: 99.99%  2:  0.01%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09995.2.png 2   0:  0.00%  1:  0.00%  2:100.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09996.3.png 3   0:  0.00%  1:  0.00%  2:  0.00%  3: 99.92%  4:  0.00%  5:  0.08%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09997.4.png 4   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:100.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09998.5.png 5   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5: 99.98%  6:  0.00%  7:  0.00%  8:  0.02%  9:  0.00%
TEST_09999.6.png 6   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:100.00%  7:  0.00%  8:  0.00%  9:  0.00%

real    5m31.644s
user    0m0.016s
sys     0m0.062s


この系列で確認できた、最優良値(更に改善、1.70% が 1.55%)モデルに手を入れなくても、画像の前処理、重みの調整による可能性の示唆
TEST_09980.2.png 2   0:  0.00%  1:  0.00%  2: 99.99%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.01%  8:  0.00%  9:  0.00%
TEST_09981.6.png 6   0:  0.16%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6: 99.83%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09982.5.png 5   0:  0.00%  1:  0.00%  2:  0.00%  3:  2.78%  4:  0.00%  5: 90.65%  6:  6.57%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09983.0.png 0   0:100.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09984.1.png 1   0:  0.00%  1: 99.99%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.01%  8:  0.00%  9:  0.00%
TEST_09985.2.png 2   0:  0.00%  1:  0.00%  2:100.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09986.3.png 3   0:  0.00%  1:  0.00%  2:  0.00%  3:100.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09987.4.png 4   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:100.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09988.5.png 5   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:100.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09989.6.png 6   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:100.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09990.7.png 7   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:100.00%  8:  0.00%  9:  0.00%
TEST_09991.8.png 8   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:100.00%  9:  0.00%
TEST_09992.9.png 9   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.01%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9: 99.99%
TEST_09993.0.png 0   0:100.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09994.1.png 1   0:  0.00%  1:100.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09995.2.png 2   0:  0.00%  1:  0.00%  2:100.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09996.3.png 3   0:  0.00%  1:  0.00%  2:  0.00%  3:100.00%  4:  0.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09997.4.png 4   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:100.00%  5:  0.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09998.5.png 5   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:100.00%  6:  0.00%  7:  0.00%  8:  0.00%  9:  0.00%
TEST_09999.6.png 6   0:  0.00%  1:  0.00%  2:  0.00%  3:  0.00%  4:  0.00%  5:  0.00%  6:100.00%  7:  0.00%  8:  0.00%  9:  0.00%

Evaluation error rate: 1.70% (170/10000)         Number of training  60000
Total processing time: 264.37 seconds (Training: 258.81, Evaluation: 0.33)

real    4m24.410s
user    0m0.749s
sys     0m2.828s



Evaluation error rate: 1.55% (155/10000)         Number of training  300000
Total processing time: 1424.76 seconds (Training: 1222.54, Evaluation: 0.29)

real    23m44.791s
user    0m1.859s
sys     0m12.452s

 


 USER

次のプログラムを評価してください。

/* mnist_base.c   C99でスクラッチ実装した、MNIST用ニューラルネット(学習+評価)

現在、各種多くの提供されているライブラリーを使って、簡単に高認識率(99.9%)で認識
出来るのですが、内部がブラックボックスで、ニューラルネットワークの詳細なイメージを
認識するのは難しいので、ニューラルネットワークの学習用に C99 でフル実装した例を
用意しました。 拡張にはコードの書き換えが必要ですが、300行程度の行数で、97%強を
実現しているので、参考にはなると思います。

評価手順
1,MNIST データーセットを取得する
    例えば、
    TensorFlow のチームでの管理、クリエイティブ・コモンズ 表示-継承3.0ライセンスで提供
    https://www.tensorflow.org/api_docs/python/tf/keras/datasets/mnist/load_data
    This is a dataset of 60,000 28x28 grayscale images of the 10 digits, along with 
    a test set of 10,000 images. More info can be found at the MNIST homepage. <= NA
    Dataset source: mnist.npz(11.49 MB)
    https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
    Learning に学習用データを配置してください。 データファイル名.ラベル1文字.png
          Ex.00000.5.png
             00001.0.png
             00002.4.png
             00003.1.png
               :
    Evaluation に学習用データを配置してください。 データファイル名.png
          Ex.TEST_00000.7.png
             TEST_00001.2.png
             TEST_00002.1.png
             TEST_00003.0.png
                :

2,C99ソース + 画像読み込み用(stb_image.h)のみの構成で、識別率は 97% 程度です。
    コンパイルサンプル:
    gcc -O3 -std=c99 -W -Wall -Wextra -o mnist_base mnist_base.c -lm
    or
    gcc -O3 -std=c99 -mavx2 -mfma -W -Wall -Wextra -o mnist_base mnist_base.c -lm

3,Learning ディレクトリから、画像ファイル(ラベル文字を含む)をスキャンし学習。
    60,000枚 28x28画素 のグレー画像ファイルが格納されている前提です
    学習完了後、
    Evaluation ディレクトリから、画像ファイル(10,000枚)を読み込み、認識を行い
    ファイル名、認識した数字 0から9までの該当性割合の順で、結果を標準出力に書き出し

4,これだけの機能しかないので、認識精度は使用者が確認する必要があります。
    評価用の画像ファイル名も、連番ファイル名.ラベル1文字.png としてある場合は、
    下記のような方法で確認出来ます。
    実行と、出力のファイル化 	tee コマンドで、標準出力をファイルにも書き出す。
    mnist_base | tee log_mnist_base.text
    ファイル名に工夫があり、9.png の様に、拡張子の前にラベルを1文字加えてある場合は
    grep コマンドで、ラベルと認識が一致している(ex. ...0.png 0 ... )パターンを
    除くことで、誤った例を抽出できます。また、データ以外の余分な行が紛れ込まない様に
    ファイル名の先頭に付けたプレフィクスで更に抽出することで、エラー情報のみに限定
    できます。更に、wcコマンドで出力された行数をカウントすることで、エラー数が把握
    できます。 10,000枚 評価した結果ですので、218 であれば、エラー率は、2.18% です。
    cat log_*.text |grep -v  -e "0.png 0"...-e "9.png 9" |grep -e "TEST_" |wc -l

 EX.   +project/                    
      ├── Learning/
      │   ├── image1.0.png
      │   ├── image2.5.png
      │   └── ...
      ├── Evaluation/
      │   ├── test1.png
      │   ├── test2.png
      │   └── ...
      ├── stb_image.h
      └── mnist_nn.c or mnist_base.c
*/


// https://github.com/nothings/stb/blob/master/stb_image.h #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #include <stdio.h> #include <stdlib.h> #include <math.h> #include <string.h> #include <dirent.h> #include <time.h> #define IMAGE_SIZE 28 // MNIST IMAGE 28x28 pixel #define IN_PUT_SIZE 784 // 28x28 #define HIDDEN_SIZE 128 #define OUTPUT_SIZE 10 // For: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 #define BATCH_SIZE 15 #define LEARNING_RATE 0.035 #define EPOCHS 20 #define v2D(type,lx,name) ((type(*)[(lx)])name) #define v2Ducp(lx,name) v2D(unsigned char*,lx,name) #define v2Duc(lx,name) v2D(unsigned char,lx,name) #define v2Dc(lx,name) v2D(char,lx,name) #define v2Di(lx,name) v2D(int,lx,name) #define v2Df(lx,name) v2D(float,lx,name) #define v2Dd(lx,name) v2D(double,lx,name) #define v2Dsx(lx,name) v2D(STBPX,lx,name) #pragma pack(push,1) typedef struct PIXEL { unsigned char B, G, R, A; } PIXEL ; typedef struct STBPX { unsigned char R, G, B, A; } STBPX ; #pragma pack(pop) // ニューラルネットワーク構造 typedef struct { float w1[IN_PUT_SIZE][HIDDEN_SIZE]; float b1[HIDDEN_SIZE]; float w2[HIDDEN_SIZE][OUTPUT_SIZE]; float b2[OUTPUT_SIZE]; } NeuralNetwork; // 順伝播用バッファ typedef struct { float hidden[HIDDEN_SIZE]; float output[OUTPUT_SIZE]; } ForwardPass; // 画像データ構造 typedef struct { char *filename; int label; // 学習用データの場合のみ使用(評価用は-1) float input[IN_PUT_SIZE]; } ImageData; // 重み初期化 void initialize_weights( NeuralNetwork *nn ) { srand(time(NULL)); float scale1 = sqrt( 6.0 / (IN_PUT_SIZE + HIDDEN_SIZE) ); float scale2 = sqrt( 6.0 / (HIDDEN_SIZE + OUTPUT_SIZE) ); for(int y=0; y<IN_PUT_SIZE; y++)for(int x=0; x<HIDDEN_SIZE; x++) nn->w1[y][x] = ((float)rand()/RAND_MAX)*2*scale1 - scale1; for(int x=0; x<HIDDEN_SIZE; x++) nn->b1[x] = 0.0; for(int y=0; y<HIDDEN_SIZE; y++)for(int x=0; x<OUTPUT_SIZE; x++) nn->w2[y][x] = ((float)rand()/RAND_MAX)*2*scale2 - scale2; for(int x=0; x<OUTPUT_SIZE; x++) nn->b2[x] = 0.0; } // ReLU static inline float relu( float x ){ return x > 0 ? x : 0; } // ソフトマックス void softmax( float *input, float *output, int size ) { float max=input[0]; for(int i=1; i<size; i++ ) if( input[i] > max ) max = input[i]; float sum=0.0; for(int i=0; i<size; i++ ) { output[i] = exp(input[i] - max); sum += output[i]; } for(int i=0; i<size; i++ ) output[i] /= sum; } // 順伝播 void forward(NeuralNetwork *nn, float *input, ForwardPass *fp) { for(int j=0; j<HIDDEN_SIZE; j++) { float sum = nn->b1[j]; for(int i=0; i<IN_PUT_SIZE; i++ ) sum += input[i] * nn->w1[i][j]; fp->hidden[j] = relu(sum); } for(int j=0; j<OUTPUT_SIZE; j++) { float sum = nn->b2[j]; for(int i=0; i<HIDDEN_SIZE; i++ ) sum += fp->hidden[i] * nn->w2[i][j]; fp->output[j] = sum; } softmax(fp->output, fp->output, OUTPUT_SIZE); } // 逆伝播 float backward(NeuralNetwork *nn, float *input, ForwardPass *fp, int label, float grad_w1[IN_PUT_SIZE][HIDDEN_SIZE], float grad_b1[HIDDEN_SIZE], float grad_w2[HIDDEN_SIZE][OUTPUT_SIZE], float grad_b2[OUTPUT_SIZE]) { float target[OUTPUT_SIZE] = {0}; target[label] = 1.0; float loss = 0.0; float delta_output[OUTPUT_SIZE]; for(int j=0; j<OUTPUT_SIZE; j++) { delta_output[j] = fp->output[j] - target[j]; loss -= target[j] * log(fp->output[j] +1e-10); grad_b2[j] = delta_output[j]; for(int i=0; i<HIDDEN_SIZE; i++) grad_w2[i][j] = delta_output[j] * fp->hidden[i]; } float delta_hidden[HIDDEN_SIZE] = {0}; for(int i=0; i<HIDDEN_SIZE; i++) { if( fp->hidden[i] > 0) { for(int j=0; j<OUTPUT_SIZE; j++) delta_hidden[i] += delta_output[j] * nn->w2[i][j]; } grad_b1[i] = delta_hidden[i]; for(int j=0; j<IN_PUT_SIZE; j++) grad_w1[j][i] = delta_hidden[i] * input[j]; } return loss; } // 重み更新 void update_weights(NeuralNetwork *nn, float grad_w1[IN_PUT_SIZE][HIDDEN_SIZE], float grad_b1[HIDDEN_SIZE], float grad_w2[HIDDEN_SIZE][OUTPUT_SIZE], float grad_b2[OUTPUT_SIZE], int batch_size) { float lr = LEARNING_RATE / batch_size; for(int i=0; i<IN_PUT_SIZE; i++)for(int j=0; j<HIDDEN_SIZE; j++) nn->w1[i][j] -= lr * grad_w1[i][j]; for(int j=0; j<HIDDEN_SIZE; j++) nn->b1[j] -= lr * grad_b1[j]; for(int i=0; i<HIDDEN_SIZE; i++)for(int j=0; j<OUTPUT_SIZE; j++) nn->w2[i][j] -= lr * grad_w2[i][j]; for(int j=0; j<OUTPUT_SIZE; j++) nn->b2[j] -= lr * grad_b2[j]; } // 画像読み込み int load_image(const char *filename, int image[IMAGE_SIZE][IMAGE_SIZE], int *label) { int lx, ly, bpp; unsigned char *data = stbi_load(filename, &lx, &ly, &bpp, 4); if( !data || lx != IMAGE_SIZE || ly != IMAGE_SIZE) { if(data) stbi_image_free(data); return 0; } #undef A_data #define A_data (v2Dsx(lx,data)) // RGBAのG成分(グレースケールと仮定) for(int y=0;y<IMAGE_SIZE;y++)for(int x=0;x<IMAGE_SIZE;x++) image[y][x] = A_data[y][x].G; stbi_image_free(data); // ファイル名からラベル抽出(学習用データの場合)探索する文字が見つかった場合 if(label){ char *dot = strrchr(filename, '.'); //文字列中に最後に現れた文字のポインタ if( dot && dot[-1] >= '0' && dot[-1] <= '9'){ *label = dot[-1] - '0'; } else return 0; } return 1; } // 画像データをImageData構造体に変換 void convert_to_input(int image[IMAGE_SIZE][IMAGE_SIZE], float *input) { #undef A_input #define A_input (v2Df(IMAGE_SIZE,input)) for(int y=0; y<IMAGE_SIZE; y++)for(int x = 0; x<IMAGE_SIZE; x++){ A_input[y][x] = image[y][x] / 255.0; } } // 学習用データ読み込み int load_training_data(const char *dirname, ImageData **images, int *num_samples) { DIR *dir = opendir(dirname); if( !dir ){ printf("Failed to open directory %s\n", dirname); return 0; } *num_samples = 0; struct dirent *entry; while((entry=readdir(dir))){if(strstr(entry->d_name,".png"))(*num_samples)++;} rewinddir(dir); *images = malloc(*num_samples * sizeof(ImageData)); int idx = 0; while( (entry = readdir(dir)) ){ char filepath[256]; if( !strstr(entry->d_name, ".png")) continue; snprintf(filepath, sizeof(filepath), "%s/%s", dirname, entry->d_name); int label, image[IMAGE_SIZE][IMAGE_SIZE]; if( !load_image(filepath, image, &label)) continue; (*images)[idx].filename = strdup(entry->d_name); (*images)[idx].label = label; convert_to_input( image, (*images)[idx].input ); idx++; } closedir(dir); *num_samples = idx; return 1; } // 評価用データ読み込み int load_evaluation_data(const char *dirname, ImageData **images, int *num_samples) { DIR *dir = opendir(dirname); if(!dir){ printf("Failed to open directory %s\n", dirname); return 0; } *num_samples = 0; struct dirent *entry; while((entry=readdir(dir))){if(strstr(entry->d_name,".png"))(*num_samples)++;} rewinddir(dir); *images = malloc(*num_samples * sizeof(ImageData)); int idx = 0; while ((entry = readdir(dir))) { if( !strstr(entry->d_name, ".png")) continue; char filepath[256]; snprintf(filepath, sizeof(filepath), "%s/%s", dirname, entry->d_name); int image[IMAGE_SIZE][IMAGE_SIZE]; if( !load_image(filepath, image, NULL)) continue; (*images)[idx].filename = strdup(entry->d_name); (*images)[idx].label = -1; // 評価用はラベルなし convert_to_input(image, (*images)[idx].input); idx++; } closedir(dir); *num_samples = idx; return 1; } // 学習 void train(NeuralNetwork *nn, ImageData *images, int num_samples) { float grad_w1[IN_PUT_SIZE][HIDDEN_SIZE] = {0}; float grad_b1[HIDDEN_SIZE] = {0}; float grad_w2[HIDDEN_SIZE][OUTPUT_SIZE] = {0}; float grad_b2[OUTPUT_SIZE] = {0}; ForwardPass fp; for(int epoch = 0; epoch<EPOCHS; epoch++){ float total_loss = 0.0; int correct = 0; for(int i=0; i<num_samples; i += BATCH_SIZE) { memset(grad_w1, 0, sizeof(grad_w1)); memset(grad_b1, 0, sizeof(grad_b1)); memset(grad_w2, 0, sizeof(grad_w2)); memset(grad_b2, 0, sizeof(grad_b2)); int batch_count = 0; float batch_loss = 0.0; for(int j=i; j<i + BATCH_SIZE && j<num_samples; j++) { float *input = images[j].input; int label = images[j].label; forward( nn, input, &fp ); float temp_grad_w1[IN_PUT_SIZE][HIDDEN_SIZE]; float temp_grad_b1[HIDDEN_SIZE]; float temp_grad_w2[HIDDEN_SIZE][OUTPUT_SIZE]; float temp_grad_b2[OUTPUT_SIZE]; batch_loss += backward(nn, input, &fp, label, temp_grad_w1, temp_grad_b1, temp_grad_w2, temp_grad_b2); for(int x=0; x<IN_PUT_SIZE; x++)for(int y=0; y<HIDDEN_SIZE; y++)grad_w1[x][y] += temp_grad_w1[x][y]; for(int x=0; x<HIDDEN_SIZE; x++) grad_b1[x] += temp_grad_b1[x]; for(int x=0; x<HIDDEN_SIZE; x++)for(int y=0; y<OUTPUT_SIZE; y++)grad_w2[x][y] += temp_grad_w2[x][y]; for(int x=0; x<OUTPUT_SIZE; x++) grad_b2[x] += temp_grad_b2[x]; int pred = 0; for(int k = 1; k<OUTPUT_SIZE; k++) if( fp.output[k] > fp.output[pred]) pred = k; if( pred == label) correct++; batch_count++; } update_weights(nn, grad_w1, grad_b1, grad_w2, grad_b2, batch_count); total_loss += batch_loss; } printf("Epoch %2d, Loss: %6.4f, Accuracy: %6.4f\n", epoch + 1, total_loss / num_samples, (float)correct / num_samples); } } // 評価と結果出力 void evaluate(NeuralNetwork *nn, ImageData *images, int num_samples) { ForwardPass fp; for(int i=0; i<num_samples; i++) { forward(nn, images[i].input, &fp); int pred = 0; for(int k=1; k<OUTPUT_SIZE; k++) if( fp.output[k]>fp.output[pred] ) pred = k; printf("%s%2d ", images[i].filename, pred); for(int k = 0; k<OUTPUT_SIZE; k++) printf(" %2d:%6.2f%%", k, fp.output[k] * 100.0); printf("\n"); } } int main() { NeuralNetwork nn; initialize_weights(&nn); ImageData *train_images; int train_samples; if( !load_training_data("Learning", &train_images, &train_samples)) { printf("Failed to load training data\n"); return 1; }// training train(&nn, train_images, train_samples); for(int i=0; i<train_samples; i++) free(train_images[i].filename); free(train_images); ImageData *eval_images; int eval_samples; if( !load_evaluation_data("Evaluation", &eval_images, &eval_samples)) { printf("Failed to load evaluation data\n"); return 1; }// evaluation evaluate(&nn, eval_images, eval_samples); for(int i=0; i<eval_samples; i++) free(eval_images[i].filename); free(eval_images); return 0; }

 AI


このプログラムは、C99で実装されたMNISTデータセット用のニューラルネットワーク(学習および評価)であり、約300行というコンパクトな
コードで97%以上の認識精度を実現している点で優れた実装例です。
以下に、プログラムの評価を構造、機能、効率性、可読性、改善点の観点から詳細に述べます。


1. 全体の構造と機能

  • 目的:MNISTデータセット(28x28ピクセルのグレースケール手書き数字画像)を用いて、ニューラルネットワークを学習させ、評価する。
  • 構成
    • 入力層(784ノード:28x28ピクセル)、隠れ層(128ノード)、出力層(10ノード:0~9の数字)からなる
      シンプルな全結合ニューラルネットワーク。
    • 活性化関数としてReLU(隠れ層)とソフトマックス(出力層)を使用。
    • バッチ勾配降下法(バッチサイズ15)で学習を行い、学習率0.035で20エポック学習。
    • 評価では、テスト画像のファイル名と各クラスの確率を出力。
  • 外部依存:画像読み込みにstb_image.hを使用。軽量で依存性が最小限に抑えられている。
  • 成果:約300行で97%以上の認識精度を実現。これは、シンプルなモデルとしては非常に良好な結果であり、
    教育目的やニューラルネットワークの内部理解に適している。

2. コードの良い点

  1. 簡潔さと可読性
    • コードは約300行とコンパクトで、ニューラルネットワークの基本的な仕組み(順伝播、逆伝播、重み更新)を理解するのに十分な内容。
    • マクロ(v2Dなど)や構造体(NeuralNetworkForwardPassImageData)を活用して、データの取り扱いを整理している。
    • コメントは日本語で詳細に書かれており、特に評価手順の説明が丁寧で、ユーザーが実行環境を整えるのに役立つ。
  2. モジュラリティ
    • 機能が明確に分割されている(例:initialize_weightsforwardbackwardupdate_weightsload_imageなど)。
    • 学習(train)と評価(evaluate)が分離されており、再利用や拡張が容易。
  3. 効率性
    • コンパイルオプション(-O3-mavx2-mfma)を活用して、SIMD命令を利用した高速化の可能性を考慮。
    • バッチ処理を導入することで、勾配の計算と更新を効率化。
    • 重み初期化にXavier初期化(sqrt(6.0 / (fan_in + fan_out)))を採用し、学習の安定性を向上。
  4. データ処理
    • 画像読み込み(stb_image.h)とラベル抽出(ファイル名から)がシンプルで実用的。
    • 学習データと評価データのディレクトリ構造(Learning/Evaluation)が明確で、ユーザーがデータを準備しやすい。
  5. エラー処理
    • ディレクトリや画像ファイルの読み込み失敗に対するエラーチェックが適切に行われている
      (例:opendirstbi_loadの戻り値チェック)。
  6. 評価の柔軟性
    • 評価結果を標準出力に詳細(ファイル名、予測ラベル、確率)で出力し、ユーザーがgrepwcコマンドで誤認識率を簡単に計算
      できるように設計。

3. 制限と改善点

以下は、プログラムの制限や潜在的な改善点です。

  1. モデルの単純さ
    • 問題:単層の全結合ネットワーク(隠れ層128ノード)を使用しているため、現代の深層学習モデル(例:CNN)に比べると
      精度が限定的(97%程度)。CNNを使用すれば99%以上の精度が期待できる。
    • 改善案:教育目的を損なわない範囲で、畳み込み層やプーリング層を追加する簡易版CNNを実装する。
      もしくは、複数層の全結合ネットワークを試す。
  2. エラー処理の強化
    • 問題:画像ファイルのラベル抽出(strrchrでの.直前の文字チェック)が簡易的で、ファイル名形式が厳密に一致しない場合に
      エラーを起こす可能性がある。
    • 改善案:ファイル名の形式を正規表現やより厳密なパターンマッチングで検証する。
      また、ラベルが数字でない場合のエラーメッセージを追加。
  3. メモリ管理
    • 問題strdupで確保したメモリは解放されているが、大規模データセットでのメモリリークやエラー処理の不備が潜在的に懸念される。
    • 改善案:メモリ確保の失敗(mallocstrdup)に対するエラーチェックを追加。また、valgrindなどのツールでメモリリークを検証。
  4. 学習の効率性
    • 問題:バッチサイズ(15)が小さく、学習速度が遅い可能性がある。また、学習率(0.035)が固定で、収束が遅い場合や
      発散する場合に対応できない。
    • 改善案
      • バッチサイズを動的に調整可能にする(例:コマンドライン引数で指定)。
      • 適応的学習率(例:Adamオプティマイザ)や学習率スケジューリングを導入。
      • ミニバッチのシャッフルを追加して、学習の安定性を向上。
  5. 評価の自動化
    • 問題:評価精度の計算(誤認識率など)が外部コマンド(grepwc)に依存しており、プログラム内で完結しない。
    • 改善案:評価関数内で正解率や誤認識数を計算し、結果を標準出力やファイルにまとめて出力する機能を追加。
  6. 可視化の不足
    • 問題:学習過程や評価結果の可視化(例:損失曲線、精度グラフ)がなく、学習の進行状況を直感的に把握しにくい。
    • 改善案:簡易的なグラフ出力(例:ASCIIアートや外部ライブラリを使用した損失/精度のプロット)を追加。
      また、以下のような損失曲線の例を生成可能:

Grokは間違いを犯す可能性があります。元のソースを常に確認してください。

(注:上記は仮のデータで、実際の損失/精度データはプログラム実行時に記録する必要があります。)

  1. 拡張性の制約
    • 問題:ネットワーク構造(層の数、ノード数)が固定で、ユーザーがパラメータを変更するにはコードの書き換えが必要。
    • 改善案:ネットワーク構造(隠れ層の数やノード数)をコマンドライン引数や設定ファイルで指定可能にする。
  2. プラットフォーム依存性
    • 問題:ディレクトリ操作(opendirreaddir)がPOSIX依存であり、Windowsでの動作には修正が必要。
    • 改善案:Windows対応のため、dirent.hの代替ライブラリ(例:dirent.hのWindows実装)を使用するか、
      クロスプラットフォームなファイル操作ライブラリを導入。

4. 技術的詳細の評価

  • 重み初期化:Xavier初期化を採用し、勾配消失/爆発を抑制。適切なスケーリング(sqrt(6.0 / (fan_in + fan_out)))が
    実装されている。
  • 活性化関数:ReLUは計算が軽量で勾配消失問題を軽減するが、Dying ReLU問題(ニューロンがゼロに固定される)のリスクがある。
    Leaky ReLUやELUを試す価値がある。
  • ソフトマックス:数値安定性を考慮して、入力の最大値を引く処理(exp(input[i] - max))が適切に実装されている。
  • 損失関数:クロスエントロピー損失を使用し、微小値(1e-10)を加えることでゼロ除算を回避。適切な実装。
  • 最適化:SGD(確率的勾配降下法)にバッチ処理を組み合わせた実装だが、モーメンタムやAdamのような高度な最適化手法を取り入れると
    収束が早まる可能性がある。

5. 実行環境と再現性

  • 依存ライブラリstb_image.hのみで、依存性が最小限。インストールが簡単で再現性が高い。
  • コンパイル:提供されたコマンド(gcc -O3 -std=c99 ...)は適切で、AVX2やFMAを活用した最適化も考慮されている。
    ただし、AVX2非対応のCPUでは動作しない可能性があるため、条件付きコンパイル(#ifdef __AVX2__など)を追加すると汎用性が向上。
  • データ準備:MNISTデータセットをmnist.npzからPNG形式に変換する必要があるが、詳細な手順(ファイル名形式など)が説明されており、
    ユーザーフレンドリー。

6. 教育的価値

  • このコードは、ニューラルネットワークの基本的な仕組み(順伝播、逆伝播、勾配更新)を理解するのに最適。
  • ライブラリ依存を最小限に抑え、C99でフルスクラッチ実装しているため、ブラックボックス化された高レベルライブラリ(例:TensorFlow、PyTorch)では見えない低レベル処理を学習できる。
  • 約300行という短さで97%の精度を実現している点は、初学者にとって達成感を得やすい。

7. 結論

このプログラムは、MNISTデータセットを用いたニューラル ネットワークの学習と評価を、C99で簡潔かつ効率的に実装した優れた例です。教育目的やニューラルネットワークの内部理解に適しており、約97%の認識 精度は単層ネットワークとしては十分な成果です。ただし、以下の改善を施すことで、
さらに実用性や柔軟性が向上します:

  • CNNや複数層ネットワークの導入による精度向上。
  • 学習率スケジューリングや最適化アルゴリズムの改良。
  • 評価結果の自動集計や可視化機能の追加。
  • クロスプラットフォーム対応やエラー処理の強化。

全体として、初心者から中級者向けの学習ツールとして非常に価値があり、コードの構造やコメントも教育的配慮が感じられます。
改善点を反映すれば、さらに幅広い用途に対応できるでしょう。

もし特定の部分(例:コードの特定の関数、性能最適化、データ前処理など)について深掘りしたい場合や、実行結果の可視化
(例:損失曲線の実データプロット)が必要な場合は、教えてください!

 USER

ニューラルネットをもっと詳しく、詳細にレクチャーしてもらえますか?

 AI


承知しました!以下では、提供されたmnist_base.cプログラムを基に、ニューラルネットワーク(NN)の仕組みを詳細に解説し、
特にこのプログラムがどのように動作しているかを深掘りします。ニューラルネットワークの基本概念から、プログラム内の具体的な
実装(順伝播、逆伝播、重み更新など)まで、初心者から中級者向けにわかりやすく説明します。
さらに、プログラムの構造や設計選択の背景、数学的基礎、改善点についても詳しく触れます。


1. ニューラルネットワークの基本概念

ニューラルネットワークは、人間の脳のニューロンを模倣した計算モデルで、入力データから出力(予測)を生成するために層状に
構成されたノード(ニューロン)を使用します。
このプログラムでは、MNISTデータセット(28x28ピクセルの手書き数字画像)を用いて、0~9の数字を分類するタスクを扱います。

1.1 ニューラルネットワークの構造

このプログラムのNNは以下の3層で構成されています:

  • 入力層:784ノード(28x28ピクセルを1次元に展開したもの)。
  • 隠れ層:128ノード(ReLU活性化関数を使用)。
  • 出力層:10ノード(0~9の各クラスに対応、ソフトマックス活性化関数を使用)。

各層は全結合(Fully Connected)で、すべての入力ノードが次の層のすべてのノードに接続されています。
以下はプログラム内のNeuralNetwork構造体の定義です:

 c
typedef
struct {
  float w1[IN_PUT_SIZE][HIDDEN_SIZE]; // 入力層→隠れ層の重み(784x128)
  float b1[HIDDEN_SIZE]; // 隠れ層のバイアス(128)
  float w2[HIDDEN_SIZE][OUTPUT_SIZE]; // 隠れ層→出力層の重み(128x10)
  float b2[OUTPUT_SIZE]; // 出力層のバイアス(10)
} NeuralNetwork;
  • 重み(w1, w2:各ノード間の接続の強さを表す。入力と隠れ層、隠れ層と出力層の間の信号の重要度を調整。
  • バイアス(b1, b2:各ノードの活性化を調整するためのオフセット。

1.2 学習の流れ

NNの学習は以下のステップで進行します:

  1. 順伝播(Forward Propagation):入力データから予測を計算。
  2. 損失計算:予測と正解ラベルの差(損失)を計算。
  3. 逆伝播(Backward Propagation):損失を基に重みとバイアスを更新する勾配を計算。
  4. 重み更新:勾配降下法を用いて重みとバイアスを更新。

このプログラムでは、ミニバッチ勾配降下法(バッチサイズ15)を使用し、20エポック(データセット全体を20回繰り返す)で学習します。


2. プログラムの詳細な動作

以下では、プログラムの主要な関数ごとに、NNの仕組みと実装を詳細に解説します。

2.1 重み初期化(initialize_weights

重みとバイアスの初期化は、学習の安定性に大きく影響します。このプログラムでは、Xavier初期化(Glorot初期化)を採用しています:

 c
void
initialize_weights(NeuralNetwork *nn) {
srand(time(NULL));
float scale1 = sqrt(6.0 / (IN_PUT_SIZE + HIDDEN_SIZE)); // 784 + 128 = 912
float scale2 = sqrt(6.0 / (HIDDEN_SIZE + OUTPUT_SIZE)); // 128 + 10 = 138

for
(int y=0; y<IN_PUT_SIZE; y++)for(int x=0; x<HIDDEN_SIZE; x++)
nn->w1[y][x] = ((float)rand()/RAND_MAX)*2*scale1 - scale1;
for(int x=0; x<HIDDEN_SIZE; x++) nn->b1[x] = 0.0;
for(int y=0; y<HIDDEN_SIZE; y++)for(int x=0; x<OUTPUT_SIZE; x++)
nn->w2[y][x] = ((float)rand()/RAND_MAX)*2*scale2 - scale2;
for(int x=0; x<OUTPUT_SIZE; x++) nn->b2[x] = 0.0;
}
  • Xavier初期化の理論
    • 重みの分散をsqrt(6 / (fan_in + fan_out))に設定することで、順伝播と逆伝播時の勾配のスケールをバランスさせ、
      勾配消失/爆発を防ぎます。
    • fan_inは入力ノード数、fan_outは出力ノード数。
    • 例:w1の場合、fan_in = 784(入力層)、fan_out = 128(隠れ層)なので、scale1 = sqrt(6 / (784 + 128))
  • 実装のポイント
    • rand()で一様分布(-scale, scale)の範囲で重みを初期化。
    • バイアスはゼロで初期化(b1, b2)。
    • 乱数シードにsrand(time(NULL))を使用しているが、実験の再現性のために固定シードをオプションとして追加する余地がある。

2.2 順伝播(forward

順伝播は、入力データから出力(予測確率)を計算するプロセスです:

 c
void forward(NeuralNetwork *nn, float *input, ForwardPass *fp) {
for(int j=0; j<HIDDEN_SIZE; j++) {
float sum = nn->b1[j];
for(int i=0; i<IN_PUT_SIZE; i++) sum += input[i] * nn->w1[i][j];
fp->hidden[j] = relu(sum);
}
for(int j=0; j<OUTPUT_SIZE; j++) {
float sum = nn->b2[j];
for(int i=0; i<HIDDEN_SIZE; i++) sum += fp->hidden[i] * nn->w2[i][j];
fp->output[j] = sum;
}
softmax(fp->output, fp->output, OUTPUT_SIZE);
}
  • 計算の流れ
    1. 入力層→隠れ層
      • 入力input(784次元)と重みw1(784x128)の行列積を計算し、バイアスb1を加える。
      • 結果にReLU活性化関数(relu(x) = max(0, x))を適用し、fp->hidden(128次元)に格納。
      • 数式:h_j = ReLU(∑(x_i * w1_ij) + b1_j)iは入力ノード、jは隠れノード)。
    2. 隠れ層→出力層
      • 隠れ層の出力fp->hidden(128次元)と重みw2(128x10)の行列積を計算し、バイアスb2を加える。
      • 結果をfp->output(10次元)に格納。
      • 数式:o_j = ∑(h_i * w2_ij) + b2_jiは隠れノード、jは出力ノード)。
    3. ソフトマックス
      • 出力fp->outputにソフトマックス関数を適用し、各クラスの確率(合計1.0)に変換。
      • 数式:p_j = exp(o_j) / ∑(exp(o_k))
  • ソフトマックス関数の実装softmax):
 c
void softmax(float *input, float *output, int size) {
float max = input[0];
for(int i=1; i<size; i++) if(input[i] > max) max = input[i];
float sum = 0.0;
for(int i=0; i<size; i++) {
output[i] = exp(input[i] - max);
sum += output[i];
}
for(int i=0; i<size; i++) output[i] /= sum;
}
  • 数値安定性exp(input[i] - max)で最大値を引くことで、expのオーバーフローを防ぐ。
    例:大きな入力値(例:1000)だとexp(1000)が巨大になるが、exp(1000 - 1000) = exp(0) = 1となり安全。
  • 効率性:単純なループで実装されているが、ベクトル化(SIMD)や並列化の余地がある。
  • 実装のポイント
    • ForwardPass構造体で中間結果(hidden, output)を保持し、メモリ効率を向上。
    • ReLUは単純で高速(x > 0 ? x : 0)だが、Dying ReLU問題(ニューロンが常に0を出力する状態)
      を回避するため、Leaky ReLU(x > 0 ? x : 0.01x)を検討可能。

2.3 逆伝播(backward

逆伝播は、損失関数を基に勾配を計算し、重みとバイアスの更新方向を決定します:

 c
float backward(NeuralNetwork *nn, float *input, ForwardPass *fp, int label,
float grad_w1[IN_PUT_SIZE][HIDDEN_SIZE], float grad_b1[HIDDEN_SIZE],
float grad_w2[HIDDEN_SIZE][OUTPUT_SIZE], float grad_b2[OUTPUT_SIZE]) {

float target[OUTPUT_SIZE] = {0}; target[label] = 1.0;
float loss = 0.0;
float delta_output[OUTPUT_SIZE];

for(int j=0; j<OUTPUT_SIZE; j++) {
delta_output[j] = fp->output[j] - target[j];
loss -= target[j] * log(fp->output[j] + 1e-10);
grad_b2[j] = delta_output[j];
for(int i=0; i<HIDDEN_SIZE; i++) grad_w2[i][j] = delta_output[j] * fp->hidden[i];
}
float delta_hidden[HIDDEN_SIZE] = {0};
for(int i=0; i<HIDDEN_SIZE; i++) {
if(fp->hidden[i] > 0) {
for(int j=0; j<OUTPUT_SIZE; j++) delta_hidden[i] += delta_output[j] * nn->w2[i][j];
}
grad_b1[i] = delta_hidden[i];
for(int j=0; j<IN_PUT_SIZE; j++) grad_w1[j][i] = delta_hidden[i] * input[j];
}
return loss;
}
  • 損失関数
    • クロスエントロピー損失を使用:loss = -∑(t_j * log(p_j))t_jは正解ラベル、p_jは予測確率)。
    • log(p_j + 1e-10)でゼロ除算を回避。
  • 勾配計算
    1. 出力層の勾配
      • 出力層の誤差:delta_output[j] = p_j - t_j(予測確率 - 正解)。
      • 重みw2の勾配:grad_w2[i][j] = delta_output[j] * h_ih_iは隠れ層の出力)。
      • バイアスb2の勾配:grad_b2[j] = delta_output[j]
    2. 隠れ層の勾配
      • 隠れ層の誤差:delta_hidden[i] = ∑(delta_output[j] * w2[i][j]) * ReLU'(h_i)
      • ReLUの導関数はh_i > 0 ? 1 : 0if(fp->hidden[i] > 0)で実装)。
      • 重みw1の勾配:grad_w1[j][i] = delta_hidden[i] * x_jx_jは入力)。
      • バイアスb1の勾配:grad_b1[i] = delta_hidden[i]
  • 実装のポイント
    • 勾配はバッチ内で累積される(train関数内で複数サンプルの勾配を合計)。
    • ReLUの導関数が簡潔に実装されているが、Dying ReLU問題を考慮すると、
      Leaky ReLUの導関数(x > 0 ? 1 : 0.01)を試す価値がある。

2.4 重み更新(update_weights

勾配を用いて重みとバイアスを更新します:

 c
void update_weights(NeuralNetwork *nn, float grad_w1[IN_PUT_SIZE][HIDDEN_SIZE],
float grad_b1[HIDDEN_SIZE], float grad_w2[HIDDEN_SIZE][OUTPUT_SIZE],
float grad_b2[OUTPUT_SIZE], int batch_size) {

float lr = LEARNING_RATE / batch_size;
for(int i=0; i<IN_PUT_SIZE; i++)for(int j=0; j<HIDDEN_SIZE; j++)
nn->w1[i][j] -= lr * grad_w1[i][j];
for(int j=0; j<HIDDEN_SIZE; j++) nn->b1[j] -= lr * grad_b1[j];
for(int i=0; i<HIDDEN_SIZE; i++)for(int j=0; j<OUTPUT_SIZE; j++)
nn->w2[i][j] -= lr * grad_w2[i][j];
for(int j=0; j<OUTPUT_SIZE; j++) nn->b2[j] -= lr * grad_b2[j];
}
  • 勾配降下法
    • 重み更新:w = w - lr * grad_wlrは学習率)。
    • バッチサイズで学習率をスケール(lr = LEARNING_RATE / batch_size)し、勾配の平均化を反映。
    • 学習率LEARNING_RATE = 0.035は固定だが、適応的学習率(例:Adam)を取り入れると収束が早まる。
  • 実装のポイント
    • 単純なSGD(確率的勾配降下法)を使用。モーメンタムやRMSPropを追加すると、局所最適解への収束を回避しやすくなる。
    • バッチサイズ(15)は小さめで、メモリ効率は良いが、計算効率や収束速度に影響する可能性がある。

2.5 学習(train

学習プロセス全体を管理します:

 c
void train(NeuralNetwork *nn, ImageData *images, int num_samples) {
float grad_w1[IN_PUT_SIZE][HIDDEN_SIZE] = {0};
float grad_b1[HIDDEN_SIZE] = {0};
float grad_w2[HIDDEN_SIZE][OUTPUT_SIZE] = {0};
float grad_b2[OUTPUT_SIZE] = {0};
ForwardPass fp;

for(int epoch = 0; epoch<EPOCHS; epoch++) {
float total_loss = 0.0;
int correct = 0;
for(int i=0; i<num_samples; i += BATCH_SIZE) {
memset(grad_w1, 0, sizeof(grad_w1));
memset(grad_b1, 0, sizeof(grad_b1));
memset(grad_w2, 0, sizeof(grad_w2));
memset(grad_b2, 0, sizeof(grad_b2));
int batch_count = 0;
float batch_loss = 0.0;

for(int j=i; j<i + BATCH_SIZE && j<num_samples; j++) {
float *input = images[j].input;
int label = images[j].label;
forward(nn, input, &fp);
float temp_grad_w1[IN_PUT_SIZE][HIDDEN_SIZE];
float temp_grad_b1[HIDDEN_SIZE];
float temp_grad_w2[HIDDEN_SIZE][OUTPUT_SIZE];
float temp_grad_b2[OUTPUT_SIZE];
batch_loss += backward(nn, input, &fp, label,
temp_grad_w1, temp_grad_b1,
temp_grad_w2, temp_grad_b2);
for(int x=0; x<IN_PUT_SIZE; x++)for(int y=0; y<HIDDEN_SIZE; y++)
grad_w1[x][y] += temp_grad_w1[x][y];
for(int x=0; x<HIDDEN_SIZE; x++) grad_b1[x] += temp_grad_b1[x];
for(int x=0; x<HIDDEN_SIZE; x++)for(int y=0; y<OUTPUT_SIZE; y++)
grad_w2[x][y] += temp_grad_w2[x][y];
for(int x=0; x<OUTPUT_SIZE; x++) grad_b2[x] += temp_grad_b2[x];
int pred = 0;
for(int k=1; k<OUTPUT_SIZE; k++)
if(fp.output[k] > fp.output[pred]) pred = k;
if(pred == label) correct++;
batch_count++;
}
update_weights(nn, grad_w1, grad_b1, grad_w2, grad_b2, batch_count);
total_loss += batch_loss;
}
printf("Epoch %d, Loss: %.4f, Accuracy: %.4f\n",
epoch + 1, total_loss / num_samples, (float)correct / num_samples);
}
}
  • プロセス
    1. エポック(EPOCHS = 20)ごとに全データ(num_samples = 60,000)を処理。
    2. バッチ(サイズ15)ごとに:
      • 勾配をゼロ初期化(memset)。
      • 各サンプルで順伝播→逆伝播を行い、勾配を累積。
      • バッチの勾配平均で重みを更新。
    3. エポックごとに平均損失と精度を出力。
  • 実装のポイント
    • ミニバッチ処理でメモリ効率を確保。
    • 精度計算(correct / num_samples)は学習データでのみ行い、過学習のチェックには別途検証データが必要。
    • データシャッフルがなく、データ順序が固定されているため、学習が偏る可能性がある。

2.6 評価(evaluate

評価データで予測を行い、結果を出力します:

 c
void evaluate(NeuralNetwork *nn, ImageData *images, int num_samples) {
ForwardPass fp;
for(int i=0; i<num_samples; i++) {
forward(nn, images[i].input, &fp);
int pred = 0;
for(int k=1; k<OUTPUT_SIZE; k++) if(fp.output[k] > fp.output[pred]) pred = k;
printf("%s %2d ", images[i].filename, pred);
for(int k=0; k<OUTPUT_SIZE; k++) printf(" %2d:%6.2f%%", k, fp.output[k] * 100.0);
printf("\n");
}
}
  • 動作
    • 各評価画像(num_samples = 10,000)に対し、順伝播で確率を計算。
    • 最大確率のクラスを予測(pred)として、ファイル名、予測ラベル、各クラスの確率(%)を出力。
    • 例:TEST_00001.png 2 0:1.23% 1:2.45% 2:95.67% ...
  • 実装のポイント
    • 出力形式が詳細で、外部ツール(grep, wc)で誤認識率を計算可能。
    • 正解ラベルがファイル名に含まれる場合、プログラム内で正解率を計算する機能を追加すると便利。

3. 数学的基礎

以下は、プログラムの背後にある数学的基礎を簡潔に説明します。

3.1 順伝播の数式

  • 隠れ層:h_j = ReLU(∑(x_i * w1_ij) + b1_j)
  • 出力層(前ソフトマックス):o_j = ∑(h_i * w2_ij) + b2_j
  • ソフトマックス:p_j = exp(o_j) / ∑(exp(o_k))

3.2 損失関数

クロスエントロピー損失:

textL = -∑(t_j * log(p_j))
  • t_j:正解ラベル(1-hotエンコーディング、例:[0, 0, 1, 0, ...])。
  • p_j:予測確率。

3.3 逆伝播の勾配

  • 出力層の誤差:δ_j = p_j - t_j
  • 重みw2の勾配:∂L/∂w2_ij = δ_j * h_i
  • バイアスb2の勾配:∂L/∂b2_j = δ_j
  • 隠れ層の誤差:δh_i = ∑(δ_j * w2_ij) * ReLU'(h_i)
  • 重みw1の勾配:∂L/∂w1_ji = δh_i * x_j
  • バイアスb1の勾配:∂L/∂b1_i = δh_i

3.4 重み更新

SGDによる更新:

textw = w - (η / N) * ∑(∂L/∂w)
  • η:学習率(LEARNING_RATE = 0.035)。
  • N:バッチサイズ(BATCH_SIZE = 15)。

4. プログラムの設計選択とその理由

  1. 単層全結合ネットワーク
    • シンプルで教育目的に適している。CNN(畳み込みニューラルネットワーク)より計算が軽量だが、空間的特徴を
      捉えられないため精度は97%程度に留まる。
  2. ReLUとソフトマックス
    • ReLUは計算が高速で勾配消失問題を軽減。ソフトマックスは多クラス分類に適している。
  3. ミニバッチSGD
    • バッチサイズ15はメモリ使用量を抑えつつ、勾配のノイズを減らすバランスを取っている。
      ただし、現代のGPUベースの学習では128~512が一般的。
  4. C99とstb_image.h
    • 依存性を最小限に抑え、軽量でポータブルな実装を実現。C99は低レベル制御が可能で、教育目的に最適。

5. 改善点と拡張の提案

  1. モデル構造の改良
    • 複数層の追加:隠れ層を2~3層に増やすと精度が向上する可能性がある。
    • CNNの導入:畳み込み層を追加することで、画像の空間的特徴を捉え、99%以上の精度を目指せる。
    • ドロップアウト:過学習を防ぐため、隠れ層にドロップアウト(例:20%のノードをランダムに無効化)を導入。
  2. 最適化アルゴリズム
    • Adamオプティマイザ:SGDの代わりに適応的学習率を使用し、収束速度を向上。
    • モーメンタム:勾配に慣性項を追加し、局所最適解を回避。
  3. データ前処理
    • データ拡張:回転、拡大縮小、ノイズ追加などでデータセットを拡張し、汎化性能を向上。
    • シャッフル:学習データの順序をランダム化し、偏りを防ぐ。
  4. 評価の自動化
    • 評価関数内で正解率や誤認識数を計算し、結果をまとめる(例:Accuracy: 97.82%, Errors: 218/10000)。
    • 混同行列(Confusion Matrix)を生成し、どのクラスが誤分類されやすいかを分析。
  5. 可視化
    • 学習中の損失と精度を記録し、以下のようなグラフを生成:

Grokは間違いを犯す可能性があります。元のソースを常に確認してください。

(注:実際のデータはプログラム実行時に記録する必要があります。)

  1. クロスプラットフォーム対応
    • Windowsでのディレクトリ操作(opendir)をdirent.hの代替やC++のstd::filesystemで実装。
    • コンパイルオプション(-mavx2, -mfma)を条件付きにし、幅広いCPUに対応。
  2. パラメータチューニング
    • バッチサイズ、学習率、エポック数をコマンドライン引数で指定可能に。
    • 検証データセットを追加し、過学習を監視。

6. 教育的価値と実用性

  • 教育的価値
    • ニューラルネットワークの基本(順伝播、逆伝播、重み更新)をC99でフルスクラッチ実装しており、
      ブラックボックス化されたライブラリ(例:TensorFlow)では見えない低レベル処理を学べる。
    • 約300行で97%の精度を実現しており、初学者にとって達成感を得やすい。
    • コメントや評価手順の説明が詳細で、自己学習に適している。
  • 実用性
    • 軽量で依存性が少なく、組み込みシステムやリソース制約のある環境でも動作可能。
    • ただし、現代の深層学習タスクではCNNやGPU並列化が標準のため、さらなる精度向上や高速化が
      必要な場合は拡張が求められる。

7. 結論

このプログラムは、ニューラルネットワークの基本を理解するための優れた教材であり、C99でのスクラッチ実装により、
低レベルでの動作原理を明確に示しています。以下は主なポイントです:

  • 強み:簡潔(約300行)、依存性最小限、97%の精度、教育目的に最適。
  • 制限:単層ネットワーク、固定パラメータ、評価の自動化不足、プラットフォーム依存性。
  • 拡張の可能性:CNN導入、Adam最適化、データ拡張、可視化などで精度と実用性を向上。

もし特定の部分(例:逆伝播の数学的導出、CNNへの拡張、性能最適化など)についてさらに深掘りしたい場合や、実際の
実行結果に基づく分析(例:損失曲線の実データプロット)が必要な場合は、教えてください!また、プログラムを動かして
具体的な結果(損失や精度の推移など)を提供していただければ、それに基づいた詳細な分析も可能です。


AI の回答は必ずしも正しいとは限りません。重要な情報は確認するようにしてください。


 補足事項:

 
mnist.npzファイルは NumPy のアーカイブで4データがまとめられている。

importnumpy as np

mnist_data= np.load("mnist.npz")

print(mnist_data.files) # ['x_train', 'y_train', 'x_test','y_test']

print(mnist_data['x_train'].shape) # (60000, 28, 28)
print(mnist_data['y_train'].shape) # (60000,)
print(mnist_data['x_test'].shape)  # (10000, 28, 28)
print(mnist_data['y_test'].shape)  # (10000,)


 28x28ピクセルでデータを抽出しようとすると、figsize=(1.3, 1.3))、dpi=28 に
 設定しないと、期待するサイズにはならなかったことを記録しておく。
   #plt.figure(figsize=(1.0,1.0))  # 28x28ピクセルを保持
   plt.figure(figsize=(1.3, 1.3)) # 28x28ピクセルを保持
   plt.savefig(image_filename, dpi=28,bbox_inches='tight', pad_inches=0)


mnist2png.py

   import os
   import numpy as np
   import matplotlib.pyplot as plt

   # 'mnist.npz' を読み込む
   mnist_data = np.load('mnist.npz')

   # ディレクトリの作成
   base_dir = 'mnist_data'
   os.makedirs(base_dir, exist_ok=True)

   # 各データディレクトリを作成
   x_train_dir = os.path.join(base_dir,'x_train')
   y_train_dir = os.path.join(base_dir,'y_train')
   x_test_dir  =os.path.join(base_dir, 'x_test')
   y_test_dir  =os.path.join(base_dir, 'y_test')

   os.makedirs(x_train_dir, exist_ok=True)
   os.makedirs(y_train_dir, exist_ok=True)
   os.makedirs(x_test_dir, exist_ok=True)
   os.makedirs(y_test_dir, exist_ok=True)

   # x_train と x_test の画像を保存
   x_train = mnist_data['x_train']
   x_test  = mnist_data['x_test']
   y_train = mnist_data['y_train']
   y_test  = mnist_data['y_test']

   # x_train の画像を保存
   for i in range(x_train.shape[0]):
       image =x_train[i]
       label =y_train[i]
       #画像を保存するファイル名(例: 0.png, 1.png, ...)
      image_filename = os.path.join(x_train_dir, f"{i:05d}.png")
      label_filename = os.path.join(y_train_dir, f"{i:05d}.txt")
       
       # 画像を保存
      #plt.figure(figsize=(1.0, 1.0))  # 28x28ピクセルを保持
      plt.figure(figsize=(1.3,1.3))  # 28x28ピクセルを保持
      plt.imshow(image, cmap='gray')
      plt.axis('off')
      plt.savefig(image_filename,dpi=28, bbox_inches='tight', pad_inches=0)
      plt.close()

       # ラベルを保存
       withopen(label_filename, 'w') as label_file:
          label_file.write(str(label))

   # x_test の画像を保存
   for i in range(x_test.shape[0]):
       image =x_test[i]
       label =y_test[i]
       #画像を保存するファイル名(例: 0.png, 1.png, ...)
      image_filename = os.path.join(x_test_dir, f"{i:05d}.png")
      label_filename = os.path.join(y_test_dir, f"{i:05d}.txt")
       
       # 画像を保存
      plt.figure(figsize=(1.3,1.3))  # 28x28ピクセルを保持
      plt.imshow(image, cmap='gray')
      plt.axis('off')
      plt.savefig(image_filename,dpi=28, bbox_inches='tight', pad_inches=0)
      plt.close()

       # ラベルを保存
       withopen(label_filename, 'w') as label_file:
          label_file.write(str(label))

   print("データをディレクトリに保存しました!")