1. Bag of Words (BOW)
Bag of Words란 단어들의 순서는 전혀 고려하지 않고, 단어들의 출현 빈도(frequency)에만 집중하는 텍스트 데이터의 수치화 표현 방법이다.
Bag of Words는 직역하자면 단어들의 가방입니다. 문서 내 모든 단어를 한꺼번에 가방(Bag) 안에 넣은 뒤에 흔들어서 섞는다는 의미로 Bag of Words(BOW) 모델이라고 한다.
✔ BoW를 만드는 과정
- 우선, 각 단어에 고유한 정수 인덱스를 부여합니다.
- 각 인덱스의 위치에 단어 토큰의 등장 횟수를 기록한 벡터를 만듭니다.
from konlpy.tag import Komoran
import re
# 코모란 형태소 분석기를 사용
komoran = Komoran()
# 정규 표현식을 통해 온점을 제거
text = re.sub("(\.)","",'마트에서 사는 사과의 가격과 시장에서 사는 가격은 다르다.')
# text를 형태소로 나눠주는 작업
token = komoran.morphs(text)
print(token)
# ['마트', '에서', '살', '는', '사과', '의', '가격', '과',
# '시장', '에서', '살', '는', '가격', '은', '다르', '다']
word2index = {}
bow = []
for voca in token:
# token을 읽으면서, word2index에 없는 단어는 새로 추가하고, 이미 있는 단어는 pass
if voca not in word2index.keys():
word2index[voca] = len(word2index)
# 단어의 개수는 최소 1개 이상이기 때문에 BoW 전체에 전부 기본값 1을 넣어준다
bow.insert(len(word2index) - 1, 1)
else:
# 재등장하는 단어의 인덱스를 받아온다
index = word2index.get(voca)
# 재등장한 단어는 해당하는 인덱스의 위치에 1을 더해준다.
# (corpus에서 단어의 개수를 센다. )
bow[index] = bow[index] + 1
print(word2index)
word2index = {}
bow = []
for voca in token:
# token을 읽으면서, word2index에 없는 단어는 새로 추가하고, 이미 있는 단어는 pass
if voca not in word2index.keys():
word2index[voca] = len(word2index)
# 단어의 개수는 최소 1개 이상이기 때문에 BoW 전체에 전부 기본값 1을 넣어준다
bow.insert(len(word2index) - 1, 1)
else:
# 재등장하는 단어의 인덱스를 받아온다
index = word2index.get(voca)
# 재등장한 단어는 해당하는 인덱스의 위치에 1을 더해준다.
# (corpus에서 단어의 개수를 센다. )
bow[index] = bow[index] + 1
print(word2index)
{'마트': 0, '에서': 1, '살': 2, '는': 3, '사과': 4, '의': 5,
'가격': 6, '과': 7, '시장': 8, '은': 9, '다르': 10, '다': 11}
token에 저장된 단어들에 인덱스를 저장한 결과를 word2index에 담는다.
bow
[1, 2, 2, 2, 1, 1, 2, 1, 1, 1, 1, 1]
'에서' , '살', '는', '가격'은 두번씩 등장하고 각 단어의 인덱스는 1,2,3,6으로 bow를 보면 값이 2로 갱신되어 있다.
✔ CountVectorizer 클래스로 BoW 만들기
- 사이킷런의 CountVectorizer 클래스를 활용
from sklearn.feature_extraction.text import CountVectorizer
corpus = ['you know I want your love. because I love you.']
vector = CountVectorizer()
print(vector.fit_transform(corpus).toarray()) # 코퍼스로부터 각 단어의 빈도 수를 기록
print(vector.vocabulary_) # 각 단어의 인덱스
[[1 1 2 1 2 1]]
{'you': 4, 'know': 1, 'want': 3, 'your': 5, 'love': 2, 'because': 0}
you와 love는 두 번씩 있으므로 인덱스 2와 인덱스 4는 2의 값을 가진다. you와 love를 중요한 단어로 인식한다는 뜻이다.
CountVectorizer는 기본적으로 2자리 이상의 문자에 대해서만 토큰으로 인식하기 때문에 I는 없어졌다.
✔ 불용어를 제거한 BoW 만들기
각 단어에 대한 빈도수를 수치화 하겠다는 것은 결국 텍스트 내에서 어떤 단어들이 중요한지를 보고싶다는 의미를 뜻한다.
불용어가 자주 쓰여 중요한 단어로 보일 수 있기 때문에 자연어 처리의 정확도를 높이기 위해 불용어를 제거해야한다.
1) 사용자가 직접 정의한 불용어
from sklearn.feature_extraction.text import CountVectorizer
text = ["Family is not an important thing. It's everything."]
vect = CountVectorizer(stop_words=["the", "a", "an", "is", "not"])
print(vect.fit_transform(text).toarray()) # 단어의 빈도수
print(vect.vocabulary_) # 각 단어의 인덱스
[[1 1 1 1 1]]
{'family': 1, 'important': 2, 'thing': 4, 'it': 3, 'everything': 0}
2) CountVectorizer에서 제공하는 불용어
from sklearn.feature_extraction.text import CountVectorizer
text = ["Family is not an important thing. It's everything."]
vect = CountVectorizer(stop_words="english")
print(vect.fit_transform(text).toarray()) # 단어의 빈도수
print(vect.vocabulary_) # 각 단어의 인덱스
[[1 1 1]]
{'family': 0, 'important': 1, 'thing': 2}
3) NLTK에서 지원하는 불용어
from sklearn.feature_extraction.text import CountVectorizer
from nltk.corpus import stopwords
text = ["Family is not an important thing. It's everything."]
sw = stopwords.words("english") # nltk에 있는 불용어
vect = CountVectorizer(stop_words=sw)
print(vect.fit_transform(text).toarray()) # 단어의 빈도수
print(vect.vocabulary_) # 각 단어의 인덱스
[[1 1 1 1]]
{'family': 1, 'important': 2, 'thing': 3, 'everything': 0}
2. 문서 단어 행렬(Document-Term Matrix, DTM)
문서 단어 행렬(Document-Term Matrix, DTM)이란 다수의 문서에서 등장하는 각 단어들의 빈도를 행렬로 표현한 것을 말한다.
문서1 : 먹고 싶은 사과
문서2 : 먹고 싶은 바나나
문서3 : 길고 노란 바나나 바나나
문서4 : 저는 과일이 좋아요
- | 과일이 | 길고 | 노란 | 먹고 | 바나나 | 사과 | 싶은 | 저는 | 좋아요 |
문서1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
문서2 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 |
문서3 | 0 | 1 | 1 | 0 | 2 | 0 | 0 | 0 | 0 |
문서4 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
각 문서에서 등장한 단어의 빈도를 행렬의 값으로 표시한다.
문서 단어 행렬은 문서들을 서로 비교할 수 있도록 수치화할 수 있다는 점에서 의의를 갖는다.
❌ 문서 단어 행렬(Document-Term Matrix)의 한계
1) 희소 표현(Sparse repre)
DTM도 원-핫 벡터의 단점을 가지고 있다.
DTM에서의 각 행을 문서 벡터라고 해보면, 각 문서 벡터의 차원은 원-핫 벡터와 마찬가지로 전체 단어 집합의 크기를 가진다.
원-핫 벡터나 DTM과 같은 대부분의 값이 0인 표현을 희소 벡터(sparse vector) 또는 희소 행렬(sparse matrix)라고 부르는데, 희소 벡터는 많은 양의 저장 공간과 계산을 위한 리소스를 필요로 한다.
이러한 이유로 전처리를 통해 단어 집합의 크기를 줄이는 일은 BoW표현을 사용하는 모델에서 중요하다.
텍스트 전처리 방법을 사용하여 구두점, 빈도수가 낮은 단어, 불용어를 제거하고, 어간이나 표제어 추출을 통해 단어를 정규화하여 단어 집합의 크기를 줄일 수 있다.
2) 단순 빈도 수 기반 접근
예를 들어, 불용어인 the는 어떤 문서이든 자주 등장할 수 밖에 없다.
그런데 유사한 문서인지 비교하고 싶은 문서1, 문서2, 문서3에서 동일하게 the가 빈도수가 높다고 해서 이 문서들이 유사한 문서라고 판단해서는 안된다.
각 문서에서 중요한 단어와 불필요한 단어들이 혼재되어 있는데 DTM에 불용어와 중요한 단어에 대해서 가중치를 줄 수 있는 방법을 위해 사용하는 것이 TF-IDF이다.
3. TF-IDF (Term Frequency-Inverse Document Frequency)
TF-IDF(Term Frequency-Inverse Document Frequency)는 단어의 빈도와 역 문서 빈도(문서의 빈도에 특정 식을 취함)를 사용하여 DTM 내의 각 단어들마다 중요한 정도를 가중치로 주는 방법이다.
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가 등장한 문서의 수에만 관심을 가진다.
앞서 배운 DTM에서 바나나는 문서2와 문서3에서 등장했다. 이 경우, 바나나의 df는 2이다.
3) idf(d, t) : df(t)에 반비례하는 수.
= 역 문서 빈도(문서, 단어)
$idf(d,t) = log(\frac{n}{1+df(t)})$
log를 사용하지 않고 IDF를 DF의 역수로 사용한다면 총 문서의 수 n이 커질 수록, IDF의 값은 기하급수적으로 커지게 된다.
또한 log 안의 식에서 분모에 1을 더해주는 이유는 특정 단어가 전체 문서에서 등장하지 않을 경우에 분모가 0이 되는 상황을 방지하기 위함이다.
TF-IDF는 모든 문서에서 자주 등장하는 단어는 중요도가 낮다고 판단하며, 특정 문서에서만 자주 등장하는 단어는 중요도가 높다고 판단한다.
즉, the나 a와 같이 불용어의 경우에는 모든 문서에 자주 등장하기 마련이기 때문에 자연스럽게 불용어의 TF-IDF의 값은 다른 단어의 TF-IDF에 비해서 낮아지게 된다.
- | 과일이 | 길고 | 노란 | 먹고 | 바나나 | 사과 | 싶은 | 저는 | 좋아요 |
문서1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 |
문서2 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 |
문서3 | 0 | 1 | 1 | 0 | 2 | 0 | 0 | 0 | 0 |
문서4 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
TF는 앞서 사용한 DTM을 그대로 사용하면, 그것이 각 문서에서의 각 단어의 TF가 된다.
IDF는 아래와 같이 계산한다. (로그는 대부분 자연 로그를 사용)
단어 | IDF(역 문서 빈도) |
과일이 | ln(4/(1+1)) = 0.693147 |
길고 | ln(4/(1+1)) = 0.693147 |
노란 | ln(4/(1+1)) = 0.693147 |
먹고 | ln(4/(2+1)) = 0.287682 |
바나나 | ln(4/(2+1)) = 0.287682 |
사과 | ln(4/(1+1)) = 0.693147 |
싶은 | ln(4/(2+1)) = 0.287682 |
저는 | ln(4/(1+1)) = 0.693147 |
좋아요 | ln(4/(1+1)) = 0.693147 |
DTM과 IDF를 곱해주면 TF-IDF가 나오게 된다.
- | 과일이 | 길고 | 노란 | 먹고 | 바나나 | 사과 | 싶은 | 저는 | 좋아요 |
문서1 | 0 | 0 | 0 | 0.287682 | 0 | 0.693147 | 0.287682 | 0 | 0 |
문서2 | 0 | 0 | 0 | 0.287682 | 0.287682 | 0 | 0.287682 | 0 | 0 |
문서3 | 0 | 0.693147 | 0.693147 | 0 | 0.575364 | 0 | 0 | 0 | 0 |
문서4 | 0.693147 | 0 | 0 | 0 | 0 | 0 | 0 | 0.693147 | 0.693147 |
문서2에서의 바나나의 TF-IDF 가중치와 문서3에서의 바나나의 TF-IDF 가중치가 다른 것을 볼 수 있다.
TF-IDF는 특정 문서에서 자주 등장하는 단어는 그 문서 내에서 중요한 단어로 판단하기 때문이다.
TF-IDF가 큰 값이 중요한 단어라고 판단할 수 있다.
# 사이킷런 활용 (CountVectorizer, TfidfVectorizer)
- 문서 단어 행렬 만들기 DTM
from sklearn.feature_extraction.text import CountVectorizer
corpus = [
'you know I want your love',
'I like you',
'what should I do ',
]
vector = CountVectorizer()
print(vector.fit_transform(corpus).toarray()) # 단어의 빈도수
print(vector.vocabulary_) # 각 단어의 인덱스
[[0 1 0 1 0 1 0 1 1]
[0 0 1 0 0 0 0 1 0]
[1 0 0 0 1 0 1 0 0]]
{'you': 7, 'know': 1, 'want': 5, 'your': 8, 'love': 3, 'like': 2, 'what': 6, 'should': 4, 'do': 0}
- TF-IDF 계산
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()) # 단어의 TF-IDF
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}
카운트 기반 벡터화인 CountVectorizer는 단순히 빈도를 기반으로 표현을 해주지만, TF-IDF 벡터화는 다른 문장에서의 단어 빈도도 고려하여 해당 단어의 중요도를 표현해주고 있다.
따라서 CountVectorizer보다는 TF-IDF를 일반적으로 더 많이 쓰인다.
<참고 사이트> wikidocs.net/24557
'NLP' 카테고리의 다른 글
벡터의 유사도(Vector Similarity) (0) | 2022.01.28 |
---|---|
패딩(Padding) (0) | 2022.01.28 |
정수 인코딩 (Integer Encoding) (0) | 2022.01.27 |
어간 추출(Stemming)과 표제어 추출(Lemmatization) (0) | 2022.01.27 |
토큰화 Tokenization (0) | 2022.01.27 |