StatsFragments

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

Theano で Deep Learning <1> : MNIST データをロジスティック回帰で判別する

概要

ここ数年 Deep Learning 勢の隆盛いちじるしい。自分が学生の頃は ニューラルネットワークオワコン扱いだったのに、、、どうしてこうなった?自分もちょっと触ってみようかな、と記事やらスライドやら読んでみても、活性化関数が〜 とか、 制約付き何とかマシンが〜(聞き取れず。何か中ボスっぽい名前)とか、何言っているのかよくわからん。

巷には 中身がわかっていなくてもある程度 使えるパッケージもいくつかあるようだが、せっかくなので少しは勉強したい。

Python 使って できんかな?と思って探してみると、すでに Theano というPython パッケージの開発チームが作った DeepLearning Documentation 0.1 という大部の聖典 (バイブル) があった。

当然だがこの文書では Theano の機能をいろいろ使っているため、ぱっと見では 何をやってんだかよくわからない。ということで、Theano の仕様を調べたり numpy で翻訳したりしながら読み解いてみることにした。

聖典の目次

かなり楽観的な見通しだが、 1章/週 のペースで進めても 3ヶ月近くかかるな、、。

第一回 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

全体の流れ / スクリプトは元文書を参照。以下の各セクション名には対応するセクションへのリンクを貼っている。

Theano とは

こんなことができます。

  • シンボル(変数名) / 値 (スカラー, ベクトル, 行列) / 式を 数式表現そのもの (構文木) として扱える。
    • 式の定義 / 途中計算結果の再利用が楽
    • 機械学習で頻出する勾配計算を解析的に実行できる
  • 高速な計算実行

インストール

pip install theano

準備

import numpy
import theano
import theano tensor as T

モデル

ロジスティック回帰が 入力をどのようにクラス分類するか?という方法が書いてある。ロジスティック回帰 そのものについてはこちらでまとめた。

プログラム中で まず 目に付くのは以下のような theano.shared 表現。Theano では以下 二つのメモリ空間を明示的にわけて扱う

  • Python 空間 ( user space ) : Python スクリプト上で定義された普通の変数が存在する。
  • Theano 空間 : theano.shared によって定義された変数 (以降、共有変数) が存在する。物理的に Python 空間とは別に確保され、 Theano の式表現(後述)から参照できる。また、GPU へのデータ転送を高速にする。

theano.shared で宣言された変数は 実データとして numpy のデータをもつ。

# この表現で作られるデータは
W = theano.shared(
    value=numpy.zeros(
        (n_in, n_out),
        dtype=theano.config.floatX
    ),
    name='W',
    borrow=True
)

# numpy だとこちらと同じ
W = numpy.zeros((n_in, n_out), dtype=numpy.float64) 

theano.shared による共有変数の定義

上でも指定されている引数 borrowPython 空間上で定義されたデータの実体を 共有変数でも共有するかどうかを決める。

# Python 空間で変数定義
x = numpy.array([1, 1, 1])

# theano.shared で 共有変数化
xs = theano.shared(x)

# 返り値は int32 / vector の TensorSharedVariable 型
xs
# <TensorType(int32, vector)>
type(xs)
# theano.tensor.sharedvar.TensorSharedVariable

# 内部の値を得るためには get_value
xs.get_value()
# array([1, 1, 1])

# デフォルトでは 対象オブジェクトのコピーが 共有変数の実体になるため、
# Python 空間の変数を変更しても 共有変数の実体には影響しない
x[1] = 2
x
# array([1, 2, 1])

xs.get_value()
# array([1, 1, 1])

# 同じ実体を共有したい場合は borrow=True
xs = theano.shared(x, borrow=True)

xs.get_value()
# array([1, 2, 1])

# Python 空間での変更が 共有変数へも反映される
x[1] = 3
x
# array([1, 3, 1])

xs.get_value()
# array([1, 3, 1])

theano.tensor 上の関数の呼び出し

Theano では シンボル(変数名) / スカラー / ベクトル / 行列などはテンソル型として抽象化される。テンソル型に対する処理は theano.tensor 上で定義された関数を使って行う。

例えば、多項ロジスティック回帰では 入力があるクラスに分類される確率はソフトマックス関数を用いて書けたTheano にはソフトマックス関数を計算する theano.tensor.nnet.softmax があって、

d = theano.shared(value=numpy.matrix([[0.1, 0.2, 0.3],
                                      [0.3, 0.2, 0.1],
                                      [0.1, 0.2, 0.3]],
                                     dtype=theano.config.floatX),
                  name='d', borrow=True)
sm = T.nnet.softmax(d)

# 返り値は TensorVariable 型
type(sm)
# <class 'theano.tensor.var.TensorVariable'>

# TensorVariable から結果を取り出す (関数を評価する) には eval()
sm.eval()
# [[ 0.30060961  0.33222499  0.3671654 ]
#  [ 0.3671654   0.33222499  0.30060961]
#  [ 0.30060961  0.33222499  0.3671654 ]]

ロジスティック回帰の予測値は上記の確率を最大にするラベルになる。値が最大となるラベルをとる処理は theano.tensor.argmax。これは numpy.argmaxテンソル版。以下の通り、theano.tensor の関数は通常 (共有変数化されていない) の numpy.array も受け取ることができる。その場合も返り値は TensorVariable 型になる。

# 3 番目の要素 ( index でみて 2 ) が最も大きい
am = T.argmax(numpy.array([1, 2, 3, 2, 1]))

type(am)
# theano.tensor.var.TensorVariable

am.eval()
# array(2L, dtype=int64)

損失関数の定義

これはそのまま。すべて theano.tensor の関数であることには注意。

LogisticRegression のクラス作成

LogisticRegression.errors で誤差 (ここでの誤差は真のクラスと 判別結果の差)を取っている。論理演算 != についても テンソル版である theano.tensor.neq が定義されているのでそちらを使う。

# 真のクラス
y = numpy.array([1, 0, 1, 1, 1])

# 予測値が2つ誤判別しているとする
y_pred = numpy.array([1, 1, 1, 0, 1])

# theano.tensor
T.mean(T.neq(y_pred, y)).eval()
# 0.4

# numpy
numpy.mean(y_pred != y)
# 0.4

モデルの学習

ロジスティック回帰の学習について。勾配 (Gradient) の計算は Theano だと theano.grad を使って簡単にできるようだ。ここからの一連の処理が今回のキモ。

Theano での勾配の計算 (微分)

Theano での勾配の計算については公式ドキュメントに 詳細 がある。Theano では、定義された式にあらわれるシンボル / 定数を構文木として保持している。そのため、勾配計算の際は 構文木解析によって導関数を求めて実行することができる。とりあえず { y = x^2 } を定義して微分してみると、

# シンボル x を宣言 (この時点で x に値はない) 
x = T.dscalar('x')

# y = x ** 2 という式を宣言
# シンボルに対して演算子から式を作っていけるところがカッコイイ
y = x ** 2

# y を x について微分 (まだ値はない)
gy = T.grad(y, x)

# gy の定義 (導関数) を確認
theano.pp(gy)
# '((fill((x ** TensorConstant{2}), TensorConstant{1.0}) * TensorConstant{2}) * (x ** (TensorConstant{2} - TensorConstant{1})))'

TensorConstant{} 内の値をもつスカラー。また、fill は 第一引数を 第二引数で埋める、という意味になる。順番に数値を埋めてみると、、、おお、、、ちゃんと { y' = 2x } になっている。

# '((fill((x ** TensorConstant{2}), TensorConstant{1.0}) * TensorConstant{2}) * (x ** (TensorConstant{2} - TensorConstant{1})))'
# = '((fill((x ** 2), 1.0) * 2) * (x ** (2 - 1)))'
# = '(2 * x)'

続けて、式から関数を作成し、実際の値を渡して計算 (式の評価) を行う。え、式にそのまま引数渡せないの?と思うが、どうも function の実行時に式表現をコンパイルしているのでダメらしい。

# 式から関数を作る。function の第一引数は関数に与える引数, 第二引数に関数化する式
f = function([x], gy)

# x = 4 のときの x ** 2 の傾きを求める
f(4)
# array(8.0)

# x = 8 のときの x ** 2 の傾きを求める
f(8)
# array(16.0)

さて、元の文書にもどってロジスティック回帰を定義 / 学習を始めるところをみてみる。ここでのテンソル型の式から学習器を作っていく流れは格好いい。

    # ミニバッチ確率勾配降下法で指定する index (スカラー)を入れるシンボル
    index = T.lscalar() 
    # 入力データの行列を示すシンボル
    x = T.matrix('x') 
    # ラベルのベクトルを示すシンボル
    y = T.ivector('y')  

    # ロジスティック回帰の処理も theano.tensor の関数で定義されているため、
    # シンボル x が実データなしで渡せる
    classifier = LogisticRegression(input=x, n_in=28 * 28, n_out=10)

    # 引数 y もシンボル。返り値 cost では負の対数尤度を計算する式が返ってくる 
    cost = classifier.negative_log_likelihood(y)

    # w, b の勾配を計算する式
    g_W = T.grad(cost=cost, wrt=classifier.W)
    g_b = T.grad(cost=cost, wrt=classifier.b)

で、最後にこれらを theano.functionで関数化する。引数の意味は、

  • inputs : 関数への入力 (引数)。
  • outputs : 関数化される式。
  • updates : 共有変数を更新する式。
  • givens : 引数 ( inputs ) -> 共有変数へのマッピングを行う辞書。

つまり、

    # w, b の更新を定義する式
    updates = [(classifier.W, classifier.W - learning_rate * g_W),
               (classifier.b, classifier.b - learning_rate * g_b)]

    train_model = theano.function(
        inputs=[index],
        outputs=cost,
        updates=updates,
        givens={
            x: train_set_x[index * batch_size: (index + 1) * batch_size],
            y: train_set_y[index * batch_size: (index + 1) * batch_size]
        }
    )

上の定義は

  • indexを引数として受け取り、
  • givens の指定により index の位置のデータを切り出して シンボル x, y に入れ、
  • x, youtputsの式 (負の対数尤度) に与えて計算結果を得て、
  • updates で指定された更新式によって共有変数を更新する

という一連の処理を行う関数を作っている。うーん、これは訓練されていないと ちゃっと読めないな、、、。

そして、この時点においても train_model では ロジスティック回帰の学習を 関数として定義しただけで、実計算はまだ行われていない。実際の学習 ( 続く while ループ中 ) では この定義に対して index だけを渡してやれば、 必要な値が計算 / 関連する値が自動的に更新されていく。

全部まとめて

以降は特になし。MNISTデータをローカルに保存した後、まとめ にあるスクリプトをコピペして実行すればよい。

11/30追記: つづきはこちら。

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

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