479 lines
24 KiB
Plaintext
479 lines
24 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Sieci neuronowe rekurencyjne\n",
|
|
"\n",
|
|
"W poprzednim module korzystaliśmy z bogatych reprezentacji semantycznych tekstu oraz prostego klasyfikatora liniowego na bazie osadzeń. Taka architektura pozwala uchwycić zagregowane znaczenie słów w zdaniu, ale nie uwzględnia **kolejności** słów, ponieważ operacja agregacji na osadzeniach usuwa tę informację z oryginalnego tekstu. Ponieważ te modele nie potrafią modelować kolejności słów, nie są w stanie rozwiązywać bardziej złożonych lub niejednoznacznych zadań, takich jak generowanie tekstu czy odpowiadanie na pytania.\n",
|
|
"\n",
|
|
"Aby uchwycić znaczenie sekwencji tekstu, musimy użyć innej architektury sieci neuronowej, zwanej **siecią neuronową rekurencyjną** (RNN). W RNN przekazujemy nasze zdanie przez sieć, symbol po symbolu, a sieć generuje pewien **stan**, który następnie przekazujemy z kolejnym symbolem z powrotem do sieci.\n",
|
|
"\n",
|
|
"Given the input sequence of tokens $X_0,\\dots,X_n$, RNN creates a sequence of neural network blocks, and trains this sequence end-to-end using back propagation. Each network block takes a pair $(X_i,S_i)$ as an input, and produces $S_{i+1}$ as a result. Final state $S_n$ or output $X_n$ goes into a linear classifier to produce the result. All network blocks share the same weights, and are trained end-to-end using one back propagation pass.\n",
|
|
"\n",
|
|
"Ponieważ wektory stanów $S_0,\\dots,S_n$ są przekazywane przez sieć, jest ona w stanie nauczyć się zależności sekwencyjnych między słowami. Na przykład, gdy słowo *not* pojawia się gdzieś w sekwencji, sieć może nauczyć się negować pewne elementy wektora stanu, co prowadzi do negacji.\n",
|
|
"\n",
|
|
"> Ponieważ wagi wszystkich bloków RNN na obrazku są współdzielone, ten sam obrazek można przedstawić jako jeden blok (po prawej) z rekurencyjną pętlą sprzężenia zwrotnego, która przekazuje wyjściowy stan sieci z powrotem na wejście.\n",
|
|
"\n",
|
|
"Zobaczmy, jak sieci neuronowe rekurencyjne mogą pomóc nam w klasyfikacji naszego zbioru danych z wiadomościami.\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": [
|
|
"## Prosty klasyfikator RNN\n",
|
|
"\n",
|
|
"W przypadku prostego RNN każda jednostka rekurencyjna jest prostą siecią liniową, która przyjmuje połączony wektor wejściowy i wektor stanu, a następnie generuje nowy wektor stanu. PyTorch reprezentuje tę jednostkę za pomocą klasy `RNNCell`, a sieci takich komórek - jako warstwę `RNN`.\n",
|
|
"\n",
|
|
"Aby zdefiniować klasyfikator RNN, najpierw zastosujemy warstwę osadzania (embedding layer), aby zmniejszyć wymiarowość słownika wejściowego, a następnie umieścimy na niej warstwę 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": [
|
|
"> **Note:** Używamy tutaj nieprzeszkolonej warstwy osadzania dla uproszczenia, ale dla jeszcze lepszych wyników możemy użyć wstępnie przeszkolonej warstwy osadzania z osadzeniami Word2Vec lub GloVe, jak opisano w poprzedniej jednostce. Aby lepiej zrozumieć, możesz dostosować ten kod do pracy z wstępnie przeszkolonymi osadzeniami.\n",
|
|
"\n",
|
|
"W naszym przypadku użyjemy załadowanego danych z wypełnieniem, więc każda paczka będzie zawierać kilka wypełnionych sekwencji o tej samej długości. Warstwa RNN przyjmie sekwencję tensorów osadzeń i wygeneruje dwa wyjścia:\n",
|
|
"* $x$ to sekwencja wyników komórek RNN na każdym kroku\n",
|
|
"* $h$ to końcowy stan ukryty dla ostatniego elementu sekwencji\n",
|
|
"\n",
|
|
"Następnie stosujemy w pełni połączony klasyfikator liniowy, aby uzyskać liczbę klas.\n",
|
|
"\n",
|
|
"> **Note:** Trenowanie RNN jest dość trudne, ponieważ po rozwinięciu komórek RNN wzdłuż długości sekwencji liczba warstw zaangażowanych w propagację wsteczną jest dość duża. Dlatego musimy wybrać małą szybkość uczenia się i trenować sieć na większym zbiorze danych, aby uzyskać dobre wyniki. Może to zająć dużo czasu, więc preferowane jest użycie 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": [
|
|
"## Długoterminowa Pamięć Krótkoterminowa (LSTM)\n",
|
|
"\n",
|
|
"Jednym z głównych problemów klasycznych RNN jest tzw. problem **zanikających gradientów**. Ponieważ RNN są trenowane end-to-end w jednym przebiegu wstecznej propagacji, mają trudności z propagowaniem błędu do pierwszych warstw sieci, co uniemożliwia sieci naukę relacji między odległymi tokenami. Jednym ze sposobów na uniknięcie tego problemu jest wprowadzenie **jawnego zarządzania stanem** poprzez zastosowanie tzw. **bramek**. Istnieją dwie najbardziej znane architektury tego typu: **Długoterminowa Pamięć Krótkoterminowa** (LSTM) oraz **Jednostka Bramkowana (GRU)**.\n",
|
|
"\n",
|
|
"\n",
|
|
"\n",
|
|
"Sieć LSTM jest zorganizowana w sposób podobny do RNN, ale przekazywane są dwa stany z warstwy do warstwy: aktualny stan $c$ oraz ukryty wektor $h$. W każdej jednostce ukryty wektor $h_i$ jest konkatenowany z wejściem $x_i$, a następnie kontrolują one, co dzieje się ze stanem $c$ za pomocą **bramek**. Każda bramka to sieć neuronowa z aktywacją sigmoidalną (wyjście w zakresie $[0,1]$), którą można traktować jako maskę bitową, gdy jest mnożona przez wektor stanu. Na powyższym obrazku (od lewej do prawej) znajdują się następujące bramki:\n",
|
|
"* **bramka zapominania** przyjmuje ukryty wektor i określa, które komponenty wektora $c$ należy zapomnieć, a które przepuścić dalej.\n",
|
|
"* **bramka wejściowa** pobiera pewne informacje z wejścia i ukrytego wektora, a następnie wprowadza je do stanu.\n",
|
|
"* **bramka wyjściowa** przekształca stan za pomocą pewnej warstwy liniowej z aktywacją $\\tanh$, a następnie wybiera niektóre z jej komponentów, używając ukrytego wektora $h_i$, aby wygenerować nowy stan $c_{i+1}$.\n",
|
|
"\n",
|
|
"Komponenty stanu $c$ można traktować jako pewne flagi, które można włączać i wyłączać. Na przykład, gdy w sekwencji napotkamy imię *Alice*, możemy założyć, że odnosi się ono do postaci żeńskiej, i podnieść flagę w stanie, że w zdaniu występuje rzeczownik żeński. Gdy później napotkamy frazę *and Tom*, podniesiemy flagę, że mamy rzeczownik w liczbie mnogiej. W ten sposób, manipulując stanem, możemy teoretycznie śledzić właściwości gramatyczne części zdania.\n",
|
|
"\n",
|
|
"> **Note**: Świetnym źródłem do zrozumienia wewnętrznej struktury LSTM jest ten doskonały artykuł [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) autorstwa Christophera Olaha.\n",
|
|
"\n",
|
|
"Chociaż wewnętrzna struktura komórki LSTM może wydawać się skomplikowana, PyTorch ukrywa tę implementację w klasie `LSTMCell` i udostępnia obiekt `LSTM` do reprezentowania całej warstwy LSTM. W związku z tym implementacja klasyfikatora LSTM będzie bardzo podobna do prostego RNN, który widzieliśmy powyżej:\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": [
|
|
"## Sekwencje spakowane\n",
|
|
"\n",
|
|
"W naszym przykładzie musieliśmy uzupełnić wszystkie sekwencje w minibatchu zerowymi wektorami. Choć prowadzi to do pewnego marnotrawstwa pamięci, w przypadku RNN-ów bardziej istotne jest to, że dodatkowe komórki RNN są tworzone dla uzupełnionych elementów wejściowych, które biorą udział w treningu, ale nie niosą żadnych istotnych informacji wejściowych. Znacznie lepiej byłoby trenować RNN tylko do rzeczywistej długości sekwencji.\n",
|
|
"\n",
|
|
"Aby to osiągnąć, w PyTorch wprowadzono specjalny format przechowywania uzupełnionych sekwencji. Załóżmy, że mamy uzupełniony minibatch wejściowy, który wygląda tak:\n",
|
|
"```\n",
|
|
"[[1,2,3,4,5],\n",
|
|
" [6,7,8,0,0],\n",
|
|
" [9,0,0,0,0]]\n",
|
|
"```\n",
|
|
"Tutaj 0 reprezentuje wartości uzupełniające, a rzeczywisty wektor długości sekwencji wejściowych to `[5,3,1]`.\n",
|
|
"\n",
|
|
"Aby efektywnie trenować RNN z uzupełnionymi sekwencjami, chcemy rozpocząć trening pierwszej grupy komórek RNN z dużym minibatchem (`[1,6,9]`), ale następnie zakończyć przetwarzanie trzeciej sekwencji i kontynuować trening z mniejszymi minibatchami (`[2,7]`, `[3,8]`), i tak dalej. W ten sposób spakowana sekwencja jest reprezentowana jako jeden wektor - w naszym przypadku `[1,6,9,2,7,3,8,4,5]`, oraz wektor długości (`[5,3,1]`), z którego można łatwo odtworzyć oryginalny uzupełniony minibatch.\n",
|
|
"\n",
|
|
"Aby stworzyć spakowaną sekwencję, możemy użyć funkcji `torch.nn.utils.rnn.pack_padded_sequence`. Wszystkie warstwy rekurencyjne, w tym RNN, LSTM i GRU, obsługują spakowane sekwencje jako wejście i produkują spakowane wyjście, które można zdekodować za pomocą `torch.nn.utils.rnn.pad_packed_sequence`.\n",
|
|
"\n",
|
|
"Aby móc stworzyć spakowaną sekwencję, musimy przekazać wektor długości do sieci, a zatem potrzebujemy innej funkcji do przygotowania minibatchy:\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": [
|
|
"Rzeczywista sieć byłaby bardzo podobna do `LSTMClassifier` powyżej, ale w metodzie `forward` otrzyma zarówno wyściełaną mini-partię, jak i wektor długości sekwencji. Po obliczeniu osadzenia, obliczamy zapakowaną sekwencję, przekazujemy ją do warstwy LSTM, a następnie rozpakowujemy wynik z powrotem.\n",
|
|
"\n",
|
|
"> **Uwaga**: W rzeczywistości nie używamy rozpakowanego wyniku `x`, ponieważ w dalszych obliczeniach korzystamy z wyjścia z warstw ukrytych. Dlatego możemy całkowicie usunąć rozpakowywanie z tego kodu. Powód, dla którego umieszczamy je tutaj, to umożliwienie łatwej modyfikacji tego kodu, jeśli zajdzie potrzeba użycia wyjścia sieci w dalszych obliczeniach.\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": [
|
|
"> **Uwaga:** Możesz zauważyć parametr `use_pack_sequence`, który przekazujemy do funkcji treningowej. Obecnie funkcja `pack_padded_sequence` wymaga, aby tensor długości sekwencji znajdował się na urządzeniu CPU, dlatego funkcja treningowa musi unikać przenoszenia danych długości sekwencji na GPU podczas treningu. Możesz zajrzeć do implementacji funkcji `train_emb` w pliku [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Dwukierunkowe i wielowarstwowe sieci RNN\n",
|
|
"\n",
|
|
"W naszych przykładach wszystkie sieci rekurencyjne działały w jednym kierunku, od początku sekwencji do jej końca. Wydaje się to naturalne, ponieważ przypomina sposób, w jaki czytamy i słuchamy mowy. Jednak w wielu praktycznych przypadkach mamy losowy dostęp do sekwencji wejściowej, więc może mieć sens przeprowadzenie obliczeń rekurencyjnych w obu kierunkach. Takie sieci nazywane są **dwukierunkowymi** RNN, a można je stworzyć, przekazując parametr `bidirectional=True` do konstruktora RNN/LSTM/GRU.\n",
|
|
"\n",
|
|
"Pracując z siecią dwukierunkową, potrzebujemy dwóch wektorów stanu ukrytego, po jednym dla każdego kierunku. PyTorch koduje te wektory jako jeden wektor o dwukrotnie większym rozmiarze, co jest bardzo wygodne, ponieważ zazwyczaj przekazujemy wynikowy stan ukryty do w pełni połączonej warstwy liniowej i wystarczy uwzględnić ten wzrost rozmiaru podczas tworzenia warstwy.\n",
|
|
"\n",
|
|
"Sieć rekurencyjna, jedno- lub dwukierunkowa, wychwytuje określone wzorce w sekwencji i może je przechowywać w wektorze stanu lub przekazywać do wyjścia. Podobnie jak w przypadku sieci konwolucyjnych, możemy zbudować kolejną warstwę rekurencyjną na szczycie pierwszej, aby wychwycić wzorce wyższego poziomu, zbudowane z wzorców niskiego poziomu wyodrębnionych przez pierwszą warstwę. Prowadzi to do pojęcia **wielowarstwowej RNN**, która składa się z dwóch lub więcej sieci rekurencyjnych, gdzie wyjście poprzedniej warstwy jest przekazywane jako wejście do następnej warstwy.\n",
|
|
"\n",
|
|
"\n",
|
|
"\n",
|
|
"*Obrazek pochodzi z [tego wspaniałego artykułu](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) autorstwa Fernando Lópeza*\n",
|
|
"\n",
|
|
"PyTorch ułatwia konstruowanie takich sieci, ponieważ wystarczy przekazać parametr `num_layers` do konstruktora RNN/LSTM/GRU, aby automatycznie zbudować kilka warstw rekurencyjnych. Oznacza to również, że rozmiar wektora ukrytego/stanu zwiększy się proporcjonalnie, co należy uwzględnić podczas obsługi wyjścia warstw rekurencyjnych.\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## RNN-y do innych zadań\n",
|
|
"\n",
|
|
"W tej jednostce widzieliśmy, że RNN-y mogą być używane do klasyfikacji sekwencji, ale w rzeczywistości mogą obsługiwać wiele innych zadań, takich jak generowanie tekstu, tłumaczenie maszynowe i inne. Zajmiemy się tymi zadaniami w następnej jednostce.\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"\n---\n\n**Zastrzeżenie**: \nTen dokument został przetłumaczony za pomocą usługi tłumaczenia AI [Co-op Translator](https://github.com/Azure/co-op-translator). Chociaż dokładamy wszelkich starań, aby tłumaczenie było precyzyjne, prosimy pamiętać, że automatyczne tłumaczenia mogą zawierać błędy lub nieścisłości. Oryginalny dokument w jego rodzimym języku powinien być uznawany za wiarygodne źródło. W przypadku informacji o kluczowym znaczeniu zaleca się skorzystanie z profesjonalnego tłumaczenia przez człowieka. Nie ponosimy odpowiedzialności za jakiekolwiek nieporozumienia lub błędne interpretacje wynikające z użycia tego tłumaczenia.\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-31T14:01:35+00:00",
|
|
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
|
|
"language_code": "pl"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 2
|
|
} |