{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Επαναλαμβανόμενα Νευρωνικά Δίκτυα\n", "\n", "Στο προηγούμενο κεφάλαιο, χρησιμοποιήσαμε πλούσιες σημασιολογικές αναπαραστάσεις κειμένου και έναν απλό γραμμικό ταξινομητή πάνω από τα embeddings. Αυτό που κάνει αυτή η αρχιτεκτονική είναι να συλλαμβάνει τη συνολική σημασία των λέξεων σε μια πρόταση, αλλά δεν λαμβάνει υπόψη τη **σειρά** των λέξεων, επειδή η λειτουργία συγκέντρωσης πάνω από τα embeddings αφαιρεί αυτή την πληροφορία από το αρχικό κείμενο. Επειδή αυτά τα μοντέλα δεν μπορούν να μοντελοποιήσουν τη σειρά των λέξεων, δεν μπορούν να λύσουν πιο σύνθετα ή αμφίβολα καθήκοντα, όπως η παραγωγή κειμένου ή η απάντηση σε ερωτήσεις.\n", "\n", "Για να συλλάβουμε τη σημασία της ακολουθίας του κειμένου, πρέπει να χρησιμοποιήσουμε μια άλλη αρχιτεκτονική νευρωνικού δικτύου, η οποία ονομάζεται **επαναλαμβανόμενο νευρωνικό δίκτυο**, ή RNN. Στο RNN, περνάμε την πρότασή μας μέσα από το δίκτυο ένα σύμβολο τη φορά, και το δίκτυο παράγει κάποια **κατάσταση**, την οποία στη συνέχεια περνάμε ξανά στο δίκτυο μαζί με το επόμενο σύμβολο.\n", "\n", "Δεδομένης της εισόδου ακολουθίας συμβόλων $X_0,\\dots,X_n$, το RNN δημιουργεί μια ακολουθία μπλοκ νευρωνικού δικτύου και εκπαιδεύει αυτή την ακολουθία από άκρη σε άκρη χρησιμοποιώντας back propagation. Κάθε μπλοκ δικτύου λαμβάνει ένα ζεύγος $(X_i,S_i)$ ως είσοδο και παράγει το $S_{i+1}$ ως αποτέλεσμα. Η τελική κατάσταση $S_n$ ή η έξοδος $X_n$ περνάει σε έναν γραμμικό ταξινομητή για να παραχθεί το αποτέλεσμα. Όλα τα μπλοκ του δικτύου μοιράζονται τα ίδια βάρη και εκπαιδεύονται από άκρη σε άκρη με μία διαδικασία back propagation.\n", "\n", "Επειδή οι διανύσματα κατάστασης $S_0,\\dots,S_n$ περνούν μέσα από το δίκτυο, αυτό είναι σε θέση να μάθει τις διαδοχικές εξαρτήσεις μεταξύ των λέξεων. Για παράδειγμα, όταν η λέξη *όχι* εμφανίζεται κάπου στην ακολουθία, μπορεί να μάθει να αναιρεί συγκεκριμένα στοιχεία μέσα στο διάνυσμα κατάστασης, οδηγώντας σε άρνηση.\n", "\n", "> Επειδή τα βάρη όλων των μπλοκ RNN στην εικόνα είναι κοινά, η ίδια εικόνα μπορεί να αναπαρασταθεί ως ένα μπλοκ (στα δεξιά) με έναν επαναλαμβανόμενο βρόχο ανάδρασης, ο οποίος περνά την έξοδο κατάστασης του δικτύου πίσω στην είσοδο.\n", "\n", "Ας δούμε πώς τα επαναλαμβανόμενα νευρωνικά δίκτυα μπορούν να μας βοηθήσουν να ταξινομήσουμε το dataset ειδήσεων μας.\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, θα εφαρμόσουμε πρώτα ένα στρώμα ενσωμάτωσης (embedding layer) για να μειώσουμε τη διαστατικότητα του λεξιλογίου εισόδου, και στη συνέχεια θα προσθέσουμε ένα στρώμα 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 με embeddings Word2Vec ή GloVe, όπως περιγράφηκε στην προηγούμενη ενότητα. Για καλύτερη κατανόηση, ίσως θελήσετε να προσαρμόσετε αυτόν τον κώδικα ώστε να λειτουργεί με προεκπαιδευμένα embeddings.\n", "\n", "Στην περίπτωσή μας, θα χρησιμοποιήσουμε έναν φορτωτή δεδομένων με padding, έτσι ώστε κάθε batch να περιέχει έναν αριθμό από sequences με padding του ίδιου μήκους. Το RNN layer θα λάβει τη sequence των embedding tensors και θα παράγει δύο εξόδους: \n", "* $x$ είναι μια sequence από εξόδους των RNN cells σε κάθε βήμα\n", "* $h$ είναι η τελική κρυφή κατάσταση για το τελευταίο στοιχείο της sequence\n", "\n", "Στη συνέχεια, εφαρμόζουμε έναν πλήρως συνδεδεμένο γραμμικό ταξινομητή για να πάρουμε τον αριθμό της κατηγορίας.\n", "\n", "> **Σημείωση:** Τα RNNs είναι αρκετά δύσκολο να εκπαιδευτούν, επειδή όταν τα RNN cells ξεδιπλώνονται κατά μήκος του μήκους της sequence, ο αριθμός των επιπέδων που εμπλέκονται στην οπισθοδιάδοση γίνεται αρκετά μεγάλος. Επομένως, χρειάζεται να επιλέξουμε μικρό ρυθμό εκμάθησης και να εκπαιδεύσουμε το δίκτυο σε μεγαλύτερο σύνολο δεδομένων για να παραχθούν καλά αποτελέσματα. Αυτό μπορεί να πάρει αρκετό χρόνο, οπότε η χρήση 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", "Ένα από τα κύρια προβλήματα των κλασικών RNNs είναι το λεγόμενο πρόβλημα των **εξασθενημένων βαθμίδων (vanishing gradients)**. Επειδή τα RNNs εκπαιδεύονται από άκρη σε άκρη σε μία διαδικασία οπισθοδιάδοσης, δυσκολεύονται να μεταδώσουν το σφάλμα στα πρώτα επίπεδα του δικτύου, και έτσι το δίκτυο δεν μπορεί να μάθει σχέσεις μεταξύ απομακρυσμένων συμβόλων. Ένας από τους τρόπους για να αποφευχθεί αυτό το πρόβλημα είναι η εισαγωγή της **ρητής διαχείρισης κατάστασης** μέσω της χρήσης των λεγόμενων **πυλών (gates)**. Υπάρχουν δύο πιο γνωστές αρχιτεκτονικές αυτού του είδους: η **Μνήμη Μακράς και Βραχείας Διάρκειας** (LSTM) και η **Μονάδα Ελεγχόμενης Μεταβίβασης** (GRU).\n", "\n", "![Εικόνα που δείχνει ένα παράδειγμα κυψέλης μνήμης μακράς και βραχείας διάρκειας](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "Το Δίκτυο LSTM είναι οργανωμένο με τρόπο παρόμοιο με το RNN, αλλά υπάρχουν δύο καταστάσεις που μεταφέρονται από επίπεδο σε επίπεδο: η πραγματική κατάσταση $c$ και το κρυφό διάνυσμα $h$. Σε κάθε μονάδα, το κρυφό διάνυσμα $h_i$ συνενώνεται με την είσοδο $x_i$, και ελέγχουν τι συμβαίνει στην κατάσταση $c$ μέσω των **πυλών**. Κάθε πύλη είναι ένα νευρωνικό δίκτυο με ενεργοποίηση 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", "> **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 με μηδενικά διανύσματα. Παρόλο που αυτό οδηγεί σε κάποια σπατάλη μνήμης, με τα RNNs είναι πιο κρίσιμο το γεγονός ότι δημιουργούνται επιπλέον RNN cells για τα συμπληρωμένα στοιχεία εισόδου, τα οποία συμμετέχουν στην εκπαίδευση, αλλά δεν περιέχουν καμία σημαντική πληροφορία εισόδου. Θα ήταν πολύ καλύτερο να εκπαιδεύσουμε το 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 cells με μεγάλο minibatch (`[1,6,9]`), αλλά στη συνέχεια να ολοκληρώσουμε την επεξεργασία της τρίτης ακολουθίας και να συνεχίσουμε την εκπαίδευση με μικρότερα minibatches (`[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`. Όλα τα επαναλαμβανόμενα επίπεδα, συμπεριλαμβανομένων των RNN, LSTM και GRU, υποστηρίζουν συσκευασμένες ακολουθίες ως είσοδο και παράγουν συσκευασμένη έξοδο, η οποία μπορεί να αποκωδικοποιηθεί χρησιμοποιώντας τη συνάρτηση `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` θα λαμβάνει τόσο το συμπληρωμένο minibatch όσο και το διάνυσμα των μηκών ακολουθίας. Αφού υπολογίσουμε την ενσωμάτωση, υπολογίζουμε την συσκευασμένη ακολουθία, την περνάμε στο επίπεδο 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": [] }, { "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` απαιτεί ο tensor μήκους της ακολουθίας να βρίσκεται στη συσκευή CPU, και έτσι η συνάρτηση εκπαίδευσης πρέπει να αποφεύγει τη μεταφορά των δεδομένων μήκους της ακολουθίας στη GPU κατά την εκπαίδευση. Μπορείτε να εξετάσετε την υλοποίηση της συνάρτησης `train_emb` στο αρχείο [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Διπλής κατεύθυνσης και πολυεπίπεδα RNNs\n", "\n", "Στα παραδείγματά μας, όλα τα επαναλαμβανόμενα δίκτυα λειτουργούσαν προς μία κατεύθυνση, από την αρχή μιας ακολουθίας μέχρι το τέλος. Αυτό φαίνεται φυσικό, καθώς μοιάζει με τον τρόπο που διαβάζουμε και ακούμε ομιλία. Ωστόσο, δεδομένου ότι σε πολλές πρακτικές περιπτώσεις έχουμε τυχαία πρόσβαση στην ακολουθία εισόδου, μπορεί να έχει νόημα να εκτελέσουμε επαναλαμβανόμενο υπολογισμό και προς τις δύο κατευθύνσεις. Τέτοια δίκτυα ονομάζονται **διπλής κατεύθυνσης** RNNs, και μπορούν να δημιουργηθούν περνώντας την παράμετρο `bidirectional=True` στον κατασκευαστή RNN/LSTM/GRU.\n", "\n", "Όταν δουλεύουμε με δίκτυο διπλής κατεύθυνσης, θα χρειαστούμε δύο διανύσματα κρυφής κατάστασης, ένα για κάθε κατεύθυνση. Το PyTorch κωδικοποιεί αυτά τα διανύσματα ως ένα διάνυσμα διπλάσιου μεγέθους, κάτι που είναι αρκετά βολικό, καθώς συνήθως περνάτε την προκύπτουσα κρυφή κατάσταση σε πλήρως συνδεδεμένο γραμμικό επίπεδο, και απλώς θα χρειαστεί να λάβετε υπόψη αυτήν την αύξηση μεγέθους κατά τη δημιουργία του επιπέδου.\n", "\n", "Το επαναλαμβανόμενο δίκτυο, είτε μονής κατεύθυνσης είτε διπλής κατεύθυνσης, καταγράφει ορισμένα μοτίβα μέσα σε μια ακολουθία και μπορεί να τα αποθηκεύσει σε ένα διάνυσμα κατάστασης ή να τα περάσει στην έξοδο. Όπως συμβαίνει με τα συνελικτικά δίκτυα, μπορούμε να χτίσουμε ένα άλλο επαναλαμβανόμενο επίπεδο πάνω από το πρώτο για να καταγράψουμε μοτίβα υψηλότερου επιπέδου, χτισμένα από μοτίβα χαμηλότερου επιπέδου που εξάγονται από το πρώτο επίπεδο. Αυτό μας οδηγεί στην έννοια του **πολυεπίπεδου RNN**, το οποίο αποτελείται από δύο ή περισσότερα επαναλαμβανόμενα δίκτυα, όπου η έξοδος του προηγούμενου επιπέδου περνά στο επόμενο επίπεδο ως είσοδος.\n", "\n", "![Εικόνα που δείχνει ένα πολυεπίπεδο RNN με μακροχρόνια-βραχυχρόνια μνήμη](../../../../../translated_images/el/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" ] } ], "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:52:53+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb", "language_code": "el" } }, "nbformat": 4, "nbformat_minor": 2 }