{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Embeddings\n", "\n", "នៅក្នុងឧទាហរណ៍មុនរបស់​យើង យើងបាន​ប្រតិបត្តិ​លើ​វ៉ិចទ័រ bag-of-words ដែលមានខ្សែបណ្តោយខ្ពស់ `vocab_size` និង​យើងបាន​បម្លែង​យ៉ាងច្បាស់ពី​វ៉ិចទ័រ​តំណរភាគទូលាយ ទាប ទៅជាតំណាង​តែមួយ-ក្តៅ​ដែលរាយបញ្ចាំងតិចតួច។ តំណាងតែមួយ-ក្តៅ​នេះ​មិន​មានប្រសិទ្ធភាព​ក្នុង​ការផ្ទុកចងចាំទេ បន្ថែម​ពីនេះ រាល់ពាក្យ​ត្រូវបានចាត់ទុក​ឯករាជ្យពីគ្នា ប្រែថា​វ៉ិចទ័រដែលបាន encode តែមួយ-ក្តៅ​មិនបង្ហាញន័យស្រដៀងគ្នាណាមួយ​រវាងពាក្យទេ។\n", "\n", "នៅក្នុងឯកតានេះ យើងនឹងបន្ត​សហការជាមួយ​ទិន្នន័យ **News AG**។ ដើម្បីចាប់ផ្តើម មកយើងផ្ទុកទិន្នន័យ និងទទួលយក​និយមន័យខ្លះ​ពីសៀវភៅកំណត់ហេតុមុន។\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading dataset...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "d:\\WORK\\ai-for-beginners\\5-NLP\\14-Embeddings\\data\\train.csv: 29.5MB [00:01, 18.8MB/s] \n", "d:\\WORK\\ai-for-beginners\\5-NLP\\14-Embeddings\\data\\test.csv: 1.86MB [00:00, 11.2MB/s] \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Building vocab...\n", "Vocab size = 95812\n" ] } ], "source": [ "import torch\n", "import torchtext\n", "import numpy as np\n", "from torchnlp import *\n", "train_dataset, test_dataset, classes, vocab = load_dataset()\n", "vocab_size = len(vocab)\n", "print(\"Vocab size = \",vocab_size)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## តើ embedding ជាអ្វី?\n", "\n", "គំនិតនៃ **embedding** គឺជារៀបរាប់​ពាក្យ​ជាវ៉ិចទ័រដ៏រឹងមាំដែលមានវិមាត្រកថ្លៃទាប ជាដើមដែលបញ្ជាក់អត្ថន័យ​យោងទៅលើពាក្យមួយ។ យើង​នឹងពិភាក្សាពីរបៀបកសាង word embeddings មានន័យ​ក្នុង​ពេលក្រោយ ប៉ុន្តែ​នាពេលនេះ hãyគិតថា embeddings ជាវិធីកាត់បន្ថយវិមាត្រនៃវ៉ិចទ័រពាក្យមួយ។\n", "\n", "ដូច្នេះ, ស្រទាប់ embedding នឹងទទួលពាក្យមួយជាឧបត្ថម្ភ និងបង្កើតវ៉ិចទ័រចេញ​មួយដែលមាន `embedding_size` បានកំណត់។ ក្នុងន័យមួយវាស្រដៀងទៅនឹងស្រទាប់ `Linear` តែជំនួសវិចទ័រពោលជា one-hot encoded វា​អាចទទួលលេខពាក្យដោយផ្ទាល់ជាឧបត្ថម្ភបាន។\n", "\n", "ដោយប្រើស្រទាប់ embedding ជាស្រទាប់ដំបូងក្នុងបណ្តាញរបស់យើង យើងអាចបម្លែងពីគំរូ bag-of-words ទៅគំរូ **embedding bag** ដែលនៅទីនោះយើងបម្លែងពាក្យនីមួយៗក្នុងអត្ថបទជាវ៉ិចទ័រពាក់ព័ន្ធ embedding ហើយបន្ទាប់មកគណនាអនុគមន៍សរុបលើវ៉ិចទ័រទាំងនោះ ដូចជា `sum`, `average` ឬ `max`។\n", "\n", "![រូបភាពបង្ហាញឧទាហរណ៍ embedding classifier សម្រាប់ពាក្យជាច្រើនក្នុងលំដាប់។](../../../../../translated_images/km/embedding-classifier-example.b77f021a7ee67eee.webp)\n", "\n", "បណ្តាញនឺរ៉ាល់នៃ classifer របស់យើង នឹងចាប់ផ្តើមជាមួយស្រទាប់ embedding បន្ទាប់ស្រទាប់ aggregation និងចុងក្រោយជាស្រទាប់ linear classifier៖\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "class EmbedClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, num_class):\n", " super().__init__()\n", " self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n", " self.fc = torch.nn.Linear(embed_dim, num_class)\n", "\n", " def forward(self, x):\n", " x = self.embedding(x)\n", " x = torch.mean(x,dim=1)\n", " return self.fc(x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ការដោះស្រាយទំហំលំដាប់អថេរ\n", "\n", "ជាលទ្ធផលនៃស្ថាបត្យកម្មនេះ minibatches ទៅបណ្ដាញរបស់យើងនឹងត្រូវបានបង្កើតឡើងដោយវិធីជាក់លាក់មួយ។ នៅឯកតាមុននេះ នៅពេលប្រើ bag-of-words តង់ស័រទាំងអស់ក្នុង minibatch មានទំហំពេលស្មើគ្នា `vocab_size` មិនគិតទំហំជាក់លាក់នៃលំដាប់អត្ថបទរបស់យើងឡើយ។ ពេលយើងផ្លាស់ទៅប្រើបន្ទាត់ពាក្យ word embeddings យើងនឹងបានចំនួនពាក្យប្រែប្រួលនៅក្នុងគំរូអត្ថបទនីមួយៗ ហើយពេលបញ្ចូលគំរូទាំងនោះចូលក្នុង minibatches យើងនឹងត្រូវអនុវត្តការបញ្ចូលបន្ថែម padding មួយចំនួន។\n", "\n", "នេះអាចធ្វើបានដោយប្រើបច្ចេកទេសដដែលគឺផ្តល់មុខងារ `collate_fn` ទៅដល់ datasource៖\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def padify(b):\n", " # b is the list of tuples of length batch_size\n", " # - first element of a tuple = label, \n", " # - second = feature (text sequence)\n", " # build vectorized sequence\n", " v = [encode(x[1]) for x in b]\n", " # first, compute max length of a sequence in this minibatch\n", " l = max(map(len,v))\n", " return ( # tuple of two tensors - labels and features\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", " )\n", "\n", "train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ការបណ្តុះបណ្តាលម៉ាស៊ីនចាត់ថ្នាក់ embedding\n", "\n", "ឥឡូវនេះដែលយើងបានកំណត់ dataloader ត្រឹមត្រូវហើយ យើងអាចបណ្តុះបណ្តាលម៉ូដែលដោយប្រើមុខងារ training ដែលយើងបានកំណត់នៅឯកតាមុន៖\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.6415625\n", "6400: acc=0.6865625\n", "9600: acc=0.7103125\n", "12800: acc=0.726953125\n", "16000: acc=0.739375\n", "19200: acc=0.75046875\n", "22400: acc=0.7572321428571429\n" ] }, { "data": { "text/plain": [ "(0.889799795315499, 0.7623160588611644)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = EmbedClassifier(vocab_size,32,len(classes)).to(device)\n", "train_epoch(net,train_loader, lr=1, epoch_size=25000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "> **កំណត់សម្គាល់**: យើងកំពុង ONLY បណ្តុះបណ្តាលសម្រាប់កំណត់ត្រា 25,000 នៅទីនេះ (តិចជាងមួយស្ទង់ពេញ) ដើម្បីជួយការសន្សំសំចៃពេលវេលា ប៉ុន្តែអ្នកអាចបន្តការបណ្តុះបណ្តាល សរសេរពិធីសាស្រ្តមួយសម្រាប់បណ្តុះបណ្តាលជាច្រើនស្ទង់ និងសាកល្បងជាមួយប៉ារ៉ាម៉ែត្រការសិក្សា ដើម្បីទទួលបានភាពត្រឹមត្រូវខ្ពស់ជាងមុន។ អ្នកគួរតែអាចទៅដល់ភាពត្រឹមត្រូវប្រហែល ៩០%។\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### EmbeddingBag ស្រទាប់ និង តំណាងរាងលំដាប់រៀងពីរយៈវែរីយ៉ាប៊ុល\n", "\n", "នៅក្នុងស្ថาปត្យកម្មមុន យើង​ត្រូវការបន្ថែម​ចន្លោះ​ទទេ​ទៅលើ​លំដាប់​ទាំងអស់ដើម្បីឲ្យវា​មានប្រវែង​ដូចគ្នា​ដើម្បីដាក់វាទៅក្នុង minibatch។ វា​មិនមែន​ជា​របៀបមានប្រសិទ្ធភាព​បំផុត​ក្នុងការតំណាង​លំដាប់​វែរីយ៉ាប៊ុល​នោះទេ - វិធានមួយផ្សេងគឺប្រើ​វ៉ិចទ័រ **offset** ដែលនឹងរក្សាទុកចន្លោះ offset របស់​លំដាប់ទាំងអស់​ដែលបានរក្សាទុក​ក្នុង​វ៉ិចទ័រធំបន្ទាត់មួយ។\n", "\n", "![Image showing an offset sequence representation](../../../../../translated_images/km/offset-sequence-representation.eb73fcefb29b46ee.webp)\n", "\n", "> **ចំណាំ**: នៅក្នុងរូបភាពខាងលើ យើងបង្ហាញ​លំដាប់​តួអក្សរ ប៉ុន្តែនៅក្នុងឧទាហរណ៍របស់យើង កំពុង​ដំណើរការជាលំដាប់​ពាក្យ។ ទោះយ៉ាងណា 원칙​ទូទៅ​ក្នុងការតំណាងលំដាប់ជាមួយវ៉ិចទ័រ offset នៅតែដូចគ្នា។\n", "\n", "ដើម្បីធ្វើការ​ជាមួយតំណាង offset យើងប្រើស្រទាប់ [`EmbeddingBag`](https://pytorch.org/docs/stable/generated/torch.nn.EmbeddingBag.html)។ វាស្រដៀងនឹង `Embedding` ប៉ុន្តែវាបានទទួល​ខ្សែវ៉ិចទ័រ និង offset វ៉ិចទ័រជាជាច្រើននូវការបញ្ចូល ហើយវាក៏រួមបញ្ចូលជាមួយស្រទាប់ averaging ដែលអាចជាផ្នែក `mean`, `sum` ឬ `max` ផងដែរ។\n", "\n", "នេះគឺជាបណ្តាញដែលបានកែសម្រួលដែលប្រើ `EmbeddingBag`៖\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "class EmbedClassifier(torch.nn.Module):\n", " def __init__(self, vocab_size, embed_dim, num_class):\n", " super().__init__()\n", " self.embedding = torch.nn.EmbeddingBag(vocab_size, embed_dim)\n", " self.fc = torch.nn.Linear(embed_dim, num_class)\n", "\n", " def forward(self, text, off):\n", " x = self.embedding(text, off)\n", " return self.fc(x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ដើម្បីរៀបចំទិន្នន័យសម្រាប់ហ្វឹកហាត់ យើងត្រូវការផ្តល់មុខងារបម្លែងមួយ ដែលនឹងរៀបចំពាក្យកំណត់អូស៖\n" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def offsetify(b):\n", " # first, compute data tensor from all sequences\n", " x = [torch.tensor(encode(t[1])) for t in b]\n", " # now, compute the offsets by accumulating the tensor of sequence lengths\n", " o = [0] + [len(t) for t in x]\n", " o = torch.tensor(o[:-1]).cumsum(dim=0)\n", " return ( \n", " torch.LongTensor([t[0]-1 for t in b]), # labels\n", " torch.cat(x), # text \n", " o\n", " )\n", "\n", "train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=offsetify, shuffle=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "សូមកត់សម្គាល់ថា ខុសពីគំរូទាំងអស់មុននេះ បណ្ដាញរបស់យើងឥឡូវនេះទទួលបានប៉ារាម៉ែត្រ​ពីរចូលគ្នា៖ វិចទ័រទិន្នន័យ និងវិចទ័រជំហរ ដែលមានទំហំខុសគ្នា។ ដូចគ្នានេះ អ្នកដំណើរការទិន្នន័យរបស់យើងក៏ផ្ដល់ឲ្យយើងនូវតម្លៃចំនួន៣មិនមែនត្រឹមតែ២ទេ៖ វិចទ័រទាំងអត្ថបទ និងជំហរត្រូវបានផ្ដល់ឲ្យជាលក្ខណៈពិសេស។ ដូច្នេះ យើងត្រូវកែប្រែ slightly ប្រតិបត្ដិការបណ្តុះបណ្តាលរបស់យើង ដើម្បីដោះស្រាយបញ្ហានេះ៖\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.6153125\n", "6400: acc=0.6615625\n", "9600: acc=0.6932291666666667\n", "12800: acc=0.715078125\n", "16000: acc=0.7270625\n", "19200: acc=0.7382291666666667\n", "22400: acc=0.7486160714285715\n" ] }, { "data": { "text/plain": [ "(22.771553103007037, 0.7551983365323096)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = EmbedClassifier(vocab_size,32,len(classes)).to(device)\n", "\n", "def train_epoch_emb(net,dataloader,lr=0.01,optimizer=None,loss_fn = torch.nn.CrossEntropyLoss(),epoch_size=None, report_freq=200):\n", " optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)\n", " loss_fn = loss_fn.to(device)\n", " net.train()\n", " total_loss,acc,count,i = 0,0,0,0\n", " for labels,text,off in dataloader:\n", " optimizer.zero_grad()\n", " labels,text,off = labels.to(device), text.to(device), off.to(device)\n", " out = net(text, off)\n", " loss = loss_fn(out,labels) #cross_entropy(out,labels)\n", " loss.backward()\n", " optimizer.step()\n", " total_loss+=loss\n", " _,predicted = torch.max(out,1)\n", " acc+=(predicted==labels).sum()\n", " count+=len(labels)\n", " i+=1\n", " if i%report_freq==0:\n", " print(f\"{count}: acc={acc.item()/count}\")\n", " if epoch_size and count>epoch_size:\n", " break\n", " return total_loss.item()/count, acc.item()/count\n", "\n", "\n", "train_epoch_emb(net,train_loader, lr=4, epoch_size=25000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Semantic Embeddings: Word2Vec\n", "\n", "នៅក្នុងឧទាហរណ៍មុនរបស់យើង ស្រទាប់ embedding ម៉ូដែលបានរៀនផែនទីពាក្យទៅរាងតំណាងវ៉ិចទ័រ ទោះជាយ៉ាងណា រាងតំណាងនេះមិនមានន័យsemantic ច្រើនទេ។ វានឹងល្អបើរៀនរាងតំណាងវ៉ិចទ័រដូច្នេះ ដែលពាក្យស្រដៀងគ្នា ឬពាក្យប្រហាក់ប្រហែលនឹងផ្គូផ្គងទៅនឹងវ៉ិចទ័រដែលនៅជិតគ្នាប្រកបដោយចម្ងាយវ៉ិចទ័រ (ឧ. ចម្ងាយអ៊ីយូគ្ល៊ីដៀន) ។\n", "\n", "ដើម្បីធ្វើបានដូច្នេះ យើងត្រូវបណ្ដុះម៉ូដែល embedding របស់យើងលើការប្រមូលផ្តុំអត្ថបទធំៗដោយវិធីពិសេសមួយ។ វិធីដំបូងៗក្នុងការបណ្ដុះ semantic embeddings មួយឈ្មោះ [Word2Vec](https://en.wikipedia.org/wiki/Word2vec)។ វាមានព្រណិតប្រាជ្ញាផ្លូវការចម្បងពីរដែលប្រើប្រព័ន្ធផ្សព្វផ្សាយរាងតំណាងរាងពាក្យមួយ៖\n", "\n", " - **Continuous bag-of-words** (CBoW) — ក្នុងសំណុំប្រាជ្ញាផ្លូវការនេះ យើងបណ្ដុះម៉ូដែលឲ្យទស្សនាពាក្យមួយពីបរិបទជុំវិញ។ ផ្តល់អោយ ngram $(W_{-2},W_{-1},W_0,W_1,W_2)$ គោលបំណងម៉ូដែលគឺទស្សនាពាក្យ $W_0$ ពី $(W_{-2},W_{-1},W_1,W_2)$។\n", " - **Continuous skip-gram** គឺលើកលែងពី CBoW។ ម៉ូដែលប្រើបណ្តោយបរិបទវ៉ីនដូជុំវិញ ដើម្បីទស្សនាពាក្យបច្ចុប្បន្ន។\n", "\n", "CBoW ឆាប់រហ័ស ខណៈដែល skip-gram យឺតជាង ប៉ុន្តែប្រសើរជាងក្នុងការតំណាងពាក្យដែលមិនមានប្រជាជនច្រើន។\n", "\n", "![រូបភាពបង្ហាញអាល់ហ្គរីធึម CBoW និង Skip-Gram ដើម្បីបំលែងពាក្យទៅវ៉ិចទ័រ។](../../../../../translated_images/km/example-algorithms-for-converting-words-to-vectors.fbe9207a726922f6.webp)\n", "\n", "ដើម្បីសាកល្បង embedding word2vec ដែលបានបណ្ដុះរួចនៅលើឃ្លាំងទិន្នន័យ Google News អស់យើងអាចប្រើបណ្ណាល័យ **gensim** ។ ខាងក្រោម យើងស្វែងរកពាក្យដែលស្រដៀងគ្នាច្រើនជាងគេជាមួយពាក្យ 'neural'\n", "\n", "> **Note:** រស់រាន់ពេលអ្នកបង្កើតវ៉ិចទ័រពាក្យដំបូង ការទាញយកវានឹងចំណាយពេលខ្លះ!\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "import gensim.downloader as api\n", "w2v = api.load('word2vec-google-news-300')" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "neuronal -> 0.7804799675941467\n", "neurons -> 0.7326500415802002\n", "neural_circuits -> 0.7252851724624634\n", "neuron -> 0.7174385190010071\n", "cortical -> 0.6941086649894714\n", "brain_circuitry -> 0.6923246383666992\n", "synaptic -> 0.6699118614196777\n", "neural_circuitry -> 0.6638563275337219\n", "neurochemical -> 0.6555314064025879\n", "neuronal_activity -> 0.6531826257705688\n" ] } ], "source": [ "for w,p in w2v.most_similar('neural'):\n", " print(f\"{w} -> {p}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "យើងអាចគណនាការតំណាងវ៉ែកទ័រពីពាក្យដើម្បីប្រើក្នុងការបណ្តុះមូឌែលចាត់ថ្នាក់ (យើងបង្ហាញតែ ២០ ធាតុដំបូងនៃវ៉ែកទ័រដើម្បីភាពច្បាស់):\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 0.01226807, 0.06225586, 0.10693359, 0.05810547, 0.23828125,\n", " 0.03686523, 0.05151367, -0.20703125, 0.01989746, 0.10058594,\n", " -0.03759766, -0.1015625 , -0.15820312, -0.08105469, -0.0390625 ,\n", " -0.05053711, 0.16015625, 0.2578125 , 0.10058594, -0.25976562],\n", " dtype=float32)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "w2v.word_vec('play')[:20]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "រឿងល្អណាស់អំពីសេចក្តីចងក្រងសម្ភារៈន័យគឺអ្នកអាចត្រួតពិនិត្យកូដចុះបញ្ជីវ៉ិចទ័រដើម្បីផ្លាស់ប្តូរអត្ថន័យ។ ឧទាហរណ៍ យើងអាចស្នើសុំស្វែងរកពាក្យមួយដែលការតំនាងវ៉ិចទ័របស់វានឹងនៅជិតពាក្យ *king* និង *woman* ចំបង ហើយឆ្ងាយពីពាក្យ *man*៖\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('queen', 0.7118192911148071)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "w2v.most_similar(positive=['king','woman'],negative=['man'])[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ទាំង CBoW និង Skip-Grams ជា embedding ទាំងពីរដែល “ទាយ” គឺដោយពួកវាទទួលយកតែបរិបទក្នុងតំបន់ជិតៗ។ Word2Vec មិនប្រើប្រាស់អត្ថប្រយោជន៍ពីបរិបទសកលឡើយ។\n", "\n", "**FastText** សាងសង់លើ Word2Vec ដោយរៀនតំណាងវ៉ិកទ័រសម្រាប់ពាក្យនីមួយៗ និង n-gram តួអក្សរដែលមានក្នុងពាក្យនីមួយៗ។ តម្លៃនៃតំណាងទាំងនេះនឹងត្រូវបានរាប់បូកជាមធ្យមទៅវ៉ិកទ័រតែមួយនៅក្នុងជំហានបណ្តុះបណ្តាលមួយៗ។ បើទោះបីជាវានាំឲ្យមានការគណនាថែមទៀតច្រើនក្នុងការបណ្តុះបណ្តាលមុនក៏ដោយ វាអនុញ្ញាតឲ្យ embedding ពាក្យអាចបង្ហាញព័ត៌មានក្រោមពាក្យ។\n", "\n", "វិធីផ្សេងទៀត គឺ **GloVe** អនុវត្តគំនិតម៉ាត្រីសco-occurrence ហើយប្រើវិធីណឺរ៉ាល់ដើម្បីបំបែកម៉ាត្រីស co-occurrence ទៅជាវ៉ិកទ័រពាក្យដែលមានការបង្ហាញពត៌មានច្រើន និងមិនមែនជារូបមន្តលាញ់។\n", "\n", "អ្នកអាចលេងជាមួយឧទាហរណ៍ដោយផ្លាស់ប្តូរ embeddings ទៅជា FastText និង GloVe ព្រោះ gensim គាំទ្រម៉ូដែល embedding ពាក្យជាច្រើនផ្សេងទៀត។\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ការប្រើប្រាស់ការកំណត់តម្លៃជាមុនក្នុង PyTorch\n", "\n", "យើងអាចកែប្រែឧទាហរណ៍ខាងលើដើម្បីបញ្ចូល matrix នៅលើស្រទាប់ embedding របស់យើងជាមុនជាមួយនឹងការកំណត់តម្លៃពាក្យដែលមានអត្ថន័យ ដូចជា Word2Vec។ យើងត្រូវគិតគូរថា និយមន័យនៃការកំណត់តម្លៃជាមុន និងអត្ថបទរបស់យើងប្រហែលជាមិនត្រូវគ្នា ដូច្នេះយើងនឹងផ្តាច់តម្លៃទម្លាក់សម្រាប់ពាក្យដែលខ្វះដោយតម្លៃចៃដន្យ:\n" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Embedding size: 300\n", "Populating matrix, this will take some time...Done, found 41080 words, 54732 words missing\n" ] } ], "source": [ "embed_size = len(w2v.get_vector('hello'))\n", "print(f'Embedding size: {embed_size}')\n", "\n", "net = EmbedClassifier(vocab_size,embed_size,len(classes))\n", "\n", "print('Populating matrix, this will take some time...',end='')\n", "found, not_found = 0,0\n", "for i,w in enumerate(vocab.get_itos()):\n", " try:\n", " net.embedding.weight[i].data = torch.tensor(w2v.get_vector(w))\n", " found+=1\n", " except:\n", " net.embedding.weight[i].data = torch.normal(0.0,1.0,(embed_size,))\n", " not_found+=1\n", "\n", "print(f\"Done, found {found} words, {not_found} words missing\")\n", "net = net.to(device)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ឥឡូវនេះយើងចាប់ផ្តើមបណ្តុះម៉ូឌែលរបស់យើង។ សូមចំណាំថា ពេលវេលាដែលត្រូវការដើម្បីបណ្ដុះម៉ូឌែលនេះធ្វើបានយូរជាងឧទាហរណ៍មុនសម្រាប់ស្វ័យប្រវត្តន៍ធំជាងនេះហើយដែលមានប៉ារ៉ាម៉ែត្រច្រើនជាង។ ផងដែរ ដោយសារតែហេតុនេះ យើងប្រហែលជាត្រូវការបណ្តុះម៉ូឌែលរបស់យើងលើឧទាហរណ៍ច្រើនជាងនេះបើចង់ជៀសវាងការស្វ័យប្រវត្តិ។\n" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.6359375\n", "6400: acc=0.68109375\n", "9600: acc=0.7067708333333333\n", "12800: acc=0.723671875\n", "16000: acc=0.73625\n", "19200: acc=0.7463541666666667\n", "22400: acc=0.7560714285714286\n" ] }, { "data": { "text/plain": [ "(214.1013875559821, 0.7626759436980166)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "train_epoch_emb(net,train_loader, lr=4, epoch_size=25000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "នៅក្នុងករណីរបស់យើង យើងមិនឃើញការកើនឡើងយ៉ាងខ្លាំងនៅក្នុងភាពត្រឹមត្រូវទេ ដែលអាចជាសារជាផ្ទុយគ្នានៃវចនានុក្រម។ \n", "ដើម្បីឆ្លើយតបទៅនឹងបញ្ហានៃវចនានុក្រមខុសគ្នា យើងអាចប្រើចំណុចដោះស្រាយដូចខាងក្រោមមួយ៖ \n", "* ធ្វើការបណ្តុះបណ្តាលម៉ូដែល word2vec ម្តងទៀតលើវចនានុក្រមរបស់យើង \n", "* ផ្ទុកទិន្នន័យរបស់យើងជាមួយវចនានុក្រមពីម៉ូដែល word2vec ដែលបានបណ្តុះបណ្តាលរួចហើយ។ វចនានុក្រមដែលប្រើសម្រាប់ផ្ទុកទិន្នន័យអាចកំណត់បាននៅពេលផ្ទុក។ \n", "\n", "វិធីសាស្រ្តចុងក្រោយមើលទៅងាយស្រួលជាងលើសពីព្រោះស៊ុម PyTorch `torchtext` មានការគាំទ្របង្កើត embeddings ជាផ្នែកមួយក្នុងខ្លួន។ យើងអាច ឧទាហរណ៍ បង្កើតវចនានុក្រមមានមូលដ្ឋានលើ GloVe ដូចខាងក្រោមនេះ៖\n" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "100%|█████████▉| 399999/400000 [00:15<00:00, 25411.14it/s]\n" ] } ], "source": [ "vocab = torchtext.vocab.GloVe(name='6B', dim=50)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Loaded vocabulary has the following basic operations:\n", "* `vocab.stoi` ឌិចសិនាឡរី អនុញ្ញាតឱ្យយើងបម្លែងពាក្យទៅជាអុិនដិចក្នុងឌិចសិនាឡរី\n", "* `vocab.itos` ធ្វើការប្រែប្រួលផ្ទុយ - បម្លែងលេខទៅជាពាក្យ\n", "* `vocab.vectors` គឺជាអារេនៃវ៉ិកទ័រអ៊ាំប៊ីឌិញ ដូច្នេះដើម្បីទទួលបានអ៊ាំប៊ីឌីងរបស់ពាក្យ `s` យើងត្រូវប្រើ `vocab.vectors[vocab.stoi[s]]`\n", "\n", "នេះគឺជាឧទាហរណ៍នៃការគ្រប់គ្រងអ៊ាំប៊ីឌីងដើម្បីបង្ហាញលើសមីការនេះ **kind-man+woman = queen** (ខ្ញុំបានកែប្រែសមាមាត្រតិចតួចដើម្បីឲ្យវាដំណើរការ):\n" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'queen'" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# get the vector corresponding to kind-man+woman\n", "qvec = vocab.vectors[vocab.stoi['king']]-vocab.vectors[vocab.stoi['man']]+1.3*vocab.vectors[vocab.stoi['woman']]\n", "# find the index of the closest embedding vector \n", "d = torch.sum((vocab.vectors-qvec)**2,dim=1)\n", "min_idx = torch.argmin(d)\n", "# find the corresponding word\n", "vocab.itos[min_idx]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ដើម្បីបណ្តុះបណ្តាលម៉ាស៊ីនចម្រាញ់ប្រភេទដោយប្រើ embeddings ទាំងនោះ សិនមុនយើងត្រូវបំលែងឱ្យប្រើពាក្យសម្គាល់ GloVe លើសំណុំទិន្នន័យរបស់យើង៖\n" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "def offsetify(b):\n", " # first, compute data tensor from all sequences\n", " x = [torch.tensor(encode(t[1],voc=vocab)) for t in b] # pass the instance of vocab to encode function!\n", " # now, compute the offsets by accumulating the tensor of sequence lengths\n", " o = [0] + [len(t) for t in x]\n", " o = torch.tensor(o[:-1]).cumsum(dim=0)\n", " return ( \n", " torch.LongTensor([t[0]-1 for t in b]), # labels\n", " torch.cat(x), # text \n", " o\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ដូចដែលយើងបានឃើញខាងលើ វិកទ័រសម្រាប់បញ្ចូលទាំងអស់ត្រូវបានផ្ទុកនៅក្នុងម៉ាទ្រីស `vocab.vectors`។ វាធ្វើឲ្យមានភាពងាយស្រួលយ៉ាងខ្លាំងក្នុងការបញ្ចូលទំងន់ទាំងនោះទៅក្នុងទម្រង់ទំងន់នៃស្រទាប់ embedding ដោយប្រើការចម្លងធម្មតា៖\n" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "net = EmbedClassifier(len(vocab),len(vocab.vectors[0]),len(classes))\n", "net.embedding.weight.data = vocab.vectors\n", "net = net.to(device)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ឥឡូវនេះរួចដំណើរការបណ្ដុះបណ្ដាលម៉ូដែលរបស់យើង ហើយមើលថាតើយើងទទួលបានលទ្ធផលកាន់តែល្អឬយ៉ាងដូចម្តេច៖\n" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.6271875\n", "6400: acc=0.68078125\n", "9600: acc=0.7030208333333333\n", "12800: acc=0.71984375\n", "16000: acc=0.7346875\n", "19200: acc=0.7455729166666667\n", "22400: acc=0.7529464285714286\n" ] }, { "data": { "text/plain": [ "(35.53972978646833, 0.7575175943698017)" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=offsetify, shuffle=True)\n", "train_epoch_emb(net,train_loader, lr=4, epoch_size=25000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "មួយក្នុងចំណោមហេតុផលដែលយើងមិនឃើញការកើនឡើងយ៉ាងសំខាន់នៃភាពទាន់សម័យគឺដោយសារតែកម្រិតពាក្យខ្លះពីទិន្នន័យរបស់យើងបានបាត់បង់ក្នុងវាក្យសព្ទ GloVe ដែលបានបណ្តុះមុន ហើយដូច្នេះពួកវាត្រូវបានរំលងផ្តាច់។ ដើម្បីទ្រាក់ទ្រង់លើការពិតនេះ យើងអាចបណ្តុះ embeddings ផ្ទាល់ខ្លួនលើទិន្នន័យរបស់យើងបាន។\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Contextual Embeddings\n", "\n", "ការពេញលេញចម្បងមួយនៃតំណាងការជួសជុលពីមុនដែលមានសមត្ថភាពនៃ pretrained embedding representations ដូចជា Word2Vec គឺបញ្ហានៃការប្រែប្រួលអត្ថន័យពាក្យ។ នៅពេល pretrained embeddings អាចចាប់យកអត្ថន័យខ្លះៗនៃពាក្យនៅក្នុងបរិបទជាមួយ ក៏ប៉ុន្តែអត្ថន័យដែលអាចមានទាំងអស់របស់ពាក្យត្រូវបានអចិន្រ្ត័យទៅក្នុង embedding ដូចគ្នា។ វាអាចបង្កើតបញ្ហាក្នុងម៉ូដែលក្រោយ, ពោលគឺពាក្យជាច្រើនដូចជា ពាក្យ 'play' មានអត្ថន័យខុសគ្នាច្រើនអាស្រ័យលើបរិបទដែលពួកវាត្រូវបានប្រើ។\n", "\n", "ឧទាហរណ៍ ពាក្យ 'play' នៅក្នុងប្រយោគទាំងពីរនោះមានអត្ថន័យខុសគ្នាក្រោមមួយ:\n", "- ខ្ញុំបានទៅបង្ហាញ **play** នៅលើឆាកតេអាទឺរ។\n", "- John ចង់ **play** ជាមួយមិត្តភក្តិរបស់គាត់។\n", "\n", "pretrained embeddings នៅលើនេះតំណាងឲ្យទាំងពីរអត្ថន័យនៃពាក្យ 'play' នៅក្នុង embedding ដូចគ្នា។ ដើម្បីដោះស្រាយកំណត់នេះ, យើងត្រូវតែបង្កើត embeddings បញ្ជាក់លើជារបៀប **language model**, ដែលបានបណ្តុះបណ្តាលលើនិទស្សន៍ធំមួយនៃអត្ថបទ ហើយ*ដឹង*ថាពាក្យអាចត្រូវបានដាក់ជាមួយគ្នា​របៀបខុសៗគ្នា​នៅក្នុងបរិបទផ្សេងៗ។ ការពិភាក្សាពី contextual embeddings គឺក្រៅប្រធានបទសម្រាប់ មេរៀននេះ ប៉ុន្តែយើងនឹងត្រឡប់មកមើលពួកវាទៀតនៅពេលនិយាយអំពី language models ក្នុងឯកត្តបន្ទាប់។\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": "0cb620c6d4b9f7a635928804c26cf22403d89d98d79684e4529119355ee6d5a5" }, "kernelspec": { "display_name": "py37_pytorch", "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 }