시작하며
이전 글 카카오미니의 명령어 분류 방법에서 카카오미니가 음성을 인식하고 봇과 인텐트를 분류해 슬롯을 추출한 후 요청 동작을 수행하는 전 과정을 간단히 살펴보았습니다. 이번에는 이전 글에 소개된 명령어 분류 2, 3단계인 봇 분류, 인텐트 분류에 이은 카카오미니의 특별한 슬롯 태깅 기술이 들어간 4단계, 즉 슬롯 태깅에 대해 설명하고자 합니다.
카카오미니를 통해 발화를 인식하고 명령을 수행하기 위해서는 특정 봇, 특정 인텐트로 분류된 이후 슬롯 태깅이라는 단계가 필요합니다. 예를 들어 “아이유의 좋은 날 들려줘”라는 발화가 ‘music’ 봇의 'playSong' 인텐트로 분류되었다면, 음악을 틀어달라는 의도라는 것은 알 수 있겠죠. 하지만 구체적으로 어떤 가수의 어떤 노래를 재생해야 하는가는 봇과 인텐트 정보만으로는 알 수 없습니다. 이때 필요한 것이 바로 슬롯 태깅입니다.
utterance | 아이유의 좋은 날 들려줘 |
bot | music |
intent | playSong |
slot[1]: sys.person.name | 아이유 |
slot[2]: sys.music.song | 좋은 날 |
[표 1]과 같이 슬롯 태깅이 끝나면 가수 ‘아이유'의 노래 ‘좋은 날'을 틀어야 한다는 것을 알아냈으므로 실제 음악을 플레이하는 액션이 수행될 수 있습니다. 따라서, 하나의 액션은 ‘(봇 -> 인텐트 -> 슬롯)’을 통해서 최종 실행된다고 볼 수 있습니다.
action |
music: playSong(slot[1], slot[2]) |
그러면 슬롯 태깅을 위해 카카오미니에서 어떤 기술이 사용되고 있는지 구체적으로 살펴보겠습니다.
엔티티 태깅
엔티티 태깅(Entity Tagging)은 개체명인식(Named Entity Recognition)이라는 이름으로도 잘 알려져 있으며, 기 정의된(predefined) 인물명(person), 장소명(location), 기관명(organization) 등을 입력 텍스트에 태깅하는 자연어 처리의 한 단계를 의미합니다. 여기서 ‘기 정의된’ 태그 목록에 주목해야 하는데요. 일반적으로 많이 사용되는 태그 목록이 있는 경우도 있고, 해결하려는 도메인에 맞게 별도 정의해서 사용하는 경우도 있다는 의미로 이해하시면 되겠습니다.
엔티티 태깅은 품사 태깅처럼 Sequence Labeling Task 중 하나이므로, 이런 문제에 적용 가능한 알고리즘을 사용해서 모델을 개발할 수 있습니다. 잘 알려진 알고리즘에는 Conditional Random Field(CRF), Long Short-Term Memory(LSTM) 등이 있습니다. 엔티티 태깅의 최종 목적은 입력 텍스트의 모든 토큰에 기 정의된 엔티티 태그(엔티티가 아님을 의미하는 ‘O’ 포함)를 할당하는 것이라고 할 수 있습니다. 수식으로 표현하면 아래와 같습니다.
𝒳 = set of possible tokens
𝒴 = set of possible tags
x = (x[1], x[2], ..., x[n]), y = (y[1], y[2], ..., y[n]), where (x[i], y[i]) ∈ (𝒳, 𝒴), 0 <= i <= n
find the most probable tag sequence y* given x:
y* = argmax(p(y | x)) from all possible y
엔티티 태깅 알고리즘 자체에 대해 깊이 설명하진 않겠지만 이 수식으로부터 토큰의 단위와 태그 셋을 사전에 정해야 한다는 점을 알 수 있습니다. 한국어에서 토큰 단위는 일반적으로 형태소를 의미하므로 x[i]는 개별 형태소입니다. 앞서 언급했던 ‘좋은 날’/sys.music.song과 같이 연속된 형태소 묶음이 엔티티로 잡혀야 하는 경우를 고려해야 하기 때문에, y[i]는 ‘BIO 태그’ 등을 사용해서 확장합니다.
좋다/B-sys.music.song 은/I-sys.music.song 날/I-sys.music.song
만약 태그가 10개라면 ‘B’, ‘I’ 태그를 고려해서 태그 셋의 크기는 20개가 될 것이고 여기에 ‘Out of Tag’를 의미하는 ‘O’ 태그를 포함시키면 최종 태그 셋의 크기는 21개라고 할 수 있습니다. 입력 x의 길이가 n이고 태그 셋의 크기가 m일 때, 모든 가능한 y의 가짓수는 아래와 같이 계산될 수 있습니다.
m^n : number of possible y
n : |x|, sequence length
m : |𝒴|, number of possible tags
이런 가짓수는 [그림 1]과 같은 격자 구조(lattice)를 이용해서 표현할 수도 있습니다.
격자 구조에서 모든 경로(path)는 해당하는 엔티티 태깅 결과로 대응될 수 있으며, 모든 가능한 경로 중에서 가장 확률 값이 큰 경로가 바로 최적의 태깅 결과라고 볼 수 있습니다. 이런 최적의 경로는 모든 가능한 경로에 대한 확률 값을 전부 계산한 다음, 그중에서 가장 확률 값이 큰 경로를 선택하는 과정(exhaustive computation)으로 계산이 가능합니다. 하지만, 모든 경로에 대한 확률 값을 전부 계산해서 비교하는 방법은 지나치게 비효율적으로 보이죠(combinatorial explosion). 이런 종류의 문제는 동적 프로그래밍 기법(dynamic programming)인 비터비 알고리즘(viterbi algorithm)을 이용해서 해결하는 것이 일반적입니다.
슬롯 태깅과 슬롯 필링
슬롯 태깅(Slot Tagging)은 엔티티 태깅과 거의 유사한 의미로 이해되곤 하지만 의미상 미묘한 차이가 있습니다. 예를 들어, “오늘은 아이유의 좋은 날 들어 볼까”라는 발화에 대해 엔티티 태깅을 수행하면 아래와 같은 결과가 나옵니다.
utterance | 오늘은 아이유의 좋은 날 들어 볼까 |
sys.date | 오늘 |
sys.person.name | 아이유 |
sys.music.song | 좋은 날 |
여기서 ‘오늘'/sys.date는 엔티티 태깅되었지만 실제 액션에서는 필요가 없죠. 따라서, 현재 인텐트에서 필요한 슬롯 태깅 결과는 아닙니다. 슬롯 태깅은 이처럼 현재 상태에서 필요한 정보를 엔티티 태깅을 통해 발화로부터 추출하는 프로세스라고 정의할 수 있습니다. 슬롯 필링(Slot Filling)이라는 용어도 있는데, 이것은 슬롯 태깅 결과, 유저 프로필 정보, 발화 히스토리(컨텍스트) 등을 전부 참조해서 최종 액션을 수행하기 위해 필요한 정보를 채워 넣는 과정을 통칭하는 용어라고 이해할 수 있습니다. 예를 들어, “오늘 운세 알려 줘” 발화를 수행하기 위해서는 ‘띠' 슬롯이 필요하지만, 유저 프로필 정보를 통해서 추출 가능하기 때문에 슬롯 태깅 결과가 없어도 슬롯 필링이 완료될 수 있죠. “부산 날씨 어때” 발화 이후 “차로 얼마나 걸려”라는 연속 발화에서 발화 히스토리를 참조해 목적지 슬롯에 ‘부산'/sys.location를 묵시적으로 채워 넣는 과정도 일종의 슬롯 필링이라고 볼 수 있습니다. 슬롯 필링이 무엇인지 간단하게 살펴보았는데, 이 글은 광의적인 개념의 슬롯 필링보다 발화로부터 정보를 추출하는 슬롯 태깅을 집중적으로 설명하겠습니다. 또한, 앞서 엔티티 태깅과 슬롯 태깅의 차이에 대해 설명드렸으나 두 태깅의 과정이 본질적으로 동일하여 카카오미니를 이해하는 데 큰 차이가 없으므로 이하에서는 구분 없이 엔티티 태깅으로 통일하여 사용하도록 하겠습니다.
엔티티 태깅 정책
엔티티 태깅 방법을 큰 틀에서 분류하면 사전 기반 접근법, 모델 기반 접근법으로 나눌 수 있습니다. 사전 기반 접근법에서는 사전에 없는 엔티티에 대한 태깅은 수행하지 않습니다. [그림 1] 격자 구조에서 사전에 존재하는 부분 경로만 인정하고 그렇지 않으면 경로 자체를 비활성화하는 기법으로 이해할 수 있습니다. 언뜻 생각하면, 사전에 있는 경로만 가지고 계산하기 때문에 경로 중의성이 떨어질 것이라고 생각하기 쉽겠지만, 사전의 사이즈가 수백만이 넘어서고 엔티티 태그 셋의 크기가 큰 경우에는 중의성이 매우 커질 수 있어서 쉬운 문제는 아니라고 할 수 있죠. 모델 기반 접근법은 이와는 반대로 격자 구조에서 모든 가능한 태깅이 가능하도록 완화시켜 학습 데이터에 없는 엔티티에 대해서도 태깅이 가능하도록 하는 기법을 의미합니다. 사전에 없는 엔티티도 추정할 수 있다는 것은 달리 말하면 전혀 무의미한 엔티티가 추출될 수 있다는 뜻이기도 하죠. 일반적인 개체명인식 방법은 후자를 따릅니다.
카카오미니에서는 위에 언급된 두 가지 기법을 전부 사용하는 정책을 사용하고 있습니다. 우선, 대부분의 카카오미니 봇에서는 서비스 관점에서 관리되지 않는(서비스 DB에 존재하지 않는) 엔티티는 태깅할 이유가 없다는 점과 신뢰도 측면 때문에 사전 기반 접근법을 사용하고 있습니다. 여기에서 모델 기반 접근법을 사용할 수도 있지만 실험 결과 사전 기반 접근법이 좀 더 좋은 결과를 보였습니다. 하지만 ‘카카오톡 봇’, ‘메모 봇’, ‘장소 봇' 등의 경우는 모델 기반 접근법을 이용해야 합니다. 이러한 봇에서는 사전으로 특정할 수 없는 ‘나챗방', ‘내일 10시에 회의 있다고'와 같은 비정형 문자열을 태깅해야 하기 때문입니다.(아래 예시 참고)
[나챗방]/sys.group에 [내일 10시에 회의 있다고]/sys.message 톡보내줘
엔티티 태깅 모델
사전 기반 접근법이나 모델 기반 접근법에 관계없이 격자 구조에서 최적의 경로를 찾아내기 위해서는 미리 학습된 태깅 모델이 필요합니다. 그리고 태깅 모델을 만들기 위해서는 엔티티가 태깅된 말뭉치가 있어야 하죠. 카카오미니에서는 이런 말뭉치를 자체 구축하고 여러 개의 GloVe-BiLSTM-CRF 모델을 학습시켜서 적용하고 있습니다. GloVe-BiLSTM-CRF 모델 1[1]은 Eigen/C++ 구현 시 CPU에서 문장당 3~4ms 이내의 빠른 분석 속도를 보이며 micro F1수치로는 한국어 BERT 기반 모델(layer 12, hidden 768)과 비견할만한 품질이 나오는 장점이 있습니다. 사실 한국어 엔티티 태깅에서는 ELMo embedding을 사용하면 품질이 더 좋지만, 문장당 처리 속도 측면에서 사용이 어려운 편입니다.(참고: https://github.com/dsindex/ntagger [2][3])
입력 토큰 x[i]에 대해 사전 학습된 단어 임베딩(pretrained GloVe embedding), 품사 임베딩(POS embedding), 로컬 임베딩(Local embedding), 문자 임베딩(Character embedding, 필요한 경우) 등을 통합해서 embedding e[i]를 만들고, 이를 ‘multi layer BiLSTM’에 통과시켜 ‘contextual encoding’을 수행합니다. 이렇게 얻어진 h-2[i]만 사용해도 쓸만한 태깅 결과가 나오는데요. 엔티티 태그 셋의 크기가 클수록 ‘CRF decoding layer’를 추가하면 효과가 큽니다. 특히, ‘BIO 태그'를 사용할 때, 엔티티의 시작이 ‘I’가 되는 문제를 대폭 완화시켜주기 때문에 매우 유용합니다. 다만, CRF를 사용하는 경우 추가적인 계산 비용이 들어가게 되는데, F1 수치의 향상폭과 문장당 처리 속도 사이의 ‘tradeoff’는 고려할만합니다.
시스템 엔티티 사전과 엔티티 확률
앞서 사전 기반 접근법에서는 사전에 있는 엔티티에 대해서만 태깅한다고 언급했었습니다. 그렇다면 이러한 사전은 어디에서 구축할까요? 이러한 사전은 시스템 엔티티 사전이라는 용어로 불리며, 사내에 존재하는 대규모 Knowledge Graph로부터 수집되고 있습니다. 엔티티 태그(혹은 타입)는 ‘인명, 곡명, 앨범명, 국가명, 도시명, 장소명, 기관명, 영화명, TV 프로그램명, TV 채널명, 라디오 프로그램명, 라디오 채널명, 음식명, 업체명’ 등등 매우 다양하고, 특정 태그의 경우는 수백만이 넘는 규모를 자랑하고 있죠. 하지만, 양이 많다고 해서 꼭 좋은 것은 아닙니다. 이로 인한 중의성이 심해지는 문제가 있기 때문이죠. [그림 1] 격자 구조를 통해서 살펴보면 사전의 사이즈가 커질수록 모델 기반 접근법과 비슷하게 수많은 경로가 가능해진다고 이해할 수 있습니다. 따라서, 시스템 엔티티 사전을 만들 때부터 불필요한 엔티티를 컷오프(cut off)하고 정제하는 과정이 필수적입니다.
물론 Kakao i 오픈 빌더를 사용하는 개발자 입장에서는 시스템 엔티티 관리 메뉴를 통해 해당 봇에서 필요한 엔티티 태그를 제한할 수 있기 때문에, 엔티티 태깅 과정에서 발생하는 불필요한 중의성을 미리 제거할 수도 있습니다. 예를 들어, ‘날씨 봇'을 만드는데 필요 없는 ‘인명, 곡명, 앨범명' 등을 비활성화하는 식이죠.
시스템 엔티티 사전을 만들 때, 추가적으로 고려해야 할 부분은 엔티티 확률입니다. 이것은 약간 생소할 수 있는데, 일반적으로 카카오미니로 들어오는 발화의 길이는 짧기 때문에 중의성 해소를 위한 정보가 부족할 수 있습니다. 예를 들어, “아바타”라고만 발화가 들어왔을 경우에, 학습된 모델만 가지고 태깅하기에는 정보가 부족하죠. 우연히, 학습 말뭉치에 ‘아바타'가 태깅된 예제가 많은 경우가 있을 수도 있겠지만, 대부분의 경우는 그렇지 않기 때문입니다. 이런 문제를 완화하기 위해 카카오미니에서는 다음 검색 클릭 로그를 분석해서 엔티티 확률 분포를 계산하는 방법을 적용하고 있습니다. 이렇게 계산된 엔티티 확률과 엔티티 태깅 모델로부터 얻어진 정보를 결합해서 최종적으로 [그림 1] 격자 구조에서 최적의 경로를 탐색하는 일종의 하이브리드 기법을 사용하고 있습니다.
엔티티 사전 종류와 엔티티 태깅 후처리
오픈 빌더를 이용해서 봇을 만들 때, 시스템 엔티티 관리 메뉴를 통해 제공되는 시스템 엔티티 외에 해당 봇에서만 적용되는 엔티티를 추가하고 싶은 경우가 있습니다. 이를 위해서 나의 엔티티라는 기능을 제공하고 있죠. ‘나의 엔티티' 메뉴에는 개발자가 정의한 ‘엔티티 태그, 엔티티 대표어, 엔티티 동의어’ 등을 기술해서 사용 가능하며 정책상 시스템 엔티티보다 태깅 우선순위를 갖습니다. 또한, 외부 API로부터 주기적 업데이트가 필요한 ‘나의 엔티티'도 존재하기 때문에 이를 지원하는 방법도 마련하고 있습니다. 2
‘나의 엔티티'에 태그를 추가할 때, 태그의 이름이 시스템 엔티티 태그와 같은 엔티티 오버라이딩(entity overriding)의 경우는 시스템 엔티티 사전에 추가하는 것과 동일한 효과를 볼 수 있습니다. 또한, ‘나의 엔티티'는 개발자에 의해 변경된 경우 즉시 반영될 필요가 있는데, 이를 위해서 오픈 빌더에서는 엔티티 동기화 기능을 제공해서 운영 편의성을 도모하고 있습니다.
집안의 전자제품을 제어하기 위한 ‘카카오 홈' 봇에서는 유저별로 다른 엔티티를 정의하는 경우도 있습니다. 예를 들어, ‘방2’라고 지정된 엔티티가 있을 때 “아들 방 불 켜 줘", “첫째 방에 불 꺼" 등 이름을 바꿔서 명령을 내리는 경우죠. 이를 위해서 카카오미니에서는 유저 엔티티 기능을 함께 제공하고 있습니다. 클라이언트에 지정된 유저 엔티티가 실행 시간에 [그림 1] 격자 구조에 들어와서 태깅되는 방식으로 이해할 수 있습니다.
오픈 빌더에서 지원되는 시스템 엔티티 중에는 ‘날짜/시간’, ‘숫자/단위’ 같은 수치에 관련된 엔티티도 존재합니다. 그런데 예를 들어 “오늘 오후 3시에 회의 예약해 줘" 발화를 태깅하면 아래와 같이 시간 관련 엔티티가 쪼개져 태깅되는 경우가 있습니다.
[오늘]/sys.date [오후]/sys.time.period [3시]/sys.time 회의 예약해 줘
이런 경우는 모델이 태깅한 결과를 그대로 출력하기보다는 ‘오늘 오후 3시'/sys.date.time로 병합해서 태깅하는 것이 액션을 수행하는 API 입장에서 더 좋은 결과라고 볼 수 있습니다. “https://www.kakaoenterprise.com 연결해줘” 발화에서 URL 부분을 ‘sys.url’로 태깅해주는 것도 유사한 후처리 과정을 통해서 태깅되는 것입니다. Kakao i 오픈 빌더에는 이러한 엔티티 태깅 후처리(Post Entity Tagging) 기능이 적용되어 있습니다.
마치며
지금까지 카카오미니에서 어떤 방법과 정책으로 슬롯 태깅(혹은 엔티티 태깅)을 하고 있는지 살펴보았습니다. 일반적으로 챗봇을 개발할 때와 같이 슬롯 태깅을 위한 말뭉치를 준비하고 이를 통해서 태깅 모델을 학습/서빙하는 방법과 비교해 보면 거의 유사한 기법이 사용된다고 볼 수 있습니다. 하지만 서비스 관점에서 ‘시스템 사전’, ‘시스템 엔티티’, ‘나의 엔티티’, ‘유저 엔티티’, ‘엔티티 태깅 후처리’ 등 여러 가지 요소를 고려한다는 측면에서 차이가 있습니다.
또한, 카카오미니에서 엔티티 태깅 모듈은 봇 분류, 인텐트 분류, DA 처리 등 많은 컴포넌트에서 공통적으로 사용되는 기반 모듈인 만큼 레이턴시(latency) 제어를 위해서 문장당 처리 속도가 매우 빨라야 하는 제한 조건도 고려해야 합니다. 비용 측면에서는 GPU 장비보다 일반적인 CPU 장비를 사용하는 것이 효율적이므로 CPU에서의 빠른 인퍼런스(inference)가 가능하면서 품질은 높은 엔티티 태깅 모델을 개발해야 한다는 것도 중요한 고려사항 중 하나입니다.
카카오미니에서는 보다 빠르고 정확한 엔티티 태깅을 위해, 지난 2년 동안 지속적으로 엔티티 태깅을 개선해 왔으며 사용자는 앞으로 더 발전된 서비스를 이용할 수 있을 것입니다.
참고 문헌
[1] End-to-end Sequence Labeling via Bi-directional LSTM-CNNs-CRF. (2016)
[2] BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. (2019)
[3] Deep contextualized word representations. (2018)
- 최동현 (2018). Kakaoenterprise Tech&. 카카오미니의 명령어 분류 방법. '발화를 벡터로 표현하기' [본문으로]
- 글을 작성하는 현 시점에서는 Kakao i 오픈 빌더에서 지원하지 않는 기능입니다. [본문으로]
댓글