elevne's Study Note
NLP 공부 (4-2: Seq2Seq with Attention) 본문
앞서 글에서 미리 만들어둔 전처리 함수를 데이터에 적용하여 학습용 데이터 전처리는 완료되었다. 그 후 모델을 구성해보았다.
class Encoder(tf.keras.layers.Layer):
def __init__(self, wordDicSize, embeddingDim, encDim, batchSize):
super(Encoder, self).__init__()
self.batchSize = batchSize
self.encDim = encDim
self.wordDicSize = wordDicSize
self.embeddingDim = embeddingDim
self.embedding = tf.keras.layers.Embedding(self.wordDicSize, self.embeddingDim)
self.gru = tf.keras.layers.GRU(self.encDim,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
def call(self, data):
data = self.embedding(data)
all_hiddenStates, last_cellAndHiddenState = self.gru(data, initial_state = hidden)
return all_hiddenStates, last_cellAndHiddenState
Python에서는 Java에서와 마찬가지로 Class간 상속이 가능하다. Super 명령어를 사용하면 클래스 간 상속관계에서 부모 클래스를 자식클래스가 호출할 수 있다. Encoder class의 인자로 keras.layers.Layer을 넣어주었다. Layer은 Keras에서 신경망의 기본적인 빌딩 블록이다. Class의 인자로 배치사이즈, 신경망 결과 차원의 크기, 단어사전의 크기, 임베딩 차원의 크기를 지정해두었다. 이 Class를 호출하게 되었을 때 우선 입력받은 데이터를 Embedding 함수를 통해 임베딩 처리를 하게되고, GRU신경망에 학습을 시키게된다.
이전에 LSTM을 중첩하여 사용했을 때처럼 return_sequences=True로 지정해주었고, 여기서는 return_state 또한 True로 설정하였다. 다시 한 번 이 둘에 대해 알아보자면 return_sequence=True로 설정할 시에 GRU모델의 time step별로 hidden_state를 모두 출력하게 된다.(보통 Attention을 사용할 때 설정해준다고 함) return_state=True로 지정 시에는 GRU모델의 마지막 time step의 hidden state, cell state가 출력이 되는 것이라고 한다. GRU 모델에서recurrent_initializer="glorot_uniform"으로 지정해주며 Xavier Initializer을 사용하였다. 활성화함수로 ReLU를 사용하지 않는다면 비선형함수에서 효과적인 결과를 보여주는 초기화 기법이라고 한다. 만약 ReLU함수를 사용한다면 He Initialization이 추천된다고 한다.
Seq2Seq만 사용하게 되었을 때는 성능적인 한계가 있는데, 이는 Seq2Seq 모델이 Context Vector를 사용하는 구조를 지니기 때문이라고 한다. 문장이 아무리 길어도 하나의 Context Vector 안에 모든 내용이 압축되게끔 해야하니, 이 Context Vector가 일종의 Bottle Neck으로 작용한다는 것이다. 이러한 문제점을 해결하기 위해 사용되는 것이 Transformer인데, 이는 Seq2Seq를 마친 이후 바로 공부해볼 예정이다.
Seq2Seq의 구조를 사용하면서도, Context Vector의 병목 문제를 어느정도 완화시키기 위해 Attention을 사용할 수 있다. Attention은 Decoder에서 출력 단어를 예측하는 매 time step마다 Encoder에서의 전체 입력 문장을 다시 한 번 참고하는 구조를 지니고 있다. 여기서, 전체 입력 문장을 전부 동일한 비율로 참고하는 것이 아니라, 해당 시점에서 예측해야할 단어와 높은 연관성을 지니고 있는 부분을 더 집중(Attention)해서 보게 되는 것이다. Attention도 함께 학습을 하며 이를 통해 Decoder의 각 time step마다 가중치가 다르게 작용하게 되는 것이다. Attention 함수는 주어진 Query에 대해서 모든 Key와의 유사도를 각각 구한다. 그리고 구해낸 이 유사도를 Key와 매핑 되어있는 각각의 Value에 반영해주는 것이다.
코드는 아래와 같이 적었다.
class BahdanauAttention(tf.keras.layers.Layer):
def __init__(self, outDim):
super(BahdanauAttention, self).__init__()
self.outDim1 = tf.keras.layers.Dense(outDim)
self.outDim2 = tf.keras.layers.Dense(outDim)
self.conv = tf.keras.layers.Dense(1)
def call(self, query, value):
all_hiddenStates = tf.expand_dims(query, 1)
score = self.conv(tf.nn.tanh(self.outDim1(value) + self.outDim2(all_hiddenStates)))
attention_weights = tf.nn.softmax(score, axis=1)
context_vector = attention_weights * value
context_vector = tf.reduce_sum(context_vector, axis=1)
return context_vector, attention_weights
위 바다나우 어텐션 Class에서는 세 개의 Dense를 사용하였는데, 각각이 다른 곳에 쓰인 것이다. outDim1은 Encoder의 결과값에 행렬곱이 될 가중치이고, outDim2는 Encoder의 전체 은닉층 상태 값에 행렬곱이 될 가중치이다. 또, V는 outDim1, 2를 더하고 하이퍼볼릭 탄젠트 함수 안에 넣은 값에 행렬곱을 하여 1차원 벡터를 뽑아내게 하는 가중치이다. 모델을 학습하며 이 가중치들도 마찬가지로 학습이 진행되는 것이다. 이렇게 얻은 score(1차원벡터)는 softmax에 넣어지게 되는데, 여기서 모델이 중요하다고 판단하는 값은 1에 가까워지고, 덜 중요하다고 판단될수록 0에 가까워지는 것이다.
이러한 방식으로 얻은 attention weights를 values(Encoder 결과값)와 행렬곱을 하게되면 1에 가까운 값에 위치한 value 값은 커지고 0에 가깝게 위치한 value 값은 작아지게 되는 것이다. 그 후에 이 행렬곱을 진행한 값에 reduce_sum axis=1로 돌려주어 1차원의 Context Vector, Encoder의 정보를 집약하여 가지고 있는 벡터로 만들어주는 것이다.
class Decoder(tf.keras.layers.Layer):
def __init__(self, wordDicSize, embeddingDim, decDim, batchSize):
super(Decoder, self).__init__()
self.batchSize = batchSize
self.decDim = decDim
self.wordDicSize = wordDicSize
self.embeddingDim = embeddingDim
self.embedding = tf.keras.layers.Embedding(self.vocab_size, self.embedding_dim)
self.gru = tf.keras.layers.GRU(self.dec_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
self.result = tf.keras.layers.Dense(self.wordDicSize)
self.attention = BahdanauAttention(self.decDim)
def call(self, data, all_hiddenStates, value):
context_vector, attention_weights = self.attention(all_hiddenStates, value)
data = self.embedding(data)
data = tf.concat([tf.expand_dims(context_vector, 1), data], axis=-1)
output, state = self.gru(data)
output = tf.reshape(output, (-1, output.shape[2]))
x = self.result(output)
return x, state, attention_weights
Decoder Class에서는 이전에 만든 Attention class를 활용하여 Context vector, Attention Weights를 생성하고 이 값을 활용하여 들어온 데이터와 함께 GRU 모델을 돌리게 되는 것이다. 그리고 self.result로 지정해둔 것을 이용하여 단어사전의 크기만큼의 길이를 가진 벡터로 값이 출력되게끔 하였다.
class seq2seq(tf.keras.Model):
def __init__(self, wordDicSize, embeddingDim, encOut, decOut, batchSize, endTokenIndex):
super(seq2seq, self).__init__()
self.endTokenIndex = endTokenIndex
self.encoder = Encoder(wordDicSize, embeddingDim, encOut, batchSize)
self.decoder = Decoder(wordDicSize, embeddingDim, decOut, batchSize)
def call(self, data):
input, target = data
all_hiddenStates, last_cellAndHiddenState = self.encoder(input)
predictions = list()
for t in range(0, target.shape[1]):
decInput = tf.dtypes.cast(tf.expand_dims(target[:, t], 1), tf.float32)
predicted, last_cellAndHiddenState, _ = self.decoder(decInput, last_cellAndHiddenState, all_hiddenStates)
predictions.append(tf.dtypes.cast(predicted, tf.float32))
return tf.stack(predictions, axis=1)
def guess(self, data):
input = data
all_hiddenStates, last_cellAndHiddenState = self.encoder(input)
dec_hidden = enc_hidden
decInput = tf.expand_dims([wordDic2[SOS]], 1)
predictions = list()
for t in range(0, MAXLEN):
predicted, dec_hidden, _ = self.decoder(decInput, last_cellAndHiddenState, all_hiddenStates)
predictToken = tf.argmax(predicted[0])
if predictToken == self.end_token_idx:
break
predictions.append(predictToken)
decInput = tf.dtypes.cast(tf.expand_dims([predictToken], 0), tf.float32)
return tf.stack(predictions, axis=0).numpy()
마지막으로, 지금까지 만들어준 모든 Class들을 활용하여 Seq2Seq Class를 만들어주었다. keras의 Model 클래스를 상속받으며 하나의 Model로 컴파일할 수 있게끔 해주었다. Seq2Seq의 call 함수는 Encoder, Decoder 입력값을 둘 다 data라는 인자를 통해 받는다.(list안에 두 개의 data를 넣어주고 그 리스트를 인자 data로 넣어주기) Encoder Input 데이터를 활용하여 Encoder class에서 Encoder 결과값과 은닉상태들에 대한 정보를 만들고, Decoder에서는 Sequence의 최대 길이만큼 반복하면서 디코더의 출력값을 만들어내는 것이다.
밑의 guess함수는 추후에 Model을 학습시킨 후 직접 문장을 입력해서 출력값을 만들어낼 때 사용할 함수인데, call 함수와 거의 유사한 구조를 보이고 있다.
model = seq2seq(wordDicSize, 256, 1024, 1024, 32, wordDic2[END])
model.compile(loss='sparse_categorical_crossentropy',
optimizer=tf.keras.optimizers.Adam(1e-3), metrics=[accuracy])
checkpoint = ModelCheckpoint(
"./modelTest"+ '/weights.h5', monitor='val_accuracy', verbose=1, save_best_only=True, save_weights_only=True)
earlystop = EarlyStopping(monitor='val_accuracy', min_delta=0.0001, patience=10)
model.fit([encoderInputs, decoderInputs], seq2seqTarget,
batch_size=32, epochs=5,
validation_split=0.1, callbacks=[earlystop, cp_callback])
마지막으로 위 코드를 통해 학습을 진행해줄 수 있었다. 그 후, guess 함수를 사용하여 직접 몇 번 테스트를 진행해보았다. 성능은 썩 좋진 않았다.
text = "오늘 날씨 어때?"
indexInput, _ = inputEncoder([text], wordDic2)
prediction = model.guess(indexInput)
print(' '.join([wordDic1[str(token)] for token in prediction]))
이번 실습을 진행하면서 몇 가지 문제점을 발견했다. 우선 Kkma를 활용하여 토크나이징을 진행하면 마지막에 직접 guess 해보는 단계에서 token들을 재조합 시키는 것이 어려운 점이었다. 결국 띄어쓰기 기준으로 split하여 그것을 기준으로 embedding을 진행하였고, 결과적으로 썩 뛰어난 성능을 보이지는 않았다. 이 부분에 대해서 다음에 꼭 알아봐야겠다.
출처:
https://chanhuiseok.github.io/posts/java-1/
'Machine Learning > NLP' 카테고리의 다른 글
NLP 공부 (5-2: Transformer) (0) | 2022.10.25 |
---|---|
NLP 공부 (5-1: Transformer) (0) | 2022.10.23 |
NLP 공부 (4-1: Seq2Seq with Attention) (0) | 2022.10.21 |
NLP 공부 (3: 감성분석) (1) | 2022.10.18 |
NLP 공부 (2: Embedding(Word2Vec)) (0) | 2022.10.16 |