724 lines
37 KiB
Plaintext
724 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`, но вместо того, чтобы принимать вектор в формате one-hot, он может принимать номер слова в качестве входных данных.\n",
|
||
"\n",
|
||
"Используя слой эмбеддинга в качестве первого слоя нашей сети, мы можем перейти от модели bag-of-words к модели **embedding bag**, где мы сначала преобразуем каждое слово в тексте в соответствующий эмбеддинг, а затем вычисляем некоторую агрегирующую функцию для всех этих эмбеддингов, например, `sum`, `average` или `max`.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"Наша нейронная сеть-классификатор начнется со слоя эмбеддинга, затем слоя агрегации и линейного классификатора поверх него:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class EmbedClassifier(torch.nn.Module):\n",
|
||
" def __init__(self, vocab_size, embed_dim, num_class):\n",
|
||
" super().__init__()\n",
|
||
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
|
||
" self.fc = torch.nn.Linear(embed_dim, num_class)\n",
|
||
"\n",
|
||
" def forward(self, x):\n",
|
||
" x = self.embedding(x)\n",
|
||
" x = torch.mean(x,dim=1)\n",
|
||
" return self.fc(x)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Работа с переменной длиной последовательностей\n",
|
||
"\n",
|
||
"Из-за особенностей этой архитектуры, минибатчи для нашей сети нужно будет формировать определённым образом. В предыдущем разделе, при использовании мешка слов (bag-of-words), все тензоры BoW в минибатче имели одинаковый размер `vocab_size`, независимо от фактической длины текстовой последовательности. Однако при переходе к словарным эмбеддингам количество слов в каждом текстовом образце будет варьироваться, и при объединении этих образцов в минибатчи нам потребуется добавлять некоторую \"заполняющую\" информацию (padding).\n",
|
||
"\n",
|
||
"Это можно сделать с помощью той же техники, предоставляя функцию `collate_fn` источнику данных:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def padify(b):\n",
|
||
" # b is the list of tuples of length batch_size\n",
|
||
" # - first element of a tuple = label, \n",
|
||
" # - second = feature (text sequence)\n",
|
||
" # build vectorized sequence\n",
|
||
" v = [encode(x[1]) for x in b]\n",
|
||
" # first, compute max length of a sequence in this minibatch\n",
|
||
" l = max(map(len,v))\n",
|
||
" return ( # tuple of two tensors - labels and features\n",
|
||
" torch.LongTensor([t[0]-1 for t in b]),\n",
|
||
" torch.stack([torch.nn.functional.pad(torch.tensor(t),(0,l-len(t)),mode='constant',value=0) for t in v])\n",
|
||
" )\n",
|
||
"\n",
|
||
"train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Обучение классификатора эмбеддингов\n",
|
||
"\n",
|
||
"Теперь, когда мы определили подходящий загрузчик данных, мы можем обучить модель, используя функцию обучения, которую мы определили в предыдущем разделе:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"3200: acc=0.6415625\n",
|
||
"6400: acc=0.6865625\n",
|
||
"9600: acc=0.7103125\n",
|
||
"12800: acc=0.726953125\n",
|
||
"16000: acc=0.739375\n",
|
||
"19200: acc=0.75046875\n",
|
||
"22400: acc=0.7572321428571429\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"(0.889799795315499, 0.7623160588611644)"
|
||
]
|
||
},
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"net = EmbedClassifier(vocab_size,32,len(classes)).to(device)\n",
|
||
"train_epoch(net,train_loader, lr=1, epoch_size=25000)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"> **Примечание**: Здесь мы обучаемся только на 25 тысячах записей (меньше, чем один полный эпох), чтобы сэкономить время, но вы можете продолжить обучение, написать функцию для обучения на нескольких эпохах и поэкспериментировать с параметром скорости обучения, чтобы достичь более высокой точности. Вы должны быть в состоянии достичь точности около 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",
|
||
" - **Непрерывный мешок слов** (Continuous bag-of-words, CBoW) — в этой архитектуре модель обучается предсказывать слово по окружающему контексту. Для данного n-грамма $(W_{-2},W_{-1},W_0,W_1,W_2)$ цель модели — предсказать $W_0$ на основе $(W_{-2},W_{-1},W_1,W_2)$.\n",
|
||
" - **Непрерывный skip-gram** — противоположность CBoW. Модель использует окружающее окно контекстных слов для предсказания текущего слова.\n",
|
||
"\n",
|
||
"CBoW работает быстрее, тогда как skip-gram медленнее, но лучше справляется с представлением редких слов.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"Чтобы поэкспериментировать с эмбеддингами Word2Vec, предварительно обученными на наборе данных Google News, мы можем использовать библиотеку **gensim**. Ниже приведён пример поиска слов, наиболее похожих на 'neural'.\n",
|
||
"\n",
|
||
"> **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": []
|
||
},
|
||
{
|
||
"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Этот документ был переведен с помощью сервиса автоматического перевода [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-28T12:28:48+00:00",
|
||
"source_file": "lessons/5-NLP/14-Embeddings/EmbeddingsPyTorch.ipynb",
|
||
"language_code": "ru"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
} |