? 说起特征工程,都说是机器学习建模中最为重要而且费时的一项工作,而且它涉及的知识点会非常地多,经验老道的老司机自然是轻车熟路了,但对于刚刚入门的新手司机,学习到的知识点都是东一点西一点的,不够系统化,本篇文章是在阅读了一本评分极高的特征工程书籍 ? 《特征工程入门与实践》后的一篇笔记文,记录下相对比较系统的知识点以及可运行复现的代码,希望对各位同行有所帮助哈。
? 目录
-
? 特征理解
-
? 特征增强
-
? 特征构建
-
✅ 特征选择
-
? 特征转换
-
? 特征学习
? 01 特征理解
1. 区分结构化数据与非结构化数据
如一些以表格形式进行存储的数据,都是结构化数据;而非结构化数据就是一堆数据,类似于文本、报文、日志之类的。
2. 区分定量和定性数据
定量数据:指的是一些数值,用于衡量某件东西的数量;定性数据:指的是一些类别,用于描述某件东西的性质。
3. 关键代码汇集
1)常见简易画图
# 绘制条形图
salary_ranges['Grade'].value_counts().sort_values(ascending=False).head(10).plot(kind='bar')
# 绘制饼图
salary_ranges['Grade'].value_counts().sort_values(ascending=False).head(5).plot(kind='pie')
# 绘制箱体图
salary_ranges['Union Code'].value_counts().sort_values(ascending=False).head(5).plot(kind='box')
# 绘制直方图
climate['AverageTemperature'].hist()
# 为每个世纪(Century)绘制平均温度的直方图
climate_sub_china['AverageTemperature'].hist(by=climate_sub_china['Century'],
sharex=True,
sharey=True,
figsize=(10, 10),
bins=20)
# 绘制散点图
x = climate_sub_china['year']
y = climate_sub_china['AverageTemperature']
fig, ax = plt.subplots(figsize=(10,5))
ax.scatter(x, y)
plt.show()
2)检查缺失情况
# 移除缺失值
climate.dropna(axis=0, inplace=True)
# 检查缺失个数
climate.isnull().sum()
3)变量类别转换
# 日期转换, 将dt 转换为日期,取年份, 注意map的用法
climate['dt'] = pd.to_datetime(climate['dt'])
climate['year'] = climate['dt'].map(lambda value: value.year)
# 只看中国
climate_sub_china = climate.loc[climate['Country'] == 'China']
climate_sub_china['Century'] = climate_sub_china['year'].map(lambda x:int(x/100+1))
climate_sub_china.head()
? 02 特征增强
Step1: 进行EDA(Exploratory Data Analysis),思路如下:
value_counts()
就可以解决,看看样本是否失衡。
isnull().sum()
的个数,不过需要注意的是,可能统计出来没有缺失,并不是因为真的没有缺失,而且缺失被人用某个特殊值填充了,一般会用 -9、blank、unknown、0之类的,需要注意⚠️识别,后面需要对缺失进行合理填充。
data.describe()
获取基本的描述性统计,根据均值、标准差、极大极小值等指标,结合变量含义来判断。
Step2: 处理数据缺失问题
需要还原一下:
# 处理被错误填充的缺失值0,还原为 空(单独处理)
pima['serum_insulin'] = pima['serum_insulin'].map(lambda x:x if x !=0 else None)
# 检查变量缺失情况
pima['serum_insulin'].isnull().sum()
# 批量操作 还原缺失值
columns = ['serum_insulin','bmi','plasma_glucose_concentration','diastolic_blood_pressure','triceps_thickness']
for col in columns:
pima[col].replace([0], [None], inplace=True)
# 检查变量缺失情况
pima.isnull().sum()
dropna()
来处理即可,同时我们还可以检查下我们到底删除了多少数据量:round(data.shape[0]-data_dropped.shape[0])/float(data.shape[0])
就可以统计出来了。当然,删除之后,我们还需要看看数据的分布,对比目标占比、特征分布与先前的是否存在明显差异,如果是的话,建议不要使用这种办法。
Demo:
# 使用sklearn的 Pipeline以及 Imputer来实现缺失值填充
from sklearn.pipeline import Pipeline
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import Imputer
# 调参候选
knn_params = {'classify__n_neighbors':[1,2,3,4,5,6]}
# 实例化KNN模型
knn = KNeighborsClassifier()
# 管道设计
mean_impute = Pipeline([('imputer', Imputer(strategy='mean')),
('classify',knn)
])
x = pima.drop('onset_disbetes', axis=1) # 丢弃y
y = pima['onset_disbetes']
# 网格搜索
grid = GridSearchCV(mean_impute, knn_params)
grid.fit(x, y)
# 打印模型效果
print(grid.best_score_, grid.best_params_)
# 0.73177
Step3: 标准化和归一化
经过上面的处理,模型的精度可以达到0.73177,但我们可以继续优化吗?那是肯定的。
我们可以先看看所有特征的分布(特征少的时候可以这么看):
pima_imputed_mean.hist(figsize=(15,15))
1)Z分数标准化
最为常用的标准化技术,利用了统计学中的z分数思想,也就是将数据转换为均值为0,标准差为1的分布,其在python中的调用方法:
# z分数标准化(单一特征)
from sklearn.preprocessing import StandardScaler
# 实例化方法
scaler = StandardScaler()
glucose_z_score_standarScaler =scaler.fit_transform(pima[['plasma_glucose_concentration']].fillna(-9))
# 可以看看转换之后的均值和标准差是否为0和1
glucose_z_score_standarScaler.mean(), glucose_z_score_standarScaler.std()
# z分数标准化(全部特征)
from sklearn.preprocessing import StandardScaler
# 实例化方法
scaler = StandardScaler()
pima_imputed_mean_scaled = pd.DataFrame(scaler.fit_transform(pima_imputed_mean),columns=pima_columns)
# 看下标准化之后的分布
pima_imputed_mean_scaled.hist(figsize=(15,15), sharex=True)
# 在Pipeline中使用
model = Pipeline([
('imputer', Imputer()),
('standardize', StandarScaler())
])
2)min-max标准化
min-max标准化和z-score类似,其公式为:(X - Xmin)/(Xmax - Xmin)
在python中的调用方法:
# min-max标准化
from sklearn.preprocessing import MinMaxScaler
# 实例化方法
min_max = MinMaxScaler()
# 使用min-max标准化
pima_min_maxed = pd.DataFrame(min_max.fit_transform(pima.fillna(-9)),columns=pima_columns)
3)行归一化
行归一化针对的是每一行数据,不同于上面的两种方法(针对列),对行进行处理是为了保证每行的向量长度一样(也就是单位范围,unit norm),有L1、L2范数。
在python中的调用方法:
# 行归一化
from sklearn.preprocessing import Normalizer
# 实例化方法
normalize = Normalizer()
# 使用行归一化
pima_normalized = pd.DataFrame(normalize.fit_transform(pima.fillna(-9)),columns=pima_columns)
# 查看矩阵的平均范数(1)
np.sqrt((pima_normalized**2).sum(axis=1)).mean()
? 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 特征选择
好了,经过了上面的特征衍生操作,我们现在拥有了好多好多的特征(变量)了,全部丢进去模型训练好不好?当然是不行了?,这样子既浪费资源又效果不佳,因此我们需要做一下 特征筛选 ,而特征筛选的方法大致可以分为两大类:基于统计的特征筛选 和 基于模型的特征筛选。
在进行特征选择之前,我们需要搞清楚一个概念:到底什么是更好的?有什么指标可以用来量化呢?
这大致也可以分为两大类:一类是模型指标,比如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)针对?树模型,我们可以直接调用不同树模型算法里的 特征重要度 来返回特征重要度,比如 DecisionTreeClassifier里的feature_importances_,(除此之外还有RandomForest、GBDT、XGBoost、ExtraTreesClassifier等等)都可以直接返回每个特征对于本次拟合的重要度,从而我们可以剔除重要度偏低的特征,可以结合 SelectFromModel
来实现流水线。
(3)使用正则化来筛选变量(针对线性模型)。有两种常用的正则化方法:L1正则化(Lasso)和L2正则化(岭)。
总结一下,有几点做特征选择的方法经验:
(1)如果特征是分类变量,那么可以从SelectKBest开始,用卡方或者基于树的选择器来选择变量;
(2)如果特征是定量变量,可以直接用线性模型和基于相关性的选择器来选择变量;
(3)如果是二分类问题,可以考虑使用 SelectFromModel和SVC;
(4)在进行特征选择前,还是需要做一下EDA。
? 05 特征转换
✅ 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")
一般而言,对特征进行归一化处理后会对机器学习算法的效果有比较明显的帮助,但为什么在书本的例子却是相反呢?
✅ 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 特征学习
? 数据的参数假设
原始数据的形状可以被(特征值)分解,并且可以用单个线性变换(矩阵计算)表示。
? 受限玻尔兹曼机(RBM)
# 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_
? 词嵌入
CountVectorizer
和 TfidfVectorizer
来将这些字符串进行转为向量,但这只是一些单词特征的集合而已,为了理解这些特征,我们更加要关注一个叫 gensim
的包。
PS:附本书链接~