{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# បណ្តាញខួរក្បាលសត្វពីរដង\n", "\n", "នៅក្នុងមូឌុលមុននេះ យើងបានប្រើប្រាស់តំណាងអត្ថន័យសំបូររបស់អត្ថបទ និងកូនម៉ាស៊ីនចាត់ថ្នាក់រេច�rប់លើបន្ទាត់លើបន្ថែម។ វិ​ស្ថា​នីយ​បា​នេះ​ធ្វើ​ឲ្យ​ចាប់យក​អត្ថន័យ​សម្រង់​នៃ​ពាក្យ​ក្នុង​ប្រយោគ​មួយ ប៉ុន្តែ​វា​មិនគិត​លំដាប់​នៃ​ពាក្យ​ទេ ពីព្រោះ​ប្រតិបត្តិការបញ្ចូលលើបន្ថែមបានដកអោយព័ត៌មាននេះចេញពីអត្ថបទដើម។ ដោយសារតែម៉ូដែលទាំងនេះមិនអាចនឹកស្រមៃលំដាប់ពាក្យបាន ក៏ពួកវាមិនអាចដោះស្រាយបញ្ហាស្មុគស្មាញឬអវិជ្ជមានជាទូទៅដូចជាការបង្កើតអត្ថបទ ឬការឆ្លើយសំណួរបានទេ។\n", "\n", "ដើម្បីចាប់យកអត្ថន័យនៃលំដាប់អត្ថបទ យើងត្រូវតែប្រើប្រាស់ស្ថាបត្យកម្មបណ្តាញខួរក្បាលមួយផ្សេងទៀត ដែលហៅថា **បណ្តាញខួរក្បាលសត្វពីរដង** ឬ RNN។ ក្នុង RNN យើងបញ្ជូនប្រយោគរបស់យើងតាមបណ្តាញមួយសញ្ញាមួយ ពេលហើយបណ្តាញបង្កើត **អារម្មណ៍** មួយ ហើយយើងបញ្ជូនអារម្មណ៍នោះទៅបណ្តាញម្ដងទៀតជាមួយសញ្ញាបន្ទាប់។\n", "\n", "\"RNN\"\n", "\n", "ដោយឧទាហរណ៍ លំដាប់បញ្ចូលនៃពាក្យ $X_0,\\dots,X_n$ RNN បង្កើតជាលំដាប់ប្លុកបណ្តាញខួរក្បាលហើយហ្វឹកហាត់លំដាប់នេះពីដើមដល់ចប់ដោយប្រើការផ្ទេរវិលក្រោយ។ ប្លុកបណ្តាញនីមួយៗទទួលយកគូ $(X_i,S_i)$ ជាបញ្ចូល ហើយបង្កើត $S_{i+1}$ ជាលទ្ធផល។ អារម្មណ៍ចុងក្រោយ $S_n$ ឬលទ្ធផល $X_n$ ត្រូវបញ្ចូលទៅកាន់កូនម៉ាស៊ីនចាត់ថ្នាក់បន្ទាត់ដើម្បីផលិតលទ្ធផល។ ប្លុកបណ្តាញទាំងអស់មានទម្ងន់ដូចគ្នា ហើយត្រូវបានហ្វឹកហាត់ពីដើមដល់ចប់តាមរយៈការផ្ទេរវិលក្រោយមួយ។\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 ងាយស្រួល ឯកតាដងៗដែលមានការក្រឡេកចម្លើយគឺជាបណ្ដាញបន្ទាត់មួយសាមញ្ញ ដែលទទួលវ៉ិចទ័របញ្ចូលដែលបានបញ្ចូលជាប្រភេទ concatenated និងវ៉ិចទ័រស្ថានភាព ហើយបង្កើតវ៉ិចទ័រស្ថានភាពថ្មី។ PyTorch តំណាងឱ្យឯកតានេះជាមួយថ្នាក់ `RNNCell` ហើយបណ្តាញនៃឯកតាដូចនេះ - ជាស្រទាប់ `RNN`។\n", "\n", "ដើម្បីកំណត់កម្មវិធីចាត់ថ្នាក់ RNN មួយ យើងនឹងអនុវត្តស្រទាប់ embedding ដើម្បីបន្ថយវិមាត្រពាក្យវចនានុក្រមបញ្ចូល ហើយបន្ទាប់មកមានស្រទាប់ 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 មិនបានបណ្តុះបណ្តាលនៅទីនេះសម្រាប់ភាពសាមញ្ញ ប៉ុន្តាសម្រាប់លទ្ធផលកាន់តែប្រសើរជាងនេះ យើងអាចប្រើស្រទាប់ embedding ដែលបានបណ្តុះបណ្តាលជាមុនជាមួយ Word2Vec ឬ GloVe embeddings ដូចដែលបានពណ៌នាកន្លងមក។ សម្រាប់ការយល់ដឹងកាន់តែច្បាស់ អ្នកអាចចង់ផ្លាស់ប្តូរកូដនេះឲ្យដំណើរការជាមួយ embeddings ដែលបានបណ្តុះបណ្តាលជាមុន។\n", "\n", "ក្នុងករណីរបស់យើង យើងនឹងប្រើអ្នកផ្ទុកទិន្នន័យដែលមានការ padding ដូច្នេះរាល់ថ្មបាច់នីមួយៗនឹងមានចំនួនខ្សែប្រវែងដូចគ្នា។ ស្រទាប់ RNN នឹងទទួលខ្សែប្រវែងនៃតង់ស័រអ៊ំប៊ែិត្ត ហើយផ្ដល់លទ្ធផលពីរយ៉ាង៖\n", "* $x$ គឺជាខ្សែប្រវែងនៃលទ្ធផលកោសិកា RNN ក្នុងគ្រប់ជំហាន\n", "* $h$ គឺជា រដ្ឋភាពលាក់ចុងក្រោយ សម្រាប់ធាតុខ្ទង់ចុងក្រោយនៃខ្សែប្រវែង\n", "\n", "បន្ទាប់មក យើងអនុវត្តកំណត់ចំណាត់ថ្នាក់បន្ទាត់ដែលភ្ជាប់ពេញលេញ ដើម្បីទទួលចំនួនថ្នាក់។\n", "\n", "> **ចំណាំ:** RNNs គឺពិបាកក្នុងការបណ្តុះបណ្តាលហើយ ពីព្រោះเมื่อកោសិកា RNN ត្រូវបានបង្ហាញក្នុងខ្សែប្រវែង តុល្យភាពចំនួនស្រទាប់ដែលចូលរួមក្នុងការធ្វើប្រតិកម្មបន្ទាប់ក្រោយគឺច្រើនខ្លាំង។ ដូច្នេះយើងត្រូវជ្រើសរើសអត្រាសិក្សាចំនួនតូច ហើយបណ្តុះបណ្តាលបណ្ដាញលើឯកសារទិន្នន័យធំដើម្បី ផលិតលទ្ធផលល្អ។ វាអាចយកពេលយូរអង្វែង ដូច្នេះការប្រើប្រាស់ 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 ត្រូវបណ្តុះបណ្តាលពីដើមទៅចុងតាមរយៈការផ្ទេរសារត្រឡប់មួយដង វាជួបប្រទៈនឹងការលំបាកក្នុងការផ្ទេរព្យាករណ៍​បញ្ហាទៅកាន់ស្រទាប់ដើមនៃបណ្តាញ ហេតុនេះបណ្តាញមិនអាចរៀនទំនាក់ទំនងរវាងតូកុនដែលឆ្ងាយបានទេ។ មួយក្នុងចំណោមវិធីដើម្បីជៀសវាងបញ្ហានេះគឺការណែនាំ **ការគ្រប់គ្រងស្ថិតិوض្ធពិត** ដោយប្រើអ្វីដែលហៅថា **ទ្វារ (gates)**។ មានរចនាសម្ព័ន្ធពីរដែលគេស្គាល់ច្បាស់ជាងគេសម្រាប់ប្រភេទនេះគឺ **ការចងចាំរយៈពេលខ្លីយូរ (Long Short Term Memory)** (LSTM) និង **Gated Relay Unit** (GRU)។\n", "\n", "![Image showing an example long short term memory cell](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "បណ្តាញ LSTM ត្រូវបានរៀបចំភាពដូចនឹង RNN ប៉ុន្តែមានស្ថានភាពពីរដែលត្រូវបញ្ជូនពីស្រទាប់ទៅស្រទាប់គឺស្ថានភាពពិត $c$ និងវ៉ិចទ័រលាក់ $h$។ នៅក្នុងគ្រប់ផ្នែកមួយ, វ៉ិចទ័រលាក់ $h_i$ ត្រូវបានបញ្ចូលជាមួយនឹងបញ្ជូល $x_i$ ហើយពួកវាគ្រប់គ្រងពីមានអ្វីកើតឡើងចំពោះស្ថានភាព $c$ តាមរយៈ **ទ្វារ (gates)**។ ទ្វារនីមួយៗគឺជាបណ្តាញប្រភេទណឺរ៉លដែលមានសកម្មភាព sigmoid (ផលចេញនៅចន្លោះ $[0,1]$) ដែលអាចគិតជាម៉ាសចុចប៊ីតប៉ុន្មានពេលគុណជាមួយវ៉ិចទ័រស្ថានភាព។ មានទ្វារដូចខាងក្រោម (ពីឆ្វេងទៅស្ដាំនៅក្នុងរូបភាពខាងលើ):\n", "* **ទ្វារលុបចោល (forget gate)** កាន់កាប់វ៉ិចទ័រលាក់ ហើយកំណត់មាត្រដ្ឋានណានៃវ៉ិចទ័រ $c$ ដែលយើងត្រូវលុបចោល និងណាដែលត្រូវបញ្ជូនតាមចូល។\n", "* **ទ្វារបញ្ចូល (input gate)** ទទួលបានព័ត៌មានពីបញ្ចូលនិងវ៉ិចទ័រលាក់ ហើយបញ្ចូលវាទៅក្នុងស្ថានភាព។\n", "* **ទ្វារចេញ (output gate)** បម្លែងស្ថានភាពតាមរយៈស្រទាប់ប៉ារ៉ាឡែលជាមួយសកម្មភាព $\\tanh$ បន្ទាប់មកជ្រើសរើសមួយចំនួននៃមាត្រដ្ឋានរបស់វា ដោយប្រើវ៉ិចទ័រលាក់ $h_i$ ដើម្បីបញ្ចេញស្ថានភាពថ្មី $c_{i+1}$។\n", "\n", "មាត្រដ្ឋាននៃស្ថានភាព $c$ អាចគិតបានជាផ្លាកខ្លះៗដែលអាចបិទបិទ និងបើកបាន។ ឧទាហរណ៍, នៅពេលយើងជួបឈ្មោះ *Alice* ក្នុងខ្សែអក្សរ, យើងអាចចង់សន្មតថាវាសំដៅដល់តួអក្សរនារី ហើយលើកផ្លាកក្នុងស្ថានភាពថាយើងមាននាមស្រីក្នុងវាក្យបញ្ជា។ នៅពេលពួកយើងជួបប្រកាស *and Tom* យើងនឹងលើកផ្លាកថាយើងមាននាមពហុ។ ដូច្នេះដោយការគ្រប់គ្រងស្ថានភាពយើងអាចរក្សាទុកគុណលក្ខណៈវេយ្យាករណ៍នៃផ្នែកវាក្យបាន។\n", "\n", "> **ចំណាំ**: អត្ថបទដ៏ល្អសម្រាប់យល់ពីរចនាសម្ព័ន្ធខាងក្នុងនៃ LSTM គឺអត្ថបទល្អនេះ [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) ដោយ Christopher Olah។\n", "\n", "ខណៈដែលរចនាសម្ព័ន្ធខាងក្នុងនៃកោសិកា LSTM អាចមើលទៅស្មុគស្មាញ, PyTorch យកការអនុវត្តនេះលាក់នៅក្នុងថ្នាក់ `LSTMCell` ហើយផ្តល់អOBJECT `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": [ "ឥឡូវនេះសូមបណ្ដុះបណ្ដាលបណ្តាញរបស់យើង។ សូមចំណាំថាការបណ្ដុះបណ្ដាល LSTM ក៏យឺតដូចគ្នា ហើយអ្នកប្រហែលជាចាត់ទុកមិនមែនមានកម្រិតភាពត្រឹមត្រូវកើនឡើងច្រើននៅដើមនៃការបណ្ដុះបណ្ដាលឡើយ។ លើសពីនេះ អ្នកប្រហែលជាត្រូវតែជាមួយនឹងប៉ារ៉ាម៉ែត្រ `lr` នូវអត្រាការសិក្សាដើម្បីស្វែងរកអត្រាការសិក្សាដែលធ្វើឱ្យមានល្បឿនការបណ្ដុះបណ្ដាលសមរម្យ ហើយនៅមិនបង្ករឱ្យសារជាតិអង្គចងចាំស្តុកច្រើនពេកឡើយ។\n" ] }, { "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", "ក្នុងឧទាហរណ៍របស់យើង យើងត្រូវតែបន្ថែមពណ៌តំណាងសូន្យទៅលើស្វ៊ីស៊ីសទាំងអស់នៅក្នុងបន្ទប់តូច។ បើទោះបីជាវាកើតមានការចំណាយមេម៉ូរីខ្លះ ក៏ដោយ ជាមួយ RNN វាអ្វីដែលសំខាន់ជាងគឺ កោសិកាទ្រឡប់ទួរយយនន៍បន្ថែមត្រូវបានបង្កើតសម្រាប់ធាតុខ្លក់ដែលបានបន្ថែម, ដែលចូលរួមក្នុងការបណ្តុះបណ្តាល ប៉ុន្ដែមិនមានព័ត៌មានចូលសំខាន់ឡើយ។ វាជាជម្រើសល្អជាងក្នុងការបណ្តុះបណ្តាល RNN ផ្តោតត្រឹមទំហំស្វ៊ីស៊ីសពិតប្រាកដ។\n", "\n", "ដើម្បីធ្វើដូចនេះ ដើមទោលសម្រាប់ស្វ៊ីស៊ីសដែលបានបន្ថែមត្រូវបានបង្ហាញជាផ្លូវការនៅក្នុង PyTorch ។ សន្មតថាយើងមានបន្ទប់តូចដែលបានបន្ថែមបែបនេះ៖\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 ដំបូងជាមួយបន្ទប់តូចធំហ្នឹង (`[1,6,9]`), បន្ទាប់មកបញ្ចប់ដំណើរការស្វ៊ីសីសទីបី ហើយបន្តបណ្តុះជាមួយបន្ទប់តូចសង្ខេប (`[2,7]`, `[3,8]`), តម្លៃបន្ថែមនិងបន្ត។ ដូច្នេះ ស្វ៊ីស៊ីសដាក់ជាបញ្ចុះត្រូវបានតំណាងជា វ៉ិកទ័រតែមួយ - ក្នុងករណីនេះ `[1,6,9,2,7,3,8,4,5]`, និងវ៉ិកទ័រវាស់ប្រវែង (`[5,3,1]`), ដែលយើងអាចបញ្ចប់ឡើងវិញស្វ៊ីស៊ីសដែលបានបន្ថែមដើម។\n", "\n", "ដើម្បីផលិតស្វ៊ីស៊ីសដែលបានបញ្ចុះ អ្នកអាចប្រើមុខងារ `torch.nn.utils.rnn.pack_padded_sequence` ។ ស្រទាប់ដែលមានដំណើរការជារឿយៗទាំងអស់ រួមមាន RNN, LSTM និង GRU គាំទ្រស្វ៊ីស៊ីសដែលបានបញ្ចុះជាការបញ្ចូល ហើយបង្កើតស្វ៊ីស៊ីសដែលបានបញ្ចុះជាបញ្ចេញ ដែលអាចបកប្រែបានដោយប្រើ `torch.nn.utils.rnn.pad_packed_sequence`។\n", "\n", "ដើម្បីអាចផលិតស្វ៊ីស៊ីសដែលបានបញ្ចុះ យើងត្រូវផ្តល់វ៉ិកទ័រវាស់ប្រវែងទៅកាន់បណ្តាញ ហើយដូច្នេះយើងត្រូវការមុខងារផ្សេងទៀតក្នុងការរៀបចំបន្ទប់តូច៖\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 នឹងទទួលបានទាំង minibatch ដែលបានបំពង់ និងវ៉ិចទ័រជាមួយប្រវែងរបស់រលកត(sequence lengths)។ បន្ទាប់ពីគណនាការបញ្ចូល (embedding) យើងគណនារលកជាកញ្ចប់ (packed sequence) បញ្ជូនវាទៅឲ្យស្រទាប់ LSTM ហើយបន្ទាប់មកដោះបញ្ចប់លទ្ធផលវិញ។\n", "\n", "> **បញ្ជាក់**: ជាក់ស្ដែងយើងមិនប្រើលទ្ធផលដែលបានដោះបញ្ចប់ `x` ទេ ពីព្រោះយើងប្រើលទ្ធផលពីស្រទាប់លាក់ក្នុងការគណនាតាមក្រោយ។ ដូច្នេះ យើងអាចលុបការដោះបញ្ចប់ចេញពីកូដនេះបានមិនបញ្ចេញទេ។ មូលហេតុដែលយើងដាក់វា​នៅទីនេះ គឺដើម្បីឲ្យអ្នកអាចកែប្រែកូដនេះបានយ៉ាងងាយស្រួល ប្រសិនបើអ្នកត្រូវការប្រើលទ្ធផលបណ្តាញនៅក្នុងការគណនាផ្សេងទៀត។\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": [ "ឥឡូវនេះចើងធ្វើការបណ្តុះបណ្តាល:\n" ] }, { "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": [ "> **ចំណាំ៖** អ្នកប្រហែលជាបានសម្គាល់ពីប៉ារ៉ាម៉ែត្រ `use_pack_sequence` ដែលយើងបញ្ចូនទៅឲ្យហ្វังก์សិនហ្វឹកហាត់។ សព្វថ្ងៃ, ហ្វังก์សិន `pack_padded_sequence` ត្រូវការថង់អង្កត់វែងលំដាប់លំដោយស្ថិតនៅលើឧបករណ៍ CPU ហើយដូច្នេះហ្វังก์សិនហ្វឹកហាត់ត្រូវជៀសវាងការផ្លាស់ទីទិន្នន័យអង្កត់វែងលំដាប់ទៅ GPU ពេលហ្វឹកហាត់។ អ្នកអាចមើលការអនុវត្តន៍នៃហ្វังก์សិន `train_emb` នៅក្នុងឯកសារ [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py) ។\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## បណ្ដើរពីរចំណុច និង RNN ដែលមានបណ្ដាប់ជាច្រើនស្រទាប់\n", "\n", "ក្នុងឧទាហរណ៍របស់យើង បណ្តា របួសវិលជុំគឺ បើកដំណើរការនៅតែមួយទិសពីដើមទៅចុងសមាគម។ វាហាក់ដូចជា​ធម្មតា ពីព្រោះវាស្រដៀងនឹងវិធីដែលយើងអាន និងស្តាប់សុន្ទរកថា។ ក្រៅពីនេះ ព្រោះក្នុងករណីជាច្រើន ដំណើរការប្រតិបត្តិការចូលចិត្ដមានការចូលដំណើរការលំដាប់ពីរ ឬមួយ ហើយវាអាចមានប្រសិទ្ធិភាពក្នុងការរត់ការគណនាវិលជុំទាំងពីរទិស។ បណ្តាញបែបនេះហៅថា **RNN បណ្ដោយទិសពីរទិស** ហើយអាចបង្កើតបានដោយផ្ញើប៉ារ៉ាម៉ែត្រ `bidirectional=True` ទៅកាន់កម្មវិធីបង្កើត RNN/LSTM/GRU។\n", "\n", "ពេលនៅជាមួយបណ្តាញបណ្ដោយទិសពីរទិស យើងត្រូវការវិចទ័រជាប់ស្ព័រចំរាំងពីរជាន់ សម្រាប់បណ្តោយទិសនីមួយៗ។ PyTorch កំណត់កូដវិចទ័រទាំងនេះជាវិចទ័រមួយដែលមានទំហំធំពីរដង ដែលជារឿងងាយស្រួល ព្រោះអ្នកធម្មតានឹងផ្ញើស្ព័រចំរាំងនេះទៅជាស្រទាប់រាប់បញ្ចូលការគណនា ដែលអ្នកគ្រាន់តែត្រូវគិតទំហំដែលបានបន្ថែមនេះពេលបង្កើតស្រទាប់។\n", "\n", "បណ្តាញវិលជុំ មិនថាធ្វើការជាតិស្រទាប់ទ្វេគឺ ឬបណ្ដោយទិសពីរទិស ពួកវាចាប់យកលំនាំករណីនៅក្នុងសមាគមមួយ ហើយអាចរក្សាទុកជាវិចទ័រស្ថានភាព ឬបញ្ជូនទៅលទ្ធផល។ ដូចជាបណ្ដាញបន្លាស់អាំងគោណវេស៊ីយ៉ុង យើងអាចបង្កើតស្រទាប់វិលជុំថ្មីលើស្រទាប់ដំបូង ដើម្បីចាប់យកលំនាំកម្រិតខ្ពស់ ជារូបមន្តពីលំនាំកម្រិតទាបដែលត្រូវបានដកស្រង់ជាស្រទាប់ដំបូង។ នេះនាំឱ្យយើងយល់ដឹងពីយោបល់នៃ **RNN មួយចំណុចជាច្រើនស្រទាប់** ដែលមានបណ្តារបណ្តាញវិលជុំពីរឬច្រើនជាន់ ដែលលទ្ធផលរបស់ស្រទាប់មុនត្រូវបានផ្ញើឱ្យស្រទាប់បន្ទាប់ជាកាណ្តាល។\n", "\n", "![រូបភាពបង្ហាញ RNN មួយចំណុចជាច្រើនស្រទាប់ខ្សែជីវិតក្រោម-ខ្លី](../../../../../translated_images/km/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 អាចត្រូវបានប្រើសម្រាប់ចាត់ថ្នាក់លំហាត់ តែក្រៅពីនេះ ពួកវាអាចដោះស្រាយកិច្ចការច្រើនទៀត អោយដូចជាការបង្កើតអត្ថបទ ការបកប្រែម៉ាស៊ីន និងផ្សេងទៀត។ យើងនឹងពិចារណាកិច្ចការទាំងនោះនៅក្នុងឯកតាខាងមុខ។\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n\n\n**ការបដិសេធ**៖ \nឯកសារនេះត្រូវបានបកប្រែដោយប្រើសេវាកម្មបកប្រែ AI [Co-op Translator](https://github.com/Azure/co-op-translator)។ ទោះបីយើងខិតខំបំផុតសម្រាប់ភាពត្រឹមត្រូវ ក៏សូមសម្គាល់ថាការបកប្រែដោយស្វ័យប្រវត្តិក្នុងករណីខ្លះអាចមានកំហុស ឬមិនត្រឹមត្រូវ។ ឯកសារដើមជាភាសាដើមគួរត្រូវបានពិចារណាថាជាដើមទុក្ខត្រឹមត្រូវ។ សម្រាប់ព័ត៌មានសំខាន់ៗ សូមណែនាំឱ្យប្រើការបកប្រែដោយអ្នកមានជំនាញផ្នែកមនុស្ស។ យើងមិនទទួលខុសត្រូវចំពោះការយល់ច្រឡំ ឬការបកប្រែខុសពីការប្រើប្រាស់ការបកប្រែនេះឡើយ។\n\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" } }, "nbformat": 4, "nbformat_minor": 2 }