专栏名称: 数据派THU
本订阅号是“THU数据派”的姊妹账号,致力于传播大数据价值、培养数据思维。
目录
相关文章推荐
软件定义世界(SDX)  ·  中国工程院发布中国工程科技2040发展战略研究 ·  3 天前  
软件定义世界(SDX)  ·  德勤发布《2025年技术趋势》 ·  5 天前  
软件定义世界(SDX)  ·  【PPT】红杉:2024企业数字化年度指南 ·  6 天前  
数据派THU  ·  机器学习特征工程,全面指南!(下) ·  4 天前  
51好读  ›  专栏  ›  数据派THU

机器学习特征工程,全面指南!(下)

数据派THU  · 公众号  · 大数据  · 2024-12-30 17:00

正文


本文约11200字,建议阅读20分钟
本指南是初学者的简明参考,提供了最简单但广泛使用的特征工程和选择技术。


4  特征工程


4.1  特征缩放


定义:特征缩放是一种用于标准化数据自变量或特征范围的方法。在数据处理中,它也被称为数据归一化,通常在数据预处理步骤中执行。


4.1.1  为什么特征缩放很重要


如果输入范围发生变化,在某些算法中,目标函数将无法正常工作。梯度下降在完成特征缩放后收敛得更快。


梯度下降是一种常用的优化算法,用于逻辑回归、支持向量机、神经网络等。


涉及距离计算的算法,如KNN、聚类,也受到特征大小的影响。只需考虑欧几里德距离的计算方法:取观测值之间平方差之和的平方根。这种距离会受到变量之间尺度差异的极大影响。方差较大的变量对这种度量的影响比方差较小的变量大。


注意:基于树的算法几乎是唯一不受输入量影响的算法,因为我们可以很容易地从树的构建方式中看出。在决定如何进行分割时,树算法会寻找诸如“特征值X是否大于3.0”之类的决策,并在分割后计算子节点的纯度,因此特征的规模并不重要。


4.1.2  如何处理特征缩放



面对异常值时三种方法的比较:



正如我们所看到的,归一化-标准化和最小-最大方法会将大多数数据压缩到一个较窄的范围,而鲁棒缩放器在保持数据分布方面做得更好,尽管它不能从处理结果中删除异常值。记住删除/输入异常值是数据清理中的另一个主题,应该提前完成。


关于如何选择特征缩放方法的经验:


  • 如果你的特征不是高斯分布,比如,具有偏斜分布或异常值,那么归一化-标准化不是一个好的选择,因为它会将大多数数据压缩到一个狭窄的范围。
  • 然而,我们可以将特征转换为高斯分布,然后使用归一化 - 标准化。特征转换将在第3.4节中讨论。
  • 在进行距离或协方差计算(如聚类、PCA和LDA等算法)时,最好使用归一化-标准化,因为它会消除尺度对方差和协方差的影响。
  • Min-Max缩放与Normalization-Standardization具有相同的缺点,并且新数据可能不会限制在[0,1],因为它们可能超出原始范围。一些算法,例如一些深度学习网络,更喜欢在0-1范围内输入,因此这是一个不错的选择。

以下是关于此主题的一些额外资源:

  • 当面对偏斜变量时,三种方法的比较可以在这里找到。
  • 关于特征缩放的深入研究可以在这里找到。

4.2  离散化

定义:离散化是通过创建一组跨越变量值范围的连续区间,将连续变量转换为离散变量的过程。

4.2.1  为什么离散化很重要

  • 通过将具有相似预测强度的相似属性进行分组,有助于提高模型性能
  • 引入非线性,从而提高模型的拟合能力
  • 通过分组值增强可解释性
  • 尽量减少极端值/很少反转模式的影响
  • 防止数值变量可能出现的过拟合
  • 允许连续变量之间的特征交互(第4.5.5节)

4.2.2  如何处理离散化


一般来说,没有最好的离散化方法。这确实取决于数据集和后续的学习算法。在决定之前,仔细研究你的特性和上下文。你也可以尝试不同的方法并比较模型的性能。

4.3  特征编码

4.3.1  为什么特征编码很重要

我们必须将分类变量的字符串转换为数字,以便算法能够处理这些值。即使你看到一个算法可以接受分类输入,最有可能的是,该算法将编码过程纳入其中。

4.3.2  如何处理特征编码




注意:如果我们在线性回归中使用one-hot编码,我们应该保留k-1个二进制变量以避免多重共线性。这对于在训练期间同时查看所有特征的任何算法都是如此。包括SVM、神经网络和聚类。另一方面,基于树的算法需要整个二进制变量集来选择最佳分割。


注意:不建议在树算法中使用one-hot编码。one-hot将导致分裂高度不平衡(因为原始分类特征的每个标签现在都是一个新特征),结果导致两个子节点中的任何一个都不会有很好的纯度增益。由于one-hot特征被分解为许多部分,因此one-hot特征的预测能力将弱于原始特征。

可以在这里找到关于WOE的详细介绍。

4.4  特征转换

4.4.1  为什么特征变换很重要

4.4.1.1  线性假设

回归

线性回归是一种直接的方法,用于预测定量响应Y,基于不同的预测变量X1、X2、... Xn。它假设X(s)和Y之间存在线性关系。从数学上讲,我们可以将这种线性关系写成Y≈β0+β1X1+β2X2+…+βnXn。

分类

同样,对于分类,逻辑回归假设变量与对数机率之间存在线性关系。

赔率=p/(1-p),其中p是y=1的概率

对数(概率)=β0 + β1X1 + β2X2 + ... + βnXn

为什么遵循线性假设很重要

如果机器学习模型假设预测值Xs和结果Y之间存在线性关系,当不存在这种线性关系时,模型的表现会较差。在这种情况下,我们最好尝试另一种不作这种假设的机器学习模型。

如果没有线性关系,我们必须使用线性/逻辑回归模型,数学变换/离散化可能有助于建立关系,尽管它不能保证更好的结果。

4.4.1.2 变量分布

线性回归假设

线性回归对预测变量X有以下假设:

  • 与结果Y之间存在线性关系
  • 多元正态性
  • 无或很少多重共线性
  • 同方差性

正态假设意味着每个变量X都应遵循高斯分布。

同方差性,也称为方差齐性,描述了一种情况,即误差项(即独立变量(Xs)和因变量(Y)之间的关系中的“噪声”或随机干扰)在所有独立变量的值中都是相同的。

违反同方差性和/或正态性的假设(假设数据分布是同方差或高斯的,而实际上不是)可能会导致模型性能不佳。

其余的机器学习模型,包括神经网络、支持向量机、基于树的方法和PCA,对自变量的分布没有任何假设。然而,在许多情况下,模型性能可能会受益于“类高斯”分布。

为什么模型可以从“高斯型”分布中受益?在正态分布的变量中,可用于预测Y的X观测值在更大的值范围内变化,即X的值在更大的范围内“扩散”。

在上述情况下,对原始变量的转换可以帮助使变量更接近高斯分布的钟形。

4.4.2  如何处理特征变换

对数变换在应用于偏斜分布时非常有用,因为它们往往会扩大落在较低幅度范围内的值,并倾向于压缩或减少落在较高幅度范围内的值,这有助于使偏斜分布尽可能地接近正态分布。

平方根变换在这个意义上做类似的事情。

sklearn中的Box-Cox变换是另一种属于幂变换函数家族的流行函数。该函数有一个先决条件,即要转换的数值必须是正数(类似于对数变换所期望的)。如果它们是负数,则使用常数值进行移位会有所帮助。从数学上讲,Box-Cox变换函数可以表示如下。

sklearn中的分位数变换将特征转换为遵循均匀分布或正态分布。因此,对于给定的特征,这种变换往往会分散最常见的值。它还降低了(边际)异常值的影响:因此,这是一种稳健的预处理方案。然而,这种变换是非线性的。它可能会扭曲以相同尺度测量的变量之间的线性相关性,但会使以不同尺度测量的变量更直接地具有可比性。
我们可以用QQ图来检查变量在转换后是否呈正态分布(理论分位数上值的45度直线)。

下面是一个例子,展示了sklearn的箱线图/Yeo-johnson/分位数变换的效果,将各种分布的数据映射到正态分布。

在“小”数据集(少于几百个点)上,分位数转换器很容易过度拟合。建议使用幂变
换。

4.5  特征生成

定义:通过现有功能的组合创建新功能。这是向数据集添加领域知识的好方法。

4.5.1  缺失数据派生特征

如第3.1节所述,我们可以创建一个新的二进制特征,用0/1表示原始特征上的观察值是否有缺失值。

4.5.2  简单统计派生特征

通过对原始特征进行简单的统计计算来创建新特征,包括:

  • 计数/求和
  • 平均值/中值/众数
  • 最大值/最小值/标准偏差/方差/范围/四分位数间距/变异系数
  • 时间跨度/间隔

以通话记录为例,我们可以创建新的功能,如通话次数、呼入/呼出次数、平均通话时长、每月平均通话时长、最长通话时长等。

4.5.3  特征交叉

在获得一些简单的统计衍生特征后,我们可以将它们交叉在一起。用于交叉的常见维度包括:

  • 时间
  • 区域
  • 业务类型

还是以通话记录为例,我们可以拥有交叉特征,如:夜间/日间通话次数、不同业务类型(银行/出租车服务/旅行/酒店)下的通话次数、过去3个月的通话次数等。第4.5.2节中提到的许多统计计算可以再次用于创建更多特征。

注意:可以在此处找到一个名为Featuretools的开源python框架,它可以帮助自动生成这些特征。

4.5.4  比率和比例

常见技术。例如,为了预测一个分支机构信用卡销售的未来表现,信用卡销售/销售人员或信用卡销售/营销支出等比率将比仅使用分支机构销售的绝对卡数更有说服力。

4.5.5  类别特征之间的叉积

考虑一个类别特征 A,有两个可能的值 {A1, A2}。假设 B 是一个具有可能性 {B1, B2} 的特征。那么,A 和 B 之间的特征交叉将采用以下值之一:{(A1, B1), (A1, B2), (A2, B1), (A2, B2)}。你基本上可以给这些“组合”任何你喜欢的名字。只要记住,每个组合都表示 A 和 B 的相应值所包含的信息之间的协同作用。

这是一种非常有用的技术,当某些特征共同表示一个属性时,比单独表示更好。从数学上讲,你正在对分类特征的所有可能值进行叉积。这个概念类似于第3.5.3节的特征交叉,但这个概念特别指的是两个分类特征之间的交叉。

4.5.6  多项式展开

叉积也可以应用于数值特征,从而在A和B之间产生新的交互特征。这可以通过sklearn的多项式特征轻松实现,它生成一个新的特征集,由所有特征的多项式组合组成,其次数小于或等于指定的次数。例如,三个原始特征{X1,X2,X3}可以生成一个特征集{1,X1X2,X1X3,X2X3,

4.5.7  通过树进行特征学习

在基于树的算法中,每个样本将被分配到一个特定的叶子节点。每个节点的决策路径可以被视为一个新的非线性特征,我们可以创建N个新的二元特征,其中n等于树或树集合中的叶子节点总数。然后,这些特征可以被馈送到其他算法,如逻辑回归。

本文中首次引入了使用树算法生成新特征的想法。

这种方法的好处是我们可以将几个特征的复杂组合组合在一起,这很有意义(正如树的学习算法所构造的那样)。与手动进行特征交叉相比,这为我们节省了大量时间,并且广泛用于在线广告行业的点击率(CTR)。

4.5.8  通过深度网络进行特征学习

从以上内容中我们可以看出,人工生成特征需要付出大量努力,并且可能无法保证良好的回报,特别是在我们拥有大量特征的情况下。使用树进行特征学习可以被视为自动创建特征的早期尝试,随着深度学习方法在2016年左右流行起来,它们也在这一领域取得了一些成功,如自动编码器和受限玻尔兹曼机。它们已被证明可以自动并以无监督或半监督的方式学习特征的抽象表示(压缩形式),这反过来又支持了语音识别、图像分类、物体识别等领域最先进的结果。然而,这些特征的可解释性有限,深度学习需要更多的数据才能提取高质量的结果。

4.5.9 特征生成代码

以下是一些常用的特征加工函数:

# pandas自带的聚合函数
mean(): Compute mean of groupssum(): Compute sum of group valuessize(): Compute group sizescount(): Compute count of groupstd(): Standard deviation of groupsvar(): Compute variance of groupssem(): Standard error of the mean of groupsfirst(): Compute first of group valueslast(): Compute last of group valuesnth() : Take nth value, or a subset if n is a listmin(): Compute min of group valuesmax(): Compute max of group values# 自定义函数def median(x): return np.median(x)def variation_coefficient(x): mean = np.mean(x) if mean != 0: return np.std(x) / mean else: return np.nandef variance(x): return np.var(x)def skewness(x): if not isinstance(x, pd.Series): x = pd.Series(x) return pd.Series.skew(x)def kurtosis(x): if not isinstance(x, pd.Series): x = pd.Series(x) return pd.Series.kurtosis(x)def standard_deviation(x): return np.std(x)def large_standard_deviation(x): if (np.max(x)-np.min(x)) == 0: return np.nan else: return np.std(x)/(np.max(x)-np.min(x))def variation_coefficient(x): mean = np.mean(x) if mean != 0: return np.std(x) / mean else: return np.nandef variance_std_ratio(x): y = np.var(x) if y != 0: return y/np.sqrt(y) else: return np.nandef ratio_beyond_r_sigma(x, r): if x.size == 0: return np.nan else: return np.sum(np.abs(x - np.mean(x)) > r * np.asarray(np.std(x))) / x.sizedef range_ratio(x): mean_median_difference = np.abs(np.mean(x) - np.median(x)) max_min_difference = np.max(x) - np.min(x) if max_min_difference == 0: return np.nan else: return mean_median_difference / max_min_difference
def has_duplicate_max(x): return np.sum(x == np.max(x)) >= 2def has_duplicate_min(x): return np.sum(x == np.min(x)) >= 2def has_duplicate(x): return x.size != np.unique(x).sizedef count_duplicate_max(x): return np.sum(x == np.max(x))def count_duplicate_min(x): return np.sum(x == np.min(x))def count_duplicate(x): return x.size - np.unique(x).sizedef sum_values(x): if len(x) == 0: return 0 return np.sum(x)def log_return(list_stock_prices): return np.log(list_stock_prices).diff() def realized_volatility(series): return np.sqrt(np.sum(series**2))def realized_abs_skew(series): return np.power(np.abs(np.sum(series**3)),1/3)def realized_skew(series): return np.sign(np.sum(series**3))*np.power(np.abs(np.sum(series**3)),1/3)def realized_vol_skew(series): return np.power(np.abs(np.sum(series**6)),1/6)def realized_quarticity(series): return np.power(np.sum(series**4),1/4)def count_unique(series): return len(np.unique(series))def count(series): return series.size#drawdons functions are minedef maximum_drawdown(series): series = np.asarray(series) if len(series)<2: return 0 k = series[np.argmax(np.maximum.accumulate(series) - series)] i = np.argmax(np.maximum.accumulate(series) - series) if len(series[:i])<1: return np.NaN else: j = np.max(series[:i]) return j-kdef maximum_drawup(series): series = np.asarray(series) if len(series)<2: return 0
series = - series k = series[np.argmax(np.maximum.accumulate(series) - series)] i = np.argmax(np.maximum.accumulate(series) - series) if len(series[:i])<1: return np.NaN else: j = np.max(series[:i]) return j-kdef drawdown_duration(series): series = np.asarray(series) if len(series)<2: return 0 k = np.argmax(np.maximum.accumulate(series) - series) i = np.argmax(np.maximum.accumulate(series) - series) if len(series[:i]) == 0: j=k else: j = np.argmax(series[:i]) return k-jdef drawup_duration(series): series = np.asarray(series) if len(series)<2: return 0 series=-series k = np.argmax(np.maximum.accumulate(series) - series) i = np.argmax(np.maximum.accumulate(series) - series) if len(series[:i]) == 0: j=k else: j = np.argmax(series[:i]) return k-jdef max_over_min(series): if len(series)<2: return 0 if np.min(series) == 0: return np.nan return np.max(series)/np.min(series)def mean_n_absolute_max(x, number_of_maxima = 1): """ Calculates the arithmetic mean of the n absolute maximum values of the time series.""" assert ( number_of_maxima > 0 ), f" number_of_maxima={number_of_maxima} which is not greater than 1" n_absolute_maximum_values = np.sort(np.absolute(x))[-number_of_maxima:] return np.mean(n_absolute_maximum_values) if len(x) > number_of_maxima else np.NaNdef count_above(x, t): if len(x)==0: return np.nan else: return np.sum(x >= t) / len(x)def count_below(x, t): if len(x)==0: return np.nan else: return np.sum(x <= t) / len(x)#number of valleys = number_peaks(-x, n)def number_peaks(x, n): """ Calculates the number of peaks of at least support n in the time series x. A peak of support n is defined as a subsequence of x where a value occurs, which is bigger than its n neighbours to the left and to the right. """ x_reduced = x[n:-n] res = None for i in range(1, n + 1): result_first = x_reduced > _roll(x, i)[n:-n] if res is None: res = result_first else: res &= result_first res &= x_reduced > _roll(x, -i)[n:-n] return np.sum(res)def mean_abs_change(x): return np.mean(np.abs(np.diff(x)))def mean_change(x): x = np.asarray(x) return (x[-1] - x[0]) / (len(x) - 1) if len(x) > 1 else np.NaNdef mean_second_derivative_central(x): x = np.asarray(x) return (x[-1] - x[-2] - x[1] + x[0]) / (2 * (len(x) - 2)) if len(x) > 2 else np.NaNdef root_mean_square(x): return np.sqrt(np.mean(np.square(x))) if len(x) > 0 else np.NaNdef absolute_sum_of_changes(x): return np.sum(np.abs(np.diff(x)))def longest_strike_below_mean(x): if not isinstance(x, (np.ndarray, pd.Series)): x = np.asarray(x) return np.max(_get_length_sequences_where(x < np.mean(x))) if x.size > 0 else 0def longest_strike_above_mean(x): if not isinstance(x, (np.ndarray, pd.Series)): x = np.asarray(x) return np.max(_get_length_sequences_where(x > np.mean(x))) if x.size > 0 else 0def count_above_mean(x): m = np.mean(x) return np.where(x > m)[0].sizedef count_below_mean(x): m = np.mean(x) return np.where(x < m)[0].sizedef last_location_of_maximum(x): x = np.asarray(x) return 1.0 - np.argmax(x[::-1]) / len(x) if len(x) > 0 else np.NaNdef first_location_of_maximum(x): if not isinstance(x, (np.ndarray, pd.Series)): x = np.asarray(x) return np.argmax(x) / len(x) if len(x) > 0 else np.NaNdef last_location_of_minimum(x): x = np.asarray(x) return 1.0 - np.argmin(x[::-1]) / len(x) if len(x) > 0 else np.NaNdef first_location_of_minimum(x): if not isinstance(x, (np.ndarray, pd.Series)): x = np.asarray(x) return np.argmin(x) / len(x) if len(x) > 0 else np.NaN# Test non-consecutive non-reoccuring values ?def percentage_of_reoccurring_values_to_all_values(x): if len(x) == 0: return np.nan unique, counts = np.unique(x, return_counts=True) if counts.shape[0] == 0: return 0 return np.sum(counts > 1) / float(counts.shape[0])def percentage_of_reoccurring_datapoints_to_all_datapoints(x): if len(x) == 0: return np.nan if not isinstance(x, pd.Series): x = pd.Series(x) value_counts = x.value_counts() reoccuring_values = value_counts[value_counts > 1].sum() if np.isnan(reoccuring_values): return 0 return reoccuring_values / x.sizedef sum_of_reoccurring_values(x): unique, counts = np.unique(x, return_counts=True) counts[counts < 2] = 0 counts[counts > 1] = 1 return np.sum(counts * unique)def sum_of_reoccurring_data_points(x): unique, counts = np.unique(x, return_counts=True) counts[counts < 2] = 0 return np.sum(counts * unique)def ratio_value_number_to_time_series_length(x): if not isinstance(x, (np.ndarray, pd.Series)): x = np.asarray(x) if x.size == 0: return np.nan return np.unique(x).size / x.sizedef abs_energy(x): if not isinstance(x, (np.ndarray, pd.Series)): x = np.asarray(x) return np.dot(x, x)def quantile(x, q): if len(x) == 0: return np.NaN return np.quantile(x, q)# crossing the mean ? other levels ? def number_crossing_m(x, m): if not isinstance(x, (np.ndarray, pd.Series)): x = np.asarray(x) # From https://stackoverflow.com/questions/3843017/efficiently-detect-sign-changes-in-python positive = x > m return np.where(np.diff(positive))[0].sizedef absolute_maximum(x): return np.max(np.absolute(x)) if len(x) > 0 else np.NaNdef value_count(x, value): if not isinstance(x, (np.ndarray, pd.Series)): x = np.asarray(x) if np.isnan(value): return np.isnan(x).sum() else: return x[x == value].sizedef range_count(x, min, max): return np.sum((x >= min) & (x < max))def mean_diff(x): return np.nanmean(np.diff(x.values))

5  特征选择


定义:特征选择是为机器学习模型构建选择相关特征子集的过程。


并非总是存在“数据越多,结果越好”这一真理。包含无关特征(对预测毫无帮助的特征)和冗余特征(与其他特征无关的特征)只会使学习过程不堪重负,并容易导致过拟合。


通过特征选择,我们可以:


  • 简化模型,使其更容易解释
  • 更短的训练时间和更低的计算成本
  • 数据收集成本更低
  • 避免维度诅咒
  • 通过减少过拟合来增强泛化能力

我们应该记住,不同的特征子集可以为不同的算法提供最佳性能。因此,它不是机器学习模型训练的一个单独过程。因此,如果我们为线性模型选择特征,最好使用针对这些模型的选择程序,如回归系数或套索的重要性。如果我们为树选择特征,最好使用树导出的重要性。

5.1  过滤方法

筛选方法根据性能指标选择特征,而不管以后采用哪种机器学习算法。

单变量过滤器根据特定标准对单个特征进行评估和排名,而多变量过滤器则对整个特征空间进行评估。过滤器方法有:

  • 选择变量,不考虑模型
  • 计算成本更低
  • 通常会带来较低的预测性能

因此,过滤方法适合第一步快速筛选和去除无关特征。


在记分卡开发中,WOE编码(见第4.3.2节)和IV通常齐头并进。这两个概念都来自
逻辑回归,是信用卡行业的标准做法。IV是一种流行且广泛使用的度量方法,因为与IV相关的变量选择有非常方便的经验法则,如下所述


然而,所有这些过滤方法都没有考虑特征之间的相互作用,可能会降低我们的预测能
力。我个人只使用方差和相关性来过滤一些绝对不必要的特征。

注意:在使用卡方检验或单变量选择方法时,要记住一件事,即在非常大的数据集中,大多数特征将显示较小的p值,因此看起来它们具有很高的预测性。这实际上是样本大小的影响。因此,在使用这些程序选择特征时应该小心。超小的p值并不能突出超重要的特征,而是表明数据集包含太多的样本。

注意:相关特征不一定影响模型性能(树等),但高维度会,过多的特征会损害模型的可解释性。因此,减少相关特征总是更好的。

5.2  包装方法

包装器使用搜索策略在可能的特征子集空间中搜索,并通过ML算法的性能质量评估每个子集。实际上,搜索策略和算法的任何组合都可以用作包装器。它的特点是:

  • 使用ML模型对特征子集进行评分
  • 在每个子集上训练一个新的模型
  • 计算成本非常高
  • 通常为给定的机器学习算法提供性能最佳的子集,但可能不适合另一个
  • 需要任意定义的停止标准

最常见的搜索策略组是顺序搜索,包括正向选择、反向排除和穷尽搜索。随机搜索是另一种流行的选择,包括遗传等进化计算算法和模拟退火。

包装器中的另一个关键要素是停止条件。什么时候停止搜索?一般来说有三个:

  • 性能提升
  • 性能下降
  • 达到预定义的特征数量

5.2.1  前向选择

前向特征选择首先根据预设评估标准单独评估所有特征,并选择产生最佳性能算法的特征。在第二步中,它评估所选特征和第二个特征的所有可能组合,并根据相同的预设标准选择产生最佳性能算法的组合。

预设标准可以是roc_auc(用于分类)和r平方(用于回归)。

这种选择过程被称为贪婪,因为它评估了所有可能的单、双、三等特征组合。因此,它在计算上非常昂贵,有时,如果特征空间很大,甚至不可行。

有一个专门针对python的包可以实现这种特征选择:mlxtend

5.2.2  后向消除

反向特征选择首先使用所有特征拟合模型。然后它删除一个特征。它会删除一个对特定评估标准产生最高性能算法(统计上最不显著)的特征。在第二步中,它会删除第二个特征,即再次产生最佳性能算法的特征。然后它继续删除特征,直到满足特定标准。

例如,预设标准可以是roc_auc用于分类,r平方用于回归。

5.2.3  穷举特征选择

在穷举特征选择中,通过优化特定机器学习算法的指定性能指标,在所有可能的特征子集中选择最佳特征子集。例如,如果分类器是逻辑回归,数据集由4个特征组成,则算法将评估所有15个特征组合,如下所示:

  • 1个特征的所有可能组合
  • 2个特征的所有可能组合
  • 3个特征的所有可能组合
  • 全部 4 个特征

并选择能够产生最佳性能(例如,分类准确率)的逻辑回归分类器。

这种穷举搜索的计算成本非常高。在实践中,由于这种计算成本,它很少被使用。

5.2.4  遗传算法

待办事项

5.3  嵌入方法

嵌入式方法结合了过滤器和包装器方法的优点。学习算法利用其自身的变量选择过程,同时进行特征选择和分类。常见的嵌入式方法包括Lasso和各种基于树的算法。
其特征为:

  • 将特征选择作为模型构建过程的一部分
  • 考虑特征之间的相互作用
  • 与 Wrappers 相比,计算成本更低,因为它只训练模型一次
  • 通常为给定的机器学习算法提供性能最好的子集,但可能不适用于另一个算

5.3.1  Lasso 正则化

正则化包括对机器学习模型的不同参数添加惩罚,以减少模型的自由度。因此,模型不太可能适应训练数据的噪声,因此不太可能过拟合。

在线性模型正则化中,对每个预测因子乘的系数应用惩罚。对于线性模型,通常有三种正则化:

  • L1正则化(Lasso)
  • L2正则化(Ridge)
  • L1/L2(弹性网)

从不同类型的正则化中,Lasso(L1)具有能够将某些系数收缩为零的特性。因此,可以从模型中删除该特征。

对于线性回归和逻辑回归,我们可以使用 Lasso 正则化来删除不重要的特征。请记住,增加惩罚将增加删除的特征数量。因此,你需要密切关注并监控,不要将惩罚设置得太高,以至于删除甚至重要的特征,也不要设置得太低,以至于不删除不重要的特征。

话虽如此,如果惩罚过高,重要特征被删除,你应该注意到算法性能的下降,然后意识到你需要减少正则化。

正规化是一个很大的主题。有关信息,您可以在这里参考:

  • 最陡梯度法和L1正则化回归:一项综述
  • 生物信息学中的正则化特征选择和分类
  • 特征选择分类:一项综述
  • 机器学习解析:正则化

5.3.2  随机森林重要性

随机森林是最流行的机器学习算法之一。它们之所以如此成功,是因为它们通常具有很好的预测性能、低过拟合和易于解释性。这种可解释性是由于可以直接得出每个变量在树决策中的重要性。换句话说,很容易计算出每个变量对决策的贡献程度。

随机森林是一种bagging算法,由一堆基础估计器(决策树)组成,每个基础估计器都是从数据集中随机抽取观测值和随机抽取特征构建的。不是每棵树都看到所有特征或所有观测值,这保证了树之间是去相关的,因此不容易过拟合。

每棵树也是基于单个特征或特征组合的一系列是非问题。在每次分裂时,问题将数据集分为两个桶,每个桶中的观测值之间更相似,并且与其他桶中的观测值不同。因此,每个特征的重要性是由每个桶的“纯度”决定的。

对于分类,杂质的度量是基尼杂质或信息增益/熵。对于回归,杂质的度量是方差。因此,在训练树时,可以计算每个特征减少杂质的程度。特征减少杂质越多,特征就越重要。在随机森林中,每个特征的杂质减少可以在树之间进行平均,以确定变量的最终重要性。

使用树衍生的特征重要性来选择特征是一种非常简单、快速且通常准确的方法,可以为机器学习选择好的特征。特别是,如果你打算构建树方法。

然而,与在没有相关对应物的情况下构建树时的重要性相比,相关特征在树中将以相似且降低的重要性显示出来。

限制

  • 相关特征显示了相似的重要性
  • 当构建树时没有与其相关的对应物,相关特征的重要性低于实际重要性
  • 高枢椎变量往往显示出更高的重要性

5.3.3  梯度提升树重要性

与使用随机森林导出的特征重要性选择特征类似,我们可以根据梯度提升树导出的重要性选择特征。我们可以一次性完成,也可以以递归方式完成,这取决于我们有多少时间、数据集中有多少特征以及它们是否相关。

5.4  特征改组

特征选择的一种流行方法是对特定变量的值进行随机洗牌,并确定该置换如何影响机器学习算法的性能指标。换句话说,该想法是对每个特征的值进行置换,一次一个,并测量置换对机器学习模型的准确性、roc_auc或mse的降低程度。如果变量很重要,这是高度预测性的,那么它们的值的随机置换将大大降低这些指标中的任何一个。相反,不重要/非预测性变量应该对我们评估的模型性能指标几乎没有影响。

5.5  混合方法

5.5.1  递归特征消除

该方法包括以下步骤:

  1. 根据机器学习算法得出的特征重要性对特征进行排名:可以是树重要性,或 LASSO/Ridge,或线性/逻辑回归系数。
  2. 删除一个特征(最不重要的特征),并利用剩余的特征构建机器学习算法。
  3. 计算你选择的性能指标:roc-auc、mse、rmse、准确率。
  4. 如果指标下降超过任意设置的阈值,则该特征很重要,应该保留。否则,我们可以删除该特征。
  5. 重复步骤2-4,直到所有特征都被删除(并因此被评估),并且性能下降得到评估。

该方法结合了诸如封装器之类的选择过程和来自诸如嵌入式方法之类的ML模型的特征重要性推导,因此被称为混合方法。

该方法与反向特征选择的不同之处在于,它不会首先删除所有特征以确定删除哪一个。它根据机器学习模型得出的重要性删除最不重要的特征。然后,它评估该特征是否应该删除。因此,它在选择过程中只删除每个特征一次,而反向特征选择在选择的每一步都删除所有特征。

因此,这种方法比包装器方法更快,通常比嵌入式方法更好。在实践中,它非常有效。它也考虑了相关性(取决于你设置任意性能下降阈值的严格程度)。不利的一面是,评估性能下降以决定保留或删除特征是任意设置的。下降越小,选择的功能越多,反之亦然。

示例:使用随机森林重要性进行递归特征排除

正如我们在第4.3.2节中提到的,随机森林对高度相关的特征赋予相同或相似的重要性。此外,当特征相关时,如果没有相关特征,构建的树的重要性低于特征本身的重要性。

因此,我们可以通过递归删除一个特征,并在每一轮重新计算重要性,而不是一次性地(从所有初始特征中)根据重要性删除特征,从而得到更好的选择。

在这种情况下,当删除与另一个特征高度相关的特征时,剩余特征的重要性就会增加。这可能导致更好的子集特征空间选择。不利的一面是,构建多个随机森林非常耗时,特别是当数据集包含大量特征时

5.5.2  递归特征添加

此方法包括以下步骤:

  1. 根据机器学习算法得出的特征重要性对特征进行排名:可以是树重要性,或 LASSO/Ridge,或线性/逻辑回归系数。
  2. 构建一个只有一个特征(最重要的特征)的机器学习模型,并计算模型的性能指标。
  3. 添加一个最重要的特征,并利用添加的特征和前几轮中的任何特征构建机器学习算法。
  4. 计算你选择的性能指标:roc-auc、mse、rmse、accuracy。
  5. 如果指标增加超过任意设置的阈值,则该特征很重要,应该保留。否则,我们可以删除该特征。
  6. 重复步骤2-5,直到所有特征都已被删除(并因此被评估),并且性能下降得到了评估。

此方法与向前特征选择之间的区别是相似的。它不会首先查找所有特征以确定添加哪个特征,因此它比包装器更快。

5.6  降维

PCA(主成分分析)

SVD(奇异值分解)

6  数据泄漏

数据泄露是指使用训练数据集以外的信息来创建模型。结果是你可能创建了过于乐观的模型,这些模型实际上毫无用处,不能用于生产。该模型在训练和测试数据上都显示出很好的结果,但事实上,这不是因为你的模型真的具有良好的泛化能力,而是因为它使用了测试数据中的信息。

众所周知,使用交叉验证或至少在训练和评估模型时分离验证集,但人们很容易在特征工程和选择过程中忘记这样做。请记住,测试数据集不得以任何方式用于对模型做出选择,包括特征工程和选择。

参考资料:
1、《A Short Guide for Feature Engineering and Feature Selection》
2、《 Penalized feature selection and classification in bioinformatics》
3、《 Least angle and ℓ1 penalized regression: A review》

编辑:王菁



关于我们

数据派THU作为数据科学类公众号,背靠清华大学大数据研究中心,分享前沿数据科学与大数据技术创新研究动态、持续传播数据科学知识,努力建设数据人才聚集平台、打造中国大数据最强集团军。



新浪微博:@数据派THU

微信视频号:数据派THU

今日头条:数据派THU