はじめに
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層は利用していません。
利用する際にはまた別途コードを付け加える必要があります。
少し雑になってしまっていまいましたが、時間が少し出来たときにでももうちょっと詳細に書こうと思います。
コード
参考