Ahogrammer

Deep Dive Into NLP, ML and Cloud

KerasにおけるSpatialDropoutとは何者なのか?

SpatialDropoutは、画像認識の分野でTompsonらによって提案されたドロップアウト方法です。通常のドロップアウトが各要素を独立して落とすのに対して、SpatialDropoutはある領域全体をまるごと落とします。それにより、画像認識の分野で性能向上が報告されています。

f:id:Hironsan:20180620133813p:plain:w500

本記事では、KerasにおけるSpatialDropoutの動作について理解を深めることを目的としています。KerasにはSpatialDropout1DSpatialDropout2DSpatialDropout3Dの3種類がありますが、本記事ではSpatialDropout1Dの動作を確認します。

はじめに、SpatialDropout1Dの入出力について確認しておきましょう。SpatialDropout1Dの入力は3次元のテンソル(samples, timesteps, channels)です。出力は入力と同じ形のテンソルが出力されます。ただし、出力されたテンソルにはドロップアウトが適用されています。

SpatialDropout1Dの入力についてイメージを具体化するために、テキストについて考えます。テキストを3次元のテンソルで表すと(samples, sequence_length, embedding_dim)として考えることができます。ここで、sequence_lengthは文の長さ、embedding_dimは分散表現の次元数を表しています。以下のようなイメージで表せます。

f:id:Hironsan:20180620161013p:plain:w300

このようなテキストに対してSpatialDropout1Dを適用すると、分散表現のある次元全体に対してドロップアウトが適用されます。通常のドロップアウトが各要素を独立に落としていたのに対して、SpatialDropout1Dではある次元をまるごと落としています。これらの違いは以下で表せます。

f:id:Hironsan:20180620162417p:plain:w500

実際に、コードを書いて動作を確認してみましょう。入力として(1, 7 ,5)のテンソルを用意し、DropoutとSpatialDropout1Dを適用して違いを確認してみましょう。

まずは、入力のテンソルを用意します。

>>> import numpy as np
>>> import keras.backend as K
>>> ary = np.arange(35).reshape((1, 7, 5))
>>> ary
array([[[ 0,  1,  2,  3,  4],
        [ 5,  6,  7,  8,  9],
        [10, 11, 12, 13, 14],
        [15, 16, 17, 18, 19],
        [20, 21, 22, 23, 24],
        [25, 26, 27, 28, 29],
        [30, 31, 32, 33, 34]]])
>>> inputs = K.variable(ary)

入力を用意したら通常のDropoutを適用してみましょう。ここではKerasのDropout層の中で使われているkeras.backend.dropoutを使ってテンソルにDropoutを適用します。Dropout率は0.5に設定します。Dropout率はK.dropoutlevelパラメータで指定できます。

>>> K.eval(K.dropout(inputs, level=0.5))
array([[[ 0.,  2.,  0.,  0.,  0.],
        [10., 12., 14.,  0., 18.],
        [ 0., 22.,  0.,  0., 28.],
        [ 0., 32.,  0., 36.,  0.],
        [ 0.,  0., 44., 46., 48.],
        [ 0.,  0., 54., 56., 58.],
        [60., 62., 64., 66., 68.]]], dtype=float32)

結果を見ると、各要素を独立に落としているらしいことが確認できます。

次に、SpatialDropout1Dを適用してみます。SpatialDropout1Dを適用するには、Dropoutの形を指定する必要があります。そのために使われるパラメータがnoise_shapeです。SpatialDropout1Dの場合、入力テンソルの形input_shapeに対して以下のようにnoise_shapeを指定します。

>>> input_shape=K.shape(inputs)
>>> noise_shape=(input_shape[0], 1, input_shape[2])
>>> K.eval(K.dropout(inputs, 0.5, noise_shape))
array([[[ 0.,  2.,  4.,  6.,  0.],
        [ 0., 12., 14., 16.,  0.],
        [ 0., 22., 24., 26.,  0.],
        [ 0., 32., 34., 36.,  0.],
        [ 0., 42., 44., 46.,  0.],
        [ 0., 52., 54., 56.,  0.],
        [ 0., 62., 64., 66.,  0.]]], dtype=float32)

結果を見ると、ある領域をまるごと落としていることが確認できます。

ちなみに、分散表現の縦方向ではなく横方向全体にドロップアウトをかけるには、Dropout層に以下のようなnoise_shapeを渡せばOKです。

>>> noise_shape=(input_shape[0], input_shape[1], 1)
>>> K.eval(K.dropout(inputs, 0.5, noise_shape))
array([[[ 0.,  0.,  0.,  0.,  0.],
        [10., 12., 14., 16., 18.],
        [ 0.,  0.,  0.,  0.,  0.],
        [30., 32., 34., 36., 38.],
        [40., 42., 44., 46., 48.],
        [ 0.,  0.,  0.,  0.,  0.],
        [60., 62., 64., 66., 68.]]], dtype=float32)

「Kerasのto_categoricalの挙動ってちょっと変わってるよね」という話

今日はマニアックな話。

Kerasを使っている人なら、to_categorical関数を使ったことがある人は多いのではないかと思う。to_cateogorical関数をいつ使うかというと、正解クラスをone-hotエンコーディングして出力に与えたいときに使うことが多い。Keras 2.2.0だと以下のように動作する。

>>> from keras.utils.np_utils import to_categorical
>>> to_categorical([[1, 3]], num_classes=4)
array([[[0., 1., 0., 0.],
        [0., 0., 0., 1.]]], dtype=float32)
>>> to_categorical([[1, 3]], num_classes=4).shape
(1, 2, 4)

ところが、系列長が1の入力を渡すと面白い挙動を示す。

>>> to_categorical([[1]], num_classes=4)
array([[0., 1., 0., 0.]], dtype=float32)
>>> to_categorical([[1]], num_classes=4).shape
(1, 4)

なんと、(1, 1, 4)にならない!

では、keras.backendのone_hotはどうなのかというと、想定通りの動きを示す。

>>> import keras.backend as K
>>> K.one_hot([[1]], num_classes=4)
<tf.Tensor 'one_hot:0' shape=(1, 1, 4) dtype=float32>
>>> K.eval(K.one_hot([[1]], num_classes=4))
array([[[0., 1., 0., 0.]]], dtype=float32)

バグなのか?と思ってソースを見たところ、理由はわからないが意図的にやっていることは間違いない。2017/11/15日のcommitで仕様が変わっている。

github.com

以下のIssueでも議論している。

github.com

理由があろうがなかろうが使用者的には困ることがある。そういうときは以下のようにnumpyのexpand_dimsを使うととりあえず解決できる。

>>> y = to_categorical([[1]], num_classes=4)
>>> y.shape
(1, 4)
>>> y = y if len(y.shape) == 3 else np.expand_dims(y, axis=0)
>>> y.shape
(1, 1, 4)