[LM] 한국어 Tokenizer (Python)

혜원·2021년 8월 10일
0

Language Model

목록 보기
1/1

✔ Language Model

단어들로 이루어진 문장인 단어 시퀀스확률을 할당하는 모델
즉, 가장 자연스러운 단어 시퀀스를 찾아내는 모델

Language model이란 단어 시퀀스에 확률을 할당해 가장 자연스러운 문장을 찾아내는 모델이다. 예를 들어보면 다음과 같다.

1. 기계 번역 (Machine Translation)

P ( 나는 회사에 가다 ) < P ( 나는 회사에 간다 )

2. 음성 인식 (Auto Speech Recognition)

P ( 나는 메론을 먹는다 ) > P ( 나는 메롱을 먹는다)

3. 오타 교정 (Spell Correction)

P ( 그가 당신을 살렸다 ) > P ( 그가 당신을 살렀다 )

위의 세 경우에서 언어 모델은 각 문장의 확률을 파악하고 확률이 더 높은 문장을 선택한다.

또한 어떤 단어가 나왔을 때 뒤에 나올 단어들의 확률을 계산해 다음 단어를 예측할 수 있는데 이는 우리가 이미 사용하고 있는 구글이나 휴대폰에서 지원하는 자동 완성 기능에도 활용된다.

✔ Tokenization (토큰화)

자연어 처리에서는 말뭉치를 코퍼스(Corpus)라고 부르는데 이런 코퍼스는 토큰(token)이라고 불리는 단위로 나뉜다. 이 때, 코퍼스를 토큰으로 나누는 작업을 토큰화(Tokenization)라고 한다. 토큰의 단위는 상황에 따라 달라지지만, 영어에서는 띄어쓰기를 기준으로 나눠 토큰의 기준을 단어로 한다. 예를 들면 다음과 같다.

Input : To be or not to be, that is the question.
Output : To, be, or, not, to, be, that, is, the, question

예시에서는 마침표, 쉼표, 물음표 등과 같은 기호는 제외시켜서 토큰화하였다.

✔ 한국어 토큰화의 어려움

한국어의 토큰화는 영어나 다른 언어에 비해 어려운 점이 많다.

첫 번째 이유는 한국어는 띄어쓰기가 제대로 지켜지지 않는 경우가 많다는 것이다.
따라서 영어와 같이 띄어쓰기를 기준으로 토큰을 만들기에는 어려움이 많아진다.

두 번째 이유는 한국어는 교착어이기 때문이다.
교착어라는 것은 형태론적 관점으로 언어를 분류했을 때의 한 유형으로, 어근과 접사에 의해서 단어의 기능이 결정되는 언어의 형태이다. 즉, 예를 들면 '나'라는 단어가 있을 때 실제로 단어 시퀀스에서는 '나는', '나를, '나만', '나도' 등과 같이 조사가 붙어서 굉장히 다양한 형태가 나타난다. 따라서 어절을 기준으로 토큰을 사용하기에 어려움이 많다.

따라서 한국어의 토큰화는 어절을 기준으로 하지 않고, 형태소 기준으로 토큰화를 많이 실시한다.
또한, 영어는 단어 내에서도 알파벳이라는 명확한 기준이 있지만 한국어는 기본적으로 글자 하나하나를(음절을) 자음과 모음으로도 나눌 수 있다. 이런 어려움으로 인해 아직까지 한국어의 토큰화는 어떤 기준이 정해져있지 않고, 여러가지 방법으로 진행되고 있다.

✔ 한국어 Tokenizer

본문에서는 앞에서 소개한 어절 단위(띄어쓰기로 구분), 음절, 자음, 모음 단위로 한국어를 토큰화해보고자 한다. 특히, 자음과 모음 단위로 토큰화를 하는 것에서는 초성, 중성, 종성으로 구분하고자 한다. 언어는 Python을 사용하였다.

1. 어절 단위로 토큰화

어절은 띄어쓰기로 구분이 되기 때문에 split 함수를 이용해 간단하게 구현할 수 있다.

def word_tokenizer(s):
    return s.split(' ')
    
print(word_tokenizer('나는 어제 치킨을 먹었다'))

출력 결과는 [ '나는', '어제', '치킨을', '먹었다' ] 로 어절 단위로 구분이 잘 된 것을 확인할 수 있다.

2. 음절 단위로 토큰화

음절은 한 글자씩 받아오는 것이기 때문에 replace 함수와 for문을 이용해 간단히 구현할 수 있다.

def syllable_tokenizer(s):
    temp = s.replace(' ', '')
    result = []
    for c in temp:
        result.append(c)
    return result

print(syllable_tokenizer('나는 어제 치킨을 먹었다'))

출력 결과는 [ '나', '는', '어', '제', '치', '킨', '을', '먹', '었', '다' ]로 음절 단위로 구분이 잘 된 것을 확인할 수 있다.

3. 자음, 모음 단위로 토큰화

한국어를 자음과 모음으로 토큰화하기 위해서는 유니코드에 대해서 알아야한다.

유니코드(Unicode) :
전세계적으로 사용되는 모든 문자 집합을 하나로 모아 만든 코드

문자 집합이란 표현해야 할 문자들을 정하고, 그 순서를 지정한 것으로 우리나라의 문자 집합은 '가'~'힣'의 순서로 모든 문자들이 지정되어 있다.

✔ 유니코드 목록에서 한글의 범위

한글을 표현하는 방법이 여러가지가 있는데 위 사진에서의 한글 자모, 한글 자모 확장과 같은 것은 초성과 종성을 분리하지 않고, 자음과 모음으로 구분하여 표시한 것이다. 한글 소리마디는 초성, 중성, 종성으로 분리가 가능하고 현대 한글에서 표현 가능한 11,172자('가'~'힣')을 모두 포함한다. 본문에서는 초성, 중성, 종성 단위로 글자를 분리하기 위해 한글 소리마디를 이용하였다.

한글의 초성, 중성, 종성은 각각 19개, 21개, 28개로 구성되어 있고 각 문자의 코드 값은 다음과 같이 계산할 수 있다. 이 때, 초성, 중성, 종성은 각각의 인덱스를 나타내고, 0xAC00은 가장 첫번째 글자인 '가'의 코드값이다.

문자의 코드 값 : ( ( 초성 * 21 ) + 중성 ) * 28 + 종성 + 0xAC00

위 코드 값을 구하는 식을 통해서 초성, 중성, 종성의 인덱스를 얻을 수 있고, 이를 이용해 글자의 초성, 중성, 종성을 얻을 수 있다.

3.1 초성, 중성, 종성 단위로 토큰화

먼저 초성, 중성, 종성이 어떤 순서로 인덱스가 붙어있는지 선언해주고 시작해야한다.

NO_JONGSUNG = '*'

CHOSUNGS = ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ',
'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']
JOONGSUNGS = ['ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ',
'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ']
JONGSUNGS = [NO_JONGSUNG,  'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ',
'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ']

N_CHOSUNGS = 19
N_JOONGSUNGS = 21
N_JONGSUNGS = 28

FIRST_HANGUL = 0xAC00 #'가'
LAST_HANGUL = 0xD7A3 #'힣'

유니코드의 인덱스에 따라서 CHOSUNGS, JOONGSUNGS, JONGSUNGS 리스트를 선언했고, 종성이 없을 때를 표시하기 위해서 * 문자를 사용하였다. 초성, 중성, 종성의 개수를 선언하고 첫 번째 한글인 '가'의 유니코드와 마지막 한글인 '힣'의 유니코드도 선언해주었다.

def cho_joong_jong_tokenizer(s):
    result = []
    for c in s:
        if ord(c) < FIRST_HANGUL or ord(c) > LAST_HANGUL:
            result.append(c)
        else:
            code = ord(c) - FIRST_HANGUL
            jongsung_index = code % N_JONGSUNGS
            code //= N_JONGSUNGS
            joongsung_index = code % N_JOONGSUNGS
            code //= N_JOONGSUNGS
            chosung_index = code

            result.append(CHOSUNGS[chosung_index])
            result.append(JOONGSUNGS[joongsung_index])
            result.append(JONGSUNGS[jongsung_index])

    return ''.join(result)
    
print(cho_joong_jong_tokenizer('나는 어제 치킨을 먹었다'))

ord( ) 함수는 문자를 입력으로 받아 그 문자의 유니코드를 반환해주는 함수이다. if문에서 문자의 유니코드가 우리가 사용하고자 하는 한글 소리마디에 속하는지 확인하고, 한글이 아니라면 초성, 중성, 종성으로 변환할 수 없으니 result에 그대로 넣어준다.

만약 한글이라면, 위에서 소개한 유니코드를 계산하는 식을 통해서 각 초성, 중성, 종성의 인덱스를 계산하고 인덱스를 이용해 문자를 얻어 result에 추가한다. 이후, 확인하기 쉽게 다시 하나의 문자열로 합해준 결과를 return 한다.

즉, 마지막 출력 결과는 다음과 같다.
ㄴㅏ*ㄴㅡㄴ ㅇㅓ*ㅈㅔ* ㅊㅣ*ㅋㅣㄴㅇㅡㄹ ㅁㅓㄱㅇㅓㅆㄷㅏ*

3.2 초성과 중성, 종성으로 토큰화

초성, 중성, 종성으로 토큰화한 방법과 비슷하게 구현하였다.

def chojoong_jong_tokenizer(s):
    result = []
    for c in s:
        if ord(c) < FIRST_HANGUL or ord(c) > LAST_HANGUL:
            result.append(c)
        else:
            code = ord(c) - FIRST_HANGUL
            jongsung_index = code % N_JONGSUNGS

            chojoongsung_code = ord(c) - jongsung_index
            chojoongsung = chr(chojoongsung_code)

            result.append(chojoongsung)
            result.append(JONGSUNGS[jongsung_index])

    return ''.join(result)
    
print(chojoong_jong_tokenizer('나는 어제 치킨을 먹었다'))

다른 점은 종성의 인덱스까지만 구하고, 원래 유니코드에서 종성의 인덱스를 빼서 종성을 없앤 글자를 얻어 반환한 것이다.

마지막 출력 결과는 다음과 같다.
나*느ㄴ 어*제* 치*키ㄴ으ㄹ 머ㄱ어ㅆ다*

👩🏻‍🏫 정리

  • Language Model : 단어 시퀀스에 확률을 할당하는 모델
  • 한국어 Language Model 구현의 어려움 -> 토큰화하는 방법을 다양하게!
    1. 띄어쓰기가 지켜지지 않음
    2. 어근과 조사로 이루어진 교착어
  • 한국어 Tokenizer 코드
    1. 어절로 구분
    2. 음절로 구분
    3. 자음, 모음으로 구분 (초성, 중성, 종성 & 초성+중성, 종성)

📖 참고 자료

profile
딥러닝 공부중인 대학생입니다!

0개의 댓글