디큐 NLP를 파이썬에서 써보기-2

더 자세한 활용법 / 결과 분석

Posted by 윤기현 on February 27, 2021 · 19 mins read

쓰임새 알아보기

이전 편에 이어서 계속됩니다.

실제로 분석해보기(III)

마지막으로 detail을 한번 써보도록 할까요?

from deeqnlpy import Tagger
t = Tagger()
pos_tuples = t.pos("그 가수는 밍기뉸데요.", flatten=False, detail=True, join=True)
print(*pos_tuples, sep="\n") 
['그/MMD:0.977']
['가수/NNG:0.915', '는/JX:0.950']
['밍기뉴/NNP:0.464#OUT_OF_VOCAB', '이/VCP:0.980', 'ㄴ데/EC:0.408', '요/JX:0.979', './SF:0.974']

디큐 NLP는 조금 더 많은 정보를 보여줍니다. 0.980 이 숫자는 확신을 나타내는 숫자입니다. 딥러닝을 기반으로 하는 디큐NLP에서 스스로 확신하는 정도를 숫자로 나타냅니다. 높으면 높을수록 개연성이 높습니다.

밍기뉴에는 좀 특이한 OUT_OF_VOCAB 이라는 표시가 붙어 있습니다. “밍기뉴”라는 단어는 공부한 단어가 아니라는 뜻입니다. 즉, 공부하지 않은 단어 “밍기뉴”를 “NNP” 즉 고유명사로 미뤄 집작하기는 하는데, 디큐 NLP는 조금 조심스러워 하고 있습니다. 0.464 정도의 확신을 가지고 있을 뿐입니다. OUT_OF_VOCAB 이런 표시가 없으면 공부한 범위라는 뜻입니다. 대략 디큐 NLP는 20만 단어를 공부하고, 그중에서 9만 단어 정도만 기억하고 있습니다.

실제로 분석해보기(III)

나머지 함수들, morphs(), nouns(), verbs()는 예상한 것과 다르지 않습니다. 앞서 함수의 이름과 기능을 설명했으니, 결과를 직접 눈으로 확인해볼까요?

>>> print(t.morphs("그 가수는 밍기뉸데요."))
['그', '가수', '는', '밍기뉴', '이', 'ㄴ데', '요', '.']
>>> print(t.nouns("그 가수는 밍기뉸데요."))
['가수', '밍기뉴']
>>> print(t.verbs("그 가수는 밍기뉸데요."))
[]

분석 결과를 재활용하기

지금 호출한 예제를 보면, “그 가수는 밍기뉸데요.” 라는 문장을 매번 함수의 첫번째 인자로 넘겨주었습니다. (1) 형태소를 분석할 때도, (2) 형태소만 분리할 때도, (3) 명사를 구할 때도, (4) 동사를 구할 때도 늘 같은 문장을 넘겨주었습니다.

조금만 더 깊게 들어가보면, 동일한 문자을 줄 때 나올 결과는 늘 같은 것입니다. 같아야 합니다. 그러면, 형태소 분석을 한 결과 안에서 형태소 출력을 다양하게 하거나, 명사를 구할 방법은 없을까요. 매번 문장을 인자로 넘겨서 함수를 호출하지 않고 한번만 쓸 수는 없을까요?

바로 이런 목적으로 만들어진 것이 Tagged 클래스입니다. 아래 예제에서 Tagged 클래스를 사용합니다.

from deeqnlpy import Tagger
t = Tagger()

# 형태소 분석을 한 결과를 tagged 변수로 받습니다.
tagged = t.tag("그 가수는 밍기뉸데요.")
# tagged가 도대체 뭔지 알아봅니다.
help(tagged)
# pos()를 구해봅니다.
tagged.pos()
tagged.pos(join=True, flatten=False)
tagged.pos(join=False, flatten=False, detail=True)
# 아까 봤던 nouns(), verbs(), morphs()는 그냥 됩니다.
tagged.nouns()
tagged.verbs()
tagged.morphs()

# 새로운 함수 msg(), 왜 이 함수의 이름은 msg()일까요? 무슨 메시지를 전달하길래..
sents = tagged.msg()
# 아하, json 방식으로 나오는 함수들이 몇가지가 있군요.
json_obj = tagged.as_json()
json_str = tagged.as_json_str()
# 아하, json을 바로 파일로 출력할 수도 있군요.
f = open('out.json', "w+")
tagged.print_as_json(out=f)
close(f)

위에서 보면, tagged 라는 변수로 받은 Tagged 객체에 대해서 pos(), nouns(), morphs()와 같은 메서드는 이미 살펴본 사용법과 같습니다. 새로운 msg(), as_json(), print_as_json()과 같은 메서드들이 추가되어 있는 것을 볼 수 있습니다. 이 메서드는 Tagged 클래스에서만 쓸 수 있습니다.

pos(), nouns(), morphs()와 같은 메서드는 오픈소스로 한국어 형태소 분석기를 패키징해 놓은 KoNLPy와 호환성을 유지하기 위해서 만들어진 것입니다. 상용 수준에서 쓰기 위해서 또는 디큐 NLP의 특성을 좀 더 잘 활용하기 위해서 Tagged 클래스를 만들었습니다.

약간 다른 방법들

여기에서 pos(), morphs(), nouns(), verbs() 등은 이미 앞에서 살펴본 바와 같습니다. 다른 점이 있다면, 호출할 때마다 문장을 넘겨주는 것이 아니라 이미 가져온 응답 결과물에서 보는 방법을 요리 조리 바꿔보는 것입니다. 이게 좀 더 상식적인 방법입니다.

json으로 출력하는 함수들

tagged.as_json()

결과물은 파이썬의 JSON 내장 객체를 사용할 수 있게 해줍니다. 바로 이렇게 생겼습니다.

>>> tagged.print_as_json()
{
  "sentences": [
    {
      "text": {
        "content": "그 가수는 밍기뉸데요."
      },
      "tokens": [
        {
          "text": {
            "content": "그"
          },
          "morphemes": [
            {
              "text": {
                "content": "그"
              },
              "tag": "MMD",
              "probability": 0.9767288
            }
          ],
          "lemma": "그",
          "tagged": "그/MMD"
        },
        {
          "text": {
            "content": "가수는",
            "beginOffset": 2
          },
          "morphemes": [
            {
              "text": {
                "content": "가수",
                "beginOffset": 2
              },
              "tag": "NNG",
              "probability": 0.91464984
            },
            {
              "text": {
                "content": "는",
                "beginOffset": 4
              },
              "tag": "JX",
              "probability": 0.949577
            }
          ],
          "lemma": "가수",
          "tagged": "가수/NNG+는/JX"
        },
        {
          "text": {
            "content": "밍기뉸데요.",
            "beginOffset": 6
          },
          "morphemes": [
            {
              "text": {
                "content": "밍기뉴",
                "beginOffset": 6
              },
              "tag": "NNP",
              "probability": 0.46444348,
              "outOfVocab": "OUT_OF_VOCAB"
            },
            {
              "text": {
                "content": "이",
                "beginOffset": 6
              },
              "tag": "VCP",
              "probability": 0.9800278
            },
            {
              "text": {
                "content": "ㄴ데",
                "beginOffset": 6
              },
              "tag": "EC",
              "probability": 0.40768662
            },
            {
              "text": {
                "content": "요",
                "beginOffset": 6
              },
              "tag": "JX",
              "probability": 0.97902447
            },
            {
              "text": {
                "content": ".",
                "beginOffset": 6
              },
              "tag": "SF",
              "probability": 0.9742079
            }
          ],
          "lemma": "밍기뉴",
          "tagged": "밍기뉴/NNP+이/VCP+ㄴ데/EC+요/JX+./SF"
        }
      ]
    }
  ],
  "language": "ko_KR"
}

꼼꼼하게 읽어보셨나요? 이제까지 썼던 함수들에 비해서 엄청 복잡한가요? 복잡하지 않습니다. 구조는 단순합니다.

문장 -> [ 어절 ] -> [ 형태소 ] -> { 텍스트 조각, 태그, 확률, 미등록단어 정보 }

정리하면,

  • 전체는 여러 문장으로 나뉩니다.
  • 한 문장은 여러 어절로 만들어져 있습니다. 어절에는 lemma라는 기본형 정보가 있습니다.
  • 한 어절은 하나 이상의 형태소로 되어 있습니다. 형태소는 품사 태그와 확신 확률, 미등록 단어 처리 방식 등에 대한 정보가 들어 있습니다.

JSON 객체에 접근해보기

# json 객체로 만듭니다.
jo = tagged.as_json()

## 첫번째 문장을 꺼냅니다.
sent1 = jo['sentences'][0]

## 3번째 어절을 찾아갑니다.
token3 = sent1['tokens'][2]

## 각 형태소 객체의 이름과 태그를 출력해봅니다.
for m in token3.morphemes:
    print(m['text']['content'], m['tag'])

위와 같이 전통적인 JSON 객체를 이용해서 쓸 수 있습니다. 파이썬의 JSON 객체는 dict 형식을 기본으로하기 때문에 문자열을 키로 사용해서 원하는 정보에 접근할 수 있습니다. 위에서 sentences, tokens, text, content, tag 등의 문자열이 객체 내부에서 정보를 얻기 위해서 사용된 키입니다.

어떠신가요? 좀 생각보다 쉽습니까? 구조는 여전히 간단합니다.

msg(), 전정 전하고 싶은 말?

msg() 함수는 뭔가 메시지를 전달하고 싶어서 이렇게 이름을 붙이지는 않았습니다. 프로토콜버퍼, protobuf라는 기술에서 나온 말입니다. protobuf는 JSON 포맷으로 표현 가능한 모든 데이터를 표현할 수 있으면서도 네트워크로 전송할 때 크기를 작게 할 수 있습니다. 프로토콜 버퍼에서 가장 작은 데이터 구조의 단위가 Message 입니다.

msg()라는 함수의 이름은 프로트콜 버퍼의 메시지 형식인 Message 객체를 꺼내기 위해서 붙였습니다. 즉, Tagged 결과에서 메시지 객체를 꺼내는 작업을 하므로 그렇게 붙인 것입니다.

구조는 위의 as_json() 함수를 실행한 결과 만들어진 JSON 객체와 동일합니다.

그 최상위에 문장(sentences)이 있고, 어절(tokens)이 있고, 다시 형태(morphemes) 분석 단위가 있습니다.

프로토콜 버퍼의 메시지(Message) 객체로 꺼내온 경우에는 문자열을 키로 사용하지 않고, 변수를 바로 사용할 수 있습니다. 아래와 같이 바로 변수명을 쓸 수 있습니다. 엄청난 차이입니다. 바로 이런 방식으로 파이썬 변수 이름으로 쓸 수 있도록 처리해주는 기술이 프로토콜 버퍼라고 이해해도 크게 틀리지 않습니다. 이 방식은 거의 모든 언어에 적용이 되며, C++, 자바, 파이썬, C#, NodeJS, 웹기반 자바스크립트, Go, Rust, PHP, Dart 등에서도 사용할 수 있습니다.

구글에서 프로토콜 버퍼는 매우 광범위하게 사용되는 기술입니다. 구글의 딥러닝 프레임워크인 텐서플로의 내부 데이터 포맷도 이것입니다.

m = tagged.msg()
print(len(m.sentences[0].tokens))
# 여기서 출력되는 포맷은 protocol buffer에서 정의한 텍스트 포맷입니다. 왼쪽에서 변수명을 볼 수 있습니다.

이 방식은 JSON를 다루는 방식보다는 훨씬 더 가독성이 좋고, 오타로 인해서 엉뚱한 결과를 가져오는 등 오류를 일으킬 가능성도 낮습니다. 다만, 자바, C++와 같은 언어를 사용하는 경우에는 변수명을 자동 완성시켜주는 도구들이 많지만, 파이썬의 경우는 변수명을 자동 완성시켜 주는 환경은 없습니다.

여기에 쓸 수 있는 변수 및 타입을 모두 정리하였습니다.

Sentences

속성 타입 설명
sentences 배열 sentencs[0]과 같은 방법으로 sentencs에 접근

TextSpan

속성 타입 설명
content string 텍스트 조각 문자열
begin_offset integer 원 요청 문장에서의 위치, 이것은 요청이 UTF8, UTF16, UTF32이냐에 따라 다릅니다. 파이썬 버전 3은 내부적으로 UTF32 인코딩을 사용합니다. 유니코드 문자열과 동일합니다.

Sentence

문장 타입입니다.

속성 타입 설명
text 텍스트 조각 객체 텍스트 조각 객체
text.content string 문장 문자열
text.begin_offset integer 문장의 시작 위치
tokens token 객체의 배열 어절의 배열
tokens[0] token 객체 문장을 하나 이상의 어절로 분리함

Token

어절 타입입니다.

속성 타입 설명
text TextSpan 객체 토큰 분리에 대한 시작
text.content string 토큰 내용
text.begin_offset integer 토큰의 시작 위치
lemma string 원형
tagged string 태깅 형태로 만든 문자열, 향후 폐기될 예정입니다.
morphemes 형태소 배열  
morphemes[0] 행태소(Morpheme) 객체 형태소 객체

Morpheme

형태소 타입입니다.

속성 타입 설명
text TextSpan 객체 형태소 텍스트 조각
text.content string 형태소 내용
text.begin_offset integer 형태소 시작 위치
tag enum 형태소 내용, 모두 47개의 품사가 사용됩니다. 품사표는 아래를 참조하세요.
probability float 형태 분류의 정확도
out_of_vocab enum 형태 분류 중 사전 활용의 결과 표시.
IN_WORD_EMBEDDING: 워드임베딩에 포함된 내용
OUT_OF_VOCAB: 자동 추측
IN_CUSTOM_DICT: 사용자 제공 사전에 있는 내용
IN_BUILTIN_DICT : 기본 사전에 포함된 내용

Tag (enum)

국립국어원은 2019년 모두의 말뭉치에서 새로운 형태소 태깅 규칙을 정의하였습니다. 디큐 NLP는 그 태깅 셋을 사용하여 동작합니다.

아래 표에서 내부인덱스가 사용될 수도 있습니다. msg()에서 받은 객체를 json으로 변환할 때, 아래의 enum 이름으로 출력하는 것이 기본이지만, 경우에 따라서 숫자로 나타낼 수도 있습니다. 숫자는 참고하시면 됩니다.

이름 내부인덱스 설명
NNG 24 일반 명사
NNP 25 고유 명사
NNB 23 의존 명사
NP 26 대명사
NR 27 수사
NF 22 명사 추정 범주
NA 21 분석불능범주
NV 28 용언 추정 범주
VV 41 동사
VA 38 형용사
VX 42 보조 용언
VCP 40 긍정 지정사
VCN 39 부정 지정사
MMA 18 성상 관형사
MMD 19 지시 관형사
MMN 20 수 관형사
MAG 16 일반 부사
MAJ 17 접속 부사
IC 6 감탄사
JKS 13 주격 조사
JKC 9 보격 조사
JKG 10 관형격 조사
JKO 11 목적격 조사
JKB 8 부사격 조사
JKV 14 호격 조사
JKQ 12 인용격 조사
JX 15 보조사
JC 7 접속 조사
EP 3 선어말 어미
EF 2 종결 어미
EC 1 연결 어미
ETN 5 명사형 전성 어미
ETM 4 관형형 전성 어미
XPN 43 체언 접두사
XSN 46 명사 파생 접미사
XSV 47 동사 파생 접미사
XSA 45 형용사 파생 접미사
XR 44 어근
SF 30 마침표,물음표,느낌표
SP 35 쉼표,가운뎃점,콜론,빗금
SS 36 따옴표,괄호표,줄표
SE 29 줄임표
SO 34 붙임표(물결,숨김,빠짐)
SW 37 기타기호 (논리수학기호,화폐기호)
SL 32 외국어
SH 31 한자
SN 33 숫자

OutOfVocab (enum)

디큐 NLP는 형태소 태깅을 할 때, 특정 단어를 식별하는 방법을 표시해줍니다. 사용자 사전을 만들 때, 유용하게 쓸 수 있습니다. 프로토콜버퍼는 데이터를 전송할 때, 기본값은 전송하지 않습니다. out_of_vocab 값이 표시되지 않으면 0, 즉 IN_WORD_EMBEDDING 값이 사용된 것입니다. 명사 및 일부 동사, 형용사를 제외하고는 대부분 다 여기에 속합니다.

이름 내부인덱스 설명
IN_WORD_EMBEDDING 0 워드임베딩에 포함된 내용
OUT_OF_VOCAB 1 자동 추측
IN_CUSTOM_DICT 2 사용자 제공 사전에 있는 내용
IN_BUILTIN_DICT 3 기본 사전에 포함된 내용

konlpy에서 deeqnlpy로 바꾸기

기존에 한국어 NLP, 형태 분석도구는 몇 가지가 있습니다. 가장 널리 쓰이는 것이 Mecab입니다. 기존에 이미 Mecab을 쓰고 계신 분들의 경우, 기존 코드를 조금만 고쳐도 디큐 NLP로 옮겨올 수 있습니다.

매우 간단합니다.

패키지명만 바꾸기

from konlpy.tag import Mecab

tagger = Mecab()
tagger.pos('그 가수는 밍기뉸데요!')

이전 코드가 이와 같이 되어 있을 때, 아래와 같이 바꾸면 됩니다.

from deeqnlpy import Tagger

tagger = Tagger()
tagger.pos('그 가수는 밍기뉸데요!')

Tagged 클래스를 이용해보기

tag() 함수를 이용해서 처리하는 것이 조금 더 좋은 방법입니다. 일단 tag 함수를 쓰기 시작하면 다른 메서드들도 슬슬 써볼 수 있을 것입니다.

from deeqnlpy import Tagger

tagger = Tagger()
tagger.tag('그 가수는 밍기뉸데요!').pos()

마치며

새로운, 또 다른 형태소 분석엔진은 아닙니다. 디큐 NLP는 커다란 줄기가 될 것입니다. 인공지능 시대에 구어를 잘 다루는 엔진이 핵심적으로 중요합니다.

다음 기회에 왜 디큐NLP가 다른지를 설명하도록 하겠습니다.

deeqnlpy의 소스는 깃헙에 공개되어 있습니다.