《Kaggle教程 特征工程》系列课程目录
Kaggle教程 特征工程1 Baseline-Model
Kaggle教程 特征工程2 Categorical Encodings
Kaggle教程 特征工程3 Feature Generation
Kaggle教程 特征工程4 Feature Selection
1. 介绍
通常,在各种编码和特性生成之后,您将拥有数百或数千个特性。这可能导致两个问题。首先,您拥有的特性越多,就越有可能过度适应培训和验证集。这将导致模型在泛化新数据时性能下降。
其次,拥有的特性越多,训练模型和优化超参数所需的时间就越长。此外,在构建面向用户的产品时,您希望尽可能快地进行推理。使用更少的特性可以加快推断速度,但这是以预测性能为代价的。
为了帮助解决这些问题,您需要使用特性选择技术来为您的模型保留最有信息的特性。
这节课我们会讲到。首先,下面是到目前为止您看到的代码。
%matplotlib inline
import itertools
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.preprocessing import LabelEncoder
from sklearn import metrics
ks = pd.read_csv('../input/kickstarter-projects/ks-projects-201801.csv',
parse_dates=['deadline', 'launched'])
# Drop live projects
ks = ks.query('state != "live"')
# Add outcome column, "successful" == 1, others are 0
ks = ks.assign(outcome=(ks['state'] == 'successful').astype(int))
# Timestamp features
ks = ks.assign(hour=ks.launched.dt.hour,
day=ks.launched.dt.day,
month=ks.launched.dt.month,
year=ks.launched.dt.year)
# Label encoding
cat_features = ['category', 'currency', 'country']
encoder = LabelEncoder()
encoded = ks[cat_features].apply(encoder.fit_transform)
data_cols = ['goal', 'hour', 'day', 'month', 'year', 'outcome']
baseline_data = ks[data_cols].join(encoded)
cat_features = ['category', 'currency', 'country']
interactions = pd.DataFrame(index=ks.index)
for col1, col2 in itertools.combinations(cat_features, 2):
new_col_name = '_'.join([col1, col2])
# Convert to strings and combine
new_values = ks[col1].map(str) + "_" + ks[col2].map(str)
label_enc = LabelEncoder()
interactions[new_col_name] = label_enc.fit_transform(new_values)
baseline_data = baseline_data.join(interactions)
launched = pd.Series(ks.index, index=ks.launched, name="count_7_days").sort_index()
count_7_days = launched.rolling('7d').count() - 1
count_7_days.index = launched.values
count_7_days = count_7_days.reindex(ks.index)
baseline_data = baseline_data.join(count_7_days)
def time_since_last_project(series):
# Return the time in hours
return series.diff().dt.total_seconds() / 3600.
df = ks[['category', 'launched']].sort_values('launched')
timedeltas = df.groupby('category').transform(time_since_last_project)
timedeltas = timedeltas.fillna(timedeltas.max())
baseline_data = baseline_data.join(timedeltas.rename({'launched': 'time_since_last_project'}, axis=1))
def get_data_splits(dataframe, valid_fraction=0.1):
valid_fraction = 0.1
valid_size = int(len(dataframe) * valid_fraction)
train = dataframe[:-valid_size * 2]
# valid size == test size, last two sections of the data
valid = dataframe[-valid_size * 2:-valid_size]
test = dataframe[-valid_size:]
return train, valid, test
def train_model(train, valid):
feature_cols = train.columns.drop('outcome')
dtrain = lgb.Dataset(train[feature_cols], label=train['outcome'])
dvalid = lgb.Dataset(valid[feature_cols], label=valid['outcome'])
param = {'num_leaves': 64, 'objective': 'binary',
'metric': 'auc', 'seed': 7}
print("Training model!")
bst = lgb.train(param, dtrain, num_boost_round=1000, valid_sets=[dvalid],
early_stopping_rounds=10, verbose_eval=False)
valid_pred = bst.predict(valid[feature_cols])
valid_score = metrics.roc_auc_score(valid['outcome'], valid_pred)
print(f"Validation AUC score: {valid_score:.4f}")
return bst
Univariate Feature Selection (单变量特征选择)
最简单和最快的方法是基于单变量统计检验。对于每一个功能,测量强烈目标取决于使用统计测试χ2或方差分析等功能。
从scikit学习功能选择模块,feature_selection。SelectKBest返回给定评分函数的K个最佳特性。对于我们的分类问题,该模块提供了三种不同的得分函数:χ2,方差分析f值,以及互信息的得分。f值度量特征变量与目标之间的线性依赖关系。这意味着,如果特征和目标之间的关系是非线性的,那么得分可能会低估它们之间的关系。互信息评分是非参数的,因此可以捕捉非线性关系。
通过SelectKBest,我们定义了要保留的特性的数量,这些特性的数量基于来自计分函数的分数。使用.fit_transform(features, target),我们可以得到一个只包含选定功能的数组。
from sklearn.feature_selection import SelectKBest, f_classif
feature_cols = baseline_data.columns.drop('outcome')
# Keep 5 features
selector = SelectKBest(f_classif, k=5)
X_new = selector.fit_transform(baseline_data[feature_cols], baseline_data['outcome'])
X_new
'''
array([[2015., 5., 9., 18., 1409.],
[2017., 13., 22., 31., 957.],
[2013., 13., 22., 31., 739.],
...,
[2010., 13., 22., 31., 238.],
[2016., 13., 22., 31., 1100.],
[2011., 13., 22., 31., 542.]])
'''
但是,我做错了一些事情。统计检验是使用所有的数据来计算的。这意味着来自验证和测试集的信息可能会影响我们保持的特性,从而引入泄漏源。这意味着我们应该只使用一个训练集来选择特性。
feature_cols = baseline_data.columns.drop('outcome')
train, valid, _ = get_data_splits(baseline_data)
# Keep 5 features
selector = SelectKBest(f_classif, k=5)
X_new = selector.fit_transform(train[feature_cols], train['outcome'])
X_new
'''
array([[2.015e+03, 5.000e+00, 9.000e+00, 1.800e+01, 1.409e+03],
[2.017e+03, 1.300e+01, 2.200e+01, 3.100e+01, 9.570e+02],
[2.013e+03, 1.300e+01, 2.200e+01, 3.100e+01, 7.390e+02],
...,
[2.011e+03, 1.300e+01, 2.200e+01, 3.100e+01, 5.150e+02],
[2.015e+03, 1.000e+00, 3.000e+00, 2.000e+00, 1.306e+03],
[2.013e+03, 1.300e+01, 2.200e+01, 3.100e+01, 1.084e+03]])
'''
您应该注意到所选的特性与我使用整个数据集时的不同。现在我们有了选择的特性,但它只是训练集的特性值。要从验证和测试集中删除被拒绝的特性,我们需要找出数据集中哪些列是用SelectKBest保存的。为此,我们可以使用.inverse_transform返回具有原始数据形状的数组。
# 恢复我们保留的功能,去掉所有其他功能
selected_features = pd.DataFrame(selector.inverse_transform(X_new),
index=train.index,
columns=feature_cols)
selected_features.head()
goal | hour | day | month | year | category | currency | country | category_currency | category_country | currency_country | count_7_days | time_since_last_project | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0 | 0.0 | 0.0 | 0.0 | 2015.0 | 0.0 | 5.0 | 9.0 | 0.0 | 0.0 | 18.0 | 1409.0 | 0.0 |
1 | 0.0 | 0.0 | 0.0 | 0.0 | 2017.0 | 0.0 | 13.0 | 22.0 | 0.0 | 0.0 | 31.0 | 957.0 | 0.0 |
2 | 0.0 | 0.0 | 0.0 | 0.0 | 2013.0 | 0.0 | 13.0 | 22.0 | 0.0 | 0.0 | 31.0 | 739.0 | 0.0 |
3 | 0.0 | 0.0 | 0.0 | 0.0 | 2012.0 | 0.0 | 13.0 | 22.0 | 0.0 | 0.0 | 31.0 | 907.0 | 0.0 |
4 | 0.0 | 0.0 | 0.0 | 0.0 | 2015.0 | 0.0 | 13.0 | 22.0 | 0.0 | 0.0 | 31.0 | 1429.0 | 0.0 |
这将返回与训练集具有相同索引和列的DataFrame,但是所有删除的列都用0填充。我们可以通过选择方差非零的特征来找到所选的列。
# 被删除的列的值都是0,所以var是0,删除它们
selected_columns = selected_features.columns[selected_features.var() != 0]
# 获取具有所选特性的有效数据集。
valid[selected_columns].head()
year | currency | country | currency_country | count_7_days | |
---|---|---|---|---|---|
302896 | 2015 | 13 | 22 | 31 | 1534.0 |
302897 | 2013 | 13 | 22 | 31 | 625.0 |
302898 | 2014 | 5 | 9 | 18 | 851.0 |
302899 | 2014 | 13 | 22 | 31 | 1973.0 |
302900 | 2014 | 5 | 9 | 18 | 2163.0 |
L1 regularization (L1正规化)
单变量方法在做选择决策时一次只考虑一个特征。相反,我们可以使用所有的特征来进行选择,方法是将它们包含在一个带有L1正则化的线性模型中。这种类型的正则化(有时称为Lasso)惩罚系数的绝对值,而L2 (Ridge)回归惩罚系数的平方。
随着正则化强度的增加,对预测目标不那么重要的特征被设为0。这允许我们通过调整正则化参数来执行特征选择。我们通过找出保留集的最佳性能来选择参数,或者提前决定保留多少特性。
对于回归问题,可以使用sklearn.linear_model。套索或sklearn.linear_model。LogisticRegression分类。这些可以与sklearn.feature_selection一起使用。SelectFromModel选择非零系数。否则,代码类似于单变量测试。
from sklearn.linear_model import LogisticRegression
from sklearn.feature_selection import SelectFromModel
train, valid, _ = get_data_splits(baseline_data)
X, y = train[train.columns.drop("outcome")], train['outcome']
# Set the regularization parameter C=1
logistic = LogisticRegression(C=1, penalty="l1", random_state=7).fit(X, y)
model = SelectFromModel(logistic, prefit=True)
X_new = model.transform(X)
X_new
'''
/opt/conda/lib/python3.6/site-packages/sklearn/linear_model/logistic.py:432: FutureWarning: Default solver will be changed to 'lbfgs' in 0.22. Specify a solver to silence this warning.
FutureWarning)
array([[1.000e+03, 1.200e+01, 1.100e+01, ..., 1.900e+03, 1.800e+01,
1.409e+03],
[3.000e+04, 4.000e+00, 2.000e+00, ..., 1.630e+03, 3.100e+01,
9.570e+02],
[4.500e+04, 0.000e+00, 1.200e+01, ..., 1.630e+03, 3.100e+01,
7.390e+02],
...,
[2.500e+03, 0.000e+00, 3.000e+00, ..., 1.830e+03, 3.100e+01,
5.150e+02],
[2.600e+03, 2.100e+01, 2.300e+01, ..., 1.036e+03, 2.000e+00,
1.306e+03],
[2.000e+04, 1.600e+01, 4.000e+00, ..., 9.200e+02, 3.100e+01,
1.084e+03]])
'''
与单变量测试类似,我们返回一个具有所选特性的数组。同样,我们希望将这些数据转换为DataFrame,以便获得所选的列。
# 以DataFrame的形式获取保留的特性,将删除的列作为所有的0
selected_features = pd.DataFrame(model.inverse_transform(X_new),
index=X.index,
columns=X.columns)
# 删除的列的值都是0,保留其他列
selected_columns = selected_features.columns[selected_features.var() != 0]
在L1参数C=1的情况下,我们删除了time_since_last_project列。
一般来说,L1正则化的特征选择在单变量测试中更强大,但是当你有大量的数据和特征时,它也会非常慢。在大型数据集上,单变量测试将快得多,但也可能执行得更差。
===============================================================