Zorba blog
한국어 형태소 분석기 정리 및 비교 본문
한국어 텍스트 데이터 분석을 위해 형태소 분석기를 사용하실텐데요. 인터넷에 검색해보면 무척 다양한 한국어 형태소가 나오고는 합니다. 이번 글에서는 한국어 형태소에는 어떠한 것들이 있는지 알아보고, 형태소 분석기 간 비교를 해보고자 합니다.
형태소 분석기를 쓰는 이유는 단어를 토큰화하기 위해서 입니다. 영어의 경우에는 New York, She's 와 같이 줄임말에 대한 것을 제외하고는 띄어쓰기 만으로 토큰화를 수행해도 단어 토큰화가 잘 진행되지만, 한국어의 경우 띄어쓰기만으로는 토큰화를 하는데 부족함이 많습니다.
한국어에서 띄어쓰기를 하는 단위를 "어절" 이라고 하는데, 어절 토큰화는 한국어 NLP에서 지양되고 있습니다. 어절 토큰화와 단어 토큰화는 차이가 많기 때문입니다. 근복적인 이유는 한국어가 영어와는 다른 형태를 가지는 언어인 "교착어" 라는 데 있습니다.
※ 교착어란 조사, 어미등을 붙여서 만드는 언어를 말합니다.
한국어는 교착어라는 특성을 가지고 있기 때문에 단어 토큰화를 하기 위해서는 조사를 분리해줄 필요가 있습니다. 예를 들어 그녀(She/Her)라는 주어나 목적어가 들어간 문장이 있다고 할 때, 영어는 띄어쓰기만 하면 그대로 의미가 전달되지만 한국어의 경우 그녀에게/그녀가/그녀를/그녀와 와 같이 뒤에 붙는 조사에 따라 의미가 달라지고, 다른 단어로 인식되는 경우가 발생합니다.
따라서 한국어를 분석하기 위해서는 띄어쓰기를 통한 단어 토큰화가 아닌 형태소 분석기를 통한 단어 토큰화를 진행해주어야 합니다. (ex. 그녀에게 -> 그녀 / 에게 , 그녀가 -> 그녀 / 가, 그녀를 -> 그녀 / 를)
한국어를 토큰화하기 위해서는 형태소(Morpheme)의 개념을 먼저 집고 넘어가야 합니다.
형태소(Morpheme) : 뜻을 가진 가장 작은 말의 단위
형태소는 자립 형태소와 의존 형태소 두 가지로 나뉩니다.
- 자립 형태소 : 접사, 어미, 조사와 상관없이 자립하여 사용할 수 있는 형태소. 그 자체로 단어가 된다. 체언(명사, 대명사, 수사), 수식언(관형사, 부사), 감탄사 등이 있다.
- 의존 형태소 : 다른 형태소와 결합하여 사용되는 형태소. 접사, 어미, 조사, 어간을 말한다.
용어 | 정의 | 예시 | 띄어쓰기 |
조사 | 혼자 쓰이지 않는다. 앞말에 달라붙어 그 앞말과 다른 말의 관계를 보여주는 역할. |
은/는, 이/가, 을/를, 와/과, 에게, 까지, 부터 |
띄어쓰기 하지 않는다. |
어간 | 용어 앞에서 활용함. 단어 안에서 변하지 않는 부분 |
먹는다, 먹고, 먹어서 -> 먹- 보다, 보고, 보니 -> 보- |
띄어쓰기 하지 않는다. |
어미 | 용어 뒤어에 활용함 단어 언에서 변하는 부분 |
먹다 -> 먹는다, 먹고, 먹어서 | 띄어쓰기 하지 않는다. |
접사 | 단독으로 쓰이지 않음. 항상 다른 어근이나 단어에 붙어 새로운 단어를 구성. |
접두사 : '맨' (맨밥, 맨땅, 맨주먹) 접미사 : '거리다' (기웃거리다. 까불거리다) |
띄어쓰기 하지 않는다. |
문장 하나를 예로 들어 단어 토큰화를 진행하고, 형태소 토큰화 단위로 분해해보겠습니다.
"홍길동이 산으로 뛰었다" -> "홍길동이" "산으로" "뛰었다"
자립 형태소 : 홍길동, 산
의존 형태소 : -이, -으로, 뛰-, -었, -다
이처럼 한국어를 분석할 때는 단어 토큰화 보다는 형태소 토큰화를 진행하여야 영어에서의 단어 토큰화와 유사한 형태를 얻을 수 있습니다. 이어서 한국어 형태소 분석기에는 어떠한 것들이 있는지 알아보겠습니다.
형태소 분석기는 지도학습 기반의 분석기와 비지도학습 기반의 분석기로 나뉩니다.
1. Supervised Tokenizer
Mecab
mecab은 일본어용 형태소 분석기를 한국어를 사용할 수 있도록 수정한 것 입니다.
komoran
Java 형태소 분석기를 사용하는 사람들이라면 안써본 사람이 없을 것 같은 코모란 입니다. 충분한 속도와 형태소 분석기 퀄리티를 제공합니다. 여러 어절을 하나의 품사로 분석 가능함으로써 형태소 분석기의 적용 분야에 따라 공백이 포함된 고유명사(영화 제목, 음식점명, 노래 제목, 전문 용어 등)를 더 정확하게 분석할 수 있습니다.
okt
오픈 소스 한국어 분석기이고, 과거 트위터 형태소 분석기입니다. 트위터분석기라는 이름으로 진행되었던 프로젝트가 fork되어 진행되는 것으로 기본적으로 베이스는 트위터 분석기. 기존에 미캡이 띄어쓰기에서 가장 좋은 성능을 보여주었지만 트위터 등장 후 트위터가 가장 좋은 성능을 보여줍니다.
hannanum
KAIST SWRC에서 개발한 클래스입니다. 띄어쓰기가 없는 문장은 분석 품질이 좋지 않습니다. 정제된 언어가 사용되지 않는 문서에 대한 형태소 분석 정확도가 높지 않은 문제점입니다.
kkma
서울대학교에서 만든것으로 알려진 Kkma 형태소 분석기는 다른 형태소 분석기에 비해서 분석 성능이 뛰어나다는 것으로 유명합니다. 다만 속도가 느려서 대용량을 다룬다던지 실시간으로 처리하는 부분에서 사용되기 쉽지 않습니다. 띄어쓰기 오류에 덜 민감한 한글 형태소 분석기 입니다.
khaiii
카카오에서 공개한 오픈소스 한국어 형태소 분석기입니다. GPU 없이 형태소 분석 가능하고, 실행 속도 빠릅니다. CNN 필터가 문자들을 슬라이딩해 정보를 추출합니다.
2. Unsupervised Tokenizer
Supervised tokenizer들과 달리 말뭉치를 확인한 뒤 토크나이즈를 진행하기 때문에 토크나이즈 작용 전 모델 학습이 필요합니다.
soynlp
형태소 분석, 품사 판별 등을 지원하는 한국어 자연어 처리 패키지입니다. 하나의 문장, 문서 보다 어느 정도 규모가 있으면서 동질적인 문서 집합에서 잘 작동하며, 데이터의 통계량을 확인해 만든 단어 점수 표로 작동합니다.
- 단어 점수 - 응집 확률(Cohension Probability)과 브렌칭 엔트로피(Branching Entropy)가 높을 때 해당 문자열을 형태소로 분석.
- 응집 확률 - 주어진 문자열이 유기적으로 연결돼 함께 자주 나타나는 것
- 브렌칭 엔트로피 - 단어 앞뒤로 조사, 어미 혹은 다른 단어가 등장하는 경우
구글 SentencePiece
구글에서 공개한 형태소 분석 패키지입니다. 바이트 페어 인코딩(BPE, Byte Pair Encoding) 기법 지원 (말뭉치에서 가장 많이 등장한 문자열 병합으로 문자열 압축) 합니다.
- BPR를 활용하는 토크나이즈 메커니즘 - 원하는 어휘 집합 크기가 될 때까지 반복적으로 고빈도 문자열들을 병합, 어휘 집합에 추가(BPE 학습)
BPE 기법 예시
- aaabdaaabac -> aa가 가장 많은 형태 이므로 aa를 Z로 치환하여 압축
- aaabdaaabac -> ZabdZabac 로 압축 ->ab가 가장 많은 형태이므로 Y로 치환하여 다시 압축
- aaabdaaabac -> ZabdZabac -> ZYdZYac
출처 : https://taepseon.tistory.com/156
시중에 공개된 형태소 분석기의 성능을 비교해보겠습니다. 이번에는 지도학습 기반의 분리기 중 Konlpy에서 제공하는 Mecab, Komoran, Kkma, Hannanum, Okt를 비교해보려고 합니다. 카카오에서 제공하는 Khaiii도 테스트해보고 싶었는데, 설치 시 오류가 발생하에 불가피하게 제외하였습니다.
직접 테스트를 진행하기 전, konlpy를 제공하는 사이트에서 품사 태깅 클래스 간 비교 실험을 진행한 데이터를 업로드해두었습니다. 먼저 해당 자료를 먼저 확인해보겠습니다.
로딩시간 : Kkma>Komoran>Okt>Hannanum>(Khaiii)>Mecab
실행시간 : Kkma>Komoran>Hannanum>Okt>(Khaiii)>Mecab
(자료 조사 결과, 카카오의 Khaiii는 Mecab보다 조금 더 느린것으로 확인되었습니다. https://passerby14.tistory.com/3)
Kkma의 로딩&실행 시간이 가장 오래걸리고, Mecab이 가장 빠르다고 확인되었습니다. 만약 시간이 중요한 작업이라면 Kkma 보다는 Mecab을 사용하는 것이 맞을 것 같습니다. 시간 뿐만 아니라 형태소 분석의 성능도 중요한데요. Mecab처럼 시간이 빠르다고, 무턱대고 사용하다가는 엉터리 결과물이 나올수 있기 때문입니다.
Konlpy에서는 몇 가지 샘플 문장을 통해 성능 비교를 하였습니다.
1. 띄어쓰기가 없는 문장.
txt = '아버지가방에들어가신다'
print(mec.pos(txt, flatten=False, join=True)) #mecab
print(kom.pos(txt,flatten=False, join=True)) #komoran
print(kkm.pos(txt,flatten=False, join=True)) #kkma
print(okt.pos(txt,norm=True, stem=True, join=True)) #okt
print(hannanum.pos(txt)) #hannanum
[['아버지/NNG', '가/JKS', '방/NNG', '에/JKB', '들어가/VV', '신다/EP+EC']]
[['아버지/NNG', '가방/NNP', '에/JKB', '들어가/VV', '시/EP', 'ㄴ다/EC']]
[['아버지/NNG', '가방/NNG', '에/JKM'], ['들어가/VV', '시/EPH', 'ㄴ다/EFN']]
['아버지/Noun', '가방/Noun', '에/Josa', '들어가다/Verb']
[('아버지가방에들어가', 'N'), ('이', 'J'), ('시ㄴ다', 'E')]
문맥상 아버지가 방에 들어가신다. 가 바람직한데요. mecab만 올바르게 형태소 분리를 한 것으로 확인됩니다.
2. 문맥에 따라 의미가 달라지는 단어.
txt = '나는 밥을 먹는다.'
print(mec.pos(txt, flatten=False, join=True)) #mecab
print(kom.pos(txt,flatten=False, join=True)) #komoran
print(kkm.pos(txt,flatten=False, join=True)) #kkma
print(okt.pos(txt,norm=True, stem=True, join=True)) #okt
print(hannanum.pos(txt)) #hannanum
txt = '하늘을 나는 자동차.'
print(mec.pos(txt, flatten=False, join=True)) #mecab
print(kom.pos(txt,flatten=False, join=True)) #komoran
print(kkm.pos(txt,flatten=False, join=True)) #kkma
print(okt.pos(txt,norm=True, stem=True, join=True)) #okt
print(hannanum.pos(txt)) #hannanum
[['나/NP', '는/JX'], ['밥/NNG', '을/JKO'], ['먹/VV', '는다/EF', './SF']]
[['나/NP', '는/JX', '밥/NNG', '을/JKO', '먹/VV', '는다/EF', './SF']]
[['나/NP', '는/JX'], ['밥/NNG', '을/JKO'], ['먹/VV', '는/EPT', '다/EFN', './SF']]
['나/Noun', '는/Josa', '밥/Noun', '을/Josa', '먹다/Verb', './Punctuation']
[('나', 'N'), ('는', 'J'), ('밥', 'N'), ('을', 'J'), ('먹', 'P'), ('는다', 'E'), ('.', 'S')]
[['하늘/NNG', '을/JKO'], ['나/NP', '는/JX'], ['자동차/NNG', './SF']]
[['하늘/NNG', '을/JKO', '나/NP', '는/JX', '자동차/NNG', './SF']]
[['하늘/NNG', '을/JKO'], ['날/VV', '는/ETD'], ['자동차/NNG', './SF']]
['하늘/Noun', '을/Josa', '나/Noun', '는/Josa', '자동차/Noun', './Punctuation']
[('하늘', 'N'), ('을', 'J'), ('나', 'N'), ('는', 'J'), ('자동차', 'N'), ('.', 'S')]
두번째 문장의 예시는 "나는"을 어떻게 해석하는가에 대한 차이를 확인하는 것 입니다. N으로서의 "나" 인지, 날다 뜻을 가지는 V로서의 "나"인지 확인하고자 하는데요. kkma만 올바르게 분리한 것으로 확인됩니다.
참고링크 : https://konlpy.org/ko/latest/morph/#pos-tagging-with-konlpy
3. 특정 외국어 단어
txt = """그래서 이 단계는 무엇을 마이닝 할 것인가 무엇을 분석을 할 것인가를 결정하는 단계 라고 보면 되겠구요"""
print(mec.pos(txt, flatten=False, join=True)) #mecab
print(kom.pos(txt,flatten=False, join=True)) #komoran
print(kkm.pos(txt,flatten=False, join=True)) #kkma
print(okt.pos(txt,norm=True, stem=True, join=True)) #okt
print(hannanum.pos(txt)) #hannanum
[['그래서/MAJ'], ['이/MM'], ['단계/NNG', '는/JX'], ['무엇/NP', '을/JKO'], ['마이닝/NNP'], ['할/VV+ETM'], ['것/NNB', '인가/VCP+EC'], ['무엇/NP', '을/JKO'], ['분석/NNG', '을/JKO'], ['할/VV+ETM'], ['것/NNB', '인가/VCP+EC', '를/JKO'], ['결정/NNG', '하/XSV', '는/ETM'], ['단계/NNG'], ['라고/JKQ'], ['보/VV', '면/EC'], ['되/VV', '겠/EP', '구요/EF']]
[['그래서/MAJ', '이/MM', '단계/NNG', '는/JX', '무엇/NP', '을/JKO', '마/NNG', '이닝/NNP', '하/VV', 'ㄹ/ETM', '것/NNB', '이/VCP', 'ㄴ가/EC', '무엇/NP', '을/JKO', '분석/NNG', '을/JKO', '하/VV', 'ㄹ/ETM', '것/NNB', '인가/NNP', '를/JKO', '결정/NNG', '하/XSV', '는/ETM', '단계/NNG', '라고/NNP', '보/VV', '면/EC', '되/VV', '겠/EP', '구요/EC']]
[['그래서/MAC'], ['이/MDT'], ['단계/NNG', '는/JX'], ['무엇/NNG', '을/JKO'], ['마이닝/NNG'], ['하/VV', 'ㄹ/ETD'], ['것/NNB', '이/VCP', 'ㄴ가/ECS'], ['무엇/NNG', '을/JKO'], ['분석/NNG', '을/JKO'], ['하/VV', 'ㄹ/ETD'], ['것/NNB', '이/VCP', 'ㄴ가/ECS', '를/JX'], ['결정/NNG', '하/XSV', '는/ETD'], ['단계/NNG', '라고/JX'], ['보/VV', '면/ECE'], ['되/VV', '겠/EPT', '구요/EFN']]
['그래서/Adverb', '이/Noun', '단계/Noun', '는/Josa', '무엇/Noun', '을/Josa', '마/Noun', '이닝/Noun', '하다/Verb', '것/Noun', '인가/Josa', '무엇/Noun', '을/Josa', '분석/Noun', '을/Josa', '하다/Verb', '것/Noun', '인가/Josa', '를/Noun', '결정/Noun', '하다/Verb', '단계/Noun', '라고/Josa', '보다/Verb', '되다/Verb']
[('그래서', 'M'), ('이', 'M'), ('단계', 'N'), ('는', 'J'), ('무엇', 'N'), ('을', 'J'), ('마이닝', 'N'), ('하', 'P'), ('ㄹ', 'E'), ('것', 'N'), ('이', 'J'), ('ㄴ가', 'E'), ('무엇', 'N'), ('을', 'J'), ('분석', 'N'), ('을', 'J'), ('하', 'P'), ('ㄹ', 'E'), ('것인가', 'N'), ('를', 'J'), ('결정', 'N'), ('하', 'X'), ('는', 'E'), ('단계', 'N'), ('라', 'N'), ('이', 'J'), ('고', 'E'), ('보', 'P'), ('면', 'E'), ('되겠구요', 'N')]
데이터 마이닝에서 마이닝 이라는 단어는 하나로써 의미가 있는 단어이기 때문에 분리하면 안되는 단어입니다. komoran, okt만 마, 이닝 으로 분리를 했고, mecab, kkma, hannanum은 마이닝 자체를 명사로 올바르게 판단하였습니다.
txt = """데이터를 합법적으로 뭐 api 를 통해서 받을 수 있는지 또는 스크래핑을 해야 되는지 아니면 데이터베이스 자체를 연결을 시켜서 접속할 수 있는지 이런 부분들을 알아보는 단계 라고 보시면 되겠습니다"""
print(mec.pos(txt, flatten=False, join=True)) #mecab
print(kom.pos(txt,flatten=False, join=True)) #komoran
print(kkm.pos(txt,flatten=False, join=True)) #kkma
print(okt.pos(txt,norm=True, stem=True, join=True)) #okt
print(hannanum.pos(txt)) #hannanum
[['데이터/NNG', '를/JKO'], ['합법/NNG', '적/XSN', '으로/JKB'], ['뭐/IC'], ['api/SL'], ['를/JKO'], ['통해서/VV+EC'], ['받/VV', '을/ETM'], ['수/NNG'], ['있/VA', '는지/EC'], ['또는/MAJ'], ['스/IC', '크/IC', '래핑/NNG', '을/JKO'], ['해야/VV+EC'], ['되/VV', '는지/EC'], ['아니/VCN', '면/EC'], ['데이터베이스/NNG'], ['자체/NNG', '를/JKO'], ['연결/NNG', '을/JKO'], ['시켜서/VV+EC'], ['접속/NNG', '할/XSV+ETM'], ['수/NNG'], ['있/VA', '는지/EC'], ['이런/MM'], ['부분/NNG', '들/XSN', '을/JKO'], ['알아보/VV', '는/ETM'], ['단계/NNG'], ['라고/JKQ'], ['보/VV', '시/EP', '면/EC'], ['되/VV', '겠/EP', '습니다/EF']]
[['데이터/NNG', '를/JKO', '합법/NNG', '적/XSN', '으로/JKB', '뭐/NP', 'api/SL', '를/JKO', '통하/VV', '아서/EC', '받/VV', '을/ETM', '수/NNB', '있/VV', '는지/EC', '또는/MAJ', '스크래핑을/NA', '하/VV', '아야/EC', '되/VV', '는지/EC', '아니/VCN', '면/EC', '데이터베이스/NNP', '자체/NNG', '를/JKO', '연결/NNG', '을/JKO', '시키/VV', '어서/EC', '접속/NNG', '하/XSV', 'ㄹ/ETM', '수/NNB', '있/VV', '는지/EC', '이런/MM', '부분/NNG', '들/XSN', '을/JKO', '알아보/VV', '는/ETM', '단계/NNG', '라고/NNP', '보/VV', '시/EP', '면/EC', '되/VV', '겠/EP', '습니다/EC']]
[['데이터/NNG', '를/JKO'], ['합법적/NNG', '으로/JKM'], ['뭐/NP'], ['api/OL'], ['를/JKO'], ['통하/VV', '어서/ECD'], ['받으/VV', 'ㄹ/ETD'], ['수/NNB'], ['있/VV', '는지/ECS'], ['또는/MAG'], ['스크래핑/UN', '을/JKO'], ['하/VV', '어야/ECD'], ['되/VV', '는지/ECS'], ['아니/VV', '면/ECE'], ['데이터베이스/NNG'], ['자체/NNG', '를/JKO'], ['연결/NNG', '을/JKO'], ['시키/VV', '어서/ECD'], ['접속/NNG', '하/XSV', 'ㄹ/ETD'], ['수/NNB'], ['있/VV', '는지/ECS'], ['이런/MDT'], ['부분/NNG', '들/XSN', '을/JKO'], ['알아보/VV', '는/ETD'], ['단계/NNG', '라고/JX'], ['보시/VV', '면/ECE'], ['되/VV', '겠/EPT', '습니다/EFN']]
['데이터/Noun', '를/Josa', '합법/Noun', '적/Suffix', '으로/Josa', '뭐/Noun', 'api/Alpha', '를/Noun', '통해/Noun', '서/Josa', '받다/Verb', '수/Noun', '있다/Adjective', '또는/Adverb', '스크래핑/Noun', '을/Josa', '하다/Verb', '되다/Verb', '아니다/Adjective', '데이터베이스/Noun', '자체/Noun', '를/Josa', '연결/Noun', '을/Josa', '시키다/Verb', '접속/Noun', '하다/Verb', '수/Noun', '있다/Adjective', '이렇다/Adjective', '부분/Noun', '들/Suffix', '을/Josa', '알아보다/Verb', '단계/Noun', '라고/Josa', '보시/Noun', '면/Josa', '되다/Verb']
[('데이터', 'N'), ('를', 'J'), ('합법적', 'N'), ('으로', 'J'), ('뭐', 'N'), ('api', 'F'), ('를', 'N'), ('통하', 'P'), ('어서', 'E'), ('받', 'P'), ('을', 'E'), ('수', 'N'), ('있', 'P'), ('는지', 'E'), ('또는', 'M'), ('스크래핑', 'N'), ('을', 'J'), ('하', 'P'), ('어야', 'E'), ('되', 'P'), ('는지', 'E'), ('아니', 'P'), ('면', 'E'), ('데이터베이스', 'N'), ('자체', 'N'), ('를', 'J'), ('연결', 'N'), ('을', 'J'), ('시키', 'P'), ('어서', 'E'), ('접속', 'N'), ('하', 'X'), ('ㄹ', 'E'), ('수', 'N'), ('있', 'P'), ('는지', 'E'), ('이런', 'M'), ('부분들', 'N'), ('을', 'J'), ('알', 'P'), ('아', 'E'), ('보', 'P'), ('는', 'E'), ('단계', 'N'), ('라', 'N'), ('이', 'J'), ('고', 'E'), ('보', 'P'), ('시면', 'E'), ('되', 'N'), ('이', 'J'), ('겠습니다', 'E')]
스크랩핑이라는 단어는 kkma, okt, hannanum 에서만 올바르게 분리하였습니다. kkma의 경우 스크래핑을 Unknown으로 처리하였네요.
정리하자면 띄어쓰기에서는 mecab이 올바르게 분리를 한 것을 볼 수 있었고, 문맥상 단어의 의미를 고려할 때는 kkma만 올바르게 분리하였습니다. 특정 외국어 단어의 경우 kkma, hannanum만 올바르게 분리하였습니다.
다양한 예시를 통해 추가로 확인할 수 있습니다. 자료 서칭을 하던 중 "한국어 임베딩" 의 저자분께서 okt, kkma, komoran 비교를 해둔 게시글을 확인할 수 있었는데요. 저자의 느낌상 아래와 같은 상황에 해당 분석기를 쓰면 좋겠다는 코멘트가 달려있었습니다.
- 빠른 분석이 중요할 때 : 트위터
- 정확한 품사 정보가 필요할 때 : 꼬꼬마
- 정확성, 시간 모두 중요할 때 : 코모란
참고링크 : https://ratsgo.github.io/from%20frequency%20to%20semantics/2017/05/10/postag/