LoginSignup
2
3

More than 3 years have passed since last update.

Chainerで可変長の入力を正しく扱う

Posted at

はじめに

ディープラーニングでは、各入力データの長さが一致していないと色々と不都合があり、入力に文(単語列)などを扱う場合は度々この問題が発生します。

この記事では、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つのテクニックを用いるのが直感的で簡単です。

  1. padding
  2. embedding
  3. 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を更新していく
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3