哈希特征编码实例

理解特征工程(Part 2) - 分类数据

Introduction(引言)

我们在本系列的前一篇文章中介绍了处理结构化连续数值数据的各种特征工程策略。 在本文中,我们将研究另一种类型的结构化数据,它本质上是离散的,通常被称为分类数据。 处理数字数据通常比分类数据更容易,因为我们不必处理与任何属于分类类型的数据属性中的每个类别值有关的语义的额外复杂性(Dealing with numeric data is often easier than categorical data given that we do not have to deal with additional complexities of the semantics pertaining to each category value in any data attribute which is of a categorical type. )。 我们将使用动手实践的方法来讨论处理分类数据的几种编码方案,以及一些处理大规模特征爆炸的常用技术,通常称为“维度诅咒(curse of dimensionality)”。

Motivation(动机)

我相信你现在必须意识到特征工程的动机和重要性,我们在本系列的“第1部分”中对此有详细的论述。 如有必要,请检查一下以便快速复习。 简而言之,机器学习算法无法直接使用分类数据,您需要在开始对数据建模之前对这些数据进行一些工程和转换。

Understanding Categorical Data(理解分类数据)

在深入介绍特征工程策略之前,让我们先了解一下分类数据表示。 通常,任何属于分类的数据属性表示属于特定有限类别离散值。 这些通常在被模型预测的属性或变量的上下文中也被称为属性或label(通常称为响应变量response variables)。这些离散值本质上可以是文本或数字(甚至可以是非结构化数据,如图像!)。 主要有两大类分类数据:定类(nominal)定序(ordinal)
在任何定类分类数据属性中,没有该属性的值之间顺序的概念。 考虑一个天气类别的简单示例,如下图所示。 我们可以看到在这个特定场景中我们有六个主要类别,没有任何顺序的概念(风很大并不总是在晴天之前发生,也不是比阳光更小或更大)。
在这里插入图片描述
类似地,电影、音乐和视频游戏类型,国家名称,食物和美食类型是定类分类属性的其他示例。
定序分类属性在其值中具有某种意义或顺序概念。 例如,请查看下图中的衬衫尺码。 很明显,在考虑衬衫时,顺序或在这种情况下“尺寸”很重要(S小于M,小于L等等)。
在这里插入图片描述
鞋子大小,教育水平和就业角色是定序分类属性的一些其他例子。 对分类数据有一个不错的想法,让我们现在看一些特征工程策略。

Feature Engineering on Categorical Data

虽然在各种机器学习框架中已经进行了许多改进,以接受复杂的分类数据类型,如文本标签。 通常,特征工程中的任何标准工作流都涉及将这些分类值转换为数字label的某种形式,然后对这些值应用某种编码方案。 我们在开始之前加载必要的必需的东西。

import pandas as pd
import numpy as np
  • 1
  • 2

Transforming Nominal Attributes(转换定类属性)

定类属性由离散的分类值组成,其中没有顺序的概念。 这里的想法是将这些属性转换为更具代表性的数字格式,使得下游代码和管道可以很容易地理解这种格式。 让我们看一下与视频游戏销售有关的新数据集。 此数据集也可以在Kaggle以及我的GitHub中使用。

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

Dataset for video game sales
让我们关注上面数据框中描述的视频游戏Genre属性。 很明显,这是一个定类(nominal)的分类属性,就像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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这告诉我们有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'}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

因此,已经生成了一个映射方案,其中借助于LabelEncoder对象gle将每个类型值映射到数字。 转换后的标签存储在genre_labels值中,我们可以将其写回数据框架。

vg_df['GenreLabel'] = genre_labels
vg_df[['Name', 'Platform', 'Year', 'Genre', 'GenreLabel']].iloc[1:7]
  • 1
  • 2

在这里插入图片描述
如果您计划将它们用作预测的响应变量,这些标签可以直接使用,特别是像scikit-learn这样的框架,但是如前所述,在我们将它们用作特征之前,我们还需要对这些标签进行额外的编码步骤。

Transforming Ordinal Attributes(转换定 序属性)

定序属性是值具有顺序概念的分类属性。 让我们考虑一下我们在本系列第1部分中使用的 Pokémon数据集。 让我们专注于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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

基于上述输出,我们可以看到总共有6代(generations),每个Pokémon通常属于视频游戏(当它们被发布时)的特定代,并且电视连续剧遵循类似的时间线。 这个属性通常是定序的(这里需要相关领域知识),因为属于第1代的大多数Pokémon比第2代更早引入视频游戏和电视节目中。 粉丝可以查看下图来记住每一代流行的Pokémon(神奇宝贝)(粉丝的观点可能不同!)。
在这里插入图片描述
上图的数据源在这里
因此,他们之间有顺序的观念。 通常,没有通用模块或函数来自动地将这些特征映射和转换为顺序的数字表示。 因此,我们可以使用自定义编码\映射方案。

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]
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述
从上面的代码可以看出,pandas中的map(…)函数在转换这个定序特征时非常有用。

Encoding Categorical Attributes(分类属性编码)

如果您还记得我们之前提到的内容,通常对分类数据进行特征工程涉及我们在上一节中描述的转换过程以及强制编码过程,其中我们应用特定的编码方案为特定分类属性的每个类别\值创建虚拟变量或特征。
您可能想知道,我们刚刚在上一节中将类别转换为数字标签,为什么我们现在需要这个呢? 原因很简单, 考虑到视频游戏类型,如果我们直接将GenreLabel属性作为机器学习模型中的特征提供,它会认为它是一个连续数字特征,值10(Sports)比值6(Racing)大,但这是没有意义的,因为Sports类型肯定不会比Racing更大或更小,这些本质上是不同的值或类别,无法直接比较。 因此,我们需要一个额外的编码方案层,其中为每个属性的所有不同类别中的每个唯一值或类别创建虚拟特征(dummy features)。

One-hot Encoding Scheme(one-hot编码)

假设我们有任何分类属性的m个标签(转换后)的数值表示,one-hot编码方案,将属性编码或转换为m个二元特征,这些特征只能包含值1或0。因此分类特征中的每个观察被转换为大小为m的向量,其中只有一个值为1(表示它是激活的)。 让我们把我们的Pokémon数据集的一个子集描绘出两个感兴趣的属性。

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

Subset of our Pokémon dataset
Pokémon中感兴趣的属性是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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在这里插入图片描述
特征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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

通常,您始终可以使用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]
  • 1
  • 2
  • 3
  • 4
  • 5

One-hot encoded features for Pokémon generation and legendary status
因此,您可以看到为Generation特征生成了6个虚拟变量/二元特征,为Legendary特征创建了2个虚拟变量/二元特征,因为它们分别是这些属性中不同类别的总数。 类别的激活状态由这些虚拟变量之一中的1值表示,这从上述数据框中非常明显看到。
考虑一下您在训练数据上构建了这种编码方案并构建了一些模型。

Dummy Coding Scheme(哑编码方案)

哑编码方案类似于one-hot编码方案,哑编码方案不同的情况是,当应用于具有m个不同标签的分类特征时,我们获得m-1个二元特征。 因此,分类变量的每个值都被转换为大小为m-1的向量。额外的一个特征被完全忽略,因此如果类别值的范围从{0,1,…,m-1},第0个或m-1个特征被删除然后相应这个特征的类别值通常由全零(0)的向量表示。 让我们尝试通过删除(drop)第一级二元编码特征(Gen 1)在Pokémon生成上应用虚拟编码方案。

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]
  • 1
  • 2
  • 3
  • 4

Dummy coded features for Pokémon generation
如果需要,您还可以选择删除最后一级二元编码功能(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]
  • 1
  • 2
  • 3
  • 4

Dummy coded features for Pokémon generation
基于以上描述,很清楚被丢弃(droped)特征的类别被表示为像我们之前讨论的zeros (0)的向量。

Effect Coding Scheme(效应编码方案)

效应编码方案实际上非常类似于哑编码方案,不同的是,在编码过程期间,对于在哑编码方案中用全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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述
上面的输出清楚地表明,和哑编码中全0值对应的,属于第6代的Pokémon现在由-1值的向量表示。

Bin-counting Scheme(区间计数方案)

到目前为止我们讨论过的编码方案在一般的分类数据上工作得很好,但是当特征中的不同类别的数量变得非常大时,它们就开始引起问题。 本质上对于m个不同标签的任何分类,您将得到m个独立的特征。 这可能会很容易增加特征集的大小,从而导致存储问题、模型训练问题,这都涉及到时间、空间和内存等问题。 除此之外,我们还必须处理通常所说的“维度诅咒”,其中基本上具有庞大特征而代表性样本不足,模型性能开始受到影响,经常导致过度拟合。
在这里插入图片描述
因此,我们需要针对具有大量可能类别(如IP地址)的特征寻求其他分类数据特征工程方案。 区间计数方案是处理具有许多类别的分类变量的有用方案。在此方案中,我们不使用实际标签值进行编码,而是使用基于概率的统计信息来确定我们在建模工作中要预测的值和实际目标。一个简单的例子是基于过去的IP地址历史数据和DDOS攻击中使用的数据;我们可以为任何IP地址引起的DDOS攻击建立概率值。使用此信息,我们可以对输入特征进行编码,该特征描述了如果将来出现相同的IP地址,导致DDOS攻击的概率值是多少。该方案需要历史数据作为先决条件,并且需要精心设计。用一个完整的例子来描述这种方案目前很困难,但是网上有几个资源可供你参考。

Feature Hashing Scheme(特征哈希方案)

特征hash方案是另一种用于处理大规模分类特征的有用特征工程方案。 在该方案中,hash散列函数通常与预先设置的编码特征的数量(作为预定长度的向量)一起使用,使得特征的散列值用作该预定义向量中的索引,并且值相应的被更新。 由于散列函数将大量的值映射到一小组有限值,因此多个不同的值可能会创建相同的hash值,这叫做称为hash冲突。 通常使用带符号的hash函数,使得从hash获得的值的符号用作值的符号,该值存储在最终特征向量中适当索引处。 这可以确保由于碰撞导致的较小碰撞和较少的误差累积。
hash方案适用于字符串、数字和其他结构,如向量。 您可以将散列输出视为有限的b个箱子(bin)集合,这样当hash函数应用于相同的值\类别时,它们会根据散列值分配到相同bin(或bin的子集)。 我们可以预先定义b的值,该值作为我们使用特征散列方案编码的每个分类属性的编码特征向量的最终大小。
因此,即使我们在一个特征中有超过1000个不同的类别,然后我们将b = 10设置为最终特征向量大小,如果我们使用独热(one-hot)编码方案,与1000个二元特征相比,则输出特征集仍然只有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']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我们可以看到共有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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Feature Hashing on the Genre attribute
基于上面的输出,Genre分类属性已经使用散列方案编码为6个特征而不是12个。我们还可以看到第1行和第6行表示相同类型的游戏,平台已被正确编码到同一个特征向量。

Conclusion(结论)

这些示例应该让您对离散的分类数据的特征工程的流行策略有所了解。 如果您阅读本系列的第1部分,您会看到与连续的数字数据相比,使用分类数据有点挑战,但绝对有趣! 我们还讨论了使用特征工程处理大型特征空间的一些方法,但您还应该记住,还有其他技术包括特征选择(feature selection)和降维方法(dimensionality reduction methods)来处理大型特征空间。 我们将在后面的文章中介绍其中一些方法。


本章作者主要将的是处理离散分类数据的策略,可阅读英文原文Understanding Feature Engineering (Part 2) — Categorical Data

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值