시래 블로그

단어 수 세서 문서-단어 행렬 만들기, CountVectorizer 본문

데이터 과학

단어 수 세서 문서-단어 행렬 만들기, CountVectorizer

시래 2020. 2. 8. 15:20

 

 

텍스트 데이터를 분석할 때 가장 흔히 사용하는 방법이 단어 수를 세는 것입니다. 각각의 텍스트에 등장한 단어 수를 알면 이를 기반으로 키워드를 추출한다거나, 표절 검사, 문서 분류 등 다양한 분석을 할 수 있습니다.

 

예를 들어 아래와 같은 세 개의 문장이 있을 때, 

 

  1. 누구나 한번쯤은 사랑에 웃고
  2. 누구나 한번쯤은 사랑에 울고
  3. 그것이 바로 사랑 사랑 사랑이야

 

문장별로 단어가 몇 번 등장했는지 표(행렬)로 나타낼 수 있습니다.

 

  누구나 한번쯤 사랑 웃고 울고 그것 바로
문장1 1 1 1 1      
문장2 1 1 1   1    
문장3     3     1 1

 

 

이렇게 한 번 정리를 해놓으면, 이후 여러 가지 분석에 이용할 수 있습니다. 예를 들어 '사랑이 5번 등장했으니, 무언가 중요한 단어가 않을까'라거나, '문장1과 문장2는 문장3보다 비슷하구나' 식으로 말이죠.

 

자연어 처리에서는 위와 같은 표를 '문서-단어 행렬'(document-term matrix)이라고 부릅니다.

 

또한 여기서는 단어가 사용된 순서는 무시되었는데, 이를 'bag of words'라고 부릅니다. 예를 들어 문장1은 네 개의 단어(누구나, 한번쯤, 사랑, 웃고)가 순서 없이 뒤섞여 들어 있는 가방에 비유한 것입니다.

 

수학적으로 보면 위 표는 각각의 문장과 단어를 벡터로 나타내준 것입니다. 따라서 행렬과 벡터의 성질을 이용한 알고리즘(분류, 클러스터링, 차원축소 등)을 적용해볼 수도 있습니다.

 

사이킷런의 CountVectorizer

사이킷런(sklearn)의 CountVectorizer는 각 문서에 어떤 단어가 몇 번 등장했는지를 파악할 때 사용합니다. 단어를 세서(count) 문서를 벡터화(vectorize)한다는 의미입니다. 이는 문서에서 특성(어떤 단어가 몇 번 등장했는지)을 추출하는 것이으로 볼 수 있기 때문에, CountVectorizer는 sklearn.feature_extraction 항목에 있습니다.

 

다음과 같이 세 개의 문장을 담은 text를 fit_transfrom시켜주면,

from sklearn.feature_extraction.text import CountVectorizer

text = ["누구나 한번쯤은 사랑에 웃고", 
        "누구나 한번쯤은 사랑에 울고",
        "그것이 바로 사랑 사랑 사랑이야"]
        
count_vec = CountVectorizer()
m = count_vec.fit_transform(text)
m.toarray()

 

아래와 같은 결과를 얻을 수 있습니다.

 

문서-단어 행렬에서 각각의 열(column)이 의미하는 바는 vocabulary_를 통해 확인할 수 있습니다.

count_vec.vocabulary_

예를 들어 1행은 누구나, 8행은 한점쯤은 ... 등을 나타낸다는 의미입니다. 

 

여기서 '사랑'이라는 같은 단어를 CountVectorizer는 사랑에, 사랑, 사랑이야라는 각기 다른 단어로 인식했다는 것을 알 수 있습니다. 이를 해결하는 방법은 미리 전처리를 해주든가, 아니면 내가 쓰고자 하는 토크나이저를 CountVectorizer의 초깃값으로 넣어주는 것입니다.

count_vec = CountVectorizer(tokenizer=mytokenizer)

 

CountVectorizer의 디폴트 값은 영어에 맞춰져 있기 때문에 한국어 자연어 처리에는 맞지 않습니다. 대신 tokenizer 인자에 한국어 형태소 분석기(konlpy 등)를 넣어주면 됩니다.

 

CountVectorizer는 토크나이징+벡터화를 동시에 해줍니다. 만약 토크나이징을 먼저 한 다음 list of lists 형식의 텍스트 데이터를 넣어주고 싶다면 다음과 같이 해주면 됩니다.

from sklearn.feature_extraction.text import CountVectorizer

text = [['누구나','한번쯤','사랑', '웃고'], 
        ['누구나','한번쯤','사랑', '울고'],
        ['그것', '바로', '사랑', '사랑', '사랑']]
        
count_vec = CountVectorizer(tokenizer=lambda x: x, lowercase=False)
m = count_vec.fit_transform(text)

 

tokenizer 인자에는 list를 받아서 그대로 list를 내보내는 함수를 넣어줍니다. 또한 소문자화를 하지 않도록 설정해야 에러가 나지 않습니다.

 

그런데 저는 문서-단어 행렬을 만들어야 하는 일이 있을 때, 사이킷런의 CountVectorizer를 불러오기보다는 자체 제작한 걸 사용하곤 합니다. 그 방법에 대해 알아보겠습니다.

 

CountVectorzier 자체 제작하기

사이킷런 API처럼 사용할 수 있게 myCountVectorzier 클래스를 정의했습니다. 토크나이징은 이미 되어있다고 가정합니다.

import numpy as np

class myCountVectorizer():
    
    def __init__(self):
        self.vocabulary_ = {}
    
    def fit(self, docs):
        self.vocabulary_ = {}
        for words in docs:
            for word in words:
                self.vocabulary_[word] = self.vocabulary_.get(word, len(self.vocabulary_))
        return self
    
    def transform(self, docs):
        m = np.zeros((len(docs), len(self.vocabulary_)))
        for i, words in enumerate(docs):
            for word in words:
                j = self.vocabulary_.get(word)
                if j is not None:
                    m[i, j] += 1
        return m
    
    def fit_transform(self, docs):
        self.fit(docs)
        return self.transform(docs)

 

fit 단계에서 하는 일은 단어 사전을 만드는 일입니다. 단어마다 인덱스를 부여해서, 이후에 어떤 단어가 행렬의 몇 번 행을 나타내는지 알고자 할 때 사용합니다. 이 단계를 건너 뛰고 이미 만들어져 있는 단어 사전을 이용할 수도 있습니다.

 

위 코드에서는 딕셔너리의 get 메서드를 이용했습니다. get(k, d)은 딕셔너리에 키가 있으면 해당하는 값을 반환하고, 아니면 d를 반환합니다.

 

transform 단계에서는 본격적으로 문서-단어 행렬을 만듭니다. 사용법은 CountVectorizer와 비슷합니다.

count_vec = myCountVectorizer()
m = count_vec.fit_transform(text)
m

 

참고로 문서-단어 행렬은 대규모 데이터에서 대부분의 값이 0이 될 것입니다. 하나의 텍스트에 2000개 종류의 단어를 사용한다 해도, 전체 단어 셋은 몇 만 개는 될 것이기 때문입니다. 이는 메모리 부족 문제를 초래하기 쉽기 때문에, 사이킷런의 CountVectorizer는 희소행렬을 사용합니다. 희소행렬은 반환하도록 만드는 건 np.ones를 dok_matrix로만 바꾸어주면 됩니다. 희소행렬에 대한 좀 더 자세한 내용은 여기를 참조해주세요.

 

from scipy.sparse import dok_matrix

class sparseCountVectorizer(myCountVectorizer):

    def transform(self, docs):
        m = dok_matrix((len(docs), len(self.vocabulary_)))
        for i, words in enumerate(docs):
            for word in words:
                j = self.vocabulary_.get(word)
                if j is not None:
                    m[i, j] += 1
        return m.tocsr()

 

Comments