개발자로 후회없는 삶 살기
NLP PART.단어의 표현 본문
서론
※ 이 포스트는 다음 교재의 학습이 목적임을 밝힙니다.
https://wikidocs.net/book/2155
-> 전체 코드
본론
- 단어 표현 방법
1. 국소 표현 : puppy(강아지), cute(귀여운), lovely(사랑스러운)라는 단어가 있을 때 각 단어에 1번, 2번, 3번 등과 같은 숫자를 맵핑(mapping)하여 부여한다면 이는 국소 표현 방법에 해당합니다.
2. 분산 표현 : puppy라는 단어 근처에는 주로 귀여운, '사랑스러운'이라는 단어가 자주 등장하므로 puppy라는 단어를 cute, lovely한 느낌이다로 단어를 정의합니다.
> 이 두 방법의 차이는 국소 표현 방법은 단어의 의미, 뉘앙스를 표현할 수 없지만, 분산 표현 방법은 단어의 뉘앙스를 표현할 수 있게 됩니다. > 이번 챕터의 BOW는 국소 표현에 속하며 단어의 빈도수를 카운트하여 단어를 수치화하는 단어 표현 방법입니다.
- BOW
1. bow란?
단어들의 순서는 전혀 고려하지 않고 단어들의 출현 빈도에만 집중하는 텍스트 데이터의 수치화 표현 방법입니다. 단어들이 들어있는 가방을 생각해보면 어떤 텍스트 문서에 있는 단어들을 전부 가방에 넣고 가방을 흔듭니다.
> 해당 문서 내에서 특정 단어가 N번 등장했다면 이 가방에는 특정 단어가 N개 있게 됩니다. 또한 가방을 흔들어서 단어를 섞었기 때문에 더 이상 단어의 순서는 중요하지 않습니다.
2. BOW를 만드는 과정
(1) 각 단어에 고유한 정수 인덱스를 부여합니다.
(2) 각 인덱스의 위치에 단어 토큰의 등장 횟수 기록한 벡터를 만듭니다.
ex) 문서1 : "정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다."
> 문서 1에 대해 BOW를 만듭니다. 근데 bow는 단어에 대한 표현이 아니고 문서에 대한 표현 같습니다. 역시 이번에도 문장을 가져왔습니다.
doc1 = "정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다."
okt = Okt()
doc1 = doc1.replace('.', '')
token = okt.morphs(doc1)
word_to_index = {}
bow = []
bow2 = []
print(word_to_index.keys())
# 결과 : 비어있음
dict_keys([])
for word in token: # 문장의 단어 하나씩 가져와서
if word not in word_to_index.keys(): # 키에 없는 처음 보는 단어라면
word_to_index[word] = len(word_to_index) # w2i에 문자를 정수에 맵핑함
# BoW에 전부 기본값 1을 넣는다.
bow.append(1)
print(bow)
else:
# 재등장하는 단어라면 그 단어의 현재 bow의 빈도수를 가져와서 1을 더한다.
index = word_to_index[word]
bow[index] = bow[index] + 1
# 결과
[1]
[1, 1]
[1, 1, 1]
[1, 1, 1, 1]
[1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1, 1]
[1, 2, 1, 1, 1, 1, 1, 1]
[1, 2, 1, 1, 2, 1, 1, 1, 1]
[1, 2, 1, 1, 2, 1, 1, 1, 1, 1]
문서 1에 각 단어에 대해서 인덱스를 부여한 결과가 단어 사전입니다. 단어 사전을 만드는 것입니다. bow결과를 보면 인덱스 4에 해당하는 물가상승률은 두 번 언급되었기 때문에 인덱스 4에 해당하는 값이 2입니다.
doc1 = "정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다."
print('vocabulary :', word_to_index)
print('bag of words vector :', bow)
# 결과
vocabulary : {'정부': 0, '가': 1, '발표': 2, '하는': 3, '물가상승률': 4, '과': 5, '소비자': 6, '느끼는': 7, '은': 8, '다르다': 9}
bag of words vector : [1, 2, 1, 1, 2, 1, 1, 1, 1, 1]
=> CountVectorizer 클래스로 bow 만들기
from sklearn.feature_extraction.text import CountVectorizer
corpus = ['you know I want your love. because I love you.']
vector = CountVectorizer()
# 코퍼스로부터 각 단어의 빈도수를 기록
print('bag of words vector :', vector.fit_transform(corpus).toarray())
# 각 단어의 인덱스가 어떻게 부여되었는지를 출력
print('vocabulary :',vector.vocabulary_)
# 결과
bag of words vector : [[1 1 2 1 2 1]]
vocabulary : {'you': 4, 'know': 1, 'want': 3, 'your': 5, 'love': 2, 'because': 0}
예제 문장에서 you와 love는 두 번씩 언급되었으므로 각각 인덱스 2와 인덱스 4에서 2의 값을 가지며, 그 외의 값에서는 1의 값을 가지는 것을 볼 수 있습니다.
알파벳 I는 BoW를 만드는 과정에서 사라졌는데, 이는 CountVectorizer가 기본적으로 길이가 2이상인 문자에 대해서만 토큰으로 인식하기 때문입니다. (하지만 길이로 정제하는 것은 역시 영어권에서만 의미가 있습니다.)
-> 한국어에 적용
주의할 것은 CountVectorizer는 단지 띄어쓰기만을 기준으로 단어를 자르는 낮은 수준의 토큰화를 진행하고 BoW를 만든다는 점입니다. 이는 영어의 경우 띄어쓰기만으로 토큰화가 수행되기 때문에 문제가 없지만 한국어에 CountVectorizer를 적용하면, 조사 등의 이유로 제대로 BoW가 만들어지지 않음을 의미합니다.
from sklearn.feature_extraction.text import CountVectorizer
corpus = ["정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다."]
vector = CountVectorizer()
# 코퍼스로부터 각 단어의 빈도수를 기록
print('bag of words vector :', vector.fit_transform(corpus).toarray())
# 각 단어의 인덱스가 어떻게 부여되었는지를 출력
print('vocabulary :',vector.vocabulary_)
# 결과
bag of words vector : [[1 1 1 1 1 1 1]]
vocabulary : {'정부가': 6, '발표하는': 4, '물가상승률과': 2, '소비자가': 5, '느끼는': 0, '물가상승률은': 3, '다르다': 1}
아래를 보면 '물가상승률'이라는 단어를 인식하지 못합니다. '물가상승률과'와 '물가상승률은' 으로 조사를 포함해서 하나의 단어로 판단하기 때문에 서로 다른 두 단어로 인식합니다. 따라서 각각 다른 인덱스에 1이라는 빈도의 값을 갖습니다.
∴ 위의 BOW를 만드는 함수를 작성할 줄 알아야 합니다.
=> 불용어를 제거한 BOW
BoW를 사용한다는 것은 각 단어에 대한 빈도수를 수치화 하겠다는 것으로 결국 텍스트 내에서 어떤 단어들이 중요한지를 보고 싶다는 의미를 함축하고 있습니다.(어떤 단어가 중요한지 빈도수로 보겠다!)
text = ["Family is not an important thing. It's everything."]
vect = CountVectorizer(stop_words=["the", "a", "an", "is", "not"])
print('bag of words vector :',vect.fit_transform(text).toarray())
print('vocabulary :',vect.vocabulary_)
# 결과
bag of words vector : [[1 1 1 1 1]]
vocabulary : {'family': 1, 'important': 2, 'thing': 4, 'it': 3, 'everything': 0}
CountVectorizer는 불용어를 지정하면, 불용어는 제외하고 BoW를 만들 수 있도록 불용어 제거 기능을 지원합니다.
3. 문서 단어 행렬(Document-Term Matrix)
서로 다른 문서들의 BoW들을 결합한 표현 방법입니다. 이것으로 서로 다른 문서들을 비교할 수 있습니다.
1. 표기법
문서 단어 행렬(Document-Term Matrix, DTM)이란 다수의 문서에서 등장하는 각 단어들의 빈도를 행렬로 표현한 것을 말합니다. ★ 쉽게 생각하면 각 문서에 대한 BoW를 하나의 행렬로 만든 것입니다. ★
ex) 문서1 : 먹고 싶은 사과
문서2 : 먹고 싶은 바나나
문서3 : 길고 노란 바나나 바나나
문서4 : 저는 과일이 좋아요
각 문서에서 등장한 단어의 빈도를 행렬의 값으로 표기합니다. 그러면 문서 1에 없는 단어라도 DTM의 문서 1행의 열에 있을 수 있습니다.(ex 노란) ★ 열은 동일합니다.
문서 단어 행렬은 문서들을 서로 비교할 수 있도록 수치화할 수 있다는 점에서 의의를 갖습니다. 문서들을 벡터로 만들었으니 벡터 유사도 비교를 할 수 있습니다.
4. TF-IDF(Term Frequency-Inverse Document Frequency)
DTM 내에 있는 각 단어에 대한 중요도를 계산할 수 있는 TF-IDF 가중치에 대해서 알아보겠습니다.(TF-IDF가 가중치인 것 같습니다. 밑에서 다룹니다.)
TF-IDF를 사용하면, 기존의 DTM을 사용하는 것보다 보다 많은 정보를 고려하여 문서들을 비교할 수 있습니다.(가중치를 부여하기 때문입니다.) 많은 경우에서 DTM보다 더 좋은 성능을 얻을 수 있습니다.
-> TF-IDF란?
TF-IDF(Term Frequency-Inverse Document Frequency)는 단어의 빈도와 역 문서 빈도(문서의 빈도에 특정 식을 취함)를 사용하여 DTM 내의 각 단어들마다(열마다) 중요한 정도를 가중치로 주는 방법입니다. 우선 DTM을 만든 후, TF-IDF 가중치를 부여합니다.
TF-IDF는 주로 문서의 유사도를 구하는 작업, 검색 시스템에서 검색 결과의 중요도를 정하는 작업, 문서 내에서 특정 단어의 중요도를 구하는 작업 등에 쓰일 수 있습니다.
TF-IDF는 TF와 IDF를 곱한 값을 의미하는 데 이를 식으로 표현해 보겠습니다. 문서를 d, 단어를 t, 문서의 총개수를 n이라고 표현할 때 TF, DF, IDF는 각각 다음과 같이 정의할 수 있습니다.
1) tf(d, t) : 특정 문서 d에서의 특정 단어 t의 등장 횟수
> TF는 어렵게 생각하지 마라 이미 DTM에서 구한 값들입니다. DTM이 각 문서에서의 각 단어의 등장 빈도를 나타내는 값이었기 때문입니다.
2) df(t) : 특정 단어 t가 등장한 문서의 수
> 여기서 특정 단어가 각 문서, 또는 문서들에서 몇 번 등장했는지는 관심가지지 않으며 오직 특정 단어 t가 등장한 문서의 수에만 관심을 가집니다. 앞선 예제 바나나는 문서2와 문서3에서 등장했습니다. 이때 바나나의 df인 df(바나나)는 2입니다.
3) idf(d, t) : df(t)에 반비례하는 수
-> 로그를 사용하는 이유
① 총 문서의 개수 n이 커질수록 idf가 기하급수적으로 커지게 되므로 log를 사용합니다.
② 불용어는 중요한 단어에 비해 최소 수십 배 자주 등장합니다. 로그를 씌어주지 않으면, 희귀 단어들에 엄청난 가중치가 부여될 수 있습니다. 로그를 씌우면 이런 격차를 줄이는 효과가 있습니다.
※ 분모에 1을 더하는 이유는 특정 단어가 전체 문서에서 등장하지 않을 경우 분모가 0이 되는 상황을 방지하기 위함입니다.
따라서 TF-IDF는 모든 문서에서 자주 등장하는 단어는 중요도가 낮다고 판단하며(idf가 특정 단어 t가 등장한 횟수이므로 df가 크면 반비례로 작아지도록 함) 특정 문서에서만 자주 등장하는 단어는 중요도가 높다고 판단합니다.
TF-IDF 값이 낮으면 중요도가 낮은 것이며, TF-IDF 값이 크면 중요도가 큰 것입니다. 즉, the나 a와 같이 불용어의 경우에는 모든 문서에 자주 등장하기 마련이기 때문에 자연스럽게 불용어의 TF-IDF의 값은 다른 단어의 TF-IDF에 비해서 낮아지게 됩니다.
=> TF-IDF 구하기 예제
우선 tf는 앞서 사용한 DTM을 그대로 사용하면, 그것이 각 문서의 TF입니다. "tf는 문서마다 구하면 됩니다." 이제 TF와 곱할 IDF를 구합니다. "idf는 전체 문서의 고유 단어마다 구해주면 됩니다."
문서의 총 수는 4이기 때문에 ln 안에서 분자는 늘 4로 동일합니다. 분모의 경우에는 각 단어가 등장한 문서의 수(DF)를 의미하는데, 예를 들어서 '먹고'의 경우에는 총 2개의 문서(문서1, 문서2)에 등장했기 때문에 2라는 값을 가집니다.
-> 결과
결과를 보면 문서 1개에만 등장한 단어와 문서2개에만 등장한 단어는 값의 차이를 보입니다. IDF는 여러 문서에서 등장한 단어의 가중치를 낮춥니다.
-> 최종 결과
tf와 idf를 곱합니다. 바나나를 보면 문서 3에서의 바나나가 중요도가 높습니다. 다양한 문서에서 많이 등장하면 안 좋은 것이지만 특정 문서에서 자주 등장하면 좋은 것이기 때문입니다.
-> 사이킷런 실습
from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
'you know I want your love',
'I like you',
'what should I do ',
]
tfidfv = TfidfVectorizer().fit(corpus)
print(tfidfv.transform(corpus).toarray())
print(tfidfv.vocabulary_)
# 결과
[[0. 0.46735098 0. 0.46735098 0. 0.46735098
0. 0.35543247 0.46735098]
[0. 0. 0.79596054 0. 0. 0.
0. 0.60534851 0. ]
[0.57735027 0. 0. 0. 0.57735027 0.
0.57735027 0. 0. ]]
{'you': 7, 'know': 1, 'want': 5, 'your': 8, 'love': 3, 'like': 2, 'what': 6, 'should': 4, 'do': 0}
'[AI] > [네이버 BoostCamp | 학습기록]' 카테고리의 다른 글
[22.01.19] 딥러닝 PART.CS231n 2강 (0) | 2022.12.27 |
---|---|
NLP PART.문장 유사도 (0) | 2022.12.26 |
NLP PART.언어 모델 (0) | 2022.12.19 |
NLP PART.텍스트 전처리2 (0) | 2022.12.19 |
NLP PART.텍스트 전처리 (0) | 2022.12.14 |