終末 A.I.

データいじりや機械学習するエンジニアのブログ

TensorFlow の ptb サンプルを動かす

TensorFlow で ptb を学習させるサンプルは Recurrent Neural Networks チュートリアルに記載されている通り、 github からソースを落としてきて、tensorflow/models/rnn/ptb に移動し、ptb_word_lm.py を動かすだけで簡単に動作させることができます。

ただし残念なことに、サンプルコードをぱっと見ただけで使い方を理解するのは相変わらずなかなか難しいです。API にのっていないクラスや関数が使われたり、RNN 特有の処理がさくっと書かれているため、サンプルとしてまあ不親切なわけです。

そこで今回も前回の DNN と同様、自己流にサンプルを書き直して使い方を確認してみました。書き直したコードを動作させてみて、だいたい元のコードと同じような性能が出ていそうなので、ざっくりと解説をしてみようと思います。

いつもどおり完成コードは、記事の最後の方に置かせていただいています。書き直したコード内の内部で reader ファイルは元サンプルの reader と全く同じものを利用しています。

また、RNNをまともに学習させようと思うとCPUでは非常に遅いので、動作確認には AWSGPU インスタンスを使用しています。

TensorFlow ってどうやって使うの?AWSGPUインスタンスでどうやって動かせばいいの?という方は、まずは過去記事をご参照ください。

ハイパーパラメーターと入出力用変数

さて、手法のコードを書く前にイニシャライズをする必要がありますが、DNN の場合だいたいここでハイパーパラメーターの設定と入出力用のデータを定義します。TensorFlow で書く場合もその流儀にならって書くと下記のようになります。

# config
self._batch_size = 20
self._num_steps = 2
self._hidden_size = 2
self._vocab_size = 10000
self._num_layers = 1
self._keep_prob = 0.5
self._max_grad_norm = 1

# input and output variables
self._input_data = tf.placeholder(tf.int32, [self._batch_size, self._num_steps])
self._targets = tf.placeholder(tf.int32, [self._batch_size, self._num_steps])
self._initial_state = None
self._final_state = None
self._cost = None
self._train_op = None
self._logits = None

バッチサイズなどがハイパーパラメーターで、input_data などが PlaceholderVariable などの入出力用の変数となります。Placeholder も Variable も TensorFlow で計算の時に使用される変数クラスですが、Placeholder が入力用、Variable は最終出力および中間出力用に主に使用されるものでしたね。

RNN で大事なものは、内部状態をいつまで保持するかを定義している num_steps と内部状態を表す変数である initial_state と final_state です。この2つの値に注目して以降のコードを見ていきましょう。

LSTMの利用

続いて、LSTMを利用して、次の単語を出力させる手順までを見てみましょう。損失関数の定義やパラメーター更新の方法は通常のDNNと大差ありませんので省略させていただきます。

# LSTM
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(self._hidden_size, forget_bias=0.0, state_is_tuple=True)
# add dropout
if is_training:
    lstm_cell = tf.nn.rnn_cell.DropoutWrapper(lstm_cell, output_keep_prob=self._keep_prob)
# add multi lyaers
cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * self._num_layers, state_is_tuple=True)
# initial state setup
self._initial_state = cell.zero_state(self._batch_size, tf.float32)

ここから先のコードは色々と不親切で全然サンプルになっておりませんので心して追っかけていきましょう。

まず、LSTM の基本クラスである tf.nn.rnn_cell.BasicLSTMCell が早速API一覧にのっていません。詳細が気になる方は、TensorFlow ソースコード内の tensorflow/python/ops/rnn_cell.py をご参照ください。使い方自体はそんなに難しくはなく、隠れ変数のサイズと忘却率、そして state_is_tuple という値を指定しているだけで LSTM セルを定義できてしまいます。

state_is_tuple は元のサンプルコードでは使用されていませんが、使用しないままコードを動かすと、state_is_tuple が False だと遅いし False の場合はそのうち Duplicated だからね!という警告 が r0.9 の時点では表示されます。このオプションは、LSTM の計算時に入出力される隠れ変数を Variable クラスのタプルとして表現するかどうかを示すものになります。

書き換えるだけで早くなるのであれば書き換えない手はありませんが、evaluation の時に独特な処理が必要となりますのでそこだけご注意ください。下記のコードの run_epoch 関数を参考にするとわかりやすく汎用的に書き換えることができますのでご参照ください。

続いて用いられているのが、tf.nn.rnn_cell.DropoutWrapper と tf.nn.rnn_cell.MultiRNNCell です。この2つは見たまんまなのですが、LSTM の入力にドロップアウトを適用する処理と、LSTM + Dropout を複数個並べてくれる処理になります。

使用している引数も、DropoutWrapper の場合はベースとなるLSTMセルとキープレート、MultiRNNCellの場合は組み合わせるセルの配列と上記に登場した state_is_tuple を指定しています。

最後に、RNNCell クラスの zero_state という便利関数を利用して、隠れ変数の初期値を表す Placeholder クラスを取得します。この値はのちのちの計算処理で必要になるので確実に保持しておく必要があります。

# Load predefined layer "embedding"
with tf.device("/cpu:0"):
    embedding = tf.get_variable("embedding", [self._vocab_size, self._hidden_size])
    inputs = tf.nn.embedding_lookup(embedding, self._input_data)

# Add dropout after embedding layer
if is_training:
    inputs = tf.nn.dropout(inputs, self._keep_prob)

次に、単語IDを単語ベクトルに変換する処理を行います。ここでもなかなか TensorFlow を作っている人でないと普通わからないだろうという処理を行っていまして、tf.get_variable で "embedding" という変数を取得しています。どうやら "embedding" という変数が Word Embedding 用にすでに用意されているらしく、その変数を使って単語IDをベクトルにする処理を行っているようです。仕様書読むだけでは分かりませんね。

ちなみに rnn_cell.py には入力を単語ベクトルにした後、RNN セルに適用してくれる EmbeddingWrapper クラスが存在しています。試してみていませんが多分同じような結果になると思われます。

# Calculate LSTM Layer for in _num_steps
outputs = []
state = self._initial_state
with tf.variable_scope("RNN"):
    for time_step in range(self._num_steps):
        if time_step > 0: tf.get_variable_scope().reuse_variables()
        (cell_output, state) = cell(inputs[:, time_step, :], state)
        outputs.append(cell_output)

次に、LSTM を利用した計算の肝となるシーケンスの処理部分を見てみましょう。今回のインプットデータは、ミニバッチ × シーケンス長(num_steps) × 単語ベクトル という形の配列として作成されています。ミニバッチ単位でベクトルを計算していく点は他の DNN と変わりありませんが、RNN ではシーケンシャルなデータの処理を行う必要があります。

LSTM のような隠れ変数がある層では、truncate という隠れ変数をリセットし、バックプロバケーションを打ち切る長さを決めておく必要があります。この長さはもちろん可変でも良いのですが、それではミニバッチの利用に向かず処理が遅くなるため、このサンプルでは固定長とされています。

さてこの truncate 処理ですが、それ用の関数などはなく、上記のように自前で実装する必要があります。とはいっても見ての通りで num_steps 数分、一個ずつ入力データをずらして RNN セルに入力していっているだけです。注意点としては、後の評価やパラメーター更新に使用するため、出力される変数はなんらかの形で保持しおく必要がある点くらいです。

# Final output layer for getting word label 
output = tf.reshape(tf.concat(1, outputs), [-1, self._hidden_size])
softmax_w = tf.get_variable("softmax_w", [self._hidden_size, self._vocab_size])
softmax_b = tf.get_variable("softmax_b", [self._vocab_size])
self._logits = tf.matmul(output, softmax_w) + softmax_b

最後に、出力を単語ID に変換するための通常の全結合層を挿入して終了です。先ほど取得した num_steps 数分の出力を全て処理してやらなければならない点がややこしいですが、注意すべきはそこくらいとなります。このあと損失関数を定義し、最適化関数をはさんで Session.run を実行すれば学習は完了です。

予測された次の単語を取得する処理も、logits をミニバッチおよびステップ毎に argmax などで候補となる単語IDに変換してやればOKです。rnn_cell.py の中身を熟読すればもっとシンプルに書けそうですね。

TensorFlow basic RNN sample

AWS の GPU インスタンスで TensorFlow を動かしてみた(2016.07)

前回は TensorFlow のチュートリアルを触ってみたわけですが、当然のごとく手元のノートPCではさほど速度を出すことができません。DNNをあつかう宿命としてGPU上で動作させることは避けては通れないものです。

というわけで、GPU で TensorFlow を動かすために AWSインスタンスをたてようと試みたわけですが、Chainer の時のようにさくっとは行かず。。。

どうやら依存している CUDA のバージョンの関係で、AWSNVIDIA インスタンスでは動作させることができないようです。じゃあ適当なインスタンスに CUDA 入れればいいだけじゃんと油断したのが裏目に出てしまい、色々と手こずってしまったので、個人的な備忘録としてGPU インスタンスで TensorFlow を動作させるまでの手順を残しておきたいと思います。

TensorFlow ってどういうことができるのって方は、まずは下記の記事をお読みの上で以降に進まれることをおすすめします。

前提

今回利用したもののバージョン等は下記のようになります。

cuDNN をインストールするには、NVIDIA のサイトから利用申請をしておく必要がありますので、処理を進める前に早めに完了しておくことをおすすめします。 以降は、TensorFlow のDownload and SetupOptional: Install CUDA (GPUs on Linux) を元に進めていきますので、もしバージョンが変わって必要な処理が変わっている場合は、リンク先の情報に従って処理を行うことをおすすめします。

前準備

処理に必要なもののインストールおよびモジュールのアップデートを行います。Python 3.4 の場合は最低限必要な下記をインストールしておいて貰えば十分です。

sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install python3-dev python3-pip git -y

CUDA のインストール

最初で最後のハマりポイントと言っても過言ではありません。ここで、特に何も考えずに言われる通りに CUDA をインストールしてしまうとえらい目に合います。Nouveau をブラックリストに入れ、CUDA と一緒にインストールされる NVIDIA ドライバと衝突しないようにしてやる必要があります。 Nouveau を無効にするにあたって下記の記事を参考にさせていただきました。

Nouveau の無効化は上記の記事のものをそのまま利用して、以下の処理を実行することで行うことができます。

echo -e "blacklist nouveau\nblacklist lbm-nouveau\noptions nouveau modeset=0\nalias nouveau off\nalias lbm-nouveau off\n" | sudo tee /etc/modprobe.d/blacklist-nouveau.conf
echo options nouveau modeset=0 | sudo tee -a /etc/modprobe.d/nouveau-kms.conf
sudo update-initramfs -u
sudo reboot

sudo apt-get install -y linux-image-extra-virtual
sudo reboot

上記で Nouveau を無効化後、下記を実行することで、Ubuntu x64 環境向けの CUDA 7.5 をインストールすることができます。ビルドに必要となる Linux ソースのインストール以外は、CUDA のダウンロードサイトに記載されている内容をそのまま実行しているだけですので、手順が変更されている場合は、そちらに記載の方法をまず実行することをおすすめします。

sudo apt-get install -y linux-source linux-headers-`uname -r`

wget http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1404/x86_64/cuda-repo-ubuntu1404_7.5-18_amd64.deb
sudo dpkg -i cuda-repo-ubuntu1404_7.5-18_amd64.deb
sudo apt-get update
sudo apt-get install cuda -y

※ 2016/10/8 追記 なぜかネットワークインストーラでは最新のCUDAが入ってしまい、TensorFlow がうまく動かない事があるようです。 少々サイズが必要になりますがローカルインストーラをダウンロードしてきてCUDAをインストールすることをおすすめします。

インストール完了後、nvidia-smiコマンドを実行し下記のように出力されれば CUDA のインストールは成功です。

nvidia-smi

+------------------------------------------------------+                       
| NVIDIA-SMI 352.93     Driver Version: 352.93         |                       
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GRID K520           Off  | 0000:00:03.0     Off |                  N/A |
| N/A   38C    P0    38W / 125W |     11MiB /  4095MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID  Type  Process name                               Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

cuDNN のインストール

cuDNN のインストールは、CUDA のインストールパスに cuDNN 用のファイルを配置することで実現できます。あらかじめ cuDNN のページから Linux 向けの cuDNN v4 をローカルにダウンロード後、scp でインスタンスのホームにファイルを配置しておいてください。あとは、Optional: Install CUDA (GPUs on Linux)を参考にファイルを配置すればOKです。

tar xvf cudnn-7.0-linux-x64-v4.0-prod.tar
sudo cp cuda/include/cudnn.h /usr/local/include
sudo cp cuda/lib64/libcudnn* /usr/local/cuda/lib64/

※ 2016/10/8 追記 TensorFlow のバージョン 0.10.0 以降では、cuDNN の ver.5.x が推奨となっていますので、そちらをインストールすることをおすすめします。

TensorFlow のインストール

最後は、TensorFlow をインストールして完了です。Download and Setupを参考に pip で Python3.4 向けにインストールする処理は下記のとおりです。他のバージョンや環境でインストールする場合は、その場合の処理が記載されていると思いますので、そちらを参考に行ってください。

export TF_BINARY_URL=https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.9.0-cp34-cp34m-linux_x86_64.whl
sudo pip3 install --upgrade $TF_BINARY_URL

インストール完了後、~/.bash_profile などに下記の(Optional, Linux) Enable GPU Support に記載の通りに、GPUを利用するためのおまじないを記載して実行すれば完了です。

export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/cuda/lib64"
export CUDA_HOME=/usr/local/cuda

動作確認

これでインストールは完了です。ちゃんとインストールされているかをTest the TensorFlow installationにそって確認してみましょう。

まずは、GPU を使用する設定がちゃんと動作するかを確認します。下記のコマンドを実行して、CUDA をロードしているようなログが出れば成功です。

python3 -c 'import os; import inspect; import tensorflow; print(os.path.dirname(inspect.getfile(tensorflow)))'

I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcublas.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcudnn.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcufft.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcuda.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcurand.so locally
/usr/local/lib/python3.4/dist-packages/tensorflow

続いて処理をちゃんと行えているかを見てみましょう。チュートリアルにそって MNIST の CNN モデルをロードする処理で確認します。下記のコマンドを実行後、Found device 0 with properties:のようなログが出ていれば GPU をロードして処理が行われています。

python3 -m tensorflow.models.image.mnist.convolutional 

I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcublas.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcudnn.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcufft.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcuda.so locally
I tensorflow/stream_executor/dso_loader.cc:108] successfully opened CUDA library libcurand.so locally
Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting data/train-images-idx3-ubyte.gz
Extracting data/train-labels-idx1-ubyte.gz
Extracting data/t10k-images-idx3-ubyte.gz
Extracting data/t10k-labels-idx1-ubyte.gz
I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:924] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
I tensorflow/core/common_runtime/gpu/gpu_init.cc:102] Found device 0 with properties: 
name: GRID K520
major: 3 minor: 0 memoryClockRate (GHz) 0.797
pciBusID 0000:00:03.0
Total memory: 4.00GiB
Free memory: 3.95GiB
I tensorflow/core/common_runtime/gpu/gpu_init.cc:126] DMA: 0 
I tensorflow/core/common_runtime/gpu/gpu_init.cc:136] 0:   Y 
I tensorflow/core/common_runtime/gpu/gpu_device.cc:806] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GRID K520, pci bus id: 0000:00:03.0)
Initialized!
Step 0 (epoch 0.00), 9.4 ms
Minibatch loss: 12.054, learning rate: 0.010000
Minibatch error: 90.6%
Validation error: 84.6%

最後に、チュートリアル外ですがちゃんと速度がでるようになったかを確認しましょう。今回は、TendorFlow のコードに付属している CIFA のサンプルコードで確認を行っています。下記のように学習を実行すると、minibatch 単位で学習過程をログに流してくれます。g2.2xlarge ではだいたいbatch あたり 0.2 sec で学習を進めてくれるようになります。

git clone https://github.com/tensorflow/tensorflow
cd tensorflow/tensorflow/models/image/cifar10/
python3 cifar10_train.py

TensorFlowでDNNをスクラッチするためのオレオレチュートリアル

はじめに

Chainer もなんとなしに慣れてきたので(使いこなせているレベルではありませんが)、他のDNN Framework も触ってみようと思いたち、昨日からTensorFlowをいじってみています。 最終的なアウトプットは同じなので、クラス構造などが結構似通っていて、思ったより学習コストが低かったのですが、一点苦戦したのがTensorFlowのチュートリアルの微妙さです。

以上...!

といった感じで、普通のDNNはどう書いたらいいの?な質問に答えてくれるのにぴったりなコンテンツが残念ながらありません。 Easy ML with tf.contrib.learn あたりが求めているものに近いのですが、tf.contrib.learn という3分クッキング用モジュールを使用しているため、汎用性のある使い方を知ることができません。

仕方がないので自力でどうにかするしかないかということで、tf.contrib.learn Quickstart を tf.contrib.learn モジュールを一切使用せずに置き換えたチュートリアルを作ってみました。

ゴール

tf.contrib.learn Quickstart に掲載されている、アイリスデータを3つの中間層を持つDNNで学習し精度と予測結果を出力するコードを、tf.contrib.learn モジュールを一切使用せずに置き換えます。

イメージとしては、元のコードとほとんど同じ形式で処理を呼び出し、同じ結果が得られるようなコードを実装していきます。

def main(args):
    # Load datasets.
    x_train, y_train = load_csv(filename=IRIS_TRAINING)
    x_test, y_test = load_csv(filename=IRIS_TEST)

    # Build 3 layer DNN with 10, 20, 10 units respectively.
    classifier = Classifier(hidden_units=[10, 20, 10], n_classes=CLASS_SIZE)

    # Fit model.
    classifier.fit(x_train, y_train, steps=200)

    # Evaluate accuracy.
    accuracy_score = classifier.evaluate(x_test, y_test)[0]
    print('Accuracy: {0:f}'.format(accuracy_score))

    # Classify two new flower samples.
    new_samples = np.array([[6.4, 3.2, 4.5, 1.5], [5.8, 3.1, 5.0, 1.7]], dtype=float)
    y = classifier.predict(new_samples)
    print ('Predictions: {}'.format(str(y)))

ひとまず完成品はTensorFlow basic DNNに掲載してありますので、コード読めばわかるという方はそちらをどうぞ。

TensorFlowのバージョンはr0.9、実行環境はMac x64 の CPUになります。基本的なことしかしていないので、環境やバージョンを変えても動くとは思います(思いたいです)。

データの読み込み

まずは、必要なデータの読み込みを行っていきましょう。クイックスタートのコードはここでさっそく専用のcsvロード関数を使用しています。

とはいえやっていることは簡単で、読み込んでいるアイリスデータのcsvの書き方に依存してはいますが、下記のように読み込んだデータを説明変数群と目的変数とに分離しているだけです。データのヘッダ部分はサンプル数および説明変数の数が記載されているので、データ配列を初期化するために読み込む必要があります。

def load_csv(filename):
    file = pd.read_csv(filename, header=0)

    # get sample's metadata
    n_samples = int(file.columns[0])
    n_features = int(file.columns[1])

    # divide samples into explanation variables and target variable
    data = np.empty((n_samples, n_features))
    target = np.empty((n_samples,), dtype=np.int)
    for i, row in enumerate(file.itertuples()):
        target[i] = np.asarray(row[-1], dtype=np.int)
        data[i] = np.asarray(row[1:n_features+1], dtype=np.float64)
    return (data, target)

DNNモデルクラスの作成

データは問題なく読み込むことができたと思うので、ここから最も重要なモデルクラスを実装していきましょう。TensorFlow にはモデルのベースとなるようなクラス(Chainer の Chain クラス等)のようなものはありませんが、使い方が分かってしまえば特に苦労せず使いまわしやすい実装を行うことができます。

上記のクイックスタートのような処理を行うには、学習と予測、精度算出ができればよいので、早速1つずつ実装していきましょう。

学習モデルの作成

学習を始める前に必要なのは学習モデルを構築することです。TensorFlowではVariableクラスを作成して、それぞれの Variable オブジェクトのリンク関係を定義することによりモデルを構築していきます。3つの中間層を持つDNNは下記のようなコードで表現することができます。

また、Placeholderと呼ばれる学習データを扱うための変数を定義する必要があり、下記のコードでは引数xがそれに当たります。

簡単にコードを説明していきましょう。見たまんまなのですが、入力層の後、3層の中間層、softmax出力層のあるDNNを定義しています。いずれも入力値xに重みWをかけバイアスbを足して活性化関数を通したあとに次の層に渡すという構成になっています。活性化関数は中間層の第一層は恒等関数、第二層と第三層はReLU、出力層はsoftmaxを使用しています。

重みやバイアスを定義する際に使用している tf.truncated_normal や tf.zeros は変数初期化用に用意されている関数で、numpy の初期化関数のように使うことができます。tf.truncated_normal のstddev を求めるための get_stddev 関数は元のコードと結果が近くなるように tf.contrib.layers.initializerz を参考に実装しています。効率良く学習するための変数初期化方法の例がのっていますので、興味がある方は眺めてみることをオススメします。

name_scope の内側で各層の変数を定義していますが、このことにより階層構造で内部に変数を記憶させることができます。

注意すべき点は、学習する際にバッチ単位で毎回下記の関数が呼ばれるわけではなく、Theano 等のようにあくまでこのコードではリンク関係を定義しているだけで、実際の計算自体は TensorFlow の内部で行われるということです。そのため学習途中のパラメーターを見たい場合は、下記のコードにprint文を追加することでは実現できず、Summary クラスを利用して観測するなど独特な方法で実現する必要があります。

def inference(self, x):
    hidden = []

    # Input Layer
    with tf.name_scope("input"):
        weights = tf.Variable(tf.truncated_normal([IRIS_DATA_SIZE, self._hidden_units[0]], stddev=get_stddev(IRIS_DATA_SIZE, self._hidden_units[0])), name='weights')
        biases = tf.Variable(tf.zeros([self._hidden_units[0]]), name='biases')
        input = tf.matmul(x, weights) + biases

    # Hidden Layers
    for index, num_hidden in enumerate(self._hidden_units):
        if index == len(self._hidden_units) - 1: break
        with tf.name_scope("hidden{}".format(index+1)):
            weights = tf.Variable(tf.truncated_normal([num_hidden, self._hidden_units[index+1]], stddev=get_stddev(num_hidden, self._hidden_units[index+1])), name='weights')
            biases = tf.Variable(tf.zeros([self._hidden_units[index+1]]), name='biases')
            inputs = input if index == 0 else hidden[index-1]
            hidden.append(tf.nn.relu(tf.matmul(inputs, weights) + biases, name="hidden{}".format(index+1)))

    # Output Layer
    with tf.name_scope('output'):
        weights = tf.Variable(tf.truncated_normal([self._hidden_units[-1], self._n_classes], stddev=get_stddev(self._hidden_units[-1], self._n_classes)), name='weights')
        biases = tf.Variable(tf.zeros([self._n_classes]), name='biases')
        logits = tf.nn.softmax(tf.matmul(hidden[-1], weights) + biases)

    return logits

いざ、学習

学習モデルができたところで、いよいよ訓練データを使用して学習していきましょう。学習に必要なものは、学習データと損失関数と最適化アルゴリズムです。

まず損失関数から見ていきましょう。損失関数はlossとして実装しています。出力値とラベルのベクトルの各行の積をとり、バッチ内での平均値を返す仕組みになっています。tf.reduce_mean は TensorFlow 内で Reduction と呼ばれる関数群の一つで、テンソルの各次元単位で平均を求めることができます。今回は何も指定していないので、全ての値の平均値を返すようになっています。

続いて、本丸のfit関数を見ていきましょう。最初にやっているのは、学習データを格納するための Placeholder の定義です。shapeの1番目は、バッチ学習用の次元なのでサイズを None に指定しています。次に、定義した Placeholder を上記で作成した学習モデルに通すことで結果が、結果と正解ラベルを損失関数に通すことで損失値が求められるというリンク関係を定義しています。最後に、SGDを使用して損失値が最小化するように学習させるという学習用オペレーションを定義して準備は完了です。

準備が完了したら、変数の更新や計算を行うための Session オブジェクトの用意です。Session オブジェクト自体はクラスの初期化時に作成するようにしていますので、ここでは Session オブジェクトを利用して定義済みの変数の初期化のみを行っています。

変数の初期化まで完了して、いよいよ学習処理を行うことができます。この処理では、指定されたstep回数分学習用オペレーションを実行します。この時、feed_dict に渡しているのは、Placeholder オブジェクトを Key に、実際に使用する学習データを Value に格納した辞書オブジェクトとなります。これにより、与えた学習データによる更新処理を一発で行ってくれます。

get_batch_data でバッチデータを作成しているように見せかけていますが、今のところ全てのデータを一回のバッチとして扱うような実装になっています。そのため、説明変数はそのまま、従属変数もカテゴリ値を 1-of-N なベクトルに変換して返す処理のみを行っています。

一部の Variable オブジェクトをクラス内変数に保存していますが、これは評価用関数でも使い回すための手順なので学習には直接関係ありません。(使いわすための方法としてこれが最適なのかも微妙です。もっと良い書き方がある気がします。)

def loss(self, logits, y):        
    return -tf.reduce_mean(y * tf.log(logits))

def fit(self, x_train=None, y_train=None, steps=200):
    # build model
    x = tf.placeholder(tf.float32, [None, IRIS_DATA_SIZE])
    y = tf.placeholder(tf.float32, [None, CLASS_SIZE])
    logits = self.inference(x)
    loss = self.loss(logits, y)
    train_op = tf.train.GradientDescentOptimizer(0.5).minimize(loss)

    # save variables
    self._x = x
    self._y = y
    self._logits = logits

    # init parameters
    init = tf.initialize_all_variables() 
    self._sess.run(init)

    # train
    for i in range(steps):
        batch_xs, batch_ys = get_batch_data(x_train, y_train)
        self._sess.run(train_op, feed_dict={x: batch_xs, y: batch_ys})

テストデータによる評価

最後にテストデータによる評価に必要な関数を実装していきましょう。テストデータによる正解率の算出とクラス識別を行っていますが、いずれも上記の学習処理内で保存した変数を利用していることに注意してください。x は説明変数、yは目的変数、_logitsはモデルからの出力を表す変数として、それぞれ定義していました。その変数をそのまま流用していますので、このリンク関係をそのまま利用することができます。

識別用の関数が分かりやすいのでそちらから見ていきましょう。識別関数では、出力層の3つのノードからもっとも値の大きいノードを取得しています。この処理を tf.argmax で生成した Variable オブジェクトが、Session.run 関数で入力に指定されたデータを渡した時にどのような値になるかを計算することにより求めています。

正解率の算出用関数も要領は同じです。正解率を求める過程と結果の Variable オブジェクトを定義し、テスト用データでその値を求めるという構造になっています。

上記で行ったモデルの学習も含め、TensorFlowでは、変数のリンク関係を定義し、求めたい変数と計算に用いる入力値を Session.run 関数に渡すことによりその変数の値を計算することができます。

def evaluate(self, x_test=None, y_test=None):
    x_test, y_test = get_test_data(x_test, y_test)

    # build accuracy calculate step
    correct_prediction = tf.equal(tf.argmax(self._logits, 1), tf.argmax(self._y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    # evaluate
    return self._sess.run([accuracy], feed_dict={self._x: x_test, self._y: y_test})

def predict(self, samples):
    predictions = tf.argmax(self._logits, 1)
    return self._sess.run(predictions, {self._x: samples})

おわりに

以上で、TensorFlow を利用した基本的なDNNの作成は完了です。あとはモデルの定義をCNNやRNNに変更したり、損失関数を変更したり、最適化アルゴリズムを変更するだけで使いまわせるんじゃないかと思います。一覧性が微妙なAPIドキュメントという障害もありますが、ここまでこれば余裕ですよね!

ちなみにパラメーターや学習状況を観測したい場合は、TensorBoard機能を利用すれば良いようですので、興味のある方は僕に教えて下さい。

参考

GoogleのAIはどうやって「詩」を生成するのか

一ヶ月ほど前ですが、Google人工知能が「詩」を創りだしたという話が話題になりました。

wired.jp

この話の元ネタは、[1511.06349] Generating Sentences from a Continuous Spaceで発表されている論文になります。この論文では、ベイズとDeep Learningを組み合わせた生成モデルを使用して、文章のコンテキスト情報から文章を生成する手法を提案しています。この提案手法は2つの文章の間の文章を生成することも可能で、記事で紹介されている「詩」はそのようにして生成されたものの一つになります。

この記事では、上記の論文で提案されているアルゴリズムの導入から、それを利用してどのように「詩」を生成するかを解説していきたいと思います。

この論文の肝は、VAE(Variational Autoencoder:変分オートエンコーダ)を利用して、文章のオートエンコーダーをモデル化しているところです。エンコーダーでは文章のコンテキスト情報を潜在変数として出力し、デコーダーではこの潜在変数をもとに文章を生成するというモデルとなっています。

VAEは、観測変数から潜在変数を推論するモデルと、その逆の潜在変数から観測変数を生成するモデルを、別々に等しくなるように学習する確率モデルとなります。この確率モデルのパラメーターの決定は、ニューラルネット等により行うことも可能で、この論文でもニューラルネットを使用して学習しています。

VAEの基礎は変分ベイズ法で、潜在変数から観測変数を生成するモデル{p_{\theta}(z)p_{\theta}(x|z)}とその事後分布{p_{\theta}(z|x)}を変分近似する{q_{\phi}(z|x)}を考えます。データに対して逐次学習を行い、周辺尤度の変分下界を最大化することを目標に、適当なパラメーター{\theta}{\phi}が得られるようにニューラルネットを学習していきます。

VAEの学習手順は、(私の理解が正しければ)下記のとおりです。

  1. 観測変数{x_i}を用いて、ニューラルネットでパラメーター{\phi}を求める
  2. パラメーター{\phi}を用いて、{q_{\phi}(z|x)}から{z_i}をサンプリングする。
  3. 観測変数{z_i}を用いて、ニューラルネットでパラメーター{\theta}を求める
  4. 周辺尤度{p_{\theta}(x)}の変分下界をもとに{\theta}{\phi}の勾配を求める
  5. パラメーターの勾配を元にニューラルネットワークの更新を行う
  6. 飽きるまで繰り返す

※ サンプリングの方法とか勾配の計算方法の詳細は、元論文著者のPPTを参照ください。

あとはVAEを使って計算するだけかとおもいきや、シーケンシャルなデータを扱う手前そういうわけにはいきません。 論文では、VAEの前と後ろにRNNを配置するVRAE(Variational Recurrent Autoencoder)と類似のモデルを使用しています。VRAEでは、エンコーダーの最終出力を用いて、確率分布のパラメーターを推定し、そのパラメーターを使用して確率分布から潜在変数をサンプリングします。サンプリングされた潜在変数は、デコーダーの一回目の入力として使用されます。

VRAEの学習手順は、(こちらも私の理解が正しければ)下記のとおりです。

  1. エンコーダーに系列データを入力し{x_i}を求める
  2. {x_i}からVAEの方法にそって{z_i}を求める
  3. デコーダーに{z_i}を一回目の入力とし、誤差を求めつつ順次系列データを入力する
  4. 誤差からデコーダーのRNNを更新する
  5. VAEのニューラルネットのパラメーターをVAEの手順にそって更新する
  6. {z_i}を用いて{q_{\phi}(z|x)}から{x_i^{'}}を求める
  7. エンコーダーのRNNを更新する
  8. 飽きるまで繰り返す

提案されているモデルでは、VRAEとは異なり、エンコーダーにもデコーダーにも一層のLSTMを使用したRNNを用いています。また確率分布としては、生成モデルにも推論モデルにも正規分布を使用しています。さらに、学習時にはKLダイバージェンスの項にパラメーターを導入したり、潜在変数を用いたデコードを強化し不完全な文章に強くするため、わざとデコーダーに入力する文章を歯抜けにするなどの工夫を行っています。

上記の記事で紹介されている詩は、文1をエンコードして得られた潜在変数{z_1}と文2をエンコードして得られた潜在変数{z_2}から求めた{ z(t) = z_1 \ast (1-t) + z_2 \ast t}をデコードすることにより生成します。式からわかるように、{z(t)}{z_1}{z_2}を結ぶ線分上に存在するいずれかの点になります。

下の記事で紹介されている例を見ると分かりやすいですが、この{z(t)}を用いてデコードした文章は、文章構造やトピックが2つの文章に類似したものとなります。一方、このモデルで得られる潜在変数が獲得している情報は、あくまで一つの文章内での文章構造や単語の使われ方のみのため、文意にそって総合的な文を生成するということまでは残念ながらできていまいません。

www.gizmodo.jp

ここまで読んでいただければおわかりかと思いますが、このモデルは詩を生み出そうとして作られたものではなく、トピックや文章構造に焦点を当てて文章を生成するモデルを学習した結果、生成される文章群の特徴上「詩」のような文章が生成されるという結果となっています。(論文には詩という言葉は一言も出てきません。)

一方、論文の成果を利用する立場から見ると、ベクトルの演算により2つの文章の中間っぽい表現が得られることは、分散表現を用いて処理を行う上では使い勝手が良く興味深い結果ではないかなと思います。このモデルにより得られたベクトルをさらに違うモデルにくわせることにより、文意を組んだ文章全体を生成するようなモデルを学習することもできるのではないか、期待してみていきたいところです。

参考

第6回CiNetシンポジウムを聴講してきたよ

本日グランフロント大阪にて開催された、第6回 CiNetシンポジウム「おもろい脳科学」を聴講してきました。 脳科学はテレビや本で紹介されている以上の情報は知らなかったので、人工知能分野の研究成果を理解するには何が話題になっているかくらいはおさえておきたいと思い行ってきたしだいです。

個人的にはとても刺激的なお話を聞くことができました。詳しい方には物足りないかもしれませんが、私みたいにこの分野の情報を得たいという方には、濃い時間であったことは間違いないかなと思います。大阪と東京で一年交代で、毎年このような質の高いシンポジウムを無料で開催してくださっているらしく、CiNet さん、ありがとうございます。ぜひこれからも続けてください。

さて、せっかく聞いてきたので、忘れないうちに概要をまとめてみようと思います。

エキスパートの直感思考の仕組み

最初は、田中啓治先生のご講演でした。テーマは、エキスパートの直感思考はどのような仕組みで生まれているのかというものです。内容としては、 エキスパートの直観を司る脳の仕組み | 理化学研究所にあがっているものとだいたい同じようなものになります。

エキスパートとアマチュアの一番の違いは、読みの深さや処理の速さではなく、最善手が直感的に思いつく能力と、ゲーム中に出てくる盤面を瞬間的に知覚できる能力である。このことが70年代頃までに心理学の知見として得られていたそうです。

アマチュアは盤面を見てもすぐには最善手を思いつかず、エキスパートは盤面を見た瞬間すぐに最善手を思いつく。また、プロは場面を複数のコマのまとまった配置として把握するため、通常7つのパーツの組み合わせまでしか覚えられないような一時記憶で、盤面全体を把握できているとのことでした。さらにこの複数のコマをまとめて把握するという能力は、これらのコマの組み合わせと最善手を結びつけて覚えることにより、最善手を瞬時に閃くことにも貢献しているとの事のようです。

脳科学的なアプローチからこの直感を分析した結果、基底核と呼ばれる脳の奥の方にある部分が活性化することにより直感を生み出しているのではないかと考えられるそうです。この基底核はトレーニングを積むことにより回路を鍛えることができ、特徴的な刺激に対して反応を返すようになれるそうです。つまり、直感はトレーニングによって鍛えることができるとのことのでした。

スポーツと脳

次は、内藤栄一先生のご講演でした。テーマは、スポーツと脳の関係についてです。内容としてはだいたい新技術:ネイマールのテクニックの秘密は脳にある? 先端技術で解析 (1/2) - EE Times Japanに記載されているようなものをお話されていました。

ネイマール選手と他の選手を比べてわかったこと。それは、状況に対する想像力と、特定の活動を行う際に脳全体の活動量が小さいことが、トリッキーなパフォーマンスを生み出す鍵だということだそうです。

状況に対する想像力とは、例えば、ディフェンダーと対峙している場面を見てもらい、どのようにその場面を切り抜けるかを何パターンも考えてもらい、どれだけそれが具体的か、またその時どれだけ脳が活動しているかで測ります。ネイマール選手は、具体的なテクニック名を次々とあげることができ、脳の活動も他選手と比べ非常に活発であったようです。

一方、特定の活動時に脳の活動量が小さいとはどういうことかというと、足の先を動かすようなサッカーではよくある動作を行っている状態での脳の活動状態を計測したところ、ネイマール選手は、運動をするために一部の重要な部位しか活動しておらず、他の部分はほとんど活性化していなかったということのようです。身体運動は一般的に、脳のゆらぎによるノイズが発生し成果が安定しません。しかしネイマール選手の場合は、そのノイズとなる脳活動がほとんどなく機械的に特定の運動を行えるようになっており、動きの精度が非常に高くなっていると考えられるとのことでした。

脳派から自分の無意識を知る

成瀬康先生のご講演のテーマは、脳波から自分の無意識を知るというものでした。内容としては、 IEEEがプレスセミナーを開催 『ヒューマンインタフェースと脳科学の行方』|プレスリリース配信サービス【@Press:アットプレス】に記載されている、英語のリスニングの事例が主でした。

この英語のリスニングの事例では、LとRの聞き分けは脳レベルではそれなりに行えているというところからスタートします。ただしその時に発生する脳波の違いがわずかな場合は、その違いを意識することができていません。脳波の違いが大きくなった状態で、初めて聞き分けられるようになっていると言えるわけです。

紹介いただいた研究では、脳波を大きくすることを学習することにより聞き分けられるようになるかというものです。具体的には、LとRの発音の違いにより変化する脳波を得点化し、ユーザにゲーム感覚でその得点を最大化するように取り組んでもらったところ、LとRの聞き分けもできるようになったとのことでした。

脳の知覚の可視化

西本伸志先生によるご講演のテーマは、脳の認知内容をデコードするというものでした。内容としては、夢や空想を視覚化する研究はAIでさらに進化するのか ーワークショップ「人工知能による科学・技術の革新」ー - WirelessWire News(ワイヤレスワイヤーニュース)に記載されているものとだいたい同じになります。人工知能に興味がある人は似たような感じだと思いますが、個人的にはこの講演が一番興味深かったです。

概要としては、ある知覚をしている時の脳の活動情報を取得することにより、情報と脳の知覚のエンコーダーデコーダーを生成しようというものになります。本日はデコーダーについて主に話されていましたが、視覚再現地図があると言われている後頭葉の情報から今見ているものの画像を生成したり、物体に反応する部位があると言われている側頭葉の反応状況から脳内で知覚がどのように認識されているか、どのような関係として知覚されているかを抽出することができたとのことです。

具体的な内容はリンクの写真を見てもらうのが早いので割愛しますが、個人的には、脳が処理している内容を科学的にデコードでき得るということは、人工知能にとっては良いニュースだなと思いました。デコーダーができるということはエンコーダーもできることとほぼイコールなのですから、人工知能に必要となる脳活動を測定するというアプローチからも人工知能にせまることができるのです。構造をまねるという話は良く聞きますが、実際に使用されている数理モデルそのものを移植することができる可能性は、場合によっては最短距離で人工知能へ到達する道になるかもしれません。

ロボットのココロ

トリは、浅田稔先生のご講演でした。テーマはロボットのココロについて。内容としては、ロボットで探る人間の不思議(大阪大学・浅田稔教授) 2013/5/1 | WAOサイエンスパークの動画を見ていただくと把握できるものとだいたい同じものになります。

浅田稔先生はロボット工学がご専門で、ロボットを使用しての人工知能の実現を目指されている方という認識でしたが、脳科学からのアプローチもされているとは知りませんでした。本日の講演は、ロボットにココロを持たせるにはどうするのがよさそうかを、赤ちゃんの発達過程と人間が心と知性をどう認識しているかからご説明いただきました。

盛りだくさんすぎて内容をまとめにくいのですが、一言で言うならば人の心とは周囲との相互作用により発達していくものなので、ロボットにココロを持たせるにもそれが必要であろうということです。また、人間には心と知性を別のものとして認識する機構があり、ロボットにもそれが必要だよね、というようなお話でした。

まとめ

以上で、私のつたない言葉によるシンポジウムのまとめは終わりです。今日のシンポジウムのように人間の脳に何かしらの知覚構造があると理解するのは、自分が夢想している人工知能という課題が決して夢物語ではなく、いつか実現できるものであるということが実感できてとても良い機会でした。今まで知らなかった研究内容も知ることができますし、一石二鳥なイベントでした。

もし研究内容に興味を持たれたならば、ぜひ、それぞれのリンク先の記事や関連記事を読んでみることをおすすめします。

ベイズ統計を理解する(3) 〜 グラフィカルモデル 〜

Computer Vision Modelsのテキストとベイズ推定とグラフィカルモデル:コンピュータビジョン基礎1 | Udemyを使用して、ベイズ統計について説明するコーナーの第三弾は、グラフィカルモデルについてです。

グラフィカルモデルとは、確率変数同士の関係をグラフの形で表現したものとなります。 グラフなので、有向グラフと無向グラフが存在し、それぞれベイジアンネットワーク、マルコフネットワークと呼ばれています。

ベイジアンネットワーク

それでは、それぞれのグラフが表す確率分布を見ていきましょう。 まず、ベイジアンネットワークですが、下記のような式を満たす同時分布を表す事ができます。

 { Pr(x_1...x_N) = \prod_{n=1}^N Pr(x_n|x_{pa(n)})}

確率変数{x_1}から{x_N}までの同時分布は、それぞれの変数間の条件付き分布の積で表現できるという意味です。{pa(n)}は、変数{x_n}の親要素(tail部分が接続されている矢印のhead部にあたる変数)を表しています。

とは言うものの、字面だけで説明してはよくわからないので、例で見てみましょう。下記は、変数A,B,C,Dの関係を示したベイジアンネットワークになります。

f:id:KSKSKSKS2:20160605124056p:plain

このグラフで表されている同時分布は下記のように書くことができます。

{Pr(A,B,C,D) = Pr(A)Pr(C|A)Pr(B)Pr(D|B,C)}

対応関係は見ての通り、親がいない(入ってくる矢印のない)要素の確率分布は{Pr(A)}のようにそのまま使用されます。また、親がいる(入ってくる矢印のある)要素の確率分布は{Pr(C|A)}のように、親要素の条件付き分布として表現されます。

また、ある変数Aとある変数Bが条件付き独立であるかどうかをグラフで表現することができます。A,Bが変数Cで条件付き独立となるのは、A,B間にCがある場合(head-to-tail)、A,Bの親要素がCである場合(tail-to-tail)、CがA,Bの子要素もしくは子要素の子孫要素でない場合(head-to-head)です。ちなみに、head-to-headの場合は条件付きでない場合は独立になりますので注意してください。

ベイジアンネットワークでは、下記のように添字付き変数をプレートで表現したり、確率変数以外の変数との関係を表す事ができます。

f:id:KSKSKSKS2:20160605130002p:plain

上記の図は、混合正規分布ベイジアンネットワークで表現したグラフになります、青枠で囲まれた部分をプレートと呼び、添字付き変数同士の関係を示すものです。図では、{h_i}{x_i}の間に依存関係があり、他の添字の変数とは依存しないということが表現されています。また、黒丸で表現されている変数は確率変数でない変数で、正規分布のパラメーターや重みが表現されています。

マルコフネットワーク

次にマルコフネットワークです。マルコフネットワークは、確率変数間の関係を何らかの関数で表現し、その積で同時分布を表現したグラフとなります。式で表すと下記のようになります。

{Pr(x_1...x_N) = \frac{1} {Z} \prod_{c=1}^C \phi_c(x_1...x_N)}

Z は partition function と呼ばれ、確率分布にするための正規化項です。{\phi_c}は potential function と呼ばれ、必ず正の値を取る関数です。マルコフネットワークは、一般的に下記のように対数を取りギブズ分布の形で表されます。

{Pr(x_1...x_N) = \frac{1} {Z} exp(-\sum_{c=1}^C \psi_c(x_1...x_N))}

{\psi_c(x_1...x_N) = -log(\phi_c(x_1...x_N))} と定義しコスト関数と呼ばれ、定義からわかるように実数値を返します。こちらもグラフの例を見てみましょう。

f:id:KSKSKSKS2:20160605131746p:plain

このグラフで表現される同時分布は、下記の通りです。

{Pr(x_1...x_N) = \frac{1} {Z} \phi_1(A,B,C)\phi_2(B,E)\phi_3(C,D)\phi_4(D,E)}

グラフ理論では、完全部分グラフのことをクリークと呼びます。マルコフネットワークでは、クリークごとに一つポテンシャル関数やコスト関数を定義し、その積で同時分布を表現します。ただし、実装上はエッジごとにポテンシャル関数やコスト関数を定義するようになっている場合が普通です。

マルコフネットワークの場合もグラフから条件付き独立かどうか判定することができます。ベイジアンネットワークの時と比べその判断は非常に簡単で、変数間にエッジがない場合は条件付き独立となります。

ベイジアンネットワークとマルコフネットワーク、どちらも確率変数同士の関係をグラフで表す手法であり、どちらで表現するのが良いかは難しい問題です。一つの判断基準としては、表現可能な条件付き独立の関係があります。

例えば、変数AとBは通常は独立だが変数Cが観測されている場合は条件付き従属となる関係を表現したい場合、ベイジアンネットワークでしか表現することができません。一方、変数A,Bは変数C,Dの元で条件付き独立であり、変数C,Dは変数A,Bの元で条件付き独立であるような関係は、マルコフネットワークでしか表現することができません。このようにグラフの特性を把握して、モデリングしていく必要があります。

内部状態の推定とサンプリング

グラフィカルモデルでの目的は、一般的なベイズモデルと同じく、観測変数{x_1...x_N}を与えられた時に、状態{w_1...w_N}を事後分布から推定することにあります。しかしながら、複雑なグラフィカルモデルにおいて事後分布を求めることはほぼ不可能です。そこで代替案として用いられるのが、MAP推定値、周辺事後分布、周辺事後分布の推定値、そしてサンプリングです。

サンプリング以外の方法は、事後分布と同様、計算が難しく、またそれぞれ推定結果に制約が与えられてしまいます。一方、サンプリングは代表値を取得するだけでなく、複数回サンプリングを行い事後分布の近似を作成することもでき、演算も高速であることからよく使われています。HMMなどのチェインモデルやツリーモデルのようにMAP推定値を計算できるような特殊な形のネットワークを除いては、サンプリングにより推定値を求めることが一般的です。

ベイジアンネットワークでは、サンプリングとして伝承サンプリング(Ancestral sampling)が用いられます。このサンプリング方法は、ベイジアンネットワークの一番起点となる変数から1つずつサンプリングしていき、全ての変数の値をサンプリングするという方法です。

マルコフネットワークでは、サンプリングとしてMCMC(マルコフ・チェーン・モテカルロ)が用いられます。このサンプリング方法には、いろいろとアルゴリズムがあるのですが、よく使われる Gibbs Smapling について説明します。Gibbs Smapling では、一つの変数以外の変数を固定し、その変数をサンプリングするという作業を繰り返し行い、変数セット全てをサンプリングします。

サンプリング時に気をつけることは、変数の初期値にサンプリング結果が影響されないように、Burn-in と呼ばれる、十分なサンプリングをした後に実際に使用するサンプリング点を取得する方法を行う必要がある点と、複数のサンプル点を使用する場合は、それらの間に相関関係が発生しないように、十分にサンプリング間隔をあけたサンプル点を使用する必要があるという2点です。

Gibbs Smaplingでは、サンプリングしたい変数を他の変数で条件付けした条件付き分布を求める必要がありますが、マルコフネットワークでは直接接続されているノード以外とは条件付き独立であるため、条件付き分布を簡単に利用することができます。しかし、MCMCは伝承サンプリングに比べ計算回数が圧倒的に多いため、リソースや計算時間を必要とするといった欠点もあります。また、条件付き分布を求めることが容易でないケースもあり、その場合は条件付き分布を近似した分布を用いてサンプリングを行う、メトロ・ポリス法などがあります。

グラフィカルモデルの学習

さて、上記ではグラフィカルモデルを使用してどのように未知変数を推定するかについて説明しましたが、グラフィカルモデルを使用するにはご多分にもれず学習をしてパラメーターの推定、もしくはパラメーター分布の推定を行う必要があります。もちろん、学習を一切行わず、利用者が決めた確率分布に従って推定を行うようにすることもできますが、ノードの数が多くなればなるほどそれは現実的ではありません。

とはいうものの、複雑な構造をしていますので、パラメーター学習も一般的に難しいものとなります。テキストでもあまり触れられていませんので、また別の機会に詳細を書きたいと思います。

有名なグラフィカルモデル

グラフィカルモデルの形として有名なものに、チェインモデル(HMM)、ツリーモデル、グリッドモデルがあります。

HMMは、観測できない状態wによって観測変数xが生成され、次状態{w_{n+1}}は現状態{w_n}に依存するという形をモデル化したもので下記のような形となります。

f:id:KSKSKSKS2:20160605154000p:plain

このHMMや前状態が複数あるツリーモデルにて、状態wを推定するのに用いられる方法はいくつかあり、MAP推定を行う場合は動的計画法が用いられ、周辺事後分布を推定する場合はForword-BackwordアルゴリズムやSum-productアルゴリズムなどが用いられます。

HMMの学習は、学習セットとして内部状態も与えられている(Supervisedな)場合と、与えられていない(Unsupervisedな)場合とで分けて考える必要があります。Supervisedな場合は、ML推定、MAP推定、ベイズ推定などを用いて比較的容易に学習することができます。Unsupervisedな場合は、内部状態wを隠れ変数として扱いEMアルゴリズムで学習を行うのが一般的です。

マルコフネットワークでよく使われるグリッドモデルは、その中でもマルコフ確率場(MRF)と呼ばれるモデルが良く使われます。MRFでは、ノードが格子状に接続された無向グラフで、ノードは内部状態を表す未観測の確率変数として扱います。そして、その状態に応じてノードごとに1つずつ観測変数が現れるという形をモデル化したものです。

f:id:KSKSKSKS2:20160605154406p:plain

MRFで状態wをMAP推定するために、グラフカットやα-拡張という手法が用いられます。この手法では、劣モジュラであれば厳密解を劣モジュラでなくても良い近似解を得ることができます。

上記で説明した推定方法はここでは詳しく説明しませんが、テキストには詳細が記載されていますので興味がある方はぜひ読んでみてください。

終わりに

この記事では、グラフィカルモデルについて説明しました。ベイジアンネットワーク、マルコフネットワークそれぞれで未観測変数がどのように推定されるのかを説明しました。

そして、全3回とだいぶ長々と書いてきましたが、ひとまずベイズ統計およびそれを利用した機械学習の基本の説明記事は今回にて終了です。変分ベイズ法とか、グラフィカルモデルの学習手法とか、ベイズによる教師なし学習手法とか、まだまだ抑えておきたい部分はありますが、今までの3記事の内容を理解しておけば、とりあえずベイズが分からないせいで何を言っているか分からないということにはならないだろうと思います。

来週からは、また Deep Learning 周りの話に戻りたいと思います。

ベイズ統計を理解する(2) 〜 判別モデルと生成モデル 〜

先週に引き続き、Computer Vision Modelsのテキストとベイズ推定とグラフィカルモデル:コンピュータビジョン基礎1 | Udemyを使用して、ベイズ統計について勉強中です。今週は、判別モデルと生成モデル、それぞれのモデリング方法での基礎的な学習方法を、回帰と識別それぞれのケースでどのように行うかを書いていきたいと思います。

ベイズ推論とモデリング

まずベイズを用いた学習では、観測変数xが与えられた時に、その時の状態wを求めるためのモデルPr(w|x)を学習により獲得することを目標とします。この事後分布Pr(w|x)を利用するプロセスは、

  1. xとwを関連付けるモデルをパラメーターθを用いて表す

  2. 学習データからパラメーターθを学習する

  3. 与えられた観測データxからPr(w|x,\theta)を計算する。その計算が難しい場合は、サンプル結果やMAP推定値を返す

といったようになります。

1.で使用するモデリングの方法には、Pr(w|x)を直接モデル化しパラメーターを学習する方法(判別モデル:Discriminative Model)と、Pr(x|w)をモデル化しパラメーターを学習する方法(生成モデル:Generative Model)の2つがあります。

判別モデルでは直接推論を行うことができるので簡易でありまた計算コストも抑えることができます。一方、生成モデルはデータセットをサンプリングでき、また推論時にwの事前分布を使用できるため、ノイズの多いデータでは判別モデルより頑健性の高い結果を得ることができます。

それぞれのモデルで、回帰と分類で行われるモデリングの例を見ていきましょう。

まず、回帰の例です。判別モデルでは

{ Pr(w|x,\theta) = Norm_w(\phi_0 + \phi_1 x, {\sigma}^2) }

と表現することができます。この場合、\sigma^2が0であれば、w = \phi_0 + \phi_1 xとなることからわかるように、線形回帰モデルとしてxとwの関係が表現されています。もちろん、Pr(w|x)正規分布であると仮定する必要もありませんし、wとxの関係が線形なものである必要も、分散に影響を与えないようなモデルにする必要もありません。モデル化後は、お好きな推定手法を用いてパラメーターθを推定すればOKです。

一方、生成モデルでは

{ Pr(x|w,\theta) = Norm_x(\phi_0 + \phi_1 w, {\sigma}^2) }

と表現することができます。見ていただいたように、xとwが反転しているだけで、これも線形回帰モデルとなります。この場合も基本的にモデリング自体はもっと自由に行うことができます。生成モデルが判別モデルと異なるところは、事前分布を仮定してやる必要がある点です。この場合、事前分布Pr(w)正規分布として仮定しており、必要に応じてパラメーターを学習する必要があります。

次に、分類の例です。判別モデルでは、

{ Pr(w|x,\theta) = Bern_w(sig(\phi_0 + \phi_1 x)) }

と表現することができます。この場合、Pr(w)をベルヌーイ分布で仮定し、wとxの関係をシグモイド関数で表現していることから、ロジスティック回帰となります。もちろん、分布や関係式は自由に設定することができます。

一方、生成モデルでは大きく異なり、

{ Pr(x|w,\theta) = Norm_x(\mu_w, {\sigma}^2_w) }

と表現されます。これは、wの値ごとにxの分布が変化することを確率密度関数として表しています。この時、Pr(w)はベルヌーイ分布で表現し、このパラメーターも必要に応じて学習する必要があります。

生成モデルによる分類

生成モデルによる分類は、確率密度関数の推定という形で行われます。

例えば、Pr(x|w) = Norm_w(\mu_w, \Sigma_w)とwの値ごとの確率変数xの確率密度関数を定義します。この時最尤推定を行うことにより、推定値を

\hat{\mu}_w, \hat{\Sigma}_w = argmax(\prod_{i \in S_w} Pr(x_i | \mu_w, \Sigma_w))

と求めることができます。S_wはカテゴリがwである観測データの集合であるとします。つまり二値分類でいうと、w=0の場合のパラメーターとw=1の場合のパラメーターはそれぞれのカテゴリに分類される観測データxから別々に推定されることになります。

最後に評価を行う場合は、Pr(w) = Bern_w(\lambda)を利用して、Pr(w=0|x)Pr(w=1|x)ベイズルールで求め利用します。ちなみに\lambdaの値も学習データを利用して、何かしらの方法(例えば頻度とか)で決めておく必要がありますが、以降でもあまり話題には上がりませんので「何か決まってるんだなー」くらいに思っておいてください。

以上で生成モデルによる分類を行う基本的な方法を説明しましたが、尤度が正規分布で表現されるため、この場合認識精度はそこまでよくはなりません。精度を上げるには一般的に尤度に複雑な分布を利用する必要がありますが、その場合、通常の方法では推定値を求めることができません。

そこで利用するのが、隠れ変数とEMアルゴリズムです。

隠れ変数の考え方は非常にシンプルです。尤度関数を

Pr(x|\theta) = \int Pr(x,h|\theta) dh

と隠れ変数hをおいて定義します。この時最尤推定を行うことにより、以下のように推定値を求めます。

\hat{\theta} = argmax(\sum_{i=1}^I log(\int Pr(x_i,h_i|\theta) dh_i ))

このように隠れ変数を設定すると、単純なモデルを利用してモデルの表現力を上げることができます。例えば、以下で説明する正規混合分布では、h=1の時の分布、h=2の時の分布といったように複数正規分布を利用し、それを足し合わせることにより複雑な分布を表現することができています。

しかし、上記のような積分の入った式では解析的に簡単にパラメーターの推定を行うことができません。そこで利用されるのがEMアルゴリズムと呼ばれる手法です。EMアルゴリズムでは、下界(Lower Bound)の考え方を利用して、パラメーターを段階的に更新していきます。この手法で一番重要な式は、下記の下界の定義式です。

 {LB(q_i(h_i), \theta) = \sum_{i=1}^I \int q_i(h_i) log(\frac{Pr(x_i,h_i|\theta)} {q_i(h_i)}) dh_i ) \le \sum_{i=1}^I log(\int Pr(x_i,h_i|\theta) dh_i )}

ちなみに、\int q_i(h_i) log(\frac{Pr(x_i,h_i|\theta)} {q_i(h_i)}) dh_iはKLダイバージェンスと呼ばれるもので、確率分布の非類似度を表す情報量の一つです。EMアルゴリズムはこの下界を最大化する分布q_i(h_i)とパラメーターθを交互に求めます。Eステップでは、

 \hat{q}_i(h_i) = Pr(h_i|x_i,\theta^t) = \frac{\Pr(x_i|h_i,\theta^t)Pr(h_i|\theta^t)} {Pr(x_i)}

\theta^tを固定して \hat{q}_i(h_i)を求めます。Mステップでは、

\hat{\theta}^{t+1} = argmax(\sum_{i=1}^I \int \hat{q}_i(h_i) log(Pr(x_i,h_i|\theta)) dh_i ))

のようにパラメーターの推定値を求めます。この計算を繰り返し、パラメーターをより元の尤度関数を最大化するパラメーターに近づけていきます。この手順は、Eステップで下界の関数を更新し、Mステップでその関数を最大にするパラメーターを求めていることに相当します。

では、混合正規分布を例に、EMアルゴリズムの手順を見ていきましょう。混合正規分布は、複数正規分布の和で確率分布を表現するものとなります。求める確率分布は

Pr(x|\theta) = \sum_{k=1}^K \lambda_k Norm_x(\mu_k,\Sigma_k)

と定義できます。\lambdaは合計すると1になる値です。この時、

Pr(x|h,\theta) = Norm_x(mu_h,\Sigma_h)

Pr(h|\theta) = Cat_h(\lambda)

と定義することができる。Eステップは、

\hat{q}_i(h_i) = \frac{\lambda_k Norm_{x_i}(\mu_k,\Sigma_k)} {\sum_{j=1}^K \lambda_j Norm_{x_i}(\mu_j,\Sigma_j)} = r_{ik}

となる。r_{ik}は負担率とも呼ばれる値で、観測データx_iをどの分布で負担するかを表すような値になっています。Mステップでは、この負担率を用いて以下のように更新する。

\lambda_k^{t+1} = \frac{\sum_{i=1}^I r_{ik}} {\sum_{j=1}^K \sum_{i=1}^I r_{ij}}

\mu_k^{t+1} = \frac{\sum_{i=1}^I r_{ik} x_i} {\sum_{i=1}^I r_{ik}}

\Sigma_k^{t+1} = \frac{\sum_{i=1}^I r_{ik}(x_i - \mu_k^{t+1}){(x_i - \mu_k^{t+1})}^T} {\sum_{i=1}^I r_{ik}}

このように、それぞれの正規分布の分布全体への影響度を示す\lambda_kは負担率の合計値が、平均や分散は負担率を重みとして利用した値が使われており、直感的にも妥当そうな更新式になっています。

以上で混合正規分布を用いたEMアルゴリズムの推定の方法となります。EMアルゴリズムは他にも様々な分布に適用することができます。外れ値に強くするために用いられるt-分布、部分空間を用いて正規分布の表現力を上げる因子分析、そしてt-分布に部分空間を適用したものなど、それこそ適用方法は様々で、詳しくは上記テキストの7.5 - 7.7章をご確認ください。

判別モデルによる回帰

次に、判別モデルによる回帰の方法を説明していきます。判別モデルでの回帰は、上で示したように{ Pr(w|x,\theta) = Norm_w(\phi_0 + \phi_1 x, {\sigma}^2) }と書くことができます。この式に対して下記のような最尤推定を行うことを考えます。

\hat{\theta} = argmax(Pr(w|x,\theta))

この式を解くと、推定値は解析的に求めることができ下記のようになります。

\hat{\phi} = (XX^T )^{-1}Xw

\hat{\sigma}^2 = \frac{(w - X^T \phi)^T (w - X^T \phi)} {I}

以上が判別モデルでの線形回帰の例となります。しかし、この単純なモデルで単純な推定を行うと問題があります。一つは、学習データ点のない部分についても過剰に推定の信頼性がたかくなってしまうover-confident、さらに線形関数にしか適用できない点、さらに過学習や高次元な観測値の場合パラメーターの次元が大きくなりすぎる点が挙げられます。

over-confident に対しては、ベイズ推定を用いることで、非線形化は関数の非線形化およびカーネルトリック過学習やパラメーターの次元の削減にはスパース回帰を用いることで改善できます。これらの手法は組み合わせることにより、Gaussian Process Regression やRelative Vector Regression などより高度な手法でモデリングすることができます。

それぞれを簡単に見ていきましょう。まず、ベイズ推定による線形回帰です。これは何も難しいことはありません。ベイズ推定により観測データが与えられた時のパラメーターの確率分布を推定すればよいのです。

Pr(\phi|X,w) = \frac{Pr(w|X,\phi)Pr(\phi)} {Pr(w|X)}

ですので、{ Pr(w|x,\theta) = Norm_w(\phi_0 + \phi_1 x, {\sigma}^2) }で、Pr(\phi) = Norm_\phi(0, \sigma^2 I)とおくと、Pr(\phi|X,w)を解析的に求めることができます。また新しい観測値x^*が与えられた際に、

Pr(w^*|x^*,X,w) = \int Pr(w^*|x^*,\phi) Pr(\phi|X,w) d \phi

も解析的に求めることができます。このように推定を行うと、学習データに近い部分のxとwの組み合わせの信頼性は高く、そうでない点については信頼性があまり高くない分布を得ることができます。ちなみに上記の推定はパラメーター\phiについてのものだけであり、\sigma^2については別途推定をする必要があります。このテキストでは、\phiについての周辺尤度Pr(w|X,\sigma^2)を最大化する\sigma^2を用いて、説明を行っています。

次に非線形関数の例です。非線形関数ではz_i=f(x_i)となる非線形関数fを定義し、Pr(w|X,\theta) = Norm_w(Z^T\phi,\sigma^2I)とおいてパラメーターを推定します。関数fにはRBFやarctanなどが用いられます。いずれにおいても、最尤推定ベイズ推定も線形回帰と同様に行うことができます。

ただし、新たな観測値xに対してwを計算すると逆行列等の行列計算が非常に大変になります。そこでカーネルトリックを用いてzの内積計算を簡単になるような関数fを用います。RBFカーネルなどが有名なカーネル化関数となります。

スパースな回帰では、Pr(\phi)正規分布をステューデントのt-分布を用いることにより、パラメーターが各軸が独立になるように学習させます。具体的には下記のように定義します。

Pr(\phi) = \prod_{d=1}^D Stud_{\phi_d}(0,1,\nu)

しかしこの分布を用いると単純に解析的に推定を行うことができません。そこで、\phiについて事後確率分布を周辺化することによりその値を求めることにより、直接事後確率分布を計算します。

また、次元を削減するには双対モデルを解く方法もあります。双対モデルは、パラメーター\phi\phi = X \psiとおくことでパラメーターを推定する方法です。このモデルの面白いところは、元の式で推定した場合と同じ結果を得ることができるところです。ここで\phiの次元は変数の次元数に依存し、\psiの次元は学習データの数に依存します。つまり、双対モデルを用いることにより、求めるパラメーターの次元数を削減することができます。

このように、様々なテクニックを用いることにより判別モデルの回帰を行い、分布を求めることができます。

判別モデルによる分類

判別モデルによる分類は、モデルとして{ Pr(w|x,\theta) = Bern_w(sig(\phi_0 + \phi_1 x)) }を使う以外は、回帰モデルと似たような工夫を行い、様々な分類モデルを表現することができます。

一番の違いは、このモデルでは解析的に解を求めることができず、ニュートン法などを用いて最適なパラメーターを推定する必要がある点です。最尤推定でもベイズ推定でもそれは変わりません。

また、多クラス問題への拡張を考える必要もあります。多クラス問題への拡張は、モデルをカテゴリカル分布と仮定し、そのパラメーターの値をソフトマックス関数により定義することにより表現します。