Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
507 lines (349 sloc) 15 KB

分类变量

�如果特征不是连续值,而是一些分类特征,来自一系列固定的可能取值,那么直接用于类似Logistic回归分类模型是没有意义的。

One-Hot编码(虚拟变量)

将1个特征的多种取值,改为多个特征的0/1取值,其中有一个特征为1,其他为0.

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)
[[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。

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)
[[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编码:

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、神经网络的效果。


# 对跨度较大的特征做log
X_train_log = np.log(X_train + 1)
X_test_log = np.log(X_test + 1)

对线性模型提升最明显,对树模型意义不大,对SVM/KNN/神经网络有可能受益。

自动化特征选择

上面讲的是增加特征,现在是通过分析数据来减少特征,得到一个泛化更好,更简单的模型,也就是特征选择。

一共有3种方法:

  • 单变量统计
  • 基于模型的选择
  • 迭代选择

单变量统计

每次考虑一个特征,观察特征与目标值之间是否存在统计显著性,但是没法综合考虑多个特征。

算法可以指定保留一定数量的重要特征。

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)))
(284, 30)
(284, 15)
Score with all features: 0.954
Score with only selected features: 0.954

可见,删了一半特征,对模型精度没有产生影响。

基于模型的特征选择

线性模型、决策树模型在训练的过程中自然的完成了对特征重要程度的学习。

所以可以基于这些模型进行特征选择,再将选择后的特征作为另外一个模型的输入。

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)))

X_train.shape: (284, 30)
X_train_l1.shape: (284, 15)
Test score: 0.954

模型综合度量了所有特征的重要性,所以比单变量统计强大的多。

迭代特征选择

RFE利用模型进行多轮特征选择,每轮筛掉1个最不重要的特征。

下面用随机森林做特征选择,选择出来的特征用线性LR做训练,发现线性模型精度很好。

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) ))

Test score: 0.954

如果不确定选择哪些特征作为算法输入,那么可以尝试自动特征选择。 实际情况中,特征选择不太可能大幅提升精度。

利用专家知识

通常来说,领域专家可以帮助找出有用的特征,其信息量比数据原始表示要大得多。

有一份单维度的数据,特征是时间,目标是租车量,希望可以预估未来某个时间的租车量。

不作任何特征预处理,直接交给随机森林回归:

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))
0.8660619313301664
-0.19013065877228819

训练集精度还不错,但是测试集完全无效。

这是因为树回归模型是无法外推的,测试集的时间戳超出了训练集的范围,所以模型只能预测给出训练集中出现过最近的数据点的目标值。

这时候,我们还是需要专家知识的帮助,租车量与2个因素有关:

  • 一天内的第几个小时
  • 一周内的星期几
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))
0.8903213850485228
0.8290414842725423

继续应用随机森林,因为特征取值范围有限,所以现在模型表现很不错。

模型学习到的是周几以及几点与目标之间的关系,用线性模型试试:

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))
0.19541721030927228
0.034621542544042594

精度又很差,为什么呢?

因为线性模型把特征作为连续值去线性理解,它可能认为周几越大出租量越大,实际情况并不是这种线性的。

把这俩特征作为离散值理解更有意义,所以需要one-hot编码。

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))
0.5626527099910439
0.5079052578791499

正常了一些,但模型还是太简单了。

下面添加一些多项式特征,令线性模型更复杂:

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))
0.8976345473090164
0.8037061357545114

线性模型基本已经接近随机森林。

小结

  • one-hot编码分类特征
  • 线性模型通过分箱,添加多项式、交互项来添加新特征,可以大大受益
  • 对于非线性模型(比如随机森林、SVM),无需扩展特征就可以得到不错的效果
  • 实践中,所使用的特征是机器学习表现良好的最重要因素
You can’t perform that action at this time.