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

2025. 1. 7. 21:51·공부/밑바닥부터 시작하는 딥러닝

앞챕터에서 RNN을 사용해 문장을 생성했다. 

2개의 RNN을 연결하여 시계열 데이터를 다른 시계열 데이터로 변환도 해봤다.(seq2seq)

 

이번 챕터에서는 seq2seq의 가능성, 그리고 RNN의 가능성을 한걸음 더 깊이 탐험한다.

어텐션(Attention)은 스테이블 디퓨전같은 생성 모델을 포함하여, 최근 딥러닝 분야에서 중요한 기술 중 하나이다.

 

어텐션의 구조

seq2seq를 한층 더 강력하게 하는 어텐션 매커니즘을 소개한다.

텐션 매커니즘 덕분에 seq2seq는 인간처럼 필요한 정보에만 주목할 수 있다.

 

 

seq2seq의 문제점

seq2seq에서는 Encoder가 시계열 데이터를 인코딩한다. 인코딩된 정보를 Decoder로 전달한다.

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

아무리 긴 문장이라도 Encoder는 고정 길이의 벡터를 변환한다.

이때, 필요한 정보가 벡터에 다 담기지 못하는 상황이 올 수 있다. 

 

 

Encoder 개선

지금까지는 LSTM 계층의 마지막 은닉 상태만을 Decoder에 전달했다. 

그러나 Encoder 출력의 길이는 입력 문장의 길이에 따라 바꿔주는게 좋다.

이점이 Encoder의 개선 포인트이다. 구체적으로는, 시각별 LSTM 계층의 은닉 상태 벡터를 모두 이용하는 것이다.

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

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

 

대부분의 딥러닝 프레임워크에서는 RNN계층(LSTM,GRU)을 초기화할 때, '모든 시각의 은닉 상태 벡터 반환'과 '마지막 은닉 상태 벡터만 반환'중 선택할 수 있다. 

 

위 그림에서 주목할 것은 LSTM 계층의 은닉 상태 내용이다. 

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

단지 Encoder의 은닉 상태를 모든 시각만큼 꺼냈을 뿐이지만, 이 작은 개선 덕분에 Encoder는 입력 문장의 길이에 비례한 정보를 인코딩할 수 있게 되었다. 

 

 

Decoder 개선 1

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

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

Decoder의 계층 구성을 보자.

위 그림에서 보듯 Decoder는 Encoder의 LSTM 계층의 마지막 은닉 상태만을 이용한다.

hs에서 마지막 줄만 빼내는 것이다. 

여기에서 hs 전부를 활용할 수 있도록 Decoder를 개선해보자.

 

사람이 문장을 번역할 때는 어떤 단어에 주목하여 그 단어의 변환을 수시로 하게 된다.

이 과정을 seq2seq에 적용할 수 없을까? 정확히 말하면 입력과 출력의 여러 단어 중 어떤 단어끼리 서로 관련되어 있는지 대응관계를 학습시킬 수 있을까?

 

기계 번역의 역사를 보면 '고양이 = cat'과 같은 단어의 대응 관계 지식을 이용하는 연구는 많이 이뤄져 왔다. 

단어(혹은 문구)의 대응 관계를 나타내는 정보를 얼라인먼트(alignment)라고 하는데, 지금까지는 얼라인먼트를 주로 사람이 수작업으로 만들었다. 

하지만 어텐션 기술은 얼라인먼트 아이디어를 seq2seq에 자동으로 도입하는데 성공했다.

 

앞으로 우리의 목표는 '도착어 단어'와 대응 관계에 있는 '출발어 단어'의 정보를 골라내는 것, 그리고 그 정보를 이용하여 번역을 수행하는 것이다.

다시 말해, 필요한 정보에만 주목하여 그 정보로부터 시계열 변환을 수행하는 것이 목표이다. 

이 구조를 어텐션이라 부르며, 이번 챕터의 핵심 주제이다.

전체적인 틀을 보자. 우리가 구현하고자 하는 신경망의 계층 구성은 다음 그림과 같다.

'어떤 계산'을 수행하는 계층을 추가할 것이다. 

이 계층이 받는 입력은 Encoder로부터 받는 hs와, 시각별 LSTM계층의 은닉 상태이다. 

 

위 신경망으로 하고 싶은 일은 단어들의 얼라인먼트 추출이다.

하지만, 여러 대상으로부터 몇개를 선택하는 작업은 미분할 수 없다는 문제점이 있다. 

 

신경망의 학습은 일반적으로 오차역전파법으로 이뤄진다. 따라서 미분 가능한 연산으로 신경망을 구축하면 오차역전파법의 틀 안에서 학습을 수행할 수 있다. 

반대로 미분 가능한 연산을 이용하지 않으면 기본적으로는 오차역전파법을 이용할 수 없다.

 

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

사실 이 문제를 해결할 아이디어는 아주 단순하다. 하나를 선택하는 것이 아니라, 모든것을 선택하면 된다.

그리고 다음 그림과 같이 각 단어의 중요도(기여도)를 나타내는 가중치를 별도로 계산하도록 한다.

위 그림에서 보듯, 여기에서는 각 단어의 중요도를 나타내는 가중치(기호  a)를 사용한다.

a는 확률분포처럼 각 원소가 0~1 사이의 스칼라(단일 원소)이며, 모든 원소의 총합은 1이 된다.

가중치 a와 벡터 hs로부터 가중치 합(weighted sum)을 구하여, 우리가 원하는 벡터를 얻는다.

이 일련의 계산을 그림으로 나타내면 다음과 같다.

위 그림처럼 단어 벡터의 가중합을 계산한다. 

여기에서는 그 결과를 '맥락 벡터'라고 부르고, 기호로는 c로 표기한다. 

그림을 보면 "나"에 대응하는 가중치가 0.8이다. 이것이 의미하는 바는 맥락 벡터 c에는 "나"벡터 성분이 많이 포함되어 있다는 것이다.

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

 

맥락 벡터 c에는 현 시각의 변환(번역)을 수행하는 데 필요한 정보가 담겨 있다. 더 정확하게 말하면, 그렇게 되도록 데이터로부터 학습하는 것이다. 

 

코드로 살펴보자. Encoder가 출력하는 hs와 각 단어의 가중치 a를 적당히 작성하고, 그 가중합을 구하는 구현을 볼 수 있다. 다차원 배열의 형상에 주의하자.

# -*- coding: utf-8 -*-
"""
You can do it
"""

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)

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

실행 결과

 

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

ar. a.reshape(5,1).repeat(4, axis = 1)은 다음 그림과 같은 역할을 한다.

참고로, repeat메서드 대신 numpy의 브로드캐스트를 사용해도 된다.

ar = a.reshape(5,1)까지만 한 뒤 곧바로 hs * ar을 계산하면 hs의 형상과 일치되도록  ar이 자동으로 확장된다.

다음 그림을 참고하자.

 

계속해서 미니배치 처리용 가중합을 구현해보자.

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)
# ar = a.reshape(N,T,1) # 브로드캐스트

t = hs * ar
print(t.shape)

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

가중합의 계산 그래프를 그리면 다음과 같다.

그림에서 보듯, 가중치 a를 복제하여 곱계산 후 Sum노드로 합을 구한다. 

이 계산 그래프의 역전파도 살펴보자. Repeat의 역전파는 Sum이고, Sum의 역전파는 Repeat이다. 

텐서의 형상에 주의하여 살펴보면 어떤 축에 대한 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.sym(dar,axis = 2 )   #repeat의 역전파
        
        return dhs, da

 

 

 

Decoder 개선 2

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

그런데 이 a는 어떻게 구해야 할까? 

우선 Decoder의 첫번째 시각  LSTM 계층이 은닉 상태 벡터를 출력할 때까지의 처리부터 알아봐야 한다.

Decoder의 LSTM계층 은닉 상태 벡터는 h이다. 지금 목표는 h가 hs의 각 단어 벡터와 얼마나 비슷한가를 수치로 나타내는 것이다. 방법은 여러가지가 있지만, 가장 단순한 방법인 벡터의 내적을 이용해보자. 

예를 들어 벡터 a,b의 내적은 다음과 같이 계산한다.

내적의 직관적인 의미는 두 벡터가 얼마나 같은 방향을 향하고 있는가 이다. 

따라서 두 벡터의 유사도를 표현하는 척도로 내적을 이용하는 것은 자연스러운 선택이다. 

 

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

 

결과에서 보듯, 벡터의 내적을 이용해 h와 hs의 각 단어 벡터의 유사도를 구하고, s는 결과이다. 

s는 정규화 전 값이며, 정규화를 위해 일반적으로 softmax함수를 적용한다. 

위의 과정을 계산 그래프로 보면 다음과 같다.

계산 그래프는 Repeat, 곱, Sum, Softmax로 구성된다. 계산 그래프가 표현하는 처리를 코드로 보자.

# -*- coding: utf-8 -*-
"""
You can do it
"""
import sys
sys.path.append('..')
from common.np import *  # import numpy as np
from common.layers import 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

지금까지 Decoder 개선을 두가지로 나눠 설명했다. Attention Weight, Weight Sum 계층을 하나로 결합해보자.

위 계산에 따르면 Attention Weight 계층은 Encoder가 출력하는 각 단어의 벡터 hs에 주목하여 해당 단어의 가중치 a를 구한다. 

어서 Weight Sum 계층이 a와 hs의 가중합을 구하고, 그 결과를 맥락 벡터 c로 출력한다. 

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

이상이 어텐션 기술의 핵심이다. 

Encoderer가 건네주는 정보 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)
        dhs1, dh = self.attention_weight_layer.backward(da)
        dhs = dhs0 + dhs1
        return dhs, dh

 

위 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 계층을 필요한 수만큼 만들고(T개), 각 Attention계층의 각 단어의 가중치를 attention_weight 리스트에 보관한다.

 

 


어텐션을 갖춘 seq2seq 구현

어텐션을 갖춘 seq2seq을 구현해보자. 

 

Encoder 구현 

앞 챕터에서 구현한 Encoder와 거의 같고, forward메서드에서 LSTM계층에서 모든 은닉 계층을 반환하는 점이 다르다.

따라서 앞장의 Encoder를 상속하여 구현해보자.

# -*- coding: utf-8 -*-
"""
You can do it
"""

import sys
sys.path.append('..')
from common.time_layers import *
from ch7_sentence_make_with_RNN.seq2seq import Encoder, Seq2seq
from ch8_Attention.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 구현

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

이어서 마지막으로 AttentionEncoder와 AttentionDecoder 클래스를 결합하여 AttentionSeq2seq클래스를 완성해보자.

 

 

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을 갖춘 seq2seq 구현을 모두 마친다.

 

 

 

 


어텐션 평가

이 책에서는 날짜 형식을 변경하는 문제(데이터 크기가 작고, 어느 쪽인가를 맞추는 인위적인 문제)로 어텐션을 갖춘 seq2seq의 효과를 확인해보려 한다.

 

번역용 데이터셋 중에는 WMT가 유명하다. 이 데이터셋에는 영어와 프랑스어(또는 영어와 독일어) 학습 데이터가 쌍으로 준비되어 있다. WMT 데이터셋은 seq2seq 성능을 평가하는 데도 자주 이용되지만,20GB 이상 차지해 부담이 된다.

 

날짜 형식 변환 문제

영어권에서 사용되는 다양한 날짜 형식을 표준 형식으로 변환하는 것이 목표이다. 다음 그림을 참고하자.

이 문제를 채용한 데에는 두가지 이유가 있다고 한다.

  1. 문제가 겉보기만큼 간단하지 않다. 다양한 변형이 존재하여 변환 규칙이 복잡해진다.
  2. 문제의 입력과 출력 사이에 알기 쉬운 대응 관계가 있다.(년,월,일)

dataset/date.txt를 참고하자. 날짜 변환 학습 데이터를 50000개 담고 있다.

데이터셋을 보면 입력 문장의 길이를 통일하기 위해 공백 문자로 패딩해뒀고, 입출력 구분 문자로 '_'를 사용했다. 

그리고 이 문제에서는 출력의 문자 수는 일정하기 때문에 출력의 끝을 알리는 구분 문자는 사용하지 않았다.

 

어텐션을 갖춘 seq2seq의 학습

학습 코드를 보자.

# -*- coding: utf-8 -*-
"""
You can do it
"""
# coding: utf-8
import sys
import numpy as np
sys.path.append('..')
sys.path.append('../ch7_sentence_make_with_RNN')
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 ch7_sentence_make_with_RNN.seq2seq import Seq2seq
from ch7_sentence_make_with_RNN.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()

 

 

에폭을 거듭할때마다 급격히 정답률을 높여, 2에폭부터는 모든 문제를 풀어낸다. 

 

앞 모델들과 결과를 비교하면 다음과 같다.

 

 

어텐션 시각화

어텐션이 시계열 변환을 수행할 때, 어느 원소에 주의를 기울이는지 눈으로 살펴보려는 시도이다.

seq2seq가 최초의 "1"을 출력할 때는 입력 문장의 "1"위치에 표시가 된다. 

주목할 점은 년,월,일의 대응 관계이다.

결과 그래프를 자세히 보면, 세로축(출력)의 "1983"과 "26"이 가로축(입력)에 훌륭하게 대응하고 있다.

seq2seq는 "august"가 "8월"에 대응한다는 사실을 데이터만 가지고 학습해낸 것이다.

다른 예도 몇가지 살펴보자. 여기서도 년,월,일 대응관계를 명학히 간파할 수 있다.

이처럼 어텐션을 이용하면, seq2seq는 마치 사람이 그러하듯 필요한 정보에 주의를 더 기울일 수 있다. 

 

 


어텐션에 관한 남은 이야기

지금까지 다루지 못한 주제 몇가지를 소개한다.

 

양방향 RNN 

이번 절은 seq2seq의 Encoder에 초점을 맞춘다. 

앞절의 Encoder는 다음 그림처럼 그릴 수 있다.

그림에서 보듯, LSTM의 각 시각의 은닉 상태 벡터는 hs로 모아진다. 

Encoder가 출력하는 각 hs의 각 행에는 그 행에 대응하는 단어의 성분이 많이 포함되어 있다. 

 

여기서 주목할 점은 우리는 글을 왼쪽에서 오른쪽으로 읽는다는 점이다. 

따라서 위의 그림에서는 "고양이"에 대응하는 벡터에 "나", "는", "고양이"까지 총 세단어의 정보가 인코딩되어 들어간다. 

여기에서 전체적인 균형을 생각하면, "고양이"단어의 주변 정보를 균형있게 담고 싶을 것이다.

 

그래서 LSTM을 양방향으로 처리하는 방법을 생각해볼 수 있다.

이것이 양방향 LSTM(양방향 RNN) 기술이며, 그림으로는 다음과 같다.

그림에서 보듯, 역방향으로 처리하는 LSTM계층을 추가한다. 

그리고 각 시각에서는 두 LSTM계층의 은닉 상태를 연결시킨 벡터를 최종 은닉 상태로 처리한다.(연결 외에도 합하거나 평균내는 방법 등도 생각해볼 수 있다.)

 

이처럼 양방향으로 처리함으로써, 각 단어에 대응하는 은닉 상태 벡터에는 좌,우 양쪽 방향으로부터의 정보를 집약할 수 있다. 이렇게 해서 균형 잡힌 정보가 인코딩될 수 있는 것이다.

 

 

Attention 계층 사용 방법

Attention계층 사용법을 알아보기 전에 복습부터 해보자. Attenion계층을 지금까지 우리는 다음 그림과 같이 이용하였다.

Attention계층을 LSTM과 Affine계층 사이에 삽입하였다.

하지만 Attention계층을 이용하는 장소가 반드시 위 그림과 같을 필요는 없다.

위 그림에서는 Attention계층의 출력(맥락 벡터)가 다음 시각의 LSTM계층에 입력되도록 연결하였다.

이렇게 구성하면 LSTM계층이 맥락 벡터의 정보를 이용할 수 있다. 

한편, 우리가 구현한 모델은 Affine계층이 맥락 벡터를 이용했다. 

 

그렇다면 Attention계층의 위치를 달리하는 게 최종 정확도에는 어떤 영향을 줄까? 답은 해보지 않으면 모른다.

다만, 앞의 두 모델은 모두 맥락 벡터를 잘 활용하는 구성이라 큰 차이가 없을지도 모른다.

 

또한, 구현 관점에서는 전자의 구성이 구현하기 더 쉽다. 

Decoder의 데이터 흐름이 아래에서 위로 가는 한 방향이기 때문에 Attention계층을 쉽게 모듈화할 수 있다. 

 

 

seq2seq 심층화와 skip 연결

번역 등 현실에서 애플리케이션은 풀어야 할 문제가 훨씬 복잡해진다.

그러면 어텐션을 갖춘 seq2seq에도 더 높은 표현력이 요구될 것이다. 

RNN계층(LSTM)을 깊게 쌓으면 표현력 높은 모델을 만들 수 있고, 어텐션을 갖춘 seq2seq도 다르지 않다.

Encoder와 Decoder에 3층 LSTM계층을 사용하는 모습이다.

위 예처럼 같은 층수의 LSTM계층을 사용하는 것이 일반적이다. 

한편, Attention계층의 사용법은 여러 변형이 있을 수 있다. 

 

층을 깊게 할 때 사용되는 중요한 기법 중 skip 연결이 있다.(잔차 연결 혹은 숏컷이라고도 한다.)

skip연결은 다음 그림과 같이 계층을 넘어 선을 연결하는 기법이다.

이때 skip연결의 접속부에서는 2개의 출력이 더해진다. 

이 덧셈(원소별 덧셈)이 핵심이다. 

덧셈은 역전파시 기울기를 그대로 흘려보내므로, skip연결의 기울기가 아무런 영향을 받지 않고 모든 계층으로 흐르기 때문이다. 

 

 


어텐션 응용

어텐션은 범용적이고 더 많은 가능성을 지니고 있다. 

최근 딥러닝 연구에서는 어텐션이 중요한 기술로써 다양한 장면에서 등장한다. 

 

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

기계번역의 역사를 살펴보면, 주요한 기법이 시대의 흐름과 함께 변해왔음을 알수 있다. 

규칙 기반 번역에서 용례 기반 번역으로, 다시 통계 기반 번역으로 옮겨왔다.

현재는 기존 기술들을 대신해 신경망 기계 번역(Neural Machine Translation, NMT)이 주목받고 있다. 

 

구글 번역(Google Translate)은 실제로 2016년부터 신경망 번역을 사용하고 있다. 

이 기계 번역 시스템은 구글 신경망 기계 번역(Google Neural Machine Translation, GNMT)이라 불린다. 

계층 구성을 보자.

어텐션을 갖춘 seq2seq과 마찬가지로 Encoder, Decoder, Attention으로 구성되어 있다. 

GMNT는 번역 정확도를 높이기 위해 LSTM계층의 다층화, 양방향 LSTM(Encoder의 첫번째 계층만), skip연결 등을 볼 수 있다. 

또한, 학습시간을 단축시키기 위해 다수의 GPU로 분산 학습을 수행하고 있다. 

 

GNMT에서는 아키텍처적인 연구 외에도, 낮은 빈도의 단어 처리나 추론 고속화를 위한 양자화 등 다양한 연구가 이뤄지고 있다. 

그림에서 보듯, GNMT는 기존 기법과 비교해 번역 품질을 크게 끌어올리는데 성공했다.

 

 

트랜스포머

지금까지 RNN(LSTM)을 다양한 곳에 사용해왔다. 

언어 모델에서 시작하여 문장 생성, seq2seq, 어텐션을 갖춘 seq2seq 구성에는 반드시 RNN이 등장했다. 

RNN을 통해 가변 길이 시계열 데이터를 잘 처리하여 좋은 결과를 얻었다.

하지만 RNN에도 단점은 있고, 그중 하나로 병렬 처리를 들 수 있다.

 

RNN 은 이전 시각에 계산한 결과를 이용하여 순서대로 계산한다. 

따라서 RNN의 계산을 시간 방향으로 병렬 계산하는것은 기본적으로 불가능하다. 

 

이러한 배경에서, RNN을 없애는 연구(혹은 병렬계산)가 활발히 이뤄지고 있다. 

 

트랜스포머 외에도 RNN을 제거하는 연구는 다양하게 시도되고 있다. 

 

트랜스포머는 어텐션으로 구성되는데, 그중 셀프어텐선(Self-Attention)이라는 기술을 이용하는 게 핵심이다. 

하나의 시계열 데이터를 대상으로 한 어텐션으로, 하나의 시계열 데이터 내에서 각 원소가 다른 원소들과 어떻게 관련되는지 살펴보자는 취지이다. 

Time Attention계층을 예로 설명하면, 셀프어텐션은 다음 그림과 같이 그릴 수 있다.

지금까지 우리는 어텐션에서 번역과 같이 2개의 시계열 데이터 사이의 대응 관계를 구해왔다. 

Time Attention계층에서는 서로 다른 두 시계열 데이터가 입력된다.

반면, Self-Attention은 두 입력선이 모두 하나의 시계열 데이터로부터 나온다.

이렇게 하면 하나의 시계열 데이터 내에서의 원소간 대응 관계가 구해진다. 

 

트랜스포머에서는 RNN대신 어텐션을 사용한다. 

실제로 위 그림을 보면 Encoder와 Decoder모두에서 셀프어텐션을 사용함을 알 수 있다.

Feed Forward계층은 피드포워드 신경망(시간 방향으로 독립적으로 처리하는 신경망)을 나타낸다.

정확하게는 은닉층이 1개이고 활성화 함수로 ReLU를 이용한 완전연결계층 신경망을 이용한다.

그림에서 Nx는 회색 배경으로 둘러싸인 계층을 N겹 쌓았다는 의미이다. 

 

위 그림은 트랜스포머를 단순화해 보여준다. 실제로는 이 그림의 아키텍처에 더해 skip 결과 계층 정규화 등도 이용한다. 

다수의 어텐션을 병렬로 이용하거나 시계열 데이터의 위치 정보를 인코딩하는 위치 인코딩같은 기법도 볼 수 있다. 

 

트랜스포머를 이용하면 계산량을 줄이고 GPU를 이용한 병렬 계산의 혜택도 더 많이 누릴 수 있다. 

그 결과 트랜스포머는 GNMT보다  학습 시간을 큰 폭으로 줄이는데 성공했다.

번역 품질도 상당 폭 끌어올릴 수 있었다.

 

 

 

뉴럴 튜링 머신(NTM)

인간은 복잡한 문제를 풀때 종이와 펜이라는 외부의 저장 장치 덕분에 우리의 능력이 확장되었다고 해석할 수 있다.

신경망에도 이와같이 외부 메모리를 이용하여 새로운 힘을 부여할 수 있다. 

 

RNN이나 LSTM은 내부 상태를 활용하여 시계열 데이터를 기억할 수 있었다.

그러나 이 내부 상태는 길이가 고정이라 채워 넣을 수 있는 정보량이 제한적이다.

그래서 RNN 외부에 기억 장치(메모리)를 두고 필요한 정보를 거기에 적절하게 기록하는 방안을 착안해낸 것이다.

 

어텐션을 갖춘 seq2seq에서는 Encoder가 입력 문장을 인코딩한다.

그리고 인코딩된 정보를 어텐션을 통해 Decoder가 이용했다. 

여기서 주목할 점은 어텐션의 존재이다.

이 어텐션을 통해 Encoder와 Decoder는 메모리 조작같은 작업을 수행한다.

즉, Encoder가 필요한 정보를 메모리에 쓰고, Decoder는 그 메모리로부터 필요한 정보를 읽는다고 해석할 수 있다.

 

이렇게 생각하면 컴퓨터의 메모리 조작을 신경망에서도 재현할 수 있을것 같다.

RNN의 외부에 정보 저장용 메모리 기능을 배치하고, 어텐션을 이용하여 그 메모리부터 필요한 정보를 읽고 쓰는 방법 말이다.

실제로 이러한 연구가 몇가지 이뤄지고 있고, 그중 유명한 연구가 뉴럴 튜링 머신(Neural Turing Machine,NTM)이다.

 

NTM은 딥마인드 팀에서 진행한 연구이며, 후에 DNC(Differentiable Neural Computers)라는 기법으로 개선한 논문이 과학 잡지 Nature에 실렸다. DNC는 NTM의 메모리 조작을 더욱 강화한 버전이나 본질적인 기술은 같다. 

 

NTM의 큰 틀을 살펴보자. NTM 처리의 핵심을 시각화 하면 다음과 같다.

한가운데 있는 컨트롤러 모듈이 정보를 처리하는 모듈로, 신경망(혹은 RNN)을 이용하는 것으로 예상된다. 

그림을 보면, 이 컨트롤러는 차례로 흘러 들어오는 0,1데이터를 처리하여 새로운 데이터를 출력하고 있다. 

중요한 것은 컨트롤러의 바깥에 있는 큰 종이의 존재이다.

이 큰 종이(메모리)덕분에 컨트롤러는 컴퓨터(혹은 튜링 머신)와 같은 능력을 얻는다. 

이 능력은 큰 종이에 필요한 정보를 쓰거나 불필요한 정보를 지우는, 그리고 필요한 정보를 다시 읽는 능력이다. 

큰 종이는 두루마리 형태이므로, 각 노드가 필요한 위치의 데이터를 읽고 쓸 수 있다. 원하는 목적지로 이동할 수 있다는 것이다.

 

이처럼 NTM은 외부 메모리를 읽고 쓰면서 시계열 데이터를 처리한다. 

이 메모리 조작을 미분 가능한 개선으로 구축했다. 따라서 메모리 조작 순서도 데이터로부터 학습할 수 있다. 

 

일반적인 컴퓨터는 사람이  작성한 프로그램에 따라 동작하는데 반해, NTM은 데이터로부터 프로그램을 학습한다.

즉, 알고리즘의 입력과 출력으로부터 알고리즘 자체(로직)을 학습할수 있다는 뜻이다.

 

정리하자면 NTM은 외부 메모리에 컴퓨터처럼 읽고 쓰기를 수행한다.

NTM의 계층 구성을 보면 다음과 같다.

LSTM계층이 컨트롤러가 되어 NTM의 주된 처리를 수행한다.

각 시각에서 LSTM계층의 은닉 상태를 Write Head계층이 받아 필요한 정보를 메모리에 쓴다.

그 다음 Read Head계층이 메모리로부터 중요한 정보를 읽어 다음 시각의 LSTM계층에 전달한다. 

 

그럼 Write,Read Head는 어떻게 메모리를 조작할까?

여기서도 어텐션을 사용한다. 

 

NTM은 컴퓨터의 메모리 조작을 모방하기 위해 2개의 어텐션을 이용한다.

콘텐츠 기반 어텐션과 위치 기반 어텐션이다. 

콘텐츠 기반 어텐션은 지금까지 본 어텐션과 같고, 입력으로 주어진 어느 벡터(질의 벡터,query vector)와 비슷한 벡터를 메모리부터 찾아내는 용도로 이용된다.

위치 기반 어텐션은 이전 시각에서 주문한 메모리의 위치(=메모리의 각 위치에 대한 가중치)를 기준으로 그 전후로 이동(시프트)하는 용도로 사용된다. 

자세한 설명은 생략하나, 1차원 합성곱 연산으로 구현된다고 한다. 

 

NTM의 메모리 조작은 다소 복잡하다. 앞에서 설명한 조작 외에도 어텐션의 가중치를 날카롭게 다듬는 처리, 이전 시각 어텐션의 가중치를 더해주는 처리 등도 이뤄진다. 

 

seq2seq로는 풀리지 않던 복잡한 문제도 NTM은 놀라운 성과를 보인다. 

긴 시계열을 기억하는 문제와 정렬 등의 문제를 NTM은 해결하고 있다. 

또한, NTM은 외부 메모리를 사용함으로써 알고리즘을 학습하는 능력을 얻는다. 그리고 이때 어텐션이 중요한 기술로써 이용된다. 

외부 메모리에 의한 확장, 그리고 어텐션은 앞으로 더욱 중요한 기법으로써 다양한 장소에서 이용될 것이다.

 

 


정리

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

 

 

 

 


※dataset 및 코드 제공 - 밑바닥부터 시작하는 딥러닝 GitHub: https://github.com/WegraLee/deep-learning-from-scratch-2

 

GitHub - WegraLee/deep-learning-from-scratch-2: 『밑바닥부터 시작하는 딥러닝 ❷』(한빛미디어, 2019)

『밑바닥부터 시작하는 딥러닝 ❷』(한빛미디어, 2019). Contribute to WegraLee/deep-learning-from-scratch-2 development by creating an account on GitHub.

github.com

 

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

밑바닥부터 시작하는 딥러닝2 - Chap7. RNN을 사용한 문장 생성  (2) 2025.01.03
밑바닥부터 시작하는 딥러닝2 - Chap6. 게이트가 추가된 RNN  (2) 2025.01.01
밑바닥부터 시작하는 딥러닝2 - Chap5. 순환 신경망(RNN)  (2) 2024.12.27
밑바닥부터 시작하는 딥러닝2 - Chap4. word2vec 속도 개선  (1) 2024.12.26
밑바닥부터 시작하는 딥러닝2 - Chap3. word2vec  (3) 2024.12.23
'공부/밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글
  • 밑바닥부터 시작하는 딥러닝2 - Chap7. RNN을 사용한 문장 생성
  • 밑바닥부터 시작하는 딥러닝2 - Chap6. 게이트가 추가된 RNN
  • 밑바닥부터 시작하는 딥러닝2 - Chap5. 순환 신경망(RNN)
  • 밑바닥부터 시작하는 딥러닝2 - Chap4. word2vec 속도 개선
seungdeng
seungdeng
기록을 해보자
  • seungdeng
    일기장
    seungdeng
  • 전체
    오늘
    어제
    • 홈 (26)
      • 프로젝트 (1)
        • AI기반 구인구직 웹사이트 (1)
      • 일상 (0)
        • 주절주절 (0)
        • 여행 (0)
        • 맛집 (0)
      • 공부 (25)
        • 인공지능 (1)
        • 밑바닥부터 시작하는 딥러닝 (16)
        • 논문 (8)
        • LLM의 시대: 언어모델의 혁신과 변화 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    Bert
    논문리뷰
    few shot
    qkv
    fine tuning
    self attention
    GPT
    RNN
    Attention
    Inductive Bias
    NLP
    NSP
    vision
    MLM
    Zero shot
    MLP
    transformer
    positional embedding
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
seungdeng
밑바닥부터 시작하는 딥러닝2 - Chap8. 어텐션(Attention)
상단으로

티스토리툴바