SMOTE
Synthetic Minority Over-sampling TEchnique
-
ref: N. V. Chawla, K. W. Bowyer, L. O.Hall, W. P. Kegelmeyer, "SMOTE:
synthetic minority over-sampling technique," Journal of artificial
intelligence research, 321-357, 2002.
- 글을 읽기 전 주의 사항
-
이진 분류인 경우 "마이너 클래스$^c$ = 메이저 클래스"이지만 다중 분류에서는 성립하지 않는다
-
작성한 글에서는 이진 분류, 다중 분류를 고려하지 않고 마이너 클래스와 메이저 클래스만 언급했는데 다중 분류인 경우 메이저 클래스를 마이너 클래스가 아닌 클래스로 생각하자
-
논문에 등장한 SMOTE는 이진 분류를 가정한다
-
SMOTE를 사용하기 위해선 변수들이 모두 양적 변수여야 한다 (데이터의 차원은 $n\times p$, 이때 $n$은 샘플의 개수, $p$는 변수의 개수)
1.
마이너 클래스의 각 샘플에 대하여 가장 가까운 마이너 클래스 이웃 $k$개의 인덱스를 계산한다 (유클리드 거리를 사용)
2.
합성에 사용하기 위해 마이너 클래스에서 $N$개의 샘플을 복원추출하여 인덱스를 저장한다
3.
선택된 $N$개의 샘플 각각에 대하여 $k$개의 가장 가까운 이웃중 무작위로 하나를 선택한다
4.
$N$개의 샘플 각각에 대하여 샘플과 그 샘플의 이웃간의 내분점을 무작위로 선택하여 새로운 샘플을 생성한다 (모든 변수에 대해 내분비는 동일)
X1 | X2 | class | |
---|---|---|---|
i1 | 3 | 7 | A |
i2 | 4 | 8 | A |
i3 | 2 | 5 | B |
i4 | 1 | 2 | B |
i5 | 2 | 1 | B |
i6 | 0 | 3 | B |
-
마이너 클래스는 A
, 메이저 클래스는 B
이다
-
합성할 샘플 수 $N$은 $1$로 하겠다, 고려할 이웃의 개수 $k$는 1로 하겠다
1.
마이너 클래스 샘플끼리의 거리를 계산한다
$$Dist(i_1,i_2) = \sqrt{(3-4)^2 + (7-8)^2} = \sqrt{2}$$
2.
마이너 클래스 샘플 중 무작위로 $N$개를 복원추출한다 $\Longrightarrow i_1$이 선택됐다고 가정
3.
$i_1$에 가장 가까운 이웃 $k$개를 선택한다 $\Longrightarrow i_2$가 선택됐다고 가정
4.
새로운 샘플을 생성: $$i_7 = i_1 + 0.5*(i_2-i_1) = (3.5, 7.5),\; (\text{0.5는 내분비로 0과 1 사이에 있는 선택된 임의의 수})$$
5.
합성된 샘플 $i_7$의 class는 A
이다
-
위에선 표준화를 생략했지만 정확한 결과를 얻기 위해서는 표준화를 수행해야 한다
-
메이저 클래스의 결정 영역에 존재하는 마이너 클래스 샘플을 단순 복제할 경우 해당 마이너 클래스 샘플의 영향력이 커진다
-
즉, 마이너 클래스의 결정 영역이 더욱 특별해지고 결정 트리는 새로운 분기를 생성하게 되어 오버피팅이 발생한다
-
마이너 클래스의 단순 복제는 결정 영역을 메이저 클래스의 결정 영역으로 확장시키지 못한다
-
하지만 SMOTE를 사용하면 기존에 존재하지 않던 새로운 마이너 클래스 샘플을 만들므로 마이너 클래스의 결정 영역을 확장시킬 수 있다
-
합성된 샘플은 더 크고 덜 중요한 결정 영역을 만듦으로써 분류기가 작고 특정한 결정 영역에 오버피팅 되지 않고 일반화되도록 한다
-
마이너 클래스 샘플의 가장 가까운 $k$개의 이웃 대부분이 마이너 클래스가 아닐 수 있다
-
즉, 마이너 클래스에 분산이 큰 변수가 존재하는 경우 합성된 샘플이 메이저 클래스 샘플과 겹칠 수 있다
-
이상치에 민감하다
-
유클리드 거리를 사용하므로 변수의 영향력을 동일시하기 위해 변수마다 표준화를 수행해야 한다
-
훈련 데이터에만 적용해야 한다 (합성된 샘플이 검증 데이터를 대표하지 않을 경우 검증 결과를 신뢰할 수 없음)
-
$k$는 $1$보다 큰 값을 사용해야 한다 ($k=1$일 경우 모집단의 분포를 따르지 않는 이상한 분포로 오버샘플링 된다)
- $k$값에 따른 SMOTE 비교
-
각 클래스별 군집은 $1$개로 설정하여 데이터를 생성했다
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_classification
from imblearn.over_sampling import SMOTE
## 무작위 데이터 생성
data, label = make_classification(n_features=2, n_informative=2, n_redundant=0, n_samples=150, n_clusters_per_class=1, weights=[0.9], random_state=111)
label = np.where(label == 0, 'majority', 'minority')
## 그래프
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(10, 8))
# 기존 데이터
sns.scatterplot(x=data[:, 0], y=data[:, 1], hue=label, alpha=0.8, ax=ax1)
ax1.set_title('original')
# k = 1
data_res, label_res = SMOTE(k_neighbors=1, random_state=23).fit_resample(data, label)
sns.scatterplot(x=data_res[:, 0], y=data_res[:, 1], hue=label_res, alpha=0.8, ax=ax2)
ax2.set_title('SMOTE with k = 1')
# k = 5
data_res, label_res = SMOTE(k_neighbors=5, random_state=23).fit_resample(data, label)
sns.scatterplot(x=data_res[:, 0], y=data_res[:, 1], hue=label_res, alpha=0.8, ax=ax3)
ax3.set_title('SMOTE with k = 5')
# k = 11
data_res, label_res = SMOTE(k_neighbors=11, random_state=23).fit_resample(data, label)
sns.scatterplot(x=data_res[:, 0], y=data_res[:, 1], hue=label_res, alpha=0.8, ax=ax4)
ax4.set_title('SMOTE with k = 11')
fig.tight_layout()
plt.show()
-
코드: https://github.com/scikit-learn-contrib/imbalanced-learn/blob/master/imblearn/over_sampling/_smote/base.py#L217 (0.10.1 버전)
-
다중 분류에서도 SMOTE를 사용할 수 있게 만들었다
- 가장 가까운 이웃 $k$개 계산
-
check_neighbors_object
함수를 사용해 nn_k_
라는 sklearn.neighbors.NearestNeighbors
인스턴스를 생성하여 가장 가까운 $k$개의 이웃을 계산한다
-
참고로 자기 자신과의 거리는 $0$이므로 임의의 샘플에 대하여 가장 가까운 $k$개의 이웃을 찾으면 그 중 하나는 자기 자신이다
-
그래서 check_neighbors_object
함수의 additional_neighbor
매개변수의 기본값을 $1$로 주어 자기 자신을 제외한 가장 가까운 $k$개의 이웃을 찾는다
- 거리 계산
-
sklearn.neighbors.NearestNeighbors
오브젝트는 기본값으로 거리 계산 메트릭으로 유클리드 거리를 사용한다
-
거리 계산 알고리즘으론 BallTree
, KDTree
, brute-force search
중에서 적절한 알고리즘을 찾아서 사용한다
-
적절한 알고리즘을 찾는 방법: https://scikit-learn.org/stable/modules/neighbors.html#nearest-neighbor-algorithms
- 샘플링 전략
-
sampling_strategy
매개변수를 사용하여 클래스 라벨(i.g. male)에 합성할 샘플 수(i.g. 150)를 대응시킨 딕셔너리를 만든다 (= sampling_strategy_
)
-
sampling_strategy_
딕셔너리를 순회하여 클래스를 합성할 샘플 수 만큼 오버샘플링 한다
-
sampling_strategy
매개변수를 사용하여 sampling_strategy_
딕셔너리를 만든다
-
_fit_resample
메소드 호출한다
-
_validate_estimator
메소드를 호출하여 nn_k
라는 sklearn.neighbors.NearestNeighbors
인스턴스를 생성한다
-
sampling_strategy_
를 순회하여 각 클래스(X_class
)마다 새로운 샘플을 합성한다
-
nn_k
에 X_class
를 학습시켜 X_class
의 각 샘플에 대하여 가장 가까운 $k$개의 이웃의 인덱스를 계산하여 nns
에 저장한다 (nns
의 차원은 (X_class의 원소 개수, k))
-
_make_samples
메소드를 호출한다
- 주요 파라미터
-
nn_data
: X_class
와 동일 (n, p)
-
nn_num
: nns
와 동일 (n, k)
-
n_samples
: 합성할 샘플 수
- 함수 구현부
-
합성할 데이터 샘플링: $0$부터 $nk$중에서 n_samples
만큼 복원추출하여 samples_indices
에 저장한다
-
steps
는 내분비($0\sim 1$), n_samples
크기의 벡터
-
rows
는 X_class
에서 합성을 위해 인덱싱하기 위한 n_samples
크기의 벡터
-
cols
는 $k$개의 이웃 중 몇 번째 이웃을 합성하는데 사용할지 나타낸 n_samples
크기의 벡터
-
_generate_samples
메소드를 호출한다
-
합성 공식에 따라 샘플을 합성하는 단계
-
diffs = nn_data[nn_num[rows, cols]] - X[rows]
(X
와 X_class
는 동일)
-
X_new = X[rows] + steps * diffs