데이터 전처리/머신러닝 모델의 성능 향상을 위한 전처리

클래스 불균형 문제 해결방법(1) 재샘플링

97dingdong 2022. 12. 27. 16:41

재샘플링은 클래스 비율이 맞지 않아서(클래스 변수가 불균형 한 것을) 처리하기 위한 방법이다. 

→클래스 불균형 발생 원인 첫번째 case를 처리하기 위한 방법

 

오버샘플링과 언더샘플링

언더샘플링은 결정 경계에 가까운 다수 클래스 샘플을 제거하고, 오버샘플링은 결정 경계에 가까운 소수 클래스 샘플을 생성해야 한다. 왜냐하면 기본적으로 클래스 불균형 문제 해소는 소수 클래스에 대한 결정 공간을 넓히는 것이기 때문이다. 

아래 그림에서 현재 결정 경계가 주황색 선으로 표시되어있다면 화살표 방향대로 움직여야 결정 공간이 넓어질 것이다. 

소수 클래스 샘플을 더 만들어야 하는 오버샘플링일때 결정 경계에 가까운 소수 클래스 샘플을 만들어야 결정 경계가 앞쪽으로 밀려서 결정 공간이 넓어질 것이다. 다수 클래스 샘플을 제거해야하는 언더샘플링일 때 결정 경계에 가까운 다수 클래스 샘플을 제거해야 결정 경계가 앞쪽으로 밀려서 결정 공간이 넓어질 것이다.(군사 경계선을 생각하면 이해하기가 쉽다.)

재샘플링할때 주의 사항으로는 평가 데이터에 대해서는 절대로 재샘플링을 적용하면 안된다.

오버샘플링을 평가 데이터에 썼을 때 가짜 데이터를 포함한 상태에서 평가를 하는 것이니 당연히 객관적인 평가 결과가 아니다. 그리고 가짜로 만든 데이터는 우리가 가지고 있는 데이터와 흡사하게 만들어 질수 밖에 없어서 학습 데이터와 평가 데이터가 굉장히 유사해진다는 소리이다. 그래서 평가 결과가 좋아지겠지만 그 결과는 신뢰할만한 결과가 아니다.

언더샘플링을 평가 데이터에 썼을 때 평가 데이터에서 제거되는 것은 보통 평가하기 어려운 샘플들이 제거된다. 그렇기 때문에 평가 결과가 좋아지겠지만 그 결과는 신뢰할만한 결과가 아니다. (ex.평가를 100개 했어야 했는데 언더샘플링을 함에 따라 50개만 하게된다. 그리고 제거되는 것은 보통 평가하기 어려운 샘플들이 제거된다.)

 

대표적인 오버샘플링 알고리즘: SMOTE

SMOTE(Synthetic Minority Over-Sampling Technique)는 2002년에 제안된 기법으로, 대부분의 오버 샘플링 기법이 이 기법에 기반하고 있다.

SMOTE 원리

소수 클래스 샘플을 임의로 선택하고, 선택된 샘플의 소수 클래스 이웃 가운데 하나의 샘플을 또 임의로 선택하여 그 중간에 샘플을 생성하는 과정을 반복하는 방법이다.

default는 소수 클래스 샘플을 다수 클래스 샘플과 동일하게 1대1로 만드는데, 이럴경우 재현율은 높아지지만 정확도가 너무 떨어지는 상황이 발생할 수 있다. 즉, 클래스 불균형 문제를 완전 해소하는 것은 오히려 역효과를 일으킬수 있기 때문에 적당한 수치 내에서 조정하는 것이 필요하다. 정답은 없지만 보통 3대1, 4대1 정도로 만든다.

 

●관련 문법: imblearn.over_sampling.SMOTE

-주요 입력

▶sampling_strategy: 입력하지 않으면 1대1 비율이 맞을 때까지 샘플을 생성하며, 사전 형태로 입력하여 클래스별로 샘플 개수를 조절 가능

▶k_neighbors: SMOTE에서 고려하는 이웃 수(보통 1, 3, 5 정도로 작게 설정)

 

-주요 메서드

▶.fit_resample(X, Y): X와 Y에 대해 SMOTE를 적용한 결과를 ndarray 형태로 반환

 

→sklearn의 scaler, preprocessing 인스턴스랑 굉장히 유사하지만 fit이 없고 fit과 sample이 따로 있지 않고 붙어서 쓴다.(sample은 transform이랑 비슷하다고 생각)그 이유는 재샘플링은 평가 데이터에 대해서는 절대로 적용하면 안되기 때문이다. 그러면 평가 데이터가 없는데 굳이 fit 따로 sample 따로 할 필요가 없어서 fit_sample 한번 하고 끝낸다. 그래서 fit 따로 sample 따로 하는 구문이 없고 fit_sample로 한번에 코드를 작성한다.

 

 

대표적인 언더샘플링 알고리즘: NearMiss

가장 가까운 n개의 소수 클래스 샘플까지 평균 거리가 짧은 다수 클래스 샘플을 순서대로 제거하는 방법이다.

NearMiss 원리

평균 거리가 짧은 다수 클래스 샘플을 왜 제거하는지 알아보면 소수 클래스 샘플과 가깝다는 것은 결정 경계 주위에 있다고 볼 수 있다. 그렇기 때문에 결정 경계에 가까운 것을 지우기 위해서 소수 클래스 샘플까지 거리를 활용한다고 볼 수 있다.

n은 소수 클래스 샘플 개수만큼 설정하는 것이 일반적이다.

 

●관련 문법: imblearn.under_sampling.NearMiss

-주요 입력

▶sampling_strategy: 입력하지 않으면 1대1 비율이 맞을 때까지 샘플을 제거하며, 사전 형태로 입력하여 클래스별로 샘플 개수를 조절 가능

▶n_neighbors: 평균 거리를 구하는 소수 클래스 샘플 수

▶version: NearMiss의 version으로, 2를 설정하면 모든 소수 클래스 샘플까지의 평균 거리를 사용

→버전이 1, 2, 3이 있고 그 중 제일 많이 사용하는 버전이 2이다. 버전 2는 n_neighbors를 모든 소수 클래스 샘플 수랑 같게 설정하는 것이다. 버전 1(위의 예시는 버전 1)을 썼을 때 평균 거리를 구하는 소수 클래스 샘플 수(n_neighbors)를 설정해야 하기 때문에 즉, 하이퍼 파라미터가 늘어나기 때문에 하이퍼 파라미터를 설정하는데 근거도 없고 하나하나 비교했을 때 성능 차이가 크지 않기 때문에 보통 버전 2를 사용한다.

 

-주요 메서드

▶.fit_resample(X, Y): X와 Y에 대해 NearMiss를 적용한 결과를 ndarray 형태로 반환

 


## 코드 실습 ##

●오버샘플링-SMOTE 사용

import os
os.chdir(r"C:\Users\82102\Desktop\study\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("Secom.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Y', axis=1)
Y = df['Y']
# 학습 데이터와 평가 데이터 분할
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 클래스 불균형 확인
# 언더샘플링을 적용하기에는 클래스 1에 대한 값이 너무 적어서 남는 샘플이 너무 부족해지기 때문에 부적절하다고 판단
y_train.value_counts()

# 클래스 불균형 비율 계산
y_train.value_counts().iloc[0] / y_train.value_counts().iloc[-1]

# KNN을 사용한 클래스 불균형 테스트
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import *

knn_model = KNN(n_neighbors = 11).fit(x_train, y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율이 0%로 불균형이 심각한 수준

# 오버샘플링:SMOTE
from imblearn.over_sampling import SMOTE

# SMOTE 인스턴스 생성
oversampling_instance = SMOTE(k_neighbors = 3) # sampling_strategy를 설정 안했기 때문에 default로 1대1 비율로 맞춰짐

# 오버샘플링 적용
o_x_train, o_y_train = oversampling_instance.fit_resample(x_train, y_train)

# ndarray 형태로 반환되므로 DataFrame과 Series로 변환
o_x_train = pd.DataFrame(o_x_train, columns = X.columns)
o_y_train = pd.Series(o_y_train)
o_y_train.value_counts() # 클래스 비율이 1대1이 됨을 확인

# 같은 모델로 다시 평가(클래스 비율이 1대1)
knn_model = KNN(n_neighbors = 11).fit(o_x_train, o_y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 정확도는 감소했으나, 재현율이 크게 오름을 확인
# 결론: 재현율이 크게 오른 것은 좋지만 정확도가 너무 떨어졌기 때문에 정확도를 어느정도 보장을 한 상태에서 재현율을 올리고 싶다고 판단

# 클래스 비율이 1대1이 아닌 2대1로 설정
# SMOTE 인스턴스 생성
oversampling_instance = SMOTE(k_neighbors = 3, sampling_strategy = {1:int(y_train.value_counts().iloc[0] / 2),
                                                                   -1:y_train.value_counts().iloc[0]})

# 오버샘플링 적용
o_x_train, o_y_train = oversampling_instance.fit_resample(x_train, y_train)

# ndarray 형태로 반환되므로 DataFrame과 Series로 변환
o_x_train = pd.DataFrame(o_x_train, columns = X.columns)
o_y_train = pd.Series(o_y_train)
o_y_train.value_counts() # 클래스 비율이 2대1이 됨을 확인

# 같은 모델로 다시 평가(클래스 비율이 2대1)
knn_model = KNN(n_neighbors = 11).fit(o_x_train, o_y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율은 아까보다 좋아지지 않았지만 정확도는 조금 보장됨

 

 

●언더샘플링-NearMiss 사용

import os
os.chdir(r"C:\Users\82102\Desktop\study\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("page-blocks0.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Class', axis=1)
Y = df['Class']
# 학습 데이터와 평가 데이터 분할
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 클래스 불균형 확인
y_train.value_counts()
# positve 즉, 클래스 1에 대한 값을 negative 즉, 클래스 -1에 맞춰 오버샘플링을 진행하면 데이터가 너무 많아지고 계산량이 많아지기 때문에
# 언더샘플링이 적합하다고 판단

y_train.replace({"negative":-1, "positive":1}, inplace=True)
y_test.replace({"negative":-1, "positive":1}, inplace=True)
# 클래스 불균형 비율 계산
y_train.value_counts().iloc[0] / y_train.value_counts().iloc[-1]
# 9를 넘지 못하지만 약간 심각한 수치라고 봄

# KNN을 사용한 클래스 불균형 테스트
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import *

knn_model = KNN(n_neighbors = 11).fit(x_train, y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율이 0%가 아니고 50%로 불균형이 심각한 수준은 아니라고 보임
# 일단 언더샘플링을 진행해본다

# 언더샘플링:NearMiss
from imblearn.under_sampling import NearMiss

# NearMiss 인스턴스 생성
NM_model = NearMiss(version = 2) # version = 2: 모든 소수 클래스 샘플까지의 평균 거리를 사용

# 언더샘플링 적용
u_x_train, u_y_train = NM_model.fit_resample(x_train, y_train)

# ndarray 형태로 반환되므로 DataFrame과 Series로 변환
u_x_train = pd.DataFrame(u_x_train, columns = X.columns)
u_y_train = pd.Series(u_y_train)
u_y_train.value_counts() # 클래스 비율이 1대1이 됨을 확인

# 같은 모델로 다시 평가(클래스 비율이 1대1)
knn_model = KNN(n_neighbors = 11).fit(u_x_train, u_y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율은 크게 올랐으나, 정확도가 크게 떨어짐 -> 적당한 비율에 맞게 설정해야 함

# 클래스 비율이 1대1이 아닌 5대1로 설정
# NearMiss 인스턴스 생성
NM_model = NearMiss(version = 2, sampling_strategy = {1:y_train.value_counts().iloc[-1],
                                                     -1:y_train.value_counts().iloc[-1] * 5})

# 언더샘플링 적용
u_x_train, u_y_train = NM_model.fit_resample(x_train, y_train)

# ndarray 형태로 반환되므로 DataFrame과 Series로 변환
u_x_train = pd.DataFrame(u_x_train, columns = X.columns)
u_y_train = pd.Series(u_y_train)
u_y_train.value_counts() # 클래스 비율이 5대1이 됨을 확인

# 같은 모델로 다시 평가(클래스 비율이 5대1)
knn_model = KNN(n_neighbors = 11).fit(u_x_train, u_y_train)
pred_y = knn_model.predict(x_test)
print('재현율:', recall_score(y_test, pred_y))
print('정확도:', accuracy_score(y_test, pred_y))
# 재현율도 올랐고 정확도도 약간 보장됨


정확도와 재현율의 변화 추이를 보고 적절한 클래스 비율을 선택해야한다.