github地址:点我
分类变量
如果特征不是连续值,而是一些分类特征,来自一系列固定的可能取值,那么直接用于类似Logistic回归分类模型是没有意义的。
One-Hot编码(虚拟变量)
将1个特征的多种取值,改为多个特征的0/1取值,其中有一个特征为1,其他为0.
1 2 3 4 5 6 7 8 9 10 |
from sklearn.preprocessing import OneHotEncoder # fit训练one hot, 返回非稀疏矩阵, 当transform时遇到没见过的特征值则对应one-hot编码全部为0 enc = OneHotEncoder(sparse=False, handle_unknown='ignore') X = [[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]] enc.fit(X) # one-hot编码新数据 X_one_hot = enc.transform([[1,4,2]]) print(X_one_hot) |
1 |
[[0. 1. 0. 0. 0. 0. 0. 1. 0.]] |
第1个特征取值有2个(0、1),所以在one-hot里占用2列;第2个特征取值有3个(0、1、2),所以在one-hot里占用3列;第3个特征取值有4个(0、1、2、3),所以one-hot占用4列;
对于输入特征[1,4,2],因为4在fit时没有出现过,所以在one-hot编码中被ignore为3个0.
数字可以编码分类变量
书中提到pandas库做one-hot的时候,可以指定对哪些特征对one-hot,而对其他连续特征可以不做处理。
我以sklearn为例,假设3维特征只有前2个特征是离散分类需要做one-hot,而第3个特征不需要one-hot。
1 2 3 4 5 6 7 8 9 10 |
from sklearn.preprocessing import OneHotEncoder # fit训练one hot, 返回非稀疏矩阵, 当transform时遇到没见过的特征值则对应one-hot编码全部为0 enc = OneHotEncoder(categorical_features=[0,1], sparse=False, handle_unknown='ignore') X = [[0, 0, 3], [1, 1, 0], [0, 2, 1], [1, 0, 2]] enc.fit(X) # one-hot编码新数据 X_one_hot = enc.transform([[1,4,2]]) print(X_one_hot) |
1 |
[[0. 1. 0. 0. 0. 2.]] |
通过categorical_features=[0,1]指定了只有前2个特征需要离散化。
从结果可见,0,1是第1列特征的one-hot,0,0,0是第2列特征的one-hot(因为4在fit时不存在),第3列特征没有变动。
字符串编码为整形
书中没有提到sklearn如何把字符串类型的特征转换为整形,我调研了一下作为补充。
在最新版sklearn 0.2+中,需要用ColumnTransformer来作为统一的特征处理方法。
下面,我对特征的第1列做执行OrdinalEncoder编码字符串为整形,然后再对第1列做Onehot编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
from sklearn.preprocessing import OrdinalEncoder from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer import numpy as np X = [['male', 0, 3], ['male', 1, 0], ['female', 2, 1], ['female', 0, 2]] # 字符串编码为整形 sex_enc = OrdinalEncoder(dtype = np.int) # 独热编码 one_hot_enc = OneHotEncoder(sparse=False, handle_unknown='ignore', dtype=np.int) # 对第0列的字符串做整形转换, 然后对所有列做one-hot col_transformer = ColumnTransformer(transformers = [('sex_enc', sex_enc, [0]), ('one_hot_enc', one_hot_enc, list(range(0,3)))]) # 训练编码 col_transformer.fit(X) X_trans = col_transformer.transform(X) print(X_trans) |
特征的pipeline处理非常方便。
分箱、离散化、线性模型与树
对于只有1个连续特征的线性模型,它的表现就是y=w*x的线,效果不好。
这时候可以考虑将特征取值划分范围,这就是分箱,每个箱子的数值区间不同。
这样就把1个连续特征变成了离线特征,也就是在哪个区间里,可以继续通过one-hot编码成多个0/1特征。
分箱对线性模型有提升效果,对树模型效果不会很好可能还会下降。
交互特征与多项式特征
意思就是把原始特征之间进行组合和扩充,对线性模型有提升效果,比如:添加特征的平方或立方,或者把两两特征相乘。
添加交互/多项式特征之后的线性模型,与没有交互特征的树模型/复杂模型的性能就比较相近了。
单变量非线性变换
对于简单模型(线性、朴素贝叶斯)来说,如果数据集的数据分布存在大量的小值以及个别非常大的值,会导致线性模型很难处理。
这种情况出现在一些计数性质的特征上,比如点赞次数。
此时对该特征应用log(x+1)或者exp来调节特征值得比例,可以改进线性模、SVM、神经网络的效果。
1 2 3 4 5 |
# 对跨度较大的特征做log X_train_log = np.log(X_train + 1) X_test_log = np.log(X_test + 1) |
对线性模型提升最明显,对树模型意义不大,对SVM/KNN/神经网络有可能受益。
自动化特征选择
上面讲的是增加特征,现在是通过分析数据来减少特征,得到一个泛化更好,更简单的模型,也就是特征选择。
一共有3种方法:
- 单变量统计
- 基于模型的选择
- 迭代选择
单变量统计
每次考虑一个特征,观察特征与目标值之间是否存在统计显著性,但是没法综合考虑多个特征。
算法可以指定保留一定数量的重要特征。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
from sklearn.datasets import load_breast_cancer from sklearn.feature_selection import SelectPercentile from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression # 数据集 cancer = load_breast_cancer() # 切分 X_train, X_test, y_train, y_test = train_test_split( cancer.data, cancer.target, random_state=0, test_size=.5) print(X_train.shape) # 自动选择50%的特征留下来 select = SelectPercentile(percentile=50) select.fit(X_train, y_train) # 训练集提取特征 X_train_selected = select.transform(X_train) print(X_train_selected.shape) # 测试集提取特征 X_test_selected = select.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))) |
1 2 3 4 |
(284, 30) (284, 15) Score with all features: 0.954 Score with only selected features: 0.954 |
可见,删了一半特征,对模型精度没有产生影响。
基于模型的特征选择
线性模型、决策树模型在训练的过程中自然的完成了对特征重要程度的学习。
所以可以基于这些模型进行特征选择,再将选择后的特征作为另外一个模型的输入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.feature_selection import SelectFromModel from sklearn.ensemble import RandomForestClassifier # 数据集 cancer = load_breast_cancer() # 切分 X_train, X_test, y_train, y_test = train_test_split( cancer.data, cancer.target, random_state=0, test_size=.5) # 从随机森林模型的训练的结果中进行特征选择, 取中位数偏上重要的特征,也就是保留一半最重要的特征 select = 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)) # 选择出来的特征完成训练 lr = LogisticRegression() lr.fit(X_train_l1, y_train) # 测试集特征选择 X_test_l1 = select.transform(X_test) print("Test score: {:.3f}".format(lr.score(X_test_l1, y_test))) |
1 2 3 |
X_train.shape: (284, 30) X_train_l1.shape: (284, 15) Test score: 0.954 |
模型综合度量了所有特征的重要性,所以比单变量统计强大的多。
迭代特征选择
RFE利用模型进行多轮特征选择,每轮筛掉1个最不重要的特征。
下面用随机森林做特征选择,选择出来的特征用线性LR做训练,发现线性模型精度很好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.feature_selection import RFE from sklearn.ensemble import RandomForestClassifier # 数据集 cancer = load_breast_cancer() # 切分 X_train, X_test, y_train, y_test = train_test_split( cancer.data, cancer.target, random_state=0, test_size=.5) # 基于迭代的特征选择, 保留15个特征 select = RFE(RandomForestClassifier(n_estimators=100, random_state=42), n_features_to_select=15) # 训练特征选择 select.fit(X_train, y_train) # 训练集特征选择 X_train_rfe = select.transform(X_train) # 测试集特征选择 X_test_rfe = select.transform(X_test) # 选择后的特征训练模型 lr = LogisticRegression() lr.fit(X_train_rfe, y_train) # 测试集精度 print("Test score: {:.3f}".format(lr.score(X_test_rfe, y_test) )) |
1 |
Test score: 0.954 |
如果不确定选择哪些特征作为算法输入,那么可以尝试自动特征选择。 实际情况中,特征选择不太可能大幅提升精度。
利用专家知识
通常来说,领域专家可以帮助找出有用的特征,其信息量比数据原始表示要大得多。
有一份单维度的数据,特征是时间,目标是租车量,希望可以预估未来某个时间的租车量。
不作任何特征预处理,直接交给随机森林回归:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import mglearn from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split citibike = mglearn.datasets.load_citibike() # 提取目标值(租车数量) y = citibike.values # 将时间字符串转换成unix时间戳,作为1维特征 X = citibike.index.strftime("%s").astype("int").values.reshape(-1, 1) # 切分数据集 X_train, X_test, y_train, y_test = train_test_split(X, y) # 应用随机森林回归, 100颗树 regressor = RandomForestRegressor(n_estimators=100, random_state=0) regressor.fit(X_train, y_train) # 看精度 print(regressor.score(X_train, y_train)) print(regressor.score(X_test, y_test)) |
1 2 |
0.8660619313301664 -0.19013065877228819 |
训练集精度还不错,但是测试集完全无效。
这是因为树回归模型是无法外推的,测试集的时间戳超出了训练集的范围,所以模型只能预测给出训练集中出现过最近的数据点的目标值。
这时候,我们还是需要专家知识的帮助,租车量与2个因素有关:
- 一天内的第几个小时
- 一周内的星期几
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import mglearn from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split citibike = mglearn.datasets.load_citibike() # 提取目标值(租车数量) y = citibike.values # 专家知识给出的特征为:[周几,几点] X_hour_week = np.hstack([citibike.index.dayofweek.values.reshape(-1, 1), citibike.index.hour.values.reshape(-1, 1)]) # 切分数据集 X_train, X_test, y_train, y_test = train_test_split(X_hour_week, y) # 应用随机森林回归, 100颗树 regressor = RandomForestRegressor(n_estimators=100, random_state=0) regressor.fit(X_train, y_train) # 看精度 print(regressor.score(X_train, y_train)) print(regressor.score(X_test, y_test)) |
1 2 |
0.8903213850485228 0.8290414842725423 |
继续应用随机森林,因为特征取值范围有限,所以现在模型表现很不错。
模型学习到的是周几以及几点与目标之间的关系,用线性模型试试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import mglearn from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression citibike = mglearn.datasets.load_citibike() # 提取目标值(租车数量) y = citibike.values # 专家知识给出的特征为:[周几,几点] X_hour_week = np.hstack([citibike.index.dayofweek.values.reshape(-1, 1), citibike.index.hour.values.reshape(-1, 1)]) # 切分数据集 X_train, X_test, y_train, y_test = train_test_split(X_hour_week, y) # 线性回归 regressor = LinearRegression() regressor.fit(X_train, y_train) # 看精度 print(regressor.score(X_train, y_train)) print(regressor.score(X_test, y_test)) |
1 2 |
0.19541721030927228 0.034621542544042594 |
精度又很差,为什么呢?
因为线性模型把特征作为连续值去线性理解,它可能认为周几越大出租量越大,实际情况并不是这种线性的。
把这俩特征作为离散值理解更有意义,所以需要one-hot编码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import mglearn from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import OneHotEncoder citibike = mglearn.datasets.load_citibike() # 提取目标值(租车数量) y = citibike.values # 专家知识给出的特征为:[周几,几点] X_hour_week = np.hstack([citibike.index.dayofweek.values.reshape(-1, 1), citibike.index.hour.values.reshape(-1, 1)]) # 切分数据集 X_train, X_test, y_train, y_test = train_test_split(X_hour_week, y) # 训练one-hot编码 enc = OneHotEncoder(sparse=False, handle_unknown='ignore') enc.fit(X_train) # 训练集编码 X_train_onehot = enc.transform(X_train) # 测试集编码 X_test_onehot = enc.transform(X_test) # 线性回归, regressor = LinearRegression() regressor.fit(X_train_onehot, y_train) # 看精度 print(regressor.score(X_train_onehot, y_train)) print(regressor.score(X_test_onehot, y_test)) |
1 2 |
0.5626527099910439 0.5079052578791499 |
正常了一些,但模型还是太简单了。
下面添加一些多项式特征,令线性模型更复杂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import mglearn from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import OneHotEncoder from sklearn.preprocessing import PolynomialFeatures citibike = mglearn.datasets.load_citibike() # 提取目标值(租车数量) y = citibike.values # 专家知识给出的特征为:[周几,几点] X_hour_week = np.hstack([citibike.index.dayofweek.values.reshape(-1, 1), citibike.index.hour.values.reshape(-1, 1)]) # 切分数据集 X_train, X_test, y_train, y_test = train_test_split(X_hour_week, y) # 训练one-hot编码 enc = OneHotEncoder(sparse=False, handle_unknown='ignore') enc.fit(X_train) # 训练集编码 X_train_onehot = enc.transform(X_train) # 测试集编码 X_test_onehot = enc.transform(X_test) # 交互更多特征 poly_transformer = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False) X_train_onehot_poly = poly_transformer.fit_transform(X_train_onehot) X_test_onehot_poly = poly_transformer.transform(X_test_onehot) # 线性回归, regressor = LinearRegression() regressor.fit(X_train_onehot_poly, y_train) # 看精度 print(regressor.score(X_train_onehot_poly, y_train)) print(regressor.score(X_test_onehot_poly, y_test)) |
1 2 |
0.8976345473090164 0.8037061357545114 |
线性模型基本已经接近随机森林。
小结
- one-hot编码分类特征
- 线性模型通过分箱,添加多项式、交互项来添加新特征,可以大大受益
- 对于非线性模型(比如随机森林、SVM),无需扩展特征就可以得到不错的效果
- 实践中,所使用的特征是机器学习表现良好的最重要因素
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
