Info

  • Author : 천용희 (Yonghee Cheon)
  • Type : 스터디 설명자료
  • Description : LSA에 대한 이론 설명 및 실습 코드


실습 코드

모듈 임포트

import numpy as np
import pandas as pd

from naver_news_general_purpose import *
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_extraction.text import CountVectorizer
import pickle

from sklearn.decomposition import TruncatedSVD

전처리

데이터 준비

  • 네이버 뉴스 데이터로 실험하였습니다.
  • 각 쿼리별 검색결과를 하나로 합쳐 LSA가 토픽을 잘 분리해내는지 판단해보려 합니다.
  • get_query_results: 네이버 뉴스 API를 활용하여 뉴스 링크를 받아온 후 뉴스 바디를 크롤링, 토큰화 후 가져오는 함수로 미리 작성되어 있습니다.
corpus = []
queries = ['코로나', '정치', '경제', '인국공', '아이유', '설빙']
for query in queries:
    corpus += get_query_results(query, tokenize=True)

명사 토큰만 사용

  • get_query_results에서 사용했던 한국어 토크나이저 인스턴스를 가져와서 미리 설정해놓은 include_pos에 존재하는 품사만 사용합니다.
  • 미리 Mecab 기준 NNG, NNP만 선언해놓았기 때문에 명사만 사용합니다.
tokens_matrix = [[t[0] for t in corp['tokens'] if t[1] in tok.include_poses] for corp in corpus]

DTM 생성

token2idx 생성

  • unique token 리스트를 만든 뒤 token2idx, idx2token 딕셔너리를 생성합니다.
  • token2idx를 만들지 않고 unique token 리스트의 인덱스를 그대로 활용해도 되지만 item in list는 O(n)으로 성능이 좋지 않습니다.
  • item2idx로 일반화하여 함수로 사용합니다.
def get_uniques_from_nested_lists(nested_lists):
    uniques = {}
    for one_line in nested_lists:
        for item in one_line:
            if not uniques.get(item):
                uniques[item] = 1
    return list(uniques.keys())


def get_item2idx(items, unique=False):
    item2idx, idx2item = dict(), dict()
    items_unique = items if unique else set(items)
    for idx, item in enumerate(items_unique):
        item2idx[item] = idx
        idx2item[idx] = item
    return item2idx, idx2item

unique_tokens = get_uniques_from_nested_lists(tokens_matrix)
token2idx, idx2token = get_item2idx(unique_tokens)

DTM 생성

  • 여러 방법이 있지만 가장 간단한 구현 방법을 사용합니다.
  • DTM의 가로 길이는 token_length, 다른 말로는 vocab size가 되고 세로 길이는 문서의 갯수가 됩니다.
  • Document 내 Term이 출현하면 1, 아니면 0으로 매핑하는 DTM입니다.
token_length = len(token2idx)
dtm = [[0 for _ in range(token_length)] for _ in range(len(corpus))]
for row_idx, tokens in enumerate(tokens_matrix):
    for token in tokens:
        token_idx = token2idx[token]
        dtm[row_idx][token_idx] = 1

dtm_np = np.array(dtm)

SVD

Numpy를 이용하여 직접 SVD

  • np.linalg.svd를 활용하여 직접 SVD를 실행하고, Truncated를 구현할 수도 있습니다.
U, s, VT = np.linalg.svd(dtm_np, full_matrices=True)
S = np.zeros(dtm_np.shape)
S[:len(s), :len(s)] = np.diag(s)

t = 10
S_t = S[:t, :t]
U_t = U[:, :t]
VT_t = VT[:t, :]

for vt_row in VT_t:
    top_words_idx = sorted(enumerate(vt_row), key=lambda x: x[1], reverse=True)[:20]
    print([(idx2token[i], round(s, 4)) for i, s in top_words_idx])

Package 이용하여 Truncated SVD

  • sklearn의 TfidfVectorizer와 CountVectorizer, 그리고 TruncatedSVD를 사용합니다.
  • topic 수는 10으로 잡았습니다.
tokens_df = pd.DataFrame(tokens_matrix)
tokens_matrix_joined = [' '.join(tokens) for tokens in tokens_matrix]
tfidfvectorizer = TfidfVectorizer(max_features=1000, max_df=0.5, smooth_idf=True)
countvectorizer = CountVectorizer()
X_count = countvectorizer.fit_transform(tokens_matrix_joined)
X_tfidf = tfidfvectorizer.fit_transform(tokens_matrix_joined)

svd_model = TruncatedSVD(n_components=10, algorithm='randomized', n_iter=100, random_state=122)
svd_model.fit(X_count)
len(svd_model.components_)

terms = countvectorizer.get_feature_names()


def get_topics(components, feature_names, n=10):
    for idx, topic in enumerate(components):
        print("Topic %d:" % (idx + 1), [(feature_names[i], topic[i].round(2)) for i in topic.argsort()[:-n - 1:-1]])
  • output (CountVectorizer 이용)
Topic 1: [('정규직', 0.44), ('전환', 0.25), ('비정규직', 0.24), ('의원', 0.21), ('청년', 0.16), ('문제', 0.16), ('보안', 0.15), ('국공', 0.15), ('뉴스', 0.15), ('경제', 0.13)]
Topic 2: [('코로나', 0.48), ('경제', 0.34), ('확진', 0.2), ('지역', 0.13), ('미국', 0.1), ('설빙', 0.1), ('세계', 0.09), ('확산', 0.08), ('한국', 0.08), ('상황', 0.08)]
Topic 3: [('의원', 0.39), ('정치', 0.22), ('민주당', 0.2), ('통합', 0.17), ('생각', 0.16), ('장관', 0.16), ('위원장', 0.15), ('국회', 0.15), ('문제', 0.15), ('북한', 0.12)]
Topic 4: [('설빙', 0.83), ('딸기', 0.17), ('디저트', 0.16), ('메뉴', 0.15), ('빙수', 0.12), ('메론', 0.1), ('출시', 0.08), ('인절미', 0.07), ('제공', 0.07), ('카페', 0.06)]
Topic 5: [('경제', 0.42), ('산업', 0.15), ('기업', 0.14), ('정부', 0.12), ('지원', 0.1), ('디지털', 0.09), ('전망', 0.08), ('글로벌', 0.08), ('사회', 0.08), ('위기', 0.08)]
Topic 6: [('보안', 0.2), ('장관', 0.19), ('북한', 0.18), ('노조', 0.17), ('검색', 0.17), ('공사', 0.16), ('정치', 0.15), ('요원', 0.15), ('공항', 0.13), ('노회찬', 0.12)]
Topic 7: [('노회찬', 0.41), ('정치', 0.35), ('뉴스', 0.16), ('지역', 0.12), ('대표', 0.12), ('트위터', 0.11), ('소통', 0.1), ('사람', 0.09), ('토론', 0.09), ('지방', 0.08)]
Topic 8: [('확진', 0.29), ('의원', 0.2), ('지역', 0.18), ('광주', 0.15), ('국회', 0.14), ('산업', 0.14), ('생각', 0.13), ('보안', 0.12), ('부분', 0.11), ('기업', 0.11)]
Topic 9: [('문제', 0.34), ('확진', 0.22), ('생각', 0.2), ('북한', 0.18), ('비정규직', 0.15), ('정부', 0.15), ('부분', 0.14), ('정책', 0.1), ('이야기', 0.1), ('사회', 0.1)]
Topic 10: [('의원', 0.29), ('보안', 0.17), ('미국', 0.16), ('북한', 0.16), ('위원장', 0.15), ('검색', 0.14), ('코로나', 0.12), ('통합', 0.11), ('대통령', 0.09), ('노조', 0.09)]
  • output (TfidfVectorizer 이용)
Topic 1: [('정규직', 0.42), ('비정규직', 0.23), ('의원', 0.23), ('전환', 0.22), ('청년', 0.18), ('국공', 0.17), ('보안', 0.15), ('일자리', 0.13), ('공정', 0.13), ('문제', 0.12)]
Topic 2: [('설빙', 0.77), ('디저트', 0.18), ('메뉴', 0.17), ('딸기', 0.11), ('확진', 0.1), ('빙수', 0.1), ('인절미', 0.09), ('경제', 0.09), ('출시', 0.09), ('매장', 0.08)]
Topic 3: [('확진', 0.46), ('경제', 0.16), ('환자', 0.16), ('지역', 0.15), ('감염', 0.15), ('미국', 0.13), ('판정', 0.12), ('신규', 0.12), ('발생', 0.12), ('교회', 0.12)]
Topic 4: [('확진', 0.42), ('정규직', 0.18), ('판정', 0.14), ('교회', 0.13), ('감염', 0.11), ('설빙', 0.11), ('검사', 0.09), ('발생', 0.09), ('비정규직', 0.09), ('보안', 0.09)]
Topic 5: [('의원', 0.42), ('민주당', 0.19), ('확진', 0.16), ('통합', 0.15), ('정치', 0.15), ('대표', 0.15), ('국회', 0.13), ('검찰', 0.13), ('위원장', 0.11), ('광주', 0.09)]
Topic 6: [('미국', 0.25), ('의원', 0.23), ('환자', 0.22), ('신규', 0.13), ('경제', 0.12), ('트럼프', 0.11), ('재개', 0.11), ('민주당', 0.1), ('텍사스', 0.09), ('플로리다주', 0.08)]
Topic 7: [('검찰', 0.29), ('회장', 0.29), ('모빌리티', 0.22), ('라임', 0.21), ('수사', 0.2), ('스타', 0.18), ('구속', 0.17), ('장관', 0.17), ('대표', 0.16), ('의혹', 0.14)]
Topic 8: [('국세청', 0.39), ('홍보', 0.33), ('대사', 0.26), ('성실', 0.24), ('아이유', 0.23), ('납세자', 0.18), ('이서진', 0.17), ('납세', 0.16), ('대통령', 0.15), ('환자', 0.14)]
Topic 9: [('국세청', 0.23), ('홍보', 0.19), ('경제', 0.17), ('대사', 0.16), ('의원', 0.15), ('아이유', 0.14), ('전망', 0.14), ('성실', 0.14), ('회장', 0.13), ('납세자', 0.11)]
Topic 10: [('환자', 0.27), ('회장', 0.21), ('거버넌스', 0.19), ('의원', 0.18), ('미국', 0.18), ('지방', 0.17), ('신규', 0.15), ('모빌리티', 0.15), ('라임', 0.14), ('자치', 0.13)]