AI-For-Beginners/translations/ko/lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb

414 lines
22 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 생성 네트워크\n",
"\n",
"순환 신경망(Recurrent Neural Networks, RNNs)과 Long Short Term Memory Cells(LSTMs), Gated Recurrent Units(GRUs)와 같은 게이트 셀 변형은 언어 모델링을 위한 메커니즘을 제공합니다. 즉, 단어의 순서를 학습하고 시퀀스에서 다음 단어를 예측할 수 있습니다. 이를 통해 RNN을 **생성 작업**에 사용할 수 있습니다. 예를 들어, 일반적인 텍스트 생성, 기계 번역, 심지어 이미지 캡션 생성에도 활용할 수 있습니다.\n",
"\n",
"이전 단원에서 논의한 RNN 아키텍처에서는 각 RNN 유닛이 다음 숨겨진 상태를 출력으로 생성했습니다. 그러나 각 순환 유닛에 또 다른 출력을 추가할 수도 있습니다. 이를 통해 원래 시퀀스와 길이가 동일한 **시퀀스**를 출력할 수 있습니다. 더 나아가, 각 단계에서 입력을 받지 않고 초기 상태 벡터만 받아 시퀀스 출력을 생성하는 RNN 유닛도 사용할 수 있습니다.\n",
"\n",
"이 노트북에서는 텍스트 생성을 돕는 간단한 생성 모델에 초점을 맞출 것입니다. 간단히 말해, **문자 단위 네트워크**를 구축해 텍스트를 한 글자씩 생성해 보겠습니다. 학습 과정에서는 텍스트 코퍼스를 가져와 이를 문자 시퀀스로 분할해야 합니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loading dataset...\n",
"Building vocab...\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()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 문자 어휘 구축\n",
"\n",
"문자 수준의 생성 네트워크를 구축하려면 텍스트를 단어가 아닌 개별 문자로 분리해야 합니다. 이를 위해 다른 토크나이저를 정의할 수 있습니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Vocabulary size = 82\n",
"Encoding of 'a' is 1\n",
"Character with code 13 is c\n"
]
}
],
"source": [
"def char_tokenizer(words):\n",
" return list(words) #[word for word in words]\n",
"\n",
"counter = collections.Counter()\n",
"for (label, line) in train_dataset:\n",
" counter.update(char_tokenizer(line))\n",
"vocab = torchtext.vocab.vocab(counter)\n",
"\n",
"vocab_size = len(vocab)\n",
"print(f\"Vocabulary size = {vocab_size}\")\n",
"print(f\"Encoding of 'a' is {vocab.get_stoi()['a']}\")\n",
"print(f\"Character with code 13 is {vocab.get_itos()[13]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"우리 데이터셋에서 텍스트를 어떻게 인코딩할 수 있는지 예를 봅시다:\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"tensor([ 0, 1, 2, 2, 3, 4, 5, 6, 3, 7, 8, 1, 9, 10, 3, 11, 2, 1,\n",
" 12, 3, 7, 1, 13, 14, 3, 15, 16, 5, 17, 3, 5, 18, 8, 3, 7, 2,\n",
" 1, 13, 14, 3, 19, 20, 8, 21, 5, 8, 9, 10, 22, 3, 20, 8, 21, 5,\n",
" 8, 9, 10, 3, 23, 3, 4, 18, 17, 9, 5, 23, 10, 8, 2, 2, 8, 9,\n",
" 10, 24, 3, 0, 1, 2, 2, 3, 4, 5, 9, 8, 8, 5, 25, 10, 3, 26,\n",
" 12, 27, 16, 26, 2, 27, 16, 28, 29, 30, 1, 16, 26, 3, 17, 31, 3, 21,\n",
" 2, 5, 9, 1, 23, 13, 32, 16, 27, 13, 10, 24, 3, 1, 9, 8, 3, 10,\n",
" 8, 8, 27, 16, 28, 3, 28, 9, 8, 8, 16, 3, 1, 28, 1, 27, 16, 6])"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def enc(x):\n",
" return torch.LongTensor(encode(x,voc=vocab,tokenizer=char_tokenizer))\n",
"\n",
"enc(train_dataset[0][1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 생성적 RNN 훈련하기\n",
"\n",
"RNN을 훈련시켜 텍스트를 생성하는 방법은 다음과 같습니다. 각 단계에서 `nchars` 길이의 문자 시퀀스를 가져와 네트워크가 각 입력 문자에 대해 다음 출력 문자를 생성하도록 요청합니다:\n",
"\n",
"![단어 'HELLO'를 생성하는 RNN 예제를 보여주는 이미지.](../../../../../lessons/5-NLP/17-GenerativeNetworks/images/rnn-generate.png)\n",
"\n",
"실제 시나리오에 따라 *시퀀스 종료* `<eos>`와 같은 특수 문자를 포함하고 싶을 수도 있습니다. 하지만 우리의 경우, 끝없는 텍스트 생성을 위해 네트워크를 훈련시키고자 하므로 각 시퀀스의 크기를 `nchars` 토큰으로 고정할 것입니다. 따라서 각 훈련 예제는 `nchars` 입력과 `nchars` 출력(입력 시퀀스를 왼쪽으로 한 기호씩 이동한 것)으로 구성됩니다. 미니배치는 이러한 여러 시퀀스로 구성됩니다.\n",
"\n",
"미니배치를 생성하는 방법은 길이가 `l`인 각 뉴스 텍스트를 가져와 그로부터 가능한 모든 입력-출력 조합을 생성하는 것입니다(이 조합은 `l-nchars`개가 될 것입니다). 이 조합들은 하나의 미니배치를 형성하며, 훈련 단계마다 미니배치의 크기는 달라질 것입니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(tensor([[ 0, 1, 2, ..., 28, 29, 30],\n",
" [ 1, 2, 2, ..., 29, 30, 1],\n",
" [ 2, 2, 3, ..., 30, 1, 16],\n",
" ...,\n",
" [20, 8, 21, ..., 1, 28, 1],\n",
" [ 8, 21, 5, ..., 28, 1, 27],\n",
" [21, 5, 8, ..., 1, 27, 16]]),\n",
" tensor([[ 1, 2, 2, ..., 29, 30, 1],\n",
" [ 2, 2, 3, ..., 30, 1, 16],\n",
" [ 2, 3, 4, ..., 1, 16, 26],\n",
" ...,\n",
" [ 8, 21, 5, ..., 28, 1, 27],\n",
" [21, 5, 8, ..., 1, 27, 16],\n",
" [ 5, 8, 9, ..., 27, 16, 6]]))"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"nchars = 100\n",
"\n",
"def get_batch(s,nchars=nchars):\n",
" ins = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n",
" outs = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n",
" for i in range(len(s)-nchars):\n",
" ins[i] = enc(s[i:i+nchars])\n",
" outs[i] = enc(s[i+1:i+nchars+1])\n",
" return ins,outs\n",
"\n",
"get_batch(train_dataset[0][1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"이제 생성기 네트워크를 정의해 봅시다. 이는 이전 단원에서 논의한 반복 셀(단순 RNN, LSTM, GRU 중 하나)을 기반으로 할 수 있습니다. 이번 예제에서는 LSTM을 사용할 것입니다.\n",
"\n",
"네트워크가 문자를 입력으로 받고, 어휘 크기가 비교적 작기 때문에 임베딩 레이어는 필요하지 않습니다. 원-핫 인코딩된 입력을 바로 LSTM 셀에 전달할 수 있습니다. 하지만, 입력으로 문자 번호를 전달하기 때문에, LSTM에 전달하기 전에 이를 원-핫 인코딩해야 합니다. 이는 `forward` 단계에서 `one_hot` 함수를 호출하여 수행됩니다. 출력 인코더는 은닉 상태를 원-핫 인코딩된 출력으로 변환하는 선형 레이어가 될 것입니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"class LSTMGenerator(torch.nn.Module):\n",
" def __init__(self, vocab_size, hidden_dim):\n",
" super().__init__()\n",
" self.rnn = torch.nn.LSTM(vocab_size,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, vocab_size)\n",
"\n",
" def forward(self, x, s=None):\n",
" x = torch.nn.functional.one_hot(x,vocab_size).to(torch.float32)\n",
" x,s = self.rnn(x,s)\n",
" return self.fc(x),s"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"훈련 중에는 생성된 텍스트를 샘플링할 수 있어야 합니다. 이를 위해, 초기 문자열 `start`에서 시작하여 길이가 `size`인 출력 문자열을 생성하는 `generate` 함수를 정의할 것입니다.\n",
"\n",
"작동 방식은 다음과 같습니다. 먼저, 전체 시작 문자열을 네트워크에 전달하고 출력 상태 `s`와 다음에 예측된 문자 `out`을 가져옵니다. `out`은 원-핫 인코딩되어 있으므로, `argmax`를 사용하여 어휘에서 문자 `nc`의 인덱스를 얻고, `itos`를 사용하여 실제 문자를 확인한 후 결과 문자 리스트 `chars`에 추가합니다. 이 과정을 통해 한 문자를 생성하는 작업을 `size` 횟수만큼 반복하여 필요한 수의 문자를 생성합니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"def generate(net,size=100,start='today '):\n",
" chars = list(start)\n",
" out, s = net(enc(chars).view(1,-1).to(device))\n",
" for i in range(size):\n",
" nc = torch.argmax(out[0][-1])\n",
" chars.append(vocab.get_itos()[nc])\n",
" out, s = net(nc.view(1,-1),s)\n",
" return ''.join(chars)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"이제 훈련을 시작해봅시다! 훈련 루프는 이전 예제들과 거의 동일하지만, 정확도를 출력하는 대신 1000 에포크마다 샘플링된 생성 텍스트를 출력합니다.\n",
"\n",
"특히 손실(loss)을 계산하는 방식에 주의를 기울여야 합니다. 우리는 원-핫 인코딩된 출력 `out`과 예상 텍스트 `text_out`(문자 인덱스의 리스트)을 기반으로 손실을 계산해야 합니다. 다행히도, `cross_entropy` 함수는 첫 번째 인수로 정규화되지 않은 네트워크 출력을, 두 번째 인수로 클래스 번호를 기대하며, 이는 우리가 가진 데이터와 정확히 일치합니다. 또한, 이 함수는 미니배치 크기에 대한 자동 평균화도 수행합니다.\n",
"\n",
"또한, 너무 오래 기다리지 않도록 `samples_to_train` 샘플로 훈련을 제한합니다. 더 긴 훈련을 시도해보는 것도 권장하며, 몇 에포크 동안 훈련을 진행해보는 것도 좋습니다(이 경우 이 코드를 감싸는 또 다른 루프를 만들어야 할 것입니다).\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Current loss = 4.398899078369141\n",
"today sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr s\n",
"Current loss = 2.161320447921753\n",
"today and to the tor to to the tor to to the tor to to the tor to to the tor to to the tor to to the tor t\n",
"Current loss = 1.6722588539123535\n",
"today and the court to the could to the could to the could to the could to the could to the could to the c\n",
"Current loss = 2.423795223236084\n",
"today and a second to the conternation of the conternation of the conternation of the conternation of the \n",
"Current loss = 1.702607274055481\n",
"today and the company to the company to the company to the company to the company to the company to the co\n",
"Current loss = 1.692358136177063\n",
"today and the company to the company to the company to the company to the company to the company to the co\n",
"Current loss = 1.9722288846969604\n",
"today and the control the control the control the control the control the control the control the control \n",
"Current loss = 1.8705692291259766\n",
"today and the second to the second to the second to the second to the second to the second to the second t\n",
"Current loss = 1.7626899480819702\n",
"today and a security and a security and a security and a security and a security and a security and a secu\n",
"Current loss = 1.5574463605880737\n",
"today and the company and the company and the company and the company and the company and the company and \n",
"Current loss = 1.5620026588439941\n",
"today and the be that the be the be that the be the be that the be the be that the be the be that the be t\n"
]
}
],
"source": [
"net = LSTMGenerator(vocab_size,64).to(device)\n",
"\n",
"samples_to_train = 10000\n",
"optimizer = torch.optim.Adam(net.parameters(),0.01)\n",
"loss_fn = torch.nn.CrossEntropyLoss()\n",
"net.train()\n",
"for i,x in enumerate(train_dataset):\n",
" # x[0] is class label, x[1] is text\n",
" if len(x[1])-nchars<10:\n",
" continue\n",
" samples_to_train-=1\n",
" if not samples_to_train: break\n",
" text_in, text_out = get_batch(x[1])\n",
" optimizer.zero_grad()\n",
" out,s = net(text_in)\n",
" loss = torch.nn.functional.cross_entropy(out.view(-1,vocab_size),text_out.flatten()) #cross_entropy(out,labels)\n",
" loss.backward()\n",
" optimizer.step()\n",
" if i%1000==0:\n",
" print(f\"Current loss = {loss.item()}\")\n",
" print(generate(net))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"이 예제는 이미 꽤 괜찮은 텍스트를 생성하지만, 몇 가지 방법으로 더 개선할 수 있습니다:\n",
"\n",
"* **더 나은 미니배치 생성**. 우리가 훈련 데이터를 준비한 방식은 하나의 샘플에서 하나의 미니배치를 생성하는 것이었습니다. 이는 이상적이지 않은데, 왜냐하면 미니배치의 크기가 모두 다르고, 텍스트가 `nchars`보다 작을 경우 일부 미니배치를 생성할 수 없기 때문입니다. 또한, 작은 미니배치는 GPU를 충분히 활용하지 못합니다. 더 나은 방법은 모든 샘플에서 하나의 큰 텍스트 덩어리를 가져온 다음, 모든 입력-출력 쌍을 생성하고, 이를 섞은 후, 크기가 동일한 미니배치를 생성하는 것입니다.\n",
"\n",
"* **다층 LSTM**. LSTM 셀을 2층 또는 3층으로 시도해보는 것도 의미가 있습니다. 이전 단원에서 언급했듯이, LSTM의 각 층은 텍스트에서 특정 패턴을 추출합니다. 문자 수준 생성기의 경우, 낮은 LSTM 층은 음절을 추출하고, 높은 층은 단어와 단어 조합을 담당할 것으로 예상할 수 있습니다. 이는 LSTM 생성자에 층 수 매개변수를 전달하여 간단히 구현할 수 있습니다.\n",
"\n",
"* **GRU 유닛**을 실험해보고 어떤 것이 더 나은 성능을 보이는지 확인하거나, **다양한 은닉층 크기**를 시도해볼 수도 있습니다. 은닉층이 너무 크면 과적합(예: 네트워크가 텍스트를 정확히 학습함)될 수 있고, 크기가 너무 작으면 좋은 결과를 내지 못할 수 있습니다.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 부드러운 텍스트 생성과 온도\n",
"\n",
"이전의 `generate` 정의에서는 항상 생성된 텍스트에서 다음 문자로 가장 높은 확률을 가진 문자를 선택했습니다. 이로 인해 텍스트가 종종 동일한 문자 시퀀스를 반복하는 \"순환\" 현상이 발생하곤 했습니다. 예를 들어, 아래와 같은 경우입니다:\n",
"```\n",
"today of the second the company and a second the company ...\n",
"```\n",
"\n",
"하지만 다음 문자의 확률 분포를 살펴보면, 가장 높은 확률들 간의 차이가 크지 않을 수 있습니다. 예를 들어, 한 문자의 확률이 0.2이고, 다른 문자가 0.19인 경우처럼 말이죠. 예를 들어, 시퀀스 '*play*'에서 다음 문자를 찾을 때, 다음 문자는 공백일 수도 있고, **e**일 수도 있습니다 (단어 *player*에서처럼).\n",
"\n",
"이로부터 우리는 항상 더 높은 확률을 가진 문자를 선택하는 것이 \"공정\"하지 않을 수 있다는 결론에 도달합니다. 두 번째로 높은 확률을 선택하더라도 여전히 의미 있는 텍스트를 생성할 수 있기 때문입니다. 따라서 네트워크 출력이 제공하는 확률 분포에서 문자를 **샘플링**하는 것이 더 현명합니다.\n",
"\n",
"이 샘플링은 **다항 분포**라고 불리는 것을 구현하는 `multinomial` 함수를 사용하여 수행할 수 있습니다. 이 **부드러운** 텍스트 생성을 구현하는 함수는 아래와 같이 정의됩니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--- Temperature = 0.3\n",
"Today and a company and complete an all the land the restrational the as a security and has provers the pay to and a report and the computer in the stand has filities and working the law the stations for a company and with the company and the final the first company and refight of the state and and workin\n",
"\n",
"--- Temperature = 0.8\n",
"Today he oniis its first to Aus bomblaties the marmation a to manan boogot that pirate assaid a relaid their that goverfin the the Cappets Ecrotional Assonia Cition targets it annight the w scyments Blamity #39;s TVeer Diercheg Reserals fran envyuil that of ster said access what succers of Dour-provelith\n",
"\n",
"--- Temperature = 1.0\n",
"Today holy they a 11 will meda a toket subsuaties, engins for Chanos, they's has stainger past to opening orital his thempting new Nattona was al innerforder advan-than #36;s night year his religuled talitatian what the but with Wednesday to Justment will wemen of Mark CCC Camp as Timed Nae wome a leaders\n",
"\n",
"--- Temperature = 1.3\n",
"Today gpone 2.5 fech atcusion poor cocles toparsdorM.cht Line Pamage put 43 his calt lowed to the book, that has authh-the silia rruch ailing to'ory andhes beutirsimi- Aefffive heading offil an auf eacklets is charged evis, Gunymy oy) Mony has it after-sloythyor loveId out filme, the Natabl -Najuntaxiggs \n",
"\n",
"--- Temperature = 1.8\n",
"Today plary, P.slan chly\\401 mardregationly #39;t 8.1Mide) closes ,filtcon alfly playin roven!\\grea.-QFBEP: Iss onfarchQ/itilia CCf Zivesigntwasta orce.-Peul-aw.uicrin of fuglinfsut aftaningwo, MIEX awayew Aice Woiduar Corvagiugge oppo esig ThusBratourid canthly-RyI.co lagitems\\eexciaishes.conBabntusmor I\n",
"\n"
]
}
],
"source": [
"def generate_soft(net,size=100,start='today ',temperature=1.0):\n",
" chars = list(start)\n",
" out, s = net(enc(chars).view(1,-1).to(device))\n",
" for i in range(size):\n",
" #nc = torch.argmax(out[0][-1])\n",
" out_dist = out[0][-1].div(temperature).exp()\n",
" nc = torch.multinomial(out_dist,1)[0]\n",
" chars.append(vocab.get_itos()[nc])\n",
" out, s = net(nc.view(1,-1),s)\n",
" return ''.join(chars)\n",
" \n",
"for i in [0.3,0.8,1.0,1.3,1.8]:\n",
" print(f\"--- Temperature = {i}\\n{generate_soft(net,size=300,start='Today ',temperature=i)}\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"우리는 **온도**라는 하나의 매개변수를 추가로 도입했으며, 이는 우리가 가장 높은 확률에 얼마나 강하게 고수해야 하는지를 나타내는 데 사용됩니다. 온도가 1.0이면 공정한 다항 샘플링을 수행하며, 온도가 무한대로 증가하면 모든 확률이 동일해지고 다음 문자를 무작위로 선택하게 됩니다. 아래 예시에서 온도를 너무 높이면 텍스트가 무의미해지고, 온도가 0에 가까워지면 \"순환된\" 강제 생성 텍스트와 유사해지는 것을 관찰할 수 있습니다.\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": "7673cd150d96c74c6d6011460094efb4",
"translation_date": "2025-08-31T13:43:54+00:00",
"source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb",
"language_code": "ko"
}
},
"nbformat": 4,
"nbformat_minor": 4
}