머신러닝 기초 (16) - 이상치 탐지
대다수와 다른 소수를 찾는 이상치 탐지를 통계적, 밀도 기반, 모델 기반 세 접근으로 정리하고 IsolationForest와 LOF로 확인합니다. 시리즈를 마무리하며 지도학습과 비지도학습을 한자리에 정리한 16편입니다.
머신러닝 기초 시리즈의 16편입니다. 15편에 이어 비지도학습의 마지막 주제인 이상치 탐지를 다루고, 시리즈를 마무리합니다.
14편의 군집화와 15편의 차원 축소는 데이터 전체의 구조를 다뤘습니다. 비지도학습의 마지막 주제는 방향이 반대입니다. 구조를 따르는 다수가 아니라, 그 구조에서 벗어난 소수를 찾습니다. 이상치 탐지입니다.
이상치 탐지란
이상치 탐지는 대다수 정상 데이터와 눈에 띄게 다른 소수의 관측을 찾는 문제입니다. 정상은 많고 서로 비슷하며, 이상치는 드물고 정상에서 벗어나 있습니다. 이 비대칭이 문제의 성격을 정합니다.
쓰이는 곳은 대체로 다음과 같습니다.
- 사기 거래 탐지: 평소 결제 패턴과 크게 다른 거래를 걸러냅니다.
- 장비 결함 감지: 센서 값이 정상 범위를 벗어나면 고장의 전조로 봅니다.
- 모니터링 알람: 서버 응답 시간이나 트래픽이 평소와 다르게 튀면 알립니다.
이 문제는 대부분 레이블 없이 푸는 비지도 문제입니다. 이유가 두 가지입니다. 첫째, 이상치는 드물어서 정답을 붙인 사례를 충분히 모으기 어렵습니다. 둘째, 이상치가 어떤 형태로 나타날지 미리 알 수 없습니다. 다음에 올 새로운 사기 수법이나 처음 보는 고장 유형은 과거 레이블에 없습니다. 그래서 “이상치가 무엇인지” 배우기보다 “정상이 무엇인지” 익힌 뒤 거기서 벗어난 것을 이상치로 봅니다.
접근 세 가지
무엇을 기준으로 “벗어났다”고 판단하느냐에 따라 접근이 갈립니다. 통계적, 밀도 기반, 모델 기반 셋을 봅니다.
통계적: z-score와 IQR
가장 단순한 방법은 특성 하나의 분포를 보고 극단값을 자르는 것입니다.
z-score는 값이 평균에서 표준편차 몇 배만큼 떨어져 있는지를 잽니다. z = (x - x.mean()) / x.std()로 구하고, 관례적으로 abs(z) > 3이면 이상치로 봅니다. 다만 이 기준은 데이터가 하나의 봉우리를 가진 정규분포에 가깝다고 가정합니다.
IQR은 분포 가정을 덜 합니다. 1사분위수 Q1과 3사분위수 Q3, 그 차이인 IQR = Q3 - Q1을 구한 뒤, Q1 - 1.5 * IQR보다 아래이거나 Q3 + 1.5 * IQR보다 위인 값을 이상치로 봅니다. 상자그림(boxplot)의 수염 바깥이 이 기준입니다.
두 방법의 한계는 같습니다. 기본적으로 특성 하나씩 따로 봅니다. 각 특성은 정상 범위인데 여러 특성의 조합이 이상한 경우, 예를 들어 나이는 정상이고 결제액도 정상이지만 그 둘의 조합이 드문 경우는 놓칩니다. 여러 특성이 함께 얽힌 이상치에는 다차원을 보는 방법이 필요합니다.
밀도 기반: LOF
LOF(Local Outlier Factor)는 한 점 주변이 얼마나 붐비는지를 봅니다. 이웃과 비교해 자기 주변 밀도가 뚜렷하게 낮으면 이상치입니다. 핵심은 “국소”입니다. 전체 평균 밀도가 아니라 각 점의 이웃 밀도와 비교하므로, 조밀한 군집과 성긴 군집이 한 데이터에 섞여 있어도 각자의 기준으로 판단합니다.
모델 기반: IsolationForest
IsolationForest는 데이터를 무작위로 잘라 나가며 각 점이 혼자 남기까지 몇 번 잘라야 하는지를 셉니다. 정상 점은 비슷한 점들에 둘러싸여 있어 여러 번 잘라야 고립되고, 이상치는 동떨어져 있어 몇 번만 잘라도 혼자 남습니다. 이 격리 깊이가 짧을수록 이상치로 봅니다. 밀도를 직접 계산하지 않고 트리 분할만으로 판단해서 특성 수가 많아도 비교적 빠릅니다.
contamination: 예상 이상치 비율
세 접근 모두 실제로는 각 점에 이상 정도를 나타내는 점수를 매깁니다. 여기서 “점수가 얼마나 낮아야 이상치인가”라는 경계가 필요합니다. scikit-learn에서는 이 경계를 contamination, 즉 전체에서 이상치로 볼 비율로 정합니다. contamination=0.1이면 점수가 낮은 하위 10%를 이상치로 자릅니다.
문제는 이 값을 데이터가 알려주지 않는다는 점입니다. 레이블이 없으니 진짜 비율을 모릅니다. 그래서 도메인 지식이나 가정으로 넣습니다.
contamination은 데이터에서 자동으로 추정되는 값이 아니라 사람이 넣는 가정입니다. 실제보다 높게 잡으면 정상 데이터가 이상치로 몰려 오탐이 늘고, 낮게 잡으면 진짜 이상치를 정상으로 흘려보냅니다. 도메인에서 이상 비율을 대략이라도 알면 그 값을 넣고, 모르면 결과를 보며 조정합니다.
코드로 확인
정상 군집과 소수 이상치를 만들어 두 방법을 돌려 봅니다. make_blobs로 조밀한 군집 두 개를 정상으로 만들고, 넓은 범위에 균일하게 흩뿌린 점을 이상치로 섞습니다. 뒤쪽 20개가 진짜 이상치입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
rng = np.random.RandomState(42)
# 정상 데이터: 조밀한 군집 두 개, 총 200개
X_normal, _ = make_blobs(n_samples=200, centers=[[0, 0], [5, 5]],
cluster_std=0.6, random_state=42)
# 이상치: 넓은 범위에 흩뿌린 20개 (뒤쪽 20개가 진짜 이상치)
X_outliers = rng.uniform(low=-4, high=9, size=(20, 2))
X = np.vstack([X_normal, X_outliers])
X.shape[0] # 220
이제 두 방법으로 예측합니다. 둘 다 fit_predict가 각 점에 -1(이상치) 또는 1(정상)을 돌려줍니다. 예상 이상치 비율은 20/220으로 약 0.09이므로 contamination=0.09로 둡니다.
1
2
3
4
5
6
7
iso = IsolationForest(contamination=0.09, random_state=42)
pred_iso = iso.fit_predict(X) # -1 이상치, 1 정상
(pred_iso == -1).sum() # 20
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.09)
pred_lof = lof.fit_predict(X) # 같은 규칙: -1 이상치
(pred_lof == -1).sum() # 20
두 방법 모두 220개 중 20개를 이상치로 지목했습니다. contamination=0.09가 하위 약 9%인 20개를 자른 결과입니다. 이번에는 어느 점을 주입했는지 알고 있으니, 그 20개 중 실제로 몇 개를 맞혔는지 확인해 봅니다.
1
2
3
4
5
true = np.zeros(220, dtype=bool)
true[200:] = True # 뒤쪽 20개가 주입한 이상치
(true & (pred_iso == -1)).sum() # 16
(true & (pred_lof == -1)).sum() # 16
| 방법 | 이상치로 예측 | 주입한 20개 중 맞힌 수 |
|---|---|---|
| IsolationForest | 20개 | 16개 |
| LocalOutlierFactor | 20개 | 16개 |
두 방법 다 지목한 20개 중 16개가 진짜 이상치였습니다. 나머지 4개는 정상 군집의 가장자리 점을 잘못 지목한 것이고, 주입한 이상치 중 4개는 우연히 군집 안쪽에 떨어져 놓쳤습니다. 균일하게 흩뿌리다 보면 몇 개는 정상 군집 위에 겹치는데, 그런 점은 값만 봐서는 정상과 구별되지 않습니다.
LocalOutlierFactor는 기본적으로 학습에 쓴 데이터 자체의 이상치를 판별합니다. 학습에 없던 새 데이터를 판별하려면novelty=True로 두고fit후predict를 따로 호출해야 합니다.IsolationForest는 학습한 모델로 새 데이터에 바로predict할 수 있어, 운영 중 들어오는 데이터에 붙이기 편합니다.
평가의 어려움
위 코드에서는 어느 점이 이상치인지 우리가 알고 있어서 “16개 맞힘”을 말할 수 있었습니다. 그러나 이것은 시연을 위해 일부러 정답을 심어 둔 상황입니다. 실제 이상치 탐지는 정답이 없는 경우가 대부분입니다.
레이블이 없으면 정밀도나 재현율 같은 지표를 계산할 수 없습니다. 모델이 이상치라고 지목한 점이 정말 이상한지 확인하려면 결국 사람이 하나씩 들여다봐야 합니다. 그래서 실무에서는 다음으로 보완합니다.
- 도메인 검토: 지목된 사례를 담당자가 확인해 진짜인지 판단합니다.
- 시각화: 15편의 차원 축소로 2차원에 찍어 이상치가 어디에 떨어지는지 눈으로 봅니다.
- 소수 레이블링: 지목된 일부에만 정답을 붙여 대략의 정밀도를 가늠합니다.
정량 평가가 어렵다는 점은 비지도학습 세 편의 공통 성격입니다. 군집화도 차원 축소도 “정답”이 없어 결과 해석을 사람이 맡았습니다. 이상치 탐지에서도 마찬가지로, 지표 하나로 끝나지 않고 결과를 검토하는 과정이 따라붙습니다.
시리즈 마무리
이것으로 머신러닝 기초 열여섯 편을 마칩니다.
지도학습(1~13편)은 정답 레이블이 있는 문제를 다뤘습니다. 1편의 회귀와 분류에서 시작해, 손실과 경사하강법으로 학습의 원리를 세우고(1~3), 과대적합과 규제로 잘 배우게 만드는 법을 익히고(4~6), 분류와 회귀 평가로 성능을 재고(7~8), 선형 모델부터 앙상블까지 실제 모델과 전처리, 데이터 누수를 다뤘습니다(9~13). 비지도학습(14~16편)은 레이블 없이 데이터의 구조를 찾았습니다. 군집화는 비슷한 것끼리 묶고, 차원 축소는 특성을 압축하며, 이상치 탐지는 나머지와 다른 소수를 골라냅니다. 세 편 모두 정답이 없어 평가가 어렵고 사람의 해석이 필요하다는 성격을 공유합니다.
여기까지가 개념 층입니다. 다음은 이 개념들을 실제 데이터에 붙이는 적용 층입니다. NYC 택시 수요 예측 파이프라인에서 전처리, 모델 선택, 평가, 누수 방지를 하나의 파이프라인으로 엮어, 지금까지의 개념이 실제 문제에서 어떻게 맞물리는지 봅니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
| 이상치 탐지 | 다수 정상과 크게 다른 소수를 찾는 문제 |
| 비지도 성격 | 이상치는 드물고 형태를 미리 몰라 레이블 없이 푸는 경우가 많음 |
| 통계적 | z-score, IQR로 특성 하나의 극단값을 자름. 다차원 조합은 놓침 |
| 밀도 기반(LOF) | 이웃과 비교한 국소 밀도가 낮으면 이상치 |
| 모델 기반(IsolationForest) | 무작위 분할로 빨리 고립되는 점을 이상치로 봄 |
| contamination | 이상치로 볼 비율. 데이터가 아니라 사람이 넣는 가정 |
| 예측값 | fit_predict가 -1(이상치), 1(정상)을 돌려줌 |
| 평가 | 레이블이 없으면 정량 평가가 제한적. 도메인 검토가 따라붙음 |