> For the complete documentation index, see [llms.txt](https://soojle.gitbook.io/project/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://soojle.gitbook.io/project/requirements/undefined-1/undefined-3/lsa.md).

# LSA - 잠재 의미 분석

## 개요

BoW에 기반한 **DTM**이나 **TF-IDF**는 기본적으로 단어의 빈도 수를 이용한 수치화 방법이기 때문에 단어의 의미를 고려하지 못한다는 단점이 있다. 이를 위한 대안으로 DTM의 잠재된(Latent) 의미를 이끌어내는 방법으로 잠재 의미 분석(Latent Semantic Analysis, LSA)이라는 방법이 존재한다.

## **1. 특이 값 분해(Singular Value Decomposition, SVD)**

SVD란 A가 m × n 행렬일 때, 다음과 같이 3개의 행렬의 곱으로 분해(decomposition)하는 것을 말한다.

![](/files/-Ln81EDVYEU_Fkqxz6U-)

여기서 각 3개의 행렬은 다음과 같은 조건을 만족한다.

![](/files/-Ln81QaVvB6nSo2IwUsO)

**U = m×m의 직교행렬**

![](/files/-Ln81fII2ZL_4EmsrP3d)

**V** = **n×n 직교행렬**\
**Σ** = **m×n 직사각 대각행렬**

직교행렬(orthogonal matrix)이란 자신과 자신의 전치 행렬(transposed matrix)의 곱 또는 이를 반대로 곱한 결과가 단위행렬(identity matrix)이 되는 행렬을 말한다.&#x20;

&#x20;SVD를 통해 나온 대각 행렬 Σ는 기존의 대각 행렬과 별개로 추가적인 성질을 가지게 된다. 대각 행렬 Σ의 주대각원소를 행렬 A의 특이값(singular value)라고 하며, 이를![](https://wikidocs.net/images/page/24949/%ED%8A%B9%EC%9D%B4%EA%B0%921.png)라고 표현한다고 하였을 때 특이값 ![](https://wikidocs.net/images/page/24949/%ED%8A%B9%EC%9D%B4%EA%B0%921.png)은 내림차순으로 정렬되어 있다는 특징을 가진다.

## **2. TSVD(Truncated SVD)**

LSA의 경우 위에서 생성된 Full SVD에서 나온 3개의 행렬에서 일부 벡터들을 삭제시켜 절단된 SVD(truncated SVD)를 사용한다.

![](/files/-Ln82bQQCOrx6gnIPVYU)

절단된 SVD는 대각 행렬 Σ의 대각 원소의 값 중에서 상위 값 t개가 남게 된다. 절단된 SVD를 수행하면 값의 손실이 일어나므로 기존의 행렬 A를 복구할 수 없으며  U행렬과 V행렬의 t열까지만 남는다. \
여기서 t는 토픽의 수를 반영한 하이퍼 파라미터 값으로 사용자가 직접 값을 선택하며 성능에 영향을 주는 매개변수를 뜻한다.

### 예제 실행 코드

아래와 같은 DTM 시나리오를 통해 코드 상에서 TSVD를 구현해보면 다음과 같다.

![](/files/-Ln84k7C1P00hnwrSgCN)

```python
import numpy as np
A=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]])
```

&#x20;해당 행렬의 Shape은 다음과 같다.

```python
>>> np.shape(A)
(4, 9)
```

생성된 행렬에 대하여 DTM 및 SVD를 생성한다.

```python
>>> U, s, VT = np.linalg.svd(A, full_matrices = True)

>>> np.shape(U)
[[-0.24  0.75  0.   -0.62]
 [-0.51  0.44 -0.    0.74]
 [-0.83 -0.49 -0.   -0.27]
 [-0.   -0.    1.    0.  ]]
(4, 4)

>>> np.shape(s)
[2.69 2.05 1.73 0.77]
(4,)

>>> S = np.zeros((4, 9)) # 대각 행렬의 크기인 4 x 9의 임의의 행렬 생성
>>> S[:4, :4] = np.diag(s) # 특이값을 대각행렬에 삽입
>>> np.shape(S)
[[2.69 0.   0.   0.   0.   0.   0.   0.   0.  ]
 [0.   2.05 0.   0.   0.   0.   0.   0.   0.  ]
 [0.   0.   1.73 0.   0.   0.   0.   0.   0.  ]
 [0.   0.   0.   0.77 0.   0.   0.   0.   0.  ]]
(4, 9)

>>> np.shape(VT)
[[-0.   -0.31 -0.31 -0.28 -0.8  -0.09 -0.28 -0.   -0.  ]
 [ 0.   -0.24 -0.24  0.58 -0.26  0.37  0.58 -0.   -0.  ]
 [ 0.58 -0.    0.    0.   -0.    0.   -0.    0.58  0.58]
 [ 0.   -0.35 -0.35  0.16  0.25 -0.8   0.16 -0.   -0.  ]
 [-0.   -0.78 -0.01 -0.2   0.4   0.4  -0.2   0.    0.  ]
 [-0.29  0.31 -0.78 -0.24  0.23  0.23  0.01  0.14  0.14]
 [-0.29 -0.1   0.26 -0.59 -0.08 -0.08  0.66  0.14  0.14]
 [-0.5  -0.06  0.15  0.24 -0.05 -0.05 -0.19  0.75 -0.25]
 [-0.5  -0.06  0.15  0.24 -0.05 -0.05 -0.19 -0.25  0.75]]
(9, 9)
```

이제 t를 정하고, TSVD(Truncated SVD)를 수행해야 한다. 하이퍼 파라미터의 값은 t=2로 가정하고 각 성분에 대하여 절단 작업을 수행한다.

```python
S=S[:2,:2]
U=U[:,:2]
VT=VT[:2,:]
A_prime=np.dot(np.dot(U,S), VT)
```

Full SVD(A)와 완성된 TSVD(A\_prime)의 결과를 비교해보자.

```python
# A
[[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]]
 
 # A_prime
[[ 0.   -0.17 -0.17  1.08  0.12  0.62  1.08 -0.   -0.  ]
 [ 0.    0.2   0.2   0.91  0.86  0.45  0.91  0.    0.  ]
 [ 0.    0.93  0.93  0.03  2.05 -0.17  0.03  0.    0.  ]
 [ 0.    0.    0.    0.    0.    0.    0.    0.    0.  ]]
```

대체적으로 기존에 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에서 제공하는 테스트 데이터를 사용하여 주제 분류 모델을 학습하는 코드이다.

```python
# LSA : DTM을 차원 축소 하여 축소 차원에서 근접 단어들을 토픽으로 묶는다.
import pandas as pd
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.corpus import stopwords
from sklearn.decomposition import TruncatedSVD
import numpy as np
dataset = 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(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())
# NLTK로부터 불용어 사전 로드
stop_words = stopwords.words('english')
# 토큰화 및 불용어 제거
tokenized_doc = news_df['clean_doc'].apply(lambda x: x.split())
tokenized_doc = tokenized_doc.apply(lambda x: [item for item in x if item not in stop_words])
#### 역토큰화
detokenized_doc = []
for i in range(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()
def get_topics(components, feature_names, n = 5):
	for idx, topic in enumerate(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)
```

아래는 생성된 모델에 기반형 분류된 토픽 및 해당 토픽에 기여한 중요 빈도 단어를 출력시켜 보았다.

```python
Topic 1: [('like', 0.2138), ('know', 0.20031), ('people', 0.19334), ('think', 0.17802), ('good', 0.15105)]
Topic 2: [('thanks', 0.32918), ('windows', 0.29093), ('card', 0.18016), ('drive', 0.1739), ('mail', 0.15131)]
Topic 3: [('game', 0.37159), ('team', 0.32533), ('year', 0.28205), ('games', 0.25416), ('season', 0.18464)]
Topic 4: [('drive', 0.52823), ('scsi', 0.20043), ('disk', 0.15518), ('hard', 0.15511), ('card', 0.14049)]
Topic 5: [('windows', 0.40544), ('file', 0.25619), ('window', 0.1806), ('files', 0.16196), ('program', 0.14009)]
Topic 6: [('government', 0.16085), ('chip', 0.16071), ('mail', 0.15626), ('space', 0.15047), ('information', 0.13582)]
Topic 7: [('like', 0.67121), ('bike', 0.14274), ('know', 0.11189), ('chip', 0.11043), ('sounds', 0.10389)]
.....
생략
```

## LSA 모델 분석

LSA는 단어의 잠재적인 의미를 이끌어낼 수 있어 문서의 유사도 계산 등에서 좋은 성능을 보여준다는 장점을 갖고 있다.\
그러나 SVD의 특성상 이미 계산된 LSA에 새로운 데이터가 추가될 경우, 보편적으로 처음부터 다시 계산해야 한다. \
즉, 새로운 정보에 대해 업데이트가 거의 불가능하다. 접근 방법 자체는 본 프로젝트와 유사하지만 해당 결점으로 인해 모델로 선정하기에는 부족한다고  판단된다.
