bab2min / kiwipiepy

Python API for Kiwi
Other
282 stars 27 forks source link

글의 형태가 유지된 상태로의 형태소 분석 #161

Closed smbslt3 closed 7 months ago

smbslt3 commented 7 months ago

안녕하세요 kiwipiepy를 여기저기 홍보하며 열심히 쓰고 있는 연구자입니다.

최근 xAI 프레임워크인 lime text를 쓸 일이 있는데, 아시다싶이 요즘 transformer 기반 언어 모델들의 토크나이저는 wordpiece나 sentpiece 기반이라 한국어에서 의미 단위로 토크나이징이 안됩니다. lime에서는 이 부분에 별도의 토크나이저를 입력해주는 방법이 있어 이 부분을 우회할 수 있기는 한데, 문제는 분할 된 토큰을 다시 붙였을 때 원본 문장이 되어야 합니다. 기본값인 str.split()의 경우, 모든 토큰을 붙이고 띄어쓰기를 넣으면 원래 문장이 복원되는 형태입니다. 반면 kiwi의경우 토큰을 단순히 이어붙인다고 원본 문장이 되지 않기 때문에 lime에서는 사용할 수 없습니다.

다른 한국어 토크나이저인 pecab의 경우에는 다음과 같이 두 가지 옵션으로 토크나이징을 할 수 있습니다.

from pecab import PeCab

pecab = PeCab(split_compound=False)
pecab.morphs("가벼운 냉장고를 샀어요.")
>>> ['가벼운', '냉장고', '를', '샀', '어요', '.']

pecab = PeCab(split_compound=True)
pecab.morphs("가벼운 냉장고를 샀어요.")
>>> ['가볍', 'ᆫ', '냉장', '고', '를', '사', 'ㅏㅆ', '어요', '.']

kiwi도 이렇게 원형태를 유지하는 방식으로 tokenizing이 가능할까요?

bab2min commented 7 months ago

@smbslt3 안녕하세요, Kiwi 형태소 분석기는 가능하다면 음절까지 분할하여 최소 형태소를 찾는걸 목표로 구현되어 있기 때문에 아쉽게도 말씀하신 기능은 아직 없습니다. 다만 몇 가지 대안이 있을듯하네요.

  1. Kiwi에서는 형태소 분석된 결과를 다시 합쳐서 온전한 텍스트를 생성하는 Kiwi.join()이라는 메소드를 제공하고 있습니다. 토큰을 텍스트로 변환하는 단계에서 후처리가 가능하다면 Kiwi.join() 사용하는걸 추천해드립니다.
  2. Kiwi 분석 결과를 바로 토큰으로 입력하는 대신, 음절 단위로 쪼개진 부분을 탐지하여 그 부분은 다시 합치는 후처리를 한 뒤에 토큰으로 입력하는 방법도 있을듯합니다. 아래의 PreservingKiwiTokenizer는 후처리 로직을 간단히 구현해본 것입니다.
    
    from kiwipiepy import Kiwi

class PreservingKiwiTokenizer: def init(self): self.kiwi = Kiwi()

def __call__(self, text:str):
    tokens = self.kiwi.tokenize(text)
    is_oversplit = []
    prev_start, prev_end = 0, 0
    for token in tokens:
        oversplit = max(prev_start, token.start) < min(prev_end, token.end)
        if oversplit and is_oversplit:
            is_oversplit[-1] = True
        is_oversplit.append(oversplit)
        prev_start, prev_end = token.start, token.end

    ret = []
    last = 0
    for i, (token, oversplit) in enumerate(zip(tokens, is_oversplit)):
        if oversplit:
            continue
        if last < i and all(is_oversplit[last:i]):
            ret.append(text[tokens[last].start:tokens[i - 1].end])
        ret.append(token.form)
        last = i + 1
    return ret

tokenizer = PreservingKiwiTokenizer() tokenizer("가벼운 냉장고를 샀어요.")

['가벼운', '냉장고', '를', '샀', '어요', '.']



다만 약간 의문이 드는 지점은 `PeCab(split_compound=False)`나 `PreservingKiwiTokenizer`처럼 분해를 한다해도 단순히 띄어쓰기를 넣고 이어붙이는 것만으로는 조사나 어미 앞에 모두 공백이 들어가는 문제가 있어서 이 경우에도 결국 완벽한 복원이 어려워 보이는데, 이게 의도하시는 바가 맞으실까요?
smbslt3 commented 7 months ago

join기능을 쓰면 어떻게 될 것 같긴 했는데 구현해주셔서 감사합니다.

띄어쓰기가 중요한 부분은 아니라 제 경우에는 문제가 되지 않습니다. lime text의 작동 방식이 예를 들어 이진 분류 문제라면, 일부 토큰을 [mask]로 가렸을 때 변화되는 분류 확률값을 그 토큰의 중요도로 간주합니다.

따라서 엄밀히 말하면 원본 문장을 replace 할 수 있는 토큰의 형태를 얻을 수 있으면 됩니다. 샀어요사, ㅆ, 어, 요로 분리되면 는 원본 문장에 없어서 "가벼운 냉장고를 샀어요.".replace('사', '[MASK]')를 실행할 수 없었는데, 지금 주신 코드로는 가능하게 되었습니다.