Post

[Python] ML-LDA (Linear Discriminant Analysis)

[Python] ML-LDA (Linear Discriminant Analysis)

1. 왜 등장했는가

PCA처럼 고차원 데이터를 저차원으로 줄이고 싶지만, PCA는 분산을 기준으로 축을 찾아
클래스 구분에 최적화되지 않는 문제가 있었습니다.
LDA는 클래스 간 분산을 최대화하고 클래스 내 분산을 최소화하는 방향으로 투영해
분류에 특화된 차원 축소와 분류를 동시에 수행합니다. (Fisher, 1936)


2. 핵심 아이디어 — 클래스를 가장 잘 구분하는 축 찾기

LDA는 본질적으로 같은 반 학생끼리는 뭉치고, 다른 반 학생은 멀어지도록 투영 방향을 찾습니다.

lda_idea

1
2
3
4
5
6
7
8
9
10
11
12
2D 데이터를 1D로 투영하는 두 가지 방법:

투영 방향 A (PCA 방식):             투영 방향 B (LDA 방식):
분산이 최대인 방향                   클래스 구분이 최대인 방향

  ○ ○ ○                               ○ ○ ○
   ● ● ●  ──→  투영 →                  ● ● ●  ──→  투영 →
  ○ ○ ●●                             ○ ○ ○

1D 결과: ○●○●●○○   (섞임)           1D 결과: ○○○○ | ●●●●  (분리!)

LDA는 클래스가 잘 분리되는 방향 B를 선택합니다.

lda_vs_pca_direction


3. 실제 예시로 보기 (분류 / 차원 축소)

예시 1 — 타이타닉 생존 예측 (분류)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
훈련 데이터:
┌────────┬────────┬─────┬──────────┬──────────┐
│ 이름   │ 성별   │ 나이 │ 객실 등급 │ 생존 여부 │
├────────┼────────┼─────┼──────────┼──────────┤
│ Alice  │  0     │  29  │    1     │    ✅    │
│ Bob    │  1     │  31  │    3     │    ❌    │
│ Carol  │  0     │  45  │    3     │    ✅    │
│ Dave   │  1     │  12  │    2     │    ✅    │
│ Eve    │  1     │  38  │    3     │    ❌    │
└────────┴────────┴─────┴──────────┴──────────┘

LDA 학습:
  클래스 내 분산 (SW): 같은 생존/사망 그룹 안에서 퍼짐 → 작게
  클래스 간 분산 (SB): 생존 vs 사망 그룹 사이 거리   → 크게

  → SW⁻¹SB를 최대화하는 방향 w 찾기

새 승객 예측:
  투영값 = w^T × [성별, 나이, 등급]
  → 투영값이 임계값보다 크면 ✅ 생존, 작으면 ❌ 사망

예시 2 — 차원 축소 (분류 전처리)

1
2
3
4
5
6
7
원본 특성: 10개 → LDA → 최대 (클래스 수 - 1)개 차원으로 축소

이진 분류 (생존/사망):
  10개 특성 → LDA → 1개 축 (판별 함수)

3클래스 분류 (꽃 종류 3개):
  4개 특성 → LDA → 2개 축 (최대 3-1=2)

4. 알고리즘 구성 요소

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
     ┌─────────────────────────────────────┐
     │   클래스별 평균 μₖ 계산              │  ← 각 클래스의 중심
     └─────────────────┬───────────────────┘
                       │
                       ▼
     ┌─────────────────────────────────────┐
     │   클래스 내 분산행렬 SW 계산         │  ← 같은 클래스 안에서 퍼짐
     └─────────────────┬───────────────────┘
                       │
                       ▼
     ┌─────────────────────────────────────┐
     │   클래스 간 분산행렬 SB 계산         │  ← 클래스 중심 간 거리
     └─────────────────┬───────────────────┘
                       │
                       ▼
     ┌─────────────────────────────────────┐
     │   SW⁻¹SB 고유벡터 계산              │  ← 최적 투영 방향
     └─────────────────┬───────────────────┘
                       │
                       ▼
     ┌─────────────────────────────────────┐
     │   데이터 투영 및 분류                │  ← 새 샘플을 같은 방향으로 투영
     └─────────────────────────────────────┘

lda_component

구성 요소설명비유
클래스 내 분산 SW같은 클래스 안에서의 퍼짐같은 팀 선수들 사이 거리
클래스 간 분산 SB클래스 중심들 사이 거리두 팀 중심 사이 거리
판별 함수SW⁻¹SB를 최대화하는 투영 방향두 팀이 가장 잘 분리되는 관점
사전 확률 πₖ각 클래스의 비율각 팀의 인원 비율

5. 어떻게 최적 방향을 찾는가

5-1. 목적 함수 (Fisher’s Criterion)

\[J(w) = \frac{w^T S_B w}{w^T S_W w}\]
1
2
3
4
분자: 투영 후 클래스 간 분산 → 크게!
분모: 투영 후 클래스 내 분산 → 작게!

→ 이 비율을 최대화하는 w를 구하는 것이 LDA

5-2. 해석적 해 (Closed-form)

\[w = S_W^{-1}(\mu_1 - \mu_2)\]
1
2
3
4
5
이진 분류의 경우:
  w = (클래스 내 공분산 행렬)^{-1} × (두 클래스 평균의 차이)

직관: 두 클래스 중심을 잇는 방향으로 투영하되
      클래스 내 분산으로 스케일 조정

5-3. LDA의 가정

1
2
3
4
5
6
1. 각 클래스가 정규분포를 따름 (Gaussian Distribution)
2. 모든 클래스가 동일한 공분산 행렬을 가짐 (Homoscedasticity)
3. 특성들이 서로 독립적 (Naive Bayes보다 완화된 가정)

가정이 맞을 때: LDA가 이론적 최적 선형 분류기
가정이 틀릴 때: Logistic Regression이나 트리 기반 모델 고려

6. LDA 장・단점

6-1. ✅ LDA 장점

1
2
3
4
5
6
7
8
9
10
11
1. 분류 + 차원 축소 동시 수행
   → 특성이 많을 때 (클래스 수 - 1)개 축으로 효율적으로 축소

2. 해석 가능성
   → 각 판별 함수의 계수로 어떤 특성이 분류에 중요한지 파악

3. 빠른 학습과 예측
   → 닫힌 형태의 해 존재 → 반복 학습 불필요

4. 소규모 데이터에 강함
   → 파라미터가 적어 적은 데이터로도 안정적

6-2. ❌ LDA가 약한 상황

1
2
3
4
5
6
7
8
1. 비선형 패턴
   → 선형 결정 경계만 가능 → QDA 또는 비선형 모델 고려

2. 클래스 불균형
   → 사전 확률이 클래스 비율에 영향 → class_weight 조정 필요

3. 가정 위반
   → 정규분포 가정, 동분산 가정이 맞지 않으면 성능 저하

6-2-1. 클래스 분산이 다를 때 (이분산)

LDA의 동분산 가정이 위반되는 경우입니다.

1
2
3
4
5
클래스 0 (사망): 특성 분산이 큼  ← 다양한 패턴
클래스 1 (생존): 특성 분산이 작음  ← 특정 패턴에 집중

LDA: 동분산 가정 → 경계가 왜곡될 수 있음
QDA: 각 클래스별 공분산 행렬 추정 → 더 정확한 경계

해결책 :

1
2
3
4
5
6
7
8
9
10
11
12
13
# 방법 1: QDA (이분산 허용)
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
qda = QuadraticDiscriminantAnalysis()
qda.fit(X_train, y_train)

# 방법 2: LDA solver 변경
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
lda = LinearDiscriminantAnalysis(
    solver='lsqr',    # 'svd', 'lsqr', 'eigen'
    shrinkage='auto'  # 공분산 행렬 정규화 (소규모 데이터에 유용)
)
lda.fit(X_train, y_train)
print(f"설명된 분산: {lda.explained_variance_ratio_}")

7. 한눈에 요약

항목내용
알고리즘 유형지도학습 / 분류 & 차원 축소
핵심 아이디어클래스 간 분산 ↑, 클래스 내 분산 ↓ 방향으로 투영
결정 경계선형
최대 축소 차원클래스 수 - 1
스케일링 필요?✅ 권장 (공분산 계산에 영향)
핵심 파라미터n_components, solver, shrinkage
실전 사용차원 축소 전처리, 소규모 데이터, 해석 중요할 때

8. 다른 알고리즘과 무엇이 다른가

PCA vs LDA

1
2
3
4
PCA (비지도):                        LDA (지도):
클래스 정보 없이 분산 최대 방향        클래스 정보를 이용해
→ 데이터 전체 구조 보존               클래스 구분 최대 방향
→ 분류에 최적화 안 됨                 → 분류에 최적화됨
항목PCALDA
학습 방식비지도지도
목적분산 최대화클래스 분리 최대화
최대 차원min(n, p) - 1클래스 수 - 1
분류 최적화

9. 코드로 보기 — 타이타닉 생존 예측

1
2
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.preprocessing import StandardScaler

9-1. 전처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pandas as pd
from sklearn.model_selection import train_test_split

titanic = pd.read_csv('./Data/Titanic.csv')
titanic['FamSize'] = titanic['SibSp'] + titanic['Parch']

use_cols = ['Survived', 'Pclass', 'Sex', 'Age', 'FamSize', 'Fare', 'Embarked']
titanic = titanic[use_cols].dropna(subset=['Age'])
titanic['Age'] = titanic['Age'].astype(int)
titanic = pd.get_dummies(titanic, columns=['Pclass', 'Sex', 'Embarked'], drop_first=True)

y = titanic['Survived']
X = titanic.drop('Survived', axis=1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)

# ✅ LDA는 공분산 행렬 계산에 스케일 영향 → 스케일링 권장
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test  = scaler.transform(X_test)

Note: LDA는 공분산 행렬을 직접 계산하므로 특성 간 스케일 차이가 결과에 영향을 줍니다. StandardScaler를 사용하는 것을 권장합니다.


9-2. 모델 학습

1
2
3
4
5
6
lda = LinearDiscriminantAnalysis(
    n_components=1,    # 이진 분류: 최대 1개 판별 축
    solver='svd',      # 'svd'(기본), 'lsqr', 'eigen'
    shrinkage=None     # 'auto' 또는 0~1 수치 (소규모 데이터에 유용)
)
lda.fit(X_train, y_train)

ParameterDefault역할과적합 방향
n_componentsNone투영 축 수 (최대: 클래스 수-1)-
solver'svd'공분산 행렬 계산 방법-
shrinkageNone공분산 행렬 정규화None일수록 과적합 ↑
  • n_components : 차원 축소 시 유지할 축 수
    • 값 변화별 효과
      • 이진 분류는 최대 1개, 3클래스는 최대 2개
      • 분류가 목적이면 기본값(None) 유지
      • 차원 축소가 목적이면 작은 값 지정
  • shrinkage : 공분산 행렬 정규화 강도
    • 값 변화별 효과
      • 'auto' → Ledoit-Wolf 추정량으로 자동 계산 (소규모 데이터 권장)
      • 0~1 수치 → 직접 지정
      • solver='lsqr' 또는 'eigen'과 함께 사용

9-3. 평가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from sklearn.metrics import (
    accuracy_score, confusion_matrix,
    classification_report, roc_auc_score
)

pred      = lda.predict(X_test)
pred_prob = lda.predict_proba(X_test)[:, 1]

cfx         = confusion_matrix(y_test, pred)
sensitivity = cfx[1, 1] / (cfx[1, 0] + cfx[1, 1])
specificity = cfx[0, 0] / (cfx[0, 0] + cfx[0, 1])
roc_auc     = roc_auc_score(y_test, pred_prob)

print(f"Accuracy    : {accuracy_score(y_test, pred) * 100:.2f}%")
print(f"Sensitivity : {sensitivity * 100:.2f}%")
print(f"Specificity : {specificity * 100:.2f}%")
print(f"ROC AUC     : {roc_auc:.4f}")
print()
print(classification_report(y_test, pred, target_names=['Died (0)', 'Survived (1)']))

9-4. 차원 축소로 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt

# 1D로 투영 (이진 분류)
X_lda = lda.transform(X_train)

plt.figure(figsize=(8, 4))
plt.scatter(X_lda[y_train==0, 0], [0]*sum(y_train==0),
            color='steelblue', alpha=0.6, label='Died (0)', s=40)
plt.scatter(X_lda[y_train==1, 0], [0]*sum(y_train==1),
            color='tomato', alpha=0.6, label='Survived (1)', s=40)
plt.xlabel('LDA 판별 축 1')
plt.title('LDA 투영 결과 — 두 클래스 분리 시각화')
plt.legend(fontsize=10)
plt.yticks([])
plt.grid(True, alpha=0.2)
plt.tight_layout()
plt.show()

lda_projection

투영된 1D 축에서 두 클래스가 얼마나 잘 분리되는지 확인할 수 있습니다.

This post is licensed under CC BY 4.0 by the author.