Seq2seq 구현
참고문헌: 책 『밑바닥부터 시작하는 딥러닝2』 Chapter7. RNN을 사용한 문장 생성
~ 목차 ~
0. 서론- 왜 seq2seq을 구현하고 있는가
- Seq2seq 개념
- Seq2seq 구현
2.1 Encoder 클래스
2.2 Decoder 클래스
2.3 Seq2seq 클래스 - Seq2seq 개선
3.1 입력 데이터 반전(Reverse)
3.2 엿보기(Peeky)
3.3 개선 방법들의 적용 - 소감
0. 서론- 왜 seq2seq을 구현하고 있는가
- 이것 역시 기본기 다지기의 일환 4탄이다.
- 바로 Attention모델부터 이해하려고 했다가 장렬히 전사해서 seq2seq부터 차근차근 이해해보려고 한다.
- 이번에도 국룰 책 『밑바닥부터 시작하는 딥러닝2』의 Chapter7. RNN을 사용한 문장 생성 챕터를 참고했다.
1. seq2seq 개념
- (from) sequence to sequence, 한 시계열 데이터를 다른 시계열 데이터로 변환하는 것
- RNN 2개(Encoder, Decoder)를 연결하여 구현할 수 있음
- ex) (그림7-5 참고) Encoder가 출발어 문장을 인코딩한 뒤, 인코딩한 정보를 Decoder에 전달하고, Decoder가 도착어 문장을 생성함
- Seq2seq의 Encoder와 Decoder는 각각 아래와 같이 생겼음
- Encoder:
- 시계열 데이터를 입력받아서 은닉상태벡터 h로 변환함
- h는 고정길이벡터여야 함. 고정길이벡터로 만드는 가장 단순한 방법은 padding을 주어 길이를 맞추는 방법임
- Decoder:
- h를 입력받아서 문장을 생성함
- 여기서 h는 Encoder와 Decoder를 이어주는 가교 역할을 함
- 원래 LSTM 계층은 은닉 상태로 영벡터를 입력받지만, 여기서는 h를 입력받는다는 게 특징
- 전체적인 그림은 아래와 같음 (+ 그림을 참고하니 좀 더 머리에 잘 들어온다)
2. seq2seq 구현
2.1 Encoder 클래스
class Encoder:
def __init__(self, vocab_size, wordvec_size, hidden_size):
# vocab_size: 문자의 종류수
V, D, H = vocab_size, wordvec_size, hidden_size
rn = np.random.randn
embed_W = (rn(V, D) / 100).astype('f') # 100으로 나누는 이유: 가중치 값을 더 작게 만들어서 학습의 안정성을 높이기 위해
lstm_Wx = (rn(D, 4*H) / np.sqrt(D)).astype('f') # 4: f,g,i,o 4개이기 때문
lstm_Wh = (rn(H, 4*H) / np.sqrt(H)).astype('f')
lstm_b = np.zeros(4*H).astype('f')
# 입력 토큰을 임베딩 벡터로 변환
self.embed = TimeEmbedding(embed_W)
# LSTM 계층 초기화
self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False) # stateful=False: 짧은 시계열 데이터가 여러개이기 때문에시퀀스마다 LSTM의 은닉상태를 영벡터로 초기화해야 함
# 임베딩층과 LSTM층의 파라미터들을 합침
self.params = self.embed.params + self.lstm.params
self.grads = self.embed.grads + self.lstm.grads
# LSTM의 은닉상태를 저장하기 위한 변수. forward에서 hs가 저장됨
self.hs = None
def forward(self, xs): # xs: 입력시퀀스
xs = self.embed.forward(xs)
hs = self.lstm.forward(xs)
self.hs = hs
return hs[:,-1,:] # TimeLSTM 계층의 마지막 시각의 은닉상태만 출력함
def backward(self, dh):
# 원소가 모두 0인 텐서 dhs 생성
dhs = np.zeros_like(self.hs)
# dh(TimeLSTM 계층의 마지막 시각의 은닉상태의 기울기)를 dhs의 해당 위치에 할당
dhs[:,-1,:] = dh
dout = self.lstm.backward(dhs)
dout = self.embed.backward(dout)
return dout
2.2 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):
# 생성된 단어들의 ID를 저장
sampled = []
# 처음에는 start_id가 첫 번째 단어의 ID가 되어서 단어를 하나씩 생성함
sample_id = start_id
# LSTM의 은닉상태를 h(문맥 or 이전 입력정보)로 초기화함
self.lstm.set_state(h)
# sample_size 길이의 문장 생성
for _ in range(sample_size):
x = np.array(sample_id).reshape((1,1)) # (1,1): 배치크기1, 시퀀스길이1 즉 단일 샘플이면서 하나의 단어만 입력되는 구조
out = self.embed.forward(x)
out = self.lstm.forward(out)
score = self.affine.forward(out)
# score에서 가장 높은 값을 가진 단어ID 선택
sample_id = np.argmax(score.flatten()) # flatten: np.argmax가 처리할 수 있는 1차원 배열로 변환
# 생성된 단어의 ID를 sampled 리스트에 추가
sampled.append(int(sample_id))
return sampled
2.3 Seq2seq 클래스
역할
- Encoder 클래스와 Decoder 클래스를 연결
- Time Softamx with Loss 계층을 통해 손실을 계산
class Seq2seq(BaseModel):
def __init__(self, vocab_size, wordvec_size, hidden_size):
V, D, H = vocab_size, wordvec_size, hidden_size
# Encoder, Decoder, Softmax 초기화
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):
# ts[:,:-1]: 디코더의 입력시퀀스, 타겟시퀀스의 첫 번째 단어부터 마지막 직전 단어까지
# ts[:,1:]: 타겟시퀀스에서 실제로 예측하고자 하는 부분, 두 번째 단어부터 마지막 단어까지
decoder_xs, decoder_ts = ts[:,:-1], ts[:1:]
# Encoder 순전파
h = self.encoder.forward(xs)
# Decoder 순전파
score = self.decoder.forward(decoder_xs, h)
# 손실 계산
loss = self.softmax.forward(score, decoder_ts)
return loss
def backward(self, dout=1):
# Softmax 역전파
dout = self.softmax.backward(dout)
# Decoder 역전파
dh = self.decoder.backward(dout)
# Encoder 역전파
dout = self.encoder.backward(dh)
return dout
def generate(self, xs, start_id, sample_size):
# Encoder 순전파
h = self.encoder.forward(xs)
# Decoder에서 문장 생성
sampled = self.decoder.generate(h, start_id, sample_size)
return sampled
3. seq2seq 개선
3.1 입력 데이터 반전(Reverse)
- 데이터 순서를 반전시키면 학습진행이 빨라져서 결과적으로 최종 정확도도 좋아짐. 기울기 전파가 원활해지기 때문!
(논문: Sutskever, Ilya, Oriol Vinyals, and Quoc V. Le: "Sequence to sequence learning with neural networks." Advances in neural information processing sysems. 2014.)- ex) '나는 고양이로소이다'를 'I am a cat'로 번역시킬 때, '나'와 'I'는 4개의 LSTM 계층('는', '고양이', '로소', '이다')을 지나와야 함
- 하지만 '이다 로소 고양이 는' 순으로 입력문을 반전시키면 '나'와 'I'가 바로 옆에 위치하게 되어서 기울기가 더 잘 전해지므로 학습 효율이 좋아진다고 보면 됨
# 데이터셋을 불러온 뒤 train데이터와 test데이터를 아래와 같이 반전시키면 됨
x_train, x_test = x_train[:,::-1], x_test[:,::-1]
3.2 엿보기(Peeky)
- 벡터 h를 원래 Decoder의 첫 번째 계층만 받았는데(그림7-25), 이를 혼자만 갖게 하는 게 아니라 다른 계층들에도 전해주는 방법임(그림7-26)
- 다른 계층도 인코딩된 정보 h를 '엿본다'고 표현한다는 의미로 peeky라는 이름이 붙음
3.3 개선 방법들의 적용
- 개선방법들을 적용했을 때 epoch에 따른 각각의 정확도 그래프는 아래와 같음
- 이 2가지의 개선방법보다 더 큰 개선방법이 Attention 방법임! (어떤 방법일지 궁금하다)
4. 소감
- 많이 들으면서도 이게 뭔가 싶었던 seq2seq에 대해서 이제 알게 되었다.
- 흐름을 따라가면서 공부를 하니 머릿속에 뼈대가 잡히는 느낌이다. 공부하면서도 하길 잘했다는 생각이 계속 듦
- 도대체 Attention은 어떤 방법인가! 두구두구!
'기본기 다지기 > CNN부터 Attention까지 구현' 카테고리의 다른 글
[기본이론] Attention 구현(2/3) (1) | 2024.09.27 |
---|---|
[기본이론] Attention 구현(1/3) (1) | 2024.09.26 |
[밑시딥] LSTM 구현하기 (0) | 2024.09.23 |
[밑시딥] RNN 구현하기 (0) | 2024.09.23 |
[밑시딥] CNN 구현하기 (0) | 2024.09.23 |