5 minute read

차원의 저주

  • 머신러닝에서 훈련 샘플이 수백, 수천만개 특성을 가지고 있으면 훈련이 느리고, 좋은 결과값을 얻기 어렵게 되는 현상.

  • 학습 데이터 숫자가 차원 수보다 적어져 모델의 성능이 저하되는 현상.

    • 차원의 갯수가 늘어날수록, 개별 차원 내에서 학습할 데이터 수가 적어진다.
  • 변수가 증가한다고 반드시 차원의 저주가 발생하는것이 아닌, 관측치보다 변수 수가 많아지는 경우에 차원의 저주 문제가 발생하는것.

해결 방법

  1. 데이터셋의 크기 증가.

    • 훈련 샘플의 밀도가 높아질때까지 데이터를 모아 훈련 세트의 크기를 키우는 것.

    • 개인적인 생각으로는 머신러닝의 대부분 문제에 이걸 가져다 대면 풀리는, 치트키같은 존재라고 생각한다.

    • 다만 이 방법의 단점이 늘 그러하듯, 훈련 샘플을 위한 데이터의 확보가 힘들다.

      • 특히 차원의 갯수가 늘어날수록 밀도의 유지에 필요한 데이터의 양이 기하급수적으로 늘어난다.
  2. PCA 등의 차원 축소 기법을 사용

    • raw data에 비해 훈련 속도가 빨라지고, 시각화 역시 편해진다.

    • 다만, 일부 정보가 유실될수 있으므로 raw data로 훈련을 해보고 훈련 시간 체크후 사용할것을 권장함.

차원 축소 기법

  • 데이터의 차원을 축소하는 기법인 투영(projection), 매니폴드 학습(manifold learning), 대표적인 차원 축소 알고리즘인 주성분분석(PCA)가 있다.

투영(projection)

  • 학습 데이터셋은 고차원 공간에서 저차원 부분 공간에 위치한다.

    • 고차원 데이터 특성중 일부 특성으로 데이터를 표현할수 있음을 의미함.

    예시 이미지

    • 위 이미지처럼 3차원 공간의 데이터를 2차원상으로 투영시켜 차원을 축소하는 기법이라 투영(projection)기법이다.

매니폴드 학습(manifold learning)

  • 매니폴드(manifold, 다양체)

    • 위상수학과 기하학에서, 다양체는 국소적으로 유클리드 공간과 닮은 위상 공간이다.
    • 즉, 국소적으로는 유클리드 공간과 구별할 수 없으나, 대역적으로 독특한 위상수학적 구조를 가질 수 있다.
    • (출처 : 위키피디아)

    예시이미지2

    • 위 이미지는 스위스롤이라 불리는 대표적인 2D-매니폴드이다.

    • 해당 이미지를 단순히 2차원으로 투영하게 된다면, 중간에 있는것과 같은 의미불명의 데이터를 얻게된다.

    • 하지만 3차원에서 보았을때 스위스 롤은 말려있는 2차원 도형(2D 매니폴드)이므로, 말려있는 2차원 도형을 펴서 투영하게 된다면 오른쪽처럼 명확한 데이터를 얻을수 있게 된다.

    • 이런식으로, 고차원에서 보았을때 매니폴드가 나타난다면 이상적인 데이터를 산출해낼수 있다.

  • 이러한 매니폴드를 모델링 하는 방식으로 대부분의 차원 축소 알고리즘이 작동하며, 이를 매니폴드 학습이라고 한다.

  • 매니폴드 학습은 매니폴드 가정 또는 매니폴드 가설에 의해 고차원인 실제 데이터셋이 더 낮은 저차원 매니폴드에 가깝게 놓여있다고 가정한다.

주성분분석(PCA)

  • 가장 대표적인 차원 축소 알고리즘이다.

  • 데이터에 가장 가까운 초평면을 구하고, 데이터를 이 초평면에 투영한다.

분산 보존

  • 초평면에 데이터를 투영하기 전에 데이터를 보존하기 가장 적절한 초평면을 찾아야 한다.

  • PCA는 데이터의 분산이 최대화 되는 축을 찾는다.

    • 즉, 원본 데이터셋과 투영된 데이터셋 사이의 평균제곱거리를 최소화 하는 축을 찾는다.

예시이미지3

  • c1축에 투영한 데이터는 c2에 투영한 데이터보다 분산이 크다(즉, 데이터가 더 잘 보존되었다)

주성분

  • PCA는 다음과 같은 단계로 이루어진다

    1. 학습 데이터셋에서 분산이 최대인 축(axis)을 찾는다.

    2. 이렇게 찾은 첫번째 축에서 직교하면서 분산이 최대인 두번째 축을 찾는다.

    3. 첫번째와 두번째 축에 직교하면서 분산을 최대로 보존하는 세번째 축을 찾는다.

    4. 1~3의 방법을 반복하며 데이터셋의 차원수(특성 수)만큼의 축을 찾는다.

  • 위 과정에서 i번째 축을 정의하는 단위벡터를 i번째 주성분(PC, Principal Component)이라고 한다.

구하는 방법

공분산(Covariance)

  • 두개의 특성 또는 변수 간의 상관정도를 나타낸 값.

  • 이론적인 면에 대해서는 정리가 잘 되어있는 블로그가 있어 링크를 첨부한다.

  • 공분산과 PCA의 상관관계
    • 분산을 최대로 만드는 축 = 공분산의 고유벡터

    • 공분산의 고유벡터의 열벡터 = 주성분(PC)

  • 고유값의 설명된 분산비율(explained variance ratio)

    • PCA기법은 데이터셋을 새로운 특성부분으로 압축하므로, 가장 많은 정보(=분산)을 가진 고유벡터(=주성분) 일부만 선택하게 된다.

    • 고유값의 크기에 따라 정렬하였을때, 최상위 k개의 분산비율을 고유값의 설명된 분산비율(explained variance ratio)이라고 한다.

      • 즉, 각각의 주성분 벡터가 이루는 축에 투영했을때 결과의 분산 비율이다.
    • 전체 고유값의 합에서 해당 고유값의 비율을 나타내는 것으로 구한다.

    • 결론 : 해당 고유값이 얼마나 정보를 잘 보존하는지에 대한 비율이라 생각하면 좋다.

Scikit-learn에서의 PCA 계산

  • 총 세가지 방법이 존재하는데, 요약하면 다음과 같다.

    • eigen-decomposer를 이용하여 공분산 행렬로부터 공분산 $C$의 고유값(eigenvalue) w고유벡터(eigenvector) v를 구하는 방법
      • np.linalg.eig
    • SVD를 사용하여 공분산 행렬을 사용하지 않고 구하는 방법
      • np.linalg.svd
    • Scikit-Learn의 PCA를 사용하여 구하는 방법.
  • 이번 글에서는 마지막의 Scikit-Learn을 이용하는 방법을 주로 다룰것이다.

  • 사용 예시는 다음과 같다.

from sklearn.decomposition import PCA

# n_components = 구할 주성분벡터의 갯수
# X = 구하려는 데이터
pca = PCA(n_components = 3)
pca.fit(X)

# 특이값
print('singular value :', pca.singular_values_)
# 특이값 벡터. 이때, 고유값 분해 방식으로 푼것과 부호가 다르지만, 축을 이루는 벡터는 같으므로 상관없다.
print('singular vector :\n', pca.components_.T)

# 공분산 행렬의 고유값
print('eigen_value :', pca.explained_variance_)
# 공분산 고유값의 설명된 분산 비율
print('explained variance ratio :', pca.explained_variance_ratio_)

explained variance ratio 해석

  • explained variance ratio : [0.84248607 0.14631839 0.01119554]

    • 원 데이터셋 분산의 84.2%가 첫번째 주성분 축에 놓여있고, 14.6%가 두번째 주성분 축에 놓여있음을 의미한다.

    • 세번째 주성분 축에는 1.1%정도의 매우 적은 정보만 존재함을 알수 있다.

    • 따라서, 첫번째 주성분과 두번째 주성분을 이용해 3차원 데이터셋 X를 2차원에 투영할 경우, 원래 데이터셋의 분산에서 1.1%의 분산을 잃게 된다.

      • 즉, 원래 데이터셋의 정보에서 1.1%를 손실한다고 봐도 될것이다.

적절한 차원수 선택하기

  • explained variance ratio를 이용해 축소할 차원의 수를 정할수도 있다.

  • 누적된 분산의 비율이 95%가 되는 차원을 선택하는 방식으로 진행한다.

cumsum = np.cumsum(pca.explained_variance_ratio_)
d = np.argmax(cumsum >= 0.95) + 1
print('선택할 차원 수 :', d)
  • PCA를 구할때, n_components에 0~1 사이의 값을 지정하는것으로 누적 분산 비율을 지정하여 PCA 계산을 수행할수 있다.
pca = PCA(n_components=0.95)
X_proj = pca.fit_transform(X)
print('principal component vec :\n', pca.components_.T)

데이터셋의 차원 축소

  • MNIST 데이터셋에 분산의 95%만 유지하도록 하는 예제 코드는 다음과 같다.

from tensorflow.keras.datasets import mnist
# MNIST load
(train_x, train_y), (test_x, test_y) = mnist.load_data()
# reshape
train_x = train_x.reshape(-1, 28*28)


pca = PCA(n_components=0.95)
# PCA 계산 후 투영
X_reduced = pca.fit_transform(train_x) 
  • 압축된 데이터셋은 PCA 투영변환을 반대로 적용하여 다시 원 데이터의 차원으로 복원할수 있다.

  • 이때, 위에서 5%의 분산(=정보)를 잃었기 때문에 손실이 발생한다.
    • 원 데이터와 복원한 데이터간 평균제곱오차를 재구성 오차라고 한다.
  • 압축한 데이터셋 X_reduced는 다음과 같이 복원 가능하다.

X_recovered = pca.inverse_transform(X_reduced)
  • 원데이터 / 복원결과 비교 이미지

    예시이미지

Incremental PCA (IPCA)

  • scikit-learn 기준, SVD를 수행하기 위해 전체 학습 데이터셋을 메모리에 올려야 한다는 단점이 존재한다.

  • 이러한 단점을 보완하기 위해 Incremental PCA(IPCA)알고리즘이 만들어졌다.

  • 학습 데이터셋을 미니배치로 나눈뒤 하나의 미니배치를 입력으로 넣는다.

  • IPCA는 학습 데이터셋이 클때 유용하다.


from sklearn.decomposition import IncrementalPCA

# 데이터셋을 쪼갤 미니배치 숫자
n_batches = 100

inc_pca = IncrementalPCA(n_components=154)

# train_x를 배치로 쪼개 partial_fit을 통해 부분학습시킨다.
for batch_x in np.array_split(train_x, n_batches):
    # 참고한 코드에서는 이 반복문이 꽤 길었는지 progress bar 대용으로 점을 찍는 코드가 있었다.
    # tqdm등의 라이브러리로 진행도를 표시하는편이 좋을듯하다.
    inc_pca.partial_fit(batch_x)

X_reduced = inc_pca.transform(train_x)


# 복원은 마찬가지로 inverse_transform()으로!
X_recovered_inc_pca = inc_pca.inverse_transform(X_reduced)

Kernel PCA (KPCA)

커널 함수

  • 정의

    • x를 기저함수 변환 후 변환된 독립변수벡터를 내적한 값을 하나의 함수로 나타낸 함수.
  • 주 사용처

    • 저차원 데이터를 고차원에 매핑하여 비선형 데이터에 SVM등을 적용시키기 위함.
  • Scikit-Learn에서 지원하는 커널함수의 종류

  • SVM에 사용할수 있듯이, PCA에도 이러한 커널함수를 이용한 투영이 가능한데, 이를 Kernel PCA라고 한다.

  • 스위스롤 데이터에 Kernel PCA를 적용하는 예제코드이다.

from sklearn.datasets import make_swiss_roll

# 스위스롤 데이터 제작
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=42)
from sklearn.decomposition import KernelPCA

# 커널함수는 가우시안 RBF 커널함수를 사용했다.
rbf_pca = KernelPCA(n_components = 2, kernel="rbf", gamma=0.04)

X_reduced = rbf_pca.fit_transform(X)

참고한 글