726 lines
37 KiB
Plaintext
726 lines
37 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Вграждания\n",
|
||
"\n",
|
||
"В предишния пример работихме с високодименсионални вектори на чанта от думи с дължина `vocab_size`, като изрично преобразувахме от нискодименсионални вектори на позиционно представяне в разредено едноразрядно представяне. Това едноразрядно представяне не е ефективно по отношение на паметта, освен това всяка дума се третира независимо от останалите, т.е. едноразрядно кодираните вектори не изразяват никаква семантична прилика между думите.\n",
|
||
"\n",
|
||
"В този модул ще продължим да изследваме набора от данни **News AG**. За начало, нека заредим данните и вземем някои дефиниции от предишния тефтер.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Loading dataset...\n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stderr",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"d:\\WORK\\ai-for-beginners\\5-NLP\\14-Embeddings\\data\\train.csv: 29.5MB [00:01, 18.8MB/s] \n",
|
||
"d:\\WORK\\ai-for-beginners\\5-NLP\\14-Embeddings\\data\\test.csv: 1.86MB [00:00, 11.2MB/s] \n"
|
||
]
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Building vocab...\n",
|
||
"Vocab size = 95812\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"import torch\n",
|
||
"import torchtext\n",
|
||
"import numpy as np\n",
|
||
"from torchnlp import *\n",
|
||
"train_dataset, test_dataset, classes, vocab = load_dataset()\n",
|
||
"vocab_size = len(vocab)\n",
|
||
"print(\"Vocab size = \",vocab_size)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Какво е вграждане?\n",
|
||
"\n",
|
||
"Идеята на **вграждането** е да представим думите чрез плътни вектори с по-ниска размерност, които по някакъв начин отразяват семантичното значение на думата. По-късно ще обсъдим как да създаваме смислени вграждания на думи, но засега нека просто мислим за вграждането като начин за намаляване на размерността на векторите на думите.\n",
|
||
"\n",
|
||
"Така че, слой за вграждане ще приема дума като вход и ще произвежда изходен вектор със зададен `embedding_size`. В известен смисъл, той е много подобен на слоя `Linear`, но вместо да приема вектор с едно горещо кодиране, ще може да приема номер на дума като вход.\n",
|
||
"\n",
|
||
"Използвайки слой за вграждане като първи слой в нашата мрежа, можем да преминем от модел на чанта с думи към модел на **чанта с вграждания**, където първо преобразуваме всяка дума в текста в съответното вграждане, а след това изчисляваме някаква агрегатна функция върху всички тези вграждания, като например `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",
|
||
"В резултат на тази архитектура, минипартидите за нашата мрежа трябва да бъдат създадени по определен начин. В предишния модул, когато използвахме bag-of-words, всички BoW тензори в една минипартида имаха еднакъв размер `vocab_size`, независимо от действителната дължина на текстовата последователност. След като преминем към word embeddings, ще се окажем с променлив брой думи във всяка текстова извадка, и при комбинирането на тези извадки в минипартиди ще трябва да приложим известно запълване.\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",
|
||
"Сега, след като сме дефинирали подходящия dataloader, можем да обучим модела, използвайки функцията за обучение, която дефинирахме в предишния модул:\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",
|
||
"В предишната архитектура трябваше да запълним всички последователности до еднаква дължина, за да ги включим в минипартида. Това не е най-ефективният начин за представяне на последователности с променлива дължина - друг подход би бил използването на **вектор на отместванията**, който съдържа отместванията на всички последователности, съхранени в един голям вектор.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"> **Note**: На изображението по-горе е показана последователност от символи, но в нашия пример работим с последователности от думи. Въпреки това, основният принцип на представяне на последователности с вектор на отмествания остава същият.\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) — при тази архитектура обучаваме модела да предсказва дума въз основа на заобикалящия контекст. Даден е n-грам $(W_{-2},W_{-1},W_0,W_1,W_2)$, целта на модела е да предскаже $W_0$ от $(W_{-2},W_{-1},W_1,W_2)$.\n",
|
||
" - **Непрекъснат скип-грам** е противоположен на CBoW. Моделът използва заобикалящия прозорец от контекстни думи, за да предскаже текущата дума.\n",
|
||
"\n",
|
||
"CBoW е по-бърз, докато скип-грам е по-бавен, но се справя по-добре с представянето на редки думи.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"За да експериментираме с вграждания word2vec, предварително обучени върху набора от данни Google News, можем да използваме библиотеката **gensim**. По-долу намираме думите, които са най-близки до 'neural'.\n",
|
||
"\n",
|
||
"> **Note:** Когато за първи път създавате векторни представяния на думи, изтеглянето им може да отнеме известно време!\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import gensim.downloader as api\n",
|
||
"w2v = api.load('word2vec-google-news-300')"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"neuronal -> 0.7804799675941467\n",
|
||
"neurons -> 0.7326500415802002\n",
|
||
"neural_circuits -> 0.7252851724624634\n",
|
||
"neuron -> 0.7174385190010071\n",
|
||
"cortical -> 0.6941086649894714\n",
|
||
"brain_circuitry -> 0.6923246383666992\n",
|
||
"synaptic -> 0.6699118614196777\n",
|
||
"neural_circuitry -> 0.6638563275337219\n",
|
||
"neurochemical -> 0.6555314064025879\n",
|
||
"neuronal_activity -> 0.6531826257705688\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"for w,p in w2v.most_similar('neural'):\n",
|
||
" print(f\"{w} -> {p}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Можем също така да изчислим векторни вграждания от думата, които да се използват при обучението на класификационен модел (показваме само първите 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 са „предсказателни“ вграждания, тъй като вземат предвид само локалните контексти. Word2Vec не използва глобалния контекст.\n",
|
||
"\n",
|
||
"**FastText** надгражда Word2Vec, като научава векторни представяния за всяка дума и за символните n-грамове, които се намират в думата. Стойностите на представянията се осредняват в един вектор на всяка стъпка от обучението. Въпреки че това добавя значително допълнително изчисление към предварителното обучение, то позволява на вгражданията на думи да кодират информация за поддуми.\n",
|
||
"\n",
|
||
"Друг метод, **GloVe**, използва идеята за матрица на съвместна поява и прилага невронни методи за разлагане на матрицата на съвместна поява в по-изразителни и нелинейни векторни представяния на думи.\n",
|
||
"\n",
|
||
"Можете да експериментирате с примера, като промените вгражданията на 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",
|
||
"* Зареждане на нашия набор от данни с речника от предварително обучен word2vec модел. Речникът, използван за зареждане на набора от данни, може да бъде зададен по време на зареждането. \n",
|
||
"\n",
|
||
"Вторият подход изглежда по-лесен, особено защото PyTorch `torchtext` рамката съдържа вградена поддръжка за вграждания. Можем, например, да създадем речник, базиран на 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-30T01:09:08+00:00",
|
||
"source_file": "lessons/5-NLP/14-Embeddings/EmbeddingsPyTorch.ipynb",
|
||
"language_code": "bg"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
} |