# 텍스트와 범주형 특성 다루기
앞서 다룬 ocean_proximity 가 텍스트 데이터타입이라 중간값을 계산하지 않고 남겨 두었다.
housing_cat = housing['ocean_proximity']
housing_cat.head(10)
머신러닝 알고리즘이 다루기 쉽게 ocean_proximity의 카테고리를 텍스트에서 숫자로 바꿔줄 필요가 있다. 이를 위해 각 카테고리를 다른 정숫값으로 매핑해주는 pandas의 factorize() 메서드를 사용한다.
housing_cat_encoded, housing_categories = housing_cat.factorize()
housing_cat_encoded[:10]
housing_categories
# array([0, 0, 1, 2, 0, 2, 0, 2, 0, 0], dtype=int64)
# Index(['<1H OCEAN', 'NEAR OCEAN', 'INLAND', 'NEAR BAY', 'ISLAND'], dtype='object')
factorize() 메서드는 각 카테고리가 매핑된 정수값 배열인 housing_cat_encoded, 카테고리 리스트인 housing_categories를 반환해 준다.
* 이 표현방식의 문제점은 머신러닝 알고리즘이 가까이 있는 두 값(예를들어 0과 1) 이 떨어져 있는 두 값(0과 2) 보다 더 비슷하다고 생각한다는 점이다. 실제로는 그렇지 않다(위 예제에서는 0과 1보다 0과 4가 더 비슷하다). 이 문제를 해결하기 위해 일반적으로 카테고리별 이진 특성을 만들어 해결한다. 카테고리마다 어떤 특성이 1, 나머지 특성은 0으로 되게끔 만드는 방식을 원-핫 인코딩(one-hot encoding) 이라 한다.
사이킷런의 OneHotEncoder 를 이용하여 숫자로 된 범주형 값을 원-핫 벡터로 바꿔주는 작업을 한다.
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(categories='auto')
housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1))
housing_cat_1hot
#<16512x5 sparse matrix of type '<class 'numpy.float64'>'
# with 16512 stored elements in Compressed Sparse Row format>
fit_transform() 메서드에는 2차원 배열을 넣어줘야 한다. 그런데 housing_cat_encoded는 1차원 배열이므로 구조를 바꿔줄 필요가 있다.
*numpy의 reshape() 함수에서 -1은 차원을 지정하지 않는 뜻이며, 해당 차원의 크기는 전체 배열에서 다른 차원이 정해지고 남은 크기가 된다
** reshape(-1,1) 메서드로 인해 housing_cat_encoded는 n*1 배열의 형태가 되며, 이때 한개의 특성값이 각 열의 원소가 된다. fit_transform() 메서드를 거쳐서 하나의 특성값은 0과 1의 원-핫 인코딩 벡터로 변환된다.
또한 출력의 결과값을 보면 넘파이 배열이 아닌 SciPy 희소행렬인 것을 볼 수 있다. 사이파이 희소행렬은 수천개의 카테고리가 있는 범주형 특성일 경우 매우 효율적이다. 희소행렬은 0이 아닌 원소의 위치만 저장하므로, 0을 모두 메모리에 저장하는 자원낭비를 피할 수 있다. 희소행렬을 일반적인 2차원 배열처럼 사용할 수 있지만 numpy 배열로 바꾸려면 toarray() 메서드를 호출하면 된다.
텍스트 카테고리를 숫자 카테고리로, 숫자 카테고리를 원-핫 벡터로 바꿔주는 이 두가지 변환을 CategoricalEncoder을 사용하여 한번에 처리할 수 있다. (마찬가지로 판다스 데이터프레임이라면 pd.get_dummies() 로 텍스트 특성을 가진 열을 찾아 자동을 원-핫 인코딩을 적용하고 변환된 데이터 프레임을 반환할 수 있다)
* CategorialEncoder는 사이킷런 0.20이후 버전부터 추가되었다.
from sklearn.preprocessing import CategoricalEncoder # Scikit-Learn 0.20부터 추가
cat_encoder = CategoricalEncoder()
housing_cat_reshaped = housing_cat.values.reshape(-1, 1)
housing_cat_1hot = cat_encoder.fit_transform(housing_cat_reshaped)
housing_cat_1hot
# <16512x5 sparse matrix of type '<class 'numpy.float64'>'
# with 16512 stored elements in Compressed Sparse Row format>
희소 행렬이 아닌 밀집 행렬을 출력하기를 원하는 경우 encoding 매개변수를 onehot-dense로 지정할 수 있다.
CategoricalEncoder() 의 categories_ 인스턴스 변수를 사용해 카테고리 리스트를 얻을 수 있다.
cat_encoder.categories_
#[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
# dtype=object)]
# 나만의 변환기
특별한 정제 작업이나 어떤 특성들을 조합하는 등의 작업을 위해 따로 변환기를 만들어야 할 때가 있다. 이때 내가 만든 변환기를 사이킷런의 기능과 매끄럽게 연동하고 싶을 것이다.
사이킷런은 상속이 아닌 덕 타이핑(duck typing, 상속이나 인터페이스 구현이 아닌 객체의 속성이나 메서드가 객체의 유형을 결정하는 방식)을 지원하기 때문에 fit(), transform(), fit_transform() 메서드를 구현한 파이썬 클래스를 만들면 된다. fit_transform() 메서드는 TransformerMixin을 상속하면 자동으로 생성된다.
* 파이썬에서 이름에 Mixin이 있으면 객체의 기능을 확장하려는 목적으로 만들어진 클래스를 나타낸다. TransformerMixin은 fit_transform() 메서드 하나를 갖고 있으며 이를 상속하는 모든 파이썬 클래스에 이 메서드를 제공한다.
또한 BaseEstimator를 상속하고 생성자에 *args 나 **kargs를 사용하지 않으면 하이퍼 파라미터 튜닝에 필요한 두 메서드 get_params() 와 set_params() 를 추가로 얻게 된다.
* get_params() 와 set_params() 함수는 사이킷런의 파이프라인과 그리드 탐색에 꼭 필요한 메서드이므로 모든 추정기와 변환기는 BaseEstimator를 상속해야 한다. 두 메서드는 생성자에 명시된 매개변수만을 참조하므로 *args나 **kargs를 사용할 수 없다.
앞서 이야기한 조합 특성을 추가하는 간단한 변환기를 만들어보면 다음과 같다.
from sklearn.base import BaseEstimator, TransformerMixin
# 컬럼 인덱스
rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6
class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
def __init__(self, add_bedrooms_per_room = True): # no *args or **kargs
self.add_bedrooms_per_room = add_bedrooms_per_room
def fit(self, X, y=None):
return self # nothing else to do
def transform(self, X, y=None):
rooms_per_household = X[:, rooms_ix] / X[:, household_ix]
population_per_household = X[:, population_ix] / X[:, household_ix]
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
return np.c_[X, rooms_per_household, population_per_household,
bedrooms_per_room]
else:
return np.c_[X, rooms_per_household, population_per_household]
attr_adder = CombinedAttributesAdder(add_bedrooms_per_room=False)
housing_extra_attribs = attr_adder.transform(housing.values)
이 경우 변환기는 add_bedrooms_per_room 하이퍼파라미터 하나를 가지고 있고 기본값을 True로 지정한다(합리적인 기본값). 일반적으로 100% 확신이 없는 모든 데이터 준비 단계에 대해 하이퍼파라미터를 추가할 수 있으며 머신러닝 알고리즘에 도움이 될지 안될지 하이퍼파라미터로 확인해 볼 수 있다. 이런 데이터 준비 단계를 자동화할수록 더 많은 조합을 자동으로 시도해 볼 수 있고 최상의 조합을 찾을 가능성이 높아진다.
# 특성 스케일링
트리 기반 알고리즘들을 제외하고, 머신러닝 알고리즘은 입력 숫자 특성들의 스케일이 많이 다르면 잘 작동하지 않는다. 위의 예제에서도 전체 방 개수의 범위는 6~39,320인 반면 중간소득의 범위는 0~15까지이므로 특성 스케일링이 필요하다. 일반적으로 타깃 값에 대한 스케일링은 불필요하다.
모든 특성의 범위를 같도록 만들어주는 방법으로 min-max 스케일링 과 표준화(standardization)가 널리 사용된다.
min-max 스케일링은 0~1 범위에 들도록 값을 이동하고 스케일을 조정하는 방법이며, 데이터에서 최솟값을 뺀 후 최댓값과 최솟값의 차이로 나누면 된다. 사이킷런의 MinMaxScaler 변환기를 이용하여 이를 처리할 수 있다. 범위를 변경하기 위해 feature_range 매개변수를 이용할 수 있다.
표준화는 먼저 평균을 뺀 후 표준편차로 나누어 결과 분포의 분산이 1이 되도록 한다. 표준화는 범위의 상한과 하한이 없어 어떤 알고리즘에서는 문제가 될 수 있지만(예를 들어 신경망), 이상치에 영향을 덜 받는다는 특징이 있다. 사이킷런의 StandardScaler 변환기를 이용해 이를 처리할 수 있다.
* 모든 변환기에서 스케일링은 전체 데이터가 아닌 훈련 데이터에 대해서만 fit() 메서드를 적용해야 하며, 그 후 훈련 세트와 테스트 세트(+새로운데이터)에 대해 transform() 메서드를 사용한다.
# 변환 파이프라인
사이킷런에는 연속된 변환을 순서대로 처리할 수 있도록 도와주는 Pipeline 클래스가 있다. 다음은 숫자 특성을 처리하는 간단한 파이프라인이다.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([
('imputer', SimpleImputer(strategy="median")),
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
housing_num_tr = num_pipeline.fit_transform(housing_num)
Pipeline 은 연속된 단계를 나타내는 이름, 추정기 쌍의 목록(리스트)를 입력으로 받는다. 마지막 단계에는 변환기와 추정기 모두 사용할 수 있으며 그 외에는 변환기만 사용할 수 있다. 이름은 이중 밑줄 문자(__)를 포함하지 않도록 한다.
파이프라인의 fit() 메서드를 호출하면 모든 변환기의 fit_transform() 메서드를 순서대로 호출하면서( 없다면 fit()과 transform()을 차례로) 한 단계의 출력을 다음 단계의 입력으로 전달한다. 마지막 단계에서는 fit() 메서드만 호출한다.
파이프라인 객체는 마지막 추정기와 동일한 메서드를 제공한다. 위 코드에서 마지막 추정기가 StandardScaler이므로 파이프라인이 데이터에 대해 모든 변환을 순서대로 적용하는 transform() 메서드를 가지고 있다.
수치형 column을 numpy array 로 추출하는 대신 pandas 의 데이터프레임을 파이프라인에 직접 주입할 수 있다면 좋을 것이다. 책 발행 시점에서는 사이킷런에서 판다스 데이터프레임을 다룰 도구가 없기에 이를 처리하는 변환기를 직접 만들어 볼 수 있다.
* 사이킷런 깃허브의 #3886 Pull Request 를 보면 특성별 변환을 수행할 수 있는 ColumnTransformer가 준비되고 있으며 #9012로 이어져 마스터 브랜치에 병합되어 0.20 이후 릴리스부터 포함되어 있다. pip install sklearn-pandas 명령으로 비슷한 목적을 가진 DataFrameMapper을 얻을 수도 있다.
from sklearn.base import BaseEstimator, TransformerMixin
class DataFrameSelector(BaseEstimator, TransformerMixin):
def __init__(self, attribute_names):
self.attribute_names = attribute_names
def fit(self, X, y=None):
return self
def transform(self, X):
return X[self.attribute_names].values
사이킷런 0.20 버전부터 추가된 sklearn.compose.ColumnTransformer 를 이용한 코드는 다음과 같다.
from sklearn.compose import ColumnTransformer
num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]
full_pipeline = ColumnTransformer([
("num", num_pipeline, num_attribs),
("cat", OneHotEncoder(categories='auto'), cat_attribs),
])
housing_prepared = full_pipeline.fit_transform(housing)
housing_prepared
위 코드에서 만든 DataFrameSelector를 이용해 num_pipline, cat_pipline을 각각 만들어 sklearn.pipeline.FeatureUnion을 이용해 두 파이프라인을 합칠 수 있지만, 사이킷런 0.20 버전에 추가된 ColumnTransformer로 만든 full_pipline을 사용해 더 간단하게 파이프라인을 구축한 모습이다.
'Artificial Intelligence > Machine Learning' 카테고리의 다른 글
핸즈온 머신러닝(4) - 분류 1 (0) | 2020.01.21 |
---|---|
핸즈온 머신러닝(3) - 머신러닝 프로젝트 6[마무리] (0) | 2020.01.21 |
핸즈온 머신러닝(3) - 사이킷런 설계철학[참고] (0) | 2020.01.21 |
핸즈온 머신러닝(3) - 머신러닝 프로젝트 4 (0) | 2020.01.21 |
핸즈온 머신러닝(3) - 머신러닝 프로젝트 3 (0) | 2020.01.20 |
댓글