상세 컨텐츠

본문 제목

Kiwi로 한국어 문장 분리하기

프로그래밍/NLP

by ∫2tdt=t²+c 2021. 12. 23. 02:05

본문

우리의 생각이나 감정은 문장이라는 단위를 통해 표현됩니다. 문장은 여러 개의 단어로 이루어지며 주어와 술어를 갖춤으로써 그 자체로 완결된 의미를 드러냅니다. 그래서 텍스트를 분석할 때 문장을 최소 단위로 설정하면 유용한 경우가 많죠. 맞춤법에 맞춰 쓴 문장은 항상 마침표(. ! ?)로 끝나므로 이들을 분리하는 건 굉장히 쉬운 일입니다. 그러나 문제는 인터넷 상에서 접하는 텍스트처럼 격식을 덜 갖춘 글들이죠. 사람은 글을 읽어보고 어디서 문장이 끝나는지를 쉽게 알 수 있지만, 컴퓨터에게는 매우 난감한 문제입니다. 이번 포스팅에서는 Kiwi에 문장 분리 기능이 추가된 기념으로 문장 분리라는 과제에 대해 살펴보고, 현존하는 도구들의 정확도를 평가해보는 시간을 가지도록 하겠습니다.

문제 상황

전 애초에 한숨봇 플로우를 별로 안좋아함 자신의 전문 분야에서 비전문가를 깔보고 무시하는 등 여기는 트위터리안 감성의 절정 이과한숨봇까지는 모두가 아는 상식정도를 올렸으니까 그렇다 쳐 예체능도 좀 갈수록 산으로 가는듯 얘들아 친구 좀 사겨봐

트위터에서 수집한 임의의 트윗 중 하나입니다. 마침표나 줄바꿈 등이 전혀 없이 여러 문장이 연속해서 이어지고 있습니다. 모국어 화자의 능력을 총동원해서 이를 문장 단위로 나눠보자면 다음과 같이 될 것 같네요.

1. 전 애초에 한숨봇 플로우를 별로 안좋아함.
2. 자신의 전문 분야에서 비전문가를 깔보고 무시하는 등 여기는 트위터리안 감성의 절정(이다).
3. 이과한숨봇까지는 모두가 아는 상식정도를 올렸으니까 그렇다 쳐(도) 예체능도 좀 갈수록 산으로 가는듯(하다).
4. 얘들아 친구 좀 사겨봐!

원래 한국어 문장의 경우 항상 서술어 + 종결어미의 결합으로 문장이 끝나게 되어 있으며, 종결 어미에는 -다, -아/어(요), -라, -지 등이 있습니다만, 구어나 인터넷 글 등에서는 명사형 어미(-함, -하기), 관형사형 어미(-한다는)로 문장을 마치거나, 서술어를 생략하거나 별도의 서술어 없이 문장을 마치기도 하며, 심지어 새로운 어미를 만들어서 붙이기도 합니다.

그리고 문제를 더욱 어렵게 하는 것은 애초에 문장 단위로 분리가 불가능한 문장 구조도 있다는 거죠. 인용된 문장을 포함하는 문장이 대표적인 사례입니다.

옆 자리의 직원이 "아직도 3시밖에 안 됐네. 빨리 집에 가고싶다." 라고 말했다.

이 경우는 문장이 계층적 구조를 가지므로 문장을 선형적으로 분리할 수 없고 다음과 같이 구조를 살려서 분리해야 합니다.

1. 옆 자리의 직원이 ~~ 라고 말했다.
  1-1. 아직도 3시밖에 안 됐네.
  1-2. 빨리 집에 가고싶다.

제가 알기로는 현재 이렇게 계층 구조를 살려서 문장 분리를 수행할 수 있는 오픈소스 한국어 NLP 도구는 없는 듯합니다. (앞으로 도전해볼만한 과제가 아닐까 싶네요ㅎㅎ 물론 아주 적은 돈과 아주 많은 명예를 얻을 수 있을겁니다.) 따라서 이번 포스팅에서도 인용 문장을 처리하는 문제는 뒤로 빼두고, 위와 같이 선형적인 분리만을 고려 대상으로 한정하도록 하겠습니다.

(2022-01-07 추가)
KSS 개발자님의 코멘트에 따르면, 'use_quotes_brackets_processing' 옵션을 사용하여 인용문 분절 여부를 설정할 수 있다고 합니다. 인용 문장을 따낸 다음 다시 한번 문장 분리를 돌리는 식으로 계층 구조를 살려 분석하는 것도 가능해보이네요.
(2022-10-31 추가)
최근 업데이트된 Kiwi 0.14.0버전부터는 인용문 등으로 감싸진 문장을 제대로 분리해내도록 모델이 개선됐습니다. 그래서 위의 예문의 경우 <옆 자리의 직원이 "아직도 3시밖에 안 됐네. 빨리 집에 가고싶다." 라고 말했다.>를 한 문장으로 분석하고, 이 문장의 하위 문장으로 <아직도 3시밖에 안 됐네.>와 <빨리 집에 가고싶다.>를 추출해주는 게 가능해졌습니다. 이 기능에 대한 자세한 설명은 kiwipiepy 공식 문서를 참조해주세요.

 

한국어 문장 분리에 사용할 수 있는 도구들

Kiwi의 문장 분리 기능을 소개하기에 앞서 비슷한 도구들에 어떤 것이 있는지 간략하게 소개하고 넘어가도록 하겠습니다. 

KSS

 

패턴 기반의 문장 분리기입니다. 2.x버전대에서는 별도의 형태소 분석 없이 온전히 규칙 기반으로만 작동했으나, 최근에 3.x버전대로 넘어오면서 pynori 혹은 mecab 형태소 분석 결과를 기반으로 문장을 분리하는 기능이 추가되었습니다. 최신 버전에서는 Python 및 Java를 지원합니다.

Okt(Open Korean Text)

Scala와 Java로 작성된 한국어 처리 도구이며 형태소 분석을 비롯한 여러가지 기능을 제공합니다. 

한나눔

KAIST Semantic Web Research Center에서 개발한 형태소 분석기이며 HMM을 기반으로 하고 있다고 알려져 있습니다. Java로 작성되어 있습니다.

KoalaNLP

다양한 한국어 형태소 및 구문 분석기를 쉽게 사용할 수 있도록 모은 패키지입니다. 따라서 대부분의 기능은 다른 외부 분석기에 의존하지만, 형태소 분석 결과를 이용해 문장을 분리하는 기능이 추가되어 있습니다. 코드에 따르면 1. 열린 괄호나 인용부호가 없고, 2. 숫자나 외국어로 둘러싸이지 않은 문장부호([POS.SF])가 어절의 마지막에 왔을 경우를 문장의 끝으로 간주하고 분할한다고 되어있네요.

Kiwi

짜잔, 오늘의 주인공입니다! 원래는 형태소 분석 기능만 제공했지만, 최근 0.10.3버전에서부터 문장 분리기능도 제공합니다. 형태소 분석 결과 중 EF(종결어미) 및 SF(종결부호)의 등장 위치를 기반으로 후처리를 수행해 문장을 분리해줍니다.

사용법은 간단합니다. Kiwi() 객체를 생성하고 split_into_sents 메소드를 호출해주면 됩니다. 그럼 분리된 각 문장과 그 문장의 시작지점과 끝지점을 반환해줍니다!

평가 방법

자, 그럼 위에서 소개한 도구들이 얼마나 잘 작동하는지 확인해봐야겠지요? 이를 위해서 수작업으로 평가용 데이터셋을 구축해보았습니다. 평가셋은 여기에서 오픈소스로 공개하니 누구나 확인해보시고 보강하거나 오류가 있다면 고치실 수 있습니다.

  • sample(샘플): 한국어-형태소-분석기-별-문장-분리-성능비교 에서 소개된 블로그 예제 (43문장)
  • blogs(블로그): 다양한 블로그에서 수집한 텍스트 (175문장)
  • tweets(트윗): 무작위로 선택된 트윗 텍스트 (184문장)
  • v_ending(엔딩): 사투리를 비롯한 다양한 종결어미를 포함한 텍스트 (30문장)

데이터와 함께 고민해봐야할게 평가 척도입니다. 정답과 유사하게 분리했으면 더 높은 점수를 부여하고, 그렇지 않으면 낮은 점수를 부여할 수 있는 점수 계산법이 필요한데요, 이를 위해서 두 가지 척도 EM과 F1(Dice)을 사용하기로 했습니다. 

EM(Exact Matching)

시스템이 분리한 문장이 정답과 완전히 일치할 경우에만 1점을 부여하고, 그 외에는 모두 0점을 부여합니다. 전체 문장에 대해 이를 평균 낸 점수를 EM으로 정의합니다. 구체적인 예시를 들어볼까요? ABCDEFG라는 텍스트가 있고, 문장 분리 정답은 ABC/DEFG라고 합시다.

정답 시스템 예측 부여된 점수
ABC ABC 1
DEFG DEFG 1

시스템이 정확하게 ABC/DEFG로 분할한 경우 두 문장에서 모두 1점을 받아 최종 EM은 1.0이 됩니다.

정답 시스템 예측 부여된 점수
ABC ABCD 0
DEFG EFG 0

반면 ABCD/EFG로 잘못 분할한 경우 두 문장 모두 틀렸으므로 최종 EM은 0.0이 됩니다.

정답 시스템 예측 부여된 점수
ABC ABC 1
DEFG DE 0
  FG -

ABC/DE/FG로 분할한 경우는 ABC만 맞혔으므로 최종 EM은 0.5가 되는 식이죠. 매우 간단하면서도 정답을 맞혔는지를 그대로 반영하기 때문에 직관적이죠. 다만 정답을 정확히 맞힌 경우에만 점수가 부여되기 때문에 비슷하게 맞힌 경우를 고려하지 못한다는 문제가 있습니다.

F1 (Dice 유사도)

그래서 두번째 척도로 두 집합의 유사도를 계산하는데 널리 쓰이는 Dice 유사도(F1)를 함께 사용하고자 합니다. 집합 A, B가 있을때 두 집합의 Dice 유사도는 다음과 같이 계산됩니다:

$$ \frac{2|A \cap B|}{|A| + |B|} $$

A의 크기와 B의 크기를 합친 게 분모에 들어가고, 둘의 교집합의 크기에 2를 곱한게 분자에 들어갑니다. 교집합이 없을 경우 유사도는 0이 되고, 교집합이 A, B와 동일할 경우, 즉 A=B일 경우 유사도는 1이 되는 식입니다.

정답 시스템 예측 부여된 점수
ABC ABCD $(2*3) / (3+4) = 0.86$
DEFG EFG $(2*3) / (4+3) = 0.86$

위와 같은 예시에서 시스템이 ABCD/EFG로 오답을 낸 경우입니다. ABC/DEFG와 꽤 유사하므로, F1은 평균 0.86이 나옵니다.

정답 시스템 예측 부여된 점수
ABC ABCDE $(2*3) / (3+5) = 0.75$
DEFG FG $(2*2) / (4+2) = 0.67$

이번에는 시스템이 ABCDE/FG로 위보다 더 크게 틀렸습니다. 그랬더니 평균 F1은 0.71로 더 떨어진 것을 볼수 있습니다.

정답 시스템 예측 부여된 점수
ABC - $(2*0) / (3+0) = 0.00$
DEFG ABCDEFG $(2*4) / (4+7) = 0.73$

시스템이 아예 분할을 안하고 ABCDEFG를 통째로 결과로 내보낸 경우는 어떨까요? 이 경우 ABCDEFG를 정답문장 ABC와 DEFG중 어디에 매치하느냐에 따라 최종 점수가 달라집니다. ABCDEFG를 ABC와 매칭할 경우 $(2*3) / (3+7) = 0.6$이 나오고, DEFG와 매칭할 경우 $(2*4) / (4+7) = 0.73$이 나옵니다. 최대한 최종 점수가 높은 쪽(즉, 많이 겹치는 쪽)에 매칭하는 걸 원칙으로 잡도록 하겠습니다. 이 경우 평균 F1은 0.36이 됩니다. 아예 분할을 안한것치고는 꽤 높은 값이죠?

정답 시스템 예측 부여된 점수
ABC AB $(2*2) / (3+2) = 0.8$
- CD -
DEFG EFG $(2*3) / (4+3) = 0.86$

이번엔 시스템이 AB/CD/EFG와 같이 더 잘게 쪼갠 경우를 고려해봅시다. 각 조각들을 가장 매칭잘 되는 쪽에 배치하면 CD가 남는데, 얘는 버리도록 하겠습니다. 그러면 최종적으로 각 문장에서 0.8, 0.86이 나와 평균 0.83을 달성하게 됩니다. 과하게 쪼갠 것치고는 관대한 점수죠? AB, EFG가 겹치게 나온 덕분입니다.

몇 가지 예시를 통해 살펴본 결과 F1의 경우 전반적으로 후하게 점수가 나오는 걸 볼 수 있고, 특히 아예 안 쪼갠 경우보다는 과하게 쪼갠 경우 더 높은 점수를 부여한다는 것을 알 수 있습니다. 따라서 EM과 F1 두 척도를 함께 살펴봐야만 시스템의 문장 분리 정확도를 종합적으로 판단할 수 있겠습니다.

위의 두가지 척도로 평가를 수행하는 코드는 깃헙에 정리하여 올려두었습니다. 코드를 직접 돌려서 평가를 수행해보셔도 좋겠네요.

평가 결과

평가에 사용된 각 라이브러리의 버전은 다음과 같습니다. 이중 Koala(Okt)와 Koala(Hnn)은 Okt와 한나눔 자체 분석기의 문장 분리기를 사용했고, 나머지 Koala들은 Koala의 문장 분리기를 사용하였습니다.

Kiwi KSS Koala(Okt) Koala(Hnn) Koala(Kmr) Koala(Rhino) Koala(Eunjeon) Koala(Arirang) Koala(Kkma)
0.10.3 3.3.1.1 2.1.4 2.1.4 2.1.4 2.1.5 2.1.6 2.1.4 2.1.4

가장 간단한 문장 분리 방법은 마침표(. ! ?) 뒤에 공백이 있는 경우인데요, 이 경우만 분리하는 시스템을 베이스라인으로 설정했습니다. 이것보다 잘 나오면 기본은 하는거고, 그렇지 않으면 안하느니만 못한거라고 보시면 되겠습니다.

먼저 분석 결과를 보면 블로그/트윗 간의 격차가 꽤 되는걸 볼 수 있습니다. '샘플'의 경우 사실 블로그 글에서 가져온 것이므로 블로그와 거의 동일한 특성의 데이터라고 봐도 무방합니다. 어미의 경우 굉장히 난이도가 높고 또 문장의 개수도 적어서 아직은 평가 결과가 유의미한것 같진 않습니다.

전반적으로 블로그 글에 대한 문장 분리 성능은 대부분의 분석기에서 높게 나왔습니다. KSS의 경우 형태소 분석을 사용하지 않은 버전(none)이 블로그 분석에는 더 적합한 걸로 보이네요. Okt와 한나눔은 블로그와 트윗을 비슷한 정도로 잘 분석하고 있습니다. 특이할만한 점으로 꼬꼬마 분석기의 경우 블로그 데이터의 F1 점수가 매우 높게 나왔습니다. 이는 이 블로그 포스팅에서도 지적한 것과 같이 꼬꼬마 분석기가 과하게 문장 분리를 하는 경향이 있는데, F1은 과도한 분리에 관대하기 때문에 저런 결과가 나온 거라 볼 수 있습니다.

그리고 고대하던 Kiwi의 결과는 놀라웠는데요, 블로그와 트윗 모두에서 안정적인 성능을 보이고 있고, 평균에서는 기존 도구들 중 제일 높은 정확도를 달성했습니다. 사실 기존 분석기에 별도의 추가 모델 없이 EF, SF 위치를 기반으로 자른 것이라 그저그런 성능을 보일 것이라 예상하고 있었거든요. 다만 아직 블로그 분석 결과의 경우 KSS(none)에 비해 약간 모자라는 것으로 확인되어 앞으로 이 부분을 개선해나가야 하겠습니다.

실제 분리 결과를 살펴볼까요?

블로그 분리 예시
Gold Kiwi KSS(none)
꼬치 좋아하시나요?  꼬치 좋아하시나요? 꼬치 좋아하시나요?
저는 꼬치를 너무너무 좋아해요~    저는 꼬치를 너무너무 좋아해요~
오랜만에 친구랑 꼬치 먹으며 수다를 떨기 위해 미아사거리 술집 이찌방으로 꼬치 먹으러 갔어요.  저는 꼬치를 너무너무 좋아해요~ 오랜만에 친구랑 꼬치 먹으며 수다를 떨기 위해 미아사거리 술집 이찌방으로 꼬치 먹으러 갔어요. 오랜만에 친구랑 꼬치 먹으며 수다를 떨기 위해 미아사거리 술집 이찌방으로 꼬치 먹으러 갔어요.
미아 사거리 역에서 도보 5분 정도 거리에 위치한 이찌방.  미아 사거리 역에서 도보 5분 정도 거리에 위치한 이찌방. 미아 사거리 역에서 도보 5분 정도 거리에 위치한 이찌방. 평일 저녁 8시 30분쯤 방문했는데요.
평일 저녁 8시 30분쯤 방문했는데요.  평일 저녁 8시 30분쯤 방문했는데요.  
이때만 해도 널찍했던 테이블이 30분이 지나니 다 차더라고요.  이때만 해도 널찍했던 테이블이 30분이 지나니 다 차더라고요. 이때만 해도 널찍했던 테이블이 30분이 지나니 다 차더라고요.
꼬치는 2차로 주로 2차로 먹으러 다니는데 이날은 저녁을 안 먹어서 나가사키 짬뽕도 주문했어요.  꼬치는 2차로 주로 2차로 먹으러 다니는데 이날은 저녁을 안 먹어서 나가사키 짬뽕도 주문했어요. 꼬치는 2차로 주로 2차로 먹으러 다니는데 이날은 저녁을 안 먹어서 나가사키 짬뽕도 주문했어요.
그런데..... 양이.... 어마어마 하쥬... 그런데..... 그런데.....
  양이.... 양이.... 어마어마 하쥬...
  어마어마 하쥬...  
트위터 분리 예시
Gold Kiwi KSS(none)
맞아 큰일났지  맞아  
  큰일났지  
하지만 처연한 얼굴은 갓이라고요  하지만 처연한 얼굴은 갓이라고요  
가슴이 설레서 잠이 안 와 가호극대2중첩충의극대처럼 가슴이 설레서 잠이 안 와 맞아 큰일났지 하지만 처연한 얼굴은 갓이라고요 가슴이 설레서 잠이 안 와 가호극대2중첩충의극대처럼
  가호극대2중첩충의극대처럼  
허미 원스어폰어타임 언제 다보냐. 허미 원스어폰어타임 언제 다보냐. 허미 원스어폰어타임 언제 다보냐. 1년째 보고있는데 아직도 다 못봐. (사이사이 딴거 보느라★
1년째 보고있는데 아직도 다 못봐. 1년째 보고있는데 아직도 다 못봐.  
(사이사이 딴거 보느라★ (사이사이 딴거 보느라★  
당신의 분노는 이해합니다.  당신의 분노는 이해합니다.  
하지만 우리는 기계.  하지만 우리는 기계.  
모든 일에 감정을 개입하지 않고 가장 효율적인 방식을 택하는 것이 우리의 방식입니다... 모든 일에 감정을 개입하지 않고 가장 효율적인 방식을 택하는 것이 우리의 방식입니다... 당신의 분노는 이해합니다. 하지만 우리는 기계. 모든 일에 감정을 개입하지 않고 가장 효율적인 방식을 택하는 것이 우리의 방식입니다...하지만 분명, 코어가 선택한 방법은 제가 예상했던것보다 강한 방법이었군요.
하지만 분명, 코어가 선택한 방법은 제가 예상했던것보다 강한 방법이었군요.  하지만 분명, 코어가 선택한 방법은 제가 예상했던것보다 강한 방법이었군요.  
코어의 설계자로서, 당신에겐..미안합니다. 코어의 설계자로서, 당신에겐.. 코어의 설계자로서, 당신에겐..미안합니다.
  미안합니다.  

블로그 예시를 보면 Kiwi는 "~요"로 끝나는 종결어미를 제대로 못잡아서 분리에 실패하고 또 마침표에서 분할하려는 경향이 강한것을 볼수 있습니다. 사실 이는 고질적인 Kiwi의 한계인데요 구두점이 없으면 종결어미와 연결어미를 꽤 헷갈려 하는 편입니다. 추후 모델 개선시에 꼭 개선해야할 것 같아요. 반면 KSS의 경우 "~요" 등의 어미를 잘 포착하고 있습니다.

그런데 또 반대로 트위터 예시를 보면 "맞아", "큰일났지" 등은 Kiwi가 잘 분리하고 있는걸 볼 수 있습니다. KSS에서는 마침표가 있음에도 아예 분할하지 않는 문제가 보이고 있구요. 이러한 차이 때문에 KSS는 블로그에서 좀 더 강한 모습을, Kiwi는 트윗에서 더 강한 모습을 보이는듯 합니다.

문장 분리의 경우 대량으로 수행해야하는 경우가 많을텐데요, 이 때문에 분리 속도도 간과할 수 없습니다. 아래는 단일 스레드에서 한 문장을 처리하는데 소요된 평균 시간을 바탕으로 초당 몇 문장까지 분리 가능한지를 계산해본 결과입니다.

베이스라인은 별도의 분석 없이 정규식 탐색만 수행하기 때문에 제일 빠르게 나왔구요, 그 다음으로 Okt와 한나눔 분석기가 빠른 것으로 나왔습니다. Okt와 한나눔의 경우 형태소 분석 없이 자체적으로 문장 분리를 수행하기 때문에 저렇게 빠를 수 있던 것 같네요. 반면 형태소 분석을 사용하는 KSS(mecab, pynori), 기타 Koala들은 속도가 상당히 느립니다. Kiwi가 10배 이상 빠른 걸 볼 수 있죠. 게다가 위의 결과는 단일 스레드의 실행 결과일뿐이기에 멀티코어 CPU에서는 멀티스레딩을 지원하는 Kiwi가 조금 더 유리할 수 있습니다.

마무리하며

Kiwi에 간단한 후처리 규칙을 추가하여 문장 분리 기능을 구현해보았는데요, 생각보다 성능이 잘 나와서 기분이 좋네요. 다만 실제 예시를 보면 아직 갈 길이 멀다는 걸 알 수 있습니다. 모든 경우에 통용되는 문장 분리 규칙이 없기 때문에, 한 쪽 오류를 잡으려다보면 다른 쪽에 오류가 생길 수도 있구요. 그리고 현재 종결어미를 기준으로 자르는 규칙 역시 도치된 문장 등에서는 큰 한계를 보이기 때문에 이를 개선할 수 있는 방법을 고민해봐야할 것 같네요. 쉽게 보고 달려들었는데 꽤 어려운 과제입니다. 면밀하게 분석하면서 데이터와 모델을 보강해나가는 어려운 작업이 필요하겠네요... 

 

2022-03-21 추가

Kiwi 0.11.0으로 업데이트하면서 형태소 분석 모델의 정확도가 향상되었는데요, 이 덕분에 문장 분리기의 정확도도 함께 개선되었습니다. 기존에 문제가 되던 블로그 계열의 텍스트에 대해서 문장 분리 정확도가 크게 올라갔습니다. 덕분에 모든 데이터셋에 대해 현재 제공되는 오픈 소스 모델 중 가장 좋은 성능을 보이게 됐습니다.

0.11.0 버전의 문장 분리 정확도(EM)
0.11.0 버전의 문장 분리 정확도 (F1)

 

관련글 더보기

댓글 영역