본문 바로가기
Deep Learning/NLP

[패캠] (Seq2Seq) seq2seq 모델

by 룰루셩 2022. 2. 6.
[패스트 캠퍼스] 김기현의 딥러닝을 활용한 자연어생성 올인원 패키지 Online.

Ch 04. Sequence-to-Sequence
      01. Machine Translation 소개
      02. Sequence to Sequence
      03. Encoder
      04. Decoder
      05. Generator

강의를 듣고 작성하였다.
본 게시물의 모든 출처는 [패스트 캠퍼스] 김기현의 딥러닝을 활용한 자연어생성 올인원 패키지 Online.에 있다.


(혹시 본 포스팅이 저작권 등의 문제가 있다면 알려주세요. 바로 내리도록 하겠습니다.
개인 공부 후 언제든지 다시 찾아볼 용도로 작성하고 있습니다.) 

+ 추가 
설명 중 참고한 자료: https://wikidocs.net/24996
코드 출처: 김기현님 github

 


 

Machine Translation - Seq2Seq 모델

Seq2Seq 모델이 도입되면서 기계번역 분야에서 발전 속도가 빨라졌다.

Discrete한 단어를 continuous한 값으로 변환하여 계산하기 때문에 generalization을 잘한다.

LSTM과 Attention을 적용하여 Sequence의 길이에 구애받지 않고 번역할 수 있게 되었다.

 


Sequence to Sequence

Encoder, Decoder, Generator가 있으면 Seq2Seq 모델이라고 한다.
Encoder: 문장을 하나의 벡터(Context vector)로 만들어주는 것
Decoder: Encoder로부터 Context vector를 받아서 문장을 생성 (각 시점의 RNN 셀에서 출력 벡터가 나온다)
Generator: Decoder의 hidden state를 받아 현재 time-step의 출력 token에 대한 확률 분포 반환 (출력 벡터를 소프트맥스 함수를 통해 출력 시퀀스의 각 단어별 확률값을 반환한다.)

출처: 패스트캠퍼스 김기현의 자연어생성 강의

<수식으로 seq2seq 모델이 학습하는 과정 (파라미터를 업데이트 하는 과정)을 살펴본다.>

 

먼저, 데이터셋이 다음과 같이 있다고 할 때,

 

$D={x^i, y^i}^N_{i=1}$                                                                → $x \sim P(X)$, $y \sim P(Y|x)$ 이렇게 샘플링 된 $x$와 $y$가 있다.

$x^i={x_1^i, ... , x_m^i}$ and $y^i={y_0^i, y_1^i, ... , y_n^i},$     → $x$는 m개의 단어로, $y$는 n+1개 단어로 이루어져 있다.

where $y_0=<BOS>$ and $y_n=<EOS>$.

 

우리 머리 속에 있는 번역 함수 또는, 어떤 문장을 생성하는 함수가 조건으로 주어졌을 때 그 확률분포함수를 근사하고자 하니까

likelihood를 maximize 하는 파라미터를 찾을 것이다.

 

$\hat{\theta}=\underset{\theta \in \Theta}{argmax} \sum\limits_{i=1}^N logP(y^i|x^i;\theta)$

    $=\underset{\theta \in \Theta}{argmax} \sum\limits_{i=1}^N \sum\limits_{j=1}^n logP(y_j^i|x^i, y_{<j}^i;\theta)$ → chain rule에 의해서

 

로스 관점에서 보면 minimize loss function

loss를 $\theta$로 미분한 것을 구해서 (gradient descent를 통해서) $\theta$를 업데이트 해준다. 

 

$\mathcal{L}(\theta)=-\sum\limits_{i=1}^N \sum\limits_{j=1}^n logP(y_j^i|x^i, y_{<j}^i;\theta)$

$\theta \leftarrow \theta - \alpha \nabla_{\theta}\mathcal{L}(\theta)$

 

 


Encoder

입력 문장의 모든 단어들을 순차적으로 입력받은 뒤에 마지막에 이 모든 단어의 정보들을 압축해서 하나의 벡터로 만든다. (Context Vector)
즉, Encoder는 입력 문장을 압축한 context vector를 decoder에게 넘겨준다.

입력 문장은 단어 토큰화를 통해서 단어 단위로 쪼개지고 단어 토큰 각각은 RNN 셀의 각 시점의 입력이 된다.
인코더 RNN 셀은 모든 단어를 입력받은 뒤에 인코더 RNN 셀의 마지막 시점의 hidden state를 디코더 RNN 셀로 넘겨준다.

인코더는 train/test시에 항상 문장 전체를 받는다. 인코더 자체만 놓고 보면 non-auto-regressive task라서 bi-directional RNN 사용 가능하다.

 

<수식>

데이터 셋은 위에서 정의한 것과 같고, $x^i$와 $y^i$의 사이즈는 다음과 같다. 

|$x^i$| = (bs, m, |Vs|)   Vs는 source(input)의 vocab

|$y^i$| = (bs, n, |Vt|)   Vt는 target의 vocab

 

encoder의 hidden state 구하기

$h^{enc}_t=RNN_{enc}(emb_{enc}(x_t), h^{enc}_{t-1}),$ where $h^{enc}_0.$ 

$h^{enc}_{1:m} = [h^{enc}_1;···;h^{enc}_m]$ → 각 time step의 출력들 concatenate

↓수식 설명

더보기

1. $emb_{enc}(x_t)$: $x_t$가 인코더의 embedding layer를 통과한다. 
2. rnn은 $emb_{enc}(x_t)$을 입력으로 받고, 이전 time step의 encoder의 hidden state와 함께 현재 time step의 hidden state를 계산

 

각 단계별 vector 사이즈

|$x_t$| = (bs, 1, |Vs|) 
|$emb(x_t)$| = (bs, 1, Ws)  → Ws: Word embedding vector의 size

|$h^{enc}_t$| = (bs, 1, hs)

|$h^{enc}_{1:m}$| = (bs, 1, hs)

 

bi-direcitional RNN을 사용할 경우 사이즈

|$h^{enc}_t$| = (bs, 1, 2*hs)

|$h^{enc}_{1:m}$| = (bs, 1, 2*hs)

 

↓코드↓

더보기

Encoder 코드

Encoder는 input 데이터 한 번에 넣어서 학습해도 된다. (bidirectional lstm 사용)

tensor를 padded sequence 타입의 객체로 변환할 것이다.

rnn에서 <pad>가 들어있는 것을 처리하기 쉬워진다.

class Encoder(nn.Module):

    def __init__(self, word_vec_size, hidden_size, n_layers=4, dropout_p=.2):
        super(Encoder, self).__init__()

        # Be aware of value of 'batch_first' parameter.
        # Also, its hidden_size is half of original hidden_size,
        # because it is bidirectional.
        self.rnn = nn.LSTM(
            word_vec_size,
            int(hidden_size / 2),
            num_layers=n_layers,
            dropout=dropout_p,
            bidirectional=True,
            batch_first=True,
        )

    def forward(self, emb):
        # |emb| = (batch_size, length, word_vec_size)

        if isinstance(emb, tuple):
            x, lengths = emb
            x = pack(x, lengths.tolist(), batch_first=True)

            # Below is how pack_padded_sequence works.
            # As you can see,
            # PackedSequence object has information about mini-batch-wise information,
            # not time-step-wise information.
            # 
            # a = [torch.tensor([1,2,3]), torch.tensor([3,4])]
            # b = torch.nn.utils.rnn.pad_sequence(a, batch_first=True)
            # >>>>
            # tensor([[ 1,  2,  3],
            #     [ 3,  4,  0]])
            # torch.nn.utils.rnn.pack_padded_sequence(b, batch_first=True, lengths=[3,2]
            # >>>>PackedSequence(data=tensor([ 1,  3,  2,  4,  3]), batch_sizes=tensor([ 2,  2,  1]))
        else:
            x = emb

        y, h = self.rnn(x)
        # |y| = (batch_size, length, hidden_size)
        # |h[0]| = (num_layers * 2, batch_size, hidden_size / 2)

        if isinstance(emb, tuple):
            y, _ = unpack(y, batch_first=True)

        return y, h

 

 


Decoder

인코더로부터 문장을 압축한 context vector를 바탕으로 문장을 생성(번역된 데이터를 한 개씩 순차적으로 출력한다.)
인코더의 마지막 hidden state = 디코더의 첫번째 hidden state

디코더는 conditional language model이라고 볼 수 있다.

(이전 time step의 문장이 주어지고 $x^i$ 라는 조건이 주어졌을때 단어를 예측하는 것이기 때문에)

Auto-regressive task에 속하므로, uni-directional RNN 사용한다.

 

<수식>

decoder의 hidden state 구하기

$h^{dec}_t=RNN_{dec}(emb_{dec}(\hat{y}_{t-1}), h^{dec}_{t-1}),$ where $h^{dec}_0=h^{enc}_m$.

$h^{dec}_{1:n}=[h^{dec}_1;...;h^{dec}_n]$  

 

각 단계별 vector 사이즈

|$y^i$| = (bs, n, |Vt|) 
|$\hat{y}_{t-1}$| = (bs, 1, |V|)  

|$emb_{dec}$| = (bs, 1, Ws) → Ws: Word embedding vector의 size

|$h^{dec}_{t-1}$| = (bs, 1, hs)

|$h^{dec}_{1:n}$| = (bs, n, hs)

 

 

↓코드

더보기

Decoder 코드

인코더와 달리 한 time-step씩 들어온다.

LSTM의 입력: word_vec_size + hidden_size

    word_vec_size: word embedding vector

    hidden_size: input feeding을 해줄 것이라서 이전 time-step의 $\tilde{h}_{t-1}$도 넣어줄 것이다. (concat)

 

Decoder의 forward 에선 한 time-step씩 들어온다

emb_t: $emb_t$  현재 time step

h_t_1: $h_{t-1}$    이전 time step의 hidden state 
        ($h_{t-1}=h_{t-1}, c_{t-1}$) →이전 time step의 lstm의 hidden state와 cell state의 튜플


class Decoder(nn.Module):

    def __init__(self, word_vec_size, hidden_size, n_layers=4, dropout_p=.2):
        super(Decoder, self).__init__()

        # Be aware of value of 'batch_first' parameter and 'bidirectional' parameter.
        self.rnn = nn.LSTM(
            word_vec_size + hidden_size,
            hidden_size,
            num_layers=n_layers,
            dropout=dropout_p,
            bidirectional=False,
            batch_first=True,
        )

    def forward(self, emb_t, h_t_1_tilde, h_t_1):
        # |emb_t| = (batch_size, 1, word_vec_size)
        # |h_t_1_tilde| = (batch_size, 1, hidden_size)
        # |h_t_1[0]| = (n_layers, batch_size, hidden_size)
        batch_size = emb_t.size(0)
        hidden_size = h_t_1[0].size(-1)

        if h_t_1_tilde is None:
            # If this is the first time-step,
            h_t_1_tilde = emb_t.new(batch_size, 1, hidden_size).zero_()
            # tensor type 맞춰서 0으로 채워라 (같은 device, tensor type)

        # Input feeding trick.
        x = torch.cat([emb_t, h_t_1_tilde], dim=-1)
        # |x| = (bs, 1, ws+hs)

        # Unlike encoder, decoder must take an input for sequentially.
        y, h = self.rnn(x, h_t_1)

	# |y|=(bs, 1, hs)
        # h는 튜플로 나올 것
        # |h[0]|=|h[1]| = (n_layers, bs, hs)
        
        
        return y, h

 

이 다음 time-step 과정

 

attention을 거쳐서 context vector가 나온다.

(attention 을 배우고 난 다음에 코드를 배워서 attention이라고 함 - 어텐션에 대한 설명은 다음 게시글에 있다.)

context vector와 현재 time-step의 y를 concat해사 $\tilde{h}$를 구한다.

다음 time step에서 이 $\tilde{h}$와 generator까지 가면 어떤 값이 나올 것이다.

그걸 embedding layer를 거쳐서 word embedding vector하고 입력으로 다시 들어온다.

 

 


 

Generator

디코더로부터 hidden state를 받아서 그 정보를 활용해서 linear layer와 softmax를 거쳐서 그 time step의 단어를 예측하는 것

우리가 원하는 것

|$\hat{y}_t$| = (bs, 1, |V|)  문장별 각 time step의 단어별 확률값

 

<수식>

decoder의 hidden state를 가져와서 linear layer 통과시키고 분포를 뱉어준다. (단어별 확률값을 알고 싶다.)

$\hat{y}_t = softmax(h^{dec}_t \cdot W_{gen})$

 

 

(bs, 1, |V|) = (bs, 1, hs) x (hs, |V|)

    |$y_t$|                |$h^{dec}_t$|          |$W_{gen}$|

 

<BOS>와 <EOS>

decoder에 <BOS>가 들어가서 "decoder야, 이제 문장 생성 시작해" 라고 하고, decoder엔 <EOS> 없음

generator가 <EOS> 뱉어내며, "나 이제 끝났어"라고 하며, generator엔 <BOS> 없다.

 

↓코드

더보기

Generator 코드

class Generator(nn.Module):

    def __init__(self, hidden_size, output_size):
        super(Generator, self).__init__()

        self.output = nn.Linear(hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, x):
        # |x| = (batch_size, length, hidden_size)

        y = self.softmax(self.output(x))
        # |y| = (batch_size, length, output_size)

        # Return log-probability instead of just probability.
        return y

 

'Deep Learning > NLP' 카테고리의 다른 글

[패캠] (Seq2Seq) Input Feeding & Teacher Forcing  (0) 2022.02.15
[패캠] (Seq2Seq) Attention  (0) 2022.02.14
[패캠] (LM) Neural LM  (0) 2022.02.05
[패캠] (LM) 기존의 언어 모델  (0) 2022.02.05
[패캠] (LM) 언어모델  (0) 2022.02.05

댓글