Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

형태소 기반 일치/탐색/치환 기능 추가 #70

Open
bab2min opened this issue Mar 18, 2022 · 0 comments
Open

형태소 기반 일치/탐색/치환 기능 추가 #70

bab2min opened this issue Mar 18, 2022 · 0 comments
Assignees
Labels
enhancement New feature or request

Comments

@bab2min
Copy link
Owner

bab2min commented Mar 18, 2022

문제 상황

문장 내에서 형태소를 기반으로 한 특정 패턴을 추출하거나, 일치 여부를 판정하거나, 일부 형태소를 다른 형태소로 교체해야하는 작업을 하는 경우 조건문을 이용하는 수 밖에 없는데, 이 형태소 조건들이 복잡해질 경우 이 작업이 굉장히 고단해지는 문제가 있음.
예를 들어 숫자(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를 생성하면 의외로 쉽게 구현 가능할지도 모른다.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant