※当初予定していたサブタイトル「オンライン学習とミニバッチ学習」から変更しました。
前回まで
前回までに順伝播→逆伝播の流れを完成させました。前回は各データを前から順番に1つずつみていきましたが,実際にはこのような操作を複数回繰り返します。
確率的勾配降下法(SGD: Stochastic Gradient Descent)
第3回で一瞬登場した確率的勾配降下法は,データを無作為に選んで勾配を計算する方法です。前回は訓練データの1番目から順に読み込んで勾配を更新していました。このデータ全体をそれぞれ1回ずつ見る単位をエポック(epoch)と言います。前回はデータをそれぞれ1回ずつしか見ていないため1エポック学習したことになります。
さて,確率的勾配降下法ではデータを無作為に選んで学習をおこないます。1〜10番目までの10個の訓練データがある場合, 3, 5, 9, 2, 1, 10, 2, 4, 7, 5というように選ばれるかもしれません。訓練データの個数に等しい10個のデータを選んだにも関わらず,2番目と5番目の訓練データは2度も選ばれ,1と6は1度も選ばれませんでした。このように「データを一通り見る」という言葉は,必ずしもそれぞれのデータを全て1回ずつ見ているとは限りません。無作為に10個選んで学習すれば,それは1エポック分学習したことになります。
次の関数は,データをランダムに1つ選び 順伝播→逆伝播→重みの更新 をおこなう train_per_data
関数です。自分で定義した METRICS
型の値を返します。 METRICS
型の変数は,損失や精度を保持します(このコードではデータ1つに対して精度を計算するため,精度は必ず $0$ か $1$ になってしまう)。
METRICS train_per_data(MODEL_PARAMETER model_parameter, float learning_rate, DATA *train_data, int N_train) {
...
id = rand() % N_train + 1;
x = train_data[id].x;
t[train_data[id].t] = 1.0f;
forward(model_parameter);
metrics.E_total += model_parameter.loss(y, t, model_parameter.K, 0, NULL);
backward(model_parameter);
update_parameters(model_parameter, learning_rate);
...
return metrics;
}
typedef struct {
float E_total;
float E_average;
int true_count;
float accuracy;
} METRICS;
この train_per_data
関数を訓練データ分回し,さらにこの単位を10エポック回します。また,各エポックで重みの更新が終わった後に,検証データの損失と精度を計算してみましょう。 validate_per_batch
関数は,検証データ全体の損失と精度を計算する関数です(batchは「束」の意)。
int main(int argc, char **argv) {
...
int EPOCH = 10;
...
for(epoch = 1; epoch <= EPOCH; epoch++) {
printf("Epoch: %d / %d\n", epoch, EPOCH);
float E_total = 0.0f;
int true_count = 0;
/* ========== online training ========== */
for(n = 1; n <= N_train; n++) {
metrics = train_per_data(model_parameter, LEARNING_RATE, train_data, N_train);
E_total += metrics.E_total;
true_count += metrics.true_count;
}
printf("\tloss: %f, accuracy: %f", E_total / (float) N_train, true_count / (float) N_train);
metrics = validate_per_batch(model_parameter, validation_data, N_validation);
printf("\tvalidation loss: %f, validation accuracy: %f\n", metrics.E_total / (float) N_validation, metrics.true_count / (float) N_validation);
}
...
}
10エポック回した後,テストデータの損失と精度を test_per_batch
関数で計算します。
int main(int argc, char **argv) {
...
metrics = test_per_batch(model_parameter, test_data, N_test);
printf("Test\n");
printf("\taccuracy: %f\n", metrics.true_count / (float) N_test);
return 0;
}
実行結果は次のようになりました。
$ gcc main_online_training.c
$ ./a.out ../data/train_data.txt ../data/validation_data.txt ../data/test_data.txt
Epoch: 1 / 10
loss: 0.218468, accuracy: 0.929800 validation loss: 0.022734, validation accuracy: 0.990000
Epoch: 2 / 10
loss: 0.010028, accuracy: 1.000000 validation loss: 0.008422, validation accuracy: 1.000000
Epoch: 3 / 10
loss: 0.004400, accuracy: 1.000000 validation loss: 0.009911, validation accuracy: 0.990000
Epoch: 4 / 10
loss: 0.003137, accuracy: 0.999800 validation loss: 0.005245, validation accuracy: 1.000000
Epoch: 5 / 10
loss: 0.002299, accuracy: 0.999900 validation loss: 0.008034, validation accuracy: 1.000000
Epoch: 6 / 10
loss: 0.001913, accuracy: 1.000000 validation loss: 0.010766, validation accuracy: 0.990000
Epoch: 7 / 10
loss: 0.001511, accuracy: 1.000000 validation loss: 0.006474, validation accuracy: 1.000000
Epoch: 8 / 10
loss: 0.001390, accuracy: 1.000000 validation loss: 0.006924, validation accuracy: 1.000000
Epoch: 9 / 10
loss: 0.001580, accuracy: 0.999700 validation loss: 0.025030, validation accuracy: 0.990000
Epoch: 10 / 10
loss: 0.001376, accuracy: 1.000000 validation loss: 0.011759, validation accuracy: 0.990000
Test
accuracy: 1.000000
オンライン学習
訓練データ1個単位で重みの更新をおこなう方法をオンライン学習といいます(これに対して,訓練データ複数単位で重みの更新を行うミニバッチ学習もあります)。確率的勾配降下法と同義のように扱っている記事が多くありますが,確率的勾配降下法は「データをランダムに選んでいる」という方向性から学習方法をみているものであり,別のものだと感じます(私見)1。
重みの初期値
勾配降下法では, $w_{ij}^{(l)}$ を勾配を利用して更新していました。しかし,更新される前も何らかの値を持っているわけですが,この初期値はどうすればよいのでしょうか。
重みの初期化のよくない例
例えば,全ての重みの初期値を $0$ としてみます。
void setup_parameters(MODEL_PARAMETER model_parameter) {
...
for(l = 0; l <= L; l++) {
int d_max = D[l];
int d_next_layer = D[l + 1];
for(i = 0; i <= d_max; i++) {
for(j = 0; j <= d_next_layer; j++) {
w[l][i][j] = 0.0f;
}
}
}
...
}
この状態で,オンライン学習をおこなってみます。
$ gcc main_online_training.c
$ ./a.out ../data/train_data.txt ../data/validation_data.txt ../data/test_data.txt
Epoch: 1 / 10
loss: 1.098632, accuracy: 0.327000 validation loss: 1.098612, validation accuracy: 0.400000
Epoch: 2 / 10
loss: 1.098632, accuracy: 0.335800 validation loss: 1.098612, validation accuracy: 0.400000
Epoch: 3 / 10
loss: 1.098632, accuracy: 0.342500 validation loss: 1.098612, validation accuracy: 0.400000
Epoch: 4 / 10
loss: 1.098632, accuracy: 0.334900 validation loss: 1.098612, validation accuracy: 0.400000
...
Epoch: 10 / 10
loss: 1.098632, accuracy: 0.335600 validation loss: 1.098612, validation accuracy: 0.400000
Test
accuracy: 0.330000
全く学習できず,テストデータの精度も0.33となってしまいました。3クラスの分類のため,これでは全て1と判断しているのと同じ精度になってしまっています。
実際,推測したクラスを出力させると,テストデータに対してどのデータも $1$ と予想していました。
重みの初期値を全て同じ値にしてしまうと,各層で同じように値が更新されてしまいます。つまりどの重みが識別に寄与しているかが学習されません。
正規分布に従う乱数による初期化
今度は,重みの初期値を平均 $0.0$ ,分散 $1.0$ の正規分布に従う乱数で初期化してみます。
void setup_parameters(MODEL_PARAMETER model_parameter) {
...
for(l = 0; l <= L; l++) {
int d_max = D[l];
int d_next_layer = D[l + 1];
for(i = 0; i <= d_max; i++) {
for(j = 0; j <= d_next_layer; j++) {
w[l][i][j] = rand_normal(0.0f, 1.0f);
}
}
}
...
}
この状態で,オンライン学習を再びおこなってみます。
$ gcc main_online_training.c
$ ./a.out ../data/train_data.txt ../data/validation_data.txt ../data/test_data.txt
Epoch: 1 / 10
loss: 0.079727, accuracy: 0.980400 validation loss: 0.008549, validation accuracy: 0.990000
Epoch: 2 / 10
loss: 0.005166, accuracy: 0.998700 validation loss: 0.003296, validation accuracy: 1.000000
Epoch: 3 / 10
loss: 0.002577, accuracy: 0.999200 validation loss: 0.020563, validation accuracy: 0.990000
Epoch: 4 / 10
loss: 0.001700, accuracy: 0.999600 validation loss: 0.004929, validation accuracy: 1.000000
Epoch: 5 / 10
loss: 0.001214, accuracy: 0.999600 validation loss: 0.007826, validation accuracy: 0.990000
Epoch: 6 / 10
loss: 0.000913, accuracy: 0.999800 validation loss: 0.003033, validation accuracy: 1.000000
Epoch: 7 / 10
loss: 0.001176, accuracy: 0.999600 validation loss: 0.014369, validation accuracy: 0.990000
Epoch: 8 / 10
loss: 0.000569, accuracy: 1.000000 validation loss: 0.001654, validation accuracy: 1.000000
Epoch: 9 / 10
loss: 0.000585, accuracy: 0.999900 validation loss: 0.002254, validation accuracy: 1.000000
Epoch: 10 / 10
loss: 0.000389, accuracy: 1.000000 validation loss: 0.023113, validation accuracy: 0.990000
Test
accuracy: 1.000000
今度は適切に学習が行われているようです。損失も減少しており,テストデータに対する精度も $1.0$ となっています。重みの初期値にはある程度のばらつきを持たせたいわけですが,どの程度の分散にすれば良いのでしょう。
Xavierの初期値
この問いに対する一つの案として「Xavierの初期値」と呼ばれるものがあります2。これは,隠れ層第 $l$ 層から第 $l + 1$ 層への重みの初期値の分散を $1 / n$ とするものです。標準偏差で表すと, $1 / \sqrt{n}$ となります。
void setup_parameters(MODEL_PARAMETER model_parameter) {
...
for(l = 0; l <= L; l++) {
int d_max = D[l];
int d_next_layer = D[l + 1];
float sigma = 1.0f / (sqrtf((float)d_max));
for(i = 0; i <= d_max; i++) {
for(j = 0; j <= d_next_layer; j++) {
// Xavier initialization
w[l][i][j] = rand_normal(0.0f, sigma);
}
}
}
...
}
確率的勾配降下法(SGD: Stochastic Gradient Descent)の節での実行結果はこのXavierの初期値を使った場合の実行結果です。
Heの初期値
Xaivierの初期値は,活性化関数が線形の場合に推奨されることが多いようです。活性化関数がReLU関数の場合には,「Heの初期値」が推奨されています3。Heの初期値では,隠れ層第 $l$ 層から第 $l + 1$ 層への重みの初期値の分散を $2 / n$ とします。標準偏差で表すと, $\sqrt{2 / n}$ となります。
void setup_parameters(MODEL_PARAMETER model_parameter) {
...
for(l = 0; l <= L; l++) {
int d_max = D[l];
int d_next_layer = D[l + 1];
float sigma = sqrtf(2.0f / (float)d_max);
for(i = 0; i <= d_max; i++) {
for(j = 0; j <= d_next_layer; j++) {
// He initialization
w[l][i][j] = rand_normal(0.0f, sigma);
}
}
}
...
}
Heの初期値を用いた場合次のようになりました。
Epoch: 1 / 10
loss: 0.133434, accuracy: 0.962700 validation loss: 0.017629, validation accuracy: 0.990000
Epoch: 2 / 10
loss: 0.006149, accuracy: 1.000000 validation loss: 0.008206, validation accuracy: 1.000000
Epoch: 3 / 10
loss: 0.003145, accuracy: 1.000000 validation loss: 0.009696, validation accuracy: 0.990000
Epoch: 4 / 10
loss: 0.002188, accuracy: 1.000000 validation loss: 0.004760, validation accuracy: 1.000000
Epoch: 5 / 10
loss: 0.001735, accuracy: 1.000000 validation loss: 0.004867, validation accuracy: 1.000000
Epoch: 6 / 10
loss: 0.001430, accuracy: 1.000000 validation loss: 0.007516, validation accuracy: 1.000000
Epoch: 7 / 10
loss: 0.001162, accuracy: 1.000000 validation loss: 0.004260, validation accuracy: 1.000000
Epoch: 8 / 10
loss: 0.001124, accuracy: 1.000000 validation loss: 0.003675, validation accuracy: 1.000000
Epoch: 9 / 10
loss: 0.000976, accuracy: 1.000000 validation loss: 0.006329, validation accuracy: 1.000000
Epoch: 10 / 10
loss: 0.000909, accuracy: 1.000000 validation loss: 0.004800, validation accuracy: 1.000000
Test
accuracy: 1.000000
データセットが簡単なので説得力に欠けてしまいますが,Heの初期値は収束が分散 $1.0$ の場合よりも早くなっています。
全体のコード
コードはこちら
続き
C言語でニューラルネットワークの実装(1)〜多層パーセプトロンの構造と活性化関数〜
C言語でニューラルネットワークの実装(2)〜順伝播と損失関数〜
C言語でニューラルネットワークの実装(3)〜誤差逆伝播法〜
C言語でニューラルネットワークの実装(4)〜データの準備〜
C言語でニューラルネットワークの実装(5)〜モデルの構造と順伝播の実装〜
C言語でニューラルネットワークの実装(6)〜逆伝播の実装〜
C言語でニューラルネットワークの実装(7)〜オンライン学習と重みの初期値〜 ←現在の記事
C言語でニューラルネットワークの実装(8)〜ミニバッチ学習〜
-
実際,「ゼロから作るDeep Learning」でもSGDはミニバッチ学習の説明の途中で登場しています。 ↩
-
"Understanding the difficulty of training deep feedforward neural networks", Xavier Glorot and Yoshua Bengio ↩
-
"Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification", Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun ↩