スポンサーリンク

Irisデータセットを使ってニューラルネットワークの実験をやってみよう ~機械学習による分類~

今までも何度かニューラルネットワークに関する記事を書いてますが、またC++でニューラルネットワークを作って遊んでみましたって記事になります。今回はアイリスデータセットをニューラルネットワークに分類させてみましたー。

ちなみに、機械学習に詳しい人ならもうご存知かもしれませんが、アイリスデータセットってのは、次の表のようなデータセットのことですな。

この1行で1つのアイリスに対応するデータになります(一番上の名前の行は適当に和訳して書き入れました)。なので、この表には3つのアイリスに対するデータが記録されてるってことになります。

アイリスデータセットには、こういう感じで合計150組のデータが記録されています。データセットのダウンロードはこちらからどうぞ~。

この記事では、そのアイリスデータセットを使って、こんな特徴を持つアイリスはこういう品種だよーってのをニューラルネットワークに学習させます。それから、学習に使ったデータ以外を使って、ちゃんと他のデータにも対応できるのかどうかを確認してやります(具体的な話は後述)。

要するに、分類問題をニューラルネットワークを使って解決しようって話ですな。で、そのニューラルネットワークの汎化性能はどの程度のもんかを測りたいと。

今までこのブログで取り上げたAND演算の学習とかXOR演算の学習とかよりはまだ実用的なのではないかと思います。

スポンサーリンク

やること

実際にやることとしては、

  1. アイリスデータセットを訓練データと検証データに分ける
  2. ニューラルネットワークに学習させる
  3. 学習の結果を確認する

という3つになります。

1.アイリスデータセットを訓練データと検証データに分ける

まずはアイリスデータセットを、ニューラルネットワークに学習させるためのデータ(訓練データ)とニューラルネットワークが問題なく学習できたのかをテストするためのデータ(検証データ)の2種類に分けます。

アイリスのデータは全部で150個あるので、訓練データが50個、検証データが100個になるように、ランダムにデータを分けます。

2.ニューラルネットワークに学習させる

前回までと同様、出力にはシグモイド関数を使って、学習アルゴリズムには勾配降下法を使って学習させます。今回も誤差の計算には二乗和誤差を使っています。

そんなニューラルネットワークに、ステップ1で用意した訓練データ50組を学習させます。

3.学習の結果を確認する

訓練データ50組を学習させたニューラルネットワークに、残りの100個のデータを分類させて、答え合わせをします。

ソースコード

というわけで、今回作ったニューラルネットワークのソースコードがこちらになります。利用は自己責任でお願いします。

こちらのソースコードをコンパイルして実行していただくと、LearningLog.csvって名前のファイルがexeファイルの置かれてるディレクトリに作られて、その中に、隠れ層のユニット数(1行目)と学習率(2行目)、そして3行目以下にループ回数(1列目)と出力の誤差の推移(2列目)が書かれているかと思います。

/********************************
* main.cpp
* アイリスデータセットから、正しいアイリスの分類方法を学習するプログラム
*
* 出力にはシグモイド関数を使用
*
* 学習中の誤差の推移と入力に対する出力、最終的な学習結果等を出力
*********************************/

#include <iostream>
#include <fstream>
#include <iomanip>
#include <cmath>
#include <random>

void WeightInit(double *, int); // 配列の最初の要素のポインタ, 次層のニューロン数(次層のニューロン数分だけループして初期化する)
double Sigmoid(double);         // 引数に対するシグモイド関数を返り値として返す
double Sigmoid_D(double);       // 与えられた引数を代入したシグモイド関数の偏微分値を返す
void Forward(double *, double *, int, int, double *);      // 順方向計算(重み付き和の計算) 入力するベクトル, 重み行列, 前層のニューロン数, 次層のニューロン数, 結果を格納する配列
void Output(double *, double *, double *, int, int);       // 一つ目のdouble型配列に
int MaxArray(double *, int);         // 配列中で、最も大きい値を持っている要素の番号を返す

// メルセンヌ・ツイスター法による擬似乱数生成器を、
// ハードウェア乱数をシードにして初期化
std::random_device seed_gen;
std::mt19937 engine(seed_gen());

int main()
{
  const int INPUT_NUM = 4;          // 入力層のユニット数
  const int HIDDEN_NUM = 8;         // 隠れ層のユニット数
  const int OUTPUT_NUM = 3;         // 出力層のユニット数
  const int ERR_INIT = 100;         // 誤差の初期値
  const int TRANING_NUM = 50;       // 教師データの数
  const int TEST_NUM = 100;         // 検証データの数
  const double ALPHA = 0.06;        // 学習率
  const double LIMIT_ERR = 0.01;    // 学習が収束したと判断する数値(LIMIT_ERR以下なら学習は終了)
  const int LIMIT_CALC = 50000;     // 計算の最大数(学習が収束しない場合に強制終了するループ数)
  const int DISPLAY_WIDTH = 10;     // 1つの数値を表示する幅
  const int ACCURACY = 4;           // 数値を表示するときの精度
  const int DISPLAY_LOOP = 100;     // 出力値を表示するタイミング

  int i, j, k;
  int output_counter = 0;           // 出力ユニット用のカウンタ
  int unit_counter = 0;             // ユニットを変更するループで使う変数
  int learn_counter = 0;            // 学習用のループ回数を格納するための変数
  int correct_counter = 0;          // 正解数を保持するカウンタ
  double differencial[2];           // 微分値を格納するための変数  [0]は入力層から隠れ層  [1]は隠れ層から出力層
  double error_whole = ERR_INIT;    // 教師データすべての誤差の和
  double error_part = 0;            // ある入力に対する出力と教師データとの誤差
  double input[INPUT_NUM + 1];      // 入力層の出力値を格納する配列
  double hidden[HIDDEN_NUM + 1];    // 隠れ層の出力値を格納する配列
  double output[OUTPUT_NUM];        // 出力層の出力値を格納する配列
  double w0[INPUT_NUM + 1][HIDDEN_NUM];    // w0:入力層の出力に作用する重み行列
  double w1[HIDDEN_NUM + 1][OUTPUT_NUM];   // w1:隠れ層の出力に作用する重み行列
  double correct[TRANING_NUM][INPUT_NUM + OUTPUT_NUM] = {
    {6.8,2.8,4.8,1.4,0,1,0},
{6.3,3.4,5.6,2.4,0,0,1},
{5.1,3.5,1.4,0.2,1,0,0},
{5.8,2.7,3.9,1.2,0,1,0},
{5.4,3.4,1.7,0.2,1,0,0},
{6.5,2.8,4.6,1.5,0,1,0},
{6.6,3,4.4,1.4,0,1,0},
{6.9,3.1,4.9,1.5,0,1,0},
{5.6,2.5,3.9,1.1,0,1,0},
{7.4,2.8,6.1,1.9,0,0,1},
{6.5,3,5.2,2,0,0,1},
{6.4,3.2,4.5,1.5,0,1,0},
{5.7,2.8,4.5,1.3,0,1,0},
{4.9,3.1,1.5,0.1,1,0,0},
{4.4,3.2,1.3,0.2,1,0,0},
{5,3.6,1.4,0.2,1,0,0},
{5,3.4,1.5,0.2,1,0,0},
{5.6,2.9,3.6,1.3,0,1,0},
{6,2.9,4.5,1.5,0,1,0},
{6.7,3.1,5.6,2.4,0,0,1},
{5.7,2.6,3.5,1,0,1,0},
{6.7,3,5.2,2.3,0,0,1},
{5.5,3.5,1.3,0.2,1,0,0},
{6,3.4,4.5,1.6,0,1,0},
{7.7,2.8,6.7,2,0,0,1},
{6.9,3.1,5.1,2.3,0,0,1},
{6.3,3.3,4.7,1.6,0,1,0},
{5.7,2.5,5,2,0,0,1},
{5.1,3.7,1.5,0.4,1,0,0},
{4.8,3,1.4,0.3,1,0,0},
{4.9,3.1,1.5,0.1,1,0,0},
{6.3,2.9,5.6,1.8,0,0,1},
{6.7,3.3,5.7,2.1,0,0,1},
{5.7,2.9,4.2,1.3,0,1,0},
{4.9,3.1,1.5,0.1,1,0,0},
{6.3,2.7,4.9,1.8,0,0,1},
{5.1,3.5,1.4,0.3,1,0,0},
{6.2,2.2,4.5,1.5,0,1,0},
{6.7,3,5,1.7,0,1,0},
{7.1,3,5.9,2.1,0,0,1},
{5.7,3.8,1.7,0.3,1,0,0},
{5.3,3.7,1.5,0.2,1,0,0},
{6,2.2,5,1.5,0,0,1},
{5.5,2.6,4.4,1.2,0,1,0},
{7.3,2.9,6.3,1.8,0,0,1},
{6.1,2.6,5.6,1.4,0,0,1},
{5.7,4.4,1.5,0.4,1,0,0},
{4.4,3,1.3,0.2,1,0,0},
{6.5,3,5.8,2.2,0,0,1},
{6.7,3.3,5.7,2.5,0,0,1}
  };    // 学習データセット行列
  double test[TEST_NUM][INPUT_NUM + OUTPUT_NUM] = {
    {4.3,3,1.1,0.1,1,0,0},
{5.6,2.7,4.2,1.3,0,1,0},
{4.7,3.2,1.3,0.2,1,0,0},
{5.1,3.8,1.9,0.4,1,0,0},
{6.6,2.9,4.6,1.3,0,1,0},
{5.9,3,4.2,1.5,0,1,0},
{6.2,2.9,4.3,1.3,0,1,0},
{5.6,2.8,4.9,2,0,0,1},
{6.4,2.7,5.3,1.9,0,0,1},
{5,3.3,1.4,0.2,1,0,0},
{6.4,3.2,5.3,2.3,0,0,1},
{6.4,2.8,5.6,2.1,0,0,1},
{5.4,3,4.5,1.5,0,1,0},
{7.9,3.8,6.4,2,0,0,1},
{5.1,3.8,1.6,0.2,1,0,0},
{4.5,2.3,1.3,0.3,1,0,0},
{6.1,2.8,4.7,1.2,0,1,0},
{6.8,3,5.5,2.1,0,0,1},
{5.4,3.4,1.5,0.4,1,0,0},
{5,2,3.5,1,0,1,0},
{6.7,2.5,5.8,1.8,0,0,1},
{7.7,2.6,6.9,2.3,0,0,1},
{5,3.2,1.2,0.2,1,0,0},
{7.2,3,5.8,1.6,0,0,1},
{5.1,3.4,1.5,0.2,1,0,0},
{5.5,2.4,3.8,1.1,0,1,0},
{5,2.3,3.3,1,0,1,0},
{5.8,2.7,5.1,1.9,0,0,1},
{5.5,2.5,4,1.3,0,1,0},
{7.6,3,6.6,2.1,0,0,1},
{4.6,3.2,1.4,0.2,1,0,0},
{5.8,2.7,4.1,1,0,1,0},
{6.5,3,5.5,1.8,0,0,1},
{6.3,2.5,5,1.9,0,0,1},
{5.1,2.5,3,1.1,0,1,0},
{6.9,3.1,5.4,2.1,0,0,1},
{5.2,4.1,1.5,0.1,1,0,0},
{5.2,2.7,3.9,1.4,0,1,0},
{5.8,2.6,4,1.2,0,1,0},
{6.2,2.8,4.8,1.8,0,0,1},
{5.2,3.4,1.4,0.2,1,0,0},
{6.1,3,4.6,1.4,0,1,0},
{4.9,2.5,4.5,1.7,0,0,1},
{4.8,3.4,1.9,0.2,1,0,0},
{6.7,3.1,4.4,1.4,0,1,0},
{5,3.4,1.6,0.4,1,0,0},
{7.2,3.6,6.1,2.5,0,0,1},
{5,3,1.6,0.2,1,0,0},
{6.4,2.9,4.3,1.3,0,1,0},
{6.7,3.1,4.7,1.5,0,1,0},
{5.8,2.8,5.1,2.4,0,0,1},
{4.8,3.4,1.6,0.2,1,0,0},
{6.1,2.9,4.7,1.4,0,1,0},
{7.2,3.2,6,1.8,0,0,1},
{6.4,3.1,5.5,1.8,0,0,1},
{6.8,3.2,5.9,2.3,0,0,1},
{4.9,2.4,3.3,1,0,1,0},
{5.9,3.2,4.8,1.8,0,1,0},
{6.9,3.2,5.7,2.3,0,0,1},
{5.5,4.2,1.4,0.2,1,0,0},
{5,3.5,1.6,0.6,1,0,0},
{6,2.2,4,1,0,1,0},
{5.7,2.8,4.1,1.3,0,1,0},
{6.2,3.4,5.4,2.3,0,0,1},
{4.7,3.2,1.6,0.2,1,0,0},
{6.3,3.3,6,2.5,0,0,1},
{4.6,3.1,1.5,0.2,1,0,0},
{4.6,3.6,1,0.2,1,0,0},
{5,3.5,1.3,0.3,1,0,0},
{5.7,3,4.2,1.2,0,1,0},
{6.4,2.8,5.6,2.2,0,0,1},
{4.9,3,1.4,0.2,1,0,0},
{5.5,2.4,3.7,1,0,1,0},
{6,2.7,5.1,1.6,0,1,0},
{4.6,3.4,1.4,0.3,1,0,0},
{5.4,3.7,1.5,0.2,1,0,0},
{4.8,3.1,1.6,0.2,1,0,0},
{5.8,2.7,5.1,1.9,0,0,1},
{7.7,3.8,6.7,2.2,0,0,1},
{5.4,3.9,1.7,0.4,1,0,0},
{5.8,4,1.2,0.2,1,0,0},
{5.1,3.8,1.5,0.3,1,0,0},
{6.5,3.2,5.1,2,0,0,1},
{6.3,2.3,4.4,1.3,0,1,0},
{4.8,3,1.4,0.1,1,0,0},
{5.5,2.3,4,1.3,0,1,0},
{6.3,2.8,5.1,1.5,0,0,1},
{5.1,3.3,1.7,0.5,1,0,0},
{6.1,2.8,4,1.3,0,1,0},
{6.1,3,4.9,1.8,0,0,1},
{5.2,3.5,1.5,0.2,1,0,0},
{6.3,2.5,4.9,1.5,0,1,0},
{6,3,4.8,1.8,0,0,1},
{5.9,3,5.1,1.8,0,0,1},
{7,3.2,4.7,1.4,0,1,0},
{5.6,3,4.1,1.3,0,1,0},
{4.4,2.9,1.4,0.2,1,0,0},
{5.4,3.9,1.3,0.4,1,0,0},
{5.6,3,4.5,1.5,0,1,0},
{7.7,3,6.1,2.3,0,0,1}
};          // テストデータ

  std::ofstream output_file("LearningLog.csv");

  // 基本情報の出力
  output_file << "隠れ層のユニット数," << HIDDEN_NUM << std::endl;
  output_file << "学習率," << ALPHA << std::endl;

  // 入力層、隠れ層、出力層の各数値を初期化
  for(i = 0; i < INPUT_NUM; i++){
    input[i] = 0;
  }
  input[INPUT_NUM] = -1;    // バイアス

  for(i = 0; i < HIDDEN_NUM; i++){
    hidden[i] = 0;
  }
  hidden[HIDDEN_NUM] = -1;   // バイアス

  for(i = 0; i < OUTPUT_NUM; i++){
    output[i] = 0;
  }

  // 重みの初期化
  for(i = 0; i < INPUT_NUM + 1; i++){
    WeightInit(w0[i], HIDDEN_NUM);
  }

  for(i = 0; i < HIDDEN_NUM + 1; i++){
    WeightInit(w1[i], OUTPUT_NUM);
  }

  // 微分値を格納する変数を初期化
  for(i = 0; i < 2; i++){
    differencial[i] = 0;
  }

  // 重み(入力層から隠れ層)の表示
  std::cout << "重み1" << std::endl;
  for(i = 0; i < INPUT_NUM + 1; i++){
    for(j = 0; j < HIDDEN_NUM; j++){
      std::cout << std::setw(DISPLAY_WIDTH)
                << std::setprecision(ACCURACY)
                << std::fixed
                << w0[i][j];
    }
    std::cout << std::endl;
  }
  std::cout << std::endl;

  // 重み(隠れ層から出力層)の表示
  std::cout << "重み2" << std::endl;
  for(i = 0; i < HIDDEN_NUM + 1; i++){
    for(j = 0; j < OUTPUT_NUM; j++){
      std::cout << std::setw(DISPLAY_WIDTH)
                << std::setprecision(ACCURACY)
                << std::fixed
                << w1[i][j];
    }
    std::cout << std::endl;
  }
  std::cout << std::endl;

  // 学習
  std::cout << "学習ループ開始" << std::endl;
  std::cout << std::endl;

  while(error_whole > LIMIT_ERR){
    error_whole = 0;    // 全体の誤差を0にしておく

    // 教師データの数分だけループ
    for(i = 0; i < TRANING_NUM; i++){
      // 入力
      for(j = 0; j < INPUT_NUM; j++){
        input[j] = correct[i][j];
      }

      // 順方向計算
      Forward(&input[0], &w0[0][0], INPUT_NUM + 1, HIDDEN_NUM, &hidden[0]); // 入力層から隠れ層
      Forward(&hidden[0], &w1[0][0], HIDDEN_NUM + 1, OUTPUT_NUM, &output[0]);  // 隠れ層から出力層
/*
      // 入力とそれに対する出力の表示
      if(!(learn_counter % DISPLAY_LOOP)){

        std::cout << "入力と出力" << std::endl;
        for(j = 0; j < INPUT_NUM; j++){
          std::cout << std::setw(DISPLAY_WIDTH)
                    << std::setprecision(ACCURACY)
                    << std::fixed
                    << input[j];
        }
        for(j = 0; j < OUTPUT_NUM; j++){
          std::cout << std::setw(DISPLAY_WIDTH)
                    << std::setprecision(ACCURACY)
                    << std::fixed
                    << output[j];

        }
        std::cout << std::endl;
      }
*/
      // 学習フェーズ
      // 各出力ユニットに対して重みを調整(出力ユニットの個数だけループする)
      for(output_counter = 0; output_counter < OUTPUT_NUM; output_counter++)
      {
        error_part = correct[i][INPUT_NUM + output_counter] - output[output_counter];

        // 隠れ層から出力層への重みを学習
        differencial[1] = Sigmoid_D(output[output_counter]);
        for(j = 0; j < HIDDEN_NUM + 1; j++){
            w1[j][output_counter] += ALPHA * hidden[j] * error_part * differencial[1];
        }

        // 入力層から隠れ層への重みを学習
        for(unit_counter = 0; unit_counter < HIDDEN_NUM; unit_counter++){
          differencial[0] = Sigmoid_D(hidden[unit_counter]);
          for(j = 0; j < INPUT_NUM + 1; j++){
            for(k = 0; k < HIDDEN_NUM; k++){
              w0[j][k] += ALPHA * input[j] * error_part * differencial[1] * w1[k][output_counter] * differencial[0];
            }
          }
        }

        // 各入力に対しての正解と出力の差を足す(プラスとマイナスで打ち消し合わないようにするために、2乗した数値を足しわせる)
        error_whole += error_part * error_part;
      }
    }
    if(!(learn_counter % DISPLAY_LOOP)){
      output_file << learn_counter << "," << error_whole << std::endl;
      std::cout << "error_whole = " << error_whole << "(" << learn_counter << "回目)" << std::endl;
    }

    learn_counter++;
    if(learn_counter > LIMIT_CALC) break;
  }

  // 学習結果の表示

  std::cout << std::endl;
  std::cout << "学習結果" << std::endl;
  std::cout << std::endl;

  // 重み(入力層から隠れ層)の表示
  std::cout << "学習後の重み1" << std::endl;
  for(i = 0; i < INPUT_NUM + 1; i++){
    for(j = 0; j < HIDDEN_NUM; j++){
      std::cout << std::setw(DISPLAY_WIDTH)
                << std::setprecision(ACCURACY)
                << std::fixed
                << w0[i][j];
    }
    std::cout << std::endl;
  }
  std::cout << std::endl;

  // 重み(隠れ層から出力層)の表示
  std::cout << "学習後の重み2" << std::endl;
  for(i = 0; i < HIDDEN_NUM + 1; i++){
    for(j = 0; j < OUTPUT_NUM; j++){
      std::cout << std::setw(DISPLAY_WIDTH)
                << std::setprecision(ACCURACY)
                << std::fixed
                << w1[i][j];
    }
    std::cout << std::endl;
  }
  std::cout << std::endl;

  // 教師データの数分だけループ
  for(i = 0; i < TRANING_NUM; i++){
    // 入力
    for(j = 0; j < INPUT_NUM; j++){
      input[j] = correct[i][j];
    }

    // 順方向計算
    // 入力層から隠れ層
    Forward(&input[0], &w0[0][0], INPUT_NUM + 1, HIDDEN_NUM, &hidden[0]);
    Forward(&hidden[0], &w1[0][0], HIDDEN_NUM + 1, OUTPUT_NUM, &output[0]);

    // 入力とそれに対する出力の表示
    if( i == 0 ) std::cout << "入力と出力" << std::endl;
    for(j = 0; j < INPUT_NUM; j++){
      std::cout << std::setw(DISPLAY_WIDTH)
                << std::setprecision(ACCURACY)
                << std::fixed
                << input[j];
    }
    // アイリスの種類の名前と、正解/不正解を表示する
    switch ( MaxArray(&output[0], OUTPUT_NUM) ) {
      case 0:
        std::cout << " Setosa";
        if( MaxArray(&correct[i][INPUT_NUM], OUTPUT_NUM) == 0 ){ std::cout << " 正解"; correct_counter++; }
        else{ std::cout << " 不正解"; }
        break;
      case 1:
        std::cout << " Versicolor";
        if( MaxArray(&correct[i][INPUT_NUM], OUTPUT_NUM) == 1 ){ std::cout << " 正解"; correct_counter++; }
        else{ std::cout << " 不正解"; }
        break;
      case 2:
        std::cout << " Virginica";
        if( MaxArray(&correct[i][INPUT_NUM], OUTPUT_NUM) == 2 ){ std::cout << " 正解"; correct_counter++; }
        else{ std::cout << " 不正解"; }
        break;
    }

    std::cout << std::endl;
  }

  // 正解率を表示
  std::cout << "教師データに対する正解数は" << correct_counter << std::endl;
  std::cout << "なので、正解率は" << ( (double)correct_counter / (double)TRANING_NUM ) << std::endl;

  // 汎化性能調査
  correct_counter = 0;    // 正解数をリセット
  std::cout << std::endl;
  std::cout << "検証データに対して" << std::endl;
  std::cout << std::endl;

  // 検証データの数だけループ
  for(i = 0; i < TEST_NUM; i++){
    // 入力
    for(j = 0; j < INPUT_NUM; j++){
      input[j] = test[i][j];
    }

    // 順方向計算
    // 入力層から隠れ層
    Forward(&input[0], &w0[0][0], INPUT_NUM + 1, HIDDEN_NUM, &hidden[0]);
    Forward(&hidden[0], &w1[0][0], HIDDEN_NUM + 1, OUTPUT_NUM, &output[0]);

    // 入力とそれに対する出力の表示
    if( i == 0 ) std::cout << "入力と出力" << std::endl;
    for(j = 0; j < INPUT_NUM; j++){
      std::cout << std::setw(DISPLAY_WIDTH)
                << std::setprecision(ACCURACY)
                << std::fixed
                << input[j];
    }
    // アイリスの種類の名前と、正解/不正解を表示する
    switch ( MaxArray(&output[0], OUTPUT_NUM) ) {
      case 0:
        std::cout << " Setosa";
        if( MaxArray(&test[i][INPUT_NUM], OUTPUT_NUM) == 0 ){ std::cout << " 正解"; correct_counter++; }
        else{ std::cout << " 不正解"; }
        break;
      case 1:
        std::cout << " Versicolor";
        if( MaxArray(&test[i][INPUT_NUM], OUTPUT_NUM) == 1 ){ std::cout << " 正解"; correct_counter++; }
        else{ std::cout << " 不正解"; }
        break;
      case 2:
        std::cout << " Virginica";
        if( MaxArray(&test[i][INPUT_NUM], OUTPUT_NUM) == 2 ){ std::cout << " 正解"; correct_counter++; }
        else{ std::cout << " 不正解"; }
        break;
    }

    std::cout << std::endl;
  }

  // 正解率を表示
  std::cout << "検証データに対する正解数は" << correct_counter << std::endl;
  std::cout << "なので、正解率は" << ( (double)correct_counter / (double)TEST_NUM ) << std::endl;
}

void Forward(double *prev_neuron, double *weight, int prev_num, int next_num, double *result)
{
  int i, j;

  for(i = 0; i < next_num; i++){
    result[i] = 0;
  }

  for(i = 0; i < next_num; i++){
    for(j = 0; j < prev_num; j++){
        result[i] += weight[i + j * next_num] * prev_neuron[j];
      }
  }

  // シグモイド関数の適用
  for(i = 0; i < next_num; i++){
    result[i] = Sigmoid(result[i]);
  }
}

void WeightInit(double *arr, int prev_num)
{
  // 正規分布
  // 平均0.0、標準偏差0.7071で分布させる
  std::normal_distribution<> GDist(0.0, 0.7071);

  for(int j = 0; j < prev_num; j++){
    arr[j] = GDist(engine);
  }
}

double Sigmoid(double f)
{
  return (1.0 / (1.0 + exp(-1.0 * f)) );
}

double Sigmoid_D(double out)
{
  return (out * (1 - out));
}

int MaxArray(double *arr, int size)
{
  int number = 0;

  for(int i = 1; i < size; i++)
  {
    // 配列の左から順に比較していって、
    // 右側に大きい数値が入っていたら、
    // 返すべき要素番号をその要素番号に更新する
    if(arr[i] > arr[number]) { number = i; }
  }

  return number;
}

ちょっとだけ動作の解説

ニューラルネットワークにやらせたい作業は、がくの長さ、がくの幅、花びらの長さ、花びらの幅という4つの特徴を持ったデータがどの品種か(SetosaかVersicolorかVirginicaか)を分類させるという作業になります。

なので、ニューラルネットワークの動作としては、入力として4つの数値を与えると、出力として品種を出力してほしいわけです。

その動作を実現するために、入力層には4つのユニット(INPUT_NUM = 4)、出力層には3つのユニット(OUTPUT_NUM = 3)を設定してやります。入力層の4つのユニットは、4つの特徴に対応していて、出力層の3つのユニットは3つの分類に対応してるわけですな。

出力層のどのユニットも0から1までの実数値を出力します。出力層の3つのユニットは各品種に対応していて、その出力値の大小関係によって、どの品種かを判断します。

あとは、ソースコードを見ながら理解していってください。

結果

プログラムを実行すると、コマンドラインに入力に対してプログラムがどう判断したかが表示されるかと思います(コマンドラインに表示される結果は省略しますが)。

で、それと同時に二乗和誤差の推移を記録したExcelファイルも新しく作られるってことを説明しました。そのExcelファイルの中身をグラフ化すると次のようになりました。

図1.誤差と学習経過の関係

0に近づくのが早ければ早いほど良い、最終的に0に近ければ近いほど良いって感じにお考えください。

LeaningLog.csvに書き込まれたデータを見てみると大体2000回目くらいの学習で誤差が0.1を下回るようになってまして、なかなか早くに学習が収束するんだなぁって思いましたねー。

次に、汎化性能はどれだけか?って話ですが、結果は検証データとして使った100個のデータの内95個が正解になりました。

というわけで、今回のニューラルネットワークですが、過学習も起こってなさそうだし、総じて思った以上に良い性能なんだなぁって印象を受けましたねー。

皆様も、良かったらソースコードの学習率とか隠れ層のユニット数、教師データと検証データの組み合わせなんかを調整して遊んでみてくださいませ~。ではでは~。

タイトルとURLをコピーしました