chainerでStacked denoising Autoencoder

イマドキのナウいようじょなのでもっぱらNeural networkで遊んでます.

Autoencoder(自己符号化器)の話をします.
Autoencoder、というよりNeural Net一般の問題でありますが、ネットワークの構造を深くしてゆくと逆伝搬の途中でその勾配が弱まっていってしまって, NNの前段と後段で逆伝搬される勾配が異なってくる勾配消失(Gradient vanishing)という現象が大きな問題となってきます. このへんは青い本に書いてあった気がするのですが高くて買ってないですし、あんまり詳しくないので詳しくは述べません.

 そのため深いAutoencoderを構成する際には段ごとにその前段からの入力を元に戻すように学習させ、そうして一層ずつ学習させたネットワークを積み上げる(stack)という技法が用いられ、そうして構成されたAutoencoderをStacked Autoencoderといいます.イメージとしては(面倒なので図は用意しません)
1. 入力 x -> [AE 1層目] -> [AE 1層目をひっくり返したもの] -> x となるように学習
2. 入力 x -> [AE 1層目(固定)] -> [AE 2層目] -> [AE 2層目.T] -> x
3. 入力 x -> [AE 1層目(固定)] -> [AE 2層目(固定)] -> [AE 3層目] -> [AE 3層目.T] -> x
4~n. 以下同様 といった感じです.

そんな感じの学習を行うプログラムを書いてみました.もちろんchainerで.

Stacked denoising(deep) Autoencoder 安直に小さいネットワーク作ってやってますが、もっと賢い書き方あるかもしれませんね.
まあきちんと書いてないのもありますが、わかりにくいですし長いです.もっと楽にかけたらいいんですけどね……

f:id:tochikuji:20150916210438p:plain

ん…?

f:id:tochikuji:20150916210916p:plain

あっ!!

ということでStacked Autoencoderを拙作libDNNに実装しました. (過去記事参照) 

tochikuji.hatenablog.jp

以下MNISTを3層のAutoencoderで学習させるサンプルです.

Stacked denoising(deep) Autoencoder (with libDNN)

通常のAutoEncoderと同様に大きなモデルも一気に定義してしまって,その積み上げ順と多少のお作法に則ったencoding, decodingの伝搬規則を書けば後はtrain一発で通るようにしてあります. めっちゃ便利ですね.
手がまわなくて未だドキュメントとExampleのコメントを書いてないので身体で感じてください.(あるいは誰か書いてください)
ドキュメントきちんと書きました。pushしてなかっただけです。

chainerで(より)簡単にディープラーニングできる魔法を作りました

2016/6/1追記

なんか公開してから半年以上経って、chainerも進化したし、なんでもDNNブームもあるていど去ったのに、
まだこの記事のアクセスが絶えないし、githubにお星様がついたりするので追記しておきます。


内容をご覧になる前に、この記事ではchainerのFunctionSetをラップして、
単純なミニバッチ学習・テストのインターフェースを提供するためのライブラリについて紹介しています。

しかしchainer1.5.0以降では新たなネットワーク定義の仕方として
Link, Chainが導入されその柔軟性が増したと同時に、
FunctionSetはdeprecatedになりました。

ちょちょっと学習・テストを行うフレームワークとしてはまだ良いでしょうが,
少しでも余力のある皆様は、後学のためにもこのライブラリは使わず, 
Link, Chainを使ってきっちりナウいchainerを使うことを強くおすすめします。

-- 本文ここから --

みなさんDeep learningしてますか?
僕はここ1ヶ月以上ずっと朝から晩までDeep learningして、二度と来ないこの5歳の夏を未だ見ぬ汎化性能に捧げています.

 今日日、PythonをはじめとしたLL上のDeep learning framework(Caffeとかtorch7とか)がよりどりみどりでDeep learningを実装して試す、ということはそこまで苦ではなくなってきています.
Torchは知らないのでCaffeに限っての話ではありますが、恐らく現状もっともポピュラーなDeep learning frameworkであるCaffeは長いこと使われてきて情報が多くある一方で, その設計思想からして柔軟性に欠ける,というのが大きな問題であります.

 Caffeではネットワーク構造を予め定義して固めて,データセットを作成して固めて,そしてガリガリ学習を行わせる(Define-by-Run)というのが通常のアプローチになります.
 しかしこれは,とりあえずこんなネットワークでできるかやってみよう,とか単にアプリケーションに適用しよう,のような場合には明快でよいのですが, ネットワークの腹を開けてその詳細なところを覗いたりいじってみたりとか,ちょっと決められた通常のアプローチとは違うことをしようとした時にネックとなります. それゆえ,そうしたことをすることが求められる層の人々はCaffeではなく数値計算ライブラリのTheanoでゴリゴリディープネットを作ってみたり(Effort-and-Run?), Caffeを魔改造していろいろやってみたりするわけです.

 そんな中,日本の(!)Prefferred networksというところからchainer(http://chainer.org/)という python向けニューラルネットワークフレームワークが公開され,ここ1,2ヶ月あちこちで大流行りとなっています. 詳しくはchainerのドキュメントを参照してもらうこととして,chainerではCaffeの問題であったDefine-by-Runから来る柔軟性の低さを解消すべく, ネットワークの構造であったりforwardingの処理であったりを固めずに,計算処理の中で柔軟(on-the-fly)に変更できる, Define-and-Runの設計思想をとっています.
 早い話がこのおかげで実験の中でいろいろ好き勝手できて,大変都合がよいわけです.
 様々な長さの孫の手が用意されていて痒いところに手が届く,という点で言えば,まだまだピチピチのchainerは歴史の長いCaffeには幾分か劣るのですが, chainerは新進気鋭,最近ではnumpy-likeなCUDAのラッパであるCuPyを自ら作り,バックエンドを整備したりとこれから伸びてゆくであろう, 少なくとも2.0.0ごろには形成が逆転しているのではないかなと女児なりに考えています.

 しかし,chainerは(Caffeに比べれば)まだまだ情報も少ないですし,柔軟であるがゆえにCaffeのようにデータと定義されたネットワークさえあれば,という手軽さも少々劣ります. いろいろいじるにしてもデータを学習させてテストする,といった決め打ちのそれは(Pythonでロジックを記述するような作業抜きに)簡単に書いてしまいたい, そうでもなければすでに身につけたCaffeなりの便利な乗り物を捨てようとは思わない,というのが「彼ら」の希望らしいです.(僕も少し思います)

 そこで,というか一日中GPUをブン回して地球温暖化に大いに貢献している中で必要に駆られて,numpyが「なんとなく」使えるレベルであればデータ丸投げである程度のことをやってくれるような魔法のようなライブラリを作りました. MIT Licenseの下で公開しています.

github.com

PyPiにも上げてあるので適当にpip install chainer-libdnnなりして簡単に入れることができます. 様々な背景もあり,ようじょの割に珍しく(拙い英語で)README, exampleもしっかり書いてあるのでまあいい気はするのですが, こんな感じで簡単にDeep learningっぽいことができます.

import numpy
import chainer
import chainer.functions as F
from libdnn import Classifier
import libdnn.visualizer as V
from sklearn.datasets import fetch_mldata

# ネットワーク作って(MLP)
model = chainer.FunctionSet(fh1=F.Linear(28 ** 2, 100), fh2=(100, 10))
# 伝搬規則書いて
def forward(self, x, train):
    h = F.tanh(self.model.fh1(x))

    return F.tanh(self.model.fh2(x))

# モデル作って
mlp = Classifier(model, gpu=0) # CUDAします
mlp.set_forward(forward)

# データ作って
mnist = fetch_mldata('MNIST original')
perm = numpy.random.permutation(len(mnist.data))
mnist.data = mnist.data.astype(numpy.float32)
train_data, test_data = mnist.data[perm][:60000], mnist.data[perm][60000:]
train_label, test_label = mnist.target[perm][:60000], mnist.data[perm][60000:]

# 60000(num. of train_data) iter. を1世代として100世代ミニバッチ学習
for epoch in range(100):
    err, acc = mlp.train(train_data, train_label)
    print("%d: err=%f\t acc=%f" % (epoch + 1, err, acc))

# テスト
err, acc = mlp.test(test_data, test_label)
print("on test: %f\t%f" % (err, acc))

# 可視化なんて器用なことも
imager = V.Visualizer(mlp)
plot_filters('fh1', shape=(28, 28))

# 学習済みネットワークも保存できます
mlp.save_param()

 自分で言うのもなんですがめっちゃ便利です.
 使うときに決める必要があるのは最低限model = ...のネットワークの構成の定義と def forward(...フィードフォワードのさせ方だけで,Caffeと変わりません.
 また,普通煩わしさを感じるほどそんな必要に駆られないのでしょうが,chainer使うときに個人的にちょっとめんどくさいなと思う,chainer.Variableへの変換と, GPUのオブジェクトとCPUのオブジェクトの相互変換. そのへんは完全にインターフェースから隠蔽してバンバンnumpy.arrayとしてデータを放り込んで,取ってこられるようにしています.  現時点では最低限の構成ですが,これから便利な機能を付けて付けて,どんどん太らせていきたいと思っていますので, 欲しい機能があったら文句つけるか,継ぎ接ぎしていってください.

 このライブラリを使えばここに実例があるように,女児でも簡単にDeep learningができるので,きっと学校の成績だったりお受験だったり,いろいろ役立つんじゃないでしょうか.

 以下,想定質問です.

  • なんでもうv2.0.0なんですか? : もともとはprivateなgitlab上で開発していて,まあいろいろあるんです
  • なんでREADME英語なんですか? : いろいろあるんです
  • なんでCNNだけ優遇されてるっぽいんですか? : いろいろあるんです

以上になります.

知らないauto指定子について行ってはいけない

std::vector<std::uint32t> 長いですね。
Perlの国の民にはこのような長い形名をタイプするのは苦行以外の何物でもありません。
そこで苦しむPerl民のために、C++11から型推論により長い形名のタイプから我々を開放してくれるauto指定子が導入されました。
しかしこのauto指定子はさながら、純真な女児を甘いお菓子で誘惑する知らないおじさんのようなものなのです。

純真なのでオブジェクトの参照を返すところでauto指定子を使いたいとします。
以下のソースコードをご覧ください。

#include <vector>
#include <iostream>
#include <cstdint>

class A {
    private:
        std::vector<std::uint32_t> v;
    
    public:
        A(){
            for(int i = 0; i < 10; ++i){
                v.push_back(i * 2);
            }
        }
        
        int get(int i){
            return v[i];
        }
    
        std::vector<std::uint32_t>& ref(){
            return v;
        }
};

int main(){
    A obj;
    std::cout << obj.get(3) << std::endl;
    auto r = obj.ref();
    r[3] = 1;
    
    std::cout << obj.get(3) << std::endl;
}

これをコンパイルし、実行すると恐ろしいことにこうなります。

6
6

怖いですね。知らないと3時間ぐらいは無駄にしそうですね。

ここでは普通の値渡しと同じようにコピーが行われています。
正しくは呼ぶ側のautoを

auto& r = obj.ref();

と明示的に参照の形で宣言してやる必要があります。

(実行結果)

6
1

このautoの特性はポインタに対しても同様です。

これは純真な女児を陥れる大きな罠であり、物心ついた子どもには必ず教える必要があると思います。