목차
👀, 🤷♀️ , 📜
이 아이콘들을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
INTRO
언어 모델을 사용해 ‘문장 생성’을 수행.
1) 구체적으로는 우선 말뭉치를 사용해 학습한 언어 모델을 이용하여 새로운 문장을 만들어냄
2) 그런 다음 개선된 언어 모델을 이용하여 더 자연스러운 문장을 생성
3) seq2seq라는 새로운 구조의 신경망도 다룸.
seq2seq
- “(from ) sequence to sequence (시계열에서 시계열로)”를 뜻하는 말로, 한 시계열 데이터를 다른 시계열 데이터로 변환하는 것
- 이번 장에서는 RNN 두 개를 연결하는 아주 간단한 방법
언어 모델을 사용한 문장 생성
- LSTM 계층을 이용하여 언어 모델을 구현
- 그리고 시계열 데이터를 (T개분 만큼) 모아 처리하는 Time LSTM과 Time Affine 계층 등을 만듦
[RNN을 사용한 문장 생성의 순서]
ex) “you say goodbye and I say hello.”라는 말뭉치로 학습한 언어 모델
[1단계]
입력: “I”
출력: 밑과 같은 확률분포를 출력
[2단계]
다음 단어 새로 생성
생성 방법
- ‘결정적’ deterministic 인 방법
- 확률이 가장 높은 단어를 선택하는 방법(선택지가 하나)
- 결과가 일정하게 정해짐: (알고리즘의) 결과가 하나로 정해지는 것, 결과가 예측 가능한 것을 말합니다
- ‘확률적’ probabilistic 인 방법
- 각 후보 단어의 확률에 맞게 선택하는 것
- 확률이 높은 단어는 선택되기 쉽고, 확률이 낮은 단어는 선택되기 어려워짐.
- 이 방식에서는 선택되는 단어(샘플링 단어)가 매번 다를 수 있음
우린 확률적 방법 채택
➡ “say”라는 단어가 (확률적으로) 선택
[3단계]
두 번째 단어를 샘플링
앞에서 한 작업을 되풀이.
1) 방금 생성한 단어인 “say”를 언어 모델에 입력하여 다음 단어의 확률분포를 얻음
2) 그런 다음 그 확률분포를 기초로 다음에 출현할 단어를 샘플링하는 것임
[4단계]
3) 다음은 이 작업을 원하는 만큼 반복
(또는 종결 기호가 나타날 때까지 반복 합니다).
4) 새로운 문장을 생성.
✔ POINT
이렇게 생성한 문장은 훈련 데이터에는 존재하지 않는, 새로 생성된 문장이라는 것.
언어 모델은 훈련 데이터를 암기한 것이 아니라,
훈련 데이터에서 사용된 단어의 정렬 패턴을 학습한 것이기 때문.
[문장 생성 구현]
앞 장에서 구현한 Rnnlm 클래스를 상속해 RnnlmGen
클래스를 만들고,
이 클래스에 문장 생성 메서드를 추가
- 클래스 상속: 기존 클래스를 계승하여 새로운 클래스를 만드는 메커니즘
- 파이썬에서 클래스 상속
- 기반 클래스 이름이 “Base”이고 새로 정의할 클래스 이름이 “New”라면
class New (Base ):
- 기반 클래스 이름이 “Base”이고 새로 정의할 클래스 이름이 “New”라면
👀 RnnlmGen 클래스의 구현 코드 보기
import sys
sys.path.append('..')
import numpy as np
from common.functions import softmax
from ch06.rnnlm import Rnnlm
from ch06.better_rnnlm import BetterRnnlm
cl
ass 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
📜 구현된 매서드 설명
generate (start_id, skip_ids, sample_size)
- start_id: 최초로 주는 단어의 ID
- sample_size: 샘플링 하는 단어의 수
- skip_ids: 단어 ID의 리스트
(예컨대 [12, 20 ] )
이 리스트에 속하는 단어 ID는 샘플링되지 않도록 해줌
이 인수는 PTB 데이터셋에 있는 null이나 N 등, 전처리된 단어를 샘플링하지 않게 하는 용도로 사용
model.predict (x)
- 각 단어의 점수를 출력합니다
- 점수는 정규화되기 전의 값
p = softmax (score)
- 이 점수들을 소프트맥스 함수를 이용해 정규화
- 목표로 하는 확률분포 p를 얻기 가능.
np.random.choice ( )
- 확률분포 p로부터 다음 단어를 샘플링
WARNING
model
의 predict ( )
- 미니배치 처리를 하므로 입력 x는 2차원 배열이어야 함
- 그래서 단어 ID를 하나만 입력하더라도 미니배치 크기를 1로 간주해 1×1 넘파이 배열로 성형(reshape )
[위의 RnnlmGen 클래스를 사용해 문장 생성]
- 아무런 학습도 수행하지 않은 상태에서(즉, 가중치 매개변수는 무작위 초깃값인 상태에서) 문장을 생성.
[코드 개요]
1) 첫 단어를 ‘you’로 하고, 그 단어 ID를 start_id
로 설정한 다음 문장을 생성.
2) 샘플링하지 않을 단어로는 [‘N’, ‘$’]를 지정.
3) 참고로, 문장을 생성하는 generate ( ) 메서드는 단어 ID들을 배열 형태로 반환: 그 단어 ID 배열을 문장 으로 변환해야 필요
join ( )
메서드가 수행txt = ' '.join ([id_to_word[i ] for i in word_ids ] )
join ( )
메서드는[구분자].join(리스트)
- 리스트의 단어들 사이에 구분자를 삽입해 모두 연결.
👀코드 보기
# coding: utf-8
import sys
sys.path.append('..')
from rnnlm_gen import RnnlmGen
from dataset import ptb
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)
corpus_size = len(corpus)
model = RnnlmGen()
# model.load_params('../ch06/Rnnlm.pkl')
# 시작 (start) 문자와 건너뜀 (skip) 문자 설정
start_word = 'you'
start_id = word_to_id[start_word]
skip_words = ['N', '$']
skip_ids = [word_to_id[w] for w in skip_words]
# 문장 생성
word_ids = model.generate(start_id, skip_ids)
txt = ' '.join([id_to_word[i] for i in word_ids])
txt = txt.replace( '.\n')
print(txt)
(결과)
you setback best raised fill steelworkers montgomery kohlberg told beam worthy allied ban swedish aichi mather promptly ramada explicit leslie bets discovery considering campaigns bottom petrie warm large-scale frequent temple grumman bennett ...
- 단어들을 엉터리로 나열한 글
- 모델의 가중치 초깃값으로 무작위한 값을 사용했기 때문
[앞 장에서 학습을 끝낸 가중치를 이용해 문장을 생성]
앞의 코드에서 주석 처리해둔 model.load_params
줄의 주석을 해제
- 앞 장에서 학습한 가중치 매개변수를 읽어 들임
(결과)
you 'll include one of them a good problems.
moreover so if not gene 's corr experience with the heat of bridges a new deficits model is non-violent what it 's a rule must exploit it.
there 's no tires industry could occur.
beyond my hours where he is n't going home says and japanese letter.
knight transplants d.c. turmoil with one-third of voters.
the justice department is ...
(평가)
문법적으로 이상하거나 의미가 통하지 않는 문장이 섞여 있지만, 그럴듯한 문장도 있음
어느 정도는 올바른 문장
하지만 부자연스러운 문장도 발견되니, 아직 개선할 여지 존재
➡ 물론 더 나은 언어 모델 필요
[더 좋은 문장으로]
앞 장의 단순한 RNNLM을 개선한 ‘더 좋은 RNNLM’사용
- 퍼플렉서티가 대략 136이던 모델을 75까지 개선
앞 장에서는 더 좋은 언어 모델을 BetterRnnlm라는 클래스로 구현했습니다.
여기에서는 방금 전과 마찬가지로 이 클래스를 상속한 후 문장 생성 기능을 추가.
구현 방법은 앞서 RnnlmGen 클래스에서 한 구현과 완전히 같음
(결과)
you 've seen two families and the women and two other women of students.
the principles of investors that prompted a bipartisan rule of which had a withdrawn target of black men or legislators interfere with the number of plants can do to carry it together.
the appeal was to deny steady increases in the operation of dna and educational damage in the 1950s.
(평가)
저번보다 자연스러운 문장.
seq2seq
[INTRO]
세상에는 시계열 데이터 많음.
EX) 언어 데이터, 음성 데이터, 동영상 데이터
이러한 시계열 데이터를 또 다른 시계열 데이터로 변환 요구 높음
EX) 기계 번역이나 음성 인식, 챗봇
[seq2seq 의미]
sequence to sequence
우리는 시계열 데이터를 다른 시계열 데이터로 변환하는 모델.
2개의 RNN을 이용
[seq2seq의 원리]
Encoder-Decoder 모델.
- Encoder
- 입력 데이터를 인코딩(부호화)
- 정보를 어떤 규칙에 따라 변환하는 것
- EX) ‘A’라는 문자를 ‘1000001’이라는 이진수로 변환하는 식
- Decoder: 인코딩된 데이터를 디코딩(복호화)
- EX) ‘1000001’이라는 비트 패턴을 ‘A’라는 문자로 변환
seq2seq의 구조
EX) 우리말을 영어로 번역하는 예.
“나는 고양이로소이다”라는 문장을 “I am a cat”으로 번역
이때 seq2seq는, Encoder와 Decoder가 시계열 데이터를 변환
1) Encoder
- “나는 고양이로소이다”라는 출발어(번역할 대상 언어를) 문장을 인코딩
- Encoder가 인코딩한 정보에는 번역에 필요한 정보가 조밀하게 응축되어 있음
2) 그 인코딩한 정보를 Decoder에 전달
3) Decoder
- 도착어(번역된 결과 언어) 문장 생성
- Decoder는 조밀하게 응축된 이 정보를 바탕으로 도착어 문장을 생성
- 즉, Encoder와 Decoder가 협력하여 시계열 데이터를 다른 시계열 데이터로 변환
- 그리고 Encoder와 Decoder로는 RNN을 사용 가능
[1단계] Encoder
Encoder를 구성하는 계층
- 우리말 문장을 단어 단위로 쪼개 입력한다고 가정.
- Encoder는 RNN을 이용해 시계열 데이터를 h라는 은닉 상태 벡터로 변환
- 지금 예에서는 RNN으로써 LSTM을 이용지만, ‘단순한 RNN’이나 GRU 등도 이용 가능
- h
- 마지막 은닉 상태
- 입력 문장(출발어)을 번역하는 데 필요한 정보가 인코딩됨
- LSTM의 은닉 상태 h는 고정 길이 벡터
- Encoder는 문장을 고정 길이 벡터로 변환
[2단계] Decoder
이 인코딩된 벡터를 도착어 문장을 생성하는 방법
- 앞 절에서 다룬 문장 생성 모델을 그대로 이용 가능
- Decoder는 앞 절의 신경망과 완전히 같은 구성
- 차이점
- LSTM 계층이 벡터 h를 입력받는다는 점
- 참고로, 앞 절의 언어 모델에서는 LSTM 계층이 아무것도 받지 않았음
[3단계] Decoder와 Encoder를 연결한 계층 구성
seq2seq는 LSTM 두 개(Encoder의 LSTM과 Decoder의 LSTM )로 구성.
이때 LSTM 계층의 은닉 상태
- Encoder와 Decoder를 이어주는 ‘가교’
- 순전파: Encoder에서 인코딩된 정보가 LSTM 계층의 은닉 상태를 통해 Decoder에 전해짐.
- 역전파: 이를 통해 기울기가 Decoder로부터 Encoder로 전해짐
시계열 데이터 변환용 장난감 문제
장난감 문제 toy problem: 머신러닝을 평가하고자 만든 간단한 문제
[더하기 문제(장난감 문제)]
우리는 시계열 변환 문제의 예
EX) “57 +5”와 같은 문자열을 seq2seq에 건네면 “62”라는 정답을 내놓도록 학습시킬 것임
- 이번 문제에서는 단어가 아닌 ‘문자’ 단위로 분할
- 문자 단위 분할이란, 예컨대 “57 +5”가 입력되면 [‘5’, ‘7’, ‘+’, ‘5’]라는 리스트로 처리하는 것
가변 길이 시계열 데이터
[PROBLEM]
이때 주의할 점은 덧셈 문장 (“57 +5”나 “628 +521” 등)이나 그 대답(“62”나 “1149” 등)의 문자 수가 문제마다 다름
EX) “57 +5”는 총 4 문자이고 “628 +521”은 총 7 문자
‘가변 길이 시계열 데이터’: 이처럼 이번 ‘덧셈’ 문제에서는 샘플마다 데이터의 시간 방향 크기가 다름
[SOLUTION]
신경망 학습 시 ‘미니배치 처리’를 하려면 무언가 추가 노력이 필요
미니배치로 학습할 때는 다수의 샘플을 한꺼번에 처리
- 이때 (우리 구현에서는) 한 미니배치에 속한 샘플들의 데이터 형상이 모두 똑같아야 함
가변 길이 시계열 데이터를 미니배치로 학습하기 위한 가장 단순한 방법은 패딩 padding을 사용
- 패딩: 원래의 데이터에 의미 없는 데이터를 채워 모든 데이터의 길이를 균일 하게 맞추는 기법
- 모든 입력 데이터의 길이를 통일
- 남는 공간에는 의미 없는 데이터(여기에서는 ‘공백’)를 채움
- 미니배치 학습을 위해 ‘공백 문자’로 패딩을 수행하여 입력·출력 데이터의 크기를 통일.
[문제 조건]
- 0~999 사이의 숫자 2개만 더함.
- ‘+’까지 포함하면 입력의 최대 문자 수는 7
- 덧셈 결과는 최대 4 문자입니다(999 + 999 = 1998 ).
- 정답 데이터에도 패딩을 수행해 모든 샘플 데이터의 길이를 통일
- 질문과 정답을 구분하기 위해 출력 앞에 구분자로 밑줄( _ )을 붙임
- 출력 데이터는 총 5 문자로 통일
- 이 구분자는 Decoder에 문자열을 생성하라고 알리는 신호로 사용
WARNING
Decoder 출력의 경우, 문자 출력의 종료를 알리는 구분자를 정답 레이블로 입력하도록 구현 가능
EX) “62”나 “1149”
하지만 여기에서는 이야기를 단순화하고자 그런 구분자는 넣지 않음
즉, Decoder가 문자열을 생성할 때는 항상 정해진 수의 문자(여기에서는 ‘_’ 를 포함해 5자)만 출력시키도록 함.
[PROBLEM]
원래는 존재하지 않던 패딩용 문자까지 seq2seq가 처리하게 됨.
➡ 패딩을 적용해야 하지만 정확성이 중요하다면 seq2seq에 패딩 전용 처리를 추가 필요.
EX) 예컨대 Decoder에 입력된 데이터가 패딩이라면 손실의 결과에 반영하지 않도록 함
(Softmax with Loss 계층에 ‘마스크’ 기능을 추가해 해결가능)
[SOLUTION]
한편 Encoder에 입력된 데이터가 패딩이라면 LSTM 계층이 이전 시각의 입력을 그대로 출력하게 함.
즉, LSTM 계층은 마치 처음부터 패딩이 존재하지 않았던 것처럼 인코딩할 수 있음
하지만, 이번 장에서는 이해 난이도를 낮추기 위해 패딩용 문자(공백 문자)도 특별히 구분하지 않고 일반 데이터처럼 다루겠음
📜 덧셈 데이터셋
사용할 덧셈 학습 데이터는 dataset/addition.txt에 담겨 있음
- 덧셈 예가 총 50,000개가 들어 있음
이와 같은 seq2seq용 학습 데이터(텍스트 파일)를 파이썬에서 쉽게 처리할 수 있도록 전용 모듈(dataset/sequence.py )을 제공 load_data (file_name, seed )
file_name
으로 지정한 텍스트 파일을 읽어- 텍스트를 문자 ID로 변환하고,
- 이를 훈련 데이터와 테스트 데이터로 나눠 반환
seed
: 이 메서드 내부에서 사용하는 무작위수의 초깃값.
- 이 메서드는 훈련 데이터와 테스트 데이터로 나누기 전에 전체 데이터를 뒤섞는데, 이때 무작위수를 사용
get_vocab ( )
- 문자와 문자 ID의 대응 관계를 담은 딕셔너리를 반환
- char _to _id와 id _ to _char라는 2개의 딕셔너리를 돌려줌
👀코드 보기
import sys
sys.path.append('..')
from dataset import sequence
(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt', seed=1984)
char_to_id, id_to_char = sequence.get_vocab()
print(x_train.shape, t_train.shape)
print(x_test.shape, t_test.shape)
# (45000, 7) (45000, 5)
# (5000, 7) (5000, 5)
print(x_train[0]) print(t_train[0])
# [ 3 0 2 0 0 11 5]
# [ 6 0 11 7 5]
print(''.join([id_to_char[c] for c in x_train[0]]))
print(''.join([id_to_char[c] for c in t_train[0]]))
# 71+118
# _189
(코드 해석)
- 이처럼 sequence 모듈을 이용하면 seq2seq용 데이터를 간단히 읽어 들일 수 있음
x_train
과t_train
- ‘문자 ID’가 저장되어 있음.
- 문자 ID와 문자의 대응 관계는
char_to_id
와id_to_char
를 이용해 상호 변환가능
WARNING_ 정석대로라면 데이터셋을 3개(훈련용, 검증용, 테스트용)로 나눠 사용해야 함.
- 훈련용 데이터: 학습
- 검증용 데이터: 하이퍼파라미터를 튜닝.
- 테스트용 데이터: 모델의 성능을 평가
다만, 여기에서는 이야기를 단순하게 하고자, 훈련용과 테스트용으로만 분리하여 모델을 훈련시키고 평가(검증용 데이터)
seq2seq 구현
seq2seq는 2개의 RNN을 연결한 신경망
1) 두 RNN을 Encoder 클래스와 Decoder 클래스로 각각 구현
2) 두 클래스를 연결하는 Seq2seq 클래스를 구현.
[1단계] Encoder 클래스
문자열을 받아 벡터 h로 변환
RNN을 이용해 Encoder를 구성
- LSTM 계층을 이용
- Encoder 클래스는 Embedding 계층과 LSTM 계층으로 구성됨
Embedding 계층에서는 문자(정확하게는 문자 ID)를 문자 벡터로 변환
이 문자 벡터가 LSTM 계층으로 입력됩니다.
[LSTM 계층은],
- 오른쪽(시간 방향): 은닉 상태와 셀 출력
- 위쪽: 은닉 상태만 출력
- 이 구성에서 더 위에는 다른 계층이 없으니 LSTM 계층의 위쪽 출력은 폐기
[h]
- Encoder에서는 마지막 문자를 처리한 후 LSTM 계층의 은닉 상태 출력
- 그리고 이 은닉 상태 h가 Decoder로 전달됨
WARNING
Encoder에서는 LSTM의 은닉 상태만을 Decoder에 전달합니다. LSTM의 셀도 Decoder에 전달할 수는 있지만, LSTM의 셀을 다른 계층에 전달하는 일은 일반적으로 흔치 않습니다.
- LSTM의 셀: 자기 자신만 사용한다는 전제로 설계되었기 때문
그런데 우리는 시간 방향을 한꺼번에 처리하는 계층을 Time LSTM 계층이나 Time Embedding 계층으로 구현했습니다.
이러한 Time 계층을 이용하면,
[Encoder 클래스의 코드]
👀코드 보기
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
📜 코드 설명 보기
* 어휘 수
* 여기서 어휘 수는 문자의 종류.
* 참고로 이번 에는 0~9의 숫자와 ‘+’, ‘ ’(공백 문자), ‘_’을 합쳐 총 13가지 문자를 사용
```wordvec _size```
* 문자 벡터의 차원 수
```hidden _size```
* LSTM 계층의 은닉 상태 벡터의 차원 수
* 이 초기화 메서드에서는 가중치 매개변수를 초기화하고,
* 필요한 계층을 생성.
가중치 매개변수와 기울기를 인스턴스 변수 ```params```와 ```grads``` 리스트에 각각 보관
```stateful=False```: 이번에는 Time LSTM 계층이 상태를 유지하지 않기 때문
**전의 언어 모델**
* ```Time LSTM```계층의 인수 ```stateful = True```로 설정
* ‘긴 시계열 데이터’가 하나뿐인 문제를 다룸
* 은닉 상태를 유지한 채로 ‘긴 시계열 데이터’를 처리
* 이번에는 ‘짧은 시계열 데이터’가 여러 개인 문제.
* 따라서 문제마다 LSTM의 은닉 상태를 다시 초기화한 상태(영벡터)로 설정
</div>
</details>
**[forward ( )와 backward ( ) 구현]**
<details>
<summary>👀코드 보기</summary>
<div markdown="1">
```python
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
📜 코드 설명 보기
[Encoder의 순전파]
- Time Embedding 계층과 Time LSTM 계층의
forward ( )
메서드 호출 - 그리고 Time LSTM 계층의 마지막 시각의 은닉 상태만을 추출해, 그 값을 Encoder의
forward ( )
메서드의 출력으로 반환.
[Encoder의 역전파]
- dh: LSTM 계층의 마지막 은닉 상태에 대한 기울기가 인수로 전해짐
- Decoder가 전해주는 기울기
- dhs: 역전파 구현에서는 원소가 모두 0인 텐서
- 생성 dh를 dhs의 해당 위치에 할당.
- 그다음은 Time LSTM 계층과 Time Embedding 계층의 backward ( ) 메서드를 호출.
[2단계] Decoder 클래스
Encoder 클래스가 출력한 h를 받아 목적으로 하는 다른 문자열을 출력
Decoder는 RNN으로 구현 가능
- Encoder과 마찬가지로 LSTM 계층을 사용
이때 Decoder의 계층 구성
여기에서 정답 데이터는 “_62”
,
- 입력 데이터를
['_', '6', '2', ' ']
로 주고, - 이에 대응하는 출력은
['6', '2', ' ', ' ']
이 되도록 학습시킴
WARNING RNN으로 문장을 생성할 때, 학습 시와 생성 시의 데이터 부여 방법이 다름.
- 학습 시: 정답을 알고 있기 때문에 시계열 방향의 데이터를 한꺼번에 줄 수 있음
- 추론 시: (새로운 문자열을 생성할 때)에는 최초 시작을 알리는 구분 문자(이번 예에서는
‘_’
) 하나만 줌.- 그리고 그 출력으로부터 문자를 하나 샘플링하여, 그 샘플링한 문자를 다음 입력으로 사용하는 과정을 반복
[문장 생성 문제와 차이]
문장을 생성할 때 소프트맥스 함수의 확률분포를 바탕으로 샘플링을 수행
➡ 생성되는 문장이 확률에 따라 달라짐.
(덧셈 문제)
이러한 확률적인 ‘비결정성’을 배제하고 ‘결정적’ 인 답을 생성필요.
그래서 이번에는 점수가 가장 높은 문자 하나만 고를 것임.
즉, ‘확률적’이 아닌 ‘결정적’으로 선택.
[알고리즘 개요]
Decoder의 문자열 생성 순서
argmax 노드: Affine 계층의 출력 중 값이 가장 큰 원소의 인덱스(문자 ID)를 반환
- 최댓값을 가진 원소의 인덱스(이번 예에서는 문자 ID )를 선택하는 노드
- 이번에는 Softmax 계층을 사용하지 않고, Affine 계층이 출력하는 점수가 가장 큰 문자 ID를 선택
WARNING Softmax 계층은 입력된 벡터를 정규화합니다.
이 정규화 과정에서 벡터의 각 원소의 값이 달라집니다만, 대소 관계는 바뀌지 않음. 따라서 아래 그림의 경우 Softmax 계층을 생략 가능
Decoder에서는 학습 시와 생성 시에 Softmax 계층을 다르게 취급
➡ Softmax with Loss 계층은 이후에 구현하는 Seq2seq 클래스에서 처리하기로 하고,
Decoder 클래스는 밑처럼 Time Softmax with Loss 계층의 앞까지만 담당 (학습)
[Decoder 클래스의 코드]
Decoder 클래스의 구성
- Time Embedding
- Time LSTM
- Time Affine 의 3가지 계층으로 구성.
[Decoder 클래스의 구현]
👀코드 보기
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 계층의 시간 방향으로의 기울기는 TimeLSTM 클래스의 인스턴스 변수 dh에 저장되어 있음
- 그래서 이 시간 방향의 기울기 dh를 꺼내서 Decoder 클래스의
backward ( )
의 출력으로 반환
앞서 언급한 것처럼, Decoder 클래스는 학습 시와 문장 생성 시의 동작이 다름
- forward ( ) 메서드: 학습할 때 사용된다고 가정
- generate ( ) 메서드: 문장 생성을 담당
generate ( )
메서드
h
: Encoder로부터 받는 은닉 상태start_id
: 최초로 주어지는 문자 IDsample_size
: 생성하는 문자 수
여기에서는 문자를 1개씩 주고, Affine 계층이 출력하는 점수가 가장 큰 문자 ID를 선택하는 작업을 반복.
WARNING
이번 문제에서는 Encoder의 출력 h를 Decoder의 Time LSTM 계층의 상태로 설정.
즉, Time LSTM 계층은 상태를 갖도록(stateful) 한 것이죠.
- 한 번 설정된 이 은닉 상태는 재설 정되지 않고, 즉 Encoder의 h를 유지하면서 순전파가 이뤄짐.
[3단계] Seq2seq 클래스
Seq2seq 클래스의 구현
- Encoder 클래스와 Decoder 클래스를 연결
- Time Softmax with Loss 계층을 이용해 손실을 계산
주가 되는 처리는 Encoder와 Decoder 클래스에 이미 구현되어 있음
그래서 여기에서는 그 기능들을 제대로 연결하기만 하면 된다
👀코드 보기
class Seq2seq(BaseModel):
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 = 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
seq2seq 평가
seq2seq의 학습은 기본적인 신경망의 학습과 같은 흐름으로 이뤄짐
- 학습 데이터에서 미니배치를 선택하고,
- 미니배치로부터 기울기를 계산하고,
- 기울기를 사용하여 매개변수를 갱신
[코드 구현]
- ‘Trainer 클래스’ 절에서 설명한 Trainer 클래스를 사용해 이 규칙대로 작업을 수행.
- 매 에폭마다 seq2seq가 테스트 데이터를 풀게 하여(문자열 생성을 수행하여) 학습 중간중간 정답률을 측정.
👀코드 보기
# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from dataset import sequence
from common.optimizer import Adam
from common.trainer import Trainer
from common.util import eval_seq2seq
from seq2seq import Seq2seq
from peeky_seq2seq import PeekySeq2seq
# 데이터셋 읽기
(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt')
char_to_id, id_to_char = sequence.get_vocab()
# Reverse input?
#=================================================
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
# Normal or 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))
📜 코드 설명 보기
이상의 코드는 기본적인 신경망의 학습용 코드와 같지만,
평가 척도로 정답률을 사용
- 정확하게는 에폭마다 테스트 데이터의 문제 중 몇 개를 풀게 하여 올바르게 답했는지를 채점
- 이 구현에서 정답률 측정에는
eval_seq2seq (model, question, correct, id_to_char, verbose, is_reverse )
메서드를 이용 - 이 메서드는 문제(question)를 모델(model )에 주고,
- 문자열을 생성하게 하여 그것이 답(correct )과 갖은 지를 판정.
- 모델이 내놓은 답이 맞으면 1을 돌려주고 틀리면 0 돌려줌.
eval_seq2seq (model, question, correct, id_to_char,verbose, is_reverse )
model
: 모델을 뜻함question
: 문제 문장(문자 ID의 배열)-
correct
: 정답(문자 ID의 배열) id_to_char
: 문자 ID와 문자의 변환을 수행하는 딕셔너리verbose
: 결과를 출력할지 여부is_reverse
: 입력문을 반전했는지 여부verbose=True
로 설정하면 결과를 터미널로 출력
이번 실험에서는 테스트 데이터의 최초 10개분만 표시.
[코드 실행] 그러면 다음 결과가 터미널(콘솔)에 출력
위와 같이 터미널에는 에폭별 결과가 출력됨
- 문제 문장: “Q 600 +257”
- 정답 문장:“T 857”
- 모델이 내놓은 EKQ: “ 864”
에폭마다의 정답률 그래프
25 에폭에서 중단했는데, 그 시점의 정답률은 10% 정도.
더 정확할 필요 있음
seq2seq 개선
앞 절의 seq2seq를 세분화하여 학습 ‘속도’를 개선
두 가지 개선안을 소개
입력 데이터 반전(Reverse)
첫 번째 개선안은 아주 쉬운 트릭으로, 밑에서 보듯이 입력 데이터의 순서를 반전시키는 것
이 트릭을 사용하면 많은 경우 학습 진행이 빨라져서, 결과적으로 최종 정확도도 좋아짐
[코드 구현]
입력 데이터를 반전시키려면,
앞의 학습용 코드 에서 데이터셋을 읽은 후 다음 코드 추가
👀코드 보기
# 데이터셋 읽기
(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt')
...
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]
...
이 코드와 같이, 배열의 행을 반전시키려면 x _train[:, ::-1 ]이라는 표기법을 사용하면 됨
[결과]
데이터를 반전시키는 효과는 어떤 문제를 다루느냐에 따라 다르지만, 대부 분의 경우 더 좋은 결과로 이어짐
[정확도가 높아지는 이유] 직관적으로는 기울기 전파가 원활해지기 때문
EX) “나는 고양이로소이다”를 “I am a cat”으로 번역하는 문제,
“나”라는 단어가 “I”로 변환되는 과정을 보면.
- “나”로부터 “I”까지 가려면,
- “는”, “고양이”, “로소”, “이다” 까지 총 네 단어 분량의 LSTM 계층을 거치는게 필수.
- 따라서 역전파 시 “I”로부터 전해지는 기울기가 “나”에 도달하기까지, 그 먼 거리만큼 영향을 더 받게 됨
여기서 입력문을 반전시키면,
즉 “이다 로소 고양이 는” 순으로 바꾸면,
- “나”와 “I”는 바로 옆이 되었으니 기울기가 직접 전해짐.
- 이처럼 입력 문장의 첫 부분에서는 반전 덕분에 대응하는 변환 후 단어와 가까우므로(그런 경우가 많아지므로), 기울기가 더잘 전해져서 학습 효율이 좋아짐.
- 다만, 입력 데이터를 반전해도 단어 사이의 ‘평균’적인 거리는 그대로임
엿보기 Peeky
이어서 seq2seq의 두 번째 개선.
[INTRO]
seq2seq의 Encoder 동작 REVIEW
- 입력 문장(문제 문장)을 고정 길이 벡터 h로 변환.
- 이때 h 안에는 Decoder에게 필요한 정보가 모두 담김.
- 즉, h가 Decoder에 있어서는 유일한 정보
[개선 전]
현재의 seq2seq는 최초 시각의 LSTM 계층만이 벡터 h를 이용
개선점) 이 중요한 정보인 h를 더 활용할 수는 없을까?
[개전 후]
중요한 정보가 담긴 Encoder의 출력 h를 Decoder의 다른 계층에게도 전해줌
- 모든 시각의 Affine 계층과 LSTM 계층에 Encoder의 출력 h를 전해줍 니다
- 기존에는 하나의 LSTM만이 소유하던 중요 정보 h를 여러 계층(이 예에서는 총 8개 계층)이 공유함
이 개선안은 인코딩된 정보를 Decoder의 다른 계층에도 전해주는 기법입니다.
- 달리 보면, 다른 계층도 인코딩된 정보를 ‘엿본다’라고 해석 가능 ➡ 이 개선을 더한 Decoder를 ‘Peeky Decoder’라고 함
- Peeky Decoder를 이용하는 seq2seq를 ‘Peeky seq2seq’라고 함
그런데 LSTM 계층과 Affine 계층에 입력되는 벡터가 2개씩이 됨
➡ 이는 실제로는 두 벡터가 연결 concatenate 된 것을 의미.
➡ 따라서 앞의 그림은 두 벡터를 연결시키는 concat 노드를 이용해 밑의 그림처럼 그려야 정확한 계산 그래프입니다
[Peeky Decoder 클래스의 구현]
초기화와 순전파를 구현한 __init __ ( )
와 forward ( )
만 설명.
- 역전파
backward ( )
와 문장 생성generate ( )
는 특별히 어려운 점이 없기 때문에 생략
👀코드 보기
import sys
sys.path.append('..')
from common.time_layers import *
from seq2seq import Seq2seq, Encoder
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(H + D, 4 * H) / np.sqrt(H + 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 + 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
📜 코드 설명 보기
PeekyDecoder
의 초기화
- 앞 절의 Decoder와 거의 같음(링크 삽입)
- 다른 점은 LSTM 계층의 가중치와 Affine 계층의 가중치의 형상.
- 이번 구현에서는 Encoder가 인코딩한 벡터도 입력되기 때문에 가중치 매개변수의 형상이 그만큼 커짐
forward ( )
의 구현
np.repeat ( )
: 우선h
를 시계열만큼 복제해hs
에 저장np.concatenate ( )
: 그 hs와 Embedding 계층의 출력을 연결하고, * 이를 LSTM 계층에 입력.- 마찬가지로 Affine 계층에도 hs와 LSTM 계층의 출력을 연결한 것을 입력
*앞 절과 똑같습니다. 따라서 앞 절의 Encoder 클래스를 그대로 이용
</div>
</details>
**[PeekySeq2seq 구현]**
이 클래스는 앞 절의 Seq2seq 클래스와 거의 같음.
유일한 차이는 Decoder 계층
* 앞 절의 Seq2seq 클래스: Decoder 클래스를 사용
* 이번엔: PeekyDecoder를 사용
PeekySeq2seq 클래스의 구현은 앞 절의 Seq2seq 클래스를 계승하고, 초기화 부분만을 변경
<details>
<summary>👀코드 보기</summary>
<div markdown="1">
```python
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 클래스를 사용하여 덧셈 문제에 다시 도전
- 학습용 코드는 앞 절의 코드에서 Seq2seq 클래스만 PeekySeq2seq 클래스로 변경하면 됨
👀코드 보기
# model = Seq2seq(vocab_size, wordvec_size, hideen_size)
model = PeekySeq2seq(vocab_size, wordvec_size, hideen_size)
[결과] Reverse와 Peeky가 함께 효과적으로 작동하고 있음을 알 수 있음
seq2seq를 이용하는 애플리케이션
- 기계 번역: ‘한 언어의 문장’을 ‘다른 언어의 문장’으로 변환
- 자동 요약: ‘긴 문장’을 ‘짧게 요약된 문장’으로 변환
- 질의응답: ‘질문’을 ‘응답’으로 변환
- 메일 자동 응답: ‘받은 메일의 문장’을 ‘답변 글’로 변환
[챗봇]
챗봇: 사람과 컴퓨터가 텍스트로 대화를 나누는 프로그램
대화라는 것은 ‘상대의 말’과 ‘자신의 말’로 구성되기 때문에, ‘상대의 말’을 ‘자신의 말’로 변환하는 문제.
➡ 즉, 대화의 텍스트 데이터가 준비되면 그것으로 seq2seq를 학습시킬 수 있음
- IT 헬프데스크를 대상으로 seq2seq 기반 챗봇을 실험했고, 그 성과의 하나로 고객의 VPN 연결 문제를 해결한 사례를 제시
📜 seq2seq 기반 챗봇의 대화 예
기계: 안녕하세요.
사람: 안녕하세요.
기계: 어떤 OS를 사용하시죠?
사람: 리눅스요.
......
기계: 실행할 때 어떤 오류가 발생하죠?
사람: VPN 어쩌고 하는 오류가 나옵니다.
기계: VPN을 사용하여 네트워크에 연결할 때 오류 메시지가 뭐라고 뜨나요?
사람: 연결이 되지 않거나 혹은 그와 비슷한 상태...
......
기계: 지금 집인가요, 아니면 회사인가요?
사람: 집이요.
기계: 그럼, <URL>에 가서 선생님의 이름과 암호를 입력해보세요.
사람: 했어요.
기계: <NAME>에서 로그인하여 잘 되는지 확인해보세요.
사람: 오, 잘 되네요!
기계: 좋습니다. 다른 문제는 없나요?
사람: 없어요, 매우 잘 됩니다.
[알고리즘 학습]
이번 장에서 수행한 실험은 ‘덧셈’과 같은 간단한 문제.
그러나 원리적으로는 더 고차 원적인 문제도 처리 가능
- Input은 입력
- Target은 출력
소스 코드도 (자연어와 마찬가지로) 문자로 쓰여진 시계열 데이터임
몇 줄에 걸친 코드라도 하나의 문장으로 처리 가능(줄바꿈은 개행 코드로 처리)
-> 따라서 소스 코드를 그대로 seq2seq에 입력할 수 있고, 원하는 답과 대조하여 학습시키기 가능
정리
- RNN을 이용한 언어 모델은 새로운 문장을 생성할 수 있다.
- 문장을 생성할 때는 하나의 단어(혹은 문자)를 주고 모델의 출력(확률분포)에서 샘플링하는 과정을 반복 한다.
- RNN을 2개 조합함으로써 시계열 데이터를 다른 시계열 데이터로 변환할 수 있다.
- seq2seq는 Encoder가 출발어 입력문을 인코딩하고, 인코딩된 정보를 Decoder가 받아 디코딩하여 도착어 출력문을 얻는다.
- 입력문을 반전시키는 기법(Reverse ), 또는 인코딩된 정보를 Decoder의 여러 계층에 전달하는 기법 (Peeky )은 seq2seq의 정확도 향상에 효과적이다.
- 기계 번역, 챗봇, 이미지 캡셔닝 등 seq2seq는 다양한 애플리케이션에 이용할 수 있