클래스 변수가 하나의 값에 치우친 데이터로 학습한 분류 모델이 치우친 클래스에 대해 편향되는 문제로, 이러한 모델은 대부분 샘플을 치우친 클래스 값으로만 분류하게 된다(예시: 암환자 판별 문제)
예를들어, 암환자와 정상인이 있을 때 암환자 수가 훨씬 적을텐데 이러한 데이터를 가지고 모델을 만들게 되면 이 모델은 대부분을 정상인이라고 분류를 할 것이다. 그렇게되면 우리가 더 판별하고 싶어하는 암환자를 제대로 판별하지 못하는 상황이 발생할 수 있다.
그리고 클래스 불균형 문제는 분류에서만 발생하는 문제이다.
위의 그림을 통해 알 수 있듯이, 클래스 불균형 문제가 있는 모델은 정확도가 높고, 재현율이 매우 낮은 경향이 있다.
용어
# 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값이 커질수록 더욱 민감하다는 소리이다.
클래스 불균형 문제 해결의 기본 아이디어
클래스 불균형 문제 해결의 기본 아이디어는 소수 클래스에 대한 결정 공간을 넓히는 것이다.
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하는 결론을 이끌 수 있어야 한다.)
즉, '어떤 상황에서 이렇게 하면 무조건 좋아진다'라고 하는 전처리 기법, 탐색 기법, 모델링 기법은 존재 할 수 없다.
# 평가 데이터도 같은 방법으로 전처리 수행
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
변수 범위에서 많이 벗어난 아주 작은 값이나 아주 큰 값으로, 일반화된 모델을 생성하는데 악영향을 끼치는 값으로 이상치를 포함하는 레코드를 제거하는 방법으로 이상치를 제거한다.
이상치는 결측치와 다르게 값을 추정을 하는 것이 아니라 제거해야한다.(절대 추정의 대상이 아님에 주의한다.)
이상치를 제거해야하는 대표적인 모델들은 클래스의 평균을 쓰는 모델들이다.(트리계열 모델, 거리를 사용하는 모델: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를 불러왔다.
np.quantile(DM, 0.1)
# 샘플 간 거리의 10% quantile이 0.6455정도임을 확인
여기서 10%는 작은값을 기준으로 상위 10%를 말하는 것이다. 즉, 작은값을 기준으로 상위 10%의 거리가 0.6455정도 라는 것이다. 0.6455라고 나온값은 단순히 참고하는 수치이고 이게 정확한 기준은 아니다. np.mean(DM), np.min(DM)처럼 평균이나 최솟값을 쓰기에는 대각행렬은 같은 레코드끼리 거리기 때문에 전부 0이고 대각 행렬 기준 대칭으로 값이 같기 때문에 평균이나 최솟값을 쓰기 어렵다. 그래서 분포통계량인 quantile을 사용했다.
일반화된 모델을 학습하는데 어려움이 있는 분포를 가지는 변수가 있어서, 일반화된 모델을 학습하지 못하는 문제이다.
변수 분포 문제를 해결하지 않아도 모델 학습 자체는 가능하지만 좋은 성능을 기대하기 어렵다.
변수 분포 문제의 대표적인 다섯가지를 알아볼 것이다.
1) 특징과 라벨 간 약한 관계 또는 비선형 관계일 때
2) 이상치를 포함하는 데이터가 있을 때
3) 특징 간 상관성이 높아서 발생하는 문제
4) 변수가 정규분포를 따르지 않고 일반분포(알수없는 분포)를 따르는 경우(변수 치우침 제거)
5) 변수 간 스케일 차이가 커서 발생하는 문제(스케일링)
특징과 라벨 간 약한 관계 또는 비선형 관계일 때
특징과 라벨 간 관계가 없거나 매우 약하다면, 어떠한 전처리 및 모델링을 하더라도 예측력이 높은 모델을 만들 수 없다.
가장 이상적인 해결 방안은 라벨에 유의미한 특징의 데이터를 구하는 것이다.
그 다음 이상적인 해결 방안은 각 특징에 대해, 특징과 라벨 간 관계를 나타내는 그래프를 통해 적절한 특징 변환을 수행해야 한다.(특징과 라벨 간 비선형 관계가 존재한다면, 적절한 전처리를 통해 모델 성능을 크게 향상시킬 수 있음)
예를들어, 대다수의 머신러닝 모델은 선형식(wx+b)을 포함하는데(선형회귀모델, 로지스틱회귀모델, svm, mlp) 선형회귀 모델에서 데이터랑 초록색 모델은 거의 일치하는 것을 볼수있으나 파란색 모델은 실제 데이터의 분포를 거의 반영하지 못한다. 모델1은 X를 그대로 사용하고 모델2는 X^2을 사용한 것 밖에 차이가 없지만 단순한 변환임에도 불구하고 데이터의 X와 Y간 비선형 관계가 있는 경우(특징과 라벨 간 비선형 관계가 있는 경우)에는 특징 변환을 해줬을 때 엄청난 퍼포먼스를 얻을 수 있다.
→MAE나 RMSE는 구하지 않아도 당연히 초록색 모델의 성능이 훨씬 우수할 것으로 예상한다.
(주요 모델의 구조 포스팅 참고)
하지만 특징 개수가 많고, 다른 특징에 의한 영향도 존재하는 등 그래프를 통해 적절한 변환 방법을 선택하는 것은 쉽지 않다.→현실적으로 어려움
결론: 다양한 변환 방법(x제곱도 만들어보고, 로그x도 만들어보고, exp(x)도 만들어보고, 제곱근도 만들어보고, 아니면 두 컬럼을 더해보고, 빼보고 하는 등의 특징들을 만들어보고)을 시도하여 특징을 생성한 뒤 모델의 성능이 좋아지는 것을 고르면되고, 그 가운데 특징이 너무 많이 생기니 특징 선택까지 포함시키면 문제를 해결할수 있다.
즉, 다양한 변환 방법을 사용하여 특징을 생성한 뒤 특징 선택을 수행해야 한다.(특징 선택은 추후 포스팅에서 공부할 예정이다.)
## 코드 실습 ##
이번 실습은 기존 특징들을 가지고 다양한 변환 방법을 사용하여 새로운 특징들을 임의로 생성해서 모델링을 해보고, 기존 특징들만 가지고 모델링을 한 결과와 비교를 위한 실습이다.
import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")
import pandas as pd
import numpy as np
# 특징과 라벨 분리
X = df.drop('EP', axis=1)
Y = df['EP']
# 모든 피쳐가 연속형
for i in X.columns:
print(i, len(df[i].unique()))
# 신규 데이터 생성
# 특징이 추가된 데이터를 부착할 데이터
X_added = X.copy()
# 로그, 제곱 관련 변수 추가
# 만약, 범주형 변수와 연속형 변수가 혼합인 경우 나였으면 연속형 변수만 빼서 변수 추가 후 합쳤을듯...
for col in X.columns:
X_added[col + '_squared'] = X[col]**2
X_added[col + '_log'] = np.log(X[col])
X_added.head()
(범주형 변수의 경우 원핫인코딩 후 로그변환을 하게되면 값이 달라져서 원본 데이터 손상이 되고, 제곱을 해도 똑같은 값이므로 연속형 변수만 특징변환?)
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LinearRegression as LR
# 5겹 교차검증 기반의 평가 수행
X_score = -cross_val_score(LR(), X, Y, cv=5, scoring='neg_mean_absolute_error').mean()
X_added_score = -cross_val_score(LR(), X_added, Y, cv=5, scoring='neg_mean_absolute_error').mean()
# 특징을 추가했을 때 성능이 좋아졌다.
print("특징 추가 전: {}, 특징 추가 후: {}".format(X_score, X_added_score))
● 데이터에 범주형 변수가 포함되어 있을 경우 대다수의 지도학습 모델이 학습되지 않거나 비정상적으로 학습이 된다.
▷str 타입의 범주형 변수가 포함되면 대다수의 지도학습 모델 자체가 학습이 되지 않는다. str 타입의 범주형 변수가 포함되더라도 그나마 정상적으로 학습되는 모델은 트리 계열 모델밖에 없다.
▷int 혹은 float 타입의 범주형 변수 또한 모델 학습이 가능하지만 비정상적으로 학습이 되기 때문에 적절한 범주형 변수 처리를 해야 한다.
● 모델 학습을 위해 범주형 변수는 반드시 숫자로 변환되어야 하지만, 임의로 설정하는 것은 매우 부적절하다.
예시) 종교 변수: 기독교=1, 불교=2, 천주교=3
→불교는 기독교의 2배라는 등의 대수 관계가 실제로 존재하지 않지만, 이런식으로 변환하면 비정상적인 관계가 생기기 때문에 모델에 악영향을 끼칠수 밖에 없다.
● 코드화된 범주형 변수도 적절한 숫자로 변환해줘야 한다.
결론: 문자로 된 범주형 변수, 코드화된 범주형 변수를 적절한 숫자로 변환해줘야 한다.
범주형 변수 판별 방법
범주형 변수는 상태 공간의 크기가 유한한 변수이다.
주의할 점은 int 혹은 float 타입으로 정의된 변수는 반드시 연속형 변수가 아닐 수 있다.
즉, 데이터의 type을 보고 "type이 float이니까 연속형, type이 str이니까 범주형" 이런식으로 판단하면 안된다.
그래서 반드시 도메인이나 변수의 상태 공간을 바탕으로 판단해야 한다.
예를 들어, 월(month), 시간(hour) 숫자로 되어있지만 범주형 변수이다.
범주형 변수 변환 방법 1. 더미화
가장 일반적인 범주형 변수를 변환하는 방법으로, 범주형 변수가 특정 값을 취하는지 여부를 나타내는 더미변수를 생성하는 방법이다.
위에 그림을 부과적으로 설명하자면,
불교 변수는 나머지 변수로 완벽히 추론 가능하므로 기독교, 천주교, 불교 이 세 변수 간에는 상관성이 완벽하게 존재한다. 특징 간 상관성이 완벽하게 존재하는 경우 정상적인 모델 학습이 어려울수 있기 때문에 상관성 제거를 위해 불교 변수를 제거한다.(기독교, 천주교, 불교 제거 / 기독교, 불교, 천주교 제거 / 천주교, 불교, 기독교 제거 중 하나로)
예외로, 트리계열의 모델을 사용하는 경우에는 설명력을 위해서 변수를 제거 안하는 경우가 있다. 이런 경우 설명력을 위해서 그런거지 예측력을 위해서는 좋은 접근이 아니다.
단점: 한 범주형 변수가 상태 공간이 클 때 더미화를 할 경우 추가되는 변수가 엄청나게 많아질수 있다. 예를 들어, 어떤 범주형 변수의 상태 공간의 크기가 100일 때 100개의 변수가 추가된다. 이렇게 변수가 많아지면 차원의 저주 문제로 이어질수 있다. 그리고 데이터가 sparse(희소)해 질수도 있다.
결론적으로 범주형 변수의 상태 공간이 클때는 더미화를 하는게 적합하지 않다.
범주형 변수 변환 방법 2. 연속형 변수로 치환
범주형 변수의 상태 공간 크기가 클때, 더미화는 과하게 많은 변수를 추가해서 차원의 저주 문제로 이어질 수 있다.
라벨 정보(분류, 예측 둘다 가능)를 활용하여 범주 변수를 연속형 변수로 치환하면 기존 변수가 가지는 정보가 일부 손실될 수 있고 활용이 어렵다는 단점이 있으나, 차원의 크기가 변하지 않으며 더 효율적인 변수로 변활할 수 있다는 장점이 있다.
이 방법을 쓸 때 장점은 더미화를 하는 것과 다르게 차원이 늘어나지 않는다. 그리고 범주형 변수를 라벨과 관련된 연속형 변수로 치환을 해주기 때문에 즉, 라벨을 고려해서 변환했기 때문에 모델의 성능을 높일 수 있다. 단점은 온전한 기존 정보가 손실이 어느정도 일어날 것이다. 위의 그림을 보면 만약에 A가 200, B도 200이라면 A와 B가 같은 값으로 인식하게 되어 정보 손실이 일어날 수도 있다.
결론: 상태 공간의 크기가 작으면 더미화, 상태 공간의 크기가 크면 연속형 변수로 치환하는 방법을 사용한다.
● 관련 문법: Series.unique()
Series에 포함된 유니크한 값을 반환해주는 함수로, 상태 공간을 확인하는데 사용한다.
● 관련 문법: feature_engine.categorical_encoders.OneHotCategoricalEncoder
변경:feature_engine.encoding.OneHotEncoder
더미화를 하기 위한 함수로, 활용 방법은 sklearn의 인스턴스의 활용 방법과 유사하다.
-주요 입력
▷variables: 더미화 대상이 되는 범주형 변수의 이름 목록(주의: 해당 변수는 반드시 str 타입이어야 함, 숫자로 된 범주형 변수인 경우 astype 함수를 사용해서 자료형을 str으로 바꾸고 사용해야 함) → variables에 리스트를 넣어줌
▷drop_last: 한 범주 변수로부터 만든 더미변수 가운데 마지막 더미변수를 제거할 지를 결정
▷top_categories: 한 범주 변수로부터 만든 더미변수 개수를 설정하며, 빈도 기준으로 자름(예를들어, 한 범주형 변수의 상태 공간이 100개일때 100개의 특징을 다 쓰기 어렵기 때문에 그 중에 빈도가 높은 상위 10개만 쓰는 방식이다.)
참고사항: pandas.get_dummies()는 이 함수보다 사용이 훨씬 간단하지만, 인스턴스로 저장되는 것이 아니기 때문에 학습 데이터와 새로 들어온 데이터를 똑같이 처리 할 수 없다.
학습 데이터에 포함된 범주형 변수를 처리한 방식으로 새로 들어온 데이터에 적용이 불가능하기 때문에, 실제적으로 활용이 어려워서 머신러닝 모델을 구축할때는 잘 쓰이지 않는다.
## 코드 실습 ##
import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝을 위한 필수 전처리\Part 4. 머신러닝을 위한 필수 전처리\데이터")
import pandas as pd
df= pd.read_csv("car-good.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)
# 자세한 범주형 변수 판별 -> 모든 변수가 범주형임을 확인
for col in x_train.columns:
print(col, x_train[col].unique(), len(x_train[col].unique()))
더미화를 이용한 범주 변수 처리
x_train = x_train.astype(str) # 모든 변수가 범주형이므로, 더미화를 위해 전부 string 타입으로 변환
from feature_engine.encoding import OneHotEncoder as OHE
# 인스턴스화
dummy_model = OHE(variables=x_train.columns.tolist(),
drop_last=True)
# 더미화 대상 컬럼은 모든 컬럼이기 때문에 x_train.columns를 입력
# variables로 list를 받음->tolist를 이용해서 리스트로 바꿈
# 학습(fit)
dummy_model.fit(x_train)
# transform
d_x_train = dummy_model.transform(x_train)
d_x_test = dummy_model.transform(x_test)
# 더미화를 한 뒤의 모델 테스트
from sklearn.neighbors import KNeighborsClassifier as KNN
model = KNN().fit(d_x_train, y_train)
pred_y = model.predict(d_x_test)
from sklearn.metrics import f1_score
f1_score(y_test, pred_y)
# 더미화를 해서 데이터가 sparse 해지는 경향이 있기 때문에
# 모델을 KNN을 써서 성능을 올리고 싶으면 metric으로 'jaccard'를 쓰면 좀 더 좋은 성능을 기대할 수 있다.
from sklearn.neighbors import KNeighborsClassifier as KNN
model = KNN(metric='jaccard').fit(d_x_train, y_train)
pred_y = model.predict(d_x_test)
from sklearn.metrics import f1_score
f1_score(y_test, pred_y)
# 성능이 조금 오름
연속형 변수로 치환
# 라벨 정보를 활용하기 위해서 분할했던 데이터프레임 합침
train_df = pd.concat([x_train, y_train], axis=1)
train_df.head()
# x_train 컬럼들이 모두 범주형 변수이므로 x_train.columns를 for문에 사용
# 만약, 범주형이 아닌 변수가 있을 경우 분할해서 연속형 변수로 치환 후 합침
# x_train_cate = x_train[['범주형 변수'...]], x_test_cate = x_test[['범주형 변수'...]]
for col in x_train.columns:
# col에 따른 Class의 평균을 나타내는 사전(replace를 쓰기 위해, 사전으로 만듦)
temp_dict = train_df.groupby(col)['Class'].mean().to_dict()
#print(temp_dict)
#print('\n', '*'*100, '\n')
# 변수 치환
train_df[col] = train_df[col].replace(temp_dict)
#print(train_df[col])
#print('\n', '*'*100, '\n')
# 테스트 데이터도 같이 치환해줘야 함
x_test[col] = x_test[col].astype(str).replace(temp_dict)
#print(x_test[col])
#print('\n', '*'*100, '\n')
# 모델링을 위해 학습 데이터를 다시 라벨과 분리
x_train = train_df.drop('Class', axis=1)
y_train = train_df['Class']
# 연속형 변수로 치환한 뒤의 모델 테스트
model = KNN().fit(x_train, y_train)
pred_y = model.predict(x_test)
f1_score(y_test, pred_y)
# 라벨을 고려한 전처리이므로 더미화보다 좋은 결과가 나온 것을 확인
# 차원도 줄고 모델의 성능이 더 좋아짐