Bag of Words란?
Bag of Words의 개념과 활용
Bag of Words (BoW)는 문서를 단어의 빈도수로 표현하는 방법으로, 단어의 순서를 무시하고 각 단어가 문서에 얼마나 자주 등장했는지를 수치화하는 기법입니다. BoW는 단순하지만, 자연어 처리에서 기본적인 텍스트 표현 방법으로 널리 사용되며, 특히 문서 분류, 유사도 측정, 추천 시스템 등에서 유용합니다.
Bag of Words의 특징
BoW는 텍스트를 단어의 출현 빈도 기반으로 표현하기 때문에 두 가지 주요 특징을 가지고 있습니다.
1. 단어 순서 무시: BoW에서는 문장의 구조나 단어 순서를 전혀 고려하지 않습니다. 단어의 순서가 바뀌어도 단어 빈도만 같다면 BoW 벡터는 동일합니다.
2. 단어 빈도 중심: BoW 벡터는 단어가 등장한 횟수만을 기록합니다. 특정 단어가 자주 등장할수록 해당 단어의 중요성이 높다고 가정하는 방식입니다.
이러한 특징 덕분에 BoW는 계산이 단순하고 직관적이지만, 문맥이나 의미를 반영하지 못하는 한계도 있습니다.
Bag of Words 예시
아래 예시 문장의 등장 횟수를 수치화해서 단어의 빈도를 출력해 보겠습니다.
from konlpy.tag import Okt # konlpy의 Okt 형태소 분석기 불러오기
okt = Okt() # Okt 형태소 분석기 객체 생성
def build_bag_of_words(document):
document = document.replace('.', '') # 문장부호 '.' 제거
tokenized_document = okt.morphs(document) # 형태소(단어) 단위로 토큰화
word_to_index = {} # 단어와 인덱스를 매핑할 딕셔너리
bow = [] # Bag of Words(BOW)를 저장할 리스트
for word in tokenized_document:
if word not in word_to_index: # 단어가 처음 등장하면
word_to_index[word] = len(word_to_index) # 고유 인덱스 할당
bow.insert(len(word_to_index) - 1, 1) # BOW 리스트에 해당 단어 위치에 1 추가
else:
index = word_to_index.get(word) # 기존에 있던 단어라면 해당 인덱스를 가져와서
bow[index] += 1 # 등장 횟수를 1 증가시킴
return tokenized_document, word_to_index, bow # 단어-인덱스 매핑과 BOW 벡터 반환
doc1 = "The dog loves playing in the park and the dog enjoys chasing birds."
tokenized_document, vocab, bow = build_bag_of_words(doc1)
print('토큰화 : ',tokenized_document)
print('정수 인코딩 :', vocab)
print('각 단어의 등장 횟수 :', bow)
> 토큰화 : ['The', 'dog', 'loves', 'playing', 'in', 'the', 'park', 'and', 'the', 'dog', 'enjoys', 'chasing', 'birds']
정수 인코딩 : {'The': 0, 'dog': 1, 'loves': 2, 'playing': 3, 'in': 4, 'the': 5, 'park': 6, 'and': 7, 'enjoys': 8, 'chasing': 9, 'birds': 10}
각 단어의 등장 횟수 : [1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1]
CountVectorizer 클래스로 BoW 만들기
CountVectorizer 클래스를 사용하여 주어진 문장에서 Bag of Words (BoW)를 생성하는 방법을 보여줍니다.
from sklearn.feature_extraction.text import CountVectorizer
corpus = ["The dog loves playing in the park and the dog enjoys chasing birds."]
vector = CountVectorizer()
print('각 단어의 등장 횟수 :', vector.fit_transform(corpus).toarray())
print('정수 인코딩 :',vector.vocabulary_)
> 각 단어의 등장 횟수 : [[1 1 1 2 1 1 1 1 1 3]]
정수 인코딩 : {'the': 9, 'dog': 3, 'loves': 6, 'playing': 8, 'in': 5, 'park': 7, 'and': 0, 'enjoys': 4, 'chasing': 2, 'birds': 1}
출력된 결과를 보면
Bow의 결과가 좀 다릅니다.
CountVectorizer 클래스를 사용하지 않은 Bow는
각 단어의 등장 횟수 : [1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1]의 결과가 나왔으며,
CountVectorizer 클래스를 사용한 Bow는
각 단어의 등장 횟수 : [[1 1 1 2 1 1 1 1 1 3]]의 결과가 나왔습니다.
두 방식에서 생성된 Bag of Words (BoW)의 결과가 서로 다른 이유는, 단어의 전처리 방식과 단어 집합 구성 방식이 다르기 때문입니다.
1️⃣ 단어 집합 구성 방식
CountVectorizer를 사용하지 않은 BoW에서는 모든 단어와 구두점 등을 포함해 단어 집합을 구성합니다.
CountVectorizer를 사용한 BoW에서는 기본적으로 불필요한 구두점 등을 제거하고, 각 단어를 소문자로 변환하는 등의 추가 전처리를 수행합니다.
2️⃣구두점 및 대소문자 처리
CountVectorizer는 기본적으로 구두점과 대소문자를 무시하고, 모든 텍스트를 소문자로 변환하여 단어 집합을 구성합니다. 예를 들어, 문장에 "The"와 "the"가 함께 등장하더라도 CountVectorizer는 이를 같은 단어로 간주하고 "the"로 통일합니다.
반면, CountVectorizer를 사용하지 않은 BoW에서는 대소문자를 구분하여 "The"와 "the"를 각각 다른 단어로 인식하고 별도의 인덱스를 부여합니다.
(위 코드에서도 CountVectorizer를 사용하지 않은 BoW에서는 대문자 The는 0 인덱스로, 소문자 the는 5 인덱스로 처리가 되었습니다.)
3️⃣ 출력 포맷
CountVectorizer의 결과는 일반적으로 2차원 배열 형태([[...]])로 반환됩니다. 여러 문서의 BoW를 동시에 표현할 수 있기 때문입니다.
반면, 직접 구현한 BoW의 결과는 단일 벡터([...])로 나타났습니다. 이는 단일 문서에 대해서만 BoW를 생성했기 때문입니다.
CountVectorizer + 불용어 제거 (사용자가 직접 정의)
문장의 의미 전달에 큰 영향을 미치지 않으면서 자주 등장하는 단어들인 불용어를 정의하여 제거합니다.
from sklearn.feature_extraction.text import CountVectorizer
from nltk.corpus import stopwords
text = ["The dog loves playing in the park and the dog enjoys chasing birds."]
vect = CountVectorizer(stop_words=["the", "and", "in"])
print('bag of words vector :',vect.fit_transform(text).toarray())
print('vocabulary :',vect.vocabulary_)
> bag of words vector : [[1 1 1 2 1 1 1 1]]
vocabulary : {'dog': 3, 'loves': 5, 'playing': 7, 'park': 6, 'and': 0, 'enjoys': 4, 'chasing': 2, 'birds': 1}
CountVectorizer에서 제공하는 자체 불용어 사용
from sklearn.feature_extraction.text import CountVectorizer
text = ["The dog loves playing in the park and the dog enjoys chasing birds."]
vect = CountVectorizer(stop_words="english") # 문자열로 "english" 설정
print('bag of words vector :', vect.fit_transform(text).toarray())
print('vocabulary :', vect.vocabulary_)
> bag of words vector : [[1 1 2 1 1 1 1]]
vocabulary : {'dog': 2, 'loves': 4, 'playing': 6, 'park': 5, 'enjoys': 3, 'chasing': 1, 'birds': 0}
NLTK에서 지원하는 불용어 사용
from nltk.corpus import stopwords
text = ["The dog loves playing in the park and the dog enjoys chasing birds."]
stop_words = stopwords.words("english")
vect = CountVectorizer(stop_words=stop_words)
print('bag of words vector :',vect.fit_transform(text).toarray())
print('vocabulary :',vect.vocabulary_)
> bag of words vector : [[1 1 2 1 1 1 1]]
vocabulary : {'dog': 2, 'loves': 4, 'playing': 6, 'park': 5, 'enjoys': 3, 'chasing': 1, 'birds': 0}
Bag of Words의 한계
BoW는 단순한 방법으로 텍스트를 표현하지만, 아래와 같은 한계도 있습니다.
• 문맥 정보 부족: BoW는 단어의 순서나 문맥을 반영하지 않기 때문에, 단어가 주변 단어와 함께 가지는 의미를 표현할 수 없습니다. 예를 들어, “강아지가 고양이를 쫓는다”와 “고양이가 강아지를 쫓는다”는 서로 다른 의미이지만, BoW에서는 같은 단어들이 등장하는 것으로만 인식합니다.
• 단어의 중요도 반영 어려움: 모든 단어의 빈도만 반영하기 때문에, 자주 등장하지만 정보량이 적은 불용어(stop words)들도 BoW 벡터에 포함됩니다. 이를 해결하기 위해 TF-IDF와 같은 가중치 기법을 추가로 활용하기도 합니다.