본 포스팅은 '밑바닥부터 시작하는 딥러닝2'를 읽고 공부 및 학습 내용을 정리한 글입니다. 언제든지 다시 참고할 수 있도록, 지식 공유보단 개인적인 복습을 목적으로 포스팅하였습니다.
7.1 언어 모델을 사용한 문장 생성
언어 모델을 여러 장에 걸쳐서 다뤄왔다. 언어 모델은 다양한 애플리케이션에서 활용할 수 있는데 대표적으로 기계 번역, 음석 인식, 문장 생성이 있다. 이번 장에서는 문장 생성을 구현하고자 한다.
7.1.1 RNN을 사용한 문장 생성의 순서
앞 장에서는 LSTM의 계층을 이용해 언어 모델을 구현했고, 이는 아래 그림과 같다. 또 이 시계열 데이터를 모아 처리하는 Time LSTM도 구현하였다.
그렇다면 이제 문장을 생성시키는 순서를 이해해보자. 늘 사용하던 "you say goodbye and i say hello" 언어 모델을 예로 들면 학습된 언어 모델에 "I"라는 단어를 입력으로 주면 어떻게 될까?
언어 모델은 지금까지 주어진 단어들에서 다음에 출현하는 단어의 확률 분포를 보여준다. 위 그림을 보면, I라는 단어를 주었을 때 나타나는 확률 분포를 알 수 있다. 이 결과를 바탕으로 다음 단어를 생성하려면 어떻게 해야할까?
1. 확률이 가장 높은 단어를 선택함으로써 결과가 일정하게 정해지는 '결정적'방법
2. 각 후보 단어의 확률에 맞게 선택하는 '확률적'방법
책에서는 확률적 방법을 선택한다. 그렇다면 확률적 방법은 어떻게 처리되는지 알아보자.
위 그림은 확률분포로부터 샘플링 수행의 결과로 "say"가 선택된 경우를 보여준다. 실제로 확률 분포에서 "say"의 확률이 가장 높기 때문에 "say"가 샘플링될 확률이 가장 높다. 다만 이는 필연적(=결정적)이 아니고 '확률적'으로 결정된다는 것에 주의해야 한다.
계속해서 두 번째 단어를 샘플링해보자. 즉 방금 생성한 단어인 "say"를 언어 모델에 입력해 다음 단어의 확률분포를 얻으면 된다. 이렇게 차근차근 진행하면서 원하는 만큼 반복하거나 <eos>와 같이 종결 기호가 나타날 때까지 반복하면 된다. 이렇게 처리할 경우 새로운 문장을 생성할 수 있다.
7.1.2 문장 생성 구현
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
RanlmGen 클래스를 통해 코드를 실행해보면 아래와 같은 결과를 얻을 수 있다.
다만 이는 실행할 때 마다 달라지는 것을 명심해야 한다.
7.1.3 더 좋은 문장으로
일전에 우린 챕터 6장에서 더 좋은 RNNLM을 구현했다 이를 사용하여 더 좋은 언어 모델로 문장을 생성해보도록 하자.
7.2 seq2seq
세상에는 언어, 음성, 동영상 등 시계열 데이터가 넘쳐 난다. 그리고 이 데이터를 또 다른 데이터로 변환하는 문제도 숱하게 생각할 수 있다. 지금부터 우리는 시계열 데이터를 다른 시계열 데이터로 변환하는 모델에 대해서 알아보고 이 기법으로 2개의 RNN을 이용하는 seq2seq 방법에 대해서 알아볼 것이다.
7.2.1 seq2seq의 원리
seq2seq는 Encoder-Decoder 모델이라고 불리기도 하는데 이는 이름에서 말해주듯 2개의 모듈 인코더와 디코더가 등장한다. 인코딩은 부호화를 뜻하고 디코딩은 인코딩된 데이터를 복호화하는 것이다. 예를 들면 아래 이미지와 같다.
그림 7-6처럼 인코더는 RNN을 통해 시계열 데이터 h를 은닉 상태 벡터로 변환하는데, 이 벡터 h는 LSTM 계층의 마지막 은닉 상태이다. 또한 마지막 은닉 상태 h에는 입력 문장(출발어)를 번역하는데 필요한 정보가 인코딩되어 있으며 고정 길이 벡터의 값을 가진다.
그렇다면 디코딩은 이를 어떻게 처리할까?
디코딩은 앞 절의 신경망과 완전히 같은 구성이다. 그러나 한가지가 다른데 바로 LSTM 계층이 벡터 h를 받는다는 것이 다르다. 이 사소한 차이가 언어 모델을 번역도 해낼 수 있는 디코더로 탈바꿈시켜준다.
7.2.2 시계열 데이터 변환용 장난감 문제
seq2seq를 실제로 구현해보면 더 좋을 테니 시계열 변환 문제 '더하기'로 예를 들어보자. 이는 "57+5"와 같은 문자열을 seq2seq에 건내면 "62"라는 답을 내놓도록 학습시키는 것이다.
그런데 우리는 지금까지 word2vec나 언어 모델에서 사용하는 문장을 '단어' 단위로 분할했는데 지금은 '문자'단위로 분할해야 한다. 이를 어떻게 처리해야 할까?
7.2.3 가변 길이 시계열 데이터
'덧셈' 문제는 샘플마다 데이터의 크기가 다르기 때문에 '가변 길이 시계열 데이터'를 다룬다. 따라서 이를 학습하기 위해서는 무언가 추가 노력이 필요하게 된다. 가장 단순한 방법은 '패딩'으로 원래 데이터에 의미 없는 데이터를 채워 모든 데이터의 길이를 균일하게 맞추는 것이다.
패딩을 수행해 입력 데이터와 출력 데이터에 길이를 통일시켜 처리한다.
7.2.4 덧셈 데이터셋
github의 dataset/addition.txt 파일에 50,000개 덧셈 예가 들어있다. 이를 통해 테스트 할 수 있다. 이처럼 시퀸스 모듈을 이용하면 seq2seq용 데이터를 간단히 읽어 들일 수 있다. 여기서 x_train, t_train에는 '문자 ID'가 저장되어있으며, 문자 ID와 문자 대응 관계는 char_to_id와 id_to_char를 이용해 상호 변환 가능하다.
7.3 seq2seq 구현
7.3.1 Encoder 클래스
인코더 클래스틑 문자열을 받아 벡터 h로 변환하고, RNN을 통해 인코더를 구성하고 있으므로 아래와 같이 표시할 수 있다.
그러나 마찬가지로 시간 방향을 한꺼번에 처리하는 계층 Time LSTM이나 Time Embedding처럼 이 또한 Time Eoncoder를 구현할 수 있다.
7.3.2 Decoder 클래스
디코더 클래스는 인코더 클래스가 출력한 h를 받아 목적으로 하는 다른 문자열을 출력한다. 이 또한 RNN을 통해 구성되어 있으므로 아래와 같이 표시할 수 있다.
그런데 7.1절에서 문장을 생성할 때에는 소프트맥스의 확률 분포를 바탕으로 샘플링을 수행했기 때문에 생성되는 문장이 확률에 따라 달라졌지만, 이번에는 '덧셈' 문제를 해결하기 위해 확률적인 '비결정성'을 배제하고 '결정적'인 답을 생성하고자 한다. 즉 '확률적'이 아닌 '결정적'을 선택하여 처리하는 것이다. 따라서 디코딩 클래스는 아래와 같이 구성한다.
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) 기울기를 사용하여 매개변수를 갱신한다.
또한 1.4.4에서 설명한 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
seq2seq는 초기에 정답을 잘 못맞추지만 학습이 진행됨에 따라 점점 정답률이 상승하는 것을 볼 수 있다.
7.4 seq2seq 개선
7.4.1 입력 데이터 반전
입력 데이터의 순서를 반전시킬 경우 학습 진행이 빨라지고 이를 토대로 정확도도 좋아진다.
그렇다면 왜 데이터를 반전시키는 것만으로 학습의 진행이 빨라지고 정확도가 향상될까? 이는 직관적으로 기울기 전파가 원할해지기 때문이다. 예를 들어 "나는 고양이로소이다"를 "I am a cat"으로 번역하는 문제에서, "나"라는 단어가 "I"로 변환되는 과정을 생각해보면 "나"로부터 "I"까지는 "는", "고양이", "로소", "이다"를 모두 거쳐야 하지만, 역전파로 시작할 시 "I"로 부터 전해지는 기울기가 "나"에 도달하기까지 거리의 영향을 더 받게 되기 때문이다.
7.4.2 엿보기
고정 길이 벡터 h는 디코더에게 필요한 모든 정보가 모두 담겨 있지만 현재는 LSTM 계층만이 벡터 h를 이용하고 있다. 이 h를 디코더의 다른 계층에게도 전해주는 것을 엿보기라 한다.
그렇지만 이는 가중치 매개변수가 켜져서 계산량이 늘어나는 것을 주의해야 한다. 하지만 이를 조합하여 나타나는 에폭시의 값은 매우 높게 나타난다.
7.5 seq2seq를 이용하는 애플리케이션
기계 번역 / 자동 요약 / 질의 응답 / 메일 자동 응답 등의 시계열 데이터 변환 문제에 다양하게 적용될 수 있다.
7.5.1. 챗봇
7.5.2. 알고리즘 학습 (파이썬 소스코드 등)
7.5.3. 이미지 캡셔닝 : 이미지를 문장으로 변환.
7.6 정리
* RNN을 이용한 언어 모델은 새로운 문장을 생성할 수 있다.
* 문장을 생성할 때는 하나의 단어(혹은 문자)를 주고 모델의 출력(확률분포)에서 샘플링하는 과정을 반복한다.
* RNN을 2개 조합함으로써 시계열 데이터를 다른 시계열 데이터로 변환할 수 있다.
* seq2seq는 Encoder가 출발어 입력문을 인코딩하고, 인코딩된 정보를 Decoder가 받아 디코딩하여 도착어 출력문을 얻는다.
* 입력문을 반전시키는 기법(Reverse), 또는 인코딩된 정보를 Decoder의 여러 계층에 전달하는 기법(Peeky)는 seq2seq 정확도 향상에 효과적이다.
7.7 소감
이제 RNN을 통해 정말로 새로운 문장까지 생성해보면서 책을 읽는 것 말고 별다른 학습(?)없이도 이런 문장 생성이 된다는게 너무 신기했다. RNN에서는 조금 벅찼지만 7장에서는 RNN을 이용해 이러이러한 것들도 더 할수 있다. 라는 내용이여서 조금 읽기 수월했다. 8장 어텐션이 핵심으로 보여지는데 해당 내용이 무척이나 궁금하고 기대된다. ❣️
'공부 > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글
[밑시딥2] Chapter 6. 게이트가 추가된 RNN (0) | 2024.07.29 |
---|---|
[밑시딥2] Chapter 5. 순환 신경망(RNN) (1) | 2024.07.23 |
[밑시딥2] Chapter 4. word2vec 속도 개선 (0) | 2024.07.16 |
[밑시딥2] Chapter 3. word2vec (0) | 2024.07.08 |
[밑시딥2] Chapter 2. 자연어와 단어의 분산 표현 (1) | 2024.07.01 |