未分類

RNN/LSTM

/*最近TensorFlowを動かして遊んでいますが,GPU使える環境じゃないので学習に3時間とかかかります.30万円くらいほしいものです.*/
時系列データの学習に興味が湧いてきたのでググって調べたことをまとめようと思います.
参考:
Understanding LSTM Networks

わかるLSTM ~ 最近の動向と共に

ひとつ目のサイトは図が非常にわかりやすいのでおすすめです.

RNN

RNNはRecurrent Neural Networkの略で,時刻t-1の隠れ層の出力を時刻tの入力として扱うものです.こうすることで過去のデータを考慮して現在の出力を出すことができます.

LSTM

LSTMとは

単純なRNNは理論上「長時間の相関」を考慮することができますが,実験的には短時間の相関しか考慮できませんでした.例を挙げると文章を見せて次にくる単語を予想する問題で”the clouds are in the”のあとは”sky”が来ることはわかっても”I grew up in France…(長文) I speak fluent”の後に”French”が来ることは予想できませんでした.この「長時間の相関」を考慮した出力を行うために改良されたRNNがLSTMです.LSTMはLong Short Term Memoryの略です.

用語

LSTMは普通のニューラルネットワークと比べて若干複雑な構造を持っており,いくつか用語が出てくるのでまず簡単に説明をします.

  • LSTMブロック・・・通常のニューラルネットワークでは第N層に変数がベクトル状に存在していて,({\mathbf h}_N),それらと重み行列の積が活性化関数を通して第N+1層の入力となる(a\left({\mathbf h}_N^T W+{\mathbf b}_N\right))構造をしています.LSTMではこのように単純には出力が決定されず,「ゲート」と呼ばれる構造が出力を制御しています.ゲートの集まりをLSTMブロックと言って,通常のニューラルネットワークにおける層間のつながりを複雑化したものです.
  • 状態・・・LSTMは状態を持っており,これが長時間の相関を見出す役目を持っています.状態はベクトル状の変数で,時刻0から時刻tまでずっと毎時更新されながら受け渡されていきます.
  • ゲート・・・出力と状態の制御を行う構造です.時刻t-1の状態と時刻tの入力から時刻tの状態と出力を決定します.

仕組み

Screenshot from 2016-04-10 17-28-26
参考サイトより引用

まずこの図をご覧ください.Xtが入力,htが出力です.黄色い四角がゲート,⊗や⊕は要素ごとの積や和を表します.左からひとつずつ説明します.

過去の状態と出力

Screenshot from 2016-04-10 17-36-09

この2本の矢印は時刻t-1から状態と出力{\mathbf h}_{t-1}を引き継いでいることを表します.上が状態,下が出力です.

忘却ゲート

Screenshot from 2016-04-10 17-38-47.png

時刻t-1の出力は時刻tの入力と結合されて一つのベクトルとなります.それに行列をかけてバイアスを加えたあとシグモイドに通し,時刻t-1の状態と要素ごとの積をとります.要素ごとの積を取る際に忘却ゲートの結果が0であればその要素のことを忘れて,1であれば完全に覚えたままということになります.

入力ゲート

Screenshot from 2016-04-10 17-51-23

二個目の黄色い四角が入力ゲートです.計算式としては忘却ゲートと同じになりますが,使われる重み行列とバイアスが違っています.また,隣のtanhは新たな状態の「候補」を計算する部分です.この候補と入力ゲートの結果を要素ごとに掛け算し,先に忘却処理した状態と足し合わせることで時刻tの状態を計算します.

出力ゲート

Screenshot from 2016-04-10 17-56-35

最後に時刻tの出力を決定しなければなりません.出力ゲートの計算式も忘却ゲート・入力ゲートと重み/バイアス以外同じです.その結果と時刻tの状態をtanhに通したものと要素ごとの積をとって時刻tの出力とします.

Peephole

以上がLSTMの基本構造ですが,上で述べた仕組みには,時刻t-1の状態が時刻tの状態の更新過程に関与していないという欠点がありました.Peepholeはこれを解消するもので,下の矢印を「時刻t-1の出力,時刻tの入力」から「時刻t-1の出力,時刻tの入力,時刻t-1(出力ゲートに関しては時刻t)の状態」に変更します.

最後に

最初に挙げた参考サイトは非常によくまとまっているので参照されることをおすすめします.Qiitaの記事はLSTM技術全体を俯瞰できる良い記事です.

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)
数学

[数学]N次元回転行列の成分表示

TensorFlow実験をしようと思ってN次元回転行列を返す関数をNumpyで探していたのですがどうも実装されていないので自分で実装することにしました.N次元回転行列を検索するとどうしても成分表示している文献を見つけられなかったので導出しました.

前提

「2つのN次元ベクトルが張る平面内の回転」とは,N次元ベクトルをその平面に射影して二次元の回転をしたあと,射影された時に潰した成分を加えて,再びN次元ベクトルを作る操作をいうこととします.

表記

  • {\bf u,v}…回転させたい平面を与えるベクトル
  • {\bf x}…一般のN次元ベクトル
  • R…2つのN次元ベクトルが張る平面内の回転を与える行列
  • r_{ij}…Rの(i,j)成分.成分表示の基底は{\bf x}と同じとする.
  • \theta…回転角
  • {\bf e_u}=\frac{{\bf u}}{|{\bf u}|},{\bf e_v}=\frac{{\bf v-(v\cdot e_u)e_u}}{|{\bf v-(v\cdot e_u)e_u}|}
  • \{{\bf e_i}\}…N次元の正規直交基底
  • u_i, v_i{\bf e_u,e_v}の第i成分(i=1~N)

結論

Rの成分の表式は以下のようになります.

r_{ij}=u_i [(\cos \theta -1)u_j-\sin \theta v_j]+v_i[\sin \theta u_j+(\cos\theta-1)v_j]+\delta_{ij}

導出

無題 1

図が汚くてすみません笑.これはベクトルの配置を3次元でイメージするための図です.まずu,vが張る平面へのxの正射影を計算しましょう.

{\bf p}={\bf (x\cdot e_u)e_u+(x\cdot e_v)e_v}

平面内の成分表示されたベクトルの回転行列はよく知られているように\begin{pmatrix}\cos\theta & -\sin\theta \\\ \sin\theta & \cos\theta \end{pmatrix}と表せます.したがって{\bf p}を平面内で回転させると

{\bf [\cos\theta(x\cdot e_u)-\sin\theta(x\cdot e_v)]e_u+[\sin\theta(x\cdot e_u)+\cos\theta(x\cdot e_v)]e_v}

に移ります.これに垂線ベクトル{\bf h}={\bf x-p}を加えると回転後の{\bf x}が求まるので

R{\bf x}={\bf [(\cos\theta-\textrm{1})(x\cdot e_u)-\sin\theta(x\cdot e_v)]e_u+[\sin\theta(x\cdot e_u)+(\cos\theta-\textrm{1})(x\cdot e_v)]e_v+x}

なります.さて,{\bf x,e_u,e_v}をN次元の正規直交基底\{{\bf e_i}\}を用いて

{\bf x}=\sum_i x_i{\bf e_i}, {\bf e_u}=\sum_i u_i{\bf e_i}, {\bf e_v}=\sum_i v_i{\bf e_i}

と表しましょう(u_i,v_iは紛らわしいですが{\bf e_u,e_v}の成分です).先ほどの式の右辺にこれらを代入し,えいやっと変形して{\bf e_i},{\bf x_i}についてまとめると次の式が得られます.

\sum_i\left[\sum_j\left\{u_i[(\cos\theta-1)u_j-\sin\theta v_j]+v_i[\sin\theta v_j+(\cos\theta-1)v_j]+\delta_{ij}\right\}x_j\right]{\bf e_i}

一方左辺は次のようになります.

\sum_i(\sum_j r_{ij}x_j){\bf e_i}

左辺と右辺を比較することで

r_{ij}=u_i [(\cos \theta -1)u_j-\sin \theta v_j]+v_i[\sin \theta u_j+(\cos\theta-1)v_j]+\delta_{ij}

を得ます.

追記:

{\bf u,v}を与えて{\bf u}{\bf v}に重ねる関数の実装です.回転角を適当に変えたい場合は関数中のthetaを関数の引数にするなどすると良いです.


#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

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エッジ程度の簡単な自己符号化器を作って内部表現の抽出実験をやろうと思います.多分細部の実装は相当骨が折れるので既存ライブラリを使うと思います.