데이터 분석/머신러닝, 딥러닝

LDA (Latent Dirichlet Allocation)

fullfish 2025. 11. 26. 15:22

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))