본문 바로가기

딥러닝/밑바닥부터 시작하는 딥러닝 2

8장 : 어텐션

8.1 어텐션의 구조

8.1.1 seq2seq의 문제점

  • seq2seq에서 Encoder의 출력은 고정 길이 벡터 
  • 고정 길이 벡터 - 입력 문장의 길이에 관계없이 항상 같은 길이의 벡터로 변환
    ->  필요한 정보가 벡터에 다 담기지 못하게 됨

 

8.1.2 Encoder 개선

Encoder출력의 길이를 입력 문장의 길이에 따라 바꿔줌
-> 시각별 LSTM계층의 은닉 상태 벡터를 모두 이용

-> 입력된 단어와 같은 수의 벡터

 

  • 각 시각의 은닉 상태에는 직전에 입력된 단어에 대한 정보가 많이 포함되어 있음
    -> hs : 각 단어에 해당하는 벡터들의 집합

 

8.1.3 Decoder 개선 ①

기존 Decoder - Encoder의 LSTM 계층의 마지막 은닉 상태만을 이용

-> hs 전부를 활용할 수 있도록 Decoder 개선

=> 입력과 출력의 여러 단어 중 어떤 단어끼리 서로 관련되어 있는지 대응 관계를 학습시킴

=> 필요한 정보에만 주목하여 그 정보로부터 시계열 변환을 수행하는 것  - 어텐션 구조

  • 어떤 계산을 수행하는 계층 추가
    • 입력 두가지 : hs, h (시각별 LSTM계층의 은닉 상태)
    • 입력에서 필요한 정보만 골라 위쪽의 Affine계층으로 출력 
    • Encoder의 마지막 은닉 상태 벡터는 Decoder의 첫 번째 LSTM계층에 전달
  • 각 시각에서 Decoder에 입력된 단어와 대응 관계인 단어의 벡터를 hs에서 골라내는 것
    • Decoder에서 'I' -> hs에서 '나'에 해당하는 벡터 선택
  • 선택 작업 -> 미분할 수 없음 => 모든 것을 선택 후 가중치를 별도로 계산

  • a : 각 단어의 중요도를 나타내는 가중치 
  • hs : 각 단어의 벡터 
  • a와 hs 가중합 -> c (맥락 벡터) 
  • '나'에 대응하는 가중치 0.8 -> c에 '나' 벡터 성분 많이 포함 : '나' 벡터를 선택하는 작업 

  • a(5,)를 ar(5,4)로 변환시킨 후 hs와 원소별 곱 -> 행을 기준으로 sum 
  • (5,4)*(5,4) -> (4,)

-> 미니배치  n = 10

  • (10, 5, 4) *(10,5,4) -> (10,4)

+) 가중합 계산은 행렬 곱(matmul)을 사용하는 편이 간단하고 효율적이나, 미니배치 처리로 확장하기 쉽지 않음
-> repeat +sum

+) axis 사용법 : x형상이 (X,Y,Z)일 때 np.sum(x, axis=1) 실행 -> 형상 (X,Z) : 1번째 축 사라짐 

 

##weight sum 계층 구현
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)  #(N, T, H)
        t =hs*ar  #(N, T, H)*(N, T, H)
        c = np.sum(t, axis=1)  #(N, H)

        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

 

8.1.4 Decoder 개선 ②

  • 가중치 a 구하기
    • a : h가 hs의 각 단어 벡터와 얼마나 비슷한지 수치로 나타내는 가중치

-> h와 hs 내적

  • 내적 : 두 벡터가 얼마나 같은 방향을 향하고 있는가 -> 두 벡터의 유사도를 표현 

a : hs와 h 내적(원소별 곱 +sum) -> s(유사도 점수) -> softmax함수 (s 정규화)

##AttentionWeight 계층 구현 
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)  #(N, T, H)
        t = hs * hr  #(N, T, H)*(N, T, H)
        s = np.sum(t, axis=2)  #(N, T)
        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

 

8.1.5 Decoder 개선

  • Attention Weight 계층과 Weight Sum 계층 하나로 결합

  • Attention 계층 
    • Attention Weight 계층 : Encoder가 출력하는 hs에 주목하여 해당 단어의 가중치 a 구함
    • Weight Sum 계층 : a와 hs의 가중합을 구하고, 그 결과를 맥락 벡터 c로 출력 
##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

-> LSTM계층과 Affine 계층 사이에 삽임

  • Affine 계층 : 기존의 LSTM 계층의 은닉 상태 벡터Attention계층의 맥락 벡터의 연결(concat)을 입력 받음

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

8.2 어텐션을 갖춘 seq2seq 구현 

8.2.1 Encoder 구현

  • 앞 장의 Encoder에서 마지막 은닉 상태 벡터가 아닌 모든 은닉 상태를 반환하는 것만 다름
  • 앞 자의 Encoder 상속하여 구현
##Encoder 구현
from common.time_layers import *
from seq2seq import Encoder, Seq2seq

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

 

8.2.2 Decoder 구현

  • Softmax계층 앞까지 Decoder로 구현
##Decoder 구현
class AttentionDecoder:
    def __init__(self, vocab_size, word2vec_size, hidden_size):
        #vocab_size : 어휘 수(문자의 종류)
        V, D, H = vocab_size, word2vec_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 (self.embed, self.lstm, self.affine):
            self.params += self.params
            self.grads += self.grads

    def forward(self, xs, enc_hs):  
        h = enc_hs[:, -1] 
        self.lstm.set_state(h)  #마지막 은닉 상태만 lstm에 입력

        out = self.embed.forward(xs)
        dec_hs = self.lstm.forward(out)
        c = self.attention.forward(enc_hs, dec_hs)  #lstm의 출력 h와 인코더의 hs 어텐션 계층에 입력
        out = np.concatenate((c, dec_hs), axis=2)  #원래 affine에 입력되는 h에 맥락벡터c가 연결됨
        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

 

8.2.3 seq2seq 구현

  • Encoder 대신 AttentionEncoder 사용
  • Decoder 대신 AttentionDecoder 사용
  • 앞 장의 Seq2seq 클래스 상속하고 초기화 메서드를 수정
##seq2seq구현
from seq2seq import Encoder, 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

8.3 어텐션 평가

8.3.1 날짜 형식 변환 문제

  • 영어권에서 사용되는 다양한 날짜 형식을 표준 형식으로 변환하는 것이 목표

  • 입력되는 날짜 데이터에는 다양한 변형이 존재하여 변환 규칙이 나름 복잡함
  • 문제의 입력(질문)과 출력(답변) 사이에 알기 쉬운 대응 관계가 있음 (년, 월, 일)
  • data.txt
    • 입력 문장의 길이를 통일하기 위해 공백 문자로 패딩
    • 입력과 출력의 구분 문자로 '_'(밑줄) 사용

 

8.3.2 어텐션을 갖춘 seq2seq의 학습

  • 모델로 Attentionseq2seq 사용
  • Reverse 기법 사용
from dataset import sequence
import matplotlib.pyplot as plt
from common.optimizer import Adam
from common.trainer import Trainer
from common.util import eval_seq2seq
from attention_seq2seq import AttentionSeq2seq
from seq2seq import Seq2seq
from 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  #테스트데이터 최초 10개 표시
        correct_num += eval_seq2seq(model, question, correct, id_to_char, verbose, is_reverse=True)
        #eval_seq2seq함수의 인수 1)moder, 2)question(문제문장) 3)correct_id(정답) 4)id_to_char(문자ID와 문자의 변환 수행)
        #5)verbose(True로 설정하면 결과를 터미널로 출력) 6)is_reverse(입력 데이터 반전)

    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('epochs')
plt.ylabel('accuracy')
plt.ylim(-0.05, 1.05)
plt.show()

-> 에폭 두번만에 거의 모든 문제를 풀어냄

단순한 seq2seq / peekyseq2seq

-> 단순한 seq2seq는 거의 문제를 풀지 못함

-> peeky 모델은 4에폭부터 100%에 도달 (어텐션이 학습 속도 더 빠름)

 

8.3.3 어텐션 시각화

  • 어느 원소에 주의를 기울이는지 시각화
  • attention_weight에 저장된 각 시각의 어텐션 가중치를 사용하여 입력 문장과 출력 문장의 단어 대응 관계를 2차원 맵으로 그림
import matplotlib.pyplot as plt
from dataset import sequence

(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

model = AttentionSeq2seq(vocab_size, wordvec_size, hidden_size)
model.load_params()

_idx = 0
def visualize(attention_map, row_labels, column_labels):
    fig, ax = plt.subplots()
    ax.pcolor(attention_map, cmap=plt.cm.Greys_r, vmin=0.0, vmax=1.0)

    ax.patch.set_facecolor('black')
    ax.set_yticks(np.arange(attention_map.shape[0])+0.5, minor=False)
    ax.set_xticks(np.arange(attention_map.shape[1])+0.5, minor=False)
    ax.invert_yaxis()
    ax.set_xticklabels(row_labels, minor=False)
    ax.set_yticklabels(column_labels, minor=False)

    global _idx
    _idx += 1
    plt.show()


np.random.seed(1984)
for _ in range(5):
    idx = [np.random.randint(0, len(x_test))]
    x = x_test[idx]
    t = t_test[idx]

    model.forward(x, t)
    d = model.decoder.attention.attention_weights
    d = np.array(d)
    attention_map = d.reshape(d.shape[0], d.shape[2])

    # 출력하기 위해 반전
    attention_map = attention_map[:,::-1]
    x = x[:,::-1]

    row_labels = [id_to_char[i] for i in x[0]]
    column_labels = [id_to_char[i] for i in t[0]]
    column_labels = column_labels[1:]

    visualize(attention_map, row_labels, column_labels)

-> 1983과 26이 잘 대응하고 있음

-> 08에 August 대응 


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

8.4.1 양방향 RNN

  • seq2seq의 Encoder에 초점
  • LSTM을 양방향으로 처리 -> 양방향 LSTM

  • 각 시각에서는 이 두 LSTM계층의 은닉 상태를 연결시킨 벡터를 최종 은닉 상태로 처리
  • 하나의 LSTM계층에는 입력 문장의 단어들을 반대 순서로 나열 
  • 두 LSTM계층의 출력을 연결

 

8.4.2 Attention 계층 사용 방법

  • 지금까지는 Attention계층을 LSTM계층과 Affine계층 사이에 삽입함
  • 그러나 다르게 사용할수도 있음

  • Attention계층의 출력이 다음 시각의 LSTM계층에 입력됨
  • LSTM계층이 맥락 벡터의 정보를 이용

 

8.4.3 seq2seq 심층화와 skip 연결

  • 어텐션을 갖춘 seq2seq를 깊게 함
  • 이외에도 여러 개의 Attention계층을 사용하거나, Attention 출력을 다음 시각의 LSTM계층으로 입력하는 등 다양한 변형 가능
  • 층을 깊게 할 때 사용되는 중요한 기법 중 하나 skip 연결 (잔차연결, 숏컷)
    • 계층을 건너뛰는 연결
    • skip연결의 접속부에서 2개의 출력이 더해짐 -> 역전파 시 기울기가 아무런 영향을 받지 않고 모든 계층으로 흐름
    • 층이 깊어져도 기울기가 소실(혹은 폭발)되지 않고 전파되어 결과적으로 좋은 학습 기대
  • 시간 방향의 기울기 소실  -> 게이트가 달린 RNN, 폭발 -> 기울기 클리핑


8.5 어텐션 응용

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

  • LSTM계층의 다층화 
  • 양방향 LSTM(Encoder의 첫 번째 계층만)
  • skip 연결
  • 다수의 GPU로 분산 학습 수행

-> 정확도 사람 수준

 

8.5.2 트랜스포머

  • RNN은 이전 시각에 계산한 결과를 이용하여 순서대로 계산 
    -> 병렬 계산이 불가능
    => GPU 사용 x
  • RNN을 사용하지 않는 트랜스포머 등장
  • 어텐션으로만 구성
  • 셀프어텐션 : 하나의 시계열 데이터를 대상으로 한 어텐션

-> 입력이 모두 하나의 시계열 데이터로부터 나옴

=> 하나의 시계열 데이터 내에서의 원소 간 대응 관계가 구해짐

 

  • RNN대신 어텐션 사용
  • Encoder와 Decoder 모두에서 셀프어텐션 사용
  • Feed Forward 계층 : 은닉층이 1개이고 활성화 함수로 ReLU를 이용한 완전연결계층 신경망
  • Nx : 계층들을 N겹 쌓음

-> 계산량을 줄이고 병렬 계산의 혜택도 누릴 수 있음

-> GNMT보다 학습 시간 큰 폭으로 줄이고 번역 품질도 상당 폭 끌어올림

 

8.5.3 뉴럴 튜링 머신(NTM)

  • 외부 메모리를 통한 확장
  • RNN외부에 정보 저장용 메모리 기능을 배치하고 어텐션을 이용하여 그 메모리로부터 필요한 정보를 읽거나 쓰는 방법

  • 그림 한가운데 있는 컨트롤러 모듈
    • 정보를 처리하는 모듈
    • 신경망(혹은 RNN)을 이용
    • 차례차례 들어오는 0혹은 1 데이터를 처리하여 새로운 데이터를 출력
  • 컨트롤러의 바깥, 큰 종이 (=메모리)
    • 메모리 덕분에 컨트롤러는 컴퓨터(혹은 튜링 머신)와 같은 능력
    • 필요한 정보를 쓰거나 불필요한 정보를 지우는 능력, 필요한 정보를 다시 읽어 들이는 능력
    • 큰 종이 : 두루마리 형태 - 각 노드가 필요한 위치의 데이터를 읽고 쓸 수 있음
  • 외부 메모리를 읽고 쓰면서 시계열 데이터 처리
  • 미분 가능한 계산으로 구축 -> 메모리 조작 순서도 데이터로부터 학습 

  • LSTM계층이 컨트롤러
  • 각 시각에서 LSTM계층의 은닉 상태를 write head계층이 받아서 필요한 정보를 메모리에 씀
  • read head계층이 메모리로부터 중요한 정보를 읽어 들여 다음 시각의 LSTM계층으로 전달
  • write head, read head - 어텐션 사용 
  • 2개의 어텐션 이용
    • 콘텐츠 기반 어텐션 : 지금까지 본 어텐션과 동일, 입력으로 주어진 어느 벡터와 비슷한 벡터를 메모리로부터 찾아내는 용도
    • 위치 기반 어텐션 : 이전 시각에서 주목한 메모리의 위치를 기준으로 그 전후로 이동(시프트)하는 용도

-> 긴 시계열을 기억하는 문제와 정렬 등의 문제를 해결 

-> 외부 메모리를 사용함으로써 알고리즘을 학습하는 능력


8.6 정리 

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

책 참고 : 밑바닥부터 시작하는 딥러닝 (한빛미디어)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

7장 : RNN을 사용한 문장 생성  (2) 2023.09.27
6장 : 게이트가 추가된 RNN  (0) 2023.09.26
5장 : 순환 신경망(RNN)  (0) 2023.09.26
4장 : word2vec 속도 개선  (0) 2023.09.23
3장 : word2vec  (0) 2023.09.21