724 lines
28 KiB
Plaintext
724 lines
28 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Osadzenia\n",
|
||
"\n",
|
||
"W naszym poprzednim przykładzie operowaliśmy na wysokowymiarowych wektorach typu bag-of-words o długości `vocab_size`, a także jawnie konwertowaliśmy niskowymiarowe wektory reprezentacji pozycyjnej na rzadkie reprezentacje typu one-hot. Taka reprezentacja one-hot nie jest efektywna pod względem pamięci, a dodatkowo każde słowo jest traktowane niezależnie od innych, tzn. zakodowane wektory one-hot nie wyrażają żadnego semantycznego podobieństwa między słowami.\n",
|
||
"\n",
|
||
"W tej jednostce będziemy kontynuować eksplorację zbioru danych **News AG**. Na początek załadujmy dane i pobierzmy kilka definicji z poprzedniego notatnika.\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": [
|
||
"## Czym jest embedding?\n",
|
||
"\n",
|
||
"Idea **embeddingu** polega na reprezentowaniu słów za pomocą gęstych wektorów o niższej wymiarowości, które w pewien sposób odzwierciedlają semantyczne znaczenie słowa. Później omówimy, jak tworzyć znaczące embeddingi słów, ale na razie potraktujmy embeddingi jako sposób na zmniejszenie wymiarowości wektora słowa.\n",
|
||
"\n",
|
||
"Warstwa embeddingu przyjmuje słowo jako dane wejściowe i generuje wektor wyjściowy o określonym `embedding_size`. W pewnym sensie jest to bardzo podobne do warstwy `Linear`, ale zamiast przyjmować wektor zakodowany w formie one-hot, może przyjąć numer słowa jako dane wejściowe.\n",
|
||
"\n",
|
||
"Używając warstwy embeddingu jako pierwszej warstwy w naszej sieci, możemy przejść od modelu bag-of-words do modelu **embedding bag**, w którym najpierw konwertujemy każde słowo w naszym tekście na odpowiadający mu embedding, a następnie obliczamy jakąś funkcję agregującą dla wszystkich tych embeddingów, taką jak `sum`, `average` lub `max`.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"Nasza sieć neuronowa klasyfikatora rozpocznie się od warstwy embeddingu, następnie warstwy agregacji, a na końcu liniowego klasyfikatora:\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": [
|
||
"### Radzenie sobie z różną długością sekwencji zmiennych\n",
|
||
"\n",
|
||
"W wyniku tej architektury, minibatche dla naszej sieci muszą być tworzone w określony sposób. W poprzedniej części, podczas korzystania z bag-of-words, wszystkie tensory BoW w minibatchu miały jednakowy rozmiar `vocab_size`, niezależnie od rzeczywistej długości sekwencji tekstowej. Gdy przechodzimy do osadzania słów (word embeddings), kończymy z różną liczbą słów w każdej próbce tekstowej, a podczas łączenia tych próbek w minibatche musimy zastosować pewne wypełnienie (padding).\n",
|
||
"\n",
|
||
"Można to zrobić, stosując tę samą technikę, polegającą na dostarczeniu funkcji `collate_fn` do źródła danych:\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": [
|
||
"### Trenowanie klasyfikatora osadzeń\n",
|
||
"\n",
|
||
"Teraz, gdy zdefiniowaliśmy odpowiedni ładowacz danych, możemy wytrenować model, korzystając z funkcji treningowej, którą zdefiniowaliśmy w poprzedniej jednostce:\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": [
|
||
"> **Uwaga**: Trenujemy tutaj tylko na 25 tysiącach rekordów (mniej niż jedna pełna epoka) ze względu na czas, ale możesz kontynuować trening, napisać funkcję do trenowania przez kilka epok i eksperymentować z parametrem szybkości uczenia, aby osiągnąć wyższą dokładność. Powinieneś być w stanie osiągnąć dokładność na poziomie około 90%.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Warstwa EmbeddingBag i reprezentacja sekwencji o zmiennej długości\n",
|
||
"\n",
|
||
"W poprzedniej architekturze musieliśmy uzupełniać wszystkie sekwencje do tej samej długości, aby dopasować je do minibatcha. Nie jest to jednak najbardziej efektywny sposób reprezentowania sekwencji o zmiennej długości – innym podejściem byłoby użycie wektora **offset**, który przechowuje przesunięcia wszystkich sekwencji zapisanych w jednym dużym wektorze.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"> **Note**: Na powyższym obrazku pokazano sekwencję znaków, ale w naszym przykładzie pracujemy z sekwencjami słów. Jednak ogólna zasada reprezentowania sekwencji za pomocą wektora przesunięć pozostaje taka sama.\n",
|
||
"\n",
|
||
"Aby pracować z reprezentacją przesunięć, używamy warstwy [`EmbeddingBag`](https://pytorch.org/docs/stable/generated/torch.nn.EmbeddingBag.html). Jest ona podobna do `Embedding`, ale przyjmuje jako wejście wektor zawartości oraz wektor przesunięć, a także zawiera warstwę uśredniającą, która może być `mean`, `sum` lub `max`.\n",
|
||
"\n",
|
||
"Oto zmodyfikowana sieć wykorzystująca `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": [
|
||
"Aby przygotować zestaw danych do treningu, musimy dostarczyć funkcję konwersji, która przygotuje wektor przesunięcia:\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": [
|
||
"Należy zauważyć, że w przeciwieństwie do wszystkich poprzednich przykładów, nasza sieć teraz akceptuje dwa parametry: wektor danych i wektor przesunięcia, które mają różne rozmiary. Podobnie, nasz ładowacz danych dostarcza nam 3 wartości zamiast 2: zarówno wektory tekstowe, jak i przesunięcia są dostarczane jako cechy. Dlatego musimy nieco dostosować naszą funkcję treningową, aby to uwzględnić:\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": [
|
||
"## Semantyczne osadzenia: Word2Vec\n",
|
||
"\n",
|
||
"W naszym poprzednim przykładzie warstwa osadzenia modelu nauczyła się mapować słowa na reprezentacje wektorowe, jednak ta reprezentacja nie miała dużego znaczenia semantycznego. Byłoby dobrze nauczyć się takiej reprezentacji wektorowej, w której podobne słowa lub synonimy odpowiadałyby wektorom bliskim sobie pod względem jakiejś odległości wektorowej (np. odległości euklidesowej).\n",
|
||
"\n",
|
||
"Aby to osiągnąć, musimy wstępnie wytrenować nasz model osadzenia na dużym zbiorze tekstów w specyficzny sposób. Jednym z pierwszych sposobów trenowania semantycznych osadzeń jest metoda [Word2Vec](https://en.wikipedia.org/wiki/Word2vec). Opiera się ona na dwóch głównych architekturach, które są używane do tworzenia rozproszonej reprezentacji słów:\n",
|
||
"\n",
|
||
" - **Ciągły worek słów** (CBoW) — w tej architekturze trenujemy model, aby przewidywał słowo na podstawie otaczającego kontekstu. Mając n-gram $(W_{-2},W_{-1},W_0,W_1,W_2)$, celem modelu jest przewidzenie $W_0$ na podstawie $(W_{-2},W_{-1},W_1,W_2)$.\n",
|
||
" - **Ciągły skip-gram** jest przeciwieństwem CBoW. Model wykorzystuje otaczające okno słów kontekstowych, aby przewidzieć bieżące słowo.\n",
|
||
"\n",
|
||
"CBoW działa szybciej, podczas gdy skip-gram jest wolniejszy, ale lepiej radzi sobie z reprezentacją rzadkich słów.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"Aby eksperymentować z osadzeniem word2vec wstępnie wytrenowanym na zbiorze danych Google News, możemy użyć biblioteki **gensim**. Poniżej znajdujemy słowa najbardziej podobne do 'neural'.\n",
|
||
"\n",
|
||
"> **Note:** Przy pierwszym tworzeniu wektorów słów ich pobieranie może zająć trochę czasu!\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": [
|
||
"Możemy również obliczyć osadzenia wektorowe z wyrazu, które będą używane do trenowania modelu klasyfikacyjnego (pokazujemy tylko pierwsze 20 komponentów wektora dla przejrzystości):\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": [
|
||
"Wspaniałą cechą osadzeń semantycznych jest to, że można manipulować kodowaniem wektorowym, aby zmieniać semantykę. Na przykład możemy poprosić o znalezienie słowa, którego reprezentacja wektorowa byłaby jak najbliższa słowom *król* i *kobieta*, a jak najdalsza od słowa *mężczyzna*:\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": [
|
||
"Zarówno CBoW, jak i Skip-Grams to osadzenia „predykcyjne”, ponieważ uwzględniają jedynie lokalne konteksty. Word2Vec nie wykorzystuje globalnego kontekstu.\n",
|
||
"\n",
|
||
"**FastText** rozwija Word2Vec, ucząc się reprezentacji wektorowych dla każdego słowa oraz n-gramów znakowych zawartych w każdym słowie. Wartości tych reprezentacji są następnie uśredniane do jednego wektora na każdym kroku treningu. Chociaż dodaje to dużo dodatkowych obliczeń podczas wstępnego treningu, umożliwia osadzeniom słów kodowanie informacji o pod-słowach.\n",
|
||
"\n",
|
||
"Inna metoda, **GloVe**, opiera się na idei macierzy współwystępowania i wykorzystuje metody neuronowe do dekompozycji macierzy współwystępowania na bardziej ekspresyjne i nieliniowe wektory słów.\n",
|
||
"\n",
|
||
"Możesz eksperymentować, zmieniając osadzenia na FastText i GloVe, ponieważ gensim obsługuje kilka różnych modeli osadzeń słów.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Korzystanie z wstępnie wytrenowanych osadzeń w PyTorch\n",
|
||
"\n",
|
||
"Możemy zmodyfikować powyższy przykład, aby wstępnie wypełnić macierz w naszej warstwie osadzeń semantycznymi osadzeniami, takimi jak Word2Vec. Musimy jednak pamiętać, że słowniki wstępnie wytrenowanych osadzeń i naszego korpusu tekstowego prawdopodobnie nie będą się pokrywać, więc zainicjalizujemy wagi dla brakujących słów losowymi wartościami:\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": [
|
||
"Teraz przejdźmy do trenowania naszego modelu. Zauważ, że czas potrzebny na trenowanie modelu jest znacznie dłuższy niż w poprzednim przykładzie, ze względu na większy rozmiar warstwy osadzania, a co za tym idzie, znacznie większą liczbę parametrów. Ponadto, z tego powodu możemy potrzebować trenować nasz model na większej liczbie przykładów, jeśli chcemy uniknąć przeuczenia.\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": [
|
||
"W naszym przypadku nie zauważamy dużego wzrostu dokładności, co prawdopodobnie wynika z dość różnych słownictw. \n",
|
||
"Aby rozwiązać problem różnic w słownictwie, możemy zastosować jedno z następujących rozwiązań: \n",
|
||
"* Ponowne wytrenowanie modelu word2vec na naszym słownictwie \n",
|
||
"* Załadowanie naszego zbioru danych z użyciem słownictwa z wstępnie wytrenowanego modelu word2vec. Słownictwo używane do załadowania zbioru danych można określić podczas ładowania. \n",
|
||
"\n",
|
||
"Drugie podejście wydaje się łatwiejsze, zwłaszcza że framework `torchtext` w PyTorch zawiera wbudowaną obsługę osadzeń. Możemy na przykład utworzyć słownictwo oparte na GloVe w następujący sposób: \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": [
|
||
"Załadowany słownik posiada następujące podstawowe operacje:\n",
|
||
"* Słownik `vocab.stoi` pozwala nam zamienić słowo na jego indeks w słowniku\n",
|
||
"* `vocab.itos` działa odwrotnie - zamienia liczbę na słowo\n",
|
||
"* `vocab.vectors` to tablica wektorów osadzeń, więc aby uzyskać osadzenie dla słowa `s`, musimy użyć `vocab.vectors[vocab.stoi[s]]`\n",
|
||
"\n",
|
||
"Oto przykład manipulacji osadzeniami, aby zademonstrować równanie **kind-man+woman = queen** (musiałem nieco dostosować współczynnik, aby to zadziałało):\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": [
|
||
"Aby wytrenować klasyfikator za pomocą tych osadzeń, najpierw musimy zakodować nasz zbiór danych za pomocą słownictwa 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": [
|
||
"Jak widzieliśmy powyżej, wszystkie osadzenia wektorów są przechowywane w macierzy `vocab.vectors`. Dzięki temu niezwykle łatwo jest załadować te wagi do wag warstwy osadzenia, używając prostego kopiowania:\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": []
|
||
},
|
||
{
|
||
"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": [
|
||
"Jednym z powodów, dla których nie obserwujemy znaczącego wzrostu dokładności, jest fakt, że niektóre słowa z naszego zbioru danych są nieobecne w wstępnie wytrenowanym słowniku GloVe, a zatem są zasadniczo ignorowane. Aby przezwyciężyć ten fakt, możemy wytrenować własne osadzenia na naszym zbiorze danych.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Kontekstowe osadzenia\n",
|
||
"\n",
|
||
"Jednym z kluczowych ograniczeń tradycyjnych wstępnie wytrenowanych reprezentacji osadzeń, takich jak Word2Vec, jest problem rozróżniania znaczeń słów. Chociaż wstępnie wytrenowane osadzenia mogą uchwycić część znaczenia słów w kontekście, każde możliwe znaczenie danego słowa jest kodowane w tym samym osadzeniu. Może to powodować problemy w modelach wykorzystujących te osadzenia, ponieważ wiele słów, takich jak słowo „grać”, ma różne znaczenia w zależności od kontekstu, w którym są używane.\n",
|
||
"\n",
|
||
"Na przykład słowo „grać” w tych dwóch zdaniach ma zupełnie inne znaczenie:\n",
|
||
"- Poszedłem na **sztukę** do teatru.\n",
|
||
"- Jan chce **grać** ze swoimi przyjaciółmi.\n",
|
||
"\n",
|
||
"Wstępnie wytrenowane osadzenia powyżej reprezentują oba te znaczenia słowa „grać” w tym samym osadzeniu. Aby przezwyciężyć to ograniczenie, musimy budować osadzenia oparte na **modelu językowym**, który jest wytrenowany na dużym korpusie tekstu i *wie*, jak słowa mogą być zestawiane w różnych kontekstach. Omówienie kontekstowych osadzeń wykracza poza zakres tego samouczka, ale wrócimy do tego tematu, gdy będziemy omawiać modele językowe 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 autorytatywne ź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": "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-31T14:08:16+00:00",
|
||
"source_file": "lessons/5-NLP/14-Embeddings/EmbeddingsPyTorch.ipynb",
|
||
"language_code": "pl"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
} |