終末 A.I.

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

テキスト生成モデル -SeqGAN-

この記事は、DeepLearning Advent Calendar 2016の20日目です。

今回は、時系列データに GAN の手法を適用した SeqGAN をご紹介したいと思います。SeqGAN は分かりやすく時系列データに GAN を適用しているためアルゴリズムが理解しやすく、公式の TensorFlow コードもあるので試しに動かしてみたい方にオススメできる手法です。

SeqGAN では、GAN と同じく生成モデルと識別モデルの両方を用いて生成モデルを学習させていきますが、時系列モデルに適用するにあたり( { Y_{1:t-1} } から { y_t } を求める生成モデルを作成するにあたり)、下図のように { y_{t+1} } 以降をモンテカルロ法により生成し、その結果も含めて本物か生成したものかを識別モデルに判定させることにより、{ Y_{1:t-1} } から { y_t } を求めた結果の評価値を決定します。

f:id:KSKSKSKS2:20161218112745p:plain

※ 論文から抜粋

この部分を式で表すと、{ G_{\theta} } を生成モデル、 { D_{\phi} } を識別モデル、{ Q } をリワード関数とすると、生成モデルの誤差関数は下記の用に表すことができます。

{ \displaystyle
J_{\theta} = \sum G_{\theta}(y_t|Y_{1:t-1}) Q^{G_{\theta}}_{D_{\phi}}(Y_{1:t-1}, y_t)
}

{ \displaystyle
Q^{G_{\theta}}_{D_{\phi}} = \frac{1}{N} \sum D_{\phi}(Y^{n}_{1:T}), Y^{n}_{1:T} \in MC^{G_{\beta}}(Y_{1:T};N)
}

誤差関数は、{ Y_{1:t-1} } から { y_t } を生成される最もらしさをリワード関数で表し、生成モデルにより{ Y_{1:t-1} } から { y_t } が生成される確率のもとに期待値をとった値となります。

リワード関数は、{ Y_{1:t-1} } から { y_t } を生成した後、{ y_{t+1} } 以降をパラメーター({\beta})を更新する前の生成モデルから、マルコフ連鎖により複数生成し、その生成結果を識別モデルで評価した平均を返します。

論文中では、生成モデルは LSTM を利用した RNN で、識別モデルは CNN で作成しています。

評価結果としては、最尤法で最適化した言語モデルで作成したテキストより、人間評価および oracle による評価でも改善することができました(oracle 評価に関しては学習曲線がかなり特殊なため再現性があるのかは微妙なのですが)。

テキスト生成をうまいことやるためには2つも3つも壁を超えなければならないように思いますが、時系列データの生成モデルとしては一つの参考となるモデルになるのではないでしょうか。

TensorFlow で知っていると役に立つ(かもしれない)演算系関数たち

この記事は、TensorFlow Advent Calendar 2016 の13日目です。

TensorFlow で処理をスクラッチする際に知っておくと便利な関数をご紹介したいと思います。

以降の説明は、TensorFlow v0.11.0 の動作に基づいて説明しています。挙動や名称がバージョンによって変化する場合もありますので、ご注意ください。

基本的な演算子とブロードキャスティング

まずは、基本的な演算処理である四則演算の挙動と matmul についてご紹介します。

TensorFlow でも基本的な演算子といえば、+, -, *, / などのことですが、これらの演算子は、こちらに記載のように、基本的にそれぞれの要素ごとに該当する演算を行った(* の場合は、要素ごとに * を適用した)結果を、出力するテンソルの要素とします。

a = tf.Variable([[1, 2],
                 [3, 4]])
y = a * [[1, 2], [3, 4]]
[[ 1  4]
 [ 9 16]]

一方で、これらの演算子には、numpy 等でも採用されているブロードキャスティングと呼ばれる動作も適用されます。一番簡単な例は、下記のようにテンソルスカラーを演算する場合です。この場合、テンソルの各要素にスカラーとの演算を適用した結果が、出力されるテンソルの要素となります。

a = tf.Variable([[1, 2],
                 [3, 4]])
y = a * 2
[[2 4]
 [6 8]]

ブロードキャスティングは、ベクトルなどの元のテンソルと比べて、あるランクの次元が1になっているものと演算を行った際にも適用されます。例えば、2x2 行列に対しては、1x2 の行列や、2x1 の行列との演算によりブロードキャスティングが適用されます。どのような結果になるかは、下記を見ていただくのが早いかと思います。

2x2 行列 * 1x2 行列

a = tf.Variable([[1, 2],
                 [3, 4]])
y = a * [2, 4]
[[ 2  8]
 [ 6 16]]

2x2 行列 * 2x1 行列

a = tf.Variable([[1, 2],
                 [3, 4]])
y = a * [[2], 
         [4]]
[[ 2  4]
 [12 16]]

行列の掛け算(matmul)

基本的な演算子の次によく使われるものといえば、一般的な行列の掛け算である matmul 演算でしょう。matmul 演算は非常に簡単で、LxN 行列を第一引数に、MxN 行列を第二引数に渡すと、LxM 行列を返してくれます。具体的には、下記の結果を見ていただくのが早いでしょう。

2x3 行列と3x1 行列の掛け算
a = tf.Variable([[1, 2, 3],
                 [3, 4, 5]])
y = tf.matmul(a, [[1],
                  [2],
                  [3]])
[[14]
 [26]]

2x3 行列と3x2 行列の掛け算

a = tf.Variable([[1, 2, 3],
                 [3, 4, 5]])
y = tf.matmul(a, [[1, 2],
                  [2, 3],
                  [3, 4]])
[[14 20]
 [26 38]]

パディングを入れる(tf.pad)

さて以降は、少し変わり種の関数をご紹介していきたいと思います。まずトップバッターは、tf.pad です。

データ長を揃えたりなど、パディングを入れたいという場面は、ままあると思いますが、tf.pad を用いることで簡単にその処理を行うことができます。パディングの指定は、Nx2 行列で行います。階ごとに [前に挿入する分、後に挿入する分] となるようにスカラー値を指定します。

a = tf.Variable([1, 2])
y = tf.pad(a, [[1, 2]])
[0 1 2 0 0]

バッチごとの行列演算(tf.batch_matmul)

行列演算を効率よく行うために用意されている処理が、tf.batch_matmul です。この関数は、名前の通りバッチごとに matmul 演算を行うことができます。正確には、3階以上のテンソルを行列の集合と考え、行列毎に matmul 演算を行いその結果を集約した結果を返します。

a = tf.Variable([[[1, 2],
                  [3, 4]],

                 [[1, 2],
                  [3, 4]]])
b = tf.Variable([[[1, 1],
                  [1, 1]],

                 [[2, 2],
                  [2, 2]]])
y = tf.batch_matmul(a, b)
[[[ 3  3]
  [ 7  7]]

 [[ 6  6]
  [14 14]]]

セグメント毎に値を集約する(Segmentation)

TensorFlow には、Segmentation を行う関数群が用意されています。この関数は、指定したIDに沿ってインプットしたテンソルを集約することができます。IDは0から始まり自由に指定することができますが、同じIDは連続して指定する必要があるという制約があります。

下記には、tf.segmentation_sum の例を上げていますが、乗算や平均の計算も行うことができます。基本的には、名前の通りセグメント毎にデータを集約するものですが、unsorted_segment_sum のように unsorted な実装が増えれば、ラベルごとの平均を求めたい時にも使えそうな関数です。

a = tf.Variable([[1, 2],
                 [2, 3],
                 [3, 4],
                 [4, 5]])
b = tf.Variable([0, 0, 1, 2])
y = tf.segment_sum(a, b,)
[[3 5]
 [3 4]
 [4 5]]

累積演算(Scan)

Scan は累積的に値を変化させる事ができる演算で、tf.cumsum と tf.cumprod が用意されています。i番目の出力は、元のテンソルのi番目までの要素の総和もしくは総乗になります。等差数列や等比数列を作りたい時や、重みを逓増させたり逓減させたい時に使えそうな機能となっています。

a = tf.Variable([1, 2, 3, 4])
y = tf.cumsum(a)
[ 1  3  6 10]

いかがだったでしょうか。TensorFlow には、この他にもこれ何に使うの?といった処理が、公式に用意されているケースが結構あります。自力実装しなければならない場合もありますが、計算速度のことも用意されている関数でなんとかする方が基本的には良いです。バージョンアップ時に API 一覧を眺めてみるだけでも色々と発見があって面白いので、ぜひ皆さんも自分だけの便利関数を探してみてください。

RNN より高速な Feedforward Sequential Memory Networks (FSMN) を TensorFlow で実装してみた

LSTM や GRU など RNN の一般的なアーキテクチャの弱点としては、DNN や CNN に比べた場合に処理の遅さがあげられます。それは、アーキテクチャからは自明で、LSTM や GRU のような系列の記憶としての隠れ変数を使用する層の場合、あるステップの計算を行う時には事前のステップまでの計算を完了している必要があるため計算の並列化が難しい点、ステップ数が深くなればなるほど逆伝搬が深くなってしまいその分の計算コストが高くなる点があるため、GPU を使った場合も処理速度に限界があります。

Zhang 氏らが提案している Feedforward Sequential Memory Networks: A New Structure to Learn Long-term Dependency(FSMN) は、既存の RNN の構造とは異なり、メモリを使用することによりシーケンシャルな構造を考慮した学習を行う事ができます。

論文中では、スカラーFSMN、ベクトルFSMN、双方向FSMNなどのアーキテクチャを提案していますが、この記事では代表してスカラーFSMNを紹介したいと思います。FSMNの構造はシンプルで、FSMN 層に入力された隠れ変数 {h^{l}_{t}}をメモリーに記憶しておき、そのメモリーに記憶されている値も考慮して次の層への出力を生成するという形をしています。

スカラーFSMNの更新式は、下記の2つで表すことができます。

[tex:{ \tilde{h}^{l}{t} = \sum^{N}{i} a^{l}{i} h^{l}{t-i} }]

[tex:{ h^{l+1}{t} = f(W^{l} h^{l}{t} + \tilde{W}^{l}{t} \tilde{h}^{l}{t} + b^{l}) }]

上記を見ての通りで、メモリブロックを考慮した値として [tex:{\tilde{h}^{l}{t}}] を生成しますが、スカラーFSMNの場合は[tex:{a^{l}{i}}]という係数を重みとしたメモリの和として定義します。あとはもとの入力とこの値を普通のニューラルネットの重み計算を行うことにより、FSMN層の出力とします。

順伝搬計算の場合、通常の RNN とは違い、事前のステップの入力が必要になりますがこの層内での計算結果を必要としないため、層単位で並列に計算することができます。 また逆伝搬の計算ですが、ステップ数が深くなっても学習に必要な逆伝搬が深くならず、さらに並列に計算することが可能となるため、通常の RNN と比べ計算を高速に処理することができるようになります。 一応論文中では、識別器としての性能も大差ないよと言っています。

というわけで、簡単に紹介したところで、TensorFlow で実装し既存のLSTMと比較してみましょう。比較実験のお題としては、TensorFlow の PTB サンプルを使用します。実験環境としては、g2.2xlarge を使用します。FSMN用のコードは、github にもアップしています。

実験条件としては、TensorFlow の PTB サンプルに付属している reader.py を使用して、バッチサイズを 20 、ステップ数を 50 として、エンベッド層、一層のFSMN層 もしくは LSTM層、出力層の三層構造でそれぞれの隠れユニットは 400 、エポック数は 10 とします。

TIME Validation PPL
LSTM 22m11.378s 122.462
FSMN 23m58.838s 148.335

結果を見る限りだと、PPL はともかく残念ながら速度が全然改善してませんね。論文によると4分の3くらいの速度になるようなので、僕の実装の問題なのでしょうが、TensorFlow との相性もあるんでしょうか。コード公開してもらえると再現実験楽でいいんですけどね。

2016/12/11 追記

コードの冗長性を除去して実験したところ、下記のように倍くらいの速度で処理をすることができました。PPLは残念ながら変わらずですが(変わったら変わったで何のこっちゃですが)、処理を見直すのってやはり大事ですね。修正済みのコードは、github にもアップしていますので良ければご活用ください。

TIME Validation PPL
FSMN 11m51.884s 148.740

Prisma で使われているという Neural Style を試してみた

7月くらいに話題に上がっていました Prisma ですが、皆さん覚えていらっしゃるでしょうか。Prisma では、A Neural Algorithm of Artistic Style というアルゴリズムをベースに、面白画像を生成していると言われていますが、10月の頭にその高速化手法であるPerceptual Losses for Real-Time Style Transfer and Super-Resolution が著者自身の手によりソースコードとともに公開されました。ちなみに論文自体は3月ごろには arxive に公開されており、yusuketomoto 氏による Chainer 実装 がほぼ同時期に公開されています。この手法では、低解像度の画像であればGPUを使用してほぼリアルタイムで Prisma のような画像を生成することが可能になるということで、ベース手法より実用性があがっています。

この記事ではその2つのアルゴリズムの中身がどのようになっているのかや実行速度などを比較してみたいと思います。

まず、ベース手法である A Neural Algorithm of Artistic Style ですが、こちらはコンテンツ画像とスタイル画像の2つをもとに、ImageNet などで学習させた Pre-Trained なモデル(論文中では VGG-19 のモデル)を使用して、スタイル画像から抜き出したスタイルを適用したコンテンツ画像を生成するというものです。

ランダムに生成した画像とコンテンツ画像およびスタイル画像をDCNNに入力し、生成画像とコンテンツ画像の特定層の出力の差と、生成画像とスタイル画像の特定の層までの出力の差を誤差として、SGD を生成画像に適用することにより生成画像をだんだんと期待する出力画像に近づけていくという処理を行っています。そのため一つの画像を生成するためには、500イテレーションや1000 イテレーションなどの大量の DCNN の学習処理を行う必要があり、どうしても画像の生成に時間がかかってしまいます。

一方で、改良版である Perceptual Losses for Real-Time Style Transfer and Super-Resolution は、学習により画像そのものを生成するのではなく、コンテンツ画像を入力とし、その画像に特定のスタイルを適用した画像を出力するような DCNN を生成することを学習の目的としています。上記の Neural Style で使用したような Pre-Trained のモデルを誤差計算用に用い、そこで計算された誤差をもとに画像を生成するためのニューラルネットワークを学習させていきます。

そのため画像の生成については、一回きりの順伝搬計算ですむので非常に高速にスタイルを適用した画像を生成することができます。スタイル毎にモデルを生成しなければならない点、モデルの学習に大量の時間とデータが必要になるというデメリットはありますが、実際にサーバ等で動作させることを考える場合はこちらの手法を取るほうが現実的でしょう。

以下はそれぞれの手法を AWS の g2.x2.large インスタンスで試してみた結果となります。適用スタイルとしては、Perceptual Losses for Real-Time Style Transfer and Super-Resolution で用意されているスタイルのうちモザイクのスタイルを適用しています。

実験には、Perceptual Losses for Real-Time Style Transfer and Super-Resolution の著者の一人である、jcjohnson 氏が公開している fast-neural-style という Torch のコードを使用しています。

ベース手法の方は、下記のように500イテレーション回して、GPUで3分17秒ほどかかります。

th slow_neural_style.lua -style_image images/styles/mosaic.jpg -content_image neko.jpg -output_image slow-ret.jpg -gpu 0 -backend cuda -use_cudnn 1 -optimizer adam -num_iterations 500 -save_every 100

一方で、fast-neural-style の方は、下記のようにモザイクスタイルのモデルを指定して、GPUで10秒ほどで完了します。実際はモデルのロードが完了しているような環境で実行するでしょうから、PCのスペックによってはデモにもあるようにほぼリアルタイムに変換を行うことも可能です。

th fast_neural_style.lua -model models/instance_norm/mosaic.t7 -input_image neko.jpg -output_image fast-ret.jpg -gpu 0 -backend cuda -use_cudnn 1 -cudnn_benchmark 1

変換結果は下記のようになりました。上から、ぱんくたそ さんから拝借してきたフリー素材の猫画像、ベース手法で変換した結果、高速化版で変換した結果の画像となります。高速化版はベース手法に比べてスタイルの適用だけでなくコンテンツ画像の利用もうまいこと行えていることがわかります。このあたりパラメーターチューニングの問題なのか、変換用の DCNN をかますことにより発生しているのかよく分かっていませんが、ちょっと動かすだけでこのクオリティの画像が生成されるのは驚きですね。

好きなスタイルを適用するためのモデルを生成するためのコードも公開されていますので、興味のある方はぜひ試してみてください!

元画像:

f:id:KSKSKSKS2:20161030002417j:plain:h200

ベース手法で変換した画像:

f:id:KSKSKSKS2:20161030002430j:plain:h200

高速化版手法で変換した画像:

f:id:KSKSKSKS2:20161030002438j:plain:h200

参考

TensorFlow の cifar10 サンプルを動かす

一般的な DNN、RNN と続いて、今回は CNN を TensorFlow の cifar10 サンプルを元に動かしてみたいと思います。

AWSでのGPU環境の整備や、TensorFlow の基本的な使い方については、手前味噌ですが下記の記事をご覧ください。

また、CNNって何?どういう仕組なの?という方は、以下の書籍が入門用によくまとまっていますのでご一読をおすすめします。

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

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

深層学習 Deep Learning (監修:人工知能学会)

深層学習 Deep Learning (監修:人工知能学会)

この記事で実際に学習および評価に使用したコードは、記事の一番下に記載してあります。Python は 3.4.3、TensorFlow は 0.9 を使用していますが、しばらくはバージョンが上がっても微調整でちゃんと動くのではないかと思います。

さて前置きはこれくらいにしまして、TensorFlow の cifar10 の識別用CNNを作成するサンプルですが、TensorFlow 独特の実装を行っているポイントが2つあります。 一つが読み込んだデータを元にミニバッチごとのデータを出力する部分。もう一つが TensrFlow の API を利用した CNN に必要となる Covolution 層、Pooling 層、正規化層の利用方法です。早速それぞれがどのように実装されているかを見てみましょう。

まずデータを読み込んでミニバッチ用のデータを生成する処理ですが、TensorFlow の HowTo ページの Reading dataソースコードに添付されている examples/how_tos/reading_data のサンプルコードに記載のあるように、処理用のデータを逐次読みだすための仕組みによって実現されています。

TensorFlow に付属しているサンプルコードでは、cifar10_input.py 内でバイナリ形式で保存された cifar10 のファイルを読み込んで、データをシャッフルした上でミニバッチで使用する数だけ出力するという処理を、tf.train.string_input_producer、tf.FixedLengthRecordReader、tf.train.batch を使用して実現しています。このコードでは、キューに登録してあるファイルを開いて、必要な時に必要な量だけそのファイルからメモリにロードするといった処理が行われることになります。

私が書いた方のコードでは、Python 用に公開されている Pickle 形式で保存された cifar10 のデータをひとまず全部メモリにロードしておき、ミニバッチの処理に必要な量だけを逐次 Tensor オブジェクトに変換し出力するという処理を行っており、下記のような実装になります。

# 元データをバッチで使用できる形式に変更する
label, image = tf.train.slice_input_producer([raw_data[0], raw_data[1]], shuffle=True, seed=1)
    
# データをエンキューしてバッチ化する
labels, images = tf.train.batch([label, image], batch_size=batch_size)

いずれにせよ、このミニバッチの結果出力される labels と images は Tensor 型のオブジェクトとなるため、http://ksksksks2.hatenadiary.jp/entry/20160718/1468833883 のように一から自身で TensorFlow で使えるようにしたデータとほぼ同じように使用することができます。

"ほぼ”という条件付きなのは、あらかじめ Session に関連付けた QueueRunner をデータの読み出しを行う前に起動しておく必要があるからです。とはいえ、tf.train.start_queue_runners を学習や評価の前に呼び出せばいいだけですので難しい話ではありません。呼び出してしまえば、意識しなくても毎回適当なデータ組を読みだしてくれるため大変便利に使うことができます。

次に、CNN にはかかせない Convolution 層、Pooling 層、正規化層の使い方についてです。とはいえこれも全然難しくなく、tf.nn.conv2d、tf.nn.max_pool、tf.nn.lrn 関数を利用して実装していくだけとなります。それぞれ、2次元画像用のConvolution、Max Pooling、Local Response Normalization を実現するための層となります。ちなみに LRN はドキュメントには記載がありませんが、 C++ で記述されているコアコードにはしっかり定義があるので使えなくなることは無いと思われます。Convolution層の重みの初期値を設定するために関数を独自実装していますが、これは重みのL2ロスをバッチのロスに加えるためのヘルパー関数となります。

以上で段取りは完了です。あとはGPUマシンで学習を実行し、5,6時間待っていただければ精度85%くらいの識別器の完成です。

TensorFlow Basic CNN

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