밑바닥부터 시작하는 딥러닝 - 어텐션(3)

어텐션을 갖춘 seq2seq 구현

Attention 계층의 구현을 끝낸 뒤 어텐션을 갖춘 seq2seq를 구현할 것이다.

 

Encoder 구현

AttentionEncoder 클래스를 구현할 것이다. 앞에서 구현한 Encoder 클래스와 거의 같다. Encoder 클래스의 forward() 메서드는 LSTM 계층의 마지막 은닉 상태 벡터만을 반환했다. 그에 반해, 이번에는 모든 은닉 상태를 반환할 것이다.

코드로 살펴보자.

class AttentionEncoder(Encoder):
    def forward(self, xs):
        xs = self.embed.forward(xs)
        hs = self.lstm.forward(xs)
        return hs
    
    def backward(self, dhs):
        dout = self.lstm.backward(dhs)
        dout = self.embed.backward(dout)
        return dout

 

Decoder 구현

이어서 Decoder 구현이다. 어텐션을 이용한 Decoder의 계층 구성은 아래 그림과 같다.

위의 그림에서, 앞에서의 구현과 마찬가지로 Softmax 계층의 앞까지를 Decoder로 구현할 것이다. 순전파의 forward()와 역전파의 backward() 메서드뿐 아니라 새로운 단어 열을 생성하는 generate() 메서드도 추가한다.

class AttentionDecoder:
    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(2 * H, V) / np.sqrt(2 * 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.attention = TimeAttention()
        self.affine = TimeAffine(affine_W, affine_b)
        layers = [self.embed, self.lstm, self.attention, self.affine]
        
        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads
            
    def forward(self, xs, enc_hs):
        h = enc_hs[:, -1]
        self.lstm.set_state(h)
        out = self.embed.forward(xs)
        dec_hs = self.lstm.forward(out)
        c = self.attention.forward(enc_hs, dec_hs)
        out = np.concatenate((c, dec_hs), axis = 2)
        score = self.affine.forward(out)
        
        return score
    
    def backward(self, dscore):
        dout = self.affine.backward(dscore)
        N, T, H2 = dout.shape
        H = H2 // 2

        dc, ddec_hs0 = dout[:,:,:H], dout[:,:,H:]
        denc_hs, ddec_hs1 = self.attention.backward(dc)
        ddec_hs = ddec_hs0 + ddec_hs1
        dout = self.lstm.backward(ddec_hs)
        dh = self.lstm.dh
        denc_hs[:, -1] += dh
        self.embed.backward(dout)

        return denc_hs

    def generate(self, enc_hs, start_id, sample_size):
        sampled = []
        sample_id = start_id
        h = enc_hs[:, -1]
        self.lstm.set_state(h)

        for _ in range(sample_size):
            x = np.array([sample_id]).reshape((1, 1))

            out = self.embed.forward(x)
            dec_hs = self.lstm.forward(out)
            c = self.attention.forward(enc_hs, dec_hs)
            out = np.concatenate((c, dec_hs), axis=2)
            score = self.affine.forward(out)

            sample_id = np.argmax(score.flatten())
            sampled.append(sample_id)

        return sampled

forward() 메서드에서 TimeAttention 계층의 출력과 LSTM 계층의 출력을 연결한다는 점만 주의하면 된다. 두 출력을 연결할 때는 np.concatenate() 메서드를 사용했다.

 

seq2seq 구현

AttentionSeq2seq 클래스의 구현은 앞에서 배운 seq2seq 와 거의 같다. 다른 점은 Encoder 대신 AttentionEncoder 클래스를, Decoder 대신 AttentionDecoder 클래스를 사용한 것뿐이다.

class AttentionSeq2seq(Seq2seq):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        agrs = vocab_size, wordvec_size, hidden_size
        self.encoder = AttentionEncoder(*args)
        self.decoder = AttentionDecoder(*args)
        self.softmax = TimeSoftmaxWithLoss()
        
        self.params = self.encoder.params + self.decoder.params
        self.grads = self.encoder.grads + self.decoder.grads

 

어텐션 평가

앞에서 구현한 AttentionSeq2seq 클래스를 사용해 문제를 풀 것이다.

날짜 형식을 변경하는 문제로 어텐션을 갖춘 seq2seq의 효과를 확인할 것이다.

더보기

번역용 데이터셋 중에는 'WMT'가 유명하다. 이 데이터셋에는 영어와 프랑스어 학습 데이터가 쌍으로 준비되어 있다. WMT 데이터셋은 많은 연구에서 벤치마크로 이용되고 있으며, seq2seq의 성능을 평가하는 데도 자주 이용된다.

 

날짜 형식 변환 문제

날짜 형식 변환 문제를 살펴볼 것이다. 영어권에서 사용되는 다양한 날짜 형식을 표준 형식으로 변환하는 것이 목표이다.

이 문제를 고른 이유가 두 가지가 있다. 하나는 이 문제가 겉보기만큼 간단하지 않다는 점이다. 입력되는 날짜 데이터에는 다양한 변형이 존재하여 변환 규칙이 나름 복잡해지기 때문이다. 두 번째 이유는 문제의 입력과 출력 사이에 알기 쉬운 대응 관계가 있기 때문이다. 년, 월, 일의 대응 관계가 존재하기 때문에 어텐션이 각각의 원소에 올바르게 주목하고 있는지를 확인할 수 있다.

그렇다면 학습 데이터를 살펴보자.

이 데이터셋은 입력 문장의 길이를 통일하기 위해 공백 문자로 패딩 해뒀고, 입력과 출력의 구분 문자로는 밑줄을 사용했다. 그리고 이 문제에서는 출력의 문자 수는 일정하기 때문에 출력의 끝을 알리는 구분 문자는 따로 사용하지 않았다.

 

어텐션을 갖춘 seq2seq의 학습

# 데이터 읽기
(x_train, t_train), (x_test, t_test) = sequence.load_data('date.txt')
char_to_id, id_to_char = sequence.get_vocab()

# 입력 문장 반전
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]

# 하이퍼파라미터 설정
vocab_size = len(char_to_id)
wordvec_size = 16
hidden_size = 256
batch_size = 128
max_epoch = 10
max_grad = 5.0

model = AttentionSeq2seq(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 = True)
    
    acc = float(correct_num) / len(x_test)
    acc_list.append(acc)
    print('val acc %.3f%%' % (acc * 100))
    
model.save_params()

위의 그림에서 보듯, 어텐션을 갖춘 seq2seq는 학습을 거듭할수록 정확도 높아진다.

그래프로 그리면 아래와 같다.

1 에폭부터 빠르게 정답률을 높여 2 에폭째에는 거의 모든 문제를 풀어낸다.

TAGS.

Comments