MLエンジニアへの道 #25 - RNN

Last Edited: 10/9/2024

このブログ記事では、ディープラーニングにおける回帰型ニューラルネットワーク(RNN)について紹介します。

ML

テキストは、長さが可変の連続的なデータの一種ですが、これまでに扱ってきたANNやCNNのようなニューラルネットワーク層では、 重みの適切なサイズを決定するために固定サイズの入力が必要でした。長さが可変の連続的なデータの性質によるこの課題に対処するために、 連続的な性質を持つ新しいモデルを作成しなければなりません。こうして作られたモデルの1つが回帰型ニューラルネットワーク(RNN) と呼ばれるモデルです。

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

他のニューラルネットワークがすべてのデータを一度に処理するのに対し、RNNは入力値を1つずつ処理し、それをニューロンに渡します。 RNNは現在の入力値に加えて、前の活性化状態を追加の入力として使用することにより、モデルは過去のデータと現在のデータとの関係を学習できるようになります。 以下の図はRNNの構造を示しています。

ML RNN

上記のRNNの構造を、数式でより詳細に表すことができます。

at=h1(zat)=h1(Waaat1+Waxxt+ba)yt=h2(zyt)=h2(Wyaat+by) a^t = h_1(z_a^t) = h_1(W_{aa}a^{t-1} + W_{ax}x^t + b_a) \\ y^t = h_2(z_y^t) = h_2(W_{ya}a^t + b_y)

上はRNNの構造を示しています。この構造の連続的な性質により、モデルは長さが可変の連続的な入力を処理することができます。 また、すべての入力に対して重みを共有するため、入力サイズに関係なくモデルをコンパクトに保つことができます。 さらに、モデルは値の列または単一の値を出力として生成できるため、翻訳、スパム検出、次の単語の予測など、さまざまなタスクに特に適したアーキテクチャを作成できます。 しかし、各活性化が前の活性化に依存するため、大規模なディープラーニングモデルの低遅延パフォーマンスに重要な並列化の利点を十分に活用できないことは留意しなければなりません。

逆伝播

RNNのニューロンの重みとバイアスを最適化するためには、損失関数の重みやバイアスに対する勾配を計算する必要があります。 勾配がどのように逆伝播されるかを理解するために、スパム検出の例を使用して説明します。この例では、モデルがメールがスパムかどうかの確率を最後に出力します。 まず、損失関数の最後の重み WyaW_{ya} とバイアス byb_y に対する勾配を計算することから始めます。

LWya=LyTyTzyTaTLby=LyTyTzyT \frac{\partial L}{\partial W_{ya}} = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T} a^T \\ \frac{\partial L}{\partial b_y} = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T}

勾配を逆伝播させるためには、活性化 aTa^T に対する勾配も計算する必要があります。

LaT=LyTyTzyTWya \frac{\partial L}{\partial a^T} = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T} W_{ya} \\

これを使用して、TT での WaaW_{aa}, WaxW_{ax}, bab_a に対する勾配を計算できます。

LWaa=LaTaTzaTaT1LWax=LaTaTzaTxTLba=LaTaTzaT \frac{\partial L}{\partial W_{aa}} = \frac{\partial L}{\partial a^T} \frac{\partial a^T}{\partial z_a^T} a^{T-1} \\ \frac{\partial L}{\partial W_{ax}} = \frac{\partial L}{\partial a^T} \frac{\partial a^T}{\partial z_a^T} x^{T} \\ \frac{\partial L}{\partial b_a} = \frac{\partial L}{\partial a^T} \frac{\partial a^T}{\partial z_a^T}

次に、次の層に対する aT1a^{T-1} に対する勾配を考えます。

LaT1=LaTaTzaTWaa=LyTyTzyTWyaaTzaTWaa \frac{\partial L}{\partial a^{T-1}} = \frac{\partial L}{\partial a^T} \frac{\partial a^T}{\partial z_a^T} W_{aa} \\ = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T} W_{ya} \frac{\partial a^T}{\partial z_a^T} W_{aa}

これに基づいて、T1T-1 における WaaW_{aa}, WaxW_{ax}, bab_a に対する勾配は以下のようになります。

LWaa=LaT1aT1zaT1aT2LWax=LaT1aT1zaT1xT1Lba=LaT1aT1zaT1 \frac{\partial L}{\partial W_{aa}} = \frac{\partial L}{\partial a^{T-1}} \frac{\partial a^{T-1}}{\partial z_a^{T-1}} a^{T-2} \\ \frac{\partial L}{\partial W_{ax}} = \frac{\partial L}{\partial a^{T-1}} \frac{\partial a^{T-1}}{\partial z_a^{T-1}} x^{T-1} \\ \frac{\partial L}{\partial b_a} = \frac{\partial L}{\partial a^{T-1}} \frac{\partial a^{T-1}}{\partial z_a^{T-1}}

ここから、次の層に対する aT2a^{T-2} に対する勾配を計算することができます。

LaT2=LaT1aT1zaT1Waa=LyTyTzyTWyaaTzaTWaaaT1zaT1Waa \frac{\partial L}{\partial a^{T-2}} = \frac{\partial L}{\partial a^{T-1}} \frac{\partial a^{T-1}}{\partial z_a^{T-1}} W_{aa} \\ = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T} W_{ya} \frac{\partial a^T}{\partial z_a^T} W_{aa} \frac{\partial a^{T-1}}{\partial z_a^{T-1}} W_{aa}

ここで、WaaW_{aa}, WaxW_{ax}, bab_a に対する勾配は、すべての後続の時間ステップにわたって LaT=LyTyTzyTWya\frac{\partial L}{\partial a^T} = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T} W_{ya}atzatWaa\frac{\partial a^t}{\partial z_a^t} W_{aa} の積、 そしてそれに対応する局所勾配によって求められることがわかります。各ステップの勾配が合計され、パラメータが更新されるため、次の式で逆伝播を表すことができます。

LWaa=LyTyTzyTWyat=1T[s=t+1T(aszasWaa)atzatat1]LWax=LyTyTzyTWyat=1T[s=t+1T(aszasWaa)atzatxt]Lba=LyTyTzyTWyat=1T[s=t+1T(aszasWaa)atzat] \frac{\partial L}{\partial W_{aa}} = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T} W_{ya} \sum_{t=1}^T [ \prod_{s=t+1}^{T} (\frac{\partial a^s}{\partial z_a^s}W_{aa}) \frac{\partial a^t}{\partial z_a^t} a^{t-1}] \\ \frac{\partial L}{\partial W_{ax}} = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T} W_{ya} \sum_{t=1}^T [ \prod_{s=t+1}^{T} (\frac{\partial a^s}{\partial z_a^s}W_{aa}) \frac{\partial a^t}{\partial z_a^t} x^{t}] \\ \frac{\partial L}{\partial b_a} = \frac{\partial L}{\partial y^T} \frac{\partial y^T}{\partial z_y^T} W_{ya} \sum_{t=1}^T [ \prod_{s=t+1}^{T} (\frac{\partial a^s}{\partial z_a^s}W_{aa}) \frac{\partial a^t}{\partial z_a^t}]

タスクが連続的な出力を伴う場合、総和項には毎回 yty^t からの勾配を取り込む必要があり、WyaW_{ya}byb_y も毎層で更新される必要があります。 このシナリオの勾配を自分で導いてみることをお勧めします。上記の方程式は一見複雑に見えるかもしれませんが、最後のステップから逆向きに勾配を追跡することで、 簡単に実装することができます。

この例で勾配を数学的に表現した理由は、長いステップが s=t+1T(ash1Waa)\prod_{s=t+1}^{T} \left(\frac{\partial a^s}{\partial h_1} W_{aa}\right) の積を多く行うことにつながり、 これがモデルを勾配爆発や勾配消失の問題にますます陥りやすくすることを示すためです。これには、 MLエンジニアへの道 #15 - 勾配消失・勾配爆発で議論された対策のいくつかを用いても同様です。

さらに、上記の内容は、あるステップでの勾配が後続のステップの勾配に依存していること、 そして前向きの伝播と同様に逆伝播が並列処理を活用できないことを明らかにしています。 これは、RNNのトレーニングが他のニューラルネットワーク(ANNやCNNなど)よりも大幅に遅くなる可能性があることを意味します。

コードの実装

RNNの仕組みを理解したところで、TensorFlowとPyTorchの両方でRNNモデルを実装してみましょう。 今回は、1961年にブラウン大学が作成した最初の英語の電子コーパスであるブラウンコーパスを使った簡単な問題に取り組みます(NLTK, n.d.)。 このコーパスには、ニュース、社説、レビューなど15のジャンルに分類された文書ファイルが含まれています。 このタスクでは、文書内のテキストからジャンルを分類することを目指します。

ステップ 1 & 2. データ探索と前処理

幸運にも、NLTKライブラリを使うことで、データのさまざまな側面に便利にアクセスできます。

from nltk.corpus import brown
 
fileids = brown.fileids() # file IDs
words = list(brown.words()) # Words
categories = brown.categories() # Categories
brown.raw(fileids[0]) # Raw text of a file ID

ここから、テキストを前処理して、様々な方法でトークン化することができます。ここでは、これまで使用してきたBPE(Byte Pair Encoding)サブワードトークン化を使用します(BPEについてのコードと説明は、既にカバーしているので省略します)。 サブワードトークンマップを取得した後、それを利用して文書分類タスクの準備を開始します。

# category -> index (int)
category_map = dict(zip(brown.categories(), range(len(brown.categories()))))
 
def generate_data():
  tokenized = []
  categories = []
  for fileid in fileids:
    corpus = text_preprocessing(brown.raw(fileid))
    category = brown.categories(fileid)[0]
    tokenized.append(tokenize(corpus, tokens))
    categories.append(category_map[category])
 
  tokenized_len = [len(i) for i in tokenized]
  max_len = max(tokenized_len)
  for t in tokenized:
    while (len(t) != max_len):
        # Padding added. Make sure to have '[PAD]' in tokens map.
        # Padding is added for converting to numpy arrays and to tensors (they do not expect irregular lists)
        # You can also use ragged tensors it seems like. 
        t.append(tokens['[PAD]']) 
        
  return np.array(tokenized), np.array(categories)
 
tokenized, categories = generate_data()
categories = tf.keras.utils.to_categorical(categories)

トークン化されたコーパスのNumPy配列とカテゴリから、TensorFlowおよびPyTorchの両方で使用できるデータセットを準備します。 (データの前処理としてトークンを分散表現に変換することもできますが、通常はモデル内で埋め込み層を含めることで、トークンを分散表現に変換するプロセスが行われます。)

from sklearn.model_selection import train_test_split
 
X_train, X_test, y_train, y_text = train_test_split(tokenized, categories, test_size=0.2, random_state=101)
X_val, X_test, y_val, y_text = train_test_split(X_test, y_test, test_size=0.5, random_state=101)
 
# PyTorch
X_train, X_val, X_test = map(lambda X: torch.tensor(X, dtype=torch.int32), (X_train, X_val, X_test))
y_train, y_val, y_test = map(lambda y: torch.tensor(y, dtype=torch.float32), (y_train, y_val, y_test))
 
train_dataset = torch.utils.data.TensorDataset(X_train, y_train)
val_dataset = torch.utils.data.TensorDataset(X_val, y_val)
test_dataset = torch.utils.data.TensorDataset(X_test, y_test)
 
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=8, shuffle=True)
val_loader = torch.utils.data.DataLoader(dataset=val_dataset, batch_size=4, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=1, shuffle=True)

ステップ 3. モデル

次に、TensorFlowおよびPyTorchでRNNモデルを実装する例を示します。

この記事では、学習結果やステップ4(モデル評価)を省略しますが、文書のジャンルを分類するための学習は、やや難航することに注意してください(これは、BPEを実行する回数などによっても異なります)。 また、新しい埋め込み層をモデルの一部として扱い、このタスクに適した分散表現を出力できる埋め込み層を訓練することもできるので、試してみることをお勧めします。

この作業を進める中で、モデルの訓練時間や予測にかかる時間が非常に長いことに気づくかもしれません。 これは、可変長の入力を処理するために追加されたパディングの影響もありますが、主な原因は、RNNの順次的な計算が並列処理できず、ANNやCNNのようにGPUで効率的に処理できないことにあります。

結論

この記事では、RNNの順次的な計算構造について説明し、それが可変長の入力シーケンスを処理できる点について議論しました。 また、この利点にもかかわらず、RNNには勾配の不安定性や並列処理ができないという重大な問題があることを、 数学的および実験的に確認しました。そのため、現在ではRNNはあまり実際には使用されていませんが、依然として他の重要なモデルの理論的基盤となっています。 次の記事では、RNNの不安定な勾配問題に取り組む方法について説明します。

リソース