该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
time: 2022-02-08
学习目标:“通过Python示例掌握特征工程基本原则和实际应用,增强机器学习算法效果”;
为了提取知识和做出预测,机器学习使用数学模型来拟合数据;输入即特征,指原始数据某些方面的数值表示(注意:原始数据不一定是数值型数据);
特征工程:指从原始数据中提取特征并将其转换为适合机器学习模型的格式;是机器学习中的一个环节;
建立机器学习流程的绝大部分时间都耗费在特征工程和数据清洗上;
本书每章阐述了一个数据问题,结合这些问题,对特征工程的一些基本原则进行说明;
数据反应小部分现实,综合起来才能得到完整的描述;描述散乱,由千万小段组成,而且总是存在测量噪声和缺失值;
数据处理流程往往是多阶段的迭代过程;
两个构成机器学习基础的数学实体:模型和特征;
统计模型:错误数据、冗余数据、缺失数据;
特征
是原始数据的数值表示;正确的特征应该适合当前的任务,并易于被模型所使用;
特征工程
就是在给定数据、模型和任务的情况下设计出最适合的特征的过程;
模型和特征相辅相成,对其中一个的选择会影响另一个;好的特征可以使建模更容易;
数值型数据作为最简单的数据类型(相较于文本、图像而言),也最容易被数学模型所使用,但往往数值型数据也需要进行特征工程;
好的特征既能表示出数据的主要特点,还应该符合模型的假设,因而通常需要进行数据转换;
数值型数据的特征工程很基本,只要原始数据可以转换为数值型特征,就可以应用这些技术;
模型是输入特征的平滑函数,那么它对输入的尺度很敏感:k-均值、最近邻、径向基核函数,以及所有使用欧式距离的方法都属于这种,对于这类模型和模型成分,通常需要对特征进行标准化
,以便将输出控制在期望的范围内;
逻辑函数则对输入特征的尺度并不敏感;无论输入如何,这种函数输出总是一个二值变量;另一个例子是阶梯函数(决策树模型中使用了输入特征的阶梯函数);
基于空间分割树的模型(决策树、梯度提升机、随机森林)对尺度是不敏感的;但如果特征是某种累计值,最终可能会超出训练树的取值范围,因此需要定期对输入尺度进行调整;
这种分布表现出一个特征值出现的概率;输入特征的分布对于某些模型来说十分重要;
线性回归模型的训练过程,需要假定预测误差近似地服从高斯分布;在预测目标分布在多个数量级中时,误差符合高斯分布的假定将不会被满足,一种解决方法是对输出目标进行转换,如对数变换就可以使变量的分布更加接近于高斯分布;
除了将特征转换为模型所需或训练假设,还可以将多个特征进行组合;我们希望特征更具信息量(更简洁的捕获原始数据中的重要信息),这样可以使模型本身更简单,更容易训练和评价,也能做出更好的预测;
极端情况,可以使用统计模型的输出作为复杂特征,这种思想称为模型堆叠
;
模型的输入通常表示为 数值向量
在数据世界中,抽象的向量和它的特征维度具有实际意义,所有数据的集合可以在特征空间
中形象地表示为一个点云;与在特征空间
(使用特征维度)表示数据类似,也可以在数据空间
(使用数据维度)中进行特增表示;
二值化
:
强壮
指标;(如 我们不能认为收听了某歌曲20次的人喜欢该歌的程度肯定是收听了10次的人的两倍)一种更强壮的用户偏好表示方法是 将收听次数二值化
:
这是一个对模型目标变量处理的例子,值得注意的是,目标变量并不是特征,因为它不是输入;
但为了正确地解决问题,有时确实需要修改目标变量
;
区间量化(分箱)
:
原始点评数量的直方图可视化:
import pandas as pd
import json
biz_file - open("yelp_academic_dataset_business.json")
biz_df = pd.DataFrame([json.loads(x) for x in biz_file.readlines()])
biz.file.close()
#
import matplotlib.pyplot as plt
import seaborn as sns
# 绘制点评数量直方图
sns.set_style('whitegrid')
fig, ax = plt.subplots()
biz_df["review_count"].hist(ax=ax, bins = 100)
ax.set_yscale('log')
ax.tick_params(labelsize=14)
ax.set_xlabel("Review Count", fontsize=14)
ax.set_ylabel("Occurrence", fontsize=14)
一种解决方法是对计数值进行区间量化
,然后在使用量化后的结果:
密度的测量
;对数据进行区间量化,首先需要确定每个分箱的宽度,通常有两种方法:
固定宽度分箱
线性
的,也可以是指数
性的;一个通过固定宽度分箱对计数值进行区间量化的代码段:
import numpy as np
small_counts = np.random.randint(0, 100, 20)
# 通过除法随机映射到间隔均匀的分箱中,每个分箱取值范围为0~9
np.floor_divide(small_counts, 10) # 整除 抛开余数
# 横跨若干个数量级的技术数组
large_counts = [296,8286,64011,80,3,725,867,2215,7689,11495,91897,...44,28,7971,926,122,22222]
np.floor(np.log10(large_counts)) # 向下取整
分箱操作实际是将原始数值 拉回同一量级 以便处理;
分位数分箱
分位数
是可以将数据划分为相等的若干份数的值;对应回直方图上,可以看出,分位数的数值是向较小的计数值偏斜的;
点评数量十分位数的代码示例:
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
small_counts = np.random.randint(0, 10000, 200)
small_counts_log = np.log10(small_counts)
biz_df = pd.DataFrame({'review_count':small_counts,'log_review_count':small_counts_log})
deciles = biz_df["review_count"].quantile([.1,.2,.3,.4,.5,.6,.7,.8,.9])
sns.set_style("whitegrid")
fig, ax = plt.subplots()
biz_df["review_count"].hist(ax=ax, bins=10)
# 在直方图上画出十个分位数
for pos in deciles:
print('pos', pos)
handle = plt.axvline(pos, color="r")
ax.legend([handle], ['deciles'], fontsize=14)
# ax.set_yscale('log')
# ax.set_xscale('log')
ax.tick_params(labelsize=14)
ax.set_xlabel("Review Count", fontsize=14)
ax.set_ylabel("Occurence", fontsize=14)
计算分位数并将数据映射到分位数分箱中,可以使用Pandas库:
pandas.DataFrame.quantile
、pandas.Series.quantile
可以计算分位数;pandas.qcut
可以将数据映射为所需的分位数值;import pandas as pd
# 将计数值映射为分位数
pd.qcut(large_counts, 4 , labels=False)
# 计算实际的分位数值
large_counts_series = pd.Series(large_counts)
large_counts_series.quantile( [0.25,0.5,0.75] )
之前简要提到了 通过取计数值的对数将数据映射到指数宽度分箱的方法,我们继续;
对数函数是指数函数的反函数:㏒a(b) = c, 则 a^c = b
(0,1)
这个小区间的数映射到(-∞,0)
这个包括全部负数的大区间上;可以将区间
[1,10]映射到
[0,1],将
[10,100]映射到
[1,2]`;对数函数可以对大数值的范围进行压缩,对小数值的范围进行扩展
;对于重尾分布
(质量更多的分布在尾部)的整数值的处理,对数变换是一个非常强大的工具;
经过对数变换,评论数据的直方图在低计数值的集中趋势会被减弱:
fig, (ax1, ax2) = plt.subplots(2,1)
biz_df["review_count"].hist(ax=ax1, bins=10)
ax1.tick_params(labelsize=14)
ax1.set_xlabel("review_count", fontsize=14)
ax1.set_ylabel("Occurrence", fontsize=14)
biz_df["log_review_count"].hist(ax=ax2, bins=10)
ax2.tick_params(labelsize=14)
ax2.set_xlabel('log10(review_count)', fontsize=14)
ax2.set_ylabel('Occurrence', fontsize=14)
实战过程:
使用商家点评数量 来预测 商家的平均评分
使用文章单词数量 来预测 文章流行程度
预测输出是连续值,可以使用简单线性回归来构造模型;
使用scikit-learn,对 进行了对数变换和未进行对数变换的特征上 进行10-折交叉验证的线性回归;
使用R2分数评价模型,以衡量训练出来的回归模型预测新数据的能力(R方分数越大越好,完美模型的R方值为1,它也可以是负数,说明模型很糟糕);
# 实例1
import pandas as pd
import numpy as np
import json
from sklearn import linear_model
from sklearn.model_selection import cross_val_score
# 提前计算对数变换值,且对点评数量+1,以避免当点评数为0时,对数运算结果得到负无穷大
biz_df["log_review_count"] = np.log10(biz_df["review_count"] + 1)
# 训练线性回归模型
m_orig = linear_model.LinearRegression()
scores_orig = cross_val_score(m_orig, biz_df[["review_count"]], biz_df["stars"], cv=10)
m_log = linear_model.LinearRegression()
scores_log = cross_val_score(m_log, biz_df[["log_review_count"]], biz_df["stars"], cv=10)
print("R-squared score without log transform: %.5f (+/- %.5f)"%(scores_orig.mean(), scores_orig.std() * 2))
print("R-squared score with log transform: %.5f (+/- %.5f)"%(scores_log.mean(), scores_log.std() * 2))
# 平均星级(离散) 与 评价数量 的关系 原非线性
# 实例2
# 新闻流行程度预测问题 输入和输出相关性可视化
fig2, (ax1, ax2) = plt.subplots(2,1)
ax1.scatter(df["n_tokens_count"],df["shares"])
ax1.tick_params(labelsize=14)
ax1.set_xlabel("Number of Words in Article", fontsize=14)
ax1.set_ylabel('Number of Shares', fontsize=14)
ax2.scatter(df['log_n_tokens_count'],df["shares"])
ax2.tick_params(labelsize=14)
ax1.set_xlabel("Log of the Number of Words in Article", fontsize=14)
ax1.set_ylabel('Number of Shares', fontsize=14)
值得关注的是 数据的可视化十分重要,比如 上面选择的线性回归模型 与 可视化的图形中得出数据的输入和目标之间关系是否相符;在构建模型时,使用可视化方法查看一下输入和输出之间以及各个输入特征之间的关系是一种非常好的做法;
指数变换
:
方差稳定化变换
;泊松分布(了解):
λ
表示泊松分布的均值,当其变大时,不仅整个分布向右移动,质量也更分散,方差随之变大;
平方根变换和对数变换都可以简单推广为Box-Cox
变换:
= (x^λ - 1)/λ
, 当(λ
!= 0)
= ln(x)
, 当(λ
= 0)
λ
=0 即对数变换,λ
=0.25或0.5(对应的是平方根变换的一种缩放和平移形式);
λ
值小于1时,可以压缩高端值,大于1时,起的作用相反;
只有当数据为
正
时,Box-Cox公式才有效;对非正数据,可以加一个固定常数,对数据进行平移;
可以通过极大似然方法(找到能使变换后信号的高斯似然最大化的λ
值)找到λ
,SciPy
的stats
包中有Box-Cox变换的实现方法,并包括找到最优BoxCox变换
参数的功能;
from scipy import stats
# 检查最小值 Box-Cox假定输入数据为正
biz_df['review_count'].min()
rc_log = stats.boxcos(biz_df['review_count'], lmbda=0) # 指定 λ = 0,此时对应的是对数变换
# 默认 进行Box-Cox变换时 会找到使得输出最接近正太分布的λ值
rc_bc, bc_params = stats.boxcox(biz_df['review_count'])
bc_params # 即 最终的 λ 值
概率图(probplot)
:
原始点评数量的概率图具有明显的重尾特征,相比对数变换,最优Box-Cox变换对尾部的压缩更强:
prob1 = stats.probplot(biz_df['review_count'], dist=stats.norm, plot=ax1)
prob1 = stats.probplot(biz_df['rc_log'], dist=stats.norm, plot=ax2)
prob1 = stats.probplot(biz_df['rc_bc'], dist=stats.norm, plot=ax3)
更多的图形绘制信息 参考matplotlib
如果模型对输入特征的尺度很敏感,就需要进行特征缩放;特征缩放可以改变特征的尺度,说是些人将之称为特征归一化
;
特征缩放通常对每个特征独立进行,一下是集中常用的特征缩放,他们会产生不同的特征分布;
min-max
缩放:
min-max
缩放 可以将所有特征值压缩(或扩展)到[0,1]
之间;x_n = (x-min)/(max-min)
;特征标准化/方差缩放
:
x_n = (x - mean)/sqrt(var)
;在稀疏特征上执行
min-max
缩放和标准化的时候要慎重,他们都会从原始特征值中减去一个量;对于min-max
缩放平移量是最小值,对于标准化,这个量是均值;
如果平移量不是0,那么这两种变换会将一个多数元素为0的稀疏特征向量变成密集特征向量;
词袋就是一种稀疏的表示方式,大多数分类算法的实现都针对稀疏输入进行了优化;
归一化
:
l2
范数的量,l2
范数又称为欧几里得范数
;x_n = x / (||x||2)
l2
范数是坐标空间中向量长度的一种测量;
l2 = ||x||2 = (x1^2 + x2^2 +... + xm^2)^(1/2)
l2
规范化后,特征列的范数就是1,有时候这种处理也称为l2
缩放;注意:除了可以对特征进行
l2
归一化,也可以对数据点进行l2
归一化,最终会得到带有单位范数(1)的数据向量;
特征缩放总是将特征除以一个常数(即归一化常数);因而他不会改变单特征分布的形状
as_matrix
方法的作用:
很多时候在提取完数据后其自身就是数组形式(),这只是习惯性的谨慎。很多时候取得的数据是DataFrame的形式,这个时候要记得转换成数组;
x = datas.iloc[:,:].as_matrix()
not predict
x = datas.iloc[:,:].values
predict
特征缩放的示例代码:
import pandas as pd
import sklearn.preprocessing as preproc
df = pd.read_csv("OnlineNewsPopularity.csv", delimiter=", ")
print(df["n_token_content"].as_matrix())
# min-max缩放
df['minmax'] = preproc.minmax_scale(df[['n_token_content']])
df['minmax'].as_matrix() # 或 .values
# 标准化
df['standardized'] = preproc.StandardScaler().fit_transform(df[['n_token_content']])
df['standardized'].as_matrix()
# L2归一化
df['l2_normalized'] = preproc.normalize(df[['n_token_content']], axis=0)
df['standardized'].as_matrix()
当一组输入特征的尺度相差很大,就会对模型的训练算法带来数值稳定性方面的问题,因此需要对特征进行缩放;
两个特征的乘积可以组成一对简单的交互特征
,这种关系类比与逻辑运算AND;
表示由一对条件行程的结果:地区A and 年龄B
这种特征在决策树中极其常见,广义线性模型中也常用;
y = w1x1 + ... + wnxn
y = w1x1 + ... + wnxn + w11x1x1 + w12x1x2 + ...
这样可以捕获特征间交互作用,这些特征对 就称为交互特征
如果x1和x2是二值特征,那么他们的积就是逻辑与;
如果我们的问题是基于客户档案信息来预测客户偏好,那么除了更具用户年龄或地点这些单独的特征来进行预测,还可以使用交互特征来根据用户位于某个年龄段并位于某个特征的地点来进行预测;
# 交互特征示例
from sklearn import linear_model
from sklearn.model_selection import train_test_split
import sklearn.preprocessing as preproc
# 假设一个pandas数据框
df.columns # 包含许多特征列
# 选择与内容有关的特征作为模型的单一特征,忽略那些衍生特征
features = ['n_token_title','n_token_content','num_videos']
X = df[features]
y = df[['shares']]
# 创建交互特征 (跳过固定偏移项)
X2 = preproc.PolynomialFeatures(include_bias=False).fit_transform(X)
# 为两个特征集 创建 训练集和测试集
X1_train, X1_test, X2_train, X2_test, y_train_, y_test = train_test_split(X,X2,y, test_size=0.3, random_state=123)
def evaluate_feature(X_train, X_test, y_train, y_test):
model = linear_model.LinearRegression().fit(X_train, y_train)
r_score = model.score(X_test, y_test)
return (model, r_score)
(m1, r1) = evaluate_feature(X1_train, X1_test,y_train_, y_test)
(m2, r2) = evaluate_feature(X2_train, X2_test,y_train_, y_test)
print("R-squared score with singleton features: %.5f"% r1)
print("R-squared score with pairwise features: %.10f"% r2)
交互特征虽然构造简单,但代价并不低,如果线性模型中包含有交互特征,那么它的训练时间复杂度会从O(N)增加到O(N^2),其中n是单一特征的数量;
精心设计的复杂特征需要昂贵的成本,所以数量不能太多,它们可以减少模型的训练时间,但特征本身会消耗很多计算能力,这增加了模型评分阶段的计算成本;(第8章会介绍若干复杂特征的示例)
特征选择技术可以精简掉无用的特征,以降低最终模型的复杂性,它的最终目的是得到一个简约模型,在不降低预测准确率或对预测准确率影响不大的情况下提高计算速度;
为了得到这样的模型,有些特征选择技术需要训练不止一个优选模型,换言之,特征选择不是为了减少训练时间,而是为了减少模型评分时间;
粗略的特征选择技术可分为三类:
嵌入式方法不如打包方法强大,但成本远不如打包方法那么高;相比于过滤技术,嵌入式方法可以选择出特别适合某种模型的特征;从这个意义上说嵌入式方法在计算成本和结果质量之间实现了某种平衡
;
几种常用的数值型特征工程技术:区间量化、缩放(即归一化)、对数变换(指数变换的一种)和交互特征;
并简要介绍了特征选择技术,它对于处理大量交互特征是必需的;
在统计机器学习中,所有数据最终都会转化为数值型特征;因此,所有特征工程最终都会归结为某种数值型特征工程技术。