챗봇을 만들기 위해서는 우선 앞선 포스팅인 트랜스포머에 대해서 이해하고 다음을 알아보자.
https://undeadkwandoll.tistory.com/26
데이터 로드
시작하기전 필요한 라이브러리를 불러오자.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
import urllib.request
import time
import tensorflow_datasets as tfds
import tensorflow as tf
챗봇 데이터를 로드하여 상위 5개 샘플 출력해보자.
urllib.request.urlretrieve("https://raw.githubusercontent.com/songys/Chatbot_data/master/ChatbotData%20.csv", filename="ChatBotData.csv")
train_data = pd.read_csv('ChatBotData.csv')
train_data.head()
위 데이터를 보면 Q(question), A(answer) 의 쌍으로 이루어진 데이터이다.
#총 샘플 개수 확인
print('챗봇 샘플의 개수 :',len(train_data))
챗봇 샘플의 개수 : 11823
총 샘플 갯수는 11823개이며, 불필요한 NULL값이 있는지 확인해보자.
print(train_data.isnull().sum())
Null값은 별도로 존재하지 않는다. 기본적으로 사용하는 토큰화를 위한 형태소 분석툴을 사용하지 않고, 다른 방법인 학습 기반의 토크나이저를 사용할 것이다. 그러므로, row data에서 구두점을 미리 처리해야한다.
구두점들은 단순히 제거해버릴수 있지만, 구두점 앞에 띄어쓰기를 추가하여 다른 문자들과 구분해보자.
questions=[]
for sentence in train_data['Q']:
#구두점에 대해서 띄어쓰기
#ex) 12시 땡! ->12시 땡 !
sentence=re.sub(r"([?.!,])", r" \1",sentence)
sentence=sentence.strip()
questions.append(sentence)
answers=[]
for sentence in train_data['A']:
#구두점에 대해서 띄어쓰기
#ex) 12시 땡! ->12시 땡 !
sentence=re.sub(r"([?.!,])",r" \1",sentence)
sentence=sentence.strip()
answers.append(sentence)
이후 잘 처리가 되었는지 확인해보자.
print(questions[:5])
print(answers[:5])
['12시 땡 !', '1지망 학교 떨어졌어', '3박4일 놀러가고 싶다', '3박4일 정도 놀러가고 싶다', 'PPL 심하네']
['하루가 또 가네요 .', '위로해 드립니다 .', '여행은 언제나 좋죠 .', '여행은 언제나 좋죠 .', '눈살이 찌푸려지죠 .']
다음과 같이 잘 처리되었다.
단어 집합 생성
서브워드텍스트인코더를 사용해 자주 사용되는 서브워드 단위로 토큰을 분리하는 토크나이저로 학습 데이터로부터 학습하여 서브워드로 구성된 단어 집합을 생성해보자.
#서브워드텍스트인코더를 사용하여 질문, 답변 데이터로부터 단어 집합(Vocabulary) 생성
tokenizer=tfds.deprecated.text.SubwordTextEncoder.build_from_corpus(questions+answers,target_vocab_size=2**13)
seq2seq에서의 인코더-디코더 모델 계열에는 디코더의 입력으로 사용할 시작을 의미하는 시작 토큰 SOS와 종료 토큰 EOS 또한 존재한다. 해당 토큰들도 단어 집합에 포함시킬 필요가 있으므로 이 두 토큰에 정수를 부여하자.
#시작 토큰과 종료 토큰에 대한 정수 부여.
START_TOKEN,END_TOKEN=[tokenizer.vocab_size],[tokenizer.vocab_size+1]
#시작 토큰과 종료 토큰을 고려햐여 단어 집합의 크기를 +2
VOCAB_SIZE=tokenizer.vocab_size+2
print('시작 토큰 번호: ',START_TOKEN)
print('종료 토큰 번호: ',END_TOKEN)
print('단어 집합의 크기: ',VOCAB_SIZE)
시작 토큰 번호: [8170]
종료 토큰 번호: [8171]
단어 집합의 크기: 8172
padding에 사용될 0번 토큰부터 마지막 토큰인 8171번 토큰까지의 개수를 카운트하면 단어 집합의 크기는 8172개이다.
정수 인코딩과 패딩
단어 집합 생성 후, 서브워드텍스트인코더의 토크나이저로 정수 인코딩을 진행할 수 있다. 이는 토크나이저의 .encode()를 사용하여 가능하다. 랜덤샘플 20번 질문 샘플, 즉 ,questions[20]을 갖고 인코딩 해보자.
#서브워드텍스트인코더 토크나이저의 .encode()를 사용하여 텍스트 시퀀스를 정수 시퀀스로 변환.
print('임의의 질문 샘플을 정수 인코딩 : {}'.format(tokenizer.encode(questions[20])))
임의의 질문 샘플을 정수 인코딩 : [5759, 607, 3502, 138, 681, 3740, 846]
임의의 질문 문장이 정수 시퀀스로 변환되었다. 반대로 정수 인코딩 된 결과는 다시 decode()를 사용해 기존의 텍스트 시퀀스로 복원할 수 있다.
#서브워드텍스트인코더 토크나이저의 .encode()와 .decode() 테스트해보기
#임의의 입력 문장을 sample_string에 저장
sample_string=questions[20]
#encode() : 텍스트 시퀀스 -->정수 시퀀스
tokenized_string=tokenizer.encode(sample_string)
print('정수 인코딩 후의 문장 {}'.format(tokenized_string))
#decode(): 정수 시퀀스-->텍스트 시퀀스
original_string=tokenizer.decode(tokenized_string)
print('기존 문장: {}'.format(original_string))
정수 인코딩 후의 문장 [5759, 607, 3502, 138, 681, 3740, 846]
기존 문장: 가스비 비싼데 감기 걸리겠어
정수 인코딩 된 문장을 .decode()를 하면 자동으로 서브워드들까지 다시 붙여서 기존 단어로 복원해준다. 위 결과를 보면 정수가 7개인데 기존 문장의 띄어쓰기 단위인 어절은 4개밖에 존재하지 않는다. 이는 결국 '가스비','비싼데'라는 한 어절이 정수 인코딩 후에는 두 개 이상의 정수일 수 있다는 것이다.
확인해보자.
#각 정수는 각 단어와 어떻게 mapping되는지 병렬로 출력
#서브워드텍스트인코더는 의미있는 단위의 서브워드로 토크나이징한다. 띄어쓰기 단위 x형태소 분석 단위 x
for ts in tokenized_string:
print('{}----->{}'.format(ts,tokenizer.decode([ts])))
5759----->가스
607----->비
3502----->비싼
138----->데
681----->감기
3740----->걸리
846----->겠어
샘플1개를 갖고 인코딩 디코딩을 해보았다. 이제 전체 데이터에 대해 정수 인코딩과 패딩을 하자.
#최대 길이를 40으로 정의
MAX_LENGTH=40
#토큰화/정수 인코딩/ 시작 토큰과 종료 토큰 추가/ 패딩
def tokenize_and_filter(inputs,outputs):
tokenized_inputs,tokenized_outputs=[],[]
for (sentence1,sentence2) in zip(inputs,outputs):
#encode(토큰화 +정수 인코딩), 시작 토큰과 종료 토큰 추가
sentence1=START_TOKEN+tokenizer.encode(sentence1)+END_TOKEN
sentence2=START_TOKEN+tokenizer.encode(sentence2)+END_TOKEN
tokenized_inputs.append(sentence1)
tokenized_outputs.append(sentence2)
#패딩
tokenized_inputs=tf.keras.preprocessing.sequence.pad_sequences(tokenized_inputs,maxlen=MAX_LENGTH,padding='post')
tokenized_outputs=tf.keras.preprocessing.sequence.pad_sequences(tokenized_outputs,maxlen=MAX_LENGTH,padding='post')
return tokenized_inputs,tokenized_outputs
questions,answers=tokenize_and_filter(questions,answers)
데이터의 크기를 확인해보자.
print('질문 데이터의 크기(shape): ',questions.shape)
print('답변 데이터의 크기(shape): ',answers.shape)
질문 데이터의 크기(shape): (11823, 40)
답변 데이터의 크기(shape): (11823, 40)
임의로 3번 샘플을 출력해보자.
print(questions[3])
print(answers[3])
길이 40을 맞추기 위해 뒤에 0이 임의로 패딩되어있는 것을 확인할 수 있다.
인코더와 디코더의 입력, 레이블 만들기.
데이터를 배치 단위로 불러오기 위해 tf.data.Dataset을 사용한다.
#텐서플로우 dataset을 이용하여 셔플(shuffle)을 수행하되, 배치 크기로 데이터를 묶는다.
#또한 이 과정에서 교사 강요(teacher forcing)을 사용하기 위해서 디코더의 입력과 실제 값 시퀀스를 구성한다.
BATCH_SIZE=64
BUFFER_SIZE=20000
#디코더의 실제값 시퀀스에서는 시작 토큰을 제거해야 한다.
dataset=tf.data.Dataset.from_tensor_slices((
{
'inputs':questions,
'dec_inputs':answers[:,:-1] #디코더의 입력. 마지막 패딩 토큰이 제거된다.
},
{
'outputs':answers[:,1:]
},
))
dataset=dataset.cache()
dataset=dataset.shuffle(BUFFER_SIZE)
dataset=dataset.batch(BATCH_SIZE)
dataset=dataset.prefetch(tf.data.experimental.AUTOTUNE)
#임의의 샘플에 대해서 [:,:-1] 과 [:,1:]이 어떤 의미를 가지는지 테스트해본다.
#기존 샘플
print(answers[0])
#마지막 패딩 토큰 제거하면서 길이가 39가 된다
print(answers[:1][:,:-1])
#맨 처음 토큰이 제거된다. 다시 말해 시작 토큰이 제거된다. 길이는 역시 39가 된다
print(answers[:1][:,1:])
트랜스포머 만들기
하이퍼파라미터를 조정하여 실제 논문의 트랜스포머보다는 작은 모델을 만든다.
주요 하이퍼파라미터는 다음과 같다.
tf.keras.backend.clear_session()
#Hyper-parameters
D_MODEL=256
NUM_LAYERS=2
NUM_HEADS=8
DFF=512
DROPOUT=0.1
model=transformer(
vocab_size=VOCAB_SIZE,
num_layers=NUM_LAYERS,
dff=DFF,
d_model=D_MODEL,
num_heads=NUM_HEADS,
dropout=DROPOUT)
learning rate, optimizer을 정의하고 compile 해보자
learning_rate=CustomSchedule(D_MODEL)
optimizer=tf.keras.optimizers.Adam(learning_rate,beta_1=0.9,beta_2=0.98,epsilon=1e-9)
def accuracy(y_true,y_pred):
#레이블의 크기는 (batch_size, MAX_LENGTH-1)
y_true=tf.reshape(y_true,shape=(-1,MAX_LENGTH-1))
return tf.keras.metrics.sparse_categorical_accuracy(y_true,y_pred)
model.compile(optimizer=optimizer,loss=loss_function,metrics=[accuracy])
EPOCHS=50
model.fit(dataset,epochs=EPOCHS)
Epoch 1/50
185/185 [==============================] - 272s 1s/step - loss: 1.4499 - accuracy: 0.0304
Epoch 2/50
185/185 [==============================] - 256s 1s/step - loss: 1.1818 - accuracy: 0.0495
Epoch 3/50
185/185 [==============================] - 273s 1s/step - loss: 1.0029 - accuracy: 0.0507
Epoch 4/50
185/185 [==============================] - 276s 1s/step - loss: 0.9276 - accuracy: 0.0547
Epoch 5/50
185/185 [==============================] - 263s 1s/step - loss: 0.8703 - accuracy: 0.0575
.
.
.
Epoch 45/50
185/185 [==============================] - 204s 1s/step - loss: 0.0068 - accuracy: 0.1734
Epoch 46/50
185/185 [==============================] - 206s 1s/step - loss: 0.0068 - accuracy: 0.1735
Epoch 47/50
185/185 [==============================] - 206s 1s/step - loss: 0.0062 - accuracy: 0.1736
Epoch 48/50
185/185 [==============================] - 203s 1s/step - loss: 0.0063 - accuracy: 0.1736
Epoch 49/50
185/185 [==============================] - 204s 1s/step - loss: 0.0058 - accuracy: 0.1737
Epoch 50/50
185/185 [==============================] - 204s 1s/step - loss: 0.0058 - accuracy: 0.1737
<tensorflow.python.keras.callbacks.History at 0x2076df63ca0>
챗봇 평가
def evaluate(sentence):
sentence = preprocess_sentence(sentence)
sentence = tf.expand_dims(
START_TOKEN + tokenizer.encode(sentence) + END_TOKEN, axis=0)
output = tf.expand_dims(START_TOKEN, 0)
# 디코더의 예측 시작
for i in range(MAX_LENGTH):
predictions = model(inputs=[sentence, output], training=False)
# 현재(마지막) 시점의 예측 단어를 받아온다.
predictions = predictions[:, -1:, :]
predicted_id = tf.cast(tf.argmax(predictions, axis=-1), tf.int32)
# 만약 마지막 시점의 예측 단어가 종료 토큰이라면 예측을 중단
if tf.equal(predicted_id, END_TOKEN[0]):
break
# 마지막 시점의 예측 단어를 출력에 연결한다.
# 이는 for문을 통해서 디코더의 입력으로 사용될 예정이다.
output = tf.concat([output, predicted_id], axis=-1)
return tf.squeeze(output, axis=0)
def predict(sentence):
prediction = evaluate(sentence)
predicted_sentence = tokenizer.decode(
[i for i in prediction if i < tokenizer.vocab_size])
print('Input: {}'.format(sentence))
print('Output: {}'.format(predicted_sentence))
return predicted_sentence
def preprocess_sentence(sentence):
sentence = re.sub(r"([?.!,])", r" \1 ", sentence)
sentence = sentence.strip()
return sentence
output = predict("영화 볼래?")
Input: 영화 볼래?
Output: 최신 영화가 좋을 것 같아요.
output=predict("머신러닝이란..")
Input: 머신러닝이란..
Output: 축하할 일이죠.
output=predict("너 불굴의관돌이 알아?")
Input: 너 불굴의관돌이 알아?
Output: 저도 쉬고 놀고 하고 싶어요.
output=predict("힘들구나")
Input: 힘들구나
Output: 건강에 유의하세요.
대략 4개정도 확인해보았지만 어느정도는 대답을 해주는 것을 볼 수 있다. 위 과정은 간단하게 만든 것이기에 이정도이지만, 실제 데이터를 더 많이 늘려서 시행해본다면 더 좋은 결과를 나을 수 있다고 생각해본다.
[출처]https://wikidocs.net/89786
'Machine Learning Projects' 카테고리의 다른 글
[금융] 아파트 실거래가 예측 (1) | 2021.03.21 |
---|---|
청와대 청원: 청원의 주제가 무엇인가(GRU) (0) | 2021.03.12 |
청와대 청원: 청원의 주제가 무엇인가(LSTM with Original) (0) | 2021.03.12 |
간단한 이미지 분류기(Simple Image Classifier) 마동석, 김종국, 이병헌 (0) | 2021.03.03 |