{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 再帰型ニューラルネットワーク\n", "\n", "前のモジュールでは、テキストの豊かな意味表現について学びました。これまで使用してきたアーキテクチャは、文中の単語の集合的な意味を捉えることができますが、単語の**順序**を考慮していません。埋め込み後の集約操作によって、元のテキストからこの情報が失われるためです。このようなモデルは単語の順序を表現することができないため、テキスト生成や質問応答のような、より複雑で曖昧なタスクを解決することができません。\n", "\n", "テキストシーケンスの意味を捉えるために、**再帰型ニューラルネットワーク**(Recurrent Neural Network、RNN)と呼ばれるニューラルネットワークのアーキテクチャを使用します。RNNを使用する際には、文をネットワークに1トークンずつ通し、ネットワークが生成する**状態**を次のトークンとともに再びネットワークに渡します。\n", "\n", "![再帰型ニューラルネットワーク生成の例を示す画像](../../../../../translated_images/ja/rnn.27f5c29c53d727b5.webp)\n", "\n", "トークンの入力シーケンス $X_0,\\dots,X_n$ が与えられると、RNNはニューラルネットワークブロックのシーケンスを作成し、このシーケンスをバックプロパゲーションを使用してエンドツーエンドで学習します。各ネットワークブロックは、入力としてペア $(X_i,S_i)$ を受け取り、結果として $S_{i+1}$ を生成します。最終状態 $S_n$ または出力 $Y_n$ は線形分類器に渡され、結果を生成します。すべてのネットワークブロックは同じ重みを共有し、1回のバックプロパゲーションパスでエンドツーエンドで学習されます。\n", "\n", "> 上の図は、展開された形(左側)とよりコンパクトな再帰表現(右側)の再帰型ニューラルネットワークを示しています。すべてのRNNセルが同じ**共有可能な重み**を持つことを理解することが重要です。\n", "\n", "状態ベクトル $S_0,\\dots,S_n$ がネットワークを通じて渡されるため、RNNは単語間の順序的な依存関係を学習することができます。例えば、シーケンス内のどこかに単語 *not* が現れる場合、状態ベクトル内の特定の要素を否定する方法を学習することができます。\n", "\n", "内部では、各RNNセルには2つの重み行列 $W_H$ と $W_I$、およびバイアス $b$ が含まれています。各RNNステップで、入力 $X_i$ と入力状態 $S_i$ が与えられると、出力状態は $S_{i+1} = f(W_H\\times S_i + W_I\\times X_i+b)$ として計算されます。ここで、$f$ は活性化関数(しばしば $\\tanh$)です。\n", "\n", "> テキスト生成(次のユニットで扱います)や機械翻訳のような問題では、各RNNステップで何らかの出力値を得たい場合があります。この場合、もう1つの行列 $W_O$ が存在し、出力は $Y_i=f(W_O\\times S_i+b_O)$ として計算されます。\n", "\n", "再帰型ニューラルネットワークがニュースデータセットの分類にどのように役立つかを見てみましょう。\n", "\n", "> サンドボックス環境では、必要なライブラリがインストールされ、データが事前取得されていることを確認するために、以下のセルを実行する必要があります。ローカルで実行している場合は、以下のセルをスキップできます。\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import sys\n", "!{sys.executable} -m pip install --quiet tensorflow_datasets==4.4.0\n", "!cd ~ && wget -q -O - https://mslearntensorflowlp.blob.core.windows.net/data/tfds-ag-news.tgz | tar xz" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import tensorflow as tf\n", "from tensorflow import keras\n", "import tensorflow_datasets as tfds\n", "import numpy as np\n", "\n", "# We are going to be training pretty large models. In order not to face errors, we need\n", "# to set tensorflow option to grow GPU memory allocation when required\n", "physical_devices = tf.config.list_physical_devices('GPU') \n", "if len(physical_devices)>0:\n", " tf.config.experimental.set_memory_growth(physical_devices[0], True)\n", "\n", "ds_train, ds_test = tfds.load('ag_news_subset').values()" ] }, { "cell_type": "markdown", "metadata": { "nteract": { "transient": { "deleting": false } } }, "source": [ "大規模なモデルをトレーニングする際、GPUメモリの割り当てが問題になることがあります。また、データがGPUメモリに収まるようにしつつ、トレーニングを十分に高速化するために、ミニバッチサイズを調整して試す必要があるかもしれません。このコードを自分のGPUマシンで実行している場合、トレーニングを高速化するためにミニバッチサイズを調整して試してみるとよいでしょう。\n", "\n", "> **Note**: 特定のバージョンのNVidiaドライバーでは、モデルのトレーニング後にメモリを解放しないことが知られています。このノートブックではいくつかの例を実行しており、特定の環境ではメモリが不足する可能性があります。特に、同じノートブック内で独自の実験を行っている場合にそのリスクが高まります。モデルのトレーニングを開始する際に奇妙なエラーが発生した場合、ノートブックのカーネルを再起動することを検討してください。\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": false, "source_hidden": false }, "nteract": { "transient": { "deleting": false } } }, "outputs": [], "source": [ "batch_size = 16\n", "embed_size = 64" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## シンプルなRNN分類器\n", "\n", "シンプルなRNNの場合、各リカレントユニットは単純な線形ネットワークで構成されており、入力ベクトルと状態ベクトルを受け取り、新しい状態ベクトルを生成します。Kerasでは、これを`SimpleRNN`レイヤーで表現できます。\n", "\n", "RNNレイヤーにワンホットエンコードされたトークンを直接渡すことも可能ですが、高次元であるため、これはあまり良い方法ではありません。そのため、まず埋め込みレイヤーを使用して単語ベクトルの次元を下げ、その後にRNNレイヤー、最後に`Dense`分類器を使用します。\n", "\n", "> **Note**: 次元がそれほど高くない場合、例えば文字レベルのトークン化を使用する場合には、ワンホットエンコードされたトークンを直接RNNセルに渡すのが理にかなっている場合もあります。\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Model: \"sequential\"\n", "_________________________________________________________________\n", "Layer (type) Output Shape Param # \n", "=================================================================\n", "text_vectorization (TextVect (None, None) 0 \n", "_________________________________________________________________\n", "embedding (Embedding) (None, None, 64) 1280000 \n", "_________________________________________________________________\n", "simple_rnn (SimpleRNN) (None, 16) 1296 \n", "_________________________________________________________________\n", "dense (Dense) (None, 4) 68 \n", "=================================================================\n", "Total params: 1,281,364\n", "Trainable params: 1,281,364\n", "Non-trainable params: 0\n", "_________________________________________________________________\n" ] } ], "source": [ "vocab_size = 20000\n", "\n", "vectorizer = keras.layers.experimental.preprocessing.TextVectorization(\n", " max_tokens=vocab_size,\n", " input_shape=(1,))\n", "\n", "model = keras.models.Sequential([\n", " vectorizer,\n", " keras.layers.Embedding(vocab_size, embed_size),\n", " keras.layers.SimpleRNN(16),\n", " keras.layers.Dense(4,activation='softmax')\n", "])\n", "\n", "model.summary()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **Note:** ここでは簡単のために未学習の埋め込み層を使用していますが、より良い結果を得るためには、前のユニットで説明したように、Word2Vecを使用した事前学習済みの埋め込み層を使用することができます。このコードを事前学習済みの埋め込みに対応させるのは良い練習になるでしょう。\n", "\n", "では、RNNを訓練してみましょう。一般的にRNNは訓練が非常に難しいです。なぜなら、RNNセルがシーケンスの長さに沿って展開されると、逆伝播に関与する層の数が非常に多くなるからです。そのため、学習率を小さく設定し、大規模なデータセットでネットワークを訓練する必要があります。良い結果を得るには時間がかかるため、GPUを使用することが推奨されます。\n", "\n", "処理を高速化するために、ニュースのタイトルのみにRNNモデルを訓練し、説明文は省略します。説明文を含めて訓練を試し、モデルを訓練できるか試してみてください。\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Training vectorizer\n" ] } ], "source": [ "def extract_title(x):\n", " return x['title']\n", "\n", "def tupelize_title(x):\n", " return (extract_title(x),x['label'])\n", "\n", "print('Training vectorizer')\n", "vectorizer.adapt(ds_train.take(2000).map(extract_title))" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "7500/7500 [==============================] - 82s 11ms/step - loss: 0.6629 - acc: 0.7623 - val_loss: 0.5559 - val_acc: 0.7995\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n", "model.fit(ds_train.map(tupelize_title).batch(batch_size),validation_data=ds_test.map(tupelize_title).batch(batch_size))" ] }, { "cell_type": "markdown", "metadata": { "nteract": { "transient": { "deleting": false } } }, "source": [ "> **注意** ニュースのタイトルのみでトレーニングしているため、ここでの精度は低くなる可能性があります。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 変数シーケンスの再確認\n", "\n", "`TextVectorization`レイヤーは、ミニバッチ内の可変長シーケンスを自動的にパッドトークンで埋めます。ただし、これらのトークンもトレーニングに参加し、モデルの収束を複雑にする可能性があります。\n", "\n", "パディングの量を最小限に抑えるために取れるアプローチはいくつかあります。その1つは、データセットをシーケンスの長さで並べ替え、すべてのシーケンスをサイズごとにグループ化する方法です。これは、`tf.data.experimental.bucket_by_sequence_length`関数を使用して実行できます([ドキュメント](https://www.tensorflow.org/api_docs/python/tf/data/experimental/bucket_by_sequence_length)を参照)。\n", "\n", "もう1つのアプローチは、**マスキング**を使用することです。Kerasでは、一部のレイヤーがトレーニング時に考慮すべきトークンを示す追加の入力をサポートしています。モデルにマスキングを組み込むには、`Masking`レイヤーを別途追加する([ドキュメント](https://keras.io/api/layers/core_layers/masking/)を参照)か、`Embedding`レイヤーの`mask_zero=True`パラメータを指定する方法があります。\n", "\n", "> **Note**: このトレーニングは、データセット全体で1エポックを完了するのに約5分かかります。もし途中で待ちきれなくなった場合は、トレーニングを中断しても構いません。また、トレーニングに使用するデータ量を制限することもできます。その場合は、`ds_train`や`ds_test`データセットの後に`.take(...)`句を追加してください。\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "7500/7500 [==============================] - 371s 49ms/step - loss: 0.5401 - acc: 0.8079 - val_loss: 0.3780 - val_acc: 0.8822\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def extract_text(x):\n", " return x['title']+' '+x['description']\n", "\n", "def tupelize(x):\n", " return (extract_text(x),x['label'])\n", "\n", "model = keras.models.Sequential([\n", " vectorizer,\n", " keras.layers.Embedding(vocab_size,embed_size,mask_zero=True),\n", " keras.layers.SimpleRNN(16),\n", " keras.layers.Dense(4,activation='softmax')\n", "])\n", "\n", "model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n", "model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "マスキングを使用することで、タイトルと説明の全データセットを使ってモデルを訓練できるようになりました。\n", "\n", "> **Note**: ニュースのタイトルで訓練されたベクトライザーを使用していることに気づきましたか?記事全体の本文ではなくタイトルだけを使っているため、一部のトークンが無視される可能性があります。そのため、ベクトライザーを再訓練する方が望ましいかもしれません。ただし、その影響はごくわずかである可能性が高いため、簡潔さを優先して以前の事前訓練済みベクトライザーを使用することにします。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## LSTM: 長短期記憶\n", "\n", "RNNの主な問題の1つは、**勾配消失**です。RNNは非常に長くなることがあり、逆伝播中にネットワークの最初の層まで勾配を伝播させるのが難しくなる場合があります。このような状況になると、ネットワークは離れたトークン間の関係を学習できなくなります。この問題を回避する1つの方法は、**ゲート**を使用して**明示的な状態管理**を導入することです。ゲートを導入する最も一般的なアーキテクチャは、**長短期記憶(LSTM)**と**ゲート付きリレー単位(GRU)**です。ここではLSTMについて説明します。\n", "\n", "![長短期記憶セルの例を示す画像](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "LSTMネットワークはRNNと似た構造で組織されていますが、層から層へ渡される2つの状態があります。それは、実際の状態$c$と隠れベクトル$h$です。各ユニットでは、隠れベクトル$h_{t-1}$が入力$x_t$と組み合わされ、それらが一緒になって**ゲート**を通じて状態$c_t$と出力$h_t$に何が起こるかを制御します。各ゲートにはシグモイド活性化関数(出力範囲は$[0,1]$)があり、状態ベクトルに掛け算されるとビットマスクのように機能すると考えることができます。LSTMには以下のゲートがあります(上記の図で左から右に並んでいます):\n", "* **忘却ゲート**:ベクトル$c_{t-1}$のどの成分を忘れるべきか、またどの成分を通過させるべきかを決定します。\n", "* **入力ゲート**:入力ベクトルと前の隠れベクトルからどれだけの情報を状態ベクトルに取り込むべきかを決定します。\n", "* **出力ゲート**:新しい状態ベクトルを取り、それを使って新しい隠れベクトル$h_t$を生成する際にどの成分を使用するかを決定します。\n", "\n", "状態$c$の成分は、オン・オフを切り替えられるフラグのように考えることができます。例えば、シーケンス内で*Alice*という名前に出会ったとき、それが女性を指していると推測し、文中に女性名詞があることを示すフラグを状態で立てます。その後、*and Tom*という単語に出会うと、複数名詞があることを示すフラグを立てます。このように、状態を操作することで文法的な特性を追跡することができます。\n", "\n", "> **Note**: LSTMの内部構造を理解するための素晴らしいリソースはこちらです:[Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/)(Christopher Olahによる)。\n", "\n", "LSTMセルの内部構造は複雑に見えるかもしれませんが、Kerasはこの実装を`LSTM`レイヤー内に隠しているため、上記の例で行う必要があるのは再帰レイヤーを置き換えることだけです。\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "15000/15000 [==============================] - 188s 13ms/step - loss: 0.5692 - acc: 0.7916 - val_loss: 0.3441 - val_acc: 0.8870\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n" ] }, { "data": { "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model = keras.models.Sequential([\n", " vectorizer,\n", " keras.layers.Embedding(vocab_size, embed_size),\n", " keras.layers.LSTM(8),\n", " keras.layers.Dense(4,activation='softmax')\n", "])\n", "\n", "model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n", "model.fit(ds_train.map(tupelize).batch(8),validation_data=ds_test.map(tupelize).batch(8))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 双方向および多層RNN\n", "\n", "これまでの例では、リカレントネットワークはシーケンスの始まりから終わりまで動作していました。この方法は、私たちが読む方向や話を聞く方向と一致しているため、自然に感じられます。しかし、入力シーケンスをランダムにアクセスする必要があるシナリオでは、リカレント計算を両方向で実行する方が理にかなっています。両方向で計算を可能にするRNNは**双方向RNN**と呼ばれ、リカレント層を特別な`Bidirectional`層でラップすることで作成できます。\n", "\n", "> **Note**: `Bidirectional`層は内部の層を2つコピーし、そのうちの1つの`go_backwards`プロパティを`True`に設定して、シーケンスに沿って逆方向に進むようにします。\n", "\n", "リカレントネットワーク(単方向でも双方向でも)は、シーケンス内のパターンを捉え、それを状態ベクトルに保存したり、出力として返したりします。畳み込みネットワークと同様に、最初の層で抽出された低レベルのパターンから構築された高レベルのパターンを捉えるために、最初の層の後に別のリカレント層を追加することができます。これにより、**多層RNN**という概念が生まれます。これは、2つ以上のリカレントネットワークで構成され、前の層の出力が次の層の入力として渡されます。\n", "\n", "![多層長短期記憶RNNを示す画像](../../../../../translated_images/ja/multi-layer-lstm.dd975e29bb2a59fe.webp)\n", "\n", "*Fernando Lópezによる[素晴らしい投稿](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3)からの画像。*\n", "\n", "Kerasを使用すると、これらのネットワークを簡単に構築できます。モデルにリカレント層を追加するだけで済みます。最後の層以外のすべての層では、`return_sequences=True`パラメータを指定する必要があります。これは、リカレント計算の最終状態だけでなく、すべての中間状態を返す必要があるためです。\n", "\n", "では、分類問題のために2層の双方向LSTMを構築してみましょう。\n", "\n", "> **Note** このコードは再び実行にかなり時間がかかりますが、これまでで最高の精度を達成します。そのため、待つ価値があるかもしれません。結果を確認してみましょう。\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "5044/7500 [===================>..........] - ETA: 2:33 - loss: 0.3709 - acc: 0.8706\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r5045/7500 [===================>..........] - ETA: 2:33 - loss: 0.3709 - acc: 0.8706" ] } ], "source": [ "model = keras.models.Sequential([\n", " vectorizer,\n", " keras.layers.Embedding(vocab_size, 128, mask_zero=True),\n", " keras.layers.Bidirectional(keras.layers.LSTM(64,return_sequences=True)),\n", " keras.layers.Bidirectional(keras.layers.LSTM(64)), \n", " keras.layers.Dense(4,activation='softmax')\n", "])\n", "\n", "model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n", "model.fit(ds_train.map(tupelize).batch(batch_size),\n", " validation_data=ds_test.map(tupelize).batch(batch_size))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 他のタスクにおけるRNN\n", "\n", "これまで、RNNを使ってテキストのシーケンスを分類することに焦点を当ててきました。しかし、RNNはそれ以外にも、テキスト生成や機械翻訳など、さまざまなタスクを処理することができます。これらのタスクについては次のユニットで取り上げます。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**免責事項**: \nこの文書は、AI翻訳サービス [Co-op Translator](https://github.com/Azure/co-op-translator) を使用して翻訳されています。正確性を追求しておりますが、自動翻訳には誤りや不正確な部分が含まれる可能性があることをご承知ください。元の言語で記載された文書が正式な情報源とみなされるべきです。重要な情報については、専門の人間による翻訳を推奨します。この翻訳の使用に起因する誤解や誤解釈について、当社は責任を負いません。\n" ] } ], "metadata": { "kernel_info": { "name": "conda-env-py37_tensorflow-py" }, "kernelspec": { "display_name": "py37_tensorflow", "language": "python", "name": "conda-env-py37_tensorflow-py" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.9" }, "nteract": { "version": "nteract-front-end@1.0.0" }, "coopTranslator": { "original_hash": "81351e61f619b432ff51010a4f993194", "translation_date": "2025-08-30T10:27:22+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNTF.ipynb", "language_code": "ja" } }, "nbformat": 4, "nbformat_minor": 4 }