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