AI-For-Beginners/translations/mo/lessons/5-NLP/16-RNN/RNNTF.ipynb

460 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 循環神經網路\n",
"\n",
"在上一個模組中,我們介紹了文本的豐富語義表示。我們使用的架構能夠捕捉句子中單詞的聚合意義,但它並未考慮單詞的**順序**,因為嵌入後的聚合操作會將原始文本中的這些信息移除。由於這些模型無法表示單詞的順序,因此它們無法解決更複雜或更具歧義的任務,例如文本生成或問題回答。\n",
"\n",
"為了捕捉文本序列的意義,我們將使用一種稱為**循環神經網路**Recurrent Neural Network簡稱 RNN的神經網路架構。在使用 RNN 時我們會將句子逐個標記token傳遞給網路網路會生成某種**狀態**,然後我們將該狀態與下一個標記一起再次傳遞給網路。\n",
"\n",
"![顯示循環神經網路生成示例的圖片。](../../../../../translated_images/rnn.27f5c29c53d727b546ad3961637a267f0fe9ec5ab01f2a26a853c92fcefbb574.mo.png)\n",
"\n",
"給定標記輸入序列 $X_0,\\dots,X_n$RNN 會創建一個神經網路區塊的序列,並通過反向傳播對該序列進行端到端訓練。每個網路區塊接受一對 $(X_i,S_i)$ 作為輸入,並生成 $S_{i+1}$ 作為結果。最終狀態 $S_n$ 或輸出 $Y_n$ 會進入線性分類器以生成結果。所有網路區塊共享相同的權重,並通過一次反向傳播訓練完成端到端學習。\n",
"\n",
"> 上圖展示了循環神經網路的展開形式(左側)和更緊湊的循環表示形式(右側)。需要注意的是,所有 RNN 單元都具有相同的**可共享權重**。\n",
"\n",
"由於狀態向量 $S_0,\\dots,S_n$ 是通過網路傳遞的RNN 能夠學習單詞之間的順序依賴關係。例如,當單詞 *not* 出現在序列中的某處時,它可以學會在狀態向量中否定某些元素。\n",
"\n",
"在內部,每個 RNN 單元包含兩個權重矩陣:$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 步驟中獲得一些輸出值。在這種情況下,還會有另一個矩陣 $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",
"> **注意**:某些版本的 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",
"> **注意**:在維度不是很高的情況下,例如使用字符級標記化時,直接將獨熱編碼的標記傳遞給 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": [
"> **注意:** 為了簡化,我們在這裡使用未經訓練的嵌入層,但如果想要更好的效果,可以使用 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": [
"<tensorflow.python.keras.callbacks.History at 0x7f3e0030d350>"
]
},
"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",
"我們可以採取幾種方法來減少填充的數量。其中一種方法是根據序列長度重新排列數據集,並按大小分組所有序列。這可以使用 `tf.data.experimental.bucket_by_sequence_length` 函數來完成(請參閱[文件](https://www.tensorflow.org/api_docs/python/tf/data/experimental/bucket_by_sequence_length))。\n",
"\n",
"另一種方法是使用**遮罩**。在 Keras 中,某些層支持額外的輸入,用於指示哪些標記應在訓練中被考慮。要將遮罩整合到模型中,我們可以選擇添加一個單獨的 `Masking` 層([文件](https://keras.io/api/layers/core_layers/masking/)),或者在 `Embedding` 層中指定 `mask_zero=True` 參數。\n",
"\n",
"> **Note**: 完成整個數據集的一個訓練週期大約需要 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": [
"<tensorflow.python.keras.callbacks.History at 0x7f3dec118850>"
]
},
"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",
"> **注意**:你是否注意到我們一直使用的是基於新聞標題訓練的向量化工具,而不是整篇文章的內容?這可能導致部分詞彙被忽略,因此重新訓練向量化工具會更好。然而,這可能只會帶來非常小的影響,所以為了簡化流程,我們將繼續使用之前的預訓練向量化工具。\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## LSTM: 長短期記憶\n",
"\n",
"RNN 的主要問題之一是 **梯度消失**。RNN 的結構可能會很長,在反向傳播時,梯度可能很難傳遞回網路的第一層。當這種情況發生時,網路無法學習遠距離的詞元之間的關係。為了解決這個問題,可以通過使用 **門控機制** 引入 **顯式狀態管理**。最常見的兩種引入門控機制的架構是 **長短期記憶** (LSTM) 和 **門控循環單元** (GRU)。我們在這裡將討論 LSTM。\n",
"\n",
"![顯示長短期記憶單元範例的圖片](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"LSTM 網路的組織方式與 RNN 類似,但有兩個狀態會從一層傳遞到下一層:實際的狀態 $c$ 和隱藏向量 $h$。在每個單元中,隱藏向量 $h_{t-1}$ 與輸入 $x_t$ 結合,並共同控制狀態 $c_t$ 和輸出 $h_t$ 的變化,這是通過 **門控機制** 完成的。每個門都有 sigmoid 激活函數(輸出範圍為 $[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/) by 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": [
"<tensorflow.python.keras.callbacks.History at 0x7f3d6af5c350>"
]
},
"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` 層會在其內部生成該層的兩個副本,並將其中一個副本的 `go_backwards` 屬性設置為 `True`,使其沿著序列的相反方向運算。\n",
"\n",
"無論是單向還是雙向的循環網絡,都能捕捉序列中的模式,並將其存儲到狀態向量中或作為輸出返回。與卷積網絡類似,我們可以在第一層循環層之後再構建另一層循環層,以捕捉更高層次的模式,這些模式是由第一層提取的低層次模式構建而成的。這引出了 **多層 RNN** 的概念,它由兩層或更多層循環網絡組成,其中前一層的輸出作為下一層的輸入。\n",
"\n",
"![顯示多層長短期記憶 RNN 的圖片](../../../../../translated_images/multi-layer-lstm.dd975e29bb2a59fe58b429db833932d734c81f211cad2783797a9608984acb8c.mo.jpg)\n",
"\n",
"*圖片來源:[這篇精彩的文章](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3),作者 Fernando López。*\n",
"\n",
"Keras 讓構建這些網絡變得非常簡單,因為你只需要在模型中添加更多的循環層。對於最後一層以外的所有層,我們需要指定 `return_sequences=True` 參數,因為我們需要該層返回所有中間狀態,而不僅僅是循環運算的最終狀態。\n",
"\n",
"現在,我們來為分類問題構建一個雙層雙向 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": [
"## RNNs 用於其他任務\n",
"\n",
"到目前為止,我們專注於使用 RNNs 來對文本序列進行分類。但它們還能處理更多任務,例如文本生成和機器翻譯——我們將在下一單元中探討這些任務。\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-28T12:19:31+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNTF.ipynb",
"language_code": "mo"
}
},
"nbformat": 4,
"nbformat_minor": 4
}