特征工程是机器学习流程中的关键步骤,
在此过程中,原始数据被转换为更具意义的特征,以增强模型对数据关系的理解能力。
特征工程通常涉及对现有数据应用转换,以生成或修改数据,这些转换后的数据在机器学习和数据科学的语境下用于训练模型,从而提高模型性能。
本文主要介绍处理数值变量特征工程,将探讨使用Python的Scikit-Learn库、Numpy等工具处理数值的高级
特征工程技术
,旨在提升机器学习模型的效能。
特征优化是提升机器学习模型质量的核心要素,尤其在分析复杂数据集时。有针对性地应用特征工程技术可带来以下优势:
-
揭示数据中的潜在模式:
此技术能够发现初步观察中不易察觉的隐藏关系和结构。
-
优化变量表示:
此过程将原始数据转换为更适合机器学习的格式。
-
应对数据分布和内在特性相关的挑战:
此方法解决了诸如偏度、异常值和变量可扩展性等问题。
精确实施这些特征优化技术可显著提升机器学习模型的性能。
这些改进体现在模型性能的多个方面,从预测能力到可解释性。高质量特征使模型能够捕捉到
数据中可能被忽视的细微差别和复杂模式。
特征优化还有助于增强模型的稳健性和泛化能力
,这对于实际应用至关重要,同时降低了过拟合的风险。
接下来,我们将介绍一些实用的特征工程技术。
1、归一化
归一化(也称为缩放)可能是数据科学家学习的第一个数值特征工程技术。
这种方法通过减去平均值并除以标准差来调整变量。
执行此转换后,结果变量将具有0均值和1的标准差及方差。
在机器学习中,特别是深度学习领域,将变量限制在特定范围内(如仅在0和1之间)有助于模型更快地收敛到最优解。这是一种学习型转换 - 我们使用训练数据来推导正确的均值和标准差值,然后在应用于新数据时使用这些值进行转换。
需要注意的是,
这种转换不会改变分布
,而是重新缩放了值。
我们将使用Sklearn的葡萄酒数据集进行分类任务。我们将比较使用和不使用混淆矩阵归一化的性能,使用Sklearn实现。
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import confusion_matrix
import numpy as np
X, y = load_wine(return_X_y=True)
# 将数据划分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=42)
# 定义训练模型并获取混淆矩阵的函数
def get_confusion_matrix(X_train, X_test, y_train, y_test):
model = KNeighborsClassifier(n_neighbors=5)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
return confusion_matrix(y_test, y_pred)
# 获取未归一化的混淆矩阵
cm_without_norm = get_confusion_matrix(X_train, X_test, y_train, y_test)
# 归一化数据
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 获取归一化后的混淆矩阵
cm_with_norm = get_confusion_matrix(X_train_scaled, X_test_scaled, y_train, y_test)
# 创建两个并列的子图
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 8))
# 定义绘制热图的函数
def plot_heatmap(ax, cm, title):
sns.heatmap(cm, annot=True, fmt='d', cmap='viridis', ax=ax, cbar=False)
ax.set_title(title, fontsize=16, pad=20)
ax.set_xlabel(''Predicted', fontsize=12, labelpad=10)
ax.set_ylabel('Actual', fontsize=12, labelpad=10)
# 绘制热图
plot_heatmap(ax1, cm_without_norm, 'Confusion Matrix\nWithout Normalization')
plot_heatmap(ax2, cm_with_norm, 'Confusion Matrix\nWith Normalization')
# 添加共用的颜色条
cbar_ax = fig.add_axes([0.92, 0.15, 0.02, 0.7])
sm = plt.cm.ScalarMappable(cmap='viridis', norm=plt.Normalize(vmin=0, vmax=np.max([cm_without_norm, cm_with_norm])))
fig.colorbar(sm, cax=cbar_ax)
# 调整布局并显示图表
plt.tight_layout(rect=[0, 0, 0.9, 1])
plt.show()
性能提升约为30% - 对某些算法而言,归一化的影响如此显著,以至于不正确地应用它可能导致数据科学家犯严重错误。
归一化还有一些变体。在Sklearn中,这些变体被称为RobustScaler和MinMaxScaler。
Sklearn示例中提供了一个更复杂的图表,展示了归一化和未归一化的KNNClassifier模型的分类边界对比。
2、多项式特征
多项式特征是一种在线性模型中引入非线性的有效方法。Scikit-Learn的PolynomialFeatures类能够生成多项式特征和变量间的交互项。
常见的多项式特征包括:
-
x ² (平方项)
-
x ³ (立方项)
-
x ⁴ (四次方项)
-
更高次项
对于具有多个特征的模型(x_1, x_2, …, x_n),还可以创建交互项:
-
_x__ 1 × _x__ 2 (一阶交互项)
-
_x__ 1² × _x__ 2 (二阶交互项)
-
_x__ 1 × _x__ 2² (二阶交互项)
-
更高阶交互项
多项式特征的主要目标是使线性模型能够学习数据中的非线性关系,而无需改变底层
算法。
多项式特征的主要优势在于显著增加了模型的灵活性,
使线性模型能够捕捉数据中的非线性关系。具体表现为:
另一个关键优势是能够揭示变量之间隐藏的交互。在物理学或经济学等领域,关系通常是非线性的,这个特性尤为有价值。
但是多项式特征也存在一些缺点:
-
它迅速增加了数据集的维度,为每个输入特征创建额外的列
-
过度使用可能导致过拟合
-
由于需要处理更多的特征,计算资源需求增加
从实际角度来看,多项式特征的实现相对简单,这要归功于Python中的Sklearn库。下面我们将展示如何实现它。
PolynomialFeatures是Scikit-learn中用于生成多项式特征的类,位于sklearn.preprocessing模块中。
该类将一维输入数组转换为包含所有多项式项(直至指定次数)的新数组。例如,如果原始特征是[a, b],次数为2,则结果特征将是[1, a, b, a², ab, b²]。
该类的主要参数如下:
-
degree
(int, 默认=2):多项式的次数。 确定生成的多项式项的最高次数。
-
interaction_only
(bool, 默认=False):如果为True,则仅生成交互项。不产生单个特征的幂。
-
include_bias
(bool, 默认=True):如果为True,包括一列1(偏置项)。在使用没有单独截距项的模型时有用。
-
order
(C或F, 默认=C):确定特征的输出顺序。C表示C风格顺序(最后的特征变化最快),'F'表示Fortran风格顺序。
以下是在Sklearn中实现该类的示例:
from sklearn.preprocessing import PolynomialFeatures
import numpy as np
X = np.array([[1, 2], [3, 4]])
poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly.fit_transform(X)
print(X_poly)
# 输出: [[1. 2. 1. 2. 4.]
# [3. 4. 9. 12. 16.]]
print(poly.get_feature_names(['x1', 'x2']))
# 输出: ['x1', 'x2', 'x1^2', 'x1 x2', 'x2^2']
这些生成的特征可以为机器学习模型提供额外的信息,潜在地提高其性能。
3、FunctionTransformer
FunctionTransformer是Scikit-learn中的一个多功能工具,允许将自定义函数集成到数据转换过程中。它能够将任意函数应用于数据,作为预处理或特征工程管道的一部分。它将Python函数转换为与Scikit-learn API兼容的"转换器"对象(这里的Transformer不同于深度学习模型,而是指Sklearn中的转换器概念)。
FunctionTransformer以Python函数为主要输入,创建一个转换器对象。当应用于数据时,这个对象执行指定的函数。它可以与其他转换器结合使用,或在Scikit-learn管道中使用。
一个具体的应用例子是将该对象应用于时间序列以提取三角函数特征。
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(12, 4))
average_week_demand = df.groupby(["weekday", "hour"])["count"].mean()
average_week_demand.plot(ax=ax)
_ = ax.set(
title="Average hourly bike demand during the week",
xticks=[i * 24 for i in range(7)],
xticklabels=["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
xlabel="Time of the week"
)
FunctionTransformer的典型应用包括:
FunctionTransformer充当了自定义Python函数和Scikit-learn函数之间的桥梁,为数据预处理和特征工程提供了灵活性。
以下代码展示了如何应用函数来创建上述时间序列的三角函数转换:
import numpy as np
import pandas as pd
from sklearn.preprocessing import FunctionTransformer
import matplotlib.pyplot as plt
def sin_transformer(period):
return FunctionTransformer(lambda x: np.sin(x / period * 2 * np.pi))
def cos_transformer(period):
return FunctionTransformer(lambda x: np.cos(x / period * 2 * np.pi))
hour_df = pd.DataFrame(
np.arange(26).reshape(-1, 1),
columns=["hour"],
)
hour_df["hour_sin"] = sin_transformer(24).fit_transform(hour_df)["hour"]
hour_df["hour_cos"] = cos_transformer(24).fit_transform(hour_df)["hour"]
hour_df.plot(x="hour")
_ = plt.title("Trigonometric encoding of the 'hour' feature")
FunctionTransformer在时间序列分析中有广泛的应用。
4、KBinsDiscretizer
KBinsDiscretizer是Scikit-learn中的一个预处理类,设计用于
将
连续特征转换为离散分类特征。
这个过程被称为离散化、量化或分箱。某些具有连续特征的数据集可能会从离散化中受益,因为它可以将具有连续属性的数据集转换为仅具有名义属性的数据集。
其主要目标是将连续变量的范围划分为特定数量的区间(或箱)。
每个原始值都被替换为它所属的箱的标签。
该算法的工作原理如下:
-
分析连续特征的分布
-
基于这个分布创建预定义数量的箱
-
将每个原始值分配到适当的箱中
-
用箱标签或箱的独热编码替换原始值
关键参数:
n_bins:
要创建的箱数。可以是整数或整数数组,用于每个特征的不同箱数。
encode:
编码箱的方法(onehot、ordinal或onehot-dense)。
strategy:
定义箱边界的策略(uniform、quantile或kmeans)。
需要考虑的因素:
我们将通过观察线性回归和决策树在学习连续模式与离散化模式时的性能来展示应用。创建一个随机但半线性数字的模拟数据集,将模型应用于连续数据,然后将相同的数据集应用于离散化特征。
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import KBinsDiscretizer
from sklearn.tree import DecisionTreeRegressor
# 设置随机数生成器以确保可重复性
rnd = np.random.RandomState(42)
# 创建数据集
X = rnd.uniform(-3, 3, size=100) # 在-3和3之间的100个点
y = np.sin(X) + rnd.normal(size=len(X)) / 3 # 正弦函数加噪声
X = X.reshape(-1, 1) # 重塑为sklearn所需的正确格式
# 应用KBinsDiscretizer
discretizer = KBinsDiscretizer(n_bins=10, encode="onehot")
X_binned = discretizer.fit_transform(X)
# 准备可视化
fig, (ax1, ax2) = plt.subplots(ncols=2, sharey=True, figsize=(12, 5))
line = np.linspace(-3, 3, 1000).reshape(-1, 1) # 用于绘图的点
# 训练和绘制模型的函数
def train_and_plot(X_train, X_plot, ax, title):
# 线性回归
linear_reg = LinearRegression().fit(X_train, y)
ax.plot(line, linear_reg.predict(X_plot), linewidth=2, color="green", label="Linear Regression")
# 决策树
tree_reg = DecisionTreeRegressor(min_samples_split=3, random_state=0).fit(X_train, y)
ax.plot(line, tree_reg.predict(X_plot), linewidth=2, color="red", label="Decision Tree")
# 原始数据
ax.plot(X[:, 0], y, "o", c="k", alpha=0.5)
ax.legend(loc="best")
ax.set_xlabel("Input Feature")
ax.set_title(title)
# 绘制原始数据的图
train_and_plot(X, line, ax1, "Results Before Discretization")
ax1.set_ylabel("Regression Output")
# 绘制离散化数据的图
line_binned = discretizer.transform(line)
train_and_plot(X_binned, line_binned, ax2, "Results After Discretization")
plt.tight_layout()
plt.show()
线性回归在离散化后显著改善,更好地捕捉了非线性。决策树显示的变化较小,因为它本身就能处理非线性。这个例子说明了离散化如何帮助线性模型捕捉非线性关系,在某些情况下可能会提高性能。
5、对数变换
对数变换的主要优势在于其压缩值范围的能力,这对于具有高可变性或异常值的数据特别有用。
-
范围压缩:
对数变换减少了最大值之间的距离,同时保持较小值相对不变。有助于规范化偏斜分布,使右尾分布更对称,更接近正态分布。
-
线性化:
它可以将非线性关系线性化。它将指数关系转换为线性关系,简化了分析并提高了假设变量之间线性关系的模型的性能。
-
处理异常值:
该变换有效地管理极端数据,允许处理异常值而无需删除,从而保留潜在的重要信息。
-
数学定义:
最常见的对数变换使用自然对数(以e为底),定义为 y =ln(x),其中 x 是原始值,y 是变换后的值。请注意,这种变换仅对正值的 x 定义,如果存在零或负值,可能需要添加常数。
-
特征缩放:
它可以用作特征缩放技术,补充或替代标准化或最小-最大规范化等方法。可以提高线性回归等模型的性能,这些模型受益于具有更对称分布的特征。
在机器学习中,当想要规范化一个不是自然分布的分布时,通常会使用对数变换。
例如,
一个众所周知的不正态分布的变量是年收入
—— 你经常想要对这个变量建模以提供价值预测,但使
用这种分布工作并不方便,特别是如果你使用不能正确建模非线性数据的算法。
通过对数变换,使用numpy,可以趋向正态分布,使变量更容易预测。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 创建一个具有正偏度值的示例数据集
np.random.seed(42)
data = {
'Income': np.random.exponential(scale=50000, size=1000) # 指数分布以模拟偏度
}
df = pd.DataFrame(data)
# 创建一个具有两个并排子图的图形
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
# 原始分布的图
axes[0].hist(df['Income'], bins=50, color='blue', alpha=0.7)
axes[0].set_title('Original Income Distribution')
axes[0].set_xlabel('Income')
axes[0].set_ylabel('Frequency')
# 应用对数变换
df['Log_Income'] = np.log1p(df['Income']) # log1p相当于log(x + 1)
# 变换后分布的图
axes[1].hist(df['Log_Income'], bins=50, color='green', alpha=0.7)
axes[1].set_title('Log-transformed Income Distribution')
axes[1].set_xlabel('Log_Income')
axes[1].set_ylabel('Frequency')
# 显示图形
plt.tight_layout()
plt.show()
6、PowerTransformer
PowerTransformer是Sklearn preprocessing模块中的一个类,包含用于使数据更接近高斯分布的逻辑。这对于建模与异方差(即非恒定方差)相关的问题或其他需要正态性的情况很有用。
,PowerTransformer支持Box-Cox和Yeo-Johnson变换。使用最大似然(对数似然)估计最优参数以稳定方差并最小化偏度。
Box-Cox要求输入数据严格为正,而Yeo-Johnson支持正负数据。
在机器学习的背景下,这些变换解决了几个常见挑战:
-
数据规范化:
许多机器学习算法,如线性回归、神经网络和一些聚类方法,假设数据遵循正态分布。PowerTransformer可以将偏斜或重尾分布转换为更接近高斯分布的形状,这可以提高这些模型的性能。
-
方差稳定化:
在真实数据集中,特征的方差通常随其幅度变化,这种现象称为异方差。这可能会影响许多算法的有效性。PowerTransformer有助于稳定方差,使其在特征值的不同范围内更加一致。
-
关系线性化:
一些算法,如线性回归,假设变量之间存在线性关系。PowerTransformer可以将非线性关系线性化,扩大这些模型在更复杂数据集上的适用性。
Box-Cox变换
Box-Cox变换是一系列幂变换,可以稳定方差并使数据更接近正态分布。它在数学上定义为:
其中:
-
x 是原始值,
-
y 是变换后的值,
-
λ 是变换参数
Box-Cox变换应用于正数据,并要求从数据中估计参数λ,以找到使数据正态化的最佳变换。
PowerTransformer的行为类似于Sklearn估计器,支持.fit()和.transform()方法。
Yeo-Johnson变换基于Box-Cox变换,但允许负值。本文不会详细介绍Yeo-Johnson变换。
如前所述,Yeo-Johnson变换基于Box-Cox变换,但lambda可以取的值可能会改变。这使得这些变换本质上不同,因为它们可能给出不同的结果。
在Python中,只需将其中一种变换方法作为字符串传递给PowerTransformer对象。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import PowerTransformer
np.random.seed(42)
data_positive = np.random.exponential(scale=2, size=1000)
data_negative = -np.random.exponential(scale=0.5, size=200)
data = np.concatenate([data_positive, data_negative])
pt_yj = PowerTransformer(method='yeo-johnson', standardize=False)
pt_bc = PowerTransformer(method='box-cox', standardize=False)
data_yj = pt_yj.fit_transform(data.reshape(-1, 1))
data_offset = data - np.min(data) + 1e-6
data_bc = pt_bc.fit_transform(data_offset.reshape(-1, 1))
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 5))
ax1.hist(data, bins=50, edgecolor='black')
ax1.set_title("Original Data")
ax1.set_xlabel("Value")
ax1.set_ylabel("Frequency")
ax2.histax2.hist(data_yj, bins=50, edgecolor='black')
ax2.set_title("Yeo-Johnson")
ax2.set_xlabel("Value")
ax3.hist(data_bc, bins=50, edgecolor='black')
ax3.set_title("Box-Cox")
ax3.set_xlabel("Value")
plt.tight_layout()
plt.show()