Finite State machine for chatbot

챗봇은 뜨겁다. 수많은 스타트업에서 부터 누구나 이름 들어봤을 법한 대기업들 까지 챗봇 개발에 뛰어 들고 있다. 챗봇을 만들기 위해 필요한 핵심 기능으로 머신러닝에 기반한 자언어 처리(NLU or NLP) 기술이 꼽힌다. 자언어 처리는 챗봇의 가치를 크게 높여주지만 현재 수준의 자연어 처리에서는 완벽한 챗봇을 구현하기 어렵다. 결국 현재의 챗봇은 사용자의 발화 자유도를 최대한 낮추고 서비스 제공자가 원하는 시나리오에 맞게 대화가 흘러가며 상황에 맞는 의도 분석을 하는 것이 기본이라 생각한다. 다시 말해, 시나리오의 각각의 상황에서 상황판단과 행동을 위해 머신러닝과 자연어 처리 기술이 필요하지만, 그 시나리오를 구성하는 것은 별개의 일이며 더 기본적인 일이다. 페이스북 메신저나 위챗의 다양한 템플릿들을 사용하면 자연어 처리가 없는 챗봇 또한 1차적으로 구현할 수 있기 때문이다. 이러한 시나리오를 구성하는 봇의 뼈대 기능을 유한상태기계(Finite State machine)을 통해 구현할 수 있다.

Finite State Machine

Finite state mahcine을 구성하기 위해서 다음과 같은 다섯 가지 요소들이 필요하다.

 

  • Q 는 상태의 공집합이 아닌 유한 집합이다.
  • \Sigma 는 입력 문자이다. (유한하며, 비어있지 않은 기호의 집합이다).
  • \delta 상태 전이 함수이다: \delta :Q\times \Sigma \rightarrow Q
  • s_{0} 는 초기 상태이며, Q 의 원소이다.
  • F 는 최종 상태의 집합이며, Q 의 원소이다.

 

우선 챗봇을 구성하는데 있어서 시나리오를 작성해야 한다. 시나리오의 각 단계(한번의 발화)가 하나의 상태(Q의 원소)가 될 수 있다.  \Sigma 는 사용자의 입력 발화가 된다. s_{0}와 F는 시나리오에 따라 결정되며 중요한 부분은 아니라고 생각한다. 하지만 상태 전이 함수 \delta 는 중요하다. 각각의 State에서 입력된 사용자 발화를 어떻게 처리하여 다음 응답을 할 것인지를 정하는 부분이다.

Functional Finite State Machine in Python-Django..

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2017-01-18-%e1%84%8b%e1%85%a9%e1%84%92%e1%85%ae-2-46-34

아주 간단한 형태의 챗봇을 예로 들겠다. QnA 봇을 만드는데, 초기상태는 질문을 받을 준비가 된 ‘Idle’ 상태이다. 사용자의 질문을 정확히 이해 했다면, 해당 하는 대답을 내놓고, 애매하게 이해했다면 이해한 내용이 맞는지 사용자에게 확인을 구한다(ConfirmIntent). 아예 잘못 이해했다는 생각이 들면 오류메세지를 보내고 새로운 질문을 받을 준비상태로 돌아간다. 사용자에게 확인을 구할 때 사용자가 어떤 입력을 하던 봇은 응답을 내뱉고 새로운 질문을 받을 상태(Idle)로 돌아간다. ( 확인 결과 이전 분석이 맞으면 해당하는 대답을, 틀렸으면 오류메세지를 내며, 사용자 입력이 응, 아니의 의도가 아니라면 새로운 질문으로 받아 들여 처리한다). 이를 State machine으로 나타낸 다이어그램이 위와 같다.

시나리오와 각각의 State를 정했다면, 하나의 State machine을 하나의 Class로 만들 수 있다.

class StateMachine:
    def __init__(self):
        self.states = {
            'idle': self.idle,
            'confirm_intent': self.confirm_intent
        }

하나의 클래스로 StateMachine을 정의하고, 이 클래스의 atrribute로 Q(상태의 집합, self.states)을 만드는 것이다. 이 집합은 실제 집합이 아닌 dict 자료형으로 정의한다.  key 값으로 state의 이름이 들어간다. 해당 key의 value로 들어가는 것은 \delta 이다. self.idle와 self.confirm_inent는 각각이 하나의 상태전이함수로써, 각각의 State에서 \Sigma (사용자 입력 발화)에 대한 처리와 다음 State를 정의한다. 파이썬 언어로써 이것들을 봤을 때, value로 들어간 이 값들은 class StateMachine의 클래스 메소드의 이름이다. 호출형이 아닌 함수 이름만을 가져다 value 값으로 줌으로써 파이썬의 함수형 언어적인 특성을 활용하는 것이다.

현재 진행중인 프로젝트의 상태전이 함수 중 하나는 다음과 같다. ‘idle’ state는 s_{0}에 해당한다고 볼 수 있으며, 아래 정의된 파이썬 함수는 ‘idle’ state에 대한 상태전이함수이다.

def idle(self, user_id, user_text):
    pr_intent = PredefinedRule().get_user_action(user_text)
    if pr_intent:
        nlu_response = self.set_json_response(user_text, pr_intent, 0.9)
    else:
        nlu_response = self.send_api_call(user_text)

    return self.select_answer(nlu_response, user_id)

나는 이 State에서 입력된 사용자 발화에 대해 먼저 Rule base로 작성한 간단한 엔진을 거쳐 기본 의도를 파악하게 한 후, 고난이도의 자연어처리가 필요한 경우 협력사의 자연어 처리 api를 사용하도록 하였다. 사실 이러한 State 이름에 해당하는 함수에서는 사용자 발화의 의도와 Confidence_score(자연어 처리 결과의 정확도)만을 결정한다. 이 데이터를 바탕으로 self.select_answer 함수를 호출하는데 이 함수가 실제적인 상태전이함수라고 봐도 무방하다. 이 함수에서 해당 State에서 주어진 입력에 대한 자연어 처리 결과에 상응하는 응답을 데이터베이스에서 불러오고 State를 그에 맞게 전이시키는 역할을 한다.

def select_answer(self, nlu_response, user_id):
    # Get intent and confidence score
    nlu_response['intent'] = nlu_response['nlu_result'][0]['nlu_nbest'][0]['action']
    nlu_response['confidence_score'] = nlu_response['nlu_result'][0]['nlu_nbest'][0]['confidence_score']

    # Get answer_text based on confidence_score and intent
    if nlu_response['confidence_score'] >=0.8:
        nlu_response['answer'] = Answer.objects.filter(intent=nlu_response['intent']).order_by('priority')[0].text
        state = 'idle'                  
    elif 0.6 <= nlu_response['confidence_score'] < 0.8:
        nlu_response['answer'] = Answer.objects.filter(intent=nlu_response['intent']).order_by('priority')[0].confirm_text
        state =  'confirm_intent'       
    else:
        nlu_response['answer'] = Answer.objects.filter(intent='unknown').order_by('priority')[0].text
        state = 'idle'                  

    self.redis.hset('state', user_id, state)
    return nlu_response

마지막에서 두번째 줄, self.redis.hset(‘state’, user_id, state)이 상태를 전이시키는 코드라고 할 수 있다. State는 업데이트와 참조는 빈번하지만(사용자가 한번 메세지 보낼때마다!) 차지하는 용량이 적은 데이터이기 때문에 In-memory data structure인 Redis를 활용하는 것이 효율적이다.

위와 같이 Statemachine을 설계했다면, 사용자 입력에 반응하는 것이 매우 쉬워진다. 복잡한 조건문을 통해 사용자 발화를 구분하려할 필요가 없이, Redis에서 해당 사용자의 State를 가져온 후 다음과 같이 Statemachine을 작동시킬 수 있다.

user_state = r.hget('state', user_id)
payload = StateMachine().states[user_state](user_id, user_message)

 

StateMachine 클래스의 states는 state에 따른 상태전이함수의 집합을 나타내는 dict이며, 이 dict의 키는 String인 state이다. 따라서 StateMahcine().states[user_state]를 통해 user_state를 state로 나타내는 key에 접근할수 있으며 이것의 value는 함수 포인터이다. 이 함수 포인터 뒤에 인자를 넣어주면 해당 상태전이함수를 실행시킬 수 있는 것이다.

#Chatbot #챗봇만들기 #Finitestatemachine #Redis

 

Finite State machine for chatbot”의 1개의 생각

  1. 지려따리지려따

    Liked by 1명

릉투더박 에 답글 남기기 응답 취소

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

search previous next tag category expand menu location phone mail time cart zoom edit close