Deep Learning

自己符号化器の実験 その1

目的

Deep Learningの層ごとの事前学習(局所収束を避けやすいノード重みの「良い」初期値を得る処理)には自己符号化器や制限ボルツマンマシンを用いるものが存在しています.両者とも隠れ層でデータの「内部表現」(データを簡潔に記述する変数)を獲得するようノード重みを調整します.ここで,「内部表現」とは具体的にはどんな表現を得ているのでしょうか?内部表現が複数存在する場合はどれが選ばれるのでしょうか?今回は簡単な雑音除去自己符号化器を用いて内部表現に何が獲得されるか調べてみます.

理論

理論というほど高級なものではありませんが,何をしようとしているか説明します.

結合

experiment1

今回作った雑音除去自己符号化器の構図です.単純に入力ノードを6つ,隠れ層2つ,出力ノード6つを全結合でつなげます.隠れ層は

{\bf h}=sig(W_1{\bf x}+{\bf b_1})

で表され,出力への活性化関数は恒等写像,すなわち

{\bf \hat{x}}=W_2{\bf h}+{\bf b_2}

としました.ここでW1とW2にW_2=W_{1}^{T}という束縛条件を入れる場合があるそうですが今回は自由に動かせるようにしました.

入力

入力データは6次元ベクトルです.生成手法は次のとおりです.

  1. 3次元の半径1の球面上に一様分布する点を200個生成する
  2. 半径に正規分布するノイズ(平均0,分散1)を加える
  3. 3次元ベクトルに余分な次元を3つ追加する.値は0とする
  4. 6次元空間で回転する.

このようにして生成された6次元の点は実質自由度2の球面上に密集して存在するはずですが,回転がかかっているため6つの次元が全部変化します.

正解データ

入力データの生成手順の2を飛ばして生成します.

誤差関数

6次元ベクトルの差を判定するため平均二乗誤差を使用します.最適化は勾配降下法をつかいました.

内部表現として獲得されるものの予想

データを説明するのに必要な変数は1の段階で用いた3次元球面上の天頂角と方位角のみであるはずです.ただし,曲面上の点の極座標表示はどこを始点に角度を考えても良いという任意性が存在しています.特に天頂角は数学のように北極0南極πとするパターンと緯度のように北極π/2南極-π/2とするパターンが存在しています.内部変数としては2個の角変数が獲得されると考えられますが,どういう角変数が得られるかは予想がつきません.

実験

denoised
正解データの3次元散布図
noised
入力データの3次元散布図

上図のような教師データでネットワークを訓練します.回転行列は次のコードで生成します.

rot=MathFuncs.RotationMatrix([0,0,0,1,0,0],[1,2,3,0,0,0])

RotationMatrixは前回のエントリで実装したN次元回転行列を返す関数です.生成された点はもともと[z2,z3,z4,z,x,y]のフォーマットでz2~z4が0でしたが,回転することでz2~z4も値を持つようになります.このようにして得られた教師データを使って訓練します.訓練後再びノイズありデータを入力し,自己符号化器の出力{\bf \hat{x}}を得たあと,逆回転を行い,z,x,yの値を使って散布図を作成したのが以下になります.

plane
予想出力

また,内部表現を無理やり数学の極座標表示と解釈して半径1でプロットした図が以下です.

 

something
何か

議論

画像ではわかりづらいかもしれませんが,予想出力は平面上に乗っています.出力活性化関数が恒等写像なので平面しか表せないのは当然ですが,円とは似ても似つかない形になってしまいました.また,中間層の値は無理やり極座標表示と解釈するとよくわからない図形を表すという結果になりました(天頂角をいじってみましたがあまり変化はありませんでした).点が少なすぎた,重み行列は共有すべき,最適化手法が悪かった,1層の自己符号化器の限界などの可能性がありますが,次回以降の実験で試してみようと思います.中間層の結果があまりおもしろくないのが残念でした.

ソースコード

MathFuncs.py

import numpy as np
#参照:http://wasan.hatenablog.com/entry/20110321/1300733907
#ユークリッド座標ベクトルは[Z1,Z2,...,Zn-2,X,Y]の形式
#極座標ベクトルは[r,θ1,θ2,...,θn-2,φ]の形式
def PolarToEuclidean(polVec):
ret=np.empty_like(polVec)
ret[0]=polVec[0]*np.cos(polVec[1])
for i in range(2,len(polVec)):#2個目以降に角変数が存在し,最後の角変数だけ特殊
ret[i-1]=polVec[0]*np.sin(polVec[1:i]).prod()*np.cos(polVec[i])
ret[len(polVec)-1]=polVec[0]*np.sin(polVec[1:]).prod()
return ret

def EuclideanToPolar(eVec):
ret=np.empty_like(eVec)
ret[0]=np.linalg.norm(eVec)
ret[1]=np.arccos(eVec[0]/ret[0])
for i in range(2,len(eVec)-1):#最後2つの座標は各変数の定義域が0<=theta<=2Piなので特別扱いする ret[i]=np.arccos(eVec[i-1]/ret[0]/np.sin(ret[1:i]).prod()) x=eVec[len(eVec)-2]/ret[0]/np.sin(ret[1:len(ret)-1]).prod() y=eVec[len(eVec)-1]/ret[0]/np.sin(ret[1:len(ret)-1]).prod() theta=np.arccos(np.absolute(x)) #print("x:%f y:%f theta:%f"%(x,y,theta)) if x>0 and y>=0:
ret[len(ret)-1]=theta
elif x<=0 and y>0:
ret[len(ret)-1]=np.pi-theta
elif x<0 and y<=0:
ret[len(ret)-1]=np.pi+theta
else:
ret[len(ret)-1]=np.pi*2-theta
return ret

#fromVecをrotatedVecに重ねる回転行列を返す
def RotationMatrix(fromVec,rotatedVec):
if len(fromVec)!=len(rotatedVec):
raise ArithmeticError('Incompatible vector length')
eu=fromVec/np.linalg.norm(fromVec)
ev=rotatedVec-np.dot(rotatedVec,eu)*eu
ev/=np.linalg.norm(ev)
theta=np.arccos(np.dot(eu,rotatedVec)/np.linalg.norm(rotatedVec))
ret=np.zeros([len(fromVec),len(fromVec)])
for i in range(len(fromVec)):
for j in range(len(rotatedVec)):
ret[i,j]=eu[i]*((np.cos(theta)-1)*eu[j]-np.sin(theta)*ev[j])+ev[i]*(np.sin(theta)*eu[j]+(np.cos(theta)-1)*ev[j])
ret[i,i]+=1
return ret

ManifoldLearningExperiment.py

#初期化
#numpyなどのモジュールはJupyterの設定でインポート済み
import tensorflow as tf
#高負荷な処理は行わないので手軽にInteractiveSession
sess=tf.InteractiveSession()

#便利関数のインポート
import MathFuncs

#分布データを作る
points=np.zeros((200,6)) #6次元ベクトルを200個
for i in range(points.shape[0]):
    while points[i,0]<=0:
        points[i,0]=1+np.random.randn()*0.1 #半径に雑音をのせる
    points[i,1]=points[i,2]=points[i,3]=np.pi/2
    points[i,4]=np.random.random()*np.pi
    points[i,5]=np.random.random()*np.pi*2
    #他の要素は0
denoised=np.copy(points)
for point in denoised:
    point[0]=1
#今は3個の軸が死んでるけど適当に回すことで3次元球上の点を6次元に埋め込む
rot=MathFuncs.RotationMatrix([0,0,0,1,0,0],[1,2,3,0,0,0])
rotated=np.empty_like(points)
denoisedRotated=np.empty_like(denoised)
for i in range(points.shape[0]):
    rotated[i]=np.dot(rot,MathFuncs.PolarToEuclidean(points[i]))
for i in range(denoised.shape[0]):
    denoisedRotated[i]=np.dot(rot,MathFuncs.PolarToEuclidean(denoised[i]))

#ノードの宣言
x=tf.placeholder(tf.float32,shape=[None,6])
y_=tf.placeholder(tf.float32,shape=[None,6])

def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape)
    return tf.Variable(initial)

#全結合重みの宣言
W1=weight_variable([6,2])
b1=bias_variable([2])
W2=weight_variable([2,6]) #W1の転置になるらしいけどとりあえずこう宣言しておく
b2=bias_variable([6])
sess.run(tf.initialize_all_variables())
#結合則の定義
hidden_layer=tf.sigmoid(tf.matmul(x,W1)+b1)
y=tf.matmul(hidden_layer,W2)+b2
#損失関数の定義
#ベクトルの差を見るのでmse
mse=tf.reduce_mean(tf.square(y_-y))

#損失関数を最小化する
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(mse)
for i in range(10000):
    train_step.run(feed_dict={x:rotated,y_:denoisedRotated})

#復号化された点を逆回転し極座標表示
decoded_pol=np.empty_like(decoded)
decodedUnrotated=np.dot(rot.T,decoded.T).T
for i,point in enumerate(decodedUnrotated):
    decoded_pol[i]=MathFuncs.EuclideanToPolar(point)
Deep Learning

TensorFlow Tutorial for Expertsを解読する(後半)

前回に続いて後半のCNNを使ったMNIST画像認識のコードを解読しています.前回の変数を一部再利用するのでノートブックのセルを前回分まで評価しておいてください.

TensorFlowチュートリアル作成者の言

Getting 91% accuracy on MNIST is bad. It’s almost embarrassingly bad.

単純なソフトマックス回帰の結果MNISTで91%の正答率が得られましたが「不愉快なくらい悪い」正答率だそうです.これをディープラーニングで改善しようというのが今回の趣旨です.

CNN

コードに入る前にCNNについてさらっとまとめておきます.CNNは畳み込みニューラルネットワーク(Convolutional Neural Network)の頭文字で,「畳み込み層」「プーリング層」「全結合層」「読み出し層」からなります.

畳み込み層

画像の部分部分に畳み込み演算を行います.具体的には部分をベクトルと考えて重みベクトル(フィルタ)との内積をとります.畳込みに使う領域は「ストライド」分だけずらしながら変更していきますが,重みは変更しません.したがって例えば28×28の画像の5×5の領域をストライド1で25次元の重みベクトルと畳み込んでいくと24×24個の内積結果を得ることができます.更に次の層に渡す前に活性化関数に通します.重みの種類を何セットか(例えば32セット)用意しておいてそれぞれについて24×24個の内積結果を得て活性化関数に通すのが畳み込み層の行う計算です.それぞれの重みが別々の特徴を計算しているという解釈をします.

プーリング層

畳み込み層から得られた結果を「誇張」(強調?圧縮?)する働きをします.先ほどの例で言うとひとつの重みベクトルから計算される結果は24×24個存在しました.この24×24の値を縦横に並べて再び画像とみなします.畳み込み層同様に部分領域を取り出すのですが,この中の最大の値をその領域からの出力として出力するのがプーリング(最大プーリング)です.例えば領域の大きさを2×2としてプーリングを行うと12×12の出力が得られます.これをすべての24×24画像に対して行うのがプーリング層の計算です.(プーリングの仕方は最大プーリングの他にも平均プーリングなどがあります.)

全結合層

畳込み層とプーリング層は何層も(本記事では交互に)重ねられます.何層か後に最後に置く層が全結合層です.最後のプーリング層から6×6の画像がn個得られたとします.全結合層では36n次元のベクトルとしてすべての画像を入力し,重み行列をかけ,バイアスを加え,活性化関数に通します.例えば重みベクトルが100個存在したとしたらそれらをまとめた36n×100の重み行列をかけ100次元のベクトルを得ます.それに100次元のバイアスベクトルを足したあと,ReLU活性化関数に入力します.ReLUに関しては以前深層学習の本の勉強ログを書いたのでそれを参考にするか,他のサイトの記事に当たると良いかと思います.

読み出し層

全結合層の出力をソフトマックス回帰し,分類結果とします.例えば100次元ベクトルに100×10の重み行列をかけ,バイアスを加え,ソフトマックスに通します.

ここまで書いてきましたが,他のサイトにも画像を使った優良な説明があるのでそちらをあたってもいいかもしれません.例えば下のページは画像を使ってイメージしやすく説明しています.

Convolutional Neural Networkとは何なのか – Qiita

解読

ちょっとしたユーティリティ関数

def weight_variable(shape):
  initial = tf.truncated_normal(shape, stddev=0.1)
  return tf.Variable(initial)

def bias_variable(shape):
  initial = tf.constant(0.1, shape=shape)
  return tf.Variable(initial)

def conv2d(x, W):
  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1], padding='SAME')

あとで説明しますが畳み込み層とプーリング層は2個ずつ用意します.同じ作業を繰り返すことになるのでユーティリティ関数を作っておきます.weight_variableではノイズの乗った重み行列,bias_variableではバイアスベクトルを作っています.weight_variablesにノイズを乗せておく理由は重み行列が均等に同じ値だと勾配消失問題が起こるからだそうです.

/*勾配消失問題は深層にした時に上の層で勾配が数学的には存在するが小さすぎて計算機上から扱えなくなる問題です.重みの対称性とどう関係があるんだろう…*/

conv2dでは畳み込み層の計算方法を定義しています.ここでtf.nn.conv2dで渡しているstridesがストライドの値になりますが,4次元のベクトルになっています.stridesは[入力高さ,入力幅,入力チャンネル数,出力チャンネル数]のフォーマットです.チャンネルは入力層ではRGBAみたいなもの,以降の層では各重みベクトルと畳み込まれた画像データの集合だと思っています/*違っていたら教えてください*/.これを1にするということは色データが同じサイズの別の画像で与えられたとしても別々に畳み込むことを意味します(もっともMNISTでは濃淡が一枚の画像データに記録されているだけなので今回は関係ありません).paddingは畳み込みの領域が画像をはみ出すときはどうしたら良いのかを表す引数で,’SAME’を指定すると畳み込み結果が元画像と同じサイズになるような幅だけ画像の周囲を0で埋めることになります.MNISTで5×5のフィルタを用いているのでパディング幅は4になります.max_pool_2x2は2×2の領域で最大プーリングを行う層を定義する関数です.ksizeがプーリング領域の指定,stridesでその領域の動かし方を指定します.paddingはなんのためにある引数なのか正直良くわかりませんでした.

畳み込み層とプーリング層を重ねる

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])

x_image = tf.reshape(x, [-1,28,28,1])

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

1,2行目で畳み込み層のフィルタとバイアスの宣言,4行目で入力層の宣言をしています.フィルタは5×5で32個用意しています.入力層は28×28の画像1チャンネル分なので[-1,28,28,1]という指定になります.次に実際の計算の仕方を定義しています.単純なソフトマックス回帰と異なっているのは活性化関数指定がtf.nn.reluになっていること,matmulがconv2dに置き換わっていることです.h_pool1で最大プーリングし,フィルタに畳み込まれた28×28画像を14×14に縮小します.こうして14×14画像32枚を得たあと再び畳み込み層に入力します.今回のフィルタの宣言は[5,5,32,64]となっています.これは5×5のフィルタを32枚の画像をチャンネルとみなして(色情報が32次元になったイメージ.もちろん本当の色情報ではありませんが)適用し,64個のフィルタに対して14×14画像64枚を得ます.さらにh_pool2で一枚あたり7×7に縮小して最終的に7×7画像64枚を得ます.

全結合層を定義する

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

全結合層のユニット数は適当に1024に決めます.W_fc1で7×7×64次元の重みベクトルを1024個生成します.同様にバイアス項も1024個作ります.h_pool2_flatで7×7画像64個をひとつの7×7×64次元ベクトルに変形し,h_fc1でrelu活性化関数に通して1024個のノードの値を計算します.ここの表記は前回のソフトマックス回帰と同じです.

Dropout

keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

過学習を防ぐ方法としてランダムに選んだノードを訓練事例ごとに無効化するDropoutという手法が用いられます(詳細は過去エントリや他サイトを参照してください).これを利用するためにkeep_probというノードを一個追加します.Dropoutを使うときは結果をα倍しないといけないといった留意点が存在したと思いますが,tf.nn.dropoutを使うだけでノードの遮断からα倍までやってくれます.手軽ですね.

読み出し層の定義

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

全結合層の出力をソフトマックス回帰します.これに関しては前回のエントリで説明したとおりです.

トレーニングと性能評価

cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
sess.run(tf.initialize_all_variables())
for i in range(20000):
  batch = mnist.train.next_batch(50)
  if i%100 == 0:
    train_accuracy = accuracy.eval(feed_dict={
        x:batch[0], y_: batch[1], keep_prob: 1.0})
    print(&quot;step %d, training accuracy %g&quot;%(i, train_accuracy))
  train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print(&quot;test accuracy %g&quot;%accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

今回のトレーニングはAdamを使って学習率を制御しながら確率的勾配降下を用いてクロスエントロピーを最小化します.forループの実行に非常に時間がかかるのでiが100で割り切れるごとに訓練集合に対する適合度を表示して暇を潰します.train_step.runではkeep_probに値を指定してやる必要があるようです.ここに指定する値は経験とか勘になるんでしょうか.最終結果を最終行で表示します.

実行結果

Screenshot from 2016-02-21 15-54-54

99.2%の正答率が得られました.チュートリアル作成者いわく「最新技術程ではないけど認めてやっても良い」水準だそうです.

雑感

ディープというほど深くはない(計6層)ですがそれなりの結果をこれだけ手軽に(一切数学的な操作をせずパラメータ調整も特になしに)得られるのはなかなか興味深いですね.ただし,今回自分のノートPC(CPU:Intel(R) Core(TM) i7-3630QM CPU @ 2.40GHz,RAM:8GB)で動かしてみた結果ソフトマックスは一瞬,CNNは50分かかりました.InteractiveSessionをやめて予めグラフを構築する方法を取れば少しは早くなるのかと思いますが,高負荷な画像処理はやはり強いGPU載せたマシンでないと実用には堪えないだろうと思います.次回はチュートリアルをすすめるか自己符号化器の実験をしようと思います.

Deep Learning

TensorFlow Tutorial for Expertsを解読する(前半)

/*無料版Wordpress.comの制限でフォントが大きいです.最近のWordpress.comはfont-size指定を無視するみたいなので適宜縮小して読まれることをおすすめします.*/
前回のエントリでTensorFlowの導入を行いました.今回はMNISTチュートリアルを実際にやってみたのでコードを解読していこうと思います.

MNIST

MNISTは画像認識分野でよくトレーニングに使われる手書き文字データ集です.それぞれが28×28の画像で訓練集合が6万,テスト集合が1万存在します.TensorFlowのページでは画像認識機械学習のHello Worldと言われています.このデータを使って行う学習の目標としてはソフトマックス関数で入力画像が表す数値の確率を出力し,テスト集合で高スコアを出すことです.

準備

前回のエントリの手順でAnacondaとTensorFlowを導入し,Jupyter(IPython Notebook)を起動しましょう.JupyterはインタラクティブなPython実行環境で,ブラウザ上で実行されます.起動方法は端末上で

ipython notebook

です.ブラウザが起動するのでhttp://localhost:8888に移動するとJupyterを利用することができます.使い方の詳細は

IPython Notebookを使ってみる – たけぞう瀕死ブログ

が参考になります.端末上で行いたい場合は単に

ipython

と入力するとipythonが実行できます.

解読

初期化

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
import tensorflow as tf

上のコードでMNISTのデータをなければダウンロードし,mnistという変数を介してアクセスできるようにします.三行目はユーザーが基本的に利用する名前空間のインポートです.

セッションの作成

sess = tf.InteractiveSession()

TensorFlowはバックエンドにC++を使用していてPython側から計算手順(グラフ)を構築します.一旦構築した計算手順はセッションを走らせることでC++側で実行されます.こうすることでPythonとのやり取りをできるだけ減らし,高速化を図ります.ただし,今回はインタラクティブな環境で実行するため,InteractiveSessionを使用してセッション開始後に計算手順を構築します(多分こちらの方法は遅い)

ノードの作成

x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])

入力ノードと出力ノードはplaceholderという名前の関数で構築されます.第一引数は型,第二引数は配列の形状です.xが入力ノードなので次元は28×28=784,出力は0~9までの10ラベルなので次元は10となっています.ここで,出力ノードがy_となっているのはこのノードには訓練データの正解ラベルが入力されるため,最終的な分類器の出力とは異なることを示しています.ここで宣言したノードは以下で見ていくようにあたかもPythonインタプリタ上の変数であるかのように扱うことができます.

ノード重みの作成

W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))
sess.run(tf.initialize_all_variables())

ノードの間をつなぐ重みはVariablesと呼ばれており,上のように宣言します.ここでWは入力ノードから各出力ノードにつながる次元784の重みベクトル10個分を合わせた行列,bはそれぞれの出力ノードへの活性化関数の引数に加算されるバイアス項です.三行目でVariablesをセッションによって初期化しています.

予測器の作成

y = tf.nn.softmax(tf.matmul(x,W) + b)

チュートリアルの最初では単純なソフトマックス回帰で分類するので予測関数はこのように宣言します.tf.matmulで入力ベクトルと重みの積をとることで10次元のベクトルに変換し,バイアス項を加えたあと10個の要素を用いてソフトマックス変換します.yは各クラスに分類される確率を表す10次元ベクトルになります.

損失関数の作成

cross_entropy = -tf.reduce_sum(y_*tf.log(y))

損失関数は分類問題なのでクロスエントロピーを使います.tf.reduce_sumは全ミニバッチに対して引数の和を計算するものです.ミニバッチは確率的勾配降下で用いられる何個かの教師データをまとめたものです.

誤差関数の最小化方法を決める

train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)

ソフトマックス回帰なので最適化手法は確率的勾配降下ではなく最急降下法を用い,クロスエントロピーを最小化します.0.01は学習パラメータです.たったこれだけ指定すると誤差逆伝播から実際の目的関数の最小化まで自動でやってくれるので大変便利です.ただし他のライブラリも同じことをできるようです

誤差関数を最小化する

for i in range(1000):
  batch = mnist.train.next_batch(50)
  train_step.run(feed_dict={x: batch[0], y_: batch[1]})

ここでforループを回して損失関数を最小化するようVariablesを調整します.train_step.runはfeed_dictからplaceholderに代入すべき値を持ってきて最適化を行います.そのためにbatchにmnist.train.next_batchで訓練データを取ってきています.

分類器の評価

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print(accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

訓練した分類器の性能を評価します.tf.argmaxはテンソル,テンソルの階数を引数にとって最大の要素のインデックスを返します.予測ベクトルの最大確率を与えるインデックスと正解インデックスをtf.equalで比較し,tf.reduce_meanで平均のcorrect_prediction,すなわち正答率を計算します.実際の計算は三行目のevalメソッドで行い,feed_dictからテスト用データを供給します.注意しないといけないのは二行目のtf.castで,そのままだとcorrect_predictionはboolean型なのでキャストしないといけないということです.以上のようにJupyterに入力していくとテストデータで91%くらいの正答率が得られるはずです.

雑感

今回は全然ディープラーニングに関係ないTensorFlowの使い方になってしまいましたが次回は後半の畳み込みニューラルネットワーク(CNN)のデモを実践します.APIなどが気になったら
API Documentation
をご覧になると良いかと思います.

Deep Learning

TensorFlowを自分のPCにインストールする

前回のエントリでTensorFlowを使った実験をしてみようと考えていると言いました。今回実際にインストールしたので僕の場合の導入手順を書いておこうと思います。

TensorFlowとは

Deep Learningを利用するためのライブラリです。対応言語はPython(C++も?)で、開発元はGoogleです。Deep Learningのライブラリは他にも複数あり、TensorFlow以前はTheano, Chainerなどがあったようですが、Googleのネームバリューに勝ってそれらが今後選ばれ続けるか怪しいと思っています。ライブラリは利用者が多いほうが文献が充実して更に利用者が増えるものなので…ということでDeep Learningのユーザーとして選ぶライブラリはTensorFlowが現段階ではベストなのではないかと考えています。

動作環境

TensorFlowは現状Unix互換OSでしか動きません。したがってLinux系かMac OSになると思います。実は10日もかけて依存関係バイナリからCygwinコンパイルを試みていたのですが、Bazelというバイナリがコンパイルはうまく行けどパス処理がうまく行かずちゃんと動かないという結果に終わり、Windowsでのネイティブコード動作は挫折しました。

Ubuntuをデュアルブートする

Windowsでは使えないとなると方法は限られていて、

  1. VirtualBoxなどの仮想化ソフトウェアを使ってUnix互換OSを入れる
  2. 別のパーティションにUnix互換OSを入れる

くらいが思いつきます。今回2の方法をとることにしました。1で仮想化を行って動かす方法では速度面で2の方法以下になるはずで、等号が成立するかどうかわからなかったためです。1に関しては

TensorFlowをMac & Dockerで使ってみたよ – Qiita

などが参考になるかと思います。

デュアルブートの際には

UbuntuをUSBメモリからインストール | Linuxサーバより愛を込めて。

を参考にしました。引っかかった点としてはUSBをFATでフォーマットしておかないとunebootinがPC起動時に読み込まれずインストールが始まらなかったことくらいです。あとはダイアログにしたがって行けば簡単にインストールできます。Ubuntu用のパーティション分けは100GB程度にしました。大きなデータを扱うことになるとしても外部HDDなどから読むことになると考えたからです。

Anacondaを入れる

AnacondaはPythonインタプリタから機械学習パッケージまでall-in-oneな開発環境です。いろいろ個別に入れるのは面倒なので入れておくことをおすすめします。

Bazelをインストールする

TensorFlowはPipからでもインストール可能ですが、最新版ではない可能性があると思ったこと、最新版にすぐに追随できるようにしたかったこと、という点からソースからビルドすることにしました。そのためにまずTensorFlowをビルドするために必要なBazelをインストールします。Bazelはなんなのかよくわかっていないのですが、おそらく「Google製のすごいmake」みたいなものだと思っています。

Installing Bazel – Bazel

こちらの指示にしたがってインストールを行います。依存対象がJDKのみなので難しくないかと思います。

TensorFlowをビルドしインストールする

ここまできてようやくTensorFlowをビルドする準備が出来上がります。こちらのページのInstalling from sourcesの指示にしたがって依存関係を構築し、ビルドを行ってください。僕のノートPCはNVIDIA製GPUは載っていないのでCUDA関連はインストールしませんでした。また、解説ページが若干わかりにくくなっていますが、Installation for Linuxが一通り続いたあとInstallation for Mac OS Xを飛ばし、Create the pip package and installを実行してください。

雑感など

Windowsであれだけ苦労してしかも挫折したビルドからのインストールが非常に簡単に行えました。似たような境遇の方は(いないと思いますが)早くデュアルブートして楽になることをおすすめします。

Deep Learning

「深層学習」第4章 大規模学習の実現技術

人工知能学会監修「深層学習」第4章です.この章では我々一般人には手の届かないような大規模な深層学習システムを作る話について書かれています.

  • DistBeliefはGoogleが実装した大規模ニューラルネットワーク分散並列計算システムである.このシステムではニューラルネットワークの最適化という一見並列化不可能な問題に対し,データ並列化のアプローチを採る.このアプローチではノード間の通信というボトルネックを避けるためデータのコピーを各ノードが保持し,ノード間同期を取らずに計算を行う.更にモデルを分割化して並列計算を可能にするモデル並列化を組み合わせることで確率的勾配降下を行う.
  • 他の並列化アプローチとしてGPUを使うものがある.これはKrizhenskyが最初に行ったもので,同じ計算をループ無しに大規模なデータに対して行うのが得意というGPUの特性を使う.GPUの問題としてCPUとはメモリが共有されない製品が多く,通信を減らす工夫が必要となる.
  • NgらはGPU,(スパコンの接続に使われる?)InfiniBandを使って複数のマシンを使った深層学習を行った.GPUは密行列に強く疎行列に弱いという特性を考慮して密行列を作る工夫を行って1000万枚の画像を17時間で学習することに成功した.
  • 学習収束を高速化する手法としてバッチ正規化というものがある.この手法では学習データを正規化し(\hat{x_i}=\frac{x_i-\mu_B}{\sqrt{\sigma^{2}_B}+\epsilon}),更に線形変換した変数を用いて学習する(y_i=\gamma\hat{x_i}+\beta).この\gamma,\betaは学習すべきパラメータである.これを用いることで「内部共変量シフト」という問題を回避できる.
  • 一旦深層学習やその他ので学習したあと,同じ性能を持つ浅いニューラルネットを作る手法を「蒸留」という.この手法では浅いニューラルネットは学習済みモデル(教師モデル)の出力と入力データの分布の交差エントロピーの重み付き和を最小にするよう学習する.
  • DropOutは過学習を防ぐ手法である.この手法では訓練データごとにランダムにノードを無効化することでL2正則化と同じ効果を得る.学習後,実データを入力する場合,出力はランダムに無効化したノードの組み合わせ全部を試し,幾何平均をとることで得られる.しかし,組わせ全部を試すのはメモリ的にも計算量的にも現実的ではないので各ノードの出力を無効化したノードの割合\alpha倍して近似する.
  • 活性化関数が非線形であると計算量が増大するため線形の関数で近似することが行われている.a_{ReLU}\approx max(0,x)a_{MaxOut}=max\ w^{T}_k xといった関数が提案されている.
  • SGDの学習率を調整する手法としてAdaGrad,Adamがある.Adamは多くの問題で最適な収束性能を達成することがみられる.
  • ニューラルネットで使われる超パラメータの最適化はグリッド探索よりランダム探索が良いと報告されている.これはパラメータの影響度が均等ではないことによるものと考えられている.
  • 誤差逆伝播などは実装ミスが起こりやすいので簡単なデータセットで中心差分法等で求めた勾配と比較し,デバッグすると良い.

興味が有るのは「蒸留」です.浅いニューラルネットでアンサンブルを真似することもできるそうで,ニューラルネットワークの表現力の高さが伺えます.

この章で基本的な話題が終わるようなので,一旦実験してみようかと考えています.エッジ数があまりに多いモデルは自分のPCで動かすのが厳しいので,~1000エッジ程度の簡単な自己符号化器を作って内部表現の抽出実験をやろうと思います.多分細部の実装は相当骨が折れるので既存ライブラリを使うと思います.

Deep Learning

「深層学習」第3章 事前学習とその周辺

第三章は深層学習器の事前学習について書かれた章で半分近くをCD法の説明に当てています.深層学習器の事前学習は勾配消失問題や局所収束を避けるために行われるものと2章までで説明されてきました.2章によれば層ごとの事前学習が理論的に保証する性能向上は今のところ3層までのDBMに限るとのことでしたが,実験で深層でも事前学習が有効であることが示されているそうで,実用上は必須になると考えられます.以下は学んだことの箇条書きです.

  • RBMのような分布を包括する概念として指数型ハーモニウム族(EFH)が存在する.EFHではRBMでイジングモデルのエネルギー関数を用いたところを,単独項,相互作用項として一般的な関数\alpha_{i}(v_{i}|b^{(1)}_i),\beta_j(h_j|b^{(2)}_j),\phi_{ij}(v_{i},h_{j}|w_{ij})に置き換えたものである.可視変数,隠れ変数の関係はRBMと同じ.
  • EFHの学習は可視変数の分布を出来る限り良く再現するノード重み,バイアス(w_{ij},b,以下パラメータ)を最尤法で決定することで行われる(つまりRBMと同じ).最尤法で尤度を最大化する必要があり,勾配法を用いると現在のモデル分布の期待値計算が必要になり解析的に行うのは計算量的に困難.したがってサンプリング法を用いる.ギブスサンプリングを工夫せずに使うと分布が定常になるまでに時間がかかって実用的ではない.
  • CD法はギブスサンプリングにおいて初期分布を工夫する.具体的にはp^{(0)}( v,h|\theta)=q( v)p( h|v,\theta)で定義される.つまり可視変数の経験分布からv^{(0)}を発生させ,それを初期値としてh^{(0)}を発生させる.CD-k法はこの後k回のvとhの更新を行うもので,典型的にはk=1として更新を終わらせる.初期値を工夫しない場合とくらべて更新回数が劇的に減っている.また,多数回更新した場合と比べても性能があまり低下しないことが知られている.
  • CD法がうまくいく説明は数種類存在する.①モデル分布の可視変数に対する対数尤度をパラメータで微分し,導関数をk回目の更新におけるv,hの分布関数の級数で書く.CD-k法はその級数を打ち切った近似導関数を求めて勾配に使っていると説明できる.②CD法はギブスサンプリングにおいてk回目の更新で得られる分布が経験分布に近いことを要請するコントラスティブ・バイバージェンスという損失関数を最小化している③モデル分布が無限回の更新で真の分布(可視変数が従う分布)になる条件(詳細釣り合い条件)を満たすことを促進する損失関数を最小化するアルゴリズムとしてCD法が得られる./*この辺りの説明を追うのには骨が折れました*/
  • 上記③の説明は真の分布のパラメータを得る問題を可視変数→隠れ変数→可視変数→…の離散時間マルコフ過程が定常分布として真の分布を得るようパラメータを調整する問題としてみるものであった.離散時間ではなく連続時間を考えた場合の学習アルゴリズムが最小確率流法である.この手法から導かれる学習則は問題によってはCD法より高性能(速くて精度が高い)ことが確かめられている.
  • 継続的CD法は何度もサンプリングするときに前のサンプリングデータを初期値に用いる方法である.これを用いる場合,前のサンプリングデータが偏っていれば次得られるサンプルもかたよる問題がある.これを解消する手法がパラレル・テンパリング,交換モンテカルロ法である.
  • CD法は確率的なモデルを使った事前学習であり,深層NNとは間接的な関係しか持たない.確定的なモデルを使った事前学習も提案されており,積層自己符号化器,雑音除去自己符号化器などがある.積層自己符号化器は入力を模擬する出力を得るNNを層ごとに学習していくもの,雑音除去自己符号化器は積層自己符号化器の入力に雑音を乗せて出力で真の入力を模擬することを目指すものである.
  • 深層学習ではなく入力から伸びるノードの重みをランダムにした三層のNNが提案されており,極端学習機械という.出力をシグモイドではなく線形関数にすることで最適化を容易にしている.SVMと比較した場合学習時間が短く性能も高いという利点がある./*詳細が気になりますね*/

以上勉強したことを書き連ねました.事前学習がどのように行われるのかの概略はつかめましたが,理論的な裏付けは数式を追うのがなかなかタフで復習が必要そうです.

Deep Learning

「深層学習」第2章 深層ボルツマンマシン

今日は近代科学社「深層学習」の第二章を読み終えました.第二章はボルツマンマシンの説明から入ってDBM(Deep Boltzmann Machine),DBN(Deep Belief Network)が解説されている章です.学んだことを書いていきます.

  • 深層ボルツマンマシンは未知のモデルの分布を学習するための枠組みである.
  • ボルツマンマシンでは各確率変数は無向グラフ上のノードとして表され,0,1のいずれかの値を取り,どちらの値を取るかは確率的に決まる.その確率はリンクがつながっているノードの値のみに陽に依存し,「シグモイド信念」という関数によって計算される.シグモイド信念の表式は\frac{\exp{(\lambda_i x_i)}}{1+\exp{(\lambda_i)}}であり,\lambda_i\lambda_i=b_i+\sum_j w_{ij}x_jと定義される.w_{ij}はリンクの重みである.DNNでは全層の入力をシグモイドで変換した値を出力としていたがDBMではシグモイド変換した値を確率として用い0か1を出力するのが違いである.
  • 全変数の同時分布は各変数の値をスピンの±1に見立てたイジングモデルのボルツマン分布で定義される./*おそらくこれがボルツマンマシンの名称の由来*/勿論他の変数を固定した1変数の条件付き分布は上述のシグモイド信念になる.機械学習の文脈ではこの分布をギブス分布と呼ぶ.
  • ボルツマンマシンの確率変数は可観測な変数でも隠れ変数でも構わない.
  • ボルツマンマシンの学習は可観測な変数のデータを最尤推定し,適切なリンクの重みを決定することで行われる.推定の過程には現在のパラメータで変数の期待値を計算するという作業が入って,これがボルツマンマシンの計算量を飛躍的に増大させる要因となっている.
  • 隠れ変数が入ってくると計算量は更に増え面倒になる.しかし隠れ変数を導入することはグラフ全体で表現できる分布を増やすことになり,必要なことである.
  • 計算量過多の問題解決のためギブスサンプリング,平均場近似等を用いる.ギブスサンプリングはリンクの重みを固定し,適当な初期値からぐるぐる各変数を更新していき,十分時間が経った時の変数ベクトルを同時分布に従うサンプル点とみなすものである.問題点としては更新作業に計算量を消費することである.平均場近似は相互作用項をそのノードの平均値で置き換える近似である.平均値は平均場方程式(自己無撞着方程式)から求める.
  • 制限ボルツマンマシンはグラフを隠れ変数の層と可観測変数の層の2層に分け,同一層内のリンクはなく,一方の層のある変数がもう一方の層のすべての変数と結合しているモデルである.一方の層をすべて固定するともう一方の層の変数同士は条件付き独立になるので扱いが簡単になる.
  • 制限ボルツマンマシンの学習も通常のボルツマンマシン同様に行うが計算量軽減のためにコントラスティブ・ダイバージェンス法(3章)を用いる.
  • 深層ボルツマンマシンは隠れ変数の層を積み重ねたボルツマンマシンである.全体を学習させるのは骨が折れるし局所収束の問題があるのでDNNのように二層ごとに制限ボルツマンマシンとして学習させる.これを事前学習という.事前学習で得られたパラメータを初期値として仕上げに全体のパラメータを調整する.
  • 全体の仕上げ学習は計算量軽減のためギブスサンプリングと平均場近似を組み合わせて行う.ギブスサンプリングは工夫をして前回のパラメータ更新に使ったサンプル点を初期値として新たなサンプル点を得る.これにより計算量が軽減される.
  • 深層信念ネットワークは深層ボルツマンマシンにおいて,最上段以外のリンクを有向にしたものである.事前学習は深層ボルツマンマシンと同じ手続きで行う./*深層ボルツマンマシンのほうが良い結果を出すことが多いのであまり気にしないで良い?*/

以上本章の学習メモでした.気になったのはPRMLでもそうでしたが変数をイジングモデルのスピンと思ってカノニカル分布を適用するところです/*深層学習の本なのに関係ないところに引っかかるという…*/.カノニカル分布は統計力学においては熱浴と熱平衡にある系の微視的状態を記述する分布です.統計力学の定式化は確か「同じエネルギーの微視的状態の実現確率は等しい」「熱平衡状態のマクロ量はそれを実現する微視的状態の数が最も多いように決まる」から出発して(ここはかなり怪しい)後は特に物理的な原理や法則を入れず数学的になされていたと思うので,確かに出発点を再現するようなエネルギーや温度を適切に定義すれば物理以外でも成り立つでしょう.しかしイジングモデルをそのまま使うのが適切かどうかはよくわかりませんでした.