기본기 다지기/CNN부터 Attention까지 구현

[기본이론] seq2seq 구현

syveany 2024. 9. 23. 21:13

Seq2seq 구현

참고문헌: 책 『밑바닥부터 시작하는 딥러닝2』 Chapter7. RNN을 사용한 문장 생성

~ 목차 ~
0. 서론- 왜 seq2seq을 구현하고 있는가

  1. Seq2seq 개념
  2. Seq2seq 구현
    2.1 Encoder 클래스
    2.2 Decoder 클래스
    2.3 Seq2seq 클래스
  3. Seq2seq 개선
    3.1 입력 데이터 반전(Reverse)
    3.2 엿보기(Peeky)
    3.3 개선 방법들의 적용
  4. 소감

0. 서론- 왜 seq2seq을 구현하고 있는가

  • 이것 역시 기본기 다지기의 일환 4탄이다.
  • 바로 Attention모델부터 이해하려고 했다가 장렬히 전사해서 seq2seq부터 차근차근 이해해보려고 한다.
  • 이번에도 국룰 책 『밑바닥부터 시작하는 딥러닝2』의 Chapter7. RNN을 사용한 문장 생성 챕터를 참고했다.

1. seq2seq 개념

  • (from) sequence to sequence, 한 시계열 데이터를 다른 시계열 데이터로 변환하는 것
  • RNN 2개(Encoder, Decoder)를 연결하여 구현할 수 있음
    • ex) (그림7-5 참고) Encoder가 출발어 문장을 인코딩한 뒤, 인코딩한 정보를 Decoder에 전달하고, Decoder가 도착어 문장을 생성함

image.png

  • Seq2seq의 Encoder와 Decoder는 각각 아래와 같이 생겼음

image.png

  • Encoder:
    • 시계열 데이터를 입력받아서 은닉상태벡터 h로 변환함
    • h는 고정길이벡터여야 함. 고정길이벡터로 만드는 가장 단순한 방법은 padding을 주어 길이를 맞추는 방법임
  • Decoder:
    • h를 입력받아서 문장을 생성함
    • 여기서 h는 Encoder와 Decoder를 이어주는 가교 역할을 함
    • 원래 LSTM 계층은 은닉 상태로 영벡터를 입력받지만, 여기서는 h를 입력받는다는 게 특징
  • 전체적인 그림은 아래와 같음 (+ 그림을 참고하니 좀 더 머리에 잘 들어온다)

image.png

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 방법임! (어떤 방법일지 궁금하다)

image.png

4. 소감

  • 많이 들으면서도 이게 뭔가 싶었던 seq2seq에 대해서 이제 알게 되었다.
  • 흐름을 따라가면서 공부를 하니 머릿속에 뼈대가 잡히는 느낌이다. 공부하면서도 하길 잘했다는 생각이 계속 듦
  • 도대체 Attention은 어떤 방법인가! 두구두구!