手把手教你入门和实践特征工程的全方位笔记,附代码下载
03 特征构建
如果我们对变量进行处理之后,效果仍不是非常理想,就需要进行特征构建了,也就是衍生新变量。
而在这之前,我们需要了解我们的数据集,先前两节中我们了解到了可以通过 data.info
和 data.describe()
来查看,同时结合 数据等级 (定类、定序、定距、定比)来理解变量。
基础操作
本小节中我们使用一个自定义数据集。
# 本次案例使用的数据集
import pandas as pd
X = pd . DataFrame ({ ‘city’ :[ ‘tokyo’ , None , ‘london’ , ‘seattle’ , ‘san fancisco’ , ‘tokyo’ ],
‘boolean’ :[ ‘y’ , ‘n’ , None , ‘n’ , ‘n’ , ‘y’ ],
‘ordinal_column’ :[ ‘somewhat like’ , ‘like’ , ‘somewhat like’ , ‘like’ , ‘somewhat like’ , ‘dislike’ ],
‘quantitative_column’ :[ 1 , 11 , – .5 , 10 , None , 20 ]})
X
首先我们需要对分类变量进行填充操作,类别变量一般用众数或者特殊值来填充,回顾之前的内容,我们也还是采取Pipeline的方式来进行,因此可以事先基于 TransformMixin
基类来对填充的方法进行封装,然后直接在Pipeline中进行调用,代码可以参考:
# 填充分类变量(基于TransformerMixin的自定义填充器,用众数填充)
from sklearn . base import TransformerMixin
class CustomCategoryzImputer ( 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
# 调用自定义的填充器
cci = CustomCategoryzImputer ( cols =[ ‘city’ , ‘boolean’ ])
cci . fit_transform ( X )
又或者利用 scikit-learn 的 Imputer
类来实现填充,而这个类有一个 Strategy
的方法自然就被继承过来用了,包含的有mean、median、most_frequent可供选择。
# 填充分类变量(基于Imputer的自定义填充器,用众数填充)
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
# 调用自定义的填充器
cqi = CustomQuantitativeImputer ( cols = [ ‘quantitative_column’ ], strategy = ‘mean’ )
cqi . fit_transform ( X )
对上面的两种填充进行流水线封装:
# 全部填充
from sklearn . pipeline import Pipeline
imputer = Pipeline ([( ‘quant’ , cqi ),
( ‘category’ , cci )
])
imputer . fit_transform ( X )
完成了分类变量的填充工作,接下来就需要对分类变量进行编码了(因为大多数的机器学习算法都是无法直接对类别变量进行计算的),一般有两种办法: 独热编码以及标签编码。
1)独热编码
独热编码主要是针对定类变量的,也就是不同变量值之间是没有顺序大小关系的,我们一般可以使用 scikit_learn 里面的 OneHotEncoding
来实现的,但我们这里还是使用自定义的方法来加深理解。
# 类别变量的编码(独热编码)
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
# 调用自定义的填充器
cd = CustomDummifier ( cols =[ ‘boolean’ , ‘city’ ])
cd . fit_transform ( X )
2)标签编码
标签编码是针对定序变量的,也就是有顺序大小的类别变量,就好像案例中的变量ordinal_column的值(dislike、somewhat like 和 like 可以分别用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
# 调用自定义的填充器
ce = CustomEncoder ( col = ‘ordinal_column’ , ordering =[ ‘dislike’ , ‘somewhat like’ , ‘like’ ])
ce . fit_transform ( X )
3)数值变量分箱操作
以上的内容是对类别变量的一些简单处理操作,也是比较常用的几种,接下来我们就对数值变量进行一些简单处理方法的讲解。
有的时候,虽然变量值是连续的,但是只有转换成类别才有解释的可能,比如年龄,我们需要分成年龄段,这里我们可以使用pandas的 cut
函数来实现。
# 数值变量处理——cut函数
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
# 调用自定义的填充器
cc = CustomCutter ( col = ‘quantitative_column’ , bins = 3 )
cc . fit_transform ( X )
综上,我们可以对上面自定义的方法一并在Pipeline中进行调用,Pipeline的顺序为:
1)用imputer填充缺失值
2)独热编码city和boolean
3)标签编码ordinal_column
4)分箱处理quantitative_column
代码为:
from sklearn . pipeline import Pipeline
# 流水线封装
pipe = Pipeline ([( ‘imputer’ , imputer ),
( ‘dummify’ , cd ),
( ‘encode’ , ce ),
( ‘cut’ , cc )
])
# 训练流水线
pipe . fit ( X )
# 转换流水线
pipe . transform ( X )
数值变量扩展
这一小节我们使用一个新的数据集(人体胸部加速度数据集),我们先导入数据:
# 人体胸部加速度数据集,标签activity的数值为1-7
”’
1-在电脑前工作
2-站立、走路和上下楼梯
3-站立
4-走路
5-上下楼梯
6-与人边走边聊
7-站立着说话
”’
df = pd . read_csv ( ‘./data/activity_recognizer/1.csv’ , header = None )
df . columns = [ ‘index’ , ‘x’ , ‘y’ , ‘z’ , ‘activity’ ]
df . head ()
这边只介绍一种多项式生成新特征的办法,调用 PolynomialFeatures
来实现。
# 扩展数值特征
from sklearn . preprocessing import PolynomialFeatures
x = df [[ ‘x’ , ‘y’ , ‘z’ ]]
y = df [ ‘activity’ ]
poly = PolynomialFeatures ( degree = 2 , include_bias = False , interaction_only = False )
x_poly = poly . fit_transform ( x )
pd . DataFrame ( x_poly , columns = poly . get_feature_names ()). head ()
还可以查看下衍生新变量后的相关性情况,颜色越深相关性越大:
# 查看热力图(颜色越深代表相关性越强)
% matplotlib inline
import seaborn as sns
sns . heatmap ( pd . DataFrame ( x_poly , columns = poly . get_feature_names ()). corr ())
在流水线中的实现代码:
# 导入相关库
from sklearn . neighbors import KNeighborsClassifier
from sklearn . model_selection import GridSearchCV
from sklearn . pipeline import Pipeline
knn = KNeighborsClassifier ()
# 在流水线中使用
pipe_params = { ‘poly_features__degree’ :[ 1 , 2 , 3 ],
‘poly_features__interaction_only’ :[ True , False ],
‘classify__n_neighbors’ :[ 3 , 4 , 5 , 6 ]}
# 实例化流水线
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 {‘classify__n_neighbors’: 5, ‘poly_features__degree’: 2, ‘poly_features__interaction_only’: True}
文本变量处理
文本处理一般在NLP(自然语言处理)领域应用最为广泛,一般都是需要把文本进行向量化,最为常见的方法有 词袋(bag of words)、CountVectorizer、TF-IDF。
1)bag of words
词袋法分成3个步骤,分别是 分词(tokenizing)、计数(counting)、归一化(normalizing)。
2)CountVectorizer
将文本转换为矩阵,每列代表一个词语,每行代表一个文档,所以一般出来的矩阵会是非常稀疏的,在 sklearn.feature_extraction.text
中调用 CountVectorizer
即可使用。
3)TF-IDF
TF-IDF向量化器由两个部分组成,分别为代表词频的TF部分,以及代表逆文档频率的IDF,这个TF-IDF是一个用于信息检索和聚类的词加权方法,在 sklearn.feature_extraction.text
中调用 TfidfVectorizer
即可。
TF:即Term Frequency,词频,也就是单词在文档中出现的频率。
IDF:即Inverse Document Frequency,逆文档频率,用于衡量单词的重要度,如果单词在多份文档中出现,就会被降低权重。
04 特征选择
好了,经过了上面的特征衍生操作,我们现在拥有了好多好多的特征(变量)了,全部丢进去模型训练好不好?当然是不行了:no_entry_sign:,这样子既浪费资源又效果不佳,因此我们需要做一下 特征筛选 ,而特征筛选的方法大致可以分为两大类: 基于统计的特征筛选 和 基于模型的特征筛选。
在进行特征选择之前,我们需要搞清楚一个概念: 到底什么是更好的?有什么指标可以用来量化呢?
这大致也可以分为两大类:一类是 模型指标 ,比如accuracy、F1-score、R^2等等,还有一类是 元指标 ,也就是指不直接与模型预测性能相关的指标,如: 模型拟合/训练所需的时间、拟合后的模型预测新实例所需要的时间、需要持久化(永久保存)的数据大小。
我们可以通过封装一个方法,把上面提及到的指标封装起来,方便后续的调用,代码如下:
from sklearn . model_selection import GridSearchCV
def get_best_model_and_accuracy ( model , params , x , y ):
grid = GridSearchCV ( model ,
params ,
error_score = 0. )
grid . fit ( x , y )
# 经典的性能指标
print ( “Best Accuracy:{}” . format ( grid . best_score_ ))
# 得到最佳准确率的最佳参数
print ( “Best Parameters:{}” . format ( grid . best_params_ ))
# 拟合的平均时间
print ( “Average Time to Fit (s):{}” . format ( round ( grid . cv_results_ [ ‘mean_fit_time’ ]. mean (), 3 )))
# 预测的平均时间
print ( “Average Time to Score (s):{}” . format ( round ( grid . cv_results_ [ ‘mean_score_time’ ]. mean (), 3 )))
############### 使用示例 ###############
# 导入相关库
from sklearn . neighbors import KNeighborsClassifier
from sklearn . model_selection import GridSearchCV
from sklearn . pipeline import Pipeline
knn = KNeighborsClassifier ()
# 在流水线中使用
pipe_params = { ‘poly_features__degree’ :[ 1 , 2 , 3 ],
‘poly_features__interaction_only’ :[ True , False ],
‘classify__n_neighbors’ :[ 3 , 4 , 5 , 6 ]}
# 实例化流水线
pipe = Pipeline ([( ‘poly_features’ , poly ),
( ‘classify’ , knn )])
# 网格搜索
get_best_model_and_accuracy ( pipe , pipe_params , x , y )
通过上面的操作,我们可以创建一个模型性能基准线,用于对比后续优化的效果。接下来介绍一些常用的特征选择方法。
1)基于统计的特征选择
针对于单变量,我们可以采用 皮尔逊相关系数以及假设检验 来选择特征。
(1)皮尔逊相关系数可以通过 corr() 来实现,返回的值在-1到1之间,绝对值越大代表相关性越强;
(2)假设检验也就是p值,作为一种统计检验,在特征选择中,假设测试得原则是: ” 特征与响应变量没有关系 “ (零假设)为真还是假。我们需要对每个变量进行检测,检测其与target有没有显著关系。可以使用 SelectKBest
和 f_classif
来实现。一般P值是 介于0-1 之间,简而言之, p值越小,拒绝零假设的概率就越大,也就是这个特征与target关系更大 。
2)基于模型的特征选择
(1)对于文本特征, sklearn.feature_extraction.text
里的 CountVectorizer
有自带的特征筛选的参数,分别是 max_features、min_df、max_df、stop_words ,可以通过搜索这些参数来进行特征选择,可以结合 SelectKBest
来实现流水线。
(2)针对:evergreen_tree:树模型,我们可以直接调用不同树模型算法里的 特征重要度 来返回特征重要度,比如 DecisionTreeClassifier里的 feature_importances_ ,(除此之外还有RandomForest、GBDT、XGBoost、ExtraTreesClassifier等等)都可以直接返回每个特征对于本次拟合的重要度,从而我们可以剔除重要度偏低的特征,可以结合 SelectFromModel
来实现流水线。
(3)使用正则化来筛选变量(针对线性模型)。有两种常用的正则化方法: L1正则化(Lasso)和L2正则化(岭)。
总结一下,有几点做特征选择的方法经验:
(1)如果特征是分类变量,那么可以从SelectKBest开始,用卡方或者基于树的选择器来选择变量;
(2)如果特征是定量变量,可以直接用线性模型和基于相关性的选择器来选择变量;
(3)如果是二分类问题,可以考虑使用 SelectFromModel和SVC;
(4)在进行特征选择前,还是需要做一下EDA。
05 特征转换
经过了上面几个环节的“洗礼”,我们来到特征转换的环节,也就是使用源数据集的隐藏结构来创建新的列,常用的办法有2种:PCA和LDA。
PCA:
PCA,即主成分分析(Principal Components Analysis),是比较常见的数据压缩的办法,即将多个相关特征的数据集投影到相关特征较少的坐标系上。也就是说,转换后的特征,在解释性上就走不通了,因为你无法解释这个新变量到底具有什么业务逻辑了。
PCA的原理这里就不展开来讲了,太多的文章把它讲得十分透彻了。这里主要是复现一下PCA在sklearn上的调用方法,一来继续熟悉下Pipeline的使用,二来理解一下PCA的使用方法。
# 导入相关库
from sklearn . datasets import load_iris
import matplotlib . pyplot as plt
% matplotlib inline
from sklearn . decomposition import PCA
# 导入数据集
iris = load_iris ()
iris_x , iris_y = iris . data , iris . target
# 实例化方法
pca = PCA ( n_components = 2 )
# 训练方法
pca . fit ( iris_x )
pca . transform ( iris_x )[: 5 ,]
# 自定义一个可视化的方法
label_dict = { i : k for i , k in enumerate ( iris . target_names )}
def plot ( x , y , title , x_label , y_label ):
ax = plt . subplot ( 111 )
for label , marker , color in zip (
range ( 3 ),( ‘^’ , ‘s’ , ‘o’ ),( ‘blue’ , ‘red’ , ‘green’ )):
plt . scatter ( x = x [:, 0 ]. real [ y == label ],
y = x [:, 1 ]. real [ y == label ],
color = color ,
alpha = 0.5 ,
label = label_dict [ label ]
)
plt . xlabel ( x_label )
plt . ylabel ( y_label )
leg = plt . legend ( loc = ‘upper right’ , fancybox = True )
leg . get_frame (). set_alpha ( 0.5 )
plt . title ( title )
# 可视化
plot ( iris_x , iris_y , “original iris data” , “sepal length(cm)” , “sepal width(cm)” )
plt . show ()
plot ( pca . transform ( iris_x ), iris_y , “Iris: Data projected onto first two PCA components” , “PCA1” , “PCA2” )
以上是PCA在sklearn上的简单调用和效果展示,另外,作者提出了一个很有意思的问题:
一般而言,对特征进行归一化处理后会对机器学习算法的效果有比较明显的帮助,但为什么在书本的例子却是相反呢?
给出的解释是:在对数据进行缩放后,列与列之间的协方差会更加一致,而且每个主成分解释的方差会变得分散,而不是集中在某一个主成分上。所以,在实际操作的时候,都要对缩放的未缩放的数据进行性能测试才是最稳妥的哦。
LDA:
LDA,即线性判别分析(Linear Discriminant Analysis),它是一个有监督的算法(哦对了, PCA是无监督的),一般是用于分类流水线的预处理步骤。与PCA类似,LDA也是提取出一个新的坐标轴,将原始的高维数据投影到低维空间去,而区别在于LDA不会去专注数据之间的方差大小,而是直接优化低维空间,以获得最佳的类别可分性。
# LDA的使用
# 导入相关库
from sklearn . discriminant_analysis import LinearDiscriminantAnalysis
# 实例化LDA模块
lda = LinearDiscriminantAnalysis ( n_components = 2 )
# 训练数据
x_lda_iris = lda . fit_transform ( iris_x , iris_y )
# 可视化
plot ( x_lda_iris , iris_y , “LDA Projection” , “LDA1” , “LDA2” )
06 特征学习
来到最后一章了,这章的主题是“以AI促AI”。看起来还蛮抽象的,反正我是觉得有点奇怪,特征学习算法是非参数方法,也就是不依赖数据结构而构建出来的新算法。
数据的参数假设
参数假设指的是算法对数据形状的基本假设。比如上一章的PCA,我们是假设:
原始数据的形状可以被(特征值)分解,并且可以用单个线性变换(矩阵计算)表示。
而特征学习算法,就是要去除这个“假设”来解决问题,因为这算法不会依赖数据的形状,而是依赖于随机学习(Stochastic Learning),指的是这些算法并不是每次输出相同的结果,而是一次次按轮(epoch)去检查数据点以找到要提取的最佳特征,并且可以拟合出一个最优的解决方法。
而在特征学习领域,有两种方法是比较常用的,也是下面来讲解的内容:受限玻尔兹曼机(RBM)和词嵌入。
受限玻尔兹曼机(RBM)
RBM是一种简单的深度学习架构,是一组无监督的特征学习算法,根据数据的概率模型学习一定数量的新特征,往往使用RBM之后去用线性模型(线性回归、逻辑回归、感知机等)的效果极佳。
从概念上说,RBM是一个浅层(2层)的神经网络,属于深度信念网络(DBN,deep belief network)算法的一种。它也是一种无监督算法,可以学习到的 特征数量只受限于计算能力,它可能学习到比原始要少或者多的特征,具体要学习的特征数量取决于要解决的问题。
“受限”的说法是因为它只允许层与层之间的连接(层间连接),而不允许同一层内的节点连接(层内连接)。
在这里需要理解一下“重建”(Reconstruction),也就是这个操作,使得在不涉及更深层网络的情况下,可见层(输入层)和隐含层之间可以存在数次的前向和反向传播。
在重建阶段,RBM会反转网络,可见层变成了隐含层,隐含层变成了可见层,用相同的权重将激活变量a反向传递到可见层,但是偏差不一样,然后用前向传导的激活变量重建原始输入向量。RBM就是用这种方法来进行“自我评估”的,通过将激活信息进行反向传导并获取原始输入的近似值,该网络可以调整权重,让近似值更加接近原始输入。
在训练开始时,由于权重是随机初始化的(一般做法),近似值与真实值的差异可能会极大的,接下来就会通过反向传播的方法来调整权重,最小化原始输入与近似值的距离,一直重复这个过程,直到近似值尽可能接近原始输入。(这个过程发生的次数叫 迭代次数 )
大致的原理就是上面的说法了,更加详细的解释可以自行百度哦。下面我们来讲讲RBM在机器学习管道中的应用,我们还是使用MNIST数据集,这个数据集在之前讲Keras的时候(传送门)也用到了,就是一堆数字的像素点数据,然后用来识别数字。
# RBM的使用
# 我们使用MNIST数据集来讲解
# 导入相关库
import numpy as np
import matplotlib . pyplot as plt
% matplotlib inline
from sklearn . linear_model import LogisticRegression
from sklearn . neural_network import BernoulliRBM
from sklearn . pipeline import Pipeline
# 导入数据集
images = np . genfromtxt ( ‘./data/mnist_train.csv’ , delimiter = ‘,’ )
print ( images . shape )
# 划分数据
images_x , images_y = images [:, 1 :], images [:, 0 ]
# 缩放特征到0-1
images_x = images_x / 255.
# 用RBM学习新特征
rbm = BernoulliRBM ( random_state = 0 )
lr = LogisticRegression ()
# 设置流水线的参数范围
params = { ‘clf__C’ :[ 1e-1 , 1e0 , 1e1 ],
‘rbm__n_components’ :[ 100 , 200 ]
}
# 创建流水线
pipeline = Pipeline ([( ‘rbm’ , rbm ),
( ‘clf’ , lr )])
# 实例化网格搜索类
grid = GridSearchCV ( pipeline , params )
# 拟合数据
grid . fit ( images_x , images_y )
# 返回最佳参数
grid . best_params_ , grid . best_score_
词嵌入
在NLP领域应用极为广泛了,它可以将字符串(单词或短语)投影到n维特征集中,以便理解上下文和措辞的细节,我们可以使用sklearn中的 CountVectorizer
和 TfidfVectorizer
来将这些字符串进行转为向量,但这只是一些单词特征的集合而已,为了理解这些特征,我们更加要关注一个叫 gensim
的包。
常用的词嵌入方法有两种:Word2vec和GloVe。
Word2vec:Google发明的一种基于深度学习的算法。Word2vec也是一个浅层的神经网络,含有输入层、隐含层和输出层,其中输入层和输出层的节点个数一样。
GloVe:来自斯坦福大学的算法,通过一系列矩阵统计进行学习。
词嵌入的应用很多,比如信息检索,意思是当我们输入关键词时,搜索引擎可以回忆并准确返回和关键词匹配的文章或者新闻。
PS:笔者建议可以买本书来看比较好,写得实在不错~
想要打包好的代码的盆友,可以在 公众号【SAMshare】 后台回复“特征工程”,即可获得打包下载哦~
封面图来源:pexels by Pixabay