477 lines
36 KiB
Plaintext
477 lines
36 KiB
Plaintext
{
|
|
"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",
|
|
"\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",
|
|
"\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
|
|
} |