[TENSORFLOW] LSTM Dual encoder 기반 챗봇 만들기

2017. 7. 12. 11:52machine learning

해당 포스팅은 아래의 wildml 블로그에서 소개한 코드를 참조로 작성하였다.


http://www.wildml.com/2016/04/deep-learning-for-chatbots-part-1-introduction/


챗봇을 크게 검색 기반형 챗봇과 generative 챗봇으로 나눌 수 있다.

검색 기반은 일반적으로 후보 응답 셋들과 모델을 통해 예측된 결과 벡터와의 유사도를 구해 가장 유사도가 높은 결과 응답을 정답으로 나타내며, generative의 경우 기존 응답 셋을 활용하는게 아니라 유사도 높은 단어의 조합으로 새로운 응답 결과를 나타내는 방식을 말한다.

우리는 여기서 검색 기반형 챗봇에 대해 알아볼 것이며, 기본적인 모델의 구조는 아래와 같다.


위쪽의 c1,c2,ct에 질문 문장이 구성된 단어 리스트들이 시간 순으로 들어가고 아래쪽 r1,r2,rt에 정답 응답 문장의 단어 리스트들이 들어가게 된다.  

ex) 오늘 머 먹었니? c1 : 오늘, c2 : 머, c3 : 먹었니?

    난 김치볶음밥 먹었어. r1 : 난, r2 : 김치볶음밥, r3 : 먹었어.


각 단어의 리스트들이 RNN cell을 거치게 되며, 각각의 RNN cell에서는 hidden code를 생성하게 되며 ht를 통해 최종 hidden code가 생성이 된다. 질문에 대한 hidden code c와 응답에 대한 hidden code r이 각각 생성될 것이다. 

이제 이 생성된 hidden code에 대한 유사도 값을 높이도록 학습을 진행할 것이다. 결과적으로 정답일 경우에는 c와 M 그리고 r의 행렬 곱한 값의 시그모이드 값이 1에 가깝도록 c,M,r을 각각 학습하고, 오답일 경우에는 결과 값이 0에 가깝도록 학습을 진행한다.

ex) hidden code : 256 x 1 이라고 가정하에, 아래와 같은 행렬 곱이 만들어지게 되고, 이 결과는 scalar 값이 된다. 결과 scalar를 sigmoid를 취해서 확률 값을 결정하게 된다. 참고로 (1x256) 은 c의 transpose, (256x256)은 M, (256x1)은 r을 각각 의미한다.





또한 gradient vanishing을 방지하기 위해 각각의 rnn cell은 lstm으로 구성하고 있다. 이제 모델 코드를 살펴보자. 전체 코드는 아래의 github를 참조하면 된다. 참고로 6개월 가량 방치되고 있는 코드이다. (걍 이런 것도 있다는 참조만 하자.)


https://github.com/dennybritz/chatbot-retrieval/



해당 함수가 실행되면 처음으로 embedding vector를 랜덤으로 생성한다. (랜덤으로 생성하거나 glove vector를 이용할 수 있다.) embedding vector는 vocab_size * dimension의 개수만큼 생성이 되며, 학습이 진행될수록 embedding_lookup을 통해 내부적으로 초기 랜덤 벡터에 대한 update가 이루어 진다.

이제 실제 rnn 모델 구성을 살펴보자.

dynamic_rnn을 구성하기 위해서는 cell을 만들어 줘야 하는데 gradient vanising 문제를 해결하기 위해 LSTM cell을 사용했다. 그 후에 tf.nn.dynamic_rnn을 이용해서 rnn 모델을 만들어 주자.

이 부분이 약간 혼란스러웠는데, 코드를 보면 tf.concat을 통해 질문과 답을 합쳐서 input으로 넣고 있다. 


tf.concat(0, [context_embedded, utterance_embedded])


context_embedded와 utterance_embedded는 각각 (?, 160, 100) 의 matrix를 가지고 있고 (위의 코드상에서는) concat을 했기 때문에 [(?,160,100), (?,160,100)] 이렇게 두 개의 행렬을 가진 리스트가 생성이 된다. 어짜피 질문과 응답 모두 같은 RNN 구조를 사용하기 때문에 위와 같이 한꺼번에 처리하는 것 같다.


위와 같은 식으로 rnn을 돌린 후, 아래와 같이 hidden code를 split 해서 각각 질문과 응답에 대한 hidden code 값으로 나누어 준다.


encoding_context, encoding_utterance = tf.split(0, 2, rnn_states.h)


rnn 모델 구성이 끝났다면 이번에는 예측 로직을 살펴보자. 맨 처음 dual encoder 모형을 설명하면서 언급했던 M 행렬을 선언해준다. M 행렬을 선언한 후 정규 분포 값으로 초기화를 하였다. 


M = tf.get_variable("M", 

shape=[hparams.rnn_dim, hparams.rnn_dim],

initializer=tf.truncated_normal_initializer())


그 후에 M행렬과 질문에 대한 hidden code값을 행렬 곱을 하고 그 후 응답에 대한 hidden code 값을 행렬 곱을 한다. 이렇게 곱해서 나온 scalar 값을 마지막으로 sigmoid를 취함으로써 확률 값으로 반환을 한다.


generated_response = tf.matmul(encoding_context, M)

....

logits = tf.batch_matmul(generated_response, encoding_utterance, True)

...

probs = tf.sigmoid(logits)


cost 함수는 sigmoid cross entropy를 사용한다.


losses = tf.nn.sigmoid_cross_entropy_with_logits(logits, tf.to_float(targets))


이렇게 만들어진 dual encoder 모델을 udc_train.py를 실행시킴으로써 train 시킬 수 있다.


모델 training을 위한 train data와 validation data set은 각각 아래와 같이 만들어 주자.


[train data set]


[validation data set]


train data set에서 Context는 질문, Utterance는 응답, Label은 정답 여부를 나타내며, validation data set에서의 Context는 질문, Ground Truth Utterance는 정답 응답, Distractor_0 ~ 8은 오답 응답을 나타낸다.


검색 결과 후보 리스트를 10개를 받아서 유사도를 판별하기 때문에 validation set을 위와 같이 10개의 후보군을 받도록 지정해 놓았다.