FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

フルスクラッチから作るニューラルネットワーク (9. 精度変更、固定小数点化検討)

誤差逆伝搬法を実装してMNISTの学習と認識プログラムをC言語フルスクラッチで作る。 動作するコードは完成し、今度はこれを固定小数点化しなければならない。今回は固定小数点化の検討を行う。

double –> float で精度を確認する。

まず、これまでに実装したMNISTプログラムの型をすべてdoubleからfloatに変換し、精度をチェックしておく。

github.com

やっていることは、単純にreplaceしているだけだ。これで同様にコンパイルし、実行すると、結果が得られた。

=== TestNetwork ===
Correct = 1342
=== TestNetwork ===
Correct = 5293
=== TestNetwork ===
Correct = 6388
=== TestNetwork ===
Correct = 7874
=== TestNetwork ===
Correct = 8447
=== TestNetwork ===
Correct = 8637
=== TestNetwork ===
Correct = 8700
=== TestNetwork ===
Correct = 8865
=== TestNetwork ===
Correct = 8924
=== TestNetwork ===
Correct = 8907
=== TestNetwork ===
Correct = 8966
=== TestNetwork ===
Correct = 9053

doubleのものと全く変わらない。

double –> 固定小数点

固定小数点化を行うために、まずはフレームワークを導入する。参考にしたのは以下のブログだ。

sunafukin2go.hatenablog.com

上記のサイトを参考に、以下のようにunionを導入した。

typedef union {
  int32_t i;
  struct {
    unsigned int fraction: 16;
    unsigned int decimal : 15;
    unsigned int sign : 1;
  } st_fix16;
} fix16_t;

#define FX16_RAW(fx16) (fx16.i)
#define FX16_SIGN(fx16) (fx16.st_fix16.sign)

これでdoubleで表現されているところをすべてfix16_tに書き換える。C++のtemplateを使えばより簡単になるだろうが、ここではなるべく平易なアセンブリ言語に変換されることを想定して、自分で演算も記述していく。

加算は単純にそのままの値を足すだけでよいが、乗算、除算については桁を合わせるためにひと工夫必要になる。

// 加算
A.i + B.i
// 減算
A.i - B.i
// 乗算
(A.i * B.i) >> 16
// 除算
((A.i << 32) / B.i) >> 16

と、これらの演算を定義して検証しているのだが、どうもうまくいかない。どうにかして固定小数点でのMNISTを動作させられるように、継続してデバッグしていく。

フルスクラッチから作るニューラルネットワーク (8. 性能解析)

誤差逆伝搬法を実装してMNISTの学習と認識プログラムをC言語フルスクラッチで作る、動作するコードは完成したのだが、最終的にはこれらのコードをマイコンで実行させたいし、もっと軽くしなければならない。

また現状の問題点として、C言語で書いたプログラムとPythonで書いてある例題のプログラムの実行速度がほぼ同一ということも挙げられる。バイナリ実行とPythonスクリプト実行の速度が同一だなんて!

まず、入力ファイルを少しずつ読み込んでいくのをや止め、一気に配列に読み込んでニューラルネットワークに流し込んでいくように変更する。最初は大きめの配列を確保しているとSegmentation Faultが起きて何だろうと思っていたのだが、mallocを使うと解消した。C言語忘れすぎ。

  double hi[HIDDENNO + 1];
  double *in_data = malloc (sizeof(double) * MAXINPUTNO * INPUTNO);
  double ans_data[MAXINPUTNO];
  double o[OUTPUTNO];
  int n_of_e;

これで一応一気にファイルを読み込んで実行できるようになったが、これでも全然速くならない。

Google-Profilerを使って解析してみることにした。解析方法はコチラを参考にした。

nu-pan.hatenablog.com

プロファイリングを取りたい場所に、以下の関数を挿入してプロファイリングを取得する。

  ProfilerStart("mnist.prof");

...
  ProfilerStop();

取得したプロファイリングをGUIで表示する。

google-pprof --callgrind train_twolayernet mnist.prof > train_twolayernet.cg
kcachegrind train_twolayernet.cg &

念のため、-O3をすると関数の呼び出し関係が崩れるので省略して解析した。

f:id:msyksphinz:20170704021534p:plain

異様に単純なループだが、やはりAffineのニューロンがかなりの計算量を消費していることが分かる。この部分は完全に行列演算の部分であるため、ここをどのように実装するかが鍵になるんだろう。

さて、ここら辺を考慮しつつ、今度はマイコンで実装するために、制度を落として固定小数点での実装方法を検討していく。

「GPUを支える技術」を購入

面白そうな本が出版されたので購入した。「GPUを支える技術」だ。

GPUを支える技術 ――超並列ハードウェアの快進撃[技術基礎] (WEB+DB PRESS plus)

GPUを支える技術 ――超並列ハードウェアの快進撃[技術基礎] (WEB+DB PRESS plus)

Hisa Ando氏のプロセッサ関連の書籍にはいつも大変お世話になっているため、GPUについても勉強のため購入した。 ただし、実践向け書籍ではなく一般的な内容を解説するタイプの書籍らしいため、正直読了できるか不安。。。

まとめ資料を作りながら読んでいこうかな。

RISC-V シミュレータツールチェイン rv8

こういうツールチェインが作りたいんだよなあ。見た目にも非常にきれいな、RISC-Vシミュレータツールセットだ。RISC-V命令セットからx86/64へのバイナリトランスレーション(JIT)もサポートしている。 RISC-Vのほしいツールチェイン群はすべて揃っているんじゃないか、という印象。

こんな感じのログを出力することができるらしい。きれいだなあ。

https://raw.githubusercontent.com/rv8-io/rv8/master/doc/images/screenshot-1.png

  • rv-jit - user mode x86-64 binary translator
  • rv-sim - user mode system call proxy simulator
  • rv-sys - full system emulator with soft MMU
  • rv-bin - ELF disassembler and histogram tool
  • rv-meta - code and documentation generator

rv8 - RISC-V simulator for x86-64

github.com

まだきちんと実行しているわけではないが、一応cloneしてビルドできるところまでは確認した。きちんと使いこなせるようにしないと。。。

フルスクラッチから作るニューラルネットワーク(7. MNIST誤差逆伝搬法の実装)

msyksphinz.hatenablog.com

誤差逆伝搬法をC言語で実装していたのだが、Pythonの結果とどうしても合わない部分がある。それどころが、学習を続けていくとどんどん正解率が下がっていったのでデバッグをしていた。

ニューラルネットワークの難しいところは、ランダム要素が多すぎてどこが間違っているのか分かりにくいところだと思う。 実際、今回Pythonのコードで流した学習結果と演算結果が合うように調整していったのだが、なるべくPythonのコードもランダム性がなくなるように調整していったり、かなり苦労した。

  • 重みの初期値はPythonコードの初期値をファイルに書き出してC言語側にインポートする。
  • 訓練データの選択はPythonではランダムランダム選択しているが、C言語と同様にシーケンシャルにデータを取得する方法に変更。
  • 1ステップ毎に重み値をファイルに出力してC言語側と比較。
  • そもそも入力値とラベルが正しいのかファイルに書き出して比較

そうすると自分では正しいと思っていたコードに問題が出るわ出るわ。C言語側の正規化の部分が間違っていたり、配列が実は初期化できていなかったりと問題がたくさんあったが、どうにか修正して正常に動作するようになった。

github.com

学習の結果は以下のようになった。トレーニング数60000を超えたところで、90%を超える学習率になった。

問題はPythonコードに比べて処理が遅い気がする。Pythonより遅いのは問題だなあ。。。

./train_twolayernet
gcc -g twolayernet.c -o twolayernet -lm
gcc -g train_twolayernet.c -o train_twolayernet -lm
gcc -g train_twolayernet_float.c -o train_twolayernet_float -lm
2051
60000
28
28
2049
60000
=== TestNetwork ===
Correct = 1342
=== TestNetwork ===
Correct = 5293
=== TestNetwork ===
Correct = 6388
=== TestNetwork ===
Correct = 7874
=== TestNetwork ===
Correct = 8447
=== TestNetwork ===
Correct = 8637
=== TestNetwork ===
Correct = 8700
=== TestNetwork ===
Correct = 8865
=== TestNetwork ===
Correct = 8924
=== TestNetwork ===
Correct = 8907
=== TestNetwork ===
Correct = 8966
=== TestNetwork ===
Correct = 9053

f:id:msyksphinz:20170630022526p:plain

フルスクラッチからC言語で作るニューラルネットワーク (6. MNIST 誤差逆伝搬法の実装)

前回まででデータ処理部のバッチ処理化まで完了したので、学習のための誤差逆伝搬法をC言語で実装する。 今回も、Pythonでの実装を参考にしながら、逐次データを抽出して、計算結果を合わせ込んで実装していく。

まずは、各処理についてどのような行列演算が行われているのかまとめておこう。

MNISTデータパス

今回はバッチサイズ100のニューラルネットワークを構築し、以下の4つの処理を逐次配置して実装した。

  1. Affine1処理
  2. ReLU活性化
  3. Affine2処理
  4. Softmax処理

それぞれ、行列演算の形を見ながらどのような計算をしているのかまとめておく。

f:id:msyksphinz:20170630020540p:plain

  1. Affine1は、28x28の画像をバッチサイズ分だけ並べて、 (28\times 28) のサイズの重み行列  W0 と掛け合わせた上で、重みベクトル  b0 のバイアスを加算する。
  2. ReLUはデータの形は変えずに、全ての要素に対してReLU関数を適用する。
  3. Affine2も同様。ReLUの出力行列に対して重み行列  W1 の行列積を求めたうえで、重みベクトル  b1 のバイアスを加算する。
  4. Softmaxもデータの形を変えずに、すべての要素に対してSoftMax関数を適用する。

ここまでで、バッチサイズ分のMNISTの処理が完了する。この結果に対して、最終的な教師データとの差分を計算し、誤差を逆方向に伝搬させていく。 具体的には、上記の式で出てきた  W0, b0, W1, b1 の勾配を計算して、元の重み行列を修正していくことになる。

MNIST誤差逆伝搬のデータパス

MNISTの誤差逆伝搬のためには、以下の4つの処理を実行する。上記のMNISTデータパスを逆方向にたどっていく。

  1. Softmaxの逆伝搬
  2. Affine2の逆伝搬
  3. ReLUの逆伝搬
  4. Affine1の逆伝搬

とする。例としてAffine1の誤差逆伝搬についてここでは取り上げる。

f:id:msyksphinz:20170630022526p:plain

基本的な考え方としては、前のニューロンから生成された入力値の差分、前の順方向ネットワークの計算結果を利用して、現在のニューロンに設定されている重みを更新し、ひとつ前のニューロンへ伝える誤差値を計算する。それぞれ、

として、Pythonのコードを参考にしながらC言語で実装すると以下のようになる。

github.com

double affine_backward (const int output_size,
                        const int hidden_size,
                        const int batch_size,
                        double dx[batch_size][hidden_size],
                        double db[output_size],
                        double dw[hidden_size][output_size],
                        const double dout[batch_size][output_size],
                        const double w[hidden_size][output_size],
                        const double x[batch_size][hidden_size])
{
  for (int b = 0; b < batch_size; b++) {
    for (int y = 0; y < hidden_size; y++) {
      dx[b][y] = 0.0;
      for (int x = 0;x < output_size; x++) {
        dx[b][y] += (dout[b][x] * w[y][x]);  // w is Transpose
      }
    }
  }
  for (int h = 0; h < hidden_size; h++) {
    for (int o = 0; o < output_size; o++) {
      for (int b = 0; b < batch_size; b++) {
        dw[h][o] += (x[b][h] * dout[b][o]);
      }
    }
  }

  for (int o = 0; o < output_size; o++) {
    db[o] = 0.0;
    for (int b = 0; b < batch_size; b++) {
      db[o] += dout[b][o];
    }
  }
}

これで、とりあえず一回分の学習について、Pythonのコードと同様の結果が得られるようになった。次に、この学習処理を何度も実行してMNISTの学習処理を進めていく。

ニューラルネットワーク C言語での実装 (5. バッチサイズの導入)

ニューラルネットワークを1からC言語で記述してMNISTを動作させるプロジェクト、次はバッチサイズを導入して一度に複数の画像を処理できるように拡張する。

バッチサイズを導入する前、つまり機能の時点でのネットワークの構成は、以下のようになっていた。一つの画像データに対して行列演算を実行し、答えを出す。

f:id:msyksphinz:20170629005855p:plain

これをバッチ処理を導入することにより、以下のように拡張する。

f:id:msyksphinz:20170628012908p:plain

この場合は、10000個のデータを一つの処理の中で実行している。このように一度に複数のデータに対して行列演算を実行することで、ネットワークの高速化を図ることができる。

このようにしてバッチサイズを導入し、MNISTを実装し直した。

github.com

データをファイルから取得するgetdata()は、バッチサイズ分だけ拡張してある。ここではdefine BATCHSIZE (100)としてあり、100個のデータを取得して一度にネットワークに流すようになっている。

int getdata (int fd_image, int fd_label, double in_data[BATCH_SIZE][INPUTNO], double *ans)
{
  uint8_t image[INPUTNO];
  for (int b = 0; b < BATCH_SIZE; b++) {
    read (fd_image, image, INPUTNO * sizeof(unsigned char));
    for (int j = 0; j < INPUTNO; j++) {
      in_data[b][j] = (double)image[j] / 256.0;
    }
    uint8_t label;
    read (fd_label, &label, sizeof(uint8_t));
    ans[b] = (double)label;
  }
}

これで、認識精度を落とさずにバッチサイズを導入したニューラルネットワークを構成できた。

さて、次は学習フェーズの実装だ。誤差逆伝搬法の実装を行っていこう。これも、Pythonから初期値データをもらいながら、検算して実装していく。