終末 A.I.

Deep Learning を中心に、週末に機械学習するエンジニアのブログ

あえてDNNで渦巻きデータを学習してみる

TJO さんの下記のブログに触発されまして、NNで渦巻きデータを分類するタスクをやってみました。

使用したデータは、下記のコードにより適当に生成した渦巻きデータです。Neural Network Playgroundの渦巻きデータのように、中心と周辺でデータの分布が近しくなるようにちょっと調整しています。

K = 2
N = 200
X = np.zeros((N * K, 2))

for i in range(K):
    ix = range(N * i, N * (i+1))
    a = int(N/4)
    r = np.concatenate((
        np.linspace(0.0, 1.50, a),
        np.linspace(1.50, 2.75, a),
        np.linspace(2.75, 3.90, a),
        np.linspace(3.90, 4.76, a)))
    theta = 3 * (i + r)
    X[ix] = np.c_[r * np.sin(theta), r * np.cos(theta)]

生成されたデータを適当に半分に分割して、訓練データとテストデータとします。左から、データ全体、訓練データ、テストデータとなります。

f:id:KSKSKSKS2:20170827175504p:plain:w200 f:id:KSKSKSKS2:20170827175627p:plain:w200 f:id:KSKSKSKS2:20170827175639p:plain:w200

出オチですが、普通こうするよねというの先に書いておくと、カーネルSVM で RBFカーネルを使用した場合、きれいなロール型の識別境界を学習してくれます。テストデータでの精度は 0.96 と上々の結果になります。

svc = SVC(C=10.0, probability=True)
svc.fit(X_train, y_train)

f:id:KSKSKSKS2:20170827180906p:plain:w200

シンプルな極座標で表せるデータの生成プロセスと、別の空間にデータを写像しそれらを分離する超平面を学習するカーネルSVMの特性を考えれば、上記の結果はそこまで驚くものではないと思います。

前置きが長くなりましたが、このデータをNNで分類するとどうなるかやってみたいと思います。 類似のデータでは、スタンフォード大学の授業資料内で3クラス分類をやって、非線形な分類が行える例を示しているものがあります。

この記事とSVMで学習した際の識別境界を比べても分かるように、前処理もしていないこういった形式のデータの学習に対して、NNがそこまで向いていなさそうなことは、なんとなく察することができます。

とはいえ、どれだけできるのか?どうフィッテイングしようとするのか?は試してみなければ分かりません。

というわけで、TensorFlow 1.3.0 から追加された DNNClasifier クラスを使って上記のデータを学習してみました。DNNClasifier クラスは、TensorFlow の内部構造に合わせて作られているため、よく意図がわからない変なインターフェースですが、生のAPIよりはお気軽にDNNを組むことができます。

cls = tf.estimator.DNNClassifier(
    hidden_units=[20, 20, 20, 20],
    feature_columns=[tf.feature_column.numeric_column("x", shape=[2])],
    n_classes=2,
    optimizer=tf.train.ProximalAdagradOptimizer(
      learning_rate=0.05,
      l2_regularization_strength=0.001),
    activation_fn=tf.nn.relu,
    dropout=0.2)

train_input_fn = tf.estimator.inputs.numpy_input_fn(
    x={"x": np.array(X_train)},
    y=np.array(y_train),
    batch_size=32,
    num_epochs=2000,
    shuffle=True)

cls.train(input_fn=train_input_fn)

上記のコードでは、隠れ層が4層でそれぞれ20次元、アクティベーション関数として ReLUを用い、入力データ2次元の2クラス分類を行うネットワークを構築しています。 L2正規化とドロップアウトを用い、学習アルゴリズムはAdagrad、バッチサイズは32、2000エポックの学習を行わせています。

learning_rate と dropout の値を変えながら学習した結果のうち比較的うまくいったケースを描画すると以下のようになります。

左から、learning_rate が 0.01 dropout なし、learning_rate が 0.05 dropout なし、learning_rate が 0.05 dropout 0.2、learning_rate が 0.05 dropout 0.5 の場合の識別境界と訓練データを描画したものになります。

f:id:KSKSKSKS2:20170827184009p:plain:w200 f:id:KSKSKSKS2:20170827184028p:plain:w200 f:id:KSKSKSKS2:20170827203506p:plain:w200 f:id:KSKSKSKS2:20170827203841p:plain:w200

learning_rate が 0.01 のものでは、学習がちゃんと完了できていないようで、識別境界があいまいになってしまっています。

learning_rate を 0.05 にした場合は、学習が完了し、むしろ訓練データに対して過学習してしまっている傾向が見られます。

過学習防止用に dropuout を追加すると、識別境界が緩やかになり、0.2 でちょうど渦巻きの形を再現するような識別境界を学習できる傾向にありました。

この場合のテストデータでの精度は、0.93 くらいですのでこのあたりがまあ限界ではないかと思います。

SVMの結果と比較してみますと、識別境界をきれいな渦巻状としては学習できておらず、細かい矩形を組み合わせてなんとかデータからそれっぽい境界を学習しているように見えます。

もしかするともっと良い設定があるかもしれませんが、正直なんとか頑張ってみました感がぬぐえず、それならSVMの方が良いなといったところではないでしょうか。

また、この論文にあるように、Dropout を用いて学習した重みで予測時もDropoutを使用すると、その出力はベイズ推定で得られた事後分布からサンプリングしたものと等しくなります。*1

これを利用して、上記までの結果も推定値をもとにサンプリングして、どれくらい自信のある予測結果なのかを見てみることにします。*2

以下は、dropout を使用してそれぞれの座標で予測結果を100サンプル取得して、その平均をもとに描画したものになります。*3

上から、learning_rate が 0.05 dropout 0.2、learning_rate が 0.05 dropout 0.5 の場合の通常の識別境界の描画と、dropout を予測時にも適用した場合の識別境界を描画したものです。

f:id:KSKSKSKS2:20170827203506p:plain:w200 f:id:KSKSKSKS2:20170827204859p:plain:w200

f:id:KSKSKSKS2:20170827203841p:plain:w200 f:id:KSKSKSKS2:20170827204915p:plain:w200

特に dropout 0.2 の時が分かりやすいですが、二つの点が混じってどっちか分かりにくい部分に関しては実はあまり予測結果に自身がないことが分かります。

渦巻状の識別境界になんとかフィッティングしているように見えますが、実のところ、「なんとなくこんな感じになっている気がする」といった具合に予測しているのでしょう。

このあたりは、DNNの柔軟性とも捉えることができますが、上記までの結果は、「その柔軟性が一番良いアプローチではない時もある」ということを示しているように思います。

*1:こちらのブログ記事で詳細が丁寧に説明されていますが、数学的に変分推定した結果と等しいと考えることができます。

*2:DNNClasifier を用いていると、残念なことに予測時にDropoutを使用する場合、ややトリッキーなことをする必要があります。詳しくはコードを参照ください。

*3:計算方法、描画方法ともにちょっと微妙な気がしてはいる。

畳み込みニューラルネットワークが見ている世界を可視化してみる(1)

VISUALIZING DEEP NEURAL NETWORK DECISIONS: PREDICTION DIFFERENCE ANALYSIS など、ニューラルネットワークの内部でいったいどんな処理が行われているのかを調べている論文も多く、アルゴリズムの理論的な解明を考えると、このような論文はまだまだたくさん出てくるであろうなと思われます。

実際、なぜうまくいっているのか、なぜうまくいっていないのかを調べるためには、可能であれば処理の結果や過程を可視化してみることは、機械学習分野では重要になってきます。 ニューラルネットワークの場合は、これがパラメーターやロスの変化で見る以外の方法が基本的にはなく、上記のような研究で学習の状態をなんとなく人間の側で観察できることは、実作業においても役に立ちそうだなと思います。

畳み込みニューラルネットワークは、その名前の通り画像処理における線形フィルタリングを多層に適用して画像から特徴量を抽出するわけですが、どういったフィルターを適用してどういった特徴量を使用しているのかは、基本的には特に気にせずに使用していることが多いかなと思います。

今回は、このフィルターを適用した結果、どんな風に画像が変換されるかを可視化してみます。

tensorflow/models に含まれるInception v3の学習済みモデルを使用して、このモデルの畳み込み層の一番最初の層のパラメーターで画像を変換してみます。

Inception v3 の最初の層は、3x3 の32個のカーネルを使用します。このフィルターを利用してそれぞれ一つのカーネルをカラー画像に適用したモノクロ画像を生成してみます。対象の画像は以下のくまの画像を使用しました。

f:id:KSKSKSKS2:20170806205426j:plain:w300

結果を見てみますと、セグメンテーションをしようとしているようなものや、ネガポジ反転を行おうとしているものや、元の画像をただぼかしたような画像が生成されます。

f:id:KSKSKSKS2:20170806210205j:plain:w300 f:id:KSKSKSKS2:20170806210346j:plain:w300 f:id:KSKSKSKS2:20170806210452j:plain:w300 f:id:KSKSKSKS2:20170806210613j:plain:w300

畳み込みニューラルネットワークの肝はここからで、生成された複数のチャネルにさらにカーネルを適用する事により、多様な特徴量を自動抽出することにあります。

今回は、単純に二つのカーネルの生成結果を合成したものを可視化してみました。

結果としては、単一のカーネルだけではぼやっとしていた特徴がより鮮明に浮かび上がっているようなものが見られました。

例えば、下記はの画像は上記の画像の1番目と2番目の画像、1番目と3番目の画像の合成になりますが、より画像内の線が浮かび上がるような画像が見られました。

f:id:KSKSKSKS2:20170806211017j:plain:w300 f:id:KSKSKSKS2:20170806211426j:plain:w300

実際の畳み込みニューラルネットワークでは、より複雑な演算になりますが、基本的には上記に挙げた結果をより強調したような結果が出力されるのであろうなということが予測されます。

次回は、学習によってこの可視化結果がどのように変化するかを見てみたいと思います。

CNNを利用した自然言語処理技術まとめ(2017年1月)

年末に Language Modeling with Gated Convolutional Networks が一部界隈でバズったこともあり、CNNを用いた自然言語処理が注目を集め始めています。今年の後半あたりには、派生手法や関連手法が多く登場していくのではないかと思われます。

CNNはRNNに比べて並列処理に優れているため、処理速度が圧倒的に速いという利点がありますが、時系列データの処理に特化したRNNと比べると、特に言語モデルにおいては最終性能がやや劣っているという理解が一般的でした(テキストクラシフィケーションではタスクによってはCNNのほうが性能がいいものもありました)。

Gated Convolutional Networks では、Gated Linear Unit および Residual 層を利用し学習を効率化することにより、WikiText-103 のタスクで state-of-the-art の性能を達成しました。

テキストクラシフィケーションの分野では、Gated Convolutional Networks が登場する前にも state-fo-the-art を達成した手法がいくつかありました。この記事では、それらの手法を中心にCNNを自然言語処理に利用するにあたって使われている技術をご紹介していきたいと思います。

A Convolutional Neural Network for Modelling Sentences(2014)

A Convolutional Neural Network for Modelling Sentences は、Dynamic k-Max Pooling 層を使うことにより、センチメント解析のタスクで既存のモデルより(RNNは含まれていませんが)性能を出すことに成功しています。

この手法では、まずエンベッティング層で単語系列をベクトル系列に変換します。その後、Wide Coonvolution 層で畳み込み操作を行い、Dynamic k-Max Pooling 層をはさむ処理を複数回繰り返して、最後に全結合層でクラシフィケーションタスクに適用します。Convolution 操作も Pooling 操作も時系列方向にだけスライドし、ベクトルの次元方向の長さは変化させないように行うことに注意してください。

Dynamic k-Max Pooling 層は、名前の通りk個のMax値をPooling操作により取得する操作なのですが、kの値が層により可変で、Lはネットワークの層の総数、sは入力されるセンテンスの長さ、{k_{top}} は最後のプーリング層でのkの値とした時に、l層でのkの値は下記のような式で決定します。

{ \displaystyle
k_l =  max(k_{top}, \lceil \frac{L-l}{L}s \rceil)
}

式を見ていただくと分かる通り、入力長によってピックアップする数を変えることにより、過不足なく必要な特徴を抽出することを目標としています。

Convolutional Neural Networks for Sentence Classification(2014)

Convolutional Neural Networks for Sentence Classification は、word2vec で pre-trained したエンベッティングを利用したシンプルなCNNのモデルで、テキストクラシフィケーションのタスクで RNNよりも性能を出したモデルです。

このモデルのポイントは二つで、

  1. pre-trained なエンベッティングとそれを fine-tuning したエンベッティングの二つのエンベッティング層を利用
  2. 2つのエンベッティングにそれぞれいくつかのウィンドウサイズを持ついくつかのフューチャーマップを出力するConvolution層を適用した後、時系列方向 にmax-poolingを行う

これにより、入力するセンテンス長を制限せず、さらにsemi-supervisedなトレーニングのような効果を得ることができています。

fine-tuning したエンベッティングの結果は、元の word2vec で得られたベクトルよりも、タスクによって与えられる単語の意味を反映することもできたと論文中では述べられています。

Character-Aware Neural Language Models(2015)

Character-Aware Neural Language Models は、Charcter-Aware なエンベッティング操作をCNNで行い、その出力をLSTMに入力することで、Penn Treebank で作成したランゲージモデルで、LSTM単体で実現された state-ofo-the-art な手法より60%少ないパラメーター数で肉薄するPLLを実現することができています。

この手法の肝は、もちろんCNNによる Charcter-Aware な単語エンベッティングの取得です。モデルの全体像は下記の図の通りになりますが、後半は単語エンベッティングを用いたただのLSTMになります。

f:id:KSKSKSKS2:20170122133509p:plain:h400

※ 論文より抜粋

エンベッティングを行うCNNには、単語を文字単位で入力しその単語のエンベッティングを出力します。まず、文字のエンベッティングを出力する層を通した後、複数のウィンドウサイズのコンボリューション処理を通した後、時系列方向のmax-poolingを通して、固定長のベクトルを生成します。そのベクトルに highway network を通したものをエンベッティング層の出力とします。

LSTMを利用していますが、2層のLSTM相当の性能を1層のLSTMで出すことができており、CNNを用いた言語モデルの中では、Gated Convolutional Networks に通じる成果だと言えるでしょう。

Character-level Convolutional Networks for Text Classification(2015)

Character-level Convolutional Networks for Text Classification は、文字レベルの入力をエンベッティング層を通した後、複数のコンボリューション層とプーリング層を通した結果を全結合層に入力してテキストクラシフィケーションを行います。

入力長が限られてしまう手法ではあるのですが、いくつかのタスクで state-of-the-art な性能を達成することができています。

Language Modeling with Gated Convolutional Networks(2016)

さて最後に、金字塔を打ち立てた Language Modeling with Gated Convolutional Networks ですが、Gated Linear Unit と Residual 層を利用することにより、WikiText-103 のタスクで state-of-the-art の性能をもつランゲージモデルの作成を達成した手法です。

手法の全体図は下記のとおりですが、ある層の出力のフューチャーマップに二つのコンボリューション処理を適用し、その出力を Gated Linear Function を通してその層の出力とします。この Gated Linear Unit を Residual 処理でラップしたものを一つの層として積んでいきます。

Gated Convolutional Networks では、一切の RNN 処理を挟まないため一層のLSTMの20倍の速度で、state-of-the-art なPLLを達成しています。

f:id:KSKSKSKS2:20170122144729p:plain:h500

※ 論文より抜粋

まとめ

Gated Convolutional Networks の結果をもってRNNが下火になるかと言われればそうはならないでしょうが、さくっと商業ベースで使う方や個人で使うような火力がない方にはCNNでやる方が良い、という認識が広まっていくのはないだろうかと思います。

この記事では主に自然言語処理で必要となる技術を紹介しましたが、CNNには独自に発展した技術も多くあります。Gated Convolutional Networks で使われているような Residual 層がその代表例でしょう。CNNが自然言語処理でどんどん使われるようになっていき、タスクに依存しない技術の交流が活発に行われていくようになると思うと胸熱ですね。

個人的には、Word Representation の技術が発達するのか、Character-Base な手法が発達するのかも注目ポイントです。畳み込み処理で一気に片付けられるという点では、RNNよりはCNNの方が Character-Base の手法を有効活用できそうで、形態素解析に伴うもろもろの問題を回避できそうかなとも感じています。

いずれにせよ、自然言語処理からますます目が離せなくなるのは必至ですね。

テキスト生成モデル -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

参考