카테고리 없음

(딥러닝/자연어 처리) 형태소 분석기 Khaiii 사용법: Base 모델 vs Large, 오류 대처, morphs/pos 함수 만들기

미친토끼 2021. 7. 26. 08:00

1) Khaiii 설치

카카오의 형태소 분석기인 Khaiii 설치에 애를 먹었다.

 

데스크탑B는 i7 3770에 우분투 21.04, 우분투 20.04를 번갈아 설치했지만 Khaiii 설치에 실패했다.

노트북C에는 페도라 34가 설치되어 있었지만 Khaiii 설치에 실패해서 우분투 18.04를 설치하고 나서야 Khaiii 설치에 성공했다.

데스크탑A는 i7 10700k에 우분투 20.04가 설치되어 있고, 몇 개월을 사용한 뒤였는데, '우연찮게' Khaii 설치에 성공했다.

A에서 컴파일한 바이너리를 데스크탑 B의 우분투 20.04에 가져다 실행하면 프로세서가 죽으면서 코어덤프가 뜬다. 컴파일러, 라이브러리 버전이 맞지 않을 때 나타나는 증상이다.

 

노트북C(페도라 34를 지우고 우분투 18.04를 설치함)에 카이 설치에 성공했지만 다른 문제가 발생한다. 파이썬 버전이 3.6.5여서 텐서플로 2를 설치할 수가 없다. 필자는 현재 텐서 플로(tensor flow) 2.4.1과 2.5를 사용하는데, 노트북의 우분투 18.04의 파이썬을 3.8.X 대로 업데이트 하는 과정에서 골치 아픈 문제가 생긴다. 그래서 결국, 노트북C에 성공적으로 설치된 '카이'(khaiii 폴더 밑의 'build'폴더 전체)를 우분투 20.04를 사용하는  A, B 컴퓨터에 옮겨놓았다. 유일하게 large model로 설정된 build 이다. 데스크탑A에 '우연히 설치된' 카이는 base model이라, 그마저도 문제가 생길까봐 빌드 작업을 중지한 상태다.

 

결국 현재로선, 우분투 18.04에서 빌드해서 설치한 다음, 그것의 바이너리(build 폴더) 통째로 우분투 20.04로 옮겨가는 것이 가장 현실적인 방법이다. 주의해야 할 것은 자연어 처리를 진지하게 생각하는 분이라면 base 모델이 아니라 large 모델을 설치해야 한다는 것이다. large 모델이 약 정확도 97%, base 모델이 정확도 95%로, 카이는 딥러닝 CNN 알고리즘을 이용해 형태소 분석을 하는데, 정확도 2% 차이가 얼마나 큰 의미인지는 CNN 공부하신 분이라면 알 것이다. 

 

large 모델로 빌드하려면 make resource 대신 make large_resource 를 해야 한다.

 

2) base 모델과 large 모델

 

카이는 morphs, pos 같은 메서드를 지원하지 않는다.

Mecab이나 Okt에서 pos나 morphs를 사용해 형태소 분석을 할 수 있었던 것에 비하면 약간 불편한다.

 

from khaiii import KhaiiiApi  # 카카오 형태소 분석기

api = KhaiiiApi()

sentence = "추돌한 차 옆구리가 찢기고 파란 혈액이 흐른다 네 잘못이 내 잘못인양 덩치 큰 차에 묶여 끌려가는 뒷모습 인생도 어디론가 끌려가고 있다"
analyzed = api.analyze(sentence)
morphs_list = []
for word in analyzed:
  for morph in word.morphs:
    morphs_list.append(morph.lex)
print(morphs_list)

['추', '돌', '하', 'ㄴ', '차', '옆구리', '가', '찢기', '고', '파랗', 'ㄴ', '혈액', '이', '흐르', 'ㄴ다', '너', '의', '잘못', '이', '나', '의', '잘못', '인', '양', '덩치', '크', 'ㄴ', '차', '에', '묶이', '어', '끌려가', '는', '뒷모습', '인생', '도', '어디', '로', '이', 'ㄴ가', '끌려가', '고', '있', '다']

 

'추돌'을 '추'와 '돌'로 나눈 점을 제외하고는 상당히 깔끔하게 분석했다. '어디론가'를 풀어쓰면 '어디로인가'이므로 제대로 분석했다. '파란'은 '파랗다' + 'ㄴ' 이므로 이것도 제대로 해석했다.

 

사실 이 모델은 base 모델이고, 내 컴퓨터에 설치된 large 모델을 사용하려면 라이브러리 파일과 share를 지정해줘야 한다.(카이가 어떻게 설치되었느냐에 따라 다를 수 있다.)

 

from khaiii import KhaiiiApi

so_file = "/home/don/khaiii/build_18.04/lib/libkhaiii.so"
share_dir = "/home/don/khaiii/build_18.04/share/khaiii"
api = KhaiiiApi(so_file, share_dir)

기존의 api = KhaiiiApi()에 공유 라이브러리 파일 위치와 share 폴더명을 지정해준다. 우분투 18.04에서 large 모델로 빌드한 '카이'(build 폴더 통째로)를 우분투 20.04에서 사용하는지라 버전명을 표기해두었다. 필자의 나머지 컴퓨터 2대(우분투 18.04와 우분투 20.04)에서는 이 위치가 제각각 다르다.

 

이렇게 large 모델로 형태소 분석하면 결과가 더 좋아진다.

 

['추돌', '하', 'ㄴ', '차', '옆구리', '가', '찢기', '고', '파랗', 'ㄴ', '혈액', '이', '흐르', 'ㄴ다', '너', '의', '잘못', '이', '나', '의', '잘못', '이', 'ㄴ', '양', '덩치', '크', 'ㄴ', '차', '에', '묶이', '어', '끌려가', '는', '뒷모습', '인생', '도', '어디', '로', '이', 'ㄴ가', '끌려가', '고', '있', '다']

 

이제 '추돌'을 '추'와 '돌'로 분리하지 않았다.

 

3) morphs 와 pos 함수 만들기

카이는 다른 형태소 분석기와는 달리 morphs와 pos 메서드를 지원하지 않으므로, 비슷한 함수를 만들어보자.

 

from khaiii import KhaiiiApi

def khaiii_morphs(string):
  so_file = "/home/don/khaiii/build_18.04/lib/libkhaiii.so"
  share_dir = "/home/don/khaiii/build_18.04/share/khaiii"
  api = KhaiiiApi(so_file, share_dir)

  morphs_list = []
  analyzed = api.analyze(string)
  for word in analyzed:
    for morph in word.morphs:
      morphs_list.append(morph.lex)

  return morphs_list

def khaiii_pos(string):
  so_file = "/home/don/khaiii/build_18.04/lib/libkhaiii.so"
  share_dir = "/home/don/khaiii/build_18.04/share/khaiii"
  api = KhaiiiApi(so_file, share_dir)

  morphs_list = []
  analyzed = api.analyze(string)
  for word in analyzed:
    for morph in word.morphs:
      morphs_list.append((morph.lex, morph.tag))
  
  return morphs_list

이제 간단하게 호출하여 사용할 수 있다.

필자는 이 함수들을 util.py라는 파일 안에 담아서 현재 폴더에 두고 보통 작업한다.

from util import *

sentence = "그는 언덕을 달려 내려가다가 발을 헛디뎌 땅바닥에 곤두박질쳤다."

print(khaiii_morphs(sentence))
print(khaiii_pos(sentence))

['그', '는', '언덕', '을', '달리', '어', '내려가', '다가', '발', '을', '헛디디', '어', '땅바닥', '에', '곤두박질치', '었', '다', '.']
[('그', 'NP'), ('는', 'JX'), ('언덕', 'NNG'), ('을', 'JKO'), ('달리', 'VV'), ('어', 'EC'), ('내려가', 'VV'), ('다가', 'EC'), ('발', 'NNG'), ('을', 'JKO'), ('헛디디', 'VV'), ('어', 'EC'), ('땅바닥', 'NNG'), ('에', 'JKB'), ('곤두박질치', 'VV'), ('었', 'EP'), ('다', 'EF'), ('.', 'SF')]

 

'곤두박질치다'는 동사도 있고 '곤두박질'이라는 명사도 있기에 여기서는 동사로 분석했다.

 

4) 주의할 점 (빈 문자열을 주면 오류가 발생한다)

 

용량이 수 메가바이트, 라인 수는 수천 라인이 되는 문장을 카이로 형태소 분석하다가 이런 오류가 떴다.

 

Traceback (most recent call last):
  File "empty_string.py", line 16, in <module>
    analyzed = api.analyze(s)
  File "/home/don/.local/lib/python3.8/site-packages/khaiii/khaiii.py", line 226, in analyze
    raise KhaiiiExcept(self._last_error())
khaiii.khaiii.KhaiiiExcept

 

원인을 알려주지 않고, 그냥 오류만 발생시키고 중단된 것이라 무엇이 문제인지 또 찾아헤매야 했다.

결국 주범은 텍스트 파일 제일 마지막에 있는 '\n'이었다. '\n'(개행문자) 기준으로 split()을 한 터라, 제일 마지막의 '\n'뒤의 빈 문자열이 그야말로 빈 문자열이 되어 문제를 일으킨 것이다. 다음 코드를 실행해서 행동 양태를 알아보자.

 

from khaiii import KhaiiiApi  # 카카오 형태소 분석기

api = KhaiiiApi()

ss = ["인간은 어리석다",
      "\n",
      "\t",
      " ",
      "어리석음을 아는 자는 영리하다",
      "나는 어리석다",
      "\u200b",
      "고로 나는 영리하다",
      "\u3000" ]

for s in ss:
  analyzed = api.analyze(s)
  morphs_list = []
  for word in analyzed:
   for morph in word.morphs:
     morphs_list.append(morph.lex)
  print(morphs_list)

실행 결과: 


['인간', '은', '어리석', '다']
Traceback (most recent call last):
  File "empty_string.py", line 16, in <module>
    analyzed = api.analyze(s)
  File "/home/don/.local/lib/python3.8/site-packages/khaiii/khaiii.py", line 226, in analyze
    raise KhaiiiExcept(self._last_error())
khaiii.khaiii.KhaiiiExcept

 

첫줄만 분석해주고, 다음 줄 '\n'을 형태소 분석하다가 오류가 발생했다. 카이 형태소 분석기는 개행문자, 탭문자, 공백, 확장 공백('u3000')만 있는 문자열은 분석하지 못하고 오류를 발생한다.

이상한 특수 문자들은 미리 전처리할 필요가 있다.

 

s = s.replace(u"\u200b", "") # 폭없는 공백 문자 제거
s = s.replace(u"\u3000", "") # 특수 공백 제거

 

그리고 개행문자, 공백문자, 탭문자 등을 사전에 제거하고 분석 대상이 빈 문자열("")일 경우 건너뛰는 코드를 추가하면 이런 오류를 해결할 수 있다.

 

from khaiii import KhaiiiApi  # 카카오 형태소 분석기

api = KhaiiiApi()

ss = ["인간은 어리석다",
      "\n",
      "\t",
      " ",
      "어리석음을 아는 자는 영리하다",
      "나는 어리석다",
      "\u200b",
      "고로 나는 영리하다",
      "\u3000" ]

for s in ss:
  s = s.replace(u"\u200b", "") # 폭없는 공백 문자 제거
  s = s.replace(u"\u3000", "") # 특수 공백 제거
  if s.strip() == "":
    continue

  analyzed = api.analyze(s)
  morphs_list = []
  for word in analyzed:
   for morph in word.morphs:
     morphs_list.append(morph.lex)
  print(morphs_list)

['인간', '은', '어리석', '다']
['어리석', '음', '을', '알', '는', '자', '는', '영리', '하', '다']
['나', '는', '어리석', '다']
['고로', '나', '는', '영리', '하', '다']

 

반복문 내의 첫 부분에서 특수 문자를 찾아 제거하고, strip() 함수를 사용해 좌우 공백(개행, 탭 등)을 제거하고, 그 다음에 그것이 빈 문자열일 경우, 형태소 분석을 하지 않고 continue로 건너뛰면 된다. 

 

5) 전후의 띄어쓰기에 예민한 '카이'

 

'카이'는 CNN방식을 사용해서, 분석하려는 음절의 좌우로 몇 개의 음절을  참고해 현재 음절의 형태소 분석을 한다고 한다. 그래서 그런지 앞에 어떤 띄어쓰기가 오느냐에 따라 분석 대상의 형태소 분석이 달라지는 것 같다.

 

from khaiii import KhaiiiApi  # 카카오 형태소 분석기

api = KhaiiiApi()

ss = ["추돌한 차 옆구리가 찢어져",
      "내가 추돌한 차 옆구리가 찢어져",
      "\"추돌한 차 옆구리가 찢어져",
      "고속도로에서 추돌한 차 옆구리가 찢어져",
      "뛰어내려가서 치솟아올라 내달려뛰어날아올라가서",
      "뛰어 내려가서 치달아 올라가서 내달려 뛰어 날아 올라가서"]

for s in ss:
  analyzed = api.analyze(s)
  morphs_list = []
  for word in analyzed:
   for morph in word.morphs:
     morphs_list.append(morph.lex)
  print(morphs_list)

['추', '돌', '하', 'ㄴ', '차', '옆구리', '가', '찢어지', '어']
['내', '가', '추돌', '하', 'ㄴ', '차', '옆구리', '가', '찢어지', '어']
['"', '추돌', '하', 'ㄴ', '차', '옆구리', '가', '찢어지', '어']
['고속도로', '에서', '추돌', '하', 'ㄴ', '차', '옆구리', '가', '찢어지', '어']


['뛰어내리', '어', '가', '아서', '치솟아오르', '아', '내달리', '어', '뛰어날아오르라가', '아서']
['뛰', '어', '내려가', '아서', '치닫', '아', '올라가', '아서', '내달리', '어', '뛰', '어', '날', '아', '올라가', '아서']

 

* Base 모델을 사용했지만, '추돌' 앞에 어떤 글자가 오느냐에 따라 '추돌'에 대한 형태소 분석이 달라진다. 앞에 어떤 단어가 있을 경우, 정확도가 높은 것 같다.

 

* 우리말에서는 동사를 여러 개 이어붙여 연속적인 동작을 묘사하는데, 문학 작가들의 경우에도, 문학 편집자의 경우에도 이 띄어쓰기가 일정하지 않다. 이럴 경우, 형태소 분석이 달라진다. 

 

6. 복합 명사의 형태소 분석

 

개인적으로 복합 명사의 경우 개별 명사로 분해되는 것을 좋아하는데, 그 이유는 '겨울바다'와 같이 애초에 '겨울'과 '바다'가 합쳐져 이 단어를 이루었으며 뜻도 두 단어의 합침에서 유래한다. 그리고 단어 사전에 '겨울'과 '바다'로 따로 등록되어 있는 게 좋은데, '봄바다', '가을바다', '여름바다', '우주바다' 같은 단어들이 추가될 때 이것들이 각각 개별 단어로 분리되어 단어 사전에 등록된다면, 애초의 단어 합성을 머신러닝이 학습할 수 있고 (적어도 참고할 수 있고), 단어 사전이 작아져서 문장 만들기에 한결 수월하다는 점도 있다. 감정을 처리할 수 없는 간단한 모델의 경우, '겨울바다'를 '겨울'과 '바다'와 상관없는 전혀 다른 단어로 인식한다는 게 일차적인 문제이긴 하다.

 

from khaiii import KhaiiiApi  # 카카오 형태소 분석기

api = KhaiiiApi()

ss = ["대천 해수욕장의 저녁노을, 저녁놀, 가을바다",
      "수리산자락에서 만난 겨울나무 여름열매가",
      "여름녹음, 가을나무, 겨울하늘, 전국코딩취미인공지능밀당연합"]

for s in ss:
  analyzed = api.analyze(s)
  morphs_list = []
  for word in analyzed:
   for morph in word.morphs:
     morphs_list.append(morph.lex)
  print(morphs_list)

['대천', '해수욕장', '의', '저녁노을', ',', '저녁놀', ',', '가을바다']
['수리산', '자', '락', '에서', '만나', 'ㄴ', '겨울나무', '여름', '열매', '가']
['여름', '녹음', ',', '가을나무', ',', '겨울', '하늘', ',', '전국', '코딩', '취미인공지능밀', '당', '연합']

 

* 아쉬운 결과가 아닐 수 없다. '밀당'은 최신 단어니까 이해하고, '자락'을 쪼갠 것은 잘못이고, '가을바다', '가을나무'는 국어사전에 없는 단어인데도 그대로 붙여놓았다는 것은 확실히 아쉽다.

 

*  위는 base 모델이고, large 모델로 다시 형태소 분석해보자.

 

from util import *

ss = ["대천 해수욕장의 저녁노을, 저녁놀, 가을바다",
      "수리산자락에서 만난 겨울나무 여름열매가",
      "\n",
      "\t",
      " ",
      "여름녹음, 가을나무, 겨울하늘, 전국코딩취미인공지능밀당연합"]

for s in ss:
  if s.strip() == "": continue
  print(khaiii_morphs(s))

['대천', '해수욕장', '의', '저녁노을', ',', '저녁놀', ',', '가을바다']
['수리', '산자락', '에서', '만나', 'ㄴ', '겨울나무', '여름', '열매', '가']
['여름', '녹음', ',', '가을나무', ',', '겨울', '하늘', ',', '전국코딩취미인공지능', '밀당연합']

 

* 결과가 달라졌다. 복합 명사는 여전히 붙여쓴 그대로 분석하는 편이고, '수리산'이 고유명사로 등록되지 않았는지, '수리산', '자락'으로 분리되지 않고 '수리', '산자락'으로 분리되었다. '여름녹음'과 '겨울하늘'은 각기 2음절 단어로 분리되었고, '전국...'도 10음절 + 4음절 단어로 분리되었다. base 모델보다 약간 나아진 듯하다.