An Overview of Encoding Techniques

|

카카오 아레나 이후로 데이터 컴피티션에 관심이 많이 생겨서 tabular 데이터를 다루는 법에 대해 공부중입니다.

처음 이러한 유형의 컴피티션을 접했을 때 가장 와닫지 않은 부분은 바로 categorical 데이터를 다루는 방법이었습니다.

마침 캐글에 좋은 챌린지가 있어서 1등 노트북을 필사해 봤습니다.

우선 데이터는 아래처럼 생겼습니다.

id bin_0 bin_1 bin_2 bin_3 bin_4 nom_0 nom_1 nom_2 nom_3 ... nom_9 ord_0 ord_1 ord_2 ord_3 ord_4 ord_5 day month target
0 0 0 0 0 T Y Green Triangle Snake Finland ... 2f4cb3d51 2 Grandmaster Cold h D kr 2 2 0
1 1 0 1 0 T Y Green Trapezoid Hamster Russia ... f83c56c21 1 Grandmaster Hot a A bF 7 8 0
2 2 0 0 0 F Y Blue Trapezoid Lion Russia ... ae6800dd0 1 Expert Lava Hot h R Jc 7 2 0
3 3 0 1 0 F Y Red Trapezoid Snake Canada ... 8270f0d71 1 Grandmaster Boiling Hot i D kW 2 1 1
4 4 0 0 0 F N Red Trapezoid Lion Canada ... b164b72a7 1 Grandmaster Freezing a R qP 7 8 0

5 rows × 25 columns

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 300000 entries, 0 to 299999
Data columns (total 25 columns):
id        300000 non-null int64
bin_0     300000 non-null int64
bin_1     300000 non-null int64
bin_2     300000 non-null int64
bin_3     300000 non-null object
bin_4     300000 non-null object
nom_0     300000 non-null object
nom_1     300000 non-null object
nom_2     300000 non-null object
nom_3     300000 non-null object
nom_4     300000 non-null object
nom_5     300000 non-null object
nom_6     300000 non-null object
nom_7     300000 non-null object
nom_8     300000 non-null object
nom_9     300000 non-null object
ord_0     300000 non-null int64
ord_1     300000 non-null object
ord_2     300000 non-null object
ord_3     300000 non-null object
ord_4     300000 non-null object
ord_5     300000 non-null object
day       300000 non-null int64
month     300000 non-null int64
target    300000 non-null int64
dtypes: int64(8), object(17)
memory usage: 57.2+ MB

그럼 다양한 방법으로 categorical feature들을 encoding 해보겠습니다.

1. Label encoding


사실 가장 흔한 방법이고 가장 익숙한 방법입니다.

모든 범주형 데이터를 숫자로 변경합니다. 예를들어 Grandmaster, master, expert가 있을 때, 각각을 1, 2, 3으로 변환하는 것을 뜻합니다.

이를 사용하기 위해 sklearn에서 모듈을 불러옵니다.

from sklearn.preprocessing import LabelEncoder

train=pd.DataFrame()
label=LabelEncoder()
for c in  X.columns:
    if(X[c].dtype=='object'):
        train[c]=label.fit_transform(X[c])
    else:
        train[c]=X[c]
id bin_0 bin_1 bin_2 bin_3 bin_4 nom_0 nom_1 nom_2 nom_3 ... nom_8 nom_9 ord_0 ord_1 ord_2 ord_3 ord_4 ord_5 day month
0 0 0 0 0 1 1 1 5 5 3 ... 1686 2175 2 2 1 7 3 136 2 2
1 1 0 1 0 1 1 1 4 3 5 ... 650 11635 1 2 3 0 0 93 7 8
2 2 0 0 0 0 1 0 4 4 5 ... 1932 8078 1 1 4 7 17 31 7 2

3 rows × 24 columns

모든 범주형 데이터가 숫자로 바뀐 것을 볼 수 있습니다.

>>>print('train data set has got {} rows and{}columns'.format(train.shape[0],train.shape[1]))

train data set has got 300000 rows and 24 columns

각각의 인코딩에 대해 성능을 비교하기위해 심플한 분류 모델을 만듭니다.

Logistic regression

def logistic(X,y):
    X_train,X_test,y_train,y_test=train_test_split(X,y,random_state=42,test_size=0.2)
    lr=LogisticRegression()
    lr.fit(X_train,y_train)
    y_pre=lr.predict(X_test)
    print('Accuracy : ',accuracy_score(y_test,y_pre))
>>>logistic(train,y)
Accuracy :  0.6925333333333333

2. One Hot Encoding


이것도 NLP를 공부해보셨다면 아주 익숙한 표현일 것입니다.

모든 데이터를 고차원 벡터로 변환하는데 여기서 각 차원은 하나의 값만 1이고 나머지는 모두 0입니다.(아주 sparse 하겠죠?)

마찬가지로 sklearn 모듈을 통해 변환할 수 있습니다.(pd.get_dummies 라는 함수로도 가능합니다.)

from sklearn.preprocessing import OneHotEncoder

one=OneHotEncoder()

one.fit(X)
train=one.transform(X)

print('train data set has got {} rows and {} columns'.format(train.shape[0],train.shape[1]))
train data set has got 300000 rows and 316461 columns
logistic(train,y)
Accuracy :  0.7593666666666666

보시는 것 처럼 성능이 좋습니다.

하지만 columns수가 24개에서 31만개로 급격히 늘어난 것을 볼 수 있습니다.

3. Feature hashing(a.k.a the hashing trick)


피처 해싱은 ‘one-hot-encoding’스타일로 카테고리를 sparse matrix로 표현하지만 차원이 훨씬 낮은 멋진 기술입니다.

피처 해싱에서는 해싱 함수를 카테고리에 적용한 다음 해당 인덱스로 표시합니다.

예를 들어, ‘New York’를 나타내기 위해 차원 수를 5로 선택하면, H(New York) mod 5 = 3(예를 들어) 이렇게 계산하면 ‘New York’의 표현은 (0,0,1,0,0)이 됩니다.

마찬가지로 우리의 친구 sklearn 모듈을 불러옵니다.

from sklearn.feature_extraction import FeatureHasher

X_train_hash=X.copy()
for c in X.columns:
    X_train_hash[c]=X[c].astype('str')      
hashing=FeatureHasher(input_type='string')
train=hashing.transform(X_train_hash.values)

print('train data set has got {} rows and {} columns'.format(train.shape[0],train.shape[1]))
train data set has got 300000 rows and 1048576 columns
logistic(train,y)
Accuracy :  0.7512333333333333

보시는 것 처럼 one-hot-encoding으로 변환한 데이터만큼의 성능을 보여주지만 차원 수는 훨씬 낮습니다.

4. Encoding categories with dataset statistics


이제 우리는 모델의 각 피처에 대해 비슷한 범주를 서로 가깝게 배치하는 인코딩을 사용하여 모든 범주에 대한 숫자 표현을 만들어 봅니다.

가장 쉬운 방법은 모든 범주를 데이터 집합에서 나타난 횟수로 바꾸는 것입니다.

이런식으로 뉴욕과 뉴저지가 모두 대도시이면 데이터 세트에서 여러 번 나타날 수 있으며 모델은 이들이 유사하다는 것을 알 수 있습니다.

X_train_stat=X.copy()
for c in X_train_stat.columns:
    if(X_train_stat[c].dtype=='object'):
        X_train_stat[c]=X_train_stat[c].astype('category')
        counts=X_train_stat[c].value_counts()
        counts=counts.sort_index()
        counts=counts.fillna(0)
        counts += np.random.rand(len(counts))/1000
        X_train_stat[c].cat.categories=counts

사실 random을 통해서 노이즈를 왜 추가해 주는지는 잘 모르겠다..

아시는 분 계시면 댓글 달아주세요!

변환 후 아웃풋은 아래와 같다.

id bin_0 bin_1 bin_2 bin_3 bin_4 nom_0 nom_1 nom_2 nom_3 ... nom_8 nom_9 ord_0 ord_1 ord_2 ord_3 ord_4 ord_5 day month
0 0 0 0 0 153535.000826 191633.000545 127341.000001 29855.000948 45979.000444 36942.000133 ... 271.000802 19.000267 2 77428.000323 33768.000648 24740.000509 3974.000977 506.000990 2 2
1 1 0 1 0 153535.000826 191633.000545 127341.000001 101181.000962 29487.000190 101123.000074 ... 111.000142 13.000710 1 77428.000323 22227.000155 35276.000190 18258.000088 2603.000907 7 8
2 2 0 0 0 146465.000337 191633.000545 96166.000432 101181.000962 101295.000088 101123.000074 ... 278.000558 29.000648 1 25065.000347 63908.000426 24740.000509 16927.000164 2572.000012 7 2

3 rows × 24 columns

logistic(X_train_stat,y)
Accuracy :  0.6946166666666667

5. Encoding cyclic features


날짜, 시간 등과 같이 주기를 갖는 데이터에 대해서는 삼각 함수를 사용하여 데이터를 2차원으로 변환 할 수 있습니다.

X_train_cyclic=X.copy()
columns=['day','month']
for col in columns:
    X_train_cyclic[col+'_sin']=np.sin((2*np.pi*X_train_cyclic[col])/max(X_train_cyclic[col]))
    X_train_cyclic[col+'_cos']=np.cos((2*np.pi*X_train_cyclic[col])/max(X_train_cyclic[col]))
X_train_cyclic=X_train_cyclic.drop(columns,axis=1)

X_train_cyclic[['day_sin','day_cos']].head(3)

변환 후 아웃풋은 아래와 같습니다.

day_sin day_cos
0 9.749279e-01 -0.222521
1 -2.449294e-16 1.000000
2 -2.449294e-16 1.000000

주기성을 갖지 않는 나머지 데이터에 대해 one-hot encoding을 적용하여 모델에 넣습니다.

one=OneHotEncoder()

one.fit(X_train_cyclic)
train=one.transform(X_train_cyclic)

print('train data set has got {} rows and {} columns'.format(train.shape[0],train.shape[1]))
train data set has got 300000 rows and 316478 columns
logistic(train,y)
Accuracy :  0.75935

6. Target encoding


Target-based 인코딩은 대상을 통한 범주형 변수의 숫자화입니다.

이 방법에서는 범주형 변수를 하나의 새로운 숫자형 변수로 바꾸고, 범주형 변수의 각 범주를 대상의 확률(범주형) 또는 대상의 평균(숫자인 경우)으로 대체합니다.

아래의 예시를 봅시다.

</tr>
Country Target
India 1
China 0
India 0
China 1
India 1

인도는 전체 레이블에서 3번 나왔고 실제값은 2번 나왔으므로 인도의 레이블은 2/3 = 0.666입니다.

Country Target
India 0.66
China 0.5
X_target=df_train.copy()
X_target['day']=X_target['day'].astype('object')
X_target['month']=X_target['month'].astype('object')
for col in X_target.columns:
    if (X_target[col].dtype=='object'):
        target= dict ( X_target.groupby(col)['target'].agg('sum')/X_target.groupby(col)['target'].agg('count'))
        X_target[col]=X_target[col].replace(target).values

변환 후 아웃풋은 다음과 같습니다.

id bin_0 bin_1 bin_2 bin_3 bin_4 nom_0 nom_1 nom_2 nom_3 ... nom_9 ord_0 ord_1 ord_2 ord_3 ord_4 ord_5 day month target
0 0 0 0 0 0.302537 0.290107 0.327145 0.360978 0.307162 0.242813 ... 0.368421 2 0.403885 0.257877 0.306993 0.208354 0.401186 0.322048 0.244432 0
1 1 0 1 0 0.302537 0.290107 0.327145 0.290054 0.359209 0.289954 ... 0.076923 1 0.403885 0.326315 0.206599 0.186877 0.303880 0.340292 0.327496 0
2 2 0 0 0 0.309384 0.290107 0.241790 0.290054 0.293085 0.289954 ... 0.172414 1 0.317175 0.403126 0.306993 0.351864 0.206843 0.340292 0.244432 0
3 3 0 1 0 0.309384 0.290107 0.351052 0.290054 0.307162 0.339793 ... 0.227273 1 0.403885 0.360961 0.330148 0.208354 0.355985 0.322048 0.255729 1

4 rows × 25 columns

logistic(X_target.drop('target',axis=1),y)
Accuracy :  0.6946166666666667

Summary


전체적으로 one-hot 기반이 성능이 좋은 것을 볼 수 있습니다.

저는 딥러닝을 공부하고 -> tabular데이터(ML계열)로 넘어온 케이스기 때문에 왜 인코딩을 저렇게하지? Word2Vec처럼 모델을 이용해서 object타입간의 유사도를 통한 인코딩이 훨씬 성능이 좋지않나? 라고 생각을 했습니다.

그러나 모델의 피처가 클 경우 저런 방법은 현실적으로 너무 비용이 비싸다는 문제가 있을 것 같습니다.(언젠가 저렇게 모델을 통해 피처를 인코딩하거나 전처리하는 방법을 보게되면 그 때 포스팅 하겠습니다.)

</tr> </tr>
Encoding Score Wall time
Label Encoding 0.692 973 ms
OnHotEncoder 0.759 1.84 s
Feature Hashing 0.751 4.96 s
Dataset statistic encoding 0.694 894 ms
Cyclic + OnHotEncoding 0.759 431 ms
Target encoding 0.694 2min 5s

Comments