{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# เครือข่ายประสาทแบบวนซ้ำ\n", "\n", "ในโมดูลก่อนหน้านี้ เราได้ใช้การแสดงผลเชิงความหมายที่หลากหลายของข้อความ และตัวจำแนกเชิงเส้นแบบง่ายที่อยู่บน embeddings สิ่งที่สถาปัตยกรรมนี้ทำคือการจับความหมายรวมของคำในประโยค แต่ไม่ได้คำนึงถึง **ลำดับ** ของคำ เนื่องจากการดำเนินการรวมบน embeddings ได้ลบข้อมูลนี้ออกจากข้อความต้นฉบับไปแล้ว เนื่องจากโมเดลเหล่านี้ไม่สามารถจำลองลำดับของคำได้ จึงไม่สามารถแก้ปัญหาที่ซับซ้อนหรือคลุมเครือ เช่น การสร้างข้อความหรือการตอบคำถามได้\n", "\n", "เพื่อจับความหมายของลำดับข้อความ เราจำเป็นต้องใช้สถาปัตยกรรมเครือข่ายประสาทอีกแบบหนึ่ง ซึ่งเรียกว่า **เครือข่ายประสาทแบบวนซ้ำ** หรือ RNN ใน RNN เราจะส่งประโยคผ่านเครือข่ายทีละสัญลักษณ์ และเครือข่ายจะสร้าง **สถานะ** ซึ่งเราจะส่งกลับเข้าเครือข่ายอีกครั้งพร้อมกับสัญลักษณ์ถัดไป\n", "\n", "Given the input sequence of tokens $X_0,\\dots,X_n$, RNN creates a sequence of neural network blocks, and trains this sequence end-to-end using back propagation. Each network block takes a pair $(X_i,S_i)$ as an input, and produces $S_{i+1}$ as a result. Final state $S_n$ or output $X_n$ goes into a linear classifier to produce the result. All network blocks share the same weights, and are trained end-to-end using one back propagation pass.\n", "\n", "เนื่องจากเวกเตอร์สถานะ $S_0,\\dots,S_n$ ถูกส่งผ่านเครือข่าย เครือข่ายจึงสามารถเรียนรู้ความสัมพันธ์เชิงลำดับระหว่างคำได้ ตัวอย่างเช่น เมื่อคำว่า *not* ปรากฏที่ใดที่หนึ่งในลำดับ เครือข่ายสามารถเรียนรู้ที่จะปฏิเสธองค์ประกอบบางอย่างภายในเวกเตอร์สถานะ ส่งผลให้เกิดการปฏิเสธ\n", "\n", "> เนื่องจากน้ำหนักของบล็อก RNN ทั้งหมดในภาพถูกแชร์กัน ภาพเดียวกันจึงสามารถแสดงเป็นบล็อกเดียว (ทางด้านขวา) พร้อมกับวงวนย้อนกลับ ซึ่งส่งสถานะผลลัพธ์ของเครือข่ายกลับไปยังอินพุต\n", "\n", "มาดูกันว่าเครือข่ายประสาทแบบวนซ้ำสามารถช่วยเราจำแนกชุดข้อมูลข่าวได้อย่างไร\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading dataset...\n", "Building vocab...\n" ] } ], "source": [ "import torch\n", "import torchtext\n", "from torchnlp import *\n", "train_dataset, test_dataset, classes, vocab = load_dataset()\n", "vocab_size = len(vocab)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ตัวจำแนกประเภท RNN แบบง่าย\n", "\n", "ในกรณีของ RNN แบบง่าย แต่ละหน่วยประมวลผลแบบวนซ้ำเป็นเครือข่ายเชิงเส้นแบบง่าย ซึ่งรับเวกเตอร์อินพุตที่รวมกันและเวกเตอร์สถานะ และสร้างเวกเตอร์สถานะใหม่ PyTorch แสดงหน่วยนี้ด้วยคลาส `RNNCell` และเครือข่ายของเซลล์เหล่านี้ - เป็นเลเยอร์ `RNN`\n", "\n", "เพื่อกำหนดตัวจำแนกประเภท RNN เราจะเริ่มต้นด้วยการใช้เลเยอร์ฝังตัวเพื่อปรับลดมิติของคำศัพท์อินพุต และจากนั้นเพิ่มเลเยอร์ RNN ด้านบน:\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class RNNClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.rnn = torch.nn.RNN(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " x,h = self.rnn(x)\n", " return self.fc(x.mean(dim=1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **หมายเหตุ:** เราใช้ embedding layer ที่ไม่ได้ผ่านการฝึกฝนเพื่อความเรียบง่าย แต่หากต้องการผลลัพธ์ที่ดียิ่งขึ้น เราสามารถใช้ embedding layer ที่ผ่านการฝึกฝนมาแล้ว เช่น Word2Vec หรือ GloVe embeddings ตามที่อธิบายไว้ในหน่วยก่อนหน้า เพื่อความเข้าใจที่ดีขึ้น คุณอาจต้องปรับโค้ดนี้ให้ทำงานร่วมกับ embeddings ที่ผ่านการฝึกฝนมาแล้ว\n", "\n", "ในกรณีนี้ เราจะใช้ padded data loader ดังนั้นแต่ละ batch จะมีลำดับข้อมูลที่ถูก padding ให้มีความยาวเท่ากัน RNN layer จะรับลำดับของ embedding tensors และสร้างผลลัพธ์สองอย่าง:\n", "* $x$ คือ ลำดับของผลลัพธ์จาก RNN cell ในแต่ละขั้นตอน\n", "* $h$ คือ hidden state สุดท้ายสำหรับองค์ประกอบสุดท้ายของลำดับ\n", "\n", "จากนั้นเราจะใช้ fully-connected linear classifier เพื่อให้ได้จำนวนคลาส\n", "\n", "> **หมายเหตุ:** RNNs เป็นโมเดลที่ฝึกฝนได้ยาก เนื่องจากเมื่อ RNN cells ถูก unroll ตามความยาวของลำดับ จำนวนชั้นที่เกี่ยวข้องกับการ back propagation จะมีจำนวนมาก ดังนั้นเราจำเป็นต้องเลือก learning rate ที่เล็ก และฝึกเครือข่ายบนชุดข้อมูลที่ใหญ่ขึ้นเพื่อให้ได้ผลลัพธ์ที่ดี อาจใช้เวลานาน ดังนั้นการใช้ GPU จะช่วยให้ทำงานได้เร็วขึ้น\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.3090625\n", "6400: acc=0.38921875\n", "9600: acc=0.4590625\n", "12800: acc=0.511953125\n", "16000: acc=0.5506875\n", "19200: acc=0.57921875\n", "22400: acc=0.6070089285714285\n", "25600: acc=0.6304296875\n", "28800: acc=0.6484027777777778\n", "32000: acc=0.66509375\n", "35200: acc=0.6790056818181818\n", "38400: acc=0.6929166666666666\n", "41600: acc=0.7035817307692308\n", "44800: acc=0.7137276785714286\n", "48000: acc=0.72225\n", "51200: acc=0.73001953125\n", "54400: acc=0.7372794117647059\n", "57600: acc=0.7436631944444444\n", "60800: acc=0.7503947368421052\n", "64000: acc=0.75634375\n", "67200: acc=0.7615773809523809\n", "70400: acc=0.7662642045454545\n", "73600: acc=0.7708423913043478\n", "76800: acc=0.7751822916666666\n", "80000: acc=0.7790625\n", "83200: acc=0.7825\n", "86400: acc=0.7858564814814815\n", "89600: acc=0.7890513392857142\n", "92800: acc=0.7920474137931034\n", "96000: acc=0.7952708333333334\n", "99200: acc=0.7982258064516129\n", "102400: acc=0.80099609375\n", "105600: acc=0.8037594696969697\n", "108800: acc=0.8060569852941176\n" ] } ], "source": [ "train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)\n", "net = RNNClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch(net,train_loader, lr=0.001)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## หน่วยความจำระยะยาวและระยะสั้น (LSTM)\n", "\n", "หนึ่งในปัญหาหลักของ RNN แบบดั้งเดิมคือปัญหา **การลดทอนของกราดิเอนต์** (vanishing gradients) เนื่องจาก RNN ถูกฝึกแบบ end-to-end ในการส่งผ่านแบบ back-propagation เพียงครั้งเดียว ทำให้เกิดความยากลำบากในการส่งผ่านข้อผิดพลาดไปยังเลเยอร์แรก ๆ ของเครือข่าย และเครือข่ายจึงไม่สามารถเรียนรู้ความสัมพันธ์ระหว่างโทเค็นที่อยู่ห่างกันได้ วิธีหนึ่งในการหลีกเลี่ยงปัญหานี้คือการแนะนำ **การจัดการสถานะอย่างชัดเจน** โดยใช้สิ่งที่เรียกว่า **gates** สถาปัตยกรรมที่เป็นที่รู้จักมากที่สุดในประเภทนี้คือ **Long Short Term Memory** (LSTM) และ **Gated Relay Unit** (GRU)\n", "\n", "![ภาพแสดงตัวอย่างเซลล์ LSTM](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "เครือข่าย LSTM ถูกจัดระเบียบในลักษณะที่คล้ายกับ RNN แต่มีสองสถานะที่ถูกส่งผ่านจากเลเยอร์หนึ่งไปยังอีกเลเยอร์หนึ่ง: สถานะจริง $c$ และเวกเตอร์ซ่อน $h$ ในแต่ละหน่วย เวกเตอร์ซ่อน $h_i$ จะถูกเชื่อมต่อกับอินพุต $x_i$ และพวกมันจะควบคุมสิ่งที่เกิดขึ้นกับสถานะ $c$ ผ่าน **gates** แต่ละ gate เป็นเครือข่ายประสาทที่มีการกระตุ้นแบบ sigmoid (ผลลัพธ์อยู่ในช่วง $[0,1]$) ซึ่งสามารถคิดได้ว่าเป็นหน้ากากแบบบิตเมื่อคูณกับเวกเตอร์สถานะ มี gates ดังต่อไปนี้ (จากซ้ายไปขวาในภาพด้านบน):\n", "* **forget gate** ใช้เวกเตอร์ซ่อนและกำหนดว่าคอมโพเนนต์ใดของเวกเตอร์ $c$ ที่เราต้องลืม และคอมโพเนนต์ใดที่ต้องส่งผ่าน\n", "* **input gate** รับข้อมูลบางส่วนจากอินพุตและเวกเตอร์ซ่อน และแทรกข้อมูลนั้นลงในสถานะ\n", "* **output gate** แปลงสถานะผ่านเลเยอร์เชิงเส้นบางตัวที่มีการกระตุ้นแบบ $\\tanh$ จากนั้นเลือกคอมโพเนนต์บางส่วนโดยใช้เวกเตอร์ซ่อน $h_i$ เพื่อสร้างสถานะใหม่ $c_{i+1}$\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 อาจดูซับซ้อน แต่ PyTorch ซ่อนการใช้งานนี้ไว้ในคลาส `LSTMCell` และมีออบเจ็กต์ `LSTM` เพื่อแสดงทั้งเลเยอร์ LSTM ดังนั้นการใช้งานตัวจำแนก LSTM จะคล้ายกับ RNN แบบง่ายที่เราได้เห็นก่อนหน้านี้:\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class LSTMClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n", " self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " x,(h,c) = self.rnn(x)\n", " return self.fc(h[-1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.259375\n", "6400: acc=0.25859375\n", "9600: acc=0.26177083333333334\n", "12800: acc=0.2784375\n", "16000: acc=0.313\n", "19200: acc=0.3528645833333333\n", "22400: acc=0.3965625\n", "25600: acc=0.4385546875\n", "28800: acc=0.4752777777777778\n", "32000: acc=0.505375\n", "35200: acc=0.5326704545454546\n", "38400: acc=0.5557552083333334\n", "41600: acc=0.5760817307692307\n", "44800: acc=0.5954910714285714\n", "48000: acc=0.6118333333333333\n", "51200: acc=0.62681640625\n", "54400: acc=0.6404779411764706\n", "57600: acc=0.6520138888888889\n", "60800: acc=0.662828947368421\n", "64000: acc=0.673546875\n", "67200: acc=0.6831547619047619\n", "70400: acc=0.6917897727272727\n", "73600: acc=0.6997146739130434\n", "76800: acc=0.707109375\n", "80000: acc=0.714075\n", "83200: acc=0.7209134615384616\n", "86400: acc=0.727037037037037\n", "89600: acc=0.7326674107142858\n", "92800: acc=0.7379633620689655\n", "96000: acc=0.7433645833333333\n", "99200: acc=0.7479032258064516\n", "102400: acc=0.752119140625\n", "105600: acc=0.7562405303030303\n", "108800: acc=0.76015625\n", "112000: acc=0.7641339285714286\n", "115200: acc=0.7677777777777778\n", "118400: acc=0.7711233108108108\n" ] }, { "data": { "text/plain": [ "(0.03487814127604167, 0.7728)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = LSTMClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch(net,train_loader, lr=0.001)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ลำดับที่ถูกบีบอัด\n", "\n", "ในตัวอย่างของเรา เราต้องเติมค่าศูนย์ในทุกลำดับใน minibatch ซึ่งทำให้เกิดการสิ้นเปลืองหน่วยความจำบางส่วน แต่สำหรับ RNN สิ่งที่สำคัญกว่าคือการสร้างเซลล์ RNN เพิ่มเติมสำหรับข้อมูลที่ถูกเติม ซึ่งมีส่วนร่วมในการฝึก แต่ไม่ได้มีข้อมูลสำคัญใด ๆ จะดีกว่าหากฝึก RNN เฉพาะกับขนาดลำดับที่แท้จริงเท่านั้น\n", "\n", "เพื่อทำสิ่งนี้ PyTorch ได้แนะนำรูปแบบพิเศษสำหรับการจัดเก็บลำดับที่ถูกเติมค่า สมมติว่าเรามี minibatch ที่ถูกเติมค่า ซึ่งมีลักษณะดังนี้:\n", "```\n", "[[1,2,3,4,5],\n", " [6,7,8,0,0],\n", " [9,0,0,0,0]]\n", "```\n", "ที่นี่ 0 แทนค่าที่ถูกเติม และเวกเตอร์ความยาวจริงของลำดับข้อมูลคือ `[5,3,1]`\n", "\n", "เพื่อฝึก RNN อย่างมีประสิทธิภาพด้วยลำดับที่ถูกเติมค่า เราต้องการเริ่มการฝึกกลุ่มแรกของเซลล์ RNN ด้วย minibatch ขนาดใหญ่ (`[1,6,9]`) จากนั้นหยุดการประมวลผลลำดับที่สาม และดำเนินการฝึกต่อด้วย minibatch ที่เล็กลง (`[2,7]`, `[3,8]`) และต่อไปเรื่อย ๆ ดังนั้น ลำดับที่ถูกบีบอัดจะถูกแสดงเป็นเวกเตอร์เดียว - ในกรณีของเรา `[1,6,9,2,7,3,8,4,5]` และเวกเตอร์ความยาว (`[5,3,1]`) ซึ่งเราสามารถสร้าง minibatch ที่ถูกเติมค่าเดิมขึ้นมาใหม่ได้อย่างง่ายดาย\n", "\n", "เพื่อสร้างลำดับที่ถูกบีบอัด เราสามารถใช้ฟังก์ชัน `torch.nn.utils.rnn.pack_padded_sequence` ชั้น recurrent ทั้งหมด รวมถึง RNN, LSTM และ GRU รองรับลำดับที่ถูกบีบอัดเป็น input และสร้าง output ที่ถูกบีบอัด ซึ่งสามารถถอดรหัสได้โดยใช้ `torch.nn.utils.rnn.pad_packed_sequence`\n", "\n", "เพื่อให้สามารถสร้างลำดับที่ถูกบีบอัดได้ เราจำเป็นต้องส่งเวกเตอร์ความยาวไปยังเครือข่าย และดังนั้นเราจำเป็นต้องใช้ฟังก์ชันที่แตกต่างกันในการเตรียม minibatches:\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def pad_length(b):\n", " # build vectorized sequence\n", " v = [encode(x[1]) for x in b]\n", " # compute max length of a sequence in this minibatch and length sequence itself\n", " len_seq = list(map(len,v))\n", " l = max(len_seq)\n", " return ( # tuple of three tensors - labels, padded features, length sequence\n", " torch.LongTensor([t[0]-1 for t in b]),\n", " torch.stack([torch.nn.functional.pad(torch.tensor(t),(0,l-len(t)),mode='constant',value=0) for t in v]),\n", " torch.tensor(len_seq)\n", " )\n", "\n", "train_loader_len = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=pad_length, shuffle=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "เครือข่ายจริงจะคล้ายกับ `LSTMClassifier` ด้านบนมาก แต่ `forward` pass จะรับทั้ง padded minibatch และเวกเตอร์ของความยาวลำดับ หลังจากคำนวณ embedding แล้ว เราจะคำนวณ packed sequence ส่งผ่านไปยัง LSTM layer และจากนั้นจึงแปลงผลลัพธ์กลับมา\n", "\n", "> **Note**: จริง ๆ แล้วเราไม่ได้ใช้ผลลัพธ์ที่ถูกแปลงกลับมา `x` เพราะเราใช้ผลลัพธ์จาก hidden layers ในการคำนวณต่อไป ดังนั้น เราสามารถลบขั้นตอนการแปลงกลับออกจากโค้ดนี้ได้เลย เหตุผลที่เราวางไว้ในที่นี้ก็เพื่อให้คุณสามารถปรับเปลี่ยนโค้ดนี้ได้ง่ายขึ้น ในกรณีที่คุณจำเป็นต้องใช้ผลลัพธ์ของเครือข่ายในกระบวนการคำนวณเพิ่มเติม\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "class LSTMPackClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n", " super().__init__()\n", " self.hidden_dim = hidden_dim\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n", " self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, num_class)\n", "\n", " def forward(self, x, lengths):\n", " batch_size = x.size(0)\n", " x = self.embedding(x)\n", " pad_x = torch.nn.utils.rnn.pack_padded_sequence(x,lengths,batch_first=True,enforce_sorted=False)\n", " pad_x,(h,c) = self.rnn(pad_x)\n", " x, _ = torch.nn.utils.rnn.pad_packed_sequence(pad_x,batch_first=True)\n", " return self.fc(h[-1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": 8, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.285625\n", "6400: acc=0.33359375\n", "9600: acc=0.3876041666666667\n", "12800: acc=0.44078125\n", "16000: acc=0.4825\n", "19200: acc=0.5235416666666667\n", "22400: acc=0.5559821428571429\n", "25600: acc=0.58609375\n", "28800: acc=0.6116666666666667\n", "32000: acc=0.63340625\n", "35200: acc=0.6525284090909091\n", "38400: acc=0.668515625\n", "41600: acc=0.6822596153846154\n", "44800: acc=0.6948214285714286\n", "48000: acc=0.7052708333333333\n", "51200: acc=0.71521484375\n", "54400: acc=0.7239889705882353\n", "57600: acc=0.7315277777777778\n", "60800: acc=0.7388486842105263\n", "64000: acc=0.74571875\n", "67200: acc=0.7518303571428572\n", "70400: acc=0.7576988636363636\n", "73600: acc=0.7628940217391305\n", "76800: acc=0.7681510416666667\n", "80000: acc=0.7728125\n", "83200: acc=0.7772235576923077\n", "86400: acc=0.7815393518518519\n", "89600: acc=0.7857700892857142\n", "92800: acc=0.7895043103448276\n", "96000: acc=0.7930520833333333\n", "99200: acc=0.7959072580645161\n", "102400: acc=0.798994140625\n", "105600: acc=0.802064393939394\n", "108800: acc=0.8051378676470589\n", "112000: acc=0.8077857142857143\n", "115200: acc=0.8104600694444445\n", "118400: acc=0.8128293918918919\n" ] }, { "data": { "text/plain": [ "(0.029785829671223958, 0.8138166666666666)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = LSTMPackClassifier(vocab_size,64,32,len(classes)).to(device)\n", "train_epoch_emb(net,train_loader_len, lr=0.001,use_pack_sequence=True)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNN แบบสองทิศทางและหลายชั้น\n", "\n", "ในตัวอย่างของเรา เครือข่ายประสาทแบบวนซ้ำ (recurrent networks) ทั้งหมดทำงานในทิศทางเดียว คือจากจุดเริ่มต้นของลำดับไปจนถึงจุดสิ้นสุด ซึ่งดูเป็นธรรมชาติ เพราะมันคล้ายกับวิธีที่เราอ่านหรือฟังคำพูด อย่างไรก็ตาม ในหลายกรณีที่ใช้งานจริง เราสามารถเข้าถึงลำดับข้อมูลได้แบบสุ่ม ดังนั้นจึงอาจมีเหตุผลที่จะให้การคำนวณแบบวนซ้ำทำงานในทั้งสองทิศทาง เครือข่ายแบบนี้เรียกว่า **bidirectional** RNNs และสามารถสร้างได้โดยการส่งพารามิเตอร์ `bidirectional=True` ไปยังตัวสร้าง (constructor) ของ RNN/LSTM/GRU\n", "\n", "เมื่อทำงานกับเครือข่ายแบบสองทิศทาง เราจะต้องใช้เวกเตอร์สถานะซ่อน (hidden state vectors) สองตัว ตัวหนึ่งสำหรับแต่ละทิศทาง PyTorch จะเข้ารหัสเวกเตอร์เหล่านี้เป็นเวกเตอร์เดียวที่มีขนาดใหญ่ขึ้นสองเท่า ซึ่งสะดวกมาก เพราะโดยปกติแล้วคุณจะส่งสถานะซ่อนที่ได้ไปยังเลเยอร์เชิงเส้นแบบ fully-connected และคุณเพียงแค่ต้องคำนึงถึงการเพิ่มขนาดนี้เมื่อสร้างเลเยอร์\n", "\n", "เครือข่ายแบบวนซ้ำ ไม่ว่าจะเป็นแบบทิศทางเดียวหรือสองทิศทาง จะจับรูปแบบบางอย่างภายในลำดับ และสามารถเก็บรูปแบบเหล่านั้นไว้ในเวกเตอร์สถานะหรือส่งต่อไปยังผลลัพธ์ เช่นเดียวกับเครือข่ายแบบคอนโวลูชัน (convolutional networks) เราสามารถสร้างเลเยอร์แบบวนซ้ำอีกชั้นหนึ่งบนเลเยอร์แรกเพื่อจับรูปแบบในระดับที่สูงขึ้น ซึ่งสร้างขึ้นจากรูปแบบระดับต่ำที่เลเยอร์แรกสกัดออกมา สิ่งนี้นำเราไปสู่แนวคิดของ **multi-layer RNN** ซึ่งประกอบด้วยเครือข่ายแบบวนซ้ำสองชั้นหรือมากกว่า โดยที่ผลลัพธ์ของเลเยอร์ก่อนหน้าจะถูกส่งไปยังเลเยอร์ถัดไปเป็นอินพุต\n", "\n", "![ภาพแสดง Multilayer long-short-term-memory- RNN](../../../../../translated_images/th/multi-layer-lstm.dd975e29bb2a59fe.webp)\n", "\n", "*ภาพจาก [บทความที่ยอดเยี่ยมนี้](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) โดย Fernando López*\n", "\n", "PyTorch ทำให้การสร้างเครือข่ายแบบนี้เป็นเรื่องง่าย เพราะคุณเพียงแค่ส่งพารามิเตอร์ `num_layers` ไปยังตัวสร้าง RNN/LSTM/GRU เพื่อสร้างเลเยอร์แบบวนซ้ำหลายชั้นโดยอัตโนมัติ ซึ่งหมายความว่าขนาดของเวกเตอร์สถานะ/ซ่อนจะเพิ่มขึ้นตามสัดส่วน และคุณจะต้องคำนึงถึงสิ่งนี้เมื่อจัดการกับผลลัพธ์ของเลเยอร์แบบวนซ้ำ\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNNs สำหรับงานอื่น ๆ\n", "\n", "ในบทนี้ เราได้เห็นแล้วว่า RNNs สามารถใช้สำหรับการจัดประเภทลำดับได้ แต่ในความเป็นจริง RNNs ยังสามารถจัดการงานอื่น ๆ ได้อีกมากมาย เช่น การสร้างข้อความ การแปลภาษา และอื่น ๆ อีกมากมาย เราจะพิจารณางานเหล่านั้นในบทถัดไป\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**ข้อจำกัดความรับผิดชอบ**: \nเอกสารนี้ได้รับการแปลโดยใช้บริการแปลภาษา AI [Co-op Translator](https://github.com/Azure/co-op-translator) แม้ว่าเราจะพยายามให้การแปลมีความถูกต้อง แต่โปรดทราบว่าการแปลอัตโนมัติอาจมีข้อผิดพลาดหรือความไม่แม่นยำ เอกสารต้นฉบับในภาษาต้นทางควรถือเป็นแหล่งข้อมูลที่เชื่อถือได้ สำหรับข้อมูลที่สำคัญ ขอแนะนำให้ใช้บริการแปลภาษามนุษย์ที่เป็นมืออาชีพ เราจะไม่รับผิดชอบต่อความเข้าใจผิดหรือการตีความที่ผิดพลาดซึ่งเกิดจากการใช้การแปลนี้\n" ] } ], "metadata": { "interpreter": { "hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f" }, "kernelspec": { "display_name": "Python 3.8.12 ('py38')", "language": "python", "name": "python3" }, "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.8.12" }, "coopTranslator": { "original_hash": "522ee52ae3d5ae933e283286254e9a55", "translation_date": "2025-08-29T10:53:58+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb", "language_code": "th" } }, "nbformat": 4, "nbformat_minor": 2 }