目录
0 前言
一个项目的基本流程:
1.了解项目情况:项目概述,项目目标(需求),现有解决方案情况
2.获取数据:数据来源、数据量
3.数据清洗:处理异常值、空值
4.特征工程:可视化、发现数据规律,选择特征,准备数据
5.训练模型:选择模型,进行训练,微调模型
6.解决方案:根据训练得到的模型给出解决方案
7.部署、应用、监控、维护系统
1 什么是特征工程
有这么一句话在业界广泛流传:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。
但特征工程很少在机器学习相关的书中阐述,包括很多网络课程,七月在线还是第一个在机器学习课程里讲特征工程的课。但直到现在,很多机器学习课程还是不讲特征工程,在我眼里,讲机器学习但不讲特征工程是不专业的(相信此文一出,会改进他们)。
那特征工程到底是什么,它真的有那么重要么?
顾名思义,特征工程其本质是一项工程活动,目的是最大限度地从原始数据中提取特征以供算法和模型使用。
而在公司做机器学习,大部分时间不是研究各种算法、设计高大上模型,也不是各种研究深度学习的应用,或设计N层神经网络,实际上,70~80%的时间都是在跟数据、特征打交道。
因为大部分复杂模型的算法精进都是数据科学家在做。而大多数同学在干嘛呢?在跑数据,或各种map-reduce,hive SQL,数据仓库搬砖,然后做数据清洗、分析业务和case,以及找特征。包括很多大公司不会首选用复杂的模型,有时就是一招LR打天下。
在某kaggle数据科学二分类比赛,通过有效特征的抽取,auc能提升2%(auc是评价模型好坏的常见指标之一),而通过看起来高大上的模型调参优化,auc提升只能约5‰。包括在一个电商商品推荐比赛,推荐大赛第一名的组,基于特征工程,比工程师的推荐准确 度提升16%。
通过总结和归纳,一般认为特征工程包括以下方面(下图来源于ml9学员海阔天空对特征工程一课的总结):
2 数据与特征处理
2.1 数据采集
假定我现在要预测用户对商品的下单情况(买还是不买),那我可以采集比如店家的信誉度、商品本身的评价、用户操作行为等信息,或者把这些信息交叉起来做一些组合特征。
2.2 数据格式化
首先确定存储格式。比如,时间你 年月日 or 时间戳 or 第几天 or,或者单个动作记录 or 一天行为聚合。当然,大多数情况下,需要关联非常非常多的hive表和集群文件(例如hdfs)。
2.3 数据清洗
考虑到采集到的数据可能包含一些噪声,所以数据清洗的目标就是去掉脏数据,比如用garbage in, garbage out。
当然,算法大多数时候就是一个加工机器,至于最后的产品/成品如何,很大程度上取决于原材料的好坏。而选取或改进原材料的过程会花掉一大部分时间,而效率高低取决于你对于业务的理解程度。
2.4 数据采样
很多情况下,正负样本是不均衡的。比如电商平台上,用户买和没买过的两类商品之间,数量会相差很大,因为购买需要花钱,所以大部分用户只逛不买。而大多数模型对正负样本比是敏感的(比如LR)。
那怎么处理正负样本不平衡的问题呢?为了让样本是比较均衡,一般用随机采样,和分层采样的办法。
比如如果正样本多于负样本,且量都挺大,则可以采用下采样。如果正样本大于负样本,但量不大,则可以采集更多的数据,或者oversampling(比如图像识别中的镜像和旋转),以及修改损失函数/loss function的办法来处理正负样本不平衡的问题。
2.5 特征处理
在特征处理的过程中,我们会面对各种类型的数据,比如数值型(比如年龄的大小)、类别型(比如某个品牌的口红可能有十八种色号,比如衣服大小L XL XLL,比如星期几)、时间类、文本型、统计型、组合特征等等。
2.5.1 数值型数据
针对数值型数据的特征处理有以下几种方法:幅度调整、Log等数据域变化、统计值max, min, mean, std、离散化、Hash分桶、每个类别下对应的变量统计值histogram(分布状况),以及试试 数值型 => 类别型。
接下来,重点阐述下其中几种方法。比如,把数据幅度调整到[0,1]范围内,如下代码所示:
In [36]: from sklearn import preprocessing
... import numpy as np
In [37]: X_train=np.array([[1.,-1.,2.],[2.,0.,0.],[0.,1.,-1.]])
In [38]: min_max_scaler=preprocessing.MinMaxScaler() #定标器
In [39]: X_train_minmax=min_max_scaler.fit_transform(X_train)
In [40]: X_train_minmax
Out[40]:
array([[0.5 , 0. , 1. ],
[1. , 0.5 , 0.33333333],
[0. , 1. , 0. ]])
In [4]: X_test=np.array([[-3.,-1.,4.]])
In [5]: X_test
Out[5]: array([[-3., -1., 4.]])
In [6]: X_test_minmax=min_max_scaler.transform(X_test)
In [7]: X_test_minmax
Out[7]: array([[-1.5 , 0. , 1.66666667]])
这个操作有一个专有名词,即叫归一化
为什么要归一化呢?很多同学并未搞清楚,维基百科给出的解释:1)归一化后加快了梯度下降求最优解的速度;2)归一化有可能提高精度。
下面再简单扩展解释下这两点。
1 归一化为什么能提高梯度下降法求解最优解的速度?
如下两图所示(来源:斯坦福机器学习视频)
蓝色的圈圈图代表的是两个特征的等高线。其中左图两个特征X1和X2的区间相差非常大,X1区间是[0,2000],X2区间是[1,5],像这种有的数据那么大,有的数据那么小,两类之间的幅度相差这么大,其所形成的等高线非常尖。
当使用梯度下降法寻求最优解时,很有可能走“之字型”路线(垂直等高线走),从而导致需要迭代很多次才能收敛;而右图对两个原始特征进行了归一化,其对应的等高线显得很圆,在梯度下降进行求解时能较快的收敛。
因此如果机器学习模型使用梯度下降法求最优解时,归一化往往非常有必要,否则很难收敛甚至不能收敛。
2 归一化有可能提高精度
一些分类器需要计算样本之间的距离(如欧氏距离),例如KNN。如果一个特征值域范围非常大,那么距离计算就主要取决于这个特征,从而与实际情况相悖(比如这时实际情况是值域范围小的特征更重要)。
3 归一化的类型
1)线性归一化
这种归一化方法比较适用在数值比较集中的情况。这种方法有个缺陷,如果max和min不稳定,很容易使得归一化结果不稳定,使得后续使用效果也不稳定。实际使用中可以用经验常量值来替代max和min。
2)标准差标准化
经过处理的数据符合标准正态分布,即均值为0,标准差为1,其转化函数为:
其中μ为所有样本数据的均值,σ为所有样本数据的标准差。
3)非线性归一化
经常用在数据分化比较大的场景,有些数值很大,有些很小。通过一些数学函数,将原始值进行映射。该方法包括 log、指数,正切等。需要根据数据分布的情况,决定非线性函数的曲线,比如log(V, 2)还是log(V, 10)等。
在实际应用中,通过梯度下降法求解的模型一般都是需要归一化的,比如线性回归、logistic回归、KNN、SVM、神经网络等模型。
但树形模型不需要归一化,因为它们不关心变量的值,而是关心变量的分布和变量之间的条件概率,如决策树、随机森林(Random Forest)。
接下来,我们再看下对数据做标准化的操作
In [8]: X=np.array([[1.,-1.,2.],[2.,0.,0.],[0.,1.,-1.]])
In [9]: X_scaled=preprocessing.scale(X)
In [10]: X_scaled
Out[10]:
array([[ 0. , -1.22474487, 1.33630621],
[ 1.22474487, 0. , -0.26726124],
[-1.22474487, 1.22474487, -1.06904497]])
In [11]: X_scaled.mean(axis=0)
Out[11]: array([0., 0., 0.])
In [12]: X_scaled.std(axis=0)
Out[12]: array([1., 1., 1.])
In [14]: scaler=preprocessing.StandardScaler().fit(X)
In [15]: scaler
Out[15]: StandardScaler(copy=True, with_mean=True, with_std=True)
In [16]: scaler.mean_
Out[16]: array([1. , 0. , 0.33333333])
In [17]: scaler.scale_
Out[17]: array([0.81649658, 0.81649658, 1.24721913])
In [18]: scaler.transform(X)
Out[18]:
array([[ 0. , -1.22474487, 1.33630621],
[ 1.22474487, 0. , -0.26726124],
[-1.22474487, 1.22474487, -1.06904497]])
In [19]: scaler.transform([[-1.,1.,0.]])
Out[19]: array([[-2.44948974, 1.22474487, -0.26726124]])
再比如,对数据做统计值
In [23]: df=pd.DataFrame({'one':[-0.626544,-0.138894,0.011617,np.nan],'three':[n
...: p.nan,-0.177289,0.462215,1.124472],'two':[-0.351587,1.136249,-0.448789,
...: -1.101558]},index=list('abcd'))
...:
...:
...:
In [24]: df
Out[24]:
one three two
a -0.626544 NaN -0.351587
b -0.138894 -0.177289 1.136249
c 0.011617 0.462215 -0.448789
d NaN 1.124472 -1.101558
In [25]: df.mean(0)
Out[25]:
one -0.251274
three 0.469799
two -0.191421
dtype: float64
In [26]: df.mean(1)
Out[26]:
a -0.489065
b 0.273355
c 0.008348
d 0.011457
dtype: float64
或者获取数据信息描述
In [27]: ser=pd.Series(np.random.randn(500))
In [28]: ser.describe(percentiles=[.05,.25,.75,.95])
Out[28]:
count 500.000000
mean -0.001486
std 0.964915
min -3.346455
5% -1.472339
25% -0.635061
50% -0.044945
75% 0.651877
95% 1.535528
max 2.764369
dtype: float64
或者,对数据做离散化。比如说一个人的年龄是一个连续值,但连续值放进一些模型里比如Logistic Regression则不太好使,这个时候便要对数据进行离散化,通俗理解就是把连续的值分成不同的段,每一段列成特征,从而分段处理。
In [29]: arr=np.random.randn(20)
In [30]: factor=pd.cut(arr,4)
In [31]: factor
Out[31]:
[(0.331, 1.227], (-0.566, 0.331], (-0.566, 0.331], (0.331, 1.227], (0.331, 1.227], ..., (-0.566, 0.331], (1.227, 2.124], (-0.566, 0.331], (1.227, 2.124], (0.331, 1.227]]
Length: 20
Categories (4, interval[float64]): [(-1.467, -0.566] < (-0.566, 0.331] < (0.331, 1.227] <
(1.227, 2.124]]
In [32]: factor=pd.cut(arr,[-5,-1,0,1,5])
In [33]: factor
Out[33]:
[(0, 1], (0, 1], (-1, 0], (1, 5], (0, 1], ..., (0, 1], (1, 5], (0, 1], (1, 5], (0, 1]]
Length: 20
Categories (4, interval[int64]): [(-5, -1] < (-1, 0] < (0, 1] < (1, 5]]
以及对数据做柱状分布(比例)
2.5.2 类型型数据
当我们面对一些类别型的数据,比如某品牌的口红具有多种色号,如果是人,不同的色号人类一眼即可看出。但计算机不像人的眼睛对颜色有感知,计算机只能读懂数字,严格来说,只能读懂01二进制。所以计算机为了辨别颜色,需要给各种颜色编码。
针对口红色号这种类别型数据的特征,可以使用One-hot编码/哑变量。
One-hot编码一般使用稀疏向量来节省空间,比如只有某一维取值为1,其他位置取值均为0。且需要配合特征选择来降低维度,毕竟高维度会带来一些问题
- 比如K近邻算法中,高维空间下两点之间的距离不好测量;
- 再比如logistic回归中,参数的数量会随着维度的增高而增加,容易引起过拟合问题;
- 又比如某些分类场景下,只有部分维度有用。
而针对一些文本形式的类别型数据时,也可以使用Hash技巧做一些词频统计
最后,还有一种类别型的数据,比如男性女性在很多行为上会有差别,对此,可以使用Histogram映射方法统计男女生的爱好。
男:[1/3,2/3,0]; 女:[0,1/3,2/3]; 21:[1,0,0];22:[0,0,1]…
2.5.3 时间型数据
对于时间型数据既可以看做连续值,也可以看做离散值。比如
- 这些时间型数据可以看做是连续值:持续时间(用户单页的浏览时长)、间隔时间(用户上次购买/点击离现在的时间)。
- 这些时间型数据可以看做是离散值:一天中哪个时间段(hour_0-23)、一周中星期几(week_monday...)、一年中哪个星期、一年中哪个季度、工作日/周末等方面的数据。
2.5.4 文本型数据
如果是文本类型的数据,比如词袋,则可以在文本数据预处理后,去掉停用词,剩下的词组成的list, 在词库中的映射稀疏向量。
下图所示的操作是词袋的Python处理方法
这个时候会遇到一个问题,比如李雷喜欢韩梅梅,跟韩梅梅喜欢李雷这两句话的含义是不一样的,但用上面那种词袋模型无法区分男追女还是女追男这种顺序。
为处理这个问题,可以把词袋中的词扩充到n-gram
2.5.5 文本型数据
对于文本型的数据,会经常用到Tf–idf 特征。它是用来解决这样一个问题:即怎么来判定某个词在整个文档中的重要性呢?只看它出现的频次么?好像不是,因为想的、得、地这种词通常在各篇文档中会经常出现,但不代表这类词在文档中很重要。
为了更准确的评估一字词对于一个文件集或一个语 料库中的其中一份文件的重要程度,我们会用到TF-IDF这种统计方法。字词的重要性随着它在文件中 出现的次数成正比增加,但同时会随着它在语料库中出现的频率成 反比下降。
TF: Term Frequency
TF(t) = (词t在当前文中出现次数) / (t在全部文档中出现次数)
IDF:IDF(t) = ln(总文档数/ 含t的文档数)
实际使用中,我们便经常用TF-IDF来计算权重,即TF-IDF = TF(t) * IDF(t)
对于词袋,Google于2013年提出了Word2Vec这个模型,成为目前最常用的词嵌入模型之一。
对于这个模型,现在有各种开源工具,比如:google word2vec、gensim、facebook fasttext
2.5.6 统计型数据
对于统计特征而言,历届的Kaggle/天池比赛,天猫/京东排序和推荐业务线里 模型用到的特征,比如
- 加减平均:商品价格高于平均价格多少,用户在某个品 类下消费超过平均用户多少,用户连续登录天数超过平 均多少...
- 分位线:商品属于售出商品价格的多少分位线处
- 次序型:排在第几位
- 比例类:电商中,好/中/差评比例
- 你已超过全国百分之…的同学
2.5.7 组合特征
组合特征型的数据咱们通过一个示例来说明。
之前阿里云天池(目前是七月在线的长期合作方)上有一个移动推荐算法大赛,比赛的目标是为了给移动用户在合适的时间地点下精准推荐合适的商品/内容,以提升移动用户的浏览或购买体验。
竞赛题目
在真实的业务场景下,我们往往需要对所有商品的一个子集构建个性化推荐模型。在完成这件任务的过程中,我们不仅需要利用用户在这个商品子集上的行为数据,往往还需要利用更丰富的用户行为数据。定义如下的符号:
U——用户集合
I——商品全集
P——商品子集,P ⊆ I
D——用户对商品全集的行为数据集合
那么我们的目标是利用D来构造U中用户对P中商品的推荐模型。数据说明
竞赛数据包含两个部分。第一部分是用户在商品全集上的移动端行为数据(D),表名为tianchi_mobile_recommend_train_user,包含如下字段:
对于这个问题,我们要做大量的数据处理,比如:
- 前一天的购物车商品很有可能第二天就被购买 => 规则
- 剔除掉在30天里从来不买东西的人 => 数据清洗
- 加车N件,只买了一件的,剩余的不会买=> 规则
- 购物车购买转化率 =>用户维度统计特征
- 商品热度 =>商品维度统计特征
- 对不同item点击/收藏/购物车/购买的总计 =>商品维度统计特征
- 变热门的品牌/商品 =>商品维度统计特征(差值型)
- 对不同item点击/收藏/购物车/购买平均每个user的计数=>用户维 度统计特征
- 最近第1/2/3/7天的行为数与平均行为数的比值 =>用户维度统计 特征(比例型)
- 商品在类别中的排序 =>商品维度统计特征(次序型)
- 商品交互的总人数 =>商品维度统计特征(求和型)
- 商品的购买转化率及转化率与类别平均转化率的比值=>商品维度统 计特征(比例型)
- 商品行为/同类同行为均值=>商品维度统计特征(比例型)
- 最近1/2/3天的行为(按4类统计)=>时间型+用户维度统计特征
- 最近的交互离现在的时间=>时间型
- 总交互的天数=>时间型
- 用户A对品牌B的总购买数/收藏数/购物车数=>用户维度统计特征
- 用户A对品牌B的点击数的平方 =>用户维度统计特征
- 用户A对品牌B的购买数的平方=>用户维度统计特征
- 用户A对品牌B的点击购买比=>用户维度统计特征(比例型)
- 用户交互本商品前/后,交互的商品数=>时间型+用户维度统计特征
- 用户前一天最晚的交互行为时间=>时间型
- 用户购买商品的时间(平均,最早,最晚)=>时间型
而有些特征也可以做一下组合,比如以下一些拼接型的简单组合特征
- 简单组合特征:拼接型
- user_id&&category: 10001&&女裙 10002&&男士牛仔
- user_id&&style: 10001&&蕾丝 10002&&全棉
包括实际电商点击率预估中:正负权重,喜欢&&不喜欢某种类型
以及一些模型特征组合
- 用GBDT产出特征组合路径
- 组合特征和原始特征一起放进LR训练
- 最早Facebook使用的方式,多家互联网公司在用
包括另一种基于树模型的组合特征:GBDT+LR,每一条分支都可以是一个特征,从而学习出来一系列组合特征。
3 特征选择
通过上一节的各种特征处理方法,可以产生很多特征,但会有些问题,比如
- 冗余:部分特征的相关度太高了,消耗计算性能
- 噪声:部分特征是对预测结果有负影响
针对这两个问题,咱们便得做下特征选择,包括降维
- 特征选择是指踢掉原本特征里和结果预测关系不大的
- SVD或者PCA确实也能解决一定的高维度问题
接下来,咱们了解下各种特征选择的方式。
3.1 过滤型
评估单个特征和结果值之间的相关程度,排序留下Top相关的特征部分。而计算相关程度可以用Pearson相关系数、互信息、距离相关度来计算。
这种方法的缺点是:没有考虑到特征之间的关联作用,可能把有用的关联特征误踢掉。
import numpy as np
from scipy.stats import pearsonr #计算Pearson相关系数和用于测试非相关性的p值-皮尔逊相关系数
np.random.seed(0)
x=np.random.normal(0,1,size=300)
print(pearsonr(x,x+np.random.normal(0,1,size=300)))
print(pearsonr(x,x+np.random.normal(0,100,size=300)))
过滤型特征选择Python包
from sklearn.datasets import load_iris
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
iris=load_iris()
X,y=iris.data,iris.target
print(X.shape)
X_new=SelectKBest(chi2,k=2).fit_transform(X,y) #k表示降成二维数据
print(X_new.shape)
3.2 包裹型
包裹型是指把特征选择看做一个特征子集搜索问题,筛选各种特征子集,用模型评估效果。典型的包裹型算法为 “递归特征删除算法”(recursive feature elimination algorithm)。
比如用逻辑回归,怎么做这个事情呢?
- 用全量特征跑一个模型
- 根据线性模型的系数(体现相关性),删掉5-10%的弱特征,观察准确率/auc的变化
- 逐步进行,直至准确率/auc出现大的下滑停止
包裹型特征选择Python包
from sklearn.feature_selection import RFE
from sklearn.linear_model import LinearRegression
boston=load_boston() #被墙,表示下载boston数据
X=boston['data']
Y=boston['target']
names=boston['feature']
lr=LinearRegression
rfe=RFE(lr,n_feature_to_select=1)
rfe.fit(X,Y)
print('features sorted by their rank {} :'.format(sorted(zip(map(lambda x:round(x,4),rfe.ranking_),names))))
3.3 嵌入型
嵌入型特征选择是指根据模型来分析特征的重要性(有别于上面的方式, 是从生产的模型权重等)。最常见的方式为用正则化方式来做特征选择。
且慢,什么是正则化?正则化一般分为两种:L1正则化和L2正则化。
一般回归分析中回归w表示特征的系数,从上式可以看到正则化项是对系数做了处理(限制)。L1正则化和L2正则化的说明如下:
L1正则化是指权值向量w中各个元素的绝对值之和,通常表示为||w||1
L2正则化是指权值向量w中各个元素的平方和然后再求平方根(可以看到Ridge回归的L2正则化项有平方符号),通常表示为||w||2一般都会在正则化项之前添加一个系数,Python中用α表示,一些文章也用λ表示。这个系数需要用户指定。
那添加L1和L2正则化有什么用?
L1正则化可以产生稀疏权值矩阵,即产生一个稀疏模型,可以用于特征选择
L2正则化可以防止模型过拟合(overfitting)。当然,一定程度上,L1也可以防止过拟合稀疏模型与特征选择
上面提到L1正则化有助于生成一个稀疏权值矩阵,进而可以用于特征选择。为什么要生成一个稀疏矩阵?稀疏矩阵指的是很多元素为0,只有少数元素是非零值的矩阵,即得到的线性回归模型的大部分系数都是0. 通常机器学习中特征数量很多,例如文本处理时,如果将一个词组(term)作为一个特征,那么特征数量会达到上万个(bigram)。
在预测或分类时,那么多特征显然难以选择,但是如果代入这些特征得到的模型是一个稀疏模型,表示只有少数特征对这个模型有贡献,绝大部分特征是没有贡献的,或者贡献微小(因为它们前面的系数是0或者是很小的值,即使去掉对模型也没有什么影响),此时我们就可以只关注系数是非零值的特征。这就是稀疏模型与特征选择的关系。
举个例子,最早在电商用LR做CTR预估,在3-5亿维的系数 特征上用L1正则化的LR模型。剩余2-3千万的feature,意 味着其他的feature重要度不够。
嵌入型特征选择Python包
from sklearn.svm import LinearSVC #线性支持向量分类,与参数kernel ='linear'的SVC类似,但是以liblinear而不是libsvm的形式实现,因此它在惩罚和损失函数的选择方面具有更大的灵活性,并且应该更好地扩展到大量样本。
from sklearn.datasets import load_iris #加载并返回虹膜数据集(分类),虹膜数据集是一个经典且非常简单的多类分类数据集。
from sklearn.feature_selection import SelectFromModel #元变换器,用于根据重要性权重选择特征。
iris=load_iris()
X,y=iris.data,iris.target
lsvc=LinearSVC(C=0.01,penalty='l1',dual=False).fit(X,y)
model=SelectFromModel(lsvc,prefit=True)
X_new=model.transform(X)
X_new