이번에는 데이터 전처리에 대하여 정리해보도록 하겠습니다.
머신러닝은 데이터를 바탕으로 학습하고 예측합니다.
즉, 어떤 데이터를 가지고 학습하냐에 따라 해당 모델의 성능이 결정됩니다.
그만큼 데이터는 매우 중요합니다.
하지만 모든 데이터가 완전하지는 않습니다.
데이터에는 결손값(NaN)이 존재할 수 있습니다. 이러한 결손값은 처리되어야합니다.
또한 데이터 중에는 문자열이 존재할 수 있습니다.
하지만 문자열은 모델이 인식할 수 없기 때문에 이를 인코딩하여 숫자로 변환해야 합니다.
이번 시간에는 이러한 데이터 전처리를 어떻게 하는지에 대하여 배워보도록 하겠습니다.
이는 '파이썬 머신러닝 완벽 가이드'를 정리한 내용입니다.
1. 데이터 인코딩
데이터 인코딩은 문자열 피처를 숫자형으로 인코딩하는 것입니다. 문자열 피처에는 카테고리형 피처와 텍스트형 피처가 있습니다. 카테고리형 피처는 문자열 피처를 코드형 피처로 인코딩하여야 하고, 텍스트형 픽처는 피처 벡터화를 하거나 불필요한 피처라고 판단되는 경우 삭제하는 것이 좋습니다. 이번장에서는 카테고리형 피처를 코드형 픽처로 인코딩하는 방법에 대하여 다루도록 하겠습니다. 텍스트형 픽처의 벡터화는 이후에 자세히 설명하도록 하겠습니다.
2. 레이블 인코딩
레이블 인코딩이란, 카테고리 픽처를 코드형 숫자 값으로 변환하는 것을 말합니다. 예를 들어 TV, 냉장고, 전자레인지라는 카테고리가 있을 때 TV : 1, 냉장고 : 2, 전자레인지 : 3과 같은 숫자형을 부여하는 것입니다. 이러한 레이블 인코딩은 LabelEncoder()라는 API를 통해 손쉽게 구현이 가능합니다.
from sklearn.preprocessing import LabelEncoder
items=['TV','냉장고','전자렌지','컴퓨터','선풍기','선풍기','믹서','믹서']
# LabelEncoder를 객체로 생성한 후 , fit( ) 과 transform( ) 으로 label 인코딩 수행.
encoder = LabelEncoder()
encoder.fit(items) # 각 items에 숫자를 대입
labels = encoder.transform(items) # items를 해당하는 숫자로 변환
print('인코딩 변환값:',labels)
인코딩 변환값: [0 1 4 5 3 3 2 2]
하지만 위의 결과값을 보면 알 수 있듯이 인코딩으로 변환된 값으로는 해당 숫자가 어떤 카테고리를 인코딩하였는지 직관적으로 이해하기가 어렵습니다. 이러한 경우 LabelEncoder객체의 속성 값인 classes_를 사용하면 됩니다.
print('인코딩 클래스:',encoder.classes_)
인코딩 클래스: ['TV' '냉장고' '믹서' '선풍기' '전자렌지' '컴퓨터']
또한 inverse_transform()이라는 API를 통해 인코딩 된 값을 다시 디코딩할 수 있습니다.
print('디코딩 원본 값:',encoder.inverse_transform([4, 5, 2, 0, 1, 1, 3, 3]))
디코딩 원본 값: ['전자렌지' '컴퓨터' '믹서' 'TV' '냉장고' '냉장고' '선풍기' '선풍기']
지금까지 살펴보았듯이 레이블 인코딩은 각 카테고리를 구분하기 위해 숫자형을 대입하였습니다. 하지만 이러한 레이블 인코딩은 특정 알고리즘에 대하여 예측 성능이 떨어지는 경우가 발생할 수 있습니다. 우리가 사용한 숫자는 단지 구분을 하기 위함일 뿐 숫자의 크기는 의미가 없습니다. 즉, 숫자가 크다해서 해당 카테고리가 다른 카테고리에 비해 더 중요한 것이 아닙니다. 하지만 선형 회귀와 같은 특정 ML알고리즘에서 더 큰 숫자에 더 큰 가중치를 부여하여 더 중요하게 인식할 가능성이 있습니다. 이러한 알고리즘에서는 레이블 인코딩을 사용하면 안 됩니다. 이러한 문제를 해결하기 위해 나온 것이 바로 원-핫 인코딩(One-Hot Encoding)입니다.
3. 원-핫 인코딩(One-Hot Encoding)
원-핫 인코딩이란, 각 카테고리의 해당하는 칼럼에만 1을 표시하고 나머지 칼럼에는 0을 표시하여 각 카테고리를 구분하는 방법입니다. 예를 들어 왼쪽과 같이 레이블 인코딩한 데이터를 오른쪽과 같이 변환하는 것이 원-핫 인코딩(One-Hot Encoding)입니다.
사이킷런에서는 OneHotEncoder API를 통해 손쉽게 구현할 수 있습니다. 하지만 원-핫 인코딩을 수행하기 위해서는 두 가지의 주의할 점이 있습니다. 첫 번째, OneHotEncoder로 변환하기 이전에 모든 문자열은 레이블 인코딩을 통해 숫자형으로 변환돼야 합니다. 두 번째, 입력값은 2차원 데이터이어야 합니다. 이 두 가지 규칙에 따라 원-핫 인코딩을 해보겠습니다.
from sklearn.preprocessing import OneHotEncoder
import numpy as np
items=['TV','냉장고','전자렌지','컴퓨터','선풍기','선풍기','믹서','믹서']
# 먼저 숫자값으로 변환을 위해 LabelEncoder로 변환합니다.
encoder = LabelEncoder()
encoder.fit(items)
labels = encoder.transform(items)
# 2차원 데이터로 변환합니다.
labels= labels.reshape(-1,1)
# 원-핫 인코딩
oh_encoder = OneHotEncoder()
oh_encoder.fit(labels)
oh_labels=oh_encoder.transform(labels)
print('원-핫 인코딩 데이터')
print(oh_labels.toarray())
print('원-핫 인코딩 데이터 차원')
print(oh_labels.shape)
원-핫 인코딩 데이터
[[1. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0.]
[0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 1.]
[0. 0. 0. 1. 0. 0.]
[0. 0. 0. 1. 0. 0.]
[0. 0. 1. 0. 0. 0.]
[0. 0. 1. 0. 0. 0.]]
원-핫 인코딩 데이터 차원
(8, 6)
하지만 판다스에는 원-핫 인코딩을 더욱 쉽게 해주는 get_dummies()라는 API가 있습니다. get_dummies()를 사용하면 숫자형 변환 없이 바로 변환이 가능합니다.
import pandas as pd
df = pd.DataFrame({'item':['TV','냉장고','전자렌지','컴퓨터','선풍기','선풍기','믹서','믹서'] })
pd.get_dummies(df)
# 위에서 원-핫 인코딩의 예시로 보여준 DataFrame이 코드의 실행 결과입니다.
4. 피처 스케일링과 정규화
지금까지 레이블을 인코딩하는 방법에 대하여 배웠다면 지금부터는 데이터 전처리에 중요한 피처 스케일링(feature scaling)에 대하여 다뤄보도록 하겠습니다. 먼저 피처 스케일링(feature scaling)이란, 서로 다른 변수의 값 범위를 일정한 수준으로 맞추는 작업을 의미합니다. 정의를 보면 이해가 가지 않지만 우리가 그동안 많이 접해본 표준화(Standardization)와 정규화(Nomalization)가 바로 피처 스케일링의 대표적인 방법입니다.
4.1 표준화(Standardization)-StandardScaler
표준화(Standardization)란, 데이터의 피처 각각의 평균이 0이고 분산이 1인 가우시안 정규 분포를 가진 값으로 변환하는 것을 의미합니다. 선형 회귀, 로지스틱 회귀와 같은 몇몇 알고리즘은 데이터가 가우시안 분포 즉 표준화되어있다고 가정하고 구현되어있기 때문에 표준화는 매우 중요합니다. 표준화 방법은 아래 식과 같습니다.
Zi를 새로운 i번째 데이터라고 할 때 Zi는 원래의 값에서 피처 X의 평균을 빼고 이를 피처의 표준편차 S로 나눈 값입니다.
사이키런에서 표준화는 StandardScaler라는 API를 통해 구현이 가능합니다.
표준화를 진행하기 위해 먼저 붓꽃 데이터의 각 feature의 데이터 분포를 살펴보도록 하겠습니다.
from sklearn.datasets import load_iris
import pandas as pd
# 붓꽃 데이터 셋을 로딩하고 DataFrame으로 변환합니다.
iris = load_iris()
iris_data = iris.data
iris_df = pd.DataFrame(data=iris_data, columns=iris.feature_names)
print('feature 들의 평균 값')
print(iris_df.mean())
print('\nfeature 들의 분산 값')
print(iris_df.var())
feature 들의 평균 값
sepal length (cm) 5.843333
sepal width (cm) 3.057333
petal length (cm) 3.758000
petal width (cm) 1.199333
dtype: float64
feature 들의 분산 값
sepal length (cm) 0.685694
sepal width (cm) 0.189979
petal length (cm) 3.116278
petal width (cm) 0.581006
dtype: float64
위의 결과를 보면 알 수 있듯이 각 피처들의 분포가 모두 제각각인 것을 볼 수 있습니다. 이제 각 피처가 모두 가우시안 분포를 따르도록 표준화를 진행하도록 하겠습니다.
from sklearn.preprocessing import StandardScaler
# StandardScaler 표준화객체 생성 및 표준화 수행
scaler=StandardScaler()
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)
# numpy nadarry -> DataFrame
iris_df_scaled = pd.DataFrame(data=iris_scaled, columns=iris.feature_names)
print('feature 들의 평균 값')
print(iris_df_scaled.mean())
print('\nfeature 들의 분산 값')
print(iris_df_scaled.var())
feature 들의 평균 값
sepal length (cm) -1.690315e-15
sepal width (cm) -1.842970e-15
petal length (cm) -1.698641e-15
petal width (cm) -1.409243e-15
dtype: float64
feature 들의 분산 값
sepal length (cm) 1.006711
sepal width (cm) 1.006711
petal length (cm) 1.006711
petal width (cm) 1.006711
dtype: float64
위의 결과를 보면 알 수 있듯이 각 feature들의 평균이 0에 아주 가까운 값으로, 분산은 1에 가까운 값으로 변화되었음을 알 수 있습니다. 그리 어렵지않쥬??
4.2 정규화(Nomalization) - MinMaxScaler
다음으로는 정규화(Nomalization)에 대하여 알아보도록 하겠습니다. 정규화(Nomalization)란, 서로 다른 피처의 크기를 통일하기 위해 크기를 변환해주는 개념입니다.
X_new를 새로운 i번째 데이터라고 할 때 X_new는 원래의 값에서 피처 X의 최솟값을 빼고 이를 피처의 최댓값과 최솟값의 차로 나눈 값입니다.
위의 정의와 방법만으로는 정규화의 개념을 이해하기 어렵기 때문에 예시를 통해 설명하도록 하겠습니다. 예를 들어 빼빼로의 길이가 5~30cm로 주어진다고 가정해봅시다. 그리고 빼빼로의 가격이 1000~10000원이라고 가정해봅시다. 여기서 빼빼로의 길이와 가격은 cm, 원으로 단위가 달라 두 피처를 직접적으로 비교하기가 매우 애매합니다. 이때 서로 다른 단위를 가진 두 피처를 비교하기 위해 개별 데이터의 크기를 모두 똑같은 단위, 최소 0 ~ 최대 1의 값으로 모두 변환하는 것이 정규화입니다.
정규화는 MinMaxScalar라는 API를 통해 구현이 가능합니다.
from sklearn.preprocessing import MinMaxScaler
# MinMaxScaler 정규화 객체 생성 및 정규화 수행
scaler = MinMaxScaler()
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)
# numpy nadarry -> DataFrame
iris_df_scaled = pd.DataFrame(data=iris_scaled, columns=iris.feature_names)
print('feature들의 최소 값')
print(iris_df_scaled.min())
print('\nfeature들의 최대 값')
print(iris_df_scaled.max())
feature들의 최소 값
sepal length (cm) 0.0
sepal width (cm) 0.0
petal length (cm) 0.0
petal width (cm) 0.0
dtype: float64
feature들의 최대 값
sepal length (cm) 1.0
sepal width (cm) 1.0
petal length (cm) 1.0
petal width (cm) 1.0
dtype: float64
위의 결과를 보면 알 수 있듯이 피처들의 모든 값이 최소 0에서, 최대 1의 값을 가짐을 알 수 있습니다.
4.3 학습 데이터와 테스트 데이터의 스케일링 변환 시 유의점
지금까지 피처 스케일링을 하는 대표적인 두 가지 방법에 대하여 설명하였습니다. 하지만 피처 스케일링을 할 때 유의해야 할 매우 중요한 사항이 있습니다. 유의점이 무엇인지 살표보기 전에 먼저 그동안 우리가 자주 사용하였던 fit(), transform()의 역할에 대하여 살펴보도록 하겠습니다.
먼저 fit()은 데이터 변환을 위한 기준 정보 설정을 적용해주는 것입니다. 여기서 말하는 기준 정보 설정이란 표준화라면 데이터 세트의 평균, 분산을 의미할 것이고 정규화라면 데이터 세트의 최소, 최댓값을 의미합니다.
그럼 transform()은 무엇이냐, fit()에서 설정한 기준 정보를 이용해서 실질적으로 데이터를 변환해주는 것입니다. fit()과 transform()의 이러한 역할을 이해하고 데이터 스케일링 변환 시 유의점이 무엇인지 살펴보도록 하겠습니다.
데이터 스케일링의 핵심은 바로 학습 데이터의 기준 정보로 테스트 데이터를 스케줄링해주어야 한다는 것입니다. 아래 예제 코드를 살펴보면서 이에 대해 자세히 설명하도록 하겠습니다.
from sklearn.preprocessing import MinMaxScaler
#학습 데이터는 0~10, 테스트 데이터는 0~5
# fit(), trnasform()은 2차원 이상의 데이터만 가능
train_array=np.arange(0,11).reshape(-1,1)
test_array=np.arange(0,6).reshape(-1,1)
먼저 학습 데이터와 테스트 데이터를 위와 같이 설정해줍니다.
# MinMaxScaler
scaler = MinMaxScaler()
# fit()하게 되면 train_array 데이터의 최솟값이 0, 최댓값이 10으로 설정.
scaler.fit(train_array)
# 1/10 scale로 train array 데이터 변환. 원본 10 -> 1로 변환
train_scaled = scaler.transform(train_array)
print('원본 train_array 데이터:',np.round(train_array.reshape(-1),2))
print('Scale된 train_array 데이터:',np.round(train_scaled.reshape(-1),2))
다음으로 앞서 배운 MinMaxScalar를 이용하여 학습 데이터를 정규화하도록 하겠습니다. 학습 데이터의 최댓값은 10이고 최솟값은 0이므로 이를 정규화하면 1/10 Scale을 적용될 것입니다. 정규화 결과는 아래와 같습니다.
원본 train_array 데이터: [ 0 1 2 3 4 5 6 7 8 9 10]
Scale된 train_array 데이터: [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]
그럼 이번엔 테스트 데이터도 동일하게 MinMaxScalar를 이용하여 테스트 데이터를 정규화하도록 하겠습니다.
# fit()하게 되면 test_array 데이터의 최솟값이 0, 최댓값이 5으로 설정.
scaler.fit(test_array)
# 1/5 scale로 train array 데이터 변환. 원본 5 -> 1로 변환
test_scaled = scaler.transform(test_array)
print('원본 test_array 데이터:',np.round(test_array.reshape(-1),2))
print('Scale된 test_array 데이터:',np.round(test_scaled.reshape(-1),2))
테스트 데이터의 최댓값은 5이고 최솟값은 0이므로 이를 정규화하면 1/5 Scale을 적용될 것입니다. 정규화 결과는 아래와 같습니다.
원본 test_array 데이터: [0 1 2 3 4 5]
Scale된 test_array 데이터: [0. 0.2 0.4 0.6 0.8 1. ]
이렇게 우리가 앞서 배운 방법으로 학습 데이터와 테스트 데이터를 정규화해주었습니다. 그런데 여기서 한 가지 이상한 점을 발견하실 수 있을 겁니다. 머신러닝은 학습 데이터를 통해 학습을 하고 테스트 데이터를 통해 평가를 합니다. 그런데 학습 데이터에서 1은 0.1로 변환되었는데, 테스트 데이터에서 1은 0.2로 변환되었습니다. 동일한 원본 값이 서로 다른 값으로 변환된 것입니다. 이러한 문제점이 나타는 이유는 바로 fit() 때문입니다. 즉, 서로 다른 기준정보를 가지고 변환하였기 때문입니다.
학습 데이터의 기준정보로 테스트 데이터를 변환해야 하지만 위의 테스트 예제를 변환하는 과정에서 fit()을 사용함으로써 테스트 데이터만의 기준정보를 따로 구하여 변환한 것입니다. 즉, 절대 테스트 데이터에서는 fit()을 사용하면 안 됩니다!! 절대!!!
지금부터는 위의 문제점을 해결한 코드를 살펴보도록 하겠습니다.
scaler = MinMaxScaler()
scaler.fit(train_array)
train_scaled = scaler.transform(train_array)
print('원본 train_array 데이터:',np.round(train_array.reshape(-1),2))
print('Scale된 train_array 데이터:',np.round(train_scaled.reshape(-1),2))
# test_array에서는 Scale 시 fit()을 호출하지 않고 transform()만 호출!
test_scaled = scaler.transform(test_array)
print('원본 test_array 데이터:',np.round(test_array.reshape(-1),2))
print('Scale된 test_array 데이터:',np.round(test_scaled.reshape(-1),2))
원본 train_array 데이터: [ 0 1 2 3 4 5 6 7 8 9 10]
Scale된 train_array 데이터: [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]
원본 test_array 데이터: [0 1 2 3 4 5]
Scale된 test_array 데이터: [0. 0.1 0.2 0.3 0.4 0.5]
위의 결과를 보면 알 수 있듯이 이제 드디어 학습 데이터와 테스트 데이터의 동일한 원본 값이 동일한 값으로 변환되었음을 알 수 있습니다. 이를 요약하면 다음과 같습니다.
1. 가능하다면 전체 데이터의 스케일링 변환을 적용한 뒤 학습 데이터와 테스트 데이터를 분리
2. 1이 여의치 않다면 데스트 데이터 변환 시 fit()을 사용하지 않고, 학습 데이터를 통해 구한 기준정보를 이용하여 테스트 데이터 셋에 transform()으로 적용
이상으로 데이터 전처리 파트 정리를 모두 끝냈습니다.
머신러닝의 알고리즘이 아무리 훌륭할지라도 데이터가 올바르지 않다면
절대로 제대로 된 모델은 나올 수 없습니다.
따라서 데이터를 분석하는 것 또한 머신러닝을 공부하는 데 있어
매우 중요하다는 사실을 명심해야 합니다!!
사진 출처
피처스케일링과 정규화 : https://rucrazia.tistory.com/90
'파이썬 머신러닝 완벽가이드' 카테고리의 다른 글
3.2 평가(F1 스코어, ROC와 AUC) (0) | 2021.11.21 |
---|---|
3.1 평가(정밀도/재현율) (0) | 2021.11.21 |
2.4 사이킷런으로 수행하는 타이타닉 생존자 예측 (0) | 2021.11.19 |
2.2 교차검증 (0) | 2021.11.14 |
2.1 첫 번째 머신러닝 만들어 보기 - 붓꽃 품종 예측하기 (0) | 2021.11.13 |