TF-IDF를 통한 변수선택과 t-SNE를 활용한 시각화

March 2, 2018    Text Mining

TF-IDF

순서에 상관없이 Bag-of-word 형태의 Document-Term matrix에 형태에서 중요한 단어(변수)를 선택하는 방식을 TF-IDF(Term Frequency - Inverse Document Frequency)이라고 한다.

\[w_{i,j} = tf_{i,j} \times log(\frac{N}{df_{i}})\]

$tf_{ij}$ = number of occurences of $i$ in $j$

$df_{i}$ = number of documents containing $i$

$N$ = total number of documents

아래의 예시를 살펴보면, 텍스트가 주어질 때 단어 리스트(사전)[A, AT, … ]를 만든 후 해당 단어가 몇번 등장 했는지에 대한 정보를 TF(frequency)로 정의 한다. 만약 어떤 단어가 언급된 문서의 수가 적다면 그 단어는 문서를 분류하는데 있어서 중요한 단어가 될 것이다. 따라서 그 문서 빈도의 역수 IDF(Inverse Document Frequency)를 TF의 곱으로 표현하여 등장횟수도 많고 문서 분별력 있는 단어들을 스코어링한 것이다. TF-IDF 상위 몇개의 단어를 선택할지는 특정 임계값 기준으로 뽑아내면 된다.


장점

선택된 단어는 TF-IDF 스코어를 가지며 어떤 단어가 중요한 단어인지 직관적으로 해석이 가능하며, 전처리(pos-of-tagging)가 잘 수행 되었을때 다른 변수선택/추출보다 견줄만한 성능을 가지고 있다.

단점

제외된 단어들은 학습에 사용되지 않기 때문에 새로운 단어에 대한 해석이 불가능 하며 순서를 고려하지 않기 때문에 어순에 대한 문법적인 의미를 담고 있지 않는다.



코드 실습

import pandas as pd
import matplotlib.font_manager as fm
import matplotlib.pyplot as plt
from future.utils import iteritems
from collections import Counter
from sklearn.manifold import TSNE
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import CountVectorizer


데이터 설명

사용되는 데이터는 하루마다 발생되는 뉴스기사 제목들을 이어 붙힌 그날의 있었던 이벤트 문서라고 정의 하였다. 약 3년간의 기사들을 정보들을 가지고 있으며 명사 위주의 단어들을 뽑아내여 의미있는 정보로 분석하였다. 먼저 데이터 파일~/Downloads에 다운로드 한다.

article_data = pd.read_csv('~/Downloads/selected_Title_date_from15to17.csv', encoding='utf-8', header= None)
documents = [' '.join(i[0].split(' ')[1:]) for i in article_data.values]
# nouns for a day
print(documents[0])
AK 새해 연휴 특가전 AK 을미년 새해맞이 연휴 특가전 진행 AK 을미년 새해맞이 특가전 CEO 설문 새해 경제 성장률 정부 너무 낙관 CEO 신년 사자성어 초심 CEO 풍향계 기대 양띠 CEO MW 사진 광복 새해 MW 사진 따뜻하 태양 을미년 새해 MW 사진 서울 메트로 새해 첫날 시민 책임지 MW 사진 서울 메트로 안전 점검 을미년 아침 맞이하 MW 사진 서울 시민 서울 메트로 을미년 새해 이렇 시작 MW 사진 을미년 새해 MW 사진 을미년 새해 안전 점검 이상 MW 사진 을미년 새해 오늘 분주 철도 기관사 MW 사진 을미년 새해 희망 가득 달려가 MW 사진 지축 차량 사업소 바라보 새해 MW 사진 청양 자리 새해 맞이하 NH 투자 증권 임직원 태백산 정상 해맞이 행사 NH 투자 증권 태백산 새해 각오 다짐 NH 투자 증권 태백산 신년 해맞이 NH 투자 증권 태백산 정상 해맞이 NH 투자 증권 태백산 정상 해맞이 합병 첫걸음 NH 투자 증권 태백산 정상 해맞이 행사 진행 NH 투자 증권 태백산 정상 해맞이 행사 출발 NH 투자 증권 해맞이 ...


단어 인덱싱 및 빈도세기

일반적으로 단어 인덱싱을 하는 부분은 패키지에서 자동적으로 수행되기는 하지만 분석가입장에서 이 부분을 다루는 것은 기본이기 되기 때문에 Counter기반 부터 접근해 보려고 한다.

as_one = ''
for document in documents:
    as_one = as_one + ' ' + document
words = as_one.split()
words[0:10]
['AK', '새해', '연휴', '특가전', 'AK', '을미년', '새해맞이', '연휴', '특가전', '진행']
counts = Counter(words)
counts


  • 단어들의 연속적인 시리즈로 된 리스트를 입력으로 Counter를 사용하면 각 단어들의 빈도를 dictionary로 반환해 줌

    Counter({'시상': 32,
             '퇴원율': 2,
             '죠스': 2,
             '드라이': 40,
             '물위': 1,
             '생건': 104,
             '천신': 10,
             '권길상': 2,
             '팀박스': 1,
             '전면전': 40,
             '코엑스': 102,
             '송혜민': 1,
             '베일': 151,
             '실탄': 61,
             '드라이빙': 13,
             '지준': 1, ...}


  • 단어빈도(counts.get)를 기준으로 내림차순(reverse=True) 정렬
# order by desc
vocab = sorted(counts, key=counts.get, reverse=True)
vocab
['회장',
 '포토',
 '금융',
 '출시',
 '영업익',
 '경제',
 '종합',
 '사장',...]


  • 단어들에 번호를 매겨 그 번호와 그 단어를 dictionary로 저장 e.g. {단어 : index}
word2idx = {word.encode("utf8").decode("utf8"): ii for ii, word in enumerate(vocab,1)}
word2idx
    {'시상': 8589,
     '퇴원율': 31728,
     '죠스': 31729,
     '드라이': 7429,
     '물위': 37809,
     '생건': 3869,
     '천신': 17247,
     '권길상': 31730,
     '팀박스': 37810,
     '전면전': 7430,
     '코엑스': 3917,...}


  • 위와 같은 방식으로 index가 key가 되도록 순서를 바꿈
idx2word = {ii: word for ii, word in enumerate(vocab)}
idx2word
    {0: '회장',
     1: '포토',
     2: '금융',
     3: '출시',
     4: '영업익',
     5: '경제',
     6: '종합',
     7: '사장',...}


Term Frequency

띄어쓰기로 구분되어 있는 단어의들의 집합 documents를 입력으로 하여 CountVectorizer()를 사용하면 쉽게 document-term matrix를 쉽게 구할 수 있다.

V = len(word2idx)
N = len(documents)
tf = CountVectorizer()
tf.fit_transform(documents)
<965x54810 sparse matrix of type '<class 'numpy.int64'>'
	with 712613 stored elements in Compressed Sparse Row format>
tf.fit_transform(documents)[0:1].toarray()
array([[0, 0, 0, ..., 0, 0, 0]])


TF-IDF

TF-IDF 또한 패키지가 존재하며, 같은 방식으로 documnets를 입력으로 하는 TfidfVectorizer를 사용하면 된다.

tfidf = TfidfVectorizer(max_features = 100, max_df=0.95, min_df=0)

#generate tf-idf term-document matrix
A_tfidf_sp = tfidf.fit_transform(documents)  #size D x V
#tf-idf dictionary    
tfidf_dict = tfidf.get_feature_names()
print(tfidf_dict)
['cj', 'lg', 'sk', 'tv', '결정', '경영', '경제', '계란', '공개', '구조', '국감', '국내', '국민', '그룹', '그리스', '금리', '금융', '기업', '뉴스', '대통령', '대표', '롯데', '마감', '매각', '메르스', '면세점', '발표', '부총리', '부회장', '분기', '브렉시트', '사드', '사업', '사장', '사진', '삼성', '삼성물산', '삼성전자', '상승', '새해', '서울', '선물', '세계', '속보', '수출', '시장', '신동빈', '연휴', '영업익', '영향', '오늘', '올해', '우려', '위원장', '위하', '유일호', '은행', '이사', '이재용', '이주열', '인사', '인상', '인수', '작년', '장관', '전년', '전자', '전환', '정부', '정책', '종합', '주총', '중국', '중단', '증가', '증시', '지난해', '지원', '참석', '총재', '최경환', '최대', '추가', '추석', '출시', '코스피', '투자', '트럼프', '특징주', '판매', '포토', '하락', '한국', '한진해운', '합병', '행사', '현대차', '확대', '회의', '회장']
data_array = A_tfidf_sp.toarray()
data = pd.DataFrame(data_array, columns=tfidf_dict)
data.shape

965 rows × 100 columns


TF-IDF score Top 100 단어 시각화

TF-IDF를 사용하여 단어의 중요도를 산출하였고, 선택된 100개의 단어를 t-SNE로 시각화 하였다. t-SNE는 고차원(본 예제에서는 100차원)상에 존재하는 데이터의 유사성들을 KL-divergence가 최소화되도록 저차원(2차원)으로 임베딩시키는 방법이다.

tsne = TSNE(n_components=2, n_iter=10000, verbose=1)
data_array.shape
(965, 100)
data_array.T.shape
(100, 965)


  • 우리는 100차원에 존재하는 965개의 기사들을 2차원에 965개의 기사로 표현하려고 함 (2x965)
Z = tsne.fit_transform(data_array.T)
[t-SNE] Computing 91 nearest neighbors...
[t-SNE] Indexed 100 samples in 0.000s...
[t-SNE] Computed neighbors for 100 samples in 0.013s...
[t-SNE] Computed conditional probabilities for sample 100 / 100
[t-SNE] Mean sigma: 1.035033
[t-SNE] KL divergence after 250 iterations with early exaggeration: 97.958557
[t-SNE] Error after 2750 iterations: 0.493844
print(Z[0:5])
print('Top words: ',len(Z))
[[ 1.701888   -4.630091  ]
 [ 3.2787387  -1.0056741 ]
 [ 1.6976221  -4.1572385 ]
 [ 1.7235768  -2.7884068 ]
 [ 0.98948604 -2.603648  ]]
Top words:  100


  • 본 저자는 우분투에서 matplotlib를 사용하였으며, 한글이 깨지는 것을 해결하기 위해 아래의 폰트 경로를 지정
path = '/usr/share/fonts/truetype/nanum/NanumMyeongjo.ttf'
fontprop = fm.FontProperties(fname=path, size=18)
plt.scatter(Z[:,0], Z[:,1])
for i in range(len(tfidf_dict)):
    plt.annotate(s=tfidf_dict[i].encode("utf8").decode("utf8"), xy=(Z[i,0], Z[i,1]),fontProperties =fontprop)

plt.draw()


단어들의 관계를 벡터 연산

analogies란 단어들 사이에 관계를 추론하는 것으로, 본예제에서 (‘lg’, ‘삼성’) 이라는 단어 벡터가 있을 때 tv와 어떤 단어가 같은 방향벡터를 가지는지 확인해 볼 수 있다. 구체적으로, em을 TF-IDF 100차원으로 임베딩시키는 함수라고 생각해 보면, em['lg'] + em['삼성'] - em['tv'] = 특정 벡터값을 구할 수 있다. 그 벡터값과 유사한 단어를 찾기 위해 모든 단어들과의 유사성(L2-norm, Cosine distance)을 구한 후 가장 유사한 단어를 뽑는 것이 아래의 코드 내용이다.

def find_analogies(w1, w2, w3, emb, word2idx):
    pos_word1 = emb[word2idx[w1]]
    pos_word2 = emb[word2idx[w2]]
    neg_word1 = emb[word2idx[w3]]
    v0 = pos_word1 + pos_word2 - neg_word1

    def dist1(a, b):
        return np.linalg.norm(a - b)
    def dist2(a, b):
        return 1 - a.dot(b) / (np.linalg.norm(a) * np.linalg.norm(b))

    for dist, name in [(dist1, 'Euclidean'), (dist2, 'cosine')]:
        min_dist = float('inf')
        best_word = ''
        for word, idx in iteritems(word2idx):
            if word not in (w1, w2, w3):
                v1 = emb[idx]
                d = dist(v0, v1)
                if d < min_dist:
                    min_dist = d
                    best_word = word
        print("closest match by", name, "distance:", best_word)
        print(w1, "-", w2, "=", w3, "-", best_word)
analogies_to_try = (
    ('lg', '삼성','tv' ),
)

word2idx = {word: ii for ii, word in enumerate(tfidf.get_feature_names())}
word2idx

for word_list in analogies_to_try:
    print(word_list)
    w1, w2, w3 = word_list
    find_analogies(w1=w1, w2=w2, w3=w3, emb=Z, word2idx=word2idx)
('lg', '전자', 'tv')
closest match by Euclidean distance: 전자
lg - 삼성 = tv - 전자
closest match by cosine distance: 전자
lg - 삼성 = tv - 전자

DSBA