차원의 저주란?

차원(특징 수)이 증가함에 따라 필요한 데이터의 양과 시간 복잡도가 기하급수적으로 증가하는 문제를 의미한다.

차원이 증가함에 따라서 각 공간에 들어가는 샘플이 적어질 수 밖에 없기 때문에 정확한 판단을 위해서는 샘플이 더 많아질 수 밖에 없다. 그리고 샘플이 더 많게 하기 위해서는 데이터를 더 수집해야 하기 때문에 쉬운 문제가 아니다. 그래서 샘플이 부족하기 때문에 과적합으로 이어질 것이다. 특징이 범주형인 경우에 비해서 연속형인 경우 특징이 늘면 공간이 더 세분화되고 더 늘어날 수 있어서 단순히 특징이 많고 적고도 고려해야하고 특징의 종류도 고려해야한다.

 

●차원을 줄여야 하는 이유(특징 개수를 줄여야 하는 이유)

1. 차원이 증가함에 따라 모델 학습 시간이 정비례하게 증가한다.

2. 차원이 증가하면 각 결정 공간에 포함되는 샘플 수가 적어져, 과적합으로 이어져 성능 저하가 발생할 수 있다.

.

특징이 너무 적으면 과소적합이 일어날 수 있고, 특징이 늘어날수록 모델 성능이 올라가다가 어느 순간에 떨어질 것이다. 이 떨어지는 구간이 과적합이 발생한 구간이다.

 

차원의 저주 해결방법: 특징 선택

특징 선택은 분류 및 예측에 효과적인 특징만 선택하여 차원을 축소하는 방법이다. 구체적으로 설명하자면 n개의 특징으로 구성된 특징 집합 {x1, x2, ..., xn}에서 m < n개의 특징을 선택하여, 새로운 특징 집합 {x'1, x'2, ..., x'm} ⊂ {x1, x2, ..., xn}을 구성하는 방법이다.

특징 선택을 할 때 많이 하는 오해가 있는데 특징 선택을 할 때는 특징이 많은 데이터에만 정의해야 한다고 오해하는 것이다. 즉, 특징 선택은 특징이 많은 데이터에만 적용해야 하는 것은 아니다. 왜냐하면 특징이 적더라도 특징 선택을 했을 때 모델의 성능이 좋아지는 경우가 존재하기 때문이다.

 

## 코드 실습 ##

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

import pandas as pd
df = pd.read_csv("appendicitis.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)
# 특징에 따른 SVM 모델 테스트 함수 작성
from sklearn.svm import SVC
from sklearn.metrics import f1_score

def feature_test(x_train, x_test, y_train, y_test, features):
    s_x_train = x_train[features]
    s_x_test = x_test[features]
    
    model = SVC().fit(s_x_train, y_train)
    pred_y = model.predict(s_x_test)
    return f1_score(y_test, pred_y)

base_score = feature_test(x_train, x_test, y_train, y_test, x_train.columns) # 모든 특징을 썼을 때의 점수
print(base_score)

import itertools
c_list = list(range(1, len(x_train.columns))) # base_score: 모든 특징을 사용한 경우는 제외 
outperform_ratio_list = []
best_score = 0

for c in c_list: # c는 선택한 특징 개수
    print(c)
    c_num = 0 # 특징을 c개 뽑았을 때, 모든 특징을 썼을 때보다 성능이 좋은 경우
    c_dem = 0 # 특징을 c개 뽑는 경우의 수
    
    # itertools.combinations(p,r): 이터레이터 객체 p에서 크기 r의 가능한 모든 조합을 갖는 이터레이터를 생성
    for features in itertools.combinations(x_train.columns, c):
        score = feature_test(x_train, x_test, y_train, y_test, list(features)) # itertools는 tuple 형태로 값을 반환해서 형변환을 해줌
        if score > best_score:
            best_score = score
            best_feature = list(features)
            
        if score > base_score:
            c_num += 1
        
        c_dem += 1
        
    outperform_ratio_list.append(c_num / c_dem)
    # 예를 들어, 특징 개수를 한 개 뽑는다면 6개의 combination이 있는데
    # 6개의 combination의 score값이 base_score보다 큰 경우 c_num을 1씩 더해서 업데이트
    # c_dem은 combination의 값과 동일: 특징 개수를 한 개 뽑는다면 경우의 수가 6개

%matplotlib inline
from matplotlib import pyplot as plt
plt.bar(c_list, outperform_ratio_list)

모든 특징을 썼을 때보다 성능이 좋았던 특징 집합의 비율

best_feature, best_score

위의 코드는 특징이 7개인 데이터에 대해, 모든 특징 집합을 비교해 본 결과이다.

모든 특징을 사용했을 때의 성능 f1-score는 0.2857이지만 특징을 [At1, At4] 두 개만 쓰는 경우 0.7499로 가장 좋은 성능을 확인 할 수 있다.

이런 주먹구구식 특징 선택은 선택 가능한 모든 특징 집합을 비교/평가하여 가장 좋은 특징 집합을 선택할 수 있지만 특징 개수가 많아지면 현실적으로 불가능하다. 즉, 최적의 결과를 보장할 수 있지만 최적의 결과를 얻기까지 시간이 너무 오래걸린다. 

→특징 개수가 n개라면, 2^n-1번의 모형 학습이 필요하므로, 현실적으로 적용 불가능하다. 1초에 1억 번의 모형을 학습할 수 있는 슈퍼 컴퓨터가, 1000개의 특징이 있는 데이터에 대해, 이 방법을 적용하여 가장 좋은 특징 집합을 선택하는데 소요되는 시간은 400조년이다.

 

이런 문제점을 해결하기 위한 방법을 아래에서 볼 것이다.

 

클래스 관련성 기반의 특징 선택

특징과 라벨이 얼마나 관련이 있는지를 나타내는 클래스 관련성이 높은 특징을 우선 선택하는 방법이다.

클래스 관련성은 한 특징이 클래스를 얼마나 잘 설명하는지를 나타내는 척도로 특징과 라벨 간 독립성을 나타내는 통계량상관계수, 카이제곱 통계량, 상호정보량 등을 사용하여 측정한다.(서로 독립적이면 특징은 라벨을 예측하는데 도움이 안될 것이고 서로 종속적이면 특징은 라벨을 예측하는데 도움이 될 것이다.)

즉, 클래스 관련성이 높은 특징은 분류 및 예측에 도움이 되는 특징이며, 그렇지 않은 특징은 도움이 되지 않는 특징이다.

 

예를 들어, 특징이 범주형이고 라벨도 범주형인 상황(범주형 특징-분류)

x1이 1이면 y는 90% 세모라고 분류를 해 줄 것이고, x1이 0이면 y는 90% 동그라미라고 분류를 해 줄 것이다. 원래 데이터의 분포가 5대5였는데, x1이 1이면 y가 세모일 확률이 높아지고, x1이 0이면 y가 동그라미일 확률이 높아지는 것을  보고 x1과 y가 크게 관련이 있다는 것을 알 수 있다.

반대로 x2를 보면 x2가 1이거나 0이거나 관계없이 둘 다 5대5로 똑같다. 다시 말해 원래 데이터의 분포도 5대5(동그라미 100개, 세모 100개)였는데, x2가 주어지던 안주어지던 상관이 없다. 그러면 x2를 알고 있으나 모르고 있으나 크게 도움이 안되니까 x2는 라벨을 분류하는데 도움이 안되는 특징이다.

 

클래스 관련성 척도 중 대표적으로 많이 쓰는 F-통계량을 살펴보겠다.

F-통계량은 일원분산분석(ANOVA)에서 사용하는 통계량으로, 집단 간 평균 차이가 있는지를 측정하기 위한 통계량이다.

x1의 값에 따라 y 분류가 어느정도 가능하지만, x2는 그렇지 않다.

 

★클래스 관련성 척도 분류

(범주형 변수를 더미화 했기 때문에 특징 유형이 이진형이다.)

●관련 함수: sklearn.feature_selection.SelectKBest

-주요 입력

▷scoring_func: 클래스 관련성 측정 함수(예: chi2, mutual_info_classif 등)

▷k: 선택하는 특징 개수

 

-주요 메서드

▷.fit, .transform, .fit_transform: 특징을 선택하는데 사용하는 메서드

▷.get_support(): 선택된 특징의 인덱스를 반환

 

-주요 속성

▷scores_: scoring_func으로 측정한 특징별 점수

▷pvalues_: scoring_func으로 측정한 특징별 p-value(1에 가까울수록 독립적이며, 0에 가까울수록 관련성이 높음→작을수록 좋은 특징)

→score자체를 보는 경우는 드물고 pvalue를 바탕으로 독립적인지 아닌지를 측정한다. pvalue를 보는 경우는 k값을 따로 설정하지 않고 pvalue가 얼마 미만인 경우만 고르는 경우라던가 특징 유형이 다른 경우에 관련 함수가 달라지기 때문에 그 때 통계량을 비교하기 어려우니 pvalue로 비교한다. 

특징 유형이 다른 경우 pvalue로 비교하는 방법 말고 다른 방법은 상호 정보량을 이용한다.


## 코드 실습 ##

●모든 특징 유형이 같은 경우의 특징 선택

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

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

# 특징과 라벨 분리
X = df.drop('Y', axis=1)
Y = df['Y']
# 모든 특징 연속형
for i in X.columns:
    print(i, len(df[i].unique()))
Band1 65
Band2 83
Band3 90
Band4 92
Band5 112
Band6 134
Band7 133
Band8 139
Band9 154
Band10 162
Band11 164
Band12 168
Band13 168
Band14 169
Band15 176
Band16 179
Band17 177
Band18 175
Band19 173
Band20 179
Band21 187
Band22 183
Band23 173
Band24 175
Band25 181
Band26 171
Band27 173
Band28 168
Band29 179
Band30 182
Band31 190
Band32 184
Band33 185
Band34 184
Band35 188
Band36 188
Band37 180
Band38 172
Band39 166
Band40 183
Band41 175
Band42 172
Band43 173
Band44 154
Band45 162
Band46 151
Band47 147
Band48 131
Band49 98
Band50 50
Band51 45
Band52 39
Band53 32
Band54 31
Band55 29
Band56 27
Band57 27
Band58 28
Band59 29
Band60 24
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 라벨이 문자이니 변환
y_train.replace({"M":-1, "R":1}, inplace=True)
y_test.replace({"M":-1, "R":1}, inplace=True)
x_train.shape

# 특징 선택 전 성능 확인(특징이 연속형이고 분류 문제)
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import f1_score

model = KNN().fit(x_train, y_train)
pred_y = model.predict(x_test)
print(f1_score(y_test, pred_y))

# 특징 선택 수행
from sklearn.feature_selection import *

selector = SelectKBest(f_classif, k = 30)
selector.fit(x_train, y_train)
selected_features = x_train.columns[selector.get_support()]

s_x_train = pd.DataFrame(selector.transform(x_train), columns = selected_features)
s_x_test = pd.DataFrame(selector.transform(x_test), columns = selected_features)
# transform을 해도되지만 get_support()를 통해 추출한 특징을 바로 슬라이싱 가능
# s_x_train = x_train[selected_features], s_x_test = x_test[selected_features]
# 특징 선택 후 성능 확인
model = KNN().fit(s_x_train, y_train)
pred_y = model.predict(s_x_test)
print(f1_score(y_test, pred_y))

# 특징 개수 튜닝
best_score = 0

for k in range(5, 50, 5):
    selector = SelectKBest(f_classif, k = k)
    selector.fit(x_train, y_train)
    selected_features = x_train.columns[selector.get_support()]
    
    s_x_train = pd.DataFrame(selector.transform(x_train), columns = selected_features)
    s_x_test = pd.DataFrame(selector.transform(x_test), columns = selected_features)
    
    model = KNN().fit(s_x_train, y_train)
    pred_y = model.predict(s_x_test)
    score = f1_score(y_test, pred_y)
    if score > best_score:
        best_score = score
        best_k = k
    
print(best_score, best_k)

 

 

●특징 유형이 다른 경우의 특징 선택

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

import pandas as pd
df = pd.read_csv("australian.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)
x_train.shape

# 특징 선택 전 성능 확인
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.metrics import f1_score

model = KNN().fit(x_train, y_train)
pred_y = model.predict(x_test)
print(f1_score(y_test, pred_y))

데이터 설명서를 보면 연속형 특징과 범주형 특징이 섞여있고 분류 문제이다.

# 유니크한 값의 개수를 바탕으로 연속형과 범주형 변수 구분
# type과 상태공간을 확인해야하지만 3을 기준으로 연속형인지 범주형인지 구분 할 수 있다고 가정
continuous_cols = [col for col in x_train.columns if len(x_train[col].unique()) > 3]
category_cols = [col for col in x_train.columns if len(x_train[col].unique()) <= 3]

이 데이터는 분류 문제이기 때문에 범주형 특징에 대해서는 카이제곱 통계량이 적절하고, 연속형 특징에 대해서는 F-통계량이 적절하다. 카이제곱 통계량과 F-통계량을 통계량 기준으로 구분하기 쉽지 않기 때문에 pvalue만 계산해서 pvalue가 작은 것 순서대로 선택을 할 것이다.

# 연속형 변수에 대해서는 f_classif, 범주형 변수에 대해서는 chi2를 적용
from sklearn.feature_selection import *

# scoring_func(x, y)의 결과 -> (statistics, p-value)
continuous_cols_pvals = f_classif(x_train[continuous_cols], y_train)[1]
category_cols_pvals = chi2(x_train[category_cols], y_train)[1]
print(continuous_cols_pvals)
print('\n')
print(category_cols_pvals)

# 각각을 Series로 변환(value: pvalue, index: column name)
continuous_pvals_s = pd.Series(continuous_cols_pvals, index = continuous_cols)
category_pvals_s = pd.Series(category_cols_pvals, index = category_cols)
continuous_pvals_s

category_pvals_s

# 두 Series를 합친 뒤, 오름차순으로 정렬
pvals = pd.concat([continuous_pvals_s, category_pvals_s])
pvals.sort_values(ascending=True, inplace=True)
pvals
# pvalue가 작은 것 순서대로 선택(앞에 나오는 특징부터 좋은 특징)

# 특징 선택
k = 10

s_x_train = x_train[pvals.iloc[:k].index]
s_x_test = x_test[pvals.iloc[:k].index]
# 특징 선택 후 성능 확인
model = KNN().fit(s_x_train, y_train)
pred_y = model.predict(s_x_test)
print(f1_score(y_test, pred_y))

# 특징 개수 튜닝
best_score = 0

for k in range(1, 14, 1):
    s_x_train = x_train[pvals.iloc[:k].index]
    s_x_test = x_test[pvals.iloc[:k].index]
    
    model = KNN().fit(s_x_train, y_train)
    pred_y = model.predict(s_x_test)
    score = f1_score(y_test, pred_y)
    if score > best_score:
        best_score = score
        best_k = k

print(best_score, best_k)

연속형 변수와 범주형 변수가 섞여있을 때 위의 과정은 좀 귀찮고 편하게 하고 싶으면 상호 정보량을 이용한다.

# 상호 정보량을 이용한 특징 선택 수행
selector = SelectKBest(mutual_info_classif, k = 10)
selector.fit(x_train, y_train)
selected_features = x_train.columns[selector.get_support()]

s_x_train = pd.DataFrame(selector.transform(x_train), columns = selected_features)
s_x_test = pd.DataFrame(selector.transform(x_test), columns = selected_features)
# 상호 정보량을 이용한 특징 선택 후 성능 확인
model = KNN().fit(s_x_train, y_train)
pred_y = model.predict(s_x_test)
print(f1_score(y_test, pred_y))

# 특징 개수 튜닝
best_score = 0

for k in range(1, 14, 1):
    selector = SelectKBest(mutual_info_classif, k = k)
    selector.fit(x_train, y_train)
    selected_features = x_train.columns[selector.get_support()]
    
    s_x_train = pd.DataFrame(selector.transform(x_train), columns = selected_features)
    s_x_test = pd.DataFrame(selector.transform(x_test), columns = selected_features)
    
    model = KNN().fit(s_x_train, y_train)
    pred_y = model.predict(s_x_test)
    score = f1_score(y_test, pred_y)
    if score > best_score:
        best_score = score
        best_k = k
        
print(best_score, best_k)


참고자료https://datascienceschool.net/03%20machine%20learning/14.03%20%ED%8A%B9%EC%A7%95%20%EC%84%A0%ED%83%9D.html

 

특징 선택 — 데이터 사이언스 스쿨

.ipynb .pdf to have style consistency -->

datascienceschool.net

의사결정나무, 랜덤포레스트 등 모델 기반 특징을 선택하는 방법도 있다는 것을 알게됨

비용 민감 모델은 학습 목적식이 정확도를 최대화하는 것을 방지하기 위한 방법이다.

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

 

비용 민감 모델이란?

일반적으로 학습 목적식 같은 경우 위음성 비용과 위양성 비용을 같다고 본다. 즉, 위음성 비용과 위양성 비용을 구분하지 않는다. 하지만 위음성 비용(암환자의 생명)이 위양성 비용(정상인의 돈과 시간)보다 훨씬 크다. 

→비용 민감 모델은 학습 목적식에서 위음성 비용과 위양성 비용을 다르게 설정하는 모델로, 보통 위음성 비용(소수클래스, 우리의 관심 대상이므로 긍정 클래스)을 위양성 비용(다수클래스, 우리의 관심 대상이 아니므로 부정 클래스)보다 크게 설정한다.

위음성 비용을 위양성 비용보다 w배 크게 설정하는데 w는 1보다 커야한다. w가 1이면 일반적인 모델과 같고, w가 1보다 작으면 부정 클래스에 더 초점을 두는 모델이 된다.

즉, 위음성 비용 = w x 위양성 비용(w > 1)로 설정한 모델을 비용 민감 모델이라고 한다.


비용 민감 모델을 구현할 때 확률 모델과 비확률 모델로 구분해서 알아볼 것이다.

 

확률 모델

확률 모델은 학습 목적식 자체를 바꾸는 것이 아니고 학습의 결과를 바꾸는 방식이다. 로지스틱 회귀, 나이브 베이즈 등의 확률 모델들은 cut-off value, c를 조정하는 방식으로 비용 민감 모델을 구현할 수 있다.

x에 대한 y의 확률이 c보다 크거나 같으면 긍정으로 보고, c보다 작으면 부정으로 본다. 그리고 c를 처음 설정할 때는 보통 0.5로 둔다. 예를 들어, y가 1과 -1이 있을 때 x에 대한 y가 1일 확률이 0.5 이상이면 1로 볼테고 0.5 미만이면 -1로 본다.

 

c값을 작게 둘수록 긍정 클래스로 분류하는 경우가 많아질 것이다. 즉, 긍정 클래스에 대한 결정 공간이 넓어진다는 소리이다. 그렇기 때문에 c값을 0.5보다 작게 설정한다.

→ 우리의 관심 대상인 긍정 클래스 비율이 커질 것이다.

위 그래프와 같이 c값이 작을수록 재현율이 커질테고 클수록 정확도가 커질것이고 그 반대의 경우도 마찬가지로 성립한다.

 

그리고 정확한 확률 추정은 불가능하지만 그 개념을 도입할 수 있는 모델(k-최근접 이웃, 신경망, 의사결정나무, 앙상블 모델 등)에도 역시 적용이 가능하다.

 

●관련 문법: predict_proba

확률 모델은 학습 목적식 자체를 바꾸는 것이 아니고 학습의 결과를 바꾸는 방식으로 predict_proba를 응용한다.

predict_proba는 sklearn의 확률 모델이 갖는 메서드(fit 이후)로서, X(새로 들어온 데이터, 평가 데이터 x_test)를 입력으로 받아 각 클래스에 속할 확률을 출력한다.

X(새로 들어온 데이터, 평가 데이터 x_test)에 포함된 모든 행에 대해 부정클래스와 긍정클래스에 속할 확률들을 출력해준다.

(참고: predict는 각 클래스에 속할 확률이 아닌 다이렉트로 예측값을 출력해준다, 예를 들어, 부정클래스 확률이 0.3 긍정 클래스 확률이 0.7이면 긍정클래스 확률이 더 크기 때문에 다이렉트로 긍정클래스(1)로 분류를 해주고 반대로 부정클래스 확률이 0.7이고 긍정 클래스 확률이 0.3이면 부정클래스 확률이 더 크기 때문에 다이렉트로 부정클래스(-1)로 분류를 해준다.)

 

X(새로 들어온 데이터, 평가 데이터 x_test)에 포함된 모든 행에 대해 부정클래스와 긍정클래스에 속할 확률들을 출력해준

뒤 긍정클래스에 속할 확률이 나오는 컬럼인 Pr(Pos l x)의 값이 c보다 크거나 같으면 긍정클래스로 볼 것이고 c보다 작으면 부정클래스로 볼 것이다.

이것을 구현 할때 필요한 개념이 있다. Numpy와 Pandas를 사용할 때 가능하면 배열 단위 연산을 하는 것이다.

다시말해, 유니버설 함수, 브로드캐스팅, 마스크 연산을 최대한 활용해야 한다.

(예시) 확률 모델의 cut_off_value를 0.3으로 설정하기

긍정클래스에 속할 확률이 나오는 컬럼인 Pr(Pos l x)의 값을 0.3보다 크거나 같은지를 본다. 여기서 긍정클래스에 속할 확률이 나오는 컬럼인 Pr(Pos l x)의 값들은 배열이고 0.3은 하나의 스칼라 숫자로 브로드캐스팅을 통해서 각각의 값들이 비교가 될 것이다. 그리고 0.3 이상인 것은 True를 그렇지 않은 것은 False로 나올 것이다. 

그 후 클래스가 1과 -1이다라고 하면 False를 -1로 True를 1로 바꿔야한다. replace를 사용해서 .replace{"False":-1, "True":1}로 바꿀수 있지만 2*R-1로 단순한 연산을 통해 바꿀 수도 있다.(False는 0, True는 1이기 때문)

(부울을 -1과 1로 바꾸는 테크닉 2*R-1 도 알아두면 도움이 된다.)

 

비확률 모델

비확률 모델은 확률 모델과 다르게 학습 목적식 자체를 바꾸는 것이다.

 

(1) 서포트 벡터 머신

일반 서포트 벡터 머신의 목적식에서 왼쪽에 있는 llwll가 마진을 최대화하는 부분, 오른쪽 C*크사이i가 오차를 최소화 하는 부분이다.

크사이i는 샘플i에 대한 오차이다. 다시 말해, 샘플i에 대한 오차가 위양성비용인지 위음성비용인지 구분이 안되있고 샘플i에 대한 그냥 오차이다. 

비용 민감 서포트 벡터 머신 목적식에서 비용을 두 개로 구분했다. 

yi가 1일 때 발생하는 비용
yi가 -1일 때 발생하는 비용

yi가 1일 때 발생하는 비용이라는 것은 yi가 1인데 예측을 -1로 했다는 소리이다. 즉, 긍정을 부정으로 오분류한 비용이다.

그러면 yi가 -1일 때 발생하는 비용은 부정을 긍정으로 오분류한 비용이다.

우리가 관심이 있는 것은 긍정을 부정으로 오분류한 비용으로 C1을 C2보다 크게 둬야한다.

 

(2)의사결정나무

소수 클래스에 대한 가중치를 부여하는 방식으로 가능하면 소수 클래스로 분류하도록 유도하고 k-최근접 이웃, 앙상블 모델(랜덤포레스트, xgboost, lightgbm)에도 똑같이 정의할 수 있다. 

동그라미가 다수클래스(우리의 관심 대상이 아닌 부정클래스)고 세모가 소수클래스(우리의 관심 대상 긍정클래스)라고 하면

w1을 1로 두고, w2는 긍정이 부정보다 중요한 정도를 나타내도록 설정한다. 보통은 w1을 1로 두고 w2를 클래스 불균형 비율, 클래스 불균형 비율보다 조금 작은 정도로 두는 경우가 많다.

 

●관련 문법: class_weight

의사결정나무, 서포트벡터머신, 랜덤포레스트, xgboost, lightgbm 등에서는 class_weight를 설정함으로써 비용 민감 모델을 구현할 수 있다. class_weight는 사전 형태로 입력한다.▷key: class 이름

▷value: class weight

(예시) SVC(class_weight = {1:10, -1:1}) → 클래스 1에 클래스 -1보다 10배의 가중치를 부여

 


## 코드 실습 ##

●확률 모델의 비용 민감 모델 구현

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

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)
# 클래스 불균형 확인
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%로 불균형이 심각한 수준이라 판단

# 비용 민감 모델 적용전 logistic regression 모델 테스트
from sklearn.linear_model import LogisticRegression as LR

# max_iter는 Gradient Descent 방식을 반복해서 몇번 수행할 것인가 인데, 이게 일단 수렴(Convergence)하게 되면 횟수를 늘려도 성능이 거의 달라지지 않음
# max_iter는 경사하강법을 통해 weight값을 최적화 즉, 가장 최적화된 모델을 만들기 위해서 경사하강법을 몇 번 수행 할 것인지에 대한 설정
model = LR(max_iter = 100000).fit(x_train, y_train)
pred_y = model.predict(x_test)
print("정밀도:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))

# cut off value 조정
probs = model.predict_proba(x_test)
probs = pd.DataFrame(probs, columns = model.classes_)

cut_off_value = 0.3

pred_y = 2 * (probs.iloc[:, -1] >= cut_off_value) -1
print("재현율:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))

# cut off value를 조정하는 함수 작성
def cost_sensitive_model(model, cut_off_value, x_test, y_test):
    probs = model.predict_proba(x_test)
    probs = pd.DataFrame(probs, columns = model.classes_)
    pred_y = 2 * (probs.iloc[:, -1] >= cut_off_value) -1
    recall = recall_score(y_test, pred_y)
    accuracy = accuracy_score(y_test, pred_y)
    return recall, accuracy
# cut off value에 따른 recall과 accuracy 변화 확인
from matplotlib import pyplot as plt
import numpy as np

cut_off_value_list = np.linspace(0, 1, 101)
recall_list = []
accuracy_list = []

for c in cut_off_value_list:
    recall, accuracy = cost_sensitive_model(model, c, x_test, y_test)
    recall_list.append(recall)
    accuracy_list.append(accuracy)

%matplotlib inline
plt.plot(cut_off_value_list, recall_list, label = 'recall')
plt.plot(cut_off_value_list, accuracy_list, label = 'accuracy')
plt.legend()
# 그래프를 통해 대략적인 정확도와 재현율의 변화를 보고 cut off value를 찍어보면서 조정해본다

 

 

●비확률 모델의 비용 민감 모델 구현

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

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()

# 라벨이 문자이니 변환
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]

# 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))
# 재현율을 보니 불균형이 심각한 수준은 아니라고 판단

# 비용 민감 모델 적용전 support vector machine 모델 테스트(클래스 웨이트 조정 전)
from sklearn.svm import SVC

model = SVC().fit(x_train, y_train)
pred_y = model.predict(x_test)
print("재현율:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))

# 클래스 웨이트 조정
model = SVC(class_weight = {1:8, -1:1}).fit(x_train, y_train)
pred_y = model.predict(x_test)
print("재현율:", recall_score(y_test, pred_y))
print("정확도:", accuracy_score(y_test, pred_y))

 

cut off value랑 class weight 값도 튜닝하면 좋을 것 같긴한데...

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

→클래스 불균형 발생 원인 첫번째 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))
# 재현율도 올랐고 정확도도 약간 보장됨


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

클래스 불균형 문제란?

클래스 변수가 하나의 값에 치우친 데이터로 학습한 분류 모델이 치우친 클래스에 대해 편향되는 문제로, 이러한 모델은 대부분 샘플을 치우친 클래스 값으로만 분류하게 된다(예시: 암환자 판별 문제)

예를들어, 암환자와 정상인이 있을 때 암환자 수가 훨씬 적을텐데 이러한 데이터를 가지고 모델을 만들게 되면 이 모델은 대부분을 정상인이라고 분류를 할 것이다. 그렇게되면 우리가 더 판별하고 싶어하는 암환자를 제대로 판별하지 못하는 상황이 발생할 수 있다.

그리고 클래스 불균형 문제는 분류에서만 발생하는 문제이다.

위의 그림을 통해 알 수 있듯이, 클래스 불균형 문제가 있는 모델은 정확도가 높고, 재현율이 매우 낮은 경향이 있다.

 

  • 용어

# FP, FN, TP, TN은 머리속으로 혼동행렬을 그려본다!!

-다수 클래스: 대부분 샘플이 속한 클래스(예: 정상인)

-소수 클래스: 대부분 샘플이 속하지 않은 클래스(예: 암환자)

 

-위양성 비용(False positive; FP): 부정 클래스 샘플을 긍정 클래스 샘플로 분류해서 발생하는 비용

-위음성 비용(False negative; FN): 긍정 클래스 샘플을 부정 클래스 샘플로 분류해서 발생하는 비용

# 위양성이란 실제로 병이 없는데 검사 결과에서는 마치 병이 있는 것처럼 나오는 경우 / 위음성이란 실제로 병이 있는데 검사 결과에서는 마치 병이 없는 것처럼 나오는 경우

# '위'는 위선을 뜻하는 즉, 거짓이라는 뜻이다.

# 긍정 클래스는 우리가 예측 하고 싶은 것이고 부정 클래스는 그 반대

→보통은 위음성 비용이 위양성 비용보다 훨씬 크다.(예: 위양성 비용(정상인→암환자) vs 위음성 비용(암환자→정상인))

예를들어, 정상인을 암환자라고 분류를 한다면 정상인은 자신이 암환자라고 착각을 하고 재검사를 받을 것이다. 물론 거기서 발생하는 돈과 시간의 비용이 발생 할 것이다. 반대로 암환자를 정상인이라고 분류를 한다면 암환자는 자신이 정상이라고 착각해서 추가적인 검사나 수술을 하지 않고 퇴원을 할 것이다. 그런데 실제로 암환자였기 때문에 암이 더 악화되면서 죽음을 맞이할 것이다. 

즉, 위양성 비용은 돈과 시간, 위음성 비용은 생명이라는 비용이 들고 당연히 생명이라는 비용이 더 큰 비용이라는 것을 알 수 있다.

이와 동일하게 양품과 불양품 예시도 마찬가지이다.

 

  • 발생 원인

1. 근본적인 원인은 클래스 비율이 맞지 않기 때문이다. 

2. 대부분 분류 모형의 학습 목적식은 정확도를 최대화하는 것이다. 정확도를 최대화하다 보니 굳이 소수클래스를 잘 분류하기 위한 노력을 할 필요가 없다. 그래서 대부분 샘플을 다수 클래스라고 분류하도록 학습된다.

위 두 그림 모두 동그라미 데이터가 8개, 별 데이터가 2개이다. 별 데이터가 긍정 클래스라고 가정한다. 

어느 모델이 더 좋냐라고 했을 때 대부분의 분류 모형은 왼쪽을 더 좋다고 평가한다. 즉, 학습 목적식에 의해서 정확도만 최대화 한다는 소리이다. 그래서 기본적으로 분류 모형의 학습 목적식에 의해서 정확도만 높아지는 현상이 발생하는것이다.

 

 

클래스 불균형 탐색 방법 1. 클래스 불균형 비율

클래스 불균형 비율이 일반적으로 9 이상이면 편향된 모델이 학습될 가능성이 있다.

다만, 클래스 불균형 비율이 높다고 해서 반드시 편향된 모델을 학습하는 것은 아니다.

→클래스 불균형 문제라는 것은 클래스 자체가 불균형한게 문제가 아니고, 불균형하기 때문에 편향된 모델이 만들어지는 것이 문제다. 그래서 클래스 불균형 비율은 참고용으로 보고 실제로 모델이 편향이 되는지 아닌지를 판단하기 위한 방법이 필요하다. 그 방법이 k-최근접 이웃을 활용하는 방법이다.

 

클래스 불균형 탐색 방법 2. k-최근접 이웃을 활용하는 방법

k-최근접 이웃은 이웃의 클래스 정보를 바탕으로 분류를 하기에 클래스 불균형에 매우 민감하므로, 클래스 불균형 문제를 진단하는데 적절하다.(k-최근접 이웃은 이웃의 클래스 정보를 바탕으로 분류를 하기에 클래스 불균형에 매우 민감해서 클래스 불균형 문제가 있는 경우 사용하면 안되는 모델이지만, 역으로 그걸 활용해서 클래스 불균형 문제에 테스트 하는데 쓸 수 있는 아이디어)

k값이 크면 클수록(이웃이 많으면 많을수록) 더욱 민감하므로, 보통 5~11 정도의 k를 설정하여 문제를 진단한다.(11로 많이 설정)

[참고]정상인:1000명, 암환자:10명 가정

k값이 커질수록, 즉, 이웃이 많을수록 민감하다는 말은 클래스 불균형으로 인해 정상인이 엄청 많을텐데 거기에 k(이웃)의 수를 늘리면 주변 이웃이 정상인들이 있을 확률이 더 높아져 정상인으로 분류를 할 확률이 더 높아지기 때문에 k값이 커질수록 더욱 민감하다는 소리이다. 

 

 

클래스 불균형 문제 해결의 기본 아이디어

클래스 불균형 문제 해결의 기본 아이디어는 소수 클래스에 대한 결정 공간을 넓히는 것이다.

특징 간 스케일 차이 문제

특징 간 스케일이 달라서 발생하는 문제로, 스케일이 큰 변수에 의해 혹은 스케일이 작은 변수에 의해 모델이 크게 영향을 받는 문제를 의미한다.

▷스케일이 큰 변수에 영향을 받는 모델: k-최근접 이웃

k-최근접 이웃은 거리 기반 모델이기 때문에 거리는 스케일이 큰 변수에 영향을 많이 받는다. 

▷스케일이 작은 변수에 영향을 받는 모델: 회귀모델, 서포트 벡터 머신, 신경망('wx+b' 꼴의 함수가 들어간 모델)

▷스케일에 영향을 받지 않는 모델: 나이브베이즈, 의사결정나무(이진 분류에 한함)

 

특징 간 스케일 차이 해결 방법

스케일링을 사용하여 변수 간 스케일 차이를 줄이는 방법으로 해결한다.

Standar Scaling은 범위가 이론적으로 [- ~ +]이지만 보통 [-3 ~ +3]로 왔다갔다 하는게 일반적이다.

Min-max Scaling은 범위가 항상 [0 ~ 1]에서 왔다갔다 한다.

 

●모델에 따른 스케일러 선택

▷Standard Scaler: 특징의 정규 분포를 가정하는 모델(예: 회귀모델, 로지스틱회귀모델)

▷Min-Max Scaler: 특정 분포를 가정하지 않는 모델(예: 신경망, k-최근접 이웃)

→개인적으로 Standard Scaler를 사용하더라도 스케일이 완벽하게 변수간에 맞지 않기 때문에 변수간에 스케일을 완벽하게 갖출수 있는 Min-Max Scaler가 좀 더 바람직하다고 생각한다.

 

●관련 문법: sklearn.preprocessing.MinMaxScaler & StandardScaler

Min max scaling과 standard scaling을 수행하는 인스턴스를 생성하는 함수이다.

- 주요 메서드

▷fit: 변수별 통계량을 계산하여 저장(min max scaler: 최대값 및 최소값, standard scaler: 평균 및 표준편차)

▷transform: 변수별 통계량을 바탕으로 스케일링 수행

▷inverse_transform: 스케일링된 값을 다시 원래 값으로 변환

 

## 코드 실습 ##

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

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

# 특징과 라벨 분리
X = df.drop('Salary', axis=1)
Y = df['Salary']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 특징 간 스케일 차이 확인
x_train.max() - x_train.min() # 특징 간 스케일 차이가 큼
# 스케일이 작은 특징은 KNN 모델에 영향을 거의 주지 못할 것이라 예상
# 'Free_agency_eligibility', 'Free_agent', 'Arbitration_eligibility', 'Arbitration' 변수들은 1인걸로 보아 이진형 변수

# 스케일링 전에 성능 확인
from sklearn.neighbors import KNeighborsRegressor as KNN
from sklearn.metrics import mean_absolute_error as MAE

model = KNN().fit(x_train, y_train)
pred_y = model.predict(x_test)
score = MAE(y_test, pred_y)
print(score)

# 스케일링 수행
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler().fit(x_train)
s_x_train = scaler.transform(x_train)
s_x_test = scaler.transform(x_test)
# 스케일링 후에 성능 확인
model = KNN().fit(s_x_train, y_train)
pred_y = model.predict(s_x_test)
score = MAE(y_test, pred_y)
print(score)

스케일링을 했을 때 성능이 좋아진 것을 볼 수 있다.


부가설명(참고)

 

기존 스케일은 다음과 같다. 위에서 KNN의 모델의 default가 유클리디안이기 때문에 Runs ,Hits, Strike-Outs 같은 스케일이 큰 변수들이 모델에 거의 영향을 미쳤을 것이다. 예를들어, Batting_average는 스케일 차이가 아무리 나봤자 0.346인데 Runs같은 경우 스케일 차이가 많이 나면 100도 그냥 넘기 때문에 스케일이 0.346 차이나는 Batting_average와 스케일이 133 차이나는 Runs의 간극에 의해서 Batting_average는 무시가 됐을 것이다. 즉, Batting_average 같은 좋은 특징이 있음에도 불구하고 이거는 스케일이 작아서 모델에 영향을 거의 주지 않았을 것이다.

 

이번 실습에서는 스케일링을 해서 성능이 좋아졌지만, 스케일링을 해서 성능이 떨어지는 경우도 당연히 존재한다.

예를들어, Batting_average 변수가 라벨과 아무런 관련이 없는 쓸모 없는 변수라고 가정한다면 이 변수는 스케일이 작아서 무시되고 있으니 오히려 잘된것이다. 그런데 스케일링을 하다보니 쓸모 없는 변수가 영향력이 커져 성능이 저하될수 있다.

(아주 쓸모가 없는 변수인데 스케일도 작아서 애초에 모델에 영향을 안주고 있었는데 스케일링을 해서 스케일을 맞출경우 모델에 안좋은 영향이 커질 수 있기 때문이다.)

그래서 스케일링을 했을때 성능이 안좋아졌다면 그 근거가 뭔지 찾아야 한다. (ex. 어떤 변수가 스케일이 작아서 애초에 모델에 영향을 안 주고 있고, 스케일링 하지 않은 모델 성능과 스케일링 한 모델 성능을 비교해보니 스케일링 했을 때 성능이 안좋아 졌다->그러면 어떤 변수는 스케일링 문제가 아니고 변수 자체가 의미가 없다고 판단하고 drop하는 결론을 이끌 수 있어야 한다.)

 

즉, '어떤 상황에서 이렇게 하면 무조건 좋아진다'라고 하는 전처리 기법, 탐색 기법, 모델링 기법은 존재 할 수 없다.

 

 

 

 

 

 

 

 

변수 치우침 문제

모델링에 가장 적합한 확률 분포는 정규 분포이나, 실제로 많은 변수가 특정 방향으로 치우쳐 있다.

한 쪽으로 치우친 변수에서 치우친 반대 방향의 값(꼬리 부분)들이 이상치처럼 작용할 수 있으므로, 이러한 치우침을 제거해야 한다.

 변수 치우침 확인 방법: 왜도(skewness)

변수 치우침을 확인하기 가장 적절한 척도로는 왜도(skewness)가 있다. 왜도는 분포의 비대칭도를 나타내는 통계량으로, 왜도 값에 따른 분포는 다음과 같다.

일반적으로 왜도의 절대값이 1.5이상이면 치우쳤다고 판단한다.

 

  • 관련 문법: scipy.stats

다양한 확률 통계 관련 함수를 제공하는 모듈이다.

- scipy.stats.skew: 왜도를 구하는 함수

- scipy.stats.mode: 최빈값을 구하는 함수

- scipy.stats.kurtosis: 첨도를 구하는 함수

 

변수 치우침 해결 방법

변수 치우침을 해결하는 기본 아이디어는 값 간 차이를 줄이는데 있다. 대표적인 처리 방법은 다음과 같다.

 

## 코드 실습 ##

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

import pandas as pd
df = pd.read_csv("Sonar_Mines_Rocks.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)
# 왜도 확인
x_train.skew() # Band4의 왜도가 가장 크다 -> 어떻게 생겼는지 그래프를 그려보자
Band1     2.138244
Band2     2.262066
Band3     2.676642
Band4     3.463153
Band5     2.109983
Band6     1.402875
Band7     1.022341
Band8     1.223893
Band9     1.609178
Band10    1.337977
Band11    0.944374
Band12    0.611857
Band13    0.817726
Band14    1.153231
Band15    0.849569
Band16    0.721450
Band17    0.652667
Band18    0.540473
Band19    0.321763
Band20    0.014367
Band21   -0.169245
Band22   -0.394846
Band23   -0.530914
Band24   -0.680878
Band25   -0.764100
Band26   -0.654374
Band27   -0.592475
Band28   -0.703936
Band29   -0.404197
Band30   -0.177567
Band31    0.200206
Band32    0.375580
Band33    0.448510
Band34    0.515404
Band35    0.496416
Band36    0.509472
Band37    0.593171
Band38    1.052417
Band39    1.024995
Band40    0.984552
Band41    0.831029
Band42    0.785252
Band43    0.940512
Band44    1.303075
Band45    1.414533
Band46    1.772946
Band47    1.946918
Band48    1.324837
Band49    1.248168
Band50    1.504062
Band51    2.767722
Band52    1.998543
Band53    0.837772
Band54    1.177324
Band55    1.493668
Band56    1.088704
Band57    1.459808
Band58    1.592631
Band59    1.630487
Band60    2.880505
dtype: float64
%matplotlib inline
df['Band4'].hist()

  • 치우침을 제거했을 때의 성능 비교를 위한 모델 개발
# 라벨 숫자로 바꾸기
y_train.replace({'M':-1, 'R':1}, inplace=True)
y_test.replace({'M':-1, 'R':1}, inplace=True)
# 원본 데이터로 모델링
from sklearn.neural_network import MLPClassifier as MLP
from sklearn.metrics import f1_score

model = MLP(random_state = 2313, max_iter = 1000)
model.fit(x_train, y_train)
pred_y = model.predict(x_test)
score = f1_score(y_test, pred_y)
print(score)

# 왜도 기반 치우친 변수 추출(왜도 절대값이 1.5 이상인 변수 추출)
import numpy as np
biased_variables = x_train.columns[x_train.skew().abs() > 1.5]
biased_variables

# 변수 치우침 제거
x_train[biased_variables] = x_train[biased_variables] - x_train[biased_variables].min() + 1
x_train[biased_variables] = np.log10(x_train[biased_variables])

log를 취해도 되지만 상용로그(log10)을 취했다.

# 평가 데이터도 같은 방법으로 전처리 수행
x_test[biased_variables] = x_test[biased_variables] - x_test[biased_variables].min() + 1
x_test[biased_variables] = x_test[biased_variables].apply(np.log10)
# 치우침 제거 후 모델 평가
model = MLP(random_state = 2313, max_iter = 1000).fit(x_train, y_train)
pred_y = model.predict(x_test)
score = f1_score(y_test, pred_y)
print(score)

변수 치우침을 제거 후 모델의 성능이 더 좋아졌음을 확인한다.

특징 간 상관성이 높을 때 문제점

●회귀 모델, 신경망, SVM과 같이 'wx+b' 형태의 선형식이 모델에 포함되는 경우, 특징 간 상관성이 높으면 강건한 파라미터 추정이 어렵다. 즉, 추정할 때마다 결과가 달라질 수 있다.(모델로 predict(예측)하는 값이 달라질 수 있다.)

 

아래 예시를 보자.

x1과 x2를 이용하여 y를 예측하는 회귀 모델에서 y=2x1이고, x2=x1이라면 결론적으로 w1과 w2가 무수히 많은 해를 갖는 다는 문제가 있다. 그래서 강건한 파라미터 추정이 어려워서 추정할 때마다 결과가 달라질 수 있다.

 

회귀 모델의 경우 잔차제곱합을 최소화를 하는 과정에서 미분을 해야하는데 미분을 할 때 결과식에 역행렬이 포함이 된다. 특징 간 상관성이 높으면 역행렬이 존재하지 않을 수 있어서 해를 구하는게 불가능 할 수도 있다.

 

●트리 계열의 모델은 사실 특징 간 상관성이 높다고 해서 모델 예측 성능에 영향을 받지 않지만, 상관성이 높은 변수 중 소수만 모델에 포함되기 때문에 설명력이 크게 영향을 받을 수 있다.

 

극단적인 예를 들어, x1과 x2처럼 아에 같은 특징이 있다면 x1을 기준으로 분리를 하던 x2를 기준으로 분리를 하던 똑같기 때문에 둘 중 하나를 임의로 선택을 할 것이다. 그러면 x2로 분리를 했으면 x1은 분리할 필요가 없고, x1으로 분리를 했으면 x2는 분리할 필요가 없다. 모델상에서 x1과 x2중 둘 중 하나만 등장하기 때문에 남은 다른 하나가 어떤 영향을 끼치는지 모델상에서 반영되지 않기 때문에 설명력 문제가 발생할 수 있다.(x2가 등장 했다면 x1은 모델에 어떤 영향을 끼치는지 반영이 안되기 때문에 설명력 문제가 발생할 수 있다.)

 

 

해결 방법 1. VIF 활용

주로 회귀 모델에서 다중공선성 문제를 해결할 때 사용하는 지표이다.

Variance Inflation factors(VIF, 분산 팽창 계수)는 한 특징을 라벨로 간주하고, 해당 라벨을 예측하는데 다른 특징을 사용한 회귀 모델이 높은 R2(R 스퀘어(결정계수, 설명력): 회귀 모델에서 적합성 지표로 0~1 사이의 범위를 가지고 1로 갈수록 좋은 모델이라고 판단할 수 있는 지표)을 보이는 경우 해당 특징이 다른 특징과 상관성이 있다고 판단한다.

VIF가 높은 순서대로 특징을 제거하거나, VIF가 10 이상인 경우 그 변수는 다른 변수와 상관성이 매우 높다고 판단해서 일반적으로 삭제한다.

 

해결 방법 2. 주성분 분석(PCA)

주성분 분석을 이용하여 특징이 서로 직교하도록 만들어 특징 간 상관성을 줄이는 방법이다.

 

아래의 그림을 예를 들어보면, 기존에 차원이 x1과 x2로 구성되어 있고 데이터들이 있다. 이 데이터들을 원래 특징(x1, x2)이 아니라 새로운 차원으로 데이터를 투영시키는 방법이다. z1이라는 축과 z2라는 축을 찾아서 이 두 개의 축을 바탕으로 데이터를 투영시킨다. (z1과 z2는 우연히 찾은 것이 아니고 이 데이터의 방향을 보니 z1 방향과 z2 방향으로 배치되어 있기 때문이다.) 그리고 이 z1과 z2를 주성분이라고 부르고 이 그림에서는 수학적으로 z1이 z2보다 이 데이터의 분산을 더 잘 설명한다. 

주성분 분석으로 데이터를 투영시키고 나면 데이터의 차원은 같지만 데이터의 분산을 설명하는 정도를 측정해서 정도가 높은 차원만 골라서 차원을 줄인다. 그리고 주성분으로 축을 회전시켜서 특징이 서로 직교하도록 만들어(데이터를 더 잘 설명 하는 축 방향으로) 특징 간 상관성을 줄인다.

→n차원의 데이터는 총 n개의 주성분이 존재하지만, 주성분을 다쓰는 것은 의미가 없기 때문에 차원 축소 등을 위해 분산의 대부분을 설명하는 m < n 주성분만 사용하는 것이 일반적이다.(n보다 작은 m개의 주성분만 사용)

 

  • 관련 문법: sklearn.decomposition.PCA

주성분 분석을 수행하는 인스턴스를 생성하는 함수이다.

-주요 입력

▷n_components: 사용할 주성분 개수를 나타내며, 이 값은 기존 차원 수(변수 수)보다 작아야 함

-주요 attribute

▷.explained_variance_ratio: 각 주성분이 원 데이터의 분산을 설명하는 정도, 보통 1등부터 n등까지 점수가 있을 때 누적합이 보통 90%(0.9)가 넘는 정도에서 끊는다.

 

 

## 코드 실습 ##

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

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

# 특징과 라벨 분리
X = df.drop('Age', axis=1)
Y = df['Age']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 특징 간 상관관계를 보기위해 상관행렬 출력
x_train.corr() # 특징 간 상관관계가 존재하며 높음

  •  VIF 기준 특징 선택
# VIF 계산
from sklearn.linear_model import LinearRegression as LR
VIF_dict = dict()

# 하나의 특징을 라벨로 간주하고, 다른 특징들로 해당 라벨을 맞추기 위한 선형회귀모델을 학습해서 그 모델의 R스퀘어를 측정하는 방식임
for col in x_train.columns:
    model = LR().fit(x_train.drop([col], axis=1), x_train[col])
    r2 = model.score(x_train.drop([col], axis=1), x_train[col]) # LinearRegression의 score가 r2 점수임
    VIF = 1 / (1 - r2)
    VIF_dict[col] = VIF
VIF_dict
# Height를 제외하곤 VIF가 모두 10보다 큼
# 이러한 상황에서는 사실 PCA를 사용하는 것이 바람직
# 모델 성능 비교를 위해 VIF 점수가 30점 미만인 특징만 사용하기로 결정

from sklearn.neural_network import MLPRegressor as MLP
from sklearn.metrics import mean_absolute_error as MAE

# 전체 특징을 모두 사용하였을 때
model = MLP(random_state = 2313, max_iter = 500)
model.fit(x_train, y_train)
pred_y = model.predict(x_test)
score = MAE(y_test, pred_y)
print(score)

# VIF 점수가 30점 미만인 특징만 사용하였을 때
# VIF 점수가 30점 미만인 특징만 추출
selected_features = [key for key, val in VIF_dict.items() if val < 30]
selected_features

 

model = MLP(random_state = 2313, max_iter = 500)
model.fit(x_train[selected_features], y_train)
pred_y = model.predict(x_test[selected_features])
score = MAE(y_test, pred_y)
print(score)

VIF 점수가 30점 미만인 특징만 사용했을 때 성능이 더 좋아졌음을 확인한다.

 

  • PCA 사용
from sklearn.decomposition import PCA
# 인스턴스화 및 학습
PCA_model = PCA(n_components = 3).fit(x_train)

# transform
z_train = PCA_model.transform(x_train)
z_test = PCA_model.transform(x_test)

print(z_train.shape) # 주성분 3개

model = MLP(random_state = 2313, max_iter = 500)
model.fit(z_train, y_train)
pred_y = model.predict(z_test)
score = MAE(y_test, pred_y)
print(score)

PCA를 사용하니 모델의 성능이 더 좋아진걸 확인한다.

이상치란?

변수 범위에서 많이 벗어난 아주 작은 값이나 아주 큰 값으로, 일반화된 모델을 생성하는데 악영향을 끼치는 값으로 이상치를 포함하는 레코드를 제거하는 방법으로 이상치를 제거한다.

이상치는 결측치와 다르게 값을 추정을 하는 것이 아니라 제거해야한다.(절대 추정의 대상이 아님에 주의한다.)

 

이상치를 제거해야하는 대표적인 모델들은 클래스의 평균을 쓰는 모델들이다.(트리계열 모델, 거리를 사용하는 모델:KNN, 회귀모델(회귀모델도 학습될 때 미분 하는 과정에서 평균을 사용)) →이런 모델들은 이상치를 제거해야만 일반화된 모델을 만들수 있다.

위의 그림에서 총 7개의 레코드가 있다. 빨간색 레코드는 다른 레코드에 비해서 많이 벗어나있다.

회귀 모델을 만들었는데 빨간색 레코드의 영향을 받아서 일반적인 다른 레코드를 설명하지 못하고 위쪽으로 쏠렸다.

빨간색 레코드인 이상치를 제거한 다음에 모델을 만들면 일반화된 모델이 만들어질 것이다.

 

이상치 판단 방법 1. IQR 규칙 활용

변수별로 IQR 규칙을 만족하지 않는 샘플들을 판단하여 삭제하는 방법이다.

직관적이고 사용이 간편하다는 장점이 있지만, 단일 변수로 이상치를 판단하기 어려운 경우가 있다는 문제가 있다.

즉, 단일 변수로 보면 이상치이지만 여러개의 변수로 같이 보면 이상치가 아니거나, 단일 변수로 보면 이상치가 아니지만 여러개의 변수로 같이 보면 이상치인 경우가 있다는 말이다. 이럴 경우 IQR을 사용하게 되면 완벽히 무시되는 단점이 있다.

결론: IQR 기준만 가지고 이상치라고 단언할 수는 없다.

(TIP) IQR rule이라는 것은 boxplot을 그릴 때 점으로 표시된 것들이 IQR rule에 의한 이상치이다.

 

  • 관련 문법: numpy.quantile

array의 q번째 quantile(분위수)을 구하는 함수이다.

-주요 입력

▷a: input array(list, ndarray등)

▷q: quantile(0과 1사이)

 

 

## 코드 실습 ##

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

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

# 특징과 라벨 분리
X = df.drop('Glass_type', axis=1)
Y = df['Glass_type']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
x_train.shape

x_train1 = x_train.copy() # 이상치 비율을 조정할수도 있으니 복사본 생성
import numpy as np
def IQR_rule(val_list): # 한 특징에 포함된 값(열 벡터)
    # IQR 계산
    Q1 = np.quantile(val_list, 0.25) # 제 1사분위수(25%)
    Q3 = np.quantile(val_list, 0.75) # 제 3사분위수(75%)
    IQR = Q3-Q1
    
    # IQR rule을 위배하지 않는 bool list 계산(True: 이상치 x, False: 이상치 0)
    not_outlier_condition = (Q3 + 1.5 * IQR > val_list) & (Q1 - 1.5 * IQR < val_list)
    return not_outlier_condition
# apply를 이용하여 모든 컬럼에 IQR rule 함수 적용
conditions = x_train.apply(IQR_rule)
conditions

# 하나라도 IQR 규칙을 위반하는 요소를 갖는 레코드를 제거하기 위한 규칙
total_condition = conditions.sum(axis=1) == len(x_train.columns)
x_train = x_train.loc[total_condition] # 이상치 제거
x_train

x_train.shape # 45개 삭제됨

이상치 비율이 45 / 160 = 0.28125로 약 30프로이다.

그렇다면 이상치가 30프로라는게 일반적인 수치라고 볼 수 있을까?

전체 데이터에서 이상치가 30프로라는 것은 사실 말이 안된다. 그래서 이 수치는 이상치 비율이라고 보기에는 매우 높다.

정답은 없지만 일반적으로 이상치의 비율은 1프로 미만이다.  

 

위에서 (Q3 + 1.5 * IQR > val_list) & (Q1 - 1.5 * IQR < val_list)로 IQR_rule을 계산했는데 1.5는 절대적인 수치가 아니므로 이를 조절해도 무방하다. 이 수치를 크게 둘수록 이상치의 비율이 떨어진다.
그래서 이상치의 비율이 1프로 미만이 되도록 조정하는 것이 더 바람직하다.

def IQR_rule(val_list): # 한 특징에 포함된 값(열 벡터)
    # IQR 계산
    Q1 = np.quantile(val_list, 0.25) # 제 1사분위수(25%)
    Q3 = np.quantile(val_list, 0.75) # 제 3사분위수(75%)
    IQR = Q3-Q1
    
    # IQR rule을 위배하지 않는 bool list 계산(True: 이상치 x, False: 이상치 0)
    not_outlier_condition = (Q3 + 6 * IQR > val_list) & (Q1 - 6 * IQR < val_list)
    return not_outlier_condition
# apply를 이용하여 모든 컬럼에 IQR rule 함수 적용
conditions = x_train1.apply(IQR_rule) # axis=0
conditions
# 하나라도 IQR 규칙을 위반하는 요소를 갖는 레코드를 제거하기 위한 규칙
total_condition = conditions.sum(axis=1) == len(x_train1.columns)
x_train1 = x_train1.loc[total_condition] # 이상치 제거
x_train1
x_train1.shape # 3개 삭제됨

이상치의 비율이 약 1프로이다.(3 / 160)

 


이상치 판단 방법 2. 밀도 기반 군집화 활용

밀도 기반 군집화 기법은 군집에 속하지 않은 샘플을 이상치라고 간주하므로, 밀도 기반 군집화 결과를 활용하여 이상치를 판단할 수 있다. 이 방법의 대표적인 알고리즘이 DBSCAN이다.

이 방법은 단일 변수로 이상치를 판단하는 IQR_rule과 다르게,

다른 특징과의 관계까지 반영할 수 있다는 장점이 있다. 하지만 DBSCAN 등의 밀도 기반 군집화 모델은 파라미터 튜닝이 쉽지 않다는 단점이 있다.(DBSCAN으로 모델을 만들 때 eps, min_samples와 같은 파라미터 튜닝이 어렵다.)

 

  • 관련 문법: sklearn.cluster.DBSCAN

DBSCAN 군집화를 수행하는 인스턴스를 생성하는 함수이다.

-주요 입력

▷eps(앱실론): 이웃이라 판단하는 반경

▷min_samples: 중심점이라 판단하기 위해, eps 내에 들어와야 하는 최소 샘플 수

▷metric: 사용하는 거리 척도

-주요 attribute

▷.labels_: 각 샘플이 속한 군집 정보(-1이면 이상치)

 

 

## 코드 실습 ##

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

import pandas as pd
import numpy as np
df = pd.read_csv("glass.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Glass_type', axis=1)
Y = df['Glass_type']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
x_train.shape

from scipy.spatial.distance import cdist
from sklearn.cluster import DBSCAN

 

cdist를 불러온 이유는 DBSCAN의 파라미터를 조정할 때 참고하기 위해서 불러왔다. 앱실론이라는 것은 결국 거리일텐데 그 거리가 데이터의 스케일마다 차이가 크기 때문에 거리를 미리 판단하기 쉽지 않아서 참고하고자 cdist를 불러왔다.

즉, cdist를 이용하면 쉽게 데이터간에 거리를 구할 수 있다.

# x_train과 x_train 거리 행렬계산 => DBSCAN의 파라미터를 설정하기 위함
DM = cdist(x_train, x_train)
DM

np.quantile(DM, 0.1)
# 샘플 간 거리의 10% quantile이 0.6455정도임을 확인

여기서 10%는 작은값을 기준으로 상위 10%를 말하는 것이다. 즉, 작은값을 기준으로 상위 10%의 거리가 0.6455정도 라는 것이다. 0.6455라고 나온값은 단순히 참고하는 수치이고 이게 정확한 기준은 아니다.
np.mean(DM), np.min(DM)처럼 평균이나 최솟값을 쓰기에는 대각행렬은 같은 레코드끼리 거리기 때문에 전부 0이고 대각 행렬 기준 대칭으로 값이 같기 때문에 평균이나 최솟값을 쓰기 어렵다. 그래서 분포통계량인 quantile을 사용했다.

cluster_model = DBSCAN(eps=0.6455, min_samples=3).fit(x_train)
print(sum(cluster_model.labels_ == -1))
# 33개가 이상치로 판단

33개가 이상치로 판단됐다. 이 정도면 너무 많은 양의 이상치라고 판단되어서 파라미터 조정을 하기로 결정했다.

cluster_model = DBSCAN(eps=2, min_samples=3).fit(x_train) # eps=2로 파라미터 조정
print(sum(cluster_model.labels_ == -1))

앱실론을 크게 늘리면 당연히 반경내에 샘플이 많이 들어갈 가능성이 높다. 그래서 이상치라고 판단되는 데이터의 수도 줄어들 것이다.

결과  5 / 160으로 이상치 비율이 약 3프로로 일반적인 이상치 비율인 1프로 미만보다 많은 수치이긴 하지만 이 정도면 괜찮겠다라고 판단했다. 즉, 5개 정도면 괜찮은 양이라고 판단하여 삭제를 한다.

x_train = x_train[cluster_model.labels_ != -1]
x_train.shape

 


=>학습 데이터에서 이상치를 제거해서 일반화된 모델을 만든 후 평가 데이터로 모델 성능 평가

+ Recent posts