轻松学特征工程(二)
原文:
annas-archive.org/md5/817912ba981171919811b1ecb5aec399译者:飞龙
第四章:特征构建
在上一章中,我们使用 Pima Indian Diabetes Prediction 数据集来更好地了解我们数据集中哪些给定的特征最有价值。使用我们可用的特征,我们识别了列中的缺失值,并采用了删除缺失值、填充以及归一化/标准化数据的技术,以提高我们机器学习模型的准确性。
需要注意的是,到目前为止,我们只处理了定量特征。现在,我们将转向处理除了具有缺失值的定量数据之外,还要处理分类数据。我们的主要焦点将是使用给定的特征来构建模型可以从中学习的新特征。
我们可以利用各种方法来构建我们的特征,最基本的方法是从 Python 中的 pandas 库开始,通过乘数来缩放现有特征。我们将深入研究一些更数学密集的方法,并使用通过 scikit-learn 库提供的各种包;我们还将创建自己的自定义类。随着我们进入代码,我们将详细介绍这些类。
我们将在我们的讨论中涵盖以下主题:
-
检查我们的数据集
-
填充分类特征
-
编码分类变量
-
扩展数值特征
-
文本特定特征构建
检查我们的数据集
为了演示目的,在本章中,我们将使用我们创建的数据集,这样我们就可以展示各种数据级别和类型。让我们设置我们的 DataFrame 并深入了解我们的数据。
我们将使用 pandas 创建我们将要工作的 DataFrame,因为这是 pandas 中的主要数据结构。pandas DataFrame 的优势在于,我们有几个属性和方法可用于对数据进行操作。这使我们能够逻辑地操作数据,以全面了解我们正在处理的内容,以及如何最好地构建我们的机器学习模型:
- 首先,让我们导入
pandas:
# import pandas as pd
- 现在,我们可以设置我们的
DataFrame X。为此,我们将利用 pandas 中的DataFrame方法,该方法创建一个表格数据结构(具有行和列的表格)。此方法可以接受几种类型的数据(例如 NumPy 数组或字典)。在这里,我们将传递一个字典,其键为列标题,值为列表,每个列表代表一列:
X = pd.DataFrame({'city':['tokyo', None, 'london', 'seattle', 'san francisco', 'tokyo'],
'boolean':['yes', 'no', None, 'no', 'no', 'yes'],
'ordinal_column':['somewhat like', 'like', 'somewhat like', 'like', 'somewhat like', 'dislike'],
'quantitative_column':[1, 11, -.5, 10, None, 20]})
- 这将给我们一个具有四列和六行的 DataFrame。让我们打印我们的 DataFrame
X并查看数据:
print X
我们得到以下输出:
| 布尔值 | 城市 | 有序列 | 定量列 | |
|---|---|---|---|---|
| 0 | 是 | 东京 | 有点像 | 1.0 |
| 1 | 否 | 无 | 喜欢的 | 11.0 |
| 2 | 无 | 伦敦 | 有点像 | -0.5 |
| 3 | 否 | 西雅图 | 喜欢的 | 10.0 |
| 4 | 否 | 旧金山 | 有点像 | NaN |
| 5 | 是 | 东京 | 不喜欢的 | 20.0 |
让我们看一下我们的列,并确定我们的数据级别和类型:
-
布尔:这个列由二元分类数据(是/否)表示,处于名称级别 -
城市:这个列由分类数据表示,也处于名称级别 -
序号列:正如你可能从列名猜到的,这个列由序数数据表示,处于序数级别 -
定量列:这个列由整数在比例级别表示
填充分类特征
现在我们已经了解了我们正在处理的数据,让我们看看我们的缺失值:
-
为了做到这一点,我们可以使用 pandas 为我们提供的
isnull方法。此方法返回一个与值大小相同的boolean对象,指示值是否为空。 -
然后,我们将
sum这些值以查看哪些列有缺失数据:
X.isnull().sum()
>>>>
boolean 1
city 1
ordinal_column 0
quantitative_column 1
dtype: int64
在这里,我们可以看到我们有三列数据缺失。我们的行动方案将是填充这些缺失值。
如果你还记得,我们在上一章中实现了 scikit-learn 的Imputer类来填充数值数据。Imputer确实有一个分类选项,most_frequent,然而它只适用于已经被编码为整数的分类数据。
我们可能并不总是想以这种方式转换我们的分类数据,因为它可能会改变我们解释分类信息的方式,因此我们将构建自己的转换器。在这里,我们所说的转换器是指一种方法,通过这种方法,列将填充缺失值。
事实上,在本章中,我们将构建几个自定义转换器,因为它们对于对我们的数据进行转换非常有用,并为我们提供了在 pandas 或 scikit-learn 中不可轻易获得的选择。
让我们从我们的分类列城市开始。正如我们用均值填充缺失行来填充数值数据,我们也有一个类似的方法用于分类数据。为了填充分类数据的值,用最常见的类别填充缺失行。
要这样做,我们需要找出城市列中最常见的类别:
注意,我们需要指定我们正在处理的列来应用一个名为value_counts的方法。这将返回一个按降序排列的对象,因此第一个元素是最频繁出现的元素。
我们将只获取对象中的第一个元素:
# Let's find out what our most common category is in our city column
X['city'].value_counts().index[0]
>>>>
'tokyo'
我们可以看到东京似乎是最常见的城市。现在我们知道要使用哪个值来填充我们的缺失行,让我们填充这些空位。有一个fillna函数允许我们指定我们想要如何填充缺失值:
# fill empty slots with most common category
X['city'].fillna(X['city'].value_counts().index[0])
城市列现在看起来是这样的:
0 tokyo
1 tokyo
2 london
3 seattle
4 san francisco
5 tokyo
Name: city, dtype: object
太好了,现在我们的城市列不再有缺失值。然而,我们的其他分类列布尔仍然有。与其重复同样的方法,让我们构建一个能够处理所有分类数据填充的自定义填充器。
自定义填充器
在我们深入代码之前,让我们快速回顾一下管道:
-
管道允许我们按顺序应用一系列转换和一个最终估计器
-
管道的中间步骤必须是 转换器,这意味着它们必须实现
fit和transform方法 -
最终估计器只需要实现
fit
管道的目的是组装几个可以一起交叉验证的步骤,同时设置不同的参数。一旦我们为需要填充的每一列构建了自定义的转换器,我们将它们全部通过管道传递,以便我们的数据可以一次性进行转换。让我们从构建自定义类别填充器开始。
自定义类别填充器
首先,我们将利用 scikit-learn 的 TransformerMixin 基类来创建我们自己的自定义类别填充器。这个转换器(以及本章中的所有其他自定义转换器)将作为一个具有 fit 和 transform 方法的管道元素工作。
以下代码块将在本章中变得非常熟悉,因此我们将逐行详细讲解:
from sklearn.base import TransformerMixin
class CustomCategoryImputer(TransformerMixin):
def __init__(self, cols=None):
self.cols = cols
def transform(self, df):
X = df.copy()
for col in self.cols:
X[col].fillna(X[col].value_counts().index[0], inplace=True)
return X
def fit(self, *_):
return self
这个代码块中发生了很多事情,所以让我们逐行分解:
- 首先,我们有一个新的
import语句:
from sklearn.base import TransformerMixin
- 我们将从 scikit-learn 继承
TransformerMixin类,它包括一个.fit_transform方法,该方法调用我们将创建的.fit和.transform方法。这允许我们在转换器中保持与 scikit-learn 相似的结构。让我们初始化我们的自定义类:
class CustomCategoryImputer(TransformerMixin):
def __init__(self, cols=None):
self.cols = cols
- 我们已经实例化了我们的自定义类,并有了我们的
__init__方法,该方法初始化我们的属性。在我们的情况下,我们只需要初始化一个实例属性self.cols(它将是我们指定的参数中的列)。现在,我们可以构建我们的fit和transform方法:
def transform(self, df):
X = df.copy()
for col in self.cols:
X[col].fillna(X[col].value_counts().index[0], inplace=True)
return X
- 在这里,我们有我们的
transform方法。它接受一个 DataFrame,第一步是复制并重命名 DataFrame 为X。然后,我们将遍历我们在cols参数中指定的列来填充缺失的槽位。fillna部分可能感觉熟悉,因为我们已经在第一个例子中使用了这个函数。我们正在使用相同的函数,并设置它,以便我们的自定义类别填充器可以一次跨多个列工作。在填充了缺失值之后,我们返回填充后的 DataFrame。接下来是我们的fit方法:
def fit(self, *_):
return self
我们已经设置了我们的 fit 方法简单地 return self,这是 scikit-learn 中 .fit 方法的标准。
- 现在我们有一个自定义方法,允许我们填充我们的类别数据!让我们通过我们的两个类别列
city和boolean来看看它的实际效果:
# Implement our custom categorical imputer on our categorical columns.
cci = CustomCategoryImputer(cols=['city', 'boolean'])
- 我们已经初始化了我们的自定义类别填充器,现在我们需要将这个填充器
fit_transform到我们的数据集中:
cci.fit_transform(X)
我们的数据集现在看起来像这样:
| boolean | city | ordinal_column | quantitative_column | |
|---|---|---|---|---|
| 0 | yes | tokyo | somewhat like | 1.0 |
| 1 | no | tokyo | like | 11.0 |
| 2 | no | london | somewhat like | -0.5 |
| 3 | no | seattle | like | 10.0 |
| 4 | no | san francisco | somewhat like | NaN |
| 5 | yes | tokyo | dislike | 20.0 |
太好了!我们的city和boolean列不再有缺失值。然而,我们的定量列仍然有 null 值。由于默认的填充器不能选择列,让我们再做一个自定义的。
自定义定量填充器
我们将使用与我们的自定义分类填充器相同的结构。这里的主要区别是我们将利用 scikit-learn 的Imputer类来实际上在我们的列上执行转换:
# Lets make an imputer that can apply a strategy to select columns by name
from sklearn.preprocessing import Imputer
class CustomQuantitativeImputer(TransformerMixin):
def __init__(self, cols=None, strategy='mean'):
self.cols = cols
self.strategy = strategy
def transform(self, df):
X = df.copy()
impute = Imputer(strategy=self.strategy)
for col in self.cols:
X[col] = impute.fit_transform(X[[col]])
return X
def fit(self, *_):
return self
对于我们的CustomQuantitativeImputer,我们增加了一个strategy参数,这将允许我们指定我们想要如何为我们的定量数据填充缺失值。在这里,我们选择了mean来替换缺失值,并且仍然使用transform和fit方法。
再次,为了填充我们的数据,我们将调用fit_transform方法,这次指定了列和用于填充的strategy:
cqi = CustomQuantitativeImputer(cols=['quantitative_column'], strategy='mean')
cqi.fit_transform(X)
或者,而不是分别调用和fit_transform我们的CustomCategoryImputer和CustomQuantitativeImputer,我们也可以将它们设置在一个 pipeline 中,这样我们就可以一次性转换我们的 dataset。让我们看看如何:
- 从我们的
import语句开始:
# import Pipeline from sklearn
from sklearn.pipeline import Pipeline
- 现在,我们可以传递我们的自定义填充器:
imputer = Pipeline([('quant', cqi), ('category', cci)]) imputer.fit_transform(X)
让我们看看我们的 dataset 在 pipeline 转换后看起来像什么:
| boolean | city | ordinal_column | quantitative_column | |
|---|---|---|---|---|
| 0 | yes | tokyo | somewhat like | 1.0 |
| 1 | no | tokyo | like | 11.0 |
| 2 | no | london | somewhat like | -0.5 |
| 3 | no | seattle | like | 10.0 |
| 4 | no | san francisco | somewhat like | 8.3 |
| 5 | yes | tokyo | dislike | 20.0 |
现在我们有一个没有缺失值的 dataset 可以工作了!
编码分类变量
回顾一下,到目前为止,我们已经成功填充了我们的 dataset——包括我们的分类和定量列。在这个时候,你可能想知道,我们如何利用分类数据与机器学习算法结合使用?
简而言之,我们需要将这个分类数据转换为数值数据。到目前为止,我们已经确保使用最常见的类别来填充缺失值。现在这件事已经完成,我们需要更进一步。
任何机器学习算法,无论是线性回归还是使用欧几里得距离的 KNN,都需要数值输入特征来学习。我们可以依赖几种方法将我们的分类数据转换为数值数据。
名义级别的编码
让我们从名义级别的数据开始。我们主要的方法是将我们的分类数据转换为虚拟变量。我们有两种方法来做这件事:
-
利用 pandas 自动找到分类变量并将它们转换为虚拟变量
-
使用虚拟变量创建我们自己的自定义转换器以在 pipeline 中工作
在我们深入探讨这些选项之前,让我们先了解一下虚拟变量究竟是什么。
虚拟变量取值为零或一,以表示类别的缺失或存在。它们是代理变量,或数值替代变量,用于定性数据。
考虑一个简单的回归分析来确定工资。比如说,我们被给出了性别,这是一个定性变量,以及教育年限,这是一个定量变量。为了看看性别是否对工资有影响,我们会在女性时将虚拟编码为女性 = 1,在男性时将女性编码为 0。
在使用虚拟变量时,重要的是要意识到并避免虚拟变量陷阱。虚拟变量陷阱是指你拥有独立的变量是多线性的,或者高度相关的。简单来说,这些变量可以从彼此预测。所以,在我们的性别例子中,虚拟变量陷阱就是如果我们同时包含女性作为(0|1)和男性作为(0|1),实际上创建了一个重复的分类。可以推断出 0 个女性值表示男性。
为了避免虚拟变量陷阱,只需省略常数项或其中一个虚拟类别。省略的虚拟变量可以成为与其他变量比较的基础类别。
让我们回到我们的数据集,并采用一些方法将我们的分类数据编码为虚拟变量。pandas 有一个方便的get_dummies方法,实际上它会找到所有的分类变量,并为我们进行虚拟编码:
pd.get_dummies(X,
columns = ['city', 'boolean'], # which columns to dummify
prefix_sep='__') # the separator between the prefix (column name) and cell value
我们必须确保指定我们想要应用到的列,因为它也会对序数列进行虚拟编码,这不会很有意义。我们将在稍后更深入地探讨为什么对序数数据进行虚拟编码没有意义。
我们的数据,加上我们的虚拟编码列,现在看起来是这样的:
| ordinal_column | quantitative_column | city__london | city_san francisco | city_seattle | city_tokyo | boolean_no | boolean_yes | |
|---|---|---|---|---|---|---|---|---|
| 0 | somewhat like | 1.0 | 0 | 0 | 0 | 1 | 0 | 1 |
| 1 | like | 11.0 | 0 | 0 | 0 | 0 | 1 | 0 |
| 2 | somewhat like | -0.5 | 1 | 0 | 0 | 0 | 0 | 0 |
| 3 | like | 10.0 | 0 | 0 | 1 | 0 | 1 | 0 |
| 4 | somewhat like | NaN | 0 | 1 | 0 | 0 | 1 | 0 |
| 5 | dislike | 20.0 | 0 | 0 | 0 | 1 | 0 | 1 |
我们对数据进行虚拟编码的另一种选择是创建自己的自定义虚拟化器。创建这个虚拟化器允许我们设置一个管道,一次将整个数据集转换。
再次强调,我们将使用与之前两个自定义填充器相同的结构。在这里,我们的transform方法将使用方便的 pandas get_dummies方法为指定的列创建虚拟变量。在这个自定义虚拟化器中,我们唯一的参数是cols:
# create our custom dummifier
class CustomDummifier(TransformerMixin):
def __init__(self, cols=None):
self.cols = cols
def transform(self, X):
return pd.get_dummies(X, columns=self.cols)
def fit(self, *_):
return self
我们的定制虚拟化器模仿 scikit-learn 的OneHotEncoding,但具有在完整 DataFrame 上工作的附加优势。
对序数级别的编码
现在,让我们看看我们的有序列。这里仍然有一些有用的信息,然而,我们需要将字符串转换为数值数据。在有序级别,由于数据具有特定的顺序,因此使用虚拟变量是没有意义的。为了保持顺序,我们将使用标签编码器。
通过标签编码器,我们指的是在我们的有序数据中,每个标签都将与一个数值相关联。在我们的例子中,这意味着有序列值(dislike、somewhat like和like)将被表示为0、1和2。
以最简单的方式,代码如下所示:
# set up a list with our ordinal data corresponding the list index
ordering = ['dislike', 'somewhat like', 'like'] # 0 for dislike, 1 for somewhat like, and 2 for like
# before we map our ordering to our ordinal column, let's take a look at the column
print X['ordinal_column']
>>>>
0 somewhat like
1 like
2 somewhat like
3 like
4 somewhat like
5 dislike
Name: ordinal_column, dtype: object
在这里,我们设置了一个列表来排序我们的标签。这是关键,因为我们将利用列表的索引来将标签转换为数值数据。
在这里,我们将在我们的列上实现一个名为map的函数,它允许我们指定我们想要在列上实现的函数。我们使用一个称为lambda的结构来指定这个函数,它本质上允许我们创建一个匿名函数,或者一个没有绑定到名称的函数:
lambda x: ordering.index(x)
这段特定的代码创建了一个函数,该函数将我们的列表ordering的索引应用于每个元素。现在,我们将此映射到我们的有序列:
# now map our ordering to our ordinal column:
print X['ordinal_column'].map(lambda x: ordering.index(x))
>>>>
0 1
1 2
2 1
3 2
4 1
5 0
Name: ordinal_column, dtype: int64
我们的有序列现在被表示为标记数据。
注意,scikit-learn 有一个LabelEncoder,但我们没有使用这种方法,因为它不包括排序类别的能力(0表示不喜欢,1表示有点喜欢,2表示喜欢),正如我们之前所做的那样。相反,默认是排序方法,这不是我们在这里想要使用的。
再次,让我们创建一个自定义标签编码器,使其适合我们的管道:
class CustomEncoder(TransformerMixin):
def __init__(self, col, ordering=None):
self.ordering = ordering
self.col = col
def transform(self, df):
X = df.copy()
X[self.col] = X[self.col].map(lambda x: self.ordering.index(x))
return X
def fit(self, *_):
return self
我们在本章中维护了其他自定义转换器的结构。在这里,我们使用了前面详细说明的map和lambda函数来转换指定的列。注意关键参数ordering,它将确定标签将编码成哪些数值。
让我们称我们的自定义编码器为:
ce = CustomEncoder(col='ordinal_column', ordering = ['dislike', 'somewhat like', 'like'])
ce.fit_transform(X)
经过这些转换后,我们的数据集看起来如下所示:
| 布尔值 | 城市 | 有序列 | 数量列 | |
|---|---|---|---|---|
| 0 | yes | tokyo | 1 | 1.0 |
| 1 | no | None | 2 | 11.0 |
| 2 | None | london | 1 | -0.5 |
| 3 | no | seattle | 2 | 10.0 |
| 4 | no | san francisco | 1 | NaN |
| 5 | yes | tokyo | 0 | 20.0 |
我们的有序列现在被标记。
到目前为止,我们已经相应地转换了以下列:
-
布尔值、城市: 虚拟编码 -
有序列: 标签编码
将连续特征分桶到类别中
有时,当你有连续数值数据时,将连续变量转换为分类变量可能是有意义的。例如,假设你有年龄,但使用年龄范围可能更有用。
pandas 有一个有用的函数cut,可以为你对数据进行分箱。通过分箱,我们指的是它将为你的数据创建范围。
让我们看看这个函数如何在我们的quantitative_column上工作:
# name of category is the bin by default
pd.cut(X['quantitative_column'], bins=3)
cut函数对我们定量列的输出看起来是这样的:
0 (-0.52, 6.333]
1 (6.333, 13.167]
2 (-0.52, 6.333]
3 (6.333, 13.167]
4 NaN
5 (13.167, 20.0]
Name: quantitative_column, dtype: category
Categories (3, interval[float64]): [(-0.52, 6.333] < (6.333, 13.167] < (13.167, 20.0]]
当我们指定bins为整数(bins = 3)时,它定义了X范围内的等宽bins的数量。然而,在这种情况下,X的范围在每边扩展了 0.1%,以包括X的最小值或最大值。
我们也可以将labels设置为False,这将只返回bins的整数指标:
# using no labels
pd.cut(X['quantitative_column'], bins=3, labels=False)
这里是我们quantitative_column的整数指标看起来是这样的:
0 0.0
1 1.0
2 0.0
3 1.0
4 NaN
5 2.0
Name: quantitative_column, dtype: float64
使用cut函数查看我们的选项,我们还可以为我们的管道构建自己的CustomCutter。再次,我们将模仿我们的转换器的结构。我们的transform方法将使用cut函数,因此我们需要将bins和labels作为参数设置:
class CustomCutter(TransformerMixin):
def __init__(self, col, bins, labels=False):
self.labels = labels
self.bins = bins
self.col = col
def transform(self, df):
X = df.copy()
X[self.col] = pd.cut(X[self.col], bins=self.bins, labels=self.labels)
return X
def fit(self, *_):
return self
注意,我们已经将默认的labels参数设置为False。初始化我们的CustomCutter,指定要转换的列和要使用的bins数量:
cc = CustomCutter(col='quantitative_column', bins=3)
cc.fit_transform(X)
使用我们的CustomCutter转换quantitative_column,我们的数据现在看起来是这样的:
| boolean | city | ordinal_column | quantitative_column | |
|---|---|---|---|---|
| 0 | yes | tokyo | somewhat like | 1.0 |
| 1 | no | None | like | 11.0 |
| 2 | None | london | somewhat like | -0.5 |
| 3 | no | seattle | like | 10.0 |
| 4 | no | san francisco | somewhat like | NaN |
| 5 | yes | tokyo | dislike | 20.0 |
注意,我们的quantitative_column现在是序数,因此不需要对数据进行空编码。
创建我们的管道
为了回顾,到目前为止,我们已经以以下方式转换了数据集中的列:
-
boolean, city: 空编码 -
ordinal_column: 标签编码 -
quantitative_column: 序数级别数据
由于我们现在已经为所有列设置了转换,让我们将所有内容组合到一个管道中。
从导入我们的Pipeline类开始,来自 scikit-learn:
from sklearn.pipeline import Pipeline
我们将汇集我们创建的每个自定义转换器。以下是我们在管道中遵循的顺序:
-
首先,我们将使用
imputer来填充缺失值 -
接下来,我们将对分类列进行空编码
-
然后,我们将对
ordinal_column进行编码 -
最后,我们将对
quantitative_column进行分桶
让我们按照以下方式设置我们的管道:
pipe = Pipeline([("imputer", imputer), ('dummify', cd), ('encode', ce), ('cut', cc)])
# will use our initial imputer
# will dummify variables first
# then encode the ordinal column
# then bucket (bin) the quantitative column
为了查看我们使用管道对数据进行完整转换的样子,让我们看看零转换的数据:
# take a look at our data before fitting our pipeline
print X
这是我们数据在开始时,在执行任何转换之前的样子:
| boolean | city | ordinal_column | quantitative_column | |
|---|---|---|---|---|
| 0 | yes | tokyo | somewhat like | 1.0 |
| 1 | no | None | like | 11.0 |
| 2 | None | london | somewhat like | -0.5 |
| 3 | no | seattle | like | 10.0 |
| 4 | no | san francisco | somewhat like | NaN |
| 5 | yes | tokyo | dislike | 20.0 |
我们现在可以fit我们的管道:
# now fit our pipeline
pipe.fit(X)
>>>>
Pipeline(memory=None,
steps=[('imputer', Pipeline(memory=None,
steps=[('quant', <__main__.CustomQuantitativeImputer object at 0x128bf00d0>), ('category', <__main__.CustomCategoryImputer object at 0x13666bf50>)])), ('dummify', <__main__.CustomDummifier object at 0x128bf0ed0>), ('encode', <__main__.CustomEncoder object at 0x127e145d0>), ('cut', <__main__.CustomCutter object at 0x13666bc90>)])
我们已经创建了管道对象,让我们转换我们的 DataFrame:
pipe.transform(X)
在所有适当的列变换之后,我们的最终数据集看起来是这样的:
| ordinal_column | quantitative_column | boolean_no | boolean_yes | city_london | city_san francisco | city_seattle | city_tokyo | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
| 1 | 2 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
| 2 | 1 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
| 3 | 2 | 1 | 1 | 0 | 0 | 0 | 1 | 0 |
| 4 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 0 |
| 5 | 0 | 2 | 0 | 1 | 0 | 0 | 0 | 1 |
扩展数值特征
数值特征可以通过各种方法进行扩展,从而创建出更丰富的特征。之前,我们看到了如何将连续数值数据转换为有序数据。现在,我们将进一步扩展我们的数值特征。
在我们深入探讨这些方法之前,我们将引入一个新的数据集进行操作。
从单个胸挂加速度计数据集进行活动识别
这个数据集收集了十五名参与者进行七种活动的可穿戴加速度计上的数据。加速度计的采样频率为 52 Hz,加速度计数据未经校准。
数据集按参与者分隔,包含以下内容:
-
顺序号
-
x 加速度
-
y 加速度
-
z 加速度
-
标签
标签用数字编码,代表一个活动,如下所示:
-
在电脑上工作
-
站立、行走和上/下楼梯
-
站立
-
行走
-
上/下楼梯
-
与人边走边谈
-
站立时说话
更多关于这个数据集的信息可以在 UCI 机器学习仓库上找到:
archive.ics.uci.edu/ml/datasets/Activity+Recognition+from+Single+Chest-Mounted+Accelerometer
让我们来看看我们的数据。首先,我们需要加载我们的 CSV 文件并设置列标题:
df = pd.read_csv('../data/activity_recognizer/1.csv', header=None)
df.columns = ['index', 'x', 'y', 'z', 'activity']
现在,让我们使用.head方法检查前几行,默认为前五行,除非我们指定要显示的行数:
df.head()
这表明:
| index | x | y | z | activity | |
|---|---|---|---|---|---|
| 0 | 0.0 | 1502 | 2215 | 2153 | 1 |
| 1 | 1.0 | 1667 | 2072 | 2047 | 1 |
| 2 | 2.0 | 1611 | 1957 | 1906 | 1 |
| 3 | 3.0 | 1601 | 1939 | 1831 | 1 |
| 4 | 4.0 | 1643 | 1965 | 1879 | 1 |
这个数据集旨在训练模型,根据智能手机等设备上的加速度计的x、y和z位置来识别用户的当前身体活动。根据网站信息,activity列的选项如下:
-
1: 在电脑上工作
-
2: 站立并上/下楼梯
-
3: 站立
-
4: 走路
-
5: 上/下楼梯
-
6: 与人边走边谈
-
7: 站立时说话
activity列将是我们将尝试预测的目标变量,使用其他列。让我们确定我们的机器学习模型中要击败的零准确率。为此,我们将调用value_counts方法,并将normalize选项设置为True,以给出最常见的活动作为百分比:
df['activity'].value_counts(normalize=True)
7 0.515369
1 0.207242
4 0.165291
3 0.068793
5 0.019637
6 0.017951
2 0.005711
0 0.000006
Name: activity, dtype: float64
要击败的零准确率是 51.53%,这意味着如果我们猜测七个(站立时说话),那么我们正确的时间会超过一半。现在,让我们来进行一些机器学习!让我们逐行进行,设置我们的模型。
首先,我们有我们的import语句:
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV
你可能对上一章中的这些导入语句很熟悉。再一次,我们将利用 scikit-learn 的K-Nearest Neighbors(KNN)分类模型。我们还将使用网格搜索模块,该模块自动找到最适合我们数据的 KNN 模型的最佳参数组合。接下来,我们为我们的预测模型创建一个特征矩阵(X)和一个响应变量(y):
X = df[['x', 'y', 'z']]
# create our feature matrix by removing the response variable
y = df['activity']
一旦我们的X和y设置好,我们就可以引入我们成功运行网格搜索所需的变量和实例:
# our grid search variables and instances
# KNN parameters to try
knn_params = {'n_neighbors':[3, 4, 5, 6]}
接下来,我们将实例化一个 KNN 模型和一个网格搜索模块,并将其拟合到我们的特征矩阵和响应变量:
knn = KNeighborsClassifier()
grid = GridSearchCV(knn, knn_params)
grid.fit(X, y)
现在,我们可以print出最佳的准确率和用于学习的参数:
print grid.best_score_, grid.best_params_
0.720752487677 {'n_neighbors': 5}
使用五个邻居作为其参数,我们的 KNN 模型能够达到 72.07%的准确率,比我们大约 51.53%的零准确率要好得多!也许我们可以利用另一种方法将我们的准确率进一步提高。
多项式特征
处理数值数据并创建更多特征的关键方法是通过 scikit-learn 的PolynomialFeatures类。在其最简单的形式中,这个构造函数将创建新的列,这些列是现有列的乘积,以捕捉特征交互。
更具体地说,这个类将生成一个新的特征矩阵,包含所有小于或等于指定度的特征的多项式组合。这意味着,如果你的输入样本是二维的,如下所示:[a, b],那么二次多项式特征如下:[1, a, b, a², ab, b²]。
参数
在实例化多项式特征时,有三个参数需要考虑:
-
degree
-
interaction_only -
include_bias
度数对应于多项式特征的度数,默认设置为二。
interaction_only是一个布尔值,当为 true 时,只产生交互特征,这意味着是不同度数特征的乘积。interaction_only的默认值是 false。
include_bias也是一个布尔值,当为 true(默认)时,包括一个bias列,即所有多项式幂为零的特征,添加一列全为 1。
让我们先导入类并使用我们的参数实例化,来设置一个多项式特征实例。首先,让我们看看当将 interaction_only 设置为 False 时我们得到哪些特征:
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=False)
现在,我们可以将这些多项式特征 fit_transform 到我们的数据集中,并查看扩展数据集的 shape:
X_poly = poly.fit_transform(X)
X_poly.shape
(162501, 9)
我们的数据集现在扩展到了 162501 行和 9 列。
让我们把数据放入一个 DataFrame 中,设置列标题为 feature_names,并查看前几行:
pd.DataFrame(X_poly, columns=poly.get_feature_names()).head()
这显示给我们:
| x0 | x1 | x2 | x0² | x0 x1 | x0 x2 | x1² | x1 x2 | x2² | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1502.0 | 2215.0 | 2153.0 | 2256004.0 | 3326930.0 | 3233806.0 | 4906225.0 | 4768895.0 | 4635409.0 |
| 1 | 1667.0 | 2072.0 | 2047.0 | 2778889.0 | 3454024.0 | 3412349.0 | 4293184.0 | 4241384.0 | 4190209.0 |
| 2 | 1611.0 | 1957.0 | 1906.0 | 2595321.0 | 3152727.0 | 3070566.0 | 3829849.0 | 3730042.0 | 3632836.0 |
| 3 | 1601.0 | 1939.0 | 1831.0 | 2563201.0 | 3104339.0 | 2931431.0 | 3759721.0 | 3550309.0 | 3352561.0 |
| 4 | 1643.0 | 1965.0 | 1879.0 | 2699449.0 | 3228495.0 | 3087197.0 | 3861225.0 | 3692235.0 | 3530641.0 |
探索性数据分析
现在我们可以进行一些探索性数据分析。由于多项式特征的目的是在原始数据中获得更好的特征交互感,最佳的可视化方式是通过相关性 heatmap。
我们需要导入一个数据可视化工具,以便我们可以创建 heatmap:
%matplotlib inline
import seaborn as sns
Matplotlib 和 Seaborn 是流行的数据可视化工具。我们现在可以如下可视化我们的相关性 heatmap:
sns.heatmap(pd.DataFrame(X_poly, columns=poly.get_feature_names()).corr())
.corr 是一个我们可以调用我们的 DataFrame 的函数,它给我们一个特征的相关矩阵。让我们看一下我们的特征交互:
热图上的颜色基于纯数值;颜色越深,特征的相关性越大。
到目前为止,我们已经查看了我们设置 interaction_only 参数为 False 时的多项式特征。让我们将其设置为 True 并看看没有重复变量时我们的特征看起来如何。
我们将按照之前的方式设置这个多项式特征实例。注意唯一的区别是 interaction_only 现在是 True:
poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=True) X_poly = poly.fit_transform(X) print X_poly.shape
(162501, 6)
我们现在有 162501 行和 6 列。让我们看一下:
pd.DataFrame(X_poly, columns=poly.get_feature_names()).head()
DataFrame 现在看起来如下:
| x0 | x1 | x2 | x0 x1 | x0 x2 | x1 x2 | |
|---|---|---|---|---|---|---|
| 0 | 1502.0 | 2215.0 | 2153.0 | 3326930.0 | 3233806.0 | 4768895.0 |
| 1 | 1667.0 | 2072.0 | 2047.0 | 3454024.0 | 3412349.0 | 4241384.0 |
| 2 | 1611.0 | 1957.0 | 1906.0 | 3152727.0 | 3070566.0 | 3730042.0 |
| 3 | 1601.0 | 1939.0 | 1831.0 | 3104339.0 | 2931431.0 | 3550309.0 |
| 4 | 1643.0 | 1965.0 | 1879.0 | 3228495.0 | 3087197.0 | 3692235.0 |
由于这次interaction_only被设置为True,因此x0²、x1²和x2²消失了,因为它们是重复变量。现在让我们看看我们的相关矩阵现在是什么样子:
sns.heatmap(pd.DataFrame(X_poly,
columns=poly.get_feature_names()).corr())
我们得到了以下结果:
我们能够看到特征是如何相互作用的。我们还可以使用新的多项式特征对 KNN 模型进行网格搜索,这些特征也可以在管道中进行网格搜索:
- 让我们先设置管道参数:
pipe_params = {'poly_features__degree':[1, 2, 3], 'poly_features__interaction_only':[True, False], 'classify__n_neighbors':[3, 4, 5, 6]}
- 现在,实例化我们的
Pipeline:
pipe = Pipeline([('poly_features', poly), ('classify', knn)])
- 从这里,我们可以设置我们的网格搜索并打印出最佳得分和参数以供学习:
grid = GridSearchCV(pipe, pipe_params)
grid.fit(X, y)
print grid.best_score_, grid.best_params_
0.721189408065 {'poly_features__degree': 2, 'poly_features__interaction_only': True, 'classify__n_neighbors': 5}
我们现在的准确率是 72.12%,这比我们使用多项式特征扩展特征时的准确率有所提高!
文本特定特征构建
到目前为止,我们一直在处理分类数据和数值数据。虽然我们的分类数据以字符串的形式出现,但文本一直是单一类别的一部分。现在我们将更深入地研究更长的文本数据。这种文本数据的形式比单一类别的文本数据要复杂得多,因为我们现在有一系列类别,或称为标记。
在我们进一步深入处理文本数据之前,让我们确保我们清楚当我们提到文本数据时我们指的是什么。考虑一个像 Yelp 这样的服务,用户会撰写关于餐厅和企业的评论来分享他们的体验。这些评论,都是以文本格式编写的,包含大量对机器学习有用的信息,例如,在预测最佳餐厅访问方面。
在当今世界,我们的大部分沟通都是通过书面文字进行的,无论是在消息服务、社交媒体还是电子邮件中。因此,通过建模可以从这些信息中获得很多。例如,我们可以从 Twitter 数据中执行情感分析。
这种类型的工作可以被称为自然语言处理(NLP)。这是一个主要关注计算机与人类之间交互的领域,特别是计算机可以被编程来处理自然语言。
现在,正如我们之前提到的,需要注意的是,所有机器学习模型都需要数值输入,因此当我们处理文本并将此类数据转换为数值特征时,我们必须富有创意并战略性地思考。有几种方法可以实现这一点,让我们开始吧。
词袋表示
scikit-learn 有一个方便的模块叫做feature_extraction,它允许我们,正如其名所示,从机器学习算法支持的格式中提取文本等数据的特征。这个模块为我们提供了在处理文本时可以利用的方法。
展望未来,我们可能会将我们的文本数据称为语料库,具体来说,是指文本内容或文档的集合。
将语料库转换为数值表示的一种最常见方法,称为向量化,是通过一种称为词袋模型的方法实现的。词袋模型背后的基本思想是,文档由单词出现来描述,而完全忽略单词在文档中的位置。在其最简单形式中,文本被表示为一个袋,不考虑语法或单词顺序,并作为一个集合维护,给予多重性以重要性。词袋模型表示通过以下三个步骤实现:
-
分词
-
计数
-
正则化
让我们从分词开始。这个过程使用空白和标点符号将单词分开,将它们转换为标记。每个可能的标记都被赋予一个整数 ID。
接下来是计数。这一步只是简单地计算文档中标记的出现次数。
最后是正则化,这意味着当标记在大多数文档中出现时,它们的权重会随着重要性的降低而降低。
让我们考虑更多用于向量化的方法。
CountVectorizer
CountVectorizer是将文本数据转换为它们的向量表示的最常用方法。在某种程度上,它与虚拟变量相似,因为CountVectorizer将文本列转换为矩阵,其中列是标记,单元格值是每个文档中每个标记的出现次数。得到的矩阵被称为文档-词矩阵,因为每一行将代表一个文档(在这种情况下,是一条推文),每一列代表一个术语(一个单词)。
让我们查看一个新的数据集,看看CountVectorizer是如何工作的。Twitter 情感分析数据集包含 1,578,627 条分类推文,每行标记为正情感为 1,负情感为 0。
关于此数据集的更多信息可以在thinknook.com/twitter-sentiment-analysis-training-corpus-dataset-2012-09-22/找到。
让我们使用 pandas 的read_csv方法加载数据。请注意,我们指定了一个可选的encoding参数,以确保我们正确处理推文中的所有特殊字符:
tweets = pd.read_csv('../data/twitter_sentiment.csv', encoding='latin1')
这使我们能够以特定格式加载数据,并适当地映射文本字符。
看看数据的前几行:
tweets.head()
我们得到以下数据:
| 项目 ID | 情感 | 情感文本 | |
|---|---|---|---|
| 0 | 1 | 0 | 我为我的 APL 朋友感到难过... |
| 1 | 2 | 0 | 我错过了新月天体... |
| 2 | 3 | 1 | omg 它已经 7:30 :O |
| 3 | 4 | 0 | .. Omgaga. Im sooo im gunna CRy. I'... |
| 4 | 5 | 0 | 我觉得我的 bf 在欺骗我!!! ... |
我们只关心情感和情感文本列,所以现在我们将删除项目 ID列:
del tweets['ItemID']
我们的数据看起来如下:
| 情感 | 情感文本 | |
|---|---|---|
| 0 | 0 | 我为我的 APL 朋友感到难过... |
| 1 | 0 | 我错过了新月天体... |
| 2 | 1 | omg its already 7:30 :O |
| 3 | 0 | .. Omgaga. Im sooo im gunna CRy. I'... |
| 4 | 0 | i think mi bf is cheating on me!!! ... |
现在,我们可以导入CountVectorizer,更好地理解我们正在处理文本:
from sklearn.feature_extraction.text import CountVectorizer
让我们设置我们的X和y:
X = tweets['SentimentText']
y = tweets['Sentiment']
CountVectorizer类与迄今为止我们一直在使用的自定义转换器非常相似,并且有一个fit_transform函数来处理数据:
vect = CountVectorizer()
_ = vect.fit_transform(X)
print _.shape
(99989, 105849)
在我们的CountVectorizer转换我们的数据后,我们有 99,989 行和 105,849 列。
CountVectorizer有许多不同的参数可以改变构建的特征数量。让我们简要回顾一下这些参数,以更好地了解这些特征是如何创建的。
CountVectorizer 参数
我们将要讨论的一些参数包括:
-
stop_words -
min_df -
max_df -
ngram_range -
analyzer
stop_words是CountVectorizer中常用的一个参数。你可以向该参数传递字符串english,并使用内置的英语停用词列表。你也可以指定一个单词列表。这些单词将被从标记中删除,并且不会出现在你的数据中的特征中。
这里有一个例子:
vect = CountVectorizer(stop_words='english') # removes a set of english stop words (if, a, the, etc)
_ = vect.fit_transform(X)
print _.shape
(99989, 105545)
你可以看到,当没有使用停用词时,特征列从 105,849 减少到 105,545,当设置了英语停用词时。使用停用词的目的是从特征中去除噪声,并移除那些在模型中不会带来太多意义的常用词。
另一个参数称为min_df。该参数用于通过忽略低于给定阈值或截止值的文档频率较低的术语来筛选特征数量。
这里是带有min_df的我们的CountVectorizer实现:
vect = CountVectorizer(min_df=.05) # only includes words that occur in at least 5% of the corpus documents
# used to skim the number of features
_ = vect.fit_transform(X)
print _.shape
(99989, 31)
这是一个用于显著减少创建的特征数量的方法。
同样还有一个参数称为max_df:
vect = CountVectorizer(max_df=.8) # only includes words that occur at most 80% of the documents
# used to "Deduce" stop words
_ = vect.fit_transform(X)
print _.shape
(99989, 105849)
这类似于试图了解文档中存在哪些停用词。
接下来,让我们看看ngram_range参数。该参数接受一个元组,其中 n 值的范围的下限和上限表示要提取的不同 n-gram 的数量。N-gram 代表短语,所以一个值代表一个标记,然而两个值则代表两个标记一起。正如你可以想象的那样,这将显著扩大我们的特征集:
vect = CountVectorizer(ngram_range=(1, 5)) # also includes phrases up to 5 words
_ = vect.fit_transform(X)
print _.shape # explodes the number of features
(99989, 3219557)
看看,我们现在有 3,219,557 个特征。由于单词集(短语)有时可以传达更多的意义,使用 n-gram 范围对于建模是有用的。
你还可以在CountVectorizer中将分析器作为一个参数设置。分析器确定特征应该由单词或字符 n-gram 组成。默认情况下是单词:
vect = CountVectorizer(analyzer='word') # default analyzer, decides to split into words
_ = vect.fit_transform(X)
print _.shape
(99989, 105849)
由于默认情况下是单词,我们的特征列数量与原始数据变化不大。
我们甚至可以创建自己的自定义分析器。从概念上讲,单词是由词根或词干构建的,我们可以构建一个考虑这一点的自定义分析器。
词干提取是一种常见的自然语言处理方法,它允许我们将词汇表简化,或者通过将单词转换为它们的词根来缩小它。有一个名为 NLTK 的自然语言工具包,它有几个包允许我们对文本数据进行操作。其中一个包就是 stemmer。
让我们看看它是如何工作的:
- 首先,导入我们的
stemmer并初始化它:
from nltk.stem.snowball import SnowballStemmer
stemmer = SnowballStemmer('english')
- 现在,让我们看看一些词是如何进行词根提取的:
stemmer.stem('interesting')
u'interest'
- 因此,单词
interesting可以被缩减到其词根。现在我们可以使用这个来创建一个函数,允许我们将单词标记为其词根:
# define a function that accepts text and returns a list of lemmas
def word_tokenize(text, how='lemma'):
words = text.split(' ') # tokenize into words
return [stemmer.stem(word) for word in words]
- 让我们看看我们的函数输出的是什么:
word_tokenize("hello you are very interesting")
[u'hello', u'you', u'are', u'veri', u'interest']
- 我们现在可以将这个标记函数放入我们的分析器参数中:
vect = CountVectorizer(analyzer=word_tokenize)
_ = vect.fit_transform(X)
print _.shape # fewer features as stemming makes words smaller
(99989, 154397)
这给我们带来了更少的功能,这在直观上是有意义的,因为我们的词汇量随着词干提取而减少。
CountVectorizer 是一个非常有用的工具,可以帮助我们扩展特征并将文本转换为数值特征。还有一个常见的向量化器我们将要探讨。
Tf-idf 向量化器
Tf-idfVectorizer 可以分解为两个组件。首先,是 tf 部分,它代表 词频,而 idf 部分则意味着 逆文档频率。这是一种在信息检索和聚类中应用的词—权重方法。
一个权重被赋予以评估一个词在语料库中的文档中的重要性。让我们更深入地看看每个部分:
-
tf:词频:衡量一个词在文档中出现的频率。由于文档的长度可能不同,一个词在较长的文档中可能出现的次数比在较短的文档中多得多。因此,词频通常被除以文档长度,或者文档中的总词数,作为归一化的方式。
-
idf:逆文档频率:衡量一个词的重要性。在计算词频时,所有词都被视为同等重要。然而,某些词,如 is、of 和 that,可能出现很多次,但重要性很小。因此,我们需要减少频繁词的权重,同时增加罕见词的权重。
为了再次强调,TfidfVectorizer 与 CountVectorizer 相同,即它从标记中构建特征,但它更进一步,将计数归一化到语料库中出现的频率。让我们看看这个动作的一个例子。
首先,我们的导入:
from sklearn.feature_extraction.text import TfidfVectorizer
为了引用之前的代码,一个普通的 CountVectorizer 将输出一个文档-词矩阵:
vect = CountVectorizer()
_ = vect.fit_transform(X)
print _.shape, _[0,:].mean()
(99989, 105849) 6.61319426731e-05
我们的 TfidfVectorizer 可以设置如下:
vect = TfidfVectorizer()
_ = vect.fit_transform(X)
print _.shape, _[0,:].mean() # same number of rows and columns, different cell values
(99989, 105849) 2.18630609758e-05
我们可以看到,这两个向量化器输出相同数量的行和列,但在每个单元格中产生不同的值。这是因为 TfidfVectorizer 和 CountVectorizer 都用于将文本数据转换为定量数据,但它们填充单元格值的方式不同。
在机器学习管道中使用文本
当然,我们的向量器的最终目标是将它们用于使文本数据可被我们的机器学习管道摄取。因为CountVectorizer和TfidfVectorizer就像我们在本书中使用的任何其他转换器一样,我们将不得不利用 scikit-learn 管道来确保我们的机器学习管道的准确性和诚实性。在我们的例子中,我们将处理大量的列(数以万计),因此我将使用在这种情况下已知更有效的分类器,即朴素贝叶斯模型:
from sklearn.naive_bayes import MultinomialNB # for faster predictions with large number of features...
在我们开始构建管道之前,让我们获取响应列的空准确率,该列要么为零(消极),要么为一(积极):
# get the null accuracy
y.value_counts(normalize=True)
1 0.564632 0 0.435368 Name: Sentiment, dtype: float64
使准确率超过 56.5%。现在,让我们创建一个包含两个步骤的管道:
-
使用
CountVectorizer对推文进行特征提取 -
MultiNomialNB朴素贝叶斯模型用于区分积极和消极情绪
首先,让我们设置我们的管道参数如下,然后按照以下方式实例化我们的网格搜索:
# set our pipeline parameters
pipe_params = {'vect__ngram_range':[(1, 1), (1, 2)], 'vect__max_features':[1000, 10000], 'vect__stop_words':[None, 'english']}
# instantiate our pipeline
pipe = Pipeline([('vect', CountVectorizer()), ('classify', MultinomialNB())])
# instantiate our gridsearch object
grid = GridSearchCV(pipe, pipe_params)
# fit the gridsearch object
grid.fit(X, y)
# get our results
print grid.best_score_, grid.best_params_
0.755753132845 {'vect__ngram_range': (1, 2), 'vect__stop_words': None, 'vect__max_features': 10000}
我们得到了 75.6%,这很棒!现在,让我们加快速度,并引入TfidfVectorizer。而不是使用 tf-idf 重建管道而不是CountVectorizer,让我们尝试使用一些不同的东西。scikit-learn 有一个FeatureUnion模块,它促进了特征的横向堆叠(并排)。这允许我们在同一个管道中使用多种类型的文本特征提取器。
例如,我们可以在我们的推文中运行一个featurizer,它同时运行一个TfidfVectorizer和一个CountVectorizer,并将它们水平连接(保持行数不变但增加列数):
from sklearn.pipeline import FeatureUnion
# build a separate featurizer object
featurizer = FeatureUnion([('tfidf_vect', TfidfVectorizer()), ('count_vect', CountVectorizer())])
一旦我们构建了featurizer,我们就可以用它来查看它如何影响我们数据的形状:
_ = featurizer.fit_transform(X)
print _.shape # same number of rows , but twice as many columns as either CV or TFIDF
(99989, 211698)
我们可以看到,将两个特征提取器合并会导致具有相同行数的数据集,但将CountVectorizer或TfidfVectorizer的数量翻倍。这是因为结果数据集实际上是两个数据集并排放置。这样,我们的机器学习模型可以同时从这两组数据中学习。让我们稍微改变一下featurizer对象的params,看看它会产生什么差异:
featurizer.set_params(tfidf_vect__max_features=100, count_vect__ngram_range=(1, 2),
count_vect__max_features=300)
# the TfidfVectorizer will only keep 100 words while the CountVectorizer will keep 300 of 1 and 2 word phrases
_ = featurizer.fit_transform(X)
print _.shape # same number of rows , but twice as many columns as either CV or TFIDF
(99989, 400)
让我们构建一个更全面的管道,它结合了两个向量器的特征合并:
pipe_params = {'featurizer__count_vect__ngram_range':[(1, 1), (1, 2)], 'featurizer__count_vect__max_features':[1000, 10000], 'featurizer__count_vect__stop_words':[None, 'english'],
'featurizer__tfidf_vect__ngram_range':[(1, 1), (1, 2)], 'featurizer__tfidf_vect__max_features':[1000, 10000], 'featurizer__tfidf_vect__stop_words':[None, 'english']}
pipe = Pipeline([('featurizer', featurizer), ('classify', MultinomialNB())])
grid = GridSearchCV(pipe, pipe_params)
grid.fit(X, y)
print grid.best_score_, grid.best_params_
0.758433427677 {'featurizer__tfidf_vect__max_features': 10000, 'featurizer__tfidf_vect__stop_words': 'english', 'featurizer__count_vect__stop_words': None, 'featurizer__count_vect__ngram_range': (1, 2), 'featurizer__count_vect__max_features': 10000, 'featurizer__tfidf_vect__ngram_range': (1, 1)}
很好,甚至比单独使用CountVectorizer还要好!还有一点值得注意的是,CountVectorizer的最佳ngram_range是(1, 2),而TfidfVectorizer是(1, 1),这意味着单独的单词出现并不像两个单词短语的出现那样重要。
到目前为止,应该很明显,我们可以通过以下方式使我们的管道变得更加复杂:
-
- 对每个向量器进行数十个参数的网格搜索
-
- 在我们的管道中添加更多步骤,例如多项式特征构造
但这对本文来说会很繁琐,而且在大多数商业笔记本电脑上运行可能需要数小时。请随意扩展这个管道,并超越我们的分数!
呼,这可真不少。文本处理可能会很困难。在讽刺、拼写错误和词汇量方面,数据科学家和机器学习工程师的工作量很大。本指南将使您,作为读者,能够对自己的大型文本数据集进行实验,并获得自己的结果!
摘要
到目前为止,我们已经介绍了在分类和数值数据中填充缺失值的方法,对分类变量进行编码,以及创建自定义转换器以适应管道。我们还深入探讨了针对数值数据和基于文本数据的特征构造方法。
在下一章中,我们将查看我们构建的特征,并考虑选择适当的方法来选择用于我们的机器学习模型的正确特征。
第五章:特征选择
我们已经完成了文本的一半,并且我们已经处理了大约一打数据集,看到了许多我们作为数据科学家和机器学习工程师在工作和生活中可能利用的特征选择方法,以确保我们能够从预测建模中获得最大收益。到目前为止,在处理数据时,我们已经使用了包括以下方法在内的方法:
-
通过识别数据级别来理解特征
-
特征改进和缺失值填充
-
特征标准化和归一化
上述每种方法都在我们的数据处理流程中占有一席之地,而且往往两种或更多方法会相互配合使用。
文本的剩余部分将专注于其他特征工程方法,这些方法在本质上比本书前半部分更为数学化和复杂。随着先前工作流程的增长,我们将尽力避免让读者了解我们调用的每一个统计测试的内部机制,而是传达一个更广泛的测试目标图景。作为作者和讲师,我们始终欢迎您就本工作的任何内部机制提出问题。
在我们讨论特征的过程中,我们经常遇到一个问题,那就是噪声。我们常常不得不处理那些可能不是高度预测响应的特征,有时甚至可能阻碍我们的模型在预测响应方面的性能。我们使用标准化和归一化等工具来尝试减轻这种损害,但最终,噪声必须得到处理。
在本章中,我们将讨论一种称为特征选择的特征工程子集,这是从原始特征集中选择哪些特征在模型预测流程中是最佳的过程。更正式地说,给定 n 个特征,我们寻找一个包含 k 个特征(其中 k < n)的子集,以改善我们的机器学习流程。这通常归结为以下陈述:
特征选择旨在去除数据中的噪声并消除它。
特征选择的定义涉及两个必须解决的问题:
-
我们可能找到的 k 个特征子集的方法
-
在机器学习背景下更好的定义
本章的大部分内容致力于探讨我们如何找到这样的特征子集以及这些方法运作的基础。本章将特征选择方法分为两大类:基于统计和基于模型的特征选择。这种划分可能无法完全捕捉特征选择这一科学和艺术领域的复杂性,但它有助于在我们的机器学习流程中产生真实且可操作的结果。
在我们深入探讨许多这些方法之前,让我们首先讨论如何更好地理解和定义更好的概念,因为它将界定本章的其余部分,以及界定本文本的其余部分。
我们在本章中将涵盖以下主题:
-
在特征工程中实现更好的性能
-
创建一个基线机器学习管道
-
特征选择类型
-
选择正确的特征选择方法
在特征工程中实现更好的性能
在整本书中,我们在实施各种特征工程方法时,都依赖于对更好的基本定义。我们的隐含目标是实现更好的预测性能,这种性能仅通过简单的指标来衡量,例如分类任务的准确率和回归任务的 RMSE(主要是准确率)。我们还可以测量和跟踪其他指标来衡量预测性能。例如,我们将使用以下指标进行分类:
-
真阳性率和假阳性率
-
灵敏度(也称为真阳性率)和特异性
-
假阴性率和假阳性率
对于回归,将应用以下指标:
-
均方误差
-
R²
这些列表将继续,虽然我们不会放弃通过如前所述的指标量化性能的想法,但我们也可以测量其他元指标,或者不直接与模型预测性能相关的指标,而是所谓的元指标试图衡量预测周围的性能,包括以下想法:
-
模型需要拟合/训练到数据的时间
-
调整模型以预测新数据实例所需的时间
-
如果数据必须持久化(存储以供以后使用),则数据的大小
这些想法将丰富我们对更好的机器学习的定义,因为它们有助于涵盖我们机器学习管道(除了模型预测性能之外)的更广阔的图景。为了帮助我们跟踪这些指标,让我们创建一个足够通用的函数来评估多个模型,但同时又足够具体,可以为我们每个模型提供指标。我们将我们的函数命名为get_best_model_and_accuracy,它将执行许多工作,例如:
-
它将搜索所有给定的参数以优化机器学习管道
-
它将输出一些指标,帮助我们评估输入管道的质量
让我们定义这样一个函数,以下代码将提供帮助:
# import out grid search module
from sklearn.model_selection import GridSearchCV
def get_best_model_and_accuracy(model, params, X, y):
grid = GridSearchCV(model, # the model to grid search
params, # the parameter set to try
error_score=0.) # if a parameter set raises an error, continue and set the performance as a big, fat 0
grid.fit(X, y) # fit the model and parameters
# our classical metric for performance
print "Best Accuracy: {}".format(grid.best_score_)
# the best parameters that caused the best accuracy
print "Best Parameters: {}".format(grid.best_params_)
# the average time it took a model to fit to the data (in seconds)
print "Average Time to Fit (s): {}".format(round(grid.cv_results_['mean_fit_time'].mean(), 3))
# the average time it took a model to predict out of sample data (in seconds)
# this metric gives us insight into how this model will perform in real-time analysis
print "Average Time to Score (s): {}".format(round(grid.cv_results_['mean_score_time'].mean(), 3))
这个函数的整体目标是作为一个基准,我们将用它来评估本章中的每个特征选择方法,以给我们一个评估标准化的感觉。这实际上与我们之前所做的是一样的,但现在我们将我们的工作正式化为一个函数,并且还使用除了准确率之外的指标来评估我们的特征选择模块和机器学习管道。
一个案例研究——信用卡违约数据集
通过从数据中智能提取最重要的信号并忽略噪声,特征选择算法实现了两个主要成果:
-
改进模型性能:通过移除冗余数据,我们不太可能基于噪声和不相关数据做出决策,这也使得我们的模型能够专注于重要特征,从而提高模型管道的预测性能
-
减少训练和预测时间:通过将管道拟合到更少的数据,这通常会导致模型拟合和预测时间的改进,从而使我们的管道整体运行更快
为了获得对噪声数据如何以及为何会阻碍我们的现实理解,让我们介绍我们的最新数据集,一个信用卡违约数据集。我们将使用 23 个特征和一个响应变量。该响应变量将是一个布尔值,意味着它将是 True 或 False。我们使用 23 个特征的原因是我们想看看哪些特征可以帮助我们在机器学习管道中,哪些会阻碍我们。我们可以使用以下代码导入数据集:
import pandas as pd
import numpy as np
# we will set a random seed to ensure that whenever we use random numbers
# which is a good amount, we will achieve the same random numbers
np.random.seed(123)
首先,让我们引入两个常用的模块,numpy 和 pandas,并设置一个随机种子以确保结果的一致性。现在,让我们引入最新的数据集,使用以下代码:
# archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients
# import the newest csv
credit_card_default = pd.read_csv('../data/credit_card_default.csv')
让我们继续进行一些必要的探索性数据分析。首先,让我们检查我们正在处理的数据集有多大,使用以下代码:
# 30,000 rows and 24 columns
credit_card_default.shape
因此,我们拥有 30,000 行(观测值)和 24 列(1 个响应变量和 23 个特征)。我们在此不会深入描述列的含义,但我们鼓励读者查看数据来源(archive.ics.uci.edu/ml/datasets/default+of+credit+card+clients#)。目前,我们将依靠传统的统计方法来获取更多信息:
# Some descriptive statistics
# We invoke the .T to transpose the matrix for better viewing
credit_card_default.describe().T
输出如下:
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| LIMIT_BAL | 30000.0 | 167484.322667 | 129747.661567 | 10000.0 | 50000.00 | 140000.0 | 240000.00 | 1000000.0 |
| 性别 | 30000.0 | 1.603733 | 0.489129 | 1.0 | 1.00 | 2.0 | 2.00 | 2.0 |
| 教育程度 | 30000.0 | 1.853133 | 0.790349 | 0.0 | 1.00 | 2.0 | 2.00 | 6.0 |
| 婚姻状况 | 30000.0 | 1.551867 | 0.521970 | 0.0 | 1.00 | 2.0 | 2.00 | 3.0 |
| 年龄 | 30000.0 | 35.485500 | 9.217904 | 21.0 | 28.00 | 34.0 | 41.00 | 79.0 |
| PAY_0 | 30000.0 | -0.016700 | 1.123802 | -2.0 | -1.00 | 0.0 | 0.00 | 8.0 |
| PAY_2 | 30000.0 | -0.133767 | 1.197186 | -2.0 | -1.00 | 0.0 | 0.00 | 8.0 |
| PAY_3 | 30000.0 | -0.166200 | 1.196868 | -2.0 | -1.00 | 0.0 | 0.00 | 8.0 |
| PAY_4 | 30000.0 | -0.220667 | 1.169139 | -2.0 | -1.00 | 0.0 | 0.00 | 8.0 |
| PAY_5 | 30000.0 | -0.266200 | 1.133187 | -2.0 | -1.00 | 0.0 | 0.00 | 8.0 |
| PAY_6 | 30000.0 | -0.291100 | 1.149988 | -2.0 | -1.00 | 0.0 | 0.00 | 8.0 |
| BILL_AMT1 | 30000.0 | 51223.330900 | 73635.860576 | -165580.0 | 3558.75 | 22381.5 | 67091.00 | 964511.0 |
| BILL_AMT2 | 30000.0 | 49179.075167 | 71173.768783 | -69777.0 | 2984.75 | 21200.0 | 64006.25 | 983931.0 |
| BILL_AMT3 | 30000.0 | 47013.154800 | 69349.387427 | -157264.0 | 2666.25 | 20088.5 | 60164.75 | 1664089.0 |
| BILL_AMT4 | 30000.0 | 43262.948967 | 64332.856134 | -170000.0 | 2326.75 | 19052.0 | 54506.00 | 891586.0 |
| BILL_AMT5 | 30000.0 | 40311.400967 | 60797.155770 | -81334.0 | 1763.00 | 18104.5 | 50190.50 | 927171.0 |
| BILL_AMT6 | 30000.0 | 38871.760400 | 59554.107537 | -339603.0 | 1256.00 | 17071.0 | 49198.25 | 961664.0 |
| PAY_AMT1 | 30000.0 | 5663.580500 | 16563.280354 | 0.0 | 1000.00 | 2100.0 | 5006.00 | 873552.0 |
| PAY_AMT2 | 30000.0 | 5921.163500 | 23040.870402 | 0.0 | 833.00 | 2009.0 | 5000.00 | 1684259.0 |
| PAY_AMT3 | 30000.0 | 5225.681500 | 17606.961470 | 0.0 | 390.00 | 1800.0 | 4505.00 | 891586.0 |
| PAY_AMT4 | 30000.0 | 4826.076867 | 15666.159744 | 0.0 | 296.00 | 1500.0 | 4013.25 | 621000.0 |
| PAY_AMT5 | 30000.0 | 4799.387633 | 15278.305679 | 0.0 | 252.50 | 1500.0 | 4031.50 | 426529.0 |
| PAY_AMT6 | 30000.0 | 5215.502567 | 17777.465775 | 0.0 | 117.75 | 1500.0 | 4000.00 | 528666.0 |
| default payment next month | 30000.0 | 0.221200 | 0.415062 | 0.0 | 0.00 | 0.0 | 0.00 | 1.0 |
下个月的默认付款是我们的响应列,其余的都是特征/潜在的预测因子。很明显,我们的特征存在于截然不同的尺度上,这将是我们处理数据和选择模型的一个因素。在前面章节中,我们大量处理了不同尺度的数据和特征,使用了如StandardScaler和归一化等解决方案来缓解这些问题;然而,在本章中,我们将主要选择忽略这些问题,以便专注于更相关的问题。
在本书的最后一章中,我们将关注几个案例研究,这些研究将几乎将本书中的所有技术结合在一起,对数据集进行长期分析。
正如我们在前面的章节中看到的,我们知道在处理机器学习时,空值是一个大问题,所以让我们快速检查一下,确保我们没有要处理的空值:
# check for missing values, none in this dataset
credit_card_default.isnull().sum()
LIMIT_BAL 0
SEX 0
EDUCATION 0
MARRIAGE 0
AGE 0
PAY_0 0
PAY_2 0
PAY_3 0
PAY_4 0
PAY_5 0
PAY_6 0
BILL_AMT1 0
BILL_AMT2 0
BILL_AMT3 0
BILL_AMT4 0
BILL_AMT5 0
BILL_AMT6 0
PAY_AMT1 0
PAY_AMT2 0
PAY_AMT3 0
PAY_AMT4 0
PAY_AMT5 0
PAY_AMT6 0
default payment next month 0
dtype: int64
呼!这里没有缺失值。同样,我们将在未来的案例研究中再次处理缺失值,但现在我们还有更重要的事情要做。让我们继续设置一些变量,用于我们的机器学习流程,使用以下代码:
# Create our feature matrix
X = credit_card_default.drop('default payment next month', axis=1)
# create our response variable
y = credit_card_default['default payment next month']
如同往常,我们创建了我们的X和y变量。我们的X矩阵将有 30,000 行和 23 列,而y始终是一个 30,000 长的 pandas Series。因为我们将会进行分类,所以我们通常需要确定一个空值准确率,以确保我们的机器学习模型的表现优于基线。我们可以使用以下代码来获取空值准确率:
# get our null accuracy rate
y.value_counts(normalize=True)
0 0.7788
1 0.2212
因此,在这个案例中需要超越的准确率是77.88%,这是没有违约(0 表示没有违约)的人的百分比。
创建基线机器学习流程
在前面的章节中,我们向读者提供了一个单一的机器学习模型在整个章节中使用。在本章中,我们将做一些工作来找到最适合我们需求的机器学习模型,然后通过特征选择来增强该模型。我们将首先导入四个不同的机器学习模型:
-
逻辑回归
-
K-最近邻
-
决策树
-
随机森林
导入学习模型的代码如下所示:
# Import four machine learning models
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
一旦我们完成这些模块的导入,我们将通过我们的get_best_model_和_accuracy函数运行它们,以获得每个模块处理原始数据的基线。为此,我们首先需要建立一些变量。我们将使用以下代码来完成这项工作:
# Set up some parameters for our grid search
# We will start with four different machine learning model parameters
# Logistic Regression
lr_params = {'C':[1e-1, 1e0, 1e1, 1e2], 'penalty':['l1', 'l2']}
# KNN
knn_params = {'n_neighbors': [1, 3, 5, 7]}
# Decision Tree
tree_params = {'max_depth':[None, 1, 3, 5, 7]}
# Random Forest
forest_params = {'n_estimators': [10, 50, 100], 'max_depth': [None, 1, 3, 5, 7]}
如果你以上列出的任何模型感到不舒服,我们建议阅读相关文档,或者参考 Packt 出版的《数据科学原理》一书,www.packtpub.com/big-data-and-business-intelligence/principles-data-science,以获得算法的更详细解释。
因为我们将把每个模型通过我们的函数发送,该函数调用网格搜索模块,我们只需要创建没有设置自定义参数的空白状态模型,如下所示:
# instantiate the four machine learning models
lr = LogisticRegression()
knn = KNeighborsClassifier()
d_tree = DecisionTreeClassifier()
forest = RandomForestClassifier()
现在,我们将运行每个四个机器学习模型通过我们的评估函数,看看它们在我们的数据集上的表现如何(或不好)。回想一下,我们目前要超越的数字是.7788,这是基线零准确率。我们将使用以下代码来运行这些模型:
get_best_model_and_accuracy(lr, lr_params, X, y)
Best Accuracy: 0.809566666667
Best Parameters: {'penalty': 'l1', 'C': 0.1}
Average Time to Fit (s): 0.602
Average Time to Score (s): 0.002
我们可以看到,逻辑回归已经使用原始数据超越了零准确率,平均而言,只需要 6/10 秒来拟合训练集,并且只需要 20 毫秒来评分。如果我们知道在scikit-learn中,逻辑回归必须创建一个大的矩阵存储在内存中,但为了预测,它只需要将标量相乘和相加,这是有道理的。
现在,让我们使用以下代码对 KNN 模型做同样的事情:
get_best_model_and_accuracy(knn, knn_params, X, y)
Best Accuracy: 0.760233333333
Best Parameters: {'n_neighbors': 7}
Average Time to Fit (s): 0.035
Average Time to Score (s): 0.88
我们的 KNN 模型,正如预期的那样,在拟合时间上表现更好。这是因为,为了拟合数据,KNN 只需要以某种方式存储数据,以便在预测时可以轻松检索,这会在时间上造成损失。还值得一提的是一个显而易见的事实,即准确率甚至没有超过零准确率!你可能想知道为什么,如果你说“嘿,等等,KNN 不是利用欧几里得距离来做出预测吗,这可能会被非标准化数据所影响,而其他三个机器学习模型都没有这个问题”,那么你完全正确。
KNN 是一种基于距离的模型,它使用空间中相似度的度量,假设所有特征都在相同的尺度上,但我们已经知道我们的数据并不是这样的。因此,对于 KNN,我们将不得不构建一个更复杂的管道来更准确地评估其基线性能,以下代码展示了如何实现:
# bring in some familiar modules for dealing with this sort of thing
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# construct pipeline parameters based on the parameters
# for KNN on its own
knn_pipe_params = {'classifier__{}'.format(k): v for k, v in knn_params.iteritems()}
# KNN requires a standard scalar due to using Euclidean distance # as the main equation for predicting observations
knn_pipe = Pipeline([('scale', StandardScaler()), ('classifier', knn)])
# quick to fit, very slow to predict
get_best_model_and_accuracy(knn_pipe, knn_pipe_params, X, y)
print knn_pipe_params # {'classifier__n_neighbors': [1, 3, 5, 7]}
Best Accuracy: 0.8008
Best Parameters: {'classifier__n_neighbors': 7}
Average Time to Fit (s): 0.035
Average Time to Score (s): 6.723
首先要注意的是,我们修改后的代码管道,现在包括了一个StandardScalar(它通过 z 分数标准化我们的特征),至少在 null accuracy 上有所提升,但同时也严重影响了我们的预测时间,因为我们增加了一个预处理步骤。到目前为止,逻辑回归在最佳准确率和更好的整体管道时间上处于领先地位。让我们继续前进,看看我们的两个基于树的模型,先从两个模型中较简单的一个开始,即决策树,以下代码将提供帮助:
get_best_model_and_accuracy(d_tree, tree_params, X, y)
Best Accuracy: 0.820266666667
Best Parameters: {'max_depth': 3}
Average Time to Fit (s): 0.158
Average Time to Score (s): 0.002
太棒了!我们已经在新准确率上取得了领先,而且决策树在拟合和预测方面都很快。事实上,它在拟合时间上击败了逻辑回归,在预测时间上击败了 KNN。让我们通过以下代码评估随机森林来完成我们的测试:
get_best_model_and_accuracy(forest, forest_params, X, y)
Best Accuracy: 0.819566666667
Best Parameters: {'n_estimators': 50, 'max_depth': 7}
Average Time to Fit (s): 1.107
Average Time to Score (s): 0.044
比逻辑回归或 KNN 都要好,但不如决策树。让我们汇总这些结果,看看我们应该在优化时使用哪种模型:
| 模型名称 | 准确率 (%) | 拟合时间 (s) | 预测时间 (s) |
|---|---|---|---|
| 逻辑回归 | .8096 | .602 | .002 |
| KNN(缩放) | .8008 | .035 | 6.72 |
| 决策树 | .8203 | .158 | .002 |
| 随机森林 | .8196 | 1.107 | .044 |
决策树在准确率上排名第一,与逻辑回归在预测时间上并列第一,而经过缩放的 KNN 在拟合我们的数据方面速度最快。总的来说,决策树似乎是我们继续前进的最佳模型,因为它在我们的两个最重要的指标上排名第一:
-
我们肯定希望获得最佳准确率,以确保样本外预测的准确性
-
考虑到模型将被用于实时生产使用,预测时间是有用的
我们采取的方法是在选择任何特征之前选择一个模型。这不是必须的工作方式,但我们发现,在时间紧迫的情况下,这种方式通常可以节省最多的时间。就你的目的而言,我们建议你同时实验多种模型,不要将自己限制在单一模型上。
知道我们将使用决策树来完成本章的剩余部分,我们还知道两件事:
-
新的基线准确率是.8203,这是树在拟合整个数据集时获得的准确率
-
我们不再需要使用
StandardScaler,因为决策树在模型性能方面不受其影响
特征选择类型
回想一下,我们进行特征选择的目标是通过提高预测能力和减少时间成本来提升我们的机器学习能力。为了实现这一点,我们引入了两种广泛的特征选择类别:基于统计和基于模型。基于统计的特征选择将严重依赖于与我们的机器学习模型分开的统计测试,以便在我们的管道训练阶段选择特征。基于模型的选择依赖于一个预处理步骤,该步骤涉及训练一个二级机器学习模型,并使用该模型的预测能力来选择特征。
这两种类型的特征选择都试图通过从原始特征中仅选择具有最高预测能力的最佳特征来减少我们的数据集大小。我们可能可以智能地选择哪种特征选择方法最适合我们,但现实中,在这个领域工作的一个非常有效的方法是逐一研究每种方法的示例,并衡量结果管道的性能。
首先,让我们看看依赖于统计测试从数据集中选择可行特征的特性选择模块的子类。
基于统计的特征选择
统计为我们提供了相对快速和简单的方法来解释定量和定性数据。我们在前面的章节中使用了一些统计度量来获取关于我们数据的新知识和视角,特别是我们认识到均值和标准差作为度量,使我们能够计算 z 分数并缩放我们的数据。在本章中,我们将依靠两个新概念来帮助我们进行特征选择:
-
皮尔逊相关性
-
假设检验
这两种方法都被称为特征选择的单变量方法,这意味着当问题是要一次选择一个单个特征以创建更好的机器学习管道数据集时,它们既快又方便。
使用皮尔逊相关性选择特征
我们在这本书中已经讨论过相关性,但不是在特征选择的背景下。我们已经知道,我们可以通过调用以下方法在 pandas 中调用相关性计算:
credit_card_default.corr()
前面代码的输出结果是以下内容:
作为前一个表的延续,我们有:
皮尔逊相关系数(这是 pandas 的默认值)衡量列之间的线性关系。系数的值在-1 和+1 之间变化,其中 0 表示它们之间没有相关性。接近-1 或+1 的相关性表示非常强的线性关系。
值得注意的是,皮尔逊的相关性通常要求每个列都是正态分布的(我们并没有假设这一点)。我们也可以在很大程度上忽略这一要求,因为我们的数据集很大(超过 500 是阈值)。
pandas .corr() 方法为每一列与其他每一列计算皮尔逊相关系数。这个 24 列乘以 24 行的矩阵非常混乱,在过去,我们使用 热图 来尝试使信息更易于理解:
# using seaborn to generate heatmaps
import seaborn as sns
import matplotlib.style as style
# Use a clean stylizatino for our charts and graphs
style.use('fivethirtyeight')
sns.heatmap(credit_card_default.corr())
生成的 热图 将如下所示:
注意,热图 函数自动选择了与我们最相关的特征来显示。话虽如此,我们目前关注的是特征与响应变量的相关性。我们将假设一个特征与响应的相关性越强,它就越有用。任何相关性不那么强的特征对我们来说就不那么有用。
相关系数也用于确定特征交互和冗余。减少机器学习中过拟合的关键方法之一是发现并去除这些冗余。我们将在基于模型的选择方法中解决这个问题。
让我们使用以下代码来隔离特征与响应变量之间的相关性:
# just correlations between every feature and the response
credit_card_default.corr()['default payment next month']
LIMIT_BAL -0.153520
SEX -0.039961
EDUCATION 0.028006
MARRIAGE -0.024339
AGE 0.013890
PAY_0 0.324794
PAY_2 0.263551
PAY_3 0.235253
PAY_4 0.216614
PAY_5 0.204149
PAY_6 0.186866
BILL_AMT1 -0.019644
BILL_AMT2 -0.014193
BILL_AMT3 -0.014076
BILL_AMT4 -0.010156
BILL_AMT5 -0.006760
BILL_AMT6 -0.005372
PAY_AMT1 -0.072929
PAY_AMT2 -0.058579
PAY_AMT3 -0.056250
PAY_AMT4 -0.056827
PAY_AMT5 -0.055124
PAY_AMT6 -0.053183
default payment next month 1.000000
我们可以忽略最后一行,因为它表示响应变量与自身完全相关的响应变量。我们正在寻找相关系数值接近 -1 或 +1 的特征。这些是我们可能认为有用的特征。让我们使用 pandas 过滤器来隔离至少有 .2 相关性(正或负)的特征。
我们可以通过首先定义一个 pandas 掩码 来实现这一点,它将作为我们的过滤器,使用以下代码:
# filter only correlations stronger than .2 in either direction (positive or negative)
credit_card_default.corr()['default payment next month'].abs() > .2
LIMIT_BAL False
SEX False
EDUCATION False
MARRIAGE False
AGE False
PAY_0 True
PAY_2 True
PAY_3 True
PAY_4 True
PAY_5 True
PAY_6 False
BILL_AMT1 False
BILL_AMT2 False
BILL_AMT3 False
BILL_AMT4 False
BILL_AMT5 False
BILL_AMT6 False
PAY_AMT1 False
PAY_AMT2 False
PAY_AMT3 False
PAY_AMT4 False
PAY_AMT5 False
PAY_AMT6 False
default payment next month True
在前面的 pandas Series 中,每个 False 都代表一个相关值在 -0.2 到 0.2(包括)之间的特征,而 True 值对应于相关值在 0.2 或更小于 -0.2 的特征。让我们将这个掩码插入到我们的 pandas 过滤器中,使用以下代码:
# store the features
highly_correlated_features = credit_card_default.columns[credit_card_default.corr()['default payment next month'].abs() > .2]
highly_correlated_features
Index([u'PAY_0', u'PAY_2', u'PAY_3', u'PAY_4', u'PAY_5',
u'default payment next month'],
dtype='object')
变量 highly_correlated_features 应该包含与响应变量高度相关的特征;然而,我们必须去掉响应列的名称,因为将其包含在我们的机器学习管道中将是作弊:
# drop the response variable
highly_correlated_features = highly_correlated_features.drop('default payment next month')
highly_correlated_features
Index([u'PAY_0', u'PAY_2', u'PAY_3', u'PAY_4', u'PAY_5'], dtype='object')
因此,现在我们从原始数据集中提取了五个特征,这些特征旨在预测响应变量,让我们在以下代码的帮助下尝试一下:
# only include the five highly correlated features
X_subsetted = X[highly_correlated_features]
get_best_model_and_accuracy(d_tree, tree_params, X_subsetted, y)
# barely worse, but about 20x faster to fit the model
Best Accuracy: 0.819666666667
Best Parameters: {'max_depth': 3}
Average Time to Fit (s): 0.01
Average Time to Score (s): 0.002
我们的准确率肯定比要打败的准确率 .8203 差,但也要注意,拟合时间大约增加了 20 倍。我们的模型能够用只有五个特征来学习,几乎与整个数据集一样好。此外,它能够在更短的时间内学习到同样多的知识。
让我们把我们的 scikit-learn 管道重新引入,并将我们的相关性选择方法作为预处理阶段的一部分。为此,我们必须创建一个自定义转换器,它将调用我们刚刚经历的逻辑,作为一个管道就绪的类。
我们将把我们的类命名为 CustomCorrelationChooser,它必须实现拟合和转换逻辑,如下所示:
-
拟合逻辑将选择特征矩阵中高于指定阈值的列
-
转换逻辑将子集任何未来的数据集,只包括被认为重要的列
from sklearn.base import TransformerMixin, BaseEstimator
class CustomCorrelationChooser(TransformerMixin, BaseEstimator):
def __init__(self, response, cols_to_keep=[], threshold=None):
# store the response series
self.response = response
# store the threshold that we wish to keep
self.threshold = threshold
# initialize a variable that will eventually
# hold the names of the features that we wish to keep
self.cols_to_keep = cols_to_keep
def transform(self, X):
# the transform method simply selects the appropiate
# columns from the original dataset
return X[self.cols_to_keep]
def fit(self, X, *_):
# create a new dataframe that holds both features and response
df = pd.concat([X, self.response], axis=1)
# store names of columns that meet correlation threshold
self.cols_to_keep = df.columns[df.corr()[df.columns[-1]].abs() > self.threshold]
# only keep columns in X, for example, will remove response variable
self.cols_to_keep = [c for c in self.cols_to_keep if c in X.columns]
return self
让我们用以下代码来试用我们新的相关特征选择器:
# instantiate our new feature selector
ccc = CustomCorrelationChooser(threshold=.2, response=y)
ccc.fit(X)
ccc.cols_to_keep
['PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5']
我们的这个类别选择了我们之前找到的相同的五列。让我们通过在X矩阵上调用它来测试转换功能,以下代码如下:
ccc.transform(X).head()
上述代码产生以下表格作为输出:
| PAY_0 | PAY_2 | PAY_3 | PAY_4 | PAY_5 | |
|---|---|---|---|---|---|
| 0 | 2 | 2 | -1 | -1 | -2 |
| 1 | -1 | 2 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0 | 0 | 0 |
| 3 | 0 | 0 | 0 | 0 | 0 |
| 4 | -1 | 0 | -1 | 0 | 0 |
我们看到transform方法已经消除了其他列,只保留了满足我们.2相关阈值的特征。现在,让我们在以下代码的帮助下将所有这些整合到我们的管道中:
# instantiate our feature selector with the response variable set
ccc = CustomCorrelationChooser(response=y)
# make our new pipeline, including the selector
ccc_pipe = Pipeline([('correlation_select', ccc),
('classifier', d_tree)])
# make a copy of the decisino tree pipeline parameters
ccc_pipe_params = deepcopy(tree_pipe_params)
# update that dictionary with feature selector specific parameters
ccc_pipe_params.update({
'correlation_select__threshold':[0, .1, .2, .3]})
print ccc_pipe_params #{'correlation_select__threshold': [0, 0.1, 0.2, 0.3], 'classifier__max_depth': [None, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]}
# better than original (by a little, and a bit faster on
# average overall
get_best_model_and_accuracy(ccc_pipe, ccc_pipe_params, X, y)
Best Accuracy: 0.8206
Best Parameters: {'correlation_select__threshold': 0.1, 'classifier__max_depth': 5}
Average Time to Fit (s): 0.105
Average Time to Score (s): 0.003
哇!我们在特征选择的第一尝试中已经超过了我们的目标(尽管只是略微)。我们的管道显示,如果我们以0.1为阈值,我们就已经消除了足够的噪声以改善准确性,并且还减少了拟合时间(从没有选择器的 0.158 秒)。让我们看看我们的选择器决定保留哪些列:
# check the threshold of .1
ccc = CustomCorrelationChooser(threshold=0.1, response=y)
ccc.fit(X)
# check which columns were kept
ccc.cols_to_keep
['LIMIT_BAL', 'PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6']
看起来我们的选择器决定保留我们找到的五列,以及另外两列,即LIMIT_BAL和PAY_6列。太棒了!这是 scikit-learn 中自动化管道网格搜索的美丽之处。它允许我们的模型做它们最擅长的事情,并直觉到我们自己无法做到的事情。
使用假设检验进行特征选择
假设检验是统计学中的一种方法,它允许对单个特征进行更复杂的统计检验。通过假设检验进行特征选择将尝试从数据集中选择最佳特征,正如我们在自定义相关选择器中所做的那样,但这些测试更多地依赖于形式化的统计方法,并通过所谓的p 值进行解释。
假设检验是一种统计检验,用于确定在给定数据样本的情况下,我们是否可以应用对整个总体适用的某个条件。假设检验的结果告诉我们是否应该相信假设,或者拒绝它以选择另一个假设。基于来自总体的样本数据,假设检验确定是否拒绝零假设。我们通常使用p 值(一个非负的小数,其上界为 1,基于我们的显著性水平)来得出这个结论。
在特征选择的情况下,我们希望检验的假设是这样的:“这个特征与响应变量无关。” 我们希望对每个特征进行这种假设检验,并决定这些特征在预测响应变量时是否具有某种重要性。从某种意义上说,这就是我们处理相关逻辑的方式。我们基本上说,如果一个列与响应变量的相关性太弱,那么我们就说该特征无关的假设是正确的。如果相关系数足够强,那么我们就可以拒绝该特征无关的假设,转而支持一个替代假设,即该特征确实与响应变量有关。
要开始使用这个工具处理我们的数据,我们需要引入两个新的模块:SelectKBest 和 f_classif,使用以下代码:
# SelectKBest selects features according to the k highest scores of a given scoring function
from sklearn.feature_selection import SelectKBest
# This models a statistical test known as ANOVA
from sklearn.feature_selection import f_classif
# f_classif allows for negative values, not all do
# chi2 is a very common classification criteria but only allows for positive values
# regression has its own statistical tests
SelectKBest 实际上只是一个包装器,它保留了一定数量的特征,这些特征是根据某些标准排序最高的。在这种情况下,我们将使用完成假设检验的 p 值作为排序标准。
解释 p 值
p 值是介于 0 和 1 之间的十进制数,表示在假设检验下,给定的数据偶然发生的概率。简单来说,p 值越低,我们拒绝零假设的机会就越大。对于我们的目的来说,p 值越小,特征与我们的响应变量相关的可能性就越大,我们应该保留它。
对于更深入的统计检验处理,请参阅 Packt 出版的《数据科学原理》Principles of Data Science,www.packtpub.com/big-data-and-business-intelligence/principles-data-science。
从这个例子中我们可以得出一个重要的结论:f_classif 函数将对每个特征单独执行 ANOVA 测试(一种假设检验类型),并为该特征分配一个 p 值。SelectKBest 将根据那个 p 值(越低越好)对特征进行排序,并仅保留最好的 k 个(由人工输入)特征。让我们在 Python 中尝试一下。
p 值排序
让我们先实例化一个 SelectKBest 模块。我们将手动输入一个 k 值,5,这意味着我们希望只保留根据结果 p 值得出的前五个最佳特征:
# keep only the best five features according to p-values of ANOVA test
k_best = SelectKBest(f_classif, k=5)
然后,我们可以拟合并转换我们的 X 矩阵,选择我们想要的特征,就像我们之前使用自定义选择器所做的那样。
# matrix after selecting the top 5 features
k_best.fit_transform(X, y)
# 30,000 rows x 5 columns
array([[ 2, 2, -1, -1, -2],
[-1, 2, 0, 0, 0],
[ 0, 0, 0, 0, 0],
...,
[ 4, 3, 2, -1, 0],
[ 1, -1, 0, 0, 0],
[ 0, 0, 0, 0, 0]])
如果我们想直接检查 p-values 并查看哪些列被选中,我们可以深入了解选择 k_best 变量:
# get the p values of columns
k_best.pvalues_
# make a dataframe of features and p-values
# sort that dataframe by p-value
p_values = pd.DataFrame({'column': X.columns, 'p_value': k_best.pvalues_}).sort_values('p_value')
# show the top 5 features
p_values.head()
上述代码将生成以下表格作为输出:
| 列 | p 值 | |
|---|---|---|
| 5 | PAY_0 | 0.000000e+00 |
| 6 | PAY_2 | 0.000000e+00 |
| 7 | PAY_3 | 0.000000e+00 |
| 8 | PAY_4 | 1.899297e-315 |
| 9 | PAY_5 | 1.126608e-279 |
我们可以看到,我们的选择器再次选择了 PAY_X 列作为最重要的列。如果我们看一下我们的 p-value 列,我们会注意到我们的值非常小,接近于零。p-value 的一个常见阈值是 0.05,这意味着小于 0.05 的值可能被认为是显著的,根据我们的测试,这些列非常显著。我们还可以直接使用 pandas 过滤方法看到哪些列达到了 0.05 的阈值:
# features with a low p value
p_values[p_values['p_value'] < .05]
上述代码生成了以下表格作为输出:
| 列 | p_value | |
|---|---|---|
| 5 | PAY_0 | 0.000000e+00 |
| 6 | PAY_2 | 0.000000e+00 |
| 7 | PAY_3 | 0.000000e+00 |
| 8 | PAY_4 | 1.899297e-315 |
| 9 | PAY_5 | 1.126608e-279 |
| 10 | PAY_6 | 7.296740e-234 |
| 0 | LIMIT_BAL | 1.302244e-157 |
| 17 | PAY_AMT1 | 1.146488e-36 |
| 18 | PAY_AMT2 | 3.166657e-24 |
| 20 | PAY_AMT4 | 6.830942e-23 |
| 19 | PAY_AMT3 | 1.841770e-22 |
| 21 | PAY_AMT5 | 1.241345e-21 |
| 22 | PAY_AMT6 | 3.033589e-20 |
| 1 | SEX | 4.395249e-12 |
| 2 | EDUCATION | 1.225038e-06 |
| 3 | MARRIAGE | 2.485364e-05 |
| 11 | BILL_AMT1 | 6.673295e-04 |
| 12 | BILL_AMT2 | 1.395736e-02 |
| 13 | BILL_AMT3 | 1.476998e-02 |
| 4 | AGE | 1.613685e-02 |
大多数列的 p-value 都很低,但并非所有。让我们使用以下代码查看具有更高 p_value 的列:
# features with a high p value
p_values[p_values['p_value'] >= .05]
上述代码生成了以下表格作为输出:
| 列 | p_value | |
|---|---|---|
| 14 | BILL_AMT4 | 0.078556 |
| 15 | BILL_AMT5 | 0.241634 |
| 16 | BILL_AMT6 | 0.352123 |
这三个列的 p-value 非常高。让我们使用我们的 SelectKBest 在管道中查看我们是否可以通过网格搜索进入更好的机器学习管道,以下代码将有所帮助:
k_best = SelectKBest(f_classif)
# Make a new pipeline with SelectKBest
select_k_pipe = Pipeline([('k_best', k_best),
('classifier', d_tree)])
select_k_best_pipe_params = deepcopy(tree_pipe_params)
# the 'all' literally does nothing to subset
select_k_best_pipe_params.update({'k_best__k':range(1,23) + ['all']})
print select_k_best_pipe_params # {'k_best__k': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 'all'], 'classifier__max_depth': [None, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21]}
# comparable to our results with correlationchooser
get_best_model_and_accuracy(select_k_pipe, select_k_best_pipe_params, X, y)
Best Accuracy: 0.8206
Best Parameters: {'k_best__k': 7, 'classifier__max_depth': 5}
Average Time to Fit (s): 0.102
Average Time to Score (s): 0.002
我们的 SelectKBest 模块似乎达到了与我们的自定义转换器相同的准确度,但达到这个准确度要快一些!让我们看看我们的测试选择了哪些列,以下代码将有所帮助:
k_best = SelectKBest(f_classif, k=7)
# lowest 7 p values match what our custom correlationchooser chose before
# ['LIMIT_BAL', 'PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6']
p_values.head(7)
上述代码生成了以下表格作为输出:
| 列 | p_value | |
|---|---|---|
| 5 | PAY_0 | 0.000000e+00 |
| 6 | PAY_0 | 0.000000e+00 |
| 7 | PAY_0 | 0.000000e+00 |
| 8 | PAY_0 | 1.899297e-315 |
| 9 | PAY_0 | 1.126608e-279 |
| 10 | PAY_0 | 7.296740e-234 |
| 0 | LIMIT_BAL | 1.302244e-157 |
它们似乎是我们其他统计方法选择的相同列。我们的统计方法可能限制为不断为我们选择这七个列。
除了 ANOVA 之外,还有其他测试可用,例如 Chi² 以及其他回归任务,它们都包含在 scikit-learn 的文档中。有关通过单变量测试进行特征选择的更多信息,请查看 scikit-learn 的以下文档:
scikit-learn.org/stable/modules/feature_selection.html#univariate-feature-selection
在我们转向基于模型的特征选择之前,进行一次快速的合理性检查是有帮助的,以确保我们走在正确的道路上。到目前为止,我们已经看到了两种用于特征选择的统计方法,它们为我们提供了相同的七个列以实现最佳精度。但如果我们排除这七个列以外的所有列呢?我们应该预期精度会大大降低,整体流程也会变得更差,对吧?让我们确保这一点。以下代码帮助我们实现合理性检查:
# sanity check
# If we only the worst columns
the_worst_of_X = X[X.columns.drop(['LIMIT_BAL', 'PAY_0', 'PAY_2', 'PAY_3', 'PAY_4', 'PAY_5', 'PAY_6'])]
# goes to show, that selecting the wrong features will
# hurt us in predictive performance
get_best_model_and_accuracy(d_tree, tree_params, the_worst_of_X, y)
Best Accuracy: 0.783966666667
Best Parameters: {'max_depth': 5}
Average Time to Fit (s): 0.21
Average Time to Score (s): 0.002
好吧,通过选择除了这七个列以外的列,我们不仅看到了更差的精度(几乎和零精度一样差),而且平均拟合时间也更慢。有了这一点,我相信我们可以继续到我们的下一个特征选择技术子集,即基于模型的方法。
基于模型的特征选择
我们上一节讨论了使用统计方法和测试来从原始数据集中选择特征,以提高我们的机器学习流程,无论是在预测性能上还是在时间复杂度上。在这个过程中,我们亲自见证了使用特征选择的效果。
自然语言处理简要回顾
如果从本章一开始就谈论特征选择听起来很熟悉,几乎就像我们在开始相关系数和统计测试之前就已经在做这件事一样,那么,你并没有错。在第四章,特征构建中,当我们处理特征构建时,我们介绍了CountVectorizer的概念,它是 scikit-learn 中的一个模块,用于从文本列中构建特征并在机器学习流程中使用它们。
CountVectorizer有许多参数可以调整以寻找最佳流程。具体来说,有几个内置的特征选择参数:
-
max_features:这个整数设置了一个硬限制,即特征提取器可以记住的最大特征数量。被记住的特征是基于一个排名系统决定的,其中令牌的排名是该令牌在语料库中的计数。 -
min_df:这个浮点数通过规定一个规则来限制特征的数量,即一个令牌只有在语料库中以高于min_df的比率出现时才可以在数据集中出现。 -
max_df:与min_df类似,这个浮点数通过仅允许在语料库中以低于max_df设置的值严格出现的令牌来限制特征的数量。 -
stop_words:通过将其与静态令牌列表进行匹配来限制允许的令牌类型。如果发现存在于stop_words集合中的令牌,无论其是否以min_df和max_df允许的数量出现,都将忽略该词。
在上一章中,我们简要介绍了一个旨在仅基于推文中单词预测推文情感的数据集。让我们花些时间来刷新我们对如何使用这些参数的记忆。让我们通过以下代码开始引入我们的tweet数据集:
# bring in the tweet dataset
tweets = pd.read_csv('../data/twitter_sentiment.csv',
encoding='latin1')
为了刷新我们的记忆,让我们查看前五个tweets,以下代码将提供帮助:
tweets.head()
上述代码生成了以下表格作为输出:
| ItemID | Sentiment | SentimentText | |
|---|---|---|---|
| 0 | 1 | 0 | 我的好朋友 APL 真是太伤心了... |
| 1 | 2 | 0 | 我错过了新月观测... |
| 2 | 3 | 1 | omg 它已经 7:30 了 :O |
| 3 | 4 | 0 | .. Omgaga. Im sooo im gunna CRy. I'... |
| 4 | 5 | 0 | 我想我的男朋友在骗我!!! ... |
让我们创建一个特征和一个响应变量。回想一下,因为我们正在处理文本,所以我们的特征变量将仅仅是文本列,而不是通常的二维矩阵:
tweets_X, tweets_y = tweets['SentimentText'], tweets['Sentiment']
让我们设置一个管道,并使用本章中我们一直在使用的相同函数来评估它,以下代码将提供帮助:
from sklearn.feature_extraction.text import CountVectorizer
# import a naive bayes to help predict and fit a bit faster
from sklearn.naive_bayes import MultinomialNB
featurizer = CountVectorizer()
text_pipe = Pipeline([('featurizer', featurizer),
('classify', MultinomialNB())])
text_pipe_params = {'featurizer__ngram_range':[(1, 2)],
'featurizer__max_features': [5000, 10000],
'featurizer__min_df': [0., .1, .2, .3],
'featurizer__max_df': [.7, .8, .9, 1.]}
get_best_model_and_accuracy(text_pipe, text_pipe_params,
tweets_X, tweets_y)
Best Accuracy: 0.755753132845
Best Parameters: {'featurizer__min_df': 0.0, 'featurizer__ngram_range': (1, 2), 'featurizer__max_df': 0.7, 'featurizer__max_features': 10000}
Average Time to Fit (s): 5.808
Average Time to Score (s): 0.957
一个不错的分数(记住,基线准确度为.564),但我们通过在上一个章节中使用FeatureUnion模块结合TfidfVectorizer和CountVectorizer的特征来击败了这个分数。
为了尝试本章中我们看到的技术,让我们继续在一个带有CountVectorizer的管道中应用SelectKBest。让我们看看我们是否可以不依赖于内置的CountVectorizer特征选择参数,而是依赖统计测试:
# Let's try a more basic pipeline, but one that relies on SelectKBest as well
featurizer = CountVectorizer(ngram_range=(1, 2))
select_k_text_pipe = Pipeline([('featurizer', featurizer),
('select_k', SelectKBest()),
('classify', MultinomialNB())])
select_k_text_pipe_params = {'select_k__k': [1000, 5000]}
get_best_model_and_accuracy(select_k_text_pipe,
select_k_text_pipe_params,
tweets_X, tweets_y)
Best Accuracy: 0.755703127344
Best Parameters: {'select_k__k': 10000}
Average Time to Fit (s): 6.927
Average Time to Score (s): 1.448
看起来SelectKBest在文本标记上表现不佳,没有FeatureUnion,我们无法与上一章的准确度分数竞争。无论如何,对于这两个管道,值得注意的是,拟合和预测所需的时间都非常差。这是因为统计单变量方法对于非常大量的特征(如从文本向量化中获得的特征)来说并不理想。
使用机器学习来选择特征
当你处理文本时,使用CountVectorizer内置的特征选择工具是很好的;然而,我们通常处理的是已经嵌入行/列结构中的数据。我们已经看到了使用纯统计方法进行特征选择的强大之处,现在让我们看看我们如何调用机器学习的强大功能,希望做到更多。在本节中,我们将使用基于树和线性模型作为特征选择的主要机器学习模型。它们都有特征排名的概念,这在子集特征集时非常有用。
在我们继续之前,我们认为再次提到这些方法是有价值的,尽管它们在选择方法上有所不同,但它们都在尝试找到最佳特征子集以改进我们的机器学习流程。我们将深入探讨的第一种方法将涉及算法(如决策树和随机森林模型)在拟合训练数据时生成的内部重要性指标。
基于树的模型特征选择指标
当拟合决策树时,树从根节点开始,并在每个节点贪婪地选择最优分割,以优化节点纯度的某个指标。默认情况下,scikit-learn 在每一步优化基尼指标。在创建分割的过程中,模型会跟踪每个分割对整体优化目标的帮助程度。因此,基于树的模型在根据此类指标选择分割时,具有特征重要性的概念。
为了进一步说明这一点,让我们使用以下代码将决策树拟合到我们的数据中,并输出特征重要性:
# create a brand new decision tree classifier
tree = DecisionTreeClassifier()
tree.fit(X, y)
当我们的树拟合到数据后,我们可以调用feature_importances_ 属性来捕获特征相对于树拟合的重要性:
# note that we have some other features in play besides what our last two selectors decided for us
importances = pd.DataFrame({'importance': tree.feature_importances_, 'feature':X.columns}).sort_values('importance', ascending=False)
importances.head()
上述代码产生以下表格作为输出:
| 特征 | 重要性 | |
|---|---|---|
| 5 | PAY_0 | 0.161829 |
| 4 | AGE | 0.074121 |
| 11 | BILL_AMT1 | 0.064363 |
| 0 | LIMIT_BAL | 0.058788 |
| 19 | PAY_AMT3 | 0.054911 |
这个表格告诉我们,在拟合过程中最重要的特征是列PAY_0,这与我们在本章前面提到的统计模型所告诉我们的相匹配。更值得注意的是第二、第三和第五个最重要的特征,因为在使用我们的统计测试之前,它们并没有真正出现。这是一个很好的迹象,表明这种特征选择方法可能为我们带来一些新的结果。
记得,之前我们依赖于一个内置的 scikit-learn 包装器,称为 SelectKBest,根据排名函数(如 ANOVA p 值)来捕获前k个特征。我们将介绍另一种类似风格的包装器,称为SelectFromModel,它就像SelectKBest一样,将捕获前 k 个最重要的特征。然而,它将通过监听机器学习模型内部的特征重要性指标来完成,而不是统计测试的 p 值。我们将使用以下代码来定义SelectFromModel:
# similar to SelectKBest, but not with statistical tests
from sklearn.feature_selection import SelectFromModel
SelectFromModel和SelectKBest在用法上的最大区别是,SelectFromModel不接收一个整数 k,它代表要保留的特征数,而是SelectFromModel使用一个阈值进行选择,这个阈值充当被选中重要性的硬性下限。通过这种方式,本章中的基于模型的筛选器能够从保留人类输入的特征数转向依赖相对重要性,只包含管道需要的特征数。让我们如下实例化我们的类:
# instantiate a class that choses features based
# on feature importances according to the fitting phase
# of a separate decision tree classifier
select_from_model = SelectFromModel(DecisionTreeClassifier(),
threshold=.05)
让我们将SelectFromModel类拟合到我们的数据中,并调用 transform 方法来观察我们的数据在以下代码的帮助下被子集化:
selected_X = select_from_model.fit_transform(X, y)
selected_X.shape
(30000, 9)
现在我们已经了解了模块的基本机制,让我们在管道中使用它来为我们选择特征。回想一下,我们要打败的准确率是.8206,这是我们通过相关选择器和 ANOVA 测试得到的(因为它们都返回了相同的特征):
# to speed things up a bit in the future
tree_pipe_params = {'classifier__max_depth': [1, 3, 5, 7]}
from sklearn.pipeline import Pipeline
# create a SelectFromModel that is tuned by a DecisionTreeClassifier
select = SelectFromModel(DecisionTreeClassifier())
select_from_pipe = Pipeline([('select', select),
('classifier', d_tree)])
select_from_pipe_params = deepcopy(tree_pipe_params)
select_from_pipe_params.update({
'select__threshold': [.01, .05, .1, .2, .25, .3, .4, .5, .6, "mean", "median", "2.*mean"],
'select__estimator__max_depth': [None, 1, 3, 5, 7]
})
print select_from_pipe_params # {'select__threshold': [0.01, 0.05, 0.1, 'mean', 'median', '2.*mean'], 'select__estimator__max_depth': [None, 1, 3, 5, 7], 'classifier__max_depth': [1, 3, 5, 7]}
get_best_model_and_accuracy(select_from_pipe,
select_from_pipe_params,
X, y)
# not better than original
Best Accuracy: 0.820266666667
Best Parameters: {'select__threshold': 0.01, 'select__estimator__max_depth': None, 'classifier__max_depth': 3}
Average Time to Fit (s): 0.192
Average Time to Score (s): 0.002
首先要注意的是,作为阈值参数的一部分,我们可以包含一些保留词,而不是表示最小重要性的浮点数。例如,mean的阈值仅选择重要性高于平均值的特征。同样,将median作为阈值仅选择比中值更重要特征的值。我们还可以包含这些保留词的倍数,因此2.*mean将仅包含比两倍平均重要性值更重要特征的值。
让我们看看我们的基于决策树的选择器为我们选择了哪些特征。我们可以通过调用SelectFromModel中的get_support()方法来实现。它将返回一个布尔数组,每个原始特征列一个,并告诉我们它决定保留哪些特征,如下所示:
# set the optimal params to the pipeline
select_from_pipe.set_params(**{'select__threshold': 0.01,
'select__estimator__max_depth': None,
'classifier__max_depth': 3})
# fit our pipeline to our data
select_from_pipe.steps[0][1].fit(X, y)
# list the columns that the SVC selected by calling the get_support() method from SelectFromModel
X.columns[select_from_pipe.steps[0][1].get_support()]
[u'LIMIT_BAL', u'SEX', u'EDUCATION', u'MARRIAGE', u'AGE', u'PAY_0', u'PAY_2', u'PAY_3', u'PAY_6', u'BILL_AMT1', u'BILL_AMT2', u'BILL_AMT3', u'BILL_AMT4', u'BILL_AMT5', u'BILL_AMT6', u'PAY_AMT1', u'PAY_AMT2', u'PAY_AMT3', u'PAY_AMT4', u'PAY_AMT5', u'PAY_AMT6']
哇!所以树决定保留除了两个特征之外的所有特征,并且仍然和没有选择任何特征时的树一样做得好:
关于决策树及其如何使用基尼指数或熵进行拟合的更多信息,请查阅 scikit-learn 文档或其他更深入探讨此主题的文本。
我们可以继续尝试其他基于树的模型,例如 RandomForest、ExtraTreesClassifier 等,但也许我们可以通过利用非基于树的模型来做得更好。
线性模型和正则化
SelectFromModel 选择器能够处理任何在拟合后暴露 feature_importances_ 或 coef_ 属性的机器学习模型。基于树的模型暴露前者,而线性模型暴露后者。拟合后,如线性回归、逻辑回归、支持向量机等线性模型都将系数放在表示该特征斜率或该特征变化时对响应影响程度的特征之前。SelectFromModel 可以将此等同于特征重要性,并根据在拟合过程中分配给特征的系数来选择特征。
然而,在我们可以使用这些模型之前,我们必须引入一个称为 正则化 的概念,这将帮助我们选择真正最重要的特征。
正则化简介
在线性模型中,正则化是一种对学习模型施加额外约束的方法,其目标是防止过拟合并提高数据的泛化能力。这是通过向正在优化的 损失函数 中添加额外项来实现的,这意味着在拟合过程中,正则化的线性模型可能会严重减少,甚至破坏特征。有两种广泛使用的正则化方法,称为 L1 和 L2 正则化。这两种正则化技术都依赖于 L-p 范数,它对于一个向量定义为:
-
L1 正则化,也称为 lasso 正则化,使用 L1 范数,根据上述公式,可以简化为向量元素的绝对值之和,以限制系数,使其可能完全消失并变为 0。如果特征的系数降至 0,那么该特征在预测新数据观测值时将没有任何发言权,并且肯定不会被
SelectFromModel选择器选中。 -
L2 正则化,也称为 ridge 正则化,将 L2 范数作为惩罚(向量元素平方和)施加,这样系数就不能降至 0,但可以变得非常非常小。
正则化也有助于解决多重共线性问题,即在数据集中存在多个线性相关的特征。Lasso 惩罚(L1)将迫使依赖特征的系数变为 0,确保它们不会被选择模块选中,从而帮助对抗过拟合。
线性模型系数作为另一个特征重要性指标
我们可以使用 L1 和 L2 正则化来找到特征选择的最佳系数,就像我们在基于树的模型中做的那样。让我们使用逻辑回归模型作为我们的基于模型的选择器,并在 L1 和 L2 范数之间进行网格搜索:
# a new selector that uses the coefficients from a regularized logistic regression as feature importances
logistic_selector = SelectFromModel(LogisticRegression())
# make a new pipeline that uses coefficients from LogistisRegression as a feature ranker
regularization_pipe = Pipeline([('select', logistic_selector),
('classifier', tree)])
regularization_pipe_params = deepcopy(tree_pipe_params)
# try l1 regularization and l2 regularization
regularization_pipe_params.update({
'select__threshold': [.01, .05, .1, "mean", "median", "2.*mean"],
'select__estimator__penalty': ['l1', 'l2'],
})
print regularization_pipe_params # {'select__threshold': [0.01, 0.05, 0.1, 'mean', 'median', '2.*mean'], 'classifier__max_depth': [1, 3, 5, 7], 'select__estimator__penalty': ['l1', 'l2']}
get_best_model_and_accuracy(regularization_pipe,
regularization_pipe_params,
X, y)
# better than original, in fact the best so far, and much faster on the scoring side
Best Accuracy: 0.821166666667 Best Parameters: {'select__threshold': 0.01, 'classifier__max_depth': 5, 'select__estimator__penalty': 'l1'}
Average Time to Fit (s): 0.51
Average Time to Score (s): 0.001
最后!我们的准确率超过了我们的统计测试选择器。让我们看看我们的基于模型的选择器通过再次调用 SelectFromModel 的 get_support() 方法决定保留哪些特征:
# set the optimal params to the pipeline
regularization_pipe.set_params(**{'select__threshold': 0.01,
'classifier__max_depth': 5,
'select__estimator__penalty': 'l1'})
# fit our pipeline to our data
regularization_pipe.steps[0][1].fit(X, y)
# list the columns that the Logisti Regression selected by calling the get_support() method from SelectFromModel
X.columns[regularization_pipe.steps[0][1].get_support()]
[u'SEX', u'EDUCATION', u'MARRIAGE', u'PAY_0', u'PAY_2', u'PAY_3', u'PAY_4', u'PAY_5']
太神奇了!我们基于逻辑回归的选取器保留了大部分PAY_X列,同时还能推断出人的性别、教育和婚姻状况将在预测中起到作用。让我们继续我们的冒险之旅,使用一个额外的模型和我们的SelectFromModel选取器模块,一个支持向量机分类器。
如果你不太熟悉支持向量机,它们是尝试在空间中绘制线性边界以分离二元标签的分类模型。这些线性边界被称为支持向量。目前,逻辑回归和支持向量分类器之间最重要的区别是,SVC 通常更适合优化系数以最大化二元分类任务的准确率,而逻辑回归在建模二元分类任务的概率属性方面更出色。让我们像对决策树和逻辑回归所做的那样,从 scikit-learn 实现一个线性 SVC 模型,看看它的表现如何,以下代码如下:
# SVC is a linear model that uses linear supports to
# seperate classes in euclidean space
# This model can only work for binary classification tasks
from sklearn.svm import LinearSVC
# Using a support vector classifier to get coefficients
svc_selector = SelectFromModel(LinearSVC())
svc_pipe = Pipeline([('select', svc_selector),
('classifier', tree)])
svc_pipe_params = deepcopy(tree_pipe_params)
svc_pipe_params.update({
'select__threshold': [.01, .05, .1, "mean", "median", "2.*mean"],
'select__estimator__penalty': ['l1', 'l2'],
'select__estimator__loss': ['squared_hinge', 'hinge'],
'select__estimator__dual': [True, False]
})
print svc_pipe_params # 'select__estimator__loss': ['squared_hinge', 'hinge'], 'select__threshold': [0.01, 0.05, 0.1, 'mean', 'median', '2.*mean'], 'select__estimator__penalty': ['l1', 'l2'], 'classifier__max_depth': [1, 3, 5, 7], 'select__estimator__dual': [True, False]}
get_best_model_and_accuracy(svc_pipe,
svc_pipe_params,
X, y)
# better than original, in fact the best so far, and much faster on the scoring side
Best Accuracy: 0.821233333333
Best Parameters: {'select__estimator__loss': 'squared_hinge', 'select__threshold': 0.01, 'select__estimator__penalty': 'l1', 'classifier__max_depth': 5, 'select__estimator__dual': False}
Average Time to Fit (s): 0.989
Average Time to Score (s): 0.001
太棒了!我们迄今为止得到的最佳准确率。我们可以看到拟合时间有所下降,但如果我们对此可以接受,将迄今为止的最佳准确率与出色的快速预测时间相结合,我们就拥有了出色的机器学习管道;一个利用支持向量分类中正则化力量的管道,将显著特征输入到决策树分类器中。让我们看看我们的选取器选择了哪些特征,以给我们迄今为止的最佳准确率:
# set the optimal params to the pipeline
svc_pipe.set_params(**{'select__estimator__loss': 'squared_hinge',
'select__threshold': 0.01,
'select__estimator__penalty': 'l1',
'classifier__max_depth': 5,
'select__estimator__dual': False})
# fit our pipeline to our data
svc_pipe.steps[0][1].fit(X, y)
# list the columns that the SVC selected by calling the get_support() method from SelectFromModel
X.columns[svc_pipe.steps[0][1].get_support()]
[u'SEX', u'EDUCATION', u'MARRIAGE', u'PAY_0', u'PAY_2', u'PAY_3', u'PAY_5']
这些特征与我们的逻辑回归得到的特征之间的唯一区别是PAY_4列。但我们可以看到,即使删除单个列也能影响我们整个管道的性能。
选择正确的特征选择方法
到目前为止,你可能会觉得本章中的信息有点令人不知所措。我们介绍了几种执行特征选择的方法,一些基于纯统计,一些基于二级机器学习模型的输出。自然地,你会想知道如何决定哪种特征选择方法最适合你的数据。理论上,如果你能够尝试多种选项,就像我们在本章中所做的那样,那将是理想的,但我们理解这可能并不切实际。以下是一些你可以遵循的经验法则,当你试图确定哪个特征选择模块更有可能提供更好的结果时:
-
如果你的特征大多是分类的,你应该首先尝试实现一个带有 Chi²排名器的
SelectKBest或基于树的模型选取器。 -
如果你的特征大多是定量(就像我们的一样),使用基于模型的线性模型作为选取器,并依赖于相关性,往往会产生更好的结果,正如本章所展示的。
-
如果您正在解决一个二元分类问题,使用支持向量分类(SVC)模型以及
SelectFromModel选择器可能会非常合适,因为 SVC 试图找到优化二元分类任务的系数。 -
一点点的探索性数据分析(EDA)在手动特征选择中可以发挥很大的作用。在数据来源领域拥有领域知识的重要性不容小觑。
话虽如此,这些(内容)仅作为指导方针。作为一名数据科学家,最终决定保留哪些特征以优化您选择的指标的是您自己。本文中提供的方法旨在帮助您发现被噪声和多重共线性隐藏的特征的潜在力量。
摘要
在本章中,我们学习了关于选择特征子集的方法,以提高我们的机器学习管道在预测能力和时间复杂度方面的性能。
我们选择的数据集特征数量相对较少。然而,如果从大量特征(超过一百个)中选择,那么本章中的方法可能会变得过于繁琐。在本章中,当我们尝试优化一个CountVectorizer管道时,对每个特征进行单变量测试所需的时间不仅天文数字般的长;而且,仅仅因为巧合,我们特征中的多重共线性风险会更大。
在下一章中,我们将介绍我们可以应用于数据矩阵的纯数学变换,以减轻处理大量特征或少量难以解释的特征的麻烦。我们将开始处理一些不同于之前所见的数据集,例如图像数据、主题建模数据等。