작성자: admin 작성일시: 2016-06-14 23:38:35 조회수: 5602 다운로드: 202
카테고리: 머신 러닝 태그목록:

Scikit-Learn의 문서 전처리 기능

BOW (Bag of Words)

문서를 숫자 벡터로 변환하는 가장 기본적인 방법은 BOW (Bag of Words) 이다. BOW 방법에서는 전체 문서 $\{D_1, D_2, \ldots, D_n\}$ 를 구성하는 고정된 단어장(vocabulary) $\{W_1, W_2, \ldots, W_m\}$ 를 만들고 $D_i$라는 개별 문서에 단어장에 해당하는 단어들이 포함되어 있는지를 표시하는 방법이다.

$$ \text{ 만약 단어 } W_j \text{가 문서} D_i \text{ 안에 있으면 }, \;\; \rightarrow x_{ij} = 1 $$

Scikit-Learn 의 문서 전처리 기능

Scikit-Learn 의 feature_extraction 서브패키지와 feature_extraction.text 서브 패키지는 다음과 같은 문서 전처리용 클래스를 제공한다.

  • DictVectorizer:
    • 단어의 수를 세어놓은 사전에서 BOW 벡터를 만든다.
  • CountVectorizer:
    • 문서 집합으로부터 단어의 수를 세어 BOW 벡터를 만든다.
  • TfidfVectorizer:
    • 문서 집합으로부터 단어의 수를 세고 TF-IDF 방식으로 단어의 가중치를 조정한 BOW 벡터를 만든다.
  • HashingVectorizer:
    • hashing trick 을 사용하여 빠르게 BOW 벡터를 만든다.

DictVectorizer

DictVectorizer는 사전 형태로 되어 있는 feature 정보를 matrix 형태로 변환하기 위한 것으로 feature_extraction 서브 패키지에서 제공한다. 사전 정보는 텍스트 정보에서 corpus 상의 각 단어의 사용 빈도를 나타내는 경우가 많다.

In:
from sklearn.feature_extraction import DictVectorizer
v = DictVectorizer(sparse=False)
D = [{'A': 1, 'B': 2}, {'B': 3, 'C': 1}]
X = v.fit_transform(D)
X
Out:
array([[ 1.,  2.,  0.],
       [ 0.,  3.,  1.]])
In:
v.feature_names_
Out:
['A', 'B', 'C']
In:
v.inverse_transform(X)
Out:
[{'A': 1.0, 'B': 2.0}, {'B': 3.0, 'C': 1.0}]
In:
v.transform({'C': 4, 'D': 3})
Out:
array([[ 0.,  0.,  4.]])

CountVectorizer

CountVectorizer는 다양한 인수를 가진다. 그 중 중요한 것들은 다음과 같다.

  • stop_words : 문자열 {‘english’}, 리스트 또는 None (디폴트)
    • stop words 목록.‘english’이면 영어용 스탑 워드 사용.
  • analyzer : 문자열 {‘word’, ‘char’, ‘char_wb’} 또는 함수
    • 단어 n-그램, 문자 n-그램, 단어 내의 문자 n-그램
  • tokenizer : 함수 또는 None (디폴트)
    • 토큰 생성 함수 .
  • token_pattern : string
    • 토큰 정의용 정규 표현식
  • ngram_range : (min_n, max_n) 튜플
    • n-그램 범위
  • max_df : 정수 또는 [0.0, 1.0] 사이의 실수. 디폴트 1
    • 단어장에 포함되기 위한 최대 빈도
  • min_df : 정수 또는 [0.0, 1.0] 사이의 실수. 디폴트 1
    • 단어장에 포함되기 위한 최소 빈도
  • vocabulary : 사전이나 리스트
    • 단어장
In:
from sklearn.feature_extraction.text import CountVectorizer
corpus = [
    'This is the first document.',
    'This is the second second document.',
    'And the third one.',
    'Is this the first document?',
    'The last document?',    
]
vect = CountVectorizer()
vect.fit(corpus)
vect.vocabulary_
Out:
{'and': 0,
 'document': 1,
 'first': 2,
 'is': 3,
 'last': 4,
 'one': 5,
 'second': 6,
 'the': 7,
 'third': 8,
 'this': 9}
In:
vect.transform(['This is the second document.']).toarray()
Out:
array([[0, 1, 0, 1, 0, 0, 1, 1, 0, 1]])
In:
vect.transform(['Something completely new.']).toarray()
Out:
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
In:
vect.transform(corpus).toarray()
Out:
array([[0, 1, 1, 1, 0, 0, 0, 1, 0, 1],
       [0, 1, 0, 1, 0, 0, 2, 1, 0, 1],
       [1, 0, 0, 0, 0, 1, 0, 1, 1, 0],
       [0, 1, 1, 1, 0, 0, 0, 1, 0, 1],
       [0, 1, 0, 0, 1, 0, 0, 1, 0, 0]])

Stop Words

Stop Words 는 문서에서 단어장을 생성할 때 무시할 수 있는 단어를 말한다. 보통 영어의 관사나 접속사, 한국어의 조사 등이 여기에 해당한다. stop_words 인수로 조절할 수 있다.

In:
vect = CountVectorizer(stop_words=["and", "is", "the", "this"]).fit(corpus)
vect.vocabulary_
Out:
{'document': 0, 'first': 1, 'last': 2, 'one': 3, 'second': 4, 'third': 5}
In:
vect = CountVectorizer(stop_words="english").fit(corpus)
vect.vocabulary_
Out:
{'document': 0, 'second': 1}

토큰

analyzer, tokenizer, token_pattern 등의 인수로 사용할 토큰 생성기를 선택할 수 있다.

In:
vect = CountVectorizer(analyzer="char").fit(corpus)
vect.vocabulary_
Out:
{' ': 0,
 '.': 1,
 '?': 2,
 'a': 3,
 'c': 4,
 'd': 5,
 'e': 6,
 'f': 7,
 'h': 8,
 'i': 9,
 'l': 10,
 'm': 11,
 'n': 12,
 'o': 13,
 'r': 14,
 's': 15,
 't': 16,
 'u': 17}
In:
vect = CountVectorizer(token_pattern="t\w+").fit(corpus)
vect.vocabulary_
Out:
{'the': 0, 'third': 1, 'this': 2}
In:
import nltk
nltk.download("punkt")
vect = CountVectorizer(tokenizer=nltk.word_tokenize).fit(corpus)
vect.vocabulary_
[nltk_data] Downloading package punkt to /home/dockeruser/nltk_data...
[nltk_data]   Package punkt is already up-to-date!
Out:
{'.': 0,
 '?': 1,
 'and': 2,
 'document': 3,
 'first': 4,
 'is': 5,
 'last': 6,
 'one': 7,
 'second': 8,
 'the': 9,
 'third': 10,
 'this': 11}

n-그램

n-그램은 단어장 생성에 사용할 토큰의 크기를 결정한다. 1-그램은 토큰 하나만 단어로 사용하며 2-그램은 두 개의 연결된 토큰을 하나의 단어로 사용한다.

In:
vect = CountVectorizer(ngram_range=(2,2)).fit(corpus)
vect.vocabulary_
Out:
{'and the': 0,
 'first document': 1,
 'is the': 2,
 'is this': 3,
 'last document': 4,
 'second document': 5,
 'second second': 6,
 'the first': 7,
 'the last': 8,
 'the second': 9,
 'the third': 10,
 'third one': 11,
 'this is': 12,
 'this the': 13}
In:
vect = CountVectorizer(ngram_range=(1,2), token_pattern="t\w+").fit(corpus)
vect.vocabulary_
Out:
{'the': 0, 'the third': 1, 'third': 2, 'this': 3, 'this the': 4}

빈도수

max_df, min_df 인수를 사용하여 문서에서 토큰이 나타난 횟수를 기준으로 단어장을 구성할 수도 있다. 토큰의 빈도가 max_df로 지정한 값을 초과 하거나 min_df로 지정한 값보다 작은 경우에는 무시한다. 인수 값은 정수인 경우 횟수, 부동소수점인 경우 비중을 뜻한다.

In:
vect = CountVectorizer(max_df=4, min_df=2).fit(corpus)
vect.vocabulary_, vect.stop_words_
Out:
({'document': 0, 'first': 1, 'is': 2, 'this': 3},
 {'and', 'last', 'one', 'second', 'the', 'third'})
In:
vect.transform(corpus).toarray().sum(axis=0)
Out:
array([4, 2, 3, 3])

TF-IDF

TF-IDF(Term Frequency – Inverse Document Frequency) 인코딩은 단어를 갯수 그대로 카운트하지 않고 모든 문서에 공통적으로 들어있는 단어의 경우 문서 구별 능력이 떨어진다고 보아 가중치를 축소하는 방법이다.

구제적으로는 문서 $d$(document)와 단어 $t$ 에 대해 다음과 같이 계산한다.

$$ \text{tf-idf}(d, t) = \text{tf}(d, t) \cdot \text{idf}(d, t) $$

여기에서

  • $\text{tf}(d, t)$: 단어의 빈도수
  • $\text{idf}(d, t)$ : inverse document frequency

    $$ \text{idf}(d, t) = \log \dfrac{n_d}{1 + \text{df}(t)} $$

  • $n_d$ : 전체 문서의 수

  • $\text{df}(t)$: 단어 $t$를 가진 문서의 수
In:
from sklearn.feature_extraction.text import TfidfVectorizer
In:
tfidv = TfidfVectorizer().fit(corpus)
tfidv.transform(corpus).toarray()
Out:
array([[ 0.        ,  0.38947624,  0.55775063,  0.4629834 ,  0.        ,
         0.        ,  0.        ,  0.32941651,  0.        ,  0.4629834 ],
       [ 0.        ,  0.24151532,  0.        ,  0.28709733,  0.        ,
         0.        ,  0.85737594,  0.20427211,  0.        ,  0.28709733],
       [ 0.55666851,  0.        ,  0.        ,  0.        ,  0.        ,
         0.55666851,  0.        ,  0.26525553,  0.55666851,  0.        ],
       [ 0.        ,  0.38947624,  0.55775063,  0.4629834 ,  0.        ,
         0.        ,  0.        ,  0.32941651,  0.        ,  0.4629834 ],
       [ 0.        ,  0.45333103,  0.        ,  0.        ,  0.80465933,
         0.        ,  0.        ,  0.38342448,  0.        ,  0.        ]])

Hashing Trick

CountVectorizer는 모든 작업을 메모리 상에서 수행하므로 처리할 문서의 크기가 커지면 속도가 느려지거나 실행이 불가능해진다. 이 때 HashingVectorizer를 사용하면 해시 함수를 사용하여 단어에 대한 인덱스 번호를 생성하기 때문에 메모리 및 실행 시간을 줄일 수 있다.

In:
from sklearn.datasets import fetch_20newsgroups
twenty = fetch_20newsgroups()
len(twenty.data)
Out:
11314
In:
%time CountVectorizer().fit(twenty.data).transform(twenty.data)
CPU times: user 6.72 s, sys: 40 ms, total: 6.76 s
Wall time: 6.76 s
Out:
<11314x130107 sparse matrix of type ''
	with 1787565 stored elements in Compressed Sparse Row format>
In:
from sklearn.feature_extraction.text import HashingVectorizer
hv = HashingVectorizer(n_features=10)
In:
%time hv.transform(twenty.data)
CPU times: user 2.64 s, sys: 0 ns, total: 2.64 s
Wall time: 2.65 s
Out:
<11314x10 sparse matrix of type ''
	with 112863 stored elements in Compressed Sparse Row format>

다음은 Scikit-Learn의 문자열 분석기를 사용하여 웹사이트에 특정한 단어가 어느 정도 사용되었는지 빈도수를 알아보는 코드이다.

In:
from urllib.request import urlopen
import json
import string
from konlpy.utils import pprint
from konlpy.tag import Hannanum
hannanum = Hannanum()

f = urlopen("https://www.datascienceschool.net/download-notebook/708e711429a646818b9dcbb581e0c10a/")
json = json.loads(f.read())
cell = ["\n".join(c["source"]) for c in json["cells"] if c["cell_type"] == "markdown"]
docs = [w for w in hannanum.nouns(" ".join(cell)) if ((not w[0].isnumeric()) and (w[0] not in string.punctuation))]

여기에서는 하나의 문서가 하나의 단어로만 이루어져 있다. 따라서 CountVectorizer로 이 문서 집합을 처리하면 각 문서는 하나의 원소만 1이고 나머지 원소는 0인 벡터가 된다. 이 벡터의 합으로 빈도를 알아보았다.

In:
vect = CountVectorizer().fit(docs)
count = vect.transform(docs).toarray().sum(axis=0)
idx = np.argsort(-count)
count = count[idx]
feature_name = np.array(vect.get_feature_names())[idx]
plt.bar(range(len(count)), count)
plt.show()
In:
pprint(list(zip(feature_name, count)))
[('컨테이너', 72),
 ('도커', 40),
 ('이미지', 34),
 ('명령', 32),
 ('사용', 25),
 ('경우', 14),
 ('중지', 14),
 ('가동', 14),
 ('mingw64', 13),
 ('이름', 11),
 ('아이디', 11),
 ('삭제', 10),
 ('다음', 9),
 ('시작', 8),
 ('옵션', 6),
 ('a181562ac4d8', 6),
 ('목록', 6),
 ('입력', 6),
 ('명령어', 5),
 ('확인', 5),
 ('포트', 5),
 ('해당', 5),
 ('호스트', 5),
 ('존재', 4),
 ('출력', 4),
 ('컴퓨터', 4),
 ('프롬프트', 4),
 ('외부', 4),
 ('터미널', 3),
 ('시스템', 3),
 ('문자열', 3),
 ('항목', 3),
 ('마찬가지', 3),
 ('377ad03459bf', 3),
 ('수행', 3),
 ('대화형', 3),
 ('dockeruser', 3),
 ('가상', 3),
 ('호스트간', 2),
 ('가능', 2),
 ('지정', 2),
 ('생각', 2),
 ('상태', 2),
 ('특정', 2),
 ('저장', 2),
 ('추가', 2),
 ('문헌', 2),
 ('작업', 2),
 ('의미', 2),
 ('관련하', 2),
 ('동작', 2),
 ('재시작', 2),
 ('명시해', 2),
 ('자동', 1),
 ('최소한', 1),
 ('초간단', 1),
 ('가지', 1),
 ('중복', 1),
 ('주의해', 1),
 ('개념', 1),
 ('작동', 1),
 ('정지', 1),
 ('자체', 1),
 ('조합', 1),
 ('컨테이너상', 1),
 ('공유', 1),
 ('a1e4ed2ac65b', 1),
 ('핵심', 1),
 ('container', 1),
 ('daemon', 1),
 ('하나', 1),
 ('표시', 1),
 ('폴더', 1),
 ('id', 1),
 ('포워딩', 1),
 ('파일', 1),
 ('툴박스', 1),
 ('image', 1),
 ('태그', 1),
 ('콜론', 1),
 ('tag', 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),
 ('요약', 1),
 ('길벗', 1),
 ('나오기', 1),
 ('오류', 1),
 ('연습', 1),
 ('연결', 1),
 ('여기', 1),
 ('내부', 1),
 ('아래', 1),
 ('실행', 1),
 ('내용', 1),
 ('대표적', 1),
 ('이해', 1),
 ('의존', 1)]

질문/덧글

vocabulary_ moon*** 2016년 10월 17일 10:57 오후

1. vect.vocabulary_ 에서 vocabulary 옆에 _ 가 붙는 이유는 vect가 가지고 있는 속성이름이 vocabulary_ 이기 때문인가요?

2. import nltk
nltk.download("punkt")
vect = CountVectorizer(tokenizer=nltk.word_tokenize).fit(corpus)
vect.vocabulary_
에서 punkt를 다운로드한 이유는 무엇인가요?

3. TF-IDF에서 로그 안에 있는 n_d와 df(t) 둘다 최대값일 경우 idf가 음수가 될 수 있을 것 같은데 가능한가요?

답변: vocabulary_ 관리자 2016년 10월 20일 3:08 오후

네, 음수가 될 수 있습니다. 실제 코드 구현시에는 다른 공식을 쓰거나 smoothing을 하거나 Exception 처리를 합니다.