はじめに
本記事はライブラリを一切使わず、ニューラルネットワークを組む方法について数学的な説明と共に解説する記事となっています。説明はC言語で書きますが、他言語を使う人でも参考になると思います。
ニューラルネットワークをNNと略している部分があります。
第一回:ニューラルネットワークを理解したい!
第二回目のテーマは「ニューラルネットワークを作りたい!」です。
前回の記事では「NNは神経のシミュレーションではなくパラメータ調整可能な関数」という事を話しました。それを意識しながら作っていきましょう。
入力層? 隠れ層? 出力層? ただの「配列」だよ!
ニューラルネットワークには入力層、隠れ層、出力層と言う物がありますが、今回これらの言葉は無視します。使いません。だって全部ただの配列なのですから。まず理解を簡単にする為に下のようなニューラルネットワークを考えます。
A[0]とA[1]に入力を入れるとE[0]とE[1]という出力を得ます。これをanswerと比較してLossを計算します。なお、Lossは平均二乗誤差を使うとします。
float A[2];
float B[3];
float C[3];
float D[2];
float E[2];
float answer[2];
float loss;
はい。ただの配列ですね。それ以上でもそれ以下でもありません。
ニューラルネットワークは基本配列に過ぎない!
全結合層? 活性化関数? ただの「演算」だよ!
現時点ではAに数字を代入してもEは計算されません。そりゃあそうですよね、まだ全く計算式を書いていないのですから。今からそれを実装していきます。
さて、ニューラルネットワークの用語として「全結合層」や「活性化関数」という言葉があります。これも今回は無視します。どちらもただの演算に過ぎません。
……と言いたいところなのですが、ちょっとだけ解説します。
全結合層における演算
AからBのような演算を全結合層と言い、以下のような式で表されます。
$$
B_i=Bias_i + \sum_{j}(A_j \times P_{i,j})
$$
「ぎゃあ! 難しそう!」
と思ったそこのあなた。ご安心ください、プログラムで書けば超簡単です。
// パラメータを変数として定義
float P_AB[3][2];
float Bias_AB[3];
// 実際の計算
void calculateB(){
for(int i=0; i<3; i++){
float result = 0.0f;
result += Bias_AB[i];
for(int j=0; j<2; j++){
result += A[j] * P_AB[i][j];
}
B[i] = result;
}
}
// B[0]=Bias_AB[0] + A[0]*P_AB[0][0]+A[1]*P_AB[0][1]
// B[1]=Bias_AB[1] + A[0]*P_AB[1][0]+A[1]*P_AB[1][1]
// B[2]=Bias_AB[2] + A[0]*P_AB[2][0]+A[1]*P_AB[2][1]
これだけです! 凄く単純ですよね。
ここで注目して頂きたいのが「パラメータ」の存在です。Bias_ABとP_ABというパラメータが登場しました。これを今後、調整していくわけですね。
同様にCとDの間も定義できます。
float P_CD[2][3];
float Bias_CD[2];
// 実際の計算
void calculateD(){
for(int i=0; i<2; i++){
float result = 0.0f;
result += Bias_CD[i];
for(int j=0; j<3; j++){
result += C[j] * P_CD[i][j];
}
D[i] = result;
}
}
活性化関数における演算
BからCのような演算が活性化関数です。もう一度下に図を載せますね。
「線も少ないし、さっきよりも単純そう……?」
はい、先ほどの演算よりも簡単です。いきなりプログラムで書いてみますね。
void calculateC(){
for(int i=0; i<3; i++){
if(B[i] < 0) C[i] = 0.0f;
else C[i] = B[i];
}
}
これだけです! びっくりするくらい簡単ですね。B[i]が負ならC[i]G=0、B[i]が正ならC[i]=B[i]です。それ以上でもそれ以下でもありません。
こういう関数をReLUと言います。
活性化関数とはパラメータが存在せず、単純に計算するだけである。
ReLUとは負なら0、正ならそのまま代入する物である。
また、Eは次のようになります。少しだけ複雑な関数「Softmax」というものを実装しています。
これは数式で書くと次のようになります。
$$
E_i = \frac{e^{D_i}}{\sum_{j}e^{D_j}}=\frac{e^{D_i}}{e^{D_0}+e^{D_1}}=\frac{e^{D_i-C}}{e^{D_0-C}+e^{D_1-C}}
$$
物凄く複雑そうな式ですね……。これはいったい何をしているのでしょうか?
結論から言うと、E[i]の合計が1になるようにしています。例えば手書き文字認識だとE[0]+E[1]+...+E[9]=1となるようにして、「入力された数字が○○である確率」を計算します。その為に合計が1になるようにしたいのです。
単純に合計を1にするのだったら合計で割ればいいのですが、Softmax関数では各要素をe乗してから割ります。こうする事のメリットはここでは割愛しますね。
以下がC言語での実装です。なおexpf(x)
とは$e^x$を計算する関数です。
void calculateE(){
float C = max(D[0], D[1]);
float sum = 0.0f;
for(int i=0; i<2; i++){
sum += expf(D[i]-C);
}
for(int i=0; i<2; i++){
E[i] = expf(D[i]-C)/sum;
}
}
なお、C = max(D[0], D[1]);
としているのはオーバーフローを防ぐためです。
Softmaxは「e乗した物 ÷(e乗した物の合計)」
Softmaxではオーバーフローを防ぐための工夫が必要。
Softmaxの結果は合計が1になる。
Lossの計算
平均二乗誤差を求めます。コードは以下のようになります。
void calculateLoss(){
loss=0;
for(int i=0; i<2; i++){
loss+=(E[i]-answer[i])*(E[i]-answer[i])/2.0f
}
}
もうニューラルネットワークが完成してしまいました。わーパチパチ。
と言っても本番はここから、ニューラルネットワークのパラメータ調節をしない事には学習が進まないですよね。
第三回「ニューラルネットワークを作りたい! Part2」に続きます。