핸즈온 머신러닝
코딩관련깃허브
Author : Duck Hyeun, Ryu
안녕하세요. 팀 언플(Team Unsolved Problem)의 Polar B 입니다!
저번 포스트에서는 앙상블 학습과 랜덤 포레스트를 함께 살펴보았습니다. 이번 포스트에선 차원 축소에 대해서 공부하겠습니다.
[Hands-On ML] Chapter 7. Ensemble Learning and Random Forests
그럼, 시작하겠습니다!
기본설정
# 공통
import numpy as np
import os
# 일관된 출력을 위해 유사난수 초기화
np.random.seed(42)
# 맷플롯립 설정
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
plt.rcParams['axes.labelsize'] = 14
plt.rcParams['xtick.labelsize'] = 12
plt.rcParams['ytick.labelsize'] = 12
# 한글출력
matplotlib.rc('font', family='NanumBarunGothic')
matplotlib.rcParams['axes.unicode_minus'] = False
# 작업할 디렉토리
PROJECT_ROOT_DIR = "C:\\Python\\MLPATH" ##파이썬 디렉토리 저장
CHAPTER_ID = "decision_trees"
IMAGE_PATH = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID)
def image_path(fig_id, image_path_k2h=IMAGE_PATH):
return os.path.join(image_path_k2h, fig_id) ##사진을 저장할 위치
훈련 샘플 각각이 너무 많은 특성을 가지고 있다면 훈련을 느리게 만들고 어렵게 만듭니다.(이것을 차원의 저주라고 합니다.) 따라서 특성 수를 잘 줄여서 훈련을 빠르게 하기위해 차원 축소를 합니다.
(예를 들면 MNIST 이미지에서 경계에 있는 픽셀은 제거해도 많은 정보를 잃지 않기 때문에 제거하면 훈련속도는 올라가고 결과는 양호하게 나옵니다.)
또한 데이터를 시각화 가능하게 만들어줍니다.
주의해야할 점은 속도가 빨라지는 대신 시스템의 성능이 조금 나빠질 수 있습니다. 또한 파이프 라인이 복잡하게 되어서 유지관리가 어려워집니다.
더 자세하게 알아보도록 하죠
고차원 공간에서는 많은 것이 저차원과 다르게 작동합니다.
2차원에서 단위 면적 안에 있는 점을 무작위로 선택하면 경계선에서 0.001이내에 위치할 가능성은 0.4%이지만 10000차원의 단위 면적을 가진 초입방체에서는 이 가능성이 99.999%보다 높습니다. 모든 점들이 경계와 가까이 있다는 뜻입니다.
3차원 큐브에서 임의의 두 점을 선택하면 평균거리는 대략 0.66입니다. 하지만 1,000,000차원의 초입방체에서 두 점을 무작위로 선택하면 거리는 428.25입니다.
모든 샘플이 서로 멀리 떨어져 있으므로 예측을 하더라도 불안정합니다. 이것을 차원의 저주라고 합니다. 이론적인 해결 방법은 훈련 샘플의 밀도가 충분히 높아질 때까지 훈련 세트의 크기를 키우는 것입니다. 하지만 특성이 100개만 가지고 있더라도 각 샘플을 서로 평균 0.1 이내에 위치시키려면 관측 가능한 우주에 있는 원자 수 모두를 합친 것보다 더 많은 훈련 샘플을 모아야 합니다.
간단히 말해 특성 수(차원 수)를 무작정 많이 늘인다고 해도 모델이 개선이 될지 불확실하고 데이터의 양만 기하학적으로 증가해서 훈련시간만 무지 길어집니다.
차원을 감소시키는 데는 두 가지 주요한 접근법인 투영과 매니폴드 학습이 있습니다.
실제 세상의 현실적인 문제들의 훈련 샘플들은 사실 고차원 공간에 균일하게 퍼져 있는 게 아닌 그 안의 저차원 부분공간에(혹은 그 근처에) 놓여있습니다.
위 그림을 보면 모든 훈련 샘플이 거의 평면 형태로 놓여 있습니다. 그리고 회색의 평면이 3차원 공간에 있는 저차원 즉 2차원 부분 공간입니다.
이 3차원 훈련 샘플들을 이 부분공간에 수직으로 투영하여 2차원 데이터셋을 얻는 것이 투영입니다.
하지만 투영이 최선의 방법은 아닙니다.
이렇게 공간에서 휘어 있는 데이터셋을 스위스롤 데이터셋이라 부릅니다. 밑의 왼쪽 그림은 그냥 투영시킨 것이고 오른쪽 그림은 스위스롤을 펼친 것입니다.
평면에 투영시키면 실제 데이터셋을 잘 보존을 못하지만 스위스 롤을 펼치면 뭉개지지 않고 데이터 셋을 얻을 수 있습니다.
이런 스위스롤을 2D 매니폴드라 합니다. 일반적으로 d차원 매니폴드는 국부적으로 d차원 초평면으로 보일 수 잇는 n차원 공간의 일부입니다.(d<n)
이런 매니폴드를 모델링하는 식으로 차원을 축소하는 알고리즘을 매니폴드 학습이라고 합니다. MNIST를 예로 들어 보겠습니다. 숫자 이미지들은 경계가 흰색이거나 선으로 연결되어 있는 등 어느 정도의 규칙이 있습니다. 이는 실제 모든 공간에 균일하게 퍼져 있는 것이 아닌 몇 가지 특성에 모여 있다는 뜻이고 이는 저차원 매니폴드로 압축할 수 있도록 도와줍니다.
하지만 언제나 매니폴드 학습을 한 것이 낫거나 좋은 솔루션이 되는 것은 아닙니다.
위 그림을 보면 첫 번째 행의 그림은 매니폴드 학습을 하면 쉽게 선형 분류가 가능 하지만 두 번째 행의 그림은 매니폴드를 하기 전이 더 분류하기 쉽습니다.
따라서 결론적으로 차원을 감소시키면 훈련속도는 빨라지지만 성능이 더 나아지는 것은 데이터셋에 달렸습니다.
데이터에서 가장 가까운 초평면을 정의한 다음, 데이터를 이 평면에 투영시키는 차원 축소 알고리즘입니다.
어떤 초평면을 선택해야 할까요? 밑의 그림을 보죠.
2차원에서 1차원 직선으로 투영을 시키고자 할 때는 분산이 잘 보존된 직선에 투영시키는 것이 좋습니다. 다르게 말하면 원본 데이터셋과 투영된 데이터셋 사이의 평균 제곱거리를 최소화하는 축으로 투영을 시키는 것이 좋습니다. 위의 그림에서는 첫 번째 실선이 분산을 잘 보존시킵니다.
차원이 더 높을 때 나머지 축은 어떻게 찾을까요?
일단 분산이 최대인 축을 찾고 난 후 다음 축은 첫번째 축에 직교하면서 분산이 최대한 보존되는 축을 찾습니다. 나머지 축도 이런 과정을 통해 찾습니다. $i$번째 축을 정의하는 단위 벡터를 $i$번째 주성분이라고 부릅니다.
실제로 어떻게 찾을까요? 바로 특잇값 분해를 사용해서 구합니다.
\(A = U · \Sigma ·V^t\)
$(A^t A)^t = A^tA$이므로 $A^t A$는 diagonalizable합니다.
$A^t A = UDU^t$ 여기서 $U$는 $A^t A$의 eigenvector를 열벡터로 가지는 행렬이고 D는 eigenvalue를 diagonal entry로 가지는 행렬입니다. 여기서 U는 orthgonal matrix입니다.
똑같이 $AA^t = VDV^t$입니다. 똑같이 $V$는 $AA^t$의 eigenvecor를 열벡터로 가지는 행렬이고 D는 eigenvalue를 diagonal entry로 가지는 행렬입니다. 여기서 $V$는 orthgonal matrix이고, D는 같은 행렬입니다.
$v_i$를 $U$의 $i$번째 열벡터, $\lambda$를 $v_i$에 해당하는 eigenvalue 라고 하면 $A^t A v_i = \lambda v_i$가 성립합니다.
양쪽의 앞에 $A$를 곱하면 $(AA^t)(Av_i) = \lambda (Av_i)$ 이 되므로 $Av_i$가 $AA^t$의 eigenvector인 것을 알 수 있습니다.
길이가 1인지도 알아보죠.
\(\Vert{Av_i}\Vert^2 = (Av_i)^t · (Av_i) = (v_i)^t · A^t · A · v_i\)
\(= (v_i)^t · \lambda_i · v_i = \lambda_i · (v_i)^t · v_i\)
\(= \lambda_i \text{where} \lambda_i \text{ is i th eigenvalue of } AA^t\)
길이가 1이 아니므로 $\sqrt{\lambda_i}$으로 $Av_i$를 나눈 벡터가 orthonormal eigenvector입니다.
$U$의 $i$ 번째 열벡터를 $u_i$라 하면
$u_i = \frac{1}{\sqrt{\lambda_i}} Av_i$
$\sqrt{\lambda_i} u_i = Av_i$
$AV = U \Sigma$
(여기서 $\Sigma$는 $AA^t$의 eigenvalue의 제곱근을 diagonal entry로 갖는 행렬)
따라서 $A = U \Sigma V^t$
그래서 $V$가 주성분 행렬이 됩니다.
코드를 통해 주성분을 구하는 예제를 보겠습니다.
np.random.seed(4)
m = 60
w1, w2 = 0.1, 0.3
noise = 0.1
## 예제 데이터 생성
angles = np.random.rand(m) * 3 * np.pi / 2 - 0.5
X = np.empty((m, 3))
X[:, 0] = np.cos(angles) + np.sin(angles)/2 + noise * np.random.randn(m) / 2
X[:, 1] = np.sin(angles) * 0.7 + noise * np.random.randn(m) / 2
X[:, 2] = X[:, 0] * w1 + X[:, 1] * w2 + noise * np.random.randn(m)
## 주성분 구하기
X_centered = X-X.mean(axis=0) ## axis = 0 는 열의 mean을 구하라는 뜻
U,s,Vt = np.linalg.svd(X_centered)
c1 = Vt.T[:,0]
c2 = Vt.T[:,1]
$d$차원의 하이퍼플레인의 주성분을 구했으면 투영을 시켜보겠습니다.
\(X_{d-proj} = X \cdot W_d\)
($W_d$는 $V$ 행렬의 첫 $d$번째 까지의 열벡터를 가지고 온 것)
코드로 구현해보면 아래와 같이 됩니다.
W2 = Vt.T[:,:2] # 2개의 주성분을 가져와
X2D = X_centered.dot(w2) # 2차원으로 투영
PCA는 사이킷런에 구현되어 있습니다.
from sklearn.decomposition import PCA
pca = PCA(n_components = 2)
X2D = pca.fit_transform(X)
## 사이킷런은 자동으로 데이터를 중앙에 맞춤
pca.components_.T[:,0]
## components_라는 객체변수에 주성분이 행벡터로 존재
array([-0.93636116, -0.29854881, -0.18465208])
주성분의 축을 따라 있는 데이터셋의 분산 비율을 나타냅니다.
pca.explained_variance_ratio_
array([0.84248607, 0.14631839])
차원 수를 제한하기 보다는 충분한 분산이 될 때까지 더해야 할 차원 수를 선택하는 것이 좋습니다. 물론 데이터 시각화를 위해 축소하는 경우는 제외입니다.
분산 95%를 유지하는 데 필요한 최소한의 차원 수 계산
pca = PCA()
pca.fit(X)
cumsum = np.cumsum(pca.explained_variance_ratio_) ## 모든 배열의 원소들을 누적으로 다 더한 수들을 반환
d = np.argmax(cumsum>=0.95)+1 ## argmax는 제일 큰 수의 index를 1차원 배열로 봤을 때의 index로 반환
pca = PCA(n_components= d) ## d를 주성분의 숫자로 지정
## 비율을 지정하는 방법
pca = PCA(n_components = 0.95) ## 보존하려는 분산의 비율을 0.95로 지정
X_reduced = pca.fit_transform(X)
PCA를 통해 얻을 수 있는 장점은 훈련 셋의 크기가 줄어든 다는 것입니다. 예를 들어 MNIST 데이터셋의 원래 특성은 784개이지만 95%의 분산을 유지하여 PCA를 적용시키면 특성은 150개 정도만 가지고 있습니다. 또한 다시 PCA를 반대로 변환하는 알고리즘도 있습니다. 일정량의 정보를 잃어버렸지만 원본 데이터셋과 비슷할 것입니다. 원본데이터와 재구성된 데이터 사이의 평균 제곱 거리를 재구성 오차(reconstruction error)라고 합니다.
from six.moves import urllib
from sklearn.datasets import fetch_mldata
# mnist = fetch_mldata('MNIST original')
mnist = fetch_mldata('MNIST original')
from sklearn.model_selection import train_test_split
X = mnist["data"]
y = mnist["target"]
X_train, X_test, y_train, y_test = train_test_split(X, y)
X_train
pca = PCA(n_components=154)
X_reduced = pca.fit_transform(X_train) ## X_reduced.shape : (52500, 154)
## 다시 돌리기
X_recovered = pca.inverse_transform(X_reduced) ## X_recovered.shape : (52500, 784)
역변환 공식은 다음과 같습니다.
\(X_{recoverd} = X_{d \cdot proj} \cdot W_d^T\)
PCA의 문제는 전체 훈련세트를 메모리에 올려야 한다는 것입니다. 하지만 점진적 PCA(Incremental PCA)(IPCA)가 개발되어서 미니 배치로 나눈 뒤 IPCA 알고리즘에 한 번에 하나씩 주입합니다. 이런 방식은 훈련 세트가 클 때 유용합니다.
IPCA를 한번 써보겠습니다.
from sklearn.decomposition import IncrementalPCA
n_batches = 100
inc_pca = IncrementalPCA(n_components=154) ## 154개의 특성을 가지는 IPCA
for X_batch in np.array_split(X_train, n_batches): ## split 합수는 X_trin을 100개의 배치로 나눠주는 함수
print(".", end="") # not shown in the book
inc_pca.partial_fit(X_batch) ## fit 함수 대신에 partial_fit 함수 사용
X_reduced = inc_pca.transform(X_train) ## X_reduced.shape : (52500, 154)
또 다른 방법은 memmap 파이썬 클래스를 사용해 하드 디스크의 이진 파일에 저장된 매우 큰 배열을 메모리에 들어 있는 것처럼 다루는 것입니다.
X_mm = np.memmap(filename, dtype="float32", mode="readonly", shape=(m, n))
batch_size = m // n_batches
inc_pca = IncrementalPCA(n_components=154, batch_size=batch_size)
inc_pca.fit(X_mm)
IncrementalPCA(batch_size=525, copy=True, n_components=154, whiten=False)
사이킷런에서 제공하는 알고리즘으로 첫 $d$개의 주성분에 대해서 근사값을 빠르게 찾습니다. $d$가 $n$보다 많이 작으면 앞선 알고리즘 보다 빨라집니다.
랜덤 PCA는 아래와 같이 사용할 수 있습니다.
## PCA파라미터의 svd_solver를 'radomized'로 지정
rnd_pca = PCA(n_components=154, svd_solver="randomized", random_state=42)
X_reduced = rnd_pca.fit_transform(X_train)
5장에서 배웠던 커널트릭을 PCA에도 적용해 복잡한 비선형 투영으로 차원 축소를 가능하게 하는 PCA입니다. 투영된 후에도 샘플의 군집을 유지하거나 꼬인 매니폴드에 가까운 데이터셋을 펼칠 때 유용합니다.
from sklearn.datasets import make_swiss_roll
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=42)
## RBF 커널로 KPCA를 적용
from sklearn.decomposition import KernelPCA
rbf_pca = KernelPCA(n_components = 2, kernel="rbf", gamma=0.04)
X_reduced = rbf_pca.fit_transform(X)
튜닝 방법에 두가지가 있습니다.
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=42)
y = t > 6.9
## 지도학습의 전처리 단계로서 성능을 기준으로 그리드 탐색을 한 예
## 로지스택 회귀를 하기 전에 kpca를 사용한 예
## 그리드 kpca pipeline에 대해 그리드 탐색을 실시하면 됨
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
clf = Pipeline([
("kpca", KernelPCA(n_components=2)),
("log_reg", LogisticRegression(solver='liblinear'))
]) ## kpca와 로지스틱 regression을 이은 파이프라인
param_grid = [{
"kpca__gamma": np.linspace(0.03, 0.05, 10),
"kpca__kernel": ["rbf", "sigmoid"]
}] ## 그리드 서치의 후보군 설정
grid_search = GridSearchCV(clf, param_grid, cv=3)
grid_search.fit(X, y)
print(grid_search.best_params_) ## best_params_ 변수에 저장
{'kpca__gamma': 0.043333333333333335, 'kpca__kernel': 'rbf'}
kPCA는 PCA처럼 역전시키면 데이터 포인트가 원본공간이 아닌 커널트릭을 이용한 특성 공간에 놓이게 됩니다. 이때 재구성된 포인트에 가깝게 매핑된 원본 공간의 포인트를 찾을 수 있습니다. 이를 재구성 원상(Pre-image)라고 합니다. 이 재구성 원상과 원본 샘플의 오차를 최소화하는 방향으로 최상의 하이퍼파라미터와 커널을 찾습니다. 재구성 원상을 찾는 방법 중 하나는 투영된 샘플을 훈련 세트로, 원본 샘플을 타깃으로 하는 지도학습 회귀모델을 훈련시키는 것입니다. 사이킷런에서는 IPCA의 변수 중 fit_inverse_transform을 True로 지정하면 이를 자동으로 수행합니다.
rbf_pca = KernelPCA(n_components = 2, kernel="rbf", gamma=0.0433, fit_inverse_transform=True)
X_reduced = rbf_pca.fit_transform(X)
X_preimage = rbf_pca.inverse_transform(X_reduced)
from sklearn.metrics import mean_squared_error
mean_squared_error(X, X_preimage)
32.78630879576608
투영에 의존하지 않는 매니폴드 학습이자 비선형 차원축소 방법입니다.
\(\hat{W} = argmax_W \sum_{i = 1}^{m} \Vert x^{(i)} - \sum_{j=1}^{m} w_{i,j}x^{(j)} \Vert ^2\)
\(\text{where}\)
\(\begin{cases}
w_{i,j} = 0, & \text{if $x^{(j)}$ is not one of the $k$ c.n. of $x^{(i)}$} \\
\sum_{j=1}^{m} w_{i,j} = 1, & \text{for i = 1,2, $\cdots$ ,m}
\end{cases}\)
($\hat{W}$는 지역 선형관계를 담고 있음. 두 번째 조건은 훈련 샘플에 대한 가중치를 정규화 한 것)
\(\hat{Z} = argmax_Z \sum_{i = 1}^{m} \Vert z^{(i)} - \sum_{j=1}^{m} \hat{w_{i,j}}z^{j} \Vert ^2\)
이를 코드로 구현해보겠습니다.
from sklearn.manifold import LocallyLinearEmbedding
lle = LocallyLinearEmbedding(n_components=2, n_neighbors=10, random_state=42)
## n_neigbors가 위 식의 k가 됨
X_reduced = lle.fit_transform(X)
다차원 스케일링 : 샘픍나의 거리를 보존하면서 차원을 축소
Isomap : 샘플을 가장 가까운 이웃과 연결하여 그래프를 만든 후 지오데식 거리를 유지하면서 차원 축소
t-SNE(t-Distributed Stochastic Neighbor EMbedding) : 비슷한 샘플은 가까이, 비슷하지 않은 샘플은 멀리 떨어지도록 하면서 차원 축소 , 시각화에 많이 사용, 특히 고차원 공간에 있는 샘플의 군집을 시각화 할 때 사용
선형 판별 분석(Linear Discrimininat Analysis)(LDA) : 분류 알고리즘 이지만 훈련 과정에서 클래스 사이를 장 구분하는 축을 학습 하고 이 축을 데이터가 투영되는 초평면을 정의하는데 사용가능. 장점은 투영을 통해 클래스를 멀리 떨어지게 유지시키므로 SVM 분류 같은 다른 분류알고리즘을 적용하기 전에 차원 축소시키는데 좋음
저희는 이번에 차원 축소에 대해 알아보았습니다. 다음 단원부터는 이제 신경망과 딥러닝을 배우기 앞서 텐서플로를 알아보는 포스트로 찾아오겠습니다.