우리는 수많은 문장 속에서 살아갑니다. 친구에게 온 메세지, 지금 읽고 계신 이 글까지도 문장입니다. 이 모든 것들을 일상에서 자연히 발생하여 쓰이는 언어, 자연어(Natural language)라고 부릅니다.
자연어의 반대 말로 대표적인 것이 프로그래밍 언어(Programming language)입니다. 그렇다면 우리가 일상적으로 사용하는 자연어와 프로그래밍 언어 사이의 본질적인 차이가 무엇일까요?
형식 언어 이론에 따르면
- 자연언어 : 문맥 의존 언어 (context sensitive language)
- 프로그래밍언어 : 문맥 자유 언어 (context free language)
로 구분할 수 있습니다. 문맥 자유 언어란 문맥 의존 언어에 포함되는 개념이라 프로그래밍 언어도 자연언어 범주안에 들어 있습니다. 그리고 이 두 언어도 각각 따르는 문법이 있습니다. 먼저 문법의 네가지 등급을 보면
- 무제약 문법 (unrestricted grammer)
- 문맥 의존 문법 (context sensitive grammer) : 문법 기호의 앞뒤에 위치하는 구절의 내용, 문맥에 영향을 받음
- 문맥 자유 문법 (context free grammer) : 문맥 의존 문법의 특수한 경우, 표현력은 약화되지만 처리하기에 용히
- 정규 문법
이 있습니다.
프로그래밍 언어는 만들어진 언어이기 때문에 처리의 용이성을 중시하여 문맥자유 문법으로 설계되었으며, 특히 문법의 모호함이 없도록 만들어졌습니다.
자연언어는 인위적으로 만들어진 게 아니라 자연 발생적으로 만들어졌다는 점에서 처리에 많은 어려움이 있습니다. 형식언어 이론에서는 자연언어가 문맥의존 문법에 해당한다고 보고 있습니다. 현재 자연어처리를 위한 노력들은 일단 문맥자유 문법을 기본으로 하여, 여기에 문맥 의존적 요소를 처리할 수 있는 기능을 부가하는 방향으로 이루어지고 있습니다.
2016년 구글에서 선보인 자연어 파서 모델을 소개하면서, 자연어 파싱의 어려움을 함께 설명한 글을 읽어보면 왜 자연어처리가 어려운지에 대해 알 수 있습니다.
여기서 파싱(parsing)이란 구문 분석또는 구문 분석하는 과정입니다. 문장이 이루고 있는 구성 성분을 분해하고 분해된 성분의 위계 관계를 분석하여 구조를 결정하는 것입니다. 즉 데이터를 분해 분석하여 원하는 형태로 조립하고 다시 빼내는 프로그램을 말합니다.
구글에서 작성한 글이 설명하는 SyntaxNet이 작동하는 과정을 보겠습니다. 문장이 주어지면 각 단어에 대한 품사 태깅을 진행합니다. 이 과정을 진행하며 각 단어들의 문법적 기능을 설명하고, 문장에 있는 단어들 사이의 문법적 관계를 나타내는 dependency parse tree를 만듭니다.
좀 더 복잡한 문장에 대한 dependency parse tree를 보겠습니다.
앞서 본 간단한 예와 비슷하게 Alice와 Bob을 동사 saw에 대한 주어와 목적어로 분류 했습니다. 거기에 Alice는 동사 reading이 이루는 관계사 절에 의해 설명됩니다. 이렇게 문법적 관계가 표현된 dependency tree 구조는 우리가 다양한 질문에 답할 수 있게 해줍니다. ( whom did Alice see?, who saw Bob? 등)
그렇다면 왜 컴퓨터가 똑바로 파싱하는게 어려울까요?
한 문장을 예로 들겠습니다.
"Alice drove down the street in her car"라는 문장이 있을 때 두 경우로 해석될 수 있습니다.
1. 앨리스가 자기 차를 운전하여 도로를 달렸다.
2. 앨리스가 자기 차 안에있는 도로를 운전해 달렸다.
이런 해석의 모호성(ambiguity)가 발생하는 이유는 바로 in이 drove를 수식하거나 street을 수식할 수 있기 때문입니다. 이러한 모호성은 문장이 길어지면 길어질수록 증가하게 됩니다.
이러한 문제를 해결하기 위해 필요한 것은 실제 지식(real world knowledge)이라고 합니다. (예를 들어, 도로는 자동차 안에 위치할 수 없다.)
사람이 프로그래밍을 통해 파서를 만드는 방식으로는 도저히 해낼 수 없었던 자연어처리 태크스가 단어 속에 담긴 의미를 어떤 의미 벡터 공간에 매핑하는 기법인 워드 벡터를 통해 훨씬 정확하게 처리될 수 있는 가능성이 생겼습니다.
하지만 오늘 다루게 될 내용은 자연어를 의미 단위로 쪼개는 작업에 대한 내용입니다.
자연어를 의미 단위로 쪼개는 토큰화(Tokenization) 기법은 자연어처리 모델의 성능에 결정적 영향을 미칩니다. 어떻게 문장을 쪼개느냐에 따라 같은 문장도 완전히 다른 워드 벡터가 되기 때문입니다.
자연어의 노이즈 제거
우리가 사용하는 자연어는 다양한 형태가 있지만 아래와 같은 형태도 있습니다.
우리가 원하는 이상적인 데이터의 형태는 교과서적인 문장들입니다. 우리가 다루게 될 언어 모델이 바로 단어가 출현하게 될 확률 모델로 이루어져 있습니다. 그러므로 언어 출현 확률의 일정한 패턴을 통계적으로든 딥러닝을 이용하던 학습시키려면 문법이나 언어 사용 관습의 일정한 패턴을 따른 텍스트 데이터가 이상적입니다.
하지만 그림과 같은 채팅 데이터는 띄어쓰기와 맞춤법, 약어 사용 등에서 우리가 원하는 형태의 데이터가 아닙니다. 이렇듯 우리가 사용하는 일상어들은 교과서적인 문장에서 예외적으로 변형될 여지가 무궁무진합니다. 이러한 변형들이 자연어처리 모델의 입장에서는 노이즈가 됩니다.
그럼 이런 노이즈에는 어떤 유형이 있는지 보겠습니다.
- 불완전한 문장으로 구성된 대화 : 한 문장씩 주고 받는 대화가 아니라 한 문장을 여러 번에 나눠 전송하거나 여러 문장을 한 번에 전송하는 경우
예) 철수 : '아니아니', '오늘이 아니고', '내일이지' / 영희 : '그럼 오늘 사야겠네. 내일 필요하니까?' - 문장의 길이가 너무 길거나 짧은 경우 : 아주 짧은 문장은 의미가 없을 수 있고, 대체로 사용빈도가 높은 리액션에 해당하는 경우가 많아서 언어 모델을 왜곡시킬 우려가 있기 때문에 제외해 주는 게 좋습니다.
예) 'ㅋㅋㅋ', 'ㅜㅜ'
아주 긴 문장은 대화와는 관계가 없는 문장일 수 있습니다.
예) '이 편지는 영국에서부터 시작되어...' - 채팅 데이터에서 문장 시간 간격이 너무 긴 경우 : 메신저는 누가 자판을 치는지 모르기 때문에 서로의 말이 얽히게 됩니다. 따라서 서로의 대화 텀이 짧으면 그것은 대화가 아니라 서로 할말만 하는 상태일 수 있습니다.
예) 철수 : '어제 친구랑 롤하는데' / 영희 : '이김?' / 철수 : '바로 뒷자리 사람 만남 ㅋㅋ' / 영희 : '오늘 퇴근하ㄱ 아니 ㅡㅡ' - 바람직하지 않은 문장의 사용 : 욕설의 비율이나, 오타의 비율이 높은 문장은 자연어 모델 학습에 사용하지 않는 것이 좋습니다.
이렇게 텍스트 데이터의 특성에 따라 고려해야 할 노이즈의 특성이 무궁무진합니다. 하지만 지금은 좀 더 단순하고 근본적인 노이즈에 집중하겠습니다. 예를 들면 아래와 같은 문장들이 있습니다.
- Hi, my name is John.('Hi', 'my', ... , 'John'으로 분리됨) - 문장 부호
- First, open the first chapter.('First'와 'first'를 다른 단어로 인식) - 대소문자
- He is a ten-year-old boy. ( ten-year-old를 한 단어로 인식) - 특수문자
대표적인 세 가지 노이즈 유형입니다. 하나하나 해결하며 완벽한 말뭉치(corpus)를 만들어 보겠습니다.
유형 1. 문장부호
먼저 문장부호입니다. 우리는 문장부호를 배웠으니 'Hi,'가 'Hi'와 ','의 결합인 것을 알지만 컴퓨터는 명시해 주지 않으면 ','도 알파벳이라고 생각할 수 있습니다. 문장부호와 단어를 분리해주기 위해 문장부호 양쪽에 공백을 추가하겠습니다.
def pad_punctuation(sentence, punc):
for p in punc:
sentence = sentence.replace(p, ' '+p+' ')
return sentence
sentence = 'Hi, my name is john.'
print(pad_punctuation(sentence, ['.', '?', '!', ',']))
# outputs
Hi , my name is john .
Python의 replace() 함수를 사용하면 쉽게 해결할 수 있습니다.
유형 2. 대소문자
그 다음 노이즈 유형은 대소문자입니다. 영어에서 발생하는 문제인데, First와 first는 같은 의미를 갖고 있음에도 컴퓨터는 f와 F를 다르다고 구분 지어 버릴 수 있습니다. 이를 방지하기 위해 모든 단어를 소문자로 바꾸는 방법을 취하겠습니다.
sentence = 'First, open the first chapter.'
# 소문자로 바꾸기
print(sentence.lower())
# outputs
first, open the first chapter.
# 대문자로 바꾸기
print(sentence.upper())
# outputs
FIRST, OPEN THE FIRST CHAPTER.
유형 3. 특수문자
ten-year-old와 seven-year-old, 그 외의 수많은 나이 표현들을 각각의 단어로 취급해버리는 일이 발생할 수 있습니다. 그런 일이 일어나는 것을 막기 위해 특수문자를 제거하려 합니다. 하지만 모든 특수문자를 정의하여 제거할 수는 없습니다. 따라서 우리는 사용할 알파벳과 기호들을 정의해 이를 제외하고 모두 지우겠습니다.
import re
sentence = 'He is a ten-year-old boy.'
sentence = re.sub('([^a-zA-Z.,?!])', ' ', sentence)
print(sentence)
# outputs
He is a ten year old boy.
re 패키지는 정규표현식 사용을 도와주는 패키지입니다.
위의 노이즈 제거 방식을 종합하여 함수로 정의하겠습니다.
corpus = \
"""
In the days that followed I learned to spell in this uncomprehending way a great many words, among them pin, hat, cup and a few verbs like sit, stand and walk.
But my teacher had been with me several weeks before I understood that everything has a name.
One day, we walked down the path to the well-house, attracted by the fragrance of the honeysuckle with which it was covered.
Some one was drawing water and my teacher placed my hand under the spout.
As the cool stream gushed over one hand she spelled into the other the word water, first slowly, then rapidly.
I stood still, my whole attention fixed upon the motions of her fingers.
Suddenly I felt a misty consciousness as of something forgotten—a thrill of returning thought; and somehow the mystery of language was revealed to me.
I knew then that "w-a-t-e-r" meant the wonderful cool something that was flowing over my hand.
That living word awakened my soul, gave it light, hope, joy, set it free!
There were barriers still, it is true, but barriers that could in time be swept away.
"""
def cleaning_text(text, punc):
# 유형 1 문장부호 공백추가
for p in punc:
text = text.replace(p, ' '+p+' ')
# 유형 2, 3 소문자화 및 특수문자 제거
text = text.lower()
text = re.sub('([^a-zA-Z0-9.,?!\n])', ' ', text)
return text
print(cleaning_text(corpus, ['.', ',', '!', '?']))
in the days that followed i learned to spell in this uncomprehending way a great many words , among them pin , hat , cup and a few verbs like sit , stand and walk . but my teacher had been with me several weeks before i understood that everything has a name . one day , we walked down the path to the well house , attracted by the fragrance of the honeysuckle with which it was covered . some one was drawing water and my teacher placed my hand under the spout . as the cool stream gushed over one hand she spelled into the other the word water , first slowly , then rapidly . i stood still , my whole attention fixed upon the motions of her fingers . suddenly i felt a misty consciousness as of something forgotten a thrill of returning thought and somehow the mystery of language was revealed to me . i knew then that w a t e r meant the wonderful cool something that was flowing over my hand . that living word awakened my soul , gave it light , hope , joy , set it free ! there were barriers still , it is true , but barriers that could in time be swept away . |
분산표현
단어의 희소 표현과 분산 표현
임베딩 레이어(Embedding Layer)를 통해 단어의 분산 표현(distributed representation)을 구현할 수 있습니다. 임베딩 레이어는 자연어 처리 분야에서 거의 기본에 해당하는 요소입니다. 그럼 분산 표현에 대해 알아 보겠습니다.
분산 표현과 반대되는 표현으로 희소 표현(sparse representation)이라는 것이 있습니다. 어느 표현이든 단어를 벡터로 표현하겠다는 점에서는 동일합니다. 하지만, 단어의 의미를 표현하는 접근 방식에서 큰 차이가 있습니다. 희소 표현방식은 벡터의 각 차원마다 단어의 특정 의미 속성을 대응시키는 방식입니다.
예를 들어, 사람의 성별을 표현하는 남자와 여자라는 두 단어를 수로 표현하려면 어떻게 하면 될까요? 남자 : [1], 여자 : [-1]의 형태로 표현할 수 있습니다.
다음으로 소년과 소녀를 표현하는 방법에 대해 생각해보겠습니다. 두 단어는 각각 어린 남자와 여자를 의미하니, 앞서 생성한 성별 이라는 속성에 나이가 어리다라는 속성을 추가해야합니다. 즉 소년 : [1, 1], 소녀 : [-1, 1] 이 됩니다. 나이가 어리다는 속성을 1로 표현했습니다. 그렇다면 나이가 많은 남자, 여자를 나타내는 할아버지, 할머니는 자동적으로 할아버지 : [1, -1], 할머니 : [-1, -1]이 되는 것도 알 수 있습니다.
성별 | 연령 | |
남자 | 1 | 0 |
여자 | -1 | 0 |
소년 | 1 | 1 |
소녀 | -1 | 1 |
할아버지 | 1 | -1 |
할머니 | -1 | -1 |
지금 우리가 한 것이 바로 희소 표현(sparse representation)입니다. 단어를 고차원 벡터로 표현하는 것이죠. 사람을 나이와 성별로 구분하기 위해선 적어도 2차원이 필요함을 배웠습니다. 2차원이라 뭐가 고차원인지, 뭐가 희소하다는 건지 느낌이 안 오실 수 있습니다.
위 예시는 모든 단어들이 사람이라는 속성을 가진다고 가정했습니다. 하지만 세상엔 바나나도 있고 사과도 있습니다. 과일인지 아닌지 판단하는 속성을 추가하고, 색깔 속성을 추가해서 이번엔 바나나와 사과를 구분해보겠습니다.
성별 | 연령 | 과일 | 색깔 | |
남자 | 1 | 0 | 0 | 0 |
여자 | -1 | 0 | 0 | 0 |
사과 | 0 | 0 | 1 | 1 |
바나나 | 0 | 0 | 1 | -1 |
이렇게 속성의 종류가 늘어나고 워드 벡터의 차원이 늘어나면서 0이 자주 나오기 시작하는 것이 보입니다. 이렇게 대부분의 값이 0으로 표현되는 것을 희소 표현이라고 합니다.
이런 방식의 문제점은 그럼 무엇일까요? 바로 단어가 많아지고 속성이 많아질수록 엄청난 고차원이 필요하고, 희소 표현의 워드 벡터끼리는 단어들 간의 의미적 유사도를 계산할 수 없다는 점입니다.
두 고차원 벡터의 유사도는 코사인 유사도(Cosine Similarity)를 통해 구할 수 있습니다.
코사인 유사도는 쉽게 말해 두 벡터간의 사잇각의 코사인값 이라고 볼 수 있습니다.
예를 들어 위의 희소표현에서 남자 벡터와 사과 벡터의 코사인 유사도를 구하면 0이 됩니다.
그래서 우린 Embedding 레이어를 사용해 각 단어가 몇 차원의 속성을 가질지 정의하는 방식으로 단어의 분산 표현(distributed representation)을 구하는 방식을 주로 사용하게 됩니다. 만약 100개의 단어를 256차원의 속성으로 표현하고 싶다면 Embedding레이어는 아래와 같이 정의됩니다.
embedding_layer = tf.keras.layers.Embedding(input_dim=100, output_dim=256)
위 단어의 분산 표현에는 우리가 일일이 정의할 수 없는 어떤 추상적인 속성들이 256차원 안에 골고루 분산되어 표현됩니다. 희소 표현처럼 속성값을 임의로 지정해 주는 것이 아니라, 수많은 텍스트 데이터를 읽어가며 적합한 값을 찾아갈 것입니다.
단어 사전 구성과 활용의 문제
하지만 짚고 넘어가야 할 점이 있습니다. 위의 Embedding layer를 사용해 구현한 분산 표현은 컴퓨터 입장에서는 단어 사전이 됩니다. 하지만 우리가 영어 사전만 들고 미국으로 간다고 그 나라 말을 알아듣고 해석할 수는 없습니다. 애초에 단어 단위로 명확하게 듣는 것 조차 힘들기 때문입니다.
컴퓨터도 비슷합니다. 우리는 컴퓨터가 문장에서 각 단어에 해당하는 분산표현을 언제든지 찾을 수 있다고 생각하지만, 정작 컴퓨터는 문장을 단어 단위로 정확하게 끊어 읽지 못하기 때문에 전혀 다른 단어로 해석하거나 단어 사전에서 그 단어를 찾기 못하는 일이 생기게 됩니다.
그렇다면 한 문장에서 단어의 수는 어떻게 정의할 수 있을까요?
"그녀는 나와 밥을 먹는다"라는 문장이 주어지면 공백 기준으로 나누어 '그녀는', '나와', '밥을', '먹는다' 4개 단어로 이루어졌다고 단정 지을 수 있을까요?
어쩌면 '그녀' , '는', '나', '와', '밥', '을', '먹는다' 처럼 잘게 쪼개어 7개의 단어로 이루어졌다고도 할 수 있습니다. 그것은 우리가 정의할 토큰화 기법이 결정할 부분입니다.
토큰화
공백 기반 토큰화
앞서 자연어 노이즈 제거 방법 중 하나로 우리는 'Hi,' 를 'Hi' 와 ','로 나누기 위해 문장부호 양옆에 공백을 추가해 주었습니다. 그 이유는 공백 기반 토큰화를 사용하기 위해서였습니다. 당시의 예제 코드를 다시 가져와 공백 기반 토큰화를 진행해 보겠습니다.
corpus = \
"""
in the days that followed i learned to spell in this uncomprehending way a great many words , among them pin , hat , cup and a few verbs like sit , stand and walk .
but my teacher had been with me several weeks before i understood that everything has a name .
one day , we walked down the path to the well house , attracted by the fragrance of the honeysuckle with which it was covered .
some one was drawing water and my teacher placed my hand under the spout .
as the cool stream gushed over one hand she spelled into the other the word water , first slowly , then rapidly .
i stood still , my whole attention fixed upon the motions of her fingers .
suddenly i felt a misty consciousness as of something forgotten a thrill of returning thought and somehow the mystery of language was revealed to me .
i knew then that w a t e r meant the wonderful cool something that was flowing over my hand .
that living word awakened my soul , gave it light , hope , joy , set it free !
there were barriers still , it is true , but barriers that could in time be swept away .
"""
tokens = corpus.split()
print("문장이 포함하는 Tokens:", tokens)
문장이 포함하는 Tokens: ['in', 'the', 'days', 'that', 'followed', 'i', 'learned', 'to', 'spell', 'in', 'this', 'uncomprehending', 'way', 'a', 'great', 'many', 'words', ',', 'among', 'them', 'pin', ',', 'hat', ',', 'cup', 'and', 'a', 'few', 'verbs', 'like', 'sit', ',', 'stand', 'and', 'walk', '.', 'but', 'my', 'teacher', 'had', 'been', 'with', 'me', 'several', 'weeks', 'before', 'i', 'understood', 'that', 'everything', 'has', 'a', 'name', '.', 'one', 'day', ',', 'we', 'walked', 'down', 'the', 'path', 'to', 'the', 'well', 'house', ',', 'attracted', 'by', 'the', 'fragrance', 'of', 'the', 'honeysuckle', 'with', 'which', 'it', 'was', 'covered', '.', 'some', 'one', 'was', 'drawing', 'water', 'and', 'my', 'teacher', 'placed', 'my', 'hand', 'under', 'the', 'spout', '.', 'as', 'the', 'cool', 'stream', 'gushed', 'over', 'one', 'hand', 'she', 'spelled', 'into', 'the', 'other', 'the', 'word', 'water', ',', 'first', 'slowly', ',', 'then', 'rapidly', '.', 'i', 'stood', 'still', ',', 'my', 'whole', 'attention', 'fixed', 'upon', 'the', 'motions', 'of', 'her', 'fingers', '.', 'suddenly', 'i', 'felt', 'a', 'misty', 'consciousness', 'as', 'of', 'something', 'forgotten', 'a', 'thrill', 'of', 'returning', 'thought', 'and', 'somehow', 'the', 'mystery', 'of', 'language', 'was', 'revealed', 'to', 'me', '.', 'i', 'knew', 'then', 'that', 'w', 'a', 't', 'e', 'r', 'meant', 'the', 'wonderful', 'cool', 'something', 'that', 'was', 'flowing', 'over', 'my', 'hand', '.', 'that', 'living', 'word', 'awakened', 'my', 'soul', ',', 'gave', 'it', 'light', ',', 'hope', ',', 'joy', ',', 'set', 'it', 'free', '!', 'there', 'were', 'barriers', 'still', ',', 'it', 'is', 'true', ',', 'but', 'barriers', 'that', 'could', 'in', 'time', 'be', 'swept', 'away', '.'] |
형태소 기반 토큰화
하지만 우리는 영어 문장보다 한국어 문장을 처리할 일이 더 많을 것입니다. 한국어 문장은 공백 기준으로 토큰화를 했다간 엉망진창의 단어들이 등장하는 것을 볼 수 있습니다.
이를 해결할 수 있는 방법의 힌트는 형태소에 있습니다.
형태소의 정의는 다음과 같습니다
(명사) 뜻을 가진 가장 작은 말의 단위
한국어 형태소 분석기에서 대표적인 KoNLPy를 사용해 봅시다.
KoNLPy는 내부적으로 5가지의 형태소 분석 Class를 포함하고 있습니다. 형태소 분석기들은 각 Class마다 차이가 있으니 직접 테스트 해보고 적합한 것을 선택해 사용하면 됩니다.
그럼 한 번 한국어 형태소 분석기 비교 실험을 해보겠습니다.
from konlpy.tag import Hannanum, KKma, Komoran, Mecab, Okt
tokenizer_list = [Hannanum(), Kkma(), Komoran(), Mecab(), Okt()]
kor_text = '코로나바이러스는 2019년 12월 중국 우한에서 처음 발생한 뒤 전 세계로 확산된, 새로운 유형의 호흡기 감염 질환입니다.'
for tokenizer in tokenizer_list:
print('[{}] \n{}'.format(tokenizer.__class__.__name__, tokenizer.pos(kor_text)))
[Hannanum] [('코로나바이러스', 'N'), ('는', 'J'), ('2019년', 'N'), ('12월', 'N'), ('중국', 'N'), ('우한', 'N'), ('에서', 'J'), ('처음', 'M'), ('발생', 'N'), ('하', 'X'), ('ㄴ', 'E'), ('뒤', 'N'), ('전', 'N'), ('세계', 'N'), ('로', 'J'), ('확산', 'N'), ('되', 'X'), ('ㄴ', 'E'), (',', 'S'), ('새롭', 'P'), ('은', 'E'), ('유형', 'N'), ('의', 'J'), ('호흡기', 'N'), ('감염', 'N'), ('질환', 'N'), ('이', 'J'), ('ㅂ니다', 'E'), ('.', 'S')] [Kkma] [('코로나', 'NNG'), ('바', 'NNG'), ('이러', 'MAG'), ('슬', 'VV'), ('는', 'ETD'), ('2019', 'NR'), ('년', 'NNM'), ('12', 'NR'), ('월', 'NNM'), ('중국', 'NNG'), ('우', 'NNG'), ('하', 'XSV'), ('ㄴ', 'ETD'), ('에', 'VV'), ('서', 'ECD'), ('처음', 'NNG'), ('발생', 'NNG'), ('하', 'XSV'), ('ㄴ', 'ETD'), ('뒤', 'NNG'), ('전', 'NNG'), ('세계', 'NNG'), ('로', 'JKM'), ('확산', 'NNG'), ('되', 'XSV'), ('ㄴ', 'ETD'), (',', 'SP'), ('새', 'NNG'), ('롭', 'XSA'), ('ㄴ', 'ETD'), ('유형', 'NNG'), ('의', 'JKG'), ('호흡기', 'NNG'), ('감염', 'NNG'), ('질환', 'NNG'), ('이', 'VCP'), ('ㅂ니다', 'EFN'), ('.', 'SF')] [Komoran] [('코로나바이러스', 'NNP'), ('는', 'JX'), ('2019', 'SN'), ('년', 'NNB'), ('12월', 'NNP'), ('중국', 'NNP'), ('우', 'NNP'), ('한', 'NNP'), ('에서', 'JKB'), ('처음', 'NNG'), ('발생', 'NNG'), ('하', 'XSV'), ('ㄴ', 'ETM'), ('뒤', 'NNG'), ('전', 'MM'), ('세계로', 'NNP'), ('확산', 'NNG'), ('되', 'XSV'), ('ㄴ', 'ETM'), (',', 'SP'), ('새롭', 'VA'), ('ㄴ', 'ETM'), ('유형', 'NNP'), ('의', 'JKG'), ('호흡기', 'NNG'), ('감염', 'NNP'), ('질환', 'NNG'), ('이', 'VCP'), ('ㅂ니다', 'EF'), ('.', 'SF')] [Mecab] [('코로나', 'NNP'), ('바이러스', 'NNG'), ('는', 'JX'), ('2019', 'SN'), ('년', 'NNBC'), ('12', 'SN'), ('월', 'NNBC'), ('중국', 'NNP'), ('우한', 'NNP'), ('에서', 'JKB'), ('처음', 'NNG'), ('발생', 'NNG'), ('한', 'XSV+ETM'), ('뒤', 'NNG'), ('전', 'NNG'), ('세계', 'NNG'), ('로', 'JKB'), ('확산', 'NNG'), ('된', 'XSV+ETM'), (',', 'SC'), ('새로운', 'VA+ETM'), ('유형', 'NNG'), ('의', 'JKG'), ('호흡기', 'NNG'), ('감염', 'NNG'), ('질환', 'NNG'), ('입니다', 'VCP+EF'), ('.', 'SF')] [Okt] [('코로나바이러스', 'Noun'), ('는', 'Josa'), ('2019년', 'Number'), ('12월', 'Number'), ('중국', 'Noun'), ('우한', 'Noun'), ('에서', 'Josa'), ('처음', 'Noun'), ('발생', 'Noun'), ('한', 'Josa'), ('뒤', 'Noun'), ('전', 'Noun'), ('세계', 'Noun'), ('로', 'Josa'), ('확산', 'Noun'), ('된', 'Verb'), (',', 'Punctuation'), ('새로운', 'Adjective'), ('유형', 'Noun'), ('의', 'Josa'), ('호흡기', 'Noun'), ('감염', 'Noun'), ('질환', 'Noun'), ('입니다', 'Adjective'), ('.', 'Punctuation')] |
하지만 이렇게 토큰을 생성할 때 데이터에 포함되는 모든 단어를 처리할 수는 없기 때문에 자주 등장하는 N개의 단어만 사용하고 나머지는 <unk>같은 특수한 토큰으로 치환합니다. 자원이 무한하지 않기 때문에 이런 일이 발생합니다. 이처럼 토큰에 없는 단어때문에 발생하는 문제를 OOV(Out-Of-Vocabulary)문제라고 합니다. 이를 해결하기 위한 시도들이 있는데, 그것이 바로 Wordpiece Model입니다.
BPE( Byte Pair Encoding)
BPE는 1994년 데이터 압축을 위해 생긴 알고리즘입니다. 데이터에서 가장 많이 등장하는 바이트 쌍(Byte Pair)를 새로운 단어로 치환하여 압축하는 작업을 반복하는 방식으로 동작합니다.
이러한 개념을 이용해 모든 단어를 문자(바이트)들의 집합으로 취급하여 자주 등장하는 문자 쌍을 합치면, 접두어나 접미어의 의미를 파악할 수 있고, 처음 등장하는 단어는 문자들의 조합으로 나타내 OOV문제를 해결할 수 있다는 개념입니다.
위 논문에서 제공해 주는 예제로 동작 방식을 자세히 들여다보겠습니다.
import re, collections
# 임의의 데이터에 포함된 단어들
# 오른쪽 정수는 데이터에 해당 단어가 포함된 빈도수
vocab = {
'l o w ' : 5,
'l o w e r ' : 2,
'n e w e s t ' : 6,
'w i d e s t ' : 3
}
num_merges = 5
def get_stats(vocab):
pairs = collections.defaultdict(int)
for word, freq in vocab.items():
symbols = word.split()
for i in range(len(symbols) -1):
pairs[symbols[i], symbols[i+1]] += freq
return pairs
def merge_vocab(pair, v_in):
v_out = {}
bigram = re.escape(' '.join(pair))
p = re.compiler(r'(?<!\S)' + bigram + r'(?!\S)')
for word in v_in:
w_out = p.sub(''.join(pair), word)
v_out[w_out] = v_in[word]
return v_out, pair[0] + pair[1]
token_vocab = []
for i in range(num_merges):
print(">> step {0}".format(i+1))
pairs = get_stats(vocab)
best = max(pairs, key=pairs.get)
vocab, merge_tok = merge_vocab(best, vocab)
print('다음 문자 쌍을 치환 : ', merge_tok)
print('변환된 Voab : \n', vocab, '\n')
token_vocab.append(merge_tok)
print('Merge Vocab: ', token_vocab)
>> Step 1 다음 문자 쌍을 치환: es 변환된 Vocab: {'l o w ': 5, 'l o w e r ': 2, 'n e w es t ': 6, 'w i d es t ': 3} >> Step 2 다음 문자 쌍을 치환: est 변환된 Vocab: {'l o w ': 5, 'l o w e r ': 2, 'n e w est ': 6, 'w i d est ': 3} >> Step 3 다음 문자 쌍을 치환: lo 변환된 Vocab: {'lo w ': 5, 'lo w e r ': 2, 'n e w est ': 6, 'w i d est ': 3} >> Step 4 다음 문자 쌍을 치환: low 변환된 Vocab: {'low ': 5, 'low e r ': 2, 'n e w est ': 6, 'w i d est ': 3} >> Step 5 다음 문자 쌍을 치환: ne 변환된 Vocab: {'low ': 5, 'low e r ': 2, 'ne w est ': 6, 'w i d est ': 3} Merge Vocab: ['es', 'est', 'lo', 'low', 'ne'] |
이렇게 단어장을 만들어 놓으면 만약 lowest라는 처음 보는 단어가 등장하더라도, 어느 정도 의미가 파악된 low와 est의 결합으로 표현할 수 있습니다.
하지만 이런 BPE도 완벽하다고 할 수는 없습니다.
이에 구글에서 BPE를 변형해 제안한 알고리즘이 바로 WPM입니다.
WPM (Wordpiece Model)
WPM은 BPE에 대해 두 가지 차별성을 가집니다.
- 공백 복원을 위해 단어의 시작 부분에 언더바('_')를 추가합니다.
- 빈도수 기반이 아닌 가능도(Likelihood)를 증가시키는 방향으로 문자 쌍을 합칩니다.
https://static.googleusercontent.com/media/research.google.com/ko//pubs/archive/37842.pdf
자세한 내용은 논문 3절과 4절에 나와있습니다.
soynlp
soynlp는 한국어 자연어 처리를 위한 라이브러리입니다. 토크나이저 외에도 단어 추출, 품사 판별, 전처리 기능도 제공합니다.
https://github.com/lovit/soynlp
토큰에 의미 부여
Word2Vec
Word2Vec은 단어를 벡터로 만든다는 의미입니다. '오늘 점심으로 제육볶음 먹었다.' 라는 문장의 각 단어, 즉 동시에 등장하는 단어끼리는 연관성이 있다는 아이디어로 시작된 알고리즘 입니다.
FastText
https://brunch.co.kr/@learning/7
ELMo - the 1st Contextualized Word Embedding
위의 알고리즘들은 훌륭하지만 동음이의어를 처리할 수 없다는 문제가 있습니다. ELMo라는 모델은 데이터에 단어가 등장한 순간, 그 주변 단어 정보를 사용해 Embedding을 구축하는 개념을 도입했습니다.
https://brunch.co.kr/@learning/12
http://www.aistudy.co.kr/linguistics/natural/language_kim.htm
http://wiki.hash.kr/index.php/%ED%8C%8C%EC%8B%B1
https://ai.googleblog.com/2016/05/announcing-syntaxnet-worlds-most.html
https://konlpy-ko.readthedocs.io/ko/v0.4.3/
https://arxiv.org/pdf/1508.07909.pdf
https://jjangjjong.tistory.com/41
'AI > NLP Study' 카테고리의 다른 글
[NLP] 임베딩 편향성 (0) | 2022.10.02 |
---|---|
[NLP] 워드 임베딩 (1) | 2022.10.01 |
[NLP] 텍스트 카테고리 분류 (1) | 2022.09.29 |
[NLP] 텍스트 벡터화 (0) | 2022.09.27 |
[NLP] 단어사전 만들기 (1) | 2022.09.25 |