인공지능 응용 공학

seq2seq RNN

coding art 2023. 2. 3. 18:08
728x90

기계 번역 분야에서 매우 인기가 있었던 Sequence to Sequence 모델은 두 개의 RNN 즉 인코더와 디코더를 짝으로 구성된다. 인코더는 입력을 순차적으로 처리하고 모든 입력 데이터를 감안한 은닉층 벡터를 생성한다. 그 생성된 은닉층 데이터는 해당 컨텍스트를 사용하여 적절한 대상 시퀀스(번역, 챗봇의 응답)를 예측 학습하는 디코더로 전달된다.

 

중요 응용 분야는 다음과 같다.

기계 번역: 한 언어의 문장을 다른 언어로 번역

자동 용약: 긴 문장을 짧게 요약된 문장으로 변환

질의 응답: 질문을 응답으로 변환

메일 자동 응답: 수신한 이메일 문장을 답변 글로 변환

sequenceRNN 이나 LSTM에서 일정한 길이를 가지는 데이터를 의미한다. 인코더의 역할은 특정 언어의 입력 데이터를 받아들여 LSTM RNN 처리 후 일정 길이의 은닉층 벡터 h를 생성하여 디코더에 전달하여 순전파 및 역전파에 따른 학습이 이루어지게 된다.

 

다음 그림은 인코더와 디코더를 이용하는 간단한 기계 번역 구조를 보여준다. 인코더에서는 Embedding 레이어에 이어 LSTM 레이어 처리로 종료한 후 은닉층 정보 h 를 준비한다, 인코더에서는 더이상 상류로의 출력 처리는 없다.

아울러 디코더 구조에서는 번역어를 사용하여 LSTM 학습 구조를 갖추면 충분하다. 단 여기서 인코더의 은닉층 정보를 디코더에 어떻게 전달하느냐에 따라 기계 번역기의 성능에 큰 영향을 미친다. 한편 일반적인 LSTM 학습 구조에 인코더에서 전달되는 은닉층 정보 h 로부터 번역을 위한 입출력 데이터와의 연관성을 보강해 주어야 번역 효율이 더욱 좋아진다.

한빛 미디어, Deep Learning from scatch, 2, 317p., 7.3.4 seq2seq 평가 내용과 관련 코드를 참조한다.

train_seq2seq.py 학습 코드를 살펴보자. import 하는 sequence.py, Trainer, Seq2seq 클라스 명령에 주목하자.

sequence.py 에서는 질문에 해당하는 questions 와 정답에 해당하는 answers를 문자 텍스트로 입력받아 ’+‘ 포함 문자 7개와 ’-‘포함 해답 5글자를 padding 까지 고려하여 처리한다.

seq2seq.py 에 포함되어 있는 Seq2seq 클라스를 model로 선언하여 학습을 위한 Trainer 클라스에 인수로 옵티마이져 ’Adam’과 함께 제공한다.

1 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

 

특별히 addition.txt에서 앞부분을 잘라 아래와 같이 13개의 입력 데이터 즉 N=13add.txt 를 준비한다. 12개의 데이터는 학습용으로 나머지 1개는 validation 용으로 사용한다.

1 16+75 _91 8 84+317 _401
2 52+607 _659 9 9+3 _12
3 75+22 _97 10 6+2 _8
4 63+22 _85 11 18+8 _26
5 795+3 _798 12 85+52 _137
6 706+796_1502 13 9+1 _10
7 8+4 _12    

 

데이터 수가 작고 하이퍼 파라메터 값들이 작으면 출력을 통해 코드 구조 이해에 도움이 된다.

3 vocab_size = len(char_to_id)
wordvec_size = 3 # (N, T, D) ->D
hideen_size = 2; batch_size = 3; max_epoch = 1
max_grad = 5.0

shape V vocab_size 13 으로 ‘0’ ~ ‘9’, ‘+’, ‘_’ ‘ ’(블랭크, space bar)를 포함한다.

id_to_char {0: '1', 1: '6', 2: '+', 3: '7', 4: '5', 5: ' ', 6: '_', 7: '9', 8: '2', 9: '0', 10: '3', 11: '8', 12: '4'}

shape D 에 해당하는 wordvec_size 16 정도의 크기가 제대로 된 값이지만 최소화 해서 3 으로 설정한다.

shape H 에 해당하는 hidden_size 128 로 충분히 큰 값을 취해야 하나 여기서는 간단히 2 로 둔다.

batch_size =128 로 충분히 큰 값을 취해야 하나 여기서는 3 으로 둔다.

max_epoch = 5 정도로 두어야 하난 여기서는 1 로 둔다.

 

하이퍼 파라메터들을 인위적으로 조절해 둔 상태에서 출력값을 살펴보면서 코드의 실행을 시뮬레이션해 본다.

 

sequence.py 의 메서드 load_data 를 인수에 해당하는 입력 데이터 파일 ‘add.txt’를 지정하여 실행시키면 학습 데이터와 validation 데이터가 얻어진다.

13개의 데이터를 가진 ‘add.txt’를 사용하면 학습용 12개와 validation1개로 나누어 char_to_id id_to_char 데이터가 준비된다.

 

Seq1seq 클라스를 model 로 선언한다. 인수는 N,D,H 에 해당하는 하이퍼 파라메터 값들이다. 학습을 위해 model optimizer Trainer 클라스에 인수로 제공한다.

4 model = Seq2seq(vocab_size, wordvec_size, hidden_size)
optimizer = Adam()
trainer = Trainer(model, optimizer)

Trainer 의 메서드인 fit을 사용하여 학습을 시키도록 하자. model optimizer instance 변수로 둔다.

5 class Trainer:
def __init__(self, model, optimizer):
self.model = model
self.optimizer = optimizer
self.loss_list = []
self.eval_interval = None
self.current_epoch = 0

매서드 fit 에서는 instance 변수 인수들 즉 self.model, self.optimizer 를 일반 변수명인 model, optimizer 로 바꾸어 사용한 후 메서드 fit 의 실행이 끝나도 다시 instance 변수 형태로 fit 외부로 내보낼 필요가 없다.

batch_size 에 해당하는 미니배치의 수를 나타내는 변수 max_iters 13 // 3 = 4... 나머지 1 이므로 4가 된다.

6 def fit(self, x, t, max_epoch=10, batch_size=32, max_grad=None, eval_interval=20):
data_size = len(x)
max_iters = data_size // batch_size
self.eval_interval = eval_interval
model, optimizer = self.model, self.optimizer
total_loss = 0; loss_count = 0

메인코드인 위 4항의 학습을 위한 코드 준비에 이어 메서드 fit을 이용한 학습을 실행하자. 간단한 수치 실험을 위해 가장 작은 값인 max_epoch = 1 로 선언하였다. 인식률을 epoch 별로 계산하여 덧붙일 수 있도록 즉 append 할 수 있도록 비어있는 리스트 변수 acc_list 를 준비 선언한다.

7 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)

6항의 메서드 fit 명령들에 이어지는 실행 명령들을 살펴보자. epoch 별로 전체 데이터 N=13개 중 학습을 위한 12개 데이터와 1개의 validation 데이터를 numpy.random.permutation 명령을 사용 무작위로 섞는다.

학습 데이터 중 x[] 는 입력 데이터이며 t[] 는 정답 레이블 데이터이다.

max_iters = 4 이므로 iters = 0, 1, 2, 3 에 해당하는 미니배치 데이터를 준비하자. 3개의 데이터를 가지는 미니배치 별로 batch_x[] batch_t[] 를 준비한다. 미니배치 4개를 준비하면 딱 떨어진다.

model(Seq2seq클라스)의 순전파 model.forward 연산에 의해 Cost 함수 loss 를 수치적으로 계산하고 이어서 오차 역전파 연산을 수행한다.

8 for epoch in range(max_epoch):
idx = numpy.random.permutation( numpy.arange( data_size ) ) # 무작위 뒤섞기
x = x[idx]
t = t[idx]
for iters in range(max_iters):
batch_x = x[iters*batch_size:(iters+1)*batch_size]
batch_t = t[iters*batch_size:(iters+1)*batch_size]
# 기울기 구해 매개변수 갱신
loss = model.forward(batch_x, batch_t)
model.backward()

loss 순전파 연산을 위해서 사용하는 model.forward 는 코드 seq2seq.py에서 Seq2seq 클라스에 포함된 메서드 __init__ 과 메서드 forward 를 사용하게 된다.

초기화 과정에서 함수 Encoder(V, D, H) self.encoder 로 둔다.

 

아울러 메서드 forward에서는 디코딩을 위해 미니배치 데이터인 batch_tts 로 받은 후 한자리씩 밀어서 디코더용 입력 ts[:, :-1] 과 정답 레이블 ts[:, 1:] 를 준비한다.

 

decorde_xs 와 연산된 hself.decorder 에 입력하여 디코딩을 실행한다.

9 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()
...
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)

아래 관계식을 참조하면서 함수 Encoder(V, D, H)에서 이루어지는 초기화와 연산 내용을 살펴보자.

embed_W shape (V, D) 이며 모두 랜덤 수로 초기화 한다.

lstm_Wx shape (D, 4 * H) 이며 모두 랜덤 수로 초기화 한다.

lstm_Wh shape (H, 4 * H) 이며 모두 랜덤 수로 초기화 한다.

lstm_b shape (4 * H) 로서 모두 0(zeros) 으로 초기화 한다.

11 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')
...
class Decoder:

11항에서 초기화된 가중치 값들이 아래에서처럼 instance 변수들 self.embed self.lstm 의 연산이 이루어지고 이들을 상속하여 self.embed.params self.lstm.params 를 얻을 수 있다. 하지만 이 둘을 더 하여 self.params 로 둔다고 해서 덮어쓰기가 이루어지는 것이 아니라 각 단위별 shape 과 성분 값들을 그대로 유지하면서 단지 하나의 유닛으로 모으는 것이다. 이는 각각을 출력해 보면 알 수 있다.

12 self.embed = TimeEmbedding(embed_W)
self.lstm=TimeLSTM(lstm_Wx,lstm_Wh,lstm_b, stateful=False)
self.params = self.embed.params + self.lstm.params

다시 9항으로 돌아가 보면 Encoder 클라스에서는 TimeEmbedding TimeLSTM 순전파 연산이 이루어진다. 반면에 Decorder 클라스에서는 TimeEmbedding, TimeLSTM, 그리고 Affine 순전파 연산을 실행시키게 될 것이다. 마지막으로 self.softmax 순전파 연산이 뒤따른다.

 

이러한 작업은 학습을 시키기 위한 8항의 메인코드에서 미니배치 단위로 옵티마이져가 가중치 업데이트 작업을 통해 더 이상 기울기 변화가 없을 정도로 수렴하게 되면 학습이 완료되어 멈추게 될 것이다.

 

12항에서 self.embed 가중치 embed_W를 인수로 받는 함수 TimeEmbedding 으로 선언되어 있다. 이 클라스는 encoder 의 순전파 연산 및 decoder 의 입력 수전파 연산에 공통으로 사용됨에 유의하자.

13 class TimeEmbedding:
def __init__(self, W):
...
def forward(self, xs):
N, T = xs.shape
V, D = self.W.shape
out = np.empty((N, T, D), dtype='f')
self.layers = []
for t in range(T):
layer = Embedding(self.W)
out[:, t, :] = layer.forward(xs[:, t])
self.layers.append(layer)
return out