3. 파이토치 nn.RNN( )
■ 파이토치의 torch.nn.RNN( )을 통해서 RNN 셀을 구현할 수 있으며, torch.nn.RNN( )의 파라미터를 통해 깊은 RNN과 양방향 RNN도 구현할 수 있다. 파라미터는 다음과 같다.
torch.nn.RNN(input_size, hidden_size, num_layers=1, nonlinearity='tanh', bias=True,
batch_first=False, dropout=0.0, bidirectional=False, device=None, dtype=None)
- input_size: 입력할 특성(feature)의 개수이다.
- hidden_size: 은닉 상태 벡터의 차원이다.
- num_layers: RNN 레이어를 입력-출력 방향으로 쌓아 올리는 것(stacking RNN)을 위해 사용한다. RNN 레이어 2개층을 겹겹이 쌓아올릴 것이라면 num_layers = 2로 설정하면 된다. 기본값은 1이다.
- nonlinearity: 은닉 상태 \( h \)를 계산하기 위한 비선형 활성화 함수를 지정하는 파라미터이다. tanh함수나 relu 함수를 선택할 수 있다. 기본값은 tanh이다.
- batch_first: batch_first = True로 설정하면, 입력 및 출력 텐서의 형상 중 첫 번째 차원을 batch size로 지정할 수 있다. True이면 입력 및 출력 텐서의 형상은 (seq, batch, feature) 대신 (batch, seq, feature)가 된다. 단, hidden state나 cell state에는 적용되지 않는다. 기본값은 False
- dropout: 마지막 레이어를 제외한 각 RNN 레이어의 출력에 드롭아웃 레이어를 도입하기 위해 사용한다. 기본값은 0
- bidirectional: True로 설정할 경우, 양방향 RNN을 적용한다. 기본값은 False
3.1 (일반적인) RNN
■ 예를 들어 RNN을 다음과 같이 정의했다고 했을 때,
import torch
import torch.nn as nn
input_size = 5 # 입력의 크기
hidden_size = 8 # 은닉 상태의 크기 # 은닉 상태 벡터의 차원
rnn_cell = nn.RNN(input_size, hidden_size, batch_first = True)
■ 다음과 같이 (batch_size, sequence_length = time steps, input_size)의 형상을 갖는 입력 텐서를 위에서 생성한 RNN 셀에 넣으면, 그 결과로 hidden states와 hidden state가 반환된다.
inputs = torch.Tensor(1, 10, 5) # (batch_size, sequence_length = time steps, input_size)
rnn_outputs, hidden = rnn_cell(inputs)
- RNN 셀인 cell에 배치 크기가 1, 그리고 10번의 time step 동안 5차원의 입력 벡터 입력 텐서 inputs이 통과되어 그 결과로 rnn_outputs, hidden이 나온 것이다.
■ 첫 번째 리턴값인 outputs은 모든 시점(time steps)의 은닉 상태들이며, 두 번째 리턴값인 hidden은 마지막 시점의 은닉 상태이다.
print(rnn_outputs.shape, hidden.shape)
```#결과#```
torch.Size([1, 10, 8]) torch.Size([1, 1, 8])
````````````
- 모든 시점의 은닉 상태 벡터들을 모아 놓은 outputs의 크기는 (1, 10, 8)이고 마지막 은닉 상태 벡터가 저장된 hidden의 크기는 (1, 1, 8)인 것을 볼 수 있다.
- 이는 outputs에는 10 time step 동안 8차원의 은닉 상태 벡터가 출력되었다는 의미이다.
- hidden은 마지막 한 시점(1 time step)의 은닉 상태 벡터가 8차원의 은닉 상태 벡터임을 의미한다.
■ 그리고 outputs은 모든 시점의 은닉 상태 벡터들, hidden은 마지막 시점의 은닉 상태 벡터가 담겨 있으므로 outputs의 마지막 원소와 hidden은 동일한 원소를 가진다.
rnn_outputs
```#결과#```
tensor([[[-0.5062, -0.0444, -0.1614, 0.5231, -0.2982, -0.1866, -0.1845,
0.1882],
[-0.3859, 0.0637, 0.0306, 0.5026, -0.3680, -0.3655, -0.3439,
0.1242],
[-0.3893, 0.1513, -0.0753, 0.5788, -0.5079, -0.4076, -0.3910,
0.1357],
...,
[-0.3135, 0.2685, -0.1414, 0.5226, -0.5940, -0.4005, -0.5274,
0.0371],
[-0.3115, 0.2704, -0.1423, 0.5205, -0.5931, -0.3990, -0.5288,
0.0353],
[-0.3116, 0.2708, -0.1434, 0.5199, -0.5929, -0.3985, -0.5290,
0.0351]]], grad_fn=<TransposeBackward1>)
````````````
rnn_outputs.squeeze(0)[-1, :] == hidden
```#결과#```
tensor([[[True, True, True, True, True, True, True, True]]])
````````````
- rnn_outputs의 마지막 행 & 모든 열의 값이 hidden의 값과 모두 일치하는 것을 볼 수 있다.
3.2 깊은 RNN(Deep RNN)과 스킵 연결(Skip Connection)
■ 깊은 RNN은 다음과 같이 num_layers의 값을 설정하면 된다. 다음 코드는 RNN 레이어가 2개인 깊은 RNN이다
deep_rnn_cell = nn.RNN(input_size, hidden_size, num_layers = 2, batch_first = True )
■ 마찬가지로, RNN의 두 개의 출력 결과인 outputs과 hidden의 크기를 확인해 보면 다음과 같다.
print(deep_rnn_outputs.shape, hidden.shape)
```#결과#```
torch.Size([1, 10, 8]) torch.Size([2, 1, 8])
````````````
■ 첫 번째 리턴값의 크기는 3.1에서 RNN 레이어가 1개였던 RNN 셀 때와 달라지지 않았다. 하지만, hidden은 마지막 시점의 모든 층의 은닉 상태 벡터들이 반환된 것을 볼 수 있다.
hidden
```#결과#```
tensor([[[ 0.1283, -0.2845, 0.0071, 0.0158, -0.3544, 0.2853, 0.2297,
0.0923]],
[[-0.2799, -0.6486, -0.0258, 0.2627, 0.2109, 0.4322, 0.1404,
0.6808]]], grad_fn=<StackBackward0>)
````````````
■ 3.1과 3.2의 결과를 통해 RNN 셀의 출력 결과에 대한 크기를 정리하면,
- outputs은 (batch_size, time steps = sequence_length, hidden_dim)의 크기를 가지며,
- hidden은 (num_layers, batch_size, hidden_dim)의 크기를 가진다.
■ 이렇게 깊은 순환 신경망을 만들 때, 다음과 같은 스킵 연결(잔차 연결)을 통해 기울기 소실(혹은 폭발) 문제를 완화할 수 있다.
■ '깊이 방향'으로 더 많은 계층을 추가하는(쌓는) 배경에는 계층들이 많을수록 더 복잡한 특징(패턴)을 학습한다는 직관이 있기 때문이다.
■ 단, 역전파 과정에서 연쇄 법칙에 따라 총 손실에 대한 그래디언트(기울기)에 항을 계속해서 곱해져 나가기 때문에 계층의 깊이가 깊어질수록 연쇄 법칙이 길어지게 된다. 만약, 1보다 작은 값들이 많이 곱해진 상황이라면 기울기(그래디언트) 소실 문제가 발생하기 쉽다.
■ 특히 비선형 활성화 함수로 tanh나 일반적인 relu 함수를 사용한다면, 역전파 과정에서 기울기를 계산했을 때, tanh는 0과 1사이의 값으로, relu는 'dead relu' 현상.
■ 즉, 역전파 과정에서 초기 계층(얕은 계층)으로 갈수록 기울기가 0에 가까워져 학습이 어려워지고, 경우에 따라 전혀 업데이트가 일어나지 않는 상황이 발생할 수 있다. 이것을 기울기 소실 문제라고 한다.
■ 이런 '깊이 방향' 기울기 소실의 대응 방안으로 스킵 연결이 있다.
■ 스킵 연결은 역전파 과정에서 그래디언트에 대한 대체 경로를 제공해주는 방법이라고 볼 수 있다.
■ 이는 스킵 연결이 신경망의 특정 계층을 건너 뛰고, 한 계층의 출력을 다음 계층들(또는 더 이후의 계층들) 입력으로 직접 전달함으로써 이루어지기 때문이다.
■ 스킵 연결을 위한 방법 중 '잔차 구조에서의 덧셈 방식'이 있다.
- 이 접근 방식은 레이어들이 본래의 매핑을 직접 학습하는 대신, 잔차(residual) 매핑을 학습하게 한다.
■ 예를 들어 입력이 \( x \)이고 원하는 매핑이 \( H(x) \)라고 했을 때, 잔차 구조에서의 덧셈 방식은 다음 왼쪽 그림처럼 레이어들이 직접 본래의 매핑 \( H(x) \)를 학습하는 대신, 오른쪽 그림처럼 잔차 매핑 F(x)를 학습하도록 구조를 설계한다.
수식으로는 다음과 같다.
\( H(x) = F(x) + x \)
■ 이렇게 입력 \( x \)를 출력에 직접 더해줬기 때문에 어떤 경로가 생성된다. 이 경로를 통해 기울기가 소멸되지 않고 그대로 전파된다.
cf) 이렇게 \( F(x) \)와 입력을 그대로 전달하는 스킵 연결로 구성된 블록을 잔차 블록이라고 부른다.
■ 스킵 연결(잔차 연결)을 사용하지 않고 원래 방식대로 학습을 진행하려면, 각 가중치 레이어마다 학습을 수행해야 한다. 그러나 스킵 연결을 도입하면, 다음과 같이 입력 \( x \)를 그대로 전달하고 잔여(잔차) 정보인 \( F(x) \)만 추가적으로 더해주는 형태로 단순화된다.
- 오른쪽 식에서 \( W \)는 가중치, \( \sigma \)는 활성화 함수
■ 위의 그림에서 크게 2가지 경로를 생각할 수 있다.
■ 첫 번째 경로는 가중치 경로이다. F(x)를 학습하기 위해 배치된 레이어들을 통과하는 경로. 두 번째 경로는 항등 경로이다. 입력이 그대로 출력에 더해지는 경로이다.
■ 이 덕분에 가중치 경로의 기울기가 0에 가깝게 되어도, 항등 경로가 기울기를 계속 이어주어 학습이 가능해진다.
■ 이러한 이유는 다음과 같이 \( H(x) = F(x) + x \)에 대해 입력 \( x \)에 대한 그래디언트를 나타내면 확인할 수 있다.
\( \dfrac{\partial Loss}{\partial x} = \dfrac{\partial Loss}{\partial H} \times \dfrac{\partial H}{\partial x} = \dfrac{\partial Loss}{\partial H} \times \left( \dfrac{\partial F(x)}{\partial x} + 1 \right) \)
■ 여기서 \( \dfrac{\partial F(x)}{\partial x} \)이 0이 되어서 첫 번째 경로가 소멸한다고 해도 두 번째 경로(항등 경로)를 통한 기울기 1이 그대로 남아 있기 때문에 기울기가 소멸되지 않고 전파될 수 있는 것이다.
■ 항등 경로인 이유가 여기서 나온다. \( F(x) = 0 \)이 된다면 \( H(x) = F(x) + x = x \)로 입력 = 출력인 항등 함수가 된다. 그러므로 학습 과정 중 최소한 전파되는 값을 손실 없이 통과시켜 주기 때문에 성능 저하가 방지되는 일종의 '안전장치'역할을 한다.
cf) 신경망의 입력과 출력이 동일해야 할 경우(항등 매핑) 잔차 \( F(x) \)가 0이 되도록 학습하는 것이 더 쉽다. 위쪽 레이어의 가중치와 편향을 0에 가깝게 만들면 되기 때문이다.
■ 이 방법에는 작은 문제가 있다. \( H(x) = F(x) + x \) 형태의 덧셈을 구현하려면, \( F(x) \)와 \( x \)의 차원이 일치해야 한다. 차원이 불일치하다면, 단순히 더할 수 없다.
■ 예를 들어, 다음과 같은 구조를 RNN으로 구현한다고 하면, 다음과 같다.
class ResidualRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.rnn_layer1 = nn.RNN(input_size, hidden_size, batch_first=True)
sefl.rnn_lyaer2 = nn.RNN(hidden_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
# 입력 차원과 hidden 차원이 다를 경우 잔차 연결을 위한 크기 조정
if input_size != hidden_size:
self.residual = nn.Linear(input_size, hidden_size)
else:
self.residual = nn.Identity()
def forward(self, x):
out1, _ = self.rnn_layer1(x)
out2, _ = self.rnn_layer2(out1)
residual = self.residual(x)
output = out2 + residual # skip connection
output = self.fc(output)
return output
- 입력 차원(input_size)과 RNN의 은닉 차원(hidden_size)이 다른 경우, 두 텐서를 그대로 더할 수 없으므로 두 텐서를 연결해주는 self.residual같은 계층(layer)이 필요하다.
- 만약, 둘의 차원이 동일하다면, 단순히 x를 그대로 더해도 되므로 nn.Identity()를 사용한다. nn.Identity()는 입력을 그대로 반환하는 레이어이다.
■ RNN을 사용할 때 발생할 수 있는 문제와 그 문제에 대한 대응 방안을 정리하면,
■ RNN의 큰 단점은 단기 기억만 가능하다는 것이다. 시퀀스 길이가 길어지면 RNN 계층의 역전파에서는 시간 축(time step) 방향에서 기울기 소실 혹은 폭발이 일어나기 쉽기 때문에 시퀀스 길이가 길다면 장기 기억 의존성 문제가 있다.
■ 이러한 '기울기 소실'에는 'LSTM과 GRU 등의 게이트(gate)가 달린 RNN'으로, '기울기 폭발'에는 '기울기 클리핑'으로 대응할 수 있다.
■ 그리고 입력-출력 방향으로 RNN 레이어를 쌓았을 때, 깊이 방향에서 기울기 소실이 발생할 수 있다. 이러한 깊이 방향 기울기 소실에서는 스킵 연결이 효과적이다.
■ RNN, LSTM 등의 순환 신경망 레이어를 입력-출력 방향으로 쌓았을 때, 스킵 연결을 통해 기울기가 소실(혹은 폭발)되지 않고 전파되므로 좋은 학습을 기대할 수 있다.
3.3 양방향 RNN(Bidirectional RNN)
■ 양방향 RNN은 다음과 같이 bidirectional을 True로 설정하면 된다.
bi_rnn_cell = nn.RNN(input_size, hidden_size, num_layers = 2, batch_first=True, bidirectional = True)
bi_rnn_outputs, hidden = bi_rnn_cell(inputs)
■ 반환되는 결과의 크기를 확인하면 다음과 같다.
print(bi_rnn_outputs.shape, hidden.shape)
```#결과#```
torch.Size([1, 10, 16]) torch.Size([4, 1, 8])
````````````
hidden
```#결과#```
tensor([[[ 0.0401, 0.2533, -0.1738, -0.3567, 0.0276, -0.0359, 0.2477,
-0.0960]],
[[-0.5718, -0.1283, 0.1017, 0.5688, -0.1700, -0.4934, 0.0979,
0.6009]],
[[ 0.1573, 0.0366, 0.2269, 0.4417, -0.3505, 0.3033, -0.4745,
0.1069]],
[[ 0.6418, -0.0634, 0.1476, 0.1719, 0.1772, -0.6329, 0.4549,
0.0846]]], grad_fn=<StackBackward0>)
````````````
- 마지막 시점의 은닉 상태 벡터 4개가 반환되는 것을 확인할 수 있다.
-- 이것은 양방향 RNN이므로 정방향 기준에서는 마지막 시점의 은닉 상태 벡터이며, 역방향 기준에서는 첫 번째 시점의 은닉 상태 벡터이다.
- 첫 번째 리턴값의 크기를 보면, 은닉 상태의 크기가 2배가 된 것을 볼 수 있다. 두 번째 리턴값의 크기도 3.2의 깊은 RNN에 비해 2배가 된 것을 볼 수 있다.
■ 3.1, 3.2, 3.3의 결과를 통해 RNN 셀의 출력 결과에 대한 크기를 정리하면,
- outputs.shape은 (batch_size, sequence_length, hidden_size x 2)
- hidden은 (num_layers x bidirectional(2), batch_size, hidden_size)
4. RNN 예제 - 문자 단위 RNN
■ 문자(character) 시퀀스를 입력받으면, 그다음 문자를 출력해서 문장을 생성하는 RNN을 구현하기 위해 wikitext-2 텍스트에서 다음과 같은 문장을 불러와 문자 단위로 나눈 다음, 이에 대한 단어 집합을 생성하였다.
import numpy as np
import re
def preprocess_sentence(sent):
sent = re.sub(r'[^a-zA-Z]', ' ', sent)
sent = re.sub(r'\s+', ' ', sent)
return sent.lower()
sentences = []
with open('wikitext-2.txt', encoding = 'utf-8') as f:
for lines in f:
if '=' in lines or lines == '\n':
continue
lines = re.sub(r'[^a-zA-Z]', ' ', lines)
lines = re.sub(r'\s+', ' ', lines)
lines = lines.strip().lower()
sentences.append(lines)
if len(sentences)==1: break
sentences[:1]
```#출력#```
['senj no valkyria unrecorded chronicles japanese lit valkyria of the battlefield commonly
referred to as valkyria chronicles iii outside japan is a tactical role playing video game
developed by sega and media vision for the playstation portable released in january in japan
it is the third game in the valkyria series employing the same fusion of tactical and real
time gameplay as its predecessors the story runs parallel to the first game and follows the
nameless a penal military unit serving the nation of gallia during the second europan war
who perform secret black operations and are pitted against the imperial unit calamaty raven']
````````````
char_data = ' '.join(sentences)
char_data[:2]
```#결과#```
'se'
````````````
char_list = list(set(char_data))
char_vocab = dict((c, i) for i, c in enumerate(char_list))
print(char_vocab)
```#결과#```
{'t': 0, 'd': 1, 'k': 2, 'f': 3, 'm': 4, ' ': 5, 'n': 6, 'l': 7, 'r': 8, 'v': 9, 'w': 10, 'h': 11, 'b': 12, 'g': 13, 'j': 14, 'i': 15, 'o': 16, 's': 17, 'p': 18, 'u': 19, 'y': 20, 'e': 21, 'c': 22, 'a': 23}
````````````
■ 이렇게 만든 vocabulary를 이용해서 입출력 데이터를 만든다. 만드는 방법은 다음과 같이 원문을 문자열로 분해한 char_data에 한 번에 모델에 입력할 문자열의 길이(=time step) 10 단위로 입출력 문자열을 만드는 것이다.
time_steps = 10
x_data, y_data = [], []
for i in range(0, len(char_data)-time_steps):
x_str = char_data[i : i+time_steps]
y_str = char_data[i+1: i+time_steps+1]
if i==0: print(i, x_str, '->',y_str)
x_data.append([char_vocab[c] for c in x_str])
y_data.append([char_vocab[c] for c in y_str])
```#결과#```
0 senj no va -> enj no val
````````````
■ 이때 시퀀스 데이터를 만들기 위해 문자열 x_str의 범위는 i부터 i+time_steps까지, 문자열 y_str의 범위는 한 단계 다음 시점인 i+1부터 i+time_steps+1로 설정한다.
■ 이 방식은 시퀀스 데이터를 만들기 위한 방법으로 모델이 주어진 시퀀스 다음에 올 문자를 예측하도록 한다.
- 예를 들어 'senj no va'가 입력이라면 'enj no val'가 타깃이 된다.
■ 모델이 이를 인식할 수 있도록 미리 만든 vocabulary를 이용해 각 문자열 데이터를 정수 인덱스로 변환한다.
print(x_data[0])
print(y_data[0])
```#결과#```
[17, 21, 6, 14, 5, 6, 16, 5, 9, 23] # senj no va
[21, 6, 14, 5, 6, 16, 5, 9, 23, 7] # enj no val
````````````
■ 그리고 np.eye()를 사용해 입력 데이터를 원-핫 벡터로 변환한다. 대안으로 임베딩 레이어를 사용할 수도 있지만, 여기서는 단순한 원-핫 인코딩을 사용했다.
x_one_hot = [np.eye(len(char_vocab))[x] for x in x_data]
■ 그다음, 모델 학습에 사용할 수 있게 파이토치의 텐서로 변환한다.
x_array = np.array(x_one_hot)
print(x_array.dtype)
```#결과#```
float64
````````````
X = torch.FloatTensor(x_one_hot)
y = torch.LongTensor(y_data)
```#결과#```
torch.Size([627, 10, 24]) torch.Size([627, 10])
````````````
■ 예제에 사용할 모델 구조는 다음과 같다.
class Char_RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.rnn = nn.RNN(input_size, hidden_size, num_layers=2, bidirectional=True, batch_first = True)
self.fc = nn.Linear(hidden_size*2, output_size)
def forward(self, x):
output, hidden = self.rnn(x)
output = self.fc(output)
return output
- 입력-출력 방향으로 2개의 RNN 레이어가 있으며, 양방향 RNN을 사용하여 앞뒤 문맥을 모두 고려한다.
- rnn 레이어의 출력인 은닉 상태는 양방향 RNN을 사용했으므로 크기가 hidden_size*2이다.
- fc 레이어는 이 출력을 입력으로 받고, 단어 집합의 크기(이 예에서는 출력 클래스 개수와 동일)로 변환하여, 각 시점마다 다음 문자에 대한 확률 분포 점수를 계산한다.
■ 그러므로 이 모델의 input_size와 output_size는 다음과 같이 단어 집합의 크기로 설정한다. 그리고 위에서 softmax 레이어를 넣지 않은 이유는 손실 함수로 CrossEntropyLoss() 함수를 사용하기 위해서이다.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
vocab_size = len(char_vocab)
hidden_size = 32
model = Char_RNN(vocab_size, hidden_size, vocab_size).to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr = 0.0001)
■ 그리고 다음과 같이 모델의 예측 결과를 다시 문자로 변환하기 위해 다음과 같은 index_to_char을 정의한다.
index_to_char = {}
for index, word in enumerate(char_vocab):
index_to_char[index] = word
■ 그리고 다음과 같이 학습을 진행하였다. loss를 계산하는 과정에서 view를 사용해 예측값과 실제값(타깃)을 펼쳐 CrossEntropyLoss의 요구사항에 맞게 손실을 계산한다.
X = X.to(device)
y = y.to(device)
for i in range(2001):
optimizer.zero_grad()
output = model(X) # ouput.shape: (627, 10, 24) # 24는 클래스의 개수
# loss_function((N, C), C) = (batch_size * time steps(=seq_len), # of class), (batch_size * time steps(=seq_len))
loss = loss_function(output.view(-1, output.size(-1)), y.view(-1))
loss.backward()
optimizer.step()
predicted = output.argmax(dim=2) # dim2 <=> time step # time step마다 가장 높은 확률을 가진 정수 인덱스 선택
# predicted.shape: (627, 10)
predicted_str = ''.join([index_to_char[int(token)] for token in predicted[:, 0]])
if i % 200 == 0: print(i, ':', predicted_str);print()
```#결과#```
0 : naknoooooknnnnnooarnanoanakooanoponnaoonanaoanoonnoooknnnnkkoaoooaoonknonnonnnkononnornnonaoohnaonoooaoookannnkkooanoponnaoonnnooaooonnonanaooookoononnonknononoonnknoaoooknaooaanoonaoknanakonnonnakookoonnnnoooonnaoooaaooaoonnknnnknnaoononoannoonknaanakoooonaoaaanoooonanaoonooooooaoooanaoonanooooooaoooknnnanoonoanaoonnknnnoaoooaoonknoooannoooaoononnonknooakonnknooonooaannnnknoaoonoooknknanaonoaoooroonnoanoaaaoonknknnknnooooaooooanooaknoookkononnoaoooaoooknknoooooonaoknonnnnnaanoaonoooonpoaoooaaorknnoooaoonknnkooraaoaoooaoonanoaookannnaooaanokaoonanooaoonoannoonnaaaoananknnoaooomooaaoonnooakoanpoonooorooknnannknoaokoonknk
200 : e e e a e e e e a e a e a a e e e a e e e a a a a e e a a e e e a e e e e e a e e a e a e e e e e a a e e e e e a e a e e a a
400 : e e ran rea e re ersed ere i ees aaa ese e t ran rea ee t e att e ae e eaaen re e se e ae ran rea ere i ees i a e t ese ana ea a ta tanae rere sea a e resee ea e sere red e se a and res a re en eor t e sea aeatan sereaeee re e sea in a are in ana it ea t e t ere are e t e ran rea se ied e ee a a t e are e aen oe ta ta al and real ta e iareaea aa its erese e ere t e re re re s earanle t t e e se are a e eoe e a t e are e e a re ae ren tare e e serei a t e atan oe tan a e ri a t e se e e e rerae ear e e sereore se ree oea e ese atan s a s are aes aaaa e t e i sereal e t ana
600 : e a e ral eria e re erded oereninled nananese n t ral eria on the tattle ield rearenle rede red to as ral eria oereninled ial o tstse nanan is a ta tinal rele snarine risee tare serel red oe se a and res a ririon ior the inanstation sertatle reseased in nan are in naran it is the t erd tare in the ral eria seried e el rinl the iare e rion oe ta tinal and real tare iaresnal as its trederessers the store re d paranlel te the rirse tare and tollens the naredess a renal rin tare in t seriinl the nation ol tal ia s rina the serene e roral tar tse seriors se ret onane oserations ans are r tres anainse the i serial in t oanan
800 : enl e val oria onrecorded ooroninles napanese nit val oria on the tattlerield conronle rederred to as val oria ooroninles iis o tstse napan is a ta tinal rele pnarinl risee gare serelered ol sela and resia rision for the pla station sortanle released in nan are in napan it is the therd gare in the val oria series e elorinl the iare eosion ot ta tinal and real tare gareslal as its trederessors the store rind parallel to the tirst gare and iollons the nareless a penal rinitare onit seriinl the nation ol gallia d rinl the second e roran gar the reriors se ret olane orerations and are rithed anainst the inderial onit canan
1000 : enl ne val oria onrecorded coronicles napanese nit val oria ot the gattlerield conmonle rederred to as val oria coronicles iis o tside napan is a tactical rele plarinl ridee game derelored ol seya and redia rision for the pla station portanle released in nan are in napan it is the third game in the val oria series e plorinl the same tosion ot tactical and real tire gameplal as its tredecessors the store rind parallel to the first game and follors the nameless a penal rinitare onit seriing the nation ol gallia d rinl the second e ropan gar the periors se ret olace orerations and are pitted anainst the iaperial onit canan
...,
1800 : eng no valkyria unrecorded chronicles japanese lit valkyria of the jattlefield commonly referred to as valkyria chronicles iii outside japan is a tactical role playing video game developed by sega and media vision for the playstation portanle released in january in japan it is the third game in the valkyria series emploling the same fusion of tactical and real time gameplay as its predecessors the story runs parallel to the first game and follors the nameless a penal military unit serving the nation of gallia during the second europan jar cho perform secret black operations and are pitted against the imperial unit calam
2000 : eng no valkyria unrecorded chronicles japanese lit valkyria of the battlefield commonly referred to as valkyria chronicles iii outside japan is a tactical role playing video game developed by sega and media vision for the playstation portanle released in january in japan it is the third game in the valkyria series emploling the same fusion of tactical and real time gameplay as its predecessors the story runs parallel to the first game and follors the nameless a penal military unit serving the nation of gallia during the second europan jar cho perform secret black operations and are pitted against the imperial unit calam
````````````
■ 결과를 보면, 학습 초기에는 가중치가 무작위로 초기화되어 있기 때문에 모델은 특정 문자를 편향적으로 반복하거나 의미 없는 패턴을 생성하는 경향이 있음을 볼 수 있다.
■ 학습이 진행될수록, 모델이 데이터에서 등장한 문자의 순서를 어렴풋이 익히기 시작한다. 다만, 전체 문맥을 충분히 학습하지 못해, 원문의 일부 조각만 불완전하게 출력되는 것을 볼 수 있으며, 후반부로 갈수록 원문이 거의 복원되는 것을 볼 수 있다.
- 이와 같은 결과는 학습 데이터가 한 줄의 텍스트이며, 양방향 RNN을 사용했기 때문에 앞뒤 문맥 정보를 모두 참고했기 때문인 것으로 보인다.
'자연어처리' 카테고리의 다른 글
LSTM, GRU (2) (0) | 2025.03.29 |
---|---|
LSTM, GRU (1) (0) | 2025.03.28 |
RNN (1) (1) | 2025.03.23 |
시퀀스투시퀀스(Sequence‑to‑Sequence, seq2seq) (1) (0) | 2025.03.19 |
Subword Tokenizer - (3) Unigram Tokenization (0) | 2025.03.17 |