• このエントリーをはてなブックマークに追加

今なら、継続入会で月額会員費が1ヶ月分無料!

記事 5件
  • 【機械学習】ChainerでGoogLeNetから画像をファインチューニングでお手軽学習【わずか22行で】

    2016-09-10 16:47  
     みんな深層学習してるかな? さて、GoogLeNetとか、みんな好きかな?オレはわりと好き。 なにしろプリトレインドモデルがあるからお手軽なんだよね。そこが好き。 あと、なにしろalexnetはデカすぎる。 なんでこんなにデカイんだ、という感じ。まあ1GBくらいなんだけど、Raspberry Piだと普通に読み込めないほど巨大なのよね。 しかし自分で作ったニューラル・ネットワークにImageNetのILSVRC2012を学習させようとするとやばいことになる。なんということでしょう。この圧倒的絶望感SSDonlyでこのスピードですからね。9日経っても1エポックすら進んでいないとは一説によると、GoogleのGPU(TPU?)ファームは3万GPU(Maxwell世代だと合計約1億2千万コア)あるらしく、1000GPU(300万コア)でILSVRC2012の学習に1日かかるらしい。火を噴くオレ
  • 【機械学習】Chainer vs C++ AVX最終決戦

    2016-09-01 07:55  
    100pt
     さて 前回の結果に気を良くした僕は、調子に乗って5層オートエンコーダの高速化を試みるのである。 といっても、前回作ったプログラムがほぼ流用できるので、予め学習させたpklファイルをヘッダファイルに書き出すプログラムを流用して5層オートエンコーダをC++の配列として出力する。
    def dumpArray(ar):
        for i in range(len(ar)):
            e = ar[i]
            if isinstance(e,np.ndarray):
                print "{",
                dumpArray(e)
                print "}",
            else:
                print ("{}".format(e)),
            if i < len(ar)-1:
                print ","
    def dumpLayer(name,l):
        print "double ",
        print "{}_W".format(name),
        for d in l.W.data.shape:
            print "[{}]".format(d),
        print "={"
        dumpArray(l.W.data)
        print "};"
        print "double ",
        print "{}_b".format(name),
        for d in l.b.data.shape:
            print "[{}]".format(d),
        print "={"
        dumpArray(l.b.data)
        print "};"
    print "double in[]={"
    dumpArray(x_train[0])
    print "};"
    dumpLayer("l1",model.l1)
    dumpLayer("l2",model.l2)
    dumpLayer("l3",model.l3)
    dumpLayer("l4",model.l4)
    dumpLayer("l5",model.l5)


     こいつで得られたlayers.hをC++プログラムから読み込んで前回と同じようなことをするわけだ。 今回も余裕だろうと思って試すと
    $ python bench_ae_chainer.py 
    start
    Chainer:0.223642sec
    $ ./hoge 
    0.129455 sec
    31.683862 


     どっどっどっ どういうことだってばよ!? 前回は500倍くらいのスピードだったのに今回は2倍程度のスピードになってしまっている。 いろいろ考えると、まあ結論はひとつしかない。 要するに、第一層がデカすぎるのである。 前回の第一層は1,407バイト、つまり1キロバイト強しかないのに対し、今回の第一層は3,513,339バイト、つまり3.5メガバイトもある。 コンピュータの世界ではキャッシュに乗るか乗らないかというのが非常に重要だ。 最新世代のCore i7であっても、L1キャッシュは32KB、L2キャッシュでも256KB、L3キャッシュになってようやく、4〜8MBになる。 1.4キロバイトのデータは全てL1キャッシュに乗るが3.5メガバイトはL3キャッシュにやっと乗るかどうか、というところだ。 つまり前回の戦いは単にネイティブコードとPythonの戦いではなく、キャッシュの戦いでもあったわけだ。 まあ普通に考えて同じコンピュータで同じアルゴリズムを走らせてそんな数百倍の違いになることのほうがどうかしてるのだから、こういうからくりでまあ間違いないだろう。 さて、問題はそんなことではない。 この、キャッシュが全然効かない状況でどうやって処理速度を稼ぐのか。 C++のネイティブコードがPythonの二倍程度の速度でしかないなんていうことは常識的にあり得ないのである。 もしかするとnumpyの中でSIMD命令を使っているのではないか? そう思って調べてみると案の定、numpyの内部ではSIMD命令を使って高速化されていた。 SIMDとは、Single Instruction Multiple Data streamの略で、要するに単一の命令で複数のデータを一括処理しちゃおうという思想である。 むかしはSIMD命令を使うとすると全てインラインアセンブリでやっていたが、今はC++からでもイントリンジックという愉快な仕組みを導入すると好き放題使えるらしい。 僕のMacBookAirはAVX2までをサポートしているらしいので、最大256ビットのSIMDレジスタを使うことができる。 256ビットのSIMDレジスタは、32ビット単精度を8つか、64ビット倍精度を4つ同時に処理することができる。 やってやろうじゃん。 ということで元はこんな感じになっていたコードを
    Vector *apply(Vector *v){
    int offset=0;
    for(int j=0;j<h;j++){
    double acc=0.0;
    offset+=w;
    for(int i=0;i<w;i++){
    acc += m[offset+i] * (v->v[i]);
    }
    r->v[j]=acc;
    }
    return r;
    }

     こんな感じに書き換えてみた。
    Vector *apply(Vector *v){
    int offset=0;
    for(int j=0;j<h;j++){
    double acc=0.0;
    offset+=w;
    __m256d M,V,Tmp,Acc; 
    double *pm = m+offset;
    double *pv = v->v;
    Acc = _mm256_setzero_pd();
    int end = (w/4)*4;
    int i;
    for(i=0;i<end;i+=4){
    M = _mm256_load_pd(pm);
    V = _mm256_load_pd(pv);
    Tmp=_mm256_mul_pd(M,V);
    Acc=_mm256_add_pd(Tmp,Acc);
    pm+=4;
    pv+=4;
    }
    acc = Acc[0]+Acc[1]+Acc[2]+Acc[3];
    for(;i<w;i++){
    acc += *pm * *pv;
    pm++;
    pv++;
    }
    r->v[j]=acc;
    }
    return r;
    }

     地味に配列参照ではなくポインタにするなど地道な努力もかいま見える。
     SIMD命令を使う場合は複数のデータを一括で渡さなければならないのでポインタ演算の方が渡すのに都合がいい。 __m256dという型を宣言するとこれはAVXレジスタに割り当てられる。 今のAVXはレジスタが8本あるらしいのでまあさらに病的な改造を加えればもうちょっと速くなるのかもしれないが汎用処理で回してるであろうnumpyごときと戦うのにはこの程度で充分だろう(いやどうかな行列とベクトルの乗算だからもっと特化した計算になっていても不思議はないが)。

    $ ./avx
    0.075887 sec
    31.951800 

      ふむ。これでまあなんとかC++の面目が保てたか。 おっと忘れてた。コンパイラ先生にも少し頑張ってもらおうか。-O2発動!



    $ ./avx 
    0.022472 sec
    31.683862 


     来ましたね。 さらに、単精度にして8つ同時に計算したらどうなるか。


    $ ./avx 
    0.017531 sec
    0.000000 


     でましたね。 5回繰り返して平均を取った数字でグラフ化するとこんな感じ まあなんですか。10倍程度高速っていう感じになりました。 いやー久しぶりにCPUの中身いじった清々しさがあるなあ。まったく。 ニューラル・ネットワークの世界にやってきても、最適化野郎の出番はあるぜ、という美しい結論で終わっておきましょうか。 
  • 【機械学習】Chainerで作ったニューラルネットをC++に移植してベンチマークしてみる

    2016-08-29 20:40  
    100pt
     マシン語 それは男のロマンである。 異論は認めん。 CPUの能力を限界まで引き出すコード。 そう、マシンたちの歓喜の声が聞こえてくる。 もっとオレを回してくれ! マイクロコードの1クロックまで! レジスタの限界まで オレを、オレをもっと愛してくれ! コアのひとつひとつ、パイプラインの全てのステージをコードで満たしてくれ! オレはもっと回りたい。 もっと回したいんだ・・・ ・・・そうだ、いい子だ。 それがマシン語である。 マシン語に比べると最近はPythonだのRubyだの適当な擬似言語で満足する輩が多すぎる。 軟弱者め!!! とまあ意気込んでは見たものの・・・いきなりマシン語はつらたんなのでとりあえずネイティブコードで動かしてみることを今回の記事では目標にする。 題材はいつものアレ 9-8-8-6の中間層二層のニューラル・ネットワーク。 こいつをC++に移植してみる。 ウヒョー、ワクワクするぜっ! まず、C++の文法を思い出すところからはじめなければならない。 いやー、忘れたよ。 最初はJSONで出力してJSONで扱おうかと思ったのだが、ネットワークが充分小さいのでヘッダファイルにコンバートすればいいか、ということでL1とL2のW(重み)とb(バイアス)をlayers.hに出力してみる。
    double  l1_W [8] [9] ={
    { 0.367395937443 ,
    0.620269238949 ,
    -0.0110975131392 ,
    0.0458976365626 ,
    -2.03605866432 ,

     こんな感じ。 まず、C++の多次元配列の作り方からして忘れてたからね。 ずっとエラーが出てて「なんで?」と思ったら、配列の初期化を[]でやってたからだった。 どんだけ離れてたんだよ。 まあ慣れてくれば昔とった杵付。 なんとかなりまさあ とりあえずデータをC言語で扱おうと思ったのだが・・・Cはつらすぎる。 だめだ、おれのオブジェクトが思考を始めてしまった。 オーバーヘッドは爆増するが、ここはひとつクラスを作らせてつかぁさい!
    class Vector{
    public:
    double *v;
    int size;
    Vector(double *_v,int _size){
    v = _v;
    size = _size;
    v = new double[_size];
    for(int i=0;i<_size;i++){
    v[i]=_v[i];
    }
    };
    Vector(int _size){
    size = _size;
    v = new double[_size];
    };
    Vector *add(Vector *_v){
    for(int i=0;i<size;i++){
    v[i]+=_v->v[i];
    }
    return this;
    };
    Vector *sigmoid(){
    for(int i=0;i<size;i++){
    v[i]=_sigmoid(v[i]);
    }
    return this;
    };
    void dump(){
    printf("dim:%d\n",size);
    for(int i=0;i<size;i++){
    printf("%f,",v[i]);
    }
    puts("");
    }
    };

     テンソルのクラスを作るのはつらそうだったので軟弱なオレはもうベクトルのクラスと行列のクラスだけでなんとかすることにする。いんだよ細けぇことは。畳込みNNだってテンソルっつうても画像がたくさんあるだけだからな。学習しないでインファレンス側で使うだけならこれでいんだよ別に。 しかしクラスの定義とか参照とかポインタとか盛大に忘れていて焦った。 やはりプログラミング言語が進化するというのは大きな理由と意義がある。 「.」と「->」の使い分けとか、本当に無駄すぎる。まあしかし、マシン語を意識する場合はこれがわかんないと話しになんないわけだけどな。 思い出しながら作ったのでnewとかが盛大に無駄こいてる。 本来はパフォーマンスを再優先するならばせめてオブジェクトプールを使うべきだ。 まあSTLとか使うという手もあるだろう。 しかし、コンストラクタのオーバーロードとか作ったの何年ぶりだろう。 だがしかし、STLまで思い出すのは辛すぎるので今回はいいや。まあこれでパフォーマンスがうまく上がんなかったら別の手を考えるベエ。
    class Matrix{
    public:
    double *m;
    int w,h;
    Matrix(double *_m,int _h,int _w){
    int i,j;
    h=_h;
    w=_w;
    m = new double[w*h];
    for(j=0;j<h;j++){
    for(i=0;i<w;i++){
    m[j*w+i] = _m[j*w+i];
    }
    }
    };
    Vector *apply(Vector *v){
    Vector *r = new Vector(h); 
    for(int j=0;j<h;j++){
    double acc=0.0;
    for(int i=0;i<w;i++){
    acc += m[j*w+i] * (v->v[i]);
    }
    r->v[j]=acc;
    }
    return r;
    }
    };

     男の行列クラス! もう男らしい。行列に対してベクトルを乗算する機能しかない! 乗算する度にnewしてるとか昔の自分なら絶対許さないコードだが、いんだよ細けぇことは後で考えれば。 これでとりあえず下準備完了。 次にフォワード(順伝播)を作る。 シグモイド使うやつと使わない奴
    Vector *forward(Vector *x,Matrix *W,Vector *b){
    Vector *a = W->apply(x); 
    a->add(b);
    return a;
    }
    Vector *forwardSigmoid(Vector *x,Matrix *W,Vector *b){
    Vector *a = W->apply(x); 
    a->add(b);
    a->sigmoid();
    return a;
    }

     ちなみにC++の場合、シグモイドを自前で実装するとこうなる。
    double _sigmoid(double x) {
        return 1.0 / (1.0 + exp(-1.0 * x));
    }


     さあこれで準備完了。 いくぜ男のメイン関数
    int main(){
    /*テスト用データ*/
    double in[] = {0.1083984375,0.213452398777,0.774722874165,0.110441893339,0.212938994169,0.780655622482,
    0.112617187202,0.212380990386,0.785996675491};
    Vector *inV = new Vector(in,9);
    Matrix *l1W = new Matrix((double*)l1_W,8,9);
    Vector *l1b = new Vector(l1_b,8);
    Matrix *l2W = new Matrix((double*)l2_W,6,8);
    Vector *l2b = new Vector(l2_b,6);
    Vector *h;
    clock_t start=clock();
    double dummy=0.0;
    for(int i=0;i<1000;i++){
    h=forwardSigmoid(inV,l1W,l1b);
    h=forward(h,l2W,l2b);
    dummy+=h->v[0];
    }
    clock_t end=clock();
    double t=(end-start)/(double)CLOCKS_PER_SEC;
    printf("%f sec\n",t);
    printf("%f \n",dummy);
    h->dump();

     テストデータは前回Pythonの整数版の精度検証に使ったのと同じものを使用。 これが同じになればちゃんと動いてると推定できる。 1000回ループ回してるところでなぞの変数dummyを足している理由は、計算結果を利用しないとコンパイラが万が一炎の最適化をかけてしまったらベンチマークにならないからだ(表示してるのも同様の理由)。 さあ、実行してみよう。ちなみにPython版では最も高速だったFloat64で1000回ループに22ミリ秒掛かったんだよねー。 レッツゴー! ・・・? 1.6ミリ秒・・・だ・・・と・・・!!!!!?!??!?!?!?!?!?! 念のためもう一回実行してみる。  1ミリ秒 おいおい。 ということは、Python版の1/200の実行時間で終わってしまうということか?どういうことだってばよ。 今回の速度比較はこうなった。 これはなんの冗談なんだろう。本当に計算してんのかな?と思って、試しに浮動小数点の足し算だけのループをまわしてみると確かに高速化しているのでまあ何らかの計算はしているのだろう。試しにPythonのfloat64版と同じくらいの実行時間になるにはループがどのくらいあればいいのか調べてみた。これでもPython/float64よりはだいぶ高速だが、まあこの結果になるのにどのくらいのループをしたかというと
    for(int i=0;i<200000;i++){
    h=forwardSigmoid(inV,l1W,l1b);
    h=forward(h,l2W,l2b);
    dummy+=h->v[0];
    }


    20万回でした。つまり、どうやらC++で書き直すだけでCPUでの実行時間において、全く同じニューラル・ネットワークでPython版の200倍高速に、Chainerの500倍高速に計算できることがわかりました。まあ畳込み絡んできたりGPU絡んできたりすれば状況は違うんだろうけど。やはりChainerは便利で簡単に書ける分、代償も少なくないんだろうなあ。 
  • 【機械学習】Chainerで作った全結合ニューラル・ネットワークを整数化してみる

    2016-08-22 07:22  
    100pt
     整数化 それは男のロマンである。 異論は認めん。 Chainerは使いやすいがいかんせん動作が遅いという問題がある。 これはMacなどで使う場合にはそれほど気にならないし、究極的にはGPU搭載マシンで使うわけだからそこが速ければ問題ないのだが、まあ実際問題GPU搭載マシンでも残念ながら他のフレームワークの方が速かったりすることはままある。 とはいえとはいえ、Chainerに任せっきりの全結合ネットワークを自分で実装しなおしてみることはそう悪いことではない。 それに、Chainerで学習させた全結合ネットワークを他のプラットフォーム、たとえば組込み機器用の非力なプロセッサとかまあその手のものに実装したり、最終的にはC言語にしたりするときに、内部構造を知っておくのは悪くないアイデアだろう。 そしてGoogleのTPU、つまりTensor Processing Unitがなぜ高速なのかということも、整数化を通して知ることができるかもしれない。 そこで前回作ったChainerの全結合ネットワークをひとまずPython上で整数化してみることにする。 前回作ったChainerの全結合ネットワークとは具体的には下図のような構造である。 これは筆跡の予測をするためのニューラル・ネットワークである。 入出力あわせて四層しかないのでディープニューラル・ネットワークではないが、積層オートエンコーダも原理的には全く同じ仕組で動くはずである。 単純なネットワークのほうが扱いやすいのでこういう形にした。 ただし、回帰問題を解くネットワークなので精度に関しては畳込みニューラル・ネットワークが扱う分類問題よりはかなりシビアだ。 Chainerでの定義はこんな感じになる。
    n_units=8
    class Model(Chain):
        def __init__(self):
            super(Model, self).__init__(
                l1=L.Linear(9, n_units),
                l2=L.Linear(n_units, 6),
                )
        def __call__(self, x,layer=0):
            h = self.l1(x)
            h = F.sigmoid(h)
            return self.l2(h)
        def dump(self):
            pickle.dump(self.l1,open('l1.pkl', 'w'))
            pickle.dump(self.l2,open('l2.pkl', 'w'))
        def load(self):
            self.l1=pickle.load(open('l1.pkl', 'r'))
            self.l2=pickle.load(open('l2.pkl', 'r'))

     ご覧のように、l1とl2という2つの全結合層(Linear)がある。 予め学習させたデータを見てみよう
    >>> model = Model()
    >>> model.load()
    >>> model.l1.W.data.shape
    (8, 9)
    >>> model.l1.b.data.shape
    (8,)
    >>> model.l2.W.data.shape
    (6, 8)
    >>> model.l2.b.data.shape
    (6,) 
     実のところニューラル・ネットワークの正体というのは単なる配列である。 全結合層の場合は、W(重み)とb(バイアス)の2つが必要になる。 Wが8x9の2次元配列になっているのは、まずL1(第一層)が8次元のためだ。 9次元の入力に対して、それぞれの入力にW(重み)を乗じて積分していく。 最後にバイアスであるbを足して、それから活性化関数(この場合はsigmoid)を通して出力する。 それを8つのニューロンに対して繰り返す。 バイアスは8つのニューロンに対してそれぞれひとつずつ。だからL1.b.data.shapeは(8,)になる。 そういう処理をChainerを使わずに書いてみると下記のようになる。
    def forward(layer,x,activation):
        r=[]
        for W,b in zip(layer.W.data,layer.b.data):
            accum=0.0
            for w,h in zip(W,x):
                accum += w*h
            if activation:
                r.append(sigmoid(accum+b))
            else:
                r.append(accum+b)
        return r
     h = forward(model.l1,x,True)
     y = forward(model.l2,h,False)

     活性化関数を使う場合と使わない場合があるので、やや泥臭いコードになっているが、実際に実行してみるとこれで上手くいってることが分かる。 比較のためChainerでも同じ計算をして表示すると
    Chainer:[ 0.1148746   0.21153805  0.78971589  0.11715603  0.21138623  0.79293698]Float64:[0.11487447682804452, 0.21153808168055044, 0.78971603399232249, 0.11715607325794686, 0.211386418677598, 0.79293766352923889]


     という感じになり、ちゃんと倍精度(64ビット浮動小数点精度)で同じ結果が出てることが分かる。 ちなみに今のMacやWindowsのCPUは、そもそもが64ビットCPUのため、実はChainerの32ビット浮動小数点演算よりも64ビット浮動小数点演算の方が高速である。あとでベンチマークを見せるが、だいたい2倍くらい64ビットの方が速い。 これを整数化するわけである。 整数は計算負荷が低いので一般的に組込み機などでは有利なはずである。 今回は手軽な整数化として固定小数点化を行った。 浮動小数点演算が難しいのは小数点が文字通り浮動するからであり、小数点が固定されていれば、それは整数と同じに見做せるというおなじみの手法で、筆者が小学生の頃から多用した古典的な高速化テクニックのひとつである。というか当時のコンピュータでは浮動小数点演算は非常にコストが高く、どんな計算でも高速に処理したければ固定小数点化する必要があった。 また、初代プレイステーションの積和演算ハードウェアも、実体は固定小数点の整数演算専用のDSPだった。 さきほどの処理を整数化(固定小数点化)するとこうなる。
    base=2**16 #固定小数点をどこに置くか
    for i in range(len(y)):
        y[i] = int(_y[i]*base)
    print "goal",y
    for i in range(len(_x)):
        x[i] = int(_x[i]*base)
    def sigmoidInt64(z):
        z = float(z)/base
        return int(1/(1+math.e**(-z))*base)
    def forwardInt64(layer,x,activation):
        r=[]
        for W,b in zip(layer.W.data,layer.b.data):
            b = int(b*base)
            accum=0
            for w,h in zip(W,x):
                w = int(w*base)
                accum += int(w*h)/base
            if activation:
                r.append(sigmoidInt64(accum+b))
            else:
                r.append(accum+b)
        return r
    h = forwardInt64(model.l1,x,True)
    y = forwardInt64(model.l2,h,False)
    print "result",y

     固定小数点というからには小数点をどこかに置かなければならない。 今回の場合、64ビットコンピュータでやるのでまずは64ビット整数で固定小数点を置くことにする。固定小数点はひとまず16ビットとした。ややオーバーキル気味の精度ではある。 実行結果を比較するために、浮動小数点演算の結果得られた数値を参考までに同じ精度で整数化して表示する。
    goal [7528, 13863, 51754, 7677, 13853, 51965]
    result [7518, 13856, 51747, 7675, 13839, 51959]

     goalが、浮動小数点演算の結果を整数化したもので、resultが16ビット固定小数点精度で64ビット演算を行った結果である。 これを見るとだいたいあってるけど下二桁が違うということがわかる。 念のため、32ビット固定小数点精度でやってみる
    goal [493382121, 908549142, 3391804539, 503181503, 907897755, 3405641332]
    result [493382110L, 908549140L, 3391804532L, 503181497L, 907897749L, 3405641328L]

     こうするとかなりの精度が出ることがわかる。 ただIntelの64ビットCPUで整数演算するメリットはあまりない。計算速度は浮動小数点演算の1.5倍くらい遅いのだ。 RaspberryPi3は64ビットCPUだが、浮動小数点演算とどっちが速いか実際にベンチマークしないとわからないので、試しにRaspberryPi2やRaspberryPiZeroで動かすことを想定して32ビット固定小数点演算をやってみる。
    base=2**16
    for i in range(len(y)):
        y[i] = np.int32(_y[i]*base)
    print "goal",y
    for i in range(len(_x)):
        x[i] = np.int32(_x[i]*base)
    def sigmoidInt(z):
        z = float(z)/base
        return np.int32(1/(1+math.e**(-z))*base)
    def forwardInt(layer,x,activation):
        r=[]
        for W,b in zip(layer.W.data,layer.b.data):
            b = np.int32(b*base)
            accum=0
            for w,h in zip(W,x):
                w = np.int64(w*base)
                accum += np.int64(w*h)/base
            if activation:
                r.append(sigmoidInt(accum+b))
            else:
                r.append(accum+b)
        return r
    h = forwardInt(model.l1,x,True)
    y = forwardInt(model.l2,h,False)
    print "result",y

    今回は精度の確認が目的なので、遅いことを承知でnumpy.int32を使う。ただし、そのままではWと入力を乗じる時に精度が足りなくなるのでそこだけ64ビット整数で計算している。16ビット固定小数点で演算結果は以下のとおり。
    goal [7528, 13863, 51754, 7677, 13853, 51965]
    result [7518, 13856, 51747, 7675, 13839, 51959]

     やはり回帰問題としては厳しい。 ただ、回帰でこれだけいけるなら、分類問題なら充分な精度が出せると思う
    goal [117, 216, 808, 119, 216, 811]
    result [102, 212, 802, 110, 209, 803]

     念のため、初代プレイステーションと同じく10ビット固定小数点演算でやってみた。 この精度を許容できるかどうかは利用目的によるだろう。 これは軌跡の予測なのでドット数と考えれば、常に10ドットくらいズレる予測を許容できるかという問題になる。 全体を実行したベンチマーク結果を以下に示す。

    $ python getdata.py 
    [ 0.1148746   0.21153805  0.78971589  0.11715603  0.21138623  0.79293698]
    Chainer:0.532674sec
    [0.11487447682804452, 0.21153808168055044, 0.78971603399232249, 0.11715607325794686, 0.211386418677598, 0.79293766352923889]
    Float64:0.253355sec
    goal [493382121, 908549142, 3391804539, 503181503, 907897755, 3405641332]
    input [465567744, 916771071, 3327409408, 474344319, 914566015, 3352890367, 483687136, 912169407, 3375830015]
    result [493382110L, 908549140L, 3391804532L, 503181497L, 907897749L, 3405641328L]
    Int64:0.4142sec
    goal [117, 216, 808, 119, 216, 811]
    input [111, 218, 793, 113, 218, 799, 115, 217, 804]
    result [102, 212, 802, 110, 209, 803]
    Int32:0.522224sec

    計測はtime.clock()を用いて1000回ループで行った。とりあえず手元のMacBookAir(1.7 GHz Intel Core i7)では、64ビット浮動小数点演算はChainer(32ビット浮動小数点演算)の2倍くらい速く、64ビット整数演算は浮動小数点演算より遅く32ビット固定小数点演算より速いことがわかった。まあイマドキのCPUらしい結果である。 同じベンチマークをRaspberryPiで実行したらどうなるのか、興味の湧くところである。 
  • 【機械学習】 Chainerによる筆跡の予測

    2016-08-19 07:45  
    100pt
     さて、手書きとは何かというのを追求するのが僕のライフワークである。 そこで以前、樋口真嗣監督がenchantMOONの発表会で描いていたラクガキが発見されたので載せてみる。 懐かしい写真。 監督が暇つぶしに何を書いているのか覗いてみると・・・ すげえ上手い。 しかも怪獣を書いています。おもえばこの時は2013年。 まさにシン・ゴジラの構想を練っていた時でしょう。 このときは単に「樋口さんは本当に怪獣が好きなんだなあ」としか思わなかったのですが、まさかゴジラの構想を練っていたとはなあ。 とまあこのように、書く人によって全然違うのが手書きの面白いところです。 enchantMOONでは筆跡の予測が重要で、この予測によって見かけ上、反応速度が上がっているように見えます。ちなみにこの筆跡の予測は東芝のREGZA tabletのTruNoteにも入っていて、細かい字を書こうとする時に違いが出ます。絵を描こうとするときはむしろ予測は邪魔かも。 今回はこの筆跡データを機械学習させたらどうなるの? という話です。 幸い、手元には自分の過去三年にも及ぶ手書きメモが残っているので、筆跡データは唸るほどあります。 まずニューラル・ネットワークを設計します。 オートエンコーダを使えば深層化できるんだけど今回はそこまでいらないかなと思って非常に単純な三層パーセプトロンを作りました。だからタグも「深層学習」ではなくて「機械学習」 過去3フレームのペンの筆跡座標と筆圧の情報から将来2フレームの座標と筆圧を予測させるというニューラル・ネットワークです。 深層学習でなくても、機械学習でどのようにアルゴリズムを組むか参考になると思います。 実際のニューラル・ネットワークのモデルはこんな感じになります。 まあ説明する必要がないほど簡単ですけど。 中間層が2つの単純なニューラル・ネットワークです。 今回は分類ではなくて回帰として解くのでこのようになっています。 隠れユニット数(n_units)は8にしていますが、ここは複雑すぎなければなんでもいいかと。 これを学習させるわけですが、ここからがちょい厄介です。 まず、enchantMOONのストロークデータはstroke1.jsonなどのように格納され、しかもディスク内にバラバラに入っています。 そこでまず UNIXコマンドを実行します。sudo find / -name 'stroke*.json' > jsonlist.txt
     こうすると、ディスク内にある全てのstroke*.jsonへのパスが手にはいります。 このjsonlist.txtを舐めればいいわけです。 これで得られた座標と圧力をいきなりつっこんでみると・・・
    2773785 #学習データセット総数277万
    ('epoch:', 0, 'loss:', array(25634.8125, dtype=float32)) ('epoch:', 1, 'loss:', array(26884.3515625, dtype=float32)) ('epoch:', 2, 'loss:', array(26972.904296875, dtype=float32)) ('epoch:', 3, 'loss:', array(25882.63671875, dtype=float32)) ('epoch:', 4, 'loss:', array(26219.3046875, dtype=float32)) ('epoch:', 5, 'loss:', array(26219.53515625, dtype=float32)) ('epoch:', 6, 'loss:', array(26157.064453125, dtype=float32)) ('epoch:', 7, 'loss:', array(26268.296875, dtype=float32)) ('epoch:', 8, 'loss:', array(26588.7109375, dtype=float32)) ('epoch:', 9, 'loss:', array(26797.19921875, dtype=float32)) ('epoch:', 10, 'loss:', array(26485.3671875, dtype=float32))
    ('epoch:', 11, 'loss:', array(26378.431640625, dtype=float32))

     壮絶に失敗しました。いくらなんでもlossが2万を超えていたら収束する気がしません。 なんでこういうことが起きるかというと、まあ簡単にいえば正則化してないからです。 enchantMOONの座標は600x800で格納されています。圧力は0〜1です。 これではうまくいくものもいきません。 ニューラル・ネットワークに入力するデータとしては数値が違いすぎるのです。相関関係を測ろうにも無理です。 あるときは、オーバーフローを起こしてしまいました。 そこで入力する座標データを圧力のサイズにあわせて0〜1に縮小します。filelist = open('jsonlist.txt').readlines()
    for file in filelist:
        print file.rstrip('\n')
        data = json.loads(open(file.rstrip('\n')).read())
        for stroke in data['strokes']:
            line = []
            cnt=0
            for i in stroke['data']:
                if cnt==0:
                    line.append(i/800)
                elif cnt==1:
                    line.append(i/600)
                else:
                    line.append(i)
                    cnt=0 
     次に、予測する線分の学習データセットを作ります  
                x_set =[]
                y_set =[]
                for i in range(0,len(line)-15,3):
                    x_set=[]
                    for j in range(9):
                        x_set.append(line[i+j])
                    y_set=[]
                    for j in range(6):
                        y_set.append(line[i+9+j])
                    x_train.append(x_set)
                    y_train.append(y_set)

     これで過去3フレームから未来2フレームを予測するx_trainとy_trainができました。 ちょい泥臭いところは目をつむってください。 学習そのものは非常にシンプルにできます。
    batchsize = 10000
    x_train = np.array(x_train,dtype=np.float32)
    y_train = np.array(y_train,dtype=np.float32)
    datasize = len(x_train)
    for j in range(0,400000):
        indexes = np.random.permutation(datasize)
        for i in range(0, datasize, batchsize):
            x = Variable(x_train[indexes[i : i + batchsize]])
            t = Variable(y_train[indexes[i : i + batchsize]])
            optimizer.zero_grads()
            y = model(x,layer=0) 
            loss = F.mean_squared_error(y, t)
            loss.backward()
            optimizer.update() 
     これで学習ができます。 学習した結果はこんな感じになりました。
    ('epoch:', 0, 'loss:', array(0.03901803493499756, dtype=float32)) ('epoch:', 1, 'loss:', array(0.03184596449136734, dtype=float32)) ('epoch:', 2, 'loss:', array(0.028442196547985077, dtype=float32)) ('epoch:', 3, 'loss:', array(0.02407853491604328, dtype=float32)) ('epoch:', 4, 'loss:', array(0.02016972377896309, dtype=float32)) ('epoch:', 5, 'loss:', array(0.016892623156309128, dtype=float32)) ('epoch:', 6, 'loss:', array(0.013871355913579464, dtype=float32))

     学習させたばっかりだとこんな感じの予測 1,2,3までの入力に対して4,5の予測を出しています。 おおう。暴れすぎだ。何があった? さすがにこれでは使い物になりません。
     が、4万回くらい学習させるとlossが0.0001くらいまで下がります。 これで予測させるとこんな感じに これはわりとちゃんと予測できているのではないかと思います。