725 lines
29 KiB
Plaintext
725 lines
29 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 임베딩\n",
|
|
"\n",
|
|
"이전 예제에서는 길이가 `vocab_size`인 고차원 bag-of-words 벡터를 사용했으며, 저차원 위치 표현 벡터를 희소한 원-핫 표현으로 명시적으로 변환했습니다. 하지만 이 원-핫 표현은 메모리 효율적이지 않을 뿐만 아니라, 각 단어가 서로 독립적으로 처리됩니다. 즉, 원-핫 인코딩된 벡터는 단어 간의 의미적 유사성을 전혀 표현하지 못합니다.\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",
|
|
"**임베딩(embedding)**의 개념은 단어를 낮은 차원의 밀집 벡터로 표현하는 것입니다. 이 벡터는 단어의 의미를 어느 정도 반영합니다. 나중에 의미 있는 단어 임베딩을 만드는 방법에 대해 논의하겠지만, 지금은 임베딩을 단어 벡터의 차원을 줄이는 방법으로 생각해 봅시다.\n",
|
|
"\n",
|
|
"임베딩 레이어는 단어를 입력으로 받아 지정된 `embedding_size` 크기의 출력 벡터를 생성합니다. 어떤 면에서는 `Linear` 레이어와 매우 유사하지만, 원-핫 인코딩된 벡터를 입력으로 받는 대신 단어 번호를 입력으로 받을 수 있습니다.\n",
|
|
"\n",
|
|
"네트워크의 첫 번째 레이어로 임베딩 레이어를 사용하면, 우리가 사용하는 모델을 **임베딩 백(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`로 동일한 크기를 가졌습니다. 하지만 단어 임베딩으로 전환하면, 각 텍스트 샘플에 포함된 단어 수가 달라지게 됩니다. 이러한 샘플들을 미니배치로 결합할 때는 패딩을 적용해야 합니다.\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,000개의 레코드(전체 에포크보다 적음)만 학습하지만, 여러 에포크 동안 학습을 계속하고 학습률 매개변수를 실험하여 더 높은 정확도를 달성할 수 있습니다. 약 90%의 정확도에 도달할 수 있어야 합니다.\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### EmbeddingBag 레이어와 가변 길이 시퀀스 표현\n",
|
|
"\n",
|
|
"이전 아키텍처에서는 미니배치에 맞추기 위해 모든 시퀀스를 동일한 길이로 패딩해야 했습니다. 이는 가변 길이 시퀀스를 표현하는 가장 효율적인 방법은 아닙니다. 다른 접근법으로는 **offset** 벡터를 사용하는 것이 있습니다. 이 벡터는 하나의 큰 벡터에 저장된 모든 시퀀스의 오프셋을 포함합니다.\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": [
|
|
"이전에 나온 모든 예제와 달리, 이제 우리의 네트워크는 서로 다른 크기의 데이터 벡터와 오프셋 벡터라는 두 개의 매개변수를 받습니다. 마찬가지로, 우리의 데이터 로더도 2개 대신 3개의 값을 제공합니다: 텍스트와 오프셋 벡터가 모두 특징으로 제공됩니다. 따라서, 이를 처리하기 위해 우리의 훈련 함수를 약간 조정해야 합니다:\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",
|
|
" - **연속적 Bag-of-Words** (CBoW) — 이 아키텍처에서는 주변 문맥으로부터 단어를 예측하도록 모델을 학습시킵니다. n그램 $(W_{-2},W_{-1},W_0,W_1,W_2)$가 주어졌을 때, 모델의 목표는 $(W_{-2},W_{-1},W_1,W_2)$로부터 $W_0$를 예측하는 것입니다.\n",
|
|
" - **연속적 Skip-Gram** — CBoW와 반대입니다. 이 모델은 현재 단어를 예측하기 위해 주변 문맥 단어의 윈도우를 사용합니다.\n",
|
|
"\n",
|
|
"CBoW는 더 빠르지만, Skip-Gram은 더 느리며 드문 단어를 표현하는 데 더 효과적입니다.\n",
|
|
"\n",
|
|
"\n",
|
|
"\n",
|
|
"Google News 데이터셋에서 사전 학습된 Word2Vec 임베딩을 실험하려면 **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": [
|
|
"문장의 임베딩의 훌륭한 점은 벡터 인코딩을 조작하여 의미를 변경할 수 있다는 것입니다. 예를 들어, 우리는 *king*과 *woman*이라는 단어에 최대한 가까우면서 *man*이라는 단어와는 최대한 멀리 떨어진 벡터 표현을 가진 단어를 찾을 수 있습니다:\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**는 공기행렬(co-occurrence matrix)의 아이디어를 활용하며, 공기행렬을 더 표현력 있고 비선형적인 단어 벡터로 분해하기 위해 신경망 방법을 사용합니다.\n",
|
|
"\n",
|
|
"gensim은 여러 가지 단어 임베딩 모델을 지원하므로, FastText와 GloVe로 임베딩을 변경하여 예제를 실험해볼 수 있습니다.\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` 프레임워크가 임베딩에 대한 내장 지원을 포함하고 있기 때문입니다. \n",
|
|
"예를 들어, 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이 문서는 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-31T14:06:05+00:00",
|
|
"source_file": "lessons/5-NLP/14-Embeddings/EmbeddingsPyTorch.ipynb",
|
|
"language_code": "ko"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 2
|
|
} |