理解特徵工程Part 1——類別型變量

【導讀】不管是機器學習、深度學習或統計方法,任何的智能系統都需要數據支持。而原始數據往往很難被算法直接利用,因此特徵工程顯得尤爲重要。這是一篇完全手把手教你在實際應用中如何理解特徵工程的教程,在上一篇,作者研究了關於連續數值數據的特徵工程的流行策略,通過實例和代碼詳細展示了連續數值數據特徵工程的過程。

【乾貨】理解特徵工程Part 1——連續數值數據(附代碼)

在本文中,我們將看到另一種類型的結構化數據,它本質上是離散的,通常被稱爲類別數據。本文介紹了一些在離散類別數據上進行特徵工程的主流策略,並且談到了使用特徵工程處理大型特徵空間的一些方法。

作者 | Dipanjan (DJ) Sarkar

編譯 | 專知

翻譯 | Chaofan, Xiaowen

img

Understanding Feature Engineering (Part 2) —Categorical Data

簡介



在本系列的前一篇文章中,我們介紹了用於處理結構化連續數值數據的各種特徵工程策略。在本文中,我們將看看另一種類型的結構化數據,它本質上是離散的,通常被稱爲類別數據。處理數值數據通常比類別數據更容易,因爲我們無需處理屬於任何類別類型的任何數據屬性中與每個類別值有關的附加語義複雜性。我們將來討論處理類別數據的幾種編碼方案,以及一些用於處理大規模特徵爆炸(通常稱爲“維度詛咒”)的流行技術。

動機



我確信,現在你必須認識到特徵工程的動機和重要性,我們在本系列的’第一部分’中也對此做了相同的詳細說明。簡而言之,機器學習算法不能直接處理類別數據,在開始對數據建模之前,你需要對此數據進行一定量轉換。

瞭解分類數據



在深入研究特徵工程策略之前,先了解分類數據表示。典型地,任何本質上屬於類別的數據屬性表示屬於特定有限類別(categories)或類(class)的離散值。在模型預測的屬性或變量的上下文中,這些通常也被稱爲類或標籤(通常稱爲響應變量)。這些離散值本質上可以是文本或數字(甚至可以是非結構化數據,如圖像!)。有兩類主要類別的數據,Nominal(定類變量)定類和Ordinal(定序變量)定序。

在任何定類定類分類數據屬性中,該屬性的值之間沒有排序的概念。考慮一個天氣類別的簡單例子,如下圖所示。我們可以看到,在這個特定的場景中,我們有六個主要的類別,沒有任何概念或順序(颳風並不總是在晴天之前發生,也不是小於或大於晴天)。

img

同樣,電影,音樂和視頻遊戲類型,國名,食物和美食類也是定類定類數據的。

定序定序類別屬性在其價值中有一定的順序意義或概念。 例如請看下圖的襯衫大小。 很明顯,在考慮襯衫時(S小於M,小於L等等),在這種情況下,考慮襯衫的順序或尺寸很重要。

img

定序定序類別屬性還包括鞋子的大小,教育水平和就業角色等。理解了類別數據之後,讓我們看看特徵工程的一些策略。

類別數據的特徵工程雖然在各種機器學習框架中已經取得了很多進展,如可以接受像文本標籤這樣複雜的類別數據類型。 通常,特徵工程中的任何標準工作流程都涉及將這些分類值轉換爲數字標籤的某種形式,然後對這些值應用一些編碼方案。 我們在開始之前加載必要的模塊:

import pandas as pd
import numpy as np

變換定類屬性



定類定類屬性由離散的分類值組成,它沒有概念或順序的意思。 這裏的想法是將這些屬性轉換爲更具代表性的數字格式。 我們來看看與視頻遊戲銷售有關的新數據集。

vg_df = pd.read_csv(‘datasets/vgsales.csv’, encoding=‘utf-8’)
vg_df[[‘Name’, ‘Platform’, ‘Year’, ‘Genre’, ‘Publisher’]].iloc[1:7]

img

讓我們專注於上述數據中描述的視頻遊戲Genre屬性。 很明顯,這是一個像Publisher和Platform一樣的定類上的分類屬性。 我們可以很容易地得到獨特的視頻遊戲類型列表如下。

genres = np.unique(vg_df['Genre'])
genres
Output
------
array(['Action', 'Adventure', 'Fighting', 'Misc', 'Platform',  
       'Puzzle', 'Racing', 'Role-Playing', 'Shooter', 'Simulation',  
       'Sports', 'Strategy'], dtype=object)

這告訴我們有12個不同的視頻遊戲類型。 我們現在可以生成一個標籤編碼方案,通過利用scikit-learn將每個類別映射到一個數值。

from sklearn.preprocessing import LabelEncoder
gle = LabelEncoder()
genre_labels = gle.fit_transform(vg_df['Genre'])
genre_mappings = {index: label for index, label in 
                  enumerate(gle.classes_)}
genre_mappings

Output
------
{0: 'Action', 1: 'Adventure', 2: 'Fighting', 3: 'Misc',
 4: 'Platform', 5: 'Puzzle', 6: 'Racing', 7: 'Role-Playing',
 8: 'Shooter', 9: 'Simulation', 10: 'Sports', 11: 'Strategy'}

因此,在LabelEncoder對象gle的幫助下,每個類型值被映射到一個數字,從而生成了一個映射方案。 轉換後的標籤存儲在我們可以寫回data frame的

genre_labels值中。
vg_df['GenreLabel'] = genre_labels
vg_df[['Name', 'Platform', 'Year', 'Genre', 'GenreLabel']].iloc[1:7]

img

如果你計劃將它們用作預測的響應變量,那麼這些標籤通常可以直接用於scikit-learn等框架,但是如前所述,我們需要額外的編碼步驟,然後才能將它們用作特徵。

轉換定序屬性



定序屬性是類別屬性中具有序列特徵的屬性。讓我們考慮一下我們在本系列第1部分中使用的神奇寶貝數據集( Pokémon dataset )。 以Generation屬性爲例。

poke_df = pd.read_csv('datasets/Pokemon.csv', encoding='utf-8')
poke_df = poke_df.sample(random_state=1, 
                         frac=1).reset_index(drop=True)
np.unique(poke_df['Generation'])
Output
------
array(['Gen 1', 'Gen 2', 'Gen 3', 'Gen 4', 'Gen 5', 'Gen 6'], 
         dtype=object)

根據以上輸出,我們可以看到共有6代,每個神奇寶貝通常屬於基於視頻遊戲的特定時代(當它們被髮布時),並且電視系列也遵循類似的時間線。 這個屬性通常是有序的(這裏需要領域知識),因爲屬於第一代的大多數神奇寶貝都是比第二代更早在視頻遊戲和電視節目中推出的。 動漫迷們可以查看下圖,它列出了每一代流行的神奇寶貝的一部分(球迷們的看法可能不同!)。

img

因此,他們是有序的。 一般來說,沒有通用的模塊或功能來根據訂單自動將這些特徵映射和轉換爲數字表示。 因此,我們可以使用自定義編碼\映射方案。

gen_ord_map = {'Gen 1': 1, 'Gen 2': 2, 'Gen 3': 3, 
               'Gen 4': 4, 'Gen 5': 5, 'Gen 6': 6}
poke_df['GenerationLabel'] = poke_df['Generation'].map(gen_ord_map)
poke_df[['Name', 'Generation', 'GenerationLabel']].iloc[4:10] 

img

從上面的代碼中可以明顯看出,來自pandas的map(…)函數在轉換這個定序特徵時非常有用。

編碼分類屬性



如果你記得我們之前提到的內容,通常對類別數據的特徵工程包括一個轉換過程,我們在前一部分描述了一個轉換過程,以及一個強制編碼過程,我們應用特定的編碼方案爲特定的每個類別\值創建虛擬變量或特徵分類屬性。

你可能會好奇,我們只需要將類別轉換爲數字標籤就行了,爲什麼還需要對它們編碼?原因很簡單。考慮到視頻遊戲的類型,如果我們直接在機器學習模型中提供GenreLabel屬性作爲特徵,它會認爲它是一個連續的數字特徵,並認爲10(Sports)大於6(Racing),但這是沒有意義的,因爲體育類型當然不會比賽車更大或更小,但它們本質上是不同的價值或類別,不能直接比較。因此,我們需要額外的一層編碼方案,其中針對每個屬性的所有不同類別中的每個唯一值或類別創建虛擬特徵。

One-hot編碼方案考慮到我們有任何具有m個標籤(轉換後)的分類屬性的數字表示,這種one-hot編碼方案將該屬性編轉換爲m個只能包含1或0值的二元特徵。分類中的每個觀察值特徵因此被轉換爲大小爲m的矢量,其中只有一個值爲1(表示它是激活的)。我們來看看我們的神奇寶貝數據集的一個子集,它描述了兩個感興趣的屬性。

poke_df [['Name','Generation','Legendary']].iloc [4:10]

img

感興趣的屬性是神奇寶貝Generation及Legendary。 第一步是根據我們以前學到的內容將這些屬性轉換爲數字表示。

from sklearn.preprocessing import OneHotEncoder, LabelEncoder
# transform and map pokemon generations
gen_le = LabelEncoder()
gen_labels = gen_le.fit_transform(poke_df['Generation'])
poke_df['Gen_Label'] = gen_labels
# transform and map pokemon legendary status
leg_le = LabelEncoder()
leg_labels = leg_le.fit_transform(poke_df['Legendary'])
poke_df['Lgnd_Label'] = leg_labels
poke_df_sub = poke_df[['Name', 'Generation', 'Gen_Label',  
                       'Legendary', 'Lgnd_Label']]
poke_df_sub.iloc[4:10]

img

Gen_Label和Lgnd_Label用數字表示了類別特徵。 現在讓我們在這些特徵上應用一種one-hot編碼方案。

# encode generation labels using one-hot encoding scheme
gen_ohe = OneHotEncoder()
gen_feature_arr = gen_ohe.fit_transform(
                              poke_df[['Gen_Label']]).toarray()
gen_feature_labels = list(gen_le.classes_)
gen_features = pd.DataFrame(gen_feature_arr, 
                            columns=gen_feature_labels)
# encode legendary status labels using one-hot encoding scheme
leg_ohe = OneHotEncoder()
leg_feature_arr = leg_ohe.fit_transform(
                                poke_df[['Lgnd_Label']]).toarray()
leg_feature_labels = ['Legendary_'+str(cls_label) 
                           for cls_label in leg_le.classes_]
leg_features = pd.DataFrame(leg_feature_arr, 
                            columns=leg_feature_labels)

通常,你可以使用fit_transform(…)函數將兩個特徵一起編碼,並將兩個特徵的二維數組傳遞給它們。但是我們分別對每個特徵進行編碼,以便更容易理解。 現在讓我們連接這些特徵並查看最終結果。

poke_df_ohe = pd.concat([poke_df_sub, gen_features, leg_features], axis=1)
columns = sum([['Name', 'Generation', 'Gen_Label'],   
               gen_feature_labels, ['Legendary', 'Lgnd_Label'], 
               leg_feature_labels], [])
poke_df_ohe[columns].iloc[4:10]

img

因此,你可以看到已爲Generation創建了6個虛擬變量或二元特徵,並且爲Legendary創建了2個虛擬變量或二元特徵,因爲它們分別是這些屬性中不同類別的總數。。 一個類別的激活狀態由這些虛擬變量之一中的1值表示,這在上面的數據中是很明顯的。

因爲在你的訓練數據上建立了這種編碼方案,並建立了一些模型,在使用新數據預測之前,對新數據也需要這樣的編碼,如下所示。

new_poke_df = pd.DataFrame([['PikaZoom', 'Gen 3', True], 
                           ['CharMyToast', 'Gen 4', False]],
                       columns=['Name', 'Generation', 'Legendary'])
new_poke_df

img

你可以通過調用scikit-learn的LabeLEncoder和OneHotEncoder API來處理數據。 首先我們進行Transformation。

new_gen_labels = gen_le.transform(new_poke_df['Generation'])
new_poke_df['Gen_Label'] = new_gen_labels
new_leg_labels = leg_le.transform(new_poke_df['Legendary'])
new_poke_df['Lgnd_Label'] = new_leg_labels
new_poke_df[['Name', 'Generation', 'Gen_Label', 'Legendary', 
             'Lgnd_Label']]

img

生成數字標籤之後,即可應用編碼方案.

new_gen_feature_arr = gen_ohe.transform(new_poke_df[['Gen_Label']]).toarray()
new_gen_features = pd.DataFrame(new_gen_feature_arr, 
                                columns=gen_feature_labels)
new_leg_feature_arr = leg_ohe.transform(new_poke_df[['Lgnd_Label']]).toarray()
new_leg_features = pd.DataFrame(new_leg_feature_arr, 
                                columns=leg_feature_labels)
new_poke_ohe = pd.concat([new_poke_df, new_gen_features, new_leg_features], axis=1)
columns = sum([['Name', 'Generation', 'Gen_Label'], 
               gen_feature_labels,
               ['Legendary', 'Lgnd_Label'], leg_feature_labels], [])
new_poke_ohe[columns]

img

因此,你可以看到,通過利用scikit-learn強大的API,可以輕鬆地將此方案應用於新數據。

你還可以通過利用pandas的to_dummies(…)函數輕鬆應用one-hot編碼方案。

gen_onehot_features = pd.get_dummies(poke_df['Generation'])
pd.concat([poke_df[['Name', 'Generation']], gen_onehot_features], 
           axis=1).iloc[4:10]

img

上述數據描述了應用於Generation屬性的one-hot編碼方案,並且結果與預期的早期結果相比相同。

虛擬編碼方案(Dummy Coding )



虛擬編碼方案類似於one-hot編碼方案,不同點在於,在虛擬編碼方案中,當應用於具有m個不同標籤的分類特徵時,我們得到m-1個二進制特徵。 因此,類別變量的每個值被轉換爲大小爲m-1的向量。額外的特徵完全被忽略,因此如果分類值從{0,1,…,m-1}到第0或第m-1 特徵列被丟棄,對應的類別值通常由全零(0)的向量表示。 讓我們嘗試通過刪除第一級二進制編碼特徵(Gen 1)來

在神奇寶貝Generation中應用僞編碼方案。
gen_dummy_features = pd.get_dummies(poke_df['Generation'], 
                                    drop_first=True)
pd.concat([poke_df[['Name', 'Generation']], gen_dummy_features], 
          axis=1).iloc[4:10]

img

如果你願意,你也可以選擇向下面那樣捨棄最後一級二進制編碼特徵(Gen 6)。

gen_onehot_features = pd.get_dummies(poke_df['Generation'])
gen_dummy_features = gen_onehot_features.iloc[:,:-1]
pd.concat([poke_df[['Name', 'Generation']], gen_dummy_features],  
          axis=1).iloc[4:10]

img

基於上述描述,很明顯屬於丟棄特徵的類別被表示爲像我們之前討論的零向量(0)。

效應編碼方案(Effect Coding)



效應編碼方案實際上與虛擬編碼方案很像。除了在編碼時,效應編碼將虛擬編碼中全爲0的特徵值變成了-1。下面是一個例子。

gen_onehot_features = pd.get_dummies(poke_df['Generation'])
gen_effect_features = gen_onehot_features.iloc[:,:-1]
gen_effect_features.loc[np.all(gen_effect_features == 0, 
                               axis=1)] = -1.
pd.concat([poke_df[['Name', 'Generation']], gen_effect_features], 
          axis=1).iloc[4:10]

img

上面的輸出清楚地表明,屬於第六代的神奇寶貝現在用虛擬編碼中的零值表示-1值。

計數方案(Bin-counting)



到目前爲止我們討論的編碼方案在一般的分類數據上工作得很好,但是當任何特徵中類別數的數量變得非常大時,問題就來了。m個標籤就會產生m個獨立的特徵。這可以很容易地增加特徵集的大小,導致出現如存儲問題,模型訓練在時間、空間和內存方面的問題。 除此之外,我們還必須處理通常所謂的“維度詛咒”,即特徵多但是並且沒有足夠的代表性樣本,模型性能開始受到影響,經常導致過擬合。

img

因此,我們需要針對具有大量可能類別(如IP地址)的特徵尋找其他解決方案。二進制計數方案是處理具有多個類別的分類變量的有用方案。在這個方案中,我們不是使用實際的標籤值進行編碼,而是使用基於概率的關於值的統計信息以及我們在建模過程中預期的實際目標或響應值。一個簡單的例子,將基於過去的IP地址歷史數據和DDOS攻擊中使用的歷史數據,我們可以爲任何IP地址造成的DDOS攻擊建立概率值。使用這些信息,我們可以對輸入特徵進行編碼,該輸入特徵描述瞭如果將來出現相同的IP地址,那麼引起DDOS攻擊的概率值是多少。這個方案需要詳盡的歷史數據作爲先決條件。用一個完整的例子來描述這個目前是困難的,但是有幾個在線的資源,你可以參考相同的資源。

特徵哈希方案(Feature Hashing)



特徵哈希方案是處理大規模分類特徵的另一個有用的特徵工程方案。在該方案中,哈希函數通常與預先設置的編碼特徵的數量(作爲預定義長度的向量)一起使用,使得特徵的哈希值被用作這個預定向量中的索引,並且值是相應更新的。由於哈希函數將大量值映射爲小的有限值集合,因此多個不同的值可能會創建相同的哈希,稱爲衝突。通常,使用帶符號的哈希函數,以便將從哈希獲得的值的符號用作存儲在適當索引處的最終特徵向量中的值的符號。這應確保較少衝突和由衝突造成的誤差積累。

哈希方案適用於字符串,數字和其他結構,如向量。你可以將哈希輸出視爲有限的一組b bins,以便當哈希函數應用於相同的值\ categories時,根據哈希值將它們分配到b bins中相同的bin(或bin的子集) 。我們可以預先定義b值,它將成爲我們使用特徵哈希方案編碼的每個分類屬性的編碼特徵向量的最終大小。

因此,即使我們在一個特徵中有1000多個不同的類別,並且我們將b = 10設置爲最終的特徵向量大小,但如果我們使用one-hot編碼方案,輸出特徵集合仍然只有10個特徵。我們來考慮我們視頻遊戲數據集中的Genre屬性。

unique_genres = np.unique(vg_df[['Genre']])
print("Total game genres:", len(unique_genres))
print(unique_genres)
Output
------
Total game genres: 12
['Action' 'Adventure' 'Fighting' 'Misc' 'Platform' 'Puzzle' 'Racing'
 'Role-Playing' 'Shooter' 'Simulation' 'Sports' 'Strategy']

我們可以看到總共有12種類型的視頻遊戲。 如果我們在Genre特徵上使用了one-hot編碼,那我們最終得到12個特徵。 相反,我們現在將通過利用scikit-learn的FeatureHasher類來使用功能哈希方案,該類使用帶有簽名的32位Murmurhash3哈希函數。 在這種情況下,我們將預先定義最終的特徵向量大小爲6。

from sklearn.feature_extraction import FeatureHasher
fh = FeatureHasher(n_features=6, input_type='string')
hashed_features = fh.fit_transform(vg_df['Genre'])
hashed_features = hashed_features.toarray()
pd.concat([vg_df[['Name', 'Genre']], pd.DataFrame(hashed_features)], 
          axis=1).iloc[1:7]

img

結論



這些例子爲你提供了一個在離散的類別數據上應用特徵工程的好例子。如果你閱讀本系列的第1部分,你會發現與連續的數值數據相比,處理類別數據有一定難度,但絕對有趣!我們還討論了使用特徵工程處理大型特徵空間的一些方法,但你還應該記住還有其他技術,包括特徵選擇和降維方法來處理大型特徵空間。我們將在後面的文章中介紹其中的一些方法。

本文中使用的所有代碼和數據集都可以從下面鏈接中獲取。

https://github.com/dipanjanS/practical-machine-learning-with-python/tree/master/notebooks/Ch04_Feature_Engineering_and_Selection

原文鏈接:

https://towardsdatascience.com/understanding-feature-engineering-part-1-continuous-numeric-data-da4e47099a7b


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章