본문 바로가기

데이터와 함께 탱고를/머신러닝

Categorical Value Encoding 과 Mean Encoding

이번 글에서는, 가장 인기있는 Categorical Value Encoding 을 하나씩 정리해보려고 한다.
다음의 내용을 다룬다.

  1. One-hot Encoding
  2. Label Encoding
  3. Mean Encoding

특히 마지막에 3.Mean Encoding 은 최근 Kaggler 들에 의해 많이 쓰이고 있다.
무엇이 Mean Encoding 이고, 어떻게 쓰는지, 장단점은 무엇인지 살펴본다.
한편, Mean Encoding 은 특히 Gradient Boosting Tree 계열에 많이 쓰이고 있다.
앞으로 모델 학습 성능을 말할 때, 별다른 말이 없으면 Tree 기반 모델을 두고 말하는 것이라 보면 되겠다.

글로만 설명하지 않고, 직접 눈으로 Encoding 을 확인하기 위해, Titanic Dataset 을 활용한다.
많은 Categorical value 가 있겠지만, 여기서는 Sex Feature 위주로 Encoding 해보려 한다.

import pandas as pd
import numpy as np
import plotly.plotly as py 
import cufflinks as cf 
cf.go_offline(connected=True)
df = pd.read_csv('data/titanic/train.csv')
df.head()
  PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

1. One-hot Encoding

0과 1의 배열(벡터)을 하나의 라벨(카테고리 값)에 대응하여 Encoding 한다.

df = df.join(pd.get_dummies(df['Sex'], prefix='Sex'))
df[['Sex', 'Sex_female', 'Sex_male']].head()
  Sex Sex_female Sex_male
0 male 0 1
1 female 1 0
2 female 1 0
3 female 1 0
4 male 0 1

male -> [0, 1]
female -> [1, 0]
으로 Encoding 되었다.

장점

  • 피처내 값들이 서로 분리 되어있기 때문에, 우리가 모를 수 있는 어떤 관계나 영향을 주지 않는다.
  • 구현하기가 매우 쉽다.

단점

  • 피처 내 값의 종류가 많을 경우(High Cardinaliry), 매우 많은 Feature 들을 만들어 낸다.
  • 이는, 모델 훈련의 속도를 낮추고 훈련에 더 많은 데이터를 필요로 하게 한다. (차원의 저주 문제)
  • 단순히 0과 1로만 결과를 내어 큰 정보이득 없이 Tree 의 depth 만 깊게 만든다.
    중요한건, Tree Depth 를 증가시키는 것에 비해, 2가지 경우로만 트리를 만들어 나간다는 것이다.
  • Random Forest 와 같이, 일부 Feature 만 Sampling 하여 트리를 만들어나가는 경우,
    One-hot Feature 로 생성된 Feature 의 수가 많기 때문에,
    이 Feature 들이 다른 Feature 들 보다 더 많이 쓰인다.

2. Label Encoding

하나의 라벨을 하나의 정수와 대응시켜 Encoding 한다.

df[['Sex']].head()
  Sex
0 male
1 female
2 female
3 female
4 male
df['Sex_label'] = pd.Categorical(df['Sex']).codes
df[['Sex', 'Sex_label']].head()
  Sex Sex_label
0 male 1
1 female 0
2 female 0
3 female 0
4 male 1

male -> 1
female -> 0
으로 Encoding 되었다.

장점

  • One-hot 와 같이 많은 Feature 를 만들지 않고, 하나의 Labeled Feature 만 만든다.
  • 따라서 One-hot 보다 학습속도가 빠르고, One-hot 의 문제를 어느정도 해결한다.
  • 구현하기가 매우 쉽다.

단점

  • 아무래도 한 Feature 내에 피처 값들을 Numeric 형태로 Encoding 하다보니,
    Categorical 값들은 순서개념이 없음에도, 모델은 순서개념을 전제하여 학습하게 된다.
    (이는 Numeric 형태의 어쩔 수 없는 특성임...)
  • 위와 같은 이유로 Linear Regression 모델에 적합한 방법은 아니다.

3. Mean Encoding (default)

위 두 Encoding 에서 Encoding 된 값 자체는 별로 의미가 없다.
그냥 랜덤하게 0,1 혹은 숫자를 부여하여 그냥 '구분' 하는 것 자체로 만족했기 때문이다.

하지만 Mean Encoding 은 구분을 넘어 좀 더 의미있는 Encoding 을 하자는 시도를 한다.
내가 Encoding 하는 Feature와 예측하랴고하는 Target 간의 어떤 수치적인 관계를 Categorical 에서도 찾자는 것이다.
Categorial 'Label 값 자체'가 '예측 값' 과 연관이 있을리 없다.
다만, Label 값에 따른 예측 값의 평균은 Label 값에 따라 달라질 수 있다.
즉, Label 값을 수치적으로 표현하면서도, 서로 구분할 수 있다는 것이다.
또한 이렇게 Encoding 한 값은 예측값하고도 수치적인 연관이 있다.

# target 설정
target = 'Survived'
# target 에 대한 sex 내 각 변수의 mean 을 구함.
sex_mean = df.groupby('Sex')[target].mean()
sex_mean
Sex
female    0.742038
male      0.188908
Name: Survived, dtype: float64
# 가존 변수에 encoded 된 값을 매핑
df['Sex_mean'] = df['Sex'].map(sex_mean)
df[['Sex', 'Sex_mean']].head()
  Sex Sex_mean
0 male 0.188908
1 female 0.742038
2 female 0.742038
3 female 0.742038
4 male 0.188908

male -> 0.188908
female -> 0.742038
로 Encoding 되었다.

즉, 평균적으로 여자가 남자보다 1값이 많다는 경우인데,
이를 Categorical Feature를 Encoding 하는 과정에 녹여낸 것이다.

아래 그래프를 보자.

df.pivot_table(columns=target, index=df.index, values='Sex_mean')\
    .iplot(kind='histogram', bins=100, xrange=(0,1))

0.188908 은 남자의 encoding 된 값이다.
예측값(target)에서 0이 더 많고 1이 적게 있음을 볼 수 있다.
여자의 경우는 그 반대다.

Label 값과 Target 값 사이의 일종의 Correlation 효과를 볼 수 있는 것이다!

장점

  • 만들어지는 Feature 수가 매우 적어, One-hot의 문제였던 차원의 저주가 없다.
  • 고로 비교적 빠른 학습이 이루어진다.
  • Regression 이든 Classification 이든 이런 Feature 는 예측 값에 좀 더 가깝게 학습되게 한다.
    즉, Less bias 를 가진다.

단점

  • 구현과 검증이 조금 까다롭다.
  • 오버피팅의 문제가 있다.
    • 하나는 Data Leakage 문제인데, 사실 훈련 데이터에는 예측 값에 대한 정보가 전혀 들어가면 안되는게 일반적이다.
      그런데, Mean encoding 과정을 보면, 사실 encoding 된 값에는 예측 값에 대한 정보가 포함되어있다.
      이러한 문제는 모델을 Training set 에만 오버피팅 되도록 만든다.
    • 다른 하나는 하나의 label 값의 대표값을 trainset의 하나의 mean 으로만 사용한다는 점이다.
      만약 testset 에 해당 label 값의 통계적인 분포가 trainset 과 다르다면, 오버피팅이 일어날 수 밖에 없다.
    • 특히, 이런 상황이 발생하는 경우는, Categorical 변수 내 Label의 분포가 매우 극단적인 경우에 발생한다.
      예를 들어, Trainset 에는 남자가 100명, 여자가 5명이고, Testset 에는 50명, 50명이라고 하자.
      우리는 Trainset 으로 Mean encoding 할텐데, 여자 5명의 평균값이 Testset 의 여자 50명을 대표할 수 있을까?
      어려울 수 밖에 없다.

그래서 Mean Encoding 에서는 특히 Data Leakage와 Overfitting 을 최소화하려는 다양한 기법들이 존재한다.
이제 그 대표적인 기법들을 하나씩 살펴보자.

Smoothing

Smoothing은 위에 단점의 마지막 상황 (Trainset 에는 남자가 100명, 여자가 5명이고, Testset 에는 50명, 50명인 경우) 인 경우를 고려한 기법이다.
저 5명의 평균이 여자 전체의 평균을 대표하다가 보기엔 힘드니, 그 평균을 남녀 무관한 전체 평균(global mean) 에 좀 더 가깝게 만드는 것이다.
즉 치우쳐진 평균을 전체 평균에 가깝도록, 기존 값을 스무스하게 만든다.
그래서 이름도 Smoothing 이다.

스무싱 시키는 방법은 다음과 같다.

alpha 는 하이퍼 파라미터로, 유저가 임의로 주는 것이다.
0을 주면, 기존과 달라지는게 없다. (저 식에 0을 넣으면 그냥 원래 encoding 된 값이다.)
반대로 alpha 값이 커질수록 더 스무스하게 만든다.

코드로 적용해보면 다음과 같다.

df['Sex_n_rows'] = df['Sex'].map(df.groupby('Sex').size())
global_mean = df[target].mean()
alpha = 0.7

def smoothing(n_rows, target_mean):
    return (target_mean*n_rows + global_mean*alpha) / (n_rows + alpha)

df['Sex_mean_smoothing'] = df.apply(lambda x:smoothing(x['Sex_n_rows'], x['Sex_mean']), axis=1)
df[['Sex_mean', 'Sex_mean_smoothing']].head()
  Sex_mean Sex_mean_smoothing
0 0.188908 0.189144
1 0.742038 0.741241
2 0.742038 0.741241
3 0.742038 0.741241
4 0.188908 0.189144

male -> 0.188908 (before) -> 0.189144 (after)
female -> 0.742038 (before) -> 0.741241 (after)

이전보다 값들이 평균쪽으로 좀 더 조정된 것을 알 수 있다.

df.pivot_table(columns=target, index=df.index, values='Sex_mean_smoothing')\
    .iplot(kind='histogram', bins=100, xrange=(0,1))

 

CV loop

CV Loop 는 Trainset 내에서 Cross validation 을 통한 Mean Encoding 을 통해 Data Leakage 를 줄이고, 이전 보다 Label 값에 따른 Encoding 값을 다양하게 만드는 시도를 한다.
Encoding 값을 다양하게 만들면, 트리가 만들어질 때, 더 세분화되서 나누어져, 더 좋은 훈련효과를 볼 수 있다.

from sklearn.model_selection import train_test_split

# trainset 과 testset 분리.
# encoding은 무조건 trainset 만 사용해야 한다.
train, test = train_test_split(df, test_size=0.2, random_state=42, shuffle=True)

# train -> train_new 로 될 예정. 미리 데이터프레임 만들어주기.
train_new = train.copy()
train_new[:] = np.nan
train_new['Sex_mean'] = np.nan
from sklearn.model_selection import StratifiedKFold

# Kfold 만들어 주기.
X_train = train.drop(target, axis=1)
Y_train = train[target]
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# 각 Fold iteration.
for tr_idx, val_idx in skf.split(X_train, Y_train):
    X_train, X_val = train.iloc[tr_idx], train.iloc[val_idx]

    # train set 에서 구한 mean encoded 값을 validation set 에 매핑해줌.
    means = X_val['Sex'].map(X_train.groupby('Sex')[target].mean())
    X_val['Sex_mean'] = means
    train_new.iloc[val_idx] = X_val

# 폴드에 속하지못한 데이터들은 글로벌 평균으로 채워주기.
global_mean = train[target].mean()
train_new['Sex'] = train_new['Sex'].fillna(global_mean)
train_new[['Sex', 'Sex_mean']].head()
  Sex Sex_mean
331 male 0.187166
733 male 0.182796
382 male 0.176152
704 male 0.195251
813 female 0.734694

male -> 0.187166, 0.182796, 0.176152, 0.195251 , ...
로 Encoding 된 것을 볼 수 있다. 즉 이전보다 Label encoding 값이 더 다양해졌다.

위에서 Fold 의 수는 5개 였으니, 각각의 train fold 마다 Mean encoding 값을 만들어 낼 것이고,
그러면 한 Label 값당 5개의 encoding 값이 나오는 것을 알 수 있다.

train_new.pivot_table(columns=target, index=train_new.index, values='Sex_mean')\
    .iplot(kind='histogram', bins=100, xrange=(0,1))

 

Expanding mean

Expanding mean 은 Label 당 encoded 되는 값을 좀 더 많이 만들어보자는 시도이다.
즉 위 CV Loop 기법에서는 encoded 값이 Fold 수만큼 나올 수 밖에 없었다.
뭔가 아쉽다. 더 많이 만들어서 Tree 가 좀 더 잘 학습하게 할 순 없을까?

Expandig mean 은 다음과 같은 방법을 사용한다.
cumsum()cumcount() 를 이용하여, encoded 된 값의 특성은 지니면서, 값을 좀 더 잘게 나누는 테크닉이다.
하지만 이렇게 더 만들어 낸 값이 유용한 값일지, noise 인지 확신할 수는 없다.
경우에 따라서 잘 써야하는 것이다.

아래 코드와 그래프를 보면 좀 더 잘 이해가 될 듯 하다.

cumsum = train.groupby('Sex')[target].cumsum() - train[target]
cumcnt = train.groupby('Sex').cumcount() + 1
train_new['Sex_mean'] = cumsum / cumcnt
cumsum.head()
331    0
733    0
382    0
704    0
813    0
Name: Survived, dtype: int64
train_new[['Sex','Sex_mean']].tail()
  Sex Sex_mean
106 female 0.733607
270 male 0.187097
860 male 0.186695
435 female 0.734694
102 male 0.186296
train_new.pivot_table(columns=target, index=train_new.index, values='Sex_mean')\
    .iplot(kind='histogram', bins=100, xrange=(0,1))

아까보다 훨씬 경우가 많아졌다.
CatBoost 모델에서 이 기법이 Built-in 되어 기본적인 성능 향상을 시켰다고 한다.

레퍼런스 & 더 볼만한 내용들

'데이터와 함께 탱고를 > 머신러닝' 카테고리의 다른 글

코사인 vs 유클리디안 유사도, 케이스로 이해하기  (0) 2019.12.11
Catboost 주요 개념과 특징 이해하기  (7) 2019.10.23
Gradient Boost  (3) 2019.08.09
AdaBoost  (5) 2019.08.07
Random Forest  (2) 2019.08.07