데이터 전처리(Data Preprocessing)
04 Apr 2023 | Machine-Learning
데이터 전처리(Data Preprocessing) 는 ML 알고리즘만큼 중요 합니다. ML 알고리즘은 데이터에 기반하고 있기 때문에 어떤 데이터를 입력으로 가지느냐에 따라 결과도 크게 달라질 수 있습니다(GarbageIn, Garbage Out).
사이킷런의 ML 알고리즘을 적용하기 전에 데이터에 대해 미리 처리해야 할 기본사항이 있습니다.
- 결손값, 즉 NaN, Null 값은 허용되지 않습니다.
- 그러므로 Null 값은 고정된 다른 값으로 변환해야 합니다.
- 사이킷런의 머신러닝 알고리즘은 문자열 값을 입력값으로 허용하지 않습니다.
- 그래므로 모든 문자열 값은 인코딩돼서 숫자 형으로 변환해야 합니다.
데이터 인코딩
머신러닝을 위한 대표적인 인코딩 방식은 레이블 인코딩(Label encoding) 과 원 핫 인코딩(One Hotencoding) 이 있습니다.
레이블 인코딩(Label encoding)
레이블 인코딩 은 카테고리 피처를 코드형 숫자 값으로 변환하는 것 입니다.
사이킷런의 레이블 인코딩은 LabelEncoder 클래스로 구현합니다. LabelEncoder 를 객체로 생성한 후 fit() 과 transform() 을 호출해 레이블 인코딩을 수행합니다.
from sklearn.preprocessing import LabelEncoder
items=['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서']
# LabelEncoder를 객체로 생성한 후 , fit() 과 transform() 으로 label 인코딩 수행.
encoder = LabelEncoder()
encoder.fit(items)
labels = encoder.transform(items)
print('인코딩 변환값:', labels)
[output]
인코딩 변환값: [0 1 4 5 3 3 2 2]
TV는 0, 냉장고는 1, 전자레인지는 4, 컴퓨터는 5, 선풍기는 3, 믹서는 2로 변환됐습니다. 위 예제는 데이터가 작아서 문자열 값이 어떤 숫자 값으로 인코딩됐는지 LabelEncoder 객체의 classes_ 속성값으로 확인할 수 있습니다.
print('인코딩 클래스:', encoder.classes_)
[output]
인코딩 클래스: ['TV' '냉장고' '믹서' '선풍기' '전자레인지' '컴퓨터']
classes_ 속성은 0번부터 순서대로 변환된 인코딩 값에 대한 원본값을 가지고 있습니다. 따라서 TV가 0, 냉장고 1, 믹서 2, 선풍기 3, 전자레인지 4, 컴퓨터가 5로 인코딩됐음을 알 수 있습니다. inverse_transform() 을 통해 인코딩된 값을 다시 디코딩할 수 있습니다.
print('디코딩 원본 값:', encoder.inverse_transform([4, 5, 2, 0, 1, 1, 3, 3]))
[output]
디코딩 원본 값: ['전자레인지' '컴퓨터' '믹서' 'TV' '냉장고' '냉장고' '선풍기' '선풍기']
상품 데이터가 상품 분류, 가격 두 개의 속성으로 돼 있을 때 상품 분류를 레이블 인코딩하면 다음과 같이 변환될 수 있습니다.
레이블 인코딩은 간단하게 문자열 값을 숫자형 카테고리 값으로 변환합니다. 하지만 레이블 인코딩이 일괄적인 숫자 값으로 변환이 되면서 몇몇 ML 알고리즘에는 이를 적용할 경우 예측 성능이 떨어지는 경우가 발생할 수 있습니다. 이는 숫자 값의 경우 크고 작음에 대한 특성이 작용하기 때문 입니다.
즉, 냉장고가 1, 믹서가 2로 변환되면, 1보다 2가 더 큰 값이므로 특정 ML 알고리즘에서 가중치가 더 부여되거나 더 중요하게 인식할 가능성이 발생합니다. 하지만 냉장고와 믹서의 숫자 변환 값은 단순 코드이지 숫자 값에 따른 순서나 중요도로 인식돼서는 안 됩니다. 이러한 특성 때문에 레이블 인코딩은 선형 회귀와 같은 ML 알고리즘에는 적용하지 않아야 합니다. 트리 계열의 ML 알고리즘은 이러한 특성을 반영하지 않으므로 레이블 인코딩도 별문제가 없습니다.
원-핫 인코딩(One-Hot Encoding)
원–핫 인코딩(One-Hot Encoding) 은 레이블 인코딩의 문제점을 해결하기 위한 인코딩 방식 으로, 피처 값의 유형에 따라 새로운 피처를 추가해 고유 값에 해당하는 칼럼에만 1을 표시하고 나머지 칼럼에는 0을 표시하는 방식 입니다.
즉, 행 형태로 돼 있는 피처의 고유 값을 열 형태로 차원을 변환한 뒤, 고유 값에 해당하는 칼럼에만 1을 표시하고 나머지 칼럼에는 0을 표시 하며, 이러한 특성으로 원-핫(여러 개의 속성 중 단 한 개의 속성만 1로표시) 인코딩으로 명명하게 됐습니다.
원-핫 인코딩은 사이킷런에서 OneHotEncoder 클래스 로 변환이 가능합니다. 약간 주의할 점으로, 입력값으로 2차원 데이터가 필요하다는 것 과, OneHotEncoder 를 이용해 변환한 값이 희소 행렬(Sparse Matrix) 형태이므로 이를 다시 toarray() 메서드를 이용해 밀집행렬(Dense Matrix)로 변환해야 한다는 것 입니다.
OneHotEncoder 를 이용해 앞의 데이터를 원-핫인코딩으로 변환해 보겠습니다.
from sklearn.preprocessing import OneHotEncoder
import numpy as np
items=['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서']
# 2차원 ndarray로 변환합니다.
items = np.array(items).reshape(-1, 1)
# 원-핫 인코딩을 적용합니다.
oh_encoder = OneHotEncoder()
oh_encoder.fit(items)
oh_labels = oh_encoder.transform(items)
# OneHotEncoder로 변환한 결과는 희소행렬이므로 toarray()를 이용해 밀집 행렬로 변환.
print('인코딩 클래스:', oh_encoder.categories_)
print('OneHotEncoder로 변환한 값: 희소 행렬(Sparse Matrix)')
print(oh_labels)
print('원-핫 인코딩 데이터 : 밀집행렬(Dense Matrix) 변환')
print(oh_labels.toarray())
print('원-핫 인코딩 데이터 차원')
print(oh_labels.shape)
[output]
인코딩 클래스: [array(['TV', '냉장고', '믹서', '선풍기', '전자레인지', '컴퓨터'], dtype='<U5')]
OneHotEncoder로 변환한 값: 희소 행렬(Sparse Matrix)
(0, 0) 1.0
(1, 1) 1.0
(2, 4) 1.0
(3, 5) 1.0
(4, 3) 1.0
(5, 3) 1.0
(6, 2) 1.0
(7, 2) 1.0
원-핫 인코딩 데이터 : 밀집행렬(Dense Matrix) 변환
[[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)
위 예제 코드의 변환 절차는 다음 그림과 같이 정리할 수 있습니다.
판다스 에는 문자열 카테고리 값을 숫자 형으로 변환할 필요 없이 바로 변환할 수 있는, 원-핫 인코딩을 더 쉽게 지원하는 API가 있습니다. get_dummies() 를 이용하면 됩니다.
import pandas as pd
df = pd.DataFrame({'item': ['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서'] })
pd.get_dummies(df)
get_dummies() 를 이용하면 숫자형 값으로 변환 없이도 바로 변환이 가능함을 알 수 있습니다.
피처 스케일링과 정규화
서로 다른 변수의 값 범위를 일정한 수준으로 맞추는 작업 을 피처 스케일링(feature scaling) 이라고 합니다. 대표적인 방법으로 표준화(Standardization) 와 정규화(Normalization) 가 있습니다.
표준화 는 데이터의 피처 각각이 평균이 0이고 분산이 1인 가우시안 정규 분포를 가진 값으로 변환하는것을 의미 합니다. 표준화를 통해 변환될 피처 $x$ 의 새로운 $i$ 번째 데이터를 ${x_i}_new$ 라고 한다면 이 값은 원래 값에서 피처 $x$ 의 평균을 뺀 값을 피처 $x$ 의 표준편차로 나눈 값으로 계산할 수 있습니다.
\[{x_i}\_new=\frac{x_i-mean(x)}{stdev(x)}\]
정규화 는 서로 다른 피처의 크기를 통일하기 위해 크기를 변환해주는 개념 입니다. 즉, 개별 데이터의 크기를 모두 똑같은 단위로 변경하는 것 입니다.
새로운 데이터 ${x_i}_new$ 는 원래 값에서 피처 $x$ 의 최솟값을 뺀 값을 피처 $x$ 의 최댓값과 최솟값의 차이로 나눈 값으로 변환할 수 있습니다.
\[{x_i}\_new=\frac{x_i-min(x)}{max(x)-min(x)}\]
사이킷런의 Normalizer 모듈은 선형대수에서의 정규화 개념이 적용됐으며, 개별 벡터의 크기를 맞추기 위해 변환하는 것을 의미합니다. 즉, 개별 벡터를 모든 피처 벡터의 크기로 나눠 줍니다. 세 개의 피처 $x,\ y,\ z$ 가 있다고 하면 새로운 데이터 ${x_i}_new$ 는 원래 값에서 세 개의 피처의 $i$ 번째 피처 값에 해당하는 크기를 합한 값으로 나눠줍니다.
\[{x_i}\_new=\frac{x_i}{\sqrt{x_i^2+y_i^2+z_i^2}}\]
StandardScaler
StandardScaler 는 앞에서 설명한 표준화를 쉽게 지원하기 위한 클래스 입니다. 즉, 개별 피처를 평균이 0이고, 분산이 1인 값으로 변환 해줍니다. 이렇게 가우시안 정규 분포를 가질 수 있도록 데이터를 변환하는 것은 몇몇 알고리즘(데이터가 가우시안 분포를 가지고 있다고 가정하고 구현)에서 매우 중요합니다.
StandardScaler가 어떻게 데이터 값을 변환하는지 데이터 세트로 확인해 보겠습니다.
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())
[output]
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
이제 StandardScaler 를 이용해 각 피처를 한 번에 표준화해 변환하겠습니다. StandardScaler 객체를 생성한 후에 fit() 과 transform() 메서드에 변환 대상 피처 데이터 세트를 입력하고 호출하면 간단하게 변환됩니다.
from sklearn.preprocessing import StandardScaler
# StandardScaler객체 생성
scaler = StandardScaler()
# StandardScaler 로 데이터 셋 변환. fit() 과 transform() 호출.
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)
#transform( )시 scale 변환된 데이터 셋이 numpy ndarry로 반환되어 이를 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())
[output]
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
모든 칼럼 값의 평균이 0에 아주 가까운 값으로, 그리고 분산은 1에 아주 가까운 값으로 변환됐음을 알수 있습니다.
MinMaxScaler
MinMaxScaler 는 데이터값을 0과 1 사이의 범위 값으로 변환합니다(음수 값이 있으면 -1 에서 1 값으로 변환합니다). 데이터의 분포가 가우시안 분포가 아닐 경우에 Min, Max Scale을 적용해 볼 수 있습니다. MinMaxScaler가 어떻게 동작하는지 확인해 보겠습니다.
from sklearn.preprocessing import MinMaxScaler
# MinMaxScaler객체 생성
scaler = MinMaxScaler()
# MinMaxScaler 로 데이터 셋 변환. fit() 과 transform() 호출.
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)
# transform()시 scale 변환된 데이터 셋이 numpy ndarry로 반환되어 이를 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())
[output]
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 사이의 값으로 변환되는 스케일링이 적용됐음을 알 수 있습니다.
학습 데이터와 테스트 데이터의 스케일링 변환 시 유의점
StandardScaler 나 MinMaxScaler 와 같은 Scaler 객체를 이용해 데이터의 스케일링 변환 시 일반적으로 fit() 은 데이터 변환을 위한 기준 정보 설정을 적용하며 transform() 은 설정된 정보를 이용해 데이터를 변환하며, fit_transform() 은 fit() 과 transform() 을 한 번에 적용하는 기능을 수행합니다.
그런데 학습 데이터 세트와 테스트 데이터 세트에 이 fit() 과 transform() 을 적용할 때 주의가 필요한데, 학습 데이터로 fit() 이 적용된 스케일링 기준 정보를 그대로 테스트 데이터에 적용해야 하며, 그렇지 않고 테스트 데이터로 다시 새로운 스케일링 기준 정보를 만들게 되면 학습 데이터와 테스트 데이터의 스케일링 기준 정보가 서로 달라지기 때문에 올바른 예측 결과를 도출하지 못할 수 있습니다.
다음 코드를 통해서 테스트 데이터에 fit() 을 적용할 때 어떠한 문제가 발생하는지 알아보겠습니다.
from sklearn.preprocessing import MinMaxScaler
import numpy as np
# 학습 데이터는 0 부터 10까지, 테스트 데이터는 0 부터 5까지 값을 가지는 데이터 세트로 생성
# Scaler클래스의 fit(), transform()은 2차원 이상 데이터만 가능하므로 reshape(-1, 1)로 차원 변경
train_array = np.arange(0, 11).reshape(-1, 1)
test_array = np.arange(0, 6).reshape(-1, 1)
학습 데이터인 train_array 부터 MinMaxScaler 를 이용해 변환하겠습니다.
# MinMaxScaler 객체에 별도의 feature_range 파라미터 값을 지정하지 않으면 0~1 값으로 변환
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))
[output]
원본 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. ]
학습 데이터는 0부터 10까지 값을 가지는데, 이 데이터에 MinMaxScaler 객체의 fit() 을 적용하면 최솟값 0, 최댓값 10이 설정되며 1/10 Scale이 적용되며, transform() 을 호출하면 1/10 scale로 학습 데이터를 변환하게 되며 원본 데이터 1은 0.1로 2는 0.2, 그리고 5는 0.5, 10은 1로 변환됩니다.
이번에는 테스트 데이터 세트를 변환하는데, fit() 을 호출해 스케일링 기준 정보를 다시 적용한 뒤 transform() 을 수행한 결과를 확인해 보겠습니다.
# MinMaxScaler에 test_array를 fit()하게 되면 원본 데이터의 최솟값이 0, 최댓값이 5로 설정됨
scaler.fit(test_array)
# 1/5 scale로 test_array 데이터 변환함. 원본 5->1로 변환.
test_scaled = scaler.transform(test_array)
# test_array의 scale 변환 출력.
print('원본 test_array 데이터:', np.round(test_array.reshape(-1), 2))
print('Scale된 test_array 데이터:', np.round(test_scaled.reshape(-1), 2))
[output]
원본 test_array 데이터: [0 1 2 3 4 5]
Scale된 test_array 데이터: [0. 0.2 0.4 0.6 0.8 1. ]
출력 결과를 확인하면 학습 데이터와 테스트 데이터의 스케일링이 맞지 않음을 알 수 있습니다. 테스트 데이터의 경우는 최솟값 0, 최댓값 5이므로 1/5로 스케일링됩니다. 따라서 원본값 1은 0.2로, 원본값 5는 1로 변환이 됩니다. 앞서 학습 데이터는 스케일링 변환으로 원본값 2가 0.2로 변환됐고, 원본값 10이 1로 변환됐습니다. 이렇게 되면 학습 데이터와 테스트 데이터의 서로 다른 원본값이 동일한 값으로 변환되는 결과를 초래 합니다.
머신러닝 모델은 학습 데이터를 기반으로 학습되기 때문에 반드시 테스트 데이터는 학습 데이터의 스케일링 기준에 따라 변환돼야 합니다. 따라서 테스트 데이터에 다시 fit() 을 적용해서는 안 되며 학습 데이터로 이미 fit() 이 적용된 Scaler 객체를 이용해 transform() 으로 변환해야 합니다.
다음 코드는 테스트 데이터에 fit() 을 호출하지 않고 학습 데이터로 fit() 을 수행한 MinMaxScaler 객체의 transform() 을 이용해 데이터를 변환합니다. 출력 결과를 확인해 보면 학습 데이터, 테스트 데이터 모두 동일하게 변환됐음을 확인할 수 있습니다.
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('\n원본 test_array 데이터:', np.round(test_array.reshape(-1), 2))
print('Scale된 test_array 데이터:', np.round(test_scaled.reshape(-1), 2))
[output]
원본 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]
fit_transform() 을 적용할 때도 마찬가지입니다. fit_transform() 은 fit() 과 transform() 을 순차적으로 수행하는 메서드로, 학습 데이터에서는 상관없지만 테스트 데이터에서는 절대 사용해서는 안 됩니다. 학습과 테스트 데이터 세트로 분리하기 전에 먼저 전체 데이터 세트에 스케일링을 적용한 뒤 학습과 테스트 데이터 세트로 분리하는 것이 더 바람직 합니다.
학습 데이터와 테스트 데이터의 fit(), transform(), fit_transform() 을 이용해 스케일링 변환 시 유의할 점을 요약하면 다음과 같습니다.
- 가능하다면 전체 데이터의 스케일링 변환을 적용한 뒤 학습과 테스트 데이터로 분리
- 1이 여의치 않다면 테스트 데이터 변환 시에는 fit() 이나 fit_transform() 을 적용하지 않고 학습 데이터로 이미 fit() 된 Scaler 객체를 이용해 transform() 으로 변환
이 유의 사항은 차원 축소 변환이나 텍스트의 피처 벡터화 변환 작업 시에도 동일하게 적용됩니다.
데이터 전처리(Data Preprocessing) 는 ML 알고리즘만큼 중요 합니다. ML 알고리즘은 데이터에 기반하고 있기 때문에 어떤 데이터를 입력으로 가지느냐에 따라 결과도 크게 달라질 수 있습니다(GarbageIn, Garbage Out).
사이킷런의 ML 알고리즘을 적용하기 전에 데이터에 대해 미리 처리해야 할 기본사항이 있습니다.
- 결손값, 즉 NaN, Null 값은 허용되지 않습니다.
- 그러므로 Null 값은 고정된 다른 값으로 변환해야 합니다.
- 사이킷런의 머신러닝 알고리즘은 문자열 값을 입력값으로 허용하지 않습니다.
- 그래므로 모든 문자열 값은 인코딩돼서 숫자 형으로 변환해야 합니다.
데이터 인코딩
머신러닝을 위한 대표적인 인코딩 방식은 레이블 인코딩(Label encoding) 과 원 핫 인코딩(One Hotencoding) 이 있습니다.
레이블 인코딩(Label encoding)
레이블 인코딩 은 카테고리 피처를 코드형 숫자 값으로 변환하는 것 입니다.
사이킷런의 레이블 인코딩은 LabelEncoder 클래스로 구현합니다. LabelEncoder 를 객체로 생성한 후 fit() 과 transform() 을 호출해 레이블 인코딩을 수행합니다.
from sklearn.preprocessing import LabelEncoder
items=['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서']
# LabelEncoder를 객체로 생성한 후 , fit() 과 transform() 으로 label 인코딩 수행.
encoder = LabelEncoder()
encoder.fit(items)
labels = encoder.transform(items)
print('인코딩 변환값:', labels)
[output]
인코딩 변환값: [0 1 4 5 3 3 2 2]
TV는 0, 냉장고는 1, 전자레인지는 4, 컴퓨터는 5, 선풍기는 3, 믹서는 2로 변환됐습니다. 위 예제는 데이터가 작아서 문자열 값이 어떤 숫자 값으로 인코딩됐는지 LabelEncoder 객체의 classes_ 속성값으로 확인할 수 있습니다.
print('인코딩 클래스:', encoder.classes_)
[output]
인코딩 클래스: ['TV' '냉장고' '믹서' '선풍기' '전자레인지' '컴퓨터']
classes_ 속성은 0번부터 순서대로 변환된 인코딩 값에 대한 원본값을 가지고 있습니다. 따라서 TV가 0, 냉장고 1, 믹서 2, 선풍기 3, 전자레인지 4, 컴퓨터가 5로 인코딩됐음을 알 수 있습니다. inverse_transform() 을 통해 인코딩된 값을 다시 디코딩할 수 있습니다.
print('디코딩 원본 값:', encoder.inverse_transform([4, 5, 2, 0, 1, 1, 3, 3]))
[output]
디코딩 원본 값: ['전자레인지' '컴퓨터' '믹서' 'TV' '냉장고' '냉장고' '선풍기' '선풍기']
상품 데이터가 상품 분류, 가격 두 개의 속성으로 돼 있을 때 상품 분류를 레이블 인코딩하면 다음과 같이 변환될 수 있습니다.
레이블 인코딩은 간단하게 문자열 값을 숫자형 카테고리 값으로 변환합니다. 하지만 레이블 인코딩이 일괄적인 숫자 값으로 변환이 되면서 몇몇 ML 알고리즘에는 이를 적용할 경우 예측 성능이 떨어지는 경우가 발생할 수 있습니다. 이는 숫자 값의 경우 크고 작음에 대한 특성이 작용하기 때문 입니다.
즉, 냉장고가 1, 믹서가 2로 변환되면, 1보다 2가 더 큰 값이므로 특정 ML 알고리즘에서 가중치가 더 부여되거나 더 중요하게 인식할 가능성이 발생합니다. 하지만 냉장고와 믹서의 숫자 변환 값은 단순 코드이지 숫자 값에 따른 순서나 중요도로 인식돼서는 안 됩니다. 이러한 특성 때문에 레이블 인코딩은 선형 회귀와 같은 ML 알고리즘에는 적용하지 않아야 합니다. 트리 계열의 ML 알고리즘은 이러한 특성을 반영하지 않으므로 레이블 인코딩도 별문제가 없습니다.
원-핫 인코딩(One-Hot Encoding)
원–핫 인코딩(One-Hot Encoding) 은 레이블 인코딩의 문제점을 해결하기 위한 인코딩 방식 으로, 피처 값의 유형에 따라 새로운 피처를 추가해 고유 값에 해당하는 칼럼에만 1을 표시하고 나머지 칼럼에는 0을 표시하는 방식 입니다.
즉, 행 형태로 돼 있는 피처의 고유 값을 열 형태로 차원을 변환한 뒤, 고유 값에 해당하는 칼럼에만 1을 표시하고 나머지 칼럼에는 0을 표시 하며, 이러한 특성으로 원-핫(여러 개의 속성 중 단 한 개의 속성만 1로표시) 인코딩으로 명명하게 됐습니다.
원-핫 인코딩은 사이킷런에서 OneHotEncoder 클래스 로 변환이 가능합니다. 약간 주의할 점으로, 입력값으로 2차원 데이터가 필요하다는 것 과, OneHotEncoder 를 이용해 변환한 값이 희소 행렬(Sparse Matrix) 형태이므로 이를 다시 toarray() 메서드를 이용해 밀집행렬(Dense Matrix)로 변환해야 한다는 것 입니다.
OneHotEncoder 를 이용해 앞의 데이터를 원-핫인코딩으로 변환해 보겠습니다.
from sklearn.preprocessing import OneHotEncoder
import numpy as np
items=['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서']
# 2차원 ndarray로 변환합니다.
items = np.array(items).reshape(-1, 1)
# 원-핫 인코딩을 적용합니다.
oh_encoder = OneHotEncoder()
oh_encoder.fit(items)
oh_labels = oh_encoder.transform(items)
# OneHotEncoder로 변환한 결과는 희소행렬이므로 toarray()를 이용해 밀집 행렬로 변환.
print('인코딩 클래스:', oh_encoder.categories_)
print('OneHotEncoder로 변환한 값: 희소 행렬(Sparse Matrix)')
print(oh_labels)
print('원-핫 인코딩 데이터 : 밀집행렬(Dense Matrix) 변환')
print(oh_labels.toarray())
print('원-핫 인코딩 데이터 차원')
print(oh_labels.shape)
[output]
인코딩 클래스: [array(['TV', '냉장고', '믹서', '선풍기', '전자레인지', '컴퓨터'], dtype='<U5')]
OneHotEncoder로 변환한 값: 희소 행렬(Sparse Matrix)
(0, 0) 1.0
(1, 1) 1.0
(2, 4) 1.0
(3, 5) 1.0
(4, 3) 1.0
(5, 3) 1.0
(6, 2) 1.0
(7, 2) 1.0
원-핫 인코딩 데이터 : 밀집행렬(Dense Matrix) 변환
[[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)
위 예제 코드의 변환 절차는 다음 그림과 같이 정리할 수 있습니다.
판다스 에는 문자열 카테고리 값을 숫자 형으로 변환할 필요 없이 바로 변환할 수 있는, 원-핫 인코딩을 더 쉽게 지원하는 API가 있습니다. get_dummies() 를 이용하면 됩니다.
import pandas as pd
df = pd.DataFrame({'item': ['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서'] })
pd.get_dummies(df)
get_dummies() 를 이용하면 숫자형 값으로 변환 없이도 바로 변환이 가능함을 알 수 있습니다.
피처 스케일링과 정규화
서로 다른 변수의 값 범위를 일정한 수준으로 맞추는 작업 을 피처 스케일링(feature scaling) 이라고 합니다. 대표적인 방법으로 표준화(Standardization) 와 정규화(Normalization) 가 있습니다.
표준화 는 데이터의 피처 각각이 평균이 0이고 분산이 1인 가우시안 정규 분포를 가진 값으로 변환하는것을 의미 합니다. 표준화를 통해 변환될 피처 $x$ 의 새로운 $i$ 번째 데이터를 ${x_i}_new$ 라고 한다면 이 값은 원래 값에서 피처 $x$ 의 평균을 뺀 값을 피처 $x$ 의 표준편차로 나눈 값으로 계산할 수 있습니다.
\[{x_i}\_new=\frac{x_i-mean(x)}{stdev(x)}\]정규화 는 서로 다른 피처의 크기를 통일하기 위해 크기를 변환해주는 개념 입니다. 즉, 개별 데이터의 크기를 모두 똑같은 단위로 변경하는 것 입니다.
새로운 데이터 ${x_i}_new$ 는 원래 값에서 피처 $x$ 의 최솟값을 뺀 값을 피처 $x$ 의 최댓값과 최솟값의 차이로 나눈 값으로 변환할 수 있습니다.
\[{x_i}\_new=\frac{x_i-min(x)}{max(x)-min(x)}\]사이킷런의 Normalizer 모듈은 선형대수에서의 정규화 개념이 적용됐으며, 개별 벡터의 크기를 맞추기 위해 변환하는 것을 의미합니다. 즉, 개별 벡터를 모든 피처 벡터의 크기로 나눠 줍니다. 세 개의 피처 $x,\ y,\ z$ 가 있다고 하면 새로운 데이터 ${x_i}_new$ 는 원래 값에서 세 개의 피처의 $i$ 번째 피처 값에 해당하는 크기를 합한 값으로 나눠줍니다.
\[{x_i}\_new=\frac{x_i}{\sqrt{x_i^2+y_i^2+z_i^2}}\]StandardScaler
StandardScaler 는 앞에서 설명한 표준화를 쉽게 지원하기 위한 클래스 입니다. 즉, 개별 피처를 평균이 0이고, 분산이 1인 값으로 변환 해줍니다. 이렇게 가우시안 정규 분포를 가질 수 있도록 데이터를 변환하는 것은 몇몇 알고리즘(데이터가 가우시안 분포를 가지고 있다고 가정하고 구현)에서 매우 중요합니다.
StandardScaler가 어떻게 데이터 값을 변환하는지 데이터 세트로 확인해 보겠습니다.
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())
[output]
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
이제 StandardScaler 를 이용해 각 피처를 한 번에 표준화해 변환하겠습니다. StandardScaler 객체를 생성한 후에 fit() 과 transform() 메서드에 변환 대상 피처 데이터 세트를 입력하고 호출하면 간단하게 변환됩니다.
from sklearn.preprocessing import StandardScaler
# StandardScaler객체 생성
scaler = StandardScaler()
# StandardScaler 로 데이터 셋 변환. fit() 과 transform() 호출.
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)
#transform( )시 scale 변환된 데이터 셋이 numpy ndarry로 반환되어 이를 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())
[output]
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
모든 칼럼 값의 평균이 0에 아주 가까운 값으로, 그리고 분산은 1에 아주 가까운 값으로 변환됐음을 알수 있습니다.
MinMaxScaler
MinMaxScaler 는 데이터값을 0과 1 사이의 범위 값으로 변환합니다(음수 값이 있으면 -1 에서 1 값으로 변환합니다). 데이터의 분포가 가우시안 분포가 아닐 경우에 Min, Max Scale을 적용해 볼 수 있습니다. MinMaxScaler가 어떻게 동작하는지 확인해 보겠습니다.
from sklearn.preprocessing import MinMaxScaler
# MinMaxScaler객체 생성
scaler = MinMaxScaler()
# MinMaxScaler 로 데이터 셋 변환. fit() 과 transform() 호출.
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)
# transform()시 scale 변환된 데이터 셋이 numpy ndarry로 반환되어 이를 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())
[output]
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 사이의 값으로 변환되는 스케일링이 적용됐음을 알 수 있습니다.
학습 데이터와 테스트 데이터의 스케일링 변환 시 유의점
StandardScaler 나 MinMaxScaler 와 같은 Scaler 객체를 이용해 데이터의 스케일링 변환 시 일반적으로 fit() 은 데이터 변환을 위한 기준 정보 설정을 적용하며 transform() 은 설정된 정보를 이용해 데이터를 변환하며, fit_transform() 은 fit() 과 transform() 을 한 번에 적용하는 기능을 수행합니다.
그런데 학습 데이터 세트와 테스트 데이터 세트에 이 fit() 과 transform() 을 적용할 때 주의가 필요한데, 학습 데이터로 fit() 이 적용된 스케일링 기준 정보를 그대로 테스트 데이터에 적용해야 하며, 그렇지 않고 테스트 데이터로 다시 새로운 스케일링 기준 정보를 만들게 되면 학습 데이터와 테스트 데이터의 스케일링 기준 정보가 서로 달라지기 때문에 올바른 예측 결과를 도출하지 못할 수 있습니다.
다음 코드를 통해서 테스트 데이터에 fit() 을 적용할 때 어떠한 문제가 발생하는지 알아보겠습니다.
from sklearn.preprocessing import MinMaxScaler
import numpy as np
# 학습 데이터는 0 부터 10까지, 테스트 데이터는 0 부터 5까지 값을 가지는 데이터 세트로 생성
# Scaler클래스의 fit(), transform()은 2차원 이상 데이터만 가능하므로 reshape(-1, 1)로 차원 변경
train_array = np.arange(0, 11).reshape(-1, 1)
test_array = np.arange(0, 6).reshape(-1, 1)
학습 데이터인 train_array 부터 MinMaxScaler 를 이용해 변환하겠습니다.
# MinMaxScaler 객체에 별도의 feature_range 파라미터 값을 지정하지 않으면 0~1 값으로 변환
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))
[output]
원본 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. ]
학습 데이터는 0부터 10까지 값을 가지는데, 이 데이터에 MinMaxScaler 객체의 fit() 을 적용하면 최솟값 0, 최댓값 10이 설정되며 1/10 Scale이 적용되며, transform() 을 호출하면 1/10 scale로 학습 데이터를 변환하게 되며 원본 데이터 1은 0.1로 2는 0.2, 그리고 5는 0.5, 10은 1로 변환됩니다.
이번에는 테스트 데이터 세트를 변환하는데, fit() 을 호출해 스케일링 기준 정보를 다시 적용한 뒤 transform() 을 수행한 결과를 확인해 보겠습니다.
# MinMaxScaler에 test_array를 fit()하게 되면 원본 데이터의 최솟값이 0, 최댓값이 5로 설정됨
scaler.fit(test_array)
# 1/5 scale로 test_array 데이터 변환함. 원본 5->1로 변환.
test_scaled = scaler.transform(test_array)
# test_array의 scale 변환 출력.
print('원본 test_array 데이터:', np.round(test_array.reshape(-1), 2))
print('Scale된 test_array 데이터:', np.round(test_scaled.reshape(-1), 2))
[output]
원본 test_array 데이터: [0 1 2 3 4 5]
Scale된 test_array 데이터: [0. 0.2 0.4 0.6 0.8 1. ]
출력 결과를 확인하면 학습 데이터와 테스트 데이터의 스케일링이 맞지 않음을 알 수 있습니다. 테스트 데이터의 경우는 최솟값 0, 최댓값 5이므로 1/5로 스케일링됩니다. 따라서 원본값 1은 0.2로, 원본값 5는 1로 변환이 됩니다. 앞서 학습 데이터는 스케일링 변환으로 원본값 2가 0.2로 변환됐고, 원본값 10이 1로 변환됐습니다. 이렇게 되면 학습 데이터와 테스트 데이터의 서로 다른 원본값이 동일한 값으로 변환되는 결과를 초래 합니다.
머신러닝 모델은 학습 데이터를 기반으로 학습되기 때문에 반드시 테스트 데이터는 학습 데이터의 스케일링 기준에 따라 변환돼야 합니다. 따라서 테스트 데이터에 다시 fit() 을 적용해서는 안 되며 학습 데이터로 이미 fit() 이 적용된 Scaler 객체를 이용해 transform() 으로 변환해야 합니다.
다음 코드는 테스트 데이터에 fit() 을 호출하지 않고 학습 데이터로 fit() 을 수행한 MinMaxScaler 객체의 transform() 을 이용해 데이터를 변환합니다. 출력 결과를 확인해 보면 학습 데이터, 테스트 데이터 모두 동일하게 변환됐음을 확인할 수 있습니다.
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('\n원본 test_array 데이터:', np.round(test_array.reshape(-1), 2))
print('Scale된 test_array 데이터:', np.round(test_scaled.reshape(-1), 2))
[output]
원본 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]
fit_transform() 을 적용할 때도 마찬가지입니다. fit_transform() 은 fit() 과 transform() 을 순차적으로 수행하는 메서드로, 학습 데이터에서는 상관없지만 테스트 데이터에서는 절대 사용해서는 안 됩니다. 학습과 테스트 데이터 세트로 분리하기 전에 먼저 전체 데이터 세트에 스케일링을 적용한 뒤 학습과 테스트 데이터 세트로 분리하는 것이 더 바람직 합니다.
학습 데이터와 테스트 데이터의 fit(), transform(), fit_transform() 을 이용해 스케일링 변환 시 유의할 점을 요약하면 다음과 같습니다.
- 가능하다면 전체 데이터의 스케일링 변환을 적용한 뒤 학습과 테스트 데이터로 분리
- 1이 여의치 않다면 테스트 데이터 변환 시에는 fit() 이나 fit_transform() 을 적용하지 않고 학습 데이터로 이미 fit() 된 Scaler 객체를 이용해 transform() 으로 변환
이 유의 사항은 차원 축소 변환이나 텍스트의 피처 벡터화 변환 작업 시에도 동일하게 적용됩니다.