2023. 2. 26. 18:16ㆍNatural Language Processing
저번 장에서는 LSTM과 같은 모델을 통해서
게이트가 추가된 RNN 모델이 어떻게 장기 기억을 가져가는지 알아볼 수 있었다.
이번 장에서는 이 LSTM 모델을 Encoder와 Decoder라는 2가지 형태로 구현하여
하나의 시퀀스(시계열)를 다른 시퀀스로 변환하는 방법을 알아보도록 하자.
seq2seq 모델은 하나의 순환 신경망(Encoder)에서 입력을 통과시켜 출력된 마지막 은닉 상태(h)를 다른 신경망(Decoder)
에 전달한다. 이 은닉 상태(h)는 입력(x)에 대한 정보를 하나로 응축시킨 데이터라고 할 수 있다.
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
self.hs = None
Encoder 가 임베딩과 LSTM 계층만 가지고 있는 이유는, Encoder의 목적은 입력 데이터를 잘 표현할 수 있는
마지막 은닉 상태(h)를 출력하는 것이기 때문에 임베딩 계층과 LSTM 계층으로만 이루어져있다.
def forward(self, xs):
xs = self.embed.forward(xs)
hs = self.lstm.forward(xs)
self.hs = hs
#lstm이 출력한 은닉상태 h들의 집합
return hs[:,-1,:] #Decoder에 전달할 가장 마지막 은닉상태 h를 반환
Encoder 계층은 입력 데이터들을 먼저 임베딩한 뒤, LSTM 계층에 통과시켜서 은닉 상태를 출력한다.
def backward(self, dh):
dhs = np.zeros_like(self.hs)
dhs[:, -1, :] = dh
#Decoder에서 전파된 dh를 Encoder의 가장 마지막 시점의 역전파로 받아들임
dout = self.lstm.backward(dhs)
dout = self.embed.backward(dout)
return dout
Encoder의 역전파는 Decoder에서 전파된 dh를 Encoder의 가장 마지막 LSTM 계층의 역전파로 받아들임으로써 시작된다.
LSTM과 임베딩 계층 순으로 역전파가 진행된다.
다음으로 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)
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
Decoder는 Encoder로부터 전달받은 은닉 상태(h)와 Decoder에 들어오는 입력데이터를
임베딩, LSTM, 어파인 변환 계층을 차례로 통과시켜서 최종 결과를 출력한다.
이 최종 결과는 전체 단어 사전에서 각 단어에 대한 확률로 이해할 수 있다.
예를 들어 Encoder에 ['나는', '밥을', '먹는다']를 입력으로 주어져서 이 데이터들이 하나의 은닉 상태 h로 변환되면
Decoder는 이 은닉 상태(h)를 이어받고, ['i', 'have', 'a meal']를 차례로 출력시켜야 한다.
이 때 각 시점에서의 출력은 i가 80, you가 60, have가 15 처럼 출력 단어들의 확률(로서 해석할수 있는) 값이 된다.
def forward(self, xs, h):
#Encoder에서 최종 출력한 마지막 은닉상태 h를 이어받는다.
self.lstm.set_state(h)
out = self.embed.forward(xs)
out = self.lstm.forward(out)
score = self.affine.forward(out)
return score
이 확률로서 해석될 수 있는 데이터가 score 값이다.
def backward(self, dscore):
dout = self.affine.backward(dscore)
dout = self.lstm.backward(dout)
dout = self.embed.backward(dout)
dh = self.lstm.dh
#Encoder에게 전달할 dh를 반환
return dh
Decoder의 역전파는 Affine 변환, LSTM, 임베딩 계층 차례로 역전파한다.
Decoder의 dh는 Encoder에게 전달되어 Encoder 역전파에 이용된다.
def generate(self, h, start_id, sample_size):
sampled = []
sample_id = start_id
# 가장 처음의 단어의 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())
#score : softmax 안 거친 값이지만 확률처럼 해석
#가장 큰 값(확률)을 가진 값의 index(단어 id)를 반환
sampled.append(sample_id)
return sampled
Decoder는 Encoder가 응축한 은닉상태 h를 전달받고, 입력되는 데이터를 차례로 입력받아
가장 등장확률이 높다고 판단되는 다음 단어(혹은 문자)를 출력한다.
generate는 이러한 과정을 수행하는 함수이다.
Encoder와 Decoder를 하나로 연결하여 Seq2seq 모델을 구현해보자.
class 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 = Decoder(V, D, H)
self.softmax = TimeSoftmaxWithLoss()
self.params = self.encoder.params + self.decoder.params
self.grads = self.encoder.grads + self.decoder.grads
encoder와 decoder를 가지고 Encoder에 들어온 입력 정보에 대한 Decoder의 출력 정보를 생성한다.
def forward(self, xs, ts):
#Seq2seq 모델을 훈련시키기 위한 forward
decoder_xs, decoder_ts = ts[:, :-1], ts[:,1:]
#Decoder의 target은 input보다 한발짝 늦는 상태
h = self.encoder.forward(xs)
#Encoder에서 출력한 마지막 은닉상태 h를 Decoder에 전달
score = self.decoder.forward(decoder_xs, h)
loss = self.softmax.forward(score, decoder_ts)
return loss
seq2seq의 순전파 과정은 위와 같다. 이 순전파(forward)는 훈련을 위해 사용되므로, decoder의 출력 데이터(score)는
소프트맥스-크로스엔트로피 손실 계층에 통과시켜서 손실값(loss)을 계산한다.
def backward(self, dout = 1):
dout = self.softmax.backward(dout)
dh = self.decoder.backward(dout)
dout = self.encoder.backward(dh)
return dout
Seq2seq의 역전파는 위와 같다.
def generate(self, xs, start_id, sample_size):
h = self.encoder.forward(xs)
sampled = self.decoder.generate(h, start_id, sample_size)
return sampled
훈련이 종료된 seq2seq는 Encoder - Decoder 를 통해 하나의 시퀀스를 다른 하나의 시퀀스로 변환한다.
'Natural Language Processing' 카테고리의 다른 글
[NLP] 단어 임베딩을 이용한 기사 분류 신경망 (0) | 2023.03.28 |
---|---|
[NLP] Attention (0) | 2023.02.26 |
[NLP] LSTM(Long Short-Term Memory) (0) | 2023.02.25 |
[NLP] RNN(순환 신경망) (0) | 2023.02.22 |
[NLP] word2vec 개선 - 임베딩, 네거티브 샘플링 (0) | 2023.02.19 |