귀퉁이 서재

NLP - 9. 토픽 모델링: 잠재 의미 분석(LSA) 본문

자연어 처리 (NLP)

NLP - 9. 토픽 모델링: 잠재 의미 분석(LSA)

Baek Kyun Shin 2020. 3. 19. 23:17

토픽 모델링(Topic Modeling)이란 문서 집합에 숨어 있는 '주제'를 찾아내는 텍스트 마이닝기법 중 하나입니다. 사람이 모든 문서를 읽고 그 주제를 파악할 수도 있겠지만, 그러기에는 시간과 노력이 매우 많이 소요됩니다. 이럴 때 토픽 모델링을 통하여 문서에 함축되어 있는 주요 주제를 효과적으로 찾아낼 수 있습니다. 철학에 관한 문서 A와 웨이트 트레이닝에 관한 문서 B가 있다고 해봅시다. A문서에는 '소크라테스', '니체', '실존주의', '형이상학' 등과 같은 단어가 많이 들어가 있고, B문서에는 '스쿼트', '데드리프트', '벤치프레스', '복강 내압', '척추 중립' 등과 같은 단어가 많이 들어가 있을 것입니다. 그리고 두 문서에 모두 '그', '그리고', '그래서', '그러나', '~이다', '~이고, '와 같은 단어가 공통적으로 많이 들어가 있습니다. 이와 같이 A, B문서에서 의미가 없는 단어는 무시하고, 의미가 있는 단어만 추출하여 주요 주제를 찾아내는 것이 바로 토픽 모델링입니다.

토픽 모델링에 자주 사용되는 기법은 LSA(Latent Semantic Analysis)와 LDA(Latent Dirichlet Allocation)입니다. 이번 장에서는 잠재 의미 분석이라 불리는 LSA(Latent Semantic Analysis)에 대해 배워보겠습니다. 기존의 카운트 기반 벡터화나 TF-IDF를 통해 만든 문서-단어 행렬(Document Term Matrix, DTM)은 기본적으로 단어의 빈도수를 이용해 행렬을 만들어줬습니다. 하지만 단어의 빈도 수만으로는 문서의 주제(잠재 의미)를 찾아내기가 힘듭니다. 단어의 의미를 정확히 고려하지 못하기 때문입니다. 이에 대한 대안이 바로 문서의 잠재된 의미를 찾아내는 잠재 의미 분석(LSA)입니다. LSA를 이해하기 위해서는 특이값 분해(SVD)를 우선 이해해야 합니다. 특이값 분해(SVD)에 대해 잘 모르시는 분들은 (머신러닝 - 20. 특이값 분해(SVD))를 참고해주시기 바랍니다. LSA는 SVD를 활용해 문서에 함축된 주제를 찾아내는 것이기 때문에, SVD만 이해해도 LSA는 거의 다 이해한 것입니다.

잠재 의미 분석(Latent Semantic Analysis, LSA)

LSA는 SVD를 활용하여 문서에 숨어있는 의미를 이끌어내기 위한 방법이라고 했습니다. 아래 그림을 통해 설명해보겠습니다.

출처: https://ratsgo.github.io/from%20frequency%20to%20semantics/2017/04/06/pcasvdlsa/

n개의 문서에 포함된 중복을 제거한 단어의 총개수가 m 개라고 합시다 이를 기반으로 문서-단어 행렬(DTM)인 A를 만든다면, m x n의 크기를 갖는 행렬 A가 됩니다. A를 특이값 분해하면 U ∑ Vt 가 됩니다. 이때 ∑는 총 r개의 특이값을 갖습니다. 사용자가 r보다 작은 k를 설정하고, k만큼의 특이값만 남기고 k x k 크기를 갖는 ∑k를 만듭니다. 특이값 분해를 하면 ∑의 대각 원소는 크기 순으로 정렬이 되기 때문에, k개만 남기고 잘라주면 가장 중요한 특이값 k개만 남게 됩니다. 이와 대응되게 U와 Vt행렬에 대해서도 k개만 남기고 m x k크기를 갖는 Uk와 k x n크기를 갖는 Vkt를 만들어 줍니다. k만큼만 남기고 잘라내었다는 것은 중요한 토픽만 남기고 불필요한 토픽은 제거한다는 의미입니다. 

이렇게 만든 Ak에는 중요한 토픽만 남게 됩니다. A 행렬의 크기도 m x n이고, Ak 행렬의 크기도 m x n으로 동일하지만, A 행렬에는 불필요한 토픽이 많이 포함되어 있고, Ak 행렬에는 불필요한 토픽은 제거된 상태, 즉 문서의 잠재 의미(주요 주제, 중요 단어)만 남아 있는 상태입니다.

LSA 실습

사이킷런에서 제공하는 fetch_20 newsgroups라는 데이터 셋을 활용해 LSA 실습을 해보겠습니다. 실습 코드는 딥 러닝을 이용한 자연어 처리 입문을 참고했습니다. fetch_20 newsgroups는 20개의 토픽을 가진 11,314개의 뉴스 기사 데이터 셋을 제공합니다. 아래의 코드로 데이터셋을 받아올 수 있습니다.

import pandas as pd
from sklearn.datasets import fetch_20newsgroups

dataset = fetch_20newsgroups(shuffle=True, random_state=1, remove=('headers', 'footers', 'quotes'))
documents = dataset.data
len(documents)
11314

문서의 수가 총 11,314개임을 알 수 있습니다. target_names라는 명령어를 통해 20개 토픽이 각각 무엇인지 볼 수 있습니다.

dataset.target_names
['alt.atheism',
 'comp.graphics',
 'comp.os.ms-windows.misc',
 'comp.sys.ibm.pc.hardware',
 'comp.sys.mac.hardware',
 'comp.windows.x',
 'misc.forsale',
 'rec.autos',
 'rec.motorcycles',
 'rec.sport.baseball',
 'rec.sport.hockey',
 'sci.crypt',
 'sci.electronics',
 'sci.med',
 'sci.space',
 'soc.religion.christian',
 'talk.politics.guns',
 'talk.politics.mideast',
 'talk.politics.misc',
 'talk.religion.misc']

서로 다른 20개의 토픽을 보여줍니다.

LSA를 하기 전에 데이터 전처리부터 해보겠습니다. 정규 표현식을 이용해 알파벳 이외의 문자는 제거해주고, 길이가 3 이하인 문자도 제거하겠습니다. 그리고 대소문자의 구분을 없애기 위해 모두 소문자로 바꿔보겠습니다.

news_df = pd.DataFrame({'document': documents})

# 알파벳 이외의 문자 제거
news_df['clean_doc'] = news_df['document'].str.replace("[^a-zA-Z#]", " ")

# 길이가 3이하인 문자 제거
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: ' '.join([w for w in x.split() if len(w) > 3]))

# 소문자로 바꾸기
news_df['clean_doc'] = news_df['clean_doc'].apply(lambda x: x.lower())

그다음 문서 데이터에 대해 TF-IDF 벡터화를 해보겠습니다. max_features = 1000은 1,000의 단어까지만 벡터화를 하겠다는 뜻입니다.

from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(stop_words='english', 
max_features= 1000, # 1,000개의 단어만 추출
max_df = 0.5, 
smooth_idf=True)

X = vectorizer.fit_transform(news_df['clean_doc'])

X.shape # DTM의 행렬 크기 반환
(11314, 1000)

총 11,314개의 문서에서 1,000개의 단어만 활용하여 문서-단어 행렬(DTM)을 만들었습니다. 다음으로 Truncated SVD를 활용하여 토픽 모델링을 해보겠습니다. 뉴스 토픽이 총 20개이므로 n_components는 20으로 설정했습니다. 이는 Truncated SVD 분해를 할 때, 상위 20개의 특이값만 사용하겠다는 뜻입니다.

from sklearn.decomposition import TruncatedSVD

# SVD represent documents and terms in vectors 
svd_model = TruncatedSVD(n_components=20, algorithm='randomized', n_iter=100, random_state=122)

svd_model.fit(X)

svd_model.components_.shape
(20, 1000)

이는 Vt 행렬의 크기가 20 x 1000이라는 뜻입니다. singular_values_를 통해 20개의 특이값을 알아볼 수 있습니다.

svd_model.singular_values_
array([17.15952833,  9.93882749,  8.17139855,  7.92032011,  7.62377374,
        7.5257242 ,  7.25096862,  7.00623237,  6.88289372,  6.85602044,
        6.68476301,  6.56045782,  6.52895929,  6.42222944,  6.33939436,
        6.21686249,  6.17477882,  6.09487639,  6.00247117,  5.90654237])

크기 순으로 20개의 특이값이 나열된 것을 볼 수 있습니다. 훈련이 된 vectorizer에 get_feature_names() 명령어를 취해주면 1000개의 단어 피처 값을 받아올 수 있습니다.

terms = vectorizer.get_feature_names()
len(terms)
1000

terms의 길이는 총 단어 피처 수인 1000이 됨을 볼 수 있습니다. 마지막으로 20개의 토픽에 대해 주요 단어를 나열해보겠습니다.

n = 8
components = svd_model.components_
for index, topic in enumerate(components):
    print('Topic %d: '%(index + 1), [terms[i] for i in topic.argsort()[: -n - 1: -1]])
Topic 1:  ['just', 'like', 'know', 'people', 'think', 'does', 'good', 'time']
Topic 2:  ['thanks', 'windows', 'card', 'drive', 'mail', 'file', 'advance', 'files']
Topic 3:  ['game', 'team', 'year', 'games', 'drive', 'season', 'good', 'players']
Topic 4:  ['drive', 'scsi', 'disk', 'hard', 'problem', 'drives', 'just', 'card']
Topic 5:  ['drive', 'know', 'thanks', 'does', 'just', 'scsi', 'drives', 'hard']
Topic 6:  ['just', 'like', 'windows', 'know', 'does', 'window', 'file', 'think']
Topic 7:  ['just', 'like', 'mail', 'bike', 'thanks', 'chip', 'space', 'email']
Topic 8:  ['does', 'know', 'chip', 'like', 'card', 'clipper', 'encryption', 'government']
Topic 9:  ['like', 'card', 'sale', 'video', 'offer', 'jesus', 'good', 'price']
Topic 10:  ['like', 'drive', 'file', 'files', 'sounds', 'program', 'window', 'space']
Topic 11:  ['people', 'like', 'thanks', 'card', 'government', 'windows', 'right', 'think']
Topic 12:  ['think', 'good', 'thanks', 'need', 'chip', 'know', 'really', 'bike']
Topic 13:  ['think', 'does', 'just', 'mail', 'like', 'game', 'file', 'chip']
Topic 14:  ['know', 'good', 'people', 'windows', 'file', 'sale', 'files', 'price']
Topic 15:  ['space', 'know', 'think', 'nasa', 'card', 'year', 'shuttle', 'article']
Topic 16:  ['does', 'israel', 'think', 'right', 'israeli', 'sale', 'jews', 'window']
Topic 17:  ['good', 'space', 'card', 'does', 'thanks', 'year', 'like', 'nasa']
Topic 18:  ['people', 'does', 'window', 'problem', 'space', 'using', 'work', 'server']
Topic 19:  ['right', 'bike', 'time', 'windows', 'space', 'does', 'file', 'thanks']
Topic 20:  ['file', 'problem', 'files', 'need', 'time', 'card', 'game', 'people']

불용어가 일부 포함이 되어 있네요. nltk를 활용해 불용어를 제거한 뒤 토픽 모델링을 하면 결과가 더 좋을 것입니다. Topic 3을 보면 game, team, season, players라는 단어가 있습니다. Topic 3는 스포츠와 관련된 기사일 것입니다. Topic 18을 보면 window, problem, space, work, server라는 단어가 있습니다. 이는 컴퓨터와 관련된 기사일 것입니다. 불용어를 제거하면 더 정확한 결과가 나올 수 있겠네요. 

LSA의 장점과 단점

지금까지 잠재 의미 분석(LSA)에 대해 알아봤습니다. LSA는 쉽고 빠르게 구현이 가능합니다. 하지만 문서에 포함된 단어가 가우시안 분포를 따라야만 LSA를 적용할 수 있습니다. 일반적으로 가우시안 분포를 따르겠지만 모든 문서의 단어가 가우시안 분포를 따르는 것은 아니기 때문에 적용하기가 힘들 때도 있습니다. 또한 문서가 업데이트가 된다면 처음부터 다시 SVD를 적용해줘야 하므로 자원이 많이 소모됩니다.

References

reference1: 위키피디아 (토픽 모델링)

reference2: 다크 프로그래머스 (특이값 분해의 활용)

reference3: 파이썬 머신러닝 완벽 가이드 (권철민 저)

reference4: Text Mining 101: A Stepwise Introduction to Topic Modeling using Latent Semantic Analysis (using Pyhon)

reference5: 딥 러닝을 이용한 자연어 처리 입문 (잠재 의미 분석)

 

Comments