본문 바로가기
Deep Learning/밑바닥부터 시작하는 딥러닝

어텐션(Attention)

by 대소기 2022. 2. 18.

어텐션의 구조

 

seq2seq의 문제점

* seq2seq의 encoder는 고정 길이 벡터를(hidden state vector) decoder에 넘겨주게 된다. 이 때 hidden state vector로는 문장의 길이와 상관 없이 특정 길이의 벡터만 사용해야 된다. 이는 문장 정보를 vector에 충분히 담지 못하게 되는 문제를 야기한다.

 

 

Encoder 개선

 * 마지막 time step의 hidden state vector만 decoder에 넘겨주는 것이 아니라 매 time step의 hidden state vector를 모두 decoder에게 넘겨주면 된다. 이 때 각 time step에서 출력된 vector들은 하나의 행렬로서 decoder에게 전달된다. 이를 통해 고정길이 벡터라는 제한을 넘어설 수 있다.

* 각 time step의 hidden state 는 각 time step의 입력 단어를 vector로 표현한 것으로 간주할 수 있다. 해당 hidden state에는 해당 tiem step의 입력이 가장 많은 영향을 주기 때문이다.

* 우리가 다룰 예제에서는 keras를 사용하지는 않지만, keras를 사용하기 위해서는 LSTM계층의 parameter 중 return_sequence를 True로 설정해 주면 된다.

 

 

 

Decoder 개선 - 1

* encoder에서 hidden state 여러개가 전달되기 위해서는 decoder도 개선되어야 한다.

* 이를 위해 encoder의 hidden state들을 입력으로 받아서 처리를 해주는 계층을 생성해주어야 한다. 또한 이 계층은 decoder의 LSTM 셀의 출력도 입력으로 받는다. 이 계층에서는 출력할 단어에 맞춰 hidden state 들 중 알맞은 hidden state vector를 고르는 기능을 하게 된다. 하지만, 이 vector를 선택하는 작업은 미분이 불가능하다. 미분이 불가능하면 backpropagation을 시행할 수가 없어진다.

* 때문에 이 선택하는 작업을 미분 가능하게 만들기 위해 연산으로서 구현하게 된다. 

 

* 위와 같이 각 단어의 중요도를 표현하는 가중치 a를 사용해 각 vector와 가중치를 곱해서 모두 더한 가중합으로서 c vector를 최종적으로 사용하게 된다. 이 c vector는 context vector라고 부른다.

* 이를 계산 그래프로 살펴보면 위와 같다. a를 repeat해 ar 행렬을 만들고 이를 hs와 곱해 1번째 axis끼리 더한다.

 

class WeightSum:
    def __init__(self):
        self.params, self.grads = [], []
        self.cache = None

    def forward(self, hs, a):
        N, T, H = hs.shape

        ar = a.reshape(N, T, 1)#.repeat(T, axis=1)
        t = hs * ar
        c = np.sum(t, axis=1)

        self.cache = (hs, ar)
        return c

    def backward(self, dc):
        hs, ar = self.cache
        N, T, H = hs.shape
        dt = dc.reshape(N, 1, H).repeat(T, axis=1)
        dar = dt * hs
        dhs = dt * ar
        da = np.sum(dar, axis=2)

        return dhs, da

* 이 계층은 계산만 하는 계층이므로 학습할 파라미터가 존재하지 않아 self.params=[] 로 설정한다.

 

 

 

Decoder 개선 - 2

* 개선 두번째 방안에서는 가중치 a를 구하는 방법을 알아보겠다.

* 가중치 a를 구하기 위해서는 hs와 h를 곱한다. 여기서 h는 decoder lstm의 출력이다.

* 위와 같이 벡터간의 내적을 통해 나온 score(유사도)를 아래와 같이 softmax를 이용해 정규화 해준다.

 

* 이 softmax과정까지 계산 그래프에 포함하면 아래와 같다.

* 코드로 구현해보자.

class AttentionWeight:
    def __init__(self):
        self.params, self.grads = [], []
        self.softmax = Softmax()
        self.cache = None

    def forward(self, hs, h):
        N, T, H = hs.shape

        hr = h.reshape(N, 1, H).repeat(T, axis=1)
        t = hs * hr
        s = np.sum(t, axis=2)
        a = self.softmax.forward(s)

        self.cache = (hs, hr)
        return a

    def backward(self, da):
        hs, hr = self.cache
        N, T, H = hs.shape

        ds = self.softmax.backward(da)
        dt = ds.reshape(N, T, 1).repeat(H, axis=2)
        dhs = dt * hr
        dhr = dt * hs
        dh = np.sum(dhr, axis=1)

        return dhs, dh

 

 

 

Decoder 개선-3

 

* Attention weight 계층과 wieght sum계층을 하나로 결합해보자.

 

* 위와 같이 Attention weight을 구하고, 이를 통해 weight sum을 하는 계층을 하나의 계층으로서 Attention 계층이라고 부르겠다.

 

* 이를 코드로 구현하면 아래와 같다.

 

class Attention:
    def __init__(self):
        self.params, self.grads = [], []
        self.attention_weight_layer = AttentionWeight()
        self.weight_sum_layer = WeightSum()
        self.attention_weight = None
        
    def forward(self, hs, h):
        a = self.attention_weight_layer.forward(hs, h)
        out = self.weight_sum_layer.forward(hs, a)
        self.attention_weight = a
        return out
    
    def backward(self, dout):
        dhs0, da = self.weight_sum_layer.backward(dout)
        dhs1, dh = self.attention_weight_layer.backward(da)
        dhs = dhs0 + dhs1
        return dhs, dh

 

 

* 앞장에서 생성한 decoder와 Attention 계층이 추가된 decoder를 비교해보자면 위와 같다.

 

 

* Time Attention 계층을 구현해보자.

 

class TimeAttention:
    def __init__(self):
        self.params, self.grads = [], []
        self.layers = None
        self.attention_weights = None

    def forward(self, hs_enc, hs_dec):
        N, T, H = hs_dec.shape
        out = np.empty_like(hs_dec)
        self.layers = []
        self.attention_weights = []

        for t in range(T):
            layer = Attention()
            out[:, t, :] = layer.forward(hs_enc, hs_dec[:,t,:])
            self.layers.append(layer)
            self.attention_weights.append(layer.attention_weight)

        return out

    def backward(self, dout):
        N, T, H = dout.shape
        dhs_enc = 0
        dhs_dec = np.empty_like(dout)

        for t in range(T):
            layer = self.layers[t]
            dhs, dh = layer.backward(dout[:, t, :])
            dhs_enc += dhs
            dhs_dec[:,t,:] = dh

        return dhs_enc, dhs_dec

 

 

 

Attention을 갖춘 seq2seq 구현

 

 

 

Encoder 구현

import sys
sys.path.append('..')
from common.time_layers import *
from ch07.seq2seq import Encoder, Seq2seq
from ch08.attention_layer import TimeAttention


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 구현

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

 

 

 

 

 

seq2seq 구현

class AttentionSeq2seq(Seq2seq):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        args = 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

 

 

 

 

 

Attention 평가

 

 

 

날짜 형식 변환 문제

* 날짜 형식에는 다향한 표기법이 있어 복잡하지만, 입력과 출력 사이에 년, 월, 일의 대응관계가 있기 때문에 attention의 성능을 평가하기 좋다.

 

 

 

 

# coding: utf-8
import sys
sys.path.append('..')
sys.path.append('../ch07')
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 attention_seq2seq import AttentionSeq2seq
from ch07.seq2seq import Seq2seq
from ch07.peeky_seq2seq import PeekySeq2seq


# 데이터 읽기
(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)
# model = Seq2seq(vocab_size, wordvec_size, hidden_size)
# model = PeekySeq2seq(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('정확도 %.3f%%' % (acc * 100))


model.save_params()

# 그래프 그리기
x = np.arange(len(acc_list))
plt.plot(x, acc_list, marker='o')
plt.xlabel('에폭')
plt.ylabel('정확도')
plt.ylim(-0.05, 1.05)
plt.show()

* 성능을 앞장에서 살펴봤던 모델과 비교해보면 더 좋다는 것을 알 수 있다. seq2seq은 이 문제를 아예 풀지 못한다.

 

 

 

 

 

Attention 시각화

* Attention이 시계열 변환을 수행할 때 어느 원소에 주의를 기울이는지 눈으로 확인해보자.

* 코드에서 attention_weights에 각 시각의 attention 가중치가 저장되기 때문에 이를 활용해 입력 문장과 출력 문장의 단어 대응 관계를 살펴볼 수 있다.

 

 

 

 

Attention에 관한 남은 이야기

 

 

양방향 RNN

* hs에 담기는 각 vector들은 해당 입력 이전의 정보까지만 담겨있다.

* 하지만, 인간이 번역을 할 때를 생각해보면 문장 전체를 보는 것이(해당 단어 뒷부분의 정보까지) 더 번역에 있어 성능이 좋을 것이다.

* 이를 반영한 것이 양방향 lstm이다. 역방향 lstm계층을 하나 더 추가한 구조를 띤다.

* 구현할 때는 동일한 lstm계층에 입력만 뒤에서부터 주면 된다. 그리고 lstm 두 계층의 출력 vector들을 concatenate해주거나 sum해주거나 mean해주는 방법으로 최종 vector를 생성한다.

 

class TimeBiLSTM:
    def __init__(self, Wx1, Wh1, b1,
                 Wx2, Wh2, b2, stateful=False):
        self.forward_lstm = TimeLSTM(Wx1, Wh1, b1, stateful)
        self.backward_lstm = TimeLSTM(Wx2, Wh2, b2, stateful)
        self.params = self.forward_lstm.params + self.backward_lstm.params
        self.grads = self.forward_lstm.grads + self.backward_lstm.grads

    def forward(self, xs):
        o1 = self.forward_lstm.forward(xs)
        o2 = self.backward_lstm.forward(xs[:, ::-1])
        o2 = o2[:, ::-1]

        out = np.concatenate((o1, o2), axis=2)
        return out

    def backward(self, dhs):
        H = dhs.shape[2] // 2
        do1 = dhs[:, :, :H]
        do2 = dhs[:, :, H:]

        dxs1 = self.forward_lstm.backward(do1)
        do2 = do2[:, ::-1]
        dxs2 = self.backward_lstm.backward(do2)
        dxs2 = dxs2[:, ::-1]
        dxs = dxs1 + dxs2
        return dxs

 

 

Attention 계층 사용 방법

* 우리는 위와 같이 LSTM계층과 Affine계층 사이에 Attention계층을 삽입했었다. 하지만, 이와 다른 위치에 attention계층을 삽입하는 것도 가능하다.

 

* 위와 같이 Attention계층의 출력이 Affine계층이 아닌 다음 time step의 lstm에 입력으로 들어가는 것도 가능하다.

 

 

 

seq2seq 심층화와 skip 연결

* 더 표현력 좋은 모델을 만들기 위해 위와 같이 lstm계층을 여러 개 쌓을 수 있다.

* 이 때 lstm 계층의 개수는 encoder와 decoder를 맞춰주는게 좋다.

* attention 계층은 decoder의 첫 번째 lstm 출력과 encoder의 hs를 받아 decoder의 두 번째 이상의 lstm계층으로 전달하는 구조로 설계할 수 있다. 이 외에도 다양한 방법의 attention 계층 활용법이 있다.

* skip 연결은 residual connection 혹은 short-cut이라고도 하며, 계층을 넘어 선을 연결하는 단순한 구조를 뜻한다.

* 단순히 층을 뛰어넘어 더해주는 구조인데, 이 더해주는 노드는 역전파시 기울기를 그대로 흘려주기 때문에 기울기가 소실되지 않고 전파된다.

 

 

 

 

Attention 응용

 

구글 신경망 기계 번역(GNMT)

* encoder, decoder, attention으로 구성되어 있으며, 정확도를 높이기 위해 lstm계층을 다층화 하고, 양방향 lstm을 사용하고 skip연결, 여러 GPU를 통한 분산학습을 시행하였다.

 

* 구분기반보다는 훨신 뛰어나고 사람과 비슷한 성능까지 쫒아오고 있다.

 

 

 

 

Transformer

 

* RNN은 시간 방향으로 순서대로 계산하기 때문에 GPU를 사용한 병렬 계산에 적용하기 힘들다.

* transformer는 이 RNN의 단점을 개선하기 위해 RNN을 전혀 사용하지 않고 Attention만을 사용하여 모델을 구성한다.

* 이 때 self attention을 사용한다. 오른쪽에 있는 것이 self attention의 구조이다. attention의 입력으로 모두 hs가 들어오기 때문에 hs간의 대응관계를 살펴보기 좋다.

 

* 전체적인 구조는 위와 같이 attention을 사용하는 방식으로 되어 있다.

 

 

 

Neural Turing Machine(NTM)

 

* RNN 외부에 메모리 구조를 생성하고, attention을 이용해 이 메모리에서 필요한 정보를 읽거나 쓰는 방식을 뜻한다.

 

* write head는 lstm의 hidden state를 받아 필요한 정보를 메모리에 쓰고, read head는 메모리에서 필요한 정보를 읽어들여 다음 시점의 lstm에게 전달한다.

* 여기서 읽기 및 쓰기는 attention이 담당한다.

* 콘텐츠 기반 attention은 입력으로 주어진 벡터와 기존 메모리에 있는 벡터들간의 유사도를 통해 어느 벡터를 읽을지를 결정한다.

* 위치 기반 attention은 이전 시각에서 주목한 메모리의 위치를 기준으로 그 전후로 이동하는 용도로 사용된다. 이를 통해 메모리 위치를 옮겨가며 읽어나가는 컴퓨터 특유의 움직임을 쉽게 재현할 수 있다.

'Deep Learning > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

RNN을 사용한 문장 생성  (0) 2022.02.18
게이트가 추가된 RNN  (0) 2022.02.10
순환신경망(RNN)  (0) 2022.02.10
word2vec 속도개선  (0) 2022.02.10
Word2Vec  (0) 2022.02.03