はじめに
ディープラーニングでは、各入力データの長さが一致していないと色々と不都合があり、入力に文(単語列)などを扱う場合は度々この問題が発生します。
この記事では、Chainerで可変長入力を正しく扱うためのテクニックを紹介します。(主に著者の忘記録用ですが)
前提:なぜ入力データの長さが一致していないと問題なのか?
そもそもなぜ入力データの長さが一致していないと問題なのでしょうか?
その理由は、「計算を高速に実施するために、numpy・cupy上の行列演算を使いたいから」です。
ディープラーニングではよくミニバッチ学習が採用されますが、このミニバッチ単位で効率よく損失の計算ををしたいわけです。この時にミニバッチ内の各入力を1つずつ読んでいてはあまりにも非効率です。
for x in x_lst:
h = self.encoder(x)
(以下略)
この計算を高速化するために、numpyやcupyを使用します(なぜ高速になるのかはここでは解説しません)。
import numpy
x = numpy.array(x_lst)
h = self.encoder(x)
ただし、入力の長さがそろっていないと行列化できません。上記の例の x_lst に含まれる各要素の長さが一定でないと、numpy.arrayで落ちてしまうわけですね。
可変長入力の扱い方
可変長入力を扱うときは、以下の3つのテクニックを用いるのが直感的で簡単です。
- padding
- embedding
- masking
ほかにもreshapeを駆使することでmaskingの代わりにすることもできますが、本記事での解説は省きます。
paddingとembeddingは単純かつ簡単で、本記事以外にも多数の解説があります。maskingも単純なのですが、よくミスをしてしまいがちです。maskingを適当にやってしまうと、正しい計算が行われなくなる危険性があります。
1:padding
まず長さが足りていない入力に適当な値を埋めることで、無理やりnumpy.arrayを通します。
[
[4, 2, 5]
[9]
[6, 3, 7, 1]
]
↓padding
[
[4, 2, 5, -1]
[9, -1, -1, -1]
[6, 3, 7, 1]
]
paddingについては、ディープラーニングフレームワークで可変長の入力を扱うときのTipsでわかりやすく紹介されています。
Chainerでは、functions.pad_sequence
モジュールを利用するのが良いでしょう。
2:embedding
embeddingする際にpaddingで適当に埋めた値に対しては、基本的にゼロベクトルを与えてやりましょう。
Chainerのlink.EmbedID
では、ゼロベクトルを返す値をignore_labelオプションで指定することができます。
paddingする値を-1、ignore_label=-1 としてやるのが一般的なようです。
3:masking
計算を進めていく中で、要所要所にfunction.where
によるmaskingを入れて、paddingに相当する部分をゼロにしましょう。たとえば、ゼロが入るとNaNが発生する割り算やlogを含んだ計算後で、ネットワーク内の学習対象である重み行列との演算前に、maskingの処理を入れるのが妥当です。
import numpy
import chainer.functions as F
x = F.log(x + 0.0001) # NaaN回避のため、微小値を加算
x = F.where(mask, x, numpy.full(x.shape, 0., numpy.float32))
※変数maskはxと同じshapeを持つnumpy.boolの行列です。
Chainer の whereに関しては公式ドキュメントを参照してください。
ここで問題となるのは、maskをどのように作るかです。
まず最初に思いつくのはx.data != 0
ですが、これまでの計算の結果、偶然ゼロが発生していたりすると危険です。またDropout関数を噛ませていると、paddingした箇所とDropoutした場所を判別することは困難です。
解決策の1つは、embeddingした時点でmaskを作成し、通常の入力に対するforwardの計算に合わせてmaskも更新していくことです。
x = self.embed(inputs)
mask = numpy.absolute(x.data) > 0.
x = F.dropout(x, 0.1)
x = F.reshape(x, shape)
mask = F.reshape(mask, shape)
(中略)
x = F.log(x + 0.0001)
x = F.where(mask, x, numpy.full(x.shape, 0., numpy.float32))
更新中は、maskがbool型である点に気を付けましょう。和や積などに対してbool型に対応したnumpyライブラリを使いましょう。
たとえば、以下のような形になります。
# x1, x2: 入力側の値
# x: 出力側の値
# m1, m2: x1, x2のmask
# x の maskを計算したい
x = F.sum(x1 * x2, axis=-1)
mask = numpy.all(numpy.logical_and(m1, m2), axis=-1)
まとめ
- Chainerで可変長入力を扱うときは、padding, embedding, masking
- maskingでミスが発生しやすい
- embeddingした時点でmaskを作り、forwardの計算に合わせてmaskを更新していく