LDA (Latent Dirichlet Allocation)
LDA (Latent Dirichlet Allocation: 잠재 디리클레 할당)
1. LDA의 정의 및 특징
LDA는 토픽 모델링의 대표적인 알고리즘으로, 주어진 문서들에 대해 각 문서에 어떤 주제들이 존재하는지에 대한 확률 모델입니다.
핵심은 다음 두 가지 분포를 모두 추정하는 것입니다.
- 토픽별 단어의 분포
- 문서별 토픽의 분포
2. LDA의 가정
LDA는 문서를 구성하는 방식에 대해 다음과 같이 가정합니다.
- 문서 = 여러 토픽이 섞인 것
- 토픽 = 여러 단어가 섞인 것
3. 학습 과정
LDA의 학습은 문서 집합에서 토픽-단어 분포와 문서-토픽 분포를 역으로 추정하는 과정입니다. 즉, 이미 존재하는 문서와 그 안의 단어들을 보고 그 이면에 있는 토픽의 구조를 파악해 냅니다.
4. 토픽 모델링
문서의 집합에서 토픽을 찾아내는 프로세스를 토픽 모델링이라고 하며, LDA는 이 토픽 모델링을 수행하는 주된 방법 중 하나입니다.
θ_d (문서--토픽 분포)
문서 d 안에 어떤 토픽들이 얼마나 섞여 있는지를 나타내는 비율
예: 문서 A는 정치 70퍼센트, 경제 20퍼센트, 스포츠 10퍼센트 이런 식
ϕ_k (토픽--단어 분포)
토픽 k 안에서 어떤 단어들이 얼마나 자주 등장하는지를 나타내는 확률
예: ‘스포츠’ 토픽에서는 “선수”, “경기”, “팀” 같은 단어가 높은 확률
z_dn (단어의 토픽 라벨)
문서 d의 n번째 단어가 어떤 토픽에서 나왔다고 판단되는지
예: 문서 내 한 단어가 ‘정치’, 다른 단어는 ‘경제’ 등으로 라벨링됨
LDA 학습 방식의 핵심 흐름
z_dn을 알고 있다면 θ_d와 ϕ_k를 쉽게 계산할 수 있음
하지만 실제로는 z_dn(각 단어의 토픽 라벨)을 모르기 때문에 이를 샘플링하거나 최적화 방식으로 추정함
z_dn을 하나씩 추정할 때마다 θ_d와 ϕ_k를 다시 계산하고 갱신
이 과정을 문서 전체에 반복하면서 모델이 점점 안정된 토픽 구조를 찾아감
정리하면
LDA는 문서에 숨겨진 토픽 구조를 찾아내기 위해
단어의 토픽 라벨 z를 계속 추정하고
그에 따라 문서--토픽 분포 θ와 토픽--단어 분포 ϕ를 반복적으로 업데이트하는 과정이라 보면 됨
LDA는 확률적 생성 모델
1. 확률적 생성 모델이란?
- LDA는 **데이터(문서와 단어)**가 어떤 확률적인 원리에 의해 만들어졌다고 가정하는 모델입니다.
- 이 가정한 원리(생성 과정)를 **수학적(확률 분포)**으로 표현하고 적어 놓은 것입니다.
- 우리가 관찰한 결과(문서)를 가장 잘 설명하는 숨겨진 확률 분포를 역으로 찾아내는 것이 목표입니다.
2. 생성 과정: 토픽-단어 분포 생성 단계
LDA가 문서를 생성한다고 가정할 때, 가장 먼저 일어나는 일은 '토픽'과 '단어' 간의 관계를 정의하는 것입니다.
- 1단계: 토픽 개수 K 결정
- 문서 집합에서 몇 개의 토픽이 존재할지 미리 결정합니다.
- 예시에서는 K = 3으로, '배송', '가격', '품질'이라는 세 가지 토픽을 가정했습니다.
- 2단계: 토픽별 단어 분포 추정 (생성)
- 결정된 각 토픽 K에 대해, 해당 토픽이 어떤 단어를 얼마나 높은 확률로 포함할지(좋아할지)를 결정합니다.
- 예를 들어, 토픽 '배송'은 '빠르다', '도착' 같은 단어에 높은 확률을 부여합니다.
- 결과: 토픽-단어 테이블 가정
- 이 단계를 거치면 다음과 같은 토픽-단어 테이블이 만들어졌다고 가정합니다.
- 토픽 1: "배송/빠르다/어제/도착" (이 단어들이 토픽 1에서 나올 확률이 높음)
- 토픽 2: "가격/저렴/비싸다/할인"
- 토픽 3: "품질/재질/튼튼하다/마감"
- 이 단계를 거치면 다음과 같은 토픽-단어 테이블이 만들어졌다고 가정합니다.
문서 생성 단계란?
LDA가 가정하는 “문서가 만들어지는 방식”이며, 실제 학습 과정도 이 가정을 거꾸로 추적하는 형태로 이루어짐.
1단계: 문서 전체의 토픽 비율(θ_d)을 먼저 정함
문서 하나가 어떤 토픽들로 구성되어 있는지 비율을 지정
예: θ_d = [0.7, 0.2, 0.1]
배송 관련 내용이 70퍼센트
가격 관련 내용이 20퍼센트
품질 관련 내용이 10퍼센트
이 문서는 이런 토픽 조합으로 구성된다고 가정하는 것
2단계: 문서 안의 각 단어가 어떤 토픽에서 왔는지를 결정(z_dn 선택)
문서 D1이 단어 5개를 가진 상황을 예로 들면
각 단어 위치 n마다 θ_d를 보고 토픽을 하나 뽑는다
단어 1번 위치
θ_d가 [0.9, 0.1]이라면
90퍼센트 확률로 토픽1
10퍼센트 확률로 토픽2
→ 예: z_11 = 토픽1
단어 2번 위치
다시 θ_d를 참조해서 토픽을 하나 뽑음
→ 예: z_12 = 토픽1
단어 3번 위치
→ 예: z_13 = 토픽1
단어 4번, 5번도 같은 방식으로 토픽을 무작위 선택
3단계: 선택된 토픽(z_dn)에서 실제 단어(w_dn)를 뽑는 과정
앞 단계에서 각 단어 위치마다 “이 단어는 어떤 토픽에서 왔다”라는 토픽 라벨 z_dn이 정해졌다면
이제 그 토픽의 단어 분포(ϕ_k)에서 실제 단어를 하나 샘플링한다.
예시
z_11이 토픽1이라면
토픽1의 단어 분포에서 단어를 하나 뽑음
경기, 선수, 득점, 팀 같은 단어가 등장할 확률이 높음
z_14가 토픽2라면
토픽2의 단어 분포에서 단어를 하나 뽑음
주식, 주가, 시장, 상승 같은 단어가 나올 가능성이 큼
이 과정을 문서의 모든 단어 위치에 반복하면
문서 D1은
“경기 선수 득점 주가 팀”
처럼 각 토픽에서 나온 단어들이 조합된 하나의 문서가 만들어짐
정리
1단계: 문서 전체의 토픽 비율 생성
2단계: 각 단어가 어떤 토픽에서 왔는지 선택
3단계: 선택된 토픽의 단어 분포에서 실제 단어를 샘플링
이 3단계를 통해 문서가 생성된다고 LDA는 가정하며
학습 시에는 이 과정을 거꾸로 추정해서 토픽을 찾아냄.
다시 정리
LDA가 가정하는 문서 생성 과정
문서가 만들어지는 과정을 ‘확률적 생성 모델’로 상상한 뒤
그 과정을 역추적하는 방식으로 토픽을 추정함.
1단계
문서마다 토픽 비율 θ_d를 하나 샘플링
예: [0.7, 0.2, 0.1]
2단계
문서 안의 각 단어 위치 n에 대해
θ_d를 보고 해당 단어의 토픽 z_dn을 선택
예: 대부분이 토픽1로 선택될 수 있음
3단계
선택된 토픽 z_dn의 단어 분포 ϕ_k에서
실제 단어 w_dn을 하나 샘플링
예: 토픽1이라면 “경기, 선수, 득점” 같은 단어가 나올 확률이 높음
이 과정을 단어 수만큼 반복하면 문서 하나가 생성된다고 가정함.
📘 학습(Training)은 이 과정을 ‘거꾸로’ 하는 것
실제 문서에는 단어들만 있음
토픽 비율(θ), 단어의 토픽 라벨(z), 토픽 단어 분포(ϕ)는 모두 숨겨져 있음
그래서 LDA는 다음 과정을 반복함
단어의 토픽 라벨 z 추정
→ 그에 맞춰 θ와 ϕ 갱신
→ 다시 z 추정
→ 반복하면서 토픽 구조가 점점 안정됨
즉,
단어들이 어떤 토픽에서 왔는지(z)
각 문서의 토픽 비율이 무엇인지(θ)
각 토픽이 어떤 단어로 구성되는지(ϕ)
이 세 가지를 함께 찾아가는 과정이 LDA 학습의 본질.
📘 LDA를 통해 얻을 수 있는 결과
문서별 토픽 비율
예: 문서 A는 “정치 60퍼센트, 경제 40퍼센트”
토픽별 대표 단어
토픽1: 경기, 선수, 득점
토픽2: 주식, 시장, 상승
문서의 주제 구조를 명확히 파악할 수 있어
리뷰 분석, 뉴스 클러스터링, 추천 시스템 등에서 널리 사용됨.
lda = LatentDirichletAllocation(
n_components=5,
doc_topic_prior=None, # None이면 1 / n_components 로 자동 설정
topic_word_prior=None, # None이면 1 / n_components 로 자동 설정
learning_method="batch“, # 또는 "online“
)



import numpy as np # 배열 및 수학 연산을 위한 라이브러리 (결과 출력에 사용)
from sklearn.feature_extraction.text import CountVectorizer # 텍스트를 단어 빈도 벡터로 변환하는 클래스
from sklearn.decomposition import LatentDirichletAllocation # LDA 모델을 구현한 클래스
# 분석할 문서(doc) 리스트. LDA의 입력 데이터가 됩니다.
docs = [
"배송이 빠르고 포장이 깔끔해서 좋았어요",
"배송이 느리고 박스가 찢어져 와서 별로였어요",
# .... (여기에 더 많은 문서가 있다고 가정합니다.)
]
# --- 1. 단어 카운트 벡터화 (피처 추출) ---
# CountVectorizer 객체 생성. 텍스트를 단어 빈도 기반의 행렬로 변환합니다.
vectorizer = CountVectorizer()
# fit_transform을 통해 다음 두 가지 작업을 동시에 수행합니다.
# 1. fit: docs를 분석하여 단어장(Vocabulary)을 구축합니다. (어떤 단어가 있는지 파악)
# 2. transform: docs를 단어장 기반의 빈도 행렬(문서-단어 행렬)로 변환합니다.
# X는 희소 행렬(Sparse Matrix) 형태로 저장됩니다.
X = vectorizer.fit_transform(docs)
# 문서-단어 행렬의 크기를 출력합니다.
# (문서 수, 단어 수). LDA 모델의 입력 차원입니다.
print("문서-단어 행렬 크기:", X.shape)
# --- 2. LDA 모델 설정 및 학습 ---
n_topics = 3 # 추출하고자 하는 토픽의 개수 K를 설정합니다.
# LatentDirichletAllocation 객체 생성
lda = LatentDirichletAllocation(
n_components=n_topics, # 추출할 토픽 개수 K를 지정합니다.
learning_method="batch", # 학습 방법 지정: 'batch'는 전체 데이터를 한 번에 보고 학습합니다.
random_state=42 # 재현성을 위한 난수 시드 설정
)
# 학습 (fit) 및 변환 (transform)을 동시에 수행합니다.
# fit: X를 이용해 토픽-단어 분포 (phi: lda.components_)를 추정합니다.
# transform: 학습된 phi를 이용해 X를 문서-토픽 분포 (theta: doc_topic)로 변환합니다.
# doc_topic은 각 문서별 토픽 비중이 저장된 행렬입니다.
doc_topic = lda.fit_transform(X) # shape: (n_docs, n_topics)
# (참고용 주석)
# print(doc_topic.sum(axis=1)) # 각 행(문서)의 합은 1에 가까워야 합니다.
# print(lda.components_) # 토픽-단어 분포 행렬 (phi)
# --- 3. 결과 분석 및 출력 ---
# 토픽별 상위 단어를 출력하는 함수를 정의합니다.
# model.components_는 토픽-단어 분포 (phi)를 나타냅니다.
# 이는 토픽 k에서 단어 w가 나타날 확률을 나타냅니다.
def print_topics(model, feature_names, n_top_words=5):
# model.components_는 (n_topics, n_features) 크기의 행렬입니다.
# 각 행은 하나의 토픽을, 각 열은 단어(feature)를 나타냅니다.
for topic_idx, topic in enumerate(model.components_):
# topic: 각 단어별 "카운트 비슷한 값" (클수록 그 토픽에서 더 중요한 단어)
# 1. topic.argsort(): 토픽에 대한 단어들의 중요도를 오름차순으로 정렬한 인덱스 배열을 반환합니다.
# 2. [::-1]: 인덱스 배열을 내림차순으로 뒤집습니다. (가장 중요한 단어가 앞으로 옴)
# 3. [:n_top_words]: 앞에서부터 n_top_words 개만 선택합니다.
top_indices = topic.argsort()[::-1][:n_top_words]
# 선택된 인덱스를 이용해 실제 단어(feature_names)를 추출합니다.
top_words = [feature_names[i] for i in top_indices]
# 토픽 번호와 상위 단어를 출력합니다.
print(f"Topic {topic_idx}: {', '.join(top_words)}")
# CountVectorizer가 학습한 단어장(feature) 이름을 가져옵니다.
feature_names = vectorizer.get_feature_names_out()
# 정의한 함수를 호출하여 토픽별 상위 5개 단어를 출력합니다.
print_topics(lda, feature_names, n_top_words=5)
# 문서별 토픽 비중(theta: doc_topic)을 확인합니다.
for i, topic_dist in enumerate(doc_topic):
# 각 문서가 3개의 토픽에 대해 어떤 비율로 구성되어 있는지 출력합니다.
# np.round를 사용하여 소수점 셋째 자리까지 반올림하여 출력합니다.
print(f"문서 {i} 토픽 분포:", np.round(topic_dist, 3))