スポンサーリンク

ニューラルネットワークにXORを学習させてみた ~バックプロパゲーションの実装~

記事内に広告が含まれています。

この前、ニューラルネットワークを組んでみたよーって記事を書いたんですが、その記事で紹介したニューラルネットワークでは、線形分離ができない教師データを学習できませんでした。

※線形分離”可能”な教師データ・・・教師データをグラフ上に点としてプロットしていったときに、答えがある特定の直線(場合によっては平面)で完全に区切られるような教師データのことです。

例えば、2入力のAND演算を学習させるための教師データとかは、線形分離可能な教師データとなります。教師データを(入力1, 入力2, 出力)という形で表したとします。すると、教師データは(0, 0, 0), (0, 1, 0), (1, 0, 0), (1, 1, 1)という4種類ができます。これら教師データをグラフ上にプロットすると、答えである0と1を、ある特定の直線で区切ることが出来ます。(興味のある人は、グラフに描いてみてください)

※線形分離”不能”な教師データ・・・線形分離”可能”な教師データ以外。例えば、XORとかXNORとか。それ以外はパッと思いつきませんでした^^;

今回は、バックプロパゲーションと勾配降下法を使って、元論文で提示された本来のニューラルネットワークを組んでみました。前回記事と同様に、「それなりに遊べる」をコンセプトにプログラムを組んだので、解説はほとんどありません。(分かる人なら、ソースコード内のコメントを読めば分かるかも?)

元論文の解説とか、ニューラルネットワークの理論の解説とかも、その内、気が向いたら書きます。まぁ、偏微分の計算方法とか意味、性質みたいなことも解説しないといけなくて面倒なんで、やらないような気もしますが。

スポンサーリンク

今回作ったニューラルネットワーク

次の図1のようなニューラルネットワークが今回作ったニューラルネットワークになります。とはいえ、入力層から隠れ層への重みを学習するという点以外は、前回のニューラルネットワーク記事と同じです。

図1.ニューラルネットワークの模式図

前回記事では、このニューラルネットワークを学習するときの教師データには、AND演算を表現したデータを使いましたが、今回はXORを表すデータを使います。

ソースコード

グダグダ説明しててもしょうがないんで、ソースコードを載せときます。

/********************************
* main.cpp
* 出力層ニューロンに対する重みのみを修正してXOR演算を学習するプログラム
*
* 教師データの配列をいじればXOR以外についても学習可能。
* バックプロパゲーションで最後の層への重み以外も学習するようにするから。
*
* 出力にはシグモイド関数を使用
*
* 学習中の誤差の推移と入力に対する出力、最終的な学習結果を出力
*********************************/

#include <iostream>
#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型配列に

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

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

  int i, j, k;
  int unit_counter = 0;             // ユニットを変更するループで使う変数
  int learn_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;                    // 出力層の出力値を格納する配列
  double w0[INPUT_NUM + 1][HIDDEN_NUM];    // w0:入力層の出力に作用する重み行列
  double w1[HIDDEN_NUM + 1][OUTPUT_NUM];   // w1:隠れ層の出力に作用する重み行列
  double correct[TRANING_NUM][INPUT_NUM + OUTPUT_NUM] = {
    {0.0, 0.0, 1.0},
    {0.0, 1.0, 0.0},
    {1.0, 0.0, 0.0},
    {1.0, 1.0, 1.0},
  };// 学習データセット行列

  // 入力層、隠れ層、出力層の各数値を初期化
  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;   // バイアス

  output = 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;
  for(i = 0; i < TRANING_NUM; i++){
    for(j = 0; j < INPUT_NUM + OUTPUT_NUM; j++){
      std::cout << std::setw(DISPLAY_WIDTH)
                << std::setprecision(ACCURACY)
                << std::fixed
                << correct[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];
      }

      // 入力値の表示
/*      std::cout << "入力データ" << std::endl;
      for(j = 0; j < INPUT_NUM + 1; j++){
        std::cout << std::setw(DISPLAY_WIDTH)
                  << std::setprecision(ACCURACY)
                  << std::fixed
                  << input[j];
      }
      std::cout << std::endl;
*/
      // 順方向計算
      // 入力層から隠れ層
      Forward(&input[0], &w0[0][0], INPUT_NUM + 1, HIDDEN_NUM, &hidden[0]);

      // 隠れ層の数値を表示
/*      std::cout << "隠れ層の数値" << std::endl;
      for(j = 0; j < HIDDEN_NUM + 1; j++){
        std::cout << std::setw(DISPLAY_WIDTH)
                  << std::setprecision(ACCURACY)
                  << std::fixed
                  << hidden[j];
      }
      std::cout << std::endl;
*/
      // 隠れ層から出力層
      Forward(&hidden[0], &w1[0][0], HIDDEN_NUM + 1, OUTPUT_NUM, &output);

      // 入力とそれに対する出力の表示
      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;
        }
        std::cout << std::endl;
      }

      // 学習フェーズ
      // 各入力に対して重みを調整
      error_part = correct[i][INPUT_NUM] - output;

      // 隠れ層から出力層への重みを学習
      differencial[1] = Sigmoid_D(output);
      for(j = 0; j < HIDDEN_NUM + 1; j++){
        for(k = 0; k < OUTPUT_NUM; k++){
          w1[j][k] += 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[j][k] * differencial[0];
          }
        }
      }

      // 各入力に対しての正解と出力の差を足す(プラスとマイナスで打ち消し合わないようにするために、2乗した数値を足しわせる)
      error_whole += error_part * error_part;

    }
    if(!(learn_counter % DISPLAY_LOOP))
      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);

    // 入力とそれに対する出力の表示
    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;
    }
    std::cout << 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));
}

以上のコードをMSYS2 MINGW64環境でコンパイルしました。僕の場合は、結果が次のようになりました。

重み1
   -0.1780   -0.9122
   -0.2151    0.1315
   -0.3737    0.1127

重み2
    1.2393
   -0.4577
    0.9951

教師データ
    0.0000    0.0000    1.0000
    0.0000    1.0000    0.0000
    1.0000    0.0000    0.0000
    1.0000    1.0000    1.0000

学習ループ開始

入力と出力
    0.0000    0.0000    0.3830
入力と出力
    0.0000    1.0000    0.4052
入力と出力
    1.0000    0.0000    0.4056
入力と出力
    1.0000    1.0000    0.3616
error_whole = 1.1170(0回目)

...(似たような結果が出力され続ける)

入力と出力
    0.0000    0.0000    0.9651
入力と出力
    0.0000    1.0000    0.0506
入力と出力
    1.0000    0.0000    0.0528
入力と出力
    1.0000    1.0000    0.9325
error_whole = 0.0111(45000回目)


学習結果

重み1
   -1.0143   10.4865
   -0.9411    4.3153
   -0.2905    0.1127

重み2
  -34.6465
  -28.0647
  -36.4377

入力と出力
    0.0000    0.0000    0.9669
入力と出力
    0.0000    1.0000    0.0434
入力と出力
    1.0000    0.0000    0.0502
入力と出力
    1.0000    1.0000    0.9459

重み1というのが入力層から隠れ層への重みで、重み2というのが隠れ層から出力層への重みになります。で、「学習ループ開始」よりも下に、途中段階の学習結果が表示されます。「入力と出力」の下の行に、入力1が左側、入力2が真ん中、出力が右側に表示されています。教師データが全部で4セットあるので、4回連続で入力と出力のペアが表示されています。error_wholeというのが、実際の出力値と教師データとの誤差の大きさだと思ってください。error_wholeが小さければ小さいほど学習が上手く行われていると考えてください。

で、最終的な結果が「学習結果」の下に表示されています。

この結果を見ると、教師データを学習できていることが分かります。

筆者はこのプログラムによる責任は一切負いませんが、普通にコンパイルして遊ぶくらいであればそんなに大きな問題は出ないかと思いますので、適当にコンパイルして遊んでくださいませ~。

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