AI-For-Beginners/translations/ko/lessons/5-NLP/16-RNN/RNNPyTorch.ipynb

479 lines
25 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 순환 신경망 (Recurrent Neural Networks)\n",
"\n",
"이전 모듈에서는 텍스트의 풍부한 의미 표현을 사용하고, 임베딩 위에 간단한 선형 분류기를 적용했습니다. 이 아키텍처는 문장에서 단어들의 **종합적인 의미**를 포착하지만, 임베딩 위에서의 집계 연산이 원래 텍스트의 **단어 순서** 정보를 제거하기 때문에 단어 순서를 고려하지 않습니다. 이러한 모델은 단어 순서를 모델링할 수 없기 때문에 텍스트 생성이나 질문 응답과 같은 더 복잡하거나 모호한 작업을 해결할 수 없습니다.\n",
"\n",
"텍스트 시퀀스의 의미를 포착하려면 **순환 신경망**(Recurrent Neural Network, RNN)이라고 불리는 또 다른 신경망 아키텍처를 사용해야 합니다. RNN에서는 문장을 한 번에 하나의 기호씩 네트워크에 통과시키고, 네트워크는 **상태(state)**를 생성합니다. 그런 다음 이 상태를 다음 기호와 함께 다시 네트워크에 전달합니다.\n",
"\n",
"주어진 토큰 시퀀스 $X_0,\\dots,X_n$에 대해, RNN은 신경망 블록의 시퀀스를 생성하고, 이 시퀀스를 역전파를 통해 끝까지 학습합니다. 각 네트워크 블록은 $(X_i,S_i)$ 쌍을 입력으로 받아 $S_{i+1}$을 결과로 생성합니다. 최종 상태 $S_n$ 또는 출력 $X_n$은 선형 분류기로 전달되어 결과를 생성합니다. 모든 네트워크 블록은 동일한 가중치를 공유하며, 하나의 역전파 과정을 통해 끝까지 학습됩니다.\n",
"\n",
"상태 벡터 $S_0,\\dots,S_n$이 네트워크를 통해 전달되기 때문에, RNN은 단어 간의 순차적 의존성을 학습할 수 있습니다. 예를 들어, 시퀀스 어딘가에 *not*이라는 단어가 나타날 때, 상태 벡터 내 특정 요소를 부정하도록 학습할 수 있습니다. 이는 부정적인 의미를 반영하는 결과를 가져옵니다.\n",
"\n",
"> 그림에서 모든 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",
"from torchnlp import *\n",
"train_dataset, test_dataset, classes, vocab = load_dataset()\n",
"vocab_size = len(vocab)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 간단한 RNN 분류기\n",
"\n",
"간단한 RNN의 경우, 각 순환 유닛은 입력 벡터와 상태 벡터를 결합하여 새로운 상태 벡터를 생성하는 단순 선형 네트워크입니다. PyTorch는 이 유닛을 `RNNCell` 클래스와 같은 셀 네트워크를 `RNN` 레이어로 표현합니다.\n",
"\n",
"RNN 분류기를 정의하기 위해 먼저 임베딩 레이어를 적용하여 입력 어휘의 차원을 낮추고, 그 위에 RNN 레이어를 추가할 것입니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"class RNNClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.rnn = torch.nn.RNN(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" x,h = self.rnn(x)\n",
" return self.fc(x.mean(dim=1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Note:** 여기서는 간단함을 위해 학습되지 않은 임베딩 레이어를 사용하지만, 이전 단원에서 설명한 것처럼 Word2Vec 또는 GloVe 임베딩을 사용한 사전 학습된 임베딩 레이어를 활용하면 더 나은 결과를 얻을 수 있습니다. 더 잘 이해하기 위해 이 코드를 사전 학습된 임베딩과 함께 작동하도록 수정해보는 것도 좋습니다.\n",
"\n",
"이번에는 패딩된 데이터 로더를 사용할 것이며, 각 배치는 동일한 길이의 패딩된 시퀀스를 포함하게 됩니다. RNN 레이어는 임베딩 텐서의 시퀀스를 받아들이고 두 가지 출력을 생성합니다:\n",
"* $x$: 각 단계에서 RNN 셀 출력의 시퀀스\n",
"* $h$: 시퀀스의 마지막 요소에 대한 최종 은닉 상태\n",
"\n",
"그 후, 완전 연결된 선형 분류기를 적용하여 클래스 수를 얻습니다.\n",
"\n",
"> **Note:** RNN은 훈련하기가 상당히 어렵습니다. RNN 셀이 시퀀스 길이에 따라 펼쳐지면, 역전파에 관여하는 레이어 수가 매우 많아지기 때문입니다. 따라서 작은 학습률을 선택하고 더 큰 데이터셋에서 네트워크를 훈련시켜야 좋은 결과를 얻을 수 있습니다. 시간이 오래 걸릴 수 있으므로 GPU를 사용하는 것이 권장됩니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.3090625\n",
"6400: acc=0.38921875\n",
"9600: acc=0.4590625\n",
"12800: acc=0.511953125\n",
"16000: acc=0.5506875\n",
"19200: acc=0.57921875\n",
"22400: acc=0.6070089285714285\n",
"25600: acc=0.6304296875\n",
"28800: acc=0.6484027777777778\n",
"32000: acc=0.66509375\n",
"35200: acc=0.6790056818181818\n",
"38400: acc=0.6929166666666666\n",
"41600: acc=0.7035817307692308\n",
"44800: acc=0.7137276785714286\n",
"48000: acc=0.72225\n",
"51200: acc=0.73001953125\n",
"54400: acc=0.7372794117647059\n",
"57600: acc=0.7436631944444444\n",
"60800: acc=0.7503947368421052\n",
"64000: acc=0.75634375\n",
"67200: acc=0.7615773809523809\n",
"70400: acc=0.7662642045454545\n",
"73600: acc=0.7708423913043478\n",
"76800: acc=0.7751822916666666\n",
"80000: acc=0.7790625\n",
"83200: acc=0.7825\n",
"86400: acc=0.7858564814814815\n",
"89600: acc=0.7890513392857142\n",
"92800: acc=0.7920474137931034\n",
"96000: acc=0.7952708333333334\n",
"99200: acc=0.7982258064516129\n",
"102400: acc=0.80099609375\n",
"105600: acc=0.8037594696969697\n",
"108800: acc=0.8060569852941176\n"
]
}
],
"source": [
"train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)\n",
"net = RNNClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch(net,train_loader, lr=0.001)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 장단기 메모리 (LSTM)\n",
"\n",
"고전적인 RNN의 주요 문제 중 하나는 **기울기 소실** 문제입니다. RNN은 한 번의 역전파 과정에서 끝까지 학습되기 때문에, 네트워크의 첫 번째 레이어로 오류를 전달하는 데 어려움을 겪으며, 결과적으로 네트워크는 먼 토큰 간의 관계를 학습할 수 없습니다. 이 문제를 피하는 방법 중 하나는 **게이트**를 사용하여 **명시적인 상태 관리**를 도입하는 것입니다. 이러한 종류의 가장 잘 알려진 아키텍처는 **장단기 메모리**(LSTM)와 **게이트 릴레이 유닛**(GRU)입니다.\n",
"\n",
"![장단기 메모리 셀의 예를 보여주는 이미지](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"LSTM 네트워크는 RNN과 유사한 방식으로 구성되지만, 레이어 간에 전달되는 두 가지 상태가 있습니다: 실제 상태 $c$와 숨겨진 벡터 $h$입니다. 각 유닛에서 숨겨진 벡터 $h_i$는 입력 $x_i$와 연결되며, **게이트**를 통해 상태 $c$에 어떤 일이 발생할지를 제어합니다. 각 게이트는 시그모이드 활성화 함수(출력 범위 $[0,1]$)를 가진 신경망으로, 상태 벡터와 곱해질 때 비트 마스크처럼 작동한다고 생각할 수 있습니다. 위 그림에서 왼쪽에서 오른쪽으로 다음과 같은 게이트가 있습니다:\n",
"* **포겟 게이트**는 숨겨진 벡터를 받아 벡터 $c$의 어떤 구성 요소를 잊어야 할지, 어떤 것을 통과시켜야 할지를 결정합니다.\n",
"* **입력 게이트**는 입력과 숨겨진 벡터에서 일부 정보를 가져와 상태에 삽입합니다.\n",
"* **출력 게이트**는 상태를 $\\tanh$ 활성화가 있는 일부 선형 레이어를 통해 변환한 다음, 숨겨진 벡터 $h_i$를 사용하여 새로운 상태 $c_{i+1}$를 생성할 일부 구성 요소를 선택합니다.\n",
"\n",
"상태 $c$의 구성 요소는 켜고 끌 수 있는 플래그로 생각할 수 있습니다. 예를 들어, 시퀀스에서 *Alice*라는 이름을 접했을 때, 이를 여성 캐릭터로 간주하고 상태에서 여성 명사가 있다는 플래그를 올릴 수 있습니다. 이후 *and Tom*이라는 구절을 접했을 때, 복수 명사가 있다는 플래그를 올릴 수 있습니다. 따라서 상태를 조작함으로써 문장 부분의 문법적 속성을 추적할 수 있다고 가정할 수 있습니다.\n",
"\n",
"> **Note**: LSTM의 내부 구조를 이해하는 데 훌륭한 자료는 Christopher Olah의 [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/)라는 훌륭한 기사입니다.\n",
"\n",
"LSTM 셀의 내부 구조는 복잡해 보일 수 있지만, PyTorch는 이를 `LSTMCell` 클래스 내부에 숨기고 전체 LSTM 레이어를 나타내는 `LSTM` 객체를 제공합니다. 따라서 LSTM 분류기를 구현하는 것은 위에서 본 간단한 RNN과 매우 유사할 것입니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"class LSTMClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n",
" self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" x,(h,c) = self.rnn(x)\n",
" return self.fc(h[-1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.259375\n",
"6400: acc=0.25859375\n",
"9600: acc=0.26177083333333334\n",
"12800: acc=0.2784375\n",
"16000: acc=0.313\n",
"19200: acc=0.3528645833333333\n",
"22400: acc=0.3965625\n",
"25600: acc=0.4385546875\n",
"28800: acc=0.4752777777777778\n",
"32000: acc=0.505375\n",
"35200: acc=0.5326704545454546\n",
"38400: acc=0.5557552083333334\n",
"41600: acc=0.5760817307692307\n",
"44800: acc=0.5954910714285714\n",
"48000: acc=0.6118333333333333\n",
"51200: acc=0.62681640625\n",
"54400: acc=0.6404779411764706\n",
"57600: acc=0.6520138888888889\n",
"60800: acc=0.662828947368421\n",
"64000: acc=0.673546875\n",
"67200: acc=0.6831547619047619\n",
"70400: acc=0.6917897727272727\n",
"73600: acc=0.6997146739130434\n",
"76800: acc=0.707109375\n",
"80000: acc=0.714075\n",
"83200: acc=0.7209134615384616\n",
"86400: acc=0.727037037037037\n",
"89600: acc=0.7326674107142858\n",
"92800: acc=0.7379633620689655\n",
"96000: acc=0.7433645833333333\n",
"99200: acc=0.7479032258064516\n",
"102400: acc=0.752119140625\n",
"105600: acc=0.7562405303030303\n",
"108800: acc=0.76015625\n",
"112000: acc=0.7641339285714286\n",
"115200: acc=0.7677777777777778\n",
"118400: acc=0.7711233108108108\n"
]
},
{
"data": {
"text/plain": [
"(0.03487814127604167, 0.7728)"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"net = LSTMClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch(net,train_loader, lr=0.001)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 패킹된 시퀀스\n",
"\n",
"예제에서는 미니배치의 모든 시퀀스를 0 벡터로 패딩해야 했습니다. 이는 메모리 낭비를 초래할 수 있지만, RNN에서는 패딩된 입력 항목에 대해 추가 RNN 셀을 생성하는 것이 더 중요한 문제입니다. 이러한 셀은 학습에 참여하지만 중요한 입력 정보를 포함하지 않습니다. 실제 시퀀스 크기에 맞춰 RNN을 학습시키는 것이 훨씬 더 효율적입니다.\n",
"\n",
"이를 위해 PyTorch에서는 패딩된 시퀀스를 저장하는 특별한 형식이 도입되었습니다. 예를 들어, 패딩된 미니배치 입력이 다음과 같다고 가정해봅시다:\n",
"```\n",
"[[1,2,3,4,5],\n",
" [6,7,8,0,0],\n",
" [9,0,0,0,0]]\n",
"```\n",
"여기서 0은 패딩된 값을 나타내며, 입력 시퀀스의 실제 길이 벡터는 `[5,3,1]`입니다.\n",
"\n",
"패딩된 시퀀스를 효과적으로 학습시키기 위해, 첫 번째 그룹의 RNN 셀을 큰 미니배치(`[1,6,9]`)로 학습을 시작한 후, 세 번째 시퀀스의 처리를 종료하고 짧아진 미니배치(`[2,7]`, `[3,8]`)로 학습을 계속 진행하고 싶습니다. 따라서 패킹된 시퀀스는 하나의 벡터로 표현됩니다. 이 경우 `[1,6,9,2,7,3,8,4,5]`와 길이 벡터(`[5,3,1]`)로 구성되며, 이를 통해 원래의 패딩된 미니배치를 쉽게 복원할 수 있습니다.\n",
"\n",
"패킹된 시퀀스를 생성하려면 `torch.nn.utils.rnn.pack_padded_sequence` 함수를 사용할 수 있습니다. RNN, LSTM, GRU를 포함한 모든 순환 레이어는 입력으로 패킹된 시퀀스를 지원하며, 패킹된 출력을 생성합니다. 이 출력은 `torch.nn.utils.rnn.pad_packed_sequence`를 사용하여 디코딩할 수 있습니다.\n",
"\n",
"패킹된 시퀀스를 생성하려면 네트워크에 길이 벡터를 전달해야 하며, 이를 위해 미니배치를 준비하는 다른 함수가 필요합니다:\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"def pad_length(b):\n",
" # build vectorized sequence\n",
" v = [encode(x[1]) for x in b]\n",
" # compute max length of a sequence in this minibatch and length sequence itself\n",
" len_seq = list(map(len,v))\n",
" l = max(len_seq)\n",
" return ( # tuple of three tensors - labels, padded features, length sequence\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",
" torch.tensor(len_seq)\n",
" )\n",
"\n",
"train_loader_len = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=pad_length, shuffle=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"실제 네트워크는 위의 `LSTMClassifier`와 매우 유사하지만, `forward` 패스는 패딩된 미니배치와 시퀀스 길이 벡터를 모두 받습니다. 임베딩을 계산한 후, 패킹된 시퀀스를 계산하고 이를 LSTM 레이어에 전달한 다음, 결과를 다시 언패킹합니다.\n",
"\n",
"> **Note**: 사실 우리는 언패킹된 결과 `x`를 사용하지 않습니다. 왜냐하면 이후 계산에서 숨겨진 레이어의 출력을 사용하기 때문입니다. 따라서 이 코드에서 언패킹을 완전히 제거할 수 있습니다. 여기에서 언패킹을 포함한 이유는, 만약 네트워크 출력을 추가 계산에서 사용해야 할 경우, 여러분이 이 코드를 쉽게 수정할 수 있도록 하기 위함입니다.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"class LSTMPackClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n",
" self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x, lengths):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" pad_x = torch.nn.utils.rnn.pack_padded_sequence(x,lengths,batch_first=True,enforce_sorted=False)\n",
" pad_x,(h,c) = self.rnn(pad_x)\n",
" x, _ = torch.nn.utils.rnn.pad_packed_sequence(pad_x,batch_first=True)\n",
" return self.fc(h[-1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.285625\n",
"6400: acc=0.33359375\n",
"9600: acc=0.3876041666666667\n",
"12800: acc=0.44078125\n",
"16000: acc=0.4825\n",
"19200: acc=0.5235416666666667\n",
"22400: acc=0.5559821428571429\n",
"25600: acc=0.58609375\n",
"28800: acc=0.6116666666666667\n",
"32000: acc=0.63340625\n",
"35200: acc=0.6525284090909091\n",
"38400: acc=0.668515625\n",
"41600: acc=0.6822596153846154\n",
"44800: acc=0.6948214285714286\n",
"48000: acc=0.7052708333333333\n",
"51200: acc=0.71521484375\n",
"54400: acc=0.7239889705882353\n",
"57600: acc=0.7315277777777778\n",
"60800: acc=0.7388486842105263\n",
"64000: acc=0.74571875\n",
"67200: acc=0.7518303571428572\n",
"70400: acc=0.7576988636363636\n",
"73600: acc=0.7628940217391305\n",
"76800: acc=0.7681510416666667\n",
"80000: acc=0.7728125\n",
"83200: acc=0.7772235576923077\n",
"86400: acc=0.7815393518518519\n",
"89600: acc=0.7857700892857142\n",
"92800: acc=0.7895043103448276\n",
"96000: acc=0.7930520833333333\n",
"99200: acc=0.7959072580645161\n",
"102400: acc=0.798994140625\n",
"105600: acc=0.802064393939394\n",
"108800: acc=0.8051378676470589\n",
"112000: acc=0.8077857142857143\n",
"115200: acc=0.8104600694444445\n",
"118400: acc=0.8128293918918919\n"
]
},
{
"data": {
"text/plain": [
"(0.029785829671223958, 0.8138166666666666)"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"net = LSTMPackClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch_emb(net,train_loader_len, lr=0.001,use_pack_sequence=True)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **참고:** 훈련 함수에 전달하는 매개변수 `use_pack_sequence`를 확인했을 수 있습니다. 현재 `pack_padded_sequence` 함수는 길이 시퀀스 텐서가 CPU 장치에 있어야 하며, 따라서 훈련 함수는 훈련 중에 길이 시퀀스 데이터를 GPU로 이동하는 것을 피해야 합니다. [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py) 파일의 `train_emb` 함수 구현을 확인할 수 있습니다.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 양방향 및 다층 RNN\n",
"\n",
"우리의 예제에서는 모든 순환 신경망이 시퀀스의 시작부터 끝까지 한 방향으로 작동했습니다. 이는 우리가 읽거나 말을 들을 때의 방식과 유사하기 때문에 자연스럽게 보입니다. 하지만, 많은 실제 사례에서 입력 시퀀스에 무작위로 접근할 수 있는 경우가 많으므로, 순환 연산을 양방향으로 실행하는 것이 합리적일 수 있습니다. 이러한 네트워크를 **양방향** RNN이라고 하며, RNN/LSTM/GRU 생성자에 `bidirectional=True` 매개변수를 전달하여 생성할 수 있습니다.\n",
"\n",
"양방향 네트워크를 다룰 때는 각 방향에 대해 하나씩 두 개의 은닉 상태 벡터가 필요합니다. PyTorch는 이 벡터들을 두 배 크기의 하나의 벡터로 인코딩합니다. 이는 매우 편리한데, 왜냐하면 일반적으로 결과 은닉 상태를 완전 연결 선형 계층에 전달하기 때문입니다. 이때 계층을 생성할 때 크기 증가를 고려하기만 하면 됩니다.\n",
"\n",
"순환 신경망은 단방향이든 양방향이든 시퀀스 내 특정 패턴을 포착하고 이를 상태 벡터에 저장하거나 출력으로 전달할 수 있습니다. 합성곱 신경망과 마찬가지로, 첫 번째 계층에서 추출한 저수준 패턴을 기반으로 더 높은 수준의 패턴을 포착하기 위해 첫 번째 계층 위에 또 다른 순환 계층을 쌓을 수 있습니다. 이를 **다층 RNN**의 개념이라고 하며, 이는 두 개 이상의 순환 신경망으로 구성되며, 이전 계층의 출력을 다음 계층의 입력으로 전달합니다.\n",
"\n",
"![다층 장단기 메모리 RNN을 보여주는 이미지](../../../../../lessons/5-NLP/16-RNN/images/multi-layer-lstm.jpg)\n",
"\n",
"*Fernando López의 [이 훌륭한 글](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3)에서 가져온 그림*\n",
"\n",
"PyTorch는 이러한 네트워크를 구성하는 작업을 매우 쉽게 만들어줍니다. RNN/LSTM/GRU 생성자에 `num_layers` 매개변수를 전달하기만 하면 여러 계층의 순환 구조를 자동으로 생성할 수 있습니다. 이는 또한 은닉/상태 벡터의 크기가 비례적으로 증가함을 의미하며, 순환 계층의 출력을 처리할 때 이를 고려해야 합니다.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 기타 작업을 위한 RNN\n",
"\n",
"이번 단원에서는 RNN이 시퀀스 분류에 사용될 수 있다는 것을 배웠습니다. 하지만 실제로는 텍스트 생성, 기계 번역 등 훨씬 더 많은 작업을 처리할 수 있습니다. 이러한 작업들은 다음 단원에서 다룰 예정입니다.\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": "522ee52ae3d5ae933e283286254e9a55",
"translation_date": "2025-08-31T13:59:42+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
"language_code": "ko"
}
},
"nbformat": 4,
"nbformat_minor": 2
}