BoW에 기반한 DTM이나 TF-IDF는 기본적으로 단어의 빈도 수를 이용한 수치화 방법이기 때문에 단어의 의미를 고려하지 못한다는 단점이 있다. 이를 위한 대안으로 DTM의 잠재된(Latent) 의미를 이끌어내는 방법으로 잠재 의미 분석(Latent Semantic Analysis, LSA)이라는 방법이 존재한다.
1. 특이 값 분해(Singular Value Decomposition, SVD)
SVD란 A가 m × n 행렬일 때, 다음과 같이 3개의 행렬의 곱으로 분해(decomposition)하는 것을 말한다.
여기서 각 3개의 행렬은 다음과 같은 조건을 만족한다.
U = m×m의 직교행렬
V = n×n 직교행렬Σ = m×n 직사각 대각행렬
직교행렬(orthogonal matrix)이란 자신과 자신의 전치 행렬(transposed matrix)의 곱 또는 이를 반대로 곱한 결과가 단위행렬(identity matrix)이 되는 행렬을 말한다.
2. TSVD(Truncated SVD)
LSA의 경우 위에서 생성된 Full SVD에서 나온 3개의 행렬에서 일부 벡터들을 삭제시켜 절단된 SVD(truncated SVD)를 사용한다.
절단된 SVD는 대각 행렬 Σ의 대각 원소의 값 중에서 상위 값 t개가 남게 된다. 절단된 SVD를 수행하면 값의 손실이 일어나므로 기존의 행렬 A를 복구할 수 없으며 U행렬과 V행렬의 t열까지만 남는다.
여기서 t는 토픽의 수를 반영한 하이퍼 파라미터 값으로 사용자가 직접 값을 선택하며 성능에 영향을 주는 매개변수를 뜻한다.
예제 실행 코드
아래와 같은 DTM 시나리오를 통해 코드 상에서 TSVD를 구현해보면 다음과 같다.
import numpy as npA=np.array([[0,0,0,1,0,1,1,0,0],[0,0,0,1,1,0,1,0,0],[0,1,1,0,2,0,0,0,0],[1,0,0,0,0,0,0,1,1]])
대체적으로 기존에 0인 값들은 0에, 1인 값들은 1에 가까운 값이 나오는 것을 확인할 수 있다.
축소된 U는 4 × 2의 크기를 가지며, 이는 문서의 개수 × 토픽의 수 t의 크기이다. 즉, U의 각 행은 잠재 의미를 표현하기 위한 수치화 된 각각의 문서 벡터이며, 2 × 9의 크기를 가지는 축소된 VT는 토픽의 수 t × 단어의 개수의 크기를 뜻한다.
3. 잠재 의미 분석(Latent Semantic Analysis, LSA)
기존의 DTM이나 DTM에 단어의 중요도에 따른 가중치를 주었던 TF-IDF 행렬은 단어의 의미를 전혀 고려하지 못한다는 단점을 갖고 있다. 그리하여 LSA는 기본적으로 DTM이나 TF-IDF 행렬에TSVD(truncated SVD)를 적용 시 차원을 축소시키고, 단어들의 잠재적인 의미를 끌어내는 방식이다.
아래의 코드는 LSA를 기반으로 Sklearn에서 제공하는 테스트 데이터를 사용하여 주제 분류 모델을 학습하는 코드이다.
# LSA : DTM을 차원 축소 하여 축소 차원에서 근접 단어들을 토픽으로 묶는다.import pandas as pdfrom sklearn.datasets import fetch_20newsgroupsfrom sklearn.feature_extraction.text import TfidfVectorizerfrom nltk.corpus import stopwordsfrom sklearn.decomposition import TruncatedSVDimport numpy as npdataset =fetch_20newsgroups(shuffle=True, random_state=1, remove=('headers', 'footers', 'quotes'))#####list, 테스트용 뉴스데이터documents = dataset.data# 실제 토픽(답안)#print(len(dataset.target_names), dataset.target_names)#### 전처리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(lambdax: ' '.join([w for w in x.split() iflen(w)>3]))# 전체 단어에 대한 소문자 변환news_df['clean_doc']= news_df['clean_doc'].apply(lambdax: x.lower())# NLTK로부터 불용어 사전 로드stop_words = stopwords.words('english')# 토큰화 및 불용어 제거tokenized_doc = news_df['clean_doc'].apply(lambdax: x.split())tokenized_doc = tokenized_doc.apply(lambdax: [item for item in x if item notin stop_words])#### 역토큰화detokenized_doc = []for i inrange(len(news_df)): t =" ".join(tokenized_doc[i]) detokenized_doc.append(t)news_df['clean_doc']= detokenized_doc#### TF-IDF 행렬 만들기vectorizer =TfidfVectorizer(stop_words ="english",#max_features = 10000, # 최대 단어 제한 max_df =0.5, smooth_idf =True)X = vectorizer.fit_transform(news_df['clean_doc'])#### 토픽모델링svd_model =TruncatedSVD(n_components =20, algorithm ='randomized', n_iter =100, random_state =122)svd_model.fit(X)#(토픽의 수, 해당 토픽과 관련된 단어)print(np.shape(svd_model.components_))#결과출력terms = vectorizer.get_feature_names()defget_topics(components,feature_names,n=5):for idx, topic inenumerate(components):print("Topic %d:"% (idx+1), [(feature_names[i], topic[i]) for i in topic.argsort()[:-n -1:-1]])get_topics(svd_model.components_, terms)
아래는 생성된 모델에 기반형 분류된 토픽 및 해당 토픽에 기여한 중요 빈도 단어를 출력시켜 보았다.
LSA는 단어의 잠재적인 의미를 이끌어낼 수 있어 문서의 유사도 계산 등에서 좋은 성능을 보여준다는 장점을 갖고 있다.
그러나 SVD의 특성상 이미 계산된 LSA에 새로운 데이터가 추가될 경우, 보편적으로 처음부터 다시 계산해야 한다.
즉, 새로운 정보에 대해 업데이트가 거의 불가능하다. 접근 방법 자체는 본 프로젝트와 유사하지만 해당 결점으로 인해 모델로 선정하기에는 부족한다고 판단된다.
SVD를 통해 나온 대각 행렬 Σ는 기존의 대각 행렬과 별개로 추가적인 성질을 가지게 된다. 대각 행렬 Σ의 주대각원소를 행렬 A의 특이값(singular value)라고 하며, 이를라고 표현한다고 하였을 때 특이값 은 내림차순으로 정렬되어 있다는 특징을 가진다.