[패스트 캠퍼스] 김기현의 딥러닝을 활용한 자연어생성 올인원 패키지 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 steph_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 |
댓글