본문 바로가기

Machine Learning_모델설계_Python

판별분석_3 선형판별분석(LDA)

안녕하십니까 배도리입니다. 너무 오랜만에 게시글을 작성하네요. 저는 그동안 공모전, 과제를 준비하면서 블로그 관리에 소홀히 했지만만 대신 공모전에서 대상을 수상했습니다. 호호. 그러나 이것에 만족하고 멈출 수 없습니다. 만일 발전없이 그냥 평화롭게 살면 어떻게 될까요 바로 '비육지탄' 되는겁니다. 무슨 뜻일까요?

 

화장실에 간 유비는 바지를 벗고 앉다가 살이 두둑하게 오른 자기 넓적다리를 보았다. 문득 신세가 한심스러워진 유비는 저도 모르게 눈물을 흘렸다. 자리에 돌아온 유비의 얼굴을 무심코 바라본 유표는 깜짝 놀라 물었다.

“아니, 대체 무슨 일이시오? 얼굴에 눈물 흔적이 있소이다.”

유비가 깊이 한숨 쉬며 대답했다.

“지난날, 몸이 하루도 말안장을 떠난 적이 없어 넓적다리에 살이 오를 틈이 없었는데 이제 보니 살이 두둑이 올라 있습니다. 말을 타고 전장을 누빈 지가 오래됐기 때문인가 봅니다. 아무런 공도 쌓지 못한 채로 헛되게 세월을 보내는 사이, 몸마저 늙으니 마음이 서러워 저절로 눈물이 흘렀습니다.”

비육지탄(髀肉之嘆)’은 여기에서 비롯했다. ‘비육’은 넓적다리 살을 말한다. 장수가 말을 타고 싸움터를 헤집고 다니면 거기에 살이 오를 틈이 없다. 10여 년을 평화롭게 지낸 유비의 말처럼 비육지탄은 “넓적다리 살이 오른 것을 한탄한다”라는 뜻으로 헛되이 세월만 보냄을 뜻한다.

 

닭의 넓적다리 살이 두둑이 올라오면 좋으나 저의 다리가 그리 되게 해선 안됩니다. 꾸준한 스쿼트의 중요함을 배울수 있는 사자성어였습니다. 꾸준히 스쿼드를 하듯이 꾸준히 게시글을 올려 한탄치 않겠습니다.

 

이전에 배운 선형판별분석 실습을 진행해 보겠습니다.

세 개의 다변량 정규 분포를 생성하고, 각 분포에서 100개의 샘플을 추출한 후, 이 샘플들을 결합하여 최종 데이터셋을 만들겠습니다. 이 데이터셋은 다변량 특징을 가진 세 개의 클래스를 포함하며, 이를 분류하는 데 사용할 수 있습니다.

 
from scipy.stats import multivariate_normal
import numpy as np

#다변량 정규 분포는 다차원의 연속 확률 변수들이 정규 분포를 따를 때 사용하는 분포입니다. 각 변수에 대한 평균과 이들 변수 간의 공분산 행렬로 정의됩니다.
#코드에서 평균 벡터는 각 다변량 정규 분포를 정의할 때 나타납니다. 평균 벡터는 다변량 정규 분포의 중심 위치를 나타내는데 사용되며,
 
N = 100 #sample count #다변량을 만든거
rv1 = multivariate_normal([ 0, 0], [[0.7, 0.0], [0.0, 0.7]]) #mean, cov
rv2 = multivariate_normal([ 1, 1], [[0.8, 0.2], [0.2, 0.8]]) #mean, cov
rv3 = multivariate_normal([-1, 1], [[0.8, 0.2], [0.2, 0.8]]) #mean, cov
#이 예제에서는 각 다변량 정규 분포(rv1, rv2, rv3)에 대해 평균 벡터를 지정하고 있습니다.
 
np.random.seed(0) #random seed
X1 = rv1.rvs(N
X2 = rv2.rvs(N) #각 다변량 정규 분포에서 N개의 무작위 샘플을 추출합니다.
X3 = rv3.rvs(N)

y1 = np.zeros(N)  #class: 0 np.zeros(N) 함수는 길이가 N인 0으로 채워진 배열을 생성합니다.
#여기서는 이 배열이 클래스 0에 해당하는 레이블을 나타냅니다. 즉, X1 샘플들은 모두 클래스 0에 속합니다.
y2 = np.ones(N)   #class: 1 np.ones(N) 함수는 길이가 N인 1로 채워진 배열을 생성합니다.
y3 = 2*np.ones(N) #class: 2 2*np.ones(N)은 길이가 N인 2로 채워진 배열을 생성합니다. 여기서는 이 배열이 클래스 2에 해당하는 레이블을 나타냅니다.

X = np.vstack([X1, X2, X3]) #추출한 샘플들을 수직으로 쌓아 데이터 행렬 X를 만듭니다.
y = np.hstack([y1, y2, y3]) #클래스 레이블을 수평으로 연결하여 레이블 벡터 y를 만듭니다.

 

혹시 공분산이 헷갈리시는 분들을 위해 다시 한번 설명드리겠습니다. 공분산(Covariance)은 두 변수간의 선형 관계를 측정하는 통계적 척도입니다. 공분산은 두 변수가 함께 어떻게 변화하는지를 나타내며, 두 변수의 편차곱의 평균으로 계산됩니다. 공분산이 양수일 경우 두 변수가 같은 방향으로 변화하는 경향이 있으며, 음수일 경우 두 변수가 반대 방향으로 변화하는 경향이 있습니다. 0에 가까울 경우 두 변수간에 선형 관계가 약하거나 존재하지 않음을 나타냅니다.

공분산의 주요 단점은 단위에 따라 값이 크게 달라질 수 있다는 점입니다. 이로 인해 공분산만으로 두 변수 간의 관계를 명확하게 파악하기 어렵습니다. 이러한 문제를 해결하기 위해 피어슨 상관계수(Pearson's correlation coefficient)와 같은 정규화된 척도를 사용하는 것이 일반적입니다.

 

공분산 행렬특성 간의 공분산을 나타내며, 이를 통해 특성 간의 관계를 이해할 수 있습니다. 또한 클래스 간의 경계를 이해하는 데 도움이 되며, 이를 사용하여 분류기의 성능을 평가하거나 개선할 수 있습니다. 다차원 확률 변수들 간의 공분산을 나타내공분산 행렬은 변수들 간의 선형 관계와 분포의 형태를 설명하는 데 사용되며, 대각선 원소는 각 변수의 분산을 나타내고, 대각선이 아닌 원소는 두 변수 간의 공분산을 나타냅니다.

 

 

잘됐는지 한번 볼까요

X1

 

 

한번 시각화를 해볼게요

import matplotlib.pyplot as plt

plt.scatter(X1[:,0], X1[:,1], alpha=0.8, s=50, marker="o", color='r', label="class 1") #alpha: marker의 투명도
#plt.scatter 함수는 X1 데이터셋의 첫 번째 열과 두 번째 열에 있는 x, y 좌표를 사용하여 클래스 1의 데이터 포인트를 산점도에 표시합니다.
plt.scatter(X2[:,0], X2[:,1], alpha=0.8, s=50, marker="s", color='g', label="class 2")
plt.scatter(X3[:,0], X3[:,1], alpha=0.8, s=50, marker="x", color='b', label="class 3")
plt.xlim(-5, 5) #이 코드는 그래프의 x축과 y축의 범위를 설정합니다. x축의 범위는 -5에서 5까지이고, y축의 범위는 -4에서 5까지입니다.
plt.ylim(-4, 5)
plt.xlabel("$x_1$")
plt.ylabel("$x_2$")
plt.legend()
plt.show()

선형 판별 분석을 사용하여 입력 데이터 X와 레이블 벡터 y를 기반으로 분류기를 학습시키겠습니다.

 
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
lda = LinearDiscriminantAnalysis(store_covariance=True).fit(X, y)
#LinearDiscriminantAnalysis 클래스의 인스턴스를 생성하고, store_covariance=True 옵션을 사용하여 생성 모델에서 공분산 행렬을 저장하도록 지시합니다.
#이렇게 하면 모델이 학습된 후에 공분산 행렬을 검사할 수 있습니다.fit(X, y) 함수를 호출하여 데이터 X와 레이블 벡터 y를 사용하여 LDA 모델을 학습시킵니다.
#학습이 완료되면 lda 객체에 학습된 LDA 모델이 저장되어 있으며, 이를 사용하여 새로운 데이터에 대한 예측을 수행할 수 있습니다.

 

 

lda.means_ 란 각 클래스의 특성 평균 값을 저장하는 속성입니다. 각 클래스의 평균 벡터를 확인하겠습니다.

lda.means_
#LDA 모델이 학습된 후, lda.means_ 속성은 각 클래스에 대한 특성 평균을 포함하는 2차원 배열을 반환합니다.
 
array(
[[-8.01254084e-04, 1.19457204e-01],
[ 1.16303727e+00, 1.03930605e+00],
[-8.64060404e-01, 1.02295794e+00]])

 

첫 번째 행 [-8.01254084e-04, 1.19457204e-01]은 첫 번째 클래스(class 1)의 평균 벡터를 나타냅니다. 이 클래스의 데이터 포인트의 평균 x 좌표는 약 -0.0008이고, 평균 y 좌표는 약 0.1195입니다.
두 번째 행 [1.16303727e+00, 1.03930605e+00]은 두 번째 클래스(class 2)의 평균 벡터를 나타냅니다. 이 클래스의 데이터 포인트의 평균 x 좌표는 약 1.163이고, 평균 y 좌표는 약 1.039입니다.
세 번째 행 [-8.64060404e-01, 1.02295794e+00]은 세 번째 클래스(class 3)의 평균 벡터를 나타냅니다. 이 클래스의 데이터 포인트의 평균 x 좌표는 약 -0.864이고, 평균 y 좌표는 약 1.023입니다.

 

 

공분산 행렬을 검사하기 위해 lda.covariance_ 를 이용하겠습니다. LDA에서 사용하는 공분산 행렬은 각 클래스에 대한 공분산 행렬이 아니라, 모든 클래스에 대해 가정된 공통 공분산 행렬입니다. 이를 이용하여 LDA는 각 클래스의 중심 위치를 고려한 선형 경계를 찾습니다. LDA에서 이 공분산 행렬은 클래스 내의 분산을 나타내며, 이를 사용하여 클래스 간의 구분을 최대화하고 클래스 내의 분산을 최소화하는 선형 변환을 찾는 것이 LDA의 목표입니다

lda.covariance_
#3개 클래스의 개별 공분산 행렬이 아닌 공통 공분산 행렬을 사용하는 이유는,
# LDA가 클래스 내 분산을 최소화하고 클래스 간 분산을 최대화하려는 목적을 달성하기 위해 공통된 특성 공간을 기반으로 작동하기 때문입니다.
# 이러한 접근 방식은 계산 효율성을 높이고, 모델의 복잡성을 줄여 일반화 성능을 향상시킵니다.
 
 
array(
[[0.7718516 , 0.13942905],
[0.13942905, 0.7620019 ]])

 

(0, 0) 위치의 원소 0.7718516은 첫 번째 특성 간의 공분산입니다. 이 값은 첫 번째 특성의 분산을 나타냅니다.
(1, 1) 위치의 원소 0.7620019는 두 번째 특성 간의 공분산입니다. 이 값은 두 번째 특성의 분산을 나타냅니다.
(0, 1) 위치의 원소 0.13942905는 첫 번째 특성과 두 번째 특성 간의 공분산을 나타냅니다.
(1, 0) 위치의 원소 0.13942905는 두 번째 특성과 첫 번째 특성 간의 공분산을 나타냅니다. 공분산 행렬은 대칭 행렬이므로, (0, 1) 위치의 원소와 동일한 값을 가집니다.

 

 

 

이제 LDA가 적용되서 클래스 예측을 해보겠습니다. 코드가 저에겐 복잡해서 상세하게 부연설명을 해봤습니다.

import seaborn as sns
import matplotlib as mpl

#X1, X2, X3 데이터 포인트는 이미 LDA 모델을 학습하는 데 사용되었으며, 이후에는 직접 사용되지 않았습니다.
#대신, XX1과 XX2 격자 좌표를 사용하여 클래스 레이블을 예측하는 데 초점을 맞추고 있습니다.
#lda.predict가 적용된 클래스들의 경계선을 표현하기 위해서 격자에다가도 lda.predict를 적용했다고 생각하겠습니다.
x1min, x1max = -5, 5 #x1min, x1max, x2min, x2max를 사용하여 격자(grid)를 만듭니다.
x2min, x2max = -4, 5
XX1, XX2 = np.meshgrid(np.arange(x1min, x1max, (x1max-x1min)/1000), #격차의 가로 세로 간격을 정의
                       np.arange(x2min, x2max, (x2max-x2min)/1000))
#np.meshgrid()는 두 개의 1차원 배열을 인수로 받아, 이 두 배열을 조합하여 2차원 격자를 만듭니다. 각 점은 공간상의 위치를 나타냅니다.

YY = np.reshape(lda.predict(np.array([XX1.ravel(), XX2.ravel()]).T), XX1.shape)
#이 줄은 lda.predict() 함수를 사용하여 격자 상의 모든 점에 대한 클래스를 예측하고, 이를 YY에 저장합니다.
#ravel() 함수는 각 격자 행렬을 1차원 배열로 평탄화(flatten)합니다. 이렇게 하면, 각 격자 점의 좌표를 선형 판별 분석 모델의 입력으로 사용하기에 적합한 형태가 됩니다.
#np.array([XX1.ravel(), XX2.ravel()]).T: 평탄화된 격자 배열을 결합하여 입력 특성의 배열을 만듭니다.
#T는 전치(transpose) 연산으로, 배열의 행과 열을 바꿉니다. 이렇게 하면 각 행이 격자 점의 좌표를 나타내는 형태가 됩니다.

# XX1과 XX2가 각각 1차원 배열로 변환되고, 이 두 배열은 각각 한 개의 열로 구성됩니다.
# 그런 다음 이 두 배열을 np.array([XX1.ravel(), XX2.ravel()])로 결합하여 두 개의 열을 가진 배열을 생성합니다. 이제 전치를 수행하면,
# 격자 점의 좌표를 나타내는 2차원 배열이 생성되며, 이 배열에서 행이 2개가 됩니다 이후에 predict함수사용하여 예측을 수행한 후 1차원 배열을 얻습니다.
#이후에 np.reshape을 써서 2차원으로 만듭니다.

#lda.predict(...): LDA 모델을 사용하여 각 격자 점의 클래스 레이블을 예측합니다. 이 예측 결과는 1차원 배열로 반환됩니다.
#np.reshape(...): np.reshape() 함수를 사용하여 예측된 클래스 레이블 배열을 격자의 형태로 재구성합니다.
#이렇게 하면, 격자 상의 각 점에 해당하는 클래스 레이블을 얻을 수 있습니다. XX1.shape를 사용하여 원래 격자의 형태를 전달합니다.
 
#lda.predict() 함수는 1차원 배열 형태로 클래스 레이블을 반환합니다. 그러나 시각화를 위해 격자 상의 각 점에 대한 예측 결과를 2차원 형태로 표현하려 합니다.
#이 때 np.reshape() 함수에 XX1.shape를 전달하면, 함수는 주어진 예측 결과 배열을 XX1.shape와 동일한 모양의 2차원 배열로 변환하게 됩니다.
#이렇게 하면 원래 격자 형태와 동일한 모양의 2차원 배열이 생성되어, 예측된 클래스에 따라 격자 상의 영역을 색칠하는 데 사용할 수 있습니다.
cmap = mpl.colors.ListedColormap(sns.color_palette(["r", "g", "b"]).as_hex())
#cmap을 사용하여 색상 팔레트를 설정합니다. 이 줄은 mpl.colors.ListedColormap을 사용하여 세 개의 클래스에 대한 색상 팔레트를 생성하고, 이를 cmap 변수에 저장합니다
plt.contourf(XX1, XX2, YY, cmap=cmap, alpha=0.5)
#lda.predict() 함수를 격자점들에 적용하여, 각 격자점에 대응하는 클래스를 예측한 후 plt.contourf() 함수를 사용하여 이를 시각화하여 경계선 표현
plt.scatter(X1[:, 0], X1[:, 1], alpha=0.8, s=50, marker="o", color='r', label="class 1")
plt.scatter(X2[:, 0], X2[:, 1], alpha=0.8, s=50, marker="s", color='g', label="class 2")
plt.scatter(X3[:, 0], X3[:, 1], alpha=0.8, s=50, marker="x", color='b', label="class 3")
plt.xlim(x1min, x1max)
plt.ylim(x2min, x2max)
plt.xlabel("$x_1$")
plt.ylabel("$x_2$")
plt.legend()
plt.title("LDA Result")
plt.show()