コンテンツにスキップするには Enter キーを押してください

chainerで作製したモデルをc++で読み込む。畳込み層利用

はじめに

chainerでは学習したモデルを吐き出して、また読み込んで予測なり追加の学習することが出来ます。
その吐き出したモデルファイルをC++で読み込んで予測をしてみようと思います。
参考にしたサイト(Chainerで学習したモデルをC++で読み込む)では全結合層のみの実装だったのですが、筆者は畳込み層を利用しているので、ここではCNNの実装についてに重きを置いて書こうと思います。
基本方針は参照サイトに則っています。
なので一回そのサイトを見てきてからのほうがわかりやすいかもしれません。




モデルをC++で読み込むために保存しなおす

chainerのsave_npzで保存されたmodelファイルはzip形式で保存されています。
なのでzipinfoで情報を見ることが出来ます。

$ zipinfo my_s4_e100.model                                                                     [±master ●●]
Archive:  my_s4_e100.model   114377968 bytes   12 files
-rw-------  2.0 unx     1104 b- defN  8-Nov-16 00:23 predictor/conv3/b.npy
-rw-------  2.0 unx      592 b- defN  8-Nov-16 00:23 predictor/conv2/b.npy
-rw-------  2.0 unx     4176 b- defN  8-Nov-16 00:23 predictor/fc5/b.npy
-rw-------  2.0 unx      336 b- defN  8-Nov-16 00:23 predictor/conv1/b.npy
-rw-------  2.0 unx   294992 b- defN  8-Nov-16 00:23 predictor/conv2/W.npy
-rw-------  2.0 unx 104857680 b- defN  8-Nov-16 00:23 predictor/fc4/W.npy
-rw-------  2.0 unx  1179744 b- defN  8-Nov-16 00:23 predictor/conv3/W.npy
-rw-------  2.0 unx   143440 b- defN  8-Nov-16 00:23 predictor/fc6/W.npy
-rw-------  2.0 unx      220 b- defN  8-Nov-16 00:23 predictor/fc6/b.npy
-rw-------  2.0 unx    16464 b- defN  8-Nov-16 00:23 predictor/fc4/b.npy
-rw-------  2.0 unx 16777296 b- defN  8-Nov-16 00:23 predictor/fc5/W.npy
-rw-------  2.0 unx     2384 b- defN  8-Nov-16 00:23 predictor/conv1/W.npy
12 files, 123278428 bytes uncompressed, 114376554 bytes compressed:  7.2%

これをc++で読み込むためにバイナリ形式にして保存しなおします。
今回利用するモデルはある画像をいれたら35通りの結果を出力するものです。

model.predictor.conv1.W.data.reshapeで4次元のフィルタ情報(フィルタ数,チャンネル数,y,x)を一次元にします。
それをfor in構文でバイナリ化していきます。
最後にopen(“and.dat”,”w”).write(d)で保存します。
重用なコードをまとめると

#モデルを読み込んで
chainer.serializers.load_npz('./my_s4_e100.model', model)
#バイナリ形式
d = bytearray()
#convのフィルタとバイアスをdに付け加えていく
for v in model.predictor.conv.W.data.reshape():
    d += struct.pack('f',v)
for v in model.predictor.fc.b.data:
    d += struct.pack('f',v)
#バイナリ化された学習したデータを保存する
open("and.dat","w").write(d)

convの部分が参考サイトに無かった部分です。
コードの全文はgithubに置いときます。
ただし、write(d)の箇所に関してはpython2系で実行することをおすすめします。
3系だとうまくいかないで別にいろいろ書かなきゃいけないそうなので
筆者の環境ではpyenvを使っているので、exprot_cnn.pyがあるフォルダのみpython2.7.9を使うようにして対処しました。

これを実行すると

$ python export_cnn.py                                                                         [±master ●●]
64 1 3 3 64
128 64 3 3 128
256 128 3 3 256
4096 6400 4096
1024 4096 1024
35 1024 35
open  ./input_data.txt

0 [[[ 0.17204301  0.18768328  0.19159335  0.17986315  0.18768328]
  [ 0.16813295  0.17204301  0.17595308  0.17986315  0.17986315]
  [ 0.16422288  0.1603128   0.16422288  0.18377322  0.17986315]
  [ 0.19159335  0.17595308  0.19159335  0.19159335  0.19550343]
  [ 0.26588467  0.2737048   0.28543499  0.31671554  0.33235583]]]
  --- 10 ---
   [ 11.98847389  -0.29122806   1.88734341   9.93099976 -14.8091383
  -1.41204703   5.53009319   3.38871956  12.86570263  25.51378632
  35.32830811  26.81847763  13.20487499  17.91465378   1.49911404
   4.16572952  -7.55884075  -0.18048927  -8.46850109  -3.79065585
 -21.80434608  -0.58537745 -14.3086586   -5.02628946  -0.75923669
 -10.5741291   -1.18469274 -17.06903648 -12.13731384 -11.85645866
 -13.1838131  -15.95362568  -0.03898798   3.13622522 -11.50479794]

1 [[[ 0.18768328  0.18768328  0.18377322  0.17986315  0.19159335]
  [ 0.17986315  0.17595308  0.18768328  0.18768328  0.19159335]
  [ 0.17986315  0.18768328  0.20332356  0.18768328  0.19550343]
  [ 0.19550343  0.20723362  0.21505377  0.2111437   0.20723362]
  [ 0.33235583  0.33235583  0.31671554  0.28543499  0.26979473]]]
  --- 13 ---
   [ 18.32457352   3.70922542 -20.77893448  -6.46802998 -16.2413578
 -12.42859364 -10.80910015 -14.11770821  13.33351994  12.4656086
  25.0657444   39.00862122  45.26373672  53.36868668   4.40824699
   5.14150524   5.00128317   7.08090973  -9.38079071  -9.25302696
 -39.44549179  -1.54959571  -4.07606125  -8.5797205  -10.6527586
  -1.38832951  10.76959515  -0.6677748   -4.90247679 -11.57387829
  -7.86254835 -21.02538109  -7.46027374   0.35013577 -26.41744041]
...
...
...

このようになるのですが、上から順に
第一層(畳込み層) フィルタ数 入力チャンネル数 フィルタサイズ フィルタサイズ バイアス数
第二層(畳込み層) フィルタ数 入力チャンネル数 フィルタサイズ フィルタサイズ バイアス数
第三層(畳込み層) フィルタ数 入力チャンネル数 フィルタサイズ フィルタサイズ バイアス数
第四層(全結合層) 出力数 入力数 バイアス数
第五層(全結合層) 出力数 入力数 バイアス数
第六層(全結合層) 出力数 入力数 バイアス数
open ファイル名
予測番号 入力データ
—予測結果—
35通りそれぞれの予測結果

予測番号はただのデータのインデックスなので気にしなくていいです。

C++で読み込んで実装

畳込みを実際にする箇所以外はほぼ参考と同じだと思います。

     // paddingを考慮した入力を作る: 端は0
        std::vector< vf > tempX1( x.size() ,vf((in_size+n_padding*2)*(in_size+n_padding*2), 0));

        // 出力のベクトル
        std::vector< vf > y ( n_out_filter_num, vf( out_size*out_size ) );
        // フィルタごとのfor
        for (int fil_i=0; fil_i<n_out_filter_num; fil_i++) {
            // 出力画像のfor
            for (int in_y=n_padding; in_y<in_size+n_padding*2-1; in_y+=n_stride) {
                for (int in_x=n_padding; in_x<in_size+n_padding*2-1; in_x+=n_stride) {
                    y[fil_i][(in_y-n_padding)*out_size+(in_x-n_padding)] = 0;
                    // チャンネルのfor
                    for (int c=0; c<n_in_channels; c++) {
                        // フィルタ内のfor:畳込み
                        for (int k=0; k<n_filter_size; k++) {
                            for (int l=0; l<n_filter_size; l++) {
                                float pixVal = tempX1[(in_y+k-half_of_filsize)*(in_size+n_padding*2)+(in_x+l-half_of_filsize)];
                                float filVal = C[fil_i*(n_in_channels*n_filter_size*n_filter_size) +
                                                c*(n_filter_size*n_filter_size) +
                                                k*(n_filter_size) +
                                                 l];
                                y[fil_i][(in_y-n_padding)*out_size+(in_x-n_padding)] += pixVal * filVal;
                            }
                        }
                    }
                    // バイアスを足す
                    y[fil_i][(in_y-n_padding)*out_size+(in_x-n_padding)] += b[fil_i];
                }
            }
        }

畳込み層ではフィルタごとにバイアスを足すのでチャンネル毎のforが終わった後に足します。
書いてて思ったのは、0で初期化するかわりにバイアスで初期化してもいいのかなとも。

これのコードの全文もgithubに置いておきます。

実行

$ g++ import_cnn.cpp

$ ./a.out                                                                                      [±master ●●]
set convolution
read ifs
set linear
prediction

0 [0.172043 0.187683 0.191593, ... ,0.285435 0.316716 0.332356]
  ---10---
11.9885 -0.291233 1.88735 9.931 -14.8092 -1.41204 5.53011 3.38873 12.8657 25.5138 35.3283 26.8185 13.2049 17.9146 1.49911 4.16573 -7.55884 -0.18049 -8.4685 -3.79066 -21.8043 -0.585367 -14.3087 -5.02629 -0.759238 -10.5741 -1.18469 -17.069 -12.1373 -11.8565 -13.1838 -15.9536 -0.038984 3.13623 -11.5048

1 [0.187683 0.187683 0.183773, ... ,0.316716 0.285435 0.269795]
  ---13---
18.3246 3.70923 -20.7789 -6.46805 -16.2414 -12.4286 -10.8091 -14.1177 13.3335 12.4656 25.0657 39.0086 45.2637 53.3687 4.40825 5.14152 5.00128 7.0809 -9.38079 -9.25301 -39.4455 -1.5496 -4.07606 -8.57972 -10.6528 -1.38833 10.7696 -0.667782 -4.90249 -11.5739 -7.86255 -21.0254 -7.46028 0.350141 -26.4174
...
...
...

pythonで実行したものと値が同じなのでうまく動いています。
ただ、今回の実装にはpooling層は利用していません。
利用する際にはまた別途コードを付け加える必要があります。
少し雑になってしまっていまいましたが、時間が少し出来たときにでももうちょっと詳細に書こうと思います。

 

コード

コード全文




参考

Chainerで学習したモデルをC++で読み込む

 

 

コメントする

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です