목차
👀 코드 보기 , 🤷♀️
이 두개의 아이콘을 누르시면 코드, 개념 부가 설명을 보실 수 있습니다:)
LDA란?
[등장 배경]
LDA는 LSA의 약점을 보완하기 위해서 등장한다.
🚫 LSA의 문제
새로운 문헌을 생성해내는 것을 잘 다루지 못함
⭕ 해결
이를 LDA가 (문헌별 주제) 디리클레 분포(Dirichlet)를 적용하여 보완
[LDA의 의미]
토픽 모델링은 문서의 집합에서 토픽을 찾아내는 프로세스를 말한다.
사용) 이는 검색 엔진, 고객 민원 시스템 등과 같이 문서의 주제를 알아내는 일이 중요한 곳에서 사용된다.
LDA는 문서들은 토픽들의 혼합으로 구성되어져 있으며, 토픽들은 확률 분포에 기반하여 단어들을 생성한다고 가정한다.
데이터가 주어지면, LDA는 문서가 생성되던 과정을 역추적한다.
- Latent: Latent Semantic(잠재 의미=>토픽)의 Latent이기도 하고, 또한 LDA 모델에 들어가는 잠재변수들을 가리키기도 한다.
- Dirichlet: 디리클레 분포(문헌별 주제)를 사용했으니깐 D를 써야할텐데 그게 S 자리를 밀어내서 LDA라는 이름이 만들어지게 된 것이다.
- Allocation: 그리고 Analysis의 A 대신 Allocation의 A를 써서, 각각의 단어가 특정 주제에 할당된다는 것을 강조한다.
LDA의 개요
LDA에 대해서 본격적으로 들어가기 전에 LDA가 어떤식으로 작동하는지 대락적으로 살펴보겠다.
아래와 같은 3개의 문서가 있다고 가정하자.(실제로는 문서가 크고 많다)
그리고 우린 LDA를 통해서 아래의 문서들을 통한 주제 찾기(토픽 모델링)을 할 것이다.
📜 문서1 : 저는 사과랑 바나나를 먹어요
📜 문서2 : 우리는 귀여운 강아지가 좋아요
📜 문서3 : 저의 깜찍하고 귀여운 강아지가 바나나를 먹어요
하이퍼 파라미터 설정
이를 위해 사용자가 토픽이 몇개 존재할지(토픽개수 변수 K) 정해줘야한다.
➡️ 2개의 토픽을 찾아달라고 해보자 (k=2)
❌ k의 값을 잘못 선택하면 원치않는 이상한 결과가 나올 수 있다.
LDA가 위의 세 문서로부터 2개의 토픽을 찾은 결과는 아래와 같다.
여기서는 LDA 입력 전에 주어와 불필요한 조사 등을 제거하는 전처리 과정은 거쳤다고 가정하자.
즉, 전처리 과정을 거친 문헌-용어 행렬이 LDA의 입력이 되었다고 가정한다
LDA는 각 문서의 토픽 분포와 각 토픽 내의 단어 분포를 추정한다.
<각 문서의="" 토픽="" 분포=""> 문서1 : 토픽 A 100% 문서2 : 토픽 B 100% 문서3 : 토픽 B 60%, 토픽 A 40% <각 토픽의="" 단어="" 분포=""> 토픽A : **사과 20%, 바나나 40%, 먹어요 40%**, 귀여운 0%, 강아지 0%, 깜찍하고 0%, 좋아요 0% 토픽B : 사과 0%, 바나나 0%, 먹어요 0%, **귀여운 33%, 강아지 33%, 깜찍하고 16%, 좋아요 16%** LDA는 토픽의 제목을 정해주지 않지만, 이 시점에서 알고리즘의 사용자는 위 결과로부터 두 토픽이 각각 과일에 대한 토픽과 강아지에 대한 토픽이라고 판단해볼 수 있다. 이제 LDA에 대해서 알아보자. ----- ## LDA의 가정 LDA는 문서의 집합으로부터 어떤 토픽이 존재하는지를 알아내기 위한 알고리즘이다. LDA는 앞서 배운 빈도수 기반의 표현 방법인 BoW의 행렬 [문헌-용어 행렬](https://yerimoh.github.io/DL13.3/#lsa-%EC%82%AC%EC%9A%A9) 또는 TF-IDF 행렬(통계 기반 모델링)을 입력으로 하는데, 이로부터 알 수 있는 사실은 LDA는 ~~단어의 순서~~는 신경쓰지 않겠다는 것이다. LDA는 문서들로부터 토픽을 뽑아내기 위해 아래와 같은 **가정**을 염두해 둔다. ➡️ 모든 문서 하나, 하나가 작성될 때 그 문서의 작성자는 이러한 생각을 한다. ➡️ '나는 이 문서를 작성하기 위해서 이런 주제들을 넣을거고, 이런 주제들을 위해서는 이런 단어들을 넣을 거야.' 조금 더 구체적으로 예를 들어보자. 각각의 문서는 다음과 같은 과정을 거쳐서 작성되었다고 가정한다. 1️⃣ 문서에 사용할 단어의 개수 N을 정한다 Ex) 5개의 단어를 정했다. 2️⃣ 문서 집합에서 얻은 분포로부터 토픽을 뽑는다. Ex) 위 예제와 같이 토픽이 2개라고 하였을 때 강아지 토픽을 60%, 과일 토픽을 40%와 같이 선택할 수 있다. 3️⃣ 해당 토픽에 해당하는 단어들을 뽑는다. * **1)** 토픽 분포에서 토픽 T를 확률적으로 고른다. Ex) 60% 확률로 강아지 토픽을 선택하고, 40% 확률로 과일 토픽을 선택할 수 있다. * **2)** 선택한 토픽 T에서 **단어의 출현 확률 분포**에 기반해 문서에 사용할 단어를 고른다. Ex) 강아지 토픽을 선택하였다면, 33% 확률로 강아지란 단어를 선택할 수 있다. 4️⃣ 이제 3)을 반복하면서 문서를 완성한다. <각 문서의="" 토픽="" 분포=""> 문서1 : 토픽 A 100% 문서2 : 토픽 B 100% 문서3 : 토픽 B 60%, 토픽 A 40% <각 토픽의="" 단어="" 분포=""> 토픽A : **사과 20%, 바나나 40%, 먹어요 40%**, 귀여운 0%, 강아지 0%, 깜찍하고 0%, 좋아요 0% 토픽B : 사과 0%, 바나나 0%, 먹어요 0%, **귀여운 33%, 강아지 33%, 깜찍하고 16%, 좋아요 16%** 이러한 과정을 통해 문서가 작성되었다는 가정 하에 LDA는 토픽을 뽑아내기 위하여 위 과정을 역으로 추적하는 역공학(reverse engneering)을 수행한다. ----- ---- # **LDA 수행** 이제 LDA의 수행 과정을 정리해보겠다. 1️⃣ **사용자는 알고리즘에게 토픽의 개수 k를 알려준다.** 앞서 말하였듯이 LDA에게 토픽의 개수를 알려주는 역할은 사용자의 역할이다. LDA는 토픽의 개수 k를 입력받으면, k개의 토픽이 M개의 전체 문서에 걸쳐 분포되어 있다고 가정한다. 2️⃣ **모든 단어를 k개 중 하나의 토픽에 할당한다.** 이제 LDA는 모든 문서의 모든 단어에 대해서 k개 중 하나의 토픽을 랜덤으로 할당한다. 이 작업이 끝나면 각 문서는 토픽을 가지며, 토픽은 단어 분포를 가지는 상태이다. 물론 랜덤으로 할당하였기 때문에 사실 이 결과는 전부 틀린 상태이다. 만약 한 단어가 한 문서에서 2회 이상 등장하였다면, 각 단어는 서로 다른 토픽에 할당되었을 수도 있다. 3️⃣ **이제 모든 문서의 모든 단어에 대해서 아래의 사항을 반복 진행한다.** **3-1)** 어떤 문서의 각 단어 w는 자신은 잘못된 토픽에 할당되어져 있지만, 다른 단어들은 전부 올바른 토픽에 할당되어져 있는 상태라고 가정한다. 이에 따라 단어 w는 아래의 두 가지 기준에 따라서 토픽이 재할당된다. * p(topic t | document d) : 문서 d의 단어들 중 토픽 t에 해당하는 단어들의 비율 * p(word w | topic t) : 각 토픽들 t에서 해당 단어 w의 분포 이를 반복하면, 모든 할당이 완료된 수렴 상태가 됩니다. 두 가지 기준이 어떤 의미인지 예를 들어보겠습니다. 설명의 편의를 위해서 두 개의 문서라는 새로운 예를 사용합니다.📜 대각행렬이란?
대각행렬(diagonal matrix)은 주대각선을 제외한 곳의 원소가 모두 0인 행렬을 말합니다. 아래의 그림에서는 주대각선의 원소를 라고 표현하고 있습니다. 만약 대각 행렬 Σ가 3 × 3 행렬이라면, 다음과 같은 모양을 가집니다.
여기까진 정사각 행렬이기 때문에 직관적으로 이해가 쉽습니다. 그런데 정사각 행렬이 아니라 직사각 행렬이 될 경우를 잘 보아야 헷갈리지 않습니다. 만약 행의 크기가 열의 크기보다 크다면 다음과 같은 모양을 가집니다. 즉, m × n 행렬일 때, m > n인 경우입니다.
반면 n > m인 경우에는 다음과 같은 모양을 가집니다.
👀 코드 보기
import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from common.util import preprocess, create_co_matrix, ppmi
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
vocab_size = len(id_to_word)
C = create_co_matrix(corpus, vocab_size, window_size=1)
W = ppmi(C)
# SVD
U, S, V = np.linalg.svd(W)
👀 코드 보기
import sys
sys.path.append('..')
from dataset import ptb
# ptb.load _data ( )는 데이터를 읽어들임
# 인수로 ‘train’, ‘test’,‘valid’ 중 하나 지정 가능
# 차례대로 ‘훈련용’, ‘테스트용’, ‘검증용’ 데이터
corpus, word _to _id, id _to _word = ptb.load _data('train')
print(' 말뭉치 크기 :', len(corpus))
print('corpus[:30]:', corpus[:30])
print()
print('id_to_word[0]:', id_to_word[0])
print('id_to_word[1]:', id_to_word[1])
print('id_to_word[2]:', id_to_word[2])
print()
print("word_to_id['car']:", word_to_id['car'])
print("word_to_id['happy']:", word_to_id['happy'])
print("word_to_id['lexus']:", word_to_id['lexus'])
결과
corpus size: 929589
corpus[:30]: [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29]
id_to_word[0]: aer
id_to_word[1]: banknote
id_to_word[2]: berlitz
word_to_id['car']: 3856
word_to_id['happy']: 4428
word_to_id['lexus']: 7426
👀 코드 보기
import sys
sys.path.append('..')
import numpy as np
from common.util import most_similar, create_co_matrix, ppmi
from dataset import ptb
window _size = 2
wordvec _size = 100
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)
print(' 동시발생 수 계산 ...')
C = create_co_matrix(corpus, vocab _size, window _size)
print('PPMI 계산 ...')
W = ppmi(C, verbose=True)
print('SVD 계산 ...')
#SVD 수행하는 데 sklearn의 randomized _svd () 메서드 이용
try:
# truncated SVD ( 빠르다 !)
from sklearn.utils.extmath import randomized_svd # 🤷♀️sklearn의 randomized_svd ( ) 메서드
U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5, random_state=None)
except ImportError:
# SVD ( 느리다 )
U, S, V = np.linalg.svd(W)
word _vecs = U[:, :wordvec _size]
querys = ['you', 'year', 'car', 'toyota']
for query in querys:
most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
🤷♀️sklearn의 randomized_svd ( ) 메서드
무작위 수를 사용한 Truncated SVD
- 특잇값이 큰 것들만 계산하여 기본적인 SVD보다 훨씬 빠름.
- Truncated SVD는 무작위 수를 사용하므로 결과가 매번 다름.