StatsFragments

Python, R, Rust, 統計, 機械学習とか

Theano で Deep Learning <2> : 多層パーセプトロン

Python Theano を使って Deep Learning の理論とアルゴリズムを学ぶ会、第二回。

目次

DeepLearning 0.1 より、

第一回 MNIST データをロジスティック回帰で判別する
第二回 多層パーセプトロン (今回)
第三回 畳み込みニューラルネットワーク
第四回 Denoising オートエンコーダ
第五回 多層 Denoising オートエンコーダ
第六回の準備1 networkx でマルコフ確率場 / 確率伝搬法を実装する -
第六回の準備2 ホップフィールドネットワーク -
第六回 制約付きボルツマンマシン
Deep Belief Networks
Hybrid Monte-Carlo Sampling
Recurrent Neural Networks with Word Embeddings
LSTM Networks for Sentiment Analysis
Modeling and generating sequences of polyphonic music with the RNN-RBM

多層パーセプトロンとは

ロジスティック回帰では判別関数が直線のため、入力データが線形分離できない場合は判別率が落ちていた。

多層パーセプトロンでは、入力を非線形変換/次元変換することによってデータを分離しやすくし、そのような場合の判別率を上げようとするもの。

モデル

多層パーセプトロンを DeepLearning 0.1 Multilayer Perceptron の式をベースに図示するとこんな感じになる。

f:id:sinhrks:20141214220250p:plain

  • { W^{(1)} } : 入力層 - 隠れ層の間で適用される係数行列。次元は 入力データの説明変数の数 { D } x 隠れ層のユニット数 { D_h }
  • { b^{(1)} } : 入力層 - 隠れ層の間で適用される重みベクトル。次元は 隠れ層のユニット数 { D_h }
  • { s } : 隠れ層の活性化関数。シグモイド関数もしくは { tanh } (ハイパボリックタンジェント) をよく使う。
  • { W^{(2)} } : 隠れ層 - 出力層の間で適用される係数行列 = ロジスティック回帰の係数行列。次元は 隠れ層のユニット数 x 出力データのクラス数
  • { b^{(2)} } : 隠れ層 - 出力層の間で適用される重みベクトル = ロジスティック回帰の重みベクトル。次元は 出力データのクラス数
  • { G } : 出力層の活性化関数。2クラスの場合は シグモイド、多クラスの場合はソフトマックス関数 (ロジスティック回帰と同じ)。

つまり 3層パーセプトロンでは、

  1. 入力層 - 隠れ層: { D } 次元の入力を { W^{(1)} }, { b^{(1)} } によって { D_h } 次元へと写像し、
  2. 隠れ層 - 出力層: { D_h } 次元へと写像された入力を { W^{(2)} }, { b^{(2)} } によってロジスティック回帰で学習 & クラス判別する

また、多層パーセプトロンの各層のユニット数 = その層に渡ってくるデータの次元 と考えればよい。

補足 PRML ではバイアス項をダミーのユニットを作って扱っているため、ユニット数 / 係数行列の次元など定義が異なる。

Pattern Recognition and Machine Learning (Information Science and Statistics)

Pattern Recognition and Machine Learning (Information Science and Statistics)

ロジスティック回帰から多層パーセプトロンへ

まず、隠れ層をあらわす HiddenLayer クラスを定義する。

係数行列の初期値の決め方

前回の LogisticRegression クラスと同じく、HiddenLayer クラスは 係数行列 W ( 上式 { W^{(1)} } ) , バイアス b ( 上式 { b^{(1)} } ) を Theano の共有変数として宣言する。

係数行列 W の初期値は以下の範囲の一様乱数からとると (特にネットワークの層が深い場合に) 学習の進みがよいらしい

  • 活性化関数が tanh : f:id:sinhrks:20141129233659p:plain

  • 活性化関数が sigmoid : f:id:sinhrks:20141129233721p:plain

{ fan_{in} }, { fan_{out} } は、それぞれ係数行列の前層 / 後続層のユニット数

そのため、HiddenLayer クラスでは 指定範囲の一様乱数を生成する numpy.random.uniform を使って係数行列 W の初期値を設定している。宣言中で使われている以下の変数は HiddenLayer.__init__ の引数として渡ってきているもの。

  • rng : 乱数のシード
  • n_in : 入力層のユニット数
  • n_out : 隠れ層のユニット数
    W_values = numpy.asarray(
        rng.uniform(
            low=-numpy.sqrt(6. / (n_in + n_out)),
            high=numpy.sqrt(6. / (n_in + n_out)),
            size=(n_in, n_out)
        ),
        dtype=theano.config.floatX
    )
    if activation == theano.tensor.nnet.sigmoid:
        W_values *= 4

    W = theano.shared(value=W_values, name='W', borrow=True)

例えば、前層のユニット数が 3, 後続層のユニット数が 4 で 活性化関数が tanh の場合、係数行列の初期値は以下のようになる。

import numpy

n_in = 3
n_out = 4
numpy.random.uniform(
    low=-numpy.sqrt(6. / (n_in + n_out)),
    high=numpy.sqrt(6. / (n_in + n_out)),
    size=(n_in, n_out)
)
# [[ 0.01572058 -0.46004291 -0.23671422 -0.54857948]
#  [ 0.081257    0.87090393 -0.72051252 -0.38176917]
#  [-0.09591302  0.04587085 -0.42426934 -0.35021306]]

また、バイアス b の初期値は 0ベクトルでよいようだ。

    b_values = numpy.zeros((n_out,), dtype=theano.config.floatX)
    b = theano.shared(value=b_values, name='b', borrow=True)

続けて、隠れ層からの出力を計算する式を HiddenLayer.output プロパティとして定義している。宣言中で使われている以下の変数は HiddenLayer.__init__ の引数として渡ってきているもの。

  • input : 入力データをあらわすシンボル ( TensorVariable 型)。
  • activation : 活性化関数。デフォルトは theano.tensor.tanh ( np.tanhテンソル版 )。

また、HiddenLayer はこの時点では実際のデータを受け取っておらず、Theano のシンボルから式を作っているだけ。ここも考え方は 前回同様。

    lin_output = T.dot(input, self.W) + self.b
    self.output = (
        lin_output if activation is None
        else activation(lin_output)
    )

MLP クラスの定義

続けて 多層パーセプトロン自体をあらわすクラス MLP を定義している。このクラスは 以下二つのインスタンスをプロパティとして持つ。

正則化 (Regularization)

MLP クラスでは 学習の際に 負の対数尤度だけでなく、以下 二つのノルムを正則化項として最小化するように設定している。一般には、これらを罰則 ( Penalty ) として最小化することで過学習をさけ、学習器の汎化性能 (未知のデータへのあてはまりの度合い) を上げることができる。

  • L1 ノルム : 係数行列 { W^{(1)} }, { W^{(2)} } の各要素の絶対値の和
  • L2 ノルム : 係数行列 { W^{(1)} }, { W^{(2)} } の各要素の二乗和
    # L1 ノルム
    self.L1 = (
        abs(self.hiddenLayer.W).sum()
        + abs(self.logRegressionLayer.W).sum()
    )

    # L2 ノルム
    self.L2_sqr = (
        (self.hiddenLayer.W ** 2).sum()
        + (self.logRegressionLayer.W ** 2).sum()
    )

負の対数尤度と判別誤差

他、モデル自体の負の対数尤度 ( negative_log_likelihood ) / 判別誤差 ( errors ) はモデルの出力に対して計算すればよいため、出力層である LogisticRegression での計算結果をそのまま使っている。

    self.negative_log_likelihood = (
        self.logRegressionLayer.negative_log_likelihood
    )

    self.errors = self.logRegressionLayer.errors

損失関数の定義

これら 負の対数尤度と正則化項を足し合わせて損失関数を計算する式 cost を作っている。L1_reg, L2_reg はそれぞれ L1ノルム, L2ノルムの正則化項の重み。既定値は L1_reg=0.00, L2_reg=0.0001 のため、サンプルをそのまま実行した場合は L2 ノルムのみが正則化項として考慮されることになる。

    cost = (
        classifier.negative_log_likelihood(y)
        + L1_reg * classifier.L1
        + L2_reg * classifier.L2_sqr
    )

勾配の計算

ここが今回のポイント。多層パーセプトロンの学習は 誤差逆伝播法 ( Back propagation) というアルゴリズムで行われ、一般には以下のような処理 (順逆モデリング) が必要になる。

  1. 学習器の出力値と、真の値との差異 = 出力誤差を計算
  2. 1 で求めた出力誤差を小さくする { W^{(2)} }, { b^{(2)} } の勾配を計算し、 { W^{(2)} }, { b^{(2)} } を更新
  3. 1 で求めた出力誤差に対して "隠れ層 - 出力層間の処理の逆演算"を行い、"出力誤差が隠れ層からでたと仮定した場合の出力値 = 隠れ層誤差" を求める
  4. 3 で求めた隠れ層誤差を小さくする { W^{(1)} }, { b^{(1)} } の勾配を計算し、 { W^{(1)} }, { b^{(1)} } を更新

、、、めんどくさい、、、。

が、Theano では式表現に対して偏微分を行い偏導関数を求めることができる。そのため、上で定義した損失関数 cost について、最適化したいパラメータ HiddenLayer.W, HiddenLayer.b, LogisticRegression.W, LogisticRegression.b それぞれで偏微分した偏導関数を求めておけば、学習の際はそれらを使ってパラメータごとに勾配計算 / 更新をかけていくことができる。

つまり、今後 パーセプトロンの層 / パラメータが増えた場合でも、モデル全体の損失関数を偏微分することによって各パラメータを個別に学習できることになる。これは強力だな、、、。

  • classifier.params : HiddenLayer.W, HiddenLayer.b, LogisticRegression.W, LogisticRegression.b からなるリスト
  • learning_rate : 学習率。デフォルトは 0.01
    gparams = [T.grad(cost, param) for param in classifier.params]

    updates = [
        (param, param - learning_rate * gparam)
        for param, gparam in zip(classifier.params, gparams)
    ]

続けて定義されている theano.function の読み方は 前回と同じ。以下 一連の処理を行う関数を作っている。

  • indexを引数として受け取り、
  • givens の指定により index の位置のデータを切り出して シンボル x, y に入れ、
  • x, youtputsの式 (負の対数尤度 + 正則化項) に与えて計算結果を得て、
  • updates で指定された更新式 ( 損失関数の偏導関数からの勾配計算 ) によって共有変数 ( 各係数行列、バイアス ) を更新する

全部まとめて

プログラムの量としては Theano のおかげで、え?こんだけでいいの?という感じだ。最後に置いてあるスクリプトを前回と同じディレクトリに入れて実行すればよい。

パラメータ調整のコツ

また、元文書では補足的に以下のパラメータ調整についてカッコ内のようなことが記載されている。自分でパラメータ調整したい方は読むといいですね。

  • 隠れ層の活性化関数 ( tanh がいい感じ )
  • 係数行列の初期値 (上の内容と同じ)
  • 学習率 ( { 10^{-1}, 10^{-2} ... } で試せ、時間で減衰させてみろ )
  • 隠れ層のユニット数 ( データによる。モデルが複雑な場合は増やせ )

隠れ層からの出力の可視化

最後。元文書にはない内容だが、今回のサンプルについて 入力が隠れ層においてどういった感じで分離されるのか、以下の方法で可視化してみた。

  • 学習データ ( train_set_x, train_set_y ) を対象
  • MNIST のラベル 0 〜 9 について、隣り合う 9組 ( 0と1, 1と2 ... ) を比較
  • 隠れ層からの出力データ 500 次元に主成分分析をかけて 2次元に写像
  • ランダムサンプリングした 300 レコードを描画

入力 - 隠れ層 間の変換によって各クラスが 主成分 ( = 分散大 ) の方向に離れていけば、分離超平面がみえるはずだ。

学習開始前 (隠れ層出力)

f:id:sinhrks:20141129231153p:plain

50 エポック学習後 (隠れ層出力)

曇りのない目で見てみると、なんか少し分離しやすそうになったかも?といえなくもない感じですね!

f:id:sinhrks:20141129231204p:plain

プログラム

プロット部分のみ gist に置いた ( 他は元文書のスクリプトと同じなので )。

Theano に関してのポイントは 1 点だけ。隠れ層からの出力を計算する HiddenLayer.outputTensorVariable 型のため、実際に計算するには一度 theano.function を通すこと。

    # 隠れ層の出力 z_data を計算
    apply_hidden = theano.function(inputs=[x_symbol], outputs=classifier.hiddenLayer.output)
    z_data = apply_hidden(x_data.get_value())
    labels = y_data.eval() 

Visualize Multilayer Perceptron Example in deeplearning.net · GitHub

11/30追記: 隠れ層での変換についてもう少しわかりやすい例をつくった。

12/07追記 続きはこちら。

深層学習 (機械学習プロフェッショナルシリーズ)

深層学習 (機械学習プロフェッショナルシリーズ)