https://soochan8.tistory.com/12
PCA(Principal Component Analysis) 이상탐지 - 개념, 수식
앞선 포스팅에서 One Class / 재구축오차 기반 이상탐지 알고리즘의 개요에 대해 다루었다. One Class / Reconstruction Error based 이상탐지 알고리즘? (+ 이상탐지에서의 Supervised, Unsupervised Learning)재구
soochan8.tistory.com
PCA는 파이썬의 scikit-learn 라이브러리에 잘 구현되어 있다. 보통은 그냥 딸깍 해서 쓰면 되지만,
처음 배울 때 확실히 이해했는지 확인해보고 싶다면 numpy정도만 사용해 직접 구현을 해 보시는 것을 추천한다.
이해했다고 생각했지만 구현하면서 알게 된 것도 많다.
이론편에서 있는 step과 최대한 비슷하게 구성했다.
정말 간단한 3차원의 데이터 10개를 임의로 생성한 후, 표준화하고 공분산행렬을 계산한다.
import numpy as np
np.random.seed(0)
# 1. 샘플 데이터 준비 (3차원 데이터, 10개의 샘플)
X = np.array([[2.5, 2.4, 3.2],
[0.5, 0.7, 1.0],
[2.2, 2.9, 3.1],
[1.9, 2.2, 2.9],
[3.1, 3.0, 3.5],
[2.3, 2.7, 3.0],
[2.0, 1.6, 2.5],
[1.0, 1.1, 1.2],
[1.5, 1.6, 2.0],
[1.1, 0.9, 1.5]])
# 2. 표준화
X_mean = np.mean(X, axis=0)
X_std = np.std(X, axis=0)
X_standardized = (X - X_mean) / X_std
# 3. 공분산 행렬 계산
cov_matrix = np.cov(X_standardized.T) #변수들을 행으로 배치해야 하기 떄문에 전치시킴
print("표준화된 데이터 행렬\n", X_standardized)
print("\n 공분산 행렬 \n", cov_matrix)
<출력>
표준화된 데이터 행렬
[[ 0.92627881 0.61016865 0.9461558 ]
[-1.7585873 -1.506743 -1.62365008]
[ 0.52354889 1.23278973 0.82934645]
[ 0.12081898 0.36112022 0.59572773]
[ 1.73173864 1.35731394 1.29658388]
[ 0.6577922 0.9837413 0.71253709]
[ 0.25506228 -0.38602507 0.12849029]
[-1.08737078 -1.00864614 -1.39003137]
[-0.41615425 -0.38602507 -0.4555565 ]
[-0.95312747 -1.25769457 -1.03960329]]
공분산 행렬
[[1.11111111 1.0288103 1.07518529]
[1.0288103 1.11111111 1.06198995]
[1.07518529 1.06198995 1.11111111]]
고유값분해를 하면 고유벡터와 고유값이 나온다.
이 중 고유값이 큰 순서대로 정렬해 첫번째 주성분 축(고유벡터)을 찾고,
여기에 표준화된 데이터를 projection한다. 데이터를 벡터에 프로젝션하는건 내적연산 (np.dot)으로 가능하다.
10x3 데이터와 3x1 벡터 (고유값이 가장 큰 첫번째 고유벡터) 내적해 10x1의 데이터를 만드는 것이다.
pc를 몇 개를 쓸 건지에 따라서 고유벡터 수를 선택할 수 있다
# 4. 고유값과 고유벡터 계산
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
# 5. 고유값 정렬 및 첫 번째 주성분 선택
sorted_indices = np.argsort(eigenvalues)[::-1] # 고유값 내림차순 정렬
eigenvectors = eigenvectors[:, sorted_indices] # 정렬된 고유벡터
k = 1
W = eigenvectors[:, :k] # 첫 번째 주성분 축 (3x1)
# 6. 첫번째 eigenvector에 데이터를 projection
Z = np.dot(X_standardized, W)
print("eigenvalues: ",eigenvalues, "\n eigenvectors \n" ,eigenvectors)
print("\n PC Score: \n", Z) # 각 샘플이 주성분 축에서 얼마나 떨어져있는가?
<출력>
eigenvalues: [3.22188767 0.08335334 0.02809232] -> 3개 주성분의 고유값
eigenvectors -> 각 주성분에 대응되는 고유벡터. 이 축에 데이터를 projection한다. (주성분공간으로 투영)
[[-0.57616336 -0.64223112 -0.50554423]
[-0.5737167 0.75832621 -0.30950041]
[-0.58213824 -0.11171637 0.8053785 ]]
PC Score: -> 위 코드에서 Z에 해당하는 부분. 10개 데이터를 PC1에 투영시켰을 때 값
[[-1.43454533e+00]
[ 2.82286599e+00]
[-1.49171602e+00]
[-6.23588059e-01]
[-2.53126909e+00]
[-1.35817966e+00]
[-2.87627888e-04]
[ 2.01437075e+00]
[ 7.26438714e-01]
[ 1.87591033e+00]]
PC 1에 투영시키는 것의 의미는
아래 그림처럼 우리 샘플 데이터는 3차원 공간에 있으니, 빨간색 축이 첫번째 고유벡터(PC1)이라고 가정한다면
저 축 위에다가 점들을 projection시키는 것이다. 위 결과의 PC Score가 그 값이고..

2차원의 경우에는 이렇다. 3차원도 동일하게 PC를 하나만 선택했을 때는 선에,
두 개를 선택했다면 2차원 평면에 주성분을 나타낼 수 있다.

여기가 핵심이다.
우리는 주성분 공간에 투영된 1x10 데이터 Z를 가지고 있다.
이걸 원래 차원인 3차원으로 복원시키려면? 아까 내적했던 고유벡터를 전치시킨 다음에 또 곱해주면 된다. 나는 이걸 이해하는데 꽤 오래 걸렸다..
# 7. 복원
X_reconstructed = np.dot(Z, W.T) # Z에 eigenvector의 전치를 내적 -> 다시 3차원데이터로 복원
X_reconstructed = X_reconstructed * X_std + X_mean # 정규화 과정의 역
print("원본 데이터 \n" , X_standardized)
print(" \n 재구축된 데이터 \n" , X_reconstructed)
<출력>
원본 데이터
[[ 0.92627881 0.61016865 0.9461558 ]
[-1.7585873 -1.506743 -1.62365008]
[ 0.52354889 1.23278973 0.82934645]
[ 0.12081898 0.36112022 0.59572773]
[ 1.73173864 1.35731394 1.29658388]
[ 0.6577922 0.9837413 0.71253709]
[ 0.25506228 -0.38602507 0.12849029]
[-1.08737078 -1.00864614 -1.39003137]
[-0.41615425 -0.38602507 -0.4555565 ]
[-0.95312747 -1.25769457 -1.03960329]]
재구축된 데이터
[[2.42569734 2.57093379 3.10492875]
[0.59844464 0.60942937 0.9831793 ]
[2.45023462 2.59727387 3.13342069]
[2.07763986 2.19730387 2.70077514]
[2.8964039 3.07622406 3.65149869]
[2.3929217 2.53575006 3.06687069]
[1.81012345 1.91013252 2.39014334]
[0.94544544 0.98192497 1.38610592]
[1.49821732 1.57531006 2.02796758]
[1.00487173 1.04571743 1.4551099 ]]
이건 임의로 생성한 데이터라 의미는 없을테지만
이런식으로 처음과 같은 3x10형태로 데이터가 복원된 것을 확인할 수 있다.
이후 원래 데이터와의 차이(L2 norm)를 이용해 재구축오차를 계산한다.
# 8. 샘플별 재구축 오차 계산
sample_errors = np.linalg.norm(X - X_reconstructed, axis=1)
print("샘플별 재구축 오차:\n", sample_errors)
<출력>
샘플별 재구축 오차:
[0.20923142 0.1348233 0.39417942 0.26693395 0.26497798 0.20021032
0.37987336 0.22705358 0.03734912 0.17971666]
이것을 이상치 스코어로 사용하는 것이다..
여하튼 PCA는 이런 식으로 구현할 수 있다.
PCA기반 이상탐지 - 간단한 예제
조금 더 높은 차원의 데이터를 생성해 이상탐지하는 과정을 테스트해보았다.
아래 코드에서는 사이킷런의 pca를 이용했다.
n_samples = 300
n_features = 10
# 밀도가 다른 두 개의 정상데이터 군집을 생성
X1, _ = make_blobs(n_samples=150, centers=1, cluster_std=0.50, random_state=0, center_box=(-5.0, 5.0), n_features=n_features) # 밀도가 높은 군집
X2, _ = make_blobs(n_samples=150, centers=1, cluster_std=1.5, random_state=1, center_box=(5.0, 15.0), n_features=n_features) # 밀도가 낮은 군집
X = np.vstack((X1, X2))
# 고르게 분포된 이상치 추가
outliers = np.random.uniform(low=-10, high=20, size=(20, n_features))
X = np.vstack((X, outliers))
df = pd.DataFrame(X, columns=[f'Feature {i+1}' for i in range(n_features)])
<출력>



feature 1,2,3만 시각화해보면 이런 모습이다. 세 개의 차원만 확인한 것이긴 하지만, 분포로부터 벗어난 이상치들이 생성된 것을 대략적으로 확인할 수 있다.
Scree plot을 보면 너무 간단해서인지 첫 번쨰 주성분이 전체 분산의 70% 이상을 설명하는 모습이다.
from pyod.models.pca import PCA # PCA 기반 이상치 탐지 모델
# PCA 기반 이상치 탐지 모델
pca_model = pyodPCA(contamination=0.06)
pca_model.fit(X)
# 이상치 예측 (1: 이상치, 0: 정상)
y_pred = pca_model.labels_
pca = SKPCA(n_components=2)
X_pca = pca.fit_transform(X)
# 재구축 오차 계산 (10D -> 2D -> 10D 복원)
X_reconstructed = pca.inverse_transform(X_pca)
reconstruction_error = np.mean(np.square(X - X_reconstructed), axis=1)
plt.figure(figsize=(8, 6))
# 정상 데이터와 이상치 다른 색으로 표시
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y_pred, cmap='coolwarm', s=10, label='Data Points')
# 이상치에만 재구축 오차 텍스트 표시
for i, txt in enumerate(np.round(reconstruction_error, 2)):
if y_pred[i] == 1: # 이상치인 경우에만
plt.annotate(txt, (X_pca[i, 0], X_pca[i, 1]), fontsize=8, color='black')
plt.title('PCA-based Outlier Detection with Reconstruction Error Score')
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.show()

주성분 1,2를 축으로 2차원그래프를 그리고, 이상치로 판단한 데이터들의 이상치스코어를 표기했다.
이때 재구축오차의 상위 6%값을 임계값으로 지정하여 이를 이상치로 간주하였다.
임의로 생성한 데이터기에, contamination 값을 바꿔 가며 PC1, PC2의 그래프를 보고 임계값을 정했지만, 실제 데이터에 적용한다면 불량/정상 라벨링이 되어 있을 것이기에 재현율, 정밀도 등을 비교해가며 임계값을 조정하면 될 것이다.
'데이터 분석 > Anomaly Detection 이상탐지' 카테고리의 다른 글
| PCA(Principal Component Analysis) 이상탐지 - 개념, 수식 (0) | 2025.02.25 |
|---|---|
| One Class / Reconstruction Error based 이상탐지 알고리즘? (+ 이상탐지에서의 Supervised, Unsupervised Learning) (0) | 2025.02.25 |
| LOF(Local Outlier Factor) 실습 - PYOD 사용 및 직접 구현 (1) | 2024.12.26 |
| LOF(Local Outlier Factor) - 이론,개념 (0) | 2024.12.24 |
| [Anomaly Detection] 이상탐지 개요 (2) | 2024.12.18 |