深入研究 Softmax 回归
原文:
towardsdatascience.com/deep-dive-into-softmax-regression-62deea103cb8
理解 softmax 回归背后的数学原理以及如何用它解决图像分类任务
·发表于 Towards Data Science ·阅读时间 13 分钟·2023 年 5 月 25 日
–
Softmax 回归(或多项式逻辑回归)是逻辑回归在多类问题上的一种推广。
它可以用来预测某个事件不同可能结果的概率,例如,根据患者的特征(性别、年龄、血压、各种测试结果等),预测患者是否会患上某种特定疾病。
在这篇文章中,我们将从基本原理推导 softmax 回归模型,并展示如何用它解决图像分类任务。
在阅读这篇文章之前,我强烈推荐你阅读我之前关于逻辑回归的文章:
从理论到 Python 实现
towardsdatascience.com
背景:多类分类问题
回顾一下,在监督式机器学习问题中,我们给定一个包含n个标记样本的训练集:D = {(x₁, y₁), (x₂, y₂), … , (xₙ, yₙ)},其中xᵢ 是一个m维向量,包含了样本i 的特征,而 yᵢ 表示该样本的标签。我们的目标是构建一个模型,使其预测尽可能接近真实标签。
在多类分类问题中,目标标签可以是 k 个类别中的任何一个,即 y ∈ {1, …, k}。例如,在手写数字识别任务中,k = 10,因为有 10 个可能的数字(0–9)。
如同在二分类问题中,我们区分两种分类器:
-
概率分类器输出 k 个类别的概率估计,然后基于这些概率分配标签(通常是概率最高的类别的标签)。此类分类器的示例包括 softmax 回归,朴素贝叶斯分类器 和在输出层使用 softmax 的 神经网络。
Softmax 回归模型
给定一个样本(x,y),softmax 回归模型输出一个概率向量 p = (p₁, …, pₖ)ᵗ,其中 pᵢ 表示样本属于类别 i: 的概率。
向量中的概率之和必须为 1:
在(二分类)逻辑回归中,我们假设 对数几率(p 和 1 − p 之间的比率的对数)是输入特征(向量 x)的线性组合。
在 softmax 回归中,我们选择一个概率作为参考(假设为 pₖ),并假设每个概率 pᵢ 和 pₖ 之间的对数几率比是输入特征的线性组合。
换句话说,我们可以将 pᵢ 和 pₖ 之间的对数几率表示为某个权重向量wᵢ 和特征向量x的点积:
对数几率作为特征的线性组合
请注意,在 softmax 回归中,我们为每个类别 i 拥有一个单独的参数向量 wᵢ。模型的所有参数集合通常存储在一个大小为 (m + 1) × k 的矩阵 W 中,通过将 w₁, …, wₖ 连接成列获得:
参数矩阵
通过对对数几率方程的两边取指数,我们得到:
由于所有 k 个概率之和为 1,我们可以写成:
我们现在可以使用 pₖ 的表达式来找到所有其他概率:
由于所有 k 个概率之和必须为 1,一旦知道了其他所有概率,概率 pₖ 就完全确定。因此,我们只有 k − 1 个可以单独识别的系数向量。这意味着我们可以任意选择 wₖ = 0 来确保 exp(wₖᵗ) = 1。这样,我们可以更紧凑地写出上述方程:
将线性函数wᵗx 转换为概率的函数称为softmax,接下来将对此进行描述。
软最大函数
软最大函数,σ(z):ℝᵏ → ℝᵏ, 将一个包含k个实数的向量z = (z₁, …, zₖ)ᵗ 转换为一个概率向量 (σ(z₁), …, σ(zₖ))ᵗ:
软最大函数
可以很容易地验证所有σ(z)的分量都在(0,1)范围内,并且它们的和为 1。
“softmax”这个名字源于该函数是 argmax 函数的平滑近似。例如,向量(1, 2, 6)的 softmax 大约为(0.007, 0.018, 0.976),这几乎将所有的权重集中在向量的最大元素上。
软最大函数是sigmoid(逻辑)函数在多类情况中的扩展。换句话说,可以证明当只有两个类别时,softmax 变成 sigmoid 函数(留给读者作为练习)。
软最大函数也常用于神经网络中,将网络最后一层的输出转换为概率。
以下图表总结了软最大回归模型的计算过程:
软最大回归模型
从数学角度来看,这个过程可以写作如下:
交叉熵损失
我们的目标是找到一组参数W,使得模型的预测p = σ(w₁ᵗx,…, wₖᵗx) 尽可能接近真实标签 y。
请注意,我们模型的输出是一个概率向量,而真实标签是一个标量。为了使它们可比,我们使用独热编码对标签进行编码,即,我们将每个标签y 转换为一个二进制向量y = (y₁, …, yₖ)ᵗ,其中 yᵢ = 1 表示真实类别 i,其他位置为 0。
损失函数用于衡量我们模型的预测与真实标签之间的差距。软最大回归中使用的损失函数称为交叉熵损失,这是对多类情况的对数损失的扩展。它的定义如下:
交叉熵损失
例如,假设在一个三类问题中(k = 3),我们有一个真实类别为第 2 类的样本(即y = (0, 1, 0)ᵗ),并且我们模型对该样本的预测是p = (0.3, 0.6, 0.1)ᵗ。那么由该样本引起的交叉熵损失是:
类似于对数损失,我们可以证明交叉熵损失是模型对数似然的负值,前提是标签是从一个分类分布中抽取的(这是伯努利分布对k种可能结果的一种推广)。
证明:
给定一个数据(标签)的模型作为具有概率p = (p₁, …, pₖ)ᵗ的分类分布,则一个给定样本属于类别i的概率是pᵢ:
因此,样本的真实标签为y的概率是:
解释:如果给定样本的正确类别是 i,则 yᵢ = 1,对于所有 j ≠ i,yⱼ = 0。因此,P*(y|p)* = pᵢ,这就是样本属于类别 i 的概率。
因此,我们模型的对数似然是:
交叉熵损失正好是这个函数的负值。因此,最小化交叉熵损失等价于最大化模型的对数似然。
梯度下降
与逻辑回归一样,没有解析解可以找到最小化交叉熵损失的最佳W。因此,我们需要使用迭代优化方法,如梯度下降,来寻找最小损失。
幸运的是,交叉熵损失的梯度非常简单(尽管其推导并不简单……)。交叉熵损失对每个参数向量wⱼ的梯度是:
证明:
我们首先将交叉熵损失写成权重的显式函数:
使用链式法则,我们有:
我们从计算第一个偏导数开始。利用导数的加法规则、对数的导数规则和链式法则,我们得到:
接下来,我们使用商法则计算偏导数 ∂pᵢ/∂zⱼ(即 softmax 函数的导数)。我们区分两种情况:
- 如果i = j,则:
- 如果i ≠ j,则:
结合这两个方程,我们得到 softmax 函数的导数:
利用这一结果,我们可以完成对L的导数计算:
由于恰好一个yᵢ是 1,其他都是 0,我们可以进一步简化这个导数为:
zⱼ关于wⱼ的偏导数就是输入向量x:
(参见 这篇文章 了解矩阵微积分的基本规则)。
因此,交叉熵损失相对于每个权重向量的导数是:
利用这些梯度,我们可以使用(随机)梯度下降来最小化给定训练集上的损失函数。
练习题
给定一组图像,你需要将它们分类为狗/猫和户外/室内。你应该实现两个逻辑回归分类器还是一个 softmax 回归分类器?
解决方案可以在文章末尾找到。
Scikit-Learn 中的 softmax 回归
类别 LogisticRegression 可以处理二分类和多分类问题。它有一个名为 multi_class 的参数,默认设置为‘auto’。这个选项的意思是,当 Scikit-Learn 检测到问题是多分类且选择的求解器支持多项式损失的优化时(所有求解器都支持,除了‘liblinear’),它将自动应用 softmax 回归。
示例:分类手写数字
例如,我们在 MNIST 数据集 上训练一个 softmax 回归模型,这是一个广泛使用的图像分类任务数据集。
数据集包含 60,000 张训练图像和 10,000 张手写数字测试图像。每张图像的大小为 28 × 28 像素,通常由一个范围为 [0, 255] 的 784 个数字组成。任务是将这些图像分类为十个数字(0–9)之一。
加载数据集
我们首先使用 fetch_openml() 函数获取 MNIST 数据集:
from sklearn.datasets import fetch_openml
X, y = fetch_openml('mnist_784', return_X_y=True, as_frame=False)
让我们检查一下 X 的形状:
print(X.shape)
(70000, 784)
X 由 70,000 个向量组成,每个向量有 784 个像素。
让我们显示数据集中前 50 个数字:
fig, axes = plt.subplots(5, 10, figsize=(10, 5))
i = 0
for ax in axes.flat:
ax.imshow(X[i].reshape(28, 28), cmap='binary')
ax.axis('off')
i += 1
MNIST 数据集中的前 50 个数字
接下来,我们将输入数据缩放到 [0, 1] 的范围,而不是 [0, 255]:
X = X / 255
特征缩放在你使用迭代优化方法(如梯度下降)来训练模型时非常重要。
现在我们将数据分为训练集和测试集。请注意,MNIST 中的前 60,000 张图像已被指定用于训练,因此我们可以仅使用简单的切片来进行分割:
train_size = 60000
X_train, y_train = X[:train_size], y[:train_size]
X_test, y_test = X[train_size:], y[train_size:]
构建模型
现在我们创建一个具有默认设置的 LogisticRegression 分类器,并将其拟合到训练集上:
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression()
clf.fit(X_train, y_train)
我们收到一条警告信息,表明已经达到最大迭代次数:
ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.
Increase the number of iterations (max_iter) or scale the data as shown in:
https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
n_iter_i = _check_optimize_result(
让我们将 max_iter 增加到 1000(而不是默认的 100):
clf = LogisticRegression(max_iter=1000)
clf.fit(X_train, y_train)
这次学习在达到最大迭代次数之前已经收敛。我们实际上可以通过检查 n_iter_ 属性来查看收敛所需的迭代次数:
print(clf.n_iter_)
[795]
学习收敛花费了 795 次迭代。
模型评估
模型在训练集和测试集上的准确率为:
print('Training set accuracy: ', np.round(clf.score(X_train, y_train), 4))
print('Test set accuracy:' , np.round(clf.score(X_test, y_test), 4))
Training set accuracy: 0.9393
Test set accuracy: 0.9256
这些结果很好,但最近的深度神经网络在这个数据集上可以取得更好的结果(测试准确率高达 99.91%)。Softmax 回归模型大致相当于一个使用 softmax 激活函数的单层感知机神经网络。因此,深度网络能够取得比我们的模型更好的结果并不令人惊讶。
为了更好地理解我们模型的错误,让我们显示其在测试集上的混淆矩阵:
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
y_test_pred = clf.predict(X_test)
cm = confusion_matrix(y_test, y_test_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=clf.classes_)
disp.plot(cmap='Blues')
测试集上的混淆矩阵
我们可以看到模型的主要混淆是在数字 5⇔8 和 4⇔9 之间。这是合理的,因为这些数字在手写时常常互相类似。为了帮助我们的模型区分这些数字,我们可以增加这些数字的更多示例(例如,通过数据增强)或从图像中提取额外特征(例如,数字中的闭环数量)。
我们还可以打印分类报告,以获取每个类别的精确度、召回率和 F1 分数:
from sklearn.metrics import classification_report
print(classification_report(y_test, y_test_pred))
precision recall f1-score support
0 0.95 0.97 0.96 980
1 0.96 0.98 0.97 1135
2 0.93 0.90 0.91 1032
3 0.90 0.92 0.91 1010
4 0.94 0.94 0.94 982
5 0.90 0.87 0.88 892
6 0.94 0.95 0.95 958
7 0.93 0.92 0.93 1028
8 0.88 0.88 0.88 974
9 0.91 0.92 0.91 1009
accuracy 0.93 10000
macro avg 0.92 0.92 0.92 10000
weighted avg 0.93 0.93 0.93 10000
如预期,模型得分最低的数字是 5 和 8。
权重可视化
Softmax 回归的一个优势是高度可解释(与神经网络等“黑箱”模型不同)。每个特征相关的权重表示该特征的重要性。
例如,我们可以绘制每个数字类别中每个像素相关的权重(wⱼ 对于每个 j ∈ {1, …, 10})。这将展示用于检测每个数字的图像中的重要区域。
模型的权重矩阵存储在名为 coef_ 的属性中:
print(clf.coef_.shape)
(10, 784)
该矩阵的 i 行包含了模型对类别 i 的学习权重。我们可以将每一行显示为 28 × 28 像素的图像,以检查每个类别中与每个像素相关的权重:
fig, axes = plt.subplots(2, 5, figsize=(15, 5))
digit = 0
for coef, ax in zip(clf.coef_, axes.flat):
im = ax.imshow(coef.reshape(28, 28), cmap='gray')
ax.axis('off')
ax.set_title(str(digit))
digit += 1
fig.colorbar(im, ax=axes.flat)
亮色像素对预测有正面影响,而暗色像素有负面影响。灰色级别接近 0 的像素对预测没有影响(如图像边缘附近的像素)。
总结
Softmax 回归与其他多类分类模型相比的优缺点是:
优点:
-
提供类别概率估计
-
高度可扩展,需要的参数数量与特征数量线性相关
-
高度可解释(每个特征相关的权重表示其重要性)
-
能处理冗余特征(通过将其权重分配接近 0)
缺点:
-
只能找到类别之间的线性决策边界
-
通常被更复杂的模型超越
-
无法处理缺失值
练习题的解答
由于类别之间并非完全互斥(所有四种狗/猫与户外/室内的组合都是可能的),你应该训练两个逻辑回归模型。这是一个多标签问题,一个 softmax 分类器无法处理。
最终说明
你可以在我的 GitHub 上找到本文的代码示例:github.com/roiyeho/medium/tree/main/softmax_regression
除非另有说明,所有图像均为作者提供。
MNIST 数据集信息:
-
引用: Deng, L., 2012. 用于机器学习研究的手写数字图像的 mnist 数据库。IEEE 信号处理杂志, 29(6), 第 141–142 页。
-
许可: Yann LeCun 和 Corinna Cortes 拥有 MNIST 数据集的版权,该数据集在知识共享署名-相同方式共享 4.0 国际许可协议下提供 (CC BY-SA)。
感谢阅读!
Deep GPVAR:升级 DeepAR 实现多维度预测
原文:
towardsdatascience.com/deep-gpvar-upgrading-deepar-for-multi-dimensional-forecasting-e39204d90af3
Amazon 的新时间序列预测模型
·发表于 Towards Data Science ·19 分钟阅读·2023 年 3 月 24 日
–
使用 DALLE 创建 [1]
警告: 关于该模型的信息,包括下面的教程,可能不是最新的。阅读更新版本 这里
阅读新论文时最令人愉快的事情是什么?对我而言,就是以下内容:
想象一下,一个流行的模型突然升级——只需几个优雅的调整。
三年后,Amazon 工程师发布了其改版版本,称为 Deep GPVAR [2] (Deep Gaussian-Process Vector Auto-regressive)。
这是对原始版本的显著改进版。此外,它是开源的。本文中,我们讨论:
-
深入了解 Deep GPVAR 的工作原理。
-
DeepAR 和 Deep GPVAR 的不同之处。
-
** Deep GPVAR 解决了哪些问题,以及为何它比 DeepAR 更好?**
-
有关能源需求预测的实践教程。
我推出了 AI Horizon Forecast, 这是一个专注于时间序列和创新 AI 研究的新闻通讯。订阅 这里 扩展你的视野!
什么是 Deep GPVAR?
Deep GPVAR 是一种自回归深度学习模型,利用低秩高斯过程共同建模数千个时间序列,考虑它们之间的相互依赖性。
简要回顾 Deep GPVAR 的优点:
-
多时间序列支持: 该模型使用多个时间序列数据来学习全局特征,从而提高其准确预测能力。
-
额外协变量: Deep GPVAR 允许额外特征(协变量)。
-
可扩展性: 该模型利用低秩高斯分布将训练扩展到多个时间序列同时进行。
-
多维建模: 与其他全球预测模型相比,Deep GPVAR模型将时间序列一起建模,而不是单独建模。这通过考虑它们的相互依赖性来提高预测准确性。
最后一部分是Deep GPVAR与 DeepAR 的区别所在。我们将在下一节中深入探讨这一点。
全球预测机制
在多个时间序列上训练的全球模型并不是一个新概念。但是为什么需要全球模型?
在我之前的公司,客户对时间序列预测项目感兴趣,主要需求如下:
“我们有 10,000 个时间序列,我们希望创建一个单一模型,而不是 10,000 个单独的模型。”
时间序列可以代表产品销售、期权价格、气候污染等——这并不重要。重要的是,公司需要大量资源来训练、评估、部署和监控(概念漂移)10,000个生产中的时间序列。
所以,这是一个很好的理由。此外,那时没有N-BEATS或Temporal Fusion Transformer。
然而,如果我们要创建一个全球模型,它应该学习什么?模型是否仅仅学习一个聪明的映射,根据输入条件处理每个时间序列?但这假设时间序列是独立的。
模型是否应该学习适用于数据集中所有时间序列的全球时间模式?
时间序列的相互依赖性
Deep GPVAR在 DeepAR 的基础上,寻求一种更先进的方法来利用多个时间序列之间的依赖关系,从而提高预测效果。
对于许多任务,这种做法是有意义的。
一个将全球数据集的时间序列视为独立的模型会失去在金融和零售等应用中有效利用它们关系的能力。例如,风险最小化的投资组合需要预测资产的协方差,而不同卖家的概率预测必须考虑竞争和侵蚀效应。
因此,一个强大的全球预测模型不能假设潜在的时间序列是独立的。
Deep GPVAR 与 DeepAR 的区别在于两个方面:
-
高维估计: Deep GPVAR将时间序列一起建模,考虑它们的关系。为此,模型使用低秩高斯近似来估计它们的协方差矩阵。
-
扩展性: Deep GPVAR并不像其前身那样仅仅对每个时间序列进行标准化。相反,模型学习如何通过使用高斯 Copulas首先转换每个时间序列来缩放它们。
以下部分详细描述了这两个概念的工作原理。
低秩高斯近似——简介
正如我们之前所说,研究多个时间序列之间关系的最佳方法之一是估计协方差矩阵。
然而,由于内存和数值稳定性限制,将此任务扩展到数千个时间序列并不容易实现。此外,协方差矩阵估计是一个耗时的过程——在训练期间,需要为每个时间窗口估计协方差。
为了解决这个问题,作者使用低秩近似来简化协方差估计。
让我们从基础开始。下面是多变量正态分布N**∼**(μ,Σ)
的矩阵形式,其中均值**μ** **∈** (k,1)
,协方差**Σ** **∈** (k,k)
方程 1: 矩阵形式的多变量高斯分布
这里的问题是协方差矩阵**Σ**
的大小是与数据集中时间序列的数量N
平方相关的。
我们可以使用一种近似形式来解决这个挑战,称为低秩高斯近似。这种方法源于因子分析,并与SVD(奇异值分解)密切相关。
我们可以通过计算以下内容来近似,而不是计算大小为**(**N,N**)**
**的完整协方差矩阵:
方程 2: 低秩协方差矩阵公式
其中**D** **∈** R(N,N)
是对角矩阵,**V** **∈** R(N,r)
。
但为什么我们使用低秩格式来表示协方差矩阵呢?因为r<<N
,证明了高斯似然可以使用O(Nr² + r³)
操作来计算,而不是O(N³)
(证明可以在论文的附录中找到)。
低秩正态分布是 PyTorch 的分布模块的一部分。可以随意尝试,看看它是如何工作的:
# multivariate low-rank normal of mean=[0,0], cov_diag=[1,1], cov_factor=[[1],[0]]
# covariance_matrix = cov_diag + cov_factor @ cov_factor.T
m = LowRankMultivariateNormal(torch.zeros(2), torch.tensor([[1.], [0.]]),
torch.ones(2))
m.sample()
#tensor([-0.2102, -0.5429])
深度 GPVAR 架构
符号: 从现在开始,所有粗体变量都被视为向量或矩阵。
注意: 在AI 项目文件夹中找到一个关于 Deep GPVAR 的动手项目,该文件夹会不断更新最新时间序列模型的教程!
现在我们已经了解了低秩正态近似的工作原理,接下来我们将深入探讨Deep GPVAR的架构。
首先,Deep GPVAR类似于 DeepAR——该模型也使用 LSTM 网络。假设我们的数据集中包含N
个时间序列,索引从i= [1…N]
在每个时间步t
,我们有:
-
一个 LSTM 单元将前一时间步
t-1
的目标变量z_t-1
用于部分时间序列作为输入。此外,LSTM 接收前一时间步的隐藏状态**h_t-1**
。 -
模型使用 LSTM 来计算其隐藏向量
**h_t**
。 -
隐藏向量
**h_t**
将被用来计算多变量高斯分布N∼(μ,Σ)
的**μ**
和**Σ**
参数。这是一种特别的正态分布,称为高斯 copula(稍后会详细介绍)。
这个过程如图 1所示:
**图 1:**Deep GPVAR 的两个训练步骤。协方差矩阵 Σ 表示为 Σ=D+V*V^T (来源)
在每个时间步t
,Deep GPVAR 随机选择B << N
个时间序列来实现低秩参数化:左侧,模型从(1、2 和 4)时间序列中选择,右侧,模型从(1、3 和 4)时间序列中选择。
作者在实验中将B
配置为 20。对于可能包含超过N=1000
个时间序列的数据集,这种方法的好处变得明显。
我们的模型估计了 3 个参数:
-
正态分布的均值
**μ**
。 -
协方差矩阵参数是
d
和**v**
,根据方程 2。
它们都从**h_t**
LSTM 隐藏向量中计算得出。图 2展示了低秩协方差参数:
图 2:低秩协方差矩阵的参数化,如方程 2所示
注意符号:
μ_i
、d_i
、**v_i**
指的是我们数据集中第i-th
个时间序列,其中i ∈ [1..N]
。
对于每个时间序列i
,我们创建**y_i**
向量,它将**h_i**
与**e_i**
连接在一起——**e_i**
向量包含第i-th
时间序列的特征/协变量。因此,我们有:
图 3展示了时间步t
的训练快照:
**图 3:**低秩参数化的详细架构
注意:
-
μ
和d
是标量,而**v**
是向量。例如,μ = **w_μ**^T * **y**
,维度为(1,p)
*(p,1)
,因此μ
是标量。 -
所有时间序列都使用相同的 LSTM,包括密集层投影
**w_μ**
、**w_d**
和**w_u**
。
因此,神经网络参数**w_μ**
、**w_d**
、**w_u**
和**y**
被用来计算**μ**
和**Σ**
参数,这些参数在图 3中展示。
高斯 Copula 函数
问题:
**μ**
和**Σ**
参数化什么?
它们参数化了一种特殊的多变量高斯分布,称为高斯 Copula。
但为什么 Deep GPVAR 需要高斯 Copula?
记住,Deep GPVAR 进行多维预测,因此我们不能像在 DeepAR 中那样使用简单的单变量高斯分布。
好吧。那么为什么不使用我们熟悉的多变量高斯分布,而是使用像方程 1中那样的 Copula 函数呢?
2 个原因:
1) 因为多元高斯分布需要高斯随机变量作为边际分布。 我们也可以使用混合分布,但它们过于复杂,不适用于每种情况。相反,高斯 Copulas 更易于使用,可以处理任何输入分布——这里的输入分布指的是我们数据集中单个时间序列。
因此,copula 学会在不对数据做出假设的情况下估计潜在的数据分布。
2) 高斯 Copula 可以通过控制协方差矩阵的参数化来建模这些不同分布之间的依赖结构 ***Σ***
*。 这就是 Deep GPVAR 学会考虑输入时间序列之间的相互依赖性,而其他全球预测模型则不做此类处理。
记住:时间序列可能具有不可预测的相互依赖性。 例如,在零售中,我们有产品的自相残杀:一个成功的产品会从其类别中的类似商品中吸引需求。因此,我们在预测产品销售时也应该考虑这种现象。通过 copulas,Deep GPVAR 自动学习这些相互依赖性。
什么是 copulas?
Copula 是一个描述多个随机变量之间相关性的数学函数。
注意: 如果你对 copulas 完全陌生,这篇文章 深入解释了 copulas 是什么以及我们如何构造它们。
Copulas 在定量金融中被广泛用于投资组合风险评估。它们的误用在 2008 年金融危机中也起到了重要作用。
更正式地说,copula 函数 **C**
是 N
个随机变量的累积分布函数(CDF),其中每个随机变量(边际)均匀分布:
图 4 显示了一个由 2 个边际分布组成的双变量 copula 的图像。copula 定义在 [0–1]² 域(x、y 轴)中,输出值在 [0–1](z 轴):
图 4: 由 2 个 beta 分布作为边际分布组成的高斯 copula CDF 函数
对于 **C**
的一个流行选择是高斯 Copula——图 4 中的 copula 也是高斯的。
我们如何构造 copulas
我们不会在这里深入讨论,但可以简要概述一下。
起初,我们有一个随机向量——一组随机变量。在我们的例子中,每个随机变量 z_i
代表时间序列 i
在时间步 t
的观测值:
然后,我们通过使用概率积分变换来使我们的变量均匀分布:任何连续随机变量的累积分布函数(CDF)输出是均匀分布的,F(z)=U
:
最后,我们应用我们的高斯 copula:
其中Φ^-1 是标准高斯 CDF N∼(0,1)
的逆函数,φ(小写字母)是一个用*μ*
和*Σ*
参数化的高斯分布*。注意Φ^-1[F(z)] = x
,其中x~(-Inf, Inf)
,因为我们使用的是标准逆 CDF。
那么,这里发生了什么?
我们可以取任何连续随机变量,将其边际化为均匀分布,然后将其转化为高斯分布。操作链如下:
这里有 2 种变换:
-
F(z) = U
,被称为概率积分变换。简单来说,这种变换将任何连续随机变量转换为均匀分布。 -
Φ^-1(U)=x
,被称为逆采样。简单来说,这种变换将任何均匀随机变量转换为我们选择的分布——在这里,Φ
是高斯分布,因此x
也成为高斯随机变量。
在我们的案例中,**z**
是我们数据集中时间序列的过去观察值。由于我们的模型对过去观察值的分布没有假设,我们使用经验 CDF——一种非参数(经验)计算任何分布 CDF 的特殊函数。
注意: 如果你不熟悉经验 CDF,请参考我的文章以获取详细解释。
换句话说,在F(z) = U
变换中,F
是经验 CDF,而不是变量z
的实际高斯 CDF。作者在实验中使用了m=100
个过去的观察值来计算经验 CDF。
回顾 copulas
总结来说,高斯 copula 是一个多变量函数,使用
μ
和Σ
直接参数化两个或更多随机变量的相关性。
但高斯 copula 如何与高斯多变量概率分布(PDF)不同?此外,高斯 copula 只是一个多变量 CDF。
-
高斯 copula 可以使用任何随机变量作为边际分布,而不仅仅是高斯分布。
-
数据的原始分布无关紧要——通过使用概率积分变换和经验 CDF,我们可以将原始数据转化为高斯分布,无论它们如何分布。
深度 GPVAR 中的高斯 copula
现在我们已经了解了 copula 的工作原理,是时候看看Deep GPVAR如何使用它们了。
回到图 3。使用 LSTM,我们已经计算了*μ*
和*Σ*
参数。我们所做的是以下操作:
步骤 1:将我们的观察值转换为高斯分布
使用 copula 函数,我们将观察到的时间序列数据点 **z**
转换为高斯 **x**
。转换公式为:
其中 f(z_i,t)
实际上是时间序列 i
的边际转换 Φ^-1(F(z_i))
。
在实际应用中,我们的模型对过去观察值 z
的分布没有假设。因此,无论原始观察值的分布如何,我们的模型都可以有效地学习它们的行为,包括它们的相关性——这一切都归功于高斯 copula 的强大功能。
步骤 2:使用计算出的高斯参数。
我提到我们应该将观察值转换为高斯分布,但高斯分布的参数是什么?换句话说,当我说 f(z_i) = Φ^-1(F(z_i))
时,Φ
的参数是什么?
答案是 *μ*
和 *Σ*
参数——这些参数是从图 3中所示的密集层和 LSTM 计算得到的。
步骤 3:计算损失并更新我们的网络参数
总结一下,我们将观察值转换为高斯分布,并假设这些观察值由低秩正态高斯分布参数化。因此,我们有:
其中 f1(z1)
是第一个时间序列的转换后的预测,f2(z2)
指的是第二个时间序列,而 f_n(z_n)
指的是我们数据集中的第 N 个时间序列。
最后,我们通过最大化多元 高斯对数似然函数来训练我们的模型。论文采用了最小化损失函数**—**即前面带有负号的高斯对数似然。
使用高斯对数似然损失,Deep GPVAR 更新其 LSTM 和图 3中显示的共享密集层权重。
另外,请注意:
-
**z**
不是单个观察值,而是所有N
个时间序列在时间t
的观察向量。求和会循环到T
,即最大查找窗口——在此之后计算高斯对数似然。 -
由于 z 是观察向量,高斯对数似然实际上是多元的。相比之下,DeepAR 使用的是单变量高斯似然。
Deep GPVAR: 大致概况
我建议多次阅读文章,以掌握模型的工作原理。
一般来说,你可以将 Deep GPVAR 视为一个高斯随机过程。
对于那些不熟悉随机过程的人,你可以将随机过程视为一组随机变量——每个变量代表某一时刻随机事件的结果。
随机变量由某个集合(通常是时间)索引,这些随机变量的值彼此依赖。因此,一个事件的结果可以影响未来事件的结果。这种相互依赖性也解释了随机过程的时间性。
在我们的案例中,时间序列y_i
的每个数据点可以视为一个随机变量。因此,Deep GPVAR可以被视为一个高斯过程GP
,在每个数据点y
上评估如下:
现在,高斯过程的参数是函数,而不是变量。在上述方程中,**μ**
、**d**
、**v**
(带有波浪线) 是时间函数,由**y**
索引——其中**y**
是在某个时间步的向量观察值。
在每个时间步,函数**μ**
、**d**
、**v**
(带有波浪线) 本质上是 LSTM 和 Dense 层,有一个实现 **μ**
、**d**
、**v**
(不带波浪线)。这个实现参数化了时间步*t*
下 copula 函数的*μ*
和*Σ*
。因此,在训练过程中,在每个时间步,*μ*
和*Σ*
会变化,以最佳地解释我们的观察结果。
因此,Deep GPVAR作为高斯随机过程的概念是完全合理的。
Deep GPVAR 变体
根据当前论文,亚马逊创建了 2 个模型,Deep GPVAR(我们在本文中描述)和DeepVAR。
DeepVAR 类似于Deep GPVAR。不同之处在于 DeepVAR 使用一个全球多变量 LSTM,一次接收和预测所有时间序列。另一方面,Deep GPVAR在每个时间序列上分别展开 LSTM。
在他们的实验中,作者将 DeepVAR 称为Vec-LSTM,而Deep GPVAR称为GP*。
-
Vec-LSTM和GP项在原始论文的表 1中提到。
-
Deep GPVAR和DeepVAR术语在亚马逊的预测库Gloun TS中提到。
本文描述了Deep GPVAR变体,该变体在平均情况下表现更好,并且比DeepVAR具有更少的参数。可以阅读原始论文以了解更多实验过程。
项目教程 — 需求能源预测
本教程使用了来自 UCI 的ElectricityLoadDiagrams20112014 [4]数据集。该示例的笔记本可以从这里下载:
注意: 目前,PyTorch Forecasting 库提供了 DeepVAR 版本。这就是我们在本教程中展示的变体。你还可以尝试所有 DeepAR 变体,包括亚马逊开源的Gluon TS 库中的 GPVAR。
下载数据
!wget https://archive.ics.uci.edu/ml/machine-learning-databases/00321/LD2011_2014.txt.zip
!unzip LD2011_2014.txt.zip
数据预处理
data = pd.read_csv('LD2011_2014.txt', index_col=0, sep=';', decimal=',')
data.index = pd.to_datetime(data.index)
data.sort_index(inplace=True)
data.head(5)
每一列代表一个消费者。每个单元格中的值是每 15 分钟的电力使用量。
此外,我们将数据汇总为每小时数据。由于模型的大小和复杂性,我们仅使用 5 个消费者进行模型训练(仅考虑非零值)。
data = data.resample('1h').mean().replace(0., np.nan)
earliest_time = data.index.min()
df=data[['MT_002', 'MT_004', 'MT_005', 'MT_006', 'MT_008' ]]
接下来,我们将数据集转换为 PyTorch Forecasting 理解的特殊格式——称为TimeSeriesDataset。这种格式非常方便,因为它允许我们指定特征的性质(例如,它们是时间变化的还是静态的)。
注意: 如果你想了解更多关于TimeSeriesDataset格式的信息,请查看我的Temporal Fusion Transformer 文章,该文章详细解释了该格式的工作原理。
为了将数据集修改为 TimeSeriesDataset 格式,我们将所有时间序列垂直堆叠,而不是水平堆叠。换句话说,我们将数据框从“宽”格式转换为“长”格式。
此外,我们从日期列创建新的特征,如day
和month
—这些特征将帮助我们的模型更好地捕捉季节性动态。
df_list = []
for label in df:
ts = df[label]
start_date = min(ts.fillna(method='ffill').dropna().index)
end_date = max(ts.fillna(method='bfill').dropna().index)
active_range = (ts.index >= start_date) & (ts.index <= end_date)
ts = ts[active_range].fillna(0.)
tmp = pd.DataFrame({'power_usage': ts})
date = tmp.index
tmp['hours_from_start'] = (date - earliest_time).seconds / 60 / 60 + (date - earliest_time).days * 24
tmp['hours_from_start'] = tmp['hours_from_start'].astype('int')
tmp['days_from_start'] = (date - earliest_time).days
tmp['date'] = date
tmp['consumer_id'] = label
tmp['hour'] = date.hour
tmp['day'] = date.day
tmp['day_of_week'] = date.dayofweek
tmp['month'] = date.month
#stack all time series vertically
df_list.append(tmp)
time_df = pd.concat(df_list).reset_index(drop=True)
# match results in the original paper
time_df = time_df[(time_df['days_from_start'] >= 1096)
& (time_df['days_from_start'] < 1346)].copy()
最终预处理的数据框称为time_df
。让我们打印其内容:
time_df
现在已转换为TimeSeriesDataset的正确格式。此外,TimeSeriesDataset格式要求我们的数据具有时间索引。在这种情况下,我们使用hours_from_start
变量。此外,power_usage
是我们模型将尝试预测的目标变量。
创建数据加载器
在这一步,我们将time_df
转换为TimeSeriesDataSet格式。我们这样做的原因是:
-
这使我们免去了编写自己的数据加载器的麻烦。
-
我们可以指定模型如何处理特征。
-
我们可以轻松地对数据集进行归一化。在我们的例子中,归一化是强制性的,因为所有时间序列在幅度上有所不同。因此,我们使用GroupNormalizer来单独归一化每个时间序列。
我们的模型使用一周(7*24)的回溯窗口来预测接下来的 24 小时的功率使用。
另外,请注意,hours_from_start
既是时间索引,又是时间变化特征。为了演示,我们的验证集是最后一天:
max_prediction_length = 24
max_encoder_length = 7*24
training_cutoff = time_df["hours_from_start"].max() - max_prediction_length
training = TimeSeriesDataSet(
time_df[lambda x: x.hours_from_start <= training_cutoff],
time_idx="hours_from_start",
target="power_usage",
group_ids=["consumer_id"],
max_encoder_length=max_encoder_length,
max_prediction_length=max_prediction_length,
static_categoricals=["consumer_id"],
time_varying_known_reals=["hours_from_start","day","day_of_week", "month", 'hour'],
time_varying_unknown_reals=['power_usage'],
target_normalizer=GroupNormalizer(
groups=["consumer_id"]
), # we normalize by group
add_relative_time_idx=True,
add_target_scales=True,
add_encoder_length=True,
)
validation = TimeSeriesDataSet.from_dataset(training, time_df, predict=True, stop_randomization=True, min_prediction_idx=training_cutoff + 1)
# create dataloaders for our model
batch_size = 64
# if you have a strong GPU, feel free to increase the number of workers
train_dataloader = training.to_dataloader(train=True, batch_size=batch_size, num_workers=2, batch_sampler="synchronized")
val_dataloader = validation.to_dataloader(train=False, batch_size=batch_size * 10, num_workers=2, batch_sampler="synchronized")
基线模型
同样,记得创建一个基线模型。
我们创建了一个朴素的基线模型,用于预测前一天的功率使用曲线:
actuals = torch.cat([y for x, (y, weight) in iter(val_dataloader)])
baseline_predictions = Baseline().predict(val_dataloader)
(actuals - baseline_predictions).abs().mean().item()
# ➢25.139617919921875
构建和训练我们的模型
我们现在可以开始训练我们的模型了。我们将使用来自 PyTorch Lightning 库的Trainer接口。
首先,我们需要配置超参数和回调函数:
-
使用EarlyStopping回调函数来监控验证损失。
-
Tensorboard用于记录我们的训练和验证指标。
-
hidden_size
和rnn_layers
分别指代 LSTN 单元的数量和 LSTM 层的数量。 -
我们还使用
gradient_clip_val
(梯度裁剪)来防止过拟合。此外,模型的默认 dropout 为0.1
——我们保留默认值。
我们的损失函数是**MultivariateNormalDistributionLoss**(rank : int)
。该函数将rank参数作为一个参数——这是我们在开始时解释的R
值。
记住,值越低,训练期间的加速效果越大。作者在他们的实验中使用rank=10——我们在这里也做了相同的处理:
pl.seed_everything(42)
early_stop_callback = EarlyStopping(monitor="val_loss", min_delta=1e-4, patience=4, verbose=True, mode="min")
lr_logger = LearningRateMonitor(logging_interval='step', log_momentum=True) # log the learning rate
logger = TensorBoardLogger("lightning_logs") # logging results to a tensorboard
trainer = pl.Trainer(
max_epochs=50,
accelerator='auto',
devices=1,
enable_model_summary=True,
gradient_clip_val=0.1,
callbacks=[lr_logger, early_stop_callback],
logger=logger,
)
net = DeepAR.from_dataset(
training,
learning_rate=0.001,
hidden_size=40,
rnn_layers=3,
loss=MultivariateNormalDistributionLoss(rank=10)
)
trainer.fit(
net,
train_dataloaders=train_dataloader,
val_dataloaders=val_dataloader
)
在 6 个周期后,EarlyStopping 触发,训练结束。
注意: 如果你想运行 DeepAR 而不是 DeepVAR,请使用NormalDistributionLoss。
加载和保存最佳模型
best_model_path = trainer.checkpoint_callback.best_model_path
print(best_model_path)
best_deepvar = DeepAR.load_from_checkpoint(best_model_path)
#path:
#lightning_logs/lightning_logs/version_0/checkpoints/epoch=6-step=40495.ckpt
不要忘记下载你的模型:
#zip and download the model
!zip -r model.zip lightning_logs/lightning_logs/version_0/*
这就是如何重新加载模型——你只需要记住最佳模型路径:
!unzip model.zip
best_model_path='lightning_logs/lightning_logs/version_0/checkpoints/epoch=6-step=40495.ckpt'
best_deepvar = DeepAR.load_from_checkpoint(best_model_path)
Tensorboard 日志
使用 Tensorboard 仔细查看训练和验证曲线。你可以通过执行以下命令启动它:
# Start tensorboard
%load_ext tensorboard
%tensorboard --logdir lightning_logs
模型评估
获取验证集上的预测并计算平均P50(分位数中位数)损失:
actuals = torch.cat([y[0] for x, y in iter(val_dataloader)])
predictions = best_deepvar.predict(val_dataloader)
#average p50 loss overall
print((actuals - predictions).abs().mean().item())
#average p50 loss per time series
print((actuals - predictions).abs().mean(axis=1))
# 6.78986
# tensor([ 1.1948, 6.9811, 2.7990, 8.3856, 14.5888])
注意,我们获得的分数比TFT 实现略差。我们使用的损失函数是概率性的——因此,每次得到的分数会略有不同。
最后两个时间序列的损失略高,因为它们的相对幅度较大。
绘制验证数据上的预测
raw_predictions, x = best_deepvar.predict(val_dataloader, mode="raw", return_x=True)
for idx in range(5): # plot all 5 consumers
fig, ax = plt.subplots(figsize=(10, 4))
best_deepvar.plot_prediction(x, raw_predictions, idx=idx, add_loss_to_title=True,ax=ax);
图 5: MT_002 的验证数据预测
图 6: MT_004 的验证数据预测
图 7: MT_005 的验证数据预测
图 8: MT_006 的验证数据预测
图 9: MT_008 的验证数据预测
验证集上的预测相当令人印象深刻。
同时,请注意我们没有进行任何超参数调整,这意味着我们可以获得更好的结果。
绘制特定时间序列的预测
之前,我们使用idx
参数绘制了验证数据上的预测,该参数遍历了数据集中的所有时间序列。我们可以更具体地输出特定时间序列的预测:
fig, ax = plt.subplots(figsize=(10, 5))
raw_prediction, x = best_deepvar.predict(
training.filter(lambda x: (x.consumer_id == "MT_004") & (x.time_idx_first_prediction == 26512)),
mode="raw",
return_x=True,
)
best_deepvar.plot_prediction(x, raw_prediction, idx=0, ax=ax);
图 10: MT_004 在训练集上的第二天预测
在图 10中,我们绘制了时间索引=26512 的MT_004消费者的第二天预测。
记住,我们的时间索引列hours_from_start
从 26304 开始,我们可以从 26388 开始获取预测(因为我们之前设置了min_encoder_length=max_encoder_length // 2
,即26304 + 168//2=26388
)。
绘制协方差矩阵
Deep (GP)VAR最大的优势在于协方差矩阵的估计——这提供了对数据集中时间序列相互依赖关系的一些洞察。
在我们的案例中,进行此计算没有意义,因为我们的数据集中只有 5 个时间序列。然而,以下是展示如何在有多个时间序列的数据集上进行计算的代码:
cov_matrix = best_deepvar.loss.map_x_to_distribution(
best_deepvar.predict(val_dataloader, mode=("raw", "prediction"), n_samples=None)
).base_dist.covariance_matrix.mean(0)
# normalize the covariance matrix diagnoal to 1.0
correlation_matrix = cov_matrix / torch.sqrt(torch.diag(cov_matrix)[None] * torch.diag(cov_matrix)[None].T)
fig, ax = plt.subplots(1, 1, figsize=(10, 10))
ax.imshow(correlation_matrix, cmap="bwr");
最终,PyTorch Forecasting 库提供了额外的功能,如样本外预测和 Optuna 的超参数调优。你可以在 TFT 教程中深入了解这些内容。
结束语
Deep GPVAR及其变体是一种强大的时间序列预测家族,具备了亚马逊研究的专业知识。
该模型通过考虑数千个时间序列之间的相互依赖性来处理多变量预测。
此外,如果你想了解更多关于DeepAR初始架构的信息,欢迎查阅这篇相关文章。
感谢阅读!
## AutoGluon-TimeSeries : 创建强大的集成预测 - 完整教程
亚马逊的时间序列预测框架应有尽有。
aihorizonforecast.substack.com
参考文献
[1] 使用 Stable Diffusion 创建,CreativeML Open RAIL-M 许可证。文本提示:“在太空中旅行的星云,数字艺术,插图”,到 rg
[2] D. Salinas 等人,DeepAR: Probabilistic forecasting with autoregressive recurrent networks,《国际预测期刊》(2019)
[3] D. Salinas 等人,高维多变量预测与低秩高斯 Copula 过程
[4] ElectricityLoadDiagrams20112014 数据集,由 UCI 提供,CC BY 4.0。
所有图片均由作者创建,除非另有说明
深度学习用于预测:数据预处理和训练
原文:
towardsdatascience.com/deep-learning-for-forecasting-preprocessing-and-training-49d2198fc0e2
如何使用多个时间序列训练深度神经网络
·发表于Towards Data Science ·8 分钟阅读·2023 年 3 月 22 日
–
图片由Tamara Malaniy拍摄,来源于Unsplash
这篇文章是对上一篇文章的后续。在那里,我们学习了如何为深度学习转换时间序列。
我们继续探索深度神经网络在预测中的应用。在这篇文章中,我们将:
-
学习如何使用深度学习训练全球预测模型,包括基本的预处理步骤;
-
探索 keras 回调函数以推动神经网络的训练过程。
深度学习用于预测
深度神经网络通过自回归解决预测问题。自回归是一种建模技术,涉及使用过去的观察值来预测未来的观察值。
深度神经网络可以设计成不同的结构,如递归网络或卷积网络。递归神经网络通常更适合处理时间序列数据。除此之外,这种网络在建模长期依赖关系方面表现出色。这一特性对预测性能有着强大的影响。
这里介绍了一种特定类型的递归神经网络,称为 LSTM(长短期记忆)。注释提供了每个模型元素的简要描述。
from keras.models import Sequential
from keras.layers import (Dense,
LSTM,
TimeDistributed,
RepeatVector,
Dropout)
# Number of variables in the time series.
# 1 means the time series is univariate
N_FEATURES = 1
# Number of lags in the auto-regressive model
N_LAGS = 24
# Number of future steps to be predicted
HORIZON = 12
# 'Sequential' instance is used to create a linear stack of layers
# ... each layer feeds into the next one.
model = Sequential()
# Adding an LSTM layer with 32 units and relu activation
model.add(LSTM(32, activation='relu', input_shape=(N_LAGS, N_FEATURES)))
# Using dropout to avoid overfitting
model.add(Dropout(.2))
# Repeating the input vector HORIZON times to match the shape of the output.
model.add(RepeatVector(HORIZON))
# Another LSTM layer, this time with 16 units
# Also returning the output of each time step (return_sequences=True)
model.add(LSTM(16, activation='relu', return_sequences=True))
# Using dropout again with 0.2 dropout rate
model.add(Dropout(.2))
# Adding a standard fully connected neural network layer
# And distributing the layer to each time step
model.add(TimeDistributed(Dense(N_FEATURES)))
# Compiling the model using ADAM and setting the objective to minimize MSE
model.compile(optimizer='adam', loss='mse')
之前,我们学习了如何转换时间序列以训练此模型。但是,有时你会有多个时间序列可用。
如何处理这些情况?
使用多时间序列进行深度学习
全球方法的兴起
预测模型通常是使用时间序列的历史数据创建的。这些模型可以被称为局部模型。相比之下,全球方法则通过汇总多个时间序列的历史数据来建立模型。
当一种称为 ES-RNN 的方法赢得 M4 竞赛——一个包含 100000 个不同时间序列的预测竞赛——时,对全球模型的兴趣激增。
何时以及为何使用全球模型
全球模型在涉及多个时间序列的预测问题中可以提供相当大的价值。例如,在零售领域,目标是预测多种产品的销售量。
另一个使用这种方法的动机是获得更多的数据。机器学习算法在训练集更大时表现更好。这一点在具有大量参数的方法中尤为明显,如深度神经网络。这些已知为数据需求大。
全球预测模型不假设基础时间序列是相关的。也就是说,一个序列的滞后值可以用来预测另一个序列的未来值。
相反,这些技术利用来自多个时间序列的信息来估计模型的参数。在预测时间序列的未来时,模型的主要输入是该序列的过去近期滞后值。
实践操作
在本文的其余部分,我们将探讨如何使用多个时间序列训练深度神经网络。
数据
我们将使用关于美国 8 个地区电力消耗的数据集:
美国 8 个地区的日常电力消耗(对数)。数据来源于参考文献[1]。图像由作者提供。
目标是预测未来几天的电力消耗。这一问题对电力系统操作员至关重要。准确的预测有助于平衡能源的供需。
我们可以按如下方式读取数据:
import pandas as pd
# https://github.com/vcerqueira/blog/tree/main/data
data = pd.read_csv('data/daily_energy_demand.csv',
parse_dates=['Datetime'],
index_col='Datetime')
print(data.head())
预处理步骤
在训练多个时间序列的深度神经网络时,需要应用一些预处理步骤。在这里,我们将探讨以下两种方法:
-
均值缩放
-
对数变换
可用的时间序列集可能具有不同的尺度。因此,将每个序列归一化到一个共同的值范围是很重要的。对于全球预测模型,这通常是通过将每个观测值除以相应序列的均值来完成的。
from sklearn.model_selection import train_test_split
# leaving last 20% of observations for testing
train, test = train_test_split(data, test_size=0.2, shuffle=False)
# computing the average of each series in the training set
mean_by_series = train.mean()
# mean-scaling: dividing each series by its mean value
train_scaled = train / mean_by_series
test_scaled = test / mean_by_series
在均值缩放之后,对数变换也可以是有帮助的。
在上一篇文章中,我们探讨了如何通过对时间序列取对数来处理异方差性。对数变换还可以帮助避免神经网络的饱和区域。饱和发生在神经网络对不同输入变得不敏感时。这会阻碍学习过程,导致模型效果不佳。
import numpy as np
class LogTransformation:
@staticmethod
def transform(x):
xt = np.sign(x) * np.log(np.abs(x) + 1)
return xt
@staticmethod
def inverse_transform(xt):
x = np.sign(xt) * (np.exp(np.abs(xt)) - 1)
return x
# log transformation
train_scaled_log = LogTransformation.transform(train_scaled)
test_scaled_log = LogTransformation.transform(test_scaled)
自回归
在预处理每个时间序列之后,我们需要将它们从序列转换为观测数据集。对于单个时间序列,你可以查看之前的文章以了解此过程的详细信息。
对于多个时间序列,思路类似。我们为每个序列分别创建一组观测数据。然后,这些数据被连接成一个单一的数据集。
这是你可以做到的方法:
# src module here: https://github.com/vcerqueira/blog/tree/main/src
from src.tde import time_delay_embedding
N_FEATURES = 1 # time series is univariate
N_LAGS = 3 # number of lags
HORIZON = 2 # forecasting horizon
# transforming time series for supervised learning
train_by_series, test_by_series = {}, {}
# iterating over each time series
for col in data:
train_series = train_scaled_log[col]
test_series = test_scaled_log[col]
train_series.name = 'Series'
test_series.name = 'Series'
# creating observations using a sliding window method
train_df = time_delay_embedding(train_series, n_lags=N_LAGS, horizon=HORIZON)
test_df = time_delay_embedding(test_series, n_lags=N_LAGS, horizon=HORIZON)
train_by_series[col] = train_df
test_by_series[col] = test_df
之后,你通过按行连接的方式将每个时间序列的数据组合在一起。
train_df = pd.concat(train_by_series, axis=0)
print(train_df)
最后,我们将目标变量与解释变量分开,如前所述:
# defining target (Y) and explanatory variables (X)
predictor_variables = train_df.columns.str.contains('\(t\-|\(t\)')
target_variables = train_df.columns.str.contains('\(t\+')
X_tr = train_df.iloc[:, predictor_variables]
Y_tr = train_df.iloc[:, target_variables]
# transforming the data from matrix into a 3-d format for deep learning
X_tr_3d = from_matrix_to_3d(X_tr)
Y_tr_3d = from_matrix_to_3d(Y_tr)
# defining the neural network
model = Sequential()
model.add(LSTM(32, activation='relu', input_shape=(N_LAGS, N_FEATURES)))
model.add(Dropout(.2))
model.add(RepeatVector(HORIZON))
model.add(LSTM(16, activation='relu', return_sequences=True))
model.add(Dropout(.2))
model.add(TimeDistributed(Dense(N_FEATURES)))
model.compile(optimizer='adam', loss='mse')
# spliting training into a development and validation set
X_train, X_valid, Y_train, Y_valid = \
train_test_split(X_tr_3d, Y_tr_3d, test_size=.2, shuffle=False)
# training the neural network
model.fit(X_train, Y_train, validation_data=(X_valid,Y_valid), epochs=100)
使用回调函数训练深度神经网络
深度神经网络是迭代方法。它们在训练数据集上循环多次,称为周期。
在上面的例子中,我们运行了 100 个周期。但不清楚应该运行多少个周期来训练一个网络。周期太少可能导致欠拟合;周期太多则可能导致过拟合。
处理这个问题的一种方法是监控每个周期后神经网络的性能。每当模型性能提高时,你会在继续训练过程之前保存它。然后,在训练结束后,你将得到保存的最佳模型。
在 keras 中,你可以使用回调函数来处理这个过程。回调函数是一个在训练过程中执行某些操作的函数。你可以查看keras 文档以获取完整的回调函数列表,或者学习如何编写你自己的回调函数!
用于在训练过程中保存模型的回调函数称为 ModelCheckPoint:
from keras.callbacks import ModelCheckpoint
model_checkpoint = ModelCheckpoint(
filepath='best_model_weights.h5',
save_weights_only=True,
monitor='val_loss',
mode='min',
save_best_only=True)
model = Sequential()
model.add(LSTM(32, activation='relu', input_shape=(N_LAGS, N_FEATURES)))
model.add(Dropout(.2))
model.add(RepeatVector(HORIZON))
model.add(LSTM(16, activation='relu', return_sequences=True))
model.add(Dropout(.2))
model.add(TimeDistributed(Dense(N_FEATURES)))
model.compile(optimizer='adam', loss='mse')
history = model.fit(X_train, Y_train,
epochs=300,
validation_data=(X_valid,Y_valid),
callbacks=[model_checkpoint])
另一个有趣的回调函数是EarlyStopping。它可以在性能停止改善时停止训练。
做出预测
训练后,我们可以检索最佳模型并对测试集进行预测。
# The best model weights are loaded into the model.
model.load_weights('best_model_weights.h5')
# Inference on DAYTON region
test_dayton = test_by_series['DAYTON']
# spliting target variables from explanatory ones
X_ts = test_df.iloc[:, predictor_variables]
Y_ts = test_df.iloc[:, target_variables]
X_ts_3d = from_matrix_to_3d(X_ts)
# predicting on normalized data
preds = model.predict_on_batch(X_ts_3d)
preds_df = from_3d_to_matrix(preds, Y_ts.columns)
# reverting log transformation
preds_df = LogTransformation.inverse_transform(preds_df)
# reverting mean scaling
preds_df *= mean_by_series['DAYTON']
关键要点
-
许多预测问题涉及多个时间序列,例如零售领域。在这种情况下,全球方法通常更适合构建模型;
-
在训练神经网络之前,预处理时间序列很重要。均值缩放有助于将所有序列带入一个共同的值范围。取对数可以稳定方差,避免饱和区域;
-
使用回调函数来驱动神经网络的训练过程。
感谢阅读,下次故事见!
参考文献
[1] 每小时能源消耗时间序列(许可证: CC0: 公共领域)
[2] Hewamalage, Hansika, Christoph Bergmeir 和 Kasun Bandara. “用于时间序列预测的递归神经网络:现状与未来方向。” 国际预测学杂志 37.1 (2021):388–427。
[3] Slawek Smyl, Jai Ranganathan 和 Andrea Pasqua. M4 预测竞赛:介绍一种新的混合 ES-RNN 模型,2018. URL eng.uber.com/
m4-forecasting-competition/。
推荐系统中的深度学习:入门指南
原文:
towardsdatascience.com/deep-learning-in-recommender-systems-a-primer-96e4b07b54ca
现代工业推荐系统背后最重要的技术突破概述
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 6 月 26 日
–
图片来源:Pixabay
推荐系统是目前发展最快的工业机器学习应用之一。从商业角度来看,这并不奇怪:更好的推荐带来更多的用户。就是这么简单。
然而,基础技术远非简单。自从深度学习的兴起——由 GPU 的商品化驱动——推荐系统变得越来越复杂。
在这篇文章中,我们将回顾过去十年中一些最重要的建模突破,粗略重建标志着深度学习在推荐系统中崛起的关键点。这是一个关于技术突破、科学探索,以及跨越大陆和合作的军备竞赛的故事。
准备好吧。我们的旅程从 2017 年的新加坡开始。
NCF(新加坡大学,2017 年)
图片来源:He 等(2017 年)
任何关于推荐系统中深度学习的讨论,如果不提到该领域最重要的突破之一——神经协作过滤(NCF),都将是不完整的。这个突破由He 等(2017 年)在新加坡大学提出。
在 NCF 之前,推荐系统的黄金标准是矩阵分解,其中我们为用户和项目学习潜在向量(也称为嵌入),然后通过计算用户向量和项目向量之间的点积来生成用户的推荐。正如我们在线性代数中所知,点积越接近 1,预测的匹配度就越高。因此,矩阵分解可以简单地被视为潜在因素的线性模型。
NCF 中的关键理念是用神经网络替代矩阵分解中的内积。在实践中,这通过首先将用户和项目的嵌入连接起来,然后将它们传递到一个具有单一任务头的多层感知器(MLP)中,任务头预测用户参与度,如点击量。然后,通过反向传播损失梯度,在模型训练期间学习 MLP 权重和嵌入权重(将 ID 映射到相应的嵌入)。
NCF 背后的假设是用户/项目的互动并非线性,如矩阵分解中假设的那样,而是非线性的。如果这一点成立,我们应该会看到随着 MLP 层数的增加,性能会有所提升。正如 He 等人发现的那样,使用 4 层,他们能够在 Movielens 和 Pinterest 基准数据集上比当时最好的矩阵分解算法高出约 5% 的命中率。
He 等人证明了深度学习在推荐系统中的巨大价值,标志着从矩阵分解到深度推荐系统的关键过渡。
Wide & Deep (Google, 2016)
图片来源:Cheng et al (2016)
我们的行程从新加坡继续到加州的山景城。
虽然 NCF 革新了推荐系统领域,但它缺乏一个对推荐系统成功至关重要的成分:交叉特征。交叉特征的理念在 Google 的 2016 年论文 “Wide & Deep Learning for Recommender Systems” 中得到了普及。
什么是交叉特征?它是通过“交叉”两个原始特征创建的二阶特征。例如,在 Google Play Store 中,一阶特征包括所需的应用或用户安装的应用列表。这两个特征可以结合起来创建强大的交叉特征,例如
AND(user_installed_app='netflix', impression_app='hulu')
如果用户安装了 Netflix 且所需的应用是 Hulu,则值为 1。
交叉特征也可以更为广泛,例如
AND(user_installed_category='video', impression_category='music')
诸如此类。作者认为,添加不同粒度的交叉特征能够实现记忆(来自更细粒度的交叉)和泛化(来自较少粒度的交叉)。
Wide&Deep 的关键架构选择是同时拥有一个宽模块,这是一个直接将所有交叉特征作为输入的线性层,以及一个深度模块,本质上是一个 NCF,然后将两个模块合并为一个单一的输出任务头,从用户/应用参与度中学习。
确实,Wide&Deep 的效果非常好:作者发现通过从仅深度模型转向宽度加深度,在线应用获取的提升达到 1%。考虑到谷歌每年从 Play Store 中获得数十亿的收入,很容易看出 Wide&Deep 的影响力。
DCN(谷歌,2017)
图片来源:Wang et al (2017)
Wide&Deep 证明了交叉特征的重要性,但它有一个巨大的缺点:交叉特征需要手动工程,这是一项繁琐的过程,需要工程资源、基础设施和领域专长。Wide&Deep 式的交叉特征成本高昂,不具备扩展性。
进入“深度和交叉神经网络”(DCN),这是 2017 年谷歌的一篇论文中介绍的。DCN 的核心思想是用“交叉神经网络”替代 Wide&Deep 中的宽度组件,这是一种专门学习任意高阶交叉特征的神经网络。
交叉神经网络与标准 MLP 有何不同?作为提醒,在 MLP 中,下一层的每个神经元都是前一层所有神经元的线性组合:
相比之下,在交叉神经网络中,下一层是通过形成第一层与自身的二阶组合来构建的:
因此,深度为 L 的交叉神经网络将以最高 L 次的多项式形式学习交叉特征。神经网络越深,学习的高阶交互作用也越高。
实验确实确认了 DCN 的有效性。与仅有深度组件的模型相比,DCN 在 Criteo 展示广告基准数据集上的 logloss 低了 0.1%(这被认为是统计显著的)。而且这是没有任何手动特征工程的,如同 Wide&Deep!
(如果能看到 DCN 与 Wide&Deep 之间的比较就好了。可惜的是,DCN 的作者没有找到合适的方法来手动创建 Criteo 数据集的交叉特征,因此跳过了这个比较。)
DeepFM(华为,2017)
图片来源:Guo et al (2017)
接下来,我们的旅程将从 2017 年的谷歌转向 2017 年的华为。
华为的深度推荐解决方案“DeepFM”同样用一个专门学习交叉特征的神经网络替代了 Wide&Deep 中的手动特征工程。然而,与 DCN 不同,宽度组件不是交叉神经网络,而是所谓的 FM(“矩阵分解机”)层。
FM 层的作用是什么?它只是计算所有嵌入对的点积。例如,如果电影推荐系统使用 4 个 id 特征作为输入,比如用户 id、电影 id、演员 id 和导演 id,那么模型将学习所有这些 id 特征的嵌入,FM 层计算 6 个点积,分别对应用户-电影、用户-演员、用户-导演、电影-演员、电影-导演和演员-导演。这是矩阵分解思想的回归。然后,FM 层的输出与深度组件的输出结合,通过一个 sigmoid 激活的输出,产生模型的预测结果。
的确,正如你可能猜到的,DeepFM 已经被证明有效。作者展示了 DeepFM 在公司内部数据上比一系列竞争者(包括 Google 的 Wide&Deep)在 AUC 和 Logloss 上分别高出 0.37%和 0.42%。
DLRM(Meta,2019)
图片来源:Naumov et al (2019)
让我们暂时搁置 Google 和华为。我们的下一站是 2019 年的 Meta。
Meta 的 DLRM(“用于推荐系统的深度学习”)架构,如Naumov et al (2019)所述,工作原理如下:所有类别特征通过嵌入表转换为嵌入。所有稠密特征也被传递到一个 MLP 中,该 MLP 为这些特征计算嵌入。重要的是,所有嵌入具有相同的维度。然后,我们简单地计算所有嵌入对的点积,将它们连接成一个单一的向量,并通过一个最终的 MLP,该 MLP 有一个 sigmoid 激活的任务头,生成预测。
DLRM,几乎可以看作是 DeepFM 的简化版:如果你取下 DeepFM 的深度组件(仅保留 FM 组件),你会得到类似 DLRM 的东西,但没有 DLRM 的稠密 MLP。
在实验中,Naumov 等人展示了 DLRM 在 Criteo 展示广告基准数据集上的训练和验证准确率上都优于 DCN。这一结果表明,DCN 中的深度组件可能确实是多余的,而我们真正需要的只是特征交互,在 DLRM 中,这些交互通过点积来捕获。
DHEN(Meta,2022)
图片来源:Zhang et al (2022)
与 DCN 相比,DLRM 中的特征交互仅限于二阶:它们只是所有嵌入对的点积。回到电影的例子(特征包括用户、电影、演员、导演),二阶交互包括用户-电影、用户-演员、用户-导演、电影-演员、电影-导演以及演员-导演。三阶交互可能是用户-电影-导演、演员-演员-用户、导演-演员-用户,等等。某些用户可能是斯蒂文·斯皮尔伯格执导、汤姆·汉克斯主演电影的粉丝,应该有一个交叉特征!然而,在标准的 DLRM 中,没有。这是一个主要的限制。
进入DHEN,这是我们现代推荐系统巡演中的最后一篇地标论文。DHEN 代表“深度层次集成网络”,其关键思想是创建一个交叉特征的“层次结构”,随着 DHEN 层数的增加而变得更深。
最容易理解 DHEN 的方法是先用一个简单的例子。假设我们有两个输入特征进入 DHEN,我们用 A 和 B 来表示它们(例如可以是用户 ID 和视频 ID)。一个 2 层的 DHEN 模块会创建到二阶的整个交叉特征层次结构,即:
A, AxA, AxB, B, BxB,
其中“x”可以是以下 5 种交互中的一种或多种组合:
-
点积,
-
自注意力,
-
卷积,
-
线性:y = Wx,或者
-
来自 DCN 的交叉模块。
DHEN 是一只猛兽,其计算复杂度(由于其递归特性)令人头疼。为了使其正常工作,DHEN 论文的作者不得不发明一种新的分布式训练范式,称为“混合分片数据并行”,其吞吐量比(当时的)最先进技术高出 1.2 倍。
但最重要的是,这只猛兽确实有效:在他们对内部点击率数据的实验中,作者使用了一堆 8 (!) DHEN 层,相较于 DLRM,NE 提升了 0.27%。
总结
Criteo 展示广告竞赛排行榜的演变。截图来自 paperswithcode.com。
这就结束了我们的巡演。请允许我用一个标题总结这些地标:
-
NCF:我们只需要用户和项目的嵌入。MLP 会处理其余的。
-
Wide&Deep:交叉特征很重要。实际上,它们如此重要,我们将它们直接输入到任务头中。
-
DCN:交叉特征很重要,但不应手动工程化。让交叉神经网络来处理这些特征。
-
DeepFM:让我们在 FM 层生成交叉特征,同时保留 Wide&Deep 的深度组件。
-
DRLM:FM 就是我们需要的一切——还有一个专门的 MLP 用于密集特征。
-
DHEN:FM 还不够。我们需要一个更高阶的(超越二阶)的层次结构特征交互。还有一系列优化以确保它在实践中有效。
旅程其实才刚刚开始。在撰写本文时,DCN 已经演变成了 DCN-M,DeepFM 也演变成了 xDeepFM,而 Criteo 比赛的排行榜已经被华为最新的发明 FinalMLP 占据。
鉴于对更好推荐的巨大经济激励,未来可预见的时间里我们一定会继续看到这一领域的新突破。敬请关注。
深度强化学习改进了排序算法
谷歌 DeepMind 如何创建出更高效的排序算法
·
关注 发表在 数据科学前沿 · 8 分钟阅读 · 2023 年 6 月 13 日
–
上周,Google DeepMind 在《自然》杂志上发表了一篇论文,声称通过使用深度强化学习(DLR)发现了一种更高效的排序算法。DeepMind 以推动强化学习(RL)的边界而闻名。几年前,他们利用类似程序击败了围棋游戏中的最佳选手 AlphaGo,之后又用相似的程序战胜了现有的国际象棋引擎。在 2022 年,DeepMind 推出了 AlphaTensor,这是一种利用 DLR 找到更高效矩阵乘法算法的程序。所使用的技术类似于 DeepMind 最新的成就:改进标准排序算法。在本文中,我将讨论他们如何通过引入强化学习、蒙特卡洛树搜索以及 DeepMind 的方法和代理 AlphaDev 的细节来实现这一点。
排序
编写排序算法很简单:逐个比较所有值以找到最低(或最高)值,将此值保存在返回列表的第一个元素中,然后继续处理下一个值,直到没有更多值为止。虽然这个算法有效,但远非高效。由于排序在计算机程序中非常常见,因此高效的排序算法受到深入研究。DeepMind 专注于两种排序变体:固定排序和变量排序。固定排序涉及对具有固定预定义长度的值序列进行排序。在变量排序中,值列表的大小没有预先定义。只提供了列表的最大大小。这两种排序变体在最先进的排序算法中广泛使用。通常,通过反复排序列表的小部分来对大列表进行排序。
目前,最先进的算法在固定排序和变量排序中都使用了所谓的排序网络。在本文中,我不会讨论排序网络的详细信息。你可以在这里找到更多信息。
强化学习
在这一部分,我将介绍强化学习的主要概念。强化学习是机器学习的一个分支,其中一个智能体被任务赋予在环境中找到最佳行动,基于当前状态。环境的状态包含了所有可能影响所采取行动的相关方面。最佳行动定义为最大化(折扣)累积奖励的行动。行动是依次进行的,在每次行动后,获得的奖励和新状态都会被记录。通常,环境有一些终止标准,在这些标准之后,下一个回合开始。早期版本的 RL 使用表格来跟踪某些状态和行动的值,目的是总是采取具有最高值的行动。深度神经网络常常替代这些表格,因为它们能够更好地泛化,而列举所有状态通常是不可能的。当深度神经网络用于强化学习时,称之为深度强化学习。
蒙特卡洛树搜索
AlphaDev 使用深度强化学习和蒙特卡洛树搜索进行训练,这是一种在给定初始状态下寻找最佳行动的搜索算法。它通常与 DRL 结合使用,例如在 AlphaZero 和 AlphaGo 中。它值得拥有自己的一篇文章,但这里提供了一个摘要。
蒙特卡洛树搜索(MCTS)构建了一个可能结果状态的树,其中当前状态是根节点。对于给定数量的模拟,将探索这棵树以找出采取某些行动的后果。在每次模拟中,使用回放(有时称为游戏过程)来扩展一个节点为子节点,如果游戏在此时没有结束。采取哪种行动基于选择标准。在我们的案例中,选择某个行动的概率由策略网络提供,下面将讨论这一点。节点的值是衡量该节点状态好坏的指标。它由值网络确定,这也将在未来的部分中讨论。如果达到终止状态,则该值会被环境的真实奖励值所替代。
值在搜索树中向上传播。通过这种方式,当前状态的值依赖于从当前状态可以到达的状态的值。
DeepMind 的方法
由 DeepMind 训练的 DRL 代理,旨在改进排序算法实现,称为 AlphaDev。AlphaDev 的任务是编写一个更高效的汇编排序算法。汇编语言是高层语言(如 C++、Java 和 Python)与机器代码之间的桥梁。如果你在 C++ 中使用 sort,编译器会将你的程序编译成汇编代码。汇编器随后将这些代码转换为机器代码。AlphaDev 的任务是选择一系列汇编指令,以创建一个既正确又快速的排序算法。这是一个困难的任务,因为添加一条指令可能会使程序完全错误,即使之前是正确的。
AlphaDev 通过创建并解决 AssemblyGame 来寻找高效的排序算法。在每一轮中,它必须选择一个与 CPU 指令对应的动作。通过将这个问题表述为一个游戏,它可以轻松适应标准的强化学习框架。
强化学习公式
标准的 RL 公式包含状态、动作、奖励和终止标准。
状态
AssemblyGame 的状态由两部分组成。首先是当前程序的表示。AlphaDev 使用 Transformer 的输出,Transformer 是一种神经网络架构,用于表示当前算法。最近 Transformer 在大规模语言模型中取得了很大成功,并且由于算法是基于文本的,Transformer 也非常适合对当前算法进行编码。状态的第二部分是运行当前算法后内存和寄存器状态的表示。这通过传统的深度神经网络来完成。
动作
AssemblyGame 的动作是将合法的汇编指令附加到程序中。AlphaDev 可以选择添加任何合法的指令。DeepMind 为合法动作创建了以下六条规则:
-
内存位置总是按递增顺序读取
-
寄存器按递增顺序分配
-
每个内存位置读取和写入一次
-
不允许连续比较指令
-
不允许将比较或条件移动到内存中
-
未初始化的寄存器不能使用
从编程角度来看,最后两个动作是非法的,其他的则是被强制执行的,因为它们不会改变程序。通过删除这些动作,搜索空间被限制而不影响算法。
奖励
AssemblyGame 的奖励由两部分组成。奖励的第一个元素是正确性分数。根据一系列测试序列,提供的算法会被评估。算法越接近正确排序测试序列,正确性的奖励就越高。奖励的第二个元素是程序的速度或延迟。这通过程序的行数来衡量,如果没有条件分支的话。在实践中,这意味着程序的长度用于固定排序,因为该程序不需要条件。对于可变排序,算法必须根据序列的实际长度进行条件判断。由于排序时并非所有部分的算法都会被使用,因此测量的是程序的实际延迟,而不是行数。
终止和目标
根据 DeepMind 发布的论文,他们在“有限步骤”后终止 AssemblyGame。这具体意味着什么不清楚,但我猜他们是根据当前的人工基准限制步骤数量。游戏的目标是找到一个正确且快速的算法。如果 AlphaDev 提供了一个错误或慢速的算法,游戏就会失败。
策略和价值网络
使用蒙特卡罗树搜索(Monte Carlo Tree Search)时,需要一个策略网络和一个价值网络。策略网络设置每个动作的概率,而价值网络则训练用于评估状态。策略网络根据特定状态的访问次数进行更新。这创建了一个迭代过程,其中策略用于执行 MCTS,策略网络则根据 MCTS 中状态的访问次数进行更新。
价值网络输出两个值,一个是算法的正确性,一个是算法的延迟。根据 DeepMind 的说法,这比通过网络将这些值组合成一个分数的结果更好。价值网络根据获得的奖励进行更新。
结果
在训练 AlphaDev 后,它找到了一些长度为 3 和 5 的固定排序的更短算法。对于长度为 4 的固定排序,它找到了当前的实现,因此没有改进。在固定排序中,AlphaDev 通过应用两个新想法实现了这些结果。这些新想法被称为交换(swap)和复制移动(copy move),它们减少了值之间的比较次数,从而使算法更快。对于长度为 3 的固定排序,DeepMind 通过穷举所有较短程序长度的选项证明了没有更短的程序存在。对于较长的程序,这种方法不可行,因为搜索空间呈指数级增长。
对于可变排序,AlphaDev 提出了算法的新设计。例如,对于长度最多为 4 的可变排序序列,AlphaDev 建议先对 3 个值进行排序,然后对最后剩余的元素执行简化版本的排序。下图展示了 AlphaDev 提供的 Varsort(4)的算法设计。
图片由 D. Mankowitz 等人提供,《使用深度强化学习发现更快的排序算法》(2023),《自然》
- 总体而言,为固定和可变排序发现了更快的算法,证明了深度强化学习可以用于实现高效的算法。C++中的排序实现已经更新为使用这些新算法。有关 AlphaDev 实现的性能提升的详细信息,请参见下表。
表格由 D. Mankowitz 等人提供,《使用深度强化学习发现更快的排序算法》(2023),《自然》
- 为了测试这种方法是否可以推广到其他编程实现,DeepMind 还在一个竞争性编码挑战和 Google 使用的协议缓冲区上测试了 AlphaDev。在这两个案例中,AlphaDev 能够提出更高效的实现,证明了使用蒙特卡洛树搜索的深度强化学习是一种找到常见算法高效实现的有效方法。
结论
-
深度强化学习在许多不同的环境中取得了成功,从围棋和国际象棋等游戏到算法实现(如本文所示)。尽管该领域具有挑战性,通常取决于低级实现细节和超参数选择,但这些成功展示了这一强大概念能够取得的成果。
-
我希望这篇文章让你对强化学习的最新成就有了一些了解。如果你想要更详细的蒙特卡洛树搜索或我讨论的其他算法的解释,请告诉我!
来源
[1] K. Batcher,《排序网络及其应用》(1968),《1968 年 4 月 30 日至 5 月 2 日春季联合计算机会议论文集》(第 307–314 页)
[2] D. Mankowitz 等人,《使用深度强化学习发现更快的排序算法》(2023),《自然》618.7964: 257–263
[3] D. Silver 等人,《通过自我对弈掌握国际象棋和将棋的通用强化学习算法》(2017),arXiv 预印本 arXiv:1712.01815
[4] D. Silver 等人,《通过深度神经网络和树搜索掌握围棋》(2016),《自然》529.7587: 484–489
[5] A. Fawzi 等人,《通过强化学习发现更快的矩阵乘法算法》(2022),《自然》610.7930: 47–53
[6] C. Browne 等人,《蒙特卡洛树搜索方法概述》(2012),《IEEE 计算智能与游戏中的人工智能》4.1: 1–43。
对简单线性回归的深刻理解
原文:
towardsdatascience.com/deep-understanding-of-simple-linear-regression-3776afe34473
从零开始的线性回归:详细解释
·发表于数据科学之路 ·阅读时间 6 分钟·2023 年 1 月 10 日
–
作者提供的图片
动机
机器学习是一个过程,通过它,机器可以从数据中学习,并在没有明确编程的情况下对新数据做出合理决策。这些模型的基础是数学和统计学。线性回归是其中一种简单且广泛使用的回归算法。回归算法预测连续值。
例如,我们想预测价格、年龄、体重等。这些值是不可计数的。因此,它们被称为连续值。如果你仍然感到困惑,我建议你阅读这篇文章。
目录
-
**机器学习中的回归问题是什么?**
-
**我们何时使用简单线性回归?**
-
**简单线性回归详解**
-
**使用 Python 的实践实现**
机器学习中的回归问题是什么?
在数据科学中,机器学习算法用于自动化系统。在实践中,主要有两种问题——i. 监督学习和 ii. 无监督学习。
在监督学习问题中,训练数据集是有标签的。这意味着算法有一个目标值。监督学习算法尝试预测类似目标值的值,并相应地优化其参数。但在无监督学习问题中,训练数据集没有目标值。无监督学习算法尝试找出数据之间的相似性,并相应地训练模型。
监督问题可以进一步分为两类——i. 分类和 ii. 回归。 分类问题是那些需要预测分类值的问题。相反,回归问题处理的是连续值。
我们何时使用简单线性回归?
我想在简单线性回归之前介绍线性回归。线性回归是通过拟合回归线来找到回归输出的过程。它仅在我们的数据呈线性分布时有效。
简单或单变量线性回归是在只有一个自变量或特征时的回归过程。
还有多变量线性回归。我将在接下来的文章中讨论它。
何时使用——
当数据线性分布且仅包含一个特征或自变量时,简单线性回归最为合适。
详细的简单线性回归
简单线性回归只接受一个自变量。看一下直线的简单方程。
在线性回归中,方程被称为*回归线方程*。这里,m 和 c 是系数。m 表示回归线的斜率,c 是表示回归线与 y 轴交点的常数值。x 表示自变量,y 是因变量。
让我们用下面的图示更清楚地说明。
简单线性回归(作者图像)
简单线性回归处理一个自变量(x)和一个因变量(y)。自变量是输入值,因变量是输出值或目标值。
看一下图片,让我逐步解释这个过程。**x 轴**
代表自变量的值,**y 轴**
代表因变量的值。黑点是训练数据点的散点图,数据似乎呈线性分布。这些数据将用于训练回归模型的参数。为此,我们将通过散点图拟合一条直线,并使回归线的累计距离尽可能最小。如果我们找到最佳回归线,我们可以通过输入**x**
值轻松获得预测值。对于一个新的数据点,绘制一条垂直直线将与回归线相交。预测值将是从回归线交点绘制的水平线在y
轴上的交点。为了拟合最佳回归线,找出上述方程的**m**
和**c**
的系数值是我们的主要挑战。
再次说明,回归线方程是***y=mx+c***
。 我们将从数据集中获取x
的值,但m和c是未知的。***m***
和***c***
的最佳值可以产生***y***
的最佳预测值。
对于简单线性回归,我们可以用以下公式找到斜率**m**
。
将**m**
的值代入以下方程中,我们将找到**c**
的最佳值。
[在这个过程中存在一个限制。如果自变量多于一个,我们不能通过这个手动公式找到系数值。在这种情况下,我们使用梯度下降和代价函数来找到最佳值。我将在即将到来的文章中讨论它。]
使用 Python 进行实践操作
导入所需的 Python 库。
对于任何机器学习模型,数据集是主要的燃料。*我们使用的是* [*波士顿房价*](https://www.cs.toronto.edu/~delve/data/boston/bostonDetail.html) *数据集,该数据集公开可用,并且在公有领域许可下。*
从这里下载数据集。
让我们加载数据集。
由于主数据集中没有列名,所以需要设置列名。
寻找皮尔逊相关性以选择相关性最高的特征作为自变量。
我们的目标值是‘MEDV’
。从上述相关性图中,‘RM’
变量的相关性最高,为0.7
。因此,我们选择了变量‘RM’
。该变量的全称如下 []。
MEDV - Median value of owner-occupied homes in $1000's.
RM - Average number of rooms per dwelling.
定义 x 和 y。
拆分训练集和测试集。
定义计算***m 和 c***
系数值的函数。
绘制回归线。
预测函数。
我们数据集的实际值与预测值。
我们的模型在某些值上存在显著误差。这是因为我们的数据集有许多特征,而我们仅考虑了一个特征进行演示,因为我们处理的是简单线性回归。
让我们计算模型的平均绝对误差。
结论
实际上,有许多库和简单的方法可以实现线性回归。但我更喜欢从最基础的知识学习,因为这对绝对初学者很有益。我相信如果我们的基础足够坚固,我们可以在基础上构建高楼,否则可能会出现结构不稳定的问题。
这篇文章将对初学者非常有帮助,并将提供一个坚实的基础。读者将了解到简单线性回归是如何工作的。
完整的笔记本可以在 这里获取。
*[我正在编写一系列关于从零实现的机器学习算法的文章。你可以通过下面的链接阅读之前关于 KNN 和 K-means 聚类的文章。]*
KNN 算法的实现和详细解释
[towardsdatascience.com ## 从零开始的 K-means 聚类
K-means:最佳的 ML 算法来聚类数据
towardsdatascience.com
参考文献
定义通用人工智能
原文:
towardsdatascience.com/defining-artificial-general-intelligence-a4b167aa84ba
你如何判断一个系统是否达到了 AGI(通用人工智能)?
·发布于 Towards Data Science ·阅读时间 7 分钟·2023 年 11 月 23 日
–
照片由 Possessed Photography 提供,Unsplash
上周,萨姆·奥特曼被解除 OpenAI 的首席执行官职务。他离开的真正原因仍然未知。根据董事会的说法,他被解雇是因为董事会“得出结论认为他在与董事会的沟通中并不始终坦诚,这妨碍了董事会履行其职责。”这一模糊的解释引发了大量关于奥特曼被解雇原因的猜测。一些最有说服力的理论包括:
-
奥特曼将市场渗透优先于安全与隐私测试
-
奥特曼在一项重大交易中绕过了董事会
-
奥特曼的自我膨胀过大,开始与公司的使命发生冲突
有趣的是,一些最有说服力的传言指向了对 AI 伦理的观点分歧,特别是在通用人工智能(“AGI”)的发展方面。虽然奥特曼一直是 AGI 潜力的 vocal 支持者,但传言称首席科学家伊利亚·苏茨克弗对 OpenAI 内部技术的快速进展产生了日益增长的担忧。
在这篇文章中,我们将总结谷歌 DeepMind 的新论文 Levels of AGI: Operationalizing Progress on the Path to AGI 中的一些概念。这篇论文有助于定义 AGI,建立评估 AGI 系统的框架,并总结一些 AGI 可能带来的风险。
定义 AGI(通用人工智能)
在人工智能(“AI”)领域,AGI 是能够执行大多数人类水平任务的系统的一个子集。假设这些系统能够像人类一样广泛理解、学习和应用其智能。一个完整的 AGI 系统不应局限于其所训练的数据。相反,系统在存在的过程中可以收集信息并随时间学习。对于许多 AI 公司来说,AGI 是明确或隐含的长期目标。目前,AGI 尚未实现。然而,鉴于 LLM 和人工智能领域的快速进展,AGI 感觉比以往任何时候都更接近。
在论文中,DeepMind 的研究人员概述了 AGI 的定义特征。 “<DeepMind 研究人员>认为,AGI 的任何定义应满足以下六个标准:”
-
“专注于能力,而非过程”:AGI 系统的关键不在于其如何完成任务,而在于系统能够做什么任务。AGI 系统不要求以类似人类的方式思考或理解,也不需要具有人类意识。这并不是说这些系统不会表现出这些特性——但它们不必符合 AGI 的定义。
-
“专注于通用性和性能”:一些系统选择强调通用性——即处理各种任务和适应不同环境的能力。然而,通用性不应以能力为代价。这些系统需要能够以高水平的性能执行广泛的任务。
-
“专注于认知和元认知任务”:研究人员认为,系统是否能够执行物理任务并不是判断其是否为 AGI 的标准。相反,这些系统应专注于完成认知和元认知任务。在这种情况下,认知任务包括感知、记忆、语言和问题解决。元认知任务包括在需要时学习新任务或获取更多信息的能力。
-
“专注于潜力,而非部署”:任何符合 AGI 标准的系统并不需要在现实世界中部署才能被认为是 AGI。相反,系统需要证明其能够满足 AGI 的标准。根据作者的说法,“要求部署作为衡量 AGI 的条件会引入非技术性障碍,例如法律和社会考虑,以及潜在的伦理和安全问题。”
-
“专注于生态有效性”:任何致力于实现 AGI 定义的系统应关注人们在现实世界中重视的任务。换句话说,AGI 不应专注于高度专业化或抽象的任务,比如解决极其复杂的理论问题。这类任务对大多数人日常生活中并不有价值。
-
“关注通向 AGI 的路径,而不是单一的终点”:概述不同等级的 AGI 并附上明确的指标、基准和风险,使政策和进展的讨论变得更加容易。不同的等级可以使系统之间的比较更为简单,并量化进展。
这六个 AGI 标准确保研究人员和其他相关方对 AGI 有一致的理解。这些标准旨在消除与不同术语、能力或结果相关的混淆,并将讨论集中在重要的方面上。我并不是说这六个标准是正确的,但它们确实使得在考虑与 AGI 相关的系统时更容易思考。
AGI 的等级
在上面的部分中,第六个标准提到一个系统来概述不同等级的 AGI。下表总结了 DeepMind 定义的各种 AGI 等级。请注意,对于每个等级,表格审视了狭义和广义 AI 任务,其中:
-
狭义任务是明确界定或定义的。
-
一般任务是包括学习新技能的各种任务。
AGI 的等级,Google DeepMind arxiv.org/pdf/2311.02462.pdf
当我们谈论狭义的 AI 时,我们会看到许多特定等级的产品实例(例如,等级 0 的简单计算器)。当使用案例范围明确且具体时,构建完全自主的解决方案更为容易。然而,这些系统由于其专业化的性质,未能满足 DeepMind 对 AGI 的标准。例如,AlphaFold 是一个旨在预测 3D 蛋白质结构的系统。尽管其专业化能力令人印象深刻,但其狭窄的关注点意味着它缺乏对 AGI 至关重要的生态有效性。
当我们谈论像 ChatGPT 这样更为通用的系统时,我们会遇到一系列不同的挑战。最初,ChatGPT 处理各种问题的能力令人印象深刻。然而,随着使用工具的时间增加,我逐渐意识到,尽管 ChatGPT 在表面上看似超人,但在需要深入专业知识的领域,它常常有所欠缺。我们可以将这个观点与上面提到的 AGI 等级联系起来——它仅能在无技能人类(等级 1)的水平上回答问题。它的许多回答看起来似乎是事实正确的,但实际上却充满了不准确性。这一认识突显了今天的 AI 系统,尽管具备“首创”的能力,但在性能的深度上往往有所欠缺。它们优先考虑广泛的功能,但并不擅长所有这些功能,这与 DeepMind 对 AGI 的第二个标准相冲突。
AGI 的风险
在 2004 年电影机器人总动员中,机器人存在是为了服务于人类主人。这些机器人不被允许伤害人类,必须随时服从人类,并且必须不惜一切代价保护主人。随着电影情节的发展,这些机器人开始不再遵守这些规则。它们发展出了自由意志和情感。结果,这些机器人成为了对人类的存在性威胁。
尽管这种世界末日场景可能在 AGI 世界中成为风险,但还有更多实际的风险。下表总结了与窄域和通用 AI 相关的各种风险,涉及不同级别的 AI 自主性。
AGI 的风险,Google DeepMind arxiv.org/pdf/2311.02462.pdf
诚然,当我首次开始使用高级 AI 工具并研究 AGI 这一概念时,我对工具的潜在风险视而不见。我下意识地拒绝承认这些工具的发展和进步可能会带来任何负面副作用。我是说,谁不喜欢全天候访问 30 秒代码审查的服务呢?
然而,当我偶然看到这张表格时,它改变了我的观点。它让我看到了这些系统的相关风险,包括我们今天使用的工具。将 AI 作为顾问(“自主级别 2”)类似于许多人今天使用 ChatGPT 或依赖推荐系统来获取产品或电影推荐的方式。以这种方式使用工具可能导致长期的过度信任或有针对性的操控。事实上,最近我找不到人来审查我的代码,于是我让 ChatGPT 审查了我的代码。ChatGPT 的审查结果显示代码无误,于是我将代码推向了生产环境。结果,代码中充满了 bug,几乎立即被回滚。我对 ChatGPT 过度信任,付出了代价!
随着我们继续推动窄域和通用 AI 系统的发展,风险也显著增加。目前的系统可能导致过度信任和针对性操控,而更强大的系统则可能导致大量失业和权力集中。作为技术导向的一方,我对我们取得的进展感到非常惊讶,并对未来的发展充满了激动。然而,作为实践的一方,我认识到需要在构建先进 AI 系统时仔细考虑风险和后果。
定义可解释的特征
这是 MIT 研究人员总结的发现和开发的分类法。
·
关注 发表在 数据科学前沿 ·9 分钟阅读·2023 年 1 月 14 日
–
在 2022 年 2 月,麻省理工学院数据到人工智能(DAI)小组的研究人员发布了一篇名为《可解释特征的必要性:动机和分类法》的论文[1]。在这篇文章中,我旨在总结这些作者的一些主要观点和贡献,并讨论他们工作的潜在影响和批评。如果你对这些内容感兴趣,我强烈推荐阅读原始论文。此外,如果你对可解释机器学习不太熟悉,我强烈推荐Christopher Molnar 的免费书籍 [2]。虽然可解释性/解释性的定义在不同的出版物中常常变化[1],但这本书提供了理解该领域的坚实基础。
论文的核心发现是即使是像线性回归这样高度可解释的模型,非可解释的特征也可能导致难以理解的解释(例如,特征 x12 的权重为 4 对大多数人来说毫无意义)。鉴于此,论文提供了利益相关者的分类、可解释特征的现实世界应用场景、各种特征质量的分类,以及可能的可解释特征转换,这些都帮助数据科学家开发易于理解的特征。
利益相关者的定义
论文的第一个贡献是扩展了 Preece 等人提出的可能受益于机器学习解释的主要用户类型,并定义了一些他们的兴趣。虽然 Preece 等人提出了 4 种主要的利益相关者类型,但本文的作者将该列表扩展到 5 种:
-
开发者:那些训练、测试和部署机器学习模型的人,他们关注特征以提高模型性能。
-
理论家:那些对推进机器学习理论感兴趣的人,他们关注特征,以了解其对模型内部机制的影响。
-
伦理学家:那些对模型的公平性感兴趣的人,他们关注特征,以确保模型的伦理使用。
-
决策者:那些获取模型结果以完成任务和决策的人。他们对特征本身不感兴趣,但需要解释以确保他们的决策基于可靠的信息。
-
受影响的用户:这些是受模型及其使用影响的个人,但不会直接与模型互动,除非是为了理解对他们自身的影响。
各种用户在特征工程方面有不同的需求,这些需求往往相互冲突。决策者可能希望模型中的特征越简单越好,以便更好地解释,而开发者可能会选择复杂的转换,以使特征具有极高的预测能力。
现实世界应用场景
除了介绍利益相关者之外,作者还介绍了 5 个实际领域,在这些领域中,他们在试图解释自己开发的模型时遇到了障碍。
案例研究
儿童福利
在这项案例研究中,DAI 团队与社会工作者和科学家(担任决策者和伦理学家)合作,开发了一个解释性 LASSO 模型,该模型拥有超过 400 个特征,并输出潜在儿童虐待案件的风险评分。在此过程中,DAI 团队发现模型的大多数不信任来源于特征而非机器学习算法。一个突出的问题是关于一热编码分类特征(例如role of child is sibling == False
)的措辞。此外,许多社会工作者和科学家对他们认为与预测任务无关的特征表示担忧,基于他们的主题领域专业知识。
教育
在在线教育领域,作者致力于为大规模开放在线课程(例如 Coursera、edX 等免费课程)相关的各种决策任务添加可解释性。在与各种课程开发者和讲师合作时,作者发现,最有用的特征是那些将数据组合成对用户有意义的抽象概念(例如将工作完成情况和互动结合成参与
特征)的特征。此外,研究人员发现,当这些抽象概念的数据来源易于追溯时,利益相关者的反应更好。
网络安全
在第三个领域,研究人员致力于开发检测领域生成算法的模型,以帮助安全分析师应对潜在的攻击。虽然为识别这些攻击工程了许多特征,但构建这些特征的原始 DNS 日志对用户的帮助更大,作者面临的挑战是如何将特征值追溯到相关日志。
医疗记录
在医疗保健领域,研究人员与六位临床医生合作开发了一个预测手术后并发症的模型。在这项案例研究中,作者使用了 SHAP 值来解释特征贡献,但很快发现仅靠 SHAP 解释是不够的。延续网络安全领域的趋势,作者发现基于聚合函数的特征不如原始信号数据那样易于解释。
卫星监测
在这项案例研究中,作者旨在可视化时间序列异常检测解决方案的结果,并与六位领域专家一起开发了一个工具。作者随后进行了两项用户研究,分别使用领域专家和普通终端用户的股票价格数据来评估该工具。在这项工作中,作者发现对插补过程的透明度需求更高,大多数问题集中在哪些值是插补的而非真实的。
经验教训
从所有案例中总结了三个关键经验:
-
文献中大多数关注点在于选择和工程化特性以最大化模型性能,但与人类用户和决策者接口的模型需要一个可解释的特性空间才有用。
-
为了具有可解释性,特性需要具备多种属性(稍后将在分类法中讨论)。
-
虽然将特性转换为模型准备状态的重要性不言而喻,但也需要有方法来撤销这些转换以保持可解释性。
特性分类法
作者结合他们工作的领域和大量文献搜索,开发了一种特性质量的分类法,识别了用户。作者将这些特性组织为两个主要质量——模型准备性和可解释性——其中一些特性同时具备这两个质量。
模型准备属性使特性在模型中表现良好,是开发者、理论家和伦理学家关注的重点。
可解释属性是使特性对用户更易理解的属性。这些属性主要惠及决策者、用户和伦理学家。
模型准备特性属性
-
预测性:特性与预测目标的相关性。这并不意味着直接的因果关系,因为特性可能是混淆变量或虚假相关性。
-
模型兼容性:特性由模型架构支持,但可能不具备预测性或实用性。
-
模型准备性:特性与模型兼容,能够帮助生成准确预测。模型准备特性还包括通过标准化和归一化等方法转换过的特性。
可解释特性属性
-
可读性:特性以普通文本书写,用户无需查看代码即可理解所指内容。
-
人性化:该特性既可读又以自然、友好的方式描述。作者发现,儿童福利领域的利益相关者特别受益于这一特性。
-
可理解性:特性指代用户理解的现实世界指标。这个属性在很大程度上依赖于用户的专业知识,但通常是那些没有经过复杂数学操作的特性(例如,年龄是可理解的,但
log(humidity)
可能不是)。
模型准备性和可解释性属性兼具
-
有意义:特性是学科专家认为与目标变量相关的特性。一些特性可能具有预测性,但由于虚假相关性而不具意义。类似地,一些特性可能有意义,但预测性不强。然而,最好还是尽量使用有意义的特性。
-
抽象概念:特性通过某些领域专家定义的原始特性的组合计算而得,通常是通用概念(例如,参与和成就)。
-
可追踪:该特征可以与其计算原始数据准确关联。
-
可模拟:如果需要,可以从原始数据中准确重新计算该特征。所有可模拟的特征都是可追踪的,但并非所有可追踪的特征都是可模拟的。例如,
test grade over time
可能是可追踪的(它来自原始测试成绩),但不能模拟,因为这可能指的是每月或每年的平均成绩或成绩变化。
可解释变换
除了可解释特征的各种属性,作者还提出了一些特征工程方法及其可能对特征可解释性的贡献。虽然有些数据变换可以帮助特征准备模型,但这并不常见。可解释性变换旨在帮助弥补这一差距,但往往会撤销模型准备变换。这可能会降低模型的预测能力,但会引入可解释的特征属性,使其更受决策者、用户和伦理学家的信任。
-
转换为分类变量:在解释特征时,将独热编码变量转换回其分类形式。
-
语义分箱:在对数值数据进行分箱时,尽量基于现实世界的区别而不是统计区别进行分箱。例如,将
age
按child
、young-adult
、adult
和senior
分类,比按四分位数进行分箱更具可解释性。 -
标记插补:如果使用数据插补,添加一个额外的特征来识别包含插补数据的点,可以大大增加对模型的信任。
-
汇总数值特征:当数据中存在许多紧密相关的指标时,将它们汇总为一个特征可能有利于防止数据过载。例如,作者发现将各种身体和情感虐待的推荐汇总为一个单一的推荐计量,有助于决策者。
-
修改分类粒度:当许多类别彼此相关时,通过选择变量的适当总结(例如,将 森林覆盖类型 数据集中的土壤区划汇总为主要的 8 种地质土壤区)可以提高可解释性和性能。
-
转换为抽象概念:应用数值汇总和分类粒度变换器,开发一个手工制作的公式,以生成主题专家可以理解的抽象概念。
-
反向缩放和特征工程:如果应用了标准化、归一化或数学变换,在分析特征之前反转这些变换可以提高可解释性。例如,报告
age
的特征权重比报告sqrt(age)
的权重更有帮助。 -
原始数据链接:此转换扩展了反向缩放和特征工程。如果可能,请明确展示如何从原始数据计算工程特征。
虽然这不是所有可能转换的详尽列表,但它确实为数据科学家提供了一个很好的起点,介绍了一些简单的步骤,以确保他们拥有一个可解释的特征空间。
讨论与结论
图 1:Zytek 等人提出的特征分类总结 [1](图源自论文)
阅读这篇论文时,我确实有一些批评。首先,尽管作者开发了各种利益相关者,但他们从未提供过 受影响用户 和 决策者 不同的示例。虽然我们可以做出一些有根据的猜测(例如,学生在教育案例中可能是受影响的用户,而患者在医疗案例中可能是受影响的用户),但并没有提出解释性特征如何帮助这个群体的理由。
作者们自己也提出了一些可解释特征的风险。在他们的例子中,开发者可能会恶意地将种族特征包含到社会经济因素的抽象概念中,从而有效地掩盖了种族在模型中的预测作用。此外,作者承认,许多提出的可解释性转换可能会降低模型性能。一些可解释特征属性(如可读性)在数据隐私重要时也不适用。
尽管有这些批评,但不可否认的是,Zytek 等人 [1] 提供了很多关于特征如何变得可解释、如何实现可解释性以及为何这很重要的信息。此外,提出的转换相对容易实现,使其对初学者数据科学家更为友好。他们的分类在上面的图 1 中总结,可能是大多数数据科学家需要随时备份在桌上的图像。
资源与参考文献
[1] A. Zytek, I. Arnaldo, D. Liu, L. Berti-Equille, K. Veeramachaneni. 对可解释特征的需求:动机与分类(2022)。SIGKDD Explorations。
[2] C. Molnar. 可解释的机器学习(2020)。LeanPub
[3] A. Preece, D. Harborne, D. Braines, R. Tomsett, S. Chakraborty. 可解释 AI 中的利益相关者(2018)。《政府与公共部门的人工智能》第 6 页。
[3] S. Lundberg, S.I. Lee. 对模型预测的统一解释方法(2017)。《神经信息处理进展》第 31 卷第 10 页。
Delta Lake — 自动模式演变
原文:
towardsdatascience.com/delta-lake-automatic-schema-evolution-11d32bd1aa99
合并演变数据框时发生了什么,以及你可以/不能做的事情
·发表于Towards Data Science ·阅读时间 5 分钟·2023 年 3 月 10 日
–
图片由McDobbie Hu提供,来源于Unsplash
在上一篇文章中,我们讨论了事务日志和如何保持 Delta 表快速和干净。这次我们将讨论 Delta 表中的自动模式演变。
模式演变是管理数据随时间变化的关键方面。数据源不断发展以适应新的业务需求,这可能意味着需要从现有的数据模式中添加或删除字段。作为数据使用者,快速而灵活地适应数据源的新特性是至关重要的,而自动模式演变使我们能够无缝地适应这些变化。
在这篇文章中,我们将讨论在使用Delta时的自动模式演变,并使用people10m 公共数据集(这是在 Databricks Community Edition 上提供的)进行测试。我们将在几个场景中测试添加和删除字段。
设置
自动模式演变可以通过两种方式启用,具体取决于我们的工作负载。如果我们进行盲目追加,只需启用mergeSchema选项即可:
如果我们使用合并策略插入数据,则需要通过将spark.databricks.delta.schema.autoMerge.enabled设置为true来启用它。
在这篇文章中,我们将使用合并策略,因此我们将选择后者。
我们已经设置好了,可以加载我们的 Delta 表,它应该如下所示:
初始数据集
模式演变
为了模拟模式演变,我们将使用手动创建的模式创建自定义 DataFrame,并使用 Scala 的 Delta API 合并它们。
免责声明:我们将对模式进行的所有更新只是示例,并不意味着有太大意义。
初始 DataFrame 模式
模拟和合并新记录
添加字段
假设我们公司希望友好对待昵称,员工可以用他们喜欢的昵称被称呼(真棒!)。
我们将向当前模式中添加一个名为nickName的新字段,并更新 Pennie 的 nickName(id 编号 1)。
带有 nickName 的模式
添加新字段
如我们所见,添加了一个新字段,Pennie 现在可以用她的新喜欢的昵称称呼!注意到所有其他记录的值都自动填充为 null。
删除字段
由于添加了昵称,大家开始思考为何没有人使用他们的中间名,因此决定删除它。
无中间名的模式
我们还将更新 Quyen 的昵称,但由于源删除了字段,她的中间名将不会存在。表格应该如何处理?
删除中间名后的表
如果你什么也没猜到,那你是对的。每个当前目标表记录保持不变,只有新记录的middleName将是null。
为了展示这一点,我们将插入一个新的 id(0)。
插入新记录后的表
重命名列
重命名列与删除一列并用新名称添加另一列相同。如果你希望在原地重命名列,请参阅 Delta 列名映射。
我不会进一步深入这个话题,因为尽管这是一种模式演变,但它不是自动的。请记住,这个功能是不可逆的,一旦启用,你将无法关闭它。
更改列类型/顺序
更改列类型或列顺序也不是自动模式演变的一部分。
在结构中添加/删除字段
假设我们添加了一个员工历史结构,其中包括startDate和endDate,以追踪员工何时开始和离开工作。
为了更完整的历史记录,我们现在希望包括title以追踪员工在公司的职业生涯。
更新了带有‘title’的结构
如我们所见,向结构中添加字段也不是问题。如果我们尝试删除新添加的字段,它也会成功。添加和删除结构中的字段与在根中执行的方式相同。
在结构数组中添加/删除字段
现在我们要处理更复杂的情况。在这种情况下,我们将向数组中的结构体添加一个新字段。假设我们现在有一个属于某个员工的设备数组:
为了展示在数组中添加新字段的情况,我们将向结构体中添加一个serial_num,以便更好地跟踪设备。
在数组中更新的结构体添加了‘serial_num’
正如我们所见,这也按预期工作。表模式已更新,新记录具有相应的serial_num,而旧记录的serial_num被填充为null值。
如果我们再次移除新添加的字段,它会按预期工作。
在结构体的映射中添加/删除字段
现在是时候在映射中测试相同的操作了。我们添加了一个名为connections的新列,该列将负责存储每个员工的层级。
为了模拟更新,我们将向connections列中的结构体添加一个名为title的新列。
在映射中更新的结构体添加了‘title’
这一次,删除返回AnalysisException的字段,这意味着 MapType 转换不被很好地支持。
经简要调查,我发现这是因为castIfNeeded函数尚不支持 MapTypes。我已报告了一个错误,并将尝试解决这个问题。
编辑: github.com/delta-io/delta/pull/1645
结论
在这篇文章中,我们讨论了在几种不同情况下添加和删除字段的问题。我们得出结论,Delta 中的自动模式演变非常完善,支持大多数复杂场景。通过允许这些场景,我们可以避免在数据源演变时手动干预以更新模式。这在处理数百个数据源时特别有用。
作为额外的奖励,我们还发现了 MapTypes 中不支持的一个遗漏案例,这是一个很好的机会为这样一个出色的开源项目做出贡献。
希望你喜欢这篇文章!请确保关注更多内容!
Delta Lake:删除向量
原文:
towardsdatascience.com/delta-lake-deletion-vectors-65bc9dc90b63
删除向量与 DML 命令有何关联,它们如何改善写入性能?
·发表于 Towards Data Science ·9 分钟阅读·2023 年 5 月 25 日
–
能够更新和删除记录是从传统数据仓库过渡到数据湖时失去的一个功能。虽然数据湖在解决规模和成本问题上表现出色,但它们牺牲了更新和删除记录的能力。数据湖由许多文件组成,这些文件很快会变成数据沼泽,这正是问题出现的地方,而湖仓架构则解决了这一问题。
湖仓架构是一种结合了数据仓库和数据湖的混合型架构,旨在解决它们各自的问题。其中一个问题是数据仓库中备受喜爱的缺乏 DML 支持的 ACID 事务,而这正是 Delta Lake 的亮点。然而,由于 Delta 的 ACID 保证,文件的原地修改是不可能的。
在这篇文章中,我将讨论 Delta 如何支持 DML 命令、删除向量如何工作以及它们作为性能改进的重要性。
Delta Lake — DML 背后的实现
在 事务日志 的帮助下,Delta Lake 支持如 Update、Delete 和 Merge 的 DML 命令。为了简便起见,我们将专注于 Update 和 Delete 命令,因为它们更简单,并且在底层的工作方式相同。
那么,当执行以下查询时会发生什么?
更新前后的文件 — 图片来源于作者
涉及三个步骤:
-
查找谓词 gender = ‘M’ 匹配的文件
-
对于找到的每个文件,重写文件并更新记录
-
从事务日志中删除文件 2 和文件 4,并添加文件 5 和文件 6
相同的逻辑也适用于删除命令。将文件重写为所需更新的过程称为写时复制。
通过复制包含更新记录的整个文件,我们避免了就地修改,并能够同时浏览事务日志和构成 Delta 表最新状态的多个不同版本(时间旅行)。
对于上述查询,这种策略似乎没有问题,因为有很高比例的记录会被更新,但那这个呢?
在这种情况下,包含特定 id 的文件必须被没有该记录的新文件完全替换。对于小文件,这不应成为问题,但假设我们有几百 MB 的文件,这种做法是非常低效的。
总之,写时复制对于快速读取非常好,因为所有数据始终存在于同一个文件中,并且在 DML 操作不多的情况下表现良好。相反,写入操作成本高,当更新操作很多时,这种策略是不理想的,如上所述。
删除向量
Delta 删除向量(DVs)是一种机制,用于在由于写时复制而只更新文件中很小百分比的记录时,提高写入性能。
简单来说,它们是 RoaringBitmaps 的数组,直接映射到 Parquet 文件的行中(32 位整数的默认实现不足以覆盖可能的行数)。DVs 由删除或更新现有数据的命令创建,作为标记用于在扫描数据时过滤行。在删除的情况下,它们充当删除标记,正如名称所示;在更新的情况下,它们充当行无效标记,我将在下面进一步详细说明。
设置
让我们使用之前 Delta Lake 文章中使用的 people10m 公共数据集 并分析 DVs 实际上是如何工作的。
我们应该有这样的表:
Delta Table people10m — 作者提供的图片
我们需要做的第一件事是通过运行以下命令启用该功能:
设置删除向量标志
注意: 请注意,通过启用此功能,表协议会更新为readerVersion=3 和 writerVersion=7,可能会与旧版本的读取器/写入器不兼容。
让我们通过从表中删除一个 id 来强制创建一个删除向量:
没有 DVs 的话,我们应该在事务日志中有一个新文件,包含原始文件中的所有 id,除了 id 1。让我们通过检查事务日志来看看发生了什么。
解包事务日志
CommitInfo
commitInfo 条目包含有关 DELETE 操作的信息。
事务中的提交信息条目
在这里我们看到 numDeletionVectorsAdded:”1" 和 numAddedFiles:”0",这意味着我们避免了重写文件。
移除
你可能会觉得奇怪,因为它包括一个 remove 条目,这种行为类似于标准的写时复制行为,上述指标表明没有文件被移除 numRemovedFiles:”0"。
在事务日志中移除条目
添加
add 条目解释了启用 DVs 后发生的所有事情。
在事务日志中添加条目
在这里我们看到,添加的文件的 path 与移除的文件相同,但现在包含了删除向量。添加和移除条目现在可以选择性地包含一个 deletionVector 字段,其中包含与文件关联的 DVs 信息。
在支持 DVs 的版本中,文件通过文件路径和唯一删除向量 id 唯一标识,当未使用 DVs 时,默认值为 NULL。
这个 id 是如何定义的?新的 DV 结构提供了什么信息?
它主要取决于存储类型。在我们的例子中,存储类型是“u”。对于这种存储类型,pathOrInlineDv 是 <random prefix — optional><base85 encoded uuid>
。它用于存储在相对于 Delta 表路径的路径中的 DVs。DV 文件名可以从 UUID 派生(见详细信息)。
Delta 表路径中的删除向量
在我们的例子中,我们可以看到删除向量与表文件一起存储。请注意,如果表具有分区值,DV 文件不会保存在分区目录中,而是保存在 Delta 表根目录中,因为 DV 可以跨多个分区。
存储类型可以采用其他值,例如 “i”,其中向量内联,因此 pathOrInlineDv 是 <base85 encoded bytes>
,以及 “p”,其中文件存储在由 pathOrInlineDv 提供的绝对路径中。
Offset 是一个可选字段,表示数据在由文件支持的存储类型中的起始位置。当 DV 内联(存储类型 “i”)时,该字段不存在。
SizeInBytes 是 DV 的字节大小。
Cardinality 是 DV 逻辑上删除的行数。
启用删除向量的 DML
启用 DVs 后,DML 命令的工作方式有所不同,以利用新的信息。
更新时没有现有的 DVs
启用 DVs 的更新 — 作者提供的图像
启用 DVs 后,更新命令逻辑如下:
-
扫描所有满足条件并需要更新的文件(File 2, File 4)
-
为需要失效的行写入 DVs(DV 2.1,DV 4.1)
-
写入更新后的行的新文件(File 5)
-
将 (File 5, NULL) 添加到事务日志中,移除 (File 2, NULL) 和 (File 4, NULL),并添加 (File 2, DV 2.1) 和 (File 4, DV 4.1) 文件
注意: 在写作时,DVs 不支持 UPDATE 和 MERGE 命令,只支持 DELETE。不过,它们将在 未来支持。
带有 DVs 的表的读取
带有 DVs 的表的读取 — 作者提供的图像
当文件有相关的 DVs 时,扫描将隐式读取 DV,并根据向量过滤匹配的结果行。没有 DVs 时,我们读取单个文件以获得一组结果,而有 DVs 时,我们必须同时读取文件和 DV 才能找到正确的结果集,这可能会影响读取性能。
删除现有的 DVs
带有 DVs 的删除 — 作者提供的图像
在这种情况下,我们将执行一个删除操作,这会影响一个已经有相关 DV 文件的文件。由于我们只是删除,不需要写入任何包含更新行的新文件,只需更新现有 DV。
-
扫描 (File 2, DV 2.1) 以查找最新的行集
-
写一个新的 DV,包含删除旧 DV 的行以及新的行。
-
从事务日志中移除 (File 2, DV 2.1) 并添加 (File 2, DV 2.2)
带有 DVs 的清理和压缩
VACUUM
随着不断的更新,DVs 可能会不断被替换,留下许多旧文件。VACUUM 也会像处理常规文件一样处理 DVs。
OPTIMIZE
在没有 DVs 的情况下,OPTIMIZE 使用 minFileSize 属性选择应该进行压缩的文件。由于某些文件可能有大量行被作废,因此选择那些超出某个阈值的文件也是有意义的。maxDeletedRowsRatio 属性定义了文件被选择进行压缩并删除其 DV 的最大允许比例。
Z-ORDER OPTIMIZE
这个 OPTIMIZE 策略不是幂等的,每次执行时都会尝试生成一组新的有序文件,这意味着这个操作不会生成 DVs,所有以前的 DVs 和文件都标记为待删除。
性能差异
理论已经涵盖,但让我们通过运行一个简单的 DELETE 查询来查看实际中的改进。
为了模拟一个写入密集型应用程序的真实场景,并考虑平均目标文件大小,我将数据集压缩到一个大约 236 MB 和 1000 万条记录的单一文件中,并比较了启用和未启用 DVs 的情况下查询的性能。
两种方法的作业统计信息 — 作者提供的图像
我们首先注意到左侧的字节数增加,这与原始文件的字节数相同,减去刚刚删除的行。有两个文件被添加,总计 9,999,999 行,运行整个操作(包括扫描文件和重写更新后的文件)花费了 27.1 秒。
在右侧,我们可以看到 DVs 的实际应用。没有新增字节,因为没有新文件。我们看到一个包含一行(34 字节)的新 DV 被添加到一个仍包含 9,999,999 个有效行的现有文件中。总体而言,该操作耗时 2.7 秒,相比之前的方法写入速度提高了大约十倍! 通过写入一个耗时 117 毫秒的 DV,我们避免了为了在一个拥有 1000 万行的数据集中删除一行而重新写入整个文件。
总结
在这篇文章中,我们看到 DVs 在写入密集型用例中表现得非常好,尤其是在需要进行大量小的更新或删除时。虽然读取时间可能会受到读取更多文件的影响,但通过自动压缩作业或计划的OPTIMIZE作业,这些时间损失可以得到有效弥补。总的来说,这是写入性能和读取性能之间的权衡,因此在决定启用此功能之前,分析你所运行的工作负载类型至关重要。
如果你希望了解更多关于 Delta Lake 的信息,请务必阅读我之前的文章: