見出し画像

言語AIの進化史⑬再帰型ニューラルネットワーク

前回は、ジョーダン・ネットワークエルマン・ネットワークを紹介しました。

中でも、エルマン・ネットワークは、基本的な再帰型ニューラルネットワークRNN)であり、その後のRNNの原型となっています。

  • 隠れ状態を使ってシーケンスの文脈を保持する

  • 隠れ状態が再帰的に次のステップに伝達される構造がある

  • 時間軸に沿った誤差逆伝播法(Backpropagation Through Time、BPTT)

これを(単純な)RNNと捉えることもできます。

ただし、RNNは異なるタスクに対応するために、シーケンスの扱い方も多様に進化してきました。その過程で登場したさまざまな構成パターンを総称し、RNNというくくりりで呼ぶこともよくあります。その場合は、「RNN」と言っても何年もの時を経て追加された構成や機能を含むことになります。

今回は、RNNを大きな枠組みと捉え、図と数式を織り交ぜて解説します。

単純なニューラルネットワークからはじめ、再帰型の仕組みを導入し、最終的には「アテンション付き多層の双方向RNNエンコーダ・デコーダ」まで紹介します。BPTTについては次回に解説します。

RNNを枠組み(フレームワーク)として幅広く理解しておくと、後に登場するLSTMトランスフォーマーの構造の理解も深まります。

なお、PyTorchを使ったより実践的なRNNの解説はこちらの記事にあります。


RNNの基本構造

図と式を使って、単純なニューラルネットワークからRNNへと変化させながら、その構造と仕組みを解説します。

1層のニューラルネットワーク

まず、一つの層からなる単純なニューラルネットワークを考えます。

  • $${\bm{x}_1}$$:入力ベクトル

  • $${W_x}$$:入力に対する変換行列

  • $${\bm{b}_x}$$:バイアス項

  • $${\sigma_y}$$:出力のための活性化関数(tanh, sigmoidなど)

  • $${\bm{y}_1}$$:出力

この流れは、入力が線形変換され、活性化による非線形変換を受け、最終的な出力となる様子を表現しています。

入力はスカラーも可能ですが、一般には特徴量をまとめたベクトルと考えます。例えば、$${n}$$個の要素からなるベクトルならば以下のように表記できます。

$$
\bm{x}_1 = \begin{bmatrix}
\\[-10pt]
x_{11} \\
x_{12} \\
\vdots \\
x_{1n} \\[3pt]
\end{bmatrix}
$$

ここでの添え数字は、変数の番号と要素の位置を組み合わせています。よって、縦の位置が$${1}$$から$${n}$$までになっています。

この入力ベクトルを、線形変換するための重み行列を次のように定義します。

$$
W_x = \begin{bmatrix}
w_{11} & w_{12} & \cdots & w_{1n} \\
w_{21} & w_{22} & \cdots & w_{2n} \\
\vdots & \vdots & \ddots & \vdots \\
w_{m1} & w_{m2} & \cdots & w_{mn} 
\end{bmatrix}
$$

列の数は、入力ベクトルの要素数と同じです。また、行の数は、変換後のベクトルの要素数になります。なお、$${W}$$に添字として$${_x}$$が付いているのは後で登場する他の行列と区別するためです。

この行列$${W_x}$$を使って、入力ベクトル$${\bm{x}_1}$$に対して、次のような変換を行います。

$$
\begin{aligned}
W_x \,\bm{x}_1 &= \begin{bmatrix}
 w_{11} & w_{12} & \cdots & w_{1n} \\
 w_{21} & w_{22} & \cdots & w_{2n} \\
 \vdots & \vdots & \ddots & \vdots \\
 w_{m1} & w_{m2} & \cdots & w_{mn} 
\end{bmatrix} \begin{bmatrix}
\\[-10pt]
x_{11} \\
x_{12} \\
\vdots \\
x_{1n} \\[3pt]
\end{bmatrix} \\
&= \begin{bmatrix}
 w_{11} x_{11} + w_{12} x_{12} + \cdots + w_{1n} x_{1n} \\
 w_{21} x_{11} + w_{22} x_{12} + \cdots + w_{2n} x_{1n} \\
 \vdots \\
 w_{m1} x_{11} + w_{m2} x_{12} + \cdots + w_{mn} x_{1n} \\
\end{bmatrix}
\end{aligned}
$$

結果は、$${m}$$個の要素を持つベクトルになっています。なお、$${m=1}$$ならば結果はスカラーになります。ここでは、ベクトルとして話を進めます。

さらに、$${m}$$個の要素を持つベクトルであるバイアス項が加算されます。

$$
\bm{b}_x = \begin{bmatrix}
\\[-10pt]
b_{x1} \\
b_{x2} \\
\vdots \\
b_{xm} \\[3pt]
\end{bmatrix}
$$

上図をここに再掲するので、その後の流れを追いましょう。

線形変換されたベクトルは活性化関数$${\sigma_y}$$によって非線形な変換を受けます。その結果が、出力$${\bm{y}_1}$$です。これも$${m}$$個の要素を持つベクトルです。

よって、出力$${\bm{y}_1}$$は次のようになります。

$$
\begin{aligned}
\bm{y}_1 &= \sigma_y(W_x \bm{x}_1 + \bm{b}_x) \\
&= \begin{bmatrix}
\\[-8pt]
\sigma_y(\ w_{11} x_{11} + w_{12} x_{12} + \cdots + w_{1n} x_{1n} \  + b_{x1}\ ) \\
\sigma_y(\ w_{21} x_{11} + w_{22} x_{12} + \cdots + w_{2n} x_{1n} \  + b_{x2}\ ) \\
\vdots \\
\sigma_y( w_{m1} x_{11} + w_{m2} x_{12} + \cdots + w_{mn} x_{1n} + b_{xm}) \\[5pt]
\end{bmatrix}
\end{aligned}
$$

なお、活性化関数は要素ごとに適用されます。

次に、これを発展させて2層からなるニューラルネットワークを考えます。

2層のニューラルネットワーク

2層からなるニューラルネットワークは、次のようになります。

もう一つの線形変換が追加されました。中間の出力$${\bm{h}_1}$$は内部で使われる情報を保持しているので隠れ状態と呼びます。

最初の活性化関数は隠れ状態を出力するので、$${\sigma}$$の添え字を$${_h}$$にしています。さらに、二番目の線形変換とバイアスは、出力に対してなので$${W_y}$$と$${b_y}$$と表記しました。その後の処理は出力用の活性化関数$${\sigma_y}$$が使われています。

ちょっと横に長いので、2段にして書き直します。また、煩雑になるので文字による説明を省きました。

なお、隠れ状態$${h_1}$$が点線で結ばれていますが、隠れ状態が渡されているという意味です。

さらに層を追加して多層化すればディープなネットワークを作れます。これによって、より複雑な計算が可能となり、隠れ状態にも高度な情報を詰め込むことができます。多層化については後でまた触れます。

しかし、このままではシーケンスの処理には向いていません。例えば、埋め込みベクトルのシーケンスを考えてください。

$$
\bm{x}_1, \bm{x}_2, \ldots, \bm{x}_s 
$$

このシーケンスに並べられた変数は$${n}$$個の要素を持つベクトルです。よって、上記のネットワークでそれぞれを処理することは可能です。

しかし、これは同じネットワークをそれぞれの入力$${\bm{x}_1}$$と$${\bm{x}_2}$$に対して別々に適用したものです。その出力は独立に計算されるため同じシーケンス内にあるにもかかわらず、$${\bm{x}_1}$$の後に登場する$${\bm{x}_2}$$は$${\bm{x}_1}$$に関する情報を使うことができません。つまり、文脈が無い状態での処理になってしまいます。

そこで再帰を導入します。

再帰型ニューラルネットワーク

次のようにして、最初の入力ベクトル$${\bm{x}_1}$$からの隠れ状態$${\bm{h}_1}$$を次の入力ベクトル$${\bm{x}_2}$$の処理の際に追加します。

これを数式で表現すると以下になります。

$$
\begin{aligned}
\bm{h}_2 &= \sigma_h(W_x \bm{x}_2 + \bm{b}_x + W_h \bm{h}_1 + \bm{b}_h)
\end{aligned}
$$

このようにして、二番目の隠れ状態$${\bm{h}_2}$$は、一番目の隠れ状態からの情報を含むようになりました。つまり、シーケンスの中で前にあるベクトルからの文脈を得ています。

隠れ状態の初期値$${\bm{h}_0}$$をゼロベクトル(要素が全て0のベクトル)とすれば、隠れ状態$${\bm{h}_1}$$の計算も同様に定義できます。

$$
\begin{aligned}
\bm{h}_1 &= \sigma_h(W_x \bm{x}_1 + \bm{b}_x + W_h \bm{h}_0 + \bm{b}_h) \\
\bm{h}_2 &= \sigma_h(W_x \bm{x}_2 + \bm{b}_x + W_h \bm{h}_1 + \bm{b}_h)
\end{aligned}
$$

また、各ステップにおける出力も以下のように計算できます。

$$
\begin{aligned}
\bm{y}_1 &= \sigma_y(W_y \bm{h}_1 + \bm{b}_y) \\
\bm{y}_2 &= \sigma_y(W_y \bm{h}_2 + \bm{b}_y)
\end{aligned}
$$

図も再掲します。

これを一般化して、ステップ$${t}$$で表現します。

$$
\begin{aligned}
\bm{h}_0 &= \bm{0} \\
\bm{h}_t &= \sigma_h(W_x \bm{x}_t + \bm{b}_x + W_h \bm{h}_{t-1} + \bm{b}_h) \\
\bm{y}_t &= \sigma_y(W_y \bm{h}_t + \bm{b}_y) \\
\end{aligned}
$$

ここで、$${t}$$は$${1}$$からです。

このように前のステップからの隠れ状態$${\bm{h}_{t-1}}$$を次の隠れ状態$${\bm{h}_t}$$の計算へと利用するところが再帰型と呼ばれる理由です。以前のステップからの隠れ状態が綿々と次のステップへと受け継がれていきます。

また、RNNでは同じネットワークを繰り返し使用しています。つまり、重みやバイアス($${W_x, \bm{b}_x, W_h, \bm{b}_h, W_y, \bm{b}_y}$$)はすべてのステップで共通しています。

このため、可変長のシーケンスを処理できます。可変長とはシーケンスによって長さが異なることなので、長さが10でも20でも同様に扱うことができます。単にシーケンスの長さによって繰り返す回数が変わるだけです。

これがフィードフォワード型よりRNNがシーケンス処理に適している理由です。

2つのバイアス項について

隠れ層におけるバイアス項$${\bm{b}_x}$$と$${\bm{b}_h}$$は同じ次元を持つため、これらを一つにまとめて表記することが一般的です。これにより、隠れ状態の更新式は以下のように簡潔に記述できます。

$$
\bm{h}_t = \sigma_h(W_x \bm{x}_t + W_h \bm{h}_{t-1} + \bm{b}_h)
$$

なお、元の式から見ると、この$${\bm{b}_h}$$は$${\bm{b}_x}$$と$${\bm{b}_h}$$を足したものです。この式は、エルマン・ネットワークとも一致します。

ただし、ここまでは、別々のバイアスとして表記しました。そのほうが、図と数式を使って単純なニューラルネットワークから始めて説明するのにに全体的な一貫性があるからです。

ちょっと話はそれますが、実装によってはそれぞれの重みに対してバイアスを指定するようになっています。よって、別々に表記することが間違いとは限りません。

例えば、PyTorchのRNNの実装ではバイアスを2つに分けています。これはNVIDIAのcuDNNの仕様に従っているからです。

以下は、PyTorchのオンラインのドキュメントからの引用です。隠れ状態$${h_t}$$の計算式を見てください。バイアス項が$${b_{ih}}$$と$${b_{hh}}$$の2つあります。

https://pytorch.org/docs/stable/generated/torch.nn.RNN.html

添字$${_{ih}}$$は、入力(input)から隠れ状態(hidden)への意味です。また、添字$${_{hh}}$$は、隠れ状態(hidden)から隠れ状態(hidden)への意味です。

なお、デフォルトの活性化関数は$${\text{tanh}}$$ですが、他の関数を($${\text{nonlinearity}}$$という引数で)指定することも可能です。

また、PyTorchのRNNのベースクラスの実装でも確認できます。

class RNNBase(Module):
  ...
  def __init__(self, ...):
    ...
    w_ih = Parameter(torch.empty((gate_size, layer_input_size), **factory_kwargs))
    w_hh = Parameter(torch.empty((gate_size, real_hidden_size), **factory_kwargs))
    b_ih = Parameter(torch.empty(gate_size, **factory_kwargs))
    # Second bias vector included for CuDNN compatibility. Only one
    # bias vector is needed in standard definition.
    b_hh = Parameter(torch.empty(gate_size, **factory_kwargs))
    ...

このコードのコメントでは次のように書かれています。

CuDNN との互換性のために 2つ目のバイアス ベクトルが含まれています。標準的な定義では 1 つのバイアス ベクトルのみが必要です。

Second bias vector included for CuDNN compatibility. Only one bias vector is needed in standard definition.

PyTorchのコードのドキュメントからコピー引用

いずれにせよ、PyTorchを使う上で、このことを気にする必要はありません。ただし、ドキュメントを読んで疑問に思った方には役立つ情報かもしれません。

RNNのパターン

ここまでは単純なRNNですが、もっと複雑なパターン処理が可能です。

再帰の展開図と関数表現

まずは、RNNの図を以下のように簡略化します。

点線で結んでいるところは再帰を意味します。なお、バイアス項は$${\bm{b}_h}$$として一つにまとめています。

中央の箱をRNNと名付け関数(ニューラルネットワーク)として扱うと、数式は以下になります。

$$
\bm{h}_t = \text{RNN}(\bm{h}_{t-1}, \bm{x}_t)
$$

では、点線の部分を2つのステップだけに注目して展開してみます。

2つのステップに展開

RNNの箱は、上述の$${W_x, W_h, \bm{h}_h, \sigma_h}$$と同じ意味です。2つあるように見えますが、2つのステップに展開しているだけで実際には同じニューラルネットワークです。

これを数式で表現すると次のようになります。

$$
\bm{h}_{t+1} = \text{RNN}(\bm{h}_{t}, \bm{x}_{t+1}) = \text{RNN}(\text{RNN}(\bm{h}_{t-1}, \bm{x}_{t}), \bm{x}_{t+1})
$$

こうして必要なだけステップ数を増やしていけば可変長のシーケンスに対応できる様子がより明白です。

これを式で表現すると以下になります。

$$
\begin{aligned}
\bm{h}_{t+1} &= \text{RNN}(\bm{h}_{t}, \bm{x}_{t+1}) \\
&= \text{RNN}(\text{RNN}(\bm{h}_{t-1}, \bm{x}_{t}), \bm{x}_{t+1}) \\
&= \text{RNN}(\text{RNN}(\text{RNN}(\bm{h}_{t-2}, \bm{x}_{t-1}), \bm{x}_{t}), \bm{x}_{t+1}) \\
&= \text{RNN}(\text{RNN}(\text{RNN}(\text{RNN}(\ldots(\text{RNN}(\bm{h}_{0}, \bm{x}_{1}), \bm{x}_2), \ldots), \bm{x}_t), \bm{x}_{t+1}) 
\end{aligned}
$$

このようにRNNの内部で隠れ状態が逐次更新されていきます。これで、RNNがシーケンス$${\bm{x}_1, \bm{x}_2, \ldots, \bm{x}_t, \bm{x}_{t+1}}$$を処理する様子を数式で表現できました。

最後の隠れ状態から予測を行う

シーケンスの最後にステップからの隠れ状態を使って出力を計算すれば、シーケンス全体の情報を元にした予測が行えます。

回帰問題なら活性化関数$${\sigma_y}$$はありません。分類を行うならば、活性化関数としてソフトマックスを適用して確率に変換します。例えば、文章に対するセンチメント分類やスパムフィルターなどに応用できます。

各ステップからの出力を使う

音声認識や株価予測などの時系列データの処理などでは、各ステップからの出力が必要になるので、以下のような構成になります。

Outputの箱は、上述の$${W_y, \bm{h}_y, \sigma_y}$$と同じ意味です。出力層と呼びます。

シーケンスからシーケンスへと変換する

機械翻訳などでは、入力シーケンスから出力シーケンスへの変換を行います。これは、Seq2Seq(Sequence to sequence)とかエンコーダ・デコーダと呼ばれる構成です。

エンコーダ・デコーダ

緑色の部分が入力の埋め込みベクトルのシーケンス$${x_1, x_2, \ldots, x_n}$$です。

左の水色はエンコーダです。"enc"の箱はRNNで、シーケンスからの文脈を隠れ状態へと集約していきます。隠れ状態$${h_1, h_2, \ldots}$$を上部に表示しています。

この描き方は、(図の幅が狭いのもありますが)入力シーケンスから隠れ状態への変換が垂直に対応しており見やすい利点があります。これまでいろんな図が登場しましたが、RNNのパターンによって説明の図の描き方を変更するのはよくあることです。

エンコーダ・デコーダモデルでは、エンコーダが入力シーケンスの情報を隠れ状態に集約します。通常、エンコーダの 最後の隠れ状態$${\bm{h}_n}$$が、シーケンス全体の文脈を含んだ表現としてデコーダへと渡されます。また、デコーダは、この文脈情報を基に、シーケンスの出力を生成していきます。

ただし、このすぐ後で見るように、アテンション機構を導入することで、エンコーダから得られた全ての隠れ状態$${h_1, h_2, \ldots}$$をデコーダで利用することが可能になります。

右の紫色はデコーダです。"dec"の箱はRNNで、最初にエンコーダからの最後の隠れ状態$${\bm{h}_n}$$を文脈として受け取っています。デコーダへの最初の入力として”開始”を意味する埋め込みベクトルが与えられます。

よって、文脈と開始の合図で最初の隠れ状態$${g_1}$$を計算します。これを出力層$${\text{O}}$$に通せば翻訳分の最初の埋め込みベクトル$${o_1}$$が出力されます。これを繰り返して最終的に”終了”を意味する埋め込みが出力されるまで続けます。

このため、デコーダによる出力も可変長に対応しています。つまり、入力シーケンスとは異なる長さのシーケンスを生成できます。

なお、出力された埋め込みベクトルは、トークンへと変換され、最終的にはテキストになるので、全体として翻訳されたテキストが出力されます。

アテンション機構

最近はアテンションというとトランスフォーマーのイメージが強いですが、この概念は以前から存在していました。

エンコーダから出力された全ての隠れ状態は各トークンの情報と文脈を含むので、これをデコーダから利用できるようにしています。

一方、デコーダ側では、エンコーダからの隠れ状態から各ステップで必要な情報を取り出します。その選別を行うのがアテンション機構です。デコーダからの最初の出力の部分を拡大して説明します。

まず、デコーダは、エンコーダから最終の隠れ状態$${h_n}$$と”開始”を受け取り隠れ状態$${g_1}$$を出力します。$${g_1}$$とエンコーダからの各隠れ状態$${h_1, h_2, \ldots, h_n}$$との関係の強さであるスコアを$${\text{score}(h_i, g_1)}$$で計算します。例えば、ベクトルの内積などを使います。

こうして計算されたスコアからソフトマックスを使って重みを計算し、入力シーケンスの隠れ状態の加重平均(重み付けによる線形結合)を計算します。これがこのステップにおける重要な文脈情報として$${g_1}$$と共に出力層$${\text{O}}$$への入力となります。これがRNNにおけるアテンションの仕組みです。

この仕組みは、後発のトランスフォーマーのアテンションとよく似ています。つまり、トランスフォーマーはRNNのアテンションからの基本的なアイデアを継続して採用しています。

アテンションを取り入れたRNNのエンコーダ・デコーダは構造の全体像は下図になります。

アテンションの配置にフォーカスしているので細部は省きました。

RNNを積み重ねる

RNNの箱を積み重ねることによって多層化することができます。

下図は、2層からなるRNNです。

ここで、$${\text{RNN1}}$$と$${\text{RNN2}}$$は、異なるパラメータを持ちます。また、$${\text{RNN1}}$$からの隠れ状態は、次のステップだけでなく、上層の$${\text{RNN2}}$$にも渡されます。

この垂直方向の処理の流れは、ディープニューラルネットワークにおける多層化と同じ意味を持ちます。多層RNN(ディープRNN)における上層からの隠れ状態では、より複雑な(あるいは抽象的な)特徴を抽出することが可能となります。

このようにして、複数の層を積み重ねることでより複雑な隠れ状態を出力することで性能を上げられるので、多層化は複雑なタスクを扱うエンコーダ・デコーダなどで利用されます。

オリジナルのトランスフォーマーでは、エンコーダとデコーダは6層から構成されていました。この多層化もRNNベースのエンコーダ・デコーダが由来となっています。その後のトランスフォーマーをベースとしたモデルではより多層化が進められました。

双方向のRNN

双方向のRNN(Bidirectional RNN、BiRNN)は、シーケンスの情報を順方向逆方向の両方から処理します。これにより、各ステップにおいてシーケンス全体からの文脈の把握を可能にします。

特に文章を処理する場合では有効です。順方向のRNNが先頭から終わりに向かって情報を伝播する一方、逆方向のRNNは終わりから先頭に向かって情報を伝播します。これにより、双方向で文脈情報を取得できるため、各単語が持つ前後の関係をより正確に捉えたモデルが構築されます。

例えば、順方向のRNNをAとします。下図にあるように、Aからの隠れ状態を$${h_1, h_2, \ldots, h_n}$$とします。

そして、逆方向のRNNをBとします。その隠れ状態は$${g_1, g_2, \ldots, g_n}$$です。後ろから前へと処理されていることに注目してください。

このようにして得られた隠れ状態を連結して、各ステップに対して、双方向からの文脈をまとめます。

例えば、以下のようなコードで隠れ状態を連結します。

concat(h_1, g_n)

このように双方向の隠れ状態を連結した隠れ状態をSeq2Seqのエンコーダで使えば、各ステップからの隠れ状態はシーケンス全体の文脈を含むことになります。

また、多層化においては、連結した双方向の隠れ状態を次の上層に入力することでさらに豊かな文脈情報を処理することができます。

双方向からの文脈を取り入れたことは、トランスフォーマーによるグローバルな文脈抽出と同等な役割を担っています。

これまで、トランスフォーマーがRNNのエンコーダ・デコーダと似ている部分を指摘してきました。歴史的にRNNの仕組みがどんどん複雑化する一方、トランスフォーマーは 自己アテンション機構 を中心に据え、シーケンス処理では必須と考えられていた再帰構造を完全に排除しました。

このため、トランスフォーマーは全ての位置からの情報を同時に処理でき、従来のRNNに比べて並列化が容易で、大規模なデータを高速に処理できるという強みを持ちます。

それでも、トランスフォーマーをRNNの進化形と捉えられるのは、これまで見てきた通りです。

これまで紹介してきた構造や機能をまとめると、「アテンション付き多層の双方向RNNエンコーダ・デコーダ」となります。これはGoogleの機械翻訳などで
(トランスフォーマーが登場する以前)に利用されました。

次回予告

RNNは再帰の仕組みと柔軟な構成によってシーケンス処理で成果を出し、さまざまに応用され発展していきました。

次回は、RNNの学習を支える仕組みである時間軸に沿った誤差逆伝播法(Backpropagation Through Time、BPTT)を紹介します。

お楽しみに!

いいなと思ったら応援しよう!