7.1 언어 모델을 사용한 문장 생성
7.1.1 RNN을 사용한 문장 생성의 순서

- LSTM의 신경망 구성은 위와 같다. 이제 언어 모델에게 문장을 생성시키는 순서를 설명해보도록 하자.

- "You say goodbye and I say hello" 말뭉치가 있을 때 학습된 언어모델에 "I"라는 단어를 입력으로 주면 다음과 같은 확률분포를 출력한다.
- 다음 단어 생성 방법으로는 (1) 확률이 가장 높은 단어를 선택하는 "결정적" 방법, (2) 각 후보 단어의 확률에 맞게 선택하는 "확률적" 방법이 있다. 이 중 두번째를 선택해보도록 하자.
- 확률분포로부터 샘플링을 수행한 결과 "say"가 선택되었다. 다만 다른 단어들도 출현확률에 따라 샘플링될 가능성이 존재한다. (다양한 답변 생성 가능) 두 번째 단어도 방금 생성된 "say"를 언어모델에 입력해 다음 단어의 확률분포를 얻고 그에 기반해 단어를 샘플링한다. 이 과정을 원하는 만큼 반복해서 새로운 문장을 생성할 수 있다. 만약 언어 모델이 말뭉치로부터 단어의 출현 패턴을 올바르게 학습할 수 있다면, 새로 생성한 문장 역시 인간에게 친숙하고 자연스러운 형태의 문장이 될 것이다.
7.1.2 문장 생성 구현
- 앞 장에서 구현한 Rnnlm class를 상속해 문장 생성 메서드를 추가하겠다.
class RnnlmGen(Rnnlm):
def generate(self, start_id, skip_ids=None, sample_size=100):
word_ids = [start_id]
x = start_id
while len(word_ids) < sample_size:
x = np.array(x).reshape(1, 1)
score = self.predict(x)
p = softmax(score.flatten())
sampled = np.random.choice(len(p), size=1, p=p)
if (skip_ids is None) or (sampled not in skip_ids):
x = sampled
word_ids.append(int(x))
return word_ids
- start_id : 최초로 주는 단어의 ID, sample_size : 샘플링하는 단어의 수, skip_ids : 샘플링되지 않을 단어의 Id List
- model.predict(x), p = softmax(score)를 통해 정규화된 확률분포 p를 얻을 수 있으며 이로부터 다음 단어를 샘플링한다. 가중치 매개변수가 무작위 초깃값인 상태에서(학습 X 상태에서) "you" 단어로부터 시작해서 문장을 생성하면 엉터리로 단어들을 나열한 글을 출력한다. 반면, 학습을 수행한 언어모델을 활용하면 다음과 같은 문장을 생성할 수 있다 :

- 문법적으로 이상한 문장도 있지만, 그럴듯한 문장도 만들어 준다. 더 자연스러운 결과를 위해서는 더 나은 언어모델을 활용하면 된다.
7.1.3 더 좋은 문장으로
- Chapter 6에서 BetterRnnlm을 구현한 바가 있다. 그 결과를 비교해 보자. (LSTM을 더 깊게 쌓고, Dropout, 가중치 공유 방법론 활용) 위의 결과보다는 더 자연스러운 문장으로 보인다. 이처럼 언어모델을 개선하면 더 자연스러운 문장 생성이 가능하다.

7.2 seq2seq
7.2.1 seq2seq의 원리
- seq2seq = Encoder-Decoder 모델이라고도 부른다. Encoder : 입력 데이터를 인코딩(부호화) 하고 Decoder는 인코딩된 데이터를 디코딩(복호화)한다.
- ex) '나는 고양이로소이다' 라는 문장이 있다면 Encoder를 통해 문장이 인코딩된다. 이후 인코딩 된 정보를 Decoder에 전달하고, Decoder가 도착어 문장을 생성한다. 이때 Encoder가 인코딩한 정보에는 번역에 필요한 정보가 조밀하게 응축되어있고 Decoder는 이 정보를 바탕으로 도착어 문장을 생성한다. Encoder와 Decoder에는 각각 RNN을 사용할 수 있다. 그 과정을 자세히 살펴보자.

- Encoder는 RNN을 이용해 시계열 데이터를 $\mathbf{h}$라는 은닉 상태로 변환한다. RNN/LSTM/단순한RNN/GRU 모두 활용 가능하다.
- Encoder의 최종 출력 벡터 $\mathbf{h}$는 LSTM 계층의 마지막 은닉 상태이다. 이 마지막 은닉 상태에 입력 문장(출발어)을 번역하는데 필요한 정보가 인코딩된다. 여기서 중요한 점은 LSTM의 은닉 상태 $\mathbf{h}$는 고정 길이 벡터라는 사실이다. 따라서 "인코딩" = 임의 길이 문장을 고정 길이 벡터로 변환하는 작업이 된다.


- Decoder는 벡터 $\mathbf{h}$를 입력으로 받는다는 점 외에는 앞 절 신경망과 완전히 같은 구성이다. 참고로, 앞 절의 언어모델에서는 LSTM 계층이 아무것도 받지 않았다.

- 다음은 Decoder와 Encoder를 연결한 계층 구성이다. seq2seq는 LSTM 두 개(Encoder의 LSTM, Decoder의 LSTM)로 구성된다. 이때 LSTM 계층의 은닉 상태가 Encoder와 Decoder를 이어주는 '가교'가 된다. 순전파 때는 Encoder에 인코딩 된 정보가 LSTM 계층의 은닉 상태를 통해 Decoder에 전해진다. 그리고 seq2seq의 역전파 때는 이 '가교'를 통해 Decoder로부터 Encoder로 전해진다.
7.2.2 시계열 데이터 변환용 Toy Example
- seq2seq 실제 구현을 위한 Toy example로 "더하기"를 다루어보자. "57 + 5" 문자열을 "62" 정답을 내놓도록 학습시켜보자. seq2seq는 덧셈에 대해 전혀 모른다. 따라서 덧셈의 예(샘플)로부터 사용되는 문자의 패턴을 학습한다. 이런 식으로 덧셈의 규칙을 올바르게 학습할 수 있는지를 살펴보도록 하자.
7.2.3 가변 길이 시계열 데이터
- '덧셈'을 문자(숫자)의 리스트로써 다루게 되면 각 예시들의 문자 수가 문제마다 달라진다. 이런 '가변 길이 시계열 데이터'를 다룰 때 신경망 학습 시에 '미니배치 처리'를 하기 위해서 추가적인 작업이 필요하다.
- Padding(패딩) : 기존 데이터에 의미없는 데이터(ex-공백)를 채워 모든 데이터의 길이를 균일하게 맞추는 방법.

- 이번 문제에서는 0 ~ 999 사이의 숫자 2개만 더하기로 하자. 따라서 '+'까지 포함 시 입력의 최대 문자 수는 7, 덧셈 결과는 최대 4 문자가 되다. 정답 데이터에도 패딩을 수행해 모든 샘플 데이터의 길이를 통일했으며, 질문과 정답을 구분하기 위해 출력 앞에 구분자 밑줄(_)을 붙인다. 이 구분자는 Decoder에 문자열을 생성하라고 알리는 신호로 사용된다.
- 다만 원래 존재하지 않던 패딩용 문자를 처리하게 되었기 때문에 패딩 전용 처리가 필요하다. Decoder에 입력된 데이터가 패딩이라면 손실의 결과에 반영하지 않도록 하기 (Softmax with Loss 계층에 '마스크' 기능을 추가해 해결), Encoder에 입력된 데이터가 패딩이라면 LSTM 계층이 이전 시각의 입력을 그대로 출력하게 하기 등이 있다. 따라서 LSTM은 마치 처음부터 패딩이 존재하지 않았던 것처럼 인코딩할 수 있다.
7.2.4 덧셈 데이터셋
- github의 dataset/addition.txt에 50,000개 덧셈 예가 들어있다. 구체적으로 데이터를 불러오는 코드는 github 를 참고하자.
- x_train, t_train에는 '문자 ID'가 저장되어있으며 문자 ID와 문자 대응 관계는 char_to_id와 id_to_char를 이용해 상호 변환 가능하다.
7.3 seq2seq 구현
7.3.1 Encoder 클래스

- Encoder class는 문자열 ["5", "7", "+", "5", "", "", ""]를 입력으로 받아 벡터 $\mathbf{h}$로 변환한다. Encoder 클래스는 Embedding 계층과 LSTM 계층으로 구성되어있다. Embedding 계층에서는 문자ID를 문자 벡터로 변환하며 이 벡터가 LSTM 계층으로 입력된다.
- LSTM 계층은 오른쪽(시간 방향)으로는 은닉 상태(h)와 셀(c)을 출력하고, 위쪽으로는 은닉상태만 출력한다. 이 구성에서 더 위에는 다른 계층이 없으니 LSTM 계층의 위쪽 출력은 폐기된다. 마지막 문자를 처리한 뒤 나온 결과물 은닉 상태 $\mathbf{h}$가 최종적으로 Decoder로 전달된다.
- 시간 방향을 한꺼번에 처리하는 계층인 TimeEmbedding, TimeLSTM 계층을 사용하면 우리의 인코더는 다음과 같게 된다 :

class Encoder:
def __init__(self, vocab_size, wordvec_size, hidden_size):
V, D, H = vocab_size, wordvec_size, hidden_size
rn = np.random.randn
embed_W = (rn(V, D)/100).astype('f')
lstm_Wx = (rn(D, 4*H)/np.sqrt(D)).astype('f')
lstm_Wh = (rn(H, 4*H)/np.sqrt(H)).astype('f')
lstm_b = np.zeros(4*H).astype('f')
self.embed = TimeEmbedding(embed_W)
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False)
self.params = self.embed.params + self.lstm.params
self.grads = self.embed.grads + self.lstm.grads
self.hs = None
def forward(self, xs):
xs = self.embed.forward(xs)
hs = self.lstm.forward(xs)
self.hs = hs
return hs[:, -1, :]
def backward(self, dh):
dhs = np.zeros_like(self.hs)
dhs[:, -1, :] = dh
dout = self.lstm.backward(dhs)
dout = self.embed.backward(dout)
return dout
- __init__ : vocab_size, wordvec_size, hidden_size를 받는다. 본 예제에서는 0~9까지의 숫자와 '+', ' ', '_' 총 13가지 문자를 사용한다. wordvec_size는 문자 벡터의 차원 수, hidden_size는 LSTM 계층의 은닉 상태 벡터의 차원 수를 의미한다. 여기에서 가중치 매개변수를 초기화하고 필요한 계층을 생성한다. 마지막으로 가중치 매개변수와 기울기를 params와 grads를 리스트에 각각 보관한다. 그리고 이번에는 Time LSTM 계층이 상태를 유지하지 않기 때문에 stateful = False로 설정한다. (짧은 시계열 데이터가 여러 개인 문제이기 때문에 문제마다 LSTM의 은닉 상태를 다시 초기화한 상태로 설정해야 한다.)
- forward : TimeEmbedding 계층과 TimeLSTM 계층의 forward()를 호출해 마지막 시각의 은닉 상태만을 추출해 Encder의 출력으로 반환한다.
- backward : LSTM 계층의 마지막 은닉 상태에 대한 기울기가 dh 인수로 전해지며 역전파에서는 원소가 모두 0인 텐서 dhs를 설정하고 dh를 dhs의 해당 위치에 할당해 backward()를 호출한다.
7.3.2 Decoder 클래스

- Encoder 클래스가 출력한 $\mathbf{h}$를 받아 다른 문자열을 출력한다.

- 입력 데이터를 ['_', '6', '2', ' ']로 주고 대응하는 출력은 ['6', '2', ' ', ' ']가 되도록 학습시킨다.
- 이번 문제는 덧셈이므로 '결정적'인 답을 생성해야 하기 때문에 추론시(새로운 문자열을 생성할 때) '결정적'으로 선택하도록 한다.

- 따라서 'argmax' : 최댓값을 가진 원소의 인덱스를 선택하도록 추론한다. Softmax 계층을 사용하지 않고 Affine 계층이 출력하는 점수가 가장 큰 문자 ID를 선택한다.
- 이처럼 Decoder에서는 학습시와 생성시에 softmax 계층을 다르게 취급하기 때문에 Decoder class는 Time Softmax with Loss 계층의 앞까지만 담당하고 이후 계층은 Seq2seq 클래스에서 처리하도록 하자.

class Decoder:
def __init__(self, vocab_size, wordvec_size, hidden_size):
V, D, H = vocab_size, wordvec_size, hidden_size
rn = np.random.randn
embed_W = (rn(V, D)/100).astype('f')
lstm_Wx = (rn(D, 4*H)/np.sqrt(D)).astype('f')
lstm_Wh = (rn(H, 4*H)/np.sqrt(H)).astype('f')
lstm_b = np.zeros(4*H).astype('f')
affine_W = (rn(H, V)/np.sqrt(H)).astype('f')
affine_b = np.zeros(V).astype('f')
self.embed = TimeEmbedding(embed_W)
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
self.affine = TimeAffine(affine_W, affine_b)
self.params, self.grads = [], []
for layer in (self.embed, self.lstm, self.affine):
self.params += layer.params
self.grads += layer.grads
def forward(self, xs, h):
self.lstm.set_state(h)
out = self.embed.forward(xs)
out = self.lstm.forward(out)
score = self.affine.forward(out)
return score
def backward(self, dscore):
dout = self.affine.backward(dscore)
dout = self.lstm.backward(dout)
dout = self.embed.backward(dout)
dh = self.lstm.dh
return dh
def generate(self, h, start_id, sample_size):
sampled = []
sample_id = start_id
self.lstm.set_state(h)
for _ in range(sample_size):
x = np.array(sample_id).reshape((1, 1))
out = self.embed.forward(x)
out = self.lstm.forward(out)
score = self.affine.forward(out)
sample_id = np.argmax(score.flatten())
sampled.append(int(sample_id))
return sampled
- backward() 메서드는 위쪽의 softmax with loss 계층으로부터 기울기 dscore를 받아 Time Affine 계층, Time LSTM 계층, Time Embedding 계층 순서로 전파시킨다. 이때 Time LSTM 계층의 시간 방향으로의 기울기는 Time LSTM 클래스의 인스턴스 변수 dh에 저장되어있다. 그래서 이 시간 방향의 기울기 dh를 꺼내 Decoder 클래스의 backward() 출력으로 반환한다.
- generate() : Encoder로부터 받는 은닉 상태인 h, 최초로 주어지는 문자 ID인 start_id, 생성하는 문자 수인 sample_size를 인수로 받아 Affine 계층이 출력하는 가장 점수가 큰 문자 ID를 선택하는 작업을 반복한다.
- 이번에는 Encoder의 출력 h를 Decoder의 Time LSTM 계층의 상태로 설정(stateful=True)했다. 단, 한 번 설정된 이 은닉 상태는 재설정되지 않고, 즉 Encoder의 h를 유지하면서 순전파가 이뤄진다.
7.3.3 Seq2seq 클래스
- seq2seq 클래스에서는 Encoder와 Decoder class를 연결하고, Time softmax with loss 계층을 이용해 손실을 계산한다.
class Seq2seq(BaseModel):
def __init__(self, wordvec_size, hidden_size):
V, D, H = vocab_size, wordvec_size, hidden_size
self.encoder = Encoder(V, D, H)
self.decoder = Decoder(V, D, H)
self.softmax = TimeSoftmaxWithLoss()
self.params = self.encoder.params + self.decoder.params
self.grads = self.encoder.grads + self.decoder.grads
def forward(self, xs, ts):
decoder_xs, decoder_ts = ts[:, :-1], ts[:, 1:]
h = self.encoder.forward(xs)
score = self.decoder.forward(decoder_xs, h)
loss = self.softmax.forward(score, decoder_ts)
return loss
def backward(self, dout=1):
dout = self.softmax.backward(dout)
dh = self.decoder.backward(dout)
dout = self.encoder.backward(dh)
return dout
def generate(self, xs, start_id, sample_size):
h = self.encoder.forward(xs)
sampled = self.decoder.generate(h, start_id, sample_size)
return sampled
7.3.4 seq2seq 평가
- seq2seq의 학습 :
(1) 학습 데이터에서 미니배치를 선택,
(2) 미니배치로부터 기울기를 계산하고,
(3) 기울기를 사용하여 매개변수를 갱신한다. - Trainer 클래스를 사용해 구현하고, 매 에폭마다 seq2seq가 test data를 풀게 해 중간마다 정답률을 측정한다.
# 데이터셋 읽기
(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt')
char_to_id, id_to_char = sequence.get_vocab()
# 입력 반전 여부 설정 =============================================
is_reverse = False # True
if is_reverse:
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]
# ================================================================
# 하이퍼파라미터 설정
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 128
batch_size = 128
max_epoch = 25
max_grad = 5.0
# 일반 혹은 엿보기(Peeky) 설정 =====================================
model = Seq2seq(vocab_size, wordvec_size, hidden_size)
# model = PeekySeq2seq(vocab_size, wordvec_size, hidden_size)
# ================================================================
optimizer = Adam()
trainer = Trainer(model, optimizer)
acc_list = []
for epoch in range(max_epoch):
trainer.fit(x_train, t_train, max_epoch=1,
batch_size=batch_size, max_grad=max_grad)
correct_num = 0
for i in range(len(x_test)):
question, correct = x_test[[i]], t_test[[i]]
verbose = i < 10
correct_num += eval_seq2seq(model, question, correct,
id_to_char, verbose, is_reverse)
acc = float(correct_num) / len(x_test)
acc_list.append(acc)
print('검증 정확도 %.3f%%' % (acc * 100))
def eval_seq2seq(model, question, correct, id_to_char,
verbos=False, is_reverse=False):
correct = correct.flatten()
# 머릿글자
start_id = correct[0]
correct = correct[1:]
guess = model.generate(question, start_id, len(correct))
# 문자열로 변환
question = ''.join([id_to_char[int(c)] for c in question.flatten()])
correct = ''.join([id_to_char[int(c)] for c in correct])
guess = ''.join([id_to_char[int(c)] for c in guess])
if verbos:
if is_reverse:
question = question[::-1]
colors = {'ok': '\033[92m', 'fail': '\033[91m', 'close': '\033[0m'}
print('Q', question)
print('T', correct)
is_windows = os.name == 'nt'
if correct == guess:
mark = colors['ok'] + '☑' + colors['close']
if is_windows:
mark = 'O'
print(mark + ' ' + guess)
else:
mark = colors['fail'] + '☒' + colors['close']
if is_windows:
mark = 'X'
print(mark + ' ' + guess)
print('---')
return 1 if guess == correct else 0
- 에폭마다 테스트 데이터 문제 중 몇 개를 풀게 하여 올바르게 답했는지를 채점했다.
- eval_seq2seq는 문제(question)를 모델에 주고 문자열을 생성하게 하여 그것이 답과 같은지를 판정한다.

- Q: 600 + 257 줄이 문장, 아래의 T 857이 정답이다. 그리고 x 864가 모델이 내놓은 답이다. 학습이 진행됨에 에폭마다 정답률이 향상되는 것을 코드를 실행하면 알 수 있다. 하지만 여전히 seq2seq를 더 개선할 수 있는 여지가 남아있다.

7.4 seq2seq 개선
이번 장에서는 seq2seq에서 학습 '속도'를 개선할 수 있는 두가지 기법을 소개한다.
7.4.1 입력 데이터 반전(Reverse)

- 입력 데이터의 순서를 반전시키는 방법. 학습 진행이 빨라져서 최종 정확도도 좋아진다고 한다. 코드로는
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]

- 입력 데이터를 반전시킨 것 만으로도 정답률이 많이 개선되었다. 이는 기울기 전파가 원활해지기 때문이다.
- 예를 들면 '나는 고양이로소이다' -> 'I am a cat' 번역 문제에서 '나' -> 'I'로 변환하려면 '는', '고양이', '로소', '이다'까지 총 4 단어 분량의 LSTM 계층을 거쳐야 한다. 따라서 역전파 시 'I'에서 전해지는 기울기가 그 먼 거리만큼 영향을 받는다. 여기에서 입력문을 반전시키면, '이다 로소 고양이 는' 순이 되면 '나'와 'I'가 바로 옆에 위치해 기울기를 직접 전달할 수 있다.
- 다만 입력 데이터를 반전해도 단어 사이의 '평균적인' 거리는 그대로이다.
7.4.2 엿보기(Peeky)
- Seq2seq의 Encder는 입력 문장을 고정 길이 $\mathbf{h}$로 변환하며 이는 Decoder가 받을 수 있는 유일한 정보이다. 하지만 이 유용한 정보를 최초 시각의 LSTM 계층만 활용하게 되는데, 이를 더 활용할 수 있는 방안이 Peeky(엿보기) 방안이다. 즉, 중요한 정보가 담긴 Encoder의 출력 $\mathbf{h}$를 Decoder의 다른 계층에도 전해주는 것이다.

- 이렇게 모든 시각의 Affine 계층과 LSTM 계층에 Encoder의 출력 $\mathbf{h}$를 전해주면 정보를 공유하는 효과가 있어 더 올바른 결정을 내릴 가능성이 커진다.
- 그런데 그림에서 이제 LSTM 계층과 Affine 계층에 입력되는 벡터가 2개씩이 되었다. 이는 실제로는 두 벡터가 연결(concatenate) 된 것을 의미하며 따라서 두 벡터를 연결하는 concat 노드를 이용해 그리면 다음과 같이 최종적인 구조도를 그릴 수 있다.

class PeekyDecoder:
def __init__(self, vocab_size, wordvec_size, hidden_size):
V, D, H = vocab_size, wordvec_size, hidden_size
rn = np.random.randn
embed_W = (rn(V, D)/100).astype('f')
lstm_Wx = (rn(D + H, 4*H)/np.sqrt(D+H)).astype('f')
lstm_Wh = (rn(H, 4*H)/np.sqrt(H)).astype('f')
lstm_b = np.zeros(4*H).astype('f')
affine_W = (rn(H + H, V)/np.sqrt(H+H)).astype('f')
affine_b = np.zeros(V).astype('f')
self.embed = TimeEmbedding(embed_W)
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True)
self.affine = TimeAffine(affine_W, affine_b)
self.params, self.grads = [], []
for layer in (self.embed, self.lstm, self.affine):
self.params += layer.params
self.grads += layer.grads
self.cache = None
def forward(self, xs, h):
N, T = xs.shape
N, H = h.shape
self.lstm.set_state(h)
out = self.embed.forward(xs)
hs = np.repeat(h, T, axis=0).reshape(N, T, H)
out = np.concatenate((hs, out), axis=2)
out = self.lstm.forward(out)
out = np.concatenate((hs, out), axis=2)
score = self.affine.forward(out)
self.cache = H
return score
def backward(self, dscore):
H = self.cache
dout = self.affine.backward(dscore)
dout, dhs0 = dout[:, :, H:], dout[:, :, :H]
dout = self.lstm.backward(dout)
dembed, dhs1 = dout[:, :, H:], dout[:, :, :H]
self.embed.backward(dembed)
dhs = dhs0 + dhs1
dh = self.lstm.dh + np.sum(dhs, axis=1)
return dh
def generate(self, h, start_id, sample_size):
sampled = []
char_id = start_id
self.lstm.set_state(h)
H = h.shape[1]
peeky_h = h.reshape(1, 1, H)
for _ in range(sample_size):
x = np.array([char_id]).reshape((1, 1))
out = self.embed.forward(x)
out = np.concatenate((peeky_h, out), axis=2)
out = self.lstm.forward(out)
out = np.concatenate((peeky_h, out), axis=2)
score = self.affine.forward(out)
char_id = np.argmax(score.flatten())
sampled.append(char_id)
return sampled
- 초기화 : LSTM계층과 Affine 계층의 가중치 형상이 다르다.
- forward() : h를 np.repeat()로 시계열만큼 복제해서 hs에 저장한다. 그리고 np.concatenate()를 이용해 그 hs와 Embedding 계층의 출력을 연결하고, 이를 LSTM 계층에 입력한다. Affine 계층에서도 hs와 LSTM의 출력을 연결한 것을 입력한다.
마지막으로 PeekySeq2seq 구현을 살펴보자. 기존의 Seq2seq에서 Decoder 클래스를 PeekyDecoder로 바꾸어주면 나머지는 동일하다. 따라서 Seq2seq 클래스를 계승하고 초기화 부분만 변경해주면 된다.
from seq2seq import Seq2seq, Encoder
class PeekySeq2seq(Seq2seq):
def __init__(self, vocab_size, wordvec_size, hidden_size):
V, D, H = vocab_size, wordvec_size, hidden_size
self.encoder = Encoder(V, D, H)
self.decoder = PeekyDecoder(V, D, H)
self.softmax = TimeSoftmaxWithLoss()
self.params = self.encoder.params + self.decoder.params
self.grads = self.encoder.grads + self.decoder.grads
- 앞선 덧셈 문제 풀이를 PeekySeq2seq 모델을 사용하고, 첫번째 개선인 Reverse(입력 반전)도 적용하면 다음과 같이 정확도에서의 큰 개선을 확인할 수 있다 :

- 다만 Peeky를 이용하게 되면 가중치 매개변수가 커져서 계산량도 늘어난다. 또한 hyperparameter에 크게 영향을 받는다는 점 또한 주의해야 한다.
7.5 seq2seq를 이용하는 애플리케이션
- 기계 번역 / 자동 요약 / 질의 응답 / 메일 자동 응답 당의 시계열 데이터 변환 문제에 다양하게 적용될 수 있다.
- 챗봇
- 알고리즘 학습 (파이썬 소스코드 등)
- 이미지 캡셔닝 : 이미지를 문장으로 변환.

- Encoder가 LSTM에서 CNN으로 바뀐것 외에는 앞서 본 예제와 동일하다!
- 다만 이미지 feature map은 3차원이기 때문에 1차원으로 flatten 시킨 후 Affine 계층에서 LSTM이 처리할 수 있는 형태로 손질한다.
7.6 정리
- RNN을 이용한 언어 모델은 새로운 문장을 생성할 수 있다.
- 문장을 생성할 때는 하나의 단어(혹은 문자)를 주고 모델의 출력(확률분포)에서 샘플링하는 과정을 반복한다.
- RNN을 2개 조합함으로써 시계열 데이터를 다른 시계열 데이터로 변환할 수 있다.
- seq2seq는 Encoder가 출발어 입력문을 인코딩하고, 인코딩된 정보를 Decoder가 받아 디코딩하여 도착어 출력문을 얻는다.
- 입력문을 반전시키는 기법(Reverse), 또는 인코딩된 정보를 Decoder의 여러 계층에 전달하는 기법(Peeky)는 seq2seq 정확도 향상에 효과적이다.
'Deep Learning' 카테고리의 다른 글
| [밑바닥부터 시작하는 딥러닝2] Chapter 8. 어텐션 (2) | 2024.08.05 |
|---|---|
| [밑바닥부터 시작하는 딥러닝2] Chapter 6. 게이트가 추가된 RNN (9) | 2024.07.22 |
| [밑바닥부터 시작하는 딥러닝2] Chapter 5. 순환 신경망(RNN) (0) | 2024.07.15 |
| [밑바닥부터 시작하는 딥러닝2] Chapter 4. Word2Vec 속도 개선 (3) | 2024.07.08 |
| [밑바닥부터 시작하는 딥러닝2] Chapter 3. Word2Vec (1) | 2024.07.02 |