Skip to main content

토픽 모델링

토픽 모델링 (Topic Modeling)

  • 문서 집합에 내재된 주제(토픽)를 자동으로 발견하는 비지도 학습 방법 잠재변수 Z (토픽) → 관찰변수 X (단어)
  • 잠재변수 (latent variable)
    • 직접 관찰되지 않으나 모델 내에서 존재하는 것으로 가정하는 변수
    • 토픽모델링에서는 '토픽(topic)'이라는 잠재 변수를 가정 (예: '스포츠', '정치', '경제')
  • 관찰변수 (observed variable)
    • 직접 관찰되어 데이터에 존재하는 변수
    • 보통 토픽모델링은 **문서 단어 행렬(DTM)**을 관찰변수로 사용

토픽 모델링의 방법

  • 행렬 분해 (Matrix Factorization) 기반
    • Latent Semantic Analysis (LSA): SVD 기반
    • Non-negative Matrix Factorization (NMF)
  • 확률 분포 (Probabilistic Model) 기반
    • Latent Dirichlet Allocation (LDA)

잠재 의미 분석 (Latent Semantic Analysis, LSA)

  • 문서 단어 행렬(DTM) XX특이값 분해(SVD) 를 이용해 여러 행렬의 곱으로 분해하는 방법 Xm×nUm×kΣk×kVk×nTX_{m \times n} \approx U_{m \times k} \Sigma_{k \times k} V^T_{k \times n}
    • UU: 문서-토픽 행렬 (문서별 토픽 분포)
    • Σ\Sigma: 토픽 강도 (각 토픽의 중요도)
    • VTV^T: 토픽-단어 행렬 (토픽별 단어 분포)
    • kk: 토픽의 수 (사용자가 지정하는 하이퍼파라미터)
  • '토픽'은 단어들의 잠재된 의미(semantic) 공간의 축으로 간주 (연속 변수)
  • 여러 토픽(축)들의 가중합으로 원래의 문서-단어 관계를 근사한다고 가정
  • 토픽들은 단어 빈도 데이터의 분산을 설명하는 방향으로 결정됨
  • 수학적으로는 주성분분석(PCA)과 밀접한 관련 (데이터가 중앙 집중화되었을 때 유사)

행렬의 곱셈

  • 행렬 (matrix): 수를 사각형 형태로 배열한 것. 간단히 생각하면 데이터 표.
    • 예) 문서 단어 "행렬" (DTM)
  • 행렬은 일반적인 스칼라 수와 유사하게 덧셈, 곱셈 등의 연산이 정의됨
  • m×nm \times n 크기의 행렬 X는 m×km \times k 크기의 행렬 A와 k×nk \times n 크기의 행렬 B의 곱으로 표현 가능 (단, 내적 차원 kk가 일치해야 함) Xm×n=Am×kBk×nX_{m \times n} = A_{m \times k} B_{k \times n}
  • (다이어그램: X = A * B 행렬 크기 표시)

선형 모형과 행렬 곱셈

  • (단순) 선형 회귀 모형 y=wx+by = wx + b 도 행렬 곱셈 형태로 확장하여 표현 가능 (다변량, 다중 출력 등)
  • 일반적인 선형 변환: Ym×n=Xm×kWk×nY_{m \times n} = X_{m \times k} W_{k \times n} (+ Bias term Bm×nB_{m \times n})
  • (다이어그램: Y=XWY = XW 행렬 크기 표시 - 입력 XX, 가중치 WW, 출력 YY)

LSA의 기본 아이디어 (SVD 기반)

  • 문서 단어 행렬 XX를 차원 축소된 행렬 ZZWW의 곱으로 표현 (근사) Xm×nZm×kWk×nX_{m \times n} \approx Z_{m \times k} W_{k \times n}
    • Z=Um×kΣk×kZ = U_{m \times k} \Sigma_{k \times k} (문서가 k차원 토픽 공간에 표현된 좌표)
    • W=Vk×nTW = V^T_{k \times n} (단어가 k차원 토픽 공간에 표현된 좌표/방향)
  • 차원 축소: 원본 단어 공간(nn)보다 훨씬 작은 토픽 공간(kk)으로 차원을 줄임 (nkn \gg k)
  • (다이어그램: XZWX \approx Z W 행렬 크기 표시, k<nk < n)

LSA의 특징

  • 계산이 비교적 단순 (SVD 알고리즘 사용)
  • 통계학에서 매우 많이 사용하는 기법 (PCA/SVD 기반)
  • 단점:
    • 결과 행렬(U,VU, V)에 음수 값이 포함될 수 있어, 토픽(성분)의 해석이 직관적이지 않을 수 있음
    • 최적의 차원(토픽) 수 kk를 정하기 어려움 (Scree plot 등 보조적 활용)
    • SVD 해는 유일하지만, Z=UΣ1/2Z = U\Sigma^{1/2}, W=Σ1/2VTW=\Sigma^{1/2}V^T 등 다양한 방식으로 분해 가능하며, 회전에 대해 불변하지 않아 해석이 어려울 수 있음 (Varimax 등 회전 기법 필요 시)
    • 해가 무수히 많음 (회전 불확정성) - 슬라이드 설명과 약간 다름: SVD 자체 해는 유일하나, 이후 토픽으로 해석 시 회전 문제 발생

행렬 분해 (Matrix Decomposition)

  • 하나의 행렬을 여러 개의 (특정한 속성을 가진) 행렬들의 곱 형태로 나타내는 기법들의 총칭
  • 종류:
    • LU 분해 (가우시안 소거법 기반)
    • QR 분해 (그람-슈미트 직교화 기반)
    • 촐레스키(Cholesky) 분해 (대칭 양의 정부호 행렬 대상)
    • 고유값 분해 (Eigen Decomposition, 정방 행렬 대상)
    • 특이값 분해 (Singular Value Decomposition, SVD) (임의의 행렬 대상)

특이값 분해 (Singular Value Decomposition, SVD)

  • 임의의 m×nm \times n 크기 행렬 MM을 다음과 같이 세 행렬의 곱으로 분해: Mm×n=Um×mΣm×nVn×nTM_{m \times n} = U_{m \times m} \Sigma_{m \times n} V^T_{n \times n} (Full SVD)
    • UU: m×mm \times m 크기의 직교 행렬 (left singular vectors, UTU=IU^T U = I)
    • VV: n×nn \times n 크기의 직교 행렬 (right singular vectors, VTV=IV^T V = I)
    • Σ\Sigma: m×nm \times n 크기의 대각 행렬 (대각 성분 σi\sigma_i 가 특이값, σ1σ2...0\sigma_1 \ge \sigma_2 \ge ... \ge 0)
  • (축소형 SVD - Reduced SVD): k=min(m,n)k = \min(m, n) 일 때, M=Um×kΣk×kVk×nTM = U_{m \times k} \Sigma_{k \times k} V^T_{k \times n}
  • (다이어그램: M=UΣVTM = U \Sigma V^T 행렬 크기 및 형태 표시)

Truncated SVD (절단된 SVD)

  • Full SVD 또는 Reduced SVD에서 상위 kk개의 가장 큰 특이값(σ1,...,σk\sigma_1, ..., \sigma_k)과 그에 해당하는 UU의 첫 kk개 열, VTV^T의 첫 kk개 행만 남겨 원본 행렬 MM을 근사하는 방법 (kk는 사용자가 지정, k<rank(M)k < \text{rank}(M)). Mm×nUm×kΣk×kVk×nTM_{m \times n} \approx U_{m \times k} \Sigma_{k \times k} V^T_{k \times n}
  • LSA에서 주로 사용되는 형태. 정보 손실을 감수하고 차원을 축소.
  • (다이어그램: U,Σ,VTU, \Sigma, V^T 행렬에서 앞부분 kk개 성분만 회색으로 선택하는 모습 시각화)

Truncated SVD (결과 행렬)

  • MUΣ(V)TM \approx U^* \Sigma^* (V^*)^T
    • UU^*: m×km \times k 행렬 (원본 UU의 첫 kk개 열)
    • Σ\Sigma^*: k×kk \times k 대각 행렬 (원본 Σ\Sigma의 첫 kk개 특이값)
    • (V)T(V^*)^T: k×nk \times n 행렬 (원본 VTV^T의 첫 kk개 행)
  • UU^*mm개의 문서를 kk차원 토픽 공간에 매핑한 결과 (문서 임베딩의 기반)
  • (V)T(V^*)^Tnn개의 단어를 kk차원 토픽 공간에 매핑한 결과 (단어 임베딩의 기반)
  • (다이어그램: 결과 행렬 U,Σ,VTU^*, \Sigma^*, V^{*T} 시각화)

차원 축소 (Dimensionality Reduction) 효과

  • DTM은 mm개의 문서를 nn가지 단어(차원)로 표현 (nn은 매우 클 수 있음)
  • Truncated SVD의 UU^*mm개의 문서를 kk가지 토픽(차원)으로 표현 (knk \ll n) => 문서를 표현하는 차원이 축소됨 (정보 압축)
  • 차원 축소를 하는 이유 (LSA의 장점):
    • 단어 수준이 아닌 의미(토픽) 공간에서 문서를 재배치 (유사한 의미의 문서는 가깝게)
    • 동음이의어(polysemy), 동의어(synonymy) 문제가 (어느 정도) 완화될 수 있음
    • 원본 DTM의 노이즈(noise)가 줄어들어 후속 작업(예: 문서 분류)의 성능이 향상될 수 있음

LSA 실습: 샴푸 특허 데이터

  • 데이터 로드
    import pandas as pd
    df = pd.read_excel('patents.xlsx')
  • 문서 단어 행렬(DTM) 생성 (예: 명사만 추출)
    from sklearn.feature_extraction.text import CountVectorizer
    # kiwi, extract_nouns 함수가 정의되어 있다고 가정
    # min_df=10: 최소 10개 이상 문서에 등장한 단어만 포함 (너무 희귀한 단어 제거)
    cv = CountVectorizer(tokenizer=extract_nouns, min_df=10)
    dtm = cv.fit_transform(df.abstract) # 특허 초록(abstract) 사용
  • DTM 모양 확인: 행(문서 수), 열(단어 수)
    dtm.shape

SVD 적용 (TruncatedSVD)

  • 문서 단어 행렬(DTM)에 Truncated SVD 적용하여 100차원(토픽)으로 축소
    from sklearn.decomposition import TruncatedSVD

    # n_components: 축소할 목표 차원(토픽 수) = k
    # random_state: 알고리즘 내부의 무작위성 제어 (결과 재현용)
    svd = TruncatedSVD(n_components=100, random_state=1234)

    # fit_transform: DTM에 SVD를 적용하고 문서-토픽 행렬(U*Σ*)을 반환
    doc_emb = svd.fit_transform(dtm) # shape: (문서 수, 100)

유사 난수 (Pseudorandom Number)

  • 컴퓨터는 결정론적(deterministic) 알고리즘으로 작동하므로, 진정한 의미의 난수(random number)를 생성하기 어려움
  • 대신, 수학적 알고리즘을 이용해 난수처럼 보이는 긴 주기의 수열을 생성하며 이를 유사 난수라고 함
  • 간단한 알고리즘 예 (Linear Congruential Generator, LCG): xk+1=(axk+c)modmx_{k+1} = (a x_k + c) \mod m ( a,c,ma, c, m은 상수)
  • 씨앗값 (seed): 수열 생성을 시작하는 초기값. 같은 씨앗값을 사용하면 항상 동일한 수열이 생성됨.
    • 보통 씨앗값은 현재 시각 등 예측 불가능한 값을 사용하나, 결과 재현이 필요할 때는 특정 값으로 고정 (예: random_state=42)
  • (슬라이드의 xk+1=(2311)xkmod75x_{k+1} = (2^{31} - 1)x_k \mod 7^5 는 특정 LCG 파라미터 예시)

스크리 플롯 (Scree Plot)

  • TruncatedSVD 결과에서 각 토픽(성분)이 설명하는 분산의 크기 (svd.explained_variance_)를 시각화한 그래프
    • explained_variance_: 각 성분(토픽)의 분산 값 (보통 내림차순 정렬되나, scikit-learn 구현상 약간 다를 수 있음)
  • 고고학의 스크리(scree: 절벽 아래 쌓인 돌무더기 비탈) 모양에서 유래
  • 그래프에서 기울기가 급격히 감소하다가 완만해지는 지점(엘보우, elbow)을 찾아 적절한 토픽(성분)의 수 kk를 결정하는 데 참고할 수 있음
    import matplotlib.pyplot as plt
    plt.plot(svd.explained_variance_)
    plt.xlabel("Topic (Component) Index")
    plt.ylabel("Explained Variance")
    plt.title("Scree Plot")
    plt.show()
  • (이미지: Scree Plot 예시 - 초기 성분에서 분산 설명량이 크고 이후 급격히 감소하다 완만해짐)
  • (이미지: 실제 Scree 지형 사진)

단어 임베딩 (Word Embedding)

  • 단어를 저차원의 연속적인 벡터(vector) 로 표현하는 방법
  • 단어의 의미를 벡터 공간상의 좌표 또는 방향으로 수치화
  • 기본 아이디어: 의미가 비슷한 단어들은 벡터 공간에서 가까운 위치에 있도록 함
  • LSA(SVD)의 토픽-단어 행렬 (VTV^T 또는 VV)도 일종의 단어 임베딩으로 볼 수 있음 (각 단어가 토픽 공간에서 어떤 좌표/방향을 갖는지 나타냄)
  • (고급 임베딩): 단어 임베딩 벡터 간의 연산으로 의미적 관계 표현 가능 (예: vector('왕') - vector('남자') + vector('여자') ≈ vector('여왕'))

단어 임베딩 (LSA/SVD 기반) 추출 및 시각화

  • SVD 결과에서 토픽-단어 행렬(VTV^T) 추출: svd.components_
    # svd.components_ has shape (n_components, n_features) = V^T
    # Transpose to get (n_features, n_components) = V for word vectors
    word_emb = svd.components_.T
  • 특정 단어(예: '모발')의 인덱스(번호) 찾기
    words = cv.get_feature_names_out().tolist() # 단어 목록
    i = words.index('모발')
  • 해당 단어의 임베딩 벡터(각 토픽에 대한 값) 시각화
    plt.plot(word_emb[i])
    plt.xlabel("Topic Index")
    plt.ylabel("Loading")
    plt.title("Word Embedding Vector for '모발'")
  • (이미지: 특정 단어('모발')의 임베딩 벡터 시각화 - 100개 토픽 축에서의 값)

단어 임베딩의 코사인 유사도

  • 두 단어 벡터 간의 코사인 유사도를 계산하여 의미적 유사성 측정
    from sklearn.metrics.pairwise import cosine_similarity

    # word_emb shape: (n_features, n_components)
    sim = cosine_similarity(word_emb) # (n_features, n_features) similarity matrix
  • 특정 단어(i번째, 예: '모발')와 유사도가 높은 단어 10개 찾기
    import numpy as np

    sim_i = sim[i]
    s = np.argsort(sim_i)

    related_indices = s[-2:-12:-1] # Top 10 excluding self

    print(f"Words similar to '{words[i]}':")
    for j in related_indices:
    print(f"- {words[j]} (Similarity: {sim_i[j]:.4f})")

단어 번호 찾기

  • 분석 대상 단어 리스트의 인덱스를 DTM의 단어 목록에서 찾음
    indices = []
    target_words = ['모발', '손상', '두피', '모공', '용기', '내용물']
    words_list = cv.get_feature_names_out().tolist() # From CountVectorizer

    print("Finding indices for target words:")
    for w in target_words:
    try:
    idx = words_list.index(w)
    indices.append(idx)
    print(f"- '{w}': index {idx}")
    except ValueError:
    print(f"- '{w}' not found in vocabulary.")
    # Optionally handle missing words (e.g., skip or add placeholder)

    # 'indices' now holds the found indices for subsequent analysis (like MDS)

다차원 척도법 (MultiDimensional Scaling, MDS)

  • 개체(점)들 간의 (비)유사성 또는 거리(distance) 정보가 주어졌을 때, 이 정보를 최대한 보존하면서 개체들을 저차원 공간(보통 2D) 에 배치(좌표 계산)하는 기법
  • 결과를 시각화하여 개체들 간의 상대적 관계를 파악하는 데 사용
  • 예) 브랜드 포지셔닝 맵, 단어 의미 관계 시각화 등
  • (이미지: 패스트푸드 경쟁사 포지셔닝 맵 예시 - 브랜드 간 인식 거리 기반으로 2D 맵에 배치)

MDS를 이용한 단어 유사도 시각화

  • (코사인) 유사도를 거리로 변환: distance = 1 - similarity
    # Assuming 'sim' is the full similarity matrix (n_words, n_words)
    # Assuming 'indices' contains indices of target words to visualize
    # Select the similarity submatrix for target words
    sim_target = sim[indices,][:, indices]
    # Convert similarity to distance
    dist_target = 1 - sim_target
  • MDS를 이용해 2차원 좌표 계산
    from sklearn.manifold import MDS

    # dissimilarity='precomputed': Input is a distance matrix
    # n_components=2: Target dimension for visualization
    # random_state: For reproducible results
    mds = MDS(n_components=2, dissimilarity='precomputed', random_state=1234)

    # Calculate 2D coordinates
    pos = mds.fit_transform(dist_target) # shape: (len(indices), 2)

adjustText

  • Matplotlib 그래프에서 텍스트 레이블(주석)이 서로 겹치지 않도록 자동으로 위치를 조정해주는 라이브러리
  • 설치
    !pip install adjusttext
  • 임포트
    from adjustText import adjust_text

Windows 한글 글꼴 설정 (Matplotlib)

  • Matplotlib에서 한글을 표시하기 위한 설정 (Windows 환경 기준)
  • 사용 가능한 한글 글꼴 이름 찾기 (예: 'Malgun Gothic')
    from matplotlib import font_manager
    import matplotlib.pyplot as plt

    # Find available fonts
    # for font in font_manager.fontManager.ttflist:
    # if 'Malgun' in font.name or 'Nanum' in font.name: # Add other Korean font names if needed
    # print(font.name, font.fname)

    # Set default font
    plt.rc('font', family='Malgun Gothic') # Replace with an available Korean font name
  • (macOS의 경우 'AppleGothic', Linux의 경우 'NanumGothic' 등 설치 및 지정 필요)

마이너스 부호 설정 (Matplotlib)

  • Matplotlib에서 한글 폰트 사용 시, 숫자 축의 마이너스(-) 부호가 깨지는 현상 방지
  • 원인: 유니코드 마이너스 문자(U+2212)를 대부분의 한글 폰트가 지원하지 않기 때문
  • 해결: 마이너스 부호 대신 기본 아스키 하이픈-마이너스(U+002D)를 사용하도록 설정
    import matplotlib.pyplot as plt

    plt.rc('axes', unicode_minus=False)

단어 임베딩 시각화 (MDS + adjustText)

  • MDS로 계산된 2D 좌표(pos)와 대상 단어 목록(target_words)을 이용해 산점도 그리기
    import matplotlib.pyplot as plt
    from adjustText import adjust_text

    # Assuming 'pos' contains 2D coordinates (n_target_words, 2)
    # Assuming 'target_words' contains the list of word strings
    # Assuming Korean font and minus sign are set

    fig, ax = plt.subplots(figsize=(8, 8)) # Create figure and axes object
    ax.plot(pos[:, 0], pos[:, 1], 'o', markersize=5) # Plot points

    # Create text labels
    texts = []
    for i, word in enumerate(target_words):
    texts.append(ax.text(pos[i, 0], pos[i, 1], word))

    # Adjust text labels to prevent overlap
    adjust_text(texts, ax=ax, arrowprops=dict(arrowstyle='-', color='gray', lw=0.5))

    plt.title("Word Similarity Visualization using MDS")
    plt.xlabel("MDS Dimension 1")
    plt.ylabel("MDS Dimension 2")
    plt.grid(True)
    plt.show()
  • (이미지: 2D 공간에 '모발', '손상', '두피' 등 관련 단어들이 MDS로 배치되고 adjustText로 레이블이 조정된 시각화 결과)

회전 (Rotation) in Factor Analysis / Topic Modeling

  • LSA/SVD/PCA 등 행렬 분해 기반 방법의 결과는 수학적으로는 유효하지만, 각 성분(토픽, 요인)의 해석이 어려울 수 있음 (여러 변수에 걸쳐 로딩값이 분산됨)
  • 결과 행렬(VTV^T 등)에 회전 변환을 적용하여 성분(토픽)이 특정 변수(단어)들과 더 명확하게 연관되도록 구조를 단순화 (Simple Structure 추구) XZW=ZRR1W=(ZR)(R1W)=ZWX \approx ZW = Z R R^{-1} W = (ZR)(R^{-1}W) = Z'W'
    • RR: k×kk \times k 회전 행렬 (R1=RTR^{-1} = R^T for orthogonal rotation)
    • W=R1WW' = R^{-1}W: 회전된 토픽-단어 행렬
  • 목적: 기계적 성능(예: 재구성 오차, 분류 성능)은 유지하면서 사람이 해석하기 쉬운 토픽/요인 구조를 얻기 위함
  • 축소된 차원(kk) 내에서 축을 회전시켜도 데이터 점들 간의 상대적 관계(거리, 유사도)는 크게 변하지 않음
  • 기계적 처리만 할 경우에는 회전이 필수는 아님

회전의 시각적 이해

  • 차원 축소: (다이어그램: 고차원 데이터를 저차원 공간으로 투영/압축하는 과정 시각화)
  • 회전: (다이어그램: 저차원 공간에서 좌표축 자체를 회전시키는 변환. 점들의 상대적 위치는 유지되지만 축에 대한 좌표값은 변함. 예: 2D 공간에서 X, Y 축을 45도 회전)

VARIMAX 회전

  • 가장 널리 사용되는 직교 회전(orthogonal rotation) 방법 중 하나
  • 목표: 회전된 토픽-단어 행렬(WW')의 각 열(토픽) 내에서, 단어 로딩(가중치) 제곱값들의 분산(variance) 을 최대화(maximize)하도록 회전 각도를 찾음
  • 결과: 각 토픽은 가능하면 소수의 단어에만 높은 로딩값을 갖고, 나머지 단어에는 낮은 로딩값을 갖도록 함 (Simple Structure) -> 토픽 해석 용이
  • 직교 회전: 회전 후에도 토픽(축)들은 서로 직교(독립) 상태를 유지 ↔ 사교 회전(oblique rotation): 토픽 간 상관관계 허용 (예: Promax, Oblimin)

회전 적용 (factor_analyzer 사용 예시)

  • factor_analyzer 라이브러리 설치
    pip install factor_analyzer
  • Rotator 클래스를 이용해 Varimax 회전 적용
    from factor_analyzer.rotator import Rotator

    # word_emb: 원본 토픽-단어 행렬 (n_features, n_components) = V from SVD
    rotator = Rotator(method='varimax') # method default is varimax
    # rot: 회전된 토픽-단어 행렬 (n_features, n_components)
    rot = rotator.fit_transform(word_emb)
  • 회전된 단어('모발') 임베딩 벡터 시각화 (특정 토픽에서 값이 두드러짐 확인)
    plt.plot(rot[i])
    plt.xlabel("Rotated Topic Index")
    plt.ylabel("Loading")
    plt.title("Rotated Word Embedding Vector for '모발'")
    plt.show()
  • (이미지: 회전 후 '모발' 단어 임베딩 벡터 시각화 - 특정 토픽(축)에서 값이 뾰족하게 나타남)

관련도 높은 토픽 보기 (회전 후)

  • 회전된 결과를 바탕으로 특정 단어와 가장 관련 깊은 토픽 찾기
  • i번 단어('모발')와 가장 관련도가 큰 (로딩값이 가장 큰) 토픽 번호 찾기
# i번 단어와 가장 관련도가 큰 토픽 번호
t = np.argmax(np.abs(rot[i]))

# t번 토픽과 관련도가 큰 단어들
sign = np.sign(rot[i, t]) # i번 단어의 로딩값 부호
topic_words_idx = np.argsort(sign * rot[:, t])

# 관련도 순으로 출력
for j in topic_words_idx[-1:-11:-1]:
print(words[j])