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

7장

by sxlvxrjxms2s2itsmes2s2 2023. 8. 21.

7.1 언어 모델을 사용한 문장 생성

7.2 seq2seq

7.3 seq2seq 구현

7.4 seq2seq 개선

7.5 seq2seq를 이용하는 애플리케이션

7.6 정리

 

이번 장에서는 언어 모델을 사용해 '문장 생성'을 수행한다.

구체적으로는 말뭉치를 사용해 학습한 언어 모델을 이용하여 새로운 문장을 만들어낸다. 

또한 seq2seq라는 새로운 구조의 신경망을 다룬다.

Seq2Seq는 인공 신경망을 사용하여 입력 시퀀스를 다른 형태의 출력 시퀀스로 변환하는 모델 구조를 말한다.

 

7.1 언어 모델을 사용한 문장 생성

 

1) RNN을 사용한 문장 생성의 순서

 

앞장에서 구현한 모델의 신경망 구성이다.

 

언어 모델에게 문장을 생성시키는 순서를 알아보자

"you say goodbye and I say hello"라는 말뭉치로 학습한 언어모델에 " I " 라는 단어를 입력으로 주면 어떻게 될까?

이 결과를 기초로 다음 단어를 새로 생성하려면 어떻게 해야할까

 

1. 확률이 가장 높은 단어를 선택한다. = 결정적인 방법

2. 확률적으로 선택한다.

: 후보 단어의 확률에 맞게 선택하는 것으로 이 방식에서는 선택되는 단어 (샘플링 단어)가 매번 다를 수 있다.

 

2번으로 단어를 선택해보자

확률분포로부터 샘플링을 수행한 결과로 "say"가 선택됐다.

확률분포에서는 "say"의 확률이 높기 때문에 "say"가 샘플링될 확률이 가장 높다

결정적이 아니고 확률적이라는 것에 주의하자

(다른 단어들도 해당 단어의 출현 확률에 따라 정해진 비율만큼 샘플링될 가능성이 있다는 뜻)

 

두 번째 단어를 샘플링해보자

방금 생성한 단어인 "say"를 언어 모델에 입력하여 다음 단어의 확률분포를 얻는다.

이 작업을 원하는 만큼 반복한다.

 

언어 모델에게 문장을 생성 시키는 순서
("you say goodbye and I say hello." 라는 말뭉치로 학습한 언어 모델)

1. 학습 된 언어 모델에 " I "라는 단어를 입력으로 준다.
2. 언어 모델은 [그림 7-2]와 같은 확률분포를 출력한다.
3. 다음 단어를 새로 생성한다.
    (1) [결정적 선택] 확률이 가장 높은 단어를 선택한다. 
    (2) [확률적 선택] 단어의 확률에 맞게 선택한다.
4. 확률 분포에 따라 "say"라는 단어가 선택 되었다.
5. 다음 단어를 샘플링한다.
6. 원하는 만큼 반복한다. (<eos> 종결 기호)

 

주목할 것은 이렇게 생성한 문장은 훈련 데이터에는 존재하지 않는, 즉 새로 생성된 문장이라는 것이다.

언어 모델은 훈련 데이터를 암기한 것이 아니라, 훈련 데이터에서 사용된 단어의 정렬 패턴을 학습한 것이기 때문이다.

언어 모델이 말뭉치로부터 단어의 출현 패턴을 올바르게 학습할 수 있다면 그 모델이 새로 생성하는 문장은 우리 인간에게도 자연스럽고 의미가 통하는 문장일 것으로 기대할 수 있다.

 

 

2) 문장 생성 구현

 

앞 장에서 구현한 Rnnlm 클래스를 상속해 RnnlmGen 클래스를 만들고, 이 클래스에 문장 생성 메서드를 추가한다.

class BetterRnnlmGen(BetterRnnlm):
    def generate(self, start_id, skip_ids=None, sample_size=100):
        word_ids = [start_id]

        x = start_id
        while len(word_ids) < sample_size:
            x = np.array(x).reshape(1, 1)
            score = self.predict(x).flatten()
            p = softmax(score).flatten()

            sampled = np.random.choice(len(p), size=1, p=p)
            if (skip_ids is None) or (sampled not in skip_ids):
                x = sampled
                word_ids.append(int(x))

        return word_ids

    def get_state(self):
        states = []
        for layer in self.lstm_layers:
            states.append((layer.h, layer.c))
        return states

    def set_state(self, states):
        for layer, state in zip(self.lstm_layers, states):
            layer.set_state(*state)

 

generate() : 문장 생성을 수행하는 메서드

 start_id : 최초로 주는 단어의 ID

sample_size : 샘플링하는 단어의 수

skip_ids : 단어 ID의 리스트, 이 리스트에 속하는 단어 ID는 샘플링되지 않도록 해준다.

 

1. generate() 메서드는 가장 먼저 model.predict(x)를 호출해 각 단어의 점수를 출력한다.

2. 그리고 p = softmax(score) 코드에서는 이 점수들을 소프트맥스 함수를 이용해 정규화한다.

3. 이것으로 확률분포 p를 얻을 수 있다.

4. 그런 다음 확률분포 p로부터 다음 단어를 샘플링한다.

 

그렇다면 RnnlmGen 클래스를 사용해 문장을 생성해보자.

from dataset import ptb

corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)
corpus_size = len(corpus)

model = RnnlmGen()
# model.load_parms('.../ch06.Rnnlm.pkl')

# 시작(start) 문자와 건너뜀(skip) 문자 설정
start_word = 'you'
start_id = word_to_id[start_word]
skip_words = ['N', '<unk>', '$']
skip_ids = [word_to_id[w] for w in skip_words]

# 문장 생성
word_ids = model.generate(start_id, skip_ids)
txt = ' '.join([id_to_word[i] for i in word_ids])
txt = txt.replace(' <eos> ', '.\n')
print(txt)

 

1. 여기에서는 첫 단어를 'you'로 하고, 그 단어 ID를 start_id로 설정한 다음 문장을 생성한다.

2. 샘플링하지 않을 단어로는 ['N', '<unk>', '$']를 지정했다.

3. 문장을 생성하는 generate() 메서드는 단어 ID들을 배열 형태로 반환한다.

4. 그래서 그 단어 ID 배열을 문장으로 변환해야 하는데, txt = ' '.join([id_to_word[i] for i in word_ids]) 코드가 그 일을 담당한다.

 

join() 메서드는 [구분자].join 형태로 작성하여, 리스트의 단어들 사이에 구분자를 삽입해 모두를 연결한다.

 

코드를 수행하면 엉터리로 나열된 글이 나온다.

모델의 가중치 초깃값으로 무작위한 값을 사용했기 때문에 이러한 출력 결과를 보인다.

 

주석 처리해둔 줄의 주석을 해제하면 앞 장에서 학습한 가중치 매개변수를 읽어들인다.

이 경우 어느정도 올바른 문장을 출력한다.

그러나 더 자연스러운 문장이 필요해보인다.

 

 

3) 더 좋은 문장으로

 

RNNLM을 개선해 더 좋은 언어 모델을 BetterRnnlm이라는 클래스로 구현했다.

 

 

7.2 seq2seq

2개의 RNN을 이용하는 seq2seq를 살펴보자

 

1) seq2seq의 원리

 

2개의 모듈, Encoder, Decoder가 등장한다.

Encoder는 입력 데이터를 인코딩(부호화)하고, Decoder는 인코딩된 데이터를 디코딩(복호화)한다.

 

Encoder가 인코딩한 정보에는 번역에 필요한 정보가 조밀하게 응축되어 있다.

Decoder는 조밀하게 응축된 이 정보를 바탕으로 도착어 문장을 생성하는 것이다.

 

seq2seq는 Encoder와 Decoder가 협력하여 시계열 데이터를 다른 시계열 데이터로 변환하는 것이다. (Encoder와 Decoder로는 RNN을 사용할 수 있다.)

 

Encoder는 RNN을 이용해 시계열 데이터를 h라는 은닉 상태 벡터로 변환한다.

(RNN으로써 LSTM을 이용했지만, 단순한 RNN이나 GRU 등도 이용할 수 있음)

 

Encoder가 출력하는 벡터 h는 LSTM 계층의 마지막 은닉 상태이다. 이 마지막 은닉 상태 h에 입력 문장(출발어)을 번역하는 데 필요한 정보가 인코딩된다. 여기서 중요한 것은 LSTM의 은닉 상태 h는 고정 길이 벡터라는 사실이다.

 

그래서 인코딩한다 라는 말은 임의 길이의 문장을 고정 길이 벡터로 변환하는 작업이 된다.

위의 그림에서 보듯이 Encoder는 문장을 고정 길이 벡터로 변환한다.

 

Decoder는 이 인코딩된 벡터를 어떻게 요리하여 도착어 문장을 생성하는 걸까?

 Decoder는 앞에서 배운 신경망과 같은 구성이다. LSTM 계층이 벡터 h를 입력받는다는 점만 다르다.

사소한 차이가 평범한 언어 모델을 번역도 해낼 수 있는 Decoder로 바꾼다.

 

Decodet와 Encoder을 연결한 계층 구성을 보자

seq2seq은 LSTM 두 개로 구성된다.

 

LSTM 계층의 은닉 상태가 Encoder와 Decoder를 이어주는 '가교'가 된다. 순전파 때는 Encoder에서 인코딩하고 이 정보가 LSTM 계층의 은닉 상태를 통해 Decoder에 전해진다. 그리고 seq2seq의 역전파 때는 이 '가교'를 통해 기울기가 Decoder로부터 Encoder로 전해진다.

 

 

2) 시계열 데이터 변환용 장난감 문제

 

시계열 변환 문제의 예로 더하기를 제시한다.

이번 문제에서는 단어가 아닌 문자 단위로 분할하려고 한다.

57+5 가 입력 되면, ['5','7','+','5']라는 리스트로 처리하는걸 말한다.

 

 

3) 가변 길이 시계열 데이터

 

덧셈을 문자(숫자)의 리스트로써 다루기로 했다.

여기서 주의할 점은 덧셈 문장("57+5"나 "628+521" 등)이나 그 대답("62"나 "1149" 등)의 문자 수가 문제마다 다르다는 것이다.

 

덧셈 문제에서는 샘플마다 데이터의 시간 방향 크기가 다르다. 즉 "가변 길이 시계열 데이터"를 다룬다는 것이다.

따라서 신경망 학습 시 "미니배치 처리"를 하려면 어떠한 것을 추가시켜야 한다.

 

가변 길이 시계열 데이터를 미니배치로 학습하기 위한 가장 단순한 방법은 패딩을 사용하는 것이다.

패딩이란 원래의 데이터에 의미 없는 데이터를 채워 모든 데이터의 길이를 균일하게 맞추는 기법이다.

 

이처럼 패딩을 적용해 데이터 크기를 통일시키면 가변 길이 시계열 데이터도 처리할 수 있다.

그러나 원래는 존재하지 않던 패딩용 문자까지 seq2seq가 처리하게 된다.

 

=> 정확성을 위해서 seq2seq에 패딩 전용 처리를 추가한다.

      - Decoder에 입력된 데이터가 패딩이라면 손실의 결과에 반영하지 않도록 한다.

      - Encoder에 입력된 데이터가 패딩이라면 LSTM 계층이 이전 시각의 입력을 그대로 출력하게 한다. 즉, LSTM 계층은            마치 처음부터 패딩이 존재하지 않았던 것처럼 인코딩할 수 있다.

 

 

4) 덧셈 데이터셋

 

 

import sys
sys.path.append('..')
from dataset import sequence

(x_train, t_train), (x_test, t_test) = \
    sequence.load_data('addition.txt', seed=1984)
char_to_id, id_to_char = sequence.get_vocab()

print(x_train.shape, t_train.shape)
print(x_test.shape, t_test.shape)
# (45000, 7) (45000, 5)
# (5000, 7) (5000, 5)

print(x_train[0])
print(t_train[0])
# [ 3  0  2  0  0 11  5]
# [ 6  0 11  7  5]

print(''.join([id_to_char[c] for c in x_train[0]]))
print(''.join([id_to_char[c] for c in t_train[0]]))
# 71+118
# _189

 

7.3 seq2seq 구현

두 RNN을 클래스와 Encoder 클래스와 Decoder 클래스로 각각 구현. 그런 다음 두 클래스를 연결하는 seq2seq 클래스를 구현하는 흐름을 진행한다.

 

1) Encoder 클래스

Encoder 클래스는 문자열을 받아 벡h로 변환한다. 

 

Embedding 계층 : 문자 ID를 문자 벡터로 변환한다.

LSTM 계층 : (오른쪽:시간 방향) - 은닉 상태와 셀을 출력한다.
                     (위쪽) - 은닉 상태 출력

마지막 문자를 처리한 LSTM 계층의 은닉 상태 h는 Decoder로 전달된다.

 

Encoder 클래스 코드

class Encoder:
    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')
        
        self.embed = TimeEmbedding(embed_W)
        self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful = False)
        
        self.params = self.embed.params + self.lstm.params
        self.grads = self.embed.grads + self.lstm.grads
        slef.hs = None

초기화 메서드 : vocab_size, wordvec_size, hidden_size (가중치 매개변수를 초기화하고, 필요한 계층을 생성한다. / 가중치 매개변수와 기울기를 인스턴스 변수 params와 grads 리스트에 각각 보관한다.)

 

forward()와 backward() 메서드를 살펴보자.

def forward(self, xs):
        xs = self.embed.forward(xs)
        hs = self.lstm.forward(xs)
        self.hs = hs
        return hs[:, -1, :]
    
    def backward(self, dh):
        dhs = np.zeros_like(self.hs)
        dhs[:, -1, :] = dh
        
        dout = self.lstm.backward(dhs)
        dout = self.embed.backward(dout)
        return dout

forward() : Time Embedding 계층과 Time LSTM 계층의 forward() 메서드를 호출. 그리고 Time LSTM 계층의 마지막 시각의 은닉 상태만을 추출해, 그 값을 Encoder의 forward() 메서드의 출력으로 반환한다.

 

backward() : LSTM 계층의 마지막 은닉 상태에 대한 기울기가 dh 인수로 전해진다. ( dh는 Decoder가 전해주는 기울기) 역전파 구현에서는 원소가 모두 0인 텐서 dhs를 생성하고 dhdhs의 해당 위치에 할당한다. 그다음은 Time LSTM 계층과 Time Embedding 계층의 backward() 메서드를 호출한다.

 

 

2) Decoder 클래스

 

Decoder 클래스는 Encoder 클래스가 출력한 h를 받아 다른 문자열을 출력한다.

 

 

Decoder의 학습 시 계층 구성이다. 여기에서 정답 데이터는 "_62"이다. 입력 데이터를 ['_', '6', '2', '']로 주고 이에 대응하는 출력은 ['6', '2', '', '']이 되도록 학습시킨다.

 

문장 생성시 확률적 샘플링을 선택했지만, 덧셈 문제해결에서는 결정적 샘플링을 사용한다.

 

 

 

 

Softmax 계층을 사용하지 않고,

Affine 계층이 출력하는 점수가 가장 큰 문자 ID를 선택한다.

= argmax (최댓값을 가진 원소 인덱스 선택)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Decoder 클래스는 TIme Embedding, Time LSTM, Time Affine의 3가지 계층으로 구성된다.

 

 

Decoder 클래스 코드

class Decoder:
    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(H, V) / np.sqrt(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.affine = TimeAffine(affine_W, affine_b)
        self.params, self.grads = [], []
        for layer in (self.embed, self.lstm, self.affine):
            self.params += layer.params
            self.grads += layer.grads
            
    def forward(self, xs, h):
        self.lstm.set_state(h)
        
        out = self.embed.forward(xs)
        out = self.lstm.forward(out)
        score = self.affine.forward(out)
        return score
    
    def backward(self, dscore):
        dout = self.affine.backward(dscore)
        dout = self.lstm.backward(dout)
        dout = self.embed.backward(dout)
        dh = self.lstm.dh
        return dh

backward() : 위쪽의 SoftmaxwithLoss 계층으로부터 기울기 dscore를 받아 Time Affine 계층, Time LSTM 계층, Time Embedding 계층 순서로 전파시킨다. 이때 Time LSTM 계층의 시간 방향으로의 기울기는 TimeLSTM 클래스의 인스턴스 변수 dh에 저장되어 있다. 그래서 이 시간 방향의 기울기 dh를 꺼내서 Decoder 클래스의 backward()의 출력으로 반환한다.

 

Decoder 클래스는 학습 시와 문장 생성 시의 동작이 다르다. 

Decoder 클래스에 문장 생성을 담당하는 generate() 메서드를 구현해보자.

def generate(self, h, start_id, sample_size):
        sampled = []
        sample_id = start_id
        self.lstm.set_state(h)
        
        for _ in range(sample_size):
            x = np.array(sample_id).reshape((1, 1))
            out = self.embed.forward(x)
            out = self.lstm.forward(out)
            score = self.affine.forward(out)
            sample_id = np.argmax(score.flatten())
            sampled.append(int(sample_id))
        return sampled

generate() : 인수를 3개 받는다. (Encoder로부터 받는 은닉 상태인 h, 최초로 주어지는 문자 ID인 start_id, 생성하는 문자 수인 sample_size)

여기에서는 문자를 1개씩 주고, Affine 계층이 출력하는 점수가 가장 큰 문자 ID를 선택하는 작업을 반복한다.

이상이 Decoder 클래스의 구현이다.

 

 

3) seq2seq 클래스

 

class Seq2seq(BaseModel):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        self.encoder = Encoder(V, D, H)
        self.decoder = Decoder(V, D, H)
        self.softmax = TimeSoftmaxWithLoss()

        self.params = self.encoder.params + self.decoder.params
        self.grads = self.encoder.grads + self.decoder.grads

    def forward(self, xs, ts):
        decoder_xs, decoder_ts = ts[:, :-1], ts[:, 1:]

        h = self.encoder.forward(xs)
        score = self.decoder.forward(decoder_xs, h)
        loss = self.softmax.forward(score, decoder_ts)
        return loss

    def backward(self, dout=1):
        dout = self.softmax.backward(dout)
        dh = self.decoder.backward(dout)
        dout = self.encoder.backward(dh)
        return dout

    def generate(self, xs, start_id, sample_size):
        h = self.encoder.forward(xs)
        sampled = self.decoder.generate(h, start_id, sample_size)
        return sampled

 

4) seq2seq 평가

 

seq2seq 의 학습은 기본적인 신경망의 학습과 같은 흐름으로 이뤄진다.

1. 학습 데이터에서 미니배치를 선택하고
2. 매니배치로부터 기울기를 선택하고
3. 기울기를 사용하여 매개변수를 갱신한다.

 

seq2seq의 학습 코드이다.

import sys
sys.path.append('..')
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 seq2seq import Seq2seq
from peeky_seq2seq import PeekySeq2seq


# 데이터셋 읽기
(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt')
char_to_id, id_to_char = sequence.get_vocab()

# 하이퍼파라미터 설정
vocab_size = len(char_to_id)
wordvec_size = 16
hideen_size = 128
batch_size = 128
max_epoch = 25
max_grad = 5.0

model = Seq2seq(vocab_size, wordvec_size, hideen_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)

    acc = float(correct_num) / len(x_test)
    acc_list.append(acc)
    print('검증 정확도 %.3f%%' % (acc * 100))

 

 

에폭을 거듭함에 따라 정답률이 착실하게 상승하는 것을 알 수 있다.

 

 

7.4 seq2seq 개선

 

1) 입력 데이터 반전

 

다음 그림과 같이 입력 데이터의 순서를 반전시킨다.

= 학습 진행이 빨라진다.

= 최종 정확도도 좋아진다.

 

 

입력데이터 "나는 고양이로소이다" => 출력데이터 "I am a cat"

'나' -> 'I'까지 너무 멀다

입력데이터를 반전시켜주면 

"이다, 로소, 고양이, 나는" => " I am a cat"

"나는"과 "I" 가 가까워져서 기울기가 더 잘 전달되고, 학습 효율이 좋다. 

 

 

 

 

 

2) 엿보기(Peeky)

 

seq2seq의 두 번째 개선은 엿보기(Peeky)이다. Encoder 동작은 입력 문장(문제 문장)을 고정 길이 벡터 h로 변환한다. 이때 h 안에는 Decoder에게 필요한 정보가 모두 담겨 있다. 그러나 현재의 seq2seq는 아래 그림과 같이 최초 시각의 LSTM 계층만이 벡터 h를 이용하고 있다.

중요한 정보인 h를 더 활용할 수는 없을까?

 

Encoder의 출력 h를 Decoder의 다른 계층에게도 전해주는 것이다. (concat 노드 활용)

위의 그림과 같이 모든 시각의 Affine 계층과 LSTM 계층에 Encoder의 출력 h를 전해준다. 

LSTM 계층과 Affine 계층에 입력되는 벡터가 2개씩이 되었다. 이는 실제로 두 벡터가 연결된 것을 의미한다. 

 

=> LSTM 계층의 가중치와 Affine 계층의 가중치 형상이 바뀐다.

-> Encoder가 인코딩한 벡터도 입력되므로, 가중치 매개변수 형상이 그만큼 커진다.

 

 Peeky Decoder 클래스의 구현

class PeekyDecoder:
    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(H + D, 4 * H) / np.sqrt(H + 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(H + H, V) / np.sqrt(H + 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.affine = TimeAffine(affine_W, affine_b)
        
        self.params, self.grads = [], []
        for layer in (self.embed, self.lstm, self.affine):
            self.params += layer.params
            self.grads += layer.grads
        self.cache = None
        
    def forward(self, xs, h):
        N, T = xs.shape
        N, H = h.shape
        
        self.lstm.set_state(h)
        
        out = self.embed.forward(xs)
        hs = np.repeat(h, T, axis = 0).reshape(N, T, H)
        out = np.concatenate((hs, out), axis = 2)
        
        out = self.lstm.forward(out)
        out = np.concatenate((hs, out), axis = 2)
        
        score = self.affine.forward(out)
        self.cache = H
        return score
    
    def backward(self, dscore):
        H = self.cache

        dout = self.affine.backward(dscore)
        dout, dhs0 = dout[:, :, H:], dout[:, :, :H]
        dout = self.lstm.backward(dout)
        dembed, dhs1 = dout[:, :, H:], dout[:, :, :H]
        self.embed.backward(dembed)

        dhs = dhs0 + dhs1
        dh = self.lstm.dh + np.sum(dhs, axis=1)
        return dh

    def generate(self, h, start_id, sample_size):
        sampled = []
        char_id = start_id
        self.lstm.set_state(h)

        H = h.shape[1]
        peeky_h = h.reshape(1, 1, H)
        for _ in range(sample_size):
            x = np.array([char_id]).reshape((1, 1))
            out = self.embed.forward(x)

            out = np.concatenate((peeky_h, out), axis=2)
            out = self.lstm.forward(out)
            out = np.concatenate((peeky_h, out), axis=2)
            score = self.affine.forward(out)

            char_id = np.argmax(score.flatten())
            sampled.append(char_id)

        return sampled

PeekyDecoder의 초기화는 앞에 구현했던 Decoder와 거의 같다. 다른 점은 LSTM 계층의 가중치와 Affine 계층의 가중치의 형상뿐이다. 이번 구현에서는 Encoder가 인코딩한 벡터도 입력되기 떄문에 가중치 매개변수의 형상이 그만큼 커진다.

 

forward : h를 np.repeat()로 시계열만큼 복제해 hs에 저장한다. 다음 np.concatenate()를 이용해 그 hs와 Embedding 계층의 출력을 연결하고, 이를 LSTM 계층에 입력한다.

 

PeekySeq2seq 클래스

class PeekySeq2seq(seq2seq):
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        self.encoder = Encoder(V, D, H)
        self.decoder = PeekyDecoder(V, D, H)
        self.softmax = TimeSoftmaxWithLoss()

        self.params = self.encoder.params + self.decoder.params
        self.grads = self.encoder.grads + self.decoder.grads

Decoder 계층에서 차이를 보인다.

 

결과는 이렇다.

 

7.5 seq2seq를 이용하는 애플리케이션

  • 기계 번역: 한 언어의 문장을 다른 언어의 문장으로 변환
  • 자동 요약: 긴 문장을 짧게 요약된 문장으로 변환
  • 질의응답: 질문을 응답으로 변환
  • 메일 자동 응답: 받은 메일의 문장을 답변 글로 변환

1. 챗봇

상대방 말 -> 자신의 말 변환하여 대화한다.

 

2. 알고리즘 학습

파이썬 코드 수행

 

3. 이미지 캡셔닝

이미지를 문장으로 변환한다.

Encoder가 CNN으로 바뀌었다. 

Decoder는 동일하다. 

 

CNN의 출력은 특징맵(3차원)이다. 

Decoder의 LSTM이 잘 처리할 수 있도록 1차원으로 평탄화(flatten)하여 Affine 계층으로 전달한다.

 

7.6 정리

  • RNN을 이용한 언어 모델은 새로운 문장을 생성할 수 있다.
  • 문장을 생성할 때는 하나의 단어(혹은 문자)를 주고 모델의 출력(확률분포)에서 샘플링하는 과정을 반복한다.
  • RNN을 2개 조합함으로써 시계열 데이터를 다른 시계열 데이터로 변환할 수 있다.
  • seq2seq는 Encoder가 출발어 입력문을 인코딩하고, 인코딩된 정보를 Decoder가 받아 디코딩하여 도착어 출력문을 얻는다.
  • 입력문을 반전시키는 기법, 또는 인코딩된 정보를 Decoder의 여러 계층에 전달하는 기법(Peeky)는 seq2seq의 정확도 향상에 효과적이다.
  • 기계 번역, 챗봇, 이미지 캡셔닝 등 seq2seq는 다양한 애플리케이션에 이용할 수 있다.

 

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

8장  (0) 2023.08.28
6장  (0) 2023.08.14
5장  (0) 2023.08.13
4장  (0) 2023.08.07
3장  (0) 2023.07.20