AI-For-Beginners/translations/ko/lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.i...

577 lines
22 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 텍스트 분류 작업\n",
"\n",
"앞서 언급했듯이, 우리는 **AG_NEWS** 데이터셋을 기반으로 간단한 텍스트 분류 작업에 집중할 것입니다. 이 작업은 뉴스 헤드라인을 세계, 스포츠, 비즈니스, 과학/기술의 4가지 카테고리 중 하나로 분류하는 것입니다.\n",
"\n",
"## 데이터셋\n",
"\n",
"이 데이터셋은 [`torchtext`](https://github.com/pytorch/text) 모듈에 내장되어 있어 쉽게 접근할 수 있습니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"import torchtext\n",
"import os\n",
"import collections\n",
"os.makedirs('./data',exist_ok=True)\n",
"train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')\n",
"classes = ['World', 'Sports', 'Business', 'Sci/Tech']"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"여기서 `train_dataset`와 `test_dataset`은 각각 클래스 번호(레이블)와 텍스트 쌍을 반환하는 컬렉션을 포함합니다. 예를 들어:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(3,\n",
" \"Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\\\band of ultra-cynics, are seeing green again.\")"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"list(train_dataset)[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"그래서, 우리의 데이터셋에서 새로운 헤드라인 10개를 출력해 봅시다:\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"**Sci/Tech** -> Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\band of ultra-cynics, are seeing green again.\n",
"**Sci/Tech** -> Carlyle Looks Toward Commercial Aerospace (Reuters) Reuters - Private investment firm Carlyle Group,\\which has a reputation for making well-timed and occasionally\\controversial plays in the defense industry, has quietly placed\\its bets on another part of the market.\n",
"**Sci/Tech** -> Oil and Economy Cloud Stocks' Outlook (Reuters) Reuters - Soaring crude prices plus worries\\about the economy and the outlook for earnings are expected to\\hang over the stock market next week during the depth of the\\summer doldrums.\n",
"**Sci/Tech** -> Iraq Halts Oil Exports from Main Southern Pipeline (Reuters) Reuters - Authorities have halted oil export\\flows from the main pipeline in southern Iraq after\\intelligence showed a rebel militia could strike\\infrastructure, an oil official said on Saturday.\n",
"**Sci/Tech** -> Oil prices soar to all-time record, posing new menace to US economy (AFP) AFP - Tearaway world oil prices, toppling records and straining wallets, present a new economic menace barely three months before the US presidential elections.\n"
]
}
],
"source": [
"for i,x in zip(range(5),train_dataset):\n",
" print(f\"**{classes[x[0]]}** -> {x[1]}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"데이터셋은 반복자이기 때문에 데이터를 여러 번 사용하려면 이를 리스트로 변환해야 합니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')\n",
"train_dataset = list(train_dataset)\n",
"test_dataset = list(test_dataset)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 토크나이제이션\n",
"\n",
"이제 텍스트를 **숫자**로 변환하여 텐서로 표현해야 합니다. 단어 수준의 표현을 원한다면, 두 가지 작업이 필요합니다:\n",
"* 텍스트를 **토큰**으로 나누기 위해 **토크나이저**를 사용합니다.\n",
"* 이러한 토큰의 **어휘**를 구축합니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['he', 'said', 'hello']"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tokenizer = torchtext.data.utils.get_tokenizer('basic_english')\n",
"tokenizer('He said: hello')"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"counter = collections.Counter()\n",
"for (label, line) in train_dataset:\n",
" counter.update(tokenizer(line))\n",
"vocab = torchtext.vocab.vocab(counter, min_freq=1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"어휘를 사용하여 토큰화된 문자열을 숫자 집합으로 쉽게 인코딩할 수 있습니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Vocab size if 95810\n"
]
},
{
"data": {
"text/plain": [
"[599, 3279, 97, 1220, 329, 225, 7368]"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"vocab_size = len(vocab)\n",
"print(f\"Vocab size if {vocab_size}\")\n",
"\n",
"stoi = vocab.get_stoi() # dict to convert tokens to indices\n",
"\n",
"def encode(x):\n",
" return [stoi[s] for s in tokenizer(x)]\n",
"\n",
"encode('I love to play with my words')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 단어 묶음(Bag of Words) 텍스트 표현\n",
"\n",
"단어는 의미를 나타내기 때문에, 때로는 문장에서 단어의 순서를 고려하지 않고 개별 단어만 살펴보아도 텍스트의 의미를 파악할 수 있습니다. 예를 들어, 뉴스를 분류할 때 *날씨*, *눈*과 같은 단어는 *일기예보*를 나타낼 가능성이 높고, *주식*, *달러*와 같은 단어는 *금융 뉴스*에 해당할 가능성이 있습니다.\n",
"\n",
"**단어 묶음**(BoW) 벡터 표현은 가장 일반적으로 사용되는 전통적인 벡터 표현 방식입니다. 각 단어는 벡터의 인덱스에 연결되며, 벡터 요소는 주어진 문서에서 해당 단어가 등장한 횟수를 포함합니다.\n",
"\n",
"![단어 묶음 벡터 표현이 메모리에서 어떻게 표현되는지를 보여주는 이미지.](../../../../../lessons/5-NLP/13-TextRep/images/bag-of-words-example.png) \n",
"\n",
"> **Note**: BoW를 텍스트 내 개별 단어에 대한 모든 원-핫 인코딩 벡터의 합으로 생각할 수도 있습니다.\n",
"\n",
"아래는 Scikit Learn 파이썬 라이브러리를 사용하여 단어 묶음 표현을 생성하는 방법의 예시입니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[1, 1, 0, 2, 0, 0, 0, 0, 0]], dtype=int64)"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from sklearn.feature_extraction.text import CountVectorizer\n",
"vectorizer = CountVectorizer()\n",
"corpus = [\n",
" 'I like hot dogs.',\n",
" 'The dog ran fast.',\n",
" 'Its hot outside.',\n",
" ]\n",
"vectorizer.fit_transform(corpus)\n",
"vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"AG_NEWS 데이터셋의 벡터 표현에서 bag-of-words 벡터를 계산하려면 다음 함수를 사용할 수 있습니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"tensor([2., 1., 2., ..., 0., 0., 0.])\n"
]
}
],
"source": [
"vocab_size = len(vocab)\n",
"\n",
"def to_bow(text,bow_vocab_size=vocab_size):\n",
" res = torch.zeros(bow_vocab_size,dtype=torch.float32)\n",
" for i in encode(text):\n",
" if i<bow_vocab_size:\n",
" res[i] += 1\n",
" return res\n",
"\n",
"print(to_bow(train_dataset[0][1]))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **참고:** 여기서는 기본 어휘 크기를 지정하기 위해 전역 변수 `vocab_size`를 사용합니다. 어휘 크기가 종종 매우 크기 때문에 가장 빈번한 단어로 어휘 크기를 제한할 수 있습니다. `vocab_size` 값을 낮추고 아래 코드를 실행하여 정확도에 어떤 영향을 미치는지 확인해 보세요. 약간의 정확도 저하를 예상할 수 있지만, 성능 향상을 위해 극적인 변화는 없을 것입니다.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## BoW 분류기 학습하기\n",
"\n",
"이제 텍스트의 Bag-of-Words 표현을 만드는 방법을 배웠으니, 이를 기반으로 분류기를 학습시켜 봅시다. 먼저, 학습을 위해 데이터셋을 변환해야 합니다. 모든 위치 벡터 표현을 Bag-of-Words 표현으로 변환해야 합니다. 이를 위해 표준 torch `DataLoader`의 `collate_fn` 매개변수에 `bowify` 함수를 전달하면 됩니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [],
"source": [
"from torch.utils.data import DataLoader\n",
"import numpy as np \n",
"\n",
"# this collate function gets list of batch_size tuples, and needs to \n",
"# return a pair of label-feature tensors for the whole minibatch\n",
"def bowify(b):\n",
" return (\n",
" torch.LongTensor([t[0]-1 for t in b]),\n",
" torch.stack([to_bow(t[1]) for t in b])\n",
" )\n",
"\n",
"train_loader = DataLoader(train_dataset, batch_size=16, collate_fn=bowify, shuffle=True)\n",
"test_loader = DataLoader(test_dataset, batch_size=16, collate_fn=bowify, shuffle=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"이제 하나의 선형 계층을 포함하는 간단한 분류기 신경망을 정의해봅시다. 입력 벡터의 크기는 `vocab_size`와 같고, 출력 크기는 클래스 수(4)에 해당합니다. 분류 작업을 해결하기 때문에 최종 활성화 함수는 `LogSoftmax()`입니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [],
"source": [
"net = torch.nn.Sequential(torch.nn.Linear(vocab_size,4),torch.nn.LogSoftmax(dim=1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"이제 표준 PyTorch 훈련 루프를 정의하겠습니다. 우리의 데이터셋이 상당히 크기 때문에, 교육 목적으로 우리는 한 에포크만 훈련할 것이며, 때로는 에포크보다 적게 훈련할 수도 있습니다 (`epoch_size` 매개변수를 지정하면 훈련을 제한할 수 있습니다). 또한 훈련 중 누적된 훈련 정확도를 보고할 것이며, 보고 빈도는 `report_freq` 매개변수를 사용하여 지정됩니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"def train_epoch(net,dataloader,lr=0.01,optimizer=None,loss_fn = torch.nn.NLLLoss(),epoch_size=None, report_freq=200):\n",
" optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)\n",
" net.train()\n",
" total_loss,acc,count,i = 0,0,0,0\n",
" for labels,features in dataloader:\n",
" optimizer.zero_grad()\n",
" out = net(features)\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"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.8028125\n",
"6400: acc=0.8371875\n",
"9600: acc=0.8534375\n",
"12800: acc=0.85765625\n"
]
},
{
"data": {
"text/plain": [
"(0.026090790722161722, 0.8620069296375267)"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"train_epoch(net,train_loader,epoch_size=15000)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## BiGrams, TriGrams 및 N-Grams\n",
"\n",
"Bag of words 접근법의 한 가지 한계는 일부 단어가 여러 단어로 이루어진 표현의 일부라는 점입니다. 예를 들어, 'hot dog'이라는 단어는 다른 문맥에서 'hot'과 'dog'이라는 단어와 완전히 다른 의미를 가집니다. 만약 'hot'과 'dog'을 항상 동일한 벡터로 표현한다면, 이는 모델을 혼란스럽게 할 수 있습니다.\n",
"\n",
"이를 해결하기 위해 **N-gram 표현**이 문서 분류 방법에서 자주 사용됩니다. 여기서 각 단어, 두 단어 또는 세 단어의 빈도는 분류기를 학습시키는 데 유용한 특징이 됩니다. 예를 들어, bigram 표현에서는 원래 단어 외에도 모든 단어 쌍을 어휘에 추가합니다.\n",
"\n",
"아래는 Scikit Learn을 사용하여 bigram bag of word 표현을 생성하는 방법의 예입니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Vocabulary:\n",
" {'i': 7, 'like': 11, 'hot': 4, 'dogs': 2, 'i like': 8, 'like hot': 12, 'hot dogs': 5, 'the': 16, 'dog': 0, 'ran': 14, 'fast': 3, 'the dog': 17, 'dog ran': 1, 'ran fast': 15, 'its': 9, 'outside': 13, 'its hot': 10, 'hot outside': 6}\n"
]
},
{
"data": {
"text/plain": [
"array([[1, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],\n",
" dtype=int64)"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bigram_vectorizer = CountVectorizer(ngram_range=(1, 2), token_pattern=r'\\b\\w+\\b', min_df=1)\n",
"corpus = [\n",
" 'I like hot dogs.',\n",
" 'The dog ran fast.',\n",
" 'Its hot outside.',\n",
" ]\n",
"bigram_vectorizer.fit_transform(corpus)\n",
"print(\"Vocabulary:\\n\",bigram_vectorizer.vocabulary_)\n",
"bigram_vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"N-gram 접근 방식의 주요 단점은 어휘 크기가 매우 빠르게 증가한다는 점입니다. 실제로는 *임베딩*과 같은 차원 축소 기법과 N-gram 표현을 결합해야 하며, 이는 다음 단원에서 다룰 예정입니다.\n",
"\n",
"**AG News** 데이터셋에서 N-gram 표현을 사용하려면, 특별한 ngram 어휘를 구축해야 합니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Bigram vocabulary length = 1308842\n"
]
}
],
"source": [
"counter = collections.Counter()\n",
"for (label, line) in train_dataset:\n",
" l = tokenizer(line)\n",
" counter.update(torchtext.data.utils.ngrams_iterator(l,ngrams=2))\n",
" \n",
"bi_vocab = torchtext.vocab.vocab(counter, min_freq=1)\n",
"\n",
"print(\"Bigram vocabulary length = \",len(bi_vocab))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"우리는 위의 코드와 동일한 코드를 사용하여 분류기를 훈련시킬 수 있지만, 이는 메모리 효율성이 매우 낮을 것입니다. 다음 단원에서는 임베딩을 사용하여 빅그램 분류기를 훈련시킬 것입니다.\n",
"\n",
"> **참고:** 텍스트에서 지정된 횟수 이상 발생하는 n그램만 남길 수 있습니다. 이렇게 하면 드문 빅그램이 제외되고 차원이 크게 감소합니다. 이를 위해 `min_freq` 매개변수를 더 높은 값으로 설정하고 어휘 길이의 변화를 관찰하세요.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 용어 빈도-역문서 빈도 (TF-IDF)\n",
"\n",
"BoW 표현에서는 단어 자체와 상관없이 단어의 출현이 동일한 가중치를 갖습니다. 하지만 *a*, *in* 같은 자주 등장하는 단어들은 전문 용어에 비해 분류 작업에서 훨씬 덜 중요하다는 것은 명백합니다. 실제로 대부분의 NLP 작업에서는 특정 단어들이 다른 단어들보다 더 중요합니다.\n",
"\n",
"**TF-IDF**는 **용어 빈도-역문서 빈도**를 의미합니다. 이는 단순히 문서 내 단어의 출현 여부를 0/1로 나타내는 BoW 방식과 달리, 단어 출현 빈도와 관련된 부동소수점 값을 사용하는 방식입니다.\n",
"\n",
"좀 더 공식적으로, 문서 $j$에서 단어 $i$의 가중치 $w_{ij}$는 다음과 같이 정의됩니다:\n",
"$$\n",
"w_{ij} = tf_{ij}\\times\\log({N\\over df_i})\n",
"$$\n",
"여기서\n",
"* $tf_{ij}$는 $j$에서 $i$가 등장한 횟수, 즉 이전에 본 BoW 값입니다.\n",
"* $N$은 컬렉션 내 문서의 총 개수입니다.\n",
"* $df_i$는 컬렉션 전체에서 단어 $i$를 포함하는 문서의 개수입니다.\n",
"\n",
"TF-IDF 값 $w_{ij}$는 단어가 문서에서 등장하는 횟수에 비례하여 증가하며, 해당 단어를 포함하는 말뭉치 내 문서 수에 따라 조정됩니다. 이는 일부 단어가 다른 단어보다 더 자주 등장하는 사실을 보정하는 데 도움을 줍니다. 예를 들어, 특정 단어가 컬렉션의 *모든* 문서에 등장한다면, $df_i=N$이 되고 $w_{ij}=0$이 되어, 해당 단어는 완전히 무시됩니다.\n",
"\n",
"Scikit Learn을 사용하면 텍스트의 TF-IDF 벡터화를 쉽게 생성할 수 있습니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[0.43381609, 0. , 0.43381609, 0. , 0.65985664,\n",
" 0.43381609, 0. , 0. , 0. , 0. ,\n",
" 0. , 0. , 0. , 0. , 0. ,\n",
" 0. ]])"
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"vectorizer = TfidfVectorizer(ngram_range=(1,2))\n",
"vectorizer.fit_transform(corpus)\n",
"vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 결론\n",
"\n",
"TF-IDF 표현이 다양한 단어에 빈도 가중치를 부여하더라도, 의미나 순서를 표현할 수는 없습니다. 유명한 언어학자 J. R. Firth가 1935년에 말했듯이, “단어의 완전한 의미는 항상 문맥적이며, 문맥을 제외한 의미 연구는 진지하게 받아들일 수 없다.” 이후 강의에서 언어 모델링을 사용하여 텍스트에서 문맥 정보를 포착하는 방법을 배우게 될 것입니다.\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": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f"
},
"kernelspec": {
"display_name": "Python 3.8.12 ('py38')",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.12"
},
"coopTranslator": {
"original_hash": "7b9040985e748e4e2d4c689892456ad7",
"translation_date": "2025-08-31T14:09:03+00:00",
"source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb",
"language_code": "ko"
}
},
"nbformat": 4,
"nbformat_minor": 2
}