Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明
目录
Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明
一、简单介绍
二、单变量非线性变换
三、自动化特征选择
四、利用专家知识
附录
一、参考文献
一、简单介绍
Python是一种跨平台的计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越多被用于独立的、大型项目的开发。Python是一种解释型脚本语言,可以应用于以下领域: Web 和 Internet开发、科学计算和统计、人工智能、教育、桌面界面开发、软件开发、后端开发、网络爬虫。
Python 机器学习是利用 Python 编程语言中的各种工具和库来实现机器学习算法和技术的过程。Python 是一种功能强大且易于学习和使用的编程语言,因此成为了机器学习领域的首选语言之一。Python 提供了丰富的机器学习库,如Scikit-learn、TensorFlow、Keras、PyTorch等,这些库包含了许多常用的机器学习算法和深度学习框架,使得开发者能够快速实现、测试和部署各种机器学习模型。
Python 机器学习涵盖了许多任务和技术,包括但不限于:
- 监督学习:包括分类、回归等任务。
- 无监督学习:如聚类、降维等。
- 半监督学习:结合了有监督和无监督学习的技术。
- 强化学习:通过与环境的交互学习来优化决策策略。
- 深度学习:利用深度神经网络进行学习和预测。
通过 Python 进行机器学习,开发者可以利用其丰富的工具和库来处理数据、构建模型、评估模型性能,并将模型部署到实际应用中。Python 的易用性和庞大的社区支持使得机器学习在各个领域都得到了广泛的应用和发展。
二、单变量非线性变换
在机器学习中的特征工程中,单变量非线性变换是一种将单个特征应用非线性函数的技术,以便提高模型性能或满足模型假设。这些变换有助于处理特征与目标变量之间的非线性关系。常见的单变量非线性变换包括对数变换、平方根变换、平方变换、指数变换等。
下面是一些常用的单变量非线性变换的示例:
1)示例:对数变换
对数变换常用于将具有右偏分布的数据拉近正态分布。它在处理正数数据时特别有用。
import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split# 加载加州房价数据集 california = fetch_california_housing() X_train, X_test, y_train, y_test = train_test_split(california.data, california.target, random_state=0)# 选择一个特征进行对数变换 feature = X_train[:, 0] # 'MedInc' 特征 log_feature = np.log(feature + 1) # 加1以避免对数0的情况# 绘制原始特征和对数变换后的特征 plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.hist(feature, bins=30) plt.title("Original Feature") plt.subplot(1, 2, 2) plt.hist(log_feature, bins=30) plt.title("Log-transformed Feature") plt.show()
2)示例:平方根变换
平方根变换用于减弱特征中较大的数值的影响。它在处理计数数据时特别有用。
sqrt_feature = np.sqrt(feature)# 绘制原始特征和平方根变换后的特征 plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.hist(feature, bins=30) plt.title("Original Feature") plt.subplot(1, 2, 2) plt.hist(sqrt_feature, bins=30) plt.title("Square-root-transformed Feature") plt.show()
3)示例:平方变换
平方变换用于增加特征的非线性性。它在处理特征与目标之间具有二次关系的数据时有用。
square_feature = np.square(feature)# 绘制原始特征和平方变换后的特征 plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.hist(feature, bins=30) plt.title("Original Feature") plt.subplot(1, 2, 2) plt.hist(square_feature, bins=30) plt.title("Square-transformed Feature") plt.show()
4)示例:指数变换
指数变换用于增强特征中较小的数值的影响。
exp_feature = np.exp(feature)# 绘制原始特征和指数变换后的特征 plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.hist(feature, bins=30) plt.title("Original Feature") plt.subplot(1, 2, 2) plt.hist(exp_feature, bins=30) plt.title("Exponential-transformed Feature") plt.show()
5)将非线性变换应用到所有特征
如果你想对数据集中的所有特征进行非线性变换,可以使用
FunctionTransformer
。from sklearn.preprocessing import FunctionTransformer# 定义对数变换的函数 log_transformer = FunctionTransformer(np.log1p, validate=True)# 应用对数变换到所有特征 X_train_log = log_transformer.transform(X_train) X_test_log = log_transformer.transform(X_test)print("Original shape:", X_train.shape) print("Log-transformed shape:", X_train_log.shape)
这些非线性变换可以帮助捕捉特征和目标变量之间的复杂关系,从而提高模型的性能。选择适当的变换方法取决于数据的分布和模型的需求。
添加特征的平方或立方可以改进线性回归模型。其他变换通常也对变换某些特征有用,特别是应用数学函数,比如 log
、exp
或 sin
。虽然基于树的模型只关注特征的顺序,但线性模型和神经网络依赖于每个特征的尺度和分布。如果在特征和目标之间存在非线性关系,那么建模就变得非常困难,特别是对于回归问题。log
和 exp
函数可以帮助调节数据的相对比例,从而改进线性模型或神经网络的学习效果。我们之前对内存价格数据应用过这种函数。在处理具有周期性模式的数据时,sin
和 cos
函数非常有用。
大部分模型都在每个特征(在回归问题中还包括目标值)大致遵循高斯分布时表现最好,也就是说,每个特征的直方图应该具有类似于熟悉的“钟形曲线”的形状。使用诸如 log
和 exp
之类的变换并不稀奇,但却是实现这一点的简单又有效的方法。在一种特别常见的情况下,这样的变换非常有用,就是处理整数计数数据时。计数数据是指类似“用户 A 多长时间登录一次?”这样的特征。计数不可能取负值,并且通常遵循特定的统计模式。下面我们使用一个模拟的计数数据集,其性质与在自然状态下能找到的数据集类似。特征全都是整数值,而响应是连续的:
import numpy as nprnd = np.random.RandomState(0)
X_org = rnd.normal(size=(1000, 3))
w = rnd.normal(size=3)X = rnd.poisson(10 * np.exp(X_org))
y = np.dot(X_org, w)
我们来看一下第一个特征的前 10 个元素。它们都是正整数,但除此之外很难找出特定的模式。
如果我们计算每个值的出现次数,那么数值的分布将变得更清楚:
print("Number of feature appearances:\n{}".format(np.bincount(X[:, 0])))
Number of feature appearances: [28 38 68 48 61 59 45 56 37 40 35 34 36 26 23 26 27 21 23 23 18 21 10 917 9 7 14 12 7 3 8 4 5 5 3 4 2 4 1 1 3 2 5 3 8 2 52 1 2 3 3 2 2 3 3 0 1 2 1 0 0 3 1 0 0 0 1 3 0 10 2 0 1 1 0 0 0 0 1 0 0 2 2 0 1 1 0 0 0 0 1 1 00 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 01 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1]
数字 2 似乎是最常见的,共出现了 68 次(bincount
始终从 0 开始),更大数字的出现次数快速下降。但也有一些很大的数字,比如 134 出现了 2 次 (这里 134 实际的出现次数是 0,但 84 和 85 的出现次数是 2,作者想要表达的意思是没错的) 。我们在图 4-7 中将计数可视化。
bins = np.bincount(X[:, 0])
plt.bar(range(len(bins)), bins, color='b')
plt.ylabel("Number of appearances")
plt.xlabel("Value")plt.tight_layout()
plt.savefig('Images/04UnivariateNonlinearTransformation-01.png', bbox_inches='tight')
plt.show()
特征 X[:, 1]
和 X[:, 2]
具有类似的性质。这种类型的数值分布(许多较小的值和一些非常大的值)在实践中非常常见(这是泊松分布,对计数数据相当重要)。 但大多数线性模型无法很好地处理这种数据。我们尝试拟合一个岭回归模型:
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
score = Ridge().fit(X_train, y_train).score(X_test, y_test)
print("Test score: {:.3f}".format(score))
Test score: 0.622
你可以从相对较小的 分数中看出,Ridge
无法真正捕捉到 X
和 y
之间的关系。不过应用对数变换可能有用。由于数据取值中包括 0(对数在 0 处没有定义),所以我们不能直接应用 log
,而是要计算 log(X + 1)
:
X_train_log = np.log(X_train + 1)
X_test_log = np.log(X_test + 1)
变换之后,数据分布的不对称性变小,也不再有非常大的异常值(见图 4-8):
plt.hist(X_train_log[:, 0], bins=25, color='b')
plt.ylabel("Number of appearances")
plt.xlabel("Value")plt.tight_layout()
plt.savefig('Images/04UnivariateNonlinearTransformation-02.png', bbox_inches='tight')
plt.show()
在新数据上构建一个岭回归模型,可以得到更好的拟合:
score = Ridge().fit(X_train_log, y_train).score(X_test_log, y_test)
print("Test score: {:.3f}".format(score))
Test score: 0.875
为数据集和模型的所有组合寻找最佳变换,这在某种程度上是一门艺术。在这个例子中,所有特征都具有相同的性质,这在实践中是非常少见的情况。通常来说,只有一部分特征应该进行变换,有时每个特征的变换方式也各不相同。前面提到过,对基于树的模型而言,这种变换并不重要,但对线性模型来说可能至关重要。对回归的目标变量 y
进行变换有时也是一个好主意。尝试预测计数(比如订单数量)是一项相当常见的任务,而且使用 log(y + 1)
变换也往往有用。(这是对泊松分布非常粗略的近似,而从概率的角度来看,这是正确的解决方法)
从前面的例子中可以看出,分箱、多项式和交互项都对模型在给定数据集上的性能有很大影响,对于复杂度较低的模型更是这样,比如线性模型和朴素贝叶斯模型。与之相反,基于树的模型通常能够自己发现重要的交互项,大多数情况下不需要显式地变换数据。其他模型,比如 SVM、最近邻和神经网络,有时可能会从使用分箱、交互项或多项式中受益,但其效果通常不如线性模型那么明显。
三、自动化特征选择
自动化特征选择是机器学习中用于提高模型性能和减少模型复杂性的重要步骤。通过自动化特征选择,我们可以选择对模型预测有显著影响的特征,去除冗余或不相关的特征,从而提高模型的泛化能力和训练效率。
有了这么多种创建新特征的方法,你可能会想要增大数据的维度,使其远大于原始特征的数量。但是,添加更多特征会使所有模型变得更加复杂,从而增大过拟合的可能性。在添加新特征或处理一般的高维数据集时,最好将特征的数量减少到只包含最有用的那些特征,并删除其余特征。这样会得到泛化能力更好、更简单的模型。但你如何判断每个特征的作用有多大呢?有三种基本的策略:单变量统计 (univariate statistics)、基于模型的选择 (model-based selection)和迭代选择 (iterative selection)。我们将详细讨论这三种策略。所有这些方法都是监督方法,即它们需要目标值来拟合模型。这也就是说,我们需要将数据划分为训练集和测试集,并只在训练集上拟合特征选择。
1、单变量统计
在单变量统计中,我们计算每个特征和目标值之间的关系是否存在统计显著性,然后选择具有最高置信度的特征。对于分类问题,这也被称为方差分析 (analysis of variance,ANOVA)。这些测试的一个关键性质就是它们是单变量的 (univariate),即它们只单独考虑每个特征。因此,如果一个特征只有在与另一个特征合并时才具有信息量,那么这个特征将被舍弃。单变量测试的计算速度通常很快,并且不需要构建模型。另一方面,它们完全独立于你可能想要在特征选择之后应用的模型。
想要在 scikit-learn
中使用单变量特征选择,你需要选择一项测试——对分类问题通常是 f_classif
(默认值),对回归问题通常是 f_regression
——然后基于测试中确定的 p 值来选择一种舍弃特征的方法。所有舍弃参数的方法都使用阈值来舍弃所有 p 值过大的特征(意味着它们不可能与目标值相关)。计算阈值的方法各有不同,最简单的是 SelectKBest
和 SelectPercentile
,前者选择固定数量的 k 个特征,后者选择固定百分比的特征。我们将分类的特征选择应用于 cancer
数据集。为了使任务更难一点,我们将向数据中添加一些没有信息量的噪声特征。我们期望特征选择能能够识别没有信息量的特征并删除它们:
from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectPercentile
from sklearn.model_selection import train_test_split
import numpy as npcancer = load_breast_cancer()# 获得确定性的随机数
rng = np.random.RandomState(42)
noise = rng.normal(size=(len(cancer.data), 50))
# 向数据中添加噪声特征
# 前30个特征来自数据集,后50个是噪声
X_w_noise = np.hstack([cancer.data, noise])X_train, X_test, y_train, y_test = train_test_split(X_w_noise, cancer.target, random_state=0, test_size=.5)
# 使用f_classif(默认值)和SelectPercentile来选择50%的特征
select = SelectPercentile(percentile=50)
select.fit(X_train, y_train)
# 对训练集进行变换
X_train_selected = select.transform(X_train)print("X_train.shape: {}".format(X_train.shape))
print("X_train_selected.shape: {}".format(X_train_selected.shape))
X_train.shape: (284, 80) X_train_selected.shape: (284, 40)
如你所见,特征的数量从 80 减少到 40(原始特征数量的 50%)。我们可以用 get_support
方法来查看哪些特征被选中,它会返回所选特征的布尔遮罩(mask)(其可视化见图 4-9):
import matplotlib.pyplot as pltmask = select.get_support()
print(mask)
# 将遮罩可视化——黑色为True,白色为False
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("Sample index")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-01.png', bbox_inches='tight')
plt.show()
[ True True True True True True True True True False True FalseTrue True True True True True False False True True True TrueTrue True True True True True False False False True False TrueFalse False True False False False False True False False True FalseFalse True False True False False False False False False True FalseTrue False False False False True False True False False False FalseTrue True False True False False False False]
你可以从遮罩的可视化中看出,大多数所选择的特征都是原始特征,并且大多数噪声特征都已被删除。但原始特征的还原并不完美。我们来比较 Logistic 回归在所有特征上的性能与仅使用所选特征的性能:
from sklearn.linear_model import LogisticRegression# 对测试数据进行变换
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)))
在这个例子中,删除噪声特征可以提高性能,即使丢失了某些原始特征。这是一个非常简单的假想示例,在真实数据上的结果要更加复杂。不过,如果特征量太大以至于无法构建模型,或者你怀疑许多特征完全没有信息量,那么单变量特征选择还是非常有用的。
Score with all features: 0.919 Score with only selected features: 0.919
2、基于模型的特征选择
基于模型的特征选择使用一个监督机器学习模型来判断每个特征的重要性,并且仅保留最重要的特征。用于特征选择的监督模型不需要与用于最终监督建模的模型相同。特征选择模型需要为每个特征提供某种重要性度量,以便用这个度量对特征进行排序。决策树和基于决策树的模型提供了 feature_importances_
属性,可以直接编码每个特征的重要性。线性模型系数的绝对值也可以用于表示特征重要性。正如我们在之前所见,L1 惩罚的线性模型学到的是稀疏系数,它只用到了特征的一个很小的子集。这可以被视为模型本身的一种特征选择形式,但也可以用作另一个模型选择特征的预处理步骤。与单变量选择不同,基于模型的选择同时考虑所有特征,因此可以获取交互项(如果模型能够获取它们的话)。要想使用基于模型的特征选择,我们需要使用 SelectFromModel
变换器:
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier
select = SelectFromModel(RandomForestClassifier(n_estimators=100, random_state=42),threshold="median")
SelectFromModel
类选出重要性度量(由监督模型提供)大于给定阈值的所有特征。为了得到可以与单变量特征选择进行对比的结果,我们使用中位数作为阈值,这样就可以选择一半特征。我们用包含 100 棵树的随机森林分类器来计算特征重要性。这是一个相当复杂的模型,也比单变量测试要强大得多。下面我们来实际拟合模型:
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))
X_train.shape: (284, 80) X_train_l1.shape: (284, 40)
我们可以再次查看选中的特征(见图 4-10):
mask = select.get_support()
# 将遮罩可视化——黑色为True,白色为False
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("Sample index")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-02.png', bbox_inches='tight')
plt.show()
这次,除了两个原始特征,其他原始特征都被选中。由于我们指定选择 40 个特征,所以也选择了一些噪声特征。我们来看一下其性能:
X_test_l1 = select.transform(X_test)
score = LogisticRegression().fit(X_train_l1, y_train).score(X_test_l1, y_test)
print("Test score: {:.3f}".format(score))
Test score: 0.930
利用更好的特征选择,性能也得到了提高。
3、迭代特征选择
在单变量测试中,我们没有使用模型,而在基于模型的选择中,我们使用了单个模型来选择特征。在迭代特征选择中,将会构建一系列模型,每个模型都使用不同数量的特征。有两种基本方法:开始时没有特征,然后逐个添加特征,直到满足某个终止条件;或者从所有特征开始,然后逐个删除特征,直到满足某个终止条件。由于构建了一系列模型,所以这些方法的计算成本要比前面讨论过的方法更高。其中一种特殊方法是递归特征消除 (recursive feature elimination,RFE),它从所有特征开始构建模型,并根据模型舍弃最不重要的特征,然后使用除被舍弃特征之外的所有特征来构建一个新模型,如此继续,直到仅剩下预设数量的特征。为了让这种方法能够运行,用于选择的模型需要提供某种确定特征重要性的方法,正如基于模型的选择所做的那样。下面我们使用之前用过的同一个随机森林模型,得到的结果如图 4-11 所示:
from sklearn.feature_selection import RFE
select = RFE(RandomForestClassifier(n_estimators=100, random_state=42),n_features_to_select=40)
select.fit(X_train, y_train)
# 将选中的特征可视化:
mask = select.get_support()
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("Sample index")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-03.png', bbox_inches='tight')
plt.show()
与单变量选择和基于模型的选择相比,迭代特征选择的结果更好,但仍然漏掉了一个特征。运行上述代码需要的时间也比基于模型的选择长得多,因为对一个随机森林模型训练了 40 次,每运行一次删除一个特征。我们来测试一下使用 RFE 做特征选择时 Logistic 回归模型的精度:
X_train_rfe= select.transform(X_train)
X_test_rfe= select.transform(X_test)score = LogisticRegression().fit(X_train_rfe, y_train).score(X_test_rfe, y_test)
print("Test score: {:.3f}".format(score))
Test score: 0.930
我们还可以利用在 RFE 内使用的模型来进行预测。这仅使用被选中的特征集:
print("Test score: {:.3f}".format(select.score(X_test, y_test)))
Test score: 0.951
这里,在 RFE 内部使用的随机森林的性能,与在所选特征上训练一个 Logistic 回归模型得到的性能相同。换句话说,只要我们选择了正确的特征,线性模型的表现就与随机森林一样好。
如果你不确定何时选择使用哪些特征作为机器学习算法的输入,那么自动化特征选择可能特别有用。它还有助于减少所需要的特征数量,加快预测速度,或允许可解释性更强的模型。在大多数现实情况下,使用特征选择不太可能大幅提升性能,但它仍是特征工程工具箱中一个非常有价值的工具。
四、利用专家知识
对于特定应用来说,在特征工程中通常可以利用专家知识 (expert knowledge)。虽然在许多情况下,机器学习的目的是避免创建一组专家设计的规则,但这并不意味着应该舍弃该应用或该领域的先验知识。通常来说,领域专家可以帮助找出有用的特征,其信息量比数据原始表示要大得多。想象一下,你在一家旅行社工作,想要预测机票价格。假设你有价格以及日期、航空公司、出发地和目的地的记录。机器学习模型可能从这些记录中构建一个相当不错的模型,但可能无法学到机票价格中的某些重要因素。例如,在度假高峰月份和假日期间,机票价格通常更高。虽然某些假日的日期是固定的(比如圣诞节),其影响可以从日期中学到,但其他假日的日期可能取决于月相(比如光明节和复活节),或者由官方规定(比如学校放假)。如果每个航班都只使用公历记录日期,则无法从数据中学到这些事件。但添加一个特征是很简单的,其中编码了一个航班在公休假日或学校假期的之前、之中还是之后。利用这种方法可以将关于任务属性的先验知识编码到特征中,以辅助机器学习算法。添加一个特征并不会强制机器学习算法使用它,即使最终发现假日信息不包含关于机票价格的信息,用这一信息来扩充数据也不会有什么害处。
下面我们来看一个利用专家知识的特例——虽然在这个例子中,对这些专家知识更正确的叫法应该是“常识”。任务是预测在 Andreas 家门口的自行车出租。
在纽约,Citi Bike 运营着一个带有付费系统的自行车租赁站网络。这些站点遍布整个城市,提供了一种方便的交通方式。自行车出租数据以匿名形式公开(Citi Bike System Data | Citi Bike NYC ),并用各种方法进行了分析。我们想要解决的任务是,对于给定的日期和时间,预测有多少人将会在 Andreas 的家门口租一辆自行车——这样他就知道是否还有自行车留给他。
我们首先将这个站点 2015 年 8 月的数据加载为一个 pandas
数据框。我们将数据重新采样为每 3 小时一个数据,以得到每一天的主要趋势:
import mglearncitibike = mglearn.datasets.load_citibike()
print("Citi Bike data:\n{}".format(citibike.head()))
Citi Bike data: starttime 2015-08-01 00:00:00 3 2015-08-01 03:00:00 0 2015-08-01 06:00:00 9 2015-08-01 09:00:00 41 2015-08-01 12:00:00 39 Freq: 3h, Name: one, dtype: int64
下面这个示例给出了整个月租车数量的可视化(图 4-12):
import pandas as pdplt.figure(figsize=(10, 3))
xticks = pd.date_range(start=citibike.index.min(), end=citibike.index.max(),freq='D')
plt.xticks(xticks, xticks.strftime("%a %m-%d"), rotation=90, ha="left")
plt.plot(citibike, linewidth=1)
plt.xlabel("Date")
plt.ylabel("Rentals")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-04.png', bbox_inches='tight')
plt.show()
观察此数据,我们可以清楚地区分每 24 小时中的白天和夜间。工作日和周末的模式似乎也有很大不同。在对这种时间序列上的预测任务进行评估时,我们通常希望从过去学习 并预测未来 。也就是说,在划分训练集和测试集的时候,我们希望使用某个特定日期之前的所有数据作为训练集,该日期之后的所有数据作为测试集。这是我们通常使用时间序列预测的方式:已知过去所有的出租数据,我们认为明天会发生什么?我们将使用前 184 个数据点(对应前 23 天)作为训练集,剩余的 64 个数据点(对应剩余的 8 天)作为测试集。
在我们的预测任务中,我们使用的唯一特征就是某一租车数量对应的日期和时间。因此输入特征是日期和时间,比如 2015-08-01 00:00:00
,而输出是在接下来 3 小时内的租车数量(根据我们的 DataFrame
,在这个例子中是 3)。
在计算机上存储日期的常用方式是使用 POSIX 时间(这有些令人意外),它是从 1970 年 1 月 1 日 00:00:00(也就是 Unix 时间的起点)起至现在的总秒数。首先,我们可以尝试使用这个单一整数特征作为数据表示:
# 提取目标值(租车数量)
y = citibike.values# 将时间转换为 POSIX 时间(即时间戳)
X = citibike.index
X = pd.to_datetime(X) # 确保索引是日期时间格式
X = X.view('int64') // 10**9 # 转换为秒时间戳并转换为整数# 转换为二维数组
X = X.reshape(-1, 1)print(X[:5])
print(y[:5])
我们首先定义一个函数,它可以将数据划分为训练集和测试集,构建模型并将结果可视化:
# 使用前184个数据点用于训练,剩余的数据点用于测试
n_train = 184# 对给定特征集上的回归进行评估和作图的函数
def eval_on_features(features, target, regressor):# 将给定特征划分为训练集和测试集X_train, X_test = features[:n_train], features[n_train:]# 同样划分目标数组y_train, y_test = target[:n_train], target[n_train:]regressor.fit(X_train, y_train)print("Test-set R^2: {:.2f}".format(regressor.score(X_test, y_test)))y_pred = regressor.predict(X_test)y_pred_train = regressor.predict(X_train)plt.figure(figsize=(10, 3))plt.xticks(range(0, len(X), 8), xticks.strftime("%a %m-%d"), rotation=90,ha="left")plt.plot(range(n_train), y_train, label="train")plt.plot(range(n_train, len(y_test) + n_train), y_test, '-', label="test")plt.plot(range(n_train), y_pred_train, '--', label="prediction train")plt.plot(range(n_train, len(y_test) + n_train), y_pred, '--',label="prediction test")plt.legend(loc=(1.01, 0))plt.xlabel("Date")plt.ylabel("Rentals")
我们之前看到,随机森林需要很少的数据预处理,因此它似乎很适合作为第一个模型。我们使用 POSIX 时间特征 X
,并将随机森林回归传入我们的 eval_on_features
函数。结果如图 4-13 所示。
from sklearn.ensemble import RandomForestRegressor
regressor = RandomForestRegressor(n_estimators=100, random_state=0)
plt.figure()
eval_on_features(X, y, regressor)plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-05.png', bbox_inches='tight')
plt.show()
Test-set R^2: -0.04
在训练集上的预测结果相当好,这符合随机森林通常的表现。但对于测试集来说,预测结果是一条常数直线。R2 为 -0.04,说明我们什么都没有学到。发生了什么?
问题在于特征和随机森林的组合。测试集中 POSIX 时间特征的值超出了训练集中特征取值的范围:测试集中数据点的时间戳要晚于训练集中的所有数据点。树以及随机森林无法外推 (extrapolate)到训练集之外的特征范围。结果就是模型只能预测训练集中最近数据点的目标值,即最后一次观测到数据的时间。
显然,我们可以做得更好。这就是我们的“专家知识”的用武之地。通过观察训练数据中的租车数量图像,我们发现两个因素似乎非常重要:一天内的时间与一周的星期几。因此我们来添加这两个特征。我们从 POSIX 时间中学不到任何东西,所以删掉这个特征。首先,我们仅使用每天的时刻。如图 4-14 所示,现在的预测结果对一周内的每天都具有相同的模式:
# 获取小时信息并转换为二维数组
X_hour = citibike.index.hour.to_numpy().reshape(-1, 1)
eval_on_features(X_hour, y, regressor)plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-06.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.60
已经好多了,但预测结果显然没有抓住每周的模式。下面我们还添加一周的星期几作为特征(见图 4-15):
X_hour_week = np.hstack([citibike.index.dayofweek.to_numpy().reshape(-1, 1),citibike.index.hour.to_numpy().reshape(-1, 1)])
eval_on_features(X_hour_week, y, regressor)plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-07.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.84
现在我们的模型通过考虑一周的星期几和一天内的时间捕捉到了周期性的行为。它的 R2 为 0.84,预测性能相当好。模型学到的内容可能是 8 月前 23 天中星期几与时刻每种组合的平均租车数量。这实际上不需要像随机森林这样复杂的模型,所以我们尝试一个更简单的模型——LinearRegression
(见图 4-16):
from sklearn.linear_model import LinearRegression
eval_on_features(X_hour_week, y, LinearRegression())plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-08.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.13
LinearRegression
的效果差得多,而且周期性模式看起来很奇怪。其原因在于我们用整数编码一周的星期几和一天内的时间,它们被解释为连续变量。因此,线性模型只能学到关于每天时间的线性函数——它学到的是,时间越晚,租车数量越多。但实际模式比这要复杂得多。我们可以通过将整数解释为分类变量(用 OneHotEncoder
进行变换)来获取这种模式(见图 4-17):
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import Ridgeenc = OneHotEncoder()
X_hour_week_onehot = enc.fit_transform(X_hour_week).toarray()eval_on_features(X_hour_week_onehot, y, Ridge())plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-09.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.62
它给出了比连续特征编码好得多的匹配。现在线性模型为一周内的每天都学到了一个系数,为一天内的每个时刻都学到了一个系数。也就是说,一周七天共享“一天内每个时刻”的模式。
利用交互特征,我们可以让模型为星期几和时刻的每一种组合学到一个系数(见图 4-18):
from sklearn.preprocessing import PolynomialFeaturespoly_transformer = PolynomialFeatures(degree=2, interaction_only=True,include_bias=False)
X_hour_week_onehot_poly = poly_transformer.fit_transform(X_hour_week_onehot)
lr = Ridge()
eval_on_features(X_hour_week_onehot_poly, y, lr)plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-10.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.85
这一变换最终得到一个性能与随机森林类似的模型。这个模型的一大优点是,可以很清楚地看到学到的内容:对每个星期几和时刻的交互项学到了一个系数。我们可以将模型学到的系数作图,而这对于随机森林来说是不可能的。
首先,为时刻和星期几特征创建特征名称:
hour = ["%02d:00" % i for i in range(0, 24, 3)]
day = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
features = day + hour
然后,利用 get_feature_names
方法对 PolynomialFeatures
提取的所有交互特征进行命名,并仅保留系数不为零的那些特征:
features_poly = poly_transformer.get_feature_names_out(features)
features_nonzero = np.array(features_poly)[lr.coef_ != 0]
coef_nonzero = lr.coef_[lr.coef_ != 0]
下面将线性模型学到的系数可视化,如图 4-19 所示:
plt.figure(figsize=(15, 2))
plt.plot(coef_nonzero, 'o')
plt.xticks(np.arange(len(coef_nonzero)), features_nonzero, rotation=90)
plt.xlabel("Feature name")
plt.ylabel("Feature magnitude")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-11.png', bbox_inches='tight')
plt.show()
附录
一、参考文献
参考文献:[德] Andreas C. Müller [美] Sarah Guido 《Python Machine Learning Basics Tutorial》