ize:medium;!DOCTYPE html>
このプログラムの MNIST のような手書き数字画像分類に対する正解率(accuracy) を予測すると、
おおよそ 85〜92%の範囲 になる可能性があります。
ただし、使用する画像データセットの内容や量に大きく依存します。以下にその予測の論拠を説明します。
この推定は、小規模な1層のMLP(多層パーセプトロン)でのMNIST分類タスクにおける実績から得られています。
入力層:784(28×28ピクセル画像)
隠れ層:128ユニット + ReLU活性化
出力層:10ユニット(ソフトマックス)で分類
▶ この構成は典型的なシンプルなMLPです。
▶ 学習用画像がMNIST相当なら、この構成で 88〜92%は比較的容易に到達可能です(文献・実測から確認済み)。
この系列で確認できた、最優良値(更に改善、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%以上の認識精度を実現している点で優れた実装例です。
以下に、プログラムの評価を構造、機能、効率性、可読性、改善点の観点から詳細に述べます。
以下は、プログラムの制限や潜在的な改善点です。
(注:上記は仮のデータで、実際の損失/精度データはプログラム実行時に記録する必要があります。)
このプログラムは、MNISTデータセットを用いたニューラル
ネットワークの学習と評価を、C99で簡潔かつ効率的に実装した優れた例です。教育目的やニューラルネットワークの内部理解に適しており、約97%の認識
精度は単層ネットワークとしては十分な成果です。ただし、以下の改善を施すことで、
さらに実用性や柔軟性が向上します:
全体として、初心者から中級者向けの学習ツールとして非常に価値があり、コードの構造やコメントも教育的配慮が感じられます。
改善点を反映すれば、さらに幅広い用途に対応できるでしょう。
もし特定の部分(例:コードの特定の関数、性能最適化、データ前処理など)について深掘りしたい場合や、実行結果の可視化
(例:損失曲線の実データプロット)が必要な場合は、教えてください!
USER
ニューラルネットをもっと詳しく、詳細にレクチャーしてもらえますか?
AI
承知しました!以下では、提供されたmnist_base.cプログラムを基に、ニューラルネットワーク(NN)の仕組みを詳細に解説し、
特にこのプログラムがどのように動作しているかを深掘りします。ニューラルネットワークの基本概念から、プログラム内の具体的な
実装(順伝播、逆伝播、重み更新など)まで、初心者から中級者向けにわかりやすく説明します。
さらに、プログラムの構造や設計選択の背景、数学的基礎、改善点についても詳しく触れます。
ニューラルネットワークは、人間の脳のニューロンを模倣した計算モデルで、入力データから出力(予測)を生成するために層状に
構成されたノード(ニューロン)を使用します。
このプログラムでは、MNISTデータセット(28x28ピクセルの手書き数字画像)を用いて、0~9の数字を分類するタスクを扱います。
このプログラムのNNは以下の3層で構成されています:
各層は全結合(Fully Connected)で、すべての入力ノードが次の層のすべてのノードに接続されています。
以下はプログラム内のNeuralNetwork構造体の定義です:
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;
NNの学習は以下のステップで進行します:
このプログラムでは、ミニバッチ勾配降下法(バッチサイズ15)を使用し、20エポック(データセット全体を20回繰り返す)で学習します。
以下では、プログラムの主要な関数ごとに、NNの仕組みと実装を詳細に解説します。
重みとバイアスの初期化は、学習の安定性に大きく影響します。このプログラムでは、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;
}
順伝播は、入力データから出力(予測確率)を計算するプロセスです:
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);
}
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;
}
逆伝播は、損失関数を基に勾配を計算し、重みとバイアスの更新方向を決定します:
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;
}
勾配を用いて重みとバイアスを更新します:
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];
}
学習プロセス全体を管理します:
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);
}
}
評価データで予測を行い、結果を出力します:
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");
}
}
以下は、プログラムの背後にある数学的基礎を簡潔に説明します。
クロスエントロピー損失:
textL = -∑(t_j * log(p_j))
SGDによる更新:
textw = w - (η / N) * ∑(∂L/∂w)
(注:実際のデータはプログラム実行時に記録する必要があります。)
このプログラムは、ニューラルネットワークの基本を理解するための優れた教材であり、C99でのスクラッチ実装により、
低レベルでの動作原理を明確に示しています。以下は主なポイントです:
もし特定の部分(例:逆伝播の数学的導出、CNNへの拡張、性能最適化など)についてさらに深掘りしたい場合や、実際の
実行結果に基づく分析(例:損失曲線の実データプロット)が必要な場合は、教えてください!また、プログラムを動かして
具体的な結果(損失や精度の推移など)を提供していただければ、それに基づいた詳細な分析も可能です。