by museonghwang

BoW(Bag of Words) 개념 및 실습

|

  1. Bag of Words 개념
  2. Bag of Words 특징
  3. Bag of Words 생성 절차
  4. Bag of Words 실습(1) - 한국어 BoW 만들기
  5. Bag of Words 실습(2) - CountVectorizer 클래스로 BoW 만들기
  6. Bag of Words 실습(2) - 불용어를 제거한 BoW 만들기
    • 6.1 사용자가 직접 정의한 불용어 사용
    • 6.2 CountVectorizer에서 제공하는 자체 불용어 사용
    • 6.3 NLTK에서 지원하는 불용어 사용

카운트 기반의 단어 표현 방법인 Bag of Words(BoW) 개념과 생성 방법을 알아보겠습니다.



1. Bag of Words 개념

BoW(Bag of Words) 는 단어를 수치화하는 방법 중 하나로, 문서 내 단어들의 순서와 의미는 고려하지 않고 오직 출현 빈도(frequency)만 고려하여 단어를 표현하는 방법 입니다.

BoW는 국소 표현방법(Local Representation) 또는 이산 표현방법(Discrete Representation)의 일종으로 카운트 기반의 단어 표현방법(Count-based Word Representation)이라고 부릅니다.


2. Bag of Words 특징

BoW어떤 단어들이 몇 회 나왔는지는 파악할 수 있지만, 단어들이 어떤 순서로 구성되었는지는 파악할 수 없다는 특징 이 있습니다. Bag of Words를 직역하면 말 그대로 단어들의 가방 을 의미합니다. 가방에 문장의 단어들을 넣고 흔든다면, 단어의 순서는 무의미해집니다. 즉, 단어의 순서는 무시하고, 오직 단어의 빈도수에만 집중하는 방법 입니다.

image


3. Bag of Words 생성 절차

BoW를 만드는 과정은 두 가지의 절차로 생성됩니다.

  1. 문서 내 단어별로 고유의 정수 인덱스를 할당하여 단어 집합(Vocabulary) 생성
  2. 각 단어별 인덱스에 단어 토큰의 출현 빈도를 저장한 BoW 벡터 생성


4. Bag of Words 실습(1) - 한국어 BoW 만들기

한국어 예제를 통해서 BoW에 대해서 이해해보도록 하겠습니다. 우선 문서1이 다음과 같이 작성되어있다고 가정하겠습니다.

문서1 : 정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다.


문서1에 대해서 BoW를 만들어보겠습니다. 아래의 함수는 입력된 문서에 대해서 단어 집합(vocaburary)을 만들어 각 단어에 정수 인덱스를 할당하고, BoW를 만듭니다.

# pip install konlpy
from konlpy.tag import Okt

okt = Okt()

def build_bag_of_words(document):
    # 온점 제거 및 형태소 분석
    document = document.replace('.', '')
    tokenized_document = okt.morphs(document)
    print('tokenized_document :', tokenized_document)

    # 단어별 고유의 정수 인덱스를 할당할 단어 집합(Vocabulary)
    word_to_index = {}
    # 단어별 인덱스에 단어의 출현빈도를 저장할 BoW 벡터
    bow = []

    for word in tokenized_document:
        # 처음 출현한 단어인 경우(=단어 집합에 미존재)
        if word not in word_to_index.keys():
            # 단어가 등장한 순서를 정수 인덱스로 부여
            word_to_index[word] = len(word_to_index)
            # 처음 등장한 단어이므로 BoW에 전부 기본값 1을 넣는다.
            bow.insert(len(word_to_index) - 1, 1)
        
        # 출현 이력이 있는 단어의 경우
        else:
            # 재등장하는 단어의 인덱스
            index = word_to_index.get(word)
            # 재등장한 단어는 해당하는 인덱스의 위치에 1을 더한다.
            bow[index] = bow[index] + 1
    
    return word_to_index, bow


해당 함수에 문서1을 입력으로 넣어보겠습니다.

doc1 = "정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다."

vocab, bow = build_bag_of_words(doc1)

print('vocabulary :', vocab)
print('bag of words vector :', bow)
[output]
tokenized_document : ['정부', '가', '발표', '하는', '물가상승률', '과', '소비자', '가', '느끼는', '물가상승률', '은', '다르다']
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]


문서1에 대해 온점 제거 및 형태소 분석을 한 결과는 첫번째 출력, 문서1에 각 단어에 대해서 인덱스를 부여한 결과는 두번째 출력, 문서1의 BoW는 세번째 출력 결과입니다.

세번째 출력 결과를 보면, 인덱스 4에 해당하는 물가상승률은 두 번 언급되었기 때문에 인덱스 4에 해당하는 값이 2입니다. 만약, 한국어에서 불용어에 해당되는 조사들 또한 제거한다면 더 정제된 BoW를 만들 수도 있습니다.


다음은 문서2가 다음과 같이 작성되어있다고 가정하겠습니다.

문서2 : 소비자는 주로 소비하는 상품을 기준으로 물가상승률을 느낀다.


위 함수에 문서2를 입력으로 넣어보겠습니다.

doc2 = '소비자는 주로 소비하는 상품을 기준으로 물가상승률을 느낀다.'

vocab, bow = build_bag_of_words(doc2)

print('vocabulary :', vocab)
print('bag of words vector :', bow)
[output]
tokenized_document : ['소비자', '는', '주로', '소비', '하는', '상품', '을', '기준', '으로', '물가상승률', '을', '느낀다']
vocabulary : {'소비자': 0, '는': 1, '주로': 2, '소비': 3, '하는': 4, '상품': 5, '을': 6, '기준': 7, '으로': 8, '물가상승률': 9, '느낀다': 10}
bag of words vector : [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1]


문서1과 문서2를 합쳐서 문서 3이라고 명명하고, BoW를 만들 수도 있습니다.

문서2 : 정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다. 소비자는 주로 소비하는 상품을 기준으로 물가상승률을 느낀다.


doc3 = doc1 + ' ' + doc2
print('doc3 :', doc3)

vocab, bow = build_bag_of_words(doc3)
print('\nvocabulary :', vocab)
print('bag of words vector :', bow)
[output]
doc3 : 정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다. 소비자는 주로 소비하는 상품을 기준으로 물가상승률을 느낀다.

tokenized_document : ['정부', '가', '발표', '하는', '물가상승률', '과', '소비자', '가', '느끼는', '물가상승률', '은', '다르다', '소비자', '는', '주로', '소비', '하는', '상품', '을', '기준', '으로', '물가상승률', '을', '느낀다']
vocabulary : {'정부': 0, '가': 1, '발표': 2, '하는': 3, '물가상승률': 4, '과': 5, '소비자': 6, '느끼는': 7, '은': 8, '다르다': 9, '는': 10, '주로': 11, '소비': 12, '상품': 13, '을': 14, '기준': 15, '으로': 16, '느낀다': 17}
bag of words vector : [1, 2, 1, 2, 3, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1]


문서3의 단어 집합은 문서1과 문서2의 단어들을 모두 포함하고 있는 것들을 볼 수 있습니다. BoW는 종종 여러 문서의 단어 집합을 합친 뒤에, 해당 단어 집합에 대한 각 문서의 BoW를 구하기도 합니다. 가령, 문서3에 대한 단어 집합을 기준으로 문서1, 문서2의 BoW를 만든다고 한다면 결과는 아래와 같습니다.

문서3 단어 집합에 대한 문서1 BoW : [1, 2, 1, 1, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
문서3 단어 집합에 대한 문서2 BoW : [0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 2, 1, 1, 1]


문서3 단어 집합에서 물가상승률이라는 단어는 인덱스가 4에 해당됩니다. 물가상승률이라는 단어는 문서1에서는 2회 등장하며, 문서2에서는 1회 등장하였기 때문에 두 BoW의 인덱스 4의 값은 각각 2와 1이 되는 것을 볼 수 있습니다.

BoW각 단어가 등장한 횟수를 수치화하는 텍스트 표현 방법 이므로 주로 어떤 단어가 얼마나 등장했는지를 기준으로 문서가 어떤 성격의 문서인지를 판단하는 작업에 쓰입니다. 즉, 분류 문제나 여러 문서 간의 유사도를 구하는 문제에 주로 쓰입니다. 가령, ‘달리기’, ‘체력’, ‘근력’과 같은 단어가 자주 등장하면 해당 문서를 체육 관련 문서로 분류할 수 있을 것이며, ‘미분’, ‘방정식’, ‘부등식’과 같은 단어가 자주 등장한다면 수학 관련 문서로 분류할 수 있습니다.


5. Bag of Words 실습(2) - CountVectorizer 클래스로 BoW 만들기

사이킷런에서는 단어의 빈도를 Count하여 Vector로 만드는 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_)
[output]
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의 값을 가지는 것을 볼 수 있습니다. 주의할 것은 CountVectorizer단지 띄어쓰기만을 기준으로 단어를 자르는 낮은 수준의 토큰화를 진행하고 BoW를 만든다는 점 입니다. 이는 영어의 경우 띄어쓰기만으로 토큰화가 수행되기 때문에 문제가 없지만 한국어에 CountVectorizer를 적용하면, 조사 등의 이유로 제대로 BoW가 만들어지지 않음을 의미 합니다.

예를 들어, ‘정부가 발표하는 물가상승률과 소비자가 느끼는 물가상승률은 다르다.’ 라는 문장을 CountVectorizer를 사용하여 BoW로 만들 경우, CountVectorizer는 ‘물가상승률’이라는 단어를 인식하지 못 합니다. CountVectorizer는 띄어쓰기를 기준으로 분리한 뒤에 ‘물가상승률과’와 ‘물가상승률은’으로 조사를 포함해서 하나의 단어로 판단하기 때문에 서로 다른 두 단어로 인식 합니다.


6. Bag of Words 실습(2) - 불용어를 제거한 BoW 만들기

BoW 를 사용한다는 것은 그 문서에서 각 단어가 얼마나 자주 등장했는지를 보겠다는 것 입니다. 그리고 각 단어에 대한 빈도수를 수치화 하겠다는 것 은 결국 텍스트 내에서 어떤 단어들이 중요한지를 보고싶다는 의미를 함축 하고 있습니다. 불용어는 자연어 처리에서 별로 의미를 갖지 않는 단어들입니다. 그렇다면 BoW를 만들때 불용어를 제거하는 일은 자연어 처리의 정확도를 높이기 위해서 선택할 수 있는 전처리 기법입니다.

영어의 BoW를 만들기 위해 사용하는 CountVectorizer는 불용어를 지정하면, 불용어는 제외하고 BoW를 만들 수 있도록 불용어 제거 기능을 지원하고 있습니다.

from sklearn.feature_extraction.text import CountVectorizer
from nltk.corpus import stopwords


6.1 사용자가 직접 정의한 불용어 사용

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_)
[output]
bag of words vector : [[1 1 1 1 1]]
vocabulary : {'family': 1, 'important': 2, 'thing': 4, 'it': 3, 'everything': 0}


6.2 CountVectorizer에서 제공하는 자체 불용어 사용

text = ["Family is not an important thing. It's everything."]
vect = CountVectorizer(stop_words="english")

print('bag of words vector :', vect.fit_transform(text).toarray())
print('vocabulary :', vect.vocabulary_)
[output]
bag of words vector : [[1 1 1]]
vocabulary : {'family': 0, 'important': 1, 'thing': 2}


6.3 NLTK에서 지원하는 불용어 사용

import nltk
# nltk.download('stopwords')

text = ["Family is not an important thing. It's everything."]
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_)
[output]
bag of words vector : [[1 1 1 1]]
vocabulary : {'family': 1, 'important': 2, 'thing': 3, 'everything': 0}