本节将介绍特征工程的一些常见示例:表示分类数据的特征、表示文本的特征和表示图像的特征。另外,还会介绍提高模型复杂度的衍生特征和处理缺失数据的填充方法。这个过程通常被称为向量化,因为它把任意格式的数据转换成具有良好特性的向量形式。
1、分类数据
一种常见的非数值数据类型是分类数据。例如,浏览房屋数据的时候,除了看到“房价”(price)和“面积”(rooms)之类的数值特征,还会有“地点”(neighborhood)信息,数据可能像这样:
data = [
{'price': 850000, 'rooms': 4, 'neighborhood': 'Queen Anne'},
{'price': 700000, 'rooms': 3, 'neighborhood': 'Fremont'},
{'price': 650000, 'rooms': 3, 'neighborhood': 'Wallingford'},
{'price': 600000, 'rooms': 2, 'neighborhood': 'Fremont'}
]
你可能会把分类特征用映射关系编码成整数:
{'Queen Anne': 1, 'Fremont': 2, 'Wallingford': 3};
在Scikit-Learn 中这么做并不是一个好办法:这个程序包的所有模块都有一个基本假设,那就是数值特征可以反映代数量(algebraic quantities)。因此,这样映射编码可能会让人觉得存在
甚至还有
这显然是没有意义的。
面对这种情况,常用的解决方法是独热编码。
它可以有效增加额外的列,让0 和1 出现在对应的列分别表示每个分类值有或无。当你的数据是像上面那样的字典列表时,用Scikit-Learn 的DictVectorizer 类就可以实现:
from sklearn.feature_extraction import DictVectorizer
vec = DictVectorizer(sparse=False, dtype=int)
vec.fit_transform(data)
neighborhood 字段转换成三列来表示三个地点标签,每一行中用1 所在的列对应一个地点。当这些分类特征编码之后,你就可以和之前一样拟合Scikit-Learn 模型了:
如果要看每一列的含义,可以用下面的代码查看特征名称
vec.get_feature_names()
如果你的分类特征有许多枚举值,那么数据集的维度就会急剧增加。然而,由于被编码的数据中有许多0,因此用稀疏矩阵表示会非常高效:
vec = DictVectorizer(sparse=True, dtype=int)
vec.fit_transform(data)
在拟合和评估模型时,Scikit-Learn 的许多(并非所有)评估器都支持稀疏矩阵输入。sklearn.preprocessing.OneHotEncoder 和sklearn.feature_extraction.FeatureHasher 是Scikit-Learn 另外两个为分类特征编码的工具。
2、文本特征
另一种常见的特征工程需求是将文本转换成一组数值。例如,绝大多数社交媒体数据的自动化采集,都是依靠将文本编码成数字的技术手段。数据采集最简单的编码方法之一就是单词统计。
sample = ['problem of evil',
'evil queen',
'horizon problem']
面对单词统计的数据向量化问题时,可以创建一个列来表示单词“problem”、单词“evil”和单词“horizon”等。虽然手动做也可以,但是用Scikit-Learn 的CountVectorizer 更是可以轻松实现:
from sklearn.feature_extraction.text import CountVectorizer
vec = CountVectorizer()
X = vec.fit_transform(sample)
X
结果是一个稀疏矩阵,里面记录了每个短语中每个单词的出现次数。用带列标签的DataFrame 来表示这个稀疏矩阵
import pandas as pd
pd.DataFrame(X.toarray(), columns=vec.get_feature_names())
不过这种统计方法也有一些问题:原始的单词统计会让一些常用词聚集太高的权重,在分类算法中这样并不合理。解决这个问题的方法就是通过TF–IDF(term frequency–inversedocument frequency,词频逆文档频率),通过单词在文档中出现的频率来衡量其权重
from sklearn.feature_extraction.text import TfidfVectorizer
vec = TfidfVectorizer()
X = vec.fit_transform(sample)
pd.DataFrame(X.toarray(), columns=vec.get_feature_names())
3、图像特征
机器学习还有一种常见需求,那就是对图像进行编码。我们在处理手写数字图像时使用的方法,也是最简单的图像编码方法:用像素表示图像。
但是在其他类型的任务中,这类方法可能不太合适。虽然完整地介绍图像特征的提取技术超出了本章的范围,但是你可以在Scikit-Image 项目(http://scikit-image.org)中找到许多标准方法的高品质实现。关于同时使用Scikit-Learn 和Scikit-Image 的示例,请参见应用 人脸识别管道。
4、衍生特征
还有一种有用的特征是输入特征经过数学变换衍生出来的新特征。
将一个线性回归转换成多项式回归时,并不是通过改变模型来实现,而是通过改变输入数据!这种处理方式有时被称为基函数回归(basis function regression)。
例如,下面的数据显然不能用一条直线描述
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
x = np.array([1, 2, 3, 4, 5])
y = np.array([4, 2, 1, 3, 7])
plt.scatter(x, y);
但是我们仍然用LinearRegression 拟合出一条直线,并获得直线的最优解:
from sklearn.linear_model import LinearRegression
X = x[:, np.newaxis]
model = LinearRegression().fit(X, y)
yfit = model.predict(X)
plt.scatter(x, y)
plt.plot(x, yfit);
很显然,我们需要用一个更复杂的模型来描述x 与y 的关系。可以对数据进行变换,并增加额外的特征来提升模型的复杂度。例如,可以在数据中增加多项式特征:
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=3, include_bias=False)
X2 = poly.fit_transform(X)
print(X2)
在衍生特征矩阵中,第1 列表示 x;
第2列表示 x²;
第3 列表示 x³;
通过对这个扩展的输入矩阵计算线性回归,就可以获得更接近原始数据的结果了。
model = LinearRegression().fit(X2, y)
yfit = model.predict(X2)
plt.scatter(x, y)
plt.plot(x, yfit);
这种不通过改变模型,而是通过变换输入来改善模型效果的理念,正是许多更强大的机器学习方法的基础。
5、缺失值填充
特征工程中还有一种常见需求是处理缺失值。例如,有如下一个数据集:
from numpy import nan
X = np.array([[ nan, 0, 3 ],
[ 3, 7, 9 ],
[ 3, 5, 2 ],
[ 4, nan, 6 ],
[ 8, 8, 1 ]])
y = np.array([14, 16, -1, 8, -5])
当将一个普通的机器学习模型应用到这份数据时,首先需要用适当的值替换这些缺失数据。这个操作被称为缺失值填充,相应的策略很多,有的简单(例如用列均值替换缺失值),有的复杂(例如用矩阵填充或其他模型来处理缺失值)。
对于一般的填充方法,如均值、中位数、众数,Scikit-Learn 有Imputer 类可以实现:
from sklearn.preprocessing import Imputer
imp = Imputer(strategy='mean')
X2 = imp.fit_transform(X)
X2
结果矩阵中的两处缺失值都被所在列剩余数据的均值替代了。这个被填充的数据就可以直接放到评估器里训练了,例如LinearRegression 评估器:
model = LinearRegression().fit(X2, y)
model.predict(X2)
6、特征管道
如果经常需要手动应用前文介绍的任意一种方法,你很快就会感到厌倦,尤其是当你需要将多个步骤串起来使用时。例如,我们可能需要对一些数据做如下操作。
用均值填充缺失值。
将衍生特征转换为二次方。
拟合线性回归模型。
Scikit-Learn 提供了一个管道对象,如下所示:
from sklearn.pipeline import make_pipeline
model = make_pipeline(Imputer(strategy='mean'),
PolynomialFeatures(degree=2),
LinearRegression())
这个管道看起来就像一个标准的Scikit-Learn 对象,可以对任何输入数据进行所有步骤的处理:
model.fit(X, y) # X with missing values, from above
print(y)
print(model.predict(X))
这样的话,所有的步骤都会自动完成。请注意,出于简化演示考虑,将模型应用到已经训练过的数据上,模型能够非常完美地预测结果。