特征工程
特征提取 feature extraction
从文字、图像、声音等其他非结构化数据中提取新信息作为特征。比如从淘宝商品的名称中提取出产品类别、产品颜色、是否是网红产品等信息。
DictVectorizer
类
DictVectorizer
类可将 Python 标准的字典对象列表转换为 scikit-learn 估计其能够使用的 Numpy 数组。DictVectorizer
类可以将分类特征实现 "one-hot" 编码。
class
sklearn.feature_extraction.DictVectorizer(dtype=np.float64, separator='=', sparse=True, sort=True)
>>> measurements = [
... {'city': 'Dubai', 'temperature': 33.},
... {'city': 'London', 'temperature': 12.},
... {'city': 'San Francisco', 'temperature': 18.},
... ]
>>> from sklearn.feature_extraction import DictVectorizer
>>> vec = DictVectorizer()
>>> vec.fit_transform(measurements).toarray()
array([[ 1., 0., 0., 33.],
[ 0., 1., 0., 12.],
[ 0., 0., 1., 18.]])
>>> vec.get_feature_names()
['city=Dubai', 'city=London', 'city=San Francisco', 'temperature']
文本特征提取
专用词语
文本分析师机器学习算法的主要应用领域,然而原始的符号文字序列却不能直接作为特征矩阵传递给算法,因为特征矩阵要求固定长度的数字特征向量矩阵,而不是具有可变长度的原始文本文档。为了解决这个问题,scikit-learn
提供了从文本内容中提取数字特征的常见方法:
- 令牌化 (tokenizing):将一个句子通过空格(如英语)标点符号或者其他方式(如汉语分词)分成若干个词令牌,并给每个词赋予特定的 id;
- 统计 (counting):统计每个词令牌在文档中出现的次数;
- 标准化 (normalizing):在大多数文档中,可以减少重要的词令牌的出现次数的权重;
词袋模型 CountVectorizer()
“向量化” 是指将文本文档集合转换为数字集合特征向量的方法,这种特殊的思想叫做 "词袋"模型。基本步骤是统计文档中所有出现的词汇 (不包括停用词),构成特征值,统计每篇文档中出现的词汇,对应的特征值设为 1,其余为 0;由于大多数文档只使用了文本词向量全集中很小的一部分,所以最终得到的特征矩阵的元素大部分都是 0,因此常使用 scipy 模块的稀疏矩阵来表示。
class
sklearn.feature_extraction.text.CountVectorizer(input='content', lowercase=True, preprocessor=None, tokenizer=None, stop_words=None, analyzer='word', max_df=1.0, min_df=1, max_features=None)
>>> from sklearn.feature_extraction.text import CountVectorizer
>>> corpus = [
... 'This is the first document.',
... 'This document is the second document.',
... 'And this is the third one.',
... 'Is this the first document?',
... ]
>>> vectorizer = CountVectorizer()
>>> X = vectorizer.fit_transform(corpus)
>>> print(vectorizer.get_feature_names())
['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
>>> print(X.toarray())
[[0 1 1 1 0 0 1 0 1]
[0 2 0 1 0 1 1 0 1]
[1 0 0 1 1 0 1 1 1]
[0 1 1 1 0 0 1 0 1]]
TF-IDF 模型 TfidfVectorizer()
在一个容量很大的文本语料库中,一些单词会出现很多次,但是对文档的主题却没有实际作用,如果将这些词直接提给分类词,那么这些频繁词组很可能会掩盖住真正有价值的词。为了重新计算特征权重,并将其转化为适合分类器使用的浮点值,可以考虑使用 tf-idf 变换。
tf 表示词汇频率,idf 表示逆文档频率,, 使用 TfidfTransformer
的默认设置 TfidfTransformer (norm='l2',use_idf=True, smooth_idf=True,sublinear_tf=False)
, 计算方式为:
其中的 是为了平滑 曲线,标准的 没有后面的 +1; 是文档的总数, 是包含词汇 的文档数。
class
sklearn.feature_extraction.text.TfidfTransformer (norm='l2', use_idf=True, smooth_idf=True, sublinear_tf=False)
主题模型 LDA
词向量模型 word2vec
特征创造 feature creation
把现有的特征进行组合或者计算,得到新的特征。比如,如果一列特征是速度,另一列是距离,就可以通过这两列数据计算出所用时间;还可以对某一特征取指数或对数,从而产生另一个新的特征。
特征选择 feature selection
从所有的特征中,选出有意义、对模型有帮助的特征,以避免将所有特征都导入到模型之中。这样的好处:
- 除去部分噪声数据,可以提高模型所能达到的上限;
- 减少特征的数量,降低计算量,节省建模时间
特征选择第一步:从业务角度,理解每一列数据。
过滤法 filter
方差过滤 VarianceThreshold
sklearn.feature_selection
中定义了一个专门用于过滤方差的类 VarianceThreshold
class sklearn.feature_selection.VarianceThreshold(threshold=0)
最近邻算法 KNN、单棵决策树、支持向量机、神经网络、回归算法都需要遍历特征或者升维来进行运算,所以他们本身的运算量就很大,需要的时间就比较长,因此使用方差过滤这样的特征选择算法对他们来说尤为重要。但是对于不需要遍历特征的算法,比如随机森林,本身运算速度就比较快,它会随机地选取特征进行分枝,无论过滤法如何降低特征的数量,随机森林也只会随机地选择固定数量的特征来建模,因此特征选择对它来说效果平平。而最近邻算法 KNN 就不一样了,特征越少,距离计算的维度就越少,模型会随着特征的减少明显变得轻量。
因此特征过滤的主要对象是:需要遍历特征或者升维的算法;而特征过滤的主要目的是:在维持算法表现的前提下,帮助算法降低计算所需要的成本。
为什么特征过滤对随机森林无效,却对树模型有效?
从算法原理上来说,传统的决策树需要遍历所有的特征,计算不纯度之后进行分枝;而随机森林确实并行计算和分枝,因此随机森林的运算更快,特征过滤对随机森林几乎没用,对决策树却有用。
在 sklearn 中,决策树和随机森林都是随机选择特征进行分枝,但是决策树在建模过程中随机抽取的特征数目远远多于随机森林当中每棵树建立时随机抽取的特征数目,因此特征过滤对随机森林几乎没用,却随决策树很有帮助。
阈值很小;被过滤掉的特征比较少 | 阈值比较大,过滤的特征比较多 | |
---|---|---|
模型表现 | 不会有太大的影响 | 如果被过滤掉的特征大部分是噪声,模型会得更好;如果很多有效特征被过滤掉,模型很可能会变得很糟糕。 |
运行时间 | 可能会降低运行时间,但差别不会太大 | 一定能降低运行时间,算法在遍历特征时的计算越复杂,时间下降的越明显。 |
这里的方差阈值相当于一个超参数,要选定最优的超参数,可以通过绘制学习曲线,找到模型效果最优的点,但在实际工作中,往往不会这么做,这样做太浪费时间了。所以一般只会使用比较小的阈值过滤掉明显是噪声的数据,然后再选择更优的特征选择方法继续削减特征数量。
相关性过滤
卡方过滤 chi2
卡方过滤专门针对离散型标签 (即分类问题) 的相关性过滤。sklearn.feature_selection
中的 chi2(X, y)
函数可以计算 X 中的每一列与 y 的卡方统计量和对应的 p 值。
sklearn.feature_selection
中的 SelectKBest(func, k)
类可以根据提供的 func
筛选出前 k 个特征向量。与 卡方函数结合使用便可以过滤掉与 y 相关性很小的特征。另外,如果卡方检验检测到某个特征向量中所有的值都一样,会提示首先使用 "方差过滤" 过滤掉这些特征值。
from sklearn.feature_selection import chi2
from sklearn.feature_selection import SelectKBest
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
X = SelectKBest(chi2,4).fit_transform(X,y)
由此可以看出,k 就相当于一个超参数,那么对其该如何调节呢?
针对不同的 k 值绘制不同的学习曲线,但是这种方法在数据集很大时并不适用;
-
使用 "卡方函数" 返回的 p 值。每个特征向量与 y 值之间的卡方值没有确定的范围,但是对应的 p 值则能反映出两者之间的联系。在卡方检验中,如果 (这两个值常用来作为显著性水平的基准),则认为这两组数据具有相关性,否则则认为这两组数据相互独立。
chi2_value, p = chi2(X, y) k = (p<=0.05).sum() SelectKBest(chi2, k).fit_transform(X,y)
其中:
chi2_value
和p
分别特征向量对应的卡方值和 p 值;这样便能保留所有 p 值小于 0.05 的特征向量了。
P 值 <=0.01 或 0.05 >0.01 或 0.05 数据差异 差异不是自然形成的 这些差异是很自然的样本误差 相关性 两组数据是相关的 两组数据是相互独立的 原假设 拒绝原假设 接受原假设
F 检验
F 检验又称为 ANOVA,方差齐性检验,用来衡量每个特征与标签之间的线性关系,既可以应用于分类,又可以用来做回归,因此 sklearn.feature_selection
中包含 f_classif(F检验分类)
和 f_regression(F检验回归)
两个函数,其中F 检验分类应用于离散型变量的数据,而F检验回归用于连续性变量的数据。同 chi2()
函数一样,这两个函数也返回 F 值和 P值 两个值,F 值也没有明确的范围, 时,表示该特征值和 y 显著线性相关。
F 检验在数据服从正态分布时,效果会比较稳定,因此在使用之前应该把数据进行标准化处理。和卡方检验一样,这两个函数需要和 SelectKBest()
类结合使用。
互信息法
互信息法用来衡量每个特征与标签之间的任意关系 (包括线性和非线性关系),和 F 检验类似,即可以用来做分类,也可以做回归,sklearn.feature_selection
中包含 mutual_info_classif()
(互信息分类) 和 mutual_info_regression()
(互信息回归) 两个函数,这两个函数的用法和参数与chi2()
等一样。
互信息法不返回类似于 F 值或 P 值的统计量,而是返回 "每个特征与目标之间的互信息量的估计",这个估计量在 [0,1] 这个范围上,0 表示两个变量完全独立,1 则表示两个变量完全相关。
sklearn.feature_selection.mutual_info_classif(X, y, discrete_features=auto, n_neighbors=3, copy=True, random_state=None)
sklearn.feature_selection.mutual_info_regression(X, y, discrete_features=auto, n_neighbors=3,copy=True, random_state=None)
按百分比筛选特征值
sklearn.feature_selection
中还有一个类和 SelectKBest()
类似,不过是根据百分比对特征值进行筛选。
class
sklearn.feature_selection.SelectPercentile
(*func, percentile=10)
其中:func
需要是像 chi2()
这样返回评分(或者还有 p 值)的函数,然后返回评分前百分比的特征值,并将其他的特征值过滤掉。
Embedded 嵌入法
前面所说的过滤法都是根据特征向量和标签之间的某种统计信息,根据先验知识人为的将一些数据过滤掉。而嵌入法是一种让机器学习算法自己决定使用那些特征值的方法,即算法会使用所有的特征值进行训练,并返回每个特征值对该算法的重要性系数,类似于 feature_importances_
属性。
因此嵌入法的结果会比单纯的过滤法要好,因为它不但考虑了数据集本身的分布,还结合了具体的算法。对于无关的特征 (可以使用相关性过滤的特征) 和无区分度的特征 (可以使用方差过滤的特征) 都会因为对模型的贡献比较低而被删除。
但是也会带来另外的一些问题:
数据集比较大的时候,每次构建模型都需要大量的时间;
-
无法使用先验知识,从统计学和业务角度过滤掉一些无用的特征,从而使得调参变得更加无从下手。
sklearn
提供了feature_selection.SelectFromModel()
类class sklearn.feature_selection.SelectFromModel (estimator, threshold=None, prefit=False, norm_order=1, max_features=None)
estimator
可以是任何在拟合后具有 coef_
,feature_importances_
属性或参数中可选l1
或l2
惩罚项的评估器, feature_importances_
的取值范围是 [0,1],如果某些特征值的重要性低于设置的阈值,这些特征将会被过滤掉。如果想根据特征值的个数,而非阈值进行筛选,可以这样设置 threshold=0, max_features=N
.
对于带有惩罚项的模型,正则化惩罚项系数越大,特征向量在模型中对应的系数就会越小。当惩罚项系数大到一定程度,部分特征向量的系数将变为 0;当惩罚项系数继续增大时,所有的特征向量的系数都会趋向于 0. 所以可以用惩罚项系数的大小控制有用的特征向量的个数,也就是控制模型的复杂程度。
使用这种方法,threshold
和 max_features
便成了超参数,依然可以使用绘制学习曲线的方法选择最优的参数。
Wrapper 包装法
包装法也是特征选择和算法训练同时进行,依赖算法在拟合之后的 coef_
和 feature_importances_
属性来完成特征选择,这一点与嵌入法很类似。不同的是,需要输入最终返回的特征值的个数,而不是一个固定的阈值,这一点与使用嵌入法时,设置 max_features
很相似。
包装法的原理:
图中的 "算法" 并不是最终导入数据分类或者回归的机器学习算法,而是专业的数据挖掘算法,该算法的核心功能就是选取最佳特征子集。
最常用的目标函数是 "递归特征消除法" (Recursive Feature Elimination, RFE),它是一种贪婪优化算法,旨在找到性能最佳的特征子集。它反复创建模型,并在每次迭代时保留最佳特征,或者剔除最差特征,下一次迭代时,它会使用上次建模没有被选中的特征来构建下一个模型,直到所有的特征都消耗殆尽为止。然后它会根据自己保留和剔除特征的顺序来对所有的特征进行排名,最终选出一个最佳子集。
包装法的效果是所有特征选择方法中最有利于提升模型表现的,它可以使用很少的特征达到很优秀的效果。在特征数目相同的时候,包装法和嵌入法的效果相当,但是计算量要比嵌入法小很多。在数据集不是太大的情况下,是进行特征选择很好的选择。
class
sklearn.feature_selection.RFE
(estimator, n_features_to_select=None, step=1, verbose=0)
其中:estimator
: 要使用的机器学习评估器;n_features_to_select
: 最后要保留的特征数;step
: 每次要剔除的特征数。
该类有几个常用的属性:n_features_
: 返回保留的特征数目,等于 n_features_to_select
;support_
: True or False 表示该特征是否得以保留;ranking_
: 返回各个特征量的排序,排序越大表示该特征越重要。
>>> from sklearn.datasets import make_friedman1
>>> from sklearn.feature_selection import RFE
>>> from sklearn.svm import SVR
>>> X, y = make_friedman1(n_samples=50, n_features=10, random_state=0)
>>> estimator = SVR(kernel="linear")
>>> selector = RFE(estimator, 5, step=1)
>>> selector = selector.fit(X, y)
>>> selector.support_
array([ True, True, True, True, True, False, False, False, False,
False])
>>> selector.ranking_
array([1, 1, 1, 1, 1, 6, 4, 3, 2, 5])
在 sklearn
中还有另一个类似的类 RFECV
:
class
sklearn.feature_selection.RFECV
(estimator, step=1, min_features_to_select=1, cv=None, scoring=None, verbose=0, n_jobs=None)
在交叉验证循环中执行 RFE 以找到最佳数量的特征,增加了参数 cv
,其他用法和 RFE 一样。
用于异构数据的列转换器
许多数据集包含不同类型的特征,比如文本、浮点数和日期,每种类型的特征都需要单独的预处理或特征提取步骤。compose.ColumnTransformer
类可以对数据集的不同列执行不同的变换,该管道不存在数据泄露的问题,并且可以参数化,可以用来处于数组、稀疏矩阵和 pandas.DataFrame
结构的数据。
对每一列都会应用一个不同的变换,比如 preprocessing
或某个特定的特征抽取方法。
>>> import pandas as pd
>>> X = pd.DataFrame(
... {'city': ['London', 'London', 'Paris', 'Sallisaw'],
... 'title': ["His Last Bow", "How Watson Learned the Trick",
... "A Moveable Feast", "The Grapes of Wrath"],
... 'expert_rating': [5, 3, 4, 5],
... 'user_rating': [4, 5, 4, 3]})
对于这些数据,我们希望使用 preprocessing.OneHotEncoder
将 city
列编码为分类变量,同时使用 feature_extraction.text.CountVectorizer
来处理 title
列,默认情况下,其他列会被过滤掉 remainder = 'drop'
,如果想保留其他列,可以设置remainder = 'passthrough'
, 或者做其他处理 remainder = MinMaxScaler()
.
>>> from sklearn.compose import ColumnTransformer
>>> from sklearn.feature_extraction.text import CountVectorizer
>>> column_trans = ColumnTransformer(
... [('city_category', OneHotEncoder(dtype='int'),['city']),
... ('title_bow', CountVectorizer(), 'title')],
... remainder='passthrough')
>>> column_trans.fit(X)
ColumnTransformer(n_jobs=None, remainder='drop', sparse_threshold=0.3,
transformer_weights=None, transformers=...)
>>> column_trans.get_feature_names()
['city_category__x0_London', 'city_category__x0_Paris', 'city_category__x0_Sallisaw', 'title_bow__bow', 'title_bow__feast', 'title_bow__grapes', 'title_bow__his', 'title_bow__how', 'title_bow__last', 'title_bow__learned', 'title_bow__moveable', 'title_bow__of', 'title_bow__the', 'title_bow__trick', 'title_bow__watson', 'title_bow__wrath']
>>> column_trans.transform(X).toarray()
array([[1., 0., 0., 1., 0., 0., 1., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
[1., 0., 0., 0., 0., 0., 0., 1., 0., 1., 0., 0., 1., 1., 1., 0.],
[0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 1., 1., 0., 0., 1.]])
上面的例子中,CountVectorizer
接受一维数组作为输入,因此被指定为字符串('title'
),而 preprocessing.OneHotEncoder
接受二维数据,因此需要将列转化为字符串列表 (['city']
) .
函数 make_column_transformer()
可用来更简单地创建类对象,名字会自动给出,上面的例子等价于:
>>> from sklearn.compose import make_column_transformer
>>> column_trans = make_column_transformer(
... (OneHotEncoder(), ['city']),
... (CountVectorizer(), 'title'),
... remainder=MinMaxScaler())
>>> column_trans
ColumnTransformer(n_jobs=None, remainder=MinMaxScaler(copy=True, ...),
sparse_threshold=0.3,
transformer_weights=None,
transformers=[('onehotencoder', ...)