特征选择与特征工程

特征选择与特征工程

特征工程是机器学习的第一步,涉及清理现有数据集、提高信噪比和降低维数的所有技术。大多数算法对输入数据有很强的假设,当使用原始数据集时,它们的性能可能会受到负面影响。

另外有些特征之间高度相关,在其中一个特征提供了足够的信息之后,与之相关的其他特征往往无法提供额外的信息。这时我们就需要了解如何减少特征数量或者仅选择最佳特征。

一、scikit-learn数据集

scikit-learn提供了一些用于测试的内置数据集,这些数据集包含在sklearn.datasets中,每个数据集都包含了输入集(特征集)X和标签(目标值)y。比如波士顿房价的数据集(用于回归问题):

from sklearn.datasets import load_boston

boston = load_boston()
X = boston.data
y = boston.target

print('特征集的shape:', X.shape)
print('目标集的shape:', y.shape)
特征集的shape: (506, 13)
目标集的shape: (506,)

可以看到,这个数据集包含了506个样本、13个特征,以及1个目标值。

假如我们不想使用scikit-learn提供的数据集,那么我们还可以使用scikit-learn提供的工具来手动创建特定的数据集。相关的方法有:

  • make_classification():用于创建适用于测试分类算法的数据集;
  • make_regression():用于创建适用于测试回归模型的数据集;
  • make_blobs():用于创建适用于测试聚类算法的数据集。

二、创建训练集和测试集

一般来说,我们要在正式应用我们训练的模型前对它进行测试。因此我们需要将数据集分为训练集和测试集,顾名思义,前者用于训练模型参数,后者用于测试模型性能。在某些情况下,我们甚至还会再分出一个数据集作为交叉验证集,这种处理方式适用于有多种模型可供选择的情况。

数据集的分割有一些注意事项:首先,两个数据集必须要能反映原始数据的分布,否则在数据集失真的情况下得到的模型对于真实样本的预测效力会比较差;其次,原始数据集必须在分割之前随机混合,以避免连续元素之间的相关性。

在scikit-learn中,我们可以使用train_test_split()函数来快速实现数据集的分割。

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=1000)

这里前两个位置参数分别是特征集和目标集,test_size用于指定测试集大小占整个数据集的比例,random_state则是指定一个随机种子,这样可以确保我们在重复试验时数据不会发生变化(数据集都变了,那模型效果的变化就不知道该归因于模型的优化还是归因于数据集的变化了。)

三、管理分类数据

在许多分类问题中,目标数据集由各种类别标签组成。但是很多算法是不支持这种数据格式的,因此我们要对其进行必要的编码。

假设我们有一个由10个样本组成的数据集,每个样本有两个特征。

import numpy as np

X = np.random.uniform(0.0, 1.0, size=(10, 2))
y = np.random.choice(('Male', 'Female'), size=(10))

print('X:', X)
print('y:', y)
X: [[0.48463048 0.21682675]
 [0.27987595 0.28061459]
 [0.13723177 0.45159025]
 [0.42727284 0.99834867]
 [0.61113219 0.31892401]
 [0.14985227 0.71565914]
 [0.048201   0.49254257]
 [0.54466226 0.8419817 ]
 [0.94426201 0.78924785]
 [0.36877342 0.53250431]]
y: ['Female' 'Female' 'Male' 'Female' 'Female' 'Female' 'Male' 'Male'
 'Female' 'Male']

1. 使用LabelEncoder

from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
yt = le.fit_transform(y)

print(y)
print(yt)
print(le.classes_)
['Female' 'Female' 'Male' 'Female' 'Female' 'Female' 'Male' 'Male'
 'Female' 'Male']
[0 0 1 0 0 0 1 1 0 1]
['Female' 'Male']

获得逆变换的方法很简单:

output = [1, 0, 1, 1, 0, 0]
decoded_output = [le.classes_[i] for i in output]
print(decoded_output)
['Male', 'Female', 'Male', 'Male', 'Female', 'Female']

这种方法很简单,但是有个缺点:所有的标签都变成了数字,然后使用真实值的分类器会根据其距离考虑相似的数字,而忽略其代表的分类含义。因此我们通常优先选择独热编码(one-hot encoding,又称一次有效编码),将数据二进制化。

2. 使用LabelBinarizer

from sklearn.preprocessing import LabelBinarizer

lb = LabelBinarizer()
yb = lb.fit_transform(y)

print(y)
print(yb)
print(lb.inverse_transform(yb))
['Female' 'Female' 'Male' 'Female' 'Female' 'Female' 'Male' 'Male'
 'Female' 'Male']
[[0]
 [0]
 [1]
 [0]
 [0]
 [0]
 [1]
 [1]
 [0]
 [1]]
['Female' 'Female' 'Male' 'Female' 'Female' 'Female' 'Male' 'Male'
 'Female' 'Male']

可以看到,这里我们可以使用LabelBinarizer类的inverse_transform方法进行逆转化。

当存在多个标签时,这种方法会将其中一个标签变换为1,其余标签全部为0。这可能会导致的问题显而易见,也就是我们将多分类问题转换成了二分类问题。

四、管理缺失特征

我们可能会经常碰见数据缺失的情况,有以下选项可以解决该问题:

  • 删除整行:这个选项比较激进,一般只有当数据集足够大、缺少的特征值数量很多而且预测风险大时才会选择;
  • 创建子模型来预测这些特征值:第二个选项实现起来比较困难,因为需要确定一个监督策略来训练每个特征的模型,最后预测它们的值;
  • 使用自动策略根据其他已知值插入这些缺失的特征值:考虑到以上的利弊,这可能是最好的选项了。
from sklearn.preprocessing import Imputer

data = np.array([[1, np.nan, 2],
                 [2, 3, np.nan],
                 [-1, 4, 2]])

# 插入均值
imp = Imputer(strategy='mean')
print('Mean:\n', imp.fit_transform(data))

# 插入中位数
imp = Imputer(strategy='median')
print('Median:\n', imp.fit_transform(data))

# 插入众数
imp = Imputer(strategy='most_frequent')
print('Mode:\n', imp.fit_transform(data))
Mean:
 [[ 1.   3.5  2. ]
 [ 2.   3.   2. ]
 [-1.   4.   2. ]]
Median:
 [[ 1.   3.5  2. ]
 [ 2.   3.   2. ]
 [-1.   4.   2. ]]
Mode:
 [[ 1.  3.  2.]
 [ 2.  3.  2.]
 [-1.  4.  2.]]

五、数据缩放和归一化

一般的数据集是由不同的值组成的,可以从不同的分布得到且具有不同的尺度,有时还会有异常值。当不同特征的取值范围差异过大时,很可能会对模型产生不良影响。因此我们往往需要先规范数据集。

我们来对比一下原始数据集和经过缩放和中心化的数据集:

from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_iris
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()

# 导入数据
iris = load_iris()
data = iris.data

# 绘制原始数据散点图
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
sns.scatterplot(x=data[:, 0], y=data[:, 1], ax=axes[0])

# 数据归一化
scaler = StandardScaler()
scaled_data = scaler.fit_transform(data)

# 绘制规范化数据散点图
sns.scatterplot(x=scaled_data[:, 0], y=scaled_data[:, 1], ax=axes[1])
plt.setp(axes, xlim=[-2, 8], ylim=[-3, 5]);
image

可以看到,我们的数据分布形态没有变化,但是数据的分布范围却变了。我们将数据转化成了均值为0(几乎为0),标准差为1的归一化数据。

print('转化前均值:\n', np.mean(data, axis=0))
print('转化后均值:\n', np.mean(scaled_data, axis=0))
print('转化前方差:\n', np.std(data, axis=0))
print('转化后方差:\n', np.std(scaled_data, axis=0))
转化前均值:
 [5.84333333 3.054      3.75866667 1.19866667]
转化后均值:
 [-1.69031455e-15 -1.63702385e-15 -1.48251781e-15 -1.62314606e-15]
转化前方差:
 [0.82530129 0.43214658 1.75852918 0.76061262]
转化后方差:
 [1. 1. 1. 1.]

在数据缩放时,我们还可以使用类RobustScaler对异常值进行控制和选择分位数范围。

from sklearn.preprocessing import RobustScaler

# 转化数据1
rb1 = RobustScaler(quantile_range=(15, 85))
scaled_data1 = rb1.fit_transform(data)

# 转化数据2
rb2 = RobustScaler(quantile_range=(25, 75))
scaled_data2 = rb2.fit_transform(data)

# 转化数据3
rb3 = RobustScaler(quantile_range=(30, 60))
scaled_data3 = rb3.fit_transform(data)

# 绘制散点图
fig, axes = plt.subplots(2, 2, figsize=(10, 10))
sns.scatterplot(x=data[:, 0], y=data[:, 1], ax=axes[0, 0])
sns.scatterplot(x=scaled_data1[:, 0], y=scaled_data1[:, 1], ax=axes[0, 1])
sns.scatterplot(x=scaled_data2[:, 0], y=scaled_data2[:, 1], ax=axes[1, 0])
sns.scatterplot(x=scaled_data3[:, 0], y=scaled_data3[:, 1], ax=axes[1, 1])
plt.setp(axes, ylim=[-4, 5], xlim=[-2, 8]);
image

可以看到,数据的大致分布形态仍然很接近,但是数据的分布范围简直大变样。另外,由于我们设置了不同的分位数范围,因此数据的样本量也不太一样。

常用的还有MinMaxScalerMaxAbsScaler,前者通过删除不属于给定范围的元素,后者则通过考虑使用最大绝对值来缩放数据。

scikit-learn还为每个样本规范化提供了一个类:Normalizer。它可以对数据集的每个元素应用Max、L1和L2范数。

  • Max:每个值都除以数据集中的最大值;
  • L1:每个值都除以数据集中所有值的绝对值之和;
  • L2:每个值都除以数据集中所有值的平方和的平方根

我们来看一个例子。

from sklearn.preprocessing import Normalizer

# 生成数据
data = np.array([1, 2]).reshape(1, 2)
print('原始数据:', data)

# Max
n_max = Normalizer(norm='max')
print('Max:', n_max.fit_transform(data))

# L1范数
n_l1 = Normalizer(norm='l1')
print('L1范数:', n_l1.fit_transform(data))

# L2范数
n_l2 = Normalizer(norm='l2')
print('L2范数:', n_l2.fit_transform(data))
原始数据: [[1 2]]
Max: [[0.5 1. ]]
L1范数: [[0.33333333 0.66666667]]
L2范数: [[0.4472136  0.89442719]]

六、特征选择和过滤

不是所有的特征都能提供足够的信息的,甚至有些特征会对我们的模型训练产生障碍,因此在模型训练开始前我们要对特征做出一定的选择。

接下来我们使用SelectKBest方法结合F检验来筛选回归模型的特征。

from sklearn.feature_selection import SelectKBest, f_regression
from sklearn.datasets import load_boston

boston = load_boston()
print('Boston data shape: ', boston.data.shape)

selector = SelectKBest(f_regression)
X_new = selector.fit_transform(boston.data, boston.target)
print('Filtered Boston data shape:', X_new.shape)

print('F-Scores:', selector.scores_)
Boston data shape:  (506, 13)
Filtered Boston data shape: (506, 10)
F-Scores: [ 88.15124178  75.2576423  153.95488314  15.97151242 112.59148028
 471.84673988  83.47745922  33.57957033  85.91427767 141.76135658
 175.10554288  63.05422911 601.61787111]

然后我们使用SelectPercentile结合卡方检验来筛选分类模型的特征。

from sklearn.feature_selection import SelectPercentile, chi2
from sklearn.datasets import load_iris

iris = load_iris()
print('Boston data shape: ', iris.data.shape)

selector = SelectPercentile(chi2, percentile=15)
X_new = selector.fit_transform(iris.data, iris.target)
print('Filtered Boston data shape:', X_new.shape)

print('F-Scores:', selector.scores_)
Boston data shape:  (150, 4)
Filtered Boston data shape: (150, 1)
F-Scores: [ 10.81782088   3.59449902 116.16984746  67.24482759]

在数据预处理时,我们还经常会采用主成分分析等方法来实现数据降维等目的,不过这一部分我们完全可以单独拆出一个章节来讲解,感兴趣的朋友可以关注下后续的更新。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容