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

8장

by sxlvxrjxms2s2itsmes2s2 2023. 8. 28.

8.1 어텐션의 구조

8.2어텐션을 갖춘 seq2seq 구현

8.3 어텐션 평가

8.4 어텐션에 관한 남은 이야기

8.5 어텐션 응용

8.6 정리

 

우리는 2개의 RNN을 연결하여 하나의 시계열 데이터를 다른 시계열 데이터로 변환한 모델인 seq2seq를 배웠다.

이번 장에서는 seq2seq의 가능성과 RNN의 가능성을 한 걸음 더 깊이 탐험한다.

 

8.1 어텐션의 구조

seq2seq를 더 강력하게 하는 어텐션 메커니즘이라는 아이디어를 소개한다.

이는 필요한 정보에만 주목할 수 있게 해주며 seq2seq가 안고 있던 문제를 해결하게 도와준다.

 

1) seq2seq의 문제점

 

seq2seq에서는 Encoder가 시계열 데이터를 인코딩한다.

그리고 인코딩된 정보를 Decoder에 전달한다.

 

이때 Encoder의 출력은 고정길이의 벡터이다. 이 고정 길이라는 데에는 큰 문제가 잠재해 있다.

고정 길이 벡터는 입력 문장 길리에 관계없이 항상 같은 길이의 벡터로 변환한다는 뜻이다.

아무리 긴 문장이 입력되더라도 항상 똑같은 길이의 벡터에 밀어 넣어야 한다.

 

 

이렇게 하면 필요한 정보가 벡터에 다 담기지 못하게 된다.

 

2) Encoder 개선

 

지금까지 LSTM 계층의 마지막 은닉 상태만을 Decoder에 전달했다. 그러나 Encoder 출력의 길이는 입력 문장의 길이에 따라 바꿔주는 게 좋다. 이 점이 Encoder의 개선 포인트이다.

 

 

위의 그림처럼 시각 별 LSTM 계층의 은닉 상태 벡터를 모두 이용하면 입력된 단어와 같은 수의 벡터를 얻을 수 있다.

위의 그림에서는 5개의 단어가 입력되었고, 이때 Encoder는 5의 벡터를 출력한다.

이것으로 Encoder는 '하나의 고정 길이 벡터'라는 제약으로부터 해방된다.

 

그런데 주목해야할 것은 LSTM 계층의 은닉 상태의 내용이다.

각 시각의 은닉 상태에는 직전에 입력된 단어에 대한 정보가 많이 포함되어 있다.

예로, "고양이" 단어를 입력했을 때의 LSTM 계층의 출력(은닉 상태)은 직전에 입력한 "고양이"라는 단어의 영향을 가장 크게 받는다.
따라서 이 은닉 상태 벡터는 "고양이"의 성분이 많이 들어간 벡터라고 생각할 수 있다.

 

그렇다면 Encoder가 출력하는 hs행렬은 각 단어에 해당하는 벡터들의 집합이라고 볼 수 있다.

 

 

3) Decoder 개선

 

Encoder는 각 단어에 대응하는 LSTM 계층의 은닉 상태 벡터를 hs로 모아 출력한다.

그리고 이 hs가 Decoder에 전달되어 시계열 변환이 이뤄진다.

앞 장에서 본 가장 단순한 seq2seq에서는 Encoder의 마지막 은닉 상태 벡터만을 Decoder에 넘겼다.

= Encoder의 LSTM 계층의 마지막 은닉 상태를 Decoder의 LSTM 계층의 첫 은닉 상태로 설정

 

hs에서 마지막 줄만 빼내어 Decoder에 전달한 것이다.

 

그럼 hs 전부를 활용할 수 있도록 Decoder을 개선해보자

 

우리는 어떤 단어에 주목하여 그 단어의 변환을 수시로 하게되는데(고양이=cat) 이를 seq2seq로 재현해볼 수 있을까?

== 입력과 출력의 여러 단어 중 어떤 단어끼리 서로 관련되어 있는가 라는 대응관계를 seq2seq에 학습시킬 수 없을끼?

 

앞으로 우리의 목표는 "도착어 단어"와 대응 관계에 있는 "출발어 단어"의 정보를 골라내는 것과 그 정보를 이용하여 번역을 수행하는 것이다. 이 구조를 어텐션이라 부른다.

 

앞으로 우리가 구현하고자 하는 신경망의 계층 구성

 

위의 그림처럼 새롭게 '어떤 계산'을 수행하는 계층을 추가할 것이다.

이 '어떤 계산'이 받는 입력은 두 가지로, 하나는 Encoder로부터 받는 hs이고, 다른 하나는 시각별 LSTM 계층의 은닉 상태 h 이다.

 

여기서 필요한 정보만 골라 위쪽의 Affine 계층으로 출력한다.(지금까지와 똑같이 Encoder의 마지막 은닉 상태 벡터는 Decoder의 첫 번째 LSTM 계층에 전달한다.)

 

그런데 우리가 위의 신경망으로 하고 싶은 일은 단어들의 얼라인먼트 추출이다. = 각 시각에서 Decoder에 입력된 단어와 대응 관계인 단어의 벡터를 hs에서 골라내는 것이다.

Decoder가 " I " 를 출력할 때, hs에서 "나"에 대응하는 벡터를 선택하면 된다.

이 선택을 '어떤 계산'으로 해내겠다는 것이다.

 

하지만 선택하는 작업(여러 대상으로부터 몇 개를 선택하는 작업)은 미분할 수 없다.

선택한다 라는 작업을 미분 가능한 연산으로 대체할 수 없을까?

그 아이디어란 하나를 선택하는 것이 아닌 모든 것을 선택한다는 것이다.

그리고 각 단어의 중요도를 나타내는 '가중치'를 별도로 계산하도록 한다

 

 

단어의 중요도를 나타내는 가중치로 a를 이용한다. (a : 0.0~1.0 사이의 스칼라, 모든 원소의 총합은 1)

각 단어의 중요도를 나타내는 가중치 a와  벡터 hs로부터 가중합을 구해 원하는 벡터를 얻는다.

 

 

가중합을 맥락 벡터라고 부르고 기호로는 c라고 표기한다.

여기서 "나"에 대응하는 가중치가 0.8이다.

맥락 벡터 c에는 "나" 벡터의 성분이 많이 포함되어 있다는 것이다.

 "나" 벡터를 "선택"하는 작업을 이 가중합으로 대체하고 있다고 할 수 있다. 

 

그럼 코드를 통해 살펴보자. Encoder가 출력하는 hs와 각 단어의 가중치 a를 적당하게 작성하고, 그 가중합을 구하는 구현을 볼 수 있다.

 

import numpy as np

T, H = 5, 4
hs = np.random.randn(T, H)
a = np.array([0.8, 0.1, 0.03, 0.05, 0.02])

ar = a.reshape(5, 1).repeat(4, axis = 1)
print(ar.shape)

t = hs * ar
print(t.shape) #(5, 4)

c = np.sum(t, axis = 0)
print(c.shape) #(4, )

 

시계열의 길이는 T = 5, 은닉 상태 벡터의 원소 수를 H = 4로 하여 가중합을 구하는 과정이다.

ar = a.reshape(5, 1).repeat(4, axis = 1) 코드는 a를 아래 그림처럼 ar로 변환한다.

 

 

형상이 (5,)인 a를 복사하여, (5, 4)짜리 배열을 만드는 것이다. 원래 형상이 (5,)인 a를 a.reshape(5, 1)을 거쳐 (5, 1) 형상으로 성형한 다음, 이 배열의 한 축을 네 번 반복하여 형상이 (5, 4)인 배열을 생성한다.

 

+) repeat() 메서드 대신 넘파이의 브로드캐스트를 사용해도 된다.

 

계속해서 미니배치 처리용 가중합을 구현한다. 코드로 살펴보자.

N, T, H = 10, 5, 4
hs = np.random.randn(N, T, H)
a = np.random.randn(N, T)
ar = a.reshape(N, T, 1).repeat(H, axis = 2)

t = hs * ar
print(t.shape) # (10, 5, 4)

c = np.sum(t, axis = 1)
print(c.shape) # (10, 4)

 

가중합 계산을 계산 그래프로 그려본다.

 

여기에서는 Repeat 노드를 사용해 a를 복제한다. 이어서 'x' 노드로 원소별 곱을 계산한 다음 Sum 노드로 합을 구한다.

 

역전파를 살펴보자

Repeat의 역전파는 Sum이고 Sum의 역전파는 Repeat이다.

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(H, axis = 2)
        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) # sum의 역전파
        dar = dt * hs
        dhs = dt * ar
        da = np.sum(dar, axis = 2) # repeat의 역전파
        
        return dhs, da

 

이것이 맥락 벡터를 구하는 Weight Sum 계층의 구현이다.

이 계층은 학습하는 매개변수가 없으므로 self.params=[] 로 설정한다.

 

4) Decoder 개선 ②

 

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

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

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

 

 

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

 

벡터의 내적 계산

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

 

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

 

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

 

 

이상으로 각 단어의 가중치를 나타내는 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) # (10, 5, 4)

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

softmax = Softmax()
a = softmax.forward(s)
print(a.shape) # (10, 5)

 

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

 

 

이 계산 그래프가 표현하는 처리를 AttentionWeight 클래스로 구현한다.

 

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

 

 

5) Decoder 개선 ③

 

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

결과는 아래 그림과 같다.

 

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

 

다시 말하면, Attention Weight 계층은 Encoder가 출력하는 각 단어의 벡터 hs에 주목하여 해당 단어의 가중치 a를 구한다. 이어서 Weight Sum 계층이 a hs의 가중합을 구하고 그 결과를 맥락 벡터 c로 출력한다.

 

이 계산을 수행하는 계층을 Attention계층이라고 부르자.

 

 

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의 개선으로부터 자연스럽게 확장된 것으로 볼 수 있다.

앞 장의 Decoder에 어텐션 정보를 추가할 수 있기 때문이다

 

위의 그림의 오른쪽은 앞에서 배운 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를 구현하고, 문제를 풀어볼 것이다.

 

8.2 어텐션을 갖춘 seq2seq 구현

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

 

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

 

2) 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() 메서드를 사용했다.

 

 

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

 

8.3 어텐션 평가

 

1) 날짜 형식 변환 문제

 

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

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

 

 

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

 

2) 어텐션을 갖춘 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는 학습을 거듭할수록 정확도 높아진다.

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

 

3) 어텐션 시각화

 

시각화로 Attention이 어느 원소에 주의를 기울이는지를 볼 수 있다.
Attention 계층에서 attention_weight라는 인스턴스 변수에 가중치 a를 저장했던 것을 사용해 시각화를 하면 된다.

 

 

가로 : 입력문장

세로 : 출력문장

각 맵의 원소는 밝을수록 값이 크다.

 

"1983"과 "26"이 훌륭하게 대응하고 있는게 보인다.

 

 

8.4 어텐션에 관한 남은 이야기

 

1) 양방향 RNN

 

앞 절까지의 Encoder는 아래처럼 그릴 수 있다.

LSTM의 각 시각의 은닉 상태 벡터는 hs로 모아진다. 그리고 Encoder가 출력하는 hs의 각 행에는 그 행에 대응하는 단어의 성분이 많이 포함되어 있다.

 

주목할 것은 우리는 글을 왼쪽에서 오른쪽으로 읽는다는 것이다. 따라서 "고양이"에 대응하는 벡터에 "나","는","고양이"까지 총 세 단어의 정보가 인코딩 되어 있다.

전체적인 균형을 위해 "고양이" 단어의 왼쪽 단어만이 아니라, '주변'정보를 균형있게 담아야 한다.

 

반대로 LSTM을 오른쪽으로도 처리하여 이 두가지를 같이 적용한 양방향 LSTM기술을 떠올릴 수 있다. 

 

  • 기존 LSTM의 계층에 더해서 역방향으로 처리하는 LSTM 계층도 추가한다.
  • 이 두 LSTM 계층의 은닉 상태를 연결 시킨 벡터를 최종 은닉 상태로 처리한다.('연결'외에도, '합', '평균'의 방법도 존재한다.)

구현도 쉽다. 2개의 LSTM을 이용해 각 계층에 주는 단어를 왼쪽부터 오른쪽으로 처리하고, 오른쪽부터 왼쪽으로 처리하도록 나누어 주면 된다.

 

 

2) Attention 계층 사용 방법

 

우리는 지금까지 위와 같은 구성으로 Attention 계층을 이용했다.

 

Attention 계층은 다른 위치에 들어가도 된다.

위 그림처럼 Attention 계층의 출력을 Affine에 입력되는게 아니라 LSTM 계층에 입력이 되도록 연결 해도 된다.
이렇게 구성하면 LSTM 계층이 맥락 벡터의 정보를 이용할 수 있게 된다.

 

이를 최종 정확도에 영향을 어떻게 줄지는 직접 해봐야 아는 것이다. 다만, 둘다 모두 맥락 벡터를 잘 활용하는 구성이라서 큰 차이가 없을지도 모른다.

 

구현 관점에서 보면 LSTM 계층과 Affine 계층 사이에 Attention 계층을 삽입하는 쪽이 구현하기 쉽다.

 

 

3) seq2seq 심층화와 skip 연결

 

현실에서 시계열 데이터는 훨씬 복잡하다.

즉, 어텐션을 갖춘 seq2seq에 더 높은 표현력을 요구하는데, 우선적으로 층을 깊게 쌓는 방법이 있다.

 

위 그림은 Encoder와 Decoder로 3층 LSTM 계층을 사용하고 있다.
이 예처럼 Encoder와 Decoder에서는 같은 층수의 LSTM 계층을 사용하는 것이 일반적이다.

 

위의 그림은 하나의 예일 뿐, 여러가지 Attention 계층이 존재한다.
계층을 깊게 쌓을 수록 일반화 성능을 떨어뜨리지 않는 것이 중요하다. 이에는 드롭아웃, 가중치 공유 등의 기술이 효과적이다.

층을 깊게 할 때 사용되는 중요한 기법 중 skip 연결이 있다.

'skip 연결'은 층을 깊에 할때 사용하는 중요한 기법이다. 계층을 넘어(=계층을 건너 뛰어) '선을 연결'하는 단순한 기법이다.

  • skip 연결의 접속부에서는 2개의 출력이 '더해'진다.
  • 이는 덧셈이기 때문에 역전파시 '흘려'보내서, skip 연결의 기울기가 아무런 영향을 받지 않아 기울기 소실, 기울기 폭발을 걱정할 필요가 없다.

 

8.5 어텐션 응용

 

8.6 정리

  • 번역이나 음성 인식 등, 한 시계열 데이터를 다른 시계열 데이터로 변환하는 작업에서는 시계열 데이터 사이의 대응 관계가 존재하는 경우가 많다.
  • 어텐션은 두 시계열 데이터 사이의 대응 관계를 데이터로부터 학습한다.
  • 어텐션에서는 벡터의 내적을 사용해 벡터 사이의 유사도를 구하고, 그 유사도를 이용한 가중합 벡터가 어텐션의 출력이 된다.
  • 어텐션에서 사용하는 연산은 미분 가능하기 때문에 오차역전파법으로 학습할 수 있다.
  • 어텐션이 산출하는 가중치를 시각화하면 입출력의 대응 관계를 볼 수 있다.
  • 외부 메모리를 활용한 신경망 확장 연구 예에서는 메모리를 읽고 쓰는 데 어텐션을 사용했다.

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

7장  (0) 2023.08.21
6장  (0) 2023.08.14
5장  (0) 2023.08.13
4장  (0) 2023.08.07
3장  (0) 2023.07.20