본문 바로가기
개발/AI 프로젝트

파이썬으로 간단하게 챗봇 만들기

by beomcoder 2023. 5. 9.
728x90
반응형

우선 챗봇을 혼자 만들어보려고 만든 자료들과 논문을 읽어보았다.

하지만 컴퓨팅자원이 부족하고 일일히 대화자료를 만들어내기는 어려웠다.

그래서 찾아보았는데 비슷한 자료가 있어서 이걸 챗봇으로 구현해보았다.

 

보시기 전에 미리 말하지만 챗봇이라고 하기에는 다소 문제가 있긴 하지만,

간단하게 재미를 붙여볼 수는 있다.

우선 다 한 자료를 먼저 보고나서 끝까지 볼 사람은 봐도 된다.

 

 

 

이제 팁스 진행을 해야해서 조금더 고도화하지는 못했다.

 

토큰화, 딥러닝, 동사, 명사, 조사, 이런거 다 필요없다.

하지만 조금더 대화를 매끄럽게 하기위해서는 그정도는 해줘도 된다.

 

먼저 어렵게 찾은 API이다.

이건 무료이고 성능도 괜찮고, 한국에서 뛰어난 석,박사님들이 만든 것이다.

 

https://aiopen.etri.re.kr/guide/MRCServlet
 

AI API/DATA

JSON parsing을 위해 Gson 라이브러리를 사용하여 제공하고 있습니다. Gson 라이브러리에 대한 자세한 설명은 https://github.com/google/gson 에서 확인 하실 수 있습니다. import java.io.DataOutputStream; import java.i

aiopen.etri.re.kr

 

 

여기를 가서 API키를 발급받는다.

 

 

이렇게 하면 키를 쓸 수 있고, 나는 기계독해 API를 이용해서 챗봇을 만드려고 한다.

 

#-*- coding:utf-8 -*-
import urllib3
import json

openApiURL = "http://aiopen.etri.re.kr:8000/MRCServlet"
accessKey = "본인의 키"
passage = ''.join(open('info.txt', 'r').readlines()) # 해답지
# 예시) passage = "안녕하세요: 반갑습니다."

http = urllib3.PoolManager()

question = "질문내용"
requestJson = {"argument": { "question": question, "passage": passage }}

response = http.request(
    "POST",
    openApiURL,
    headers={"Content-Type": "application/json; charset=UTF-8","Authorization": accessKey},
    body=json.dumps(requestJson)
)

r = json.loads(str(response.data,"utf-8"))
answer = r['return_object']['MRCInfo']['answer']
print(answer)

먼저 기본적인 틀이다. accessKey에 본인이 받은 키값을 넣으면 된다.

passage에 질문에 대답할 내용들을 기입하면 된다.

그리고 question에 질문을 적어서 보내기만 하면 끝이다.

 

여기까지만 해서 코드를 API로 만들어서 통신하면 간단한 챗봇이 된다.

나는 그냥 간단하게 플라스크로 구현하여 AWS에서 돌려보았다.

 

먼저 내가 만든 text파일이다.

휴대폰을 인증하는 방법은 먼저 이용동의를 하시고, 휴대폰번호를 입력합니다. 그리고 입력한 휴대폰번호에 인증번호가 문자메시지로 오면 그 인증번호를 입력하시면 휴대폰 인증이 됩니다. [END]
모임을 만드는 방법은 모임의 이름을 입력하고, 관심사를 설정하고 개설하면 됩니다. [END]
모임을 삭제하는 방법은 모임 설정에 들어가서 모임해체하기를 누르시면 됩니다. [END]
모임이란 오프라인에서 사람들끼리 만나서 취미활동을 하는 것입니다. 한번 모임을 해보시는건 어떨까요? [END]

어플내에서 챗봇을 만들어보면 좋겠다는 취지에서 시작했으므로

어플사용에 관한 간단한 내용을 텍스트파일로 만들었다.

[END]를 붙인 이유는 question의 내용이 passage에서 제대로 검색이 안될때

전체 내용을 긁어오는 경우가 있는데 이를 방지하기 위해 END라는 답변의 끝을 설정해주었다.

 

혹시 나를 끝까지 따라해보려는 사람이 있을것 같아서 폴더는 다음과 같이 구성했다.

chatbot.py에서 아까와 같은 코드로 구성하고, main.py로 flask로 간단하게 구현했다.

그리고 html에서 처음 화면과 같이 꾸며주었다.

 

main.py
# main.py
#-*- coding:utf-8 -*-
from flask import Flask, render_template, request, redirect, url_for
import chatbot

app = Flask(__name__)
board = []
 
@app.route('/')
def index():
    return render_template('main.html', rows=board, qnas=chatbot.src)

@app.route('/question',methods=["POST"])
def question():
    if request.method == "POST":
        board.append([request.form["context"], chatbot.action(request.form["context"])])
        return redirect(url_for("index"))
    else:
        return render_template("main.html", rows=board, qnas=chatbot.src)
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True, port=8888)

 

먼저 main.py이다.

간단한 서브프로젝트여서 django나 공부하고 있는 fastapi는 사용하지 않고 flask로 구현하였다.

flask나 api, html에 대해서는 따로 설명하지는 않겠다.

 

처음에 화면을 들어가면 main.html을 불러온다.

 

 

import urllib3, json
from konlpy.tag import Okt

openApiURL = "http://aiopen.etri.re.kr:8000/MRCServlet"
accessKey = "KEY"

src = open('info.txt', 'r', encoding='utf-8').readlines()
passage = ''.join(src)
http = urllib3.PoolManager()
okt = Okt()

# def action(q):
#     requestJson = {"argument": { "question": q, "passage": passage }}
#     response = http.request("POST", openApiURL, headers={"Content-Type": "application/json; charset=UTF-8","Authorization": accessKey}, body=json.dumps(requestJson))
#     response = json.loads(str(response.data,"utf-8"))

#     print(response)

#     answer = response['return_object']['MRCInfo']['answer']
#     print('변경전:', answer)

#     if '[END]' in answer:
#         answer = answer[:answer.index('[END]')]

#     answer = passage[passage.index(answer): passage.index('[END]', passage.index(answer))]
#     print('변경후:', answer)

#     return answer

def action(q):
    q = okt.normalize(q)

    if len(okt.nouns(q)):
        # print(okt.pos(q))
        requestJson = {"argument": { "question": q, "passage": passage }}
        response = http.request("POST", openApiURL, headers={"Content-Type": "application/json; charset=UTF-8","Authorization": accessKey}, body=json.dumps(requestJson))
        response = json.loads(str(response.data,"utf-8"))

        # print(response)

        if float(response['return_object']['MRCInfo']['confidence']) > 0.10:
            answer = response['return_object']['MRCInfo']['answer']
            print('변경전:', answer)

            if '[END]' in answer:
                answer = answer[:answer.index('[END]')]

            answer = passage[passage.index(answer): passage.index('[END]', passage.index(answer))]
            print('변경후:', answer)

            return answer
        
    
    # print(q)
    # end_word = [word for word in q if '가'<=word<='힣'][-1]
    # josa = '이' if (ord(end_word)-ord("가")) % 28 > 0 else ''

    josa = ''
    return '"' + q + '"' + josa + '라는 문장은 아직 제대로 이해하지 못했습니다. 이 사항은 챗봇 응답에 추가될 예정입니다.'

 

chatbot.py 파일이다.

아까 예제 소스에서 약간만 변형했다.

시간만 더 있었으면 나도 형태소를 분석하여 조금더 질문의 양식을 일정화 시키고,

품질을 올리려 했지만 시간이 많지 않았다.

 

그래서 그냥 간단하게 일정 정확도가 넘으면 answer의 질이 가치가 있다고 판단하여

간단한 정제 후에 html에 뿌려준다.

 

나는 테스트를 해보니 passage의 내용과 똑같은 질문을 할때는 퍼센트가 높았지만 뜻이 비슷한 문장으로 질문하였을때는

13~20%의 확률이 나왔기 때문에 우선 10%만 넘어도 답할 가치가 있다고 판단하였다.

 

그리고 이제 10~20%의 정확도일때는 모델에서 찾은 답의 처음부터,

text가 끝나는 모든 내용을 담아서 정답이라고 말하기 때문에 이를 방지하기 위해

[END]기호로 정답의 끝부분을 지정해 주었다.

 

그리고 10%미만일때는 passage에 있는 내용이 아니라고 생각하고 답할수 없다는 문장을 담아주었다.

 

 

<!-- ./template/main.html -->
<!doctype html>

<html lang="ko">
    <head>
        <meta charset="UTF-8">
        <meta name="Generator" content="EditPlus®">
        <meta name="Author" content="">
        <meta name="Keywords" content="">
        <meta name="Description" content="">
        <script src="https://kit.fontawesome.com/77ad8525ff.js" crossorigin="anonymous"></script>
        <title>OE CHATBOT</title>
    
        <style type="text/css">
            * {
                padding: 0;
                margin: 0;
                box-sizing: border-box;
                
            }

            .top{
                border-radius: 10px;
                display: block;
                margin: auto;
                padding: 40px;
                width: 1000px;
                overflow: auto;
                background-color: #e2db72;
                margin-bottom: 20px;
                margin-top: 20px;

            }

            .top .title{
                text-align: center;
                margin-bottom: 5px;
            }
            
            .top .sub-title{
                text-align: center;
                margin-bottom: 5px;
            }

            a {
                text-decoration: none;
            }

            div::-webkit-scrollbar {
                display: none;
            }

            .bottom{
                border-bottom-right-radius: 10px;
                border-bottom-left-radius: 10px;

                display: block;
                margin: auto;
                padding: 40px 0;
                width: 500px;
                background-color: #A8C0D6;;
            }

            .screen{
                border-top-left-radius: 10px;
                border-top-right-radius: 10px;
                display: block;
                margin: auto;
                padding: 40px 0;
                width: 500px;
                height: 500px;
                overflow: auto;
                background-color: #A8C0D6;
            }

            .screen .chat {
                display: flex;
                align-items: flex-start;
                padding: 20px;
            }

            .screen .chat .icon {
                position: relative;
                overflow: hidden;
                width: 50px;
                height: 50px;
                border-radius: 50%;
                background-color: #eee;
            }

            .screen .chat .icon i {
                position: absolute;
                top: 10px;
                left: 50%;
                font-size: 2.5rem;
                color: #aaa;
                transform: translateX(-50%);
            }

            .screen .chat .textbox {
                position: relative;
                display: inline-block;
                max-width: calc(100% - 70px);
                padding: 10px;
                margin-top: 7px;
                font-size: 13px;
                border-radius: 10px;
            }

            .screen .chat .textbox::before {
                position: absolute;
                display: block;
                top: 0;
                font-size: 1.5rem;
            }

            .screen .ch1 .textbox {
                margin-left: 20px;
                background-color: #ffffff;
            }

            .screen .ch1 .textbox::before {
                left: -15px;
                content: "◀";
                color: #ffffff;
            }

            .screen .ch2 {
                flex-direction: row-reverse;
            }

            .screen .ch2 .textbox {
                margin-right: 20px;
                background-color: #F9EB54;
            }

            .screen .ch2 .textbox::before {
                right: -15px;
                content: "▶";
                color: #F9EB54;
            }

            .input-box {
                border: 1px solid #ccc;
                border-radius: 4px;
                padding: 10px;
                font-size: 16px;
                width: 400px;
                margin-left: 11px;
            }

            .submit-button {
                background-color: #4CAF50;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 10px 20px;
                font-size: 16px;
                cursor: pointer;
            }

            .submit-button:hover {
                background-color: #3e8e41;
            }

        </style>
    </head>
 
    <body>
        <div class="top">
            <h3 class="title">챗봇 테스트</h3>
            <h4 class="sub-title">챗봇답변리스트</h4>
            {% for qna in qnas %}
                <a>{{ loop.index }}. {{ qna }}</a><br>
            {% endfor %}
        </div>

        <div class="screen" id="screen">
            {% for row in rows %}
            <div class="chat ch2">
                <div class="icon"><i class="fa-solid fa-user"></i></div>
                <div class="textbox">{{ row[0] }}</div>
            </div>
            <div class="chat ch1">
                <div class="icon"><img src="{{ url_for('static', filename='image/beomcoder.jpg')}}" alt="TEST"></div>
                <div class="textbox">{{ row[1] }}</div>
            </div>
            {% endfor %}
        </div>
        <div class="bottom">
            <form action="/question" method="POST">
                <input type="text" class="input-box" name="context">
                <input type="submit" class="submit-button"value="전송" /><br>
            </form> 
        </div>
        <script>
            var div = document.getElementById("screen");
            div.scrollTop = div.scrollHeight;
        </script>
    </body>
</html>

main.html이다. 간단한 테스트용이라 원페이지로 구성하였다.

내가 html은 찍먹정도만 할줄 알아서 따로 css파일을 만들기에는 귀찮았다.

flask를 쓸때에는 templates에 html들이 위치해야한다. 이건만 지키면 다른 파일은 이름을 변경해도 된다.

 

https://rgy0409.tistory.com/4854
 

HTML + CSS 카카오톡 채팅창 화면 만들기 2장 - 대화창 (말풍선)

지난 시간에 이어 오늘도 연이어서 HTML + CSS로 카카오톡 채팅 대화창을 만들어 보겠습니다. 이전 강의에서는 프로필 아이콘을 만드는 방법에 대해 알아보았습니다. 이번에는 프사 바로 옆에 표

rgy0409.tistory.com

 

위 사이트에서 간단하게 카카오톡스러운 이미지를 구현하고 있어 위 소스를 참조하여 만들었다.

위 소스에서 말풍선과 프로필이미지만 가져오고 나머지는 직접 제작하였다.

 

입력창, 휴대폰처럼 보이게하고, 스크롤표시를 없애고 하였으니 알아서 더 만들고 싶은 사람이 있으면더 만들어도 상관없다.

 

시간이 된다면 형태소 분석도 하고, 간단한 qna데이터를 학습시켜서 sub model로 두고

api와 함께 사용하여 양질의 chatbot을 만들고 싶었다.

 

잠시 멈춰두고 다음에 다시 만들어보아야겠다.

 

 

 

 

728x90
반응형

댓글