Zorba blog
[NLP] 텍스트 전처리 - 토큰화 본문
아래 글은 공부 목적을 위해 "딥 러닝을 이용한 자연어 처리 입문"(https://wikidocs.net/21698)을 참고하여 작성하였습니다. 좋은 자료를 공유해주신 원작자님께 감사합니다.
자연어 처리에서 주어진 데이터가 전처리되지 않은 상태라면 목적에 맞게 토큰화 & 정제 & 정규화 하는 일이 필요.
- 토큰화 : 주어진 코퍼스(corpus)에서 토큰이라 불리는 단위로 나누는 작업을 말한다. 토큰의 단위는 보통 의미있는 단위로 정의한다.
토큰(token)은 본래 '징표', '형식물'이라는 뜻에서 유래하여 상품권이나 서비스의 교환권을 뜻하는 영단어로, 화폐의 기능을 대신하는 유가증권의 일종이다. 실물로 주조될 경우 대개 화폐와 비슷한 모양으로 발급되며 재질은 동전부터 종이띠의 형태까지 다양하다. 카지노 등지에서는 '칩(chip)'이라는 용어를 쓰기도 한다. |
1. 단어 토큰화(word tokenization)
토큰의 기준을 단어로 하는 경우, 단어 토큰화 라고 한다. 여기서 단어(word)는 단어 단위 외에도 단어구, 의미를 갖는 문자열로도 간주되기도 한다. 결국, 단어란 의미를 가지는 작은 단위를 말한다.
구두점과 같은 문자는 제외시키는 간단한 단어 토큰화 작업을 진행해보자.
- 구두점(punctuation) : 마침표(.), 컴마(,), 물음표(?), 세밀콜론(;), 느낌표(!) 등과 같은 기호를 말한다.
예시 문장 : Time is an illusion. Lunchtime double so!
토큰화 결과 : "Time", "is", "an", "illustion", "Lunchtime", "double", "so"
보통 토큰화 작업은 단순히 구두점이나 특수문자를 전부 제거하는 정제(cleaning) 작업을 수행하는 것만으로 해결되지 않는다. 그리고 위처럼 띄어쓰기 단위로 자르면, 영어의 경우는 괜찮지만 한국어의 경우 띄어쓰기만으로 단어 토큰을 구분하기 어렵다.
2. 토큰화 중 생기는 선택의 순간
토큰화를 진행하다보면 토큰화의 기준에 대해 생각해봐야 하는 경우가 발생한다. 예를 들어 영어권 언어에서 아포스트로피(')가 들어가있는 단어는 어떻게 토큰으로 분류해야 하는지에 대한 선택의 문제를 보자.
다음과 같은 문장이 있다고 했을 때,
Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop.
아포스트로피가 들어간 상황에서 Don't와 Jone's는 어떻게 토큰화할 수 있을까?
Don't / Don t / Dont / Do n't
Jone's / Jones / Jone / Jones
사용자가 직접 토큰화를 설계할 수도 있고, 기존에 공개된 도구들을 사용할 수도 있다. 편하다. NLTK는 영어 코퍼스를 토큰화하기 위한 도구들을 제공하는데 그 중 word_tokenize와 WordPunctTokenizer 그리고 케라스의 text_to_word_sequence를 사용해서 (')를 어떻게 처리하는지 확인해보자.
from nltk.tokenize import word_tokenize
from nltk.tokenize import WordPunctTokenizer
from tensorflow.keras.preprocessing.text import text_to_word_sequence
print('단어 토큰화1 :',word_tokenize("Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop."))
단어 토큰화1 : ['Do', "n't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr.', 'Jone', "'s", 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.']
print('단어 토큰화2 :',WordPunctTokenizer().tokenize("Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop."))
['Don', "'", 't', 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr', '.', 'Jone', "'", 's', 'Orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.']
word_tokenize는 Don't를 Do, n't로 분리. Jone's는 Jone, 's로 분리.
WordPunctTokenizer는 구두점을 별도로 분류. Don't를 Don과 '와 t로 분리. Jone's는 Jone과 '와 s로 분리.
print('단어 토큰화3 :',text_to_word_sequence("Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop."))
단어 토큰화3 : ["don't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', 'mr', "jone's", 'orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop']
케라스의 text_to_word_sequence는모든 알파벳을 소문자로 바꾸고, 마침표나 컴마, 느낌표 등의 구두점을 제거. 하지만 don't나 jone's와 같은 경우 아포스트로피는 보존.
이처럼 각 라이브러리에서 제공하는 Tokenizer 마다 다른 출력값을 확인할 수 있음.
3. 토큰화에서 고려해야할 사항
1) 구두점이나 특수 문자를 단순 제외해서는 안 된다.
갖고있는 코퍼스에서 단어들을 처리할 때, 구두점이나 특수 문자를 단순히 제외하는 것은 옳지 않다. 구두점조차도 하나의 토큰으로 분류하기도 하기 때문. 가장 기본적인 예로 마침표(.)와 같은 경우는 문장의 경계를 알 수 있는데 도움이 되므로 단어를 뽑아낼 때, 마침표(.)를 제외하지 않을 수 있다.
마침표를 활용하여 문장을 구분. 마침표를 날린다면 문장 구분을 하는데 어려움.
또 다른 예로 단어 자체에 구두점을 갖고 있는 경우도 있음. m.p.h나 Ph.D나 AT&T / $45.55 가격을 의미, 01/02/06은 날짜를 의미 / 숫자 사이에 컴마(,)가 들어가는 경우 123,456,789와 같이 세 자리 단위로 컴마가 있음.
구두점이 포함된 문자열 자체가 의미를 가지기 때문에 단순 제외를 할 경우 본래의 의미를 잊어버릴 수 있다.
2) 줄임말과 단어 내에 띄어쓰기가 있는 경우.
토큰화 작업에서 종종 영어권 언어의 아포스트로피(')는 압축된 단어를 다시 펼치는 역할을 하기도 한다. 예를 들어 what're는 what are의 줄임말이며, we're는 we are의 줄임말. 위의 예에서 re를 접어(clitic)이라고 한다. 즉, 단어가 줄임말로 쓰일 때 생기는 형태를 말합니다. 가령 I am을 줄인 I'm이 있을 때, m을 접어라고 한다.
New York이라는 단어나 rock 'n' roll이라는 단어, 이 단어들은 하나의 단어이지만 중간에 띄어쓰기가 존재한다. 하나의 단어 사이에 띄어쓰기가 있는 경우에도 하나의 토큰으로 봐야하는 경우도 있을 수 있다.
3) 표준 토큰화 예제
표준으로 쓰이고 있는 토큰화 방법 중 하나인 Penn Treebank Tokenization의 규칙에 대해서 소개하고, 토큰화의 결과를 확인.
규칙 1. 하이푼으로 구성된 단어는 하나로 유지한다. (home-based)
규칙 2. doesn't와 같이 아포스트로피로 '접어'가 함께하는 단어는 분리해준다. (does n't)
예시 : "Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own."
from nltk.tokenize import TreebankWordTokenizer
tokenizer = TreebankWordTokenizer()
text = "Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own."
print('트리뱅크 워드토크나이저 :',tokenizer.tokenize(text))
트리뱅크 워드토크나이저 : ['Starting', 'a', 'home-based', 'restaurant', 'may', 'be', 'an', 'ideal.', 'it', 'does', "n't", 'have', 'a', 'food', 'chain', 'or', 'restaurant', 'of', 'their', 'own', '.']
4. 문장 토큰화(Sentence Tokenization)
토큰의 단위가 문장(sentence)일 경우. 갖고있는 코퍼스 내에서 문장 단위로 구분하는 작업으로 때로는 문장 분류(sentence segmentation)라고도 부른다. 보통 코퍼스가 정제되지 않은 상태라면, 사용하고자 하는 용도에 맞게 문장 토큰화가 필요하다.
-> Text 데이터(코퍼스)가 들어오는 경우 문장에 따라 먼저 나누어야 할 필요가 있다.
?나 마침표(.)나 ! 기준으로 문장을 잘라내면 되지 않을까? "!"나 "?"는 문장의 구분을 위한 꽤 명확한 구분자(boundary) 역할. 하지만 마침표는 그렇지 않다.
EX1) IP 192.168.56.31 서버에 들어가서 로그 파일 저장해서 aaa@gmail.com로 결과 좀 보내줘. 그 후 점심 먹자.
EX2) Since I'm actively looking for Ph.D. students, I get the same question a dozen times every year.
마침표(.)로 문장을 구분짓는다고 가정하면, 문장의 끝이 나오기 전에 이미 마침표가 여러번 등장하여 예상한 결과가 나오지 않게 된다.
NLTK에서는 영어 문장의 토큰화를 수행하는 sent_tokenize를 지원한다.
from nltk.tokenize import sent_tokenize
text = "His barber kept his word. But keeping such a huge secret to himself was driving him crazy. Finally, the barber went up a mountain and almost to the edge of a cliff. He dug a hole in the midst of some reeds. He looked about, to make sure no one was near."
print('문장 토큰화1 :',sent_tokenize(text))
문장 토큰화1 : ['His barber kept his word.', 'But keeping such a huge secret to himself was driving him crazy.', 'Finally, the barber went up a mountain and almost to the edge of a cliff.', 'He dug a hole in the midst of some reeds.', 'He looked about, to make sure no one was near.']
위 코드는 text에 저장된 여러 개의 문장들로부터 문장을 구분하는 코드.
아래는 문장 중간에 마침표가 다수 등장하는 경우.
text = "I am actively looking for Ph.D. students. and you are a Ph.D student."
print('문장 토큰화2 :',sent_tokenize(text))
문장 토큰화2 : ['I am actively looking for Ph.D. students.', 'and you are a Ph.D student.']
NLTK는 Ph.D.를 문장 내의 단어로 인식하여 성공적으로 인식.
한국어의 경우에는 박상길님이 개발한 KSS(Korean Sentence Splitter) 문장 토큰화 도구 또한 존재.
pip install kss
KSS를 통해서 문장 토큰화를 진행.
import kss
text = '딥 러닝 자연어 처리가 재미있기는 합니다. 그런데 문제는 영어보다 한국어로 할 때 너무 어렵습니다. 이제 해보면 알걸요?'
print('한국어 문장 토큰화 :',kss.split_sentences(text))
한국어 문장 토큰화 : ['딥 러닝 자연어 처리가 재미있기는 합니다.', '그런데 문제는 영어보다 한국어로 할 때 너무 어렵습니다.', '이제 해보면 알걸요?']
5. 한국어에서의 토큰화의 어려움.
영어는 New York과 같은 합성어나 he's 와 같이 줄임말에 대한 예외처리만 한다면, 띄어쓰기 토큰화를 수행해도 단어 토큰화가 잘 작동. 하지만 한국어는 띄어쓰기만으로는 토큰화를 하기에 부족. 한국어의 경우에는 띄어쓰기 단위가 되는 단위를 '어절'이라고 하는데 어절 토큰화는 한국어 NLP에서 지양되고 있음. 어절 토큰화와 단어 토큰화는 같지 않기 때문.
한국어는 영어와 다른 형태를 가지는 언어인 교착어이기 때문에 토큰화가 어려움.
(교착어란 조사, 어미 등을 붙여서 말을 만드는 언어를 말한다.)
1) 교착어의 특성
영어와는 달리 한국어에는 조사라는 것이 존재. 예를 들어 한국어에 그(he/him)라는 단어 하나에도 '그가', '그에게', '그를', '그와', '그는'과 같이 다양한 조사가 '그'라는 글자 뒤에 띄어쓰기 없이 바로 붙게됨. 같은 단어임에도 서로 다른 조사가 붙어서 다른 단어로 인식되어 자연어 처리가 힘들어짐. 대부분의 한국어 NLP에서 조사는 분리해줄 필요가 있음.
한국어는 어절이 독립적인 단어로 구성되는 것이 아니라 조사 등의 무언가가 붙어있는 경우가 많아서 이를 전부 분리해줘야 한다는 의미.
한국어 토큰화에서는 형태소(morpheme) 란 개념 이해 필수. 형태소(morpheme)란 뜻을 가진 가장 작은 말의 단위를 말합니다.
- 자립 형태소 : 접사, 어미, 조사와 상관없이 자립하여 사용할 수 있는 형태소. 그 자체로 단어가 된다. 체언(명사, 대명사, 수사), 수식언(관형사, 부사), 감탄사 등이 있다.
- 의존 형태소 : 다른 형태소와 결합하여 사용되는 형태소. 접사, 어미, 조사, 어간를 말한다.
예시 : 에디가 책을 읽었다
이 문장을 띄어쓰기 단위 토큰화를 수행하면
결과 : ['에디가', '책을', '읽었다']
하지만 이를 형태소 단위로 분해하면
자립 형태소 : 에디, 책
의존 형태소 : -가, -을, 읽-, -었, -다
한국어에서 영어에서의 단어 토큰화와 유사한 형태를 얻으려면 어절 토큰화가 아니라 형태소 토큰화를 수행해야한다.
2) 한국어는 띄어쓰기가 영어보다 잘 지켜지지 않는다.
한국어의 경우 띄어쓰기가 지켜지지 않아도 글을 쉽게 이해할 수 있는 언어.
EX1) 제가이렇게띄어쓰기를전혀하지않고글을썼다고하더라도글을이해할수있습니다.
EX2) Tobeornottobethatisthequestion
6. 품사 태깅(Part-of-speech tagging)
단어는 표기는 같지만 품사에 따라서 단어의 의미가 달라지기도 함. 영어 단어 'fly'는 동사로는 '날다' / 명사로는 '파리'. 한국어도 마찬가지. '못'이라는 단어는 명사로서는 망치를 사용해서 목재 따위를 고정하는 물건. / 부사로서의 '못'은 '먹는다', '달린다'와 같은 동작 동사를 할 수 없다는 의미로 쓰임.
결국 단어의 의미를 제대로 파악하기 위해서는 해당 단어가 어떤 품사로 쓰였는지 보는 것이 주요 지표. 그에 따라 단어 토큰화 과정에서 각 단어가 어떤 품사로 쓰였는지를 구분해놓기도 함, 이 작업을 품사 태깅(part-of-speech tagging)이라고 함.
7. NLTK와 KoNLPy를 이용한 영어, 한국어 토큰화 실습
Penn Treebank POS Tags라는 기준을 사용하여 품사 태깅.
from nltk.tokenize import word_tokenize
from nltk.tag import pos_tag
text = "I am actively looking for Ph.D. students. and you are a Ph.D. student."
tokenized_sentence = word_tokenize(text)
print('단어 토큰화 :',tokenized_sentence)
print('품사 태깅 :',pos_tag(tokenized_sentence))
단어 토큰화 : ['I', 'am', 'actively', 'looking', 'for', 'Ph.D.', 'students', '.', 'and', 'you', 'are', 'a', 'Ph.D.', 'student', '.']
품사 태깅 : [('I', 'PRP'), ('am', 'VBP'), ('actively', 'RB'), ('looking', 'VBG'), ('for', 'IN'), ('Ph.D.', 'NNP'), ('students', 'NNS'), ('.', '.'), ('and', 'CC'), ('you', 'PRP'), ('are', 'VBP'), ('a', 'DT'), ('Ph.D.', 'NNP'), ('student', 'NN'), ('.', '.')]
Penn Treebank POG Tags에서 PRP는 인칭 대명사, VBP는 동사, RB는 부사, VBG는 현재부사, IN은 전치사, NNP는 고유 명사, NNS는 복수형 명사, CC는 접속사, DT는 관사를 의미.
한국어 자연어 처리를 위해서는 KoNLPy(코엔엘파이)라는 파이썬 패키지를 사용. 코엔엘파이를 통해서 사용할 수 있는 형태소 분석기로 Okt(Open Korea Text), 메캅(Mecab), 코모란(Komoran), 한나눔(Hannanum), 꼬꼬마(Kkma)가 있다. 한국어 NLP에서 형태소 분석기를 사용하여 단어 토큰화. 더 정확히는 형태소 토큰화(morpheme tokenization)를 수행.
from konlpy.tag import Okt
from konlpy.tag import Kkma
okt = Okt()
kkma = Kkma()
print('OKT 형태소 분석 :',okt.morphs("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('OKT 품사 태깅 :',okt.pos("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('OKT 명사 추출 :',okt.nouns("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
OKT 형태소 분석 : ['열심히', '코딩', '한', '당신', ',', '연휴', '에는', '여행', '을', '가봐요']
OKT 품사 태깅 : [('열심히', 'Adverb'), ('코딩', 'Noun'), ('한', 'Josa'), ('당신', 'Noun'), (',', 'Punctuation'), ('연휴', 'Noun'), ('에는', 'Josa'), ('여행', 'Noun'), ('을', 'Josa'), ('가봐요', 'Verb')]
OKT 명사 추출 : ['코딩', '당신', '연휴', '여행']
1) morphs : 형태소 추출
2) pos : 품사 태깅(Part-of-speech tagging)
3) nouns : 명사 추출
print('꼬꼬마 형태소 분석 :',kkma.morphs("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('꼬꼬마 품사 태깅 :',kkma.pos("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
print('꼬꼬마 명사 추출 :',kkma.nouns("열심히 코딩한 당신, 연휴에는 여행을 가봐요"))
꼬꼬마 형태소 분석 : ['열심히', '코딩', '하', 'ㄴ', '당신', ',', '연휴', '에', '는', '여행', '을', '가보', '아요']
꼬꼬마 품사 태깅 : [('열심히', 'MAG'), ('코딩', 'NNG'), ('하', 'XSV'), ('ㄴ', 'ETD'), ('당신', 'NP'), (',', 'SP'), ('연휴', 'NNG'), ('에', 'JKM'), ('는', 'JX'), ('여행', 'NNG'), ('을', 'JKO'), ('가보', 'VV'), ('아요', 'EFN')]
꼬꼬마 명사 추출 : ['코딩', '당신', '연휴', '여행']