FAQ 챗봇 만들기 [4] - 실제 서비스 구현해보기

     

들어가기 전에

 

지난 3개의 포스팅에서 FAQ 챗봇을 만들기 위해 질문이 들어오면 그에 대해 알맞은 답을 찾아주는 모델을 만들어보았습니다. 모델만 만들다 보니 실제적으로 챗봇은 어떻게 구현되는지, 서비스를 어떻게 만드는지, 정말 챗봇을 만들 수 있긴 한 건지라는 생각이 들 수 있습니다. ㅎ_ㅎ

개발자가 머신러닝(ML)이나 딥러닝(DL)을 배울 때 가장 어려운 부분이라고 저는 생각하는데, 기본적으로 ML/DL로 모델을 만드시는 분들은 모델의 성능을 중요시하지 그 모델로 뭔가 인풋 아웃풋이 있는 서비스를 생각하지 않습니다. 하지만 개발자들은 인풋 아웃풋이 명확한 프로그램을 만드는 것을 목적으로 하죠. 따라서 개발자인 분들이 AI수업을 들으면 기대한 수업의 흐름이 원하는 것과 달라서 많이 혼돈스러워합니다. 

저도 AI 관련 교육을 여러 번 들었는데.. 항상 수업의 마지막은

 

"자 이 모델의 정확도는 90% 정도네요. 다음으로 넘어갈게요" 

"?????"

 

제가 원하는 건 이 모델을 적용해서 뭔가 서비스를 만드는 것이었는데, 모델의 성능만 측정하고 끝납니다. ㅋㅋㅋㅋ 

그래서 저는 AI개발자를 AI모델 개발자와 AI서비스 개발자로 나누어 생각하고 있습니다. 물론 둘 다 할 수 있는 사람이 바로 AI 전문가겠죠?

 

이번 시간에는 이론, 모델로만 존재했던 챗봇의 개념을 서비스로 끌어 내려와서 뭔가 작동하는 눈에 보이는 산출물을 만들어 보도록 하겠습니다.

 

이전 글

2019/10/22 - [Study/AI] - FAQ 챗봇 만들기 [1] - 모델만들기
2019/10/24 - [Study/AI] - FAQ 챗봇 만들기 [2] - 모델다듬기

2019/11/22 - [Study/AI] - FAQ 챗봇 만들기 [3] - 많은 데이터로 실험해보기

 

Github - https://github.com/tkdlek11112/faq_chatbot_example

 

유튜브 - 

 

 

 

 

채팅창 만들기

일단 챗봇을 만들기 위해 채팅을 할 수 있는 창을 만들어야 합니다. 저는 python 웹 프레임워크 중에 하나인 Django를 이용해서 만들 예정입니다. 실제로 FAQ 챗 봇을 위해 API 서버도 만들어야 하기 때문에 Django 하나로 api서버, 프론트화면, FAQ모델링 3가지를 동시에 하겠습니다. 

 

일단 기본적인 Django API 서버를 만들고 그 위에 화면과 모델을 붙여보겠습니다. Django를 이용한 API 서버는 옛날 포스팅에서 만들었던 것을 사용하겠습니다.

 

2019/10/14 - [Study/python] - dJango로 restful API 서버만들기 [1] - django 서버 생성

 

일단 가장 먼저 만들 화면은 아래 화면입니다. 

 

 

챗봇을 위한 웹 페이지

html/css를 사용해 간단한 채팅 화면을 만듭니다. 제가 웹 프론트쪽은 많이 안 해봐서 레이아웃이 상당히 어설프고, 코드 또한 어설픕니다. ㅋㅋㅋ 뭐.. 기능만 돌아가면 되겠죠?

 

//templates/addresses/chat_test.html
<!DOCTYPE html>
<html lang="en">
<script type="text/javascript" src="/static/jquery-3.2.1.min.js"></script>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
.chat_wrap {display:none;width: 350px;height: 500px;position: fixed;bottom: 30px;right: 95px;background: #a9bdce;}
.chat_content {height: 600px;width: 500px;overflow-y:scroll;padding:10px 15px;background: cornflowerblue}
.chat_input {padding:2px 5px;}
.chat_header {padding: 10px 15px;border-bottom: 1px solid #95a6b4;}
.chat_header .close_btn {border: none;background: none;float: right;}
.send_btn {;border: 1px solid #666;background: #ffeb33;height: 28px;color: #0a0a0a;}
.msg_box:after {content: '';display: block;clear:both;}
.msg_box > span {padding: 3px 5px;word-break: break-all;display: block;max-width: 300px;margin-bottom: 10px;border-radius: 4px}
.msg_box.send > span {background:#ffeb33;float: right;}
.msg_box.receive > span {background:#fff;float: left;}
</style>
<body>
<div class="chat_header">
    <span>FAQ 챗봇</span>
    <button type="button" id="close_chat_btn" class="close_btn">X</button>
</div>
<div id="divbox" class="chat_content"></div>
<form id="form" style="display: inline">
    <input type="text" name="input1" class="chat_input" id="input1" size="74" style="display: inline; width: 460px" />
    <input type="button" value="전송" id="btn_submit" class="send_btn" style="display: inline;width: 40px"  />
</form>
<script>
    $('#btn_submit').click(function () {
        send();
    });
    $('#form').on('submit', function(e){
       e.preventDefault();
       send();
    });
    $('#close_chat_btn').on('click', function(){
        $('#chat_wrap').hide().empty();
    });
    function send(){
        $('#divbox').append('<div class="msg_box send"><span>'+$('#input1').val()+'<span></div>');
        $("#divbox").scrollTop($("#divbox")[0].scrollHeight);
        console.log("serial"+$('form').serialize())
        $.ajax({
            url:  'http://127.0.0.1:8000/chat_service/', //챗봇 api url
            type: 'post',
            dataType: 'json',
            data: $('form').serialize(),
            success: function(data) {
                <!--$('#reponse').html(data.reponse);-->
                $('#divbox').append('<div class="msg_box receive"><span>'+ data.response +'<span></div>');
                $("#divbox").scrollTop($("#divbox")[0].scrollHeight);
            }
        });
        $('#input1').val('');
    }
</script>
</body>
</html>

 

사용한 프로젝트는 포스팅 상단 github주소에 업로드되어 있습니다. 간단하게 설명하면 Django로 restfulAPI를 구현하기 위한 소스 위에 챗봇을 붙이기 위한 화면과 모델이 들어가 있는 버전입니다. 위에 소스는 화면 역할을 하는 chat_test.html 소스입니다.

개발하고 지속적으로 운영할 프로그램이 아니기 때문에 css도 하나에 문서 안에 작성해줍시다. 한 가지 체크해야 할 사항은 jquery라이브러리를 사용했기 때문에 jquery를 import 해야 하는 점입니다. 만약 github에서 소스를 받으셨으면 jquery가 포함되어있지만, 스스로 코드를 작성하신 분은 jquery.js파일을 꼭 받아서 static 폴더에 넣어주세요~!

 

jquery-3.2.1.min.js

 

jquery는 소스 하단부에 있는 script를 위해 필요합니다. 채팅에서 전송 버튼을 누르거나 엔터를 누르면 send()라는 함수가 실행되고, 이 함수는 ajax로 질문에 대한 답변을 받아오는 API를 호출합니다. 여기서는 localhost/chat_service를 호출합니다. 

 

채팅을 위한 API

화면을 만들었으면 이제 질문을 받아 답변을 생성하는 API를 만듭시다. 아직 FAQ 데이터를 학습한 모델은 넣지 않았으니, 인풋이 들어오면 더미 데이터(dummy)를 리턴하는 API를 만듭니다. 이런 API의 동작들은 view.py에서 구현할 수 있습니다.

 

#django_restful/addresses/views.py

@csrf_exempt
def chat_service(request):
    if request.method == 'POST':
        input1 = request.POST['input1']
        output = dict()
        output['response'] = "이건 응답"
        return HttpResponse(json.dumps(output), status=200)
    else:
        return render(request, 'addresses/chat_test.html')

 

Django프로젝트 안에 addresses 앱에 있는 views.py를 보면 chat_service 함수를 만들었습니다. POST형식으로 콜이 오면 response에 아웃풋 메시지를 담아서 json형태로 리턴합니다. views.py에 함수를 만들고 url로 연결하기 위해서 urls.py에 chat_service를 입력합니다.

 

 

#django_restful/restfulapiserver/urls.py

from django.conf.urls import url, include
from addresses import views
from django.urls import path
from django.contrib import admin


urlpatterns = [
    ...
    path('chat_service/', views.chat_service),
    ...
]

 

 

urls.py에서 ~/chat_service를 views.chat_service에 연결시킵니다. 이제 ~/chat_service로 콜 하면 views.chat_service가 실행됩니다. 아까 위에서 만든 채팅 페이지에서 전송 버튼을 누르면 ajax를 이용해 chat_service를 호출했습니다. 정상적으로 실행되는지 테스트해봅니다.

 

 

채팅 테스트

 

FAQ 모델 넣기

 

 

이제 이전에 만들었던 모델을 잘 녹여서 대답을 하게 만들어봅시다. 한글 버전과 영어 버전을 만들었었는데, 한글 버전은 데이터가 적으니 영어 버전으로 만들어봅시다. addresses앱 안에 새로운 py파일을 만듭니다. 

 

#django_restful/addresses/faq_chatbot.py
from gensim.models import doc2vec, Doc2Vec
from gensim.models.doc2vec import TaggedDocument
import pandas as pd
from nltk.tokenize import word_tokenize
from nltk.stem import WordNetLemmatizer
from nltk.corpus import stopwords
import nltk

# 파일로부터 모델을 읽는다. 없으면 생성한다.
try:
    d2v_faqs = Doc2Vec.load('d2v_faqs_size200_min5_epoch20_jokes.model')
    lemmatizer = WordNetLemmatizer()
    stop_words = stopwords.words('english')
    faqs = pd.read_csv('jokes.csv')
except:
    faqs = pd.read_csv('jokes.csv')
    nltk.download('punkt')
    # 토근화
    tokened_questions = [word_tokenize(question.lower()) for question in faqs['Question']]
    lemmatizer = WordNetLemmatizer()
    nltk.download('wordnet')
    # lemmatization
    lemmed_questions = [[lemmatizer.lemmatize(word) for word in doc] for doc in tokened_questions]
    nltk.download('stopwords')
    # stopword 제거 불용어 제거하기
    stop_words = stopwords.words('english')
    questions = [[w for w in doc if not w in stop_words] for doc in lemmed_questions]
    # 리스트에서 각 문장부분 토큰화
    index_questions = []
    for i in range(len(faqs)):
        index_questions.append([questions[i], i ])

    # Doc2Vec에서 사용하는 태그문서형으로 변경
    tagged_questions = [TaggedDocument(d, [int(c)]) for d, c in index_questions]
    # make model
    import multiprocessing
    cores = multiprocessing.cpu_count()
    d2v_faqs = doc2vec.Doc2Vec(
                                    vector_size=200,
                                    hs=1,
                                    negative=0,
                                    dm=0,
                                    dbow_words=1,
                                    min_count=5,
                                    workers=cores,
                                    seed=0,
                                    epochs=20
                                    )
    d2v_faqs.build_vocab(tagged_questions)
    d2v_faqs.train(tagged_questions,
                   total_examples=d2v_faqs.corpus_count,
                   epochs=d2v_faqs.epochs)

    d2v_faqs.save('d2v_faqs_size200_min5_epoch20_jokes.model')

# FAQ 답변
def faq_answer(input):
    # 테스트하는 문장도 같은 전처리를 해준다.
    tokened_test_string = word_tokenize(input)
    lemmed_test_string = [lemmatizer.lemmatize(word) for word in tokened_test_string]
    test_string = [w for w in lemmed_test_string if not w in stop_words]

    topn = 5
    test_vector = d2v_faqs.infer_vector(test_string)
    result = d2v_faqs.docvecs.most_similar([test_vector], topn=topn)
    print(result)

    for i in range(topn):
        print("{}위. {}, {} {} {}".format(i + 1, result[i][1], result[i][0], faqs['Question'][result[i][0]], faqs['Answer'][result[i][0]]))

    return faqs['Answer'][result[0][0]]


faq_answer("What do you call a person who is outside a door and has no arms nor legs?")

 

위 소스에서 상단에 있는 모델을 만드는 코드는 API 서버를 실행하는 시점에 호출됩니다. 무조건 호출하는 건 아니고, views.py에서 import를 써넣으면 최초 1번은 실행되게 됩니다. 채팅 웹페이지로부터 faq_chatbot.py에 있는 faq_answer를 호출하는것까지 flow를 그려보면 chat_test.html -> views.py (chat_service) -> faq_chatbot.py (faq_answer) 순서입니다. 따라서 views.py에서 faq_answer 함수를 호출하기 위해 import를 하게 되는데 django는 최초 실행시 views.py를 한번 읽기 때문에 faq_chatbot.py에 적어놓은 소스가 한번 실행되게됩니다. 

 

매번 서버를 실행할 때마다 모델을 새로 만들게 되면 서버 기동 속도가 느려지고 비효율적이기 때문에, 모델을 만들고 나서 파일로 저장하고, 만들어진 파일이 없다면 모델을 생성하도록 try/except를 사용했습니다. 다른 소스들은 이전 시간에 다루었던 소스기 때문에 보면 아실 거라고 생각됩니다.

 

추가적으로 프로젝트상에서 소스가 실행되기 때문에 파일 경로는 root입니다. jokes.csv가 있어야 할 곳과 모델이 생성되는 곳의 경로는 프로젝트의 root 폴더입니다.

 

자 이제 질문의 답을 찾아주는 함수가 만들어졌으니, 아까 더미 데이터로 리턴해주던 views.py의 함수를 바꿔봅시다.

 

@csrf_exempt
def chat_service(request):
    if request.method == 'POST':
        input1 = request.POST['input1']
        response = faq_answer(input1)
        output = dict()
        output['response'] = response
        return HttpResponse(json.dumps(output), status=200)
    else:
        return render(request, 'addresses/chat_test.html')

 

이전에는 response에 무조건 더미 응답을 내보냈는데, 이제는 faq_answer함수를 사용해 해당 질문에 알맞은 정답을 가져옵니다. faq_answer함수를 사용하기 위해 제일 상단에 from .faq_chatbot import faq_answer를 선언해야 합니다. 이제 한번 테스트를 해봅시다.

 

 

영어로 만들었던 모델에 대한 테스트를 해본다.

 

일단 처음에 hi라고 적었는데, 서버 로그상에서 확인해보니 "Why are neutralization reations illegal?"라는 질문과 가장 유사하다고 판단했네요. 음? ㅋㅋㅋ 이건 이 FAQ 챗봇의 문제인데, 없는 질문을 해도 최대한 비슷한 질문을 찾아서 답변을 하기 때문에 엉뚱한 답변이 나갈 수 있습니다. 두 번째는 엑셀에서 제가 적당한 질문을 찾아서 직접 입력해보았습니다.
"How does a ninja say hi?"라는 질문인데 해당하는 질문에 대한 답을 잘 찾아주었네요. 전혀 이해가 안 가는 농담이지만요 ^^

 

비슷하게 질문했는데도 잘 찾아줬습니다.

 

정확이 똑같지 않고 비슷하게 질문해도 같은 대답을 해주는 것을 알 수 있습니다. 어느 정도 작동은 할 수 있는 챗봇이네요.

 

 

마무리하며

이번 포스팅에서는 실제로 작동하는 챗봇 서비스를 만들어보았습니다. 지난 시간 동안 만들었던 FAQ 모델을 채팅 화면에서 동작하도록 구현하여, 실제로 질문을 했을 때 답이 오도록 구현해보았습니다. 모델의 정확도나 서비스의 품질 등은 조금 덜 신경 쓰며 전체적인 사이클을 한번 구현해 보자는 목적이었는데, 전체적인 FAQ챗봇 구현에 대한 흐름을 이해하셨으면 성공입니다.

 

여기서 좀 더 발전시킨다면 아래와 같은 방향이 있을 것 같네요.

 

1. 질문에 대한 답변 정확도 향상

실제로 모델 부분을 뜯어고치는 방향입니다. 현재는 doc2vec을 이용한 단순한 문장 임베딩으로 비교하고 있는데, 그 사이에 분류 모델을 추가하여 좀 더 정확도를 높히는 방법입니다. 텍스트를 처리할때 사용하는 RNN이나 LSTM을 적용한다면 좀더 높은 정확도를 만들 수 있습니다. 혹은 사용하는 단어 셋(corpus)를 늘리는 방법도 있습니다.

 

2. 챗봇 서비스 향상

현재는 웹페이지에서 간단하게 채팅 화면을 구현해보았는데, 실제 카카오톡이나 페이스북 등을 활용하여 챗봇에 채널을 늘리는 방법이 있습니다. 대부분의 sns가 챗봇을 만들 수 있는 api를 제공하기 때문에 여기서 만든 챗봇 API를 활용하여 다양한 채널이 붙여볼 수 있습니다.

 

 

첫 포스팅에서 얘기했듯이 이 FAQ 챗봇은 주어진 질문 답변 데이터셋을 활용해 특정 질문에 대한 답을 쉽게 찾아주는 역할을 합니다. 어떻게 보면 대화하는 것이 아니라 질문에 대한 답을 검색해주는 것이라고 생각할 수도 있지만, 서비스 관점에서 보면 질문과 질문의 대한 답변으로 진행되기 때문에 챗봇으로 볼 수 있습니다. 따라서 챗봇을 구현함에 있어 어떤 목적으로 만드는지 확실히 정해야 효율적인 챗봇 개발이 가능합니다. 정말로 일상적인 대화를 지원하는 목적에 이 FAQ 챗봇은 제 역할을 못하는 챗봇입니다. 하지만 단순히 자신이 원하는 질문에 대한 답변을 원하는 챗봇일 경우, 이 FAQ챗봇은 어느 정도 적절한 기능을 제공할 것이라 생각됩니다.

 

시간이 된다면 카카오 챗봇 API를 이용해 좀 더 확장된 챗봇을 만들어보도록 하겠습니다. 카카오 챗봇의 경우 자체적으로 NLP를 지원하기 때문에 단순 질의응답이 아닌 다양한 기능을 구현할 수 있습니다. 물론 우리가 만든 API를 활용할 수도 있고요. 혹시 궁금하신 점이나 이해가 안 되시는 점이 있다면 댓글 남겨주시면 답해드리도록 하겠습니다. :)

 

 

 

 

 

 

 

 

 

 

 

 

반응형

댓글(3)

  • 2020.06.28 15:00

    안녕하세요. tkdlek11112님 덕에 처음으로 doc2vec을 써 보고 챗봇도 만들 수 있었습니다. 감사합니다.

    만들다가 어려운 점이 있었는데요, 저는 챗봇을 오라클 클라우드 컴퓨터에 올려서 http://132.145.95.214:8000/chat_service/ 에서 테스트했습니다. 그런데 로컬호스트에서는 잘 되던 [전송] 버튼이 이상하게도 오라클 클라우드에서는 전송이 안 되어서 먹통이 됐습니다. 두 시간 정도를 헤매다가 /templates/addresses에 있는 chat_test.html과 chat_web.html에서 명시적으로 127.0.0.1이 쓰여있는 걸 보고 혹시나 싶어 0.0.0.0으로 바꿨는데 실패했고, 다시 혹시나 싶어 132.145.95.214를 넣었더니 신기하게 잘 작동됐습니다.

    클라우드 컴퓨터의 아이피가 바뀔 경우 이 부분을 수동으로 수정해야 하는 불편함이 있는데요, 이 문제는 어떻게 문제를 해결할 수 있을까요? ^^

    • 2020.06.29 15:38 신고

      안녕하세요~ 좋은질문이네요!

      IP가 계속 바뀌는 일이 발생하기 때문에 DNS(Domain Name Service)를 사용합니다. 예를들어 우리가 naver.com으로 네이버에 접속하지만 실제로 네이버 웹이 떠있는 ip주소가 있을거자나요? 우리가 naver.com을 치면 DNS 서버가 naver.com과 매핑된 ip주소를 대신 입력해 주는 방식입니다.

      DNS는 유료도 있지만 무료도 있기 때문에 쉽게 도메인을 발급받으실 수 있어요!. 나중에 ip가 바뀐다면 DNS에 가서 매핑된 ip만 변경하면 됩니다. 코드에는 발급받은 xxxx.dns.org 식으로만 등록해놓으면 되구요~

  • kungmo
    2020.07.05 22:07

    답변 감사합니다!!
    결국은 도메인을 하나 받아야 문제가 해결될 수 있겠군요 ㅎㅎ
    덕분에 많이 배웠습니다~ 더운 날씨에 건강 조심하셔요~~

Designed by JB FACTORY