其它章节内容请见机器学习之PyTorch和Scikit-Learn
将数据集划分为训练集和测试集
我们在第1章 赋予计算机学习数据的能力和第3章 使用Scikit-Learn的机器学习分类器之旅中简单地介绍了将数据集划分为训练集和测试集的概念。在测试集中比较预测标签和真实标签可以看成是发布上线前对模型的无偏差性能评估。本节中,我们会准备一个新的数据集,葡萄酒数据集。在预处理完数据集后,我们会探讨不同的特征选择技术来对数据集降维。
葡萄酒数据集是UCI机器学习库(archive.ics.uci.edu/ml/datasets…)中的另一个开源数据集,包含178个葡萄酒样本和13个描述其化学属性的特征。
获取葡萄酒数据集
可以在本书代码库中找到一份葡萄酒数据集(以及本书中使用的其它数据集)的拷贝,以妨读者离线操作或是UCI服务器上archive.ics.uci.edu/ml/machine-…临时断网。比如我们从本地目录读者葡萄酒数据集,只需将如下行:
df = pd.read_csv( 'https://archive.ics.uci.edu/ml/' 'machine-learning-databases/wine/wine.data', header=None )
替换为:
df = pd.read_csv( 'your/local/path/to/wine.data', header=None )
使用pandas库,我们会直接从UCI机器学习库读取开源葡萄酒数据集:
>>> df_wine = pd.read_csv('https://archive.ics.uci.edu/'
... 'ml/machine-learning-databases/'
... 'wine/wine.data', header=None)
>>> df_wine.columns = ['Class label', 'Alcohol',
... 'Malic acid', 'Ash',
... 'Alcalinity of ash', 'Magnesium',
... 'Total phenols', 'Flavanoids',
... 'Nonflavanoid phenols',
... 'Proanthocyanins',
... 'Color intensity', 'Hue',
... 'OD280/OD315 of diluted wines',
... 'Proline']
>>> print('Class labels', np.unique(df_wine['Class label']))
Class labels [1 2 3]
>>> df_wine.head()
葡萄酒数据集中的13个特征,描述了178个样本,如下表所示:
图4.4:葡萄酒数据集样本
样本属于三个不同的类1
、2
和3
之一,对应不同酒庄获取的意大利同一区域三种类型的葡萄,参见数据集的描述(archive.ics.uci.edu/ml/machine-…)。
随机将数据集分割为测试集和训练集的便捷方式是使用scikit-learn中model_selection
子模块的train_test_split
函数:
>>> from sklearn.model_selection import train_test_split
>>> X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values
>>> X_train, X_test, y_train, y_test =
... train_test_split(X, y,
... test_size=0.3,
... random_state=0,
... stratify=y)
首先我们将特征列1-13的NumPy数组形式赋值给变量X
并将第一列的类标签赋值给变量y
。然后我们使用train_test_split
函数来随机将X
和y
划分为训练集和测试集。
通过设置test_size=0.3
,我们将30%的葡萄酒样本赋值给X_test
和y_test
,剩下的70%样本分别赋值给X_train
和y_train
。对参数stratify
提供类标签y
保障了训练集和测试集具有同样类比例的原始数据。
选择合适的比例将数据集分为训练集和测试集
如果将数据集分为训练集和测试集,请铭记我们要保留利于学习算法的有价值信息。因此我们不希望对测试集分割过多信息。但测试集越小,对泛化误差的评估也就越不精准。将数据集分割为测试集和训练集就是要做出权衡。在实操中,最常见的是按初始数据集大小分割为60:40, 70:30或80:20。但对于大型数据集,90:10或 99:1也常见且合适。例如,数据集包含了100,000条以上的训练集,在测试集中保留10,000个样本来进行很的泛化表现评估也没有问题。更多的信息和描述请见作者的论文《机器学习的模型评估、模型选择和算法选择》中的第一部分,可通过arxiv.org/pdf/1811.12…免费获取。我们在第6章 学习模型评估和超参数调优的最佳实践中也会再次详细讨论到模型评估。
此外,在模型训练和评估完成后并不会丢弃所分配的测试数据,一般会对整个数据集重新训练一个分类器,因为这样可以提升模型的预测表现。虽然通常推荐这一方法,但在数据集较小及测试数据集包含异常值/离群点时会产生更差的泛化表现。并且在对整个数据集重新拟合模型后,我们就没有剩下任何独立数据用于评估表现了。
使特征处于同一量级
特征缩放是预处理流水线中很容易忘记的重要步骤。决策树和随机森林是机器学习算法中无需担心特征缩放少有的两种算法。这两种算法与量级无关。但大部分机器学习和优化算法在特征处于同一量级时表现更佳,我们在第2章 为分类训练简单机器学习算法中实现梯度下降优化算法时已经见证过。
特征缩放的重要性可通过一个简单示例来讲解。假定有两个特征,一个特征的量级为1到10,另一个特征的量级为1到100,000。
回想第2章Adaline中的均方差函数,可以理解算法主要会根据第二个特征较大的误差优化权重。另一个示例是使用欧式距离度量的k最近邻(KNN) 算法:计算的样本间距离会由第二个特征轴主导。
有两种常用方法可将特征变成同一量级:归一化(normalization)和标准化(standardization)。这两个词在不同领域中会混用,其含义需要从上下文中获知。大多数时候,归一化指将特征重新缩放到[0, 1]范围内,是最大最小值缩放(min-max scaling)的一种特例。要对数据进行归一化,我们只需对每个特征列应用最大最小缩放,比如x(i)的新值,xnorm(i)x_{norm}^{(i)},可通过如下方式计算:
这里x(i)是具体的样本,xmin是特征列中的最小值,xmax是最大值。
最大最小值缩放已在scikit-learn中实现,可通过如下方式使用:
>>> from sklearn.preprocessing import MinMaxScaler
>>> mms = MinMaxScaler()
>>> X_train_norm = mms.fit_transform(X_train)
>>> X_test_norm = mms.transform(X_test)
虽然通过最大最小值缩放进行归一化是一种常见技术,在需要有界区间内的值时很有用,但标准化对很多机器学习算法更为实用,尤其是对梯度下降这类优化算法。原因是很多线性模型,比如第3章中的逻辑回归和SVM,将权重初始化为0或接近0的随机值。使用标准化,我们将特征列的中心点放在均值0,标准差为1,这样特征列拥有与标准正态分布相同的参数(零均值和单位方差),这让学习权重更为简单。但应该强调标准化不会改变分布的形状,也不会将非正态分布数据转换为正态分布数据。除了将数据缩放为零均值和单位方差,标准差还保留异常值相关有用信息,使算法对它们的敏感度低于最大最小缩放,后者将数据缩放为限定范围值。
标准化可通过如下等式表示:
这里μxmu_x是指定样本列的样本均值,而σxsigma_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:标准化和最大最小值归一化的对比
可以通过如下示例代码手动执行表中所示的标准化和归一化:
>>> ex = np.array([0, 1, 2, 3, 4, 5])
>>> print('standardized:', (ex - ex.mean()) / ex.std())
standardized: [-1.46385011 -0.87831007 -0.29277002 0.29277002
0.87831007 1.46385011]
>>> print('normalized:', (ex - ex.min()) / (ex.max() - ex.min()))
normalized: [ 0. 0.2 0.4 0.6 0.8 1. ]
类似于MinMaxScaler
类,scikit-learn 也实现了标准化的类:
>>> from sklearn.preprocessing import StandardScaler
>>> stdsc = StandardScaler()
>>> X_train_std = stdsc.fit_transform(X_train)
>>> X_test_std = stdsc.transform(X_test)
要再次强调我们只使用StandardScaler
类对训练数据进行了一次拟合,并使用这些参数来转换测试集或其它新数据点。
scikit-learn中还有其它用于特征缩放的高阶方法,如RobustScaler
。在处理包含离群数据的小数据集时推荐使用RobustScaler
。同样,如果对数据集应用的机器学习算法偏向过拟合,RobustScaler
也会是个好选择。RobustScaler
对每个特征列独立操作,删除中间值并根据数据集的第一个和第三个四分位(即25%和75%处)缩放数据,这样极值和离群值就不那么显著了。感兴趣的读者可以在scikit-learn的官方文档中阅读RobustScaler
的详细介绍:scikit-learn.org/stable/modu…。
选择有意义的特征
如果发现模型在训练集上的表现远优于测试集,这很可能会出现过拟合。在第3章 使用Scikit-Learn的机器学习分类器之旅中讨论过,过拟合意味着模型对训练数据集的拟合过度紧密,但对新数据的泛化性不好,我们说这种模型出现了高方差(high variance)。过拟合的原因是模型对给定的训练集过于复杂。通常降低泛化误差的方法有:
- 采集更多的训练数据
- 通过正则化引入对复杂性的惩罚
- 选择更少参数的更简单模型
- 降低数据的维度
采集更多数据很多时候是不可行的。在第6章 学习模型评估和超参数调优的最佳实践中,我们会学到有用的技术检测更多的训练数据是否有益。在下面的小节中,我们会学习一些通过正则化减少过拟合、通过特征选择降维的常见方式,这会产生要求更少参数拟合数据的更简单模型。然后在第5章 通过降维压缩数据中,我们会学习到其它特征提取技术。
使用L1和L2正则化惩罚模型复杂度
读者可能还刻在第3章中讲到L2正则化是一种通过惩罚大个体权重降低模型复杂度的方法。我们定义了权重向量w的L2范式平方如下:
另一种降低模型复杂度的方法是L1正则化:
这里我们只是将权重的平方求和替换为了权重绝对值的求和。与L2正则化不同,L1正则化通常会产生稀疏特征向量,并且大部分特征权重为0。在实操中如果高维数据集中的多个特征不相关稀疏性是很有用的,尤其是在训练样本中有更多不相关维度时。从这个角度看,L1正则化可以理解为一种特征选择的技术。
L2正则化的几何解释
上一节中已经提到,L2正则化对损失函数添加了惩罚项,可有效形成与非正则化损失函数训练的模型相比不那么极端的权重值。
为更好理解L1正则化是如何促进稀疏性的,我们先回顾下正则化的几何解释。我们先以两个权重系数w1和w2绘制凸损失函数的轮廓。
这里我们会考虑第2章中用于Adaline的均方差(MSE)损失函数,它计算真实值y与预测类标签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正则为什么会导致稀疏解的数学细节不在本书讨论范畴内。如果读者对此感兴趣,可阅读T revor Hastie, Robert Tibshirani和Jerome Friedman所著《统计学习基础》(施普林格科学与商业媒体,2009年)的第3.4节中关于L2对比L1正则化的精彩讲解。
对于支持L1正则化的scikit-learn正则化模型,可以简单地设置penalty
参数为'l1'
来获取稀疏解:
>>> from sklearn.linear_model import LogisticRegression
>>> LogisticRegression(penalty='l1',
... solver='liblinear',
... multi_class='ovr')
注意我们还需要选择不同的优化算法(如solver='liblinear'
),因为'lbfgs'
当前不支持L1正则化损失优化。将L1正则化逻辑回归应用于标准化的葡萄酒数据,会产生如下的稀疏解:
>>> lr = LogisticRegression(penalty='l1',
... C=1.0,
... solver='liblinear',
... multi_class='ovr')
>>> # Note that C=1.0 is the default. You can increase
>>> # or decrease it to make the regularization effect
>>> # stronger or weaker, respectively.
>>> lr.fit(X_train_std, y_train)
>>> print('Training accuracy:', lr.score(X_train_std, y_train))
Training accuracy: 1.0
>>> print('Test accuracy:', lr.score(X_test_std, y_test))
Test accuracy: 1.0
训练和测试精度(都是100%)表示我们的模型对两个数据集都非常完美。在通过lr.intercept_
属性访问截距项时,可以看到数组返回三个值:
>>> lr.intercept_
array([-1.26317363, -1.21537306, -2.37111954])
因为我们通过一对剩余(OvR)方法对多类数据集拟合LogisticRegression
对象,第一个截距属于拟合类1对类2和类3的模型,第二个值是拟合类2对类1和类3的模型截距,第二个值是拟合类3对类1和类2的模型截距:
>>> lr.coef_
array([[ 1.24647953, 0.18050894, 0.74540443, -1.16301108,
0. ,0. , 1.16243821, 0. ,
0. , 0. , 0. , 0.55620267,
2.50890638],
[-1.53919461, -0.38562247, -0.99565934, 0.36390047,
-0.05892612, 0. , 0.66710883, 0. ,
0. , -1.9318798 , 1.23775092, 0. ,
-2.23280039],
[ 0.13557571, 0.16848763, 0.35710712, 0. ,
0. , 0. , -2.43804744, 0. ,
0. , 1.56388787, -0.81881015, -0.49217022,
0. ]])
我们通过lr.coef
属性访问的权重数组包含三行权重系数,每个类一个权重向量。每行包含13个权重,每个权重乘上13维葡萄酒数据集中各自的特征来计算新输入值:
访问scikit-learn评估器的偏置单元和权重参数
在scikit-learn中,
intercept_
对应偏置单元,coef_
对应值wj。
前面提到L1正则化是特征选择的方法,所以我们只训练了对数据集中不相关特性健壮的模型。但严格来说,前例中的权重向量不一定是稀疏的,因为其中的非零值多于零值。不过我们可以通过进一步提升正则化强度来强制稀疏性(更多的零值),也即为C
参数选择更小的值。
在本章中有关正则化的最后一个例子中,我们会变化正则化强度并绘制正则化路径,不同的正则化强度使用不同特征的权重系数:
>>> import matplotlib.pyplot as plt
>>> fig = plt.figure()
>>> ax = plt.subplot(111)
>>> colors = ['blue', 'green', 'red', 'cyan',
... 'magenta', 'yellow', 'black',
... 'pink', 'lightgreen', 'lightblue',
... 'gray', 'indigo', 'orange']
>>> weights, params = [], []
>>> for c in np.arange(-4., 6.):
... lr = LogisticRegression(penalty='l1', C=10.**c,
... solver='liblinear',
... multi_class='ovr', random_state=0)
... lr.fit(X_train_std, y_train)
... weights.append(lr.coef_[1])
... params.append(10**c)
>>> weights = np.array(weights)
>>> for column, color in zip(range(weights.shape[1]), colors):
... plt.plot(params, weights[:, column],
... label=df_wine.columns[column + 1],
... color=color)
>>> plt.axhline(0, color='black', linestyle='--', linewidth=3)
>>> plt.xlim([10**(-5), 10**5])
>>> plt.ylabel('Weight coefficient')
>>> plt.xlabel('C (inverse regularization strength)')
>>> plt.xscale('log')
>>> plt.legend(loc='upper left')
>>> ax.legend(loc='upper center',
... bbox_to_anchor=(1.38, 1.03),
... ncol=1, fancybox=True)
>>> plt.show()
生成的图为让我们进一步了解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的定义,可以将该算法总结为以下四步:
- 通过k = d初始化算法,其中d是完整特征空间Xd的维度。
- 确定最大化标准的x–:x– = argmax J(Xk – x),其中x∈Xkxin X_k。
- 从特征集中删除特征x–:Xk–1 = Xk – x–; k = k – 1。
- 如果k等于所需特征数终止,否则回到步骤2。
序列特征算法相关资源
可在以下书中找到多种序列特征算法的详细评估:大规模特征选择技术的比较研究 F. Ferri, P. Pudil, M. Hatef及 J. Kittler,第403-413页, 1994。
为练习我们的编码能力及具备实现自己算法的能力,我们从头使用Python实现:
from sklearn.base import clone
from itertools import combinations
import numpy as np
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
class SBS:
def __init__(self, estimator, k_features,
scoring=accuracy_score,
test_size=0.25, random_state=1):
self.scoring = scoring
self.estimator = clone(estimator)
self.k_features = k_features
self.test_size = test_size
self.random_state = random_state
def fit(self, X, y):
X_train, X_test, y_train, y_test =
train_test_split(X, y, test_size=self.test_size,
random_state=self.random_state)
dim = X_train.shape[1]
self.indices_ = tuple(range(dim))
self.subsets_ = [self.indices_]
score = self._calc_score(X_train, y_train,
X_test, y_test, self.indices_)
self.scores_ = [score]
while dim > self.k_features:
scores = []
subsets = []
for p in combinations(self.indices_, r=dim - 1):
score = self._calc_score(X_train, y_train,
X_test, y_test, p)
scores.append(score)
subsets.append(p)
best = np.argmax(scores)
self.indices_ = subsets[best]
self.subsets_.append(self.indices_)
dim -= 1
self.scores_.append(scores[best])
self.k_score_ = self.scores_[-1]
return self
def transform(self, X):
return X[:, self.indices_]
def _calc_score(self, X_train, y_train, X_test, y_test, indices):
self.estimator.fit(X_train[:, indices], y_train)
y_pred = self.estimator.predict(X_test[:, indices])
score = self.scoring(y_test, y_pred)
return score
在以上的实现中,我们定义了k_features
参数来指定希望返回的特征数。默认,我们使用scikit-learn中的accuracy_score
来评估特征子集上的模型(用于分类的估计器)表现。
在fit
方法的while
循环内部,由itertools.combination
函数创建的特征子集进行了评估,减少至特征子集为所需维度。在每次迭代中,将最佳子集根据内部创建的测试数据集X_test
的准确度打分收入列表self.scores_
中。稍后我们会使用这些分数评估结果。最终特征子集的列索引赋值给self.indices_
,可通过transform
方法使用它返回带已选特征列的新数组。注意除了在fit
方法内部显式地计算标准,我们只是删除了不在最佳性能特征子集中的特征。
下面逐步使用scikit-learn中KNN分类呃呃实现SBS:
>>> import matplotlib.pyplot as plt
>>> from sklearn.neighbors import KNeighborsClassifier
>>> knn = KNeighborsClassifier(n_neighbors=5)
>>> sbs = SBS(knn, k_features=1)
>>> sbs.fit(X_train_std, y_train)
虽然SBS实现已经在fit
函数中将数据集分割成测试集和训练集,我们仍需将训练集X_train
喂给算法。然后SBS fit
会新建用于测试(验证)和训练的训练子集,这也是为什么测试集也被称为验证数据集。这一方法可防止原始测试集变成训练集的一部分。
SBS算法收集每个阶段的最佳特征子集,所以我们进入实现中更有意思的部分,并绘制KNN分类器对验证数据集所计算的分类准确度。代码如下:
>>> k_feat = [len(k) for k in sbs.subsets_]
>>> plt.plot(k_feat, sbs.scores_, marker='o')
>>> plt.ylim([0.7, 1.02])
>>> plt.ylabel('Accuracy')
>>> plt.xlabel('Number of features')
>>> plt.grid()
>>> plt.tight_layout()
>>> plt.show()
通过图4.9可以看出,KNN分类器对验证集的准确度在我们减少了特征数后有了改善,很可能是由于我们在第3章中在KNN算法上下文所讨论的维数灾难的下降。同时,我们可以在下图中看出分类器对{3, 7, 8, 9, 10, 11, 12}实现了100%的准确度:
图4.9:特征数对模型准确度的影响
为满足我们自己的好奇心,我们来看下对验证集产生这种好表现的最小特征子集(k=3)长什么样:
>>> k3 = list(sbs.subsets_[10])
>>> print(df_wine.columns[1:][k3])
Index(['Alcohol', 'Malic acid', 'OD280/OD315 of diluted wines'], dtype='object')
使用上述代码,我们从sbs.subsets_
属性中的第11个位置获取了三特征子集的列索引 ,通过葡萄酒DataFrame
的列索引返回对应的特征名。
接下来,我们对原始测试集评估这个KNN分类器的表现:
>>> knn.fit(X_train_std, y_train)
>>> print('Training accuracy:', knn.score(X_train_std, y_train))
Training accuracy: 0.967741935484
>>> print('Test accuracy:', knn.score(X_test_std, y_test))
Test accuracy: 0.962962962963
在以上代码中,我们使用了完整的特征集,对训练集得到了大约97%的准确度,对测试集得到了大约96%的准确度,表示我们的模型对新数据泛化的很好。下面我们使用所选的三特征子集来看看KNN的表现如何:
>>> knn.fit(X_train_std[:, k3], y_train)
>>> print('Training accuracy:',
... knn.score(X_train_std[:, k3], y_train))
Training accuracy: 0.951612903226
>>> print('Test accuracy:',
... knn.score(X_test_std[:, k3], y_test))
Test accuracy: 0.925925925926
在使用少于四分之一的原始葡萄酒数据集特征时,测试集的预测准确度稍有下降。这可能表示这三个特征没有提供比原始数据集更少的判别信息。但我们不要忘了葡萄酒数据集是一个小数据集,很容易受随机性影响,也就是我们分割训练集和测试集的方式以及如何将测试集进一步分割成训练集和验证集。
虽然我们并没有通过减少特征数提升KNN模型的表现,但减少了数据集的大小,这对真实世界中涉及昂贵数据采集步骤的应用非常有用。同时通过减少特征数,我们得到了更简单的模型,也就更容易解释。
scikit-learn中的特征选择算法
我们可以在
mlxtend
Python包中找到多个与上面所实现的简易SBS相关的多种序列特征选择实现,位于rasbt.github.io/mlxtend/use…。虽然我们的mlxtend
实现有很多装饰,但我们与scikit-learn团队合作实现了一个用户友好的简化版本,已集成到v0.24中。其用法和行为与本章中所实现的SBS
代码非常相近。如果读者想了解更多内容,请参见文档:scikit-learn.org/stable/modu…。
在scikit-learn中有很多特征选择算法。包括基于特征权重的递归后向消元(backward elimination),基于树按重要性选择特征的方法,和单变量统计检验(univariate statistical tests)。对各种特征算法的综合讨论不在本书范畴内,可在scikit-learn.org/stable/modu…上找到带示例的总结。
使用随机森森评估特征重要性
上一节中,我们学习了如何使用L1正则化借助逻辑回归清除不相关特征,以及如何使用SBS进行特征选择并应用于KNN算法。选择数据集中相关特征的另一个有用的方法是使用随机森林,在第3章中曾介绍过这种集成技术。使用随机森林,我们可以随森森中所有决策树计算的平均杂度下降度量特征重要性,不论数据是线性可分割还是不可分割。scikit-learn中实现的随机森林为方便使用已经为我们采集了特征重要性的值,这样我们在拟合了RandomForestClassifier
后可以通过feature_importances_
属性访问这些值。通过执行以下代码,我们对葡萄酒数据集训练了一个500棵树的森森并通过它们各个的重要性度量对13个特征排名,第3章中我们讲过基于树的模型不需要使用标准化或归一化特征:
>>> from sklearn.ensemble import RandomForestClassifier
>>> feat_labels = df_wine.columns[1:]
>>> forest = RandomForestClassifier(n_estimators=500,
... random_state=1)
>>> forest.fit(X_train, y_train)
>>> importances = forest.feature_importances_
>>> indices = np.argsort(importances)[::-1]
>>> for f in range(X_train.shape[1]):
... print("%2d) %-*s %f" % (f + 1, 30,
... feat_labels[indices[f]],
... importances[indices[f]]))
>>> plt.title('Feature importance')
>>> plt.bar(range(X_train.shape[1]),
... importances[indices],
... align='center')
>>> plt.xticks(range(X_train.shape[1]),
... feat_labels[indices], rotation=90)
>>> plt.xlim([-1, X_train.shape[1]])
>>> plt.tight_layout()
>>> plt.show()
1) Proline 0.185453
2) Flavanoids 0.174751
3) Color intensity 0.143920
4) OD280/OD315 of diluted wines 0.136162
5) Alcohol 0.118529
6) Hue 0.058739
7) Total phenols 0.050872
8) Magnesium 0.031357
9) Malic acid 0.025648
10) Proanthocyanins 0.025570
11) Alcalinity of ash 0.022366
12) Nonflavanoid phenols 0.013354
13) Ash 0.013279
执行这段代码后,我们创建了一张图,其中按相关重要性对葡萄酒数据集中的不同特征进行了排名,注意特征重要性的值做了归一化,这样其总和为1.0:
图4.10:基于随机森林的葡萄酒数据集特征重要性
根据500棵决策树的平均杂度下降,可以总结出脯氨酸(proline)和类黄酮(flavonoid)水平、颜色强度、OD280/OD315衍射及酒精浓度是数据集中最具判别度的特征。有趣的是,图中排名较高中的两个特征也处于前面小节中所实现的SBS算法的三特征子集中(酒精浓度和稀释葡萄酒的OD280/OD315)。
但在可解释性方面,随机森林技术有一个重要的陷阱值得讲一下。如果两个或多个特征高度关联,一个特征可能会排名很高,而其它特征的信息可能不会完全捕获。如果只对模型的预测结果感觉兴趣而不关心对特征重要性值的解释就不太需要担心这个问题。
来到本节有关特征重要性和随机森林的结尾,有必要说明scikit-learn还实现了SelectFromModel
对象,它在模型拟合后根据用户指定的阈值选取特征,如果希望将RandomForestClassifier
用作特征选择器及scikit-learn Pipeline
对象的中间步骤会非常有用,这样可以将各预处理步骤与估计器连接,在第6章 学习模型评估和超参数调优的最佳实践中会学习到。例如,可以使用如下代码将threshold
设置为0.1
来使用数据集降为5个最重要的特征:
>>> from sklearn.feature_selection import SelectFromModel
>>> sfm = SelectFromModel(forest, threshold=0.1, prefit=True)
>>> X_selected = sfm.transform(X_train)
>>> print('Number of features that meet this threshold',
... 'criterion:', X_selected.shape[1])
Number of features that meet this threshold criterion: 5
>>> for f in range(X_selected.shape[1]):
... print("%2d) %-*s %f" % (f + 1, 30,
... feat_labels[indices[f]],
... importances[indices[f]]))
1) Proline 0.185453
2) Flavanoids 0.174751
3) Color intensity 0.143920
4) OD280/OD315 of diluted wines 0.136162
5) Alcohol 0.118529
小结
本章中我们学习了一些有用的技术确保可正确处理缺失数据。在将数据喂给机器学习算法之前,我们还要保证正确地编码分类变量,在本章中,我们还学习了如何将有序特征和标称特征与整数形式相映射。
此外,我们简单地讨论了L1正则化,可通过降低模型复杂度来帮助我们避免过拟合。作为一种删除不相关特征的替代方法,我们使用了序列特征选择算法从数据集中选取有意义的特征。
下一章中,我们会学习另一个有用的降维技术:特征提取。它可以将特征压缩到更低维子空间上,而不是像特征选择中那样完全删除特征。