Alan Hou的个人博客

第4章 构建优秀的训练数据集 – 数据预处理

其它章节内容请见机器学习之PyTorch和Scikit-Learn

数据质量及所包含的有用信息量是决定机器学习算法能学到多好的关键因素。因此,在将数据集喂给机器学习算法前对其进行检查和预处理绝对很重要。本章中,我们会讨论一些基本数据预处理技术,有助于我们构建很好的机器学习模型。

本章将要讨论的内容有:

处理缺失数据

真实世界中训练样本因各种原因缺失一个或多个值并不罕见。比如数据收集过程中可能会有错误,某些度量可能不可用或是某些字段在调研时被留空了。通常缺失值是数据表中的空白或是占位符字符串,如表示not a number的NaN或是NULL(通常用于表示关联数据库中的未知值)。可惜大部分计算工具都无法处理这种缺失值或是在忽略时产生一些不可预测的结果。 因此,在进一步分析前处理这些缺失值就极为重要了。

本节中,我们会通过一些实用技术通过删除数据集中的条目或通过其它训练样本及特征替换缺失值来处理缺失值。

识别表数据中的缺失值

在讨论处理缺失值的一些技术前,我们先通过CSV(comma-separated values)文件创建一个示例DataFrame以更好地掌握这一问题:

使用上面的代码,我们通过read_csv函数将CSV格式的数据计入pandas的DataFrame中,注意两个缺失单元格被替换成了NaN。以上示例代码中的StringIO函数只是为了演示。它让我们可以将赋值给csv_data的字符串像硬盘上的常规CSV文件一样读入到pandas的DataFrame中。

对于更大的DataFrame,手动查找缺失值会很费力,这时可使用isnull方法返回一个带布尔值的DataFrame,包含数值时单元格为False,而数据缺失时为True。使用sum方法,我们返回每列缺失值的数量,如下:

这样我们可以计算出每列缺失值的数量,在接下来的小节中,我们会学习处理缺失数据的不同策略。

处理pandas的DataFrame的方便数据

虽然原来开发的scikit-learn只处理NumPy的数组,有时使用pandas的DataFrame预处理数据更为方便。如今大部分的scikit-learn函数都支持DataFrame对象作为入参,但因为在scikit-learn API中对NumPy数组的处理更为成熟,推荐尽可能使用NumPy。注意在将数据喂入scikit-learn估计器之前我们总是可以通过values属性访问DataFrame底层的NumPy数组:

删除带缺失值的训练样本或特征

处理缺失值最简单的一咱方式是直接从数据集中删除对应的特殊(列)或训练样本(行),带缺失值的行可通过dropna方法进行删除:

类似地,我们可以通过将axis参数设置为1来删除任意行中至少有一个NaN的列:

dropna方法还支持另外几个趁手的参数:

虽然删除缺失值看上去是方便的方法,但也有一些缺点:比如,最终可能删除过多样本,这样就不可能进行可靠的分析。或是删除了过多的特征列,这样会存在丢失分类器用于区分类的有价值信息的风险。下一节中,我们会学习处理缺失值的一种最常用的替代方法:插值技术。

替换缺失值

通常删除训练样本或去除整个特征列并不可行,因为可能会丢失太多有价值的数据。这时,我们可以使用不同的插值技术来通过数据集中的其它训练样本计算缺失值。其中一种最常见的插值技术称为均值替换法(mean imputation),只需将缺失值替换为整个特征列的平均值。一种简便的实现方式是使用scikit-learn中的SimpleImputer类,代码如下:

这里,我们将每个NaN值替换为对应的平均值,通过各个特征列分别计算。strategy的其它选项有medianmost_frequent,后者是使用最常出现的值替换缺失值。这对于替换类特征值很有用,比如,存储红、绿、蓝等颜色值编码的特征列。我们会在本章稍后碰到这种数据的案例。

此外,替换缺失值甚至更方便的方式是使用pandas的fillna方法,提供一个替换方法作为其参数。比如,使用pandas,我们可以通过如下命令直接从DataFrame对象中获取相同的替换均值:

图4.1:使用均值替换数据中的缺失值

缺失值的其它替换方法

其它替换技术,包含基于k最近邻使用最近邻替换缺失特征的KNNImputer,推荐阅读scikit-learn的替换文档https://scikit-learn.org/stable/modules/impute.html

理解scikit-learn估计器API

上一节中,我们使用了scikit-learn中的SimpleImputer类来替换数据集中的缺失值。SimpleImputer类是scikit-learn中的变换器(transformer)API的一部分,它用于实现与数据变换相关的Python类。(请注意不要混淆scikit-learn中的变换器API与自然语言处理领域的transformer架构,后者会在第16章 Transformers-通过注意力机制改进自然语言处理中详细讲解。)这些估计器中的两个基本方法是fittransformfit方法用于通过训练数据学习参数,transform方法使用这些参数来变换数据。所有待变换的数组需要与用于拟合模型的数组具有相同数量的特征。

图4.2演示了scikit-learn的变换器实例如何拟合训练数据、用于变换训练集及新的测试数据集:

图4.2:使用scikit-learn API进行数据变换

第3章 使用Scikit-Learn的机器学习分类器之旅中使用的分类器属于scikit-learn中的估计器(estimator),其API在概念上与scikit-learn的变换器API非常相近。估计器有一个predict方法,但也可以有transform方法,读者在本章稍后会了解到。读者可能还记得,我们还使用了fit方法来在训练分类估计器时学习模型的参数。但在监督学习任务中,我们还提供了拟合模型的类标签,可用于通过predict方法预测新的未打标签的数据,如图4.3所示:

图4.3:使用scikit-learn API预测分类器等模型

处理分类数据

至此,如所处理的都是数值。但在现实的数据集包含一个或多个分类特征列也很常见。本节中,我们会利用简单而有效的案例来学习如何用数学计算库处理这种类型的数据。

在讨论分类数据时,我们需要进一步区分有序(ordinal)特征和标称(nominal)特征。有序特征可理解为能够进行排序的分类值。比如,T恤衫的尺寸就是一个有序特征,因为我们可以定义出排序:XL > L > M。相反,标称特征并没有排序,继续使用前面的例子,可以把T恤衫的颜色看成是标称特征,因为说红色大于蓝色是讲不通的。

使用pandas编码分类数据

在探讨处理这种分类数据的技术之前,我们先新建一个DataFrame来描述这一问题:

可以从以上输出看出,新创建的DataFrame包含一个标称特征(color)、一个有序特征(size)和一个数值特征(price) 列。类标签(假设我们创建了一个用于监督学习任务的数据集)存储于最后一列。本书中讨论的分类学习算法不使用分类标签中的有序信息。

映射有序特征

为确保学习算法能正确解释有序特征,我们需要将分类字符串值转化成整数。不幸的是没有现成的函数可以自动获取我们的size特征标签的正确排序,所以我们需要手动定义一个映射。在如下的简单示例中,我们假设知道特征间的数值差,如XL = L + 1 = M + 2:

如果稍后我们希望将整数值变换加原始的字符串形态,只需要定义一个反向映射字典,inv_size_mapping = {v: k for k, v in size_mapping.items()},可用于pandas map方法执行变换的特征列,类似于我们此前所使用的size_mapping字典。可以这样使用:

编码类标签

很多机器学习库要将类标签要编码为整数值。虽然scikit-learn中大部分分类估计器会在内部将类标签转化为整数,以整型数组提供类标签避免技术问题被视为一种良好实践。要编码类标签,我们可以使用类似前面讨论的有序特征映射的方法。我们要记住类标签不是有序的,对具体字符串标签赋哪个整数都没有问题。因此我们可以简单地枚举类标签,从0开始:

接着,我们可以使用映射字典来将类标签变换为整数:

我们可以像下面这样翻转映射字典中的键值对,来将转化的标签映射回原始字符串形式:

相应地,在scikit-learn中有一个方便的LabelEncoder类直接实现了这一功能:

注意fit_transform方法只是一个分别调用fittransform的快捷方式,我们可以使用inverse_transform方法来将整型类标签变换回原始字符串形式:

 对标称特征执行独热编码

在之前的映射有序特征一节中,我们使用了一个简单的字典映射方法将有序的size特征转化为整数。因scikit-learn的分类估计器将类标签看作不带排序的分类数据(标称),我们使用LabelEncoder来将字符串标签编码为整数。可以使用类似的方法变换数据集中的标称color列,如下:

执行以上代码后,NumPy数组X的第一列存储着新的color值,按如下进行编码:

如果到此为止将数组喂给分类器,就会犯处理分类数据最常见的一个错误。读者能发现问题在哪吗?如果颜色值没有固定的顺序,普通的分类模型,比如前面章节中讲解的,会假定green大于bluered大于green。虽然这一假设是错误的,分类器仍会产生有用的结果。但这些结果不是最优的。

解决这一问题的常用技术称为独热编码(one-hot encoding)。该方法背后的思想是问为标称特征列中的每个独立值新建一个虚拟特征。此处我们将color特征转化为3个特征:bluegreenred。然后用二进制值表示具体样本的颜色:比如blue样本可编码为blue=1green=0red=0。我们可以使用scikit-learn的preprocessing模块中的OneHotEncoder来执行这一变换:

注意我们只对一列应用了OneHotEncoder(X[:, 0].reshape(-1, 1)),以避免数组中的另两列也受到修改。如果希望有选择性地变换数组中的多个特征,可以使用ColumnTransformer,它接收一个(name, transformer, column(s))元组列表如下:

在以上示例代码中,我们通过passthrough参数指定了只想修改第一列而不动另外两列。

通过独热编码创建虚拟特征更方便的方式是使用pandas中实现的get_dummies方法。应用于DataFrameget_dummies方法只会转化字符串列而保持其它列不变:

我们在使用独热编码数据集时,需要铭记它带来了多重共线性,对于某些方法会是一个问题(比如需要求逆的矩阵)。如果特征高度相关联,矩阵在计算上就很难求逆,这会导致数值不稳定的预估。为减少变量间的关联,我们可以删除独热编码数组中的一个特征列。但删除一个特征列后我们不会丢失重要信息,比如在删除color_blue列后,因为存在color_green=0color_red=0特征信息仍保留,它表示观察的结果必定是blue

如果使用get_dummies函数,可以通过对drop_first参数传递True来去除第一列,如以下代码所示:

为通过OneHotEncoder删除冗余列,我们需要设置drop='first'categories='auto'如下:

标称数据的其它编码模式

虽然独热编码是编码无序分类变量最常见的方式,但还存在一些其它方法。有些技术对于处理具有高基数(大量独特分类)的分类特殊时非常有用。举例如下:

  • 二进制编码:产生多个类似独热编码的二进制特征,但需要更少的特征列:即K – 1变为log2(K),其中K是唯一分类数。在二进制编码中,数字首先转化为二进制形式,然后每个二进制位置会形成一个新特征列。
  • 计数或频次编码,将每个分类的标签替换为其在训练集中出现的次数或频次。

这些方法,以及其它分类编码模式,位于与scikit-learn兼容的category_encoders库中:https://contrib.scikit-learn.org/category_encoders/

虽然这些方法不能保证在模型表现上优于独热编码,但我们可以把分类编码模式看成是提升模型表现的又一个“超参数”。

可选:编码序数特征

如果不确定序数特征分类间的差别或是两个未定义的序数值之间的差别,也可以使用0/1值阈值编码来对它们进行编码。比如,我们可以将值为MLXL的特征size分割为两个新特征x > Mx > L。思考原DataFrame

可以使用pandas的DataFrameapply方法来编码自定义lambda表达式,通过阈值方式来编码这些变量:

将数据集划分为训练集和测试集

我们在第1章 赋予计算机学习数据的能力第3章 使用Scikit-Learn的机器学习分类器之旅中简单地介绍了将数据集划分为训练集和测试集的概念。在测试集中比较预测标签和真实标签可以看成是发布上线前对模型的无偏差性能评估。本节中,我们会准备一个新的数据集,葡萄酒数据集。在预处理完数据集后,我们会探讨不同的特征选择技术来对数据集降维。

葡萄酒数据集是UCI机器学习库(https://archive.ics.uci.edu/ml/datasets/Wine)中的另一个开源数据集,包含178个葡萄酒样本和13个描述其化学属性的特征。

获取葡萄酒数据集

可以在本书代码库中找到一份葡萄酒数据集(以及本书中使用的其它数据集)的拷贝,以防读者离线操作或是UCI服务器上https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data临时断网。比如我们从本地目录读取葡萄酒数据集,只需将如下行:

替换为:

使用pandas库,我们会直接从UCI机器学习库读取开源葡萄酒数据集:

葡萄酒数据集中的13个特征,描述了178个样本,如下表所示:

图4.4:葡萄酒数据集样本

样本属于三个不同的类123之一,对应不同酒庄获取的意大利同一区域三种类型的葡萄,参见数据集的描述(https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.names)。

随机将数据集分割为测试集和训练集的便捷方式是使用scikit-learn中model_selection子模块的train_test_split函数:

首先我们将特征列1-13的NumPy数组形式赋值给变量X并将第一列的类标签赋值给变量y。然后我们使用train_test_split函数来随机将Xy划分为训练集和测试集。

通过设置test_size=0.3,我们将30%的葡萄酒样本赋值给X_testy_test,剩下的70%样本分别赋值给X_trainy_train。对参数stratify提供类标签y保障了训练集和测试集具有同样类比例的原始数据。

选择合适的比例将数据集分为训练集和测试集

如果将数据集分为训练集和测试集,请铭记我们要保留利于学习算法的有价值信息。因此我们不希望对测试集分割过多信息。但测试集越小,对泛化误差的评估也就越不精准。将数据集分割为测试集和训练集就是要做出权衡。在实操中,最常见的是按初始数据集大小分割为60:40, 70:30或80:20。但对于大型数据集,90:10或 99:1也常见且合适。例如,数据集包含了100,000条以上的训练集,在测试集中保留10,000个样本来进行很的泛化表现评估也没有问题。更多的信息和描述请见作者的论文《机器学习的模型评估、模型选择和算法选择》中的第一部分,可通过https://arxiv.org/pdf/1811.12808.pdf免费获取。我们在第6章 学习模型评估和超参数调优的最佳实践中也会再次详细讨论到模型评估。

此外,在模型训练和评估完成后并不会丢弃所分配的测试数据,一般会对整个数据集重新训练一个分类器,因为这样可以提升模型的预测表现。虽然通常推荐这一方法,但在数据集较小及测试数据集包含异常值/离群点时会产生更差的泛化表现。并且在对整个数据集重新拟合模型后,我们就没有剩下任何独立数据用于评估表现了。

使特征处于同一量级

特征缩放是预处理流水线中很容易忘记的重要步骤。决策树和随机森林是机器学习算法中无需担心特征缩放少有的两种算法。这两种算法与量级无关。但大部分机器学习和优化算法在特征处于同一量级时表现更佳,我们在第2章 为分类训练简单机器学习算法中实现梯度下降优化算法时已经见证过。

特征缩放的重要性可通过一个简单示例来讲解。假定有两个特征,一个特征的量级为1到10,另一个特征的量级为1到100,000。

回想第2章Adaline中的均方差函数,可以理解算法主要会根据第二个特征较大的误差优化权重。另一个示例是使用欧式距离度量的k最近邻(KNN) 算法:计算的样本间距离会由第二个特征轴主导。

有两种常用方法可将特征变成同一量级:归一化normalization)和标准化standardization)。这两个词在不同领域中会混用,其含义需要从上下文中获知。大多数时候,归一化指将特征重新缩放到[0, 1]范围内,是最大最小值缩放(min-max scaling)的一种特例。要对数据进行归一化,我们只需对每个特征列应用最大最小缩放,比如x(i)的新值,$x_{norm}^{(i)}$,可通过如下方式计算:

这里x(i)是具体的样本,xmin是特征列中的最小值,xmax是最大值。

最大最小值缩放已在scikit-learn中实现,可通过如下方式使用:

虽然通过最大最小值缩放进行归一化是一种常见技术,在需要有界区间内的值时很有用,但标准化对很多机器学习算法更为实用,尤其是对梯度下降这类优化算法。原因是很多线性模型,比如第3章中的逻辑回归和SVM,将权重初始化为0或接近0的随机值。使用标准化,我们将特征列的中心点放在均值0,标准差为1,这样特征列拥有与标准正态分布相同的参数(零均值和单位方差),这让学习权重更为简单。但应该强调标准化不会改变分布的形状,也不会将非正态分布数据变换为正态分布数据。除了将数据缩放为零均值和单位方差,标准差还保留异常值相关有用信息,使算法对它们的敏感度低于最大最小缩放,后者将数据缩放为限定范围值。

标准化可通过如下等式表示:

这里$\mu_x$是指定样本列的样本均值,而$\sigma_x$是对应的标准差。

下表在一个包含数字0到5的简单样本数据集上展示了两种常见特征缩放技术,即标准化和归一化的区别:

输入值

标准化

最大最小值归一化

0.0

-1.46385

0.0

1.0

-0.87831

0.2

2.0

-0.29277

0.4

3.0

0.29277

0.6

4.0

0.87831

0.8

5.0

1.46385

1.0

表4.1:标准化和最大最小值归一化的对比

可以通过如下示例代码手动执行表中所示的标准化和归一化:

类似于MinMaxScaler类,scikit-learn 也实现了标准化的类:

要再次强调我们只使用StandardScaler类对训练数据进行了一次拟合,并使用这些参数来变换测试集或其它新数据点。

scikit-learn中还有其它用于特征缩放的高阶方法,如RobustScaler。在处理包含离群数据的小数据集时推荐使用RobustScaler。同样,如果对数据集应用的机器学习算法偏向过拟合,RobustScaler也会是个好选择。RobustScaler对每个特征列独立操作,删除中间值并根据数据集的第一个和第三个四分位(即25%和75%处)缩放数据,这样极值和离群值就不那么显著了。感兴趣的读者可以在scikit-learn的官方文档中阅读RobustScaler的详细介绍:https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.RobustScaler.html

选择有意义的特征

如果发现模型在训练集上的表现远优于测试集,这很可能会出现过拟合。在第3章 使用Scikit-Learn的机器学习分类器之旅中讨论过,过拟合意味着模型对训练数据集的拟合过度紧密,但对新数据的泛化性不好,我们说这种模型出现了高方差high variance)。过拟合的原因是模型对给定的训练集过于复杂。通常降低泛化误差的方法有:

采集更多数据很多时候是不可行的。在第6章 学习模型评估和超参数调优的最佳实践中,我们会学到有用的技术检测更多的训练数据是否有益。在下面的小节中,我们会学习一些通过正则化减少过拟合、通过特征选择降维的常见方式,这会产生要求更少参数拟合数据的更简单模型。然后在第5章 通过降维压缩数据中,我们会学习到其它特征提取技术。

使用L1和L2正则化惩罚模型复杂度

读者可能还刻在第3章中讲到L2正则化是一种通过惩罚大个体权重降低模型复杂度的方法。我们定义了权重向量w的L2范式平方如下:

另一种降低模型复杂度的方法是L1正则化

这里我们只是将权重的平方求和替换为了权重绝对值的求和。与L2正则化不同,L1正则化通常会产生稀疏特征向量,并且大部分特征权重为0。在实操中如果高维数据集中的多个特征不相关稀疏性是很有用的,尤其是在训练样本中有更多不相关维度时。从这个角度看,L1正则化可以理解为一种特征选择的技术。

L2正则化的几何解释

上一节中已经提到,L2正则化对损失函数添加了惩罚项,可有效形成与非正则化损失函数训练的模型相比不那么极端的权重值。

为更好理解L1正则化是如何促进稀疏性的,我们先回顾下正则化的几何解释。我们先以两个权重系数w1w2绘制凸损失函数的轮廓。

这里我们会考虑第2章中用于Adaline的均方差(MSE)损失函数,它计算真实值y与预测类标签$\hat{y}$之间的平方距离,通过训练集N个样本进行平均。因为均方差是球状的,要比逻辑回归损失函数要更容易画,但概念上没有差别。我们的目标是找到最小化训练数据损失函数的权重系数组合,如图4.5所示(椭圆的中心点):

图4.5:最小化均方差损失函数

可以把正则化看成是对损失函数添加惩罚项得到更小的权重,换句话说,我们惩罚大权重。因此通过正则化参数$\lambda$增加正则化强度,我们将权重收缩向0并降低模型对训练数据的依赖。我们通过下面的L2惩罚项图来演示这一概念:

图4.6:对损失函数应用L2正则化

平方L2正则化项通过带阴影的球表示。这里的权重系数不能超过正则化预算,权重系数的组合不能处于阴影区域之外。而另一方面,我们还希望最小化损失函数。受惩罚约束,我们尽力选择的是L2球形与未惩罚的损失函数相关部分。正则化参数$\lambda$的值越大,损失的增长越快,这会导致L2球越小。例如,如果我增加正则化参数趋近无穷大,权重系统会变为0,由L2球的中心表示。要总结样本的主要信息,我们的目标是最小化未惩罚损失和损失项之和,可理解为在没有足够训练数据拟合模型时添加偏置及偏好更简单的模型来降低方差。

L1正则化的稀疏解

下面我们来讨论L1正则化和稀疏性。L1正则化的主要概念与前面小节讨论的类似。但因为L1惩罚是权重系数绝对值之和(L2项为平方值),可通过图4.7中的菱形预算表示:

图4.7:对损失函数应用L1正则化

在上图中,我们可以看到损失函数的外围在w1 = 0处与L1菱形相交。因L1正则化系统的轮廓为尖角,很有可能最优点(也即损失函数椭圆与L1菱形边的交点)位于坐标轴上,这会鼓励稀疏性。

L1正则化与稀疏性

L1正则为什么会导致稀疏解的数学细节不在本书讨论范畴内。如果读者对此感兴趣,可阅读Trevor Hastie, Robert TibshiraniJerome Friedman所著《统计学习基础》(施普林格科学与商业媒体,2009年)的第3.4节中关于L2对比L1正则化的精彩讲解。

对于支持L1正则化的scikit-learn正则化模型,可以简单地设置penalty参数为'l1'来获取稀疏解:

注意我们还需要选择不同的优化算法(如solver='liblinear'),因为'lbfgs'当前不支持L1正则化损失优化。将L1正则化逻辑回归应用于标准化的葡萄酒数据,会产生如下的稀疏解:

训练和测试精度(都是100%)表示我们的模型对两个数据集都非常完美。在通过lr.intercept_属性访问截距项时,可以看到数组返回三个值:

因为我们通过一对剩余(OvR)方法对多类数据集拟合LogisticRegression对象,第一个截距属于拟合类1对类2和类3的模型,第二个值是拟合类2对类1和类3的模型截距,第二个值是拟合类3对类1和类2的模型截距:

我们通过lr.coef属性访问的权重数组包含三行权重系数,每个类一个权重向量。每行包含13个权重,每个权重乘上13维葡萄酒数据集中各自的特征来计算新输入值:

访问scikit-learn评估器的偏置单元和权重参数

在scikit-learn中,intercept_对应偏置单元,coef_对应值wj

前面提到L1正则化是特征选择的方法,所以我们只训练了对数据集中不相关特性健壮的模型。但严格来说,前例中的权重向量不一定是稀疏的,因为其中的非零值多于零值。不过我们可以通过进一步提升正则化强度来强制稀疏性(更多的零值),也即为C参数选择更小的值。

在本章中有关正则化的最后一个例子中,我们会变化正则化强度并绘制正则化路径,不同的正则化强度使用不同特征的权重系数:

生成的图为让我们进一步了解L1正则化的行为。可以看到如果使用强正则化参数(C < 0.01)惩罚模型所有的特征权重都将为0,C与正则化参数$\lambda$相反:

图4.8:正则化强度参数C的值的影响

序列特征选择算法

减少模型复杂度及避免过拟合的另一种方式是通过特征选择降维,这对于非正则化模型尤其有用。有两种主要的降维技术:特征选择(feature selection)和特征抽取(feature extraction)。通过特征选择,我们选择原始特征的一个子集,而在特征抽取中,我们通过构建新特征子空间来获取特征信息。

本节,我们会学习特征选择算法的一个经典系列。下一章,第5章 通过降维压缩数据,我们会学习各种特征提取技术,将数据集压缩为一个低维特征子空间。

序列特征算法是一个贪婪搜索算法系列,用于将初始d-维特征空间降为k-维特征子空间,其中k<d.。特征选择算法背后的动机是自动选择与问题最相关的特征子集,以提升计算效率,或通过删除不相关特征或噪声来降低泛化误差,这对于不支持正则化的算法非常有用。

经典的序列特征选择算法是序列后向选择(SBS),旨在通过分类器的最小性能衰减降低初始特征子空间的维度,提升计算效率。在某些场景中,在模型遭受过拟合时SBS甚至能提升模型的预测能力。

贪婪搜索算法

贪婪算法对组合搜索问题的每个阶段做出本地最优选择,通常会产生问题的次优解,与之对应的是穷举搜索算法,对所有可能的组合进行评估,保证会找到最优解。但在实操中,穷举搜索在算力上通常不可行,贪婪算法则是一种复杂度更低、计算效率更高的方案。

SBS算法背后的思想非常简单:SBS从全部特征子集序列删除特征,直至新特征子空间包含所需的特征数。要决定在各阶段删除哪个特征,我们需要定义一个我信希望最小化的判别函数J

通过判别函数计算的标准可以只是删除具体特征前后的性能差值。然后每个阶段删除的特征可定义为最大化这一标准的特征;或者更简单,每个阶段我们移除在删除后产生最小性能损失的特征。根据前述对SBS的定义,可以将该算法总结为以下四步:

  1. 通过k = d初始化算法,其中d是完整特征空间Xd的维度。
  2. 确定最大化标准的xx = argmax J(Xk – x),其中$x\in X_k$。
  3. 从特征集中删除特征xXk–1 = Xk – xk = k – 1。
  4. 如果k等于所需特征数终止,否则回到步骤2。

序列特征算法相关资源

可在以下书中找到多种序列特征算法的详细评估:大规模特征选择技术的比较研究 F. FerriP. PudilM. Hatef及J. Kittler,第403-413页, 1994。

为练习我们的编码能力及具备实现自己算法的能力,我们从头使用Python实现:

在以上的实现中,我们定义了k_features参数来指定希望返回的特征数。默认,我们使用scikit-learn中的accuracy_score来评估特征子集上的模型(用于分类的估计器)表现。

fit方法的while循环内部,由itertools.combination函数创建的特征子集进行了评估,减少至特征子集为所需维度。在每次迭代中,将最佳子集根据内部创建的测试数据集X_test的准确度打分收入列表self.scores_中。稍后我们会使用这些分数评估结果。最终特征子集的列索引赋值给self.indices_,可通过transform方法使用它返回带已选特征列的新数组。注意除了在fit方法内部显式地计算标准,我们只是删除了不在最佳性能特征子集中的特征。

下面逐步使用scikit-learn中KNN分类呃呃实现SBS:

虽然SBS实现已经在fit函数中将数据集分割成测试集和训练集,我们仍需将训练集X_train喂给算法。然后SBS fit会新建用于测试(验证)和训练的训练子集,这也是为什么测试集也被称为验证数据集。这一方法可防止原始测试集变成训练集的一部分。

SBS算法收集每个阶段的最佳特征子集,所以我们进入实现中更有意思的部分,并绘制KNN分类器对验证数据集所计算的分类准确度。代码如下:

通过图4.9可以看出,KNN分类器对验证集的准确度在我们减少了特征数后有了改善,很可能是由于我们在第3章中在KNN算法上下文所讨论的维数灾难的下降。同时,我们可以在下图中看出分类器对{3, 7, 8, 9, 10, 11, 12}实现了100%的准确度:

图4.9:特征数对模型准确度的影响

为满足我们自己的好奇心,我们来看下对验证集产生这种好表现的最小特征子集(k=3)长什么样:

使用上述代码,我们从sbs.subsets_属性中的第11个位置获取了三特征子集的列索引 ,通过葡萄酒DataFrame的列索引返回对应的特征名。

接下来,我们对原始测试集评估这个KNN分类器的表现:

在以上代码中,我们使用了完整的特征集,对训练集得到了大约97%的准确度,对测试集得到了大约96%的准确度,表示我们的模型对新数据泛化的很好。下面我们使用所选的三特征子集来看看KNN的表现如何:

在使用少于四分之一的原始葡萄酒数据集特征时,测试集的预测准确度稍有下降。这可能表示这三个特征没有提供比原始数据集更少的判别信息。但我们不要忘了葡萄酒数据集是一个小数据集,很容易受随机性影响,也就是我们分割训练集和测试集的方式以及如何将测试集进一步分割成训练集和验证集。

虽然我们并没有通过减少特征数提升KNN模型的表现,但减少了数据集的大小,这对真实世界中涉及昂贵数据采集步骤的应用非常有用。同时通过减少特征数,我们得到了更简单的模型,也就更容易解释。

scikit-learn中的特征选择算法

我们可以在mlxtend Python包中找到多个与上面所实现的简易SBS相关的多种序列特征选择实现,位于http://rasbt.github.io/mlxtend/user_guide/feature_selection/SequentialFeatureSelector/。虽然我们的mlxtend实现有很多装饰,但我们与scikit-learn团队合作实现了一个用户友好的简化版本,已集成到v0.24中。其用法和行为与本章中所实现的SBS代码非常相近。如果读者想了解更多内容,请参见文档:https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.SequentialFeatureSelector.html

在scikit-learn中有很多特征选择算法。包括基于特征权重的递归后向消元(backward elimination),基于树按重要性选择特征的方法,和单变量统计检验(univariate statistical tests)。对各种特征算法的综合讨论不在本书范畴内,可在http://scikit-learn.org/stable/modules/feature_selection.html上找到带示例的总结。

使用随机森森评估特征重要性

上一节中,我们学习了如何使用L1正则化借助逻辑回归清除不相关特征,以及如何使用SBS进行特征选择并应用于KNN算法。选择数据集中相关特征的另一个有用的方法是使用随机森林,在第3章中曾介绍过这种集成技术。使用随机森林,我们可以随森森中所有决策树计算的平均杂度下降度量特征重要性,不论数据是线性可分割还是不可分割。scikit-learn中实现的随机森林为方便使用已经为我们采集了特征重要性的值,这样我们在拟合了RandomForestClassifier后可以通过feature_importances_属性访问这些值。通过执行以下代码,我们对葡萄酒数据集训练了一个500棵树的森森并通过它们各个的重要性度量对13个特征排名,第3章中我们讲过基于树的模型不需要使用标准化或归一化特征:

执行这段代码后,我们创建了一张图,其中按相关重要性对葡萄酒数据集中的不同特征进行了排名,注意特征重要性的值做了归一化,这样其总和为1.0:

图4.10:基于随机森林的葡萄酒数据集特征重要性

根据500棵决策树的平均杂度下降,可以总结出脯氨酸(proline)和类黄酮(flavonoid)水平、颜色强度、OD280/OD315衍射及酒精浓度是数据集中最具判别度的特征。有趣的是,图中排名较高中的两个特征也处于前面小节中所实现的SBS算法的三特征子集中(酒精浓度和稀释葡萄酒的OD280/OD315)。

但在可解释性方面,随机森林技术有一个重要的陷阱值得讲一下。如果两个或多个特征高度关联,一个特征可能会排名很高,而其它特征的信息可能不会完全捕获。如果只对模型的预测结果感觉兴趣而不关心对特征重要性值的解释就不太需要担心这个问题。

来到本节有关特征重要性和随机森林的结尾,有必要说明scikit-learn还实现了SelectFromModel对象,它在模型拟合后根据用户指定的阈值选取特征,如果希望将RandomForestClassifier用作特征选择器及scikit-learn Pipeline对象的中间步骤会非常有用,这样可以将各预处理步骤与估计器连接,在第6章 学习模型评估和超参数调优的最佳实践中会学习到。例如,可以使用如下代码将threshold设置为0.1来使用数据集降为5个最重要的特征:

小结

本章中我们学习了一些有用的技术确保可正确处理缺失数据。在将数据喂给机器学习算法之前,我们还要保证正确地编码分类变量,在本章中,我们还学习了如何将有序特征和标称特征与整数形式相映射。

此外,我们简单地讨论了L1正则化,可通过降低模型复杂度来帮助我们避免过拟合。作为一种删除不相关特征的替代方法,我们使用了序列特征选择算法从数据集中选取有意义的特征。

下一章中,我们会学习另一个有用的降维技术:特征提取。它可以将特征压缩到更低维子空间上,而不是像特征选择中那样完全删除特征。

退出移动版