RNN を使ったモデルの実装方法

RNN のなんとなくの概要は掴んだつもりが全然実装できなかったので、生理のためにまとめました

はじめに

最近深層学習を勉強しています。
基本的な概念とか背景の理論の概要はつかめた気がしているのですが、いざ実装しようとなると手が動かないとか正しく動かないとか、理解が甘かったことがわかってしまいます。

今挑戦してみているのは、Sequence to sequence (Seq2seq) というやつです。
その名の通り、何らかの入力列を何らかの出力列に変換する仕事をする nn です。
代表的な応用先としては、機械翻訳やチャットボットなどが挙げられます。

Seq2seq を実装するにあたり、自分の RNN についての理解が整理できていないことがわかりました。
特に、どう実装するのかという部分が全く理解できていませんでした。
そこで、ここに今の自分の「RNN とその実装」についての理解をまとめておこうと思います。

RNN は時系列データを扱える

RNN の最大の特徴は時系列データを扱えることです。
そもそも、時系列データとはなんでしょうか。

例えば、「昨日 渡った はし」という文章があったとします。
人間には「はし」が「橋」であることがわかりますが、それは「はし」の前に「渡った」という言葉があるからです。
「はし」という単語だけを見たら「箸」かもしれないですよね。
つまり、文章は「その前後の単語たち」に影響を受けるということです。

このように、「その前後にあるデータ(列)」に影響を受けるデータのことを時系列データといいます。
普通の機械学習のモデルや CNN などのネットワークでは、時系列データはうまく扱えません。
なぜなら、推論のメカニズムがデータごとに独立して行われるからです。
“はし” について何らかの推論をするモデルは、「さっき “渡った” という単語について推論した」という情報を持っていません。
“渡った” という単語について推論したことで、モデルの内部状態が変わることはなく、それぞれの推論は独立して行われます。

では RNN はなぜ時系列データを扱えるのでしょうか。
RNN は、内部状態を持っています。
RNN の内部状態は、過去に受け付けてきた入力に関する情報を蓄積しておくメモリとして働きます。
“はし” が入力に来たときに、「さっき “渡った” という入力があった」ということの痕跡が、内部状態上に何らかの形で残っているため、時系列データに対しても適切な結果を返すことができるということです。

この内部状態というのは、学習によって値が変わるパラメータとは異なる概念です。
内部状態は、学習によって決定するのではなく、今までの入力によって変化していく値です。
(初期状態は学習によって決定される?)

RNN への入力

RNN の学習には、「時系列的に最初から最後までのすべてのデータ」を一気に渡して学習させます。
ここが僕の勘違いしていた最大のポイントでした。
「時系列データ」なので、学習時も一個ずつデータを入れて学習させるものだと思っていました。
実際にはそうではなく、 時刻 0 から時刻 N までのすべてのデータを最初から用意しておいて、一気に食わせます。

例えば、文章を学習させることを考えます。
まず文章を単語区切りで分割します。それぞれの単語がデータになり、その単語列が「時系列データ」になります。
単語を one hot encoding して [0, 0, 1,…, 0] のようなベクトルに直します。
単語の種類が N 種類、文章の最大単語数が L 単語だったとしたら、学習データの行列は L × N の行列になります。
ミニバッチで学習させる場合には、バッチ数が頭について B × L × N がモデルへの入力になります。

RNN の出力

RNN 関連のモデルを組み立てる際には、 RNN には2つの役割があると思って実装すると理解しやすいです。

1つ目は、時系列データを一つの固定サイズのベクトルに落とす役割です。
これは、「記事のクラス分類タスク」などが代表的な例です。
「記事」は「単語列」という時系列データから成ります。
その時系列データを、「クラス」という時系列でないデータに変換することで「記事のクラス分類」が可能になります。
ここでの RNN の役割は、「入力数にかかわらず、ただ一つの出力を出す」という役割です。
この役割では、 RNN を複数段重ねることができません。
なぜなら、一つの RNN を通った時点で時系列は失われるためです。

2つ目は、各時刻ごとに出力を出す役割です。
入力の長さ(=時間)の数だけ出力が得られます。
RNN を複数段重ねる場合には、こちらの出力を使います。
RNN は入力ごとに何らかの出力が得られます。
1つ目の使い方では、時刻ごとの出力は捨ててしまって、入力を最後まで与えきったあとの出力だけを使っています。
2つ目の使い方では、時刻ごとの出力をすべて使います。
出力の形は、 時間 × 時刻ごとの出力の形 という行列になります。
( keras では、 RNN(return_sequences=True) とすると、各時刻の出力が得られます)

LSTM? GRU?

LSTM や GRU は、RNN の一種で、より良い結果を得るための工夫がなされたものです。
ただし、実装という観点から見ると、LSTM とか GRU とかナイーブな RNN とかは区別する必要がほぼありません。
担っている役割は同じだと言ってよく、ハイパーパラメータが違う程度の差しかありません。
僕の理解では、もう何も考えずに LSTM を使っておいたら大体良いと思っています。

推論フェーズ

推論のときには、本当に逐次的に与えられるデータが扱えます。
「今この瞬間には、この瞬間のデータしかなく、次に何が来るかはわからない」というデータです。
例えば、センサデータなどです。

まず、学習した初期状態と重みをセットしたモデルを用意しておきます
そして、データが来るたびにそのモデルを使って推論を走らせます。
モデルの状態をリセットしたりインスタンスを作り直してはいけません。
時系列データを 1 セット推論し終わったら状態を初期状態に戻します。

Source: Deep Learning on Medium