논문 제목: Multi-Task Pre-Training for Plug-and-PlayTask-Oriented Dialogue System

게재 학회 / 게재 년도: ACL / 2022.05

 

이 논문은 기존의 모듈화된 dialogue system을 하나의 모델에서 end-to-end 방식으로 동작가능한 PPTOD라는 모델에 관한 논문이다. 기존의 dialogue system은 대개 4개의 모듈인 NLU, DST, DP, NLG로 나뉜 방식으로 구성된다. 논문에서는 이러한 모듈화된 방식에 순서성이 있다고 하여 cascaded 방식이라 부른다. 하지만 이 방식에는 크게 3가지 문제가 있다고 말하며 PPTOD 모델의 연구 배경을 말한다. 그 문제는 모듈화된 계단식 dialogue system 구성이 첫 번째로 이전 모듈에서 에러가 발생하면 이후 모듈에 에러가 전파된다는 것이며 두 번째로 모든 모듈에서 각 모델이 학습하기 위해 각각의 데이터셋 수집과 라벨링 과정이 필요하고 마지막으로 계단식이기에 필연적으로 Inference latency가 느려진다는 것이다.

따라서 이러한 문제점을 해결하기 위해 end-to-end 방식의 PPTOD라는 모델을 제시한다. 여기서 end-to-end란 기존의 NLU, DST, DP, NLG를 하나로 통합한 것이다. 따라서 유저의 utterance가 들어오면 한번에 1. 의도 분류 2. 슬롯 필링 3. 액션 생성 4. 대답 생성이 가능하다. 아래 PPTOD 모델을 만들기 위해 pre-training하는 예시를 살펴보자.

가장 앞에 "translate dialogue to A"는 하나의 모델에서 DST, NLU, DP, NLG 중 어떤 것인지 구분하기 위해 사용되는 일종의 prefix이다. 이 prefix는 위 그림과 같이 "belief state", "user intent", "dialogue act", "system response"가 있다. 두 번째로 DST를 위해 이전까지 유저와 봇이 주고 받은 대화와 현재 유저의 utterance를 모두 concatenation해준 다음 입력으로 넣어준다. 그렇게 되면 이제 prefix에 따라 생성되는 결과값이 4가지로 분리된다. 즉, slot filling된 결과, user intent, bot action, bot 응답이다. 간단히 정리하면 prefix와 유저 utterance가 입력으로 들어가고 4개의 결과를 모두 출력할 수 있는 구조인 것이다. 다르게 말하면 세부적으로 모듈화된 task-oriented dialogue 문제를 단일한 포맷의 text generation 문제로 바꾼 모델이다. 

이 T5 기반의 end-to-end 모델을 학습하기 위해 추가적인 pre-training과, fine-tuning 과정을 거쳤다. 데이터셋은 기존의 Dialogue System을 만들기 위해 공개된 아래 데이터셋을 조합했다.

총 80개 도메인의 230M개 가량의 유저 utterance 데이터셋을 사용했다. 우선 pre-training을 위해 사용한 세부적인 하이퍼파라미터는 learning rate: 5e-5, epoch: 10, optimizer: Adam, model: T5, max_seq_len: 1024, batch_size: 128, loss: maximum likelihood이며 구현의 용이성을 위해 허깅페이스 라이브러리를 사용했다. fine-tuning은 pre-training과 동일한 하이퍼 파라미터를 사용했다.

 

모델 학습에 사용하는 데이터셋의 형태는 $d = (z_t, x, y)$을 가진다. $d$는 데이터셋을 의미하고, $\displaystyle t \in \{NLU, DST, DP, NLG\}$이고 $z_t$는 "translate dialogue to A:" 형태의 prompt를 의미한다. $x$는 유저의 현재 발화 + 이전의 유저 발화 + 봇 응답이 전부 concatenation 된 형태이다. $y$는 생성해야할 target sequence를 의미한다. 학습에 사용한 loss 함수는 maximum likelihood를 사용했다. 

 

PPTOD 모델의 버전은 크게 3가지로 small, base, large 모델이 있다. 각각 t5-small, t5-base, t5-large에 대해 pre-training하고 fine-tuning한 결과이다. 다만 학습시킬 때 각각의 사이즈 별로 다른 configuration을 사용했다고 한다. 이에 대한 별도의 언급은 없다. t5 모델의 기본 configuration을 사용했을 것이다. 

 

PPTOD 모델을 평가하기 위해 크게 3가지 측면에서 벤치마킹을 수행했다. 1. end-to-end dialogue modeling 2. DST 3. user intent classification 측면이다. 벤치마킹을 위해 MultiWOZ 2.0과 MultiWOZ 2.1를 사용했다. 결과적으로 3가지 측면 모두 PPTOD 모델의 우수성을 이야기하고 있다. 

 

 

첨언하자면 MultiWOZ 데이터셋에 있는 Inform, Success, BLeU, Combined Score는 MultiWOZ 데이터셋에서 제시하는 평가 가이드라인이라고 한다. 또 Combined Score는 (Inform + Success) * 0.5 + BLUE로 계산된다고 한다. 위 표의 성능을 보면 PPTOD base 모델이 가장 좋다. PPTOD large 모델이 오히려 성능이 떨어지는 것은 사전 훈련 단계에서 보지 못했던 어휘에 대해 토큰을 생성하는 방법 학습할 때 능력이 떨어지는 것으로 분석한다고 말한다. 이 말이 잘 와닿진 않지만 일단은 PPTOD base 모델이 가장 좋다고 한다. 

 

이 논문의 저자들은 또 PPTOD 모델이 적은 데이터셋에서도 좋은 성능이 나는지 보기 위해서 학습 데이터셋을 1% 썼을 때, 5% 썼을 때, 10% 썼을 때, 20% 썼을 때에 대해 모델 성능을 비교했다. 참고로 표를 만들기 위해 5회 모델 학습에 대한 평균성능을 기재했다. 

1% 학습 데이터셋만으로도 다른 모델들보다 성능이 뛰어남을 보인다. 여기까지가 1. end-to-end dialogue modeling에 대한 벤치마크 평가다. 이외의 2. DST 측면의 평가와 3. user intent classification 측면의 평가도 모두 PPTOD large 모델이 우수하다는 것을 말하므로 생략한다. 

 

마지막으로 Inference latency 측면에서 PPTOD 모델(plug-and-play)이 기존의 cascaded 방식의 모델들보다 비약적으로 빨라졌다. 

 

200ms도 느리진 않지만 서비스 측면에서는 14배 빠른 end-to-end (plug-and-play) 모델이 경쟁력을 보일 수 있을 것이다. 

 

끝으로 이 논문의 핵심 컨트리뷰션은 기존의 챗봇이 NLU, DST, DP, NLG와 같이 모듈화되어 있었다면 이 모듈화된 것을 end-to-end 방식으로 바꿨다는 데 있다. 또 이를 통합함으로써 자연스럽게 모델 추론 속도가 향상되었다는 점이다. end-to-end 모델의 우수성은 이 논문의 PPTOD 모델로 증명되었다. 다만 TOD 챗봇을 위해 다양한 도메인의 데이터셋이 만들어진다면 앞으로 많은 end-to-end 연구가 이루어질 수 있을 것이라 생각한다.

multi-domain dialogue system의 기본적인 내용을 살펴보다 보니 2011년 논문까지 거슬러 올랐다. 이 논문은 multi-domain 챗봇 시스템에서 도메인을 어떻게 결정할 것이냐?는 문제에 대한 답을 한다. 이런 multi-domain 챗봇 시스템은 기존 single-domain 챗봇 시스템에서 새로운 도메인이 추가될 때 확장성이 요구되지만 이를 어떻게 충족시킬 수 있을까하는 물음으로부터 시작된 연구다. multi-domain 챗봇 시스템이 사용했던 기존의 방식은 사용자 발화에 대한 도메인을 결정하고 그 도메인에 해당하는 하위도메인 시스템이 결정되는 구조였다. 하지만 이는 한 가지 문제점이 있다. 만약 multi-turn일 경우 다음 이야기 하는 발화의 도메인이 이전에 했던 발화의 도메인과 의도치 않게 바뀔 수 있다는 것이다. 따라서 보통 이전 발화 정보에 가중치를 줘서 도메인 결정에 함께 사용했다고 한다. 하지만 이 방법 또한 한계점이 있다고 하는데 여기에 대해 언급된 바는 없었다. (유추해 보자면 가중치와는 무관하게 도메인이 추가될 때마다 새로 학습해야하는 것이 아닐까?)

 

다만 이 도메인 결정 문제를 해결하기 위해 3가지 요소를 사용했다. 단어, 화행, 이전 발화 도메인 정보다. 단어는 화자의 발화에 포함된 단어이고 화행은 용어에서 직관적인 이해가 되지 않았지만 예를 들어 만약 restaurant 도메인이면 ask_food, ask_location 등이 화행이다. 이전 발화 도메인 정보는 말 그대로 어떤 도메인인지를 말한다. 가령 이 논문에서는 아래와 같이 Weather, Resfinder, Songfinder, TVGuide라는 4가지 도메인을 사용했다. 

딥러닝 연구가 막 약동하기 시작할 때 쯤이여서 사용한 모델은 통계기반의 최대 엔트로피 모델을 사용했다. 결과적으로 이전 발화도메인 정보와 화행을 추가했을 때 아래와 같이 성능이 제일 높게 나왔다고 한다.

데이터셋이 적어서 일반화된 성능이라 보기는 어려울 것으로 보인다. 다만 이 논문을 통해 얻게 된 인사이트는 챗봇 시스템 구현에 있어서 이전 발화 도메인과 화행 정보를 고려해서 무분별한 도메인 전환이 이뤄지지 않도록 해야한다는 것이다. 아래의 대화 예제처럼 말이다.

 

또 도메인 결정 문제도 하나의 연구 주제가 될 수 있음을 알게 되었다. 최근 연구들에서는 어떻게 도메인 결정 문제를 해결하고 있을지 궁금해지는 대목이다.

 

Rasa 개요

챗봇을 구축하기 위해 사용할 수 있는 라이브러리로는 Rasa가 있고, 한글 버전으로는 Kochat이 있다. 웹 인터페이스 기반으로 코드를 사용하지 않고 쉽게 챗봇을 만들 수 있는 DialogFlow라는 클라우드 서비스가 있지만 간단한 챗봇을 만들 때만 쓸 수 있고, 디테일한 구현이 필요할 땐 Rasa와 같은 오픈소스 라이브러리를 써야한다. Rasa는 크게 Rasa NLU와 Rasa Core로 나뉜다. rasa nlu는 인텐트(intent) 분류와 엔티티(entity) 추출에 사용하는 라이브러리이다. 이를 통해 챗봇 커스터마이징이 가능하고 상상한 거의 모든 종류의 챗봇을 만들 수 있다고 한다. rasa core는 인텐트 추가와 같은 스케일업을 도와주는 라이브러리다. 이 rasa core를 통해 챗봇 응답과 챗봇 동작을 명시 가능하다. 이 명시되는 정보를 액션(Action)이라 하는데, 다르게 말하면 dialouge state에 응답하기 위해 취해야할 행동을 뜻한다. 더하여 rasa core는 과거 히스토리(대화내역)을 기반으로 다음에 이루어져야 할 액션을 예측하는 확률모델을 생성하는 역할을 한다. 

 

Rasa NLU

rasa nlu를 사용하면 학습과 추론을 매우 손쉽게 할 수 있다. 아래 예시 코드와 같이 라이브러리를 제외하면 단 9줄 가량의 코드로도 동작한다. 

from rasa_nlu.training_data import load_data
from rasa_nlu.model import Trainer
from rasa_nlu import config
from rasa_nlu.model import Interpreter

def train_horoscopebot(data_json, config_file, model_dir):
    training_data = load_data(data_json)
    trainer = Trainer(config.load(config_file))
    trainer.train(training_data)
    model_directory = trainer.persist(model_dir, fixed_model_name='horoscopebot')

def predict_intent(text):
    interpreter = Interpreter.load('./models/nlu/default/horoscopebot')
    print(interpreter.parse(text))

 

위 예시 코드는 별자리를 알려주는 태스크를 수행하는 챗봇을 구현하기 위해 사용된 것으로 사용자 입력이 들어오면 아래와 같이 추론 가능하다.

위 결과를 보면 네 개의 사전 정의한 인텐트(greeting, get_horoscope, dob_intent, subscription)중 예측한 인텐트에 대한 신뢰점수가 confidence로 계산되는 것을 확인할 수 있다. 사용자는 별자리를 알고 싶다 말했고, 학습을 진행했던 결과 사용자의 발화(utterance)는 별자리를 묻는 get_horoscope일 확률이 96.12%라는 것을 보여준다. 위와 같이 rasa nlu를 사용해 챗봇 시스템을 학습하고 추론하기 위해서는 간단한 아래 과정을 거친다.

 

1. 챗봇 구현 범위 설정

2. 데이터셋 생성 (data.json)

3. 모델 학습

4. 추론

 

네 가지지만 사실상 챗봇 구현 범위 설정과 데이터셋 생성이 대부분이다. 챗봇 구현 범위 설정이란 모종의 태스크를 수행하는 챗봇을 만든다 가정할 때 사용자가 어떤 종류의 의도를 가질 수 있을지 정하는 것이다. 가령 음식점에서 사용할 수 있는 챗봇을 만든다면 손님은, 주문하거나 결제하거나 화장실 위치를 묻거나 인사하거나 기타 의도를 가진 말을 할 것이다. 이러한 사용자 발화의 범위 설정이 챗봇 시스템 구현의 첫 단계이다. 이후엔 데이터셋 생성이다. rasa nlu를 통해 학습하기 위해서는 json 형태로 데이터를 만들어야 한다. 별자리를 알려주는 태스크를 학습하기 위해 생성한 데이터셋의 예시는 아래와 같다.

 

기본적으로 예상되는 사용자의 발화를 text에 입력하고 그에 따른 라벨이 되는 인텐트의 종류를 입력해준다. 빈 칸의 엔티티는 현재 별자리 운세를 알려주는 태스크에서는 사용되지 않는다. 하지만 간단한 설명을 하자면, 엔티티는 범주를 의미하는 것으로 가령 음식 주문 할 때 A라는 음식 2개를 가져달라고 요청할 수 있다고 가정하면 "음식 종류"라는 엔티티는 "A"라는 값을 가질 것이고 "음식 수량"이란 엔티티는 '2'라는 값을 가질 것이다. 이처럼 하나의 인텐트에는 여러 엔티티를 가질 수 있는 것이 특징이다. 

 

위와 같이 사용자 발화 별 인텐트와 엔티티를 지정하기 위해서 직접 json 포맷으로 작성해줄 수 있지만 쉽게 작성할 수 있는 웹 인터페이스가 있으면 좋을 것이다. rasa에서는 rasa-nlu-trainer라는 웹 인터페이스를 통해 데이터셋을 만드는 작업을 쉽게 할 수 있도록 한다. 일종의 annotation 도구인 것이다. rasa-nlu-trainer는 node.js 기반으로 동작하기 때문에 별도로 node.js를 설치해주어야 하고 sudo npm i -g rasa-nlu-trainer라는 명령어를 통해 설치할 수 있다. 여기서 i는 install이고 g는 global을 뜻한다. 이를 실행하게 되면 다음과 같이 웹으로 쉽게 데이터셋을 만들 수 있도록하는 인터페이스를 제공한다. 

 

 

데이터셋을 직접 만드는데 가장 시간이 많이 걸릴 것이다. 가급적이면 공개되어 있는 것을 가져올 수 있겠지만 내가 원하는 특정 태스크를 수행하기 위해서는 대부분 커스터마이징이 필요할 것이고 이에 따른 데이터는 직접 구축하는 경우가 많을 것이다. 

 

Rasa Core

rasa nlu를 통해 예측된 intent에 따라 챗봇의 응답과 동작이 이루어질 것이다. 이 때 사용하는 것이 rasa core이다. rasa core를 통해 챗봇 응답과 챗봇 동작을 명시할 수 있다. 챗봇에서 동작을 'Action'이라 한다. rasa core를 사용하면 과거의 대화내역을 기반으로 챗봇이 해야할 다음 Action을 예측하는 모델을 생성할 수 있다. 이를 위해 가장 먼저 도메인 파일이라 불리는 것을 만들어 주어야 한다. 도메인 파일이란 쉽게 말해 어떤 주제 속에서 대화가 이뤄지는지 정의하는 파일이다. 음식점에서 사용할 챗봇이면 도메인이 음식점이 되는 것과 같다. 이 도메인 파일은 크게 5가지 내용을 포함 해야한다. 1. 인텐트 2. 엔티티 3. 슬롯 4. 템플릿 5. 액션이다. 도메인 파일은 yml 확장자를 가지며 아래와 같은 형태로 정의할 수 있다.

 

 

1. 인텐트는 언급했듯 발화의 의도를 말한다.

2. 엔티티는 발화 속 캐치해야 할 핵심 키워드를 뜻하고

3. 슬롯은 키워드가 채워질 공간을 말한다. 별자리 별 운세를 알려주는 태스크에서는 월/일을 알아야 하므로 MM, DD같은 슬롯이 정의될 수 있다. 

4. 템플릿은 말 그대로 인텐트에 대한 기본 응답을 뜻한다. 여러 개의 기본 응답을 만들어 랜덤으로 사용한다면 다양하기에 사용자 경험에 있어 친근감을 줄 수 있는 요소가 될 것이다.

5. 액션은 사용자 발화에 따라 어떤 행동을 취할 수 있을지를 정의하는 것이다. 

 

이렇게 도메인 파일 내에 5개의 요소를 넣어 yml 파일로 정의했으면 다음으론 거의 끝으로 스토리 파일을 만들어 주어야 한다.

스토리 파일이란 대화가 어떤 시나리오로 흐를 수 있는지 정의한 파일이다. 파일은 마크다운 확장자로 만들어준다. 예시는 아래와 같다.

 

간단하다. 첫 번째 스토리는 챗봇이 사용자에게 인사하고 별자리가 무엇이냐고 물어본다. 이후 Capricorn(마갈궁자리)라는 응답을 받아 slot에 채워주고 현재 별자리 운세를 가져와서 보내주고 이 서비스를 구독하겠는지 묻고 끝나는 상황이다. 두 번째 스토리는 첫 번째와 달리 챗봇이 인사한 다음 사용자가 바로 별자리를 알려주고 cancer(게자리)라는 정보를 slot에 채우고 현재 별자리 운세를 가져와 사용자에게 전달하고 이 서비스를 구독하겠는지 묻고 사용자가 구독한다는(True) 발화를 캐치한 다음 구독자 목록에 추가하는 프로세스로 이뤄지는 것을 알 수 있다. 

 

사용자는 미리 정의된 템플릿에 따라 액션을 취할 수 있지만 이는 정적이다. 예컨데 운세를 가져오거나 구독자 추가와 같은 동적으로 이뤄저야 하는 것은 별도의 API를 사용해 외부 데이터베이스와 연결될 필요가 있다. 이 때는 Custom 액션이라 하여 파이썬 스크립트를 별도로 만들어 주어야 한다. 간단한 코드 예시는 다음과 같다.

 

class GetTodaysHoroscope(Action):
    def name(self):
        return "get_todays_horoscope"

    def run(self, dispatcher, tracker, domain):
        user_horoscope_sign = tracker.get_slot('horoscope_sign')
        base_url = http://horoscope-api.herokuapp.com/horoscope/{day}/{sign}
        url = base_url.format(**{'day':"today", 'sign':user_horoscope_sign})
        # http://horoscope-api.herokuapp.com/horoscope/today/capricorn
        res = requests.get(url)
        todays_horoscope = res.json()['horoscope']
        response = "Your today's horoscope:\n{}".format(todays_horoscope)

        dispatcher.utter_message(response)
        return [SlotSet("horoscope_sign", user_horoscope_sign)]

 

오늘 별자리 별 운세를 가져오기 위해 GetTodaysHoroscope라는 클래스를 만들어주고 내부에 run이라는 비즈니스 로직을 작성해줄 수 있다. run의 매개변수로 크게 dispatcher, tracker, domain이 들어가는 것을 알 수 있다. 먼저 tracker는 현재 상태 추적기로 슬롯에 접근해 값을 가져올 수 있고 이외에도 최근 사용자의 발화 정보 내역들에 접근할 수 있다. dispatcher는 별자리 정보를 기반으로 운세를 알려주는 API 호출 결과를 사용자에게 전달하는 역할을 한다. domain은 글자 그대로 이 챗봇의 도메인을 의미한다. 마지막으로 보면 SlotSet이 있는데 이는 추후 사용자 발화 정보를 히스토리로 활용하기 위해 슬롯이라는 공간에 저장하는 것이다. 

 

마지막으로 학습은 아래와 같이 간단한 로직으로 이루어진다.

""" Training a chatbot agent """
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

from rasa_core import utils
from rasa_core.agent import Agent
from rasa_core.policies.keras_policy import KerasPolicy
from rasa_core.policies.memoization import MemoizationPolicy
from rasa_core.policies.sklearn_policy import SklearnPolicy
import warnings
import ruamel
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)

if __name__ == "__main__":
    utils.configure_colored_logging(loglevel="DEBUG")
    training_data_file = "./data/stories.md"
    model_path = "./models/dialogue"
    agent = Agent("horoscope_domain.yml", policies=[MemoizationPolicy(), KerasPolicy(), SklearnPolicy()])
    training_data = agent.load_data(training_data_file)
    agent.train(
        training_data,
        augmentation_factor=50,
        epochs=500,
        batch_size=10,
        validation_split=0.2
    )
    agent.persist(model_path)

 

main을 살펴보면 1. 로깅 설정 2.스토리 파일 경로 설정 3. 모델 저장 경로 설정 4. 다음 액션을 가져오는 에이전트 세팅 5~6. 학습, 마지막으로 에이전트 오브젝트(모델)를 지정된 경로에 저장하여 재사용하는 로직으로 구성된다. 이렇게 해주면 에이전트 모델 학습이 이루어진다. rasa nlu를 통해 추론한 사용자 발화 의도를 기반으로 rasa core를 통해 학습한 에이전트로 다음 챗봇 Action을 수행할 수 있다. 

 

Reference

[1] 파이썬으로 챗봇 만들기 

+ Recent posts