노리(Nori)는 엘라스틱서치(Elasticsearch)에 플러그인으로 내장되어 있고, 루씬(Lucene)의 analysis 모듈에 포함되어 있는 한국어 형태소 분석기입니다. 저는 노리 토크나이저의 최장일치 로직을 구현하여 아파치 루씬 오픈소스에 아주 조금이나마 기여를 했고, 자바로 작성된 노리를 파이썬으로 포팅하고 몇 가지 개선 기능을 추가한 pynori를 개발했습니다1. 이 글에서는 노리를 중심으로 한국어 형태소 분석기에 대한 이야기를 합니다.


목차


1. 배경

1.1. 루씬(Lucene)

루씬(Lucene)은 빠른 텍스트 검색을 제공하는 아파치 오픈소스로 1999년에 릴리즈된 이후 지금까지 활발하게 발전하고 있다. Indexing과 Searching API가 심플하게 설계되어 단지 몇 개의 클래스만으로도 루씬의 검색 엔진 코어를 사용하여 검색 어플리케이션을 만들 수 있다.

루씬은 기본적으로 텍스트 데이터를 색인하고 검색한다. 데이터베이스에 저장된 데이터를 Inverted Index라는 자료구조를 통해 색인한다. 사용자 질의어가 들어오면 인덱스가 가능한 토큰을 만들어 검색한다. 색인과 검색을 진행하기 전에 Analyzer를 통해 문자열을 모두 토큰으로 쪼갠다.

루씬에서는 Analyzer를 통해 가장 작은 단위의 의미를 가진 토큰으로 구분해서 텍스트 의미를 이해하려 한다. 각 토큰은 검색 엔진의 핵심인 Inverted Index로 사용되므로 Analyzer의 품질은 매우 중요하다. Analyzer는 텍스트 처리의 첫 단추와 같다.

데이터를 색인할 때와 사용자 질의어를 분석할 때는 서로 다른 종류의 Analyzer를 사용하고 상황에 따라 토큰의 포지션 정보를 다르게 사용할 수 있다. 예를 들어, 색인할 때는 높은 재현율(recall)을 위해 포지션 정보를 무시하고, 쿼리 분석 시에는 높은 정밀도(precision)를 위해 포지션 정보를 고려한 AND 검색을 실시한다.

루씬의 Analyzer는 연산 순서가 있는 3개의 컴포넌트인 Character Filter, Tokenizer, Token Filter로 구성된다. 말그대로 Character Filter는 character 단위로 전처리를, Token Filter는 token 단위로 후처리를 실시한다2. 또한, Tokenizer에서는 형태소 분석과 함께 품사 태깅을 실시한다. 루씬에서는 다양한 종류의 Filter와 Tokenizer를 제공하고 있으며 언어와 테스크에 맞게 선택할 수 있다. 노리는 한국어를 위한 Analyzer이다.

1.2. 엘라스틱서치(Elasticsearch)

엘라스틱서치는 루씬을 부모로 두고 개발되었다3. 분산(distributed)형으로 확장성(scalability)이 높아 대용량 데이터에도 빠른 검색 속도를 유지할 수 있는 장점이 있다. 표준 RESTful API를 사용하여 특정 프로그래밍 언어에 종속되지 않고, 비정형 데이터를 포함한 모든 유형의 데이터를 쉽게 다룰 수 있어 개발이 편하다. 이에 Indexing과 Searching은 물론 클러스터 환경 등을 쉽게 조작할 수 있다. 또한, 엘라스틱서치와 함께 키바나 등이 포함된 Elastic Stack을 통해 검색뿐만이 아니라 분석과 시각화를 제공한다.

1.3. 형태소 분석기

전통적인 형태소 분석기는 보통 규칙 기반(rule-based) 방법론으로 설계된다. 딱딱한 규칙은 유연한 언어를 이해하기에는 많은 예외를 발생시키고, 규칙들이 서로 충돌할 경우 어떤 규칙을 우선으로 적용해야 하는지 판단하기 어렵다. 또한, 품질이 좋은 규칙을 만들고 유지보수하기 위해서는 전문가를 포함한 많은 인력이 필요하다. 사람의 손을 많이 거치는 규칙은 정교하고 수학(formal)적으로 표현하기 힘들고 추상적이기 때문에 불확실성이 크다고 볼 수 있다.

이러한 단점으로 최근 형태소 분석기를 포함한 대부분의 자연어처리 도구들은 사람의 힘이 아닌 많은 데이터의 힘을 빌려 숨은 패턴을 찾는 통계 기반(statistical-based) 방법론을 사용한다. 빅데이터와 이를 분석하는 통계 도구와 함께 언어를 정교하고 수학적으로 모델링을 하는 것을 잘 흉내낼 수 있다4.

통계 기반의 한국어 형태소 분석기는 대표적으로 MeCab-koKhaii가 있다. 이들은 모두 세종 코퍼스와 같은 대용량 학습 데이터와 함께 기계학습 모델을 사용한다. Mecab는 CRF (Conditional Random Field) 모델로 비지도(unsupervised) 학습을 하여 품사들의 연접(bigram)과 단어 비용을 학습하여 이를 토크나이징하는 기준으로 활용한다. Khaii는 CNN (Convolutional Neural Network) 모델을 통한 지도(supervised) 학습으로 형태소의 경계를 구분하고 품사를 예측한다.

Khaii는 딥러닝답게 원샷으로 분석할 수 있는 장점이 있지만 거대한 학습 모델이 매 음절마다 추론해야 하기에 상대적으로 MeCab 보다는 분석 속도가 느린 단점이 있다. 또한, MeCab는 사전 중심의 형태소 분석기이기에 Khaii에 비해 오분석이나 특정 케이스에 대해 빠른 대처가 가능하다.

MeCab는 원래 일본어를 위한 엔진이고 코퍼스로부터 bigram 모델을 학습하기 때문에 특정 언어에 종속되지 않고 사용할 수 있다. 루씬에는 MeCab 기반인 일본어 형태소 분석기인 Kuromoji가 있다. 루씬에 있는 이 일본어 분석기를 재사용하고 한국어 코퍼스인 mecab-ko-dic을 적용시킨 것이 노리이다. 추가로 한국어 특징에 맞게 압축률을 높이고 공백 패널티 정보를 활용한 점이 있다[2]. MeCab와 Kuromoji를 개발한 사람이 노리도 개발했다.

1.3.1. MeCab

MeCab 원리를 간략히 설명하면 다음과 같다. 먼저 아래의 그림[3]과 같이 입력 문장을 사전에 있는 가능한 모든 단어의 조합으로 구성한다. 이 중에서 하나의 베스트 조합(빨간선)을 찾아야 한다. 좋다는 기준을 잡기 위해서 노드(단어)와 엣지(인접한 태그쌍)의 점수가 필요한데, 학습 데이터로부터 이들의 점수를 추출할 수 있다. 학습 데이터 내에서 빈도수가 높은 단어가 점수가 높고, 빈번한 인접 태그쌍의 점수가 높다.

점수를 측정하는 기준으로 단순히 확률(probability)을 사용할 수 있고, 기계학습 모델의 학습된 파라미터(parameter)를 사용할 수도 있다. MeCab는 CRF 모델을 사용해서 점수를 측정한다[4]. 이제 점수의 총합이 가장 큰 조합을 찾으면 되는데, 일일이 모든 경우의 수를 하나씩 계산하면서 베스트 조합을 찾는 일은 매우 비효율적이다. 마지막 단어가 다르다고 다시 처음부터 계산하기 보다 이전의 과정들을 모두 저장해놓고 이를 재사용하는 계산 방법을 찾아야 한다. 이를 위해 MeCab에서는 Viterbi Algorithm을 사용한다[4].

제한적인 학습 데이터에서 문장과 같은 시퀀스 데이터를 통계적으로 정확히 모델링하는 일은 매우 어렵다. 확률 관점으로 보면 시퀀스의 모든 조합이 학습 데이터에 존재하지 않아 대부분을 스무딩(smoothing)으로 해결해야 하기에 ‘True’ 확률 분포를 이해하는 것은 더욱 힘들다.

이를 극복하기 위해서 Bayesian 특징, Markov 가정, Independence 가정 등 다양한 수학적 트릭을 사용하여 근사치(approximation)를 만든다. 이러한 수학적 트릭들은 모두 MeCab에 다 녹아들어가 있다. 이들에 대한 설명은 HMM (Hidden Markov Model)을 이해하는 것과 같다. 이에 대한 내용은 “은닉 마르코프 모델 (feat. POS 태깅)” 글을 참고하길 바란다.

1.3.2. Khaii

Khaii는 End-to-End 기반의 딥러닝 모델로 설계된 형태소 분석기이다. 다른 알고리즘과 함께 사용하여 학습 데이터를 부분적으로 활용하는 MeCab와 다르게 Khaii의 분석 결과는 전적으로 학습 데이터에 의존한다5. Khaii도 리소스(기분석 사전, 오분석 패치)를 활용하지만 그 비중은 크지 않다.

한국어 형태소 분석은 음절 기준으로 입력(ex.져/줄/래)과 출력(ex.지/어/주/ㄹ래)의 길이가 다른 문제로 딥러닝 모델 중에서 ‘다:다 매핑’을 하는 Sequence to Sequence (Seq2seq) 모델을 먼저 생각할 수 있다. 하지만, 입출력의 많은 경우가 ‘1:1 매핑’이고 ‘다:다 매핑’을 가지는 케이스는 개수가 적어 단순한 규칙으로 나열할 수 있다6. 따라서 학습 시그널을 항상 ‘다:다 매핑’을 위해 할애하는 Seq2seq 모델보다는 기본적인 Long-short Term Memory (LSTM)이나 윈도우 기반의 Convolutional Neural Network (CNN) 모델이 더 적합하다고 생각한다. CNN 모델의 네트워크 구조는 다음과 같다.

Khaii는 CNN 모델을 자연어처리 도메인에 성공적으로 적용시킨 Kim, Y. (2014)[6]을 베이스로 한다7. 이미지와 다르게 텍스트의 쪼갤 수 없는 성질을 고려해서 커널의 가로 사이즈는 full size로 입력의 임베딩 크기와 같고 커널의 세로 사이즈에 따라 n-gram 모델링과 비슷한 효과를 주기도 한다. Static한 CNN 구조를 보완하기 위해 윈도우-7인 문맥이 입력으로 사용된다. Kim, Y. (2014)의 모델과 다른 점으로 Khaii는 single-channel인 점과 단어가 아닌 음절, 문장이 아닌 문맥을 입력으로 둔다.


2. 노리(Nori) 형태소 분석기

노리는 독립적인 프로젝트가 아닌 루씬에 있는 일본어 형태소 분석기 프로젝트인 Kuromoji를 재활용한 것이다. 형태소로 쪼개는 메카니즘은 언어에 크게 종속되지 않아 한국어에도 손쉽게 적용할 수 있다. 사전은 언어에 맞게 구성되어야 하므로 Kuromoji는 MeCab를 통해 생성된 IPADIC 사전을, 노리MeCab를 통해 생성된 MeCab-ko-dic 사전을 활용해서 형태소 단위로 나누고 품사를 태깅한다.

한국어는 일본어와 달리 공백 정보가 품사를 판단하는 단서가 될 수 있기에 노리는 공백을 처리하기 위한 로직을 추가로 포함한다. Kuromoji는 일찍이 루씬 프로젝트에 병합되어 여러 해에 걸쳐 메모리 사용량과 속도면에서 많은 최적화가 이뤄졌기 때문에 여러모로 노리는 많은 혜택을 받았다. 추가로 노리 개발자는 경계가 명확한 교착어인 한국어 특징을 활용해 압축률을 높였다[2].

2.1. Architecture

루씬 프로젝트(lucene-solr)는 핵심 알고리즘이 있는 ‘core’ 모듈과 ‘codecs’, ‘memory’, ‘queries’ 등 다양한 모듈로 구성되고 서로 유기적인 관계를 가진다. ‘nori’는 하나의 독립적인 모듈로 존재한다. 다음 그림을 통해 ‘nori’가 어떤 모듈과 관계를 가지는지 확인할 수 있다. ‘nori’ 모듈은 ‘core’ 모듈과 상속 관계를 가지고, ‘common’과 의존 관계를 가진다. ‘core’ 모듈로부터 analyzer, tokenizer 등과 같은 analysis에 필요한 기본적인 뼈대를 가져오고, ‘common’ 모듈로부터 동의어 처리와 관련된 코드 등을 사용한다. 루씬에서는 다양한 언어의 형태소 분석기를 제공한다. 언어에 상관없이 형태소 분석기가 가지는 기본적인 기능은 공통으로 사용한다.

다음은 ‘nori’ 모듈에 있는 클래스들을 대상으로 클래스 다이어그램을 나타낸 그림이다(단, 테스트 클래스는 제외). 상속, 의존, 집합 관계 등 다양한 관계를 서로 맺고 있다. KoreanTokenizer 클래스는 가장 많은 in/out 바운드 개수를 가지는 것처럼 노리에서 핵심적인 토크나이징 알고리즘을 가진다. 또한, KoreanAnalyzer가 가장 부모 클래스인 것으로 보아 노리의 시작점임을 알 수 있다. 노리가 실행될 때는 analyzer를 통해 tokenizer와 각종 filter 클래스들이 생성되고 사용되지만, test와 같은 유닛 단위로 사용될 때는 factory 클래스를 통해 사용된다.

2.2. Setting

노리는 목적에 맞게 효율적인 형태소 분석을 할 수 있도록 다양한 옵션을 제공한다.

루씬의 Analyzer는 토크나이징 앞단에서 문자열을 대상으로 처리하는 전처리인 Character Filter와 토크나이징 결과인 토큰 리스트를 대상으로 처리하는 Token Filter를 가질 수 있다. Character Filter에서는 주로 특정 문자열, 문자열 패턴을 치환, 삭제 등을 하고, Token Filter에서는 대소문자화, 스테밍 등의 처리를 한다. 노리는 루씬에서 제공하는 Token Filter뿐만 아니라 한글로 표기된 숫자를 정수형으로 변환해주는 Korean Number Filter와 특정 POS 태그에 해당되는 토큰을 삭제할 수 있는 Korean PartOfSpeech Stop Filter8, 그리고 한자를 한글로 변환해주는 Korean ReadingForm Filter를 제공한다. 노리는 루씬에서 제공하는 Token Filter 중에서 동의어 단어를 추가할 수 있는 Synonym Graph Token Filter를 주로 사용한다9.

노리는 decompound mode를 통해 복합명사를 어떻게 분석하는 지를 결정할 수 있다. 복합명사의 원형만 살리고 싶으면 None 모드를, 서브 단어들만 살리고 싶으면 Discard 모드를, 그리고 모두 살리고 싶으면 Mixed 모드를 사용하면 된다. 텍스트를 분석하는 측면과 검색 엔진을 운용하는 입장 등과 같이 문제에 맞게 선택하면 된다. 검색 도메인에서 None 모드는 high precision에 유리하고, Discard 모드는 high recall에 유리하다.

하나의 형태소 분석기의 결과만 사용하기보다는 여러 개의 형태소 분석기의 결과를 사용하는 것이 좋을 때도 있다. 예를 들어, 노리 분석기와 whitespace 분석기를 함께 사용할 수 있다. 공백 단위로 분할된 토큰들은 검색에서 precision을 높여주는 역할을 한다. 간혹 특정 단어가 홀로 있을 때와 문장 속에 있을 때의 분석 결과가 다를 수 있는데 이를 보완해주기도 한다. 그러나 정보의 질 측면에서는 덜 중요한 정보가 추가되었기에 텍스트를 의미적으로 분석할 때는 좋지 않을 수도 있다.

엘라스틱서치는 Query DSL을 제공하여 위와 같은 다양한 옵션들을 목적에 맞게 선택해서 노리에 쉽게 세팅할 수 있도록 해준다.

2.3. Resource

2.3.1. Mecab-ko-dic

노리는 Mecab의 후손으로 토크나이징(입력을 문맥에 맞는 단어 조합으로 올바르게 쪼개기)을 위한 재료로 Mecab-ko-dic 프로젝트를 활용한다.

크게 두 가지 종류의 재료가 있다. 첫 번째, Mecab의 fork 프로젝트인 Mecab-ko 모델(한국어 특징 포함)로부터 생성된 결과물인 단어, 연접 비용과 같은 ‘비용 정보’가 있다. matrix.def 파일을 참고하면 모든 right-id와 left-id의 조합에 대한 연접 비용이 명시되어 있다. 두 번째, 명사, 형용사, 조사, 어미 등과 같은 품사와 관련된 단어와 인명, 장소와 같은 고유 단어가 있는 ‘시스템 사전’이 존재한다. 사전에는 각 단어들에 대한 단어 비용이 명시되어 있다. 이러한 비용은 Mecab에 있는 CRF 모델을 사용하여 한국어 코퍼스(세종 코퍼스)를 비지도 학습으로 학습한 결과물이다10.

다음은 시스템 단어의 속성을 나타낸다.

엔트리 / 좌문맥ID / 우문맥ID / 단어비용 / 품사태그 / 의미분류 / 종성유무 / 발음 / 타입 / 첫번째품사 / 마지막품사 / 분석결과

몇 가지 예시를 살펴보자. 다른 엔트리와는 다르게 Compound와 Inflect 엔트리는 서브 단어들이 명시되어 있다.

[ NNG.csv ]

근접,1781,3535,2798,NNG,*,T,근접,*,*,*,*
근접전,1781,3535,2835,NNG,*,T,근접전,Compound,*,*,근접/NNG/*+전/NNG/*
...

[ NNP.csv ]

룩셈부르크어,1783,3538,3534,NNP,*,F,룩셈부르크어,Compound,*,*,룩셈부르크/NNP/지명+어/NNG/*
병자호란창의록,1783,3539,3534,NNP,*,T,병자호란창의록,Compound,*,*,병자/NNG/*+호란/NNG/*+창의/NNG/*+록/NNG/*
...

[ Inflect.csv ]

가까워,1801,5,2310,VA+EF,*,F,가까워,Inflect,VA,EF,가깝/VA/*+어/EF/*
가까워도,1801,3,1664,VA+EC,*,F,가까워도,Inflect,VA,EC,가깝/VA/*+어도/EC/*
가냘퍼진,1801,10,569,VA+EC+VX+ETM,*,T,가냘퍼진,Inflect,VA,ETM,가냘프/VA/*+어/EC/*+지/VX/*+ᆫ/ETM/*
걸쳐져놓인,2417,10,0,VV+EC+VX+EC+VV+ETM,*,T,걸쳐져놓인,Inflect,VV,ETM,걸치/VV/*+어/EC/*+지/VX/*+어/EC/*+놓이/VV/*+ᆫ/ETM/*
...

[ MAG.csv ]

가까스로,736,2650,3309,MAG,성분부사/양태부사,F,가까스로,*,*,*,*
가까이,736,2650,1713,MAG,성분부사/양태부사,F,가까이,*,*,*,*
...

추가로 문자(character)에 대한 유니코드 범위와 특징 정보, 품사 단위로 구분할 수 있는 고유 식별자 등이 포함된다. 한 가지 주목할 점은 모든 비용의 품질은 학습 데이터의 품질에 종속되기 때문에 학습 데이터를 어떻게 잘 구축하느냐에 따라 노리의 전체적인 성능을 올리는 데 중요한 열쇠가 될 수 있다. Mecab-ko-dic에서 사용하는 품사 태그에 대한 정보는 여기에서 확인할 수 있다. 형태소 분석기를 구현할 때 품사 태그의 특징을 활용할 수 있기에 이들을 잘 이해하는 일은 중요하다. 예를 들어, NNBC 의미를 이해하면 수량 명사에 대한 처리를 하고, EF로는 문장 분리기를 구현할 수 있다.

2.3.2. User Dictionary

노리의 특징으로 사용자 사전이라는 자원을 활용할 수 있다는 점이다. 말그대로 사용자가 정의하는 단어로 특정 도메인 사전을 만들 수 있다. 노리 토크나이저는 내부 알고리즘 흐름 상 이들을 가장 중요하다고 판단하여 가장 먼저 처리한다. 이를 통해 노리 토크나이저를 사용자가 원하는 방향으로 세세한 케이스까지 쉽게 컨트롤할 수 있는 장점이 있다11. 또한, 시스템 사전의 구성이 불완전할 경우 사용자 사전을 활용하여 보완할 수 있다.

피땀

바늘방석

인공지능 인공 지능

언어처리 언어 처리

자연언어처리 자연 언어 처리

...

사용자 사전의 단어(word)는 크게 단일어복합어로 구성된다12(여기서 융합 합성어는 복합어가 아닌 단일어로 취급한다). 위와 같이 단일어는 홀로 명시하고 복합어는 원형어 다음에 형태소(낱말)들을 띄어쓰기와 함께 나열한다.

단일어는 2개 이상의 형태소가 결합된 융합 합성어, 어근과 접사(접두사, 접미사)가 결합한 파생어, 그리고 자립 형태소 그 자체로 구성된다(사용자사전 어뷰징을 최대한 피하기 위해서 자립 형태소의 등록은 지양하는 것이 좋다). 복합어는 2개 이상의 형태소가 단순히 붙여진 형태이다. 사용자 사전은 대부분 명사와 관련된 단일어와 복합어로 구성된다. 굴절어와 같이 동사와 관련된 엔트리(단어)는 특수하지 않으면 잘 사용되지 않는다. 노리는 내부적으로 사용자 사전의 단어들은 모두 일괄적으로 NNG(일반명사) 품사 태그를 붙인다.

사용자 사전을 정의할 때 주의할 점이 있다. 단일어와 복합어의 구분을 명확히하고, 동일한 복합어의 형태소 구성의 일관성을 유지해야 한다13. 개념을 어떻게 바라보느냐에 따라 단일어가 복합어가 되고, 복합어가 단일어가 될 수 있기에 일관된 기준이 중요하다. 예를 들어, ‘바늘’과 ‘방석’이 원형인 ‘바늘방석’의 의미를 보존하지 못하므로 단일어로 취급한다. ‘인공’과 ‘지능’은 원형인 ‘인공지능’의 의미를 보존하므로 복합어가 된다. 단, 이러한 기준은 코퍼스의 크기와 도메인에 따라 달라질 수 있다. 코퍼스의 크기가 작으면 sparsity 문제를 최소화하기 위해서 단일어의 비중이 높아질 수 있다. 또한, 정밀도(precision)를 높이기 위해 복합어를 단일어로 정의할 수 있다.

이처럼 사용자 사전은 중요한 언어 자원 중 하나이고 가장 높은 우선순위를 가지기 때문에 언어에 대한 깊은 이해를 바탕으로 엄격히 작성되어야 한다.

2.3.3. Synonym Dictionary

언어는 하나의 의미가 다양한 형태로 표현된다. 문맥에 따라 다르게 표현되고, 동일한 개념이 다양한 이름으로 불리기도 한다. 또한, 줄임말, 외래어 등과 같이 다양한 표현 방식을 가진다. 이러한 언어 특성을 다루기 위해 노리는 동의어 처리를 지원한다. (넓게보면 오타도 동의어다) 토크나이징 결과를 토대로 동의어 사전에 있는 단어를 대상으로 동의어 확장을 실시한다.

동의어 사전은 다음과 같이 구성된다. 세미콜론(;)을 기준으로 단어를 나열하고 라인에 있는 모든 단어는 동일한 의미를 가진다.

인공지능;에이아이

자연언어처리;자연어처리

...

노리에서 동의어 처리는 Synonym Graph Filter에 의해 처리된다. Filter의 한 종류이므로 토크나이징 다음에 실시된다. 입력 질의어와 동의어 단어 모두 토크나이징을 실시한 결과를 대상으로 비교한다. 예를 들어, ‘인공지능을 공부하자’라는 입력을 동의어 처리가 포함된 토크나이징하면 다음과 같은 결과가 출력된다. 여기서 ‘인공지능’은 위와 같이 복합어로, ‘에이아이’는 단일어로 사용자 사전에 등록되었다고 가정한다.

| 인공 | 지능 | 을 | 공부 | 하 | 자 |

|  에이아이   |

‘에이아이’ 토큰이 추가되었다. 단순히 flat하게 추가된 것이 아니라 포지션을 기준으로 추가되었다. Bar(|)를 통해 각 토큰의 포지션을 확인할 수 있다. 동의어는 모두 동등한 포지션을 가진다. 이러한 포지션 정보는 AND/OR 검색할 때 활용된다.

주의할 점은 decompound mode와 filter 순서에 따라 동의어 처리 결과가 달라질 수 있다. POS filter(여기선 구두점 삭제만)가 먼저 실시하고 synonym filter가 실행된다고 하자. 입력 쿼리가 ‘인공 ? 지능’일 때, 원형이 보존되는 none과 mixed 모드에서는 동의어 처리가 원형을 기준으로 하기 때문에 동의어 처리가 되지 않는다. 반면, discard 모드에서는 동의어 처리가 가능하다. 물음표가 POS filter에 의해 삭제되고 동의어 처리 기준이 원형이 아닌 연이은 서브단어들이기 때문이다.

일반적으로 동의어 사전을 위와 같이 토큰 확장을 위해 사용하지만, 하나의 대표어를 선택하는 정규화를 목적으로 사용하기도 한다. 정보량 측면에서 차이가 있지만 두 가지 모두 근본적으로 동일한 성격의 언어 처리이다.

2.4. Algorithms (for Tokenizing)

노리에는 크게 두 가지 종류의 핵심 알고리즘이 있다. 효율적인 계산으로 최적의 조합을 찾아주는 Viterbi와 단어를 저장하는 FST 알고리즘이다. Two-memory tape의 FST는 그 자체만으로 형태소 분석기를 만들 수 있지만, 여기에서는 단어를 효과적으로 저장하는 자료구조로 활용된다. 이들과 함께 토크나이징과 관련된 알고리즘을 알아보자.

2.4.1. Viterbi Algorithm

어려운 문제를 여러 개의 하위 문제로 쪼개고, 하위 문제들을 먼저 해결하는 방법으로 동적 프로그래밍이 있다. 이의 패밀리 중에서 시퀀스 데이터를 처리하는 버전으로 Viterbi 알고리즘이 있다.

노리에서 토크나이징 문제는 가능한 모든 토큰 조합들 중에서 총 비용이 가장 적은 최적의 조합을 찾는 일과 같다. Viterbi를 사용하면 최대 비용을 가지는 이전 노드만 저장하면서 토큰을 추가하므로 중복 연산 없이 효율적으로 최적의 조합을 찾을 수 있다. Viterbi는 크게 viterbi path를 구성하는 일과 optimal path를 찾는 일의 2단계로 구성되고, 노리에선 각각 add 함수backtrace 함수를 통해 실행된다. 이들은 모두 KoreanTokenizer.java 파일에 정의되어있다.

private void add(Dictionary dict, Position fromPosData, int wordPos, int endPos, int wordID, Type type) throws IOException {

  final POS.Tag leftPOS = dict.getLeftPOS(wordID);
  final int wordCost = dict.getWordCost(wordID);
  final int leftID = dict.getLeftId(wordID);
  int leastCost = Integer.MAX_VALUE;
  int leastIDX = -1;
  assert fromPosData.count > 0;
  
  for(int idx=0; idx<fromPosData.count; idx++) {
    // The number of spaces before the term
	int numSpaces = wordPos - fromPosData.pos;
	
	// Cost is path cost so far, plus word cost (added at
	// end of loop), plus bigram cost and space penalty cost.
	final int cost = fromPosData.cost[idx] + costs.get(fromPosData.lastRightID[idx], leftID) + computeSpacePenalty(leftPOS, numSpaces);
    
	if (cost < leastCost) {
	  leastCost = cost;
	  leastIDX = idx;
	}
  }
  
  leastCost += wordCost;
  positions.get(endPos).add(leastCost, dict.getRightId(wordID), fromPosData.pos, wordPos, leastIDX, wordID, type);
}

노리는 왼쪽에서 오른쪽 방향으로 문자 하나씩 이동하면서 처리한다. 현재 포지션의 문자로 시작하는 단어 중에서 사전과 일치하는 단어는 positions.get().add 함수의 인자로 들어온다. 입력 단어를 기준으로 이전 시간의 모든 단어들과의 비용을 계산하고, 가장 비용이 적은 이전 시간의 idx와 함께 입력 단어는 viterbi path에 추가된다. 즉, 입력 단어와 연결되는 과거의 path는 오로지 하나의 optimal path만 존재한다.

private void backtrace(final Position endPosData, final int fromIDX) {

  final int endPos = endPosData.pos;
  final char[] fragment = buffer.get(lastBackTracePos, endPos-lastBackTracePos);
  int pos = endPos;
  int bestIDX = from IDX;
  
  while (pos > lastBackTracePos) {
  
    final Position posData = positions.get(pos);
	  int backPos = posData.backPos[bestIDX];
	  int backWordPos = posData.backWordPos[bestIDX];
	  int length = pos - backWordPos;
	  Type backType = posData.backType[bestIDX];
	  int backID = posData.backID[bestIDX];
	  int nextBestIDX = posData.backIndex[bestIDX];
	  final int fragmentOffset = backWordPos - lastBackTracePos;
	  final Dictionary dict = getDict(backType);
	  ...
  
	  else {
	    final DictionaryToken token = new DictionaryToken(backType, dict, backID, fragment, fragmentOffset, length, backWordPos, backWordPos + length);
	
	  }
	  ...
	  pending.add(token);
	  ...
  }
}

가장 늦은 단계에서 viterbi path에 추가된 단어를 시작으로 backtrace 함수를 통해 이전 시간의 단어를 계속해서 추출해나간다. 비용이 가장 적은 이전 단어의 인덱스가 이미 add 함수로부터 저장되어 있으므로 이는 매우 간단한 과정이다. 추출된 단어 리스트가 optimal path가 되고 토크나이징의 최종 결과물이 된다.

비터비 알고리즘에 대한 내용은 “비터비 알고리즘 (FEAT. POS 태깅)” 글을 참고하길 바란다.

2.4.2. FST (Finite State Transducer)

복잡한 시/공간적 패턴을 가지는 프로세스를 FSM(Finite State Machine)로 표현하면 훨씬 더 압축시킬 수 있다. 또한, union, intersection, concatenation, complement 등과 같은 대수적 성질(algebraic properties)을 이용할 수 있어 많은 문제를 쉽고 빠르게 해결할 수 있다. FSM은 초기 상태를 포함한 상태 리스트, 상태를 변화시키는 트랜지션의 조건으로 구성된다. FSM은 심볼들의 시퀀스를 통해 정규 언어(regular language)를 잘 표현할 수 있다14. 이는 프로그래밍 언어 설계에 매우 유용하다.

FST(Finite State Transducer)는 FSM을 일반화시킨 모델이다. FSM은 구조적으로 한 줄기의 심볼 시퀀스(a single memory tape)밖에 표현하지 못하지만, FST는 두 줄기의 심볼 시퀀스(two memory tapes)를 표현할 수 있다. 다시 말해 FST는 입출력이 모두 있지만 FSM은 출력만 있다. 이와 같이 FST는 서로 다른 두 심볼 시퀀스를 매핑하는 능력이 있기 때문에 translator라고도 불리고 자연어 처리에서 형태소 분석기, 파싱 등에 사용된다.

보통 FST를 알고리즘으로 사용하지만, 자료구조로도 활용될 수 있다. 노리에서는 FST를 자료구조로 사용하여 압축된 형태로 단어를 효율적으로 저장하고 선형 시간(linear time)으로 검색을 가능케 해준다. 즉, FST를 사용하여 단어를 저장하면 메모리 효율적으로 저장할 수 있고 디스크로부터 빠른 로딩을 할 수 있다. FST는 prefix와 suffix를 모두 공유하기 때문에 압축률을 최대한 높일 수 있다. 이와 비슷한 Trie 자료구조는 오직 prefix만 공유한다.

FST는 루씬 프로젝트에 포함되어 범용적으로 사용되고 있다. 여기서는 단순히 FST를 활용한 사전의 입출력 부분만 소개하려 한다. 다음은 FST에 사용자 단어를 저장하는 부분을 나타내고 있다. 단어를 추출하는 부분은 다음 절을 참고하길 바란다.

public final class UserDictionary implements Dictionary {

  private final TokenInfoFST fst;
  ...
  
  private UserDictionary(List<String> entries) throws IOException {
    ...
    PositiveIntOutputs fstOutput = PositiveIntOutputs.getSingleton();
    Builder<Long> fstBuilder = new Builder<>(FST.INPUT_TYPE.BYTE2, fstOutput);
  
    for (String token : entries) {
      ...
      // add mapping to FST
      ...
      fstBuilder.add(scratch.get(), ord); // insert token with an unique value into fst
      ...
	}
	this.fst = new TokenInfoFST(fstBuilder.finish());
  }
  ...
}

2.4.3. Longest-Matching Algorithm

기존의 노리 토크나이저는 시스템 단어에 대해서만 최장일치(Longest-Matching)가 되고, 사용자 단어에는 적용되지 않았다. 시스템 단어는 세종 코퍼스로부터 단어와 연접 비용이 함께 생성되었기 때문에 정규화가 잘 되어 최장일치가 자연스럽게 이뤄진다. 반면, 사용자 단어의 매우 낮은 단어 비용이 연접 비용을 무시하게 만들어 최장일치가 아닌 최단일치 현상이 발생한다. 사용자 단어의 우선 순위를 위해서 매우 낮은 비용은 필수적인 요소이기 때문에 단순히 비용을 수정하는 것이 아닌 다른 방법을 생각해야 했다. 새로운 비용이나 규칙을 추가하는 방법도 있지만, 여기서는 사용자 단어를 viterbi path에 추가할 때 가장 긴 길이의 사용자 단어만 추가하는 로직을 작성했다.

private void parse() throws IOException {
  
  // Maximum posAhead of user word in the entire input
  int userWordMaxPosAhead = -1;

  // Advances over each position (character):
  while (true) {

    if (buffer.get(pos) == -1) {
      // End
      break;
    }
	...
	
    // First try user dict:
    if (userFST != null) {
      userFST.getFirstArc(arc);
      int output = 0;
      int maxPosAhead = 0;
      int outputMaxPosAhead = 0;
      int arcFinalOutMaxPosAhead = 0;

      for(int posAhead=pos;;posAhead++) {
        final int ch = buffer.get(posAhead);
        if (ch == -1) {
          break;
        }
        if (userFST.findTargetArc(ch, arc, arc, posAhead == pos, userFSTReader) == null) {  // lookup!
          break;
        }
        output += arc.output.intValue();
        if (arc.isFinal()) {
          maxPosAhead = posAhead;
          outputMaxPosAhead = output;
          arcFinalOutMaxPosAhead = arc.nextFinalOutput.intValue();
          anyMatches = true;
        }
      }

      // Longest matching for user word
      if (anyMatches && maxPosAhead > userWordMaxPosAhead) {
        if (VERBOSE) {
          System.out.println("    USER word " + new String(buffer.get(pos, maxPosAhead + 1)) + " toPos=" + (maxPosAhead + 1));
        }
        add(userDictionary, posData, pos, maxPosAhead+1, outputMaxPosAhead+arcFinalOutMaxPosAhead, Type.USER);
        userWordMaxPosAhead = Math.max(userWordMaxPosAhead, maxPosAhead);
      } 
    }	
    
    ...
    pos++;
  }
}

위 코드는 루씬 프로젝트의 JiraPRGitbox에 등록되어 있다.

2.5. Pre-processing Module

노리 토크나이징의 내부 알고리즘을 수정하는 것뿐만 아니라 토크나이징 전에 실시하는 전처리 모듈을 추가하여 우리의 목적에 맞게 커스터마이징할 수 있다. 특정 패턴의 문자열이나 의미가 거의 없는 문자열에 대해서 토크나이징을 하고 싶지 않을 때가 있다. 예를 들어, 상품명 데이터에서 모델 번호(ex. 13-AH34BC, 03943AB)에 토크나이징하는 것은 불필요한 일이 될 수 있다. 전처리 단계에서 모델 번호를 인식하고 토크나이징을 하지 않도록 따로 빼는 작업이 필요하다. 이는 마치 사전에 개체명을 인식하는 일과 비슷하다.

다음 코드는 노리에서 모델 번호를 인식하는 전처리 모듈을 나타냅니다. 다양한 모델 번호의 패턴을 인식하도록 여러 개의 정규 표현식을 정의했다. 크게 두 단계로 구성된다. High recall을 기준으로 처음에는 rough하게 최대한 많은 케이스들을 매칭하고, high precision을 위해서 매칭된 케이스에서 조건에 맞지 않는 경우들은 제외한다15. 최종 인식 단계를 통과하여 인식된 문자열은 token으로 변환되어 pending 리스트에 들어간다. 이후 parse 함수를 통해 토크나이징이 실시된다.

private final List<Token> pending = new ArrayList<>();

@Override
public boolean incrementToken() throws IOException {

  if (preprocessor) {
  
    // Input Load
    String originIn = "";
    ...
	
    // Model-number Pattern Regex
    ...
	  
    // First, Matching (All) for High Recall
    ...
    
    // Second, Filtering (Excluding) for High Precision
    ...
    
    // Add token
    ...
    
    // Reset buffer
    ...
  }
  
  while (pending.size() == 0) {
    if (end) {
      return false;
    }

    // Push Viterbi forward some more:
    parse();
  }
  ...
}

이와 같은 인식기뿐만 아니라 오타, 띄어쓰기 교정 등 다양한 종류의 전처리 작업을 추가할 수 있다.

2.6. Post-processing Module

토크나이징을 더 섬세하게 컨트롤하기 위해 후처리 모듈도 필요하다. 도메인에 따라 토크나이징 기준이 달라질 수 있다. 검색에서 정밀도(precision) 중심의 매칭을 원하는 경우 토큰들이 의미 단위로 최대한 붙어있으면 좋다. 예를 들어, ‘우유 250ml’를 일반적인 토크나이징을 하면 [‘우유’, ‘250’, ‘ml’]로 분석된다. 여기서 정밀도(Precision)을 좀 더 높이고 싶으면 ‘250’과 ‘ml’을 붙이는 것이 좋다. 또한, 사용자의 띄어쓰기 오류가 많거나 미등록어가 많은 외래어의 경우 후처리를 통해 붙일 수 있다. 그리고, ‘아메리칸 투어리스터’의 경우 사용자가 ‘투어’라는 익숙한 단어때문에 ‘아메리칸 투어 리스터’로 잘못 표기할 수 있다. 이 경우 토크나이징을 하면 [‘아메리칸’, ‘투어’, ‘리스터’]가 되는데 후처리를 통해 [‘아메리칸’, ‘투어리스터’]로 만들 수 있다. 미등록어의 경우(ex. ‘누트로지나’)는 [‘누’, ‘트’, ‘로’, ‘지’, ‘나’]처럼 1음절로 모두 쪼개지기도 한다. 미등록어 감지 후처리로 이들을 모두 하나로 합쳐야 한다. 이처럼 후처리에서 추가적인 사전, 품사 정보 등을 활용한 규칙으로 쉽게 처리할 수 있다.

노리 토크나이징 내부에서 이러한 작업을 하면 알고리즘이 복잡해지고 예외가 발생할 확률이 높아져 유지 보수가 힘들 수 있다. 그리고 보통 개체명 인식은 토크나이징 후에 실시되는데 바로 이 후처리 단계에 해당된다. 전처리에서 실시되는 개체명(문자열) 인식과의 차이점은 토크나이징 실시 여부이다. 차이점으로 전처리에서 실시하는 단순히 문자 종류, 패턴을 기반으로 처리하지만, 후처리에서는 단어의 의미를 분석한다. 각각 장단점이 있기에 문제에 맞게 잘 사용하면 됩니다.

후처리 구현은 KoreanTokenizer.java 파일에 있는 incrementToken 함수를 수정하면 된다. Pending 크기를 기준으로 마지막에 불려질 타이밍에 형태소 분석의 결과가 담겨져 있는 tokenattribute 클래스를 수정하면 후처리를 할 수 있다.

2.7. 노리 제한점 & TODO

한국어는 다른 언어들과 다르게 중의성이 많아 자연어처리를 하기 어려운 언어 중 하나이다. 언어 구조상 형태론적 변형이 다양하고 형태소의 경계를 찾기 어렵다. 축약과 생략, 이동 현상이 빈번하고 띄어쓰기 오류가 많은 언어이다. 노리는 일본어 형태소 분석기를 그대로 사용했기에 오류 보정 등과 같이 한국어 처리에 대한 부족한 부분이 있다.

전처리 모듈

노리 앞단에 오류를 보정하기 위해 전처리 모듈16이 필요하다. 대표적으로 오타 교정, 띄어쓰기 교정, 신조어 추가가 있다. 공통으로 세 가지 모두 사전 기반인 노리의 토크나이징 품질에 큰 영향을 준다. 오타 교정은 다양한 표현 방식을 하나로 통일하는 정규화와 비슷한 성격을 가지고 신조어 추가는 미등록어 처리에 도움을 준다.

노리는 띄어쓰기 비교적 정확한 코퍼스로부터 학습된 리소스를 사용하므로 띄어쓰기 오류가 있는 경우 잘못된 형태소 조합을 출력할 확률이 높다17. 예를 들어, “업무용 무전기”는 [‘업무’, ‘용’, ‘무전’, ‘기’]로 알맞게 토크나이징이 되지만 띄어쓰기가 없는 “업무용무전기”는 [‘업무’, ‘용무’, ‘전기’]로 잘못된 분석 결과를 도출한다. 또한, 노리 내부 알고리즘 특성상 띄어쓰기가 없고 첫 단어가 미등록 음절일 경우 unknown 길이가 길어져 토크나이징이 아예 안될 수 있다. 예를 들어, “퀸이불베개세트” 쿼리를 기준으로 ‘퀸’으로 시작하는 부분 단어가 사전에 아예 없는 경우 ‘퀸-트’까지 모두 unknown 토큰으로 간주하여 [‘퀸이불베개세트’]라는 분석 결과를 가진다. 올바른 결과는 [‘퀸’, ‘이불’, ‘베개’, ‘세트’]가 되어야 한다.

코퍼스 품질

노리는 mecab로부터 생성된 자원(단어, 비용)을 사용하여 토크나이징하기 때문에 그들의 품질에 노리의 성능이 결정된다. 이에 mecab가 학습하는 코퍼스와 한국어 사전 등을 최신으로 유지하는 것이 필요하다. 올바른 단어 형태, 문법, 띄어쓰기는 기본이며 품사와 같은 라벨이 올바르게 부착되어야 한다. 또한, 빠르게 변하는 언어의 특성을 반영하여 신조어와 같은 최신 정보를 업데이트해야 한다. 코퍼스는 노리에서 시스템 사전에 해당한다. 시스템 사전뿐만 아니라 사용자 사전, 동의어 사전의 품질도 항상 신경을 써야 한다.

우선순위 처리 ABUSE

노리는 토크나이징할 때 우선순위가 있는 세 개의 사전(1.USER, 2.KNOWN, 3.UNKOWN)을 사용한다. 이러한 구조는 사전 자원을 효율적으로 운용할 수 있게 하고 사용자에게 토크나이징을 직접적으로 컨트롤할 수 있는 권한을 부여할 수 있는 장점이 있지만, 이 때문에 발생하는 문제도 있다.

미등록 단어의 경우는 토크나이징을 원치 않을 수도 있다18. 미등록 단어는 우선순위가 가장 낮은 UNKNOWN 사전에서 처리해야 하지만 이보다 우선순위가 높은 KNOWN 또는 USER 사전에서 먼저 처리한다. 이에 입력 문자열의 부분 단어를 기준으로 토크나이징이 되는 현상이 빈번하다19. 예를 들어, “카페트”라는 미등록어가 텍스트를 분석하면 [‘카페’, ‘트’]라는 결과를 가지게 되어 ‘카페’라는 잘못된 의미 분석을 하게 된다. 만약, 사용자 사전에 1음절이 많이 등록되어 있다면 미등록어(신조어)가 해당 1음절을 기준으로 쪼개지는 현상이 빈번할 것이다.

한글로 연이어진 문자열을 토크나이징할 때 구문적인 단어를 포함시키는 것보다 가능한 의미적인 단어들의 조합을 구성하는 편이 좋다. KNOWN 단어들로 의미적인 단어 조합으로 구성할 수 있고, USER 단어들로 의미적 단어와 구문적 단어로 구성할 수 있다면, 전자를 선택하는 것이 좋다. 현재는 USER 사전의 우선순위가 있으므로 항상 후자의 경우를 선택할 수 밖에 없다. 예를 들어, ‘삼성대’라는 단어가 사용자 사전에 들어있는 상태에서 “삼성대리점”을 분석하면 [‘삼성대’, ‘리’, ‘점’]으로 오분석된다. 이 경우는 [‘삼성’, ‘대리점’]이 더 적합한 결과이다.

우선순위 처리 ABUSE 문제는 크리티컬한 문제가 아니므로 전체 성능에 큰 영향을 끼치지는 않는다. 그리고 우선순위 매커니즘 자체가 노리의 기본 전략이므로 근본을 고치기 보다는 위의 문제들을 고려해서 사전을 적절히 잘 운용하는 등과 같이 알고리즘을 보완할 수 있는 방향을 가지는 것이 좋다.


3. 마치며

다양한 한국어 형태소 분석기가 있지만 루씬과 엘라스틱서치에 플러그인으로 포함되는 것은 노리가 유일합니다. 노리는 기존에 한국어 형태소 분석기로 활발하게 사용되던 mecab를 기반으로 만들어졌고 여러 해에 걸쳐 최적화가 잘된 kuromoji 일본어 형태소 분석기의 메카니즘을 재활용하여 개발되었습니다. 아직 한국어 처리에 대한 미숙한 부분이 있지만 전처리 모듈과 함께 알고리즘을 개선해나간다면 좋은 한국어 형태소 분석기가 되리라 믿습니다. 친절하게 도움을 준 노리 개발자인 Jim Ferenczi에게 감사의 인사를 전합니다.


4. 각주

  1. pynori를 통해 노리를 파이썬 패키지로 이용할 수 있고, 엘라스틱서치와 독립적으로 사용할 수 있다.
  2. 이처럼 루씬 Analyzer에서 기본적인 전처리를 할 수 있는 API를 제공하지만, 단순 치환이 아닌 복잡한 패턴 인식 등을 위해서는 2.3.4절과 같은 독립적인 전처리 모듈이 필요하다.
  3. Analyzer는 검색 엔진의 코어 부분에 속한다. 따라서, 노리 형태소 분석기의 코드는 엘라스틱서치가 아닌 루씬 코드에 포함된다.
  4. 사실, 규칙 기반과 통계 기반은 서로 완전히 이질적인 관계는 아니다. 예를 들어, 기계학습에 필요한 자질을 규칙을 통해서 만들기도 한다. 어디에 중점을 두느냐에 따라 규칙 기반이 될 수 있고 통계 기반이 될 수 있다. 참고로 산업(industry)과 학계(academia)에서의 규칙 기반과 통계기반의 활용도가 극명하게 차이나는 재미있는 조사 결과도 있다[1].
  5. 학습 데이터는 기계학습 모델을 학습하는데 재료로 사용되는 데이터이다. 사전과 같은 리소스 성격의 데이터와 다른 종류이다.
  6. Khaii의 vocab.out 파일을 보면 연이은 두 개 이상의 조합이 그렇게 많지 않음을 확인할 수 있다.
  7. Kim, Y. (2014) 논문의 리뷰를 참고바란다.
  8. 오분석될 경우 의도하지 않은 토큰이 삭제될 수 있으므로 POS stoptag filter를 사용할 때 주의하자.
  9. 기본적인 Synonym Token Filter는 동의어 단어를 추가할 때 포지션이 엉키는 문제가 있었는데, Synonym Graph Token Filter는 포지션을 flatten하게 펼쳐서 이 문제를 해결한다[5].
  10. Mecab에서는 비용(cost)이라고 정의했지만, 보통 확률(probability)로 정의된다.
  11. 하지만, 사용자 사전의 강력한 우선 순위 처리 때문에 발생하는 제한점도 있다(cf.2.7장).
  12. 단어(word)는 형태소(morpheme)을 포함한다. 낱말은 형태소와 같은 의미이다. 여기서 언급하는 단일어는 자립 형태소(어근)를 뜻하고, 복합어이지만 융합 합성어를 포함한다. 복합어는 병렬 합성어와 같다. 단어(word)는 단일어와 복합어를 모두 포함한다. 어근은 자립 형태소로 구성한다.
  13. 단어의 뜻에 따라 융합 합성어가 될 수 있고 병렬 합성어가 될 수 있다. 즉, 문맥에 따라 단일어가 될 수 있고 복합어가 될 수 있다. 이러한 중의성을 해결하기 위해서는 문맥을 먼저 보고 사전처리 작업을 해야한다.
  14. 정규 언어(regular expression)는 보통 정규 표현식(regular expression)으로 표현되고, 모든 정규 표현식은 FSM이 받아들일 수 있다.
  15. 수많은 종류의 모델 번호가 있는 데이터가 있다면 해당 데이터로부터 역으로 정규 표현식을 생성하는 방법도 유용할 것이다.
  16. 전처리 모듈은 두 가지 종류가 있다. 여기서의 오류 보정용과 2.5절에서 언급한 개체명 인식용 전처리 모듈이 있다.
  17. 노리가 사용하는 Mecab-ko-dic 리소스는 대부분 세종 코퍼스로부터 생성되었다. 세종 코퍼스는 전문가들이 작성한 텍스트이므로 띄어쓰기 오류가 거의 없다.
  18. unigram으로 항상 토크나이징하는 기능도 있지만 이를 사용하기 위해서는 토크나이징이 되더라도 모두 unknown 토큰이어야만 한다.
  19. 현재는 사전에 단어를 등록하는 방법 말고는 이를 해결할 방법이 없지만 다음과 같은 방안을 생각해 볼 수 있다. 우선순위가 높은 사전에서 부분 단어가 될 확률이 가장 높은 1음절의 단어는 최대한 배제해야 한다. 품사뿐만 아니라 단어 토큰의 연접비용을 적용하여 드물게 등장하는 시퀀스에 대해서는 하나의 unknown 토큰으로 묶일 수 있도록 유도한다. 또한, 연달아 add되는 1음절 단어에 대해서 패널티를 주는 방법도 있다.

5. 참고

  1. (논문) Chiticariu, Laura, Yunyao Li, and Frederick R. Reiss. “Rule-based information extraction is dead! long live rule-based information extraction systems!.” Proceedings of the 2013 conference on empirical methods in natural language processing. 2013.
  2. (블로그) Jim Ferenczi, 공식 한국어 분석 플러그인 ‘노리’, 엘라스틱 블로그, 2018
  3. (블로그) Look at the reverse side of Japanese morphological analysis! How does MeCab analyze morphologically
  4. (논문) Kudo, Taku, Kaoru Yamamoto, and Yuji Matsumoto. “Applying conditional random fields to Japanese morphological analysis.” Proceedings of the 2004 conference on empirical methods in natural language processing. 2004.
  5. (블로그) Chaning Bits Blog, Using Finite State Transducers in Lucene, 2010)