[Python] 차원 축소

0ㅑ채
|2024. 3. 12. 13:25

1. 차원 축소

  • 머신 러닝의 많은 훈련 샘플들은 여러 개의 특성을 가지고 있다.
  • 특성의 개수가 많으면 훈련이 느려지고 좋은 솔루션을 찾기 어려워진다. = 차원의 저주
  • 실전에서는 특성 수를 크게 줄여 불가능한 문제를 가능한 범위로 변경해야 하는 경우가 많다.

 

 

1) 차원의 저주

- 고차원에서는 많은 것이 상당히 다르게 동작한다.

  • 사각형 안에서 점을 무작위로 선택하면 경계선에서 0.001 정도 거리에 존재할 확률이 0.4% 정도 되는데
  • 1만 차원이 되면 경계선이 늘어나서, 경계선 근처에 몰려 존재할 확률이 99.999999% 보다 커짐 

  • 대부분의 훈련 데이터가 서로 멀리 떨어져 있게 되고, 새로운 샘플도 멀리 떨어져있을 가능성이 높아진다.
    예측을 위해서는 훨씬 더 많은 외삽을 수행해야 한다. 
    • 외삽: 변수의 값을 추정하는 과정 중의 하나 변수들 간의 관계를 이용해서 값을 추정한다.
    • 보간법: 하나의 속성에서 값과 값 사이의 데이터를 추정하는 것
      10    20    30    ?   50  => 40
      외삽: 여러 변수 사이에서 값을 추정하는 것
      A       B    Target
      10    23    15
      22    32    29
      10    11     ?
    • 외삽을 정확하게 하기 위해서는 샘플의 개수를 늘리는 방법이 있지만
      일반적으로 비용이 많이 소모되고 피처가 많을 때는 아주 많은 샘플을 추가해야 한다. => 불가능

 

 

2) 차원 축소의 종류 

feature selection 특성 선택

  • 특정 피처에 종속성이 강한 불필요한 피처는 제거하고 데이터의 특성을 잘 나타내는 주요 피처만 선택
  • 통계적인 방법을 이용해서 feature들의 중요도에 순위를 정해 결정
  • but 정보 손실이 발생
  • 동일한 문제를 해결하는 다른 데이터 세트에서는 중요도의 순위가 달라질 수 있음

- 구현 방법

  • 필터 방식
    • 전처리 단계에서 통계 기법을 사용해서 변수 채택
    • 높은 상관계수의 feature를 사용
    • 변수 중요도나 상관도에 관련 알고리즘
      • 상관관계
      • 불순도(트리 모델에서 사용하는 지니계수, 엔트로피)
      • 카이 제곱 검정
  • 래퍼 방식
    • 예측 정확도 측면에서 가장 좋은 성능을 보이는 피처 집합을 뽑아내는 방법
    • 변수 선택 평가 모델
      • AIC (Akaike Information Criteria)
      • BIC (Bayesian Information Criteria)
      • 수정 R2 (결정계수)
    • 여러 번 머신러닝 알고리즘을 수행해야 하기 때문에 시간과 비용이 높게 발생하지만
      가장 좋은 피처의 집합을 찾는 원리이기 때문에 모델의 정확도를 위해서는 바람직한 방법
    • 알고리즘
      • Forward Selection(전진 선택법): 피처가 없는 상태에서부터 반복할 때마다 피처를 추가하면서 성능이 가장 좋아지는 지점을 찾는 방식
      • Backward Selection(후진 소거법): 모든 피처를 가지고 시작해서 덜 중요한 피처를 제거하면서 성능의 향상이 없을 때까지 수행하는 방식
      • Stepwise Selection(단계별 선택법): 피처가 없는 상태에서 출발해서 전진 선택법과 후진 소거법을 번갈아가면서 수행하는 방식인데 가장 시간이 오래 걸리는 방식이지만 가장 우수한 예측 성능을 나타내는 변수 집합을 찾아낼 가능성이 높다. 
  • 임베디드 방식
    • Filtering과 Wrapper의 조합
    • 각각의 피처를 직접 학습하며 모델의 정확도에 기여하는 피처를 선택
    • 계수가 0이 아닌 피처를 선택해서 더 낮은 복잡성으로 모델을 훈련하면서 학습 절차 최적화
    • 라쏘, 릿지, 엘라스틱넷 등을 이용해서 규제를 추가할 수 있다. 

 

피처 추출(feature extraction)

  • 기존 피처를 저차원의 중요 피처로 압축해서 추출
  • 새롭게 만들어진 피처는 기존의 피처와는 다른 값
    • 새로 만들어지는 피처는 기존 피처를 단순하게 압축하는 것이 아니라 함축적으로 더 잘 설명할 수 있는 다른 공간으로 매핑해서 추출하는 것
    • 함축적인 특성 추출은 기본 피처가 전혀 인지하기 어려웠던 잠재적인 요소를 추출
  • 이 방식은 전체 피처를 사용

- 구현 방법

  • Linea Methods: PCA, FA, LDA, SVD
  • Non-Linear Methods: Kernel PCA, t-SNE, Isomap, Auto-Encoder(-> 생성형 AI)

 

 

3) 차원 축소의 목적

- 비용, 시간, 자원, 용량의 문제

  • 불필요한 변수 저장: 용량 문제 발생
  • 차원이 많아지면 비례해서 분석 시간 증가

- Overfitting 문제 해결

  • 변수가 많으면 모델의 복잡도 증가: 일반화 가능성 낮음
  • 민감도가 증가해서 오차가 커질 수 있는 가능성 높아짐

- 군집화 분석 결과가 좋지 않음

  • 차원이 많아지면 경계선에 있을 가능성이 높아지고, 그러면 벡터 간의 거리가 유사해짐
  • 군집 성능 저하

- 차원이 높으면 설명력이 떨어짐

  • 2~3차원은 시각화가 가능하지만 3차원보다 큰 차원은 시각화가 어렵다.
  • 잠재적인 요소를 추출할 수 있다. 

- 차원을 축소하면 일부 정보가 유실되기 때문에 훈련 속도가 빨라지기는 하지만 성능은 나빠질 가능성이 높다. 

 

 

 

 

2. 투영 (Projection)

  • 물체의 그림자를 어떤 물체 위에 비추는 일 또는 그 비친 그림자를 의미
  • 어떤 물체든 그림자는 2차원으로 표현될 수 있다는 원리
  • 투영을 이용해서 구현된 알고리즘이 PCA, LDA 등

 

1) 특징

  • MNIST 같은 이미지에는 잘 적용이 되지만 스위스 롤 같은 데이터에는 적용할 수 없다.
  • 특성들의 값이 거의 비슷하고 다른 특성의 값이 많이 다르면 적용하기가 좋은데 그렇지 않은 경우는 적용하기 어렵다. 
높이가 별 차이 안나니까 눌러서 높이를 없애버려도 차이가 없으니 두개만으로도 설명이 된다.

MNIST 이미지 - 바깥 쪽은 거의 다 똑같다. (테두리는 항상 흰색)

스위스롤: 
누르면 안됨. 부딪혀버린다. 


실제 값                                          원했던 것

 

 

 

 

 

3. Manifold 방식

  • 차원을 축소하는데 특정 차원을 없애지 않고 새로운 차원을 만들어 내는 방식
  • 알고리즘: LLE, t-SNE,  Isomap, Autoencoer

  • 직선으로 구분하는 건 불가해졌지만, 곡선으로 나누면 됨 => PCA

 

 

 

4. PCA (Principal Component Analysis - 주성분 분석)

  • 여러 변수 간에 존재하는 상관관계를 이용하고 이를 대표하는 주성분을 추출해서 차원을 축소하는 기법
  • 차원을 축소할 때 데이터의 정보는 최대한 유지하면서 고차원에서 저차원으로 축소를 하는 방식
  • 데이터의 다른 부분을 보존하는 방식으로 분산을 최대한 유지하는 축을 생성
  • 이 방식은 데이터의 분포가 표준정규분포가 아닌 경우에는 적용이 어려움

 

- 동작 원리

  • 학습 데이터 셋에서 분산(변동성)이 최대인 축 (PC1)을 찾음
  • PC1과 수직이면서 분산이 최대인 축 (PC2)를 찾음
  • 첫 번째 축과 두 번째 축에 직교하고 분산을 최대한 보존하는 세 번째 축을 찾음
  • 1~3의 방법으로 차원 수만큼 찾음

데이터 구분: 차이
구분되는 성질을 가장 적게 잃어야하기 때문에, 가장 많이 차이가 나는 것을 찾아야 한다.
애-어른을 구분하는 가장 큰 차이는 나이, 그게 첫번째 주성분이 된다. 
그러면 반대편은 설명하지 못하기 때문에, 두번째 주성분은 첫번째 주성분과 직교한다.
2-3가지 정도로 판단하자!

 

 

4-1) sklearn

  • 변환기를 데이터 세트에 학습시키고 나면 결과 확인 가능
  • n_components 매개변수에 축소하려는 차원 개수 설정
#데이터 생성
#피처가 3개인 데이터 생성
np.random.seed(42)
m = 60
w1, w2 = 0.1, 0.3
noise = 0.1

angles = np.random.rand(m) * 3 * np.pi / 3 -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)

...
from sklearn.decomposition import PCA
#2개의 피처로 주성분 분석을 수행
pca = PCA(n_components=2)
X2D = pca.fit_transform(X)

print(X2D[:5])
[[ 0.88527067 -0.33376821]
 [ 0.09028918  1.11319151]
 [ 0.73610082  0.50819326]
 [ 1.86082527  0.0634811 ]
 [-0.46712242 -0.51380375]]

 

# 분산 비율 확인: explained_variance_ratio_

#분산 비율 확인
print(pca.explained_variance_ratio_)
[0.73833891 0.20993687]
  • Explained Variance Ratio: 분산을 얼마나 보존하는가 나타내는 지표 (70% 이상 채택)
  • 첫번째 주성분이 74% 정도 데이터를 설명할 수 있고 두번째 주성분은 21% 정도 설명 가능

# 데이터 복원: inverse_transform

X3D_inv = pca.inverse_transform(X2D)
print(X3D_inv[:5])
[[ 0.97487955  0.28357613  1.30383978]
 [-0.4587498   0.66634022  0.5800397 ]
 [ 0.16053264  0.52058405  1.19513314]
 [ 0.65692889  0.44306826  2.29529498]
 [ 1.06130267  0.16980808 -0.05298289]]
#원본 데이터와 복원된 데이터를 비교
print(np.allclose(X3D_inv, X))
#차이의 평균
print(np.mean(np.sum(np.square(X3D_inv - X), axis=1)))
False
0.07836731698475538
  • 원본과 복원결과는 차이가 있음, 일부 데이터는 소실됨

 

 

4-2) 적절한 차원 수 선택

  • 시각화를 위해서 주성분 분석을 하는 경우 2~3개로 수행
  • 시각화 이외의 목적인 경우는 일반적으로 주성분의 개수를 지정하기 보다 분산의 비율을 이용해서 개수 설정
    n_components에 성분의 개수가 아닌 0.0 ~ 1.0 사이의 비율을 설정하면 비율이 되는 주성분의 개수로 분석 수행
  • 시각화를 수행해서 엘보가 만들어지는 지점이 있다면 엘보를 이용해서 주성분 개수 설정
#데이터 가져오기
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split

#이미지 데이터 전부 가져오기
mnist = fetch_openml('mnist_784', version=1, as_frame=False)

mnist.target = mnist.target.astype(np.uint8)

X = mnist.data
y = mnist.target

X_train, X_test, y_train, y_test = train_test_split(X, y)
#X_train 은 784개의 피처를 가진 데이터
print(X_train.shape)
(52500, 784)
#차원수를 지정해서 PCA 수행: 154개의 피처로 주성분분석 수행
pca = PCA(n_components=154)
pca.fit_transform(X_train)
#분산의 비율 확인
print(pca.explained_variance_ratio_)

...
#분산 비율의 합계 - 95%
print(pca.explained_variance_ratio_.sum())
0.9500532025918321

#차원수를 지정해서 PCA 수행: 95% 이상되는 주성분의 개수로 주성분 분석
pca = PCA(n_components=0.95)
pca.fit_transform(X_train)
print(pca.explained_variance_ratio_.sum())
0.9504506731634907
#주성분의 개수
print(pca.n_components_)
154

 

 

 

4-3) Random PCA

  • sklean의 기본 PCA는 모든 데이터를 메모리에 적재시킨 후 훈련을 시켜야 한다. 
  • 시간이 많이 소모된다. O(m * n의 제곱) + O(n의 3제곱)   m은 행의 개수이고 n은 차원의 개수

- PCA에서 svd_solver 매개변수를 randomized 로 지정하면 Random PCA를 수행

- 확률적 알고리즘을 이용해서 주성분에 대한 근사값을 찾는 방식

  • 이 방식은 원본 데이터의 차원은 속도에 아무런 영향을 주지 못하고 주성분의 개수만 훈련 속도에 영향을 줍니다.
  • 시간 복잡도가 O(m * d의 제곱) + O(d의 3제곱)  d는 줄이고자 하는 차원의 개수
  • sklearn은 차원의 개수가 500개 이상이거나 줄이려는 차원의 개수가 원본 차원의 80% 보다 작으면 Random PCA를 수행
#차원수를 지정해서 랜덤 PCA 수행
pca = PCA(n_components=154, svd_solver='randomized')
pca.fit_transform(X_train)
print(pca.explained_variance_ratio_.sum())
0.9500820155151543

 

 

 

4-4) 점진적 PCA 

  • PCA 구현의 문제점 중 하나는 전체 훈련 세트를 메모리에 올려야 한다는 것
  • 이를 해결하기 위한 방법 중 하나가 IncrementalPCA
  • 메모리가 부족한 경우 와 실시간으로 데이터가 생성되는 경우 점진적 PCA를 사용

- 훈련 세트를 미니배치로 나눈 뒤 PCA 알고리즘에 한번에 하나씩 주입한다.

  • 훈련 세트가 클 때 유용
  • 나누어서 학습하는 것이 가능: 온라인 학습이 가능

- fit 대신 partial_fit 이라는 메서드를 이용해서 미니배치마다 호출

- 일반 PCA 보다 훈련 시간은 조금 더 걸릴 수 있다.

  • 배치 처리가 미니 배치 방식보다 좋은 점은 자원을 효율적으로 사용할 수 있고 작업 시간은 단축 시킬 수 있다는 것
  • 미니 배치 방식이 좋은 점은 훈련을 실시간으로 수행하기 때문에 훈련이 끝나는 시점만 따져보면 더 빠르다.

* 파이썬에서 메모리 정리 방법: (1) 가리키는 데이터가 없을 때 자동으로 정리.  (2) 기존 변수가 라키는 데이터가 없도록  del 변수명 을 사용

 

 

 

4-5) 3가지 PCA 훈련 시간 비교

# 일반 PCA

import time
start = time.time()

pca = PCA(n_components=154, svd_solver="full")
pca.fit_transform(X_train)

end = time.time()
print("PCA 수행 시간:", (end-start))
print(pca.explained_variance_ratio_.sum())
PCA 수행 시간: 4.382986545562744
0.9504506731634907

 

# 랜덤 PCA

import time
start = time.time()

pca = PCA(n_components=154, svd_solver="randomized")
pca.fit_transform(X_train)

end = time.time()
print("랜덤 PCA 수행 시간:", (end-start))
print(pca.explained_variance_ratio_.sum())
랜덤 PCA 수행 시간: 4.955554962158203
0.9500787743598854
  • svd_solver="randomized"를 생략해도 Random PCA - 피처의 500개가 넘으면 자동

# 점진적 PCA

from sklearn.decomposition import IncrementalPCA

#배치 사이즈: 데이터를 몇 개 씩 훈련
n_batches = 100
start = time.time()

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)
end = time.time()
print("점진적 PCA 수행 시간:", (end-start))
print(inc_pca.explained_variance_ratio_.sum())
점진적 PCA 수행 시간: 29.47389006614685
0.9497567304638014

 

 

 

 

 

4-6) Kernel PCA

- 커널 트릭 PCA: 비선형 데이터에 다항을 추가하는 것 처럼 해서 비선형 결정 경계를 만드는 PCA

  • 일반 PCA를 수행해서 분산의 비율이 제대로 표현되지 않는 경우 사용

- KernelPCA 클래스 (kernel, gamma)

  • kernel = rbf: 가우시안 정규 분포 형태의 커널을 적용
  • kernel = sigmoid: 로그 함수 형태
  • gamma: 복잡도를 설정. 0.0 ~ 1.0 사이의 값

- 하이퍼 파라미터 튜닝을 거의 하지 않지만 주성분 분석을 분류나 회귀의 전처리 과정으로 사용하는 경우에 가장 좋은 성능을 발휘하는 kernel이나 gamma 값을 찾기 위해서 튜닝 하는 경우도 있다.

#데이터 생성
from sklearn.datasets import make_swiss_roll

# X는 3차원 피처, t 는 범주
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=42)
print(X)
[[-3.29677117  4.26198454  7.69787682]
 [ 5.95972743 11.45784273 12.72625276]
 [ 6.66051523 18.15820401 -9.84713337]
 ...
 [ 6.18364276  1.44095323 -1.71242696]
 [ 5.86076169  1.09185823 12.47091112]
 [-8.16213703  5.61235668  4.51171684]]
  • 스위스 롤 같은 데이터를 차원 축소를 수행하거나 분류를 수행하는 경우 선형으로 차원 축소를 수행하거나 분류를 수행할 수 없다.
axes = [-11.5, 14, -2, 23, -12, 15]
fig = plt.figure(figsize=(6, 5))
ax = fig.add_subplot(111, projection='3d')
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=t, cmap=plt.cm.hot)
plt.show()
from sklearn.decomposition import KernelPCA

pca = PCA(n_components=2)
result = pca.fit_transform(X)
print(pca.explained_variance_ratio_.sum())
0.7098258574944849
kernel_pca = KernelPCA(n_components=2, kernel="sigmoid", fit_inverse_transform=True,
                      coef0=1, gamma=0.0001)
result = kernel_pca.fit_transform(X)

#KernelPCA는 분산의 비율을 직접 계산
explained_variance = np.var(result, axis=0)
explained_variance_ratio = explained_variance / np.sum(explained_variance)
print(explained_variance_ratio)
[0.57463469 0.42536531]

 

 

 

4-7) PCA 수행 이유

  • 시각화를 하거나 단순한 모델을 만들기 위해서 PCA 수행
  • PCA를 수행해서 분류나 회귀 작업을 수행하게 되면 성능은 떨어질 가능성이 높지만 일반화 가능성은 높아짐
  • 실세계에 존재하는 데이터는 잡음이 섞인 경우가 많아서 잡음이 섞인 데이터를 가지고 훈련을 하게되면 overfitting 될 가능성이 높다.

# 잡음 제거: 이미지에 잡음 추가하고 PCA 수행

from sklearn.datasets import load_digits

#8*8 의 MNIST 데이터 가져오기
digits = load_digits()
digits.data.shape
(1797, 64)
#이미지 출력 함수
def plot_digits(data):
    fig, axes = plt.subplots(4, 10, figsize=(10, 4),
                            subplot_kw={'xticks':[], 'yticks':[]},
                            gridspec_kw=dict(hspace=0.1, wspace=0.1))
    for i, ax in enumerate(axes.flat):
        ax.imshow(data[i].reshape(8, 8), cmap='binary',
        interpolation='nearest', clim=(0, 16))
#원본 이미지 출력
plot_digits(digits.data)
#주성분 분석
from sklearn.decomposition import PCA

pca = PCA(n_components = 0.6).fit(noisy)
print(pca.n_components_)
18
  • 60%의 분산을 나타내는 데 피처 18개 필요
  • 70%의의 분산을 나타내는데는 26개의 피처가 필요
#PCA를 수행한 데이터를 복원
components = pca.transform(noisy)
filtered = pca.inverse_transform(components)
plot_digits(filtered)
  • 잡음 제거 완료

 

 

5. 지역 선형 임베딩 LLE (Locally Linear Embedding)

- 비선형 차원 축소 방법

- 피처 내의 모든 데이터를 이용하지 않고 가장 가까운 이웃에 얼마나 선형적으로 연관되어 있는 측정한  후 국부적인 관계가 가장 잘 보존되는 저차원을 찾는 방식

- LocallyLinearEmbedding 클래스

  • n_components: 차원의 개수를 설정
  • n_neighbors: 이웃의 개수를 설정

#스위스롤은 롤 케익처럼 클래스가 구분된 데이터를 생성

  • 첫번째 리턴되는 값은 피처 3개로 구성된 피처의 배열
  • 두번째 리턴되는 값은 범주에 해당하는 타겟
X, t = make_swiss_roll(n_samples=1000, noise=0.2, random_state=42)

from sklearn.manifold import LocallyLinearEmbedding
#10개의 이웃을 이용해서 2개의 성분으로 축소
lle = LocallyLinearEmbedding(n_components=2, n_neighbors=10, random_state=42)
X_reduced = lle.fit_transform(X)

plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=t, cmap=plt.cm.hot)
plt.show()
lle = PCA(n_components=2, random_state=42)
X_reduced = lle.fit_transform(X)

plt.scatter(X_reduced[:, 0], X_reduced[:, 1], c=t, cmap=plt.cm.hot)
plt.show()

 

 

 

 

 

6. 선형 판별 분석 LDA (Linear Discriminant Analysys)

- 분류 알고리즘이지만 훈련 과정에서 개별 클래스를 분별할 수 있는 축을 학습

- 각 클래스 간의 분산과 클래스 내부에서의 분산을 이용

  • 클래스 간의 분산은 최대한 크게하고 내부 분산은 최대한 작게 가져가는 방식

- LDA 나 LLE를 이용해서 차원을 축소할 때는 스케일링 필요

- sklearn.discriminant_analysis.LinearDiscriminantAnalysis 클래스

  • 하이퍼파라미터는 n_components 

 

 

 

7. 기타 차원 축소 알고리즘

1) NMF: 행렬 분해를 이용해서 차원 축소

2) Random Projection: 랜덤 투영

3) Multi Dimensional Scailing: 샘플 간의 거리 유지하면서 차원 축소

4) Isomap

5) t-SNE

  • 비슷한 샘플은 가까이,  비슷하지 않은 샘플은 멀리 떨어지도록 하면서 차원을 축소
  • 고차원 공간에 있는 샘플의 군집을 시각화 할 때 사용
  • 고도로 군집된 데이터의 경우 가.장 잘 작동하지만 속도가 느림

- 일반 PCA가 많이 사용되었던 이유는 설명 분산을 기반으로 하기 때문에 차원의 개수를 설정하는 것이 쉽고 빠름

- PCA의 단점은 비선형 관계를 보존할 수 없다는 것

- 고차원의 데이터를 축소할 때는 t-SNE 나 Isomap을 주로 이용