문제 상황
문장 내에서 형태소를 기반으로 한 특정 패턴을 추출하거나, 일치 여부를 판정하거나, 일부 형태소를 다른 형태소로 교체해야하는 작업을 하는 경우 조건문을 이용하는 수 밖에 없는데, 이 형태소 조건들이 복잡해질 경우 이 작업이 굉장히 고단해지는 문제가 있음.
예를 들어 숫자(SN) 뒤에 의존 명사(NNB)가 오고, 그 뒤에 주격 조사(JKS) 혹은 목적격 조사(JKO)가 나오는 문자열을 탐색한다고 하면
tokens = kiwi.tokenize(some_text)
for i in range(0, len(tokens) - 3):
if tokens[i].tag == 'SN' and tokens[i + 1].tag == 'NNB' and tokens[i + 2].tag in ('JKS', 'JKO):
print(some_text[tokens[i].start : tokens[i+2].end])
와 같이 장황하게 조건을 나열해야 한다. 게다가 이는 최적화된 탐색 알고리즘을 사용하기도 어려우므로 대량의 텍스트 내에서 탐색을 수행시 비효율적인 문제까지도 있음. 탐색 후 치환의 경우는 훨씬 더 복잡해지는 문제가 있다.
제안
문자열 일치/탐색/치환에 널리 쓰이는 정규표현식 문법을 형태소 탐색용으로 개량하여 사용한다. 그리고 Python3의 표준 정규표현식 모듈에서 제공하는 re.match, re.search, re.sub와 유사한 함수를 제공하여, 한국어 텍스트를 형태소 기반으로 일치/탐색/치환할 수 있도록 한다.
pattern = kiwi.Pattern(r"(/NN) /JKO (/VV /EF /SF?)") # re.compile과 유사하게 패턴을 미리 컴파일하여 최적화한다.
m = pattern .match("밥을 먹어요?") # 일치 시 Match object 반환, 불일치 시 None 반환
m.group() # "밥을 먹어요?" (일치된 전체 텍스트)
m.group(1) # "밥" (첫번째 괄호로 지정된 텍스트)
m.group(2) # "먹어요?" (두번째 괄호로 지정된 텍스트)
m.span() # (0, 7) (일치된 전체 텍스트 영역)
m.span(1) # (0, 1) (첫번째 괄호로 지정된 텍스트 영역)
m.span(2) # (3, 7) (두번째 괄호로 지정된 텍스트 영역)
m.token() # [Token(form='밥', tag='NNG', start=0, len=1), ..., Token(form='?', tag='SF', start=6, len=1)] (일치된 전체 텍스트의 형태소 목록)
m.token(1) # [Token(form='밥', tag='NNG', start=0, len=1)] (첫번째 괄호로 지정된 텍스트의 형태소 목록)
m.token(2) # [Token(form='먹', tag='VV', start=3, len=1), ..., Token(form='?', tag='SF', start=6, len=1)] (두번째 괄호로 지정된 텍스트의 형태소 목록)
m = pattern.search("저도 밥을 먹어요?") # re.match 와 re.search의 관계와 동일.
pattern = kiwi.Pattern(r"(/VV) /EP (/EF)")
result = pattern.sub(r"\1 \2", "길을 걸었다.") # \1과 \2는 각각 패턴 내의 첫번째, 두번째 괄호와 일치.
# result: "길을 걷다." (걸었다->걷다 로 치환됨)
result = pattern.sub(r"\1 \2", "옷을 걸었다.")
# result: "옷을 걸다." (걸었다->걸다 로 치환됨)
pattern = kiwi.Pattern(r"/NNG")
result = pattern.sub(r"바다/NNG", "길을 걸었다.")
# result: "바다를 걸었다." (길->바다 로 치환됨, 이에 따라 뒤의 조사도 함께 변환됨)
형태소용으로 개량된 정규표현식
기본적으로 표현식은 각각의 형태소를 표현하기 위해 쓰이고, 문자에 대한 일치 여부는 되도록 하지 않는 것을 원칙으로 한다.
/품사태그 : 품사태그는 앞글자만 사용하고 뒷글자는 생략할 수 있다. 예를 들어 /NN은 일반명사(NNG), 고유명사(NNP), 의존명사(NNB)에 모두 일치할 수 있다. 마찬가지로 모든 동사/형용사를 지칭하는 데에는 /V, 모든 접미사에는 /XS를 사용하는 식으로 응용이 가능하다. 추가로 아무 태그도 명시하지 않은 /의 경우 모든 품사의 형태소와 일치할 수 있다.
형태/품사태그: 구체적으로 특정 형태소를 명시하기 위해서는 / 앞에 형태를 지정할 수 있다. 바다/NNG는 일반명사인 바다만을 지칭한다. 마찬가지로 형태가 바다인 모든 형태소를 가리키기 위해서 바다/와 같이 쓸 수 있다.
//SP: 문자 /는 형태소 상으로는 구두점(SP) 품사에 속하므로, / 문자 그 자체를 지칭하기 위해서는 //SP라고 표기한다.
- 연속된 형태소: 여러 형태소가 연속하는 것을 표현하기 위해서는 공백을 두고 각 형태소 표기를 연결한다. 즉, 명사 뒤에 조사가 오는 경우
/NN /J와 같이 쓸 수 있다. 여기서 공백은 형태소 사이를 구분하는 역할만 수행하며 실제 문자열 상의 공백과 일치하지는 않는다! 따라서 /NN /J나 /NN /J나 동일하게 연속하는 명사-조사 패턴을 가리킨다.
- 형태소 수량자: 정규표현식의 수량자
*, +, ?를 지원한다. 단, 이는 형태소를 수식하는데에 쓰인다. 즉 /NNG*의 경우 일반 정규표현식에서마냥 /NN, /NNG, /NNGG와 일치하는 것이 아니라 , /NNG, /NNG /NNG, /NNG /NNG /NNG 등과 일치한다. 나머지 수량자도 마찬가지.
[]: 일반 정규표현식과 마찬가지로 문자집합을 지원한다. 단, 형태나 품사태그 위치에서만 쓰일 수 있다. 예를 들어 /V[VA]는 동사와 형용사만을 지칭하고, [은는]/JX는 보조사 중 은과 는만을 지칭한다.
|: 여러 분기 중 하나와 일치하는 경우를 나타낸다. 즉 /NNG | /VV는 일반명사 혹은 동사 하나와 일치한다. 정규표현식과 마찬가지로 우선순위가 제일 낮다. /NNG | /VV+는 일반명사 오직 하나, 혹은 동사 하나 이상과 일치한다. 형태 내에서는 쓰일 수 없다.
,: 형태 내의 분기를 나타내기 위해 쓰인다. 품사 태그에는 쓰일 수 없다. 하늘,땅/NNG는 일반명사 중 하늘 혹은 땅 중 하나와 일치한다.
.: 형태 내의 글자 하나와 일치한다. 품사 태그에는 쓰일 수 없다. 가./NNG는 가로 시작하는 두 글자 일반명사 전부와 일치한다.
- 형태 내 수량자:
*, +, ?를 형태 내에서도 사용할 수 있다. 예를 들어, 가.+미/NNG의 경우 가로 시작하고 미로 끝나는 세 글자 이상의 모든 일반명사와 일치한다. 또 얼마나?/의 경우 형태가 얼마이거나 얼마나인 모든 형태소와 일치한다.
(): 괄호는 정규표현식과 마찬가지로 캡처그룹을 지정하고, 우선순위를 조절하기 위해 사용된다. 단 형태 내에서는 쓰일 수 없고, 형태소 간에서만 쓰일 수 있다. /VV (/EF | /EC)는 /VV /E[FC]와 동일하다. (/NN /J)+는 명사-조사가 연속하여 여러번 등장하는 패턴(명사-조사, 명사-조사-명사-조사 등)을 나타낸다.
구현
Python쪽에서 쉽게 구현하는 방법으로는, 형태소 분석 결과를 문자열로 직렬화하여 나타낸 다음, 위 형태소용 정규표현식을 적당히 변환하여 이 직렬화된 문자열과 일치시키는 것이 있다. 그러나 궁극적으로는 C++ 내부로 형태소용 정규표현식 일치 엔진을 가지고 들어가는게 성능 상에서 크게 유리할 듯하다. 특히 내부적으로 각 형태소는 고유 id로 변환되어 16~32bit int로 처리되므로, 위의 형태소용 정규표현식을 파싱하여 int 배열에 대한 DFA를 생성하면 의외로 쉽게 구현 가능할지도 모른다.
문제 상황
문장 내에서 형태소를 기반으로 한 특정 패턴을 추출하거나, 일치 여부를 판정하거나, 일부 형태소를 다른 형태소로 교체해야하는 작업을 하는 경우 조건문을 이용하는 수 밖에 없는데, 이 형태소 조건들이 복잡해질 경우 이 작업이 굉장히 고단해지는 문제가 있음.
예를 들어 숫자(SN) 뒤에 의존 명사(NNB)가 오고, 그 뒤에 주격 조사(JKS) 혹은 목적격 조사(JKO)가 나오는 문자열을 탐색한다고 하면
와 같이 장황하게 조건을 나열해야 한다. 게다가 이는 최적화된 탐색 알고리즘을 사용하기도 어려우므로 대량의 텍스트 내에서 탐색을 수행시 비효율적인 문제까지도 있음. 탐색 후 치환의 경우는 훨씬 더 복잡해지는 문제가 있다.
제안
문자열 일치/탐색/치환에 널리 쓰이는 정규표현식 문법을 형태소 탐색용으로 개량하여 사용한다. 그리고 Python3의 표준 정규표현식 모듈에서 제공하는
re.match,re.search,re.sub와 유사한 함수를 제공하여, 한국어 텍스트를 형태소 기반으로 일치/탐색/치환할 수 있도록 한다.형태소용으로 개량된 정규표현식
기본적으로 표현식은 각각의 형태소를 표현하기 위해 쓰이고, 문자에 대한 일치 여부는 되도록 하지 않는 것을 원칙으로 한다.
/품사태그: 품사태그는 앞글자만 사용하고 뒷글자는 생략할 수 있다. 예를 들어/NN은 일반명사(NNG), 고유명사(NNP), 의존명사(NNB)에 모두 일치할 수 있다. 마찬가지로 모든 동사/형용사를 지칭하는 데에는/V, 모든 접미사에는/XS를 사용하는 식으로 응용이 가능하다. 추가로 아무 태그도 명시하지 않은/의 경우 모든 품사의 형태소와 일치할 수 있다.형태/품사태그: 구체적으로 특정 형태소를 명시하기 위해서는/앞에 형태를 지정할 수 있다.바다/NNG는 일반명사인 바다만을 지칭한다. 마찬가지로 형태가 바다인 모든 형태소를 가리키기 위해서바다/와 같이 쓸 수 있다.//SP: 문자/는 형태소 상으로는 구두점(SP) 품사에 속하므로,/문자 그 자체를 지칭하기 위해서는//SP라고 표기한다./NN /J와 같이 쓸 수 있다. 여기서 공백은 형태소 사이를 구분하는 역할만 수행하며 실제 문자열 상의 공백과 일치하지는 않는다! 따라서/NN /J나/NN /J나 동일하게 연속하는 명사-조사 패턴을 가리킨다.*,+,?를 지원한다. 단, 이는 형태소를 수식하는데에 쓰인다. 즉/NNG*의 경우 일반 정규표현식에서마냥/NN,/NNG,/NNGG와 일치하는 것이 아니라,/NNG,/NNG /NNG,/NNG /NNG /NNG등과 일치한다. 나머지 수량자도 마찬가지.[]: 일반 정규표현식과 마찬가지로 문자집합을 지원한다. 단, 형태나 품사태그 위치에서만 쓰일 수 있다. 예를 들어/V[VA]는 동사와 형용사만을 지칭하고,[은는]/JX는 보조사 중은과는만을 지칭한다.|: 여러 분기 중 하나와 일치하는 경우를 나타낸다. 즉/NNG | /VV는 일반명사 혹은 동사 하나와 일치한다. 정규표현식과 마찬가지로 우선순위가 제일 낮다./NNG | /VV+는 일반명사 오직 하나, 혹은 동사 하나 이상과 일치한다. 형태 내에서는 쓰일 수 없다.,: 형태 내의 분기를 나타내기 위해 쓰인다. 품사 태그에는 쓰일 수 없다.하늘,땅/NNG는 일반명사 중 하늘 혹은 땅 중 하나와 일치한다..: 형태 내의 글자 하나와 일치한다. 품사 태그에는 쓰일 수 없다.가./NNG는가로 시작하는 두 글자 일반명사 전부와 일치한다.*,+,?를 형태 내에서도 사용할 수 있다. 예를 들어,가.+미/NNG의 경우가로 시작하고미로 끝나는 세 글자 이상의 모든 일반명사와 일치한다. 또얼마나?/의 경우 형태가얼마이거나얼마나인 모든 형태소와 일치한다.(): 괄호는 정규표현식과 마찬가지로 캡처그룹을 지정하고, 우선순위를 조절하기 위해 사용된다. 단 형태 내에서는 쓰일 수 없고, 형태소 간에서만 쓰일 수 있다./VV (/EF | /EC)는/VV /E[FC]와 동일하다.(/NN /J)+는 명사-조사가 연속하여 여러번 등장하는 패턴(명사-조사, 명사-조사-명사-조사 등)을 나타낸다.구현
Python쪽에서 쉽게 구현하는 방법으로는, 형태소 분석 결과를 문자열로 직렬화하여 나타낸 다음, 위 형태소용 정규표현식을 적당히 변환하여 이 직렬화된 문자열과 일치시키는 것이 있다. 그러나 궁극적으로는 C++ 내부로 형태소용 정규표현식 일치 엔진을 가지고 들어가는게 성능 상에서 크게 유리할 듯하다. 특히 내부적으로 각 형태소는 고유 id로 변환되어 16~32bit int로 처리되므로, 위의 형태소용 정규표현식을 파싱하여 int 배열에 대한 DFA를 생성하면 의외로 쉽게 구현 가능할지도 모른다.