{학습 목적}
05. 파트에서는 데이터 전처리에 대해 학습한다. 이 파트를 학습하는 이유는 ML 알고리즘 만큼 중요한 것이 데이터 전처리이기 때문이다. ML알고리즘은 어떤 데이터를 입력으로 가지느냐에 따라 결과도 크게달라질 수 있기 때문에 알고리즘을 적용하기 전에 데이터를 처리하는 것은 중요하다.
<05. 데이터 전처리>
사이킷런의 ML 알고리즘을 적용하기 전에 데이터에 대해 미리 처리해야 할 기본 사항이 있다.
1. 결손값, 즉 Null, NaN 값은 허용되지 않는다.
따라서 이러한 결손값은 고정된 다른 값으로 변환해야한다.
피처 값중 Null값이 얼마 되지 않는다면 피처의 평균값 등으로 간단히 대체될 수 있다.
하지만 Null 값이 대부분이라면 오히려 해당 피처는 드롭하는 것이 더 좋다.
가장 힘든 부분이 Null값이 일정 수준 이상 되는 경우이다. (일정 수준에 대한 기준은 없다.)
하지만 해당 피처가 중요도가 높은 피처이고 Null을 단순히 피처의 평균값으로 대체할 경우 예측 왜곡이 심할 수 있따면 업무로직 등을 상세히 검통해 정밀한 대체 값을 선정해야한다.
2. 사이킷런의 머신러닝 알고리즘은 문자열 값으로 입력값을 허용하지 않는다.
모든 문자열은 인코딩 돼서 숫자형으로 변환해야한다.
문자열 피처는 카테고리형 피처(코드값), 텍스트형 피처를 의미힌다.
텍스트형 피처의 경우 불필요한 피처라고 판단되면 삭제하는 것이 좋다.
이러한 식별자 피처는 단순히 데이터 로우를 식별하는 용도로 사용되고 예측에 중요한 요소가 되지 않아 알고리즘의 예측성능을 떨어트리기 때문이다.
[데이터 인코딩]
머신러닝의 대표적인 인코딩 방식인 레이블 인코딩(Label encoding) 과 원-핫 인코딩(One Hot encoding)에 대해 알아보자
1. 레이블 인코딩
레이블 인코딩은 카테고리 피처를 코드형 숫자 값으로 반환하는 것이다.
(*주의: '01', '02'와 같은 코드값 역시 문자열이므로 1,2와 같은 숫자형으로 변환돼야한다.)
사이킷런의 레이블인코딩은 LabelEncoder 클래스로 구현한다.
이를 객체로 생성한 후 fit(), transform()을 호출해 레이블 인코딩을 수행한다.
(비지도학습에서 사용하는 메서드, fit() - 입력데이터의 형태에 맞춰 데이터를 변환하기 위한 사전 구조 작업, transform() - 실제 작업 수행)
from sklearn.preprocessing import LabelEncoder
items = ['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서']
#LabelEncoder를 객체로 생성한 후, fit(), transform()으로 레이블 인코딩 수행
encoder = LabelEncoder()
encoder.fit(items)
labels = encoder.transform(items)
print('인코딩 변환값:', labels)
[output]
인코딩 변환값: [0 1 4 5 3 3 2 2]
위 예제는 문자열 값이 어떤 숫자로 인코딩 됐는지 직관적으로 알 수 있지만, 많은 경우에는 이를 알지 못한다.
이 경우에는 LabelEncoder 객체의 classes_속성값으로 확인하면 된다.
print('인코딩 클래스;', encoder.classes_)
[output]
인코딩 클래스; ['TV' '냉장고' '믹서' '선풍기' '전자레인지' '컴퓨터']
classes_속성은 0번부터 순서대로 변환된 인코딩 값에 대한 원본값을 가지고 있다.
inverse_transform()을 통해 인코딩된 값을 다시 디코딩 할 수 있다.
print('디코딩 원본값:', encoder.inverse_transform([4,5,2,0,1,1,3,3]))
[output]
디코딩 원본값: ['전자레인지' '컴퓨터' '믹서' 'TV' '냉장고' '냉장고' '선풍기' '선풍기']
레이블 인코딩을 통해 문자열 값이 일괄적인 숫자값으로 변환되면서 몇몇 ML 알고리즘에는 이를 적용할 경우 예측성능이 떨어지는 경우가 발생할 수 있다.
이는 숫자 값의 경우 크고 작음에 대한 특성이 작용하기 때문이다.
이러한 특성 때문에 레이블 인코딩은 선형회귀와 같은 ML 알고리즘에는 적용되면 안된다.
트리계열의 ML의 경우 이러한 특성을 반영하지 않으므로 레이블 인코딩도 별 문제가 되지 않는다.
2, 원-핫 인코딩(One-Hot Encoding)
원-핫 인코딩은 레이블 인코딩의 이러한 문제점을 해결하기 위한 인코딩 방식이다.
원-핫 인코딩은 피처 값의 유형에 따라 새로운 피처를 추가해 고유 값에 해당하는 칼럼에만 1을 표시하고 나머지 칼럼에는 0을 표시하는 방법이다.
즉, 행 형태로 되어있는 피처의 고유 값을 열 형태로 변환한 뒤 고유 값에 해당하는 칼럼에만 1을 표시하고 나머지는 0을 표시하는 것이다.
앞의 레이블 예제를 참조하면 TV가 0, 냉장고 1, 믹서 2, 선풍기 3, 전자레인지 4, 컴퓨터가 5로 인코딩 돼있었다. 0부터 5까지 6개의 상품 분류 피처를 6개의 상품 분류 고유 값 피처로 변환한다.
즉, TV를 위한 상품분류_TV, 냉장고를 위한 상품 분류_냉장고 등등 6개의 피처로 변환하는 것이다.
그리고 해당 레코드의 상품 분류가 TV인 경우에는 상품 분류_TV 피처에만 1을 입력하고, 나머지 핓처는 모두 0이다.
즉, 해당 고유 값에 매칭되는 피처만 1이 되고 나머지 피처는 0을 입력하며,
이러한 특성으로 원-핫(여러 개의 속성 중 단 한 개의 속성만 1로 표시)인코딩으로 명명하게 되었다.
원-핫 인코딩은 사이킷런에서 OneHotEncoder 클래스로 변환이 가능하다.
단, LabelEncoding과 다른점 은 입력값으로 2차원 데이터가 필요하다는 것과 OneHotEncoder를 이용해 변환한 값이 희소 행렬 형태이므로 이를 다시 toarray() 메서드를 이용해 밀집 행렬로 변환해야 한다는 것이다.
from sklearn.preprocessing import OneHotEncoder
import numpy as np
items = ['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서']
#2차원 ndarray로 변환
items = np.array(items).reshape(-1,1) #reshape의 로우인자에 -1을 입력하면 2차원의 고정된 1개의 칼럼에 맞는 로우를 자동으로 새롭게 생성
#원-핫 인코딩을 적용
oh_encoder = OneHotEncoder()
oh_encoder.fit(items)
oh_labels = oh_encoder.transform(items)
#OneHotEncoder로 변환한 결과는 희소행렬이므로 toarray()를 이용해 밀집행렬로 변환
print('원-핫 인코딩 데이터')
print(oh_labels.toarray())
print('원-핫 인코딩 데이터 차원')
print(oh_labels.shape)
[output]
원-핫 인코딩 데이터
[[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)
8개의 레코드와 1개의 칼럼을 가진 원본 데이터가 8개의 레코드와 6개의 칼럼을 가진 데이터로 변환되었다.
TV가 0, 냉장고 1, 믹서 2, 선풍기 3, 전자레인지 4, 컴퓨터가 5로 인코딩됐으므로 첫 번째 칼럼이 TV, 두 번째 칼럼이 냉장고, 세 번째 칼럼이 믹서, ..등을 나타낸다.
따라서 원본 데이터의 첫 번째 레코드가 TV이므로 변환된 데이터의 첫 번째 레코드의 첫 번째 칼럼이 1이고 나머지는 모두 0이다.
판다스에는 원-핫 인코딩을 더 쉽게 지원하는 API인 get_dummies()가 있다.
사이킷런의 OneHotEncoder와 다르게 문자열 카테고리 값을 숫자 형으로 변환할 필요 없이 바로 변환할 수 있다.
import pandas as pd
df = pd.DataFrame({'item':['TV', '냉장고', '전자레인지', '컴퓨터', '선풍기', '선풍기', '믹서', '믹서']
})
pd.get_dummies(df)
[피처 스케일링과 정규화]
피처 스케일링이란 서로 다른 변수의 값 범위를 일정한 수준으로 맞추는 작업이다.
대표적인 방법으로는 표준화(Standardization)와 정규화(Normalization)가 있다.
표준화는 데이터의 피처 각각의 평균이 0이고 분산이 1인 가우시안 정규분포를 가진 값으로 변환하는 것을 의미한다.
표준화를 통해 변환될 피처 x의 새로운 i번째 데이터를 xi_new라고 하자
정규화는 서로 다른 피처의 크기를 통일하기 위해 크기를 변환해주는 개념이다.
즉, 개별 데이터의 크기를 모두 똑같은 단위로 변경하는 것이다.
그런데 사이킷런의 전처리에서 제공하는 Normalizer 모듈과 일반적인 정규화는 약간의 차이가 있다.
사이킷런의 Normalizer 모듈은 선형대수에서의 정규화 개념이 적용됐으며, 개별 벡터의 크기를 맞추기 위해 변환하는 것을 말한다.
즉, 개별 벡터를 모든 피처 벡터의 크기로 나눠주는 것이다.
혼선을 방지하기 위해 일반적인 의미의 표준화와 정규화는 피처 스케일링으로 통칭하고 선형대수의 정규화를 벡터 정규화로 지칭한다.
먼저 사이킷런에서 제공하는 대표적인 피처 스케일링 클래스인 StandardScaler와 MinMaxScaler를 알아보자
[StandardScaler]
StandardScarler는 표준화를 쉽게 지원하기 위한 클래스이다. 즉, 개별 피처를 평균 0, 분산 1인 값으로 변환해준다.
사이킷런에서 구현한 RBF 커널을 이용하는 서포트 벡터 머신이나 선형 회귀, 로지스틱 회귀는 데이터가 가우시안 분포를 가지고 있다고 가정하고 구현됐기 때문에 사전에 표준화를 적용하는 것은 예측 성능 향상에 중요한 요소가 될 수 있다.
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() 메서드에 변환 대상 피처 데이터 세트를 입력하고 호출하면 간단하게 변환된다.
transform()을 호출할 때 스케일 변환된 데이터 세트가 넘파이의 ndarray이므로 이를 DataFrame으로 변환해 평균값과 분산값을 알아보자
from sklearn.preprocessing import StandardScaler
#StandardScaler 객체 생성
scaler = StandardScaler()
#StandardScaler로 데이터 세트 변환. fit()과 transform() 호출.
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)
#transform()시 스케일 변환된 데이터 세트가 넘파이 ndarray로 반환돼 이를 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을 적용해 볼 수 있다.
from sklearn.preprocessing import MinMaxScaler
#MinMaxScaler 객체 생성
scaler=MinMaxScaler()
#MinMaxScaler로 데이터 세트 변환. fit()과 transform() 호출.
scaler.fit(iris_df)
iris_scaled = scaler.transform(iris_df)
#transform()시 스케일 변환된 데이터 세트가 Numpy ndarray로 반환돼 이를 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
[학습 데이터와 테스트 데이터의 스케일링 변환 시 유의점]
StandardScaler나 MinMaxScaler와 같은 Scaler 객체를 이용해 데이터의 스케일링 변환 시 fit(), transform(), fit_transform()메서드를 이용한다.
fit()은 데이터 변환을 위한 기준 정보 설정을 적용하며 transform()은 이렇게 설정된 정보를 이용해 데이터를 변환한다.
그런데 학습, 테스트 데이터 세트에 이 fit(), transform()을 적용할 때 주의해야할 점이 있다.
Scaler 객체를 이용해 학습 데이터 세트로 fit, transform()을 적용하면 테스트 데이터 세트로는 다시 fit()을 수행하지 않고 학습 데이터 세트로 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 객체의 fit()을 적용하면 최솟값 0, 최댓값 10이 설정되며 1/10 Scale이 적용된다.
이제 transform()을 호출하면 1/10 scale로 학습데이터를 변환하게 되며 원본데이터 1은 0.1, 2는 0.2 ,.. ,10은 1로 변환된다.
#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. ]
이번에는 테스트 데이터 세트를 변환하는데 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('Scaled된 test_array 데이터:', np.round(test_scaled.reshape(-1),2))
[output]
원본 test_array 데이터: [0 1 2 3 4 5]
Scaled된 test_array 데이터: [0. 0.2 0.4 0.6 0.8 1. ]
출력 결과를 확인하면 학습 데이터와 테스트 데이터의 스케일링이 맞지 않음을 알 수 있다.
이렇게 되면 학습 데이터와 테스트 데이터의 서로 다른 원본값이 동일한 값으로 변환되는 결과를 초래한다.
머신러닝 모델은 학습 데이터를 기반으로 학습되기 때문에 반드시 테스트 데이터는 학습 데이터의 스케일링 기준에 따라야한다. 즉 테스트 데이터의 1 값은 학습데이터와 동일하게 0.1값으로 변환되어야한다.
따라서 테스트 데이터에 다시 fit()을 적용하면 안 되며 학습 데이터로 이미 fit()이 적용된 scaler객체를 이용해 transform()으로 변환해야한다.
scaler = MinMaxScaler()
scaler.fit(train_array)
train_scaled = scaler.transform(train_array)
print('원본 train_array 데이터:', np.round(train_array.reshape(-1),2))
print('Scaled된 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('Scaled된 test_array 데이터:', np.round(test_scaled.reshape(-1),2))
[output]
원본 train_array 데이터: [ 0 1 2 3 4 5 6 7 8 9 10]
Scaled된 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]
Scaled된 test_array 데이터: [0. 0.1 0.2 0.3 0.4 0.5]
fit_transform()을 적용할 때도 마찬가지이다.
fit_transform()은 fit, transform을 순차적으로 적용하는 메서드이믕로 학습 데이터에서는 상관없지만 테스트 데이터에서는 사용해서는 안된다.
이제까지 설명한 내용을 요약하면 다음과 같다.
- 가능하다면 전체 데이터의 스케일링 변환을 적용한 뒤 학습과 테스트 데이터로 분리
- 1이 여의치 않다면 테스트 데이터 변환 시에는 fit()이나 fit_transform()을 적용하지 않고 학습 데이터로 이미 fit()된 Scaler객체를 이용해 transform()으로 변환
Ref) 파이썬 머신러닝 완벽가이드