차원 축소란?
주성분 분석(PCA)
주성분 분석(principal component analysis)은 가장 인기 있는 차원 축소 알고리즘이다. 먼저 데이터에 가장 가까운 초평면을 정의한 다음, 데이터를 이 평면에 투영시킨다.
[보다 자세한 내용]
분산 보존
저차원의 초평면에 훈련 세트를 투영하기 전에 먼저 올바른 초평면을 선택해야 한다.
왼쪽 그래프: 간단한 2D 데이터셋이 세 개의 축과 함께 표현
오른쪽 그래프: 데이터셋이 각 축에 투영된 결과
실선에 투영된 것: 분산을 최대로 보존
점선에 투영된 것: 분산을 매우 적게 유지
파선에 투영된 것: 분산을 중간정도로 유지
▶ 다른 방향으로 투영하는 것보다 분산이 최대로 보존되는 축을 선택하는 것이 정보가 가장 적게 손실되므로 합리적이며, 이를 다른 방식으로 설명하면 원본 데이터셋과 투영된 것 사이의 평균 제곱 거리를 최소화하는 축을 선택하는 것이 정보가 가장 적게 손실된다.
PCA는 훈련 세트에서 분산이 최대인 축을 찾으며, 위의 그래프로보면 실선이 분산이 최대인 축이다.
또한, 첫 번째 축에 직교하고 남은 분산을 최대한 보존하는 두 번째 축을 찾는데, 이것은 점선이 된다.
고차원 데이터셋이라면 PCA는 이전의 두 축에 직교하는 세 번째 축을 찾으며 데이터셋에 있는 차원의 수만큼 네 번째, 다섯 번째, ... n 번째 축을 찾는다. 그리고 i번째 축을 이 데이터의 i번째 주성분이라고 부른다.
훈련 세트의 주성분은 특잇값 분해(SVD) 라는 표준 행렬 분해 기술로 찾을 수 있다.
[SVD 관련 글]
다음은 넘파이의 svd() 함수를 사용해 상단의 그림에 있는 3D 훈련 세트의 모든 주성분을 구한 후 처음 두 개의 PC를 정의하는 두 개의 단위 벡터를 추출한다.
import numpy as np
X = np.zeros((m, 3))
X_centered = X - X.mean(axis=0)
U, s, Vt = np.linalg.svd(X_centered)
c1 = Vt[0]
c2 = Vt[1]
※ PCA는 데이터셋의 평균이 0이라고 가정하기 때문에, 사이킷런의 PCA 파이썬 클래스는 이 작업을 대신 처리해준다. 만약 PCA를 직접 구현하거나 다른 라이브러리를 사용한다면 먼저 데이터를 원점에 맞춰야 한다.
d차원으로 투영하기
주성분을 모두 추출해냈다면 처음 d개의 주성분으로 정의한 초평면에 투영하여 데이터셋의 차원을 d차원으로 축소시킬 수 있고, 이 초평면은 분산을 가능한 한 최대로 보존하는 투영임을 보장한다.
다음 코드는 첫 두 개의 주성분으로 정의된 평면에 훈련 세트를 투영한다.
W2 = Vt[:2].T
X2D = X_centered @ W2
▶ PCA 변환이 되었다.
★ '@' 의미는?
PCA(주성분 분석)의 맥락에서 '@' 기호는 본질적으로 PCA 자체와 관련이 없다. 그러나 Python과 같이 프로그래밍에서 PCA를 사용하는 경우 @는 행렬 곱셈 연산자를 나타낼 수 있다.
아래는 사이킷런의 PCA 모델을 사용해 데이터셋의 차원을 2로 줄이는 코드이다.
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X2D = pca.fit_transform(X)
설명된 분산의 비율
explained_variance_ratio_ 변수에 저장된 주성분의 설명된 분산의 비율은 각 주성분의 축을 따라 있는 데이터셋의 분산 비율을 나타내며, 매우 유용한 정보이다.
pca.explained_variance_ratio_
▶ 앞서 표현한 3D 데이터셋의 처음 두 주성분에 대한 설명된 분산의 비율이며, 데이터셋 분산의 76%가 첫 번재 PC를 따라 놓여 있고 15%가 두 번째 PC를 따라 놓여있음을 알려준다. 세 번째는 9% 미만의 아주 적은 양의 정보가 들어있다.
적절한 차원 수 선택
1) 충분한 분산이 될 때까지 더해야 할 차원 수를 선택
축소할 차원 수를 임의로 정하기보다는 충분한 분산이 될 때까지 더해야 할 차원 수를 선택하는 것이 간단하다.
(데이터 시각화를 위해 차원을 축소하는 경우에는 차원을 2개나 3개로 줄인다)
아래는 MNIST 데이터셋을 로드하고 분할한 다음, 차원을 줄이지 않고 PCA를 수행한 뒤, 훈련 집합의 충분한 분산 (95%)을 보존하는 데 필요한 최소 차원 수를 계산한다.
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', as_frame=False)
X_train, y_train = mnist.data[:60_000], mnist.target[:60_000]
X_test, y_test = mnist.data[60_000:], mnist.target[60_000:]
pca = PCA()
pca.fit(X_train)
cumsum = np.cumsum(pca.explained_variance_ratio_)
d = np.argmax(cumsum >= 0.95) + 1
※ np.cumsum 함수는 입력 배열의 원소를 차례대로 누적한 배열을 반환한다.
그런 다음 n_components=d 로 설정하여 PCA를 재실행
pca = PCA(n_components=0.95)
X_reduced = pca.fit_transform(X_train)
실제 주성분 개수는 훈련 중에 결정되며 n_components_ 속성에 저장된다.
pca.n_components_
2) 설명된 분산을 차원 수에 대한 함수로 그리는 것
또 다른 방법은 설명된 분산을 차원 수에 대한 함수로 그리는 것이며, 그냥 cumsum을 그래프로 그리면 된다.
일반적으로 이 그래프에는 설명의 분산의 빠른 성장이 멈추는 변곡점이 있으며, 여기서는 차원을 약 100으로 축소해도 설명된 분산을 크게 손해보지 않을 것이다.
plt.figure(figsize=(6, 4))
plt.plot(cumsum, linewidth=3)
plt.axis([0, 400, 0, 1])
plt.xlabel("차원 수")
plt.ylabel("설명된 분산")
plt.plot([d, d], [0, 0.95], "k:")
plt.plot([0, d], [0.95, 0.95], "k:")
plt.plot(d, 0.95, "ko")
plt.annotate("Elbow", xy=(65, 0.85), xytext=(70, 0.7),
arrowprops=dict(arrowstyle="->"))
plt.grid(True)
plt.show()
3) 최적의 하이퍼파라미터 조합을 찾아 튜닝
지도 학습 작업(예. 분류)의 전처리 단계로 차원 축소를 사용하는 경우, 다른 하이퍼파라미터와 마찬가지로 차원 수를 튜닝할 수 있다.
차원 수 튜닝의 예
아래 코드는 두 단계로 구성된 파이프라인을 생성한다.
- PCA를 사용하여 차원을 줄인 다음 랜덤 포레스트를 사용하여 분류 수행
- RandomizedSearchCV를 사용하여 PCA와 랜덤 포레스트 분류기에 잘 맞는 하이퍼파라미터 조합을 찾는다.
여기서는 두 개의 하이퍼파라미터를 튜닝하기 위해 1,000개의 샘플에서 반복 횟수 10회로 설정하여 간단한 검색을 수행해본다.
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import RandomizedSearchCV
from sklearn.pipeline import make_pipeline
clf = make_pipeline(PCA(random_state=42),
RandomForestClassifier(random_state=42))
param_distrib = {
"pca__n_components": np.arange(10, 80),
"randomforestclassifier__n_estimators": np.arange(50, 500)
}
rnd_search = RandomizedSearchCV(clf, param_distrib, n_iter=10, cv=3,
random_state=42)
rnd_search.fit(X_train[:1000], y_train[:1000])
# 최상의 하이퍼 파라미터
print(rnd_search.best_params_)
▶ 이는 784개 차원의 데이터셋을 57개 차원으로 줄였다.
선형 모델인 SGDClassifier를 사용하면
from sklearn.linear_model import SGDClassifier
from sklearn.model_selection import GridSearchCV
clf = make_pipeline(PCA(random_state=42), SGDClassifier())
param_grid = {"pca__n_components": np.arange(10, 80)}
grid_search = GridSearchCV(clf, param_grid, cv=3)
grid_search.fit(X_train[:1000], y_train[:1000])
# 최상의 하이퍼 파라미터
print(grid_search.best_params_)
▶ 보다 더 많은 67개 차원을 보존해야 한다.
압축을 위한 PCA (역변환)
차원 축소 후 훈련 세트는 훨씬 적은 공간을 차지한다.
예를 들어 앞서 진행했던 MNIST 데이터셋에 95%의 분산을 유지하도록 한 후 PCA를 적용하면 784개의 특성 중에서 154개의 특성만 남게 된다. 따라서 데이터셋의 크기는 원본의 20%이하로 줄지만 분산은 5%만 손실되었다.
이는 상당한 압축률이고 이런 크기 축소는 분류 알고리즘의 속도를 크게 높일 수 있다.
또한 압축된 데이터셋에 PCA 투영의 변환을 반대로 적용하여 원래의 차원(784개)으로 되돌릴 수도 있다. 물론 투영에서 일정량의 정보를 (유실된 5%의 분산) 잃어버렸기 때문에 원본 데이터셋을 얻을 수는 없으나, 원본 데이터와 매우 비슷할 것이다.
※ 재구성 오차: 원본 데이터와 압축 후 원산 복구한 데이터(재구성 데이터) 사이의 평균 제곱 거리
이는 inverse_transform() 메서드를 사용하면 축소된 MNIST 데이터 집합을 다시 복원할 수 있다.
X_recovered = pca.inverse_transform(X_reduced)
랜덤 PCA
svd_solver 매개변수를 'randomized'로 지정하면 사이킷런은 랜덤 PCA라 부르는 확률적 알고리즘을 사용해 처음 d개의 주성분에 대한 근삿값을 빠르게 찾는다.
rnd_pca = PCA(n_components=154, svd_solver='randomized', random_state=42)
X_reduced = rnd_pca.fit_transform(X_train)
- svd_slover의 기본값은 'auto'
- max(m, n) > 500이고 n_components가 min(m, n)의 80%보다 작은 정수인 경우 사이킷런은 자동으로 랜덤 PCA 알고리즘 사용
- max(m, n) > 500이고 n_components가 min(m, n)의 80%보다 작은 정수가 아닐 경우 완전한 SVD 방식 사용
- 상단의 코드는 154<0.8 * 784 이므로 svd_solver='randomized' 매개변수를 제거하더라도 랜덤 PCA 알고리즘 사용
- 좀 더 정확한 결과를 얻기 위해 완전한 SVD를 사용하도록 하려면 svd_solver 매개변수를 'full'로 지정
점진적 PCA (incremental PCA)
PCA 구현의 문제는 SVD 알고리즘을 실행하기 위해 전체 훈련 세트를 메모리에 올려야 한다는 것이었으나, 점진적 PCA (IPCA) 알고리즘이 개발되었다. 이는 훈련 세트를 미니배치로 나눈 뒤 IPCA 알고리즘에 한 번에 하나씩 주입하는데, 이런 방식은 훈련 세트가 클 때 유용하고 새로운 데이터가 준비되는 대로 실시간으로 (온라인으로) PCA를 적용할 수 있다.
아래는 MNIST 훈련 세트를 넘파이의 array_split() 함수를 사용해 100개의 미니 배치로 나누고 사이킷런의 IncrementalPCA 파이썬 클래스에 주입하여 MNIST 데이터셋의 차원을 이전과 같은 154개로 줄인다.
전체 훈련 세트를 사용하는 fit() 메서드가 아니라 partial_fit() 메서드를 미니배치마다 호출해야 한다.
from sklearn.decomposition import IncrementalPCA
n_batches = 100
inc_pca = IncrementalPCA(n_components=154)
for X_batch in np.array_split(X_train, n_batches):
inc_pca.partial_fit(X_batch)
X_reduced = inc_pca.transform(X_train)
또는 디스크의 이진 파일에 저장된 대규모 배열을 마치 메모리에 있는 것처럼 조작할 수 있는 넘파이 memmap 클래스를 사용할 수 있는데, 이 클래스는 필요할 때 원하는 데이터만 메모리에 로드한다.
먼저 메모리 매핑된 파일(memmap)을 생성하고 MNIST 훈련 세트를 복사한 다음 flush()를 호출하여 캐시에 남아 있는 모든 데이터가 디스크에 저장되도록 해본다. (실제 환경에서 X_train은 일반적으로 메모리에 맞지 않으므로 한 청크 씩 로드하고 각 청크를 memmap 배열의 적절한 위치에 저장)
filename = 'my_mnist.mmap'
X_mmap = np.memmap(filename, dtype='float32', mode='write', shape=X_train.shape)
X_mmap[:] = X_train # 반복을 사용해 데이터를 한 청크씩 저장
X_mmap.flush()
다음으로 memmap 파일을 로드하고 일반적인 넘파이 배열처럼 사용할 수 있다.
IncrementalPCA를 사용해 차원을 줄이는데, 이 알고리즘은 특정 순간에 배열의 작은 부분만 사용하기 때문에 메모리 부족 문제가 일어나지 않으므로 partial_fit() 대신 일반적인 fit() 메서드를 호출할 수 있어 매우 편리하다.
X_mmap = np.memmap(filename, dtype='float32', mode='readonly').reshape(-1, 784)
batch_size = X_mmap.shape[0] // n_batches
inc_pca = IncrementalPCA(n_components=154, batch_size=batch_size)
inc_pca.fit(X_mmap)
다음 내용
[출처]
핸즈 온 머신러닝
'[파이썬 Projects] > <파이썬 머신러닝>' 카테고리의 다른 글
[머신러닝] 머신러닝 기반 분석 모형 선정 (1) | 2024.11.16 |
---|---|
[머신러닝] 차원 축소: 랜덤 투영, 지역 선형 임베딩 (3) | 2024.11.15 |
[머신러닝] 앙상블 : AdaBoost (2) | 2024.11.15 |
[머신러닝] 앙상블: 투표 기반 분류기, 배깅과 페이스팅 (0) | 2024.11.15 |
[머신러닝] 결정 트리 (추가) (1) | 2024.11.14 |