다운로드
작성자: admin 작성일시: 2016-06-16 23:47:29 조회수: 9935 다운로드: 416
카테고리: 머신 러닝 태그목록:

PCA

PCA(Principal Component Analysis)는 주성분 분석이라고도 하며 다차원의 정보가 주어졌을 때 원래의 데이터와 가장 비슷하면서 원래의 차원보다 낮은 차원의 데이터를 찾아내는 방법이다. 차원축소(dimension reduction)라고도 한다.

차원축소의 원리는 다음과 같다.

$N$개의 $M$차원 데이터가 있으면 보통 그 데이터들은 서로 다른 값을 가진다. 하지만 서로 다른 데이터가 아무렇게나 서로 다르지는 않다. 데이터가 생겨난 물리적, 사회적 원인에 의해 "다른" 데이터도 "어떻게 다른지" 규칙이 존재하는 경우가 있다. 이 규칙을 찾아내는 것이 PCA이다.

붓꽃 데이터의 차원축소

Scikit-Learn의 붓꽃 데이터 중에서 10송이의 데이터, 즉 10개의 표본만 선택하여 꽃잎의 길이와 꽃입의 폭 데이터를 그래프로 보이면 다음과 같다.

In [1]:
from sklearn.datasets import load_iris
iris = load_iris()
N = 10  # 앞의 10송이만 선택
X = iris.data[:N, :2]  # 꽃잎의 길와 꽃잎의 폭만 선택

plt.plot(X.T, 'o:')
plt.xticks(range(4), ["꽃잎의 길이", "꽃잎의 폭"])
plt.xlim(-0.5, 2)
plt.ylim(2.5, 6)
plt.title("붓꽃의 크기 특성")
plt.legend(["표본 {}".format(i + 1) for i in range(N)])
plt.show()
In [2]:
plt.figure(figsize=(8, 8))
ax = sns.scatterplot(0, 1, data=pd.DataFrame(X), s=100, color=".2", marker="s")
for i in range(N):
    ax.text(X[i, 0] - 0.05, X[i, 1] + 0.03, "표본 {}".format(i + 1))
plt.xlabel("꽃잎의 길이")
plt.ylabel("꽃잎의 폭")
plt.title("붓꽃의 크기 특성 (2차원 표시)")
plt.axis("equal")
plt.show()

이 10송이의 표본은 꽃잎의 길와 폭이 제각각이지만 그 값에는 공통적인 특징이 있다. 꽃잎의 길이가 크면 꽃잎의 폭도 커지며 그 비율은 거의 일정하다. 그 이유는 (꽃잎의 길이, 꽃잎의 폭)이라는 2차원 측정 데이터는 사실 "꽃의 크기"라는 보다 근본적인 데이터가 두 개의 다른 형태로 표현된 것에 지나지 않기 때문이다. 바로 측정되지는 않지만 측정된 데이터의 기저에 숨어서 측정 데이터를 결정짓는 데이터를 잠재변수(latent variable)이라고 부른다.

PCA에서는 잠재변수와 측정 데이터가 선형적인 관계로 연결되어 있다고 가정한다. 즉, $i$번째 표본의 측정 데이터 벡터 $x_i$의 각 원소를 선형조합하면 그 뒤에 숨은 $i$번째 표본의 잠재변수 $u_i$의 값을 계산할 수 있다고 가정한다. 이를 수식으로 나타내면 다음과 같다.

$$ u_i = w^Tx_i $$

이 식에서 $w$는 측정 데이터 벡터의 각 원소를 조합할 가중치 벡터이다.

붓꽃의 예에서는 꽃잎의 길이와 꽃잎의 폭을 선형조합하여 꽃의 크기를 나타내는 어떤 값을 찾은 것이라고 생각할 수 있다.

$$ u_i = w_1 x_{i,1} + w_2 x_{i,2} $$

스포츠 선수의 경우에는 경기 중 발생한 다양한 기록을 선형조합하여 그 선수의 기량을 나타내는 점수를 계산하는 방법을 많이 사용한다. 일례로 미식축구의 쿼터백의 경우에는 패서 레이트(passer rate)라는 점수를 이용하여 선수를 평가하는 경우가 있는데 이 값은 패스 성공 횟수(completions), 총 패싱 야드(passing yards), 터치다운 횟수(touchdowns), 인터셉션 횟수(interceptions)를 각각 패스 시도 횟수(attempts)로 나눈 값을 선형조합하여 계산한다.

$$ \text{passer rating} = 5 \cdot {\text{completions} \over \text{attempts}} + 0.25 \cdot {\text{passing yards} \over \text{attempts}} + 20 \cdot {\text{touchdowns} \over \text{attempts}} - 25 \cdot {\text{interceptions} \over \text{attempts}} + 0.125 $$

선수의 실력이라고 하는 잠재변수의 값이 이 4개 수치의 선형조합으로 표현될 것이라고 가정한 것이다. 차원축소의 관점에서 보면 4차원의 데이터를 1차원으로 축소한 것이다.

차원축소와 투영

차원축소문제는 다차원 벡터를 더 낮은 차원의 벡터공간에 투영하는 문제로 생각하여 풀 수 있다. 즉, 특이분해에서 살펴본 로우-랭크 근사(low-rank approximation) 문제가 된다. 이 문제는 다음과 같이 서술할 수 있다.

$N$개의 $M$차원 데이터 벡터 $x_1, x_2, \cdots, x_N$를 정규직교인 기저벡터 $w_1, w_2, \cdots, w_K$로 이루어진 $K$차원 벡터공간으로 투영하여 가장 비슷한 $N$개의 $K$차원 벡터 $x^{\Vert w}_1, x^{\Vert w}_2, \cdots, x^{\Vert w}_N$를 만들기 위한 정규직교 기저벡터 $w_1, w_2, \cdots, w_K$를 찾는다.

다만 원래의 로우-랭크 근사문제와 달리 근사 성능을 높이기 위해 직선이 원점을 지나야한다는 제한조건을 없애야 한다. 따라서 문제는 다음과 같이 바뀐다.

$N$개의 $M$차원 데이터 벡터 $x_1, x_2, \cdots, x_N$에 대해 어떤 상수 벡터 $x_0$를 뺀 데이터 벡터 $x_1-x_0, x_2-x_0, \cdots, x_N-x_0$를 정규직교인 기저벡터 $w_1, w_2, \cdots, w_K$로 이루어진 $K$차원 벡터공간으로 투영하여 가장 비슷한 $N$개의 $K$차원 벡터 $x^{\Vert w}_1, x^{\Vert w}_2, \cdots, x^{\Vert w}_N$를 만들기 위한 정규직교 기저벡터 $w_1, w_2, \cdots, w_K$와 상수 벡터 $x_0$를 찾는다.

$N$개의 데이터를 1차원 직선에 투영하는 문제라고 하면 원점을 지나는 직선을 찾는게 아니라 원점이 아닌 어떤 점 $x_0$을 지나는 직선을 찾는 문제로 바꾼 것이다.

이 문제의 답은 다음과 같다.

$x_0$는 데이터 벡터 $x_1, x_2, \cdots, x_N$의 평균벡터이고 $w_1, w_2, \cdots, w_K$는 가장 큰 $K$개의 특이값에 대응하는 오른쪽 특이벡터 $v_1, v_2, \cdots, v_K$이다.

Scikit-Learn 의 PCA 기능

Scikit-Learn 의 decomposition 서브패키지는 PCA분석을 위한 PCA 클래스를 제공한다. 사용법은 다음과 같다.

  • 입력 인수:

    • n_components : 정수
  • 메서드:

    • fit_transform : 특징행렬을 낮은 차원의 근사행렬로 변환
    • inverse_transform : 변환된 근사행렬을 원래의 차원으로 복귀
  • 속성:

    • mean_ : 평균 벡터
    • components_ : 주성분 벡터

이 명령으로 붓꽃 데이터를 1차원 근사하면 다음과 같다.

X_low는 1차원 근사 데이터의 집합이고 X2는 다시 2차원으로 복귀한 근사 데이터의 집합이다.

In [3]:
from sklearn.decomposition import PCA

pca1 = PCA(n_components=1)
X_low = pca1.fit_transform(X)
X2 = pca1.inverse_transform(X_low)
In [4]:
plt.figure(figsize=(7, 7))
ax = sns.scatterplot(0, 1, data=pd.DataFrame(X), s=100, color=".2", marker="s")
for i in range(N):
    d = 0.03 if X[i, 1] > X2[i, 1] else -0.04
    ax.text(X[i, 0] - 0.065, X[i, 1] + d, "sample {}".format(i))
    plt.plot([X[i, 0], X2[i, 0]], [X[i, 1], X2[i, 1]], "k--")
plt.plot(X2[:, 0], X2[:, 1], "o-", markersize=10)
plt.plot(X[:, 0].mean(), X[:, 1].mean(), markersize=10, marker="D")
plt.axvline(X[:, 0].mean(), c='r')
plt.axhline(X[:, 1].mean(), c='r')
plt.grid(False)
plt.xlabel("꽃잎의 길이")
plt.ylabel("꽃잎의 폭")
plt.title("Iris 데이터의 1차원 차원축소")
plt.axis("equal")
plt.show()

가장 근사값을 만드는 투영벡터는 (0.68, 0.73)이다.

In [5]:
pca1.components_
Out:
array([[0.68305029, 0.73037134]])

이 값은 평균을 제거한 특징행렬의 첫번째 오른쪽 특이벡터 또는 그 행렬의 공분산행렬의 첫번째(가장 큰 고윳값에 대응하는) 고유벡터에 해당한다. 고유벡터의 부호 즉 방향은 반대가 될 수도 있다.

NumPy로 첫번째 오른쪽 특이벡터를 구하면 (0.68, 0.73)임을 알 수 있다.

In [6]:
X0 = X - X.mean(axis=0)
U, S, VT = np.linalg.svd(X0)
VT
Out:
array([[-0.68305029, -0.73037134],
       [-0.73037134,  0.68305029]])
In [7]:
VT[:, 0]
Out:
array([-0.68305029, -0.73037134])

고유값 분해를 할 때는 NumPy가 고유값의 순서에 따른 정렬을 해주지 않으므로 사용자가 정렬해야 한다.

In [8]:
XCOV = X0.T.dot(X0)
W, V = np.linalg.eig(XCOV)
In [9]:
W
Out:
array([0.17107711, 1.44192289])
In [10]:
V
Out:
array([[-0.73037134, -0.68305029],
       [ 0.68305029, -0.73037134]])
In [11]:
V[:, np.argmax(W)]
Out:
array([-0.68305029, -0.73037134])

주식 가격의 PCA

PCA는 다양한 분야에서 사용된다. 이번에는 금융분야에서 어떻게 쓰이는지 알아보자. 우선 미국(US), 일본(JP), 유럽(EZ), 한국(KR)의 과거 20년간 주가를 살펴보자.

In [12]:
pd.core.common.is_list_like = pd.api.types.is_list_like
import pandas_datareader.data as web
import datetime

symbols = [
    "SPASTT01USM661N", 
    "SPASTT01JPM661N", 
    "SPASTT01EZM661N",
    "SPASTT01KRM661N",
]
data = pd.DataFrame()
for sym in symbols:
    data[sym] = web.DataReader(sym, data_source='fred', 
                               start=datetime.datetime(1998, 1, 1),
                               end=datetime.datetime(2017, 12, 31))[sym]
data.columns = ["US", "JP", "EZ", "KR"]
data = data / data.iloc[0] * 100
In [13]:
data.plot()
plt.title("세계 주요국의 20년간의 주가")
plt.show()

앞의 차트는 시작시점의 주가가 100이 되도록 크기를 조정하였다. 이 데이터로부터 연간 주식수익률 데이터를 구하면 다음처럼 $20 \times 4$ 크기의 특징행렬을 구할 수 있다.

In [14]:
df = ((data.pct_change() + 1).resample("A").prod() - 1).T * 100
df.iloc[:, :5]
Out:
DATE 1998-12-31 00:00:00 1999-12-31 00:00:00 2000-12-31 00:00:00 2001-12-31 00:00:00 2002-12-31 00:00:00
US 14.249290 10.800392 1.094171 -9.638213 -17.680773
JP -8.567915 47.802619 -18.365577 -23.827406 -16.923968
EZ 21.308040 36.872314 1.375330 -21.857080 -30.647772
KR 10.411582 87.623876 -46.975114 27.644005 4.448180

이 연간 주식수익률 데이터를 나타내면 다음과 같다. 붓꽃의 경우보다 (2차원에서 20차원으로) 차원은 증가했지만 각 표본이 가지는 측정 데이터 벡터의 모양(비율)이 비슷하다는 것을 알 수 있다. 붓꽃이 측정 데이터의 값을 결정하는 "크기"라는 잠재변수를 가지고 있는 것처럼 각 나라의 수익률은 "경제적 요인"이라고 하는 공통된 잠재변수에 의해서 결정되기 때문이다.

In [15]:
df.T.plot()
plt.title("주요국의 과거 20년간 연간수익률")
plt.xticks(df.columns)
plt.show()