밑바닥부터 시작하는 딥러닝2 - 7장

jLica·2023년 8월 20일
0

7장 - RNN을 사용한 문장 생성

seq2seq란 (from) sequence to sequence(시계열에서 시계열로)를 뜻하는 말로, 한 시계열 데이터를 다른 시계열 데이터로 변환하는 것을 말한다.

7.1 - 언어 모델을 사용한 문장 생성

이번 절에서는 언어 모델로 문장을 생성해보려 한다.

7.1.1 - RNN을 사용한 문장 생성의 순서

앞 장에서는 시계열 데이터를 TT개분만큼 모아 처리하는 Time LSTM과 Time Affine 계층 등이 있었다.

이번에도 "you say goodbye and I say hello."라는 corpus로 학습한 언어 모델을 예로 생각해 보자.

학습된 언어 모델에 "I"라는 단어를 입력으로 주면 어떻게 될까? 잘 학습된 모델이라면 "say"가 높은 확률을 갖는 확률 분포를 출력할 것이다.

이렇듯 언어 모델은 다음에 출현하는 단어의 확률분포를 출력하는데, 이를 바탕으로 다음 단어를 새로 생성하려면 어떻게 해야 할까?

첫 번째로 확률이 가장 높은 단어를 선택하는 방법을 생각할 수 있다. 이는 확률이 가장 높은 단어를 선택할 뿐이므로 결과가 일정하게 정해지는 결정적인 방법이다.

두 번째로 확률적으로 선택하는 방법도 있다. 각 후보 단어의 확률에 따라서 선택하여 확률이 높으면 잘 선택되고, 확률이 낮으면 잘 선택되지 않는 것이다. 이 방식에서는 선택되는 단어(샘플링 단어)가 매번 달라질 수 있다.

이번에는 후자의 방법을 사용해 보자.

확률분포로부터 샘플링을 수행한 결과 "say"가 선택되었다. 이는 확률적으로 선택된 것이며 결정적이지는 않다.

다음 과정은 샘플링된 단어를 다시 입력으로 넣으면서 샘플링을 반복하면 된다. 이 작업을 원하는 만큼, 혹은 <eos>(end of sentence) 같은 종결 기호가 나올 때까지 반복한다. 그러면 새로운 문장을 만들 수 있다.

  • 이렇게 생성한 문장은 훈련 데이터에는 존재하지 않는, 새로 생성된 문장이다. 언어 모델은 훈련 데이터를 암기한 게 아니라 훈련 데이터에 사용된 단어의 정렬 패턴을 학습했기 때문이다.

7.2 - seq2seq

언어 데이터, 음성 데이터, 동영상 데이터는 모두 시계열 데이터이다. 이러한 시계열 데이터를 또 다른 시계열 데이터로 변환하는 문제도 숱하게 생각할 수 있다. 예를 들면 기계 번역이나 음성 인식, 챗봇, 혹은 컴파일러처럼 소스 코드를 기계어로 변환하는 작업 등등이 있겠다.

지금부터 시계열 데이터를 다른 시계열 데이터로 변환하는 모델을 생각해 보자. 여기에서는 2개의 RNN을 이용하는 seq2seq라는 방법을 살펴보자.

7.2.1 - seq2seq의 원리

seq2seq는 Encoder-Decoder 모델이라고도 한다. 여기에는 2개의 모듈, Encoder와 Decoder가 등장한다.

문자 그대로 Encoder는 입력 데이터를 인코딩(부호화)하고, Decoder는 인코딩된 데이터를 디코딩(복호화)한다.

NOTE_ 인코딩이란 정보를 어떤 규칙에 따라 변환하는 것이다. 반대로 디코딩이란 인코딩된 정보를 원래의 정보로 되돌리는 것이다.

그럼 구체적인 예를 들어보자. 우리말을 영어로 번역하는 예를 생각해 보면 되겠다.

"나는 고양이로소이다"라는 문장을 "I am a cat"으로 번역해보자.

위 그림처럼 먼저 Encoder가 "나는 고양이로소이다"라는 출발어 문장을 인코딩한다. 이어서 인코딩한 정보를 Decoder에 전달하고, Decoder가 도착어 문장을 생성한다.

  • 언어 번역에서 번역할 대상 언어를 '출발어', 번역된 결과 언어를 '도착어'라고 한다.

여기서 Encoder와 Decoder로는 RNN을 사용할 수 있다. 이제 seq2seq의 전체 과정을 알아보자. 우선은 Encoder의 처리부터.

Encoder는 RNN을 이용해 시계열 데이터를 h\bold h라는 은닉 상태 벡터로 변환한다. 지금은 LSTM을 썼는데 물론 단순한 RNN이나 GRU 등도 사용할 수 있다. 그리고 여기에서는 우리말 문장을 단어 단위로 쪼개 입력한다고 가정한다.

그런데 위 그림에서 Encoder가 출력하는 벡터 h\bold h는 LSTM 계층의 '마지막' 은닉 상태 벡터이다. 여기에 출발어를 번역하는 데 필요한 정보가 인코딩된다.

여기서 중요한 점은 LSTM의 은닉 상태 벡터 h\bold h는 고정 길이 벡터라는 사실이다. 그래서 인코딩한다라 함은 결국 임의 길이의 문장을 고정 길이 벡터로 변환하는 작업이 된다.

이렇듯 Encoder는 문장을 고정 길이 벡터로 변환한다. 그렇다면 Decoder는 어떻게 이 벡터로 도착어 문장을 만드는 걸까?

이 구조는 앞 절의 문장 생성을 위한 신경망과 완전히 같은 구성이다. 다만 LSTM 계층이 벡터 h\bold h를 입력받는다는 점만이 유일한 차이점이다.

WARNING_ 위 그림에서는 <eos>라는 구분 기호(특수문자)를 이용했다. 이 기호는 말 그대로 '구분자'이며, Decoder에 문장 생성의 시작을 알려준다. 또한 단어 샘플링의 끝을 알리기 위한 종료 신호이기도 하다. 즉, <eos>를 Decoder에 '시작/종료'를 알리는 구분자로 이용한 것이다.

다음은 Decoder와 Encoder를 연결한 계층 구성을 보자.

이렇듯 LSTM 계층의 은닉 상태 벡터가 Encoder와 Decoder를 이어준다.

순전파 때는 Encdoer에서 인코딩된 정보가 은닉 상태 벡터를 통해 Decoder로 전해진다. 반대로 역전파 때는 기울기가 은닉 상태 벡터를 통해 Decoder에서 Encoder로 전해진다.

7.2.2 - 시계열 데이터 변환용 장난감 문제

seq2seq의 구현을 위해 구체적인 예시, 바로 '더하기' 문제에 대해 다뤄보자. 참고로 머신러닝을 평가하고자 만든 간단한 문제를 '장난감 문제toy problem^{toy \space problem}'라고 한다.

seq2seq는 덧셈에 사용되는 문자의 패턴을 학습한다.

그런데 우리는 지금까지 word2vec이나 언어 모델 등에서 문장을 '단어' 단위로 분할했다. 하지만 문장을 반드시 단어 단위로 분할해야 하는 건 아니다. 이번 문제에서는 '문자' 단위로 분할하고자 한다.

문자 단위라는 것은, "57+5"가 입력되면 ['5', '7', '+', '5']라는 리스트로 처리하는 걸 말한다.

7.2.3 - 가변 길이 시계열 데이터

주의할 점은 덧셈 문장("57+5"나 "628+521" 등)이나 그 대답("62"나 "1149" 등)의 문자 수가 문제마다 다르다는 것이다.

이렇듯 이번 '덧셈' 문제에서는 샘플마다 데이터의 시간 방향 크기가 다르다. 가변 길이 시계열 데이터를 다룬다는 뜻이다. 따라서 신경망 학습 시에 미니배치 처리를 하려면 무언가 추가적인 조치가 필요하다.

가장 단순한 방법은 패딩padding^{padding}을 사용하는 것이다. 패딩이란 원래의 데이터에 의미 없는 데이터를 채워 모든 데이터의 길이를 균일하게 맞추는 기법이다.

남는 공간에는 공백(의미 없는 데이터)을 넣었다.

이번 문제에서는 0~999 사이의 숫자 2개만 더하기로 하자. 따라서 '+'까지 포함하면 입력의 최대 문자 수는 7이 된다. 자연스럽게 출력의 최대 수는 4가 된다.

질문과 정답을 구분하기 위해 출력 앞에 구분자로 언더바를 붙이기로 하자. 결과적으로 입력은 7문자, 출력은 5문자로 통일한다. 참고로 이 언더바가 Decoder에 시작을 알리는 신호가 된다.

WARNING_ Decoder에서 문자 출력의 종료를 알리는 구분자는 문제의 단순화를 위해 여기서는 넣지 않는다.

이처럼 패딩을 적용하는 경우에는 이 패딩용 문자까지 seq2seq가 처리하게 된다. 따라서 패딩을 적용하면서 정확성이 중요하다면 seq2seq에 패딩 전용 처리를 추가해야 한다.

예를 들면 Decoder에 입력된 데이터가 패딩이라면 손실의 결과에 반영하지 않도록 한다. 한편, Encoder에 입력된 데이터가 패딩이라면 LSTM 계층이 이전 시각의 입력을 그대로 출력하게 한다.

7.3 - seq2seq 구현

7.3.1 Encoder 구현

Encoder는 위 그림처럼 문자열을 받아 벡터로 변환한다. 여기에서는 LSTM 계층을 이용해 Encoder를 구성하고자 한다.

  • 패딩용 문자에 대한 처리는 따로 고려하지 않겠다.

Encoder는 Embedding 계층과 LSTM 계층으로 구성된다. Embedding 계층에서는 문자 ID를 문자 벡터로 변환한다.

LSTM 계층에서 위쪽으로의 출력은 위에 다른 계층이 없기 때문에 폐기된다.

WARNING_ Encoder에서는 LSTM의 은닉 상태 벡터만을 Decoder에 전달한다. 기억 셀도 전달할 수는 있지만 LSTM의 기억 셀을 다른 계층에 전달하는 일은 일반적으로 흔치 않다.

앞에서 이용했던 Time 계층을 이용하면 Encoder는 다음과 같은 형태가 된다.

7.3.2 - Decoder 구현

다음은 Decoder를 구현할 차례이다.

Decoder의 계층 구성은 다음과 같다.

학습 시에는 정답 레이블이 있기 때문에 시계열 방향의 데이터를 한꺼번에 줄 수 있다. 그러나 추론 시에는 시작을 알리는 구분자, 여기서는 언더바 하나만 주게 된다.

앞 절에서 문장을 생성할 때는 확률적으로 생성했기 때문에 샘플링할 때마다 생성되는 문장이 달라질 수 있었다. 그러나 이번에는 덧셈이기 때문에 결정적인 답을 생성하고자 한다.

'argmax' 노드는 최댓값을 가진 원소의 인덱스(이번 예에서는 문자 ID)를 선택하는 노드이다. 이번에는 Softmax 계층을 사용하지 않고, Affine 계층이 출력하는 점수가 가장 큰 문자 ID를 선택하면 된다.

  • Softmax 계층은 점수를 확률로 정규화하는 계층이다. 벡터의 각 원소의 값이 바뀌지만 대소관계는 그대로이기 때문에 최댓값을 구하는 문제에서는 생략해도 상관이 없다.

물론 학습 시에는 손실을 계산해야 하기 때문에 Softmax with Loss 계층을 이용할 것이다. 위의 argmax 노드는 문장 생성 때에만 사용한다.

여기까지 Decoder의 구성은 다음과 같다.

학습 시와 생성 시에 Softmax 계층의 취급이 달라지므로 그 전까지만 Decoder가 담당하는 걸로 하자. 다만 이럴 경우 seq2seq에서 Decoder의 손실을 구해주는 과정을 담당해줘야 한다.

7.4 seq2seq 개선

앞 절의 seq2seq를 세분화하여 학습 속도를 개선하고자 한다.

7.4.1 - 입력 데이터 반전(Reverse)

첫 번째 개선안은 아주 쉬운 트릭으로, 입력 데이터의 순서를 반전시키는 것이다.

이 트릭을 사용하면 많은 경우에 학습 진행이 빨라져서, 결과적으로 최종 정확도도 좋아진다고 한다.

7.4.2 - 엿보기(Peeky)

seq2seq의 두 번째 개선이다. 주제로 들어가기 전에, Encoder 동작을 한 번 더 살펴보자.

Encoder는 입력 문장을 고정 길이 벡터 h\bold h로 변환한다. 이 벡터 안에 Decoder에게 필요한 정보가 모두 담겨 있다. 그러나 현재의 Decoder는 최초 시각의 LSTM 계층만이 h\bold h를 이용하고 있다.

이 중요한 정보를 더 활용할 수는 없을까?

여기서 seq2seq의 두 번째 개선안이 등장한다. 바로 h\bold h를 Decoder의 다른 계층에게도 전해주는 것이다.

모든 시각의 Affine 계층과 LSTM 계층에 h\bold h를 전해준다. 기존에는 하나의 LSTM만이 소유하던 정보를 여러 계층이 공유할 수 있게 되었다.

이는 집단지성에 비유할 수 있다. 중요한 정보를 여러 사람과 공유한다면 더 올바른 결정을 내릴 가능성이 커진다.

NOTE_ 이 개선안은 다른 계층이 인코딩된 정보를 '엿본다'라고 해석할 수도 있다. '엿보다'를 영여로 peek이라고 하기 때문에 이 개선을 더한 Decoder를 'Peeky Decoder'라고 한다. 그리고 Peeky Decoder를 이용하는 seq2seq를 'Peeky seq2seq'라고 한다.

그런데 위 그림에서는 LSTM 계층과 Affine 계층에 입력되는 벡터가 2개씩이 되었다. 이는 실제로는 두 벡터가 연결concatenate^{concatenate}된 것을 의미한다. 따라서 앞의 그림은 다음처럼 그려야 정확하다.

이번에는 계산 그래프로 나타냈는데, 두 벡터가 concat 노드를 통해 연결되어 입력된다.

profile
초보입니다.

0개의 댓글