編者按:華沙大學機器學習科學家Wojciech Rosinski介紹了類別編碼的主要方法。
介紹
這是特征工程方法系列的第一篇。在機器學習的實踐中,特征工程是最重要而定義最松散的方面之一。它可以被視為藝術,沒有嚴格的規則,創造性是其關鍵。
特征工程是要為機器學習模型創建更好的信息表示。即便使用非線性算法,如果使用原始數據,我們也無法建模數據集的變量之間的所有交互(關系)。因此,我們需要手工探查、處理數據。
這就帶來了一個問題——深度學習怎么講?深度學習是為了最小化手工處理數據的需求,使模型能夠自行學習恰當的數據表示。在圖像、語音、文本之類沒有給定其他“元數據”的數據上,深度學習會表現得更好。而在表格式數據上,沒有什么可以戰勝梯度提升樹方法,例如XGBoost或LightGBM。機器學習競賽證明了這一點——幾乎所有表格式數據的獲勝方案中,基于決策樹的模型是最佳的,而深度學習模型通常沒法達到這么好的結果(但混合基于決策樹的模型時效果非常好 ;-) )
特征工程的偏差是領域知識。取決于需要解決的問題,每個數據集應該使用不同的特征工程方法,原因正在于此。不過,仍有一些廣泛使用的方法,至少值得嘗試下能否提升模型表現。HJ vav Veen的講演中提到了大量的實用信息。下面的一些方法正是根據講演的描述實現的。
本文以KaggleDays數據集為例,編碼方法介紹參考了上面的講演。
數據集
數據來自reddit,包含問題和回答。目標是預測回答的贊數。之所以用這個數據集為例,是因為它包含文本和標準特征。
引入需要用到的庫:
import gc
import numpy as np
import pandas as pd
加載數據:
X = pd.read_csv('../input/train.csv', sep="\t", index_col='id')
列:
['question_id',
'subreddit',
'question_utc',
'question_text',
'question_score',
'answer_utc',
'answer_text',
'answer_score']
每個question_id對應一個具體問題(見question_text)。每個question_id可能出現多次,因為每一行包含對這一問題的一個不同回答(見answer_text)。問題和回答的時間日期由_utc列提供。另外還包括問題發布的subreddit(版塊)的信息。question_score是問題的贊數,而answer_score是回答的贊數。answer_score是目標變量。
數據需要根據question_id分為訓練子集和驗證子集,仿效Kaggle分訓練集和測試集的做法。
question_ids = X.question_id.unique()
question_ids_train = set(pd.Series(question_ids).sample(frac=0.8))
question_ids_valid = set(question_ids).difference(question_ids_train)
X_train = X[X.question_id.isin(question_ids_train)]
X_valid = X[X.question_id.isin(question_ids_valid)]
類別特征和數值特征
機器學習模型只能處理數字。數值(連續、定量)變量是可以在有限或無限區間內取任何值的變量,它們可以很自然地用數字表示,所以可以在模型中直接使用。原始類別變量通常以字符串的形式存在,在傳入模型之前需要變換。
subreddit是類別變量的一個好例子,其中包含41個不同的類別,例如:
['AskReddit', 'Jokes', 'politics', 'explainlikeimfive', 'gaming']
讓我們看下最流行的類別(X.subreddit.value_counts()[:5]):
AskReddit 275667
politics 123003
news 42271
worldnews 40016
gaming 32117
Name: subreddit, dtype: int64
數值變量的一個例子是question_score,可以通過X.question_score.describe()瀏覽信息:
mean 770.891169
std 3094.752794
min 1.000000
25% 2.000000
50% 11.000000
75% 112.000000
max 48834.000000
Name: question_score, dtype: float64
類別特征編碼
類別編碼的兩個基本方法是獨熱編碼(onehot encoding)和標簽編碼(label encoding)。獨熱編碼可以通過pandas.get_dummies完成。具備K個類別的變量的編碼結果是一個K列的二值矩陣,其中第i列的值為1意味著這項觀測屬于第i類。
標簽編碼直接將類別轉換為數字。pandas.factorize提供了這一功能,或者,pandas中category類型的列提供了cat.codes。使用標簽編碼能夠保持原本的維度。
還有一些不那么標準的編碼方法也值得一試,它們可能可以提升模型的表現。這里將介紹三種方法:
頻數編碼(count encoding)
labelcount編碼
目標編碼(target encoding)
頻數編碼
頻數編碼使用頻次替換類別,頻次根據訓練集計算。這個方法對離群值很敏感,所以結果可以歸一化或者轉換一下(例如使用對數變換)。未知類別可以替換為1。
盡管可能性不是非常大,有些變量的頻次可能是一樣的,這將導致碰撞——兩個類別編碼為相同的值。沒法說這是否會導致模型退化或者改善,不過原則上我們不希望出現這種情況。
def count_encode(X, categorical_features, normalize=False):
print('Count encoding: {}'.format(categorical_features))
X_ = pd.DataFrame()
for cat_feature in categorical_features:
X_[cat_feature] = X[cat_feature].astype(
'object').map(X[cat_feature].value_counts())
if normalize:
X_[cat_feature] = X_[cat_feature] / np.max(X_[cat_feature])
X_ = X_.add_suffix('_count_encoded')
if normalize:
X_ = X_.astype(np.float32)
X_ = X_.add_suffix('_normalized')
else:
X_ = X_.astype(np.uint32)
return X_
讓我們編碼下subreddit列:
train_count_subreddit = count_encode(X_train, ['subreddit'])
并查看結果。最流行的5個subreddit:
AskReddit 221941
politics 98233
news 33559
worldnews 32010
gaming 25567
Name: subreddit, dtype: int64
編碼為:
221941 221941
98233 98233
33559 33559
32010 32010
25567 25567
Name: subreddit_count_encoded, dtype: int64
基本上,這用頻次替換了subreddit類別。我們也可以除以最頻繁出現的類別的頻次,以得到歸一化的值:
1.000000 221941
0.442609 98233
0.151207 33559
0.144228 32010
0.115197 25567
Name: subreddit_count_encoded_normalized, dtype: int64
LabelCount編碼
我們下面將描述的方法稱為LabelCount編碼,它根據類別在訓練集中的頻次排序類別(升序或降序)。相比標準的頻次編碼,LabelCount具有特定的優勢——對離群值不敏感,也不會對不同的值給出同樣的編碼。
def labelcount_encode(X, categorical_features, ascending=False):
print('LabelCount encoding: {}'.format(categorical_features))
X_ = pd.DataFrame()
for cat_feature in categorical_features:
cat_feature_value_counts = X[cat_feature].value_counts()
value_counts_list = cat_feature_value_counts.index.tolist()
if ascending:
# 升序
value_counts_range = list(
reversed(range(len(cat_feature_value_counts))))
else:
# 降序
value_counts_range = list(range(len(cat_feature_value_counts)))
labelcount_dict = dict(zip(value_counts_list, value_counts_range))
X_[cat_feature] = X[cat_feature].map(
labelcount_dict)
X_ = X_.add_suffix('_labelcount_encoded')
if ascending:
X_ = X_.add_suffix('_ascending')
else:
X_ = X_.add_suffix('_descending')
X_ = X_.astype(np.uint32)
return X_
編碼:
train_lc_subreddit = labelcount_encode(X_train, ['subreddit'])
這里默認使用降序,subreddit列最流行的5個類別是:
0 221941
1 98233
2 33559
3 32010
4 25567
Name: subreddit_labelcount_encoded_descending, dtype: int64
AskReddit是最頻繁的類別,因此被轉換為0,也就是第一位。
使用升序的話,同樣這5個類別編碼如下:
40 221941
39 98233
38 33559
37 32010
36 25567
Name: subreddit_labelcount_encoded_ascending, dtype: int64
目標編碼
最后是最有技巧性的方法——目標編碼。它使用目標變量的均值編碼類別變量。我們為訓練集中的每個分組計算目標變量的統計量(這里是均值),之后會合并驗證集、測試集以捕捉分組和目標之間的關系。
舉一個更明確的例子,我們可以在每個subreddit上計算answer_score的均值,這樣,在特定subreddit發帖可以期望得到多少贊,我們可以有個大概的估計。
使用目標變量時,非常重要的一點是不要泄露任何驗證集的信息。所有基于目標編碼的特征都應該在訓練集上計算,接著僅僅合并或連接驗證集和測試集。即使驗證集中有目標變量,它不能用于任何編碼計算,否則會給出過于樂觀的驗證誤差估計。
如果使用K折交叉驗證,基于目標的特征應該在折內計算。如果僅僅進行單次分割,那么目標編碼應該在分開訓練集和驗證集之后進行。
此外,我們可以通過平滑避免將特定類別編碼為0. 另一種方法是通過增加隨機噪聲避免可能的過擬合。
處置妥當的情況下,無論是線性模型,還是非線性模型,目標編碼都是最佳的編碼方式。
def target_encode(X, X_valid, categorical_features, X_test=None,
target_feature='target'):
print('Target Encoding: {}'.format(categorical_features))
X_ = pd.DataFrame()
X_valid_ = pd.DataFrame()
if X_test isnotNone:
X_test_ = pd.DataFrame()
for cat_feature in categorical_features:
group_target_mean = X.groupby([cat_feature])[target_feature].mean()
X_[cat_feature] = X[cat_feature].map(group_target_mean)
X_valid_[cat_feature] = X_valid[cat_feature].map(group_target_mean)
X_ = X_.astype(np.float32)
X_ = X_.add_suffix('_target_encoded')
X_valid_ = X_valid_.astype(np.float32)
X_valid_ = X_valid_.add_suffix('_target_encoded')
if X_test isnotNone:
X_test_[cat_feature] = X_test[cat_feature].map(group_target_mean)
X_test_ = X_test_.astype(np.float32)
X_test_ = X_test_.add_suffix('_target_encoded')
return X_, X_valid_, X_test_
return X_, X_valid_
編碼:
train_tm_subreddit, valid_tm_subreddit = target_encode(
X_train, X_valid, categorical_features=['subreddit'],
target_feature='answer_score')
如果我們查看下編碼后的值,就會發現不同reddit的平均贊數有明顯的差別:
23.406061 220014
13.082699 98176
19.020845 33916
17.521887 31869
18.235424 25520
21.535477 24692
18.640282 20416
23.688890 20009
3.159401 18695
Name: subreddit_target_encoded, dtype: int64
AskReddit 220014
politics 98176
news 33916
worldnews 31869
gaming 25520
todayilearned 24692
funny 20416
videos 20009
teenagers 18695
Name: subreddit, dtype: int64
AskReddit中的回答平均能有23.4個贊,而politics和teenagers中的回答分別只有13.1個贊。這樣的特征可能非常強大,因為它讓我們可以在特征集中明確編碼一些目標信息。
獲取類別的編碼值
無需修改編碼函數,我們可以通過如下方式在驗證集或測試集上合并取得的值:
encoded = train_lc_subreddit.subreddit_labelcount_encoded_descending.value_counts().index.values
raw = X_train.subreddit.value_counts().index.values
encoding_dict = dict(zip(raw, encoded))
X_valid['subreddit_labelcount_encoded_descending'] = X_valid.loc[:,
'subreddit'].map(
encoding_dict)
-
編碼
+關注
關注
6文章
935瀏覽量
54768 -
機器學習
+關注
關注
66文章
8381瀏覽量
132428 -
數據集
+關注
關注
4文章
1205瀏覽量
24648
原文標題:特征工程方法:一、類別變量編碼
文章出處:【微信號:jqr_AI,微信公眾號:論智】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論