11_数据表示与特征工程
描述
前面用到的数据都是是由浮点数组成的二维数组,其中每一列是描述数据点的连续特征(continuous feature)。对于许多应用而言,数据的收集方式并不是这样。一种特别常见的特征类型就是分类特征(categorical feature),也叫离散特征(discrete feature)。这种特征通常并不是数值。分类特征与连续特征之间的区别类似于分类和回归之间的区别,只是前者在输入端而不是输出端。(前面用到的鸢尾花是典型的连续特征的例子;分类特征的例子包括产品的品牌、产品的颜色或产品的销售部门等,一件产品要么属于服装部门,要么属于图书部门。在图书和服装之间没有中间部门,不同的分类之间也没有顺序,图书不大于服装也不小于服装)。
无论数据包含哪种类型的特征,数据表示方式都会对机器学习模型的性能产生巨大影响。前面用到的数据缩放非常重要,如果没有缩放数据(比如,缩放到单位方差),那么用厘米还是英寸表示测量数据的结果将会不同。用额外的特征扩充(augment)数据也很有帮助,比如添加特征的交互项(乘积)或更一般的多项式。
对于某个特定应用来说,如何找到最佳数据表示,这个问题被称为特征工程(feature engineering)它是数据科学家和机器学习从业者在尝试解决现实世界问题时的主要任务之一。用正确的方式表示数据,对监督模型性能的影响比所选择的精确参数还要大。
分类变量
One-Hot编码(虚拟变量)
表示分类变量最常用的方法就是使用 one-hot 编码(one-hot-encoding)或者N取一编码(one-out-of-N encoding),也叫虚拟变量(dummy variable)。虚拟变量背后的思想是将一个分类变量替换为一个或多个新特征,新特征取值为 0 和 1。对于线性二分类(以及 scikit-learn 中其他所有模型)的公式而言,0 和 1 这两个值是有意义的,可以像这样对每个类别引入一个新特征,从而表示任意数量的类别。
可以使用pandas完成分类变量的 one-hot 编码(使用adult 数据集)。
比如说,workclass 特征的可能取值包括 “Government Employee”、“Private Employee”、“Self Employed” 和 “Self Employed Incorporated”。为了编码这4个可能的取值,创建了4个新特征,分别叫作 “Government Employee”、“Private Employee”、“Self Employed” 和 “Self Employed Incorporated”。如果一个人的 workclass 取某个值,那么对应的特征取值为 1,其他特征均取值为 0。因此,对每个数据点来说,4 个新特征中只有一个的取值为 1。这就是它叫作 one-hot 编码或 N 取一编码的原因。
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_splitdata = pd.read_csv(r"..\..\seaborn-data\adult.data",header=None,index_col=False,names=['age', 'workclass', 'fnlwgt', 'education', 'education-num','marital-status', 'occupation', 'relationship', 'race', 'gender','capital-gain', 'capital-loss', 'hours-per-week', 'native-country','income'])
data = data[['age', 'workclass', 'education', 'gender', 'hours-per-week','occupation', 'income']]
data_dummies = pd.get_dummies(data)
features = data_dummies.iloc[:,0:len(data_dummies.columns)-2]
X = features.values
Y = data_dummies['income_ >50K'].values
print("X.shape: {} y.shape: {}".format(X.shape, Y.shape))X_train,X_test,Y_train,Y_test = train_test_split(X,Y,random_state=0)
logreg = LogisticRegression()
logreg.fit(X_train,Y_train)
print(logreg.score(X_train,Y_train),logreg.score(X_test,Y_test))
用 pandas 编码数据有一种非常简单的方法,就是使用 get_dummies 函数。get_dummies 函数自动变换所有具有对象类型(比如字符串)的列或所有分类的列。
对同时包含训练数据和测试数据的数据框调用 get_dummies。这一点很重要,可以确保训练集和测试集中分类变量的表示方式相同。(上例中先调用的get_dummies函数,然后在返回的结果集上拆分训练集和测试集,保证了训练集和测试集的列名称相同且具有相同的语义)
使用sklearn的OneHotEncoder重新实现:
df = pd.read_csv(r"..\..\seaborn-data\adult.data",header=None,index_col=False,names=['age', 'workclass', 'fnlwgt', 'education', 'education-num','marital-status', 'occupation', 'relationship', 'race', 'gender','capital-gain', 'capital-loss', 'hours-per-week', 'native-country','income'])
df_sub = df[['age', 'workclass', 'education', 'gender', 'hours-per-week','occupation', 'income']]
list_workclass=df_sub.workclass.value_counts().index.to_list()
list_education=df_sub.education.value_counts().index.to_list()
list_gender=df_sub.gender.value_counts().index.to_list()
list_occupation=df_sub.occupation.value_counts().index.to_list()
list_hours_per_week=df_sub['hours-per-week'].value_counts().index.to_list()
list_income=df_sub.income.value_counts().index.to_list()# 生成dict
workclass_dict={}
for i,item in enumerate(list_workclass):workclass_dict[item]=ieducation_dict={}
for i,item in enumerate(list_education):education_dict[item]=igender_dict={}
for i,item in enumerate(list_gender):gender_dict[item]=ioccupation_dict={}
for i,item in enumerate(list_occupation):occupation_dict[item]=ihours_per_week_dict={}
for i,item in enumerate(list_hours_per_week):hours_per_week_dict[item]=iincome_dict={}
for i,item in enumerate(list_income):income_dict[item]=idf_sub.workclass=df_sub.workclass.map(workclass_dict)
df_sub.education=df_sub.education.map(education_dict)
df_sub.gender=df_sub.gender.map(gender_dict)
df_sub.occupation=df_sub.occupation.map(occupation_dict)
df_sub['hours-per-week']=df_sub['hours-per-week'].map(hours_per_week_dict)df_sub_X = df_sub.iloc[:,0:6]
df_sub_Y = df_sub.income.map(income_dict)from sklearn.preprocessing import OneHotEncoderone_hot = OneHotEncoder().fit(df_sub_X)
X_scaled = one_hot.transform(df_sub_X)X_train,X_test,Y_train,Y_test = train_test_split(X_scaled,df_sub_Y,random_state=0)
logreg = LogisticRegression()
logreg.fit(X_train,Y_train)
print(logreg.score(X_train,Y_train),logreg.score(X_test,Y_test))
与使用pandas处理结果差不多。建议使用OneHotEncoder,如果只需要部分列需要转换,可以通过categorical_features 参指定(默认’all’,可以设置一个数组,指定哪几列进行转换)。
数字可以编码分类变量
在 adult 数据集的例子中,分类变量被编码为字符串。一方面,可能会有拼写错误;但另一方面,它明确地将一个变量标记为分类变量。无论是为了便于存储还是因为数据的收集方式,分类变量通常被编码为整数。
例如:workclass 的回答被记录为 0(在第一个框打勾)、1(在第二个框打勾)、2(在第三个框打勾)等等。现在该列包含数字 0 到 8,而不是像 “Private” 这样的字符串。如果有人观察表示数据集的表格,很难一眼看出这个变量应该被视为连续变量还是分类变量。如果知道这些数字表示的是就业状况,那么很明显它们是不同的状态,不应该用单个连续变量来建模。
pandas 的 get_dummies 函数将所有数字看作是连续的,不会为其创建虚拟变量。为了解决这个问题,可以使用 scikit-learn 的 OneHotEncoder,指定哪些变量是连续的、哪些变量是离散的,也可以将数据框中的数值列转换为字符串。
demo_df = pd.DataFrame({'integer feature':[0,1,2,1],'string feature':['socks','fox','socks','box']
})
pd.get_dummies(demo_df) # 使用 get_dummies 只会编码字符串特征,不会改变整数特征
demo_df['integer feature'] = demo_df['integer feature'].astype(str) # 转变类型
pd.get_dummies(demo_df,columns=['integer feature','string feature'])
分箱、离散化、线性模型与树
数据表示的最佳方法不仅取决于数据的语义,还取决于所使用的模型种类。线性模型与基于树的模型(比如决策树、梯度提升树和随机森林)是两种成员很多同时又非常常用的模型,它们在处理不同的特征表示时就具有非常不同的性质。
使用 wave 回归数据集,它只有一个输入特征。下面是线性回归模型与决策树回归在这个数据集上的对比:
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
import mglearn
import matplotlib.pyplot as pltX,Y = mglearn.datasets.make_wave(n_samples=100)
line = np.linspace(-3,3,1000,endpoint=False).reshape(-1,1)
reg = DecisionTreeRegressor(min_samples_split=3).fit(X,Y)
plt.plot(line,reg.predict(line),label='decision tree')reg = LinearRegression().fit(X,Y)
plt.plot(line,reg.predict(line),label='linear regression',ls='--')plt.plot(X[:,0],Y,'o',c='k')
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.legend(loc='best')
线性模型只能对线性关系建模,对于单个特征的情况就是直线。决策树可以构建更为复杂的数据模型,但这强烈依赖于数据表示。有一种方法可以让线性模型在连续数据上变得更加强大,就是使用特征分箱(binning,也叫离散化,即 discretization)将其划分为多个特征。
假设将特征的输入范围(在这个例子中是从 -3 到 3)划分成固定个数的箱子(bin),比如 10 个,那么数据点就可以用它所在的箱子来表示。为了确定这一点,我们首先需要定义箱子。在这个例子中,我们在 -3 和 3 之间定义 10 个均匀分布的箱子。用np.linspace 函数创建 11 个元素,从而创建 10 个箱子,即两个连续边界之间的空间:
bins = np.linspace(-3,3,11)
print("bins:{}".format(bins))
第一个箱子包含特征取值在 -3 到 -2.4 之间的所有数据点,第二个箱子包含特征取值在 -2.4 到 -1.8 之间的所有数据点,以此类推。
from sklearn.preprocessing import OneHotEncoder# 这里做的是将 wave 数据集中单个连续输入特征变换为一个分类特征,用于表示数据点所在的箱子。
which_bin = np.digitize(X,bins=bins) #用于返回输入数组中每个值所属的箱子(bin)的索引。这个函数非常有用,尤其是在将数据分组到不同的区间时。encoder = OneHotEncoder(sparse_output=False)
encoder.fit(which_bin)
X_binned = encoder.transform(which_bin)
line_binned = encoder.transform(np.digitize(line,bins=bins))reg = LinearRegression().fit(X_binned,Y)
plt.plot(line,reg.predict(line_binned),label='linear regression binned',ls='--')reg = DecisionTreeRegressor(min_samples_split=3).fit(X_binned,Y)
plt.plot(line,reg.predict(line_binned),label='decision tree binned')
plt.plot(X[:,0],Y,'o',c='k')
plt.vlines(bins,-3,3,linewidth=1,alpha=0.2)
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.legend(loc='best')
虚线和实线完全重合,说明线性回归模型和决策树做出了完全相同的预测。对于每个箱子,二者都预测一个常数值。因为每个箱子内的特征是不变的,所以对于一个箱子内的所有点,任何模型都会预测相同的值。
比较对特征进行分箱前后模型学到的内容,线性模型变得更加灵活了,因为现在它对每个箱子具有不同的取值,而决策树模型的灵活性降低了。
分箱特征对基于树的模型通常不会产生更好的效果,因为这种模型可以学习在任何位置划分数据。从某种意义上来看,决策树可以学习如何分箱对预测这些数据最为有用。此外,决策树可以同时查看多个特征,而分箱通常针对的是单个特征。不过,线性模型的表现力在数据变换后得到了极大的提高。
对于特定的数据集,如果有充分的理由使用线性模型——比如数据集很大、维度很高,但有些特征与输出的关系是非线性的——那么分箱是提高建模能力的好方法。
交互特征与多项式特征
想要丰富特征表示,特别是对于线性模型而言,另一种方法是添加原始数据的交互特征(interaction feature)和多项式特征(polynomial feature)。这种特征工程通常用于统计建模,也常用于许多实际的机器学习应用中。
线性模型对 wave 数据集中的每个箱子都学到一个常数值。但线性模型不仅可以学习偏移,还可以学习斜率。想要向分箱数据上的线性模型添加斜率,一种方法是重新加入原始特征。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import mglearn
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.preprocessing import OneHotEncoder
import warnings
warnings.filterwarnings('ignore')X,Y = mglearn.datasets.make_wave(n_samples=100)
line = np.linspace(-3,3,1000,endpoint=False).reshape(-1,1)
bins = np.linspace(-3,3,11)
which_bin = np.digitize(X,bins=bins) #用于返回输入数组中每个值所属的箱子(bin)的索引。这个函数非常有用,尤其是在将数据分组到不同的区间时。
encoder = OneHotEncoder(sparse_output=False)
encoder.fit(which_bin)
X_binned = encoder.transform(which_bin)
line_binned = encoder.transform(np.digitize(line,bins=bins))#在x轴上添加原始数据,到 11 维的数据集
X_combined = np.hstack([X,X_binned]) # 在水平方向上平铺(拼接)
print(X_combined.shape)reg = LinearRegression().fit(X_combined,Y)
line_combined = np.hstack([line,line_binned])plt.plot(line,reg.predict(line_combined),label='linear regression combined')
plt.vlines(bins,-3,3,colors='k',alpha=0.2)plt.legend(loc='best')
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.plot(X[:,0],Y,'o',c='k')
模型在每个箱子中都学到一个偏移,还学到一个斜率。学到的斜率是向下的,并且在所有箱子中都相同——只有一个 x 轴特征,也就只有一个斜率。因为斜率在所有箱子中是相同的,所以它似乎不是很有用。
为了让每个箱子都有一个不同的斜率,添加交互特征或乘积特征,用来表示数据点所在的箱子以及数据点在 x 轴上的位置。这个特征是箱子指示符与原始特征的乘积。
X_product = np.hstack([X_binned,X*X_binned])
print(X_product.shape)
reg = LinearRegression().fit(X_product,Y)
line_product = np.hstack([line_binned,line*line_binned])plt.plot(line,reg.predict(line_product),label='linear regression product')
plt.vlines(bins,-3,3,colors='k',alpha=0.2)plt.legend(loc='best')
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.plot(X[:,0],Y,'o',c='k')
现在这个模型中每个箱子都有自己的偏移和斜率。
使用分箱是扩展连续特征的一种方法。另一种方法是使用原始特征的多项式(polynomial)。对于给定特征x,可以考虑x**2、x**3、x**4,等等。这在preprocessing模块的PolynomialFeatures中实现:
from sklearn.preprocessing import PolynomialFeatures# 包含直到x ** 10的多项式:
# 默认的"include_bias=True"添加恒等于1的常数特征
poly = PolynomialFeatures(degree=10,include_bias=False)
poly.fit(X)
X_poly = poly.transform(X)
print("X_poly.shape:{}".format(X_poly.shape))# 调用 get_feature_names_out 方法来获取特征的语义,给出每个特征的指数:
print("Polynomial feature names:\n{}".format(poly.get_feature_names_out()))# 将多项式特征与线性回归模型一起使用,可以得到经典的多项式回归(polynomialregression)模型
reg = LinearRegression().fit(X_poly, Y)
line_poly = poly.transform(line)
plt.plot(line,reg.predict(line_poly),label='polynomial linear regression')
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.plot(X[:,0],Y,'o',c='k')
plt.legend(loc='best')
多项式特征在这个一维数据上得到了非常平滑的拟合。但高次多项式在边界上或数据很少的区域可能有极端的表现。
作为对比,下面是在原始数据上学到的核 SVM 模型,没有做任何变换:
from sklearn.svm import SVR for gamma in [1,10]:svr = SVR(gamma=gamma).fit(X,Y)plt.plot(line,svr.predict(line),label='SVR gamma={}'.format(gamma))plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.plot(X[:,0],Y,'o',c='k')
plt.legend(loc='best')
使用更加复杂的模型(即核 SVM),我们能够学到一个与多项式回归的复杂度类似的预测结果,且不需要进行显式的特征变换。
再次观察波士顿房价数据集,作为对交互特征和多项式特征更加实际的应用(对比添加多项式和不添加多项式对性能的影响):
from mglearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScalerboston = load_boston()X_train,X_test,Y_train,Y_test = train_test_split(boston.data,boston.target,random_state=42)scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)poly = PolynomialFeatures(degree=2).fit(X_train_scaled) # 提取多项式特征和交互特征,次数最高为 2
X_train_poly = poly.fit_transform(X_train)
X_test_poly = poly.transform(X_test)from sklearn.linear_model import Ridgeridge = Ridge().fit(X_train_scaled,Y_train)
print("Score without interactions:{:f}".format(ridge.score(X_test_scaled, Y_test)))ridge = Ridge().fit(X_train_poly,Y_train)
print("Score with interactions:{:f}".format(ridge.score(X_test_poly, Y_test)))# Score without interactions:0.691730
# Score with interactions:0.775163
这里 degree=2 的意思是,我们需要由最多两个原始特征的乘积组成的所有特征。
在使用 Ridge 时,交互特征和多项式特征对性能有很大的提升。但如果使用更加复杂的模型(比如随机森林),情况会稍有不同:
from sklearn.ensemble import RandomForestRegressorrf = RandomForestRegressor(n_estimators=100).fit(X_train_scaled,Y_train)
print("Score without interactions:{:f}".format(rf.score(X_test_scaled, Y_test)))
rf = RandomForestRegressor(n_estimators=100).fit(X_train_poly,Y_train)
print("Score with interactions:{:f}".format(rf.score(X_test_poly, Y_test)))# Score without interactions:0.867132
# Score with interactions:0.852042
即使没有额外的特征,随机森林的性能也要优于 Ridge。添加交互特征和多项式特征实际上会略微降低其性能。
单变量非线性变换
添加特征的平方或立方可以改进线性回归模型。其他变换通常也对变换某些特征有用,特别是应用数学函数,比如 log、exp 或 sin。虽然基于树的模型只关注特征的顺序,但线性模型和神经网络依赖于每个特征的尺度和分布。
如果在特征和目标之间存在非线性关系,那么建模就变得非常困难,特别是对于回归问题。log 和 exp 函数可以帮助调节数据的相对比例,从而改进线性模型或神经网络的学习效果,前面对内存价格数据应用过这种函数。在处理具有周期性模式的数据时,sin 和 cos 函数非常有用。
大部分模型都在每个特征(在回归问题中还包括目标值)大致遵循高斯分布时表现最好,也就是说,每个特征的直方图应该具有类似于熟悉的“钟形曲线”的形状。使用诸如 log和 exp 之类的变换并不稀奇,但却是实现这一点的简单又有效的方法。处理整数计数数据时,这样的变换非常有用。
模拟的计数数据集,其性质与在自然状态下能找到的数据集类似。特征全都是整数值,而响应是连续的:
rnd = np.random.RandomState(0) # 是一个伪随机数生成器的容器,它提供了多种方法来从不同的概率分布中生成随机数。# 通过指定一个种子,可以创建一个RandomState实例,这样每次生成的随机数序列都是可预测的。
X_org = rnd.normal(size=(1000,3)) # 从正态分布中抽取样本。# binomial 从二项分布中抽取样本。# poisson 从泊松分布中抽取样本。
w = rnd.normal(size=3)X = rnd.poisson(10*np.exp(X_org)) # 从泊松分布中抽取样本。
Y = np.dot(X_org,w) # 两个数组的点积,即元素对应相乘。bins = np.bincount(X[:,0])
plt.bar(np.array(range(len(bins))),bins,color='c')
plt.xlabel("value")
plt.ylabel("Number of appearances")
在直方图中,可以清楚的看出每个值的出现次数。
特征 X[:, 1] 和 X[:, 2] 具有类似的性质。这种类型的数值分布(许多较小的值和一些非常大的值)在实践中非常常见(泊松分布)。 但大多数线性模型无法很好地处理这种数据。尝试拟合一个岭回归模型:
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_splitX_train, X_test, Y_train, Y_test = train_test_split(X, Y, random_state=0)
score = Ridge().fit(X_train, Y_train).score(X_test, Y_test)
print("Test score: {:f}".format(score))
由于数据取值中包括 0(对数在 0 处没有定义),所以不能直接应用 log,而是要计算 log(X + 1):
X_train_log = np.log(X_train + 1)
X_test_log = np.log(X_test + 1)
plt.hist(X_train_log[:, 0], bins=25, color='gray')
plt.ylabel("Number of appearances")
plt.xlabel("Value")
# 在新数据上构建一个岭回归模型,可以得到更好的拟合:
score = Ridge().fit(X_train_log, Y_train).score(X_test_log, Y_test)
print("Test score: {:.3f}".format(score))
分箱、多项式和交互项都对模型在给定数据集上的性能有很大影响,对于复杂度较低的模型更是这样,比如线性模型和朴素贝叶斯模型。与之相反,基于树的模型通常能够自己发现重要的交互项,大多数情况下不需要显式地变换数据。其他模型,比如 SVM、最近邻和神经网络,有时可能会从使用分箱、交互项或多项式中受益,但其效果通常不如线性模型那么明显。
自动化特征选择
添加更多特征会使所有模型变得更加复杂,从而增大过拟合的可能性。在添加新特征或处理一般的高维数据集时,最好将特征的数量减少到只包含最有用的那些特征,并删除其余特征。这样会得到泛化能力更好、更简单的模型。
如何判断每个特征作用有多大,有三种基本的策略:
- 单变量统计(univariate statistics)
- 基于模型的选择(model-based selection)
- 迭代选择(iterative selection)
单变量统计
单变量统计,需要计算每个特征和目标值之间的关系是否存在统计显著性,然后选择具有最高置信度的特征。对于分类问题,这也被称为方差分析(ANOVA)。这些测试的一个关键性质是它们只单独考虑每个特征。因此,如果一个特征只有在与另一个特征合并时才具有信息量,那么这个特征将被舍弃。单变量测试的计算速度通常很快,并且不需要构建模型。另一方面,它们完全独立于你可能想要在特征选择之后应用的模型。
在 scikit-learn 中使用单变量特征选择,需要选择一项测试——对分类问题通常是 f_classif(默认值),对回归问题通常是 f_regression——然后基于测试中确定的 p 值来选择一种舍弃特征的方法。所有舍弃参数的方法都使用阈值来舍弃所有 p 值过大的特征(意味着它们不可能与目标值相关)。计算阈值的方法各有不同,最简单的是 SelectKBest和 SelectPercentile,前者选择固定数量的 k 个特征,后者选择固定百分比的特征。
分类的特征选择应用于 cancer 数据集,cancer有30个特征,下面随机生成50个假特征添加进去,看一下SelectPercentile能不能过滤掉这些假特征:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectPercentile
from sklearn.model_selection import train_test_splitcancer = load_breast_cancer()rng = np.random.RandomState(42) # 获取确定性的随机数
noise = rng.normal(size=(len(cancer.data),50))
X_w_noise = np.hstack([cancer.data,noise]) # 向数据中添加噪声特征,前30个特征来自数据集,后50个是噪声X_train,X_test,Y_train,Y_test = train_test_split(X_w_noise,cancer.target,random_state=42,test_size=0.5)
select = SelectPercentile(percentile=50) # 使用f_classif(默认值)和SelectPercentile来选择50%的特征
select.fit(X_train,Y_train)
X_train_selected = select.transform(X_train)
print("X_train shape:{}".format(X_train.shape))
print("X_train_selected shape:{}".format(X_train_selected.shape))
特征的数量从 80 减少到 40(原始特征数量的 50%)。可以用 get_support 方法来查看哪些特征被选:
mask = select.get_support()
print(mask)
plt.matshow(mask.reshape(1,-1),cmap='gray_r')
plt.yticks([])
plt.xlabel('Sample index')
大多数所选择的特征都是原始特征,并且大多数噪声特征都已被删除。
比较 Logistic 回归在所有特征上的性能与仅使用所选特征的性能:
from sklearn.linear_model import LogisticRegression
import waX_test_selected = select.transform(X_test)lr = LogisticRegression()
lr.fit(X_train,Y_train)
print("Score with all features: {:f}".format(lr.score(X_test, Y_test)))
lr.fit(X_train_selected,Y_train)
print("Score with only selected features: {:f}".format(lr.score(X_test_selected, Y_test)))
即使丢失了某些原始特征,删除噪声特征仍然可以提高性能。
SelectKBest与SelectPercentile类似,需要设置k参数(指定获取前多少个最优特征,默认为10)
from sklearn.feature_selection import SelectKBestselect_best = SelectKBest(k=15)
select_best.fit(X_train,Y_train)X_train_selected = select_best.transform(X_train)
print("X_train shape:{}".format(X_train.shape))
print("X_train_selected shape:{}".format(X_train_selected.shape))X_test_selected = select_best.transform(X_test)
lr = LogisticRegression()
lr.fit(X_train,Y_train)
print("Score with all features: {:.3f}".format(lr.score(X_test, Y_test)))
lr.fit(X_train_selected,Y_train)
print("Score with only selected features: {:.3f}".format(lr.score(X_test_selected, Y_test)))
基于模型的特征选择
基于模型的特征选择使用一个监督机器学习模型来判断每个特征的重要性,并且仅保留最重要的特征。用于特征选择的监督模型不需要与用于最终监督建模的模型相同。特征选模型需要为每个特征提供某种重要性度量,以便用这个度量对特征进行排序。决策树和基于决策树的模型提供了 feature_importances_ 属性,可以直接编码每个特征的重要性。线性模型系数的绝对值也可以用于表示特征重要性。
想使用基于模型的特征选择,需要使用 SelectFromModel 变换器(使用 RandomForestClassifier 的 SelectFromModel 选择的特征):
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifierselect = SelectFromModel(RandomForestClassifier(n_estimators=100,random_state=42),threshold='median')
select.fit(X_train,Y_train)
X_train_l1 = select.transform(X_train)
print("X_train.shape: {}".format(X_train.shape))
print("X_train_l1.shape: {}".format(X_train_l1.shape))mask = select.get_support()
plt.matshow(mask.reshape(1,-1),cmap='gray_r')
plt.yticks([])
plt.xlabel("sample index")X_test_l1 = select.transform(X_test)
sorce = LogisticRegression().fit(X_train_l1,Y_train).score(X_test_l1,Y_test)
print("test score:{:f}".format(sorce))
利用更好的特征选择,性能也得到了提高。
迭代特征选择
在迭代特征选择中,构建一系列模型,每个模型都使用不同数量的特征。有两种基本方法:开始时没有特征,然后逐个添加特征,直到满足某个终止条件;或者从所有特征开始,然后逐个删除特征,直到满足某个终止条件。
由于构建了一系列模型,所以这些方法的计算成本要比前面讨论过的方法更高。其中一种特殊方法是递归特征消除(recursive feature elimination,RFE),它从所有特征开始构建模型,并根据模型舍弃最不重要的特征,然后使用除被舍弃特征之外的所有特征来构建一个新模型,如此继续,直到仅剩下预设数量的特征。为了让这种方法能够运行,用于选择的模型需要提供某种确定特征重要性的方法,正如基于模型的选择所做的那样。
使用随机森林分类器模型的递归特征消除选择的特征:
from sklearn.feature_selection import RFEselect = RFE(RandomForestClassifier(n_estimators=100,random_state=42),n_features_to_select=40)
select.fit(X_train,Y_train)
mask = select.get_support()
plt.matshow(mask.reshape(1,-1),cmap='gray_r')
plt.yticks([])
plt.xlabel("sample index")X_train_rfe = select.transform(X_train)
X_test_rfe = select.transform(X_test)
sorce = LogisticRegression().fit(X_train_rfe,Y_train).score(X_test_rfe,Y_test)
print("test score:{:f}".format(sorce))
如果不确定何时选择使用哪些特征作为机器学习算法的输入,那么自动化特征选择可能特别有用。它还有助于减少所需要的特征数量,加快预测速度,或允许可解释性更强的模型。在大多数现实情况下,使用特征选择不太可能大幅提升性能,但它仍是特征工程工具箱中一个非常有价值的工具。