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

Decoder 개선②

각 단어의 중요도를 나타내는 가중치 a가 있다면, 가중합을 이용해 "맥락 벡터"를 얻을 수 있다.

그렇다면 각 단어의 가중치 a를 구하는 방법이 뭐가 있을까?

이 방법을 설명하려면 우선 Decoder의 첫 번째 LSTM 계층이 은닉 상태 벡터를 출력할 때까지의 처리를 알아야 한다.

위의 그림에서 Decoder의 LSTM 계층의 은닉 상태 벡터를 h라 정했다. 목표는 h가 hs의 각 단어 벡터와 얼마나 "비슷한가"를 수치로 나타내는 것이다. 가장 단순한 방법은 벡터의 "내적"을 이용하는 것이다. 내적 계산은 다음과 같다.

벡터의 내적 계산

직관적인 의미는 "두 벡터가 얼마나 같은 방향을 향하고 있는가"이다. 따라서 두 벡터의 "유사도"를 표현하는 척도로 내적을 이용하는 것은 자연스러운 선택이라고 할 수 있다.

더보기

벡터의 유사도를 계산하는 방법은 내적 말고도 여러 가지가 있다. 유사도 점수를 출력하는 작은 신경망을 사용하는 사례도 있다.

그럼, 내적을 이용해 벡터 사이의 유사도를 산출할 때까지의 처리를 그림으로 살펴보자.

여기에서는 벡터의 내적을 이용해 h와 hs의 각 단어 벡터와의 유사도를 구한다. 그리고 s는 그 결과이다. s는 정규화하기 전의 값이며, 점수라고도 한다. 계속해서 s를 정규화하기 위해서는 일반적으로 소프트맥스 함수를 적용한다.

소프트맥스 함수를 이용하면 그 출력인 a의 각 원소는 0.0 ~ 1.0 사이의 값이 되고, 모든 원소의 총합은 1이 된다.

이상으로 각 단어의 가중치를 나타내는 a를 구해봤다.

코드를 통해 살펴보자.

N, T, H = 10, 5, 4
hs = np.random.randn(N, T, H)
h = np.random.randn(N, H)
hr = h.reshape(N, 1, H).repeat(T, axis = 1)
# hr = h.reshape(N, 1, H)

t = hs * hr
print(t.shape)

s = np.sum(t, axis = 2)
print(s.shape)

softmax = Softmax()
a = softmax.forward(s)
print(a.shape)

이 구현은 미니배치 처리를 수행할 때의 코드이다. 여기에서 reshape()와 repeat() 메서드를 이용해 적합한 형상의 hr을 생성한다.

위의 그림과 같이 계산 그래프는 Repeat 노드, 원소별 곱을 뜻하는 X 노드, Sum 노드, 그리고 Softmax 계층으로 구성된다.

from common.np import *

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

이 구현에서도 이전 WeightSum 계층의 구현과 마찬가지로 Repeat와 Sum 연산이 등장한다.

 


Decoder 개선③

Decoder 개선안을 두 가지로 나눠 설명했다. 3번째 개선은 1, 2번째 개선을 하나로 결합할 것이다.

결과는 아래 그림과 같다.

맥락 벡터를 구하는 계산 그래프의 전체 모습이다. 이 계산을 WeightSum 계층과 AttentionWeight 계층, 2개로 나눠 구현했다. 

Encoder가 건네주는 정보 hs에서 중요한 원소에 주목하여, 그것을 바탕으로 맥락 벡터를 구해 위쪽 계층으로 전파한다. 계층을 구현한 코드를 살펴보자.

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)
        shd1, dh = self.attention_weight_layer.backward(da)
        dhs = dhs0 + dhs1
        return dhs, dh

2개의 계층에 의한 순전파와 역전파를 수행할 뿐이다. 이때 각 단어의 가중치를 나중에 참조할 수 있도록 attention_weight라는 인스턴스 변수에 저장한다. 이상으로 Attention 계층의 구현은 끝이다. 이 Attention 계층을 LSTM 계층과 Affine 계층 사이에 삽입하면 된다.

위의 그림에서 각 시각의 Attention 계층에는 Encoder의 출력인 hs가 입력된다.

여기에서는 LSTM 계층의 은닉 상태 벡터를 Affine 계층에 입력한다. 

그림의 오른쪽은 앞에서 배운 Decoder에 Attention 계층이 구한 맥락 벡터 정보를 "추가"한 것으로 생각할 수 있다.

Affine 계층에는 기존과 마찬가지로 LSTM 계층의 은닉 상태 벡터를 주고, 여기에 더해 Attention 계층의 맥락 벡터까지 입력하는 것이다.

마지막으로 시계열 방향으로 펼쳐진 다수의 Attention 계층을 Time Attention 계층으로 모아 구현해볼 것이다. 그림으로는 아래 그림처럼 된다.

그럼 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 계층을 필요한 수만큼 만들고, 각각이 순전파와 역전파를 수행한다. 또한 각 Attention 계층의 각 단어의 가중치를 attention_weights 리스트에 보관한다.

여기까지 어텐션 구조를 모두 설명하고 구현까지 끝마쳤다. 다음 글에서는 어텐션을 사용해 seq2seq를 구현하고, 문제를 풀어볼 것이다.

TAGS.

Comments