726 lines
39 KiB
Plaintext
726 lines
39 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Ενσωματώσεις\n",
|
||
"\n",
|
||
"Στο προηγούμενο παράδειγμά μας, εργαστήκαμε με διανύσματα υψηλής διάστασης bag-of-words με μήκος `vocab_size`, και μετατρέπαμε ρητά από διανύσματα χαμηλής διάστασης θέσης σε αραιή αναπαράσταση one-hot. Αυτή η αναπαράσταση one-hot δεν είναι αποδοτική από άποψη μνήμης, επιπλέον, κάθε λέξη αντιμετωπίζεται ανεξάρτητα από τις άλλες, δηλαδή τα διανύσματα one-hot δεν εκφράζουν καμία σημασιολογική ομοιότητα μεταξύ των λέξεων.\n",
|
||
"\n",
|
||
"Σε αυτήν την ενότητα, θα συνεχίσουμε να εξερευνούμε το σύνολο δεδομένων **News AG**. Για να ξεκινήσουμε, ας φορτώσουμε τα δεδομένα και ας πάρουμε κάποιους ορισμούς από το προηγούμενο notebook.\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": [
|
||
"## Τι είναι η ενσωμάτωση;\n",
|
||
"\n",
|
||
"Η ιδέα της **ενσωμάτωσης** είναι να αναπαρασταθούν οι λέξεις με πυκνά διανύσματα χαμηλότερης διάστασης, τα οποία με κάποιο τρόπο αντανακλούν τη σημασιολογική έννοια μιας λέξης. Αργότερα θα συζητήσουμε πώς να δημιουργήσουμε ουσιαστικές ενσωματώσεις λέξεων, αλλά προς το παρόν ας σκεφτούμε την ενσωμάτωση ως έναν τρόπο μείωσης της διάστασης ενός διανύσματος λέξης.\n",
|
||
"\n",
|
||
"Έτσι, η στρώση ενσωμάτωσης θα παίρνει μια λέξη ως είσοδο και θα παράγει ένα διανυσματικό αποτέλεσμα με καθορισμένο `embedding_size`. Με μια έννοια, είναι πολύ παρόμοια με τη στρώση `Linear`, αλλά αντί να λαμβάνει ένα one-hot κωδικοποιημένο διάνυσμα, θα μπορεί να λαμβάνει έναν αριθμό λέξης ως είσοδο.\n",
|
||
"\n",
|
||
"Χρησιμοποιώντας τη στρώση ενσωμάτωσης ως την πρώτη στρώση στο δίκτυό μας, μπορούμε να μεταβούμε από το μοντέλο bag-of-words στο μοντέλο **embedding bag**, όπου πρώτα μετατρέπουμε κάθε λέξη στο κείμενό μας στην αντίστοιχη ενσωμάτωσή της και στη συνέχεια υπολογίζουμε κάποια συνάρτηση συσσωμάτωσης πάνω σε όλες αυτές τις ενσωματώσεις, όπως `sum`, `average` ή `max`.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"Το νευρωνικό δίκτυο ταξινομητή μας θα ξεκινά με τη στρώση ενσωμάτωσης, στη συνέχεια τη στρώση συσσωμάτωσης και έναν γραμμικό ταξινομητή στην κορυφή:\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, όλοι οι BoW tensors σε ένα minibatch είχαν ίσο μέγεθος `vocab_size`, ανεξάρτητα από το πραγματικό μήκος της ακολουθίας κειμένου μας. Μόλις περάσουμε στις ενσωματώσεις λέξεων, θα καταλήξουμε με μεταβλητό αριθμό λέξεων σε κάθε δείγμα κειμένου, και όταν συνδυάζουμε αυτά τα δείγματα σε minibatches θα πρέπει να εφαρμόσουμε κάποια συμπλήρωση.\n",
|
||
"\n",
|
||
"Αυτό μπορεί να γίνει χρησιμοποιώντας την ίδια τεχνική της παροχής της συνάρτησης `collate_fn` στην πηγή δεδομένων:\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": [
|
||
"### Εκπαίδευση του ταξινομητή ενσωμάτωσης\n",
|
||
"\n",
|
||
"Τώρα που έχουμε ορίσει τον κατάλληλο φορτωτή δεδομένων, μπορούμε να εκπαιδεύσουμε το μοντέλο χρησιμοποιώντας τη συνάρτηση εκπαίδευσης που ορίσαμε στην προηγούμενη ενότητα:\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": [
|
||
"> **Σημείωση**: Εδώ εκπαιδεύουμε μόνο για 25k εγγραφές (λιγότερο από μία πλήρη εποχή) για λόγους χρόνου, αλλά μπορείτε να συνεχίσετε την εκπαίδευση, να γράψετε μια συνάρτηση για εκπαίδευση για αρκετές εποχές και να πειραματιστείτε με την παράμετρο του ρυθμού μάθησης για να επιτύχετε μεγαλύτερη ακρίβεια. Θα πρέπει να μπορείτε να φτάσετε σε ακρίβεια περίπου 90%.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Επίπεδο EmbeddingBag και Αναπαράσταση Ακολουθιών Μεταβλητού Μήκους\n",
|
||
"\n",
|
||
"Στην προηγούμενη αρχιτεκτονική, έπρεπε να συμπληρώσουμε όλες τις ακολουθίες ώστε να έχουν το ίδιο μήκος για να τις εντάξουμε σε ένα minibatch. Αυτός δεν είναι ο πιο αποδοτικός τρόπος για να αναπαραστήσουμε ακολουθίες μεταβλητού μήκους - μια άλλη προσέγγιση θα ήταν να χρησιμοποιήσουμε έναν **διάνυσμα μετατοπίσεων (offset)**, το οποίο θα περιείχε τις μετατοπίσεις όλων των ακολουθιών που αποθηκεύονται σε ένα μεγάλο διάνυσμα.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"> **Σημείωση**: Στην παραπάνω εικόνα, δείχνουμε μια ακολουθία χαρακτήρων, αλλά στο παράδειγμά μας δουλεύουμε με ακολουθίες λέξεων. Ωστόσο, η γενική αρχή της αναπαράστασης ακολουθιών με διάνυσμα μετατοπίσεων παραμένει η ίδια.\n",
|
||
"\n",
|
||
"Για να δουλέψουμε με την αναπαράσταση μετατοπίσεων, χρησιμοποιούμε το [`EmbeddingBag`](https://pytorch.org/docs/stable/generated/torch.nn.EmbeddingBag.html) επίπεδο. Είναι παρόμοιο με το `Embedding`, αλλά δέχεται ως είσοδο ένα διάνυσμα περιεχομένου και ένα διάνυσμα μετατοπίσεων, και περιλαμβάνει επίσης ένα επίπεδο μέσου όρου, το οποίο μπορεί να είναι `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": [
|
||
"Σημειώστε ότι, σε αντίθεση με όλα τα προηγούμενα παραδείγματα, το δίκτυό μας τώρα δέχεται δύο παραμέτρους: το διανύσμα δεδομένων και το διανύσμα μετατόπισης, τα οποία έχουν διαφορετικά μεγέθη. Παρομοίως, ο φορτωτής δεδομένων μας παρέχει επίσης 3 τιμές αντί για 2: τόσο τα διανύσματα κειμένου όσο και τα διανύσματα μετατόπισης παρέχονται ως χαρακτηριστικά. Επομένως, πρέπει να προσαρμόσουμε ελαφρώς τη συνάρτηση εκπαίδευσης μας για να το λάβουμε υπόψη:\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": [
|
||
"## Σημασιολογικές Ενσωματώσεις: Word2Vec\n",
|
||
"\n",
|
||
"Στο προηγούμενο παράδειγμα, το επίπεδο ενσωμάτωσης του μοντέλου έμαθε να αντιστοιχεί λέξεις σε διανυσματικές αναπαραστάσεις, ωστόσο, αυτή η αναπαράσταση δεν είχε ιδιαίτερη σημασιολογική αξία. Θα ήταν χρήσιμο να μάθουμε μια τέτοια διανυσματική αναπαράσταση, όπου παρόμοιες λέξεις ή συνώνυμα θα αντιστοιχούσαν σε διανύσματα που βρίσκονται κοντά μεταξύ τους με βάση κάποια απόσταση διανυσμάτων (π.χ. ευκλείδεια απόσταση).\n",
|
||
"\n",
|
||
"Για να το πετύχουμε αυτό, πρέπει να προεκπαιδεύσουμε το μοντέλο ενσωμάτωσης σε μια μεγάλη συλλογή κειμένων με συγκεκριμένο τρόπο. Ένας από τους πρώτους τρόπους εκπαίδευσης σημασιολογικών ενσωματώσεων ονομάζεται [Word2Vec](https://en.wikipedia.org/wiki/Word2vec). Βασίζεται σε δύο κύριες αρχιτεκτονικές που χρησιμοποιούνται για την παραγωγή κατανεμημένων αναπαραστάσεων λέξεων:\n",
|
||
"\n",
|
||
" - **Συνεχές σακίδιο λέξεων** (CBoW) — σε αυτή την αρχιτεκτονική, εκπαιδεύουμε το μοντέλο να προβλέπει μια λέξη από το περιβάλλον της. Δεδομένου του ngram $(W_{-2},W_{-1},W_0,W_1,W_2)$, ο στόχος του μοντέλου είναι να προβλέψει το $W_0$ από το $(W_{-2},W_{-1},W_1,W_2)$.\n",
|
||
" - **Συνεχές skip-gram** — είναι το αντίθετο του CBoW. Το μοντέλο χρησιμοποιεί το παράθυρο των λέξεων του περιβάλλοντος για να προβλέψει την τρέχουσα λέξη.\n",
|
||
"\n",
|
||
"Το CBoW είναι ταχύτερο, ενώ το skip-gram είναι πιο αργό, αλλά αποδίδει καλύτερα στην αναπαράσταση σπάνιων λέξεων.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"Για να πειραματιστούμε με την ενσωμάτωση word2vec που έχει προεκπαιδευτεί στο σύνολο δεδομένων Google News, μπορούμε να χρησιμοποιήσουμε τη βιβλιοθήκη **gensim**. Παρακάτω βρίσκουμε τις λέξεις που είναι πιο παρόμοιες με τη λέξη 'neural'.\n",
|
||
"\n",
|
||
"> **Σημείωση:** Όταν δημιουργείτε για πρώτη φορά διανύσματα λέξεων, η λήψη τους μπορεί να πάρει κάποιο χρόνο!\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": [
|
||
"Μπορούμε επίσης να υπολογίσουμε ενσωματώσεις διανυσμάτων από τη λέξη, για να χρησιμοποιηθούν στην εκπαίδευση του μοντέλου ταξινόμησης (δείχνουμε μόνο τα πρώτα 20 στοιχεία του διανύσματος για σαφήνεια):\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": [
|
||
"Το σπουδαίο με τις σημασιολογικές ενσωματώσεις είναι ότι μπορείτε να χειριστείτε την κωδικοποίηση των διανυσμάτων για να αλλάξετε τη σημασιολογία. Για παράδειγμα, μπορούμε να ζητήσουμε να βρούμε μια λέξη, της οποίας η διανυσματική αναπαράσταση θα είναι όσο το δυνατόν πιο κοντά στις λέξεις *βασιλιάς* και *γυναίκα*, και όσο το δυνατόν πιο μακριά από τη λέξη *άντρας*:\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 είναι \"προβλεπτικά\" embeddings, καθώς λαμβάνουν υπόψη μόνο τοπικά συμφραζόμενα. Το Word2Vec δεν εκμεταλλεύεται το παγκόσμιο συμφραζόμενο.\n",
|
||
"\n",
|
||
"**Το FastText** βασίζεται στο Word2Vec μαθαίνοντας διανυσματικές αναπαραστάσεις για κάθε λέξη και τα χαρακτήρα n-grams που βρίσκονται μέσα σε κάθε λέξη. Οι τιμές των αναπαραστάσεων στη συνέχεια υπολογίζονται ως μέσος όρος σε ένα διάνυσμα σε κάθε βήμα εκπαίδευσης. Παρόλο που αυτό προσθέτει αρκετό επιπλέον υπολογισμό κατά την προεκπαίδευση, επιτρέπει στα word embeddings να κωδικοποιούν πληροφορίες υπολέξεων.\n",
|
||
"\n",
|
||
"Μια άλλη μέθοδος, το **GloVe**, αξιοποιεί την ιδέα του πίνακα συν-εμφάνισης, χρησιμοποιώντας νευρωνικές μεθόδους για να αποσυνθέσει τον πίνακα συν-εμφάνισης σε πιο εκφραστικά και μη γραμμικά διανύσματα λέξεων.\n",
|
||
"\n",
|
||
"Μπορείτε να πειραματιστείτε με το παράδειγμα αλλάζοντας τα embeddings σε FastText και GloVe, καθώς το gensim υποστηρίζει διάφορα μοντέλα ενσωμάτωσης λέξεων.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Χρήση Προεκπαιδευμένων Ενσωματώσεων στο PyTorch\n",
|
||
"\n",
|
||
"Μπορούμε να τροποποιήσουμε το παραπάνω παράδειγμα ώστε να προ-γεμίσουμε τον πίνακα στο επίπεδο ενσωμάτωσης μας με σημασιολογικές ενσωματώσεις, όπως το 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",
|
||
"* Φόρτωση του dataset μας με το λεξιλόγιο από το προεκπαιδευμένο μοντέλο word2vec. Το λεξιλόγιο που χρησιμοποιείται για τη φόρτωση του dataset μπορεί να καθοριστεί κατά τη διαδικασία φόρτωσης. \n",
|
||
"\n",
|
||
"Η δεύτερη προσέγγιση φαίνεται ευκολότερη, ειδικά επειδή το πλαίσιο `torchtext` της PyTorch περιέχει ενσωματωμένη υποστήριξη για 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": [
|
||
"Το φορτωμένο λεξιλόγιο έχει τις εξής βασικές λειτουργίες:\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": [
|
||
"Για να εκπαιδεύσουμε τον ταξινομητή χρησιμοποιώντας αυτές τις ενσωματώσεις, πρέπει πρώτα να κωδικοποιήσουμε το σύνολο δεδομένων μας χρησιμοποιώντας το λεξιλόγιο του 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`. Αυτό καθιστά εξαιρετικά εύκολη τη φόρτωση αυτών των βαρών στα βάρη της στρώσης ενσωμάτωσης χρησιμοποιώντας απλή αντιγραφή:\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 και, επομένως, ουσιαστικά αγνοούνται. Για να ξεπεράσουμε αυτό το γεγονός, μπορούμε να εκπαιδεύσουμε τις δικές μας ενσωματώσεις στο σύνολο δεδομένων μας.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Εννοιολογικές Ενσωματώσεις\n",
|
||
"\n",
|
||
"Ένας βασικός περιορισμός των παραδοσιακών προεκπαιδευμένων αναπαραστάσεων ενσωματώσεων, όπως το Word2Vec, είναι το πρόβλημα της αποσαφήνισης της σημασίας των λέξεων. Παρόλο που οι προεκπαιδευμένες ενσωματώσεις μπορούν να αποτυπώσουν μέρος της σημασίας των λέξεων στο πλαίσιο τους, κάθε πιθανή σημασία μιας λέξης κωδικοποιείται στην ίδια ενσωμάτωση. Αυτό μπορεί να προκαλέσει προβλήματα σε μοντέλα που βασίζονται σε αυτά, καθώς πολλές λέξεις, όπως η λέξη 'play', έχουν διαφορετικές σημασίες ανάλογα με το πλαίσιο στο οποίο χρησιμοποιούνται.\n",
|
||
"\n",
|
||
"Για παράδειγμα, η λέξη 'play' στις παρακάτω δύο προτάσεις έχει αρκετά διαφορετική σημασία:\n",
|
||
"- Πήγα σε μια **παράσταση** στο θέατρο.\n",
|
||
"- Ο Γιάννης θέλει να **παίξει** με τους φίλους του.\n",
|
||
"\n",
|
||
"Οι προεκπαιδευμένες ενσωματώσεις που αναφέρθηκαν παραπάνω αντιπροσωπεύουν και τις δύο αυτές σημασίες της λέξης 'play' στην ίδια ενσωμάτωση. Για να ξεπεράσουμε αυτόν τον περιορισμό, πρέπει να δημιουργήσουμε ενσωματώσεις βασισμένες στο **γλωσσικό μοντέλο**, το οποίο έχει εκπαιδευτεί σε ένα μεγάλο σώμα κειμένου και *γνωρίζει* πώς οι λέξεις μπορούν να συνδυαστούν σε διαφορετικά πλαίσια. Η συζήτηση για τις εννοιολογικές ενσωματώσεις είναι εκτός του πεδίου αυτού του οδηγού, αλλά θα επανέλθουμε σε αυτές όταν μιλήσουμε για γλωσσικά μοντέλα στην επόμενη ενότητα.\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": "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"
|
||
},
|
||
"coopTranslator": {
|
||
"original_hash": "f50b026abce5cf36783a560ea72cb9b1",
|
||
"translation_date": "2025-08-29T10:58:08+00:00",
|
||
"source_file": "lessons/5-NLP/14-Embeddings/EmbeddingsPyTorch.ipynb",
|
||
"language_code": "el"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
} |