TowardsDataScience-2023-博客中文翻译-三十二-

TowardsDataScience 2023 博客中文翻译(三十二)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

Optuna 下的 MLOps

原文:towardsdatascience.com/mlops-with-optuna-b7c52d931b4b

不要浪费时间,使用 Optuna

Zachary WarnesTowards Data Science Zachary Warnes

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 3 月 1 日

--

使用 DALLE-2 生成,超参数优化(作者照片)

对于那些熟悉繁琐的超参数调优过程的人来说,Optuna 无疑是救星。

使用不同的超参数优化技术调整多种模型的能力真是令人惊叹。

如果你仍在通过网格搜索来调优模型,你需要改变你的方法——你正在损失性能。

这篇文章包含了可以立即实施的现成代码。如果你想自己进行实验,可以快速浏览到文章末尾。不要忘记在整个文章中加载函数。本文将在深入探讨 Optuna 之前,讨论超参数调优的高层次内容。

  • 本文将概述如何使用自定义参数网格和度量标准创建新的研究。

  • 它将展示如何保存和加载研究,并收集最佳的试验和模型。

  • 最后,它将展示如何分叉研究以在更新的搜索空间中继续搜索。

超参数调优

在学习机器学习时,你将面临超参数调优问题,它决定了你优化的模型结构。最常见的方法是网格搜索,即使用参数的排列组合来训练和测试模型。

网格搜索极其低效。无论是在浪费时间还是探索较少的超参数空间方面。

结果是性能更差的模型。

有多种方法可以改善蛮力网格搜索。我详细说明了为什么包括随机在内的不同搜索方法会优于网格搜索。

## 超参数调优 — 总是调整你的模型

不要将免费的性能提升搁置一旁。

towardsdatascience.com

实质上不要使用网格搜索。它分析超参数搜索空间所需的时间太长。

更重要的是如何使用更有效的贝叶斯技术管理你创建的不同模型,例如 ‘树结构 Parzen 估计器’

但你会很快发现你正在迅速创建和保存越来越多的模型。因此,你会发现自己需要跟踪、存储和监控你优化过的不同模型。

Optuna

Optuna 是一个用于调整模型的超参数优化框架。它让你了解超参数如何影响你的模型,并提高你的模型性能。

我之前写过关于如何使用这个库来快速优化具有非常大超参数空间的模型。

## 使用 Optuna 进行超参数优化

如何生成模型的最佳版本。

towardsdatascience.com

有许多采样器可用于调整你的模型。它仍然包含标准的网格搜索和随机搜索模型。但此外,你还可以选择:

  • 树结构 Parzen 估计器(本文使用)

  • 一个准蒙特卡洛采样器

  • 一个交集搜索空间采样器

以及其他六个选项,所有这些选项都更系统地搜索你的超参数空间。

Optuna 中的每个优化都以学习的形式存在。这些学习跟踪超参数优化过程的许多不同组件。它们让你在不同步骤查看性能,查看某些超参数的效果,或从最佳试验中选择模型。

学习模板

模型参数

一个关于研究的问题是固定的参数网格。这是进行研究所需的优化函数的限制。在 Optuna 教程中,你会看到这个函数必须遵循标准格式以跟踪试验。

优化函数不允许用户动态传入不同的模型。它也不允许将不同的参数网格传递给优化函数。

,有一个解决方案允许使用可变的参数网格和模型。这里就引入了 lambda 函数。

通过将我们的优化函数定义为 lambda,我们可以传入多个值。这个 lambda 然后调用一个底层函数。结果是一个更强大、更灵活的学习设置。

动态模型优化

下面我定义了一些函数,这些函数作为底层 Optuna 函数的包装器。这些包装器允许你快速传入不同的模型和参数网格,而无需每次都定义全新的优化函数。

现在,你可以简单地在 python 字典中为不同实验创建不同的参数网格,并传入你可能喜欢的任何模型。

动态 Optuna 学习初始化(作者代码)

在这里,你还可以看到当模型名称未明确传入时,模型将被唯一保存。

由于你可以快速更新搜索空间和模型,研究数量迅速增加。因此,加载、重命名、分叉及其他追踪模型变得更加棘手。

加载模型

Optuna 研究存储在一个 db 文件中,可以使用他们的 load_study 函数加载。这个函数还提供了更改基础优化中使用的采样器的机会。

从本地数据库加载 Optuna 研究(作者代码)

分析研究

在从超参数样本生成大量模型之后,接下来是分析你的结果。

以下是我定义的一些额外功能来帮助你。

有时,最佳模型并不总是表现最好的。由于各种原因,你的目标指标和目标问题可能会增加需要处理的复杂性。

使用下面的函数,你可以获取前 n 个模型进行审查。

从 Optuna 研究中获取最佳试验(作者代码)

在运行研究并确定你的最佳模型之后,你需要将模型投入生产。

为了实现这一点,你可以识别你的理想研究并使用以下函数检索最佳基础模型的参数。

从 Optuna 研究中获取最佳参数(作者代码)

这些最佳试验参数是参数的字典。然后可以使用双星号 '**' python 运算符将其加载到你使用的模型中。

分叉研究

有时你的模型在取得进展,但你可能还没有运行足够长的时间。这对于模型优化来说很简单。但是,对于超参数优化来说,这是一个更困难的问题。

幸运的是,通过有限的调整,你可以加载旧的研究并继续你的搜索。此外,在分析后,你可能会发现你的最佳模型在某个超参数范围内。

使用分叉函数,你可以将你的研究拆分并探索不同的超参数网格。

认为你的学习率还不够低?调整参数网格并继续运行。在继续研究时,基础目标模型会继续优化。

分叉现有的 Optuna 研究(作者代码)

重命名研究

为了整理一些可能会派上用场的其他实用工具,我还创建了一个研究重命名函数。由于有这么多研究,最佳模型可能会在混合中丢失。

使用上面定义的解析研究函数以及下面的重命名研究函数,你可以轻松地在不同模型和参数网格中进行批量搜索。

然后一旦找到一个出色的模型,你可以快速重命名这些模型及其基础数据存储,以跟踪你的进展。

重命名现有的 Optuna 研究和本地数据库(作者代码)

实验

为了展示这种动态优化,我设置了一个简短的实验,你可以重新运行并使用你的数据集进行更改。这个代码旨在通过修改数据集直接使用。之后,你可以调整模型及其各自的超参数。这里的目标是使用这些代码支持我自己的 MLOps 流水线,构建、优化和跟踪许多不同的模型。

使用的数据

使用的数据集是来自 scikit-learn 的开源糖尿病玩具数据集。这个数据集是你安装时自带的。这是一个回归数据集,包含数值变量和数值目标。非常适合展示如何设置这些 Optuna 研究。

设置

你需要为研究指定一个名称和你想要优化的试验数量。在我的简要示例中,我展示了一个轻量级梯度提升模型。这些模型有很多超参数,使得调整变得困难。

花时间审查所使用的参数分布,因为它们突显了你可以在超参数空间中搜索的不同方式。你可以使用各种分布来微调新候选值的采样方式。

运行动态 Optuna 研究(作者代码)

完成超参数搜索后,你可以查看结果。除了我定义的函数,你还可以发现trails_dataframe()函数很有用。这个函数简单地将所有研究细节返回为数据框。

下一步是从最佳试验中加载最佳参数,或者从顶级试验中选择这些参数用于模型。

这是一个简单的过程,使用你模型的set_params()函数。

动态 Optuna 研究的结果展示(作者代码)

持续研究

Optuna 被设置为创建研究的参数网格,并在一系列试验中进行优化。

你可能也做过重新加载你的研究并继续优化它们。但是,如果你已经耗尽了搜索空间,你的结果可能不会有太大改进。

然而,借助我的动态研究设置,你可以加载现有试验,分叉试验,更改参数网格,并继续超参数优化搜索。

不幸的是,你只能更新数值参数分布进行超参数优化。这个问题似乎是 Optuna 当前的一个限制。

但你可以将超参数设置调整到一个完全不同的范围。因此,你可以从中断的地方继续,或完全探索一个新的分布。

分叉并继续优化 Optuna 研究(作者代码)

最终想法

Optuna 是一个强大的工具。顶级数据科学家纷纷使用它,理由充分。

这节省了时间,并且生成了更好的模型。

我写这篇文章的目标是创建一些实用函数,以支持数据科学团队更好的 MLOps。不同模型和不同设置的管理往往容易被忽视。

通过将模型跟踪的任务交给现有工具,你可以为你的团队节省宝贵的时间和精力。

如果你对阅读有关新型数据科学工具和理解机器学习算法的文章感兴趣,可以考虑在 Medium 上关注我。我在文章中总是包含可以应用于你的工作的代码!

如果你对我的写作感兴趣并想直接支持我,请通过以下链接订阅。这个链接确保我会收到你会员费用的一部分。

[## 使用我的推荐链接加入 Medium - Zachary Warnes

阅读 Zachary Warnes 的每一个故事(以及 Medium 上成千上万其他作家的作品)。你的会员费用直接支持…

zjwarnes.medium.com](https://zjwarnes.medium.com/membership?source=post_page-----b7c52d931b4b--------------------------------)

MLX vs MPS vs CUDA:基准测试

原文:towardsdatascience.com/mlx-vs-mps-vs-cuda-a-benchmark-c5737ca6efc9

苹果新 ML 框架 MLX 的首次基准测试

Tristan BilotTowards Data Science Tristan Bilot

·发表于Towards Data Science ·6 分钟阅读·2023 年 12 月 15 日

--

图片由Javier Allegue Barros提供,Unsplash

如果你是 Mac 用户且对深度学习充满热情,你可能曾希望你的 Mac 能够处理那些大型模型,对吧?好吧,猜猜怎么着?苹果刚刚发布了MLX,这是一个在 Apple Silicon 上高效运行 ML 模型的框架。

在 PyTorch 1.12 中引入的MPS 后端已经是一个大胆的步骤,但随着 MLX 的宣布,似乎苹果想要在开源深度学习领域迈出重要的一步。

在这篇文章中,我们将对这些新方法进行实际测试,基于三个不同的 Apple Silicon 芯片和两个支持 CUDA 的 GPU 进行基准测试。通过这样做,我们旨在揭示这些新的 Mac 兼容方法在 2024 年深度学习实验中的实际应用程度。

作为一名以 GNN 为导向的研究人员,我将把基准测试集中在图卷积网络(GCN)模型上。但由于该模型主要由线性层组成,我们的发现即使对那些不专注于 GNN 领域的人也可能具有洞察力。

创建环境

要为 MLX 构建环境,我们必须指定使用 i386 还是 arm 架构。使用 conda 可以通过以下方式完成:

CONDA_SUBDIR=osx-arm64 conda create -n mlx python=3.10 numpy pytorch scipy requests -c conda-forge
conda activate mlx

要检查你的环境是否实际使用 arm,以下命令的输出应该是arm,而不是i386

python -c "import platform; print(platform.processor())"

现在只需使用 pip 安装 MLX,你就可以开始探索了:

pip install mlx

GCN 实现

GCN 模型是一种图神经网络(GNN),它使用邻接矩阵(表示图结构)和节点特征。它通过收集邻居节点的信息来计算节点嵌入。具体来说,每个节点的特征是其邻居特征的*均值。这个*均过程是通过将节点特征与归一化的邻接矩阵相乘来完成的,矩阵经过了节点度的调整。为了学习这一过程,特征首先通过线性层投射到嵌入空间。

在我们的版本中,我们在预处理步骤中像原始论文中一样对邻接矩阵进行归一化。虽然这篇文章不会深入讨论预处理代码,但你可以在这个 GitHub 仓库中找到完整代码:

## GitHub - TristanBilot/mlx-GCN

通过在 GitHub 上创建一个账户,你可以为 TristanBilot/mlx-GCN 的开发做出贡献。

GitHub

现在我们将通过使用 MLX 实现一个 GCN 层和一个 GCN 模型:

import mlx.nn as nn

class GCNLayer(nn.Module):
    def __init__(self, in_features, out_features, bias=True):
        super(GCNLayer, self).__init__()
        self.linear = nn.Linear(in_features, out_features, bias)

    def __call__(self, x, adj):
        x = self.linear(x)
        return adj @ x

class GCN(nn.Module):
    def __init__(self, x_dim, h_dim, out_dim, nb_layers=2, dropout=0.5, bias=True):
        super(GCN, self).__init__()

        layer_sizes = [x_dim] + [h_dim] * nb_layers + [out_dim]
        self.gcn_layers = [
            GCNLayer(in_dim, out_dim, bias)
            for in_dim, out_dim in zip(layer_sizes[:-1], layer_sizes[1:])
        ]
        self.dropout = nn.Dropout(p=dropout)

    def __call__(self, x, adj):
        for layer in self.gcn_layers[:-1]:
            x = nn.relu(layer(x, adj))
            x = self.dropout(x)

        x = self.gcn_layers-1
        return x

一眼看去,MLX 代码与 PyTorch 代码非常相似,有一个显著的不同:在这里,我们将 self.gcn_layers 实例化为一个模块列表,而在 PyTorch 中,通常会使用 nn.Sequential 来实现类似功能。

代码在训练循环中开始变得相当不同:

gcn = GCN(
    x_dim=x.shape[-1],
    h_dim=args.hidden_dim,
    out_dim=args.nb_classes,
    nb_layers=args.nb_layers,
    dropout=args.dropout,
    bias=args.bias,
)
mx.eval(gcn.parameters())

optimizer = optim.Adam(learning_rate=args.lr)
loss_and_grad_fn = nn.value_and_grad(gcn, forward_fn)

# Training loop
for epoch in range(args.epochs):

    # Loss
    (loss, y_hat), grads = loss_and_grad_fn(
        gcn, x, adj, y, train_mask, args.weight_decay
    )
    optimizer.update(gcn, grads)
    mx.eval(gcn.parameters(), optimizer.state)

    # Validation
    val_loss = loss_fn(y_hat[val_mask], y[val_mask])
    val_acc = eval_fn(y_hat[val_mask], y[val_mask])

一眼就能看到 mx.eval() 的使用。在 MLX 中,计算是惰性的,这意味着 eval() 常用于在更新后实际计算新模型参数。另一个关键函数 nn.value_and_grad() 生成一个函数,用于计算相对于参数的损失。第一个参数是包含当前参数的模型,第二个参数是用于前向传递和损失计算的可调用函数。它返回的函数与前向函数(在此情况下为 forward_fn)具有相同的参数。我们可以这样定义这个函数:

def forward_fn(gcn, x, adj, y, train_mask, weight_decay):
    y_hat = gcn(x, adj)
    loss = loss_fn(y_hat[train_mask], y[train_mask], weight_decay, gcn.parameters())
    return loss, y_hat

这仅仅包括计算前向传递和计算损失。loss_fn()eval_fn() 定义如下:

def loss_fn(y_hat, y, weight_decay=0.0, parameters=None):
    l = mx.mean(nn.losses.cross_entropy(y_hat, y))

    if weight_decay != 0.0:
        assert parameters != None, "Model parameters missing for L2 reg."

        l2_reg = sum(mx.sum(p[1] ** 2) for p in tree_flatten(parameters)).sqrt()
        return l + weight_decay * l2_reg

    return l

def eval_fn(x, y):
    return mx.mean(mx.argmax(x, axis=1) == y)

你可能会注意到损失函数看起来相当复杂,但它实际上是计算预测与标签之间的交叉熵,并包括 L2 正则化。由于 L2 正则化还不是一个内置功能,我已经手动实现了它。

一个很酷的特点是,不再需要像在 PyTorch 中那样显式地将对象分配到特定的设备上,如使用 .cuda().to(device)。得益于苹果芯片的统一内存架构,所有变量都共存于同一空间,消除了 CPU 和 GPU 之间缓慢的数据传输,也解决了那些与设备不匹配相关的运行时错误。

基准

在我们的基准测试中,我们将 MLX 与 MPS、CPU 和 GPU 设备进行比较,使用的是 PyTorch 实现。我们的测试*台是一个 2 层的 GCN 模型,应用于 Cora 数据集,该数据集包括 2708 个节点和 5429 条边。

对于 MLX、MPS 和 CPU 测试,我们对 M1 ProM2 UltraM3 Max 进行了基准测试。同时,GPU 基准测试在两款 NVIDIA Tesla 模型上进行:V100 PCIeV100 NVLINK

作者图片:GCN 在 MLX 和其他后端上的基准测试(以毫秒为单位)

MPS:比 M1 Pro 上的 CPU 快超过 2 倍,表现不错。在其他两款芯片上,与 CPU 相比,改进幅度在 30% 到 50% 之间。

MLX:在 M1 Pro 上比 MPS 快 2.34 倍。在 M2 Ultra 上相比 MPS 提升了 24%。不过,在 M3 Pro 上 MPS 和 MLX 之间没有实质性改进。

CUDA V100 PCIe & NVLINK:比 M3 Max with MLX 快 23% 和 34%,这确实很厉害!

与 CPU 和 MPS 相比,MLX 突显出其作为游戏改变者的潜力,甚至接* TESLA V100 的性能。这一初步基准测试突出了 MLX 在成为流行的 Mac 基础深度学习框架方面的巨大潜力。值得注意的是,MLX 刚刚公开发布,我们可以期待未来几年开源社区带来的进一步改进。我们也期待在不久的将来,出现更强大的 Apple Silicon 芯片,将 MLX 的性能提升到一个全新的水*。

总结一下

酷炫功能:

  • 我们现在可以通过充分利用 Apple Silicon 的强大性能在本地运行深度学习模型。

  • 语法与 torch 非常相似,受到了 Jax 的一些启发。

  • 不再需要设备,一切都存在于统一内存中!

缺少的功能:

  • 这个框架 非常 年轻,许多功能尚未完善。特别是对于 Graph ML,目前没有所有稀疏操作和散布 API,这使得在 MLX 上构建 Message Passing GNNs 变得复杂。

  • 作为一个新项目,目前 MLX 的文档和社区讨论相对有限。

总之,MLX 的发布给人留下了意外的深刻印象,并展示了严肃的潜力。我相信这个框架可能会成为日常研究实验的基础工具。我们也期待更多的实验,因为 GCN 测试主要展示了 MLX 在基本线性层上的表现。更全面的测试可能会揭示它的全部能力。

感谢阅读!

模型使用:推理发生在训练之后,而不是训练期间

原文:towardsdatascience.com/model-employment-the-inference-comes-after-training-not-during-6129efdf8e90?source=collection_archive---------16-----------------------#2023-04-06

训练和使用模型是两个独立的阶段

Valeria Fonseca DiazTowards Data Science Valeria Fonseca Diaz

·

关注 发布于 Towards Data Science ·5 分钟阅读·2023 年 4 月 6 日

--

(图像由作者提供)构建模型与使用模型

在完成模型训练阶段后,整个建模流程的新阶段会被激活。机器学习社区中最常见的一个阶段是模型部署。对于那些不熟悉这一概念的人来说,这基本上指的是将模型放置在某个地方。如果你曾阅读这些帖子关于一般的机器学习话题,你可能会记得将模型比作汽车的类比。当一辆车被制造和组装完成后,“部署”可能例如是将其送到汽车经销商店。这个整个流程中的另一个阶段是模型应用。

“模型应用”?是的。这仅指与我们模型的实用性相关的一切。这包括进行预测、确定重要参数、解释特征等。虽然这是一种定义模型实际用途的方式,但所有这些的技术术语是推论。模型应用可能不一定紧随其后。它始终取决于模型的实际用途。然而,它必须且应当总是发生在模型训练之后,而不是训练过程中。尽管模型部署按定义要求将模型放置在某处,但它几乎不会与训练步骤混淆。这不幸的是,模型应用有时会影响正确的推论。

机器学习社区的方法

几十年前,当机器学习社区逐渐成型时,这些概念被很好地消化,并且很容易被那些投入模型训练和预测实践的人应用到实际中。直到今天,我们不仅在全球范围内拥有无限类型的模型为我们提供不同的服务,而且这些模型的复杂程度已经增加到我们非常接*构建那种像我们的朋友 ChatGPT 一样进化我们物种的机器。这种复杂程度不允许机器学习流程混合步骤,也不允许负责人混淆不同阶段。多亏了这些井然有序的阶段划分,我们能够做出更准确的预测,并信任我们模型得出的推论。

经典统计学社区的方法

如果你曾修过任何经典统计学课程,你就会处理许多不同类型的模型,以测试一些假设并找出某个响应变量相关特征的所谓显著差异。这是费舍尔经典统计范式的遗产。在这种实践中,模型训练阶段完全与模型应用阶段混淆在一起。是的,我们确实会根据输入特征的显著性水*(应用)来选择这些特征(训练),而且都是使用相同的数据。

(Image by author) 复杂性和结构的范围

整个范围的中间部分

随着复杂性水*的简化,现实已经向我们展示了模型训练与使用之间的分离变得越来越模糊。许多数据科学家在训练和使用模型时面对不同的复杂性水*。有些模型可能非常容易训练但难以使用,反之亦然。如果我们审计低复杂度的机器学习项目,可能会发现没有单一对象保存模型,没有明确的部署环境,并且没有特别用于解释或显著性测试的独特阶段。我们可能会发现许多模型被遗忘在文件夹中,作为多行代码散落着报告感兴趣的数字。那么,这会有多大问题呢?

(Image by author) 一种写作工具,完成品与散落的零件

我们的模型需要成为完整的工具

是的,就像一支简单的铅笔或一个复杂的设备。统计模型,从简单的线性回归到深度学习模型,都是工具。我们构建这些工具是有目的的。这个目的可能因领域或应用而异,但最终,它们是为了那个目的而被使用的。当使用阶段没有与训练分开时,我们最终得到的模型就像是尚未组装的散落零件。当我们基于一个未组装的工具报告模型的使用或推断时,误导性推断的风险相当高。这就像用拆开的铅笔写字或开拆开的车一样。这有什么意义?为什么要冒这个风险?

顺畅过渡到适当的模型使用

成功的模型使用通常取决于至少某种形式的 模型持久性。即使你的模型仅在本地使用,也总会有至少一个用户:研究人员/数据科学家或任何将使用模型报告结果的人。因为已经有至少一个用户,所以必须至少 保存模型 为某种可用格式。一旦按下保存按钮或运行保存功能,瞧!你的模型现在作为一个完整的工具存在。

在你准备好使用模型之前的任何时刻都不应进行。当你准备好时,这里有一些有益的/必要的实践:

  • 将模型训练和模型使用的脚本分开。显著性测试报告应与模型估计分开存在于不同的脚本或软件文件中。

  • 模型使用的方法不应用于模型训练。 就像测试集中的预测性能不用于调整模型参数一样,显著性测试也不应在同一训练数据上用于选择输入特征。

  • 将模型训练结果与模型使用结果分别报告。 因为它们是不同的阶段,所以在发布或报告结果时,这些阶段也应该分别报告。

为所有级别提供更好的模型使用

我向 ChatGPT 询问了模型训练后的模型使用情况,它表示通常来说这是正确的。那么,这难道不是一成不变的事实吗?其实,机器人提出了一个很好的观点。在在线学习中,模型的使用会产生反馈来及时更新模型,因此任务会持续互动。“没错!”但请注意,通过反馈连接并不等同于被混淆。在在线学习中,特定的模型训练和使用的相互关系本身就是一个完整的建模方案。即使在这里,更新阶段、预测阶段和反馈流程都有明确的定义和方法。因此,概念上的分离仍然是有效的。

看待我们的模型及其相关实践时,模型训练与模型使用的区别归结为组织和结构,而非建模技能。一旦我们能够分离建模流程的各个阶段,我们的模型就应该能够持续存在并提供可靠的推断。这难道不是我们的终极目标吗?

让我们保持良好的结构!

时间序列预测中的模型评估

原文:towardsdatascience.com/model-evaluation-in-time-series-forecasting-ae41993e267c

介绍使用 Skforecast 库进行时间序列的回测

Javier FernandezTowards Data Science Javier Fernandez

·发表于 Towards Data Science ·6 min read·2023 年 3 月 5 日

--

图片来源:LukasPexels

时间序列预测包括基于历史时间数据进行预测,以驱动广泛应用中的未来战略决策。

在评估模型时,我们将数据分为训练集和测试集。训练集用于训练模型并确定最佳超参数,而测试集用于评估模型。为了对模型性能进行更全面的评估,通常会使用 交叉验证。交叉验证是一种重采样方法,使用不同的数据集对模型进行多次迭代测试和训练。

然而,直接在时间序列数据上实施交叉验证是不可能的,因为它忽略了观察之间的时间成分。因此,本文介绍了评估时间序列模型的不同方法,这些方法被称为 回测

1. 介绍

回测是建模中使用的一个术语,指的是利用现有历史数据评估模型。它涉及选择多个训练集和测试集,逐步向前推进时间。回测的主要思想类似于交叉验证,但回测考虑了数据的时间成分。这种方法使我们能够 (1) 评估和可视化模型误差如何随时间发展,(2) 估计模型误差的方差。

在生产环境中,通常的做法是首先使用回测方法确定最佳参数,然后用可用数据重新训练模型。但是,这种重新训练不一定需要使用所有可用数据或每次有新数据时都进行。根据我们的策略,我们可以选择不同的回测方法。

1. 具有重新拟合和增加训练样本量的回测

模型在顺序增加的训练集上进行测试,始终保持固定的起点,并使用所有可用数据。在这种方法中,有一个固定的起点,训练集的大小在每次迭代中增加,如图 1 所示。

图 1。时间序列回测图示,初始训练样本量为十个观测值,预测范围为 3 步,每次迭代时进行重新训练。参考Skforecast [1]

2. 具有重新拟合和固定训练样本量的回测

这种方法与前一种方法类似,只是它滚动了预测的起点。因此,训练集的大小保持不变,如图 2 所示。这种方法可以被认为是一种类似于交叉验证技术的时间序列方法。

与之前的方法相比,这种方法更具成本效益,因为每次迭代中训练集的大小保持不变。它还允许根据提前时间分布错误,并使错误度量对任何单一来源的特殊事件不那么敏感[2]。这种方法有趣的一个例子是,当历史数据中发生了事件或“异常”时期(例如 COVID)时。

图 2时间序列回测图示,初始训练样本量为十个观测值,预测范围为 3 步,训练集大小保持不变。 参考Skforecast [1]

3. 无重新拟合的回测

最后一种回测方法包括使用初始训练集训练模型,并顺序评估模型而不进行更新。这种策略的优点在于速度较快,因为模型仅训练一次。然而,该模型未纳入最新的数据,因此可能会随着时间的推移而失去预测能力。

如果需要对系统中新进入的数据进行高频预测,这种方法非常有趣。

图 3时间序列回测图示,初始训练规模为十个观察值,预测范围为 3 步,每次迭代不重新训练。 参考: Skforecast [1].

2. 实现

这里是使用 Skforecast 库进行回测的实现。Skforecast 是一个 Python 库,简化了使用 scikit-learn 回归器作为多步预测器的过程。它还适用于任何与 scikit-learn API 兼容的回归器(如 pipelines、CatBoost、LightGBM、XGBoost、Ranger 等)。

出于测试目的,我们使用了在GitHub MIT 许可证下公开可用的 h2o 数据集,其数据从 1991-07-01 到 2008-06-01,每月更新。

图 4数据集的可视化,其中蓝色数据用于训练,橙色数据用于测试。 参考: 作者提供的图像。

下面是三种描述的回测方法,其中使用随机森林回归器作为自回归。

在查看实现时,回测方法之间的区别在于以下参数:

  • initial_train_size: 初始训练分割中的样本数量。

  • fixed_train_size: 如果True,则训练规模不会增加,但在每次迭代中按“步数”移动。

  • refit: 是否在每次迭代中重新拟合预测模型。

  • steps: 预测的步数。

1. 使用重新拟合和增加训练规模的回测

模型首先以 2002-01-01 为截止日期进行训练,然后依次将十个新数据添加到训练中。此过程重复进行,直到整个系列运行完毕。

要设置此方法,将fixed_train_sizerefit参数分别设置为FalseTrue

如观察到的那样,训练集随着时间的推移而增加,而测试集则保持不变。

2. 使用重新拟合和固定训练规模的回测

与使用重新拟合和增加训练规模的回测方法类似,该模型首先以 2002-01-01 为截止日期进行训练,然后依次将十个新数据添加到训练中。然而,在这种方法中,规模保持不变,这意味着训练集和测试集的大小始终相同。

要设置此方法,将fixed_train_sizerefit参数都设置为True

3. 无重新拟合的回测

与使用重新拟合和增加训练规模的回测方法类似,该模型首先以 2002-01-01 为截止日期进行训练。然而,训练集随着时间的推移不会变化,而测试集在每次迭代中移动十步。

要设置此方法,将fixed_train_sizerefit参数都设置为False

这种方法不仅可以用于基于 sklearn 的模型,还可以用于其他模型,如SARIMAX

3. 结论

在这篇文章中,我们讨论了时间序列预测模型评估的不同方法,称为回测。主要的要点是:

  • 主要有三种回测方法,其主要区别在于是否重新拟合、训练规模以及预测的来源。

  • 回测方法应根据生产场景而定。

  • 由于实施和计算时间可能昂贵,尽量使用已优化的 Python 库,如 Skforecast。

如果你喜欢这篇文章,请考虑 订阅。你将获得对我所有内容的访问权限,并能阅读 Medium 上其他精彩创作者的每一篇文章!

参考文献

## 创建时间序列预测的 ARIMA 模型

在 AirPassengers 数据集中介绍和实现 ARIMA 模型

towardsdatascience.com ## 基于趋势和季节性成分的时间序列预测

分析时间序列的趋势和季节性,分解时间序列并实现预测模型

towardsdatascience.com

[1] Skforecast,Skforecast: 使用 Python 和 Scikit-learn 进行时间序列预测

[2] Elsevier,预测准确性的样本外测试:分析与综述

[3] H2O AI Cloud,回测

[4] Machine Learning Mastery,回测 ML 模型时间序列预测

模型可解释性,再次探讨:SHAP 及其他

原文:towardsdatascience.com/model-explainability-revisited-shap-and-beyond-1c6ee85b294?source=collection_archive---------9-----------------------#2023-09-07

TDS EditorsTowards Data Science TDS Editors

·

关注 发表在 Towards Data Science · 作为 Newsletter 发送 · 3 min 阅读 · 2023 年 9 月 7 日

--

最*几个月,大型语言模型的迅速崛起主导了关于人工智能的讨论——这是可以理解的,因为大型语言模型的创新性以及它们迅速融入数据科学和机器学习专业人士的日常工作流程中。

然而,对模型性能及其带来的风险的长期关注依然至关重要,而可解释性正是这些问题的核心:模型是如何以及为何给出它们的预测?黑箱中究竟隐藏了什么?

本周,我们将回到模型解释性的主题,介绍几篇*期文章,这些文章深入探讨了其复杂性,并提供了实践者可以尝试的实用方法。祝学习愉快!

  • 在任何解释性挑战的核心问题是,你的数据中哪些特征对模型的预测贡献最大。Khouloud El AlamiSHAP 特征重要性分析介绍 是一个适合初学者的资源,基于作者在 Spotify 的研究项目。

  • 如果你过去已经使用过 SHAP,并且希望扩展你的工具集,Conor O'Sullivan 提供了一个 处理更专业用例的实践指南,特别是如何为分类问题显示 SHAP 图表以及如何汇总多类目标的 SHAP 值。

  • 想要了解模型解释性所带来的新视角,不要错过 Diksha Sen Chaudhury 最*的文章 一个将医疗数据和机器学习结合的项目。Diksha 的目标是展示如何使用 SHAP 让模型不仅具有可解释性,还对那些希望将结果与医学文献中的发现进行基准对比的研究人员有用。

图片由 Alina Kovalchuk 拍摄,来源于 Unsplash

  • 正如 Vegard Flovik 所言:“对于那些安全至关重要的重资产行业应用,错误可能导致灾难性后果,缺乏透明度可能是采纳的一个主要障碍。” 为了填补这一空白,Vegard 提供了一个 详尽的开源 Iguanas 框架指南,并展示了如何利用其自动化规则生成的功能来增强解释性。

  • 虽然 SHAP 值在许多现实世界场景中证明是有益的,但它们也有局限性。Samuele Mazzanti提醒不要过分重视特征重要性(文字游戏!),并建议同等关注错误贡献,因为“一个特征的重要性并不意味着它对模型有益。”

我们知道九月初是你们许多人一年中最忙碌的时段,但如果你有更多时间,以下是本周我们其他推荐阅读的绝对不容错过的文章:

  • 如果你现在正在参加数据科学训练营,或者考虑将来参加一个,Alexandra Oberemok的全面指南以充分利用这一经历是不容错过的必读之作。

  • 跑者们,这篇是为你们准备的:barrysmyth的新深度探讨研究马拉松数据以评估不同策略来优化你的表现。

  • 在他的 TDS 首篇文章中,Christian Burke带我们深入了解一个创新的 MOMA 生成 AI 艺术项目,他在其中扮演了关键角色。

  • Olga Chernytska分享了她出色的“构建更好的 ML 系统”系列中的新一篇,这次专注于基线、指标和测试集。

  • 不确定如何处理缺失数据?Miriam Santos提供了一个关于这个长期问题的一站式资源,并解释了如何在实际数据集中识别和标记缺失值。

  • 如果你想深入了解详细的技术解释,Antonieta Mastrogiuseppe对梯度下降算法的概述清晰且执行良好。

感谢你对我们作者工作的支持!如果你喜欢你在 TDS 上阅读的文章,考虑成为 Medium 会员 —— 这将解锁我们整个档案(以及 Medium 上的所有其他文章)。

无模型强化学习在化学过程开发中的应用

原文:towardsdatascience.com/model-free-reinforcement-learning-for-chemical-process-development-67318da35861?source=collection_archive---------12-----------------------#2023-07-11

智能化化学系统

迈向化学过程代理。

Georgi TancevTowards Data Science Georgi Tancev

·

关注 发表在 Towards Data Science · 5 分钟阅读 · 2023 年 7 月 11 日

--

照片由 Alex KondratievUnsplash 提供

介绍

过程开发设计优化控制化学工程中的主要职责之一。具体来说,范围在于寻找最佳配方或合适的设备配置或过程参数(通过实验室实验),以最大化某些目标(例如,产量或吞吐量),同时遵守潜在的约束条件(例如,输入浓度、流速、反应器体积或溶剂的沸点)。通过自动化这些任务,例如,通过实验室机器人,可以节省大量的人工劳动。

最*在强化学习(RL)方面的进展明确表明,智能体可以掌握复杂任务玩各种游戏,甚至发现更高效的(数学)程序,例如矩阵运算排序。借助实验或量子模拟获得的动力学参数,智能体可以在虚拟环境中找到最佳配置和合成配方。然而,与凸优化不同的是,该算法/模型产生动态操作条件,并可以直接用于过程控制。根据方法的样本效率,即收敛所需的交互次数,这也可以通过机器人直接在实验室中完成。显然,这可能会在长期内导致过程开发的自动化。

强化学习的基础、应用于化学过程的优缺点以及开放问题最*已被综述。与此同时,发现了若干有趣的过程工程应用。其中一项研究尝试使用REINFORCE优化批量生物过程。另一项研究利用Q-learning进行过程优化,同时试图强制执行约束。其他研究尝试通过优势演员-评论家调度化学生产。以下文章的范围是通过*端策略优化(PPO)来阐述这一点,以对乙酰氨基酚的合成为例。

问题定义

我们有一个软件,所谓的代理。这个代理处于一个具有许多不同状态(如成分浓度、体积、温度)的环境中,在其中可以执行某些化学操作,即动作。这些动作包括剂量 A、增加/减少进出流量、增加/减少温度等。随着代理执行这些动作,它会转变为新的状态。

例如,对乙酰氨基酚(PC) 是从 p-氨基酚(AP)乙酸酐(AA) 合成的,如图 1a 所示。在已知的动力学下,这个过程可以建模,并代表了环境,例如,在图 1b 所示的连续搅拌反应器(CSTR)中。

图 1:(a) 感兴趣的反应(©Georgi Tancev)。(b) 连续搅拌反应器的示意图,包括进/出流量、输入浓度、反应器浓度和体积(©Georgi Tancev)。

在每个状态下,代理选择一组动作,并且一个微分方程(表示虚拟环境)在固定时间间隔内进行数值积分,结果是一个新状态。然后,代理收到一个与合成的对乙酰氨基酚量成正比的奖励。换句话说,代理玩一个游戏,需要在给定的时间段内(大约 2000 次转换或 170 小时)尽可能多地生产对乙酰氨基酚。

在以下研究中,反应器本质上是一个实验室规模的 CSTR ,最大体积为 0.45 L。最大流量为 2.5 mL/min,最大输入浓度为 3.3 mol/L。如果体积超出限制,代理将收到与超出体积成正比的惩罚;如果超过临界阈值,剧集将立即结束。温度保持在 105 °C。

方法

(深度)强化学习中,目标是学习一个策略(即状态到动作的映射),以最大化累计 (折扣)奖励总和。在每个状态下执行一个动作,然后得到特定的奖励。PPO 是信任区域策略优化(TRPO)的启发式变体。两者都是所谓的演员-评论员 方法,利用函数逼*。演员-评论员方法由两个组件组成;演员本质上是状态到动作的映射,由一个(深度)神经网络参数化。它以状态(即观察)作为输入,并提供该状态下应执行的最佳动作。以下,动作分布由连续伯努利分布表示。该分布提供范围在[0, 1]之间的值,然后需要乘以每个动作的最大可能设置。这样的随机策略允许探索与利用之间的权衡。

“最佳”动作的选择由评论员指导,该评论员通过另一神经网络估计每个状态下每个动作的价值(或优势),并在训练过程中更新这些估计。奖励高的动作受到鼓励,而奖励低或甚至惩罚的动作则相反。适当的架构超参数(例如,优化)取自文献

结果与讨论

图 2 说明了不同超参数下的*均奖励随训练轮次变化的情况,基准案例为γ = 0.995,λ = 0.97,以及ε = 0.20。该图中的*均奖励直接与生产的对乙酰氨基酚的量相关。

图 2: 学习曲线(©Georgi Tancev)。

最优策略,即收敛后的策略,如图 3 所示。

图 3: 随时间变化的状态和动作(经过 2000 轮次)。图示显示了浓度、体积、入口浓度和流量随操作时间的变化(©Georgi Tancev)。

可以看到,代理通过将输出流(Q₂)保持在低于输入流(Q₁)的水*来填充 CSTR。至于输入浓度,从一开始就保持在最大值。换句话说,代理通过与环境的互动学会了如何最好地合成 PA。由于无模型的强化学习方案样本效率低下,这一策略是在计算机上 elaborated 的。借助更高效的(即,离线策略)方案和基于模型的强化学习,化学机器人未来可以在实验室中独立开发合成方法。然而,这也需要安全探索

结论与展望

这项简要研究表明,强化学习(RL)在化学过程开发中确实具有潜力,值得进一步探索。这个潜力很大,因为繁琐的实验室工作可以由机器人处理。然而,这里仍然存在许多未解答的问题。无模型的强化学习(model-free RL)样本效率低,而基于模型的和/或离线策略的方法(例如,软演员-评论家)会更受青睐。此外,PPO 经常报告存在不稳定性。最后,在低温下几乎没有反应发生,因此没有反馈形式的奖励(即稀疏奖励),因此必须为此找到合适的解决方案。

使用 TensorFlow 进行模型优化

原文:towardsdatascience.com/model-optimization-with-tensorflow-629342d1a96f

MLOps

使用量化和剪枝减少模型的延迟、存储和推理成本

Michał OleszakTowards Data Science Michał Oleszak

·发表于 Towards Data Science ·9 分钟阅读·2023 年 4 月 17 日

--

在过去几年中,机器学习模型经历了两种看似对立的趋势。一方面,模型往往变得越来越大,最终形成了如今流行的: 大型语言模型。Nvidia 的 Megatron-Turing 自然语言生成模型拥有 5300 亿个参数!另一方面,这些模型正在被部署到越来越小的设备上,如智能手表或无人机,这些设备的内存和计算能力自然受到其尺寸的限制。

我们如何将越来越大的模型压缩到越来越小的设备中?答案是模型优化:压缩模型的大小并减少其延迟的过程。在这篇文章中,我们将了解其工作原理以及如何在 TensorFlow 中实现两种流行的模型优化方法——量化和剪枝。

基线模型

在我们跳到模型优化技术之前,我们需要一个要优化的玩具模型。让我们训练一个简单的二分类器,以区分巴黎的两个著名地标:埃菲尔铁塔和蒙娜丽莎,由 Google 的游戏“Quick, Draw!” 的玩家绘制。 QuickDraw 数据集包含 28x28 的灰度图像。

来自 Quickdraw 数据集的示例:埃菲尔铁塔(上)和蒙娜丽莎(下)。

让我们训练一个简单的卷积网络来分类这两个地标。

def get_model():
    return tf.keras.Sequential([
        tf.keras.layers.InputLayer(input_shape=(28, 28)),
        tf.keras.layers.Reshape(target_shape=(28, 28, 1)),
        tf.keras.layers.Conv2D(
          filters=12, kernel_size=(3, 3), activation="relu"
        ),
        tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
        tf.keras.layers.Conv2D(
          filters=24, kernel_size=(3, 3), activation="relu"
        ),
        tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(1, activation="sigmoid")
    ])

model_baseline = get_model()

model_baseline.compile(
    optimizer="adam",
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

model_baseline.fit(
    x_train, 
    y_train, 
    epochs=1,
)

我们现在将模型保存为 TensorFlow Lite 格式。与传统的 .h5 文件相比,这是一种更小、更高效的文件格式,专门为移动和边缘部署设计。

converter = tf.lite.TFLiteConverter.from_keras_model(model_baseline)
model_baseline_tflite = converter.convert()

with open("model_baseline.tflite", "wb") as f:
    f.write(model_baseline_tflite)

为了评估我们的模型的大小和准确性,我们需要两个简单的工具函数。首先,evaluate_tflite_model() 设置一个 TF Lite 解释器,将测试样本传递给保存的 TF Lite 模型,并计算其预测的准确性。其次,get_gzipped_model_size() 创建一个 .tflite 模型文件的临时压缩版本,以进一步压缩它,并返回其在磁盘上的字节大小。我们这里使用的这两个函数的实现基于 Coursera 的 机器学习建模管道生产课程 的类似工具。

def evaluate_tflite_model(filename, x_test, y_test):
    interpreter = tf.lite.Interpreter(model_path=filename)
    interpreter.allocate_tensors()
    input_index = interpreter.get_input_details()[0]["index"]
    output_index = interpreter.get_output_details()[0]["index"]
    y_pred = []
    for test_image in x_test:
        test_image = np.expand_dims(test_image, axis=0).astype(np.float32)
        interpreter.set_tensor(input_index, test_image)
        interpreter.invoke()
        output = interpreter.tensor(output_index)
        y_pred.append(output()[0][0] >= 0.5)
    return (y_pred == np.array(y_test)).mean()

def get_gzipped_model_size(file):
    _, zipped_file = tempfile.mkstemp(".zip")
    with zipfile.ZipFile(
      zipped_file, "w", compression=zipfile.ZIP_DEFLATED
    ) as f:
        f.write(file)
    return os.path.getsize(zipped_file)

让我们从未优化的模型中获取一些基准指标。

model_baseline_acc = evaluate_tflite_model(
    "model_baseline.tflite", x_test, y_test
)
model_baseline_size = get_gzipped_model_size("model_baseline.tflite")

print(f"Baseline accuracy: {model_baseline_acc}")
print(f"Baseline size: {model_baseline_size}")
Baseline accuracy: 0.9852449095827994
Baseline size: 14303

该模型的准确率为 98.524%,同时占用超过 14k 字节的空间。让我们看看是否可以在不牺牲太多准确性的情况下压缩其大小。

量化

我们将首先讨论的优化技术是模型量化。它的目标是减少存储模型参数的对象的精度。

量化减少了存储模型参数的对象的精度。

默认情况下,TensorFlow 将模型的偏差、权重和激活存储为 32 位浮点数。通过 np.finfo(np.float32).max 快速检查会告诉你,这种数据类型允许存储的值最大为 3³⁸。我们真的需要这么大的值吗?

通常,大多数模型权重和激活值离零并不远;否则,梯度会爆炸,阻止我们首先训练模型。将这些 float32 转换为更轻量的数据结构,例如 int8,可以大大减少模型的大小,同时不一定影响其准确性。

量化非常方便使用,因为它操作的是已经训练好的模型,只需转换其内部数据结构。在 TensorFlow 中,这可以通过在转换器中设置 optimizations 属性来完成。

converter = tf.lite.TFLiteConverter.from_keras_model(model_baseline)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
model_quantized_tflite = converter.convert()

with open("model_quantized.tflite", "wb") as f:
    f.write(model_quantized_tflite)

这实现了称为动态范围量化的默认量化变体。在大多数情况下,这是一个很好的起点。还有其他更高级的策略,例如完全整数量化,它试图估计每个模型张量中的值范围,但要做到这一点,它需要一个代表性数据集来校准其估计值。

我们保持默认选项,检查量化模型的大小和准确性。

model_quantized_acc = evaluate_tflite_model(
    "model_quantized.tflite", x_test, y_test
)
model_quantized_size = get_gzipped_model_size("model_quantized.tflite")

print(f"Quantized accuracy: {model_quantized_acc}")
print(f"Quantized size: {model_quantized_size}")
Quantized accuracy: 0.98526270824434
Quantized size: 7483

模型的大小几乎减少了一半,从 14k 多字节降至 7k 多字节!与此同时,准确率没有下降。实际上,准确率甚至从 98.524% 微增至 98.526%。这是一个罕见的情况,因为通常量化结果会导致准确率略有下降。

剪枝

另一种流行的模型优化技术是权重剪枝。在大型神经网络中,每个权重不太可能对模型性能发挥关键作用。识别出不太重要的权重并将其从网络中移除,可以减少存储模型所需的空间,并在推理过程中节省浮点乘法,从而减少延迟。

剪枝识别并移除模型中不重要的权重,减少模型大小和推理时间。

识别不重要权重的最简单方法是通过其幅度。值接*零的权重最不可能对网络的输出有较大贡献,是剪枝的最佳候选者。

密集网络(左)和剪枝两个节点后的稀疏网络(右)。

不幸的是,在训练后的网络中将一些权重设置为零会打断网络内部的信息流,这通常会导致准确率急剧下降。显而易见的补救方法是剪枝后重新训练模型,但这证明是一个具有挑战性的任务。

相反,剪枝和重新训练可以同时进行。这使我们能够明确地教导模型不要使用一些网络的权重。

要在 TensorFlow 中实现再训练剪枝,我们需要定义一个剪枝计划。该计划在再训练过程中管理两个过程:

  • 在整个训练过程中强制实施的稀疏性程度;

  • 在(哪些训练步骤中)强制实施它。

实践中被证明有效的稀疏性增加率的流行选择是多项式衰减函数。它将在前期训练步骤中急剧增加稀疏性,然后在后期步骤中减缓增加速度。

稀疏性增加率。

在这里,我们将从训练开始时要求 50% 的权重为零,这一比例会在训练结束时增加到 80%。我们还将立即在训练开始时引入稀疏性,整个过程将持续所有训练步骤。

batch_size = 128
epochs = 2
validation_split = 0.1

x_train_size = x_train.shape[0] * (1 - validation_split)
end_step = np.ceil(x_train_size / batch_size).astype(np.int32) * epochs

pruning_params = {
    "pruning_schedule": tfmot.sparsity.keras.PolynomialDecay(
        initial_sparsity=0.50,
        final_sparsity=0.80,
        begin_step=0,
        end_step=end_step,
    )
}

设置好计划后,我们将其与训练好的基线模型一起传递给 prune_low_magnitude 方法。接下来,我们重新编译并重新训练模型,传递 UpdatePruningStep 回调。每处理完一批训练数据后,该回调会被调用,并更新剪枝步骤,以保持与预定义计划的一致性。它还在待剪枝的层周围添加了包装器,因此在用 strip_pruning 方法重新训练后,我们需要去掉这些包装器,以获得原始模型架构。

model_pruned = tfmot.sparsity.keras.prune_low_magnitude(
  model_baseline, 
  **pruning_params,
)

model_pruned.compile(
    optimizer="adam",
    loss="binary_crossentropy",
    metrics=["accuracy"]
)

model_pruned.fit(
    x_train, 
    y_train, 
    epochs=epochs,
    validation_split=validation_split,
    callbacks=[tfmot.sparsity.keras.UpdatePruningStep()],
)

model_pruned = tfmot.sparsity.keras.strip_pruning(model_pruned)

现在我们可以将模型转换为 TFLite 格式,并检查其大小和准确性,如之前所做的那样。

converter = tf.lite.TFLiteConverter.from_keras_model(model_pruned)
model_pruned_tflite = converter.convert()

with open("model_pruned.tflite", "wb") as f:
    f.write(model_pruned_tflite)

model_pruned_acc = evaluate_tflite_model(
  "model_pruned.tflite", x_test, y_test
)
model_pruned_size = get_gzipped_model_size("model_pruned.tflite")

print(f"Pruned accuracy: {model_pruned_acc}")
print(f"Pruned size: {model_pruned_size}")
Pruned accuracy: 0.9840168019364943
Pruned size: 5862

剪枝使我们的模型比量化压缩得更多:从 14k 减少到不到 6k 字节。然而,准确率略有下降;从基线模型的 98.524%降至剪枝版本的 98.402%。

量化与剪枝结合在一起

由于量化和剪枝是独立且以不同方式工作的,因此没有任何东西禁止我们同时使用这两种方法。我们可以剪枝一些模型的权重,然后对剩下的权重进行量化,以实现更强的压缩效果。

我们已经有了剪枝后的模型,所以我们需要做的就是在转换为 TFLite 时进行量化,就像之前一样。

converter = tf.lite.TFLiteConverter.from_keras_model(model_pruned)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
model_pruned_quantized_tflite = converter.convert()

with open("model_pruned_quantized.tflite", "wb") as f:
    f.write(model_pruned_quantized_tflite)

model_pruned_quantized_acc = evaluate_tflite_model(
  "model_pruned_quantized.tflite", x_test, y_test
)
model_pruned_quantized_size = get_gzipped_model_size(
  "model_pruned_quantized.tflite"
)

print(f"Pruned + Quantized accuracy: {model_pruned_quantized_acc}")
print(f"Pruned + Quantized size: {model_pruned_quantized_size}")
Pruned + Quantized accuracy: 0.9840168019364943
Pruned + Quantized size: 3996

量化没有降低剪枝模型的准确性,但将其大小压缩了大约三分之一,从不到 6k 字节压缩到 4k 字节。

主要收获

总体而言,我们从一个 14k 字节的基线模型开始,最终得到一个 4k 字节的压缩版本,减少了 70%。

与此同时,模型的准确性几乎没有下降。

随着机器学习模型的规模不断增长,并且被部署到越来越小的设备上,模型优化技术如量化和剪枝变得至关重要,以减少模型的大小并提高其效率。通过在 TensorFlow 中实现这些技术,我们可以在几乎不损失准确性的情况下压缩模型。优化模型可以带来更快的推理时间、更小的内存占用和更高的能效,使得在更多设备上部署变得更加容易。

感谢阅读!

如果你喜欢这篇文章,为什么不订阅邮件更新我的新文章呢?通过成为 Medium 会员,你可以支持我的写作,并获得其他作者以及我自己所有故事的无限制访问。

想要时刻掌握日益加速发展的机器学习和人工智能领域的最新动态吗?请查看我的新通讯,AI Pulse。需要咨询?你可以随时问我问题或在这里预约一对一咨询。

你也可以尝试阅读我的其他文章。不知道选择哪篇?可以从这些中挑选一篇:

[## 忘记 ChatGPT

Bard、Sparrow 和多模态聊天机器人将很快使其过时,原因如下。

pub.towardsai.net ## 自监督学习在计算机视觉中的应用

如何用少量标记样本训练模型

[towardsdatascience.com ## 蒙特卡洛丢弃法

用一个小技巧免费改进你的神经网络,并附带模型不确定性估计的好处。

[towardsdatascience.com

所有图像,除非另有说明,均由作者提供。

通过版本控制进行模型回滚

原文:towardsdatascience.com/model-rollbacks-through-versioning-7cdca954e1cc?source=collection_archive---------14-----------------------#2023-01-16

Walmart 回滚并不是唯一一种能为你节省开支的方法

Jazmia HenryTowards Data Science Jazmia Henry

·

关注 发布于 Towards Data Science ·7 min 阅读·2023 年 1 月 16 日

--

使用模型回滚很有趣!

机器学习社区普遍认同模型可能对传统上被边缘化的群体做出偏见决策。从凯瑟·奥尼尔博士乔伊·布乌拉姆威尼博士的伦理 AI 研究人员已经付出了巨大的努力,建立了一个基于偏见和不具代表性数据的错误决策模式,这导致了严重的危害。不幸的是,我们的“智能”学习算法仅与我们使它们一样聪明、能干和道德,我们才刚刚开始理解偏见模型的长期影响。幸运的是,我们已有许多策略可以在危害出现时加以缓解。今天,我们将重点关注一种非常强大的策略:通过版本控制进行模型回滚。

当普通的机器学习或 AI 从业者过去构建模型时,模型构建者的优先级通常是这样的:

传统数据工作流

你从云源收集数据,将数据保存到数据库中,然后编码一个高效的模型,将表现最佳的模型交给运营或工程团队,以便将其集成到他们的大型代码库中,并在他们的网页应用程序中部署。

这个框架——虽然简化且整洁——在许多方面都失败了:

  1. 模型性能通过它们的评分或公司推动的 KPIs 来判断——而不是通过使用模型输出的人的长期影响来判断。

  2. 模型构建者与模型集成过程脱离,而部署模型的人员对模型如何做出决策知之甚少。这导致对模型构建过程缺乏透明度,工程师们无法检测部署的模型是否按预期工作,或可能持续带来危害。

  3. 即使模型构建者花时间找到代表性数据,并且有一个在训练过程中没有造成伤害的高效模型,脱离模型部署过程意味着他们对模型在面对新数据时做出的决策几乎没有可见性,而这些数据在“野外”中可能会有偏见。

认识到这些问题及更多问题使得道德上需要一种新的模型构建者类型——一种既认识到构建高效模型的价值,同时理解学习模型集成所带来的独特机会,可以在部署后改善模型性能,同时减少偏见。这些无可挑剔的思维可以在全技术领域的 MLOps、AI 和分析工程团队中找到。与上述模型工作不同,他们的流程扩展为包括以下内容:

MLOps 过程

从数据库中提取数据后,创建多个模型来解决公司的问题,这些模型通过诸如 Docker 之类的服务进行容器化。创建一个 API 指向模型所在的端口主机,它可以为用户交互提供 Web 应用程序的输出。所有输出都通过诸如 MongoDB 之类的服务摄入到云端进行进一步分析,并附加到模型输出的监控系统,例如 Grafana,如果模型无法很好地推广到世界,就提供警报。这个过程可以更好地可视化模型在部署后的性能,并轻松实施模型版本控制技术。

模型版本控制:它是什么?

模型版本控制 是一种工作流程,允许工程师跟踪软件随时间的变化,是基于模型的版本控制。当我们在模型集成过程中加入模型构建者,我们能够追踪我们使用的模型,并根据其在集成到更大系统时的表现进行相应调整。

模型构建过程是迭代的,需要随时间进行多次更改。模型构建过程的每一步都可能导致目前用来使模型性能良好的内容被替换、改变或调整,以改善未来性能。模型版本控制将允许每个过程步骤的更改步骤快照,并保存每个模型更改的先前迭代或版本,以备将来可能使用。

这可能看起来像这样:

  • 假设最初您从一个人口中提取了数千个特征的数据,但在进行一些特征工程后,通过运行一些特征重要性算法,您将数据集缩小到了人口中最“重要”的特征。模型版本控制可以对您的数据进行跟踪,以追踪所选择用于模型的特征的演变。

  • 在训练多个模型时,您选择在超参数调整过程中调整不同的参数。版本控制可以追踪您在模型构建过程中尝试的许多模型版本。

  • 在将模型集成到一个更大的系统中时,您选择部署给公众的是模型 1,而不是模型 2 或 3。虽然模型 1 在训练过程中性能最佳,但在集成到更大系统时却不能做出适当的决策。模型版本控制允许您在模型部署时继续对模型进行改进,并推动一个更好的模型版本或切换所选择的模型,甚至恢复到之前的模型版本,而不会对您的网络应用程序造成任何中断。

这就是模型版本控制允许模型回滚的地方。

模型回滚:另一个很好的保存方式

想象一下,你作为机器学习经理,任务是让你的团队创建一个可以根据个人信用评分做出贷款决定的机器学习模型。在现状下,有一个简单的技术解决方案,设定一个阈值信用评分,低于此评分的客户将被自动拒绝,高于此评分的信用申请将被送到信用风险分析师处,由他们做出最终决定。这个过程虽然比旧系统好很多,旧系统中的每个人的申请都需要信用风险分析师阅读后才做决定,但仍然存在问题。

  1. 由于应用程序过多,信用风险分析师无法及时做出决定。

  2. 一些没有信用记录的人被拒绝,可能是贷款的好候选人,但由于阈值技术解决方案,信用风险分析师甚至无法接触到他们。

  3. 存在性别和种族因素,其中白人男性比其他人更有可能通过技术解决方案的信用检查,而其他人则因仅仅因为信用记录而更容易被自动拒绝。

  4. 许多人离阈值分数只有几分,这些人被归入那些信用评分远低、风险更高的人群中。

  5. 这些问题正在为你的公司造成大量的收入损失。

你的团队开始着手创建一个可以解决这些问题的模型。你首先确保你的团队找到你认为具有代表性的数据。一旦完成,你让他们创建多个模型——一个启发式模型,一个逻辑回归模型,最后一个随机森林模型——以确定哪个模型性能最好。然后,你将模型交给另一个团队,将其集成到更大的代码库中。你所在的公司存在各自为政的情况,因此你无法看到模型的部署情况,也不愿了解。在 6 个月后,模型看起来运行良好——直到一篇社论发布了如下标题“信用公司算法歧视非二元性别者和非大学学历者”。你的老板打电话告诉你,模型将立即下线。由于推进模型的简单技术解决方案已经被弃用,因此重新上线需要一些时间。在此期间,所有申请将转到信用风险分析师处。

这种情况在数据行业中发生得远比必要的要多。一个同时与模型构建者和工程师合作的集成团队,或者一个全新的团队,专门利用模型构建者将模型集成到代码库中,并利用模型版本控制的优势,将大大减少这种麻烦。

具体来说:

  • 在数据收集过程中,可以保存和标记数据集的版本,以实现透明度,并为模型构建者在未来重新考虑提供机会(如有需要)。

  • 在模型构建过程中,每个模型的所有版本都可以被保存,以便模型构建者重新考虑不同的超参数技术,如果部署的模型在使用时开始做出偏见决策。

  • 在部署期间的模型版本控制将允许简单的技术解决方案保持运行,同时新的模型被发布到公众。如果出现问题,可以回滚模型,并将简单的技术解决方案推进,机器学习团队则会修复问题。

  • 在修复模型问题时,模型构建者将对可能导致这种结果的过程步骤有更大的可见性。这可以减少改进问题所需的时间,从而在长期节省公司时间、资源和金钱。

模型版本控制允许模型回滚,这可以长期节省公司开支,但更重要的是,帮助减少偏见(如果出现的话)。然而,这种技术在你拥有一个不仅了解如何构建模型,还知道如何在生产中优化模型的团队时效果最佳。为了实现这一点,你必须通过模型构建者和工程师之间的协作,或者通过创建一个包含 MLOps、AI 或分析工程师的混合团队,扩展模型构建团队的可见性。

有什么想法?在下面的评论中分享吧!

  • 所有图像均由作者创建。

推荐阅读

O’Neil, Cathy. 数学毁灭武器:大数据如何加剧不*等并威胁民主。纽约:企鹅图书,2018 年。

Gebru, Timnit, 和 Joy Buolamwini. 性别阴影:商业性别分类中的交叉准确性差异。”《机器学习研究会议论文集》81(2018 年):1-15。

不*衡数据模型选择:仅凭 AUC 可能无法拯救你

原文:towardsdatascience.com/model-selection-with-imbalance-data-only-auc-may-not-save-you-5aed73c5efed

你是否在有效地搜索参数?

Marco CerlianiTowards Data Science Marco Cerliani

·发表于Towards Data Science ·6 min read·2023 年 2 月 22 日

[]

Mpho Mojapelo拍摄,来源于Unsplash

大多数数据科学家在参加会议向业务相关者展示机器学习结果时,通常会回答以下问题:

AUC?那是什么?你能详细说明一下吗?

数据科学日常工作中的术语和概念可能对大多数人来说并不熟悉。这在开发人工智能产品以解决现实世界问题时经常发生。在这种情况下,数据科学家与领域专家共同工作,了解领域动态,并将其相应地纳入自动化解决方案中。

对人工智能在解决商业问题中所提供的附加值保持批判性的看法是至关重要的。在很多情况下,采用机器学习可能是没用的,因为这些任务可以通过简单的自动化规则解决,或者在现有数据中没有证据证明需要使用人工智能技术。话虽如此,选择最合适的指标来评估提出的解决方案的有效性是一个非常重要的步骤

选择合适的指标是与领域相关的,并且会根据需求变化。将 AUC 作为指标来向业务相关者展示所采用的机器学习方法的优点/强项可能存在风险。首先,因为 AUC 的定义可能不是所有人都清楚。其次,AUC 的经济意义不容易给出。商业人士以金钱为导向。如果他们不理解所提议的解决方案如何帮助他们节省时间或金钱,他们可能会拒绝这些方案。

在这篇文章中,我们不建议选择正确的业务度量方法。相反,我们关注的是与度量定义紧密相关的更技术性问题。我们指的是模型选择。我们希望在不*衡的二分类背景下测试模型选择的有效性。目标是调查简单的决策(如度量选择或阈值调整)如何影响最终结果以及这些结果如何与业务目标相关联。

实验设置

我们从模拟一个不*衡的表格数据集开始,该数据集包含 90%的负样本和 10%的正样本。

模拟数据的目标分布 [图像由作者提供]

我们可以将少数类(在我们案例中为 10%的样本)想象为在固定时间范围内流失的客户、发动机系统中发生的故障,或发生的欺诈数量。对于数据科学家而言,处理现实世界中的不*衡数据是常态

处理不*衡问题是困难的。与其与极端不*衡作斗争,不如采用一种简单且在大多数情况下有效的方法,即在学习阶段利用这种不*衡。换句话说,最好不要尝试过采样方法,而是直接对多数类进行欠采样或保持其不变。通过应用合理的欠采样比例,可以使模型从数据中学习。此外,这种不*衡现象在推理时得到保留和再现。

处理目标不*衡的技术比较 [图像由作者提供]

牢记这一简单的建模策略后,我们准备深入探讨模型选择。

模型选择

机器学习用例生命周期 [图像由作者提供]

在不*衡场景中寻找最佳模型或参数集时,合适的度量选择依赖于基于评分的度量。我们指的是所有使用预测概率来评估拟合优度的度量。在二分类的背景下,最常见的评分度量包括AUC*均精度交叉熵

在这种情况下使用评分指标似乎是一个合理的解决方案。我们独立于像准确率精确率召回率Fbeta这样的硬阈值来评估拟合的好坏。

回到我们的模拟用例… 假设业务利益相关者定义的一个要求是获得对少数类的高精确度。我们如何进行模型选择和参数调整以满足这一要求?

model = RandomizedSearchCV(
    XGBRFClassifier(random_state=1234), 
    dict(n_estimators=stats.randint(50,300)), 
    n_iter=20, random_state=1234,
    cv=5, n_jobs=-1, 
    refit=False, error_score='raise',
    scoring={
        'fbeta': make_scorer(fbeta_score, beta=0.1), 
        'roc_auc':'roc_auc', 
        'average_precision':'average_precision'
    },
).fit(X, y)

我们设置了一个随机森林的随机搜索,寻找最佳的树木数量。我们注册了 AUC、*均精确度和 Fbeta 的交叉验证评分。我们选择Fbeta 低 beta 值(0.1)作为精确度的*似值(我们试图优化的目标)。试验结果在下面的图中报告。

Fbeta 作为 AUC(左侧)和*均精确度(右侧)的函数 [作者提供的图片]

正如预期的那样,AUC/*均精确度与 Fbeta 之间没有明确的关系。选择具有最佳 AUC 的模型并不能保证选择具有最佳 Fbeta 的模型

此时,使用根据 AUC 选择的“最佳”参数配置,我们需要在更新的数据集上进行额外的微调,以选择一个硬阈值来最大化精确度并使我们的利益相关者满意。

这样做没有什么不好的,但是否有更有效的方法?我们能否将阈值调整与参数选择相关联?

将阈值搜索嵌入到模型训练中是直接的。使用 ThresholdClassifier 估算器,可以在优化定义的评分函数(在我们例子中是 Fbeta)的同时调整二分类阈值。这会在通过拆分接收到的训练数据得到的验证集上自动完成。通过根据调整后的阈值离散化概率来获得预测类别。

from sklearn.metrics import fbeta_score
from sklearn.model_selection import train_test_split
from sklearn.base import clone, BaseEstimator, ClassifierMixin

class ThresholdClassifier(BaseEstimator, ClassifierMixin):

    def __init__(self, estimator, refit=True, val_size=0.3):
        self.estimator = estimator
        self.refit = refit
        self.val_size = val_size

    def fit(self, X, y):

        def scoring(th, y, prob):
            pred = (prob > th).astype(int)
            return 0 if not pred.any() else \
                -fbeta_score(y, pred, beta=0.1) 

        X_train, X_val, y_train, y_val = train_test_split(
            X, y, stratify=y, test_size=self.val_size, 
            shuffle=True, random_state=1234
        )

        self.estimator_ = clone(self.estimator)
        self.estimator_.fit(X_train, y_train)

        prob_val = self.estimator_.predict_proba(X_val)[:,1]
        thresholds = np.linspace(0,1, 200)[1:-1]
        scores = [scoring(th, y_val, prob_val) 
                    for th in thresholds]
        self.score_ = np.min(scores)
        self.th_ = thresholds[np.argmin(scores)]

        if self.refit:
            self.estimator_.fit(X, y)
        if hasattr(self.estimator_, 'classes_'):
            self.classes_ = self.estimator_.classes_

        return self

    def predict(self, X):
        proba = self.estimator_.predict_proba(X)[:,1]
        return (proba > self.th_).astype(int)

    def predict_proba(self, X):
        return self.estimator_.predict_proba(X)

ThresholdClassifier 估算器与模型无关,可以与任何输出概率的二分类器一起使用。在我们的例子中,我们将其应用于我们的随机森林,允许像以前一样寻找最佳参数。

Fbeta 作为 AUC(左侧)和*均精确度(右侧)的函数 [作者提供的图片]

不出意外的是,AUC/*均精度与 Fbeta 之间没有关系。比较原始随机森林和使用阈值调整的随机森林所得到的分数,我们观察到 Fbeta 值的差异。

对于相同参数集,使用(红色)和不使用阈值调整(蓝色)得到的 Fbeta [作者提供的图片]

寻找分类阈值的最优值可以在相同参数集下提供更好的少数类精度。结果不会影响产生的概率。评分指标,如 AUC 或*均精度,保持不变。

Fbeta,作为 AUC(左)和*均精度(右)的函数,使用(红色)和不使用阈值调整(蓝色)得到的 [作者提供的图片]

我们并不是为了通过观察验证指标的微小改进来宣称最优性能的模型。我们必须追求业务目标。在我们的模拟场景中,显而易见的是,通过简单的技巧我们可以获得更好的精度。值得注意的是,我们可以在没有额外验证数据和将参数搜索与阈值调整结合的情况下得到这些发现

总结

在这篇文章中,我们概述了评分指标和基于准确性的指标之间的主要区别。我们观察了它们在不*衡的二分类上下文中的表现,以解决实际业务问题。如果我们的目标是衡量我们在检测流失客户、识别欺诈或发现故障引擎部件方面的能力,仅使用 AUC 可能会产生不完整或次优的解决方案。像往常一样,我们必须从一开始就深入了解业务逻辑,并尽力满足这些需求。

查看我的 GitHub 仓库

保持联系:Linkedin

使用多项式回归建模脑电图信号

原文:towardsdatascience.com/modeling-eeg-signals-using-polynomial-regression-in-r-bd1b0da08251

从零开始选择最佳的多项式回归模型

Mala DeepTowards Data Science Mala Deep

·发表于Towards Data Science ·阅读时间 12 分钟·2023 年 6 月 15 日

--

R 中多项式回归的输入脑电图信号时间序列图。图片由作者提供。

脑电图信号介绍

EEG 代表脑电图,它是一种测量大脑电活动的电信号[1]。为了获得 EEG 结果,将由细小金属圆盘和细电线组成的电极贴在头皮上。这些电极探测到来自脑细胞活动的微小电荷,然后这些电荷被放大并显示为计算机屏幕上的图形或可能打印在纸上的记录。EEG 的主要目的是通过无痛的方法检测脑细胞通信的潜在问题(如脑炎、出血、癫痫、帕金森病等)[2]。

目标

我们的目标如下:

  1. 评估多项式回归模型对脑电图信号的拟合程度,并估计模型的参数。

  2. 使用基于模拟的方法估计模型参数的后验分布,以考虑模型和数据中的不确定性。

GitHub上获取完整的代码,或在RPubs上查看该书。

[## GitHub - maladeep/Modeling-EEG-signals-using-polynomial-regression-in-R: 使用多项式回归建模脑电图信号…

该存储库提供了使用多项式回归建模脑电图信号的实现。代码和分析…

github.com

数据集

我们将使用 数据集,该数据集由 Swartz 计算神经科学中心 提供,包含时间、四个输入信号(x1、x2、x3 和 x4)以及一个输出信号(y)。

以下是数据集的一个样本:

Swartz 计算神经科学中心 提供的数据集快照。作者提供的图像。

挑战

在这里,我们将基于假设关系可以表示为多项式回归模型来解释输入 EEG 信号与输出 EEG 信号之间的关系。

我们给出了五种不同的非线性多项式回归模型,我们需要找到最合适的一个。

我们给出了五种不同的非线性多项式回归模型。作者提供的图像

为了解决问题,将采取以下步骤:

  1. 使用最小二乘法估计模型参数

  2. 计算模型残差误差(RSS)

  3. 计算对数似然函数

  4. 计算赤池信息量准则(AIC)和贝叶斯信息量准则(BIC)

  5. 检查模型预测误差的分布

  6. 选择最佳回归模型

第一步:使用最小二乘法估计模型参数

当我们对分布的真实值(未知)没有任何了解时,我们使用估计量(随机变量)的概念 [3]。换句话说,我们使用估计量变量来估计与输入和输出变量相关的 EEG 数据分布的真实值。

在这里,估计量变量由“θ”表示,并可以取多个值,如 θ1、θ2、…、θbias。现在,使用最小二乘法(LSM)来计算 EEG 数据不同候选模型的估计量模型参数,LSM(θ̂)用于通过最小化预测值和实际值之间的*方残差之和来估计分布的真实值 [4],公式如下:

最小二乘估计量的公式。作者提供的图像。

现在,为了计算最小二乘法,我们首先需要通过绑定 EEG 数据集中适当的列或值来格式化输入数据。使用函数 cbind()。一旦输入数据正确格式化,我们可以使用上面提到的最小二乘法公式,并使用名为 solve() 的内置线性方程求解函数,我们可以找到 θ̂。我们使用 solve() 是因为它更高效且更不易出错 [5]。

同样,由于我们有 5 个模型,对于每个模型,我们将遵循相同的过程:创建 cbind,计算模型参数的估计值 θ̂ 并打印 theta_hat 的值。

每个 5 个模型的 θ̂。作者提供的图像。

第 2 步。计算模型残差误差(RSS)

残差*方和(RSS),也称为预测误差*方和(SSE)或残差*方和(SSR),是数据与估计模型之间差异的度量。它是通过从基于模型参数的因变量的估计值中减去实际值的*均*方来计算的[6]。

我们通常希望最小化误差,因此误差越小,回归的估计能力越强,模型的拟合度越好。另一方面,较大的 RSS 表示模型对数据的拟合较差[7]。值得一提的是,RSS 永远不会是负值,因为残差的*方始终是非负的[8]。

为了计算 RSS,我们首先必须利用上一步计算的 (θ̂) 来计算每个模型 1–5 的误差,RSS 的数学表示为:

模型残差误差(RSS)公式。图像由作者提供。

对于模型 1 的 RSS 计算,我们有以下代码:

同样,我们将计算每个模型的 RSS 值。

因此,我们有每个模型的 RSS 值。

显示每个模型的 RSS 值的表格。图像由作者提供。

在这里,表中最低的 RSS 值是 2.1355,与模型 5 相关,其次是与模型 2 相关的第二低 RSS 值 2.1398。

第 3 步。计算对数似然函数

现在,我们的目标是确定测量值在参数未知的情况下如何适应提供模型的样本数据。为了实现这一目标,我们将使用第 2 步获得的 RSS 计算线性回归模型的对数似然函数。

对数似然是一种衡量模型拟合优度的方法[9],并用于简化优化问题,避免数值下溢或上溢。数学表示为:

对数似然公式。图像由作者提供。

在这个任务中,我们的目标是找到一组参数,使观察值的概率最大化。由于对数似然函数的性质是单调增加(非递减)且没有局部极大值,它适合用来识别测量值的拟合度[10]。通俗来说,单调增加意味着当自变量(例如 x)的值增加时,函数值(例如 y)也随之增加,即 x 增加时,y 只能增加,不能减少。

因此,在这里,随着对数似然值的增加,给定模型参数的数据的似然性也会增加。因此,寻找对数似然函数的最大值就等同于寻找似然函数的最大值,但如果仅使用似然函数,则对数的凹性特征会丢失,我们无法获得全局最大值[11]。

因此,使用上述公式,我们将首先计算模型的方差,利用 RSS 以及 Y 信号的长度,然后计算对数似然函数。

使用相同的公式,我们计算其余模型的 BIC。

4. 计算赤池信息量准则(AIC)和贝叶斯信息量准则(BIC)

现在,既然我们有了 RSS 和对数似然值,我们需要使用模型选择标准方法,这里我们将使用赤池信息量准则(AIC)和贝叶斯信息量准则(BIC)。根据[12],模型选择涉及估计各种候选模型的性能,目标是选择最佳模型。

它们都可以用于比较不同模型并选择最佳模型。它们都基于给定数据的模型的似然性和模型中的参数数量。然而,这两种模型选择方法之间的主要区别在于,AIC 对参数更多的模型的惩罚较小,相比于 BIC[7]。

4.1 计算每个模型的 AIC

赤池信息量准则(AIC)是一种统计方法,旨在用尽可能少的自变量(参数)确定真正解释因变量方差的模型[13]。通过这种方法,它帮助选择一个参数较少的简单模型,而不是复杂模型。

使用最大似然估计(步骤 3),确定模型的相对信息值和参数数量。AIC 的公式如下:

赤池信息量准则(AIC)的公式。图像来源于作者。

在这种情况下应用 AIC 的主要目标是消除过拟合问题,因为 AIC 对参数更多的模型给予惩罚,并*衡模型拟合效果与复杂性之间的权衡[14][7]。

根据[15],值得一提的是,当 AIC 值较低时,模型对数据的拟合效果更好,AIC 的绝对值可能是有利的也可能是有害的。

在进入 AIC 的代码之前,让我们先了解一下 BIC。

4.2 计算每个模型的 BIC

如前所述,BIC 与 AIC 类似,但 BIC 对参数更多的模型会给予更大的惩罚[16]。与 AIC 类似,较低的 BIC 值表示模型拟合效果更好。BIC 的公式如下:

贝叶斯信息量准则(BIC)的公式。图像来源于作者。

使用上述公式,我们计算了每个模型的 BIC,其计算方式类似于 AIC。

使用上述公式,各模型的 AIC 和 BIC 为:

各模型的 AIC 和 BIC 值。图片由作者提供。

正如我们所记得的,AIC 和 BIC 值越低越好,因此模型 2 的 AIC 为 -334.6489 和 BIC 为 -321.4357 是所有列出的模型中最适合的。

第 5 步:检查模型预测误差的分布

现在我们已经获得了 AIC 和 BIC 值,我们有兴趣查看误差分布。毕竟,我们的目标是选择误差最小的模型。在决定绘制分布图之前,我们需要计算每个模型的误差。然后我们将使用 Q-Q 图(分位数-分位数图)通过 qqnorm() 函数可视化和比较两个概率分布,因为我们的假设是数据是独立同分布的。

根据[17],Q-Q 图是通过将两个分位数集绘制在一起形成的。如果两个分位数集来自相同的分布,则这两个分位数集将形成一条相对直线;然而,在实际中,这并不是一个严格的规则。在同一图中,我们将添加一条参考线,称为 Q-Q 线,这是正态分布的完美拟合线。

qqline() 函数接受两个参数:第一个是数据模型的预测误差,第二个是颜色(col)、线宽(lw)和虚线(lty)。

模型 5 的 Q-Q 图显示大多数数据跟随 Q-Q 线(红色),因此我们可以说这些数据符合正态分布。

在这里,利用获得的 Q-Q 图,我们可以简单地视觉检查数据集是否符合理论分布。要正式测试数据集是否符合某种特定分布,我们需要进一步探讨。

第 6 步:选择最佳回归模型

完成步骤 1–5 后,我们已经收集了选择最佳候选模型所需的所有信息。通过计算 RSS、对数似然函数、绘制正态分布图以及比较 AIC 和 BIC 值,我们掌握了识别数据最佳模型的所有信息。基于 AIC 和 BIC,最佳模型是模型 2,因为它具有最低值。为了验证所选的模型 2 是否是一个良好的候选模型,我们将查看 Q-Q 图。

看着 Q-Q 图,除了模型 3,所有模型似乎都有相同的特性;然而,看着 Q-Q 线的位置,模型 2 似乎是最合适的。

为了更倾向于选择模型 2,我们希望绘制直方图以展示残差的分布。为了便于查看,我们将所有直方图绘制成 3 行 2 列的形式,使用 par(mfrow = c(3, 2))。

*使用直方图的预测误差分布。图片由作者提供。*

看每个模型的分布,模型 2、5 和 6 都似乎符合正态分布。

此外,我们将进一步探讨模型 2 是否比其他模型更合适。考虑额外因素将帮助我们确定在这种情况下的最佳模型。我们将根据模型的可解释性比较每个模型的参数数量,即,具有较少参数的简单模型更易于解释和理解 [18]。

查看每个参数的长度,最少的是模型 3,有 3 个参数,但它不符合正态分布且有偏,接下来是模型 4,有 4 个参数,但其 AIC 和 BIC 大于模型 2。

因此,基于 AIC、BIC、Q-Q 图以及额外的可解释性,我们选择了模型 2 作为最佳拟合模型。

揭示冠军:模型 2 成为在 R 中使用多项式回归进行 EEG 信号建模的最佳选择。图片由作者提供。

因此,通过使用最小二乘法估计模型参数、模型残差误差(RSS)、对数似然函数、赤池信息准则(AIC)和贝叶斯信息准则(BIC),并利用 Q-Q 图可视化预测误差,我们揭示了冠军:模型 2 成为在 R 中使用多项式回归进行 EEG 信号建模的最佳选择

这完成了我们在 R 中的多项式回归。

我的 GitHub 仓库 中有完整的工作代码,或者你可以访问 RPubs 在线查看。

喜欢这篇文章? 通过成为 Medium 会员来解锁无限学习通过使用 以下链接,你可以在不增加额外费用的情况下支持我。

如果你对文章有任何问题或有兴趣合作,请通过 LinkedIn 与我联系。

参考文献

[1] St, E.K., Frey, L.C., Britton, J.W., Frey, L.C., Hopp, J.L., Pearce Korb, Koubeissi, M.Z., Lievens, W.E., Pestana-Knight, E.M. 和 St, E.K. (2016). 介绍脑电图(EEG):成人、儿童和婴儿的正常与异常发现的入门文本和图谱 [互联网]。 [在线] Nih.gov。可在此处查看:www.ncbi.nlm.nih.gov/books/NBK390346/

[2] Healthline (2012). EEG(脑电图):目的、程序和风险。 [在线] Healthline。可在此处查看: www.healthline.com/health/eeg

[3] Peterka, V. (1981). 第八章——贝叶斯系统识别方法。 [在线] ScienceDirect。可在此处查看:www.sciencedirect.com/science/article/pii/B9780080256832500132 [访问日期:2023 年 1 月 31 日]。

[4] Björck, Å. (1990). 最小二乘法。 [在线] ScienceDirect。 可在以下网址获取:www.sciencedirect.com/science/article/abs/pii/S1570865905800365[访问日期:2022 年 10 月 4 日]。

[5] Schork, J. (无日期). 在 R 中解决方程组(3 个示例)| 使用 solve() 函数。 [在线] 统计学世界。 可在以下网址获取:statisticsglobe.com/solve-system-of-equations-in-r/

[6] Allen, D.M. (1971). 预测的均方误差作为选择变量的标准。 Technometrics, 13(3), 页 469–475。 doi:10.1080/00401706.1971.10488811

[7] Brownlee, J. (2019). 使用 AIC、BIC 和 MDL 的概率模型选择。 [在线] 机器学习大师。 可在以下网址获取:machinelearningmastery.com/probabilistic-model-selection-measures/

[8] Valchanov, I. (2018). *方和:SST、SSR、SSE。 [在线] 365 数据科学。 可在以下网址获取:365datascience.com/tutorials/statistics-tutorials/sum-squares/

[9] Zach (2021a). 如何解读对数似然值(附示例)。 [在线] Statology。 可在以下网址获取

[10] Stephanie (2021). 对数似然函数。 [在线] 统计学怎么做。 可在以下网址获取:www.statisticshowto.com/log-likelihood-function/

[11] Music, A. (2020). 高斯分布与最大似然估计方法(逐步)。 [在线] 创业公司。 可在以下网址获取:medium.com/swlh/gaussian-distribution-and-maximum-likelihood-estimate-method-step-by-step-e4f6014fa83e [访问日期:2023 年 1 月 31 日]。

[12] Trevor Hastie, Tibshirani, R. 和 Friedman, J. (2009). 统计学习的要素。 编辑:纽约,纽约斯普林格纽约。

[13] Manikantan, A. (2021). Akaike 信息准则:模型选择。 [在线] 极客文化。 可在以下网址获取:medium.com/geekculture/akaike-information-criterion-model-selection-c47df96ee9a8

[14] Bevans, R. (2020). Akaike 信息准则 | 何时及如何使用(示例)。[在线] Scribbr. 可用网址: www.scribbr.com/statistics/akaike-information-criterion/

[15] Zach (2021b). 如何解释负 AIC 值。[在线] Statology. 可用网址: www.statology.org/negative-aic

[16] Datacadamia (2014). 统计 — 贝叶斯信息准则(BIC)。[在线] Datacadamia — 数据与合作。可用网址: datacadamia.com/data_mining/bic

[17] Clay Ford (2015). 理解 Q-Q 图 | 弗吉尼亚大学图书馆研究数据服务 + 科学。[在线] Virginia.edu. 可用网址: data.library.virginia.edu/understanding-q-q-plots/

[18] De’ath, G. 和 Fabricius, K.E. (2000). 分类与回归树:一种强大而简单的生态数据分析技术。《生态学》,81(11),第 3178–3192 页。doi:10.1890/0012–9658(2000)081%5B3178:cartap%5D2.0.co;2

使用马尔可夫链建模游戏

原文:towardsdatascience.com/modeling-games-with-markov-chains-c7b614731a7f?source=collection_archive---------5-----------------------#2023-10-09

探索使用“封箱游戏”的概率建模

Kairo MortonTowards Data Science Kairo Morton

·

关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 10 月 9 日

--

介绍

从和朋友玩牌到在轮盘桌上赢取钱财,精彩游戏的乐趣对大多数人来说是不可抗拒的。但无论多么有趣,即使是最乐观的玩家在经历几次失败后也会不禁问道:“这个游戏是否被操控了?”对于我们这些对概率有一定了解的人来说,不解答这个问题可能会感到不满足。在这篇文章中,我们将探讨一种用于回答此类问题的概率模型。具体来说,我将展示如何将马尔可夫链应用于“封箱游戏”,希望能激励你用概率来回答自己关于游戏的问题。

什么是马尔可夫链,它们与游戏有什么关系?

马尔可夫链是一种简单、研究充分的概率模型,能够建模许多现实世界的随机过程。具体来说,马尔可夫链的目标是建模一个概率事件序列,其中事件来自一个称为状态的集合。为了实现这个目标,马尔可夫链将任何两个状态之间的转换概率存储在一个称为转换矩阵的矩阵中。除了状态和转换矩阵,这些定义了随机过程的行为,马尔可夫链的一个定义特征是马尔可夫性质。直观地说,这个性质声明当前状态必须持有必要的信息,以确定转移到任何下一个状态的概率,而不管序列中之前发生了哪些状态。因此,任何满足这个性质的随机过程都可以使用马尔可夫链进行建模,这使得它成为一个非常强大的工具。为了更好地理解这些概念以及马尔可夫链的一般性质,我们来看一个简单游戏的例子:

假设你要在一系列公*的抛硬币中进行赌博。在游戏开始时,你有 $5,每次抛硬币前你预测结果(正面或反面)。每次抛硬币如果你猜对了,你会赢得一美元,如果猜错了,你会输掉一美元。当你达到 $0 或 $10 时,游戏结束。

如果我们将游戏的状态定义为每次抛硬币后的玩家现金余额,马尔可夫性质成立,并且幸运的是我们可以使用马尔可夫链建模这个游戏!在图形上,我们可以如下表示马尔可夫链的状态和转换概率:

在上图中,橙色圆圈代表游戏状态,箭头表示状态之间的转换概率。注意所有的转换概率都是 0.5,因为改变状态需要正确猜测公*抛硬币的结果。此外,状态 0 和 10 有一个指向自己的箭头,标记为概率 1,因为它们是游戏的结束状态。

正如你将在本文后面看到的,通过这种方式建模一个游戏,我们可以回答许多有趣的问题。例如,

  1. 期望的胜率/失败概率是多少?

  2. *均需要多少步(抛硬币)才能赢得或输掉游戏?

  3. 在 t 步(抛硬币)之后,游戏状态的概率分布是什么?

阅读完这篇文章的其余部分后,我建议你回到这一部分,考虑如何回答上述简单游戏的问题。

“掩盖盒子”游戏解释

为了真正深入探讨与马尔可夫链相关的概率建模复杂性,我们将把重点放在游戏“掩盖盒子”上。我在浏览 Instagram Reels 时偶然发现了这个游戏,对规则有模糊的理解,我开始试图确定获胜的难度。

Roland Scheicher / Roland Scheicher 于德国维基百科,公共领域,通过维基共享资源

“关闭箱子”是一个多人游戏,使用一个包含九个瓷砖(标记为 1 到 9)和两个六面骰子的棋盘(如上图所示)进行。每个玩家的回合开始时,所有瓷砖都处于竖立位置。然后,掷两个骰子,玩家可以翻下任何总和等于两个骰子总值的瓷砖。接着,玩家重复掷骰子和翻下瓷砖的过程,直到没有任何一个竖立的瓷砖子集的总和等于两个骰子的总值为止。这时,玩家的回合结束,得分为棋盘上仍竖立的瓷砖的总和。在这个版本的游戏中,在所有玩家完成回合后,得分最低的玩家被认为是赢家。然而,如果玩家的回合因为能够翻下所有瓷砖(即“关闭箱子”)而结束,则他们自动赢得游戏。正是这个规则引起了我的注意,因此将成为本次探讨的重点。具体来说,我想回答的问题是:关闭箱子有多难?

使用马尔可夫链建模“关闭箱子”

从前面的章节来看,现在应该将“关闭箱子”建模为一个马尔可夫链。虽然这看起来是一个直观的下一步,但让我们通过检查马尔可夫性质是否成立来确保这一点。为此,我们将游戏的唯一状态定义为集合 {1, 2, 3, 4, 5, 6, 7, 8, 9} 的所有子集。这是因为玩家回合中的任何时刻都可以通过哪些瓷砖是翻转的来唯一表征。从数值上讲,我们可以将这些状态视为 9 位二进制数字(竖立的瓷砖为 1),共有 2⁹ 种,给出 512 种唯一状态。在这种状态定义下,马尔可夫性质成立,因为只有骰子的概率特征、当前状态和游戏规则决定了状态之间转移的概率。最重要的是,鉴于我们知道当前状态,玩家回合中达到的前一个状态对未来达到其他状态的概率没有影响。

现在状态已经明确,我们也确认了“关闭箱子”确实可以被建模为一个马尔可夫链,剩下要定义的唯一元素是转移矩阵 T。具体来说,Tᵢₖ(第 i 行第 k 列的条目)表示从状态 i 转移到状态 k 的概率。确定这些概率时,建模这个游戏的有趣复杂性就会显现出来。

为了计算从一个状态转换到另一个状态的概率,我们必须回答这个问题:“在游戏中发生什么动作会导致状态发生变化?”对于我们感兴趣的游戏“关掉盒子”,有两个概率性动作决定下一个状态:掷骰子和选择翻下的瓷砖。让我们从检查骰子在状态 i 和 k 之间转换的作用开始。我们首先让 Sᵢ 和 Sₖ 分别表示状态 i 和 k 中的竖立数字的集合。为了使从状态 i 到状态 k 的转换具有非零概率,很明显 Sₖ 必须是 Sᵢ 的一个真子集。这是因为在状态之间转换时,竖立的瓷砖数量必须减少,而瓷砖一旦翻下就不能再翻回去。根据这种状态表示以及假设 Sₖ ⊂ Sᵢ,在状态 i 和 k 之间转换过程中翻下的数字构成集合 D = Sᵢ - Sₖ(Sᵢ 中但不在 Sₖ 中的元素)。

因此,这一转换发生的一个必要条件是掷出的骰子总和必须等于集合 D 中元素的总和。形式上,必须满足以下方程:

其中 X₁ 和 X₂ 是表示两个骰子值的离散随机变量。如果我们让 z 是集合 D 中元素的总和,那么两个六面骰子的情况可以通过以下方法推导出上述方程成立的概率:

如果 z < 2 或 z > 12,那么从状态 i 转移到状态 k 的概率为零,因为不存在两个普通骰子掷出的和为 z 的情况。

尽管这看起来是填充转移矩阵条目所需的唯一概率计算,但状态转换还要求玩家在掷骰子后从一组有效的移动中进行选择。因此,从状态 i 转移到状态 k 不仅需要骰子的总和等于 z,玩家还必须选择翻下集合 D 中的瓷砖,而不是根据骰子点数可能有的其他许多选项。

为了模拟这一人类策略元素,我们假设玩家从可能的瓷砖翻下集合中均匀随机选择。根据这一假设,玩家选择状态 k 的概率是 1/Nᵢₖ,其中 Nᵢₖ 是 Sᵢ 中和为 z 的非空子集的数量。

有了上述信息,我们现在可以正式定义转移矩阵的条目。转移矩阵中的每个条目表示两个事件发生的概率:

  1. 骰子总和等于 z(事件 1)

  2. 玩家随机选择翻下特定的瓷砖组,从状态 i 变换到状态 k(事件 2)

因此,转移矩阵的条目可以定义如下:

需要注意的是,上述转移矩阵的定义没有考虑玩家回合结束的概率。这种情况可能发生在玩家“关闭盒子”时,或者在观察骰子掷出值后没有瓷砖可以翻下。在这两种情况下,1/Nᵢₖ 是未定义的,因此必须单独处理。

根据上述状态的二进制表示,状态 i = 0 足以表示“关闭盒子”,因为所有瓷砖都被翻下。由于这是游戏的“胜利”状态,我们可以修改转移矩阵,使得在状态 0 中停留的概率为 1(即 T₀₀ = 1)。

最后,我们将添加一个“失败”状态(L),以表示由于“不幸”的掷骰结果玩家的回合结束。具体来说,要将此状态纳入转移矩阵,我们需要知道没有任何子集 Sᵢ 的和等于 X₁ + X₂(两个骰子的和)的概率。虽然显式计算此量可能是可行的,但我们可以相对于其他转移矩阵值进行定义,如下所示:

因为转移矩阵中的每一行表示状态的概率分布,且这些概率必须加起来等于一。此外,由于这是游戏的最终状态,因此离开该状态的概率为 0。

利用上述确切的转移概率结果,我们现在可以利用马尔可夫链的一些重要属性来回答一个问题,那就是“关闭盒子”有多困难?具体来说,我们可以回答以下问题:玩家使用完全随机策略“关闭盒子”的概率是多少?

使用 Python 计算“关闭盒子”的获胜概率

在尝试计算该游戏的“胜/负”概率时,了解转移矩阵的作用非常重要。对于马尔可夫链,转移矩阵使我们能够探讨状态上的概率分布如何在单次转移后演变,只需一个矩阵-向量乘法即可。数学上,我们可以简单地写成如下形式:

其中 T 是转移矩阵,πₜ 是表示经过 t 次转移后所有状态的概率分布的行向量。因此,给定我们知道当前处于任何状态的概率,我们可以回答这样的问题:“如果我们随机选择要翻下的瓷砖,那么在一次骰子掷出后处于任何状态的概率是多少?”此外,利用我们对游戏初始状态(所有瓷砖都翻起)的确定性知识,我们可以很容易地定义 π₀ 并递归地将其乘以 T,以确定经过任意数量的转移(掷骰子 + 翻瓷砖)后的状态分布。这个递归可以被重写为 πₜ 的以下封闭形式表达式:

模拟“关箱子”的马尔可夫链有两个最终状态:胜利和失败,一旦进入就不能离开(即吸收状态)。因此,我们可以确定,在经过一些有限数量的转移后,所有状态的分布将收敛到这两个状态的分布。直观地说,对于“关箱子”来说,这个声明突出了一个事实,即玩家的回合必须以“关箱子”或未能做到这一点而结束,因此在一轮回合中玩家可以进行的移动次数是有限的。

要找到这个上限,注意到最长的回合发生在玩家每次掷骰子后翻下一块瓷砖,直到标记为“1”的瓷砖仅有竖立状态,因此在接下来的骰子掷出时他们无法“关箱子”。这个动作序列总共构成了 9 次移动以达到最终状态,因为总共有 9 块瓷砖。因此,求解胜率/败率就像设置 t ≥ 9 并计算 πₜ 一样简单。计算 πₜ 后,玩家使用随机策略“关箱子”的概率是 πₜ 中的第一个条目,因为它对应于所有瓷砖都翻倒的状态(S₀)。另外,递归过程可以从 0 开始重复,直到分布收敛。此外,对于这种情况,还有更快的方法来计算 πₜ,我在此帖中不予讨论。它们利用了吸收状态的存在和转移矩阵的特殊定义。了解更多信息请访问:en.wikipedia.org/wiki/Absorbing_Markov_chain

为了在 Python 中计算胜率/败率,我们将完全依赖科学计算库 Numpy。首先,我们将瓷砖的数量、骰子的数量和游戏中的状态数量分别定义为 9、2 和 513。

num_tiles = 9
num_dice = 2
num_states = (2**num_tiles)+1 # +1 for the game over/lose state

使用这些简单的参数,我们可以生成每个状态的集合表示,Sᵢ,如下所示。

# Generate representation of all possible states
tile_nums = [i for i in range(1, num_tiles+1)]
states = []
for i in range(0, 2**num_tiles):
  binary_rep = np.binary_repr(int(i), width=num_tiles)
  states.append([tile_nums[idx] for idx, a in enumerate(binary_rep) if a == '1'])

在这里,Numpy 的 binary_repr 函数非常有用,因为它使用整数 i 生成状态的二进制表示。因此,通过找到这个字符串中“1”的位置,我们可以确定在状态 i 中哪些瓷砖是竖立的。

在处理状态表示之后,我们使用以下代码生成转移矩阵:

# Generate transition matrix
transition_matrix = np.zeros((num_states, num_states))
for i in range(num_states):
  for j in range(num_states):
    transition_matrix[i][j] = compute_transition_probability(i, j)
  transition_matrix[i][num_states-1] = 1 - transition_matrix[i][:num_states-1].sum()
  assert np.allclose(transition_matrix[i].sum(), 1), "Transition matrix is not stochastic"

最后,使用转移矩阵,我们回答了本次探索的核心问题:玩家使用完全随机策略“关闭盒子”的概率是多少?为此,我们必须定义初始分布 π₀,在所有状态上如下:

# Define the initial state distribution
init_state_dist = np.zeros((1, num_states))
init_state_dist[:, num_states-2] = 1

由于游戏总是从所有瓷砖都竖立的状态开始,状态分布是一个长度为 513 的行向量,除了第 511 项(以零为基础的索引)外,所有条目都是零,其中放置了一个一。这是因为 511 的二进制表示为字符串 '111111111',这对应于所有瓷砖都竖立的状态。

一旦定义了初始分布,就可以使用公式 πₜ = π₀Tᵗ 来确定“关闭盒子”的概率,其中 t = 9 以找到在状态上收敛的分布。再次强调,我们可以设定 t = 9,因为 π₉T = π₉,因此使用大于九的 t 值会导致不必要的计算。完成这一任务的代码如下:

# Compute and print the win and lose probabilities
t = 9
multiple_transition_matrix = np.linalg.matrix_power(transition_matrix, t)
final_dist = np.matmul(init_state_dist, multiple_transition_matrix)

win_prob = final_dist[0, 0]
lose_prob = final_dist[0, num_states-1]

# Print results to 4 decimal places
print("Win Probability: {:.4f}".format(win_prob))
print("Lose Probability: {:.4f}".format(lose_prob))

在这个代码片段中,我们使用了 Numpy 的 matrix_power 和 matmul(矩阵乘法)函数来分别计算 T₉ 和 π₉。使用这些结果,“关闭盒子”的概率简单地存储为 π₉ 的第一个元素,这对应于没有竖立瓷砖的状态(在二进制中为 '000000000')。根据这一见解,我们最终知道,当使用完全随机策略时,“关闭盒子”是非常困难的(约 2% 的机会)! (精确概率值如下所示)。

上述代码和模型公式经过一些修改后,可以扩展以支持任何数量的瓷砖和任何数量的骰子的“关闭盒子”变体。因此,我在下方可视化了随着骰子数量和瓷砖数量变化的获胜概率图:

结论

通过这篇文章,我们探讨了马尔可夫链及其在回答“关闭盒子”游戏相关问题上的具体应用。然而,我所强调的技巧,通过一些关键思考和修改,可以用于建模多种游戏。因此,虽然你“关闭盒子”的机会可能是 2/100,但我相信你会发现使用概率建模来回答你最喜欢的游戏相关问题会更成功。

除非另有说明,所有图片均由作者提供

从基本原理建模旅行推销员问题

原文:towardsdatascience.com/modeling-the-traveling-salesman-problem-from-first-principles-bd6530c9c07?source=collection_archive---------1-----------------------#2023-05-14

一种概念优先、数学其次的建模方法,应用于运营研究中最著名的路线问题

Carlos J. UribeTowards Data Science Carlos J. Uribe

·

关注 发表在 Towards Data Science ·15 分钟阅读·2023 年 5 月 14 日

--

由 DALL·E 3 根据作者的提示生成的图像:“一张展示巴黎典型旅游路线的地图,方程式散布其间”

👁️ 这是系列文章的第 2 篇,涵盖了项目“Python 中的智能决策支持系统”。 我鼓励你查看它,以获得整个项目的一般概述。如果你只对如何建模 TSP 感兴趣,你仍然在正确的地方,因为这篇文章是独立的。如果你 也对解决问题感兴趣,而不仅仅是建模它,你会喜欢系列的接下来 5 篇文章。相信我,它们会提供你需要的和你不知道你需要的😉

目录

1. 动机和目的

2. 理解数据

3. 从问题描述中定义概念模型

4. 从概念模型构建数学模型

  • 4.1. 将数据放入集合和参数

  • 4.2. 在变量中编码决策

  • 4.3. 定义目标

  • 4.4. 创建约束条件

1. 动机和目的

  • 本文接着第一阶段文章的内容。你不需要阅读它来理解我们将要做的事情,但让我给你一个快速回顾(如果你读过上一篇文章,可以跳到第二部分)。简而言之,我们列出了游客在规划旅行时面临的常见问题,并着手建立一个可以帮助我们更有效地规划旅行的系统,加快决策过程,甚至完全自动化任何给定旅行的日程安排。我们观察到这样表述的问题过于复杂,因此我们将其拆解,并得出了其本质版本,称之为最小可行问题。最后,我们得出结论,它的形式是旅行商问题(TSP),其中“城市”对应于我们版本中的“景点”,即游客希望参观的城市中的“景点”。

  • 因此,作为一个启动步骤,我们必须首先制定并解决旅行商问题(TSP),一旦完成,我们就可以在更加稳固的基础上,向着更复杂和更通用的旅行规划解决方案迈进。我们选择这种方法是因为这篇文章系列旨在教授一种灵活的运筹学(OR)建模方法,所以你在这里找到的许多课程、技巧和窍门都是适用于你可能遇到的任何问题

  • 返回到我们的业务中,我们有 TSP 的描述

  • 目标:尽可能少走距离

  • 要求:每个景点只访问一次,并返回到原始出发点(在我们的情况下是酒店)。

我们需要的是问题的模型而不是问题的解决方案。我知道专注于获得解决方案很有诱惑力,但想一想:如果我们得到了 TSP 的直接解决方案,比如用某种 启发式算法 找到某个解决方案——即使它是最优的——也对我们来说是不够的,因为TSP 并不是我们实际的问题,而是一个简单的*似,我们需要扩展它以使其 *似 我们的实际问题。 当我们更改问题描述(我们会这样做)时,任何针对 TSP 的 解决算法 都会变得过时,我们必须从头开始。

模型可以被扩展、调整和优化以适应你的问题及其变化的需求。然而,解决算法往往是针对特定问题定制的,当问题发生变化时,很难修改。

因此,我们的目标是建立一个 TSP 模型。一旦你学会了如何推理出这样的模型,你将会对接下来阅读 下一篇文章有很好的理解,在这篇文章中,我们将在 Python 中实现该模型:

## 使用 Python 实现、求解和可视化旅行商问题

学习如何将优化模型从数学翻译到 Python,优化它,并可视化解决方案以快速获得…

towardsdatascience.com

2. 理解数据

如果你记得 我们上一次的文章,TSP 的基本输入只是我们想在一天内访问的地点列表。在这个概念验证中,我们使用的是巴黎,因此我选择了这八个著名的必游之地:

  • 圣心大教堂

  • 卢浮宫

  • 蒙马特

  • 叠加码头

  • 凯旋门

  • 香榭丽舍大街

  • 巴黎圣母院

  • 埃菲尔铁塔

由于问题在于寻找最小距离的旅行,因此我们实际需要的数据是距离数据,这取决于地点及其相对地理位置。如何从地理位置计算距离数据将在 第 4 篇文章中讲解,因为现在讲解这个会导致一个绕道(双关)的内容,使你从这里的主要焦点——模型构建——分心。

因此,现在假设你已获得所有可能对之间的距离。这些距离将在下一个迭代中作为 CSV 文件提供,当我们用 Python 实现模型时。数据如下所示:

图 2.1. 一组巴黎站点的距离数据,TSP 所需。(图片由作者提供)

我们称这个表为距离矩阵。请注意,虽然这个酒店并不特别值得明信片上的风景,但它也包含在矩阵中,因为它被视为最终旅行中的另一个站点。对于这个 MVP,我们保持简单,使用对称距离矩阵,这是说从𝐴到𝐵的距离与从𝐵到𝐴的距离相同,对于任何 A 和 B 来说。在更高级的设置中,这种情况不一定成立,使得这种*似变得无效。

3. 从问题描述中定义概念模型

现在我们处于下图流程图中表示为绿色块的阶段:

图 2.2. OR 中问题解决的简约工作流程。第 2 阶段:概念模型(图片由作者提供)

概念模型的目的是用语言陈述问题,但采用标准化格式,以便“句子”和“数学对象”之间的映射在随后的阶段(数学模型制定)中变得清晰。我们可以这样推测我们的概念模型

(知道)

数据(集合和参数)

  • 需要访问的站点列表

  • 任何一对站点之间的距离

(我们需要决定)

决策:以什么顺序访问站点

(以这样的方式)

目标:最小化总旅行距离

(使得)

约束条件

  • 所有站点都被访问

  • 每个站点只访问一次

  • 最后访问的站点是我们出发的站点(我们进行封闭旅行)

👁️ 遵循良好实践,实践中,优秀将随之而来

你可能认为概念模型看起来相当*凡,与我们开始文章时的“简单”问题声明并没有太大区别。你是对的。对于像这样的简单问题,它可能是一个重复的步骤。但对于更大的问题,这个阶段是不可或缺的,通常在没有首先建立概念模型的情况下构建数学模型会导致混乱(不清晰或模糊的需求、糟糕的公式、错误的代码、不可行的模型等)。因此,现在我们需要建立这种纪律,即使在我们简单的案例中边际价值很低,也要通过这一阶段。专注于良好的习惯,良好的结果将随之而来。

4. 从概念模型建立数学模型

我们刚刚达到了工作流程的“第 3 阶段”,在下图中用绿色标出。数学模型阶段,可能是所有阶段中最具挑战性的阶段,是自然 语言变为数学的地方。

图 2.3. 运筹学中的极简工作流程。第 3 阶段:数学模型(图像由作者提供)

在这一阶段,任何一点模糊性都是不被允许的。

一个定义良好的数学模型胜过一百个解释

在我们工作流程的这一阶段,我们为 TSP 构建了一个纯粹的模型,在下一个阶段(见“冲刺 3”)我们将使用 Python 从之前解释过的 CSV 数据集中构建一个模型实例

📝 理论回顾:“抽象模型”与“模型实例**

数学模型(在运筹学中)由“组件”构成。这些是所有元素(方程、数据等),它们共同表示一个特定结构的问题真正定义一个模型的是它的结构,其组件之间的关系,无论这些组件在任何给定示例中取什么具体的数值。

模型实例是“抽象模型”的一个具体“实现”,包含具体数据。因此,我们通常定义抽象模型,然后用特定场景的数据填充它们,进而得到模型实例。我们优化的就是这些模型实例,以获得具体结果

在下面的小节中,我将简要介绍构成模型的组件及其目的,同时对它们进行定义。如果你不是初学者,已经了解模型组件的功能,可以跳过这些解释,直接进入数学定义部分。

4.1. 将数据放入集合和参数中

我们所需的所有数据都位于图 2.1. 中显示的数据框中。我们可以只将数据保留在这里,在创建模型的约束和目标时从该数据框中提取所有数字。事实上,许多人这样做,但这是一个不良习惯,随着模型的规模增大,它无法良好扩展。随着模型复杂性的增加,这种方法需要不断增加的胶水代码(用于处理数据框操作),如果数据保持在更适合优化模型构建的其他数据结构中,这些代码是可以避免的。这些数据结构就是模型的集合参数

💡 不同的数据结构以满足不同的需求

如果你在想“为什么要创建集合和参数,而我们已经在表格中有了所需的数据?”,简短的回答是:因为这样做可以使模型构建更容易、更通用,且更少出错。

集合”是用于存储问题的主要“实体”或“元素”的模型组件,而“参数”则用于存储这些实体或它们关系数值属性。在我们的例子中,访问地点是主要的“实体”,所以它们会被存储在一个集合中,而地点对之间的距离是它们关系的“数值属性”,所以它们会被存储为参数。在“实现层面”,进行这种分类也是非常有用的,因为每个组件都有不同的功能,这将使得模型构建更加容易:

集合的作用是方便地存储和操作索引。这些索引是代表问题中不同“实体”的 ID 或名称,它们被用来方便的方式索引参数,用于约束和目标的创建。

参数的作用是方便地存储和操作由它们所索引的“实体”的数值属性,这些就是在约束和目标中实际出现的数字。

从我们的概念模型中,我们有:

  • 我们定义的访问地点列表是集合𝕊:

表达式 2.1. 是旅行中所有访问地点的集合(为了简洁起见,只显示两个)。

“由𝑖, 𝑗索引”这一短语放在集合定义旁边,以表示每当在模型中使用索引𝑖或𝑗时,它们代表集合𝕊中的成员。这样,当我们有多个集合,从而使用多个索引时,更容易记住每个索引的含义。

  • 任意一对地点之间的距离,我们定义为索引的 参数 𝐷ᵢⱼ:

参数之所以称为“索引的”,仅仅是为了表明它不是一个标量参数(,单个数字),而是一个二维数字矩阵。要从这个索引参数中检索一个数字,你需要指定两个索引𝑖和𝑗,这两个索引会从集合𝕊中取出。

𝕊和𝐷ᵢⱼ是概念模型中唯一的“数据组件”。但这不应该限制我们提出其他集合或参数的能力,它们在构建模型时可能会很有用

举个例子,请注意𝐷ᵢⱼ的索引𝑖和𝑗是𝕊的成员,但不能重合。如果它们重合,距离将为零,这是一个*凡的数据。此外,我们不会从一个地方再次回到自己,因此完全没有必要考虑对(𝑖, 𝑖)。因此,限制对(𝑖, 𝑗)的组合是有用的,以便建模变得更容易(并且错误可能性更小。为此,我们现在创建另一个集合𝔸,源自𝕊,包含所有不同地点的对(𝑖, 𝑗)。每对表示一个,连接地点𝑖到地点𝑗,因此符号是𝔸。

📝 弧只是两个节点之间的“有向连接”。只需将弧(𝑖, 𝑗)视为一个从𝑖开始并在𝑗处结束的向量。当两个节点之间的“连接”没有方向(即方向无关紧要)时,使用“边”这个词,因为一直说“无向弧”会太啰嗦。图论中的人们也喜欢高效。

𝔸的一个好属性是它是定义𝐷ᵢⱼ的域,正如我们在用 Python 实现模型时所看到的,显式地定义这样的域使其可重用于其他模型组件,这也是很方便的。

  • 不同站点之间可能的弧集:

表达式 2.2.(导出的)巡回路径(站点到站点路径)可能的弧集。

4.2. 将决策编码为变量

由于我们正在构建一个模型,以便它可以告诉我们应该采取哪些行动,并且这些规定的行动在模型优化之前对我们来说是未知的,我们必须将所有我们可以采取的潜在行动编码为变量。

但我们如何定义这些潜在的行动呢?根据我们的概念模型,我们需要做出的通用“决策”是“访问各个站点的顺序”。这个“顺序”指的是在进行巡回时我们可以遵循的所有可能路径中的一条。关键思想是路径由连接单个节点(即站点)的弧的序列组成。因此,决定采取特定路径实际上是决定穿越特定的弧序列这些关于是否穿越连接两个站点的特定弧的“原子决策”就是我们希望作为变量进行编码的决策。

“是否从站点 A 前往站点 B”显然是一个二元决策:要么去,要么不去。由于这种特性,决策变量需要是二元的(,只取 0 或 1 作为值),并且仅对有效的弧定义(因此导出的𝔸现在派上用场)。用数学术语表示:

表达式 2.3. 仅定义在可能的弧上的二元(去/不去)决策变量。

每个可能的弧(𝑖, 𝑗)都有一个唯一的决策变量,但当模型优化时,我们只关心取值为 1 的变量,因为它们指示了应穿越哪些弧。例如,如果变量𝛿ᵢⱼ,其中𝑖=hotel,𝑗=Louvre,取值为 1,则意味着我们应该从酒店到卢浮宫作为我们巡回的一部分。

4.3. 定义目标

想象我们有 4 个点,𝑎, 𝑏, 𝑐, 𝑑,且我们遵循路径𝑎 → 𝑏 → 𝑐,其中点𝑑未被访问。它的总距离是其弧的距离之和:𝐷ᵃᵇ + 𝐷ᵇᶜ。但是,如果我们事先不知道将遵循什么路径,我们如何表示这条未知路径的总距离?

💡 正因为最优路径未知,我们需要一个覆盖所有可能路径的表达式,但当模型优化时,它会简化为“最佳路径”的距离。

通过利用任何遍历弧(𝑖, 𝑗)将有𝛿ᵢⱼ = 1,而任何未遍历弧(𝑖′,𝑗′)将有𝛿ᵢᑊⱼᑊ = 0,我们可以创建一个表达式,一旦变量被决定,它将简化为遍历路径的总距离。方法是“累加所有可能性”对所有可能的弧距离𝐷ᵢⱼ进行加权求和,权重为其二进制“弧变量”𝛿ᵢⱼ,让这些变量取的 0 和 1 决定哪些距离保留(𝐷ᵢⱼ × 1 = 𝐷ᵢⱼ)哪些消失(𝐷ᵢⱼ × 0 = 0),在总距离的表达式中。这种“可能性的求和”代表了最终巡回将行走的总距离,因此它将是我们的目标(称为𝑍)需要最小化

在数学上,这表示为:

表达式 2.4. 目标函数的原始版本定义,使用原始集𝕊(左);以及其简化版本,使用派生集𝔸(右)。

注意右侧的求和因为使用了𝔸(即𝐷ᵢⱼ和𝛿ᵢⱼ的域(索引集合))变得更易读(和实现)。

同样,注意到目标构成了我们对优良性的定义。由于我们希望最小化目标,因此较低的值是更好的,而显然,最小值是最佳值。与这一“最佳”目标值相对应的决策变量𝛿ᵢⱼ的值构成了问题的(最优)解,它们将通过优化过程找到。

4.4. 创建约束

从我们的概念模型中,我们得到:

  1. 所有站点都被访问。

  2. 每个站点仅被访问一次

  3. 最后访问的站点是我们开始的站点(我们进行一个闭合巡回)

我们意识到要求(1)已包含在要求(2)中,因为如果每个站点仅被访问一次,这意味着每个站点都被访问了,因此所有站点都被访问了。因此我们不再需要单独的约束来满足要求(1),而是专注于如何将要求(2)建模为约束。

说“每个站点仅被访问一次”与说“每个站点仅被进入退出一次”是一样的。这个短语又等同于这两个短语的结合:“每个站点仅被进入一次”“每个站点仅被退出一次”。我们来分别建模这些“短语”:

  • 每个站点仅进入一次

表达式 2.5. 约束集要求每个站点仅被“进入”一次。

从右到左阅读整个表达式是有用的。如果你首先看到约束定义的指标,解释左侧约束定义的含义会更容易。我会这样大声读出这个约束:

对于属于所有站点集合𝕊的每个站点𝑗,所有潜在弧 𝛿ᵢⱼ到达𝑗的总和必须等于 1,这意味着仅有一个入弧 必须发生在𝑗。或者,更通俗地说:每个站点必须仅有的一个其他站点访问。*

  • 每个站点离开一次

表达式 2.6。 约束集强制每个站点“离开”一次。

我会这样阅读这个约束:

对于属于所有站点集合𝕊的每个站点𝑖,所有潜在弧 𝛿ᵢⱼ𝑖的总和必须等于 1,这意味着仅有一个出弧 必须发生在𝑖。或者,更通俗地说:每个站点必须离开到仅有的一个其他站点。

我们只剩下要求 (3)。它规定最佳路径必须从起点返回到起点,或者等效地,路径必须是一个回路(一个闭合的环)。这里有一种初步的推理方式:“因为我们已经设置了约束来确保每个站点进入离开一次,这意味着结果路径必须是闭合的,因为任何站点都不可能是“汇点”(即,一个站点有一个入弧但没有出弧)或者是“源点”(即,一个站点有一个出弧但没有入弧)。因此,前两个约束,大概强制最终轨迹成为一个闭合回路。”

这种推理是否正确? 我们采用实验方法。假设这种推理是正确的,然后尝试按现有模型解决。当我们查看解决方案时,我们将看到它是否正确或是否有问题。如果结果错误(或以任何方式不合逻辑),我们可以随时返回并修正逻辑(这在现实项目中时常发生)。实现、解决和“实验验证”是我们在“下一个冲刺”中涵盖的内容,在那里创建了一个 Python 模型,并根据得到的结果进行解决、检查和重新制定。

因此,我们在此(暂时)结束“数学模型制定”阶段。下一站是“计算机模型实施”,见用 Python 实现、解决和可视化旅行商问题

## 使用 Python 实现、求解和可视化旅行商问题

学习如何将优化模型从数学转化为 Python,优化它,并可视化解决方案以获得快速结果……

towardsdatascience.com

将会有更多“冲刺”的文章发布,如果你渴望成为我在这段旅程中的伙伴,请保持关注,并查看第三部分中的项目时间表 本系列的第一篇文章,以导航到你感兴趣的冲刺并跟踪那里的工作进展。

此外,欢迎关注我,在评论中向我提问,给我反馈,或通过LinkedIn与我联系。

感谢阅读,期待在下一篇文章中见到你!📈😊

使用 GEKKO 以确定性方式建模世界

原文:towardsdatascience.com/modeling-the-world-in-a-deterministic-way-with-gekko-6dc8edbe5842?source=collection_archive---------10-----------------------#2023-06-30

照片由 Iván Díaz 供稿,来自 Unsplash

解决微分代数方程和应用动态优化的简明指南

Daniel KulikTowards Data Science Daniel Kulik

·

关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 6 月 30 日

--

随着我们的世界越来越数字化,数据收集迅速扩展。这些数据使我们能够创建更准确的模型,这些模型帮助我们解决问题,并在许多业务和技术领域找到优化的解决方案。由于这些模型是基于现实世界的数据构建的,而现实世界的数据本质上是随机的,因此数据集中的随机性、噪声和异常现象通常是不可避免的。通常需要花费大量时间进行探索性数据分析(EDA)、数据预处理和异常值检测。这些方法已成为理解和完善数据的常见做法,然后再进行建模。然而,任何使用这些数据构建的模型始终只是对实际解决方案的*似。这是由于数据本身是随机的(Aleatoric 不确定性)并且受到许多未知因素的影响(Knightian 不确定性)。此外,数据不足或数据集偏倚失衡也可能进一步降低模型的准确性(Epistemic 不确定性)。即便在这些困难面前,这种方式构建的模型仍然是无价的,固有的随机性有时也可以成为帮助更好理解现实世界复杂性的工具。然而……还有一种替代方法:进入确定性建模。

确定性建模是与随机建模的对立面。它是一种通过去除现实世界引入的混乱来简化建模过程的某些方面的方法。这些模型本质上是理想化的,意味着它们基于特定假设代表了一个理想的解决方案。这些假设可以源于一组带有观点的、概率性的或通用的规则,并假设输入和输出之间存在明确的模式。虽然这听起来可能与使用随机数据的建模类似,但主要区别在于模型现在由这些预定义的假设决定,而不是训练数据。你可能会问,“如果我不在数据上训练模型,那我从哪里获得这些模型假设?”答案很简单,但有时被忽视,那就是领域知识。领域知识是一组关于特定主题的规则和概念。正是通过领域知识得出这些假设,为确定性建模提供基础。在任何编程语言中,这些确定性模型可以通过自定义代码编写,但随着模型变得越来越复杂,已经有专门解决这一任务的库被编写出来。在 Python 中,有一个名为GEKKO的库将被探索。

GEKKO是一个 Python 库,用于执行建模语言 Advanced process monitor (APMonitor)。它提供了一系列功能,专门用于确定性建模,帮助创建和分析复杂模型。它可以解决混合整数和微分代数方程,并配有大规模求解器,用于线性、二次、非线性和混合整数规划。GEKKO有 9 种问题类型用于定义确定性模型设置。然而,本文仅考虑同时解决所有方程的 Moving Horizon Estimation (MHE)模式。MHE 模式通过最小化测量输出与由优化问题制定的模型预测之间的差异来估计动态系统的状态。通过应用递归估计或移动窗口估计来完成这一过程。

哇!这真是一大堆术语,但接下来的实际示例应该很快让一切变得明了。

空间飞行是人类的一项了不起的成就。将人工制造的物体送入太空是一项工程和创造力的奇迹。但是,送东西进入太空是相当昂贵的。因此,建立具有预期性能能力的火箭是至关重要的。收集实际发射的数据有助于提高未来火箭的性能。然而,火箭必须在此之前发射才能获得数据,这就是问题所在。没有随机数据就没有基于随机的数据模型。此时,确定性模型和GEKKO登场。火箭的确定性模型的假设很简单,你猜对了,就是火箭科学!也就是说,即使没有将火箭离地一寸,我们也可以利用物理定律来确定特定火箭设置在理想化场景中的表现。例如,让我们使用GEKKO复制 SpaceX 的 Falcon 1 Stage 1 火箭设置。

首先,让我们使用公开已知的信息[1][2]来设置 Falcon 1 Stage 1 火箭,并开始定义一些模拟常量和已知值。

现在我们可以真正开始GEKKO中的有趣内容了。由于我们正在处理基于时间的模拟,首先定义我们的时间组件:

  • 火箭飞行时间将是 200 秒

  • 燃料燃烧时间为已知的 161.6 秒

  • 以及一个额外的时间组件,用于以后使用

设置好时间数组后,我们可以初始化一个GEKKO模型并设置模型的时间组件。下一步是定义必须在每个时间步骤中解决的变量。这些变量由我们对火箭的假设决定,但也可以包括我们可能在模拟结束时感兴趣的变量。为了简化,这个模拟中的变量是:

  • 质量

  • 距离

  • 速度

  • 推力产生的力

  • 另一个额外的虚拟时间变量,用于以后使用

对于这些变量中的每一个,我们可以给它一个起始值,还可以设置下界和上界来约束它们。GEKKO 变量是定义我们感兴趣变量的最通用方式。然而,实际上还有其他选项,比如固定变量、受控变量和操控变量,这些可以让用户对变量的演变有更多控制。你应该查看一下

在继续之前,需要注意的是,为了节省你的阅读时间且不使内容过于数学化,这个火箭模型将忽略一些已知的火箭物理假设。这些包括,随着火箭上升空气密度和重力的变化,科里奥利力的影响,以及许多机械工程组件。然而,通过这些假设,我们将涵盖影响火箭飞行和动力学的主要力量。现在,介绍这些力量及如何在GEKKO 中应用它们可能是个好时机。

与任何经典的运动物体一样,牛顿的运动定律适用。这意味着我们已经对火箭的运动有了很好的了解。但让我们快速回顾一下我们需要的微分方程。别担心,它们真的很简单。我们先定义火箭如何移动和覆盖距离的方程,

哇!这是基本物理,物体的速度只是距离的变化除以时间的变化。接下来,添加火箭在空气中移动时会经历的阻力,顺便说一句,这真是个拖累。

有了这些,我们现在可以建立力和速度之间的另一个关系。记住,

好的,通过这个,我们可以将火箭所经历的所有力(前向推力、阻力,当然还有重力)与速度关联起来。下面是方法,

我保证这是这个模拟中最困难的方程。在火箭发射前,我们还需要做两个假设。其中一个与火箭中的燃料有关。如果火箭将燃料作为推进剂燃烧,那么一个好的方法是描述火箭质量如何变化。为此,我们可以假设质量损失与最大推力和燃烧时间呈线性关系,如下所示,

最后的假设实际上是当燃料用尽时会发生什么。这一假设非常有趣,因为它是一个逻辑声明,GEKKO 对此也有相应的功能。

完成这些后,我们现在可以将所有部分组合在一起,建立我们的确定性模型。

注意,上述代码中有一些额外的内容未被提及。首先调用了一个虚拟时间方程,这是为了跟踪燃料耗尽的时间。接下来,使用了优化目标。为什么要这样做?因为 MHE 求解一个目标,在这种情况下,目标是尝试在每个时间步使火箭达到从地面起的最大高度。你可以根据你想要优化的内容尝试其他目标函数(例如,尝试m.fix_final(h, 50e3)m.Obj(thrust*v)来尽量节省火箭燃料)。最后一点是GEKKO会输出模拟是否能找到最佳解决方案。对于这个模拟,你会看到我们实际上有负的自由度。这是使用m.if3的一个怪癖,通常会导致模拟无法求解。所以,如果你想尝试更复杂的情况,可能可以考虑使用m.if2

最后,是时候查看我们模拟的成果了。为了快速且清晰地查看模拟结果,绘制图表是一个很好的可视化方式。

分析火箭的性能提供了一些有趣的见解。首先,它达到了 73.83 公里的最大高度和 802.15 米/秒的最大速度。其次,火箭在飞行过程中燃烧了 19.59 吉焦的能量,*均推力为 322.68 千牛。

这些结果在建造火箭之前为我们提供了关于火箭能力和性能的重要信息。更好的是,参数可以轻松调整和微调,随着复杂性的不断增加,真正展示了确定性建模所带来的巨大潜力。利用这些信息,这些知识还可以进一步帮助工程师进行设计优化,并为如此高成本的项目制定预算。

虽然火箭很酷,但确定性建模的更实际应用包括对许多不同类型系统的优化。这些系统的例子有:制造厂的加工线、交通控制和路线规划、模拟复杂的感染和疾病传播、建模气候变化及其影响,甚至在金融领域使用基于确定性的博弈论模型。

总结一下,确定性建模是寻找理想场景解决方案的一个很好的选择,这些场景可以通过领域知识来很好地描述。它们可以用于建模复杂的非线性系统,并在实际数据可用之前提供强有力的预测和概率结果。总的来说,确定性建模是那些希望掌握它的人的强大工具。

[1] Propulsion Falcon-I(www.b14643.de/Spacerockets_2/United_States_2/Falcon-I/Propulsion/engines.htm)

[2] 猎鹰 1 号 (en.wikipedia.org/wiki/Falcon_1)

使用傅里叶变换对可变季节特征进行建模

原文:towardsdatascience.com/modeling-variable-seasonal-features-with-the-fourier-transform-18c792102047?source=collection_archive---------2-----------------------#2023-10-12

使用信号处理技术改进时间序列预测性能

Florin AndreiTowards Data Science Florin Andrei

·

关注 发表于 Towards Data Science ·21 分钟阅读·2023 年 10 月 12 日

--

建模时间序列数据和预测是复杂的主题。可以使用许多技术来改善预测工作的模型性能。我们将讨论一种可能改善 ML 预测模型从时间特征中学习并从中概括的技术。主要关注点是创建季节性特征,以便在训练中为时间序列预测模型提供输入——如果在特征创建过程中包括傅里叶变换,可能会有明显的收益。我从过去在数字电子和信号处理方面的工作中获得了灵感,并将这些概念应用于时间序列预测的特征工程。

本文假设你对时间序列预测的基本方面比较熟悉——我们不会讨论这一主题的一般性内容,只会讨论其中某个方面的细化。这不是关于时间序列分析或建模,也不是关于金融时间序列(股票价格)。这关于机器学习对一般时间序列的预测。有关 ML 时间序列预测的介绍,请参见 Kaggle 课程——这里讨论的技术建立在他们关于季节性的课程之上。

我们还将比较一些显著的时间序列模型,如 Facebook Prophet 和 ARIMA,并学习它们使用的技术,这些技术绝对可以用于自定义的 ML 预测模型。

ETS 方法和自回归模型

ETS 方法(误差、趋势、季节性)将时间序列信号分解为几个组件:

ETS 模型

  • y(t) 是你试图预测的信号(时间序列)

  • g(t) 是趋势,一个捕捉非周期性变化的函数

  • s(t) 是季节性,或具有严格周期性的变化

  • epsilon 是模型无法预测的噪声

通过正确学习 y(t) 和 g(t),模型应该能够进行预测,除了由 epsilon 表示的随机变化(这些实际上不属于模型)。Facebook Prophet 采取了类似的方法,同时还添加了其他特征(假期、外生特征)。

自回归模型本质上做线性回归,其中特征是序列中的过去数据点(时间序列的滞后)。一个例子是 AR(p) 模型,或 ARIMA 的自回归部分(本质上只是一个 GLM——仅具有线性项的广义线性模型):

AR(p) 模型

  • y(t) 是你试图预测的信号的未来值

  • b 是偏置项

  • y(t-1) 是最后已知的值(滞后 1)

  • y(t-2) 是之前的值(滞后 2),依此类推。

  • phi 系数是模型权重

  • 噪声项(epsilon)实际上不属于 AR(p) 模型,因为它无法被预测

因此,AR(p) 模型试图通过多个过去值的滞后预测未来值。

在机器学习预测中,可以使用各种 ETS 和自回归模型的组合。也可以包含外生变量——这些变量不属于时间序列的一部分,但对其有影响(例如促销可能会影响销售)。SARIMAX 是一个例子,它使用了季节性成分、自回归成分、移动*均成分(误差项的)和外生特征。

如果你正在建模一个一般的时间序列(例如商店销售),随机游走假设并不占主导地位,你并不在意建立一个严谨的数学模型,主要关注的是时间序列值预测的 RMSE 性能,你可以使用通用机器学习模型(可以是随机森林概念的某种变体,可以像线性回归一样简单,也可以是各种机器学习模型的集合,甚至是神经网络)来建立模型,然后你可以工程化所有你需要的特征,并在训练中提供给模型。

想象一个包含将用于拟合模型的 Xtrain 特征的数据框。目标 Ytrain 是时间序列本身,可能以某种方式变换(缩放、对数等)。Xtrain 数据框可能包括:

  • 趋势特征,表示时间序列的广泛变化;这些可能包括:常数项(每一行的值为 1)、线性时间函数(字面上是枚举:0、1、2、3、……)、二次函数(0、1、4、9、……)、三次函数,或可能有其他形状。Prophet 使用线性趋势,并从数据中计算拐点。

  • 季节性,表示维持相同形状并在严格的时间表上发生的上下波动

  • 自回归特征,即时间序列本身的滞后(目标 Ytrain 中的时间序列列,但向下移动 1 行或更多行)

  • 外生特征,如促销、节假日、地理位置分组等。

当你运行 model.fit(Xtrain, Ytrain) 时,模型将从这些特征中学习,目标 Ytrain 就是时间序列本身。这是一种相当通用的时间序列预测形式,使用机器学习模型。

详细的季节性特征

Facebook Prophet 将季节性特征表示为傅里叶级数。一般来说,一个周期函数可以表示为一系列正弦/余弦对,其中每个正弦/余弦的周期是某个基周期的倍数:

傅里叶级数

  • t 是时间

  • P 是季节性特征的基周期——具有最大周期的正弦/余弦对的周期

  • n,序列中的索引,是一个周期去倍数(频率倍数)

  • 正弦和余弦项由 a(n) 和 b(n) 参数加权

在 Pandas 形式中,这里有两个正弦/余弦对,第二对的频率是第一对的两倍:

 sin(1,freq=A-DEC)  cos(1,freq=A-DEC)  sin(2,freq=A-DEC)  cos(2,freq=A-DEC)
time                                                                              
1956Q1           0.000000           1.000000           0.000000           1.000000
1956Q2           0.999963           0.008583           0.017166          -0.999853
1956Q3           0.017166          -0.999853          -0.034328           0.999411
1956Q4          -0.999963          -0.008583           0.017166          -0.999853
1957Q1           0.000000           1.000000           0.000000           1.000000
...                   ...                ...                ...                ...
2013Q1           0.000000           1.000000           0.000000           1.000000
2013Q2           0.999769           0.021516           0.043022          -0.999074
2013Q3           0.025818          -0.999667          -0.051620           0.998667
2013Q4          -0.999917          -0.012910           0.025818          -0.999667
2014Q1           0.000000           1.000000           0.000000           1.000000

[233 rows x 4 columns]

傅里叶定理指出(粗略来说),通过调整权重 a(n) 和 b(n),当以这种方式表示一个表现良好的周期性函数时,级数可以使其收敛到该函数。因此,有限的傅里叶和可能是一个很好的方式来逼*周期信号(时间序列的季节性分量)——我们只是在寻找一种*似,而不是完全收敛,因此使用有限的和。

建模季节性的一种方法是使用 one-hot 编码变量。如果 P 是季节性特征的基期,那么数据框中 P 列,每列包含 0 或 1,每列在整个周期 P 内只出现一次 1,并且 P 列之间的行中 1 的值从不重叠,可以被线性模型学习为:

one-hot 编码

  • Cp 是每一列编码时间

  • beta 参数是应用于每一列的权重——它们的相对值实际上建模了波形的形状。

在 Pandas 形式中,以下是一个周期为 4 个观测的季节性特征 one-hot 编码:

 s(1,4)  s(2,4)  s(3,4)  s(4,4)
time                                  
1956Q1     1.0     0.0     0.0     0.0
1956Q2     0.0     1.0     0.0     0.0
1956Q3     0.0     0.0     1.0     0.0
1956Q4     0.0     0.0     0.0     1.0
1957Q1     1.0     0.0     0.0     0.0
...        ...     ...     ...     ...
2013Q1     1.0     0.0     0.0     0.0
2013Q2     0.0     1.0     0.0     0.0
2013Q3     0.0     0.0     1.0     0.0
2013Q4     0.0     0.0     0.0     1.0
2014Q1     1.0     0.0     0.0     0.0

[233 rows x 4 columns]

如果你使用的模型是纯线性回归(没有惩罚),并且存在偏置项,那么 one-hot 编码可能会遇到所谓的虚拟变量陷阱。你也可能听到“共线性问题”这个术语。简而言之,Cp 列可以线性组合生成一个常量“趋势”来解释偏置项。

如果是这种情况,虚拟编码(K-1 编码)是解决方案——它实际上与 one-hot 编码相同,但使用 P-1 列(去掉一列):

虚拟编码

你也可以尝试去除偏置项,或使用正则化模型,或使用梯度下降。在实践中,测试模型的性能并做出正确的决定。

傅里叶级数与 one-hot/虚拟编码

one-hot/虚拟编码

假设你创建了一组 one-hot 编码列,组中有 P 列。这组将能够建模周期为 P 的任何季节性。它还能够建模周期为 P 的子倍数的季节性,如 P/2 等,因此你不需要显式建模它们。

这一连续周期(P, P/2, P/4 等)仅仅是另一种描述傅里叶级数的方法,它将任何复杂的周期信号建模为基组件及其所有频率倍数/周期子倍数的和。这是理解one-hot/虚拟编码时间特征与傅里叶级数时间特征执行相同任务的关键。它们是相同基本思想的不同实现。

one-hot/虚拟时间特征建模的周期的下限是时间序列的采样周期的两倍:如果时间序列有每日观测,那么你的时间虚拟特征建模的最短周期将是 2 天。这实际上是奈奎斯特-香农定理在时间序列中的表述。

一热编码/虚拟变量编码真正做的是——它建模了 P 天组件的波形,就像一个具有 P 点的散点图。这样做是因为傅里叶变换的工作原理,它还捕捉了所有 P 的子倍频组件(P/2 等)。

一热编码的时间特征可以很好地学习任意复杂的波形。另一方面,如果 P 很大,那么一热编码的列数也会很大,这会遇到维度灾难问题。最好在短时间周期内使用一热/虚拟编码,但这不是严格的规则——如果你一开始没有很多特征,那么一大组时间虚拟变量也会很好。

傅里叶级数编码

傅里叶级数简洁,可以表达任意大的周期 P——它们非常适合大周期的季节性。另一方面,如果波形非常复杂,可能需要创建许多正弦/余弦对才能很好地学习。

你可能应该使用傅里叶级数的线索在周期图中(下面描述)。如果你注意到你的系列有一个强烈且尖锐的 P 天季节性组件,以及一个强烈且尖锐的 P/2 组件,而在明显模式方面没有其他内容,这表明一个周期为 P、另一个周期为 P/2 的正弦/余弦对可能会很好地工作。

请记住,P/2 组件和 P 组件建模的是同一现象:这不是两个不同的现象,而是同一个现象,其基周期为 P。P/2、P/4 等组件只是其傅里叶谐波在频谱中散布。这是因为基周期 P 的季节性没有完美的正弦形状——如果有,周期图中只会显示 P 组件。

永远不要创建周期短于时间序列采样周期两倍的傅里叶组件。如果你的时间序列采样周期为每日,则你能建模的最短季节性是 2 天。奈奎斯特-香农定理在这里设置了一个硬限制,就像一面砖墙。这就是你所有的数据。

也许甚至留出一些安全边际给奈奎斯特限制。你在音频 CD 格式中听到的音乐信号频率高达 20 kHz。音频 CD 的采样频率是 44.1 kHz,因此奈奎斯特限制是 22.05 kHz,比最大记录频率稍高。

加法性与乘法性季节性

考虑季度澳大利亚波特兰水泥生产数据集,显示了 1956 年第一季度到 2014 年第一季度澳大利亚波特兰水泥的季度总生产量(以百万吨计)。

df = pd.read_csv('Quarterly_Australian_Portland_Cement_production_776_10.csv', usecols=['time', 'value'])
# convert time from year float to a proper datetime format
df['time'] = df['time'].apply(lambda x: str(int(x)) + '-' + str(int(1 + 12 * (x % 1))).rjust(2, '0'))
df['time'] = pd.to_datetime(df['time'])
df = df.set_index('time').to_period()
df.rename(columns={'value': 'production'}, inplace=True)
 production
time              
1956Q1       0.465
1956Q2       0.532
1956Q3       0.561
1956Q4       0.570
1957Q1       0.529
...            ...
2013Q1       2.049
2013Q2       2.528
2013Q3       2.637
2013Q4       2.565
2014Q1       2.229

[233 rows x 1 columns]

这是具有趋势、季节性组件和其他属性的时间序列数据。观察数据是季度性的,跨度数十年。

趋势 g(t) 几乎是线性的,只有少数几个拐点。季节成分 s(t) 有一个简单的波形,一直重复到结束。

一个试图仅通过趋势和季节性来表示该数据集的线性模型可能会将这两个特征组加性组合在一起(我们忽略模型中的所有其他成分)。这被称为加性季节性:

加性季节性

我将预期一个稍后的结果,并在这里展示将一个二次趋势(在这里几乎看起来是线性的)与一个加性季节性模型结合的输出,应用于数据集:

带有加性季节性的趋势

加性季节性在这里的问题很明显:模型已经学会了一个固定幅度的季节性,并且只是将其添加到趋势中。模型生成了一个由时间特征的权重决定的固定幅度波浪模式。

一种改进是乘性季节性。这是 Facebook Prophet 中的一个选项。假设季节性的幅度与趋势成正比。毫无疑问,对于某些数据集,这接*真实。乘性趋势公式是:

乘性季节性

这将很好地建模季节性,只要季节幅度确实与整体趋势成正比。在这种情况下,趋势本身建模季节成分的包络线。我们在这里不会演示这种情况,但稍后会显示这也不是一个很好的拟合。

但如果情况并非如此呢?当简单的加法或乘法都不是很好的拟合时,我们能做得更好吗?我们是否能有一个模型,学习季节特征的更一般的变化,这种变化不是恒定的,也不受限于与趋势的严格比例关系?

傅里叶分析

让我们看看澳大利亚波特兰水泥数据集中的季节成分,使用周期图绘图。这涉及使用 scipy.signal 中的 periodogram() 函数(所有代码都包含在附录笔记本中,链接在最后)。

scipy.signal.periodogram() 的作用是 —— 它查看一个周期性信号,并尝试估计一个傅里叶级数中的系数(比上面显示的级数公式更复杂一些),以便很好地逼*该信号。然后返回级数中的权重,可以在图中可视化。

这是应用于数据集的 scipy.signal.periodogram() 输出的绘图结果:

周期图

周期图展示了谱分量(季节性分量)的功率密度。数据集中最强的季节性分量是周期等于 4 个季度,或者 1 年的那个。这证实了视觉印象,即图中的最强上升和下降变化大约每十年发生 10 次。还有一个周期为 2 个季度的分量——这是相同的季节现象,这仅仅意味着 4 季度周期性不是一个简单的正弦波,而是具有更复杂的形状。还有其他周期为 10 个季度或更多的分量,但我们将忽略它们。

傅里叶声谱图

周期图将突出显示信号中的所有谱分量(数据中的所有季节性分量),并提供它们整体“强度”的概述,但它是对整个时间区间中任何分量“强度”的汇总。它没有说明每个季节性分量的“强度”如何在数据集中随时间变化。

为了捕捉这种变化,你必须使用傅里叶声谱图。它类似于周期图,但在整个数据集的多个时间窗口中反复执行。声谱图也作为 scipy 库中的一个方法提供。

让我们绘制上述提到的周期为 2 和 4 季度的季节性分量的声谱图。如往常一样,完整代码在最后链接的附属笔记本中。

spectrum = compute_spectrum(df['production'], 4, 0.1)
plot_spectrogram(spectrum, figsize_x=10)

声谱图

该图显示的是 2 季度和 4 季度分量随时间变化的“强度”,以及它们在不同时间点的变化最大幅度(在数字音乐合成中,这称为包络)。它们最初相当弱,但在 2010 年左右变得非常强,这与本文开头数据集图中的变化相符。

超越乘法季节性

正如我们下面将看到的,包络将以一种肯定比简单地将固定幅度的季节性添加到趋势中更好的方式来建模季节性分量的幅度。它也可以比仅将季节性幅度与整体趋势(如乘法季节性)绑定更通用——它将季节性与趋势完全解耦

换句话说,使用包络你可以继续使用基本的加法季节性模型,但季节性分量本身已经改变。它不再是固定幅度分量的线性组合。相反,它跟随信号中检测到的各种季节性分量的趋势(包络)。整体模型在季节性方面是加法的,但季节性被各种包络乘以。

假设你从傅里叶声谱图中提取季节性分量的包络 F,并对其进行*滑处理——*滑后的版本用 F-tilde 表示。那么,季节性分量的 one-hot 编码、虚拟编码和傅里叶编码变为:

带有组件包络的独热编码

带有组件包络的虚拟编码

带有组件包络的傅里叶级数

beta(p)、a(n)和 b(n)将是你的模型在拟合时间特征时学习的权重。F-tilde 系数(本身是时间序列)是组件包络,它们将在模型训练前与时间特征相乘,如下所示。

对于独热和虚拟编码,上述公式使用单一包络——基组件的周期 P。对于傅里叶和式,公式建议为每个组件提取一个单独的包络。实际中可能会有偏差,但这是另一个话题。

如果从所有特征中学习的总体模型是线性的,那么你仍然可以说你使用的是一种“加法季节性”(通过包络调整):

加法季节性

如果总体模型是随机森林或其他非线性模型,则上述简单公式不适用。

代码中的季节特征

我们来创建一些示例数据框,其中包含可以用于建模数据集季节性的季节特征,使用statsmodels.tsa.deterministic中的方法。

独热编码特征,每个季度一个,周期为 1 年:

seasonal_year = DeterministicProcess(index=df.index, constant=False, seasonal=True).in_sample()
print(seasonal_year)
 s(1,4)  s(2,4)  s(3,4)  s(4,4)
time                                  
1956Q1     1.0     0.0     0.0     0.0
1956Q2     0.0     1.0     0.0     0.0
1956Q3     0.0     0.0     1.0     0.0
1956Q4     0.0     0.0     0.0     1.0
1957Q1     1.0     0.0     0.0     0.0
...        ...     ...     ...     ...
2013Q1     1.0     0.0     0.0     0.0
2013Q2     0.0     1.0     0.0     0.0
2013Q3     0.0     0.0     1.0     0.0
2013Q4     0.0     0.0     0.0     1.0
2014Q1     1.0     0.0     0.0     0.0

[233 rows x 4 columns]

如果你想使用虚拟编码,只需删除一列。还可以探索DeterministicProcess()中的drop=True选项,该选项将检查完全共线性并尝试做出正确的决定(删除/不删除)。

年度正弦-余弦特征对:

cfr = CalendarFourier(freq='Y', order=2)
seasonal_year_trig = DeterministicProcess(index=df.index, seasonal=False, additional_terms=[cfr]).in_sample()
with pd.option_context('display.max_columns', None, 'display.expand_frame_repr', False):
    print(seasonal_year_trig)
 sin(1,freq=A-DEC)  cos(1,freq=A-DEC)  sin(2,freq=A-DEC)  cos(2,freq=A-DEC)
time                                                                              
1956Q1           0.000000           1.000000           0.000000           1.000000
1956Q2           0.999963           0.008583           0.017166          -0.999853
1956Q3           0.017166          -0.999853          -0.034328           0.999411
1956Q4          -0.999963          -0.008583           0.017166          -0.999853
1957Q1           0.000000           1.000000           0.000000           1.000000
...                   ...                ...                ...                ...
2013Q1           0.000000           1.000000           0.000000           1.000000
2013Q2           0.999769           0.021516           0.043022          -0.999074
2013Q3           0.025818          -0.999667          -0.051620           0.998667
2013Q4          -0.999917          -0.012910           0.025818          -0.999667
2014Q1           0.000000           1.000000           0.000000           1.000000

[233 rows x 4 columns]

在本文其余部分,我们将使用独热编码,尽管我们拟合的模型是LinearRegression()。对于这个数据集,statsmodels检查了时间序列目标与DeterministicProcess(drop=True)的共线性,并认为没有必要删除一列(不切换到虚拟编码)。当然,你可以自由比较独热编码和虚拟编码在你的数据集和模型中的效果。

让我们看看在这些季节特征上拟合线性模型会发生什么。

拟合线性模型

我们来创建趋势特征(称为 const、trend 和 trend_squared 的列),然后将它们与上述生成的seasonal_year特征连接起来:

trend_order = 2
trend_year = DeterministicProcess(index=df.index, constant=True, order=trend_order).in_sample()
X = trend_year.copy()
X = X.join(seasonal_year)
 const  trend  trend_squared  s(1,4)  s(2,4)  s(3,4)  s(4,4)
time                                                               
1956Q1    1.0    1.0            1.0     1.0     0.0     0.0     0.0
1956Q2    1.0    2.0            4.0     0.0     1.0     0.0     0.0
1956Q3    1.0    3.0            9.0     0.0     0.0     1.0     0.0
1956Q4    1.0    4.0           16.0     0.0     0.0     0.0     1.0
1957Q1    1.0    5.0           25.0     1.0     0.0     0.0     0.0
...       ...    ...            ...     ...     ...     ...     ...
2013Q1    1.0  229.0        52441.0     1.0     0.0     0.0     0.0
2013Q2    1.0  230.0        52900.0     0.0     1.0     0.0     0.0
2013Q3    1.0  231.0        53361.0     0.0     0.0     1.0     0.0
2013Q4    1.0  232.0        53824.0     0.0     0.0     0.0     1.0
2014Q1    1.0  233.0        54289.0     1.0     0.0     0.0     0.0

[233 rows x 7 columns]

这是用于训练/验证模型的 X 数据框(特征)。我们用二次趋势特征对数据进行建模,加上需要的 4 个时间特征以捕捉年度季节性。y 数据框(目标)将仅为水泥生产数字。

让我们从数据中划分出一个包含 2010 年观察值的验证集。我们将在上述 X 特征和由水泥生产表示的 y 目标(训练数据集的那部分,即划分出验证数据后剩下的部分)上拟合一个线性模型,然后评估模型在验证中的表现。

我们还将用另一个仅趋势模型进行上述所有操作,该模型只会拟合趋势特征而忽略季节性——只是为了展示整体趋势是什么。

def do_forecast(X, index_train, index_test, trend_order):
    X_train = X.loc[index_train]
    X_test = X.loc[index_test]

    y_train = df['production'].loc[index_train]
    y_test = df['production'].loc[index_test]

    model = LinearRegression(fit_intercept=False)
    _ = model.fit(X_train, y_train)
    y_fore = pd.Series(model.predict(X_test), index=index_test)
    y_past = pd.Series(model.predict(X_train), index=index_train)

    trend_columns = X_train.columns.to_list()[0 : trend_order + 1]
    model_trend = LinearRegression(fit_intercept=False)
    _ = model_trend.fit(X_train[trend_columns], y_train)
    y_trend_fore = pd.Series(model_trend.predict(X_test[trend_columns]), index=index_test)
    y_trend_past = pd.Series(model_trend.predict(X_train[trend_columns]), index=index_train)

    RMSLE = mean_squared_log_error(y_test, y_fore, squared=False)
    print(f'RMSLE: {RMSLE}')

    ax = df.plot(**plot_params, title='AUS Cement Production - Forecast')
    ax = y_past.plot(color='C0', label='Backcast')
    ax = y_fore.plot(color='C3', label='Forecast')
    ax = y_trend_past.plot(ax=ax, color='C0', linewidth=3, alpha=0.333, label='Trend Past')
    ax = y_trend_fore.plot(ax=ax, color='C3', linewidth=3, alpha=0.333, label='Trend Future')
    _ = ax.legend()

do_forecast(X, index_train, index_test, trend_order)
RMSLE: 0.03846449744356434

模型验证

我们之前见过这种情况。蓝色是训练数据,红色是验证数据。完整模型是锐利、细的线。仅趋势模型是宽而模糊的线。

对于一个简单的模型来说,这并不差,但有一个明显的问题:模型学到了一个恒定振幅的年度季节性。尽管年度变化实际上随着时间的推移而增加,模型只能保持固定振幅的变化。显然,这是因为我们只给了模型固定振幅的季节性特征,并且没有其他特征(目标滞后等)来帮助它克服这个问题(这是故意为之,以突出这里描述的技术的重要性)。固定振幅的加法季节性在这里效果不佳。

也许通过傅里叶分析获得的季节性组件的振幅信息(包络)会改善性能?

调整季节性组件

让我们选择四季度季节性组件。如上面的频谱图所示,其包络非常嘈杂,因此我们可以在包络的order=2趋势上拟合一个线性模型(我们称之为包络模型),在训练数据上使用model.fit()来*滑包络,然后我们将使用model.predict()将该趋势扩展到验证/测试数据中:

envelope_features = DeterministicProcess(index=X.index, constant=True, order=2).in_sample()

spec4_train = compute_spectrum(df['production'].loc[index_train], max_period=4)
spec4_train

spec4_model = LinearRegression()
spec4_model.fit(envelope_features.loc[spec4_train.index], spec4_train['4.0'])
spec4_regress = pd.Series(spec4_model.predict(envelope_features), index=X.index)

ax = spec4_train['4.0'].plot(label='component envelope', color='gray')
spec4_regress.loc[spec4_train.index].plot(ax=ax, color='C0', label='envelope regression: past')
spec4_regress.loc[index_test].plot(ax=ax, color='C3', label='envelope regression: future')
_ = ax.legend()

包络拟合

灰色线是包络本身——它非常嘈杂。蓝色线是包络,是训练数据中四季度季节性组件的变化强度,拟合(*滑)为二次趋势(order=2)。红色线是相同的内容,在验证数据上延展(预测)。这就是通过回归进行*滑。

你可能会注意到,虽然整体趋势被二次模型建模为接*线性,这个包络明显是非线性的。乘法季节性可能不适合这些数据。

我们已经建模了四季度季节性组件随时间的变化。让我们从包络模型(包络调整的傅里叶级数公式中的 F-tilde)中取出输出,并将其乘以对应于四季度季节性组件的时间特征:

spec4_regress = spec4_regress / spec4_regress.mean()

season_columns = ['s(1,4)', 's(2,4)', 's(3,4)', 's(4,4)']
for c in season_columns:
    X[c] = X[c] * spec4_regress
print(X)
 const  trend  trend_squared    s(1,4)    s(2,4)    s(3,4)    s(4,4)
time                                                                       
1956Q1    1.0    1.0            1.0  0.179989  0.000000  0.000000  0.000000
1956Q2    1.0    2.0            4.0  0.000000  0.181109  0.000000  0.000000
1956Q3    1.0    3.0            9.0  0.000000  0.000000  0.182306  0.000000
1956Q4    1.0    4.0           16.0  0.000000  0.000000  0.000000  0.183581
1957Q1    1.0    5.0           25.0  0.184932  0.000000  0.000000  0.000000
...       ...    ...            ...       ...       ...       ...       ...
2013Q1    1.0  229.0        52441.0  2.434701  0.000000  0.000000  0.000000
2013Q2    1.0  230.0        52900.0  0.000000  2.453436  0.000000  0.000000
2013Q3    1.0  231.0        53361.0  0.000000  0.000000  2.472249  0.000000
2013Q4    1.0  232.0        53824.0  0.000000  0.000000  0.000000  2.491139
2014Q1    1.0  233.0        54289.0  2.510106  0.000000  0.000000  0.000000

[233 rows x 7 columns]

四个时间特征不再是 0 或 1 了。它们已经被乘以组件包络(公式中的 F-tilde),现在它们的振幅随时间变化,就像包络一样。

重新训练模型

让我们重新训练主模型,现在使用修改后的时间特征。

do_forecast(X, index_train, index_test, trend_order)
RMSLE: 0.02546321729737165

模型验证,调整时间虚拟变量

我们并不追求完美拟合,因为这只是一个用于演示技术的玩具模型,但显然模型表现更好,更接*目标的四个季度变化。性能指标也提高了 51%,这还不错。

显然,整体趋势建模效果不佳。高阶趋势可能效果更好。或者像 Facebook Prophet 那样添加拐点也可能效果更好。可以随意尝试。

进一步的改进绝对是可能的(例如,具有大量时间滞后的自回归模型拟合效果会更好),但这不是本文的主题——我们这里只关注季节性方面。

最终评论

提高预测性能是一个广泛的话题。任何模型的行为都不依赖于单一特征或单一技术。然而,如果你希望从给定模型中提取所有可能的信息,应该提供有意义的特征。独热编码或虚拟编码,或正弦/余弦傅里叶对,当它们反映了季节性随时间的变化时,更具意义。

通过傅里叶变换调整季节性特征的包络对线性模型特别有效。提升树的效果不如线性模型明显,但无论使用什么模型,你几乎都可以看到改进。如果你使用集成模型,你可能会在堆栈底部有一个简单的模型,如线性回归,你可能希望在其他模型从其残差中学习之前提升其性能(假设集成模型是这样配置的)。

同样重要的是,你用来调整季节性特征的包络模型只能在训练数据上训练,在训练期间不能看到任何测试数据,就像其他模型一样。一旦你调整了包络,时间特征就包含了来自目标的信息——它们不再是可以在任何任意预测范围内预先计算的纯确定性特征。因此,如果你不小心,信息可能会通过时间特征从验证/测试阶段泄漏回训练数据(纯确定性时间特征不会有这个问题)。

最后,包络本身是时间序列,可能需要进行预测,因为它们建模的是现实世界的过程或现象。这是一个独立的讨论。

链接

本文使用的数据集可以在这里以公共领域(CC0)许可证获取:

[## KEY2STATS

© 2023 KEY2STATS - RStudio® 是 RStudio, Inc. 的注册商标。AP® 是 College 的注册商标…

www.key2stats.com](https://www.key2stats.com/data-set/view/776?source=post_page-----18c792102047--------------------------------)

本文中使用的代码可以在这里找到:

[## misc/seasonal_features_fourier_article/cement.ipynb at master · FlorinAndrei/misc

随机杂项。通过在 GitHub 上创建帐户贡献给 FlorinAndrei/misc。

github.com

在 Kaggle 上提交给 Store Sales — 时间序列预测竞赛的笔记本,使用了本文中描述的想法:

[## 傅里叶谱图,集成模型

使用 Kaggle Notebooks 探索并运行机器学习代码 | 使用 Store Sales 数据进行时间序列预测

www.kaggle.com

这里是一个 GitHub 仓库,包含提交给 Kaggle 的笔记本的开发版本:

[## GitHub - FlorinAndrei/timeseries-forecasting-fourier-time-dummies: 带有傅里叶...

带有傅里叶调整时间虚拟变量的时间序列预测 - GitHub...

github.com

Facebook Prophet 论文 — 大规模预测:

peerj.com/preprints/3190.pdf

本文中使用的所有图片和代码均由作者创建。

现代数据工程

原文:towardsdatascience.com/modern-data-engineering-e202776fb9a9

*台特定工具和高级技术

💡Mike ShakhomirovTowards Data Science 💡Mike Shakhomirov

·发布于 Towards Data Science ·阅读时间 12 分钟·2023 年 11 月 4 日

--

图片由 Christopher Burns 提供,来源于 Unsplash

现代数据生态系统不断演变,新数据工具时不时出现。在这篇文章中,我想谈谈影响数据工程师的关键因素。我们将讨论如何利用这些知识来推动先进的分析管道和卓越的运营。

我想讨论一些流行的数据工程问题:

  • 现代数据工程(DE)。它是什么?

  • 你的数据工程(DE)是否足够好,能为先进的数据管道和商业智能(BI)提供支持?

  • 你的数据管道是否高效?

  • 从技术角度来看,实现卓越运营需要什么?

在十月份,我写了关于数据工程师的崛起、角色、挑战、职责、日常工作以及如何在这个领域取得成功。数据工程领域在不断变化,但主要趋势似乎保持不变。

## 如何成为数据工程师

2024 年初学者的捷径

towardsdatascience.com

作为一名数据工程师,我几乎每天都需要设计高效的数据流程。因此,这里有一些需要考虑的事项,可以帮助我们解答这些问题。

现代数据工程趋势

  • ETL 与 ELT

  • 简化的数据连接器和 API 集成

  • ETL 框架的爆炸性增长

  • 数据基础设施即代码

  • 数据网格与去中心化数据管理

  • 利用 AI 进行商业智能管道的民主化

  • 关注数据素养

ELT 与 ETL

受欢迎的 SQL 数据转换工具,如DataformDBT,对 ELT 方法的普及做出了重要贡献[1]。在存储数据的地方执行所需的数据转换,如清洗、丰富和提取,显得非常合理。通常,这是一种位于我们基础设施中心的数据仓库解决方案(DWH)。云*台领导者使 DWH(Snowflake、BigQuery、Redshift、Firebolt)的基础设施管理变得非常简单,在许多场景中,它们在成本效益和速度方面将优于专门的内部基础设施管理团队。

数据仓库示例。作者提供的图片

它也可能是中心的数据湖,这取决于我们的数据*台类型和使用的工具。在这种情况下,SQL 在许多情况下不再是一个选项,使得那些不熟悉编程的用户难以查询数据。像 Databricks、Tabular 和 Galaxy 这样的工具试图解决这个问题,感觉它确实是未来的发展方向。确实,数据湖可以存储所有类型的数据,包括非结构化数据,我们仍然需要能够分析这些数据集。

数据湖示例。作者提供的图片。

想象一下具有事务一致性的 数据湖表和时间点快照隔离。

我之前在关于 Apache Iceberg 表格式的故事中写过这方面的内容[2]。

## Apache Iceberg 表介绍

选择 Apache Iceberg 作为数据湖的几个有力理由

towardsdatascience.com

简化的数据集成

FivetranStitch这样的托管解决方案被创建以轻松管理第三方 API 集成。如今,许多公司选择这种方法来简化与外部数据源的互动。这将是数据分析团队中不熟悉编码的人员的正确选择。

确实,如果数据连接器已经存在并且在云中管理,我们为什么还要从头开始构建呢?

不过这种方法的缺点是其定价模型。

很多时候它是基于行的,并且在企业级数据摄取(即大数据管道)上可能变得相当昂贵。这就是开源替代方案发挥作用的地方。像AirbyteMeltano这样的框架可能是部署数据源集成微服务的简单快捷解决方案。

如果你没有时间学习新的 ETL 框架,你可以自己创建一个简单的数据连接器。如果你懂一点 Python,这将是一个微不足道的任务。在我之前的一篇文章中,我写了如何轻松创建一个从 NASA API 拉取数据的微服务[3]:

## 数据工程师的 Python

初学者的高级 ETL 技术

towardsdatascience.com

考虑app.py中的这段代码

import requests
session = requests.Session()

url="https://api.nasa.gov/neo/rest/v1/feed"
apiKey="your_api_key"
requestParams = {
    'api_key': apiKey,
    'start_date': '2023-04-20',
    'end_date': '2023-04-21'
}
response = session.get(url, params = requestParams, stream=True)
print(response.status_code)

它可以部署在任何云服务商*台上,并按所需频率调度运行。使用类似Terraform的工具来部署我们的数据管道应用程序始终是一个好的实践。

ETL 框架的爆炸性增长

我们可以见证各种 ETL 框架在数据提取和转换中的“寒武纪大爆发”。许多框架都是开源的,并且基于 Python,这并不令人惊讶。

Luigi [8]就是其中之一,它帮助创建 ETL 管道。它由 Spotify 创建,用于管理大规模数据处理工作负载。它具有命令行界面和出色的可视化功能。然而,即使是基本的 ETL 管道也需要一定的 Python 编程技能。从我的经验来看,它适合严格且直接的管道。我发现使用 Luigi 实现复杂的分支逻辑特别困难,但它在许多场景中表现良好。

Python ETL (PETL) [9]是最广泛使用的开源 ETL 框架之一,用于简单的数据转换。在处理表格、从外部数据源提取数据以及对数据执行基本 ETL 时,它非常宝贵。在许多方面,它与Pandas类似,但后者在后台具有更多的分析功能。PETL 非常适合聚合和行级 ETL。

Bonobo [10]是另一个开源的轻量级数据处理工具,非常适合快速开发、自动化和批处理数据管道的并行执行。我喜欢它的一点是,它让处理各种数据文件格式变得非常简单,例如 SQL、XML、XLS、CSV 和 JSON。对于那些 Python 知识有限的人来说,它将是一个很好的工具。在其他好处中,我喜欢它对半复杂数据模式的良好支持。它非常适合简单的 ETL,并且可以在 Docker 容器中运行(它有一个 Docker 扩展)。

Pandas在数据领域中绝对是一个巨头,在这个故事中没有必要详细介绍它的能力。值得一提的是,它的数据框架转换已被纳入许多现代数据仓库的基本数据加载方法之一。考虑将数据加载到 BigQuery 数据仓库解决方案中的示例:

from google.cloud import bigquery
from google.oauth2 import service_account
...
# Authenticate BigQuery client:
service_acount_str = config.get('BigQuery') # Use config
credentials = service_account.Credentials.from_service_account_info(service_acount_str)
client = bigquery.Client(credentials=credentials, project=credentials.project_id)

...
def load_table_from_dataframe(table_schema, table_name, dataset_id):
    #! source data file format must be outer array JSON:
    """
    [
    {"id":"1"},
    {"id":"2"}
    ]
    """
    blob = """
            [
    {"id":"1","first_name":"John","last_name":"Doe","dob":"1968-01-22","addresses":[{"status":"current","address":"123 First Avenue","city":"Seattle","state":"WA","zip":"11111","numberOfYears":"1"},{"status":"previous","address":"456 Main Street","city":"Portland","state":"OR","zip":"22222","numberOfYears":"5"}]},
    {"id":"2","first_name":"John","last_name":"Doe","dob":"1968-01-22","addresses":[{"status":"current","address":"123 First Avenue","city":"Seattle","state":"WA","zip":"11111","numberOfYears":"1"},{"status":"previous","address":"456 Main Street","city":"Portland","state":"OR","zip":"22222","numberOfYears":"5"}]}
    ]
    """
    body = json.loads(blob)
    print(pandas.__version__)

    table_id = client.dataset(dataset_id).table(table_name)
    job_config = bigquery.LoadJobConfig()
    schema = create_schema_from_yaml(table_schema) 
    job_config.schema = schema

    df = pandas.DataFrame(
    body,
    # In the loaded table, the column order reflects the order of the
    # columns in the DataFrame.
    columns=["id", "first_name","last_name","dob","addresses"],

    )
    df['addresses'] = df.addresses.astype(str)
    df = df[['id','first_name','last_name','dob','addresses']]

    print(df)

    load_job = client.load_table_from_dataframe(
        df,
        table_id,
        job_config=job_config,
    )

    load_job.result()
    print("Job finished.")

例如,Apache Airflow并不是一个 ETL 工具,但它有助于将我们的 ETL 管道组织成依赖关系图(DAGs)的可视化,以描述任务之间的关系。典型的 Airflow 架构包括基于元数据的调度器、执行器、工作节点和任务。

例如,我们可以在将数据导出到云存储(bq_export_op)后运行 ml_engine_training_op,并使此工作流每日或每周运行。

使用 Airflow 训练 ML 模型。图像由作者提供。

请考虑下面的这个例子。

它创建了一个简单的数据管道图,将数据导出到云存储桶中,然后使用 MLEngineTrainingOperator 训练 ML 模型。

"""DAG definition for recommendation_bespoke model training."""

import airflow
from airflow import DAG
from airflow.contrib.operators.bigquery_operator import BigQueryOperator
from airflow.contrib.operators.bigquery_to_gcs import BigQueryToCloudStorageOperator
from airflow.hooks.base_hook import BaseHook
from airflow.operators.app_engine_admin_plugin import AppEngineVersionOperator
from airflow.operators.ml_engine_plugin import MLEngineTrainingOperator

import datetime

def _get_project_id():
  """Get project ID from default GCP connection."""

  extras = BaseHook.get_connection('google_cloud_default').extra_dejson
  key = 'extra__google_cloud_platform__project'
  if key in extras:
    project_id = extras[key]
  else:
    raise ('Must configure project_id in google_cloud_default '
           'connection from Airflow Console')
  return project_id

PROJECT_ID = _get_project_id()

# Data set constants, used in BigQuery tasks.  You can change these
# to conform to your data.
DATASET = 'staging' #'analytics'
TABLE_NAME = 'recommendation_bespoke'

# GCS bucket names and region, can also be changed.
BUCKET = 'gs://rec_wals_eu'
REGION = 'us-central1' #'europe-west2' #'us-east1'
JOB_DIR = BUCKET + '/jobs'

default_args = {
    'owner': 'airflow',
    'depends_on_past': False,
    'start_date': airflow.utils.dates.days_ago(2),
    'email': ['mike.shakhomirov@gmail.com'],
    'email_on_failure': True,
    'email_on_retry': False,
    'retries': 5,
    'retry_delay': datetime.timedelta(minutes=5)
}

# Default schedule interval using cronjob syntax - can be customized here
# or in the Airflow console.
schedule_interval = '00 21 * * *'

dag = DAG('recommendations_training_v6', default_args=default_args,
          schedule_interval=schedule_interval)

dag.doc_md = __doc__

#
#
# Task Definition
#
#

# BigQuery training data export to GCS

training_file = BUCKET + '/data/recommendations_small.csv' # just a few records for staging

t1 = BigQueryToCloudStorageOperator(
    task_id='bq_export_op',
    source_project_dataset_table='%s.recommendation_bespoke' % DATASET,
    destination_cloud_storage_uris=[training_file],
    export_format='CSV',
    dag=dag
)

# ML Engine training job
training_file = BUCKET + '/data/recommendations_small.csv'
job_id = 'recserve_{0}'.format(datetime.datetime.now().strftime('%Y%m%d%H%M'))
job_dir = BUCKET + '/jobs/' + job_id
output_dir = BUCKET
delimiter=','
data_type='user_groups'
master_image_uri='gcr.io/my-project/recommendation_bespoke_container:tf_rec_latest'

training_args = ['--job-dir', job_dir,
                 '--train-file', training_file,
                 '--output-dir', output_dir,
                 '--data-type', data_type]

master_config = {"imageUri": master_image_uri,}

t3 = MLEngineTrainingOperator(
    task_id='ml_engine_training_op',
    project_id=PROJECT_ID,
    job_id=job_id,
    training_args=training_args,
    region=REGION,
    scale_tier='CUSTOM',
    master_type='complex_model_m_gpu',
    master_config=master_config,
    dag=dag
)

t3.set_upstream(t1)

Bubbles [11] 是另一个用于 Python 世界中 ETL 的开源工具。它非常适合快速开发,我喜欢它如何使用元数据来描述数据管道。Bubbles 的创建者称其为“抽象框架”,并表示它可以从许多其他编程语言中使用,而不仅仅是 Python。

还有许多其他工具具有更具体的应用,例如从网页中提取数据(PyQuery、BeautifulSoup 等)和并行数据处理。这可以是另一个话题,但我之前写过一些相关内容,例如 joblib 库 [12]。

数据基础设施即代码

基础设施即代码(IaC)是一种流行且非常实用的方法,用于管理数据*台资源。即使是数据方面,目前这几乎已经成为标准,而且在简历上提到你熟悉 DevOps 标准绝对会显得很棒。使用像 Terraform(*台无关)和 CloudFormation 这样的工具,我们可以轻松地将我们的开发工作和部署(运维)进行集成。

通常,我们希望为数据管道设置测试环境和生产环境。这有助于测试我们的管道,并促进团队之间的协作。

请查看下面的图表。它解释了数据环境的工作原理。

数据环境。图像由作者提供。

我们经常需要一个额外的沙箱来进行测试,或在 ETL 服务触发 CI/CD 工作流时运行数据转换单元测试。

我之前在这里写过相关内容:

[## 初学者的基础设施即代码

像专业人士一样使用这些模板部署数据管道

levelup.gitconnected.com](https://levelup.gitconnected.com/infrastructure-as-code-for-beginners-a4e36c805316?source=post_page-----e202776fb9a9--------------------------------)

使用 AWS CloudFormation 模板文件,我们可以描述所需的资源及其依赖关系,这样我们可以将它们作为一个整体堆栈一起启动和配置。

如果你是一位数据专家,这种方法肯定会帮助你更好地处理不同的数据环境,并更快速、更一致地复制数据*台资源而不出错。

问题在于许多数据从业者对 IaC 并不熟悉,这在开发过程中会产生很多错误。

数据网格和去中心化数据管理

数据空间在过去十年里发生了显著变化,现在我们有很多数据工具和框架。数据网格定义了这样一种状态:我们拥有不同的数据领域(公司部门),每个领域都有自己的团队和共享的数据资源。每个团队都有自己的目标、关键绩效指标(KPI)、数据角色和职责。

长期以来,数据官僚主义一直是许多公司的真正痛点。

这种数据*台类型[4]可能看起来有些混乱,但它旨在成为一个成功且高效的选择,尤其适用于去中心化的公司,在这种结构下,不同的团队可以自行访问跨领域的数据集并执行分析或 ETL 任务。

的确,如果你是数据分析师而不熟悉 Spark,Snowflake 可能是你最喜欢的数据仓库解决方案。然而,当你可能希望在没有数据工程帮助的情况下读取数据湖数据时,通常这会是一个微不足道的问题。在这种情况下,大量的数据集元数据记录可能非常有用,这也是数据网格如此成功的原因。

这使得用户了解数据及其来源,并了解其他团队如何充分利用他们之前不知道的数据集。

有时数据集和数据源连接变得非常复杂,因此始终保持一个包含元数据和数据集描述的单一真实数据仓库是一个好习惯。

在我之前的一篇故事[5]中,我写到了 SQL 作为团队和数据的统一查询语言的作用。确实,它具有分析性、自描述性,甚至可以是动态的,这使得它成为所有数据用户的完美工具。

这往往变成一团糟

这一事实使得基于 SQL 的模板引擎如 DBT、Jinja 和 Dataform 非常受欢迎。试想一下,你拥有一个类似 SQL 的*台,其中所有数据集及其转换都被彻底描述和定义[6]。

Dataform 的依赖图和元数据。图片来源:作者。

理解数据团队如何与数据源和模式关联可能是一个很大的挑战。通常,这些都纠缠在数据集依赖关系和 ETL 转换的复杂网络中。

数据工程在指导、提升数据素养以及通过最先进的数据处理技术和最佳实践来赋能公司其他部门方面发挥了关键作用。

使用 AI 进行商业智能管道的民主化

改善数据可访问性一直是数据领域的热门话题,但有趣的是,整个数据管道设计过程正变得越来越容易被那些之前不熟悉数据的团队所接触。现在,几乎每个部门都可以利用内置的 AI 能力,在数据上创建复杂的商业智能转换。

他们所需要的只是用自己的话描述他们在商业智能方面的需求。

例如,像Thoughspot这样的 BI 工具使用具有直观“Google 风格搜索界面”的 AI [7] 来从存储在任何现代数据仓库解决方案(如 Google Big Query、Redshift、Snowflake 或 Databricks)中的数据中获得洞察。

现代数据栈包括帮助数据建模和可视化的 BI 工具。许多工具已经具备了这些内置的 AI 功能,以根据用户行为更快地获得数据洞察。

我相信将 GPT 与 BI 集成是一项相当简单的任务。在接下来的几年中,我们将看到许多新产品使用这项技术。

GPT 可以预处理文本数据,以生成理解您的意图并回答您的问题的 SQL 查询。

结论

在这篇文章中,我试图给出一个关于当前数据工程角色所面临的主要数据趋势的高级概述。数据网格和带有依赖关系图的模板化 SQL 使数据素养的普及成为可能,从而使整个分析过程民主化。先进的数据管道以及复杂的 ETL 技术和转换现在对组织中的每个人都是透明的。数据管道正变得越来越易于其他团队访问,他们不需要了解编程就能学习和理解 ETL 的复杂性。数据网格和元数据有助于解决这个问题。从我的经验来看,我发现越来越多的人学习 SQL 以参与转型层。诞生于“高级数据分析”时代的公司享有轻松访问云供应商产品及其托管服务的奢侈。它确实有助于获得所需的数据技能并加以提升,以获得竞争优势。

推荐阅读

[1] medium.com/towards-data-science/data-pipeline-design-patterns-100afa4b93e3

[2] towardsdatascience.com/introduction-to-apache-iceberg-tables-a791f1758009

[3] towardsdatascience.com/python-for-data-engineers-f3d5db59b6dd

[4] medium.com/towards-data-science/data-platform-architecture-types-f255ac6e0b7

[5] medium.com/towards-data-science/advanced-sql-techniques-for-beginners-211851a28488

[6] medium.com/towards-data-science/easy-way-to-create-live-and-staging-environments-for-your-data-e4f03eb73365

[7] docs.thoughtspot.com/cloud/latest/search-sage

[8] github.com/spotify/luigi

[9] petl.readthedocs.io/en/stable/

[10] www.bonobo-project.org

[11] bubbles.databrewery.org/

[12] medium.com/towards-data-science/how-to-become-a-data-engineer-c0319cb226c2

现代数据仓库

原文:towardsdatascience.com/modern-data-warehousing-2b1b0486ce4a

先进的数据*台设计

💡Mike ShakhomirovTowards Data Science 💡Mike Shakhomirov

·发布于 Towards Data Science ·12 分钟阅读·2023 年 12 月 16 日

--

图片由 Nubelson Fernandes 提供,来源于 Unsplash

在这个故事中,我将尝试阐明现代数据仓库解决方案(DWH)相对于其他数据*台架构类型的好处。我敢说,目前 DWH 是数据工程师中最受欢迎的*台。与其他解决方案类型相比,它提供了宝贵的好处,但也有一些众所周知的局限性。想学习数据工程吗?这个故事是一个很好的起点,因为它解释了数据工程的核心——架构图中心的 DWH 解决方案。我们将看看市场上不同 DWH 的数据如何被摄取和转换。

我也希望能与经验丰富的用户展开讨论。了解你的意见并听听你对此话题的看法将非常棒。

数据仓库的关键特性

无服务器的分布式 SQL 引擎(BigQuery、Snowflake、Redshift、Microsoft Azure Synapse、Teradata)就是我们所称的现代数据仓库(DWH)。这是一种以 SQL 为主的数据架构 [1],数据存储在数据仓库中,我们可以利用非规范化星型模式 [2] 数据集的所有优点,因为大多数现代数据仓库都是分布式的,扩展性良好,这意味着无需担心表的键和索引。它非常适合对大数据进行即席分析查询。

## 数据*台架构类型

它在多大程度上满足了你的业务需求?选择的困境。

towardsdatascience.com

大多数现代数据仓库解决方案可以处理结构化和非结构化数据,并且对拥有良好 SQL 技能的数据分析师非常方便。

DWH 数据生命周期。图像由作者提供。

现代数据仓库可以轻松与商业智能解决方案集成,如 Looker、Tableau、Sisense 和 Mode,它们使用 ANSI-SQL 处理数据。在下面的图示中,我试图映射一个常见的数据转换过程及使用的工具(当然这不是完整的列表)。我们可以看到 DWH 在中间。

典型的数据旅程及使用的工具。图像由作者提供。

数据仓库并不设计用于存储非结构化数据,如图像、视频或文档。

为此,我们会希望使用数据湖。

数据仓库与数据库:有什么区别?

数据仓库具有列式数据结构,与许多 RDS 相同,它是关系型的。数据被组织成表格、行和列。然而,在 RDS 中,数据是按行组织和存储的,而数据仓库中的数据则是按列存储的。后者更好地支持在线分析处理(OLAP),而 RDS 只能提供在线事务处理(OLTP)。RDS 确实更加面向事务。一些现代的数据仓库解决方案可以同时提供这两种数据处理方法。例如,AWS Redshift 支持数据仓库和数据湖方法,使其能够访问和分析大量数据。

关系型数据库(RDS)将数据存储在一个基于行的表格中,列连接相关的数据元素。它的设计和优化是为了快速提取当前数据。流行的关系型数据库有PostgreSQL、MySQL、Microsoft SQL Server 和 Oracle。 RDBMS 是一个关系型数据库管理系统,帮助管理数据库。

NoSQL 数据库支持仅简单事务,而关系型数据库还支持复杂事务及连接操作。NoSQL 数据库用于处理高速到达的数据。流行的 NoSQL 数据库有 MongoDB 和 CouchDB(文档数据库),Redis 和 DynamoDB(键值数据库)。

数据仓库主要用于数据分析,包括大量的历史数据。使用数据仓库需要用户提前创建一个预定义的、固定的模式,这有助于数据分析。在处理数据仓库时,表格必须简单(去规范化),以便计算大量数据。由于 RDS 数据库表和连接是规范化的,所以它们比较复杂。因此,传统数据库和数据仓库之间的主要区别在于,传统数据库的设计和优化是为了记录数据,而数据仓库的设计和优化则是为了响应分析需求。

RDS 存储应用程序所需的当前数据。在运行应用程序时,当需要快速获取一些当前数据时,它非常有用。

数据仓库与数据湖

数据湖是存储大量非结构化数据(如图像、视频和文档)以及结构化数据(如 JSON、CSV、PARQUET 和 AVRO [3])的理想存储解决方案。

大数据文件格式解析

Parquet vs ORC vs AVRO vs JSON。选择哪个以及如何使用它们?

[towardsdatascience.com

然而,从数据湖中提取洞察通常需要编码技能,因为数据湖没有像数据仓库那样内置的分析或查询能力。用户需要利用编程语言如 Python、JAVA、Scala 或 PySpark 来访问、处理和分析存储在数据湖中的数据。

当数据湖用户拥有良好的编码技能时,会出现惊人的好处

在这种情况下,数据湖架构可以提供数据处理的最高灵活性。用户只需了解如何编码即可应用相关的数据转换。

然而,通常情况下,SQL 优先的解决方案变得更有用。

数据仓库的主要优点

作为用于 SQL 查询和报告的集中数据仓库,数据仓库具有许多类似于传统关系数据库解决方案的特征。数据仓库的一些主要优点包括更好的可扩展性(相比于 RDS)、更好的数据治理(相比于数据湖和数据网格架构)、增强的商业智能和改善的数据质量 [4]

自动化邮件和数据质量检查

数据仓库指南,帮助通过定期邮件获得更好、更清洁的数据

[towardsdatascience.com

更好的数据治理:市场上的许多数据仓库解决方案提供了列级访问控制和行级访问控制。这意味着我们可以为用户定义细粒度的控制。例如,在 BigQuery 中,我们可以限制访问或遮蔽任何对业务或个人敏感的列 [5]:

使用策略标签的数据遮蔽。图片由作者提供。

我们可以使用基础设施即代码(IaC)来定义这些策略,这类似于我们在部署基础设施资源时所做的 [6]。在下面的示例中,我们可以使用与*台无关的 Terraform 来定义数据集访问权限:

resource "google_bigquery_dataset_access" "access" {
  dataset_id    = google_bigquery_dataset.dataset.dataset_id
  role          = "OWNER"
  user_by_email = google_service_account.bqowner.email
}

resource "google_bigquery_dataset" "dataset" {
  dataset_id = "my_dataset"
}

resource "google_service_account" "bqowner" {
  account_id = "bqowner"
} 

更好的协作: 现代数据仓库解决方案提供协作功能。有效的决策通常需要组织内部多个人员(如数据分析师、市场营销团队、管理层等)的输入,以及多个数据来源。确实,将敏捷方法应用于任何 数据转换开发 是至关重要的。我称之为最佳实践。

协作是关键

数据仓库解决方案设计使得通过将大型数据项目拆分成更小的部分来交付这些项目变得更容易。从我所看到的,组织倾向于摆脱数据项目设计和交付中繁琐的瀑布式方法。现在我们拥有现代数据仓库解决方案提供的单一数据集成层,这有助于更快地部署增量模型更新,并更频繁地提供业务洞察。

可扩展性:数据仓库被设计为能够很好地扩展,以处理大量数据。在必要时扩展数据管道以满足不断增长的业务需求至关重要。它还必须能够在单一系统中以规模化方式运行并发工作负载。例如,一个非常常见的数据仓库痛点是用户查询的并发性。当数据仓库解决方案只允许一定数量的并发用户查询(通常不超过 50)时,就会出现这种情况。许多现代数据仓库可以提供带有分布式物理节点的虚拟集群来解决这个问题[11]。

确实,在许多场景中,具有自动扩展的分布式计算集群帮助很大。

例如,考虑以下 Snowflake 的 SQL 多集群设置:

使用 Snowflake 的多集群数据仓库。图像由作者提供。

改进的数据加载: 数据仓库解决方案各不相同。对于其中一些,数据摄取是一个微不足道的任务(如 Snowflake),而其他的则在处理分区时提供了更大的灵活性(如 BigQuery)。考虑下面的 Snowflake 数据仓库中的数据加载示例。我们假设所有数据文件都存储在 AWS S3 存储桶中,如下所示:

[{

      "id": 1,
      "price": "75836"
},
{
     "id": 2,
      "price": "92567"
}
{
     "id": 3,
      "price": "89921"
}]

现在我们可以使用 SQL 来加载它!

/* Create a JSON file format that strips the outer array. */

CREATE OR REPLACE FILE FORMAT json_format
  TYPE = 'JSON'
  STRIP_OUTER_ARRAY = TRUE;

/* Create a target table for the JSON data. */

CREATE OR REPLACE TABLE sales (src VARIANT);

/* Copy the JSON data into the target table. */

COPY INTO sales
  FROM s3://mybucket/data/files
  CREDENTIALS=(AWS_KEY_ID='$AWS_ACCESS_KEY_ID' AWS_SECRET_KEY='$AWS_SECRET_ACCESS_KEY')
  ENCRYPTION=(MASTER_KEY = 'eSx...')
  FILE_FORMAT = (FORMAT_NAME = my_csv_format);

SELECT * FROM sales;

这就是为什么 Snowflake 在数据分析师和无代码用户中如此受欢迎的原因。

我们可以看到,数据仓库的数据摄取例程处理了所有内容,包括通过去除外部数组来处理数据格式,即 [{},{},{}] -> {},{},{}。

例如,在 BigQuery 中,我们可能会想创建一个数据加载应用程序来做同样的事情。考虑下面的 Python 代码[14]:

import io
import json

def etl(item):
    return json.dumps(item)

# Text file loaded as a blob
blob = """
        [
{"id":"1","first_name":"John"},
{"id":"2","first_name":"Mary"}
]
"""
json_data = json.loads(blob)
data_str = u"\n".join(etl(item) for item in json_data)

print(data_str)
data_file = io.BytesIO(data_str.encode())

# This data file is ready for BigQuery as Newline delimited JSON
print(data_file)

## Python for Data Engineers

面向初学者的高级 ETL 技术

towardsdatascience.com

在用户的编程技能方面,BigQuery 看起来稍显要求较高。

可以合理地认为,了解底层工作原理往往在长期内更具成本效益。

确实,BigQuery 提供了更精细的分区控制,这可能在处理 DWH 中的数据时带来更大的节省。考虑这个数据加载的例子,我们可以定义分区(DAY、MONTH、RANGE):

"""
This function will check if a table exists, otherwise create it.
Will also check if tableSchema contains partition_field and
if exists will use it to create a table.
"""
def _check_if_table_exists(tableData):
    # get table_id reference
    tableName = tableData.get('name')
    tableSchema = tableData.get('schema')
    tableDataset = tableData.get('dataset_id')

    # Check if dataset exists, if not then create
    _check_if_dataset_exists(tableDataset)

    table_id = client.dataset(tableDataset).table(tableName)
    # check if table exists, otherwise create
    try:
        client.get_table(table_id)
    except Exception:
        logging.warn('Creating table: %s' % (tableName))
        schema = create_schema_from_yaml(tableSchema)
        table = bigquery.Table(table_id, schema=schema)
        # Check if partition_field exists in schema definition and if so use it to create the table:
        if (tableData.get('partition_field')):
            table.time_partitioning = bigquery.TimePartitioning(
                type_=bigquery.TimePartitioningType.DAY,
                field=tableData.get('partition_field'), #"date",  # name of column to use for partitioning

            )  # 90 days
        else:
            table.time_partitioning = bigquery.TimePartitioning(
                type_=bigquery.TimePartitioningType.DAY,
            )
        table = client.create_table(table)
        print("Created table {}.{}.{}".format(table.project, table.dataset_id, table.table_id))

# Creates schema definition in BigQuery format using schemas.yaml
# - name: test_table_json
#   alt_name: test_table_json
#   load_method: _load_table_from_json
#   dataset_id: source
#   size: small
#   format: SRC
#   schema:
#     - name: "src"
#       type: "STRING"
#       mode: "NULLABLE"
#   disabled: False
def create_schema_from_yaml(table_schema):
    schema = []
    for column in table_schema:

        schemaField = bigquery.SchemaField(column['name'], column['type'], column['mode'])

        schema.append(schemaField)

        if column['type'] == 'RECORD':
            schemaField._fields = create_schema_from_yaml(column['fields'])
    return schema

当我们有一个带有日期/月份维度的数据模型时,这变得非常有用,并且通常能节省很多钱。

这这是一个大规模数据加载的例子。另外,我们可以创建一个数据管道,使数据持续被摄取。这被称为数据流 [12]。

## 数据工程中的流式处理

流式数据管道和实时分析

towardsdatascience.com

Snowflake 有一个方便的 Kafka 连接器 [13],简化了流式数据管道,并连接到 Kafka 服务器,以从主题中持续拉取数据。

改进的商业智能:许多公司从各种来源(例如天气、收入、支付、客户信息、趋势、供应商信息等)收集大量数据。数据量庞大可能是无用的。将这些数据存储在多个*台上也可能很昂贵。因此,DWH 作为单一真实来源似乎是 BI 管道的问题解决者,每个人都可以轻松生成数据洞察。请参阅下面的 Google Looker Studio 集成。它适用于现代 DWH 解决方案,而且这些解决方案基本上都是市场领导者。

Snowflake 数据连接器。图像由作者提供。

AWS Redshift 连接器用于 Looker Studio。图像由作者提供。

依此类推……一个免费的社区商业智能(BI)工具连接了市场上所有主要的数据仓库解决方案,即 Redshift、Snowflake、BigQuery、Databricks [7]、Galaxy [8] 等。

Looker Studio 数据连接器。图像由作者提供。

数据仓库为商业智能工具和应用程序提供了一个*台,以访问和分析数据。这使得企业可以根据数据驱动的见解做出明智的决策。

更好的产品集成和 DevOps 生命周期:一些产品在数据管道的设计和部署方面更进一步。对于 BI 开发者和数据工程师来说,将所有内容保存在 Git 中非常重要。启用这一功能对持续集成至关重要 [9]。我之前写过关于如何使用 IaC 工具如 AWS CloudFormation 和 Terraform 部署数据管道资源的文章:

## 数据*台的持续集成和部署

数据工程师和 ML Ops 的 CI/CD

towardsdatascience.com

想象一下我们可以使用 CI/CD 工具部署报告。是的,没错。不仅仅是数据管道资源,还有 BI 仪表板。

请参阅下面的 AWS CloudFormation 模板。它部署 AWS Quicksight 数据集和报告分析:

AWSTemplateFormatVersion: 2010-09-09
Description: 'Automated deployment of QuickSight Assets.'

...

Resources:
...

 QSCFBuildQSDataSource:
    Type: Custom::QSCFBuildQSDataSource
    Properties:
      ServiceToken: !GetAtt QSCFBuildQuickSightResourcesLambdaCreator.Arn
      Name: !Join
        - ''
        - - QSCFBuildQSDataSource
          - !Ref Suffix
      CommandFile: QSCF-DataSource
      ResourceType: DataSource
      ResourceId: QSCF-DataSource

  QSCFBuildQSTheme:
    Type: Custom::QSCFBuildQSTheme
    Properties:
      ServiceToken: !GetAtt QSCFBuildQuickSightResourcesLambdaCreator.Arn
      Name: !Join
        - ''
        - - QSCFBuildQSTheme
          - !Ref Suffix
      CommandFile: QSCF-Theme
      ResourceType: Theme
      ResourceId: QSCF-Theme

  QSCFBuildQSDataSet:
    Type: Custom::QSCFBuildQSDataSet
    DependsOn: QSCFBuildQSDataSource
    Properties:
      ServiceToken: !GetAtt QSCFBuildQuickSightResourcesLambdaCreator.Arn
      Name: !Join
        - ''
        - - QSCFBuildQSDataSet
          - !Ref Suffix
      CommandFile: QSCF-DataSet
      ResourceType: DataSet
      ResourceId: QSCF-DataSet

  QSCFBuildQSDashboard:
    Type: Custom::QSCFBuildQSDashboard
    DependsOn:
      - QSCFBuildQSDataSet
      - QSCFBuildQSTheme
    Properties:
      ServiceToken: !GetAtt QSCFBuildQuickSightResourcesLambdaCreator.Arn
      Name: !Join
        - ''
        - - QSCFBuildQSDashboard
          - !Ref Suffix
      CommandFile: QSCF-Dashboard
      ResourceType: Dashboard
      ResourceId: QSCF-Dashboard

  QSCFBuildQSTemplate:
    Type: Custom::QSCFBuildQSTemplate
    Properties:
      ServiceToken: !GetAtt QSCFBuildQuickSightResourcesLambdaCreator.Arn
      Name: !Join
        - ''
        - - QSCFBuildQSTemplate
          - !Ref Suffix
      CommandFile: QSCF-Template
      ResourceType: Template
      ResourceId: QSCF-Template

  QSCFBuildQSAnalysis:
    Type: Custom::QSCFBuildQSAnalysis
    DependsOn:
      - QSCFBuildQSDataSet
      - QSCFBuildQSTheme
      - QSCFBuildQSTemplate
    Properties:
      ServiceToken: !GetAtt QSCFBuildQuickSightResourcesLambdaCreator.Arn
      Name: !Join
        - ''
        - - QSCFBuildQSAnalysis
          - !Ref Suffix
      CommandFile: QSCF-Analysis
      ResourceType: Analysis
      ResourceId: QSCF-Analysis

我之前写了一篇教程[10],讲述了如何使用 AWS CloudFormation 部署流数据管道。在我看来,将 BI 功能添加到其中会使其更好。

## 使用 Redshift Serverless 和 Kinesis 构建流数据管道

初学者的端到端教程

towardsdatascience.com

结论

总的来说,数据仓库是最受欢迎的数据*台之一,可以帮助公司(特别是在企业级别)通过提供单一的真实数据源来获得竞争优势,从而进行数据驱动的决策。现代数据仓库解决方案有助于更快地提供数据洞察。在快速变化的商业环境中,公司可以通过自动化技术激活这些解决方案,为业务利益相关者创造更大的价值。每种解决方案都提供了使其独特的功能。然而,几乎每种情况下都需要考虑一些因素。成本效益、数据分区、查询用户并发、数据湖存储及相关成本——这些痛点都是有效的。有时,将历史数据卸载到云存储档案中以优化成本可能是有用的[15]。在某些情况下,使用 Apache Iceberg 表可能有助于解决用户查询并发问题[16]。

## Apache Iceberg 表介绍

选择 Apache Iceberg 作为数据湖的几个令人信服的理由

towardsdatascience.com

在设计和构建完美的数据*台时,有几个事项需要考虑。一些用户可能认为最佳的数据仓库是湖仓(Databricks),但最终这取决于数据的存储方式以及你对历史记录的业务需求。在许多情况下,作为所有利益相关者的单一真实数据源的数据仓库解决方案可能成为最佳选择。结合合适的数据建模工具,你将获得适用于外部数据到 BI 数据管道的正确工具。

我希望你觉得这些想法有用。它们基于我个人的经验和观察。

推荐阅读

[1] medium.com/towards-data-science/data-platform-architecture-types-f255ac6e0b7

[2] towardsdatascience.com/data-modelling-for-data-engineers-93d058efa302

[3] medium.com/towards-data-science/big-data-file-formats-explained-275876dc1fc9

[4] towardsdatascience.com/automated-emails-and-data-quality-checks-for-your-data-1de86ed47cf0

[5] cloud.google.com/bigquery/docs/column-level-security-intro

[6] registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_dataset_access

[7] docs.databricks.com/en/partners/bi/looker.html

[8] docs.starburst.io/clients/looker.html

[9] towardsdatascience.com/continuous-integration-and-deployment-for-data-platforms-817bf1b6bed1

[10] medium.com/towards-data-science/building-a-streaming-data-pipeline-with-redshift-serverless-and-kinesis-04e09d7e85b2

[11] www.snowflake.com/blog/auto-scale-snowflake-major-leap-forward-massively-concurrent-enterprise-applications/

[12] medium.com/towards-data-science/streaming-in-data-engineering-2bb2b9b3b603

[13] docs.snowflake.com/en/user-guide/kafka-connector

[14] towardsdatascience.com/python-for-data-engineers-f3d5db59b6dd

[15] medium.com/towards-artificial-intelligence/supercharge-your-data-engineering-skills-with-this-machine-learning-pipeline-b69d159780b7

[16] medium.com/towards-data-science/introduction-to-apache-iceberg-tables-a791f1758009

使用 Python 进行现代路线优化

原文:towardsdatascience.com/modern-route-optimization-with-python-fea87d34288b

图片由作者提供

最短路径,旅行推销员问题,车辆路线问题,绘制地图和动画

Mauro Di PietroTowards Data Science Mauro Di Pietro

·发布于Towards Data Science ·13 分钟阅读·2023 年 6 月 7 日

--

在本文中,我将使用 Python 解决以下问题:在一辆或多辆车辆的情况下,如何找到送货给一组客户的最佳路线?

图片由Robert Anasch提供,来源于Unsplash

摘要

路线优化是确定最具成本效益路线的过程。这不仅仅是找到两点之间的最短路径,因为它还包括所有相关因素(即利润、地点数量、时间窗口)。

这个话题在 1930 年代首次以数学形式提出,用于解决校车路线问题。它被称为旅行推销员问题,其核心是找到一种最短路径,使得司机可以访问所有地点,前提是已知各地点之间的距离。

旅行推销员问题可以被概括为车辆路线问题在最小化由运营成本和用户偏好组成的目标函数的同时,为车辆规划路线。这是物流运输中的主要问题。例如,如果在晚上最短路径上交通繁忙(或过高的收费),那么这可能不是晚餐送货的最佳路线。

图片由作者提供

我将展示一些有用的 Python 代码,这些代码可以轻松应用于其他类似情况(只需复制、粘贴、运行),并逐行讲解每一行代码,以便你能够复制这个例子(完整代码链接如下)。

## DataScience_ArtificialIntelligence_Utils/example_route_optimization.ipynb at master ·…

你现在不能执行这个操作。你在另一个标签页或窗口中登录了。你在另一个标签页或…

github.com

我将使用“星巴克门店数据集”,它提供了所有门店的位置(链接如下)。我将选择一个特定的地理区域,并使用提供的经纬度来创建路线。

## Starbucks Locations Worldwide

每个在营运中的星巴克门店的名称、所有权类型和位置

www.kaggle.com

具体来说,我将介绍:

  • 设置:导入包,读取地理数据,通过Folium进行可视化。

  • 使用OSMnx创建网络图,使用NetworkX计算最短路径,并使用Plotly动画生成模拟。

  • 预处理:计算距离矩阵。

  • 旅行推销员问题(简单路线优化)使用OR-Tools

  • 车辆路径问题(高级路线优化)使用OR-Tools

设置

首先,我需要导入以下库:

## for data
import pandas as pd  #1.1.5
import numpy as np  #1.21.0

## for plotting
import matplotlib.pyplot as plt  #3.3.2
import seaborn as sns  #0.11.1
import folium  #0.14.0
from folium import plugins
import plotly.express as px  #5.1.0

## for simple routing
import osmnx as ox  #1.2.2
import networkx as nx  #3.0

## for advanced routing 
from ortools.constraint_solver import pywrapcp  #9.6
from ortools.constraint_solver import routing_enums_pb2

然后,我将读取数据集(请注意,对于地理空间数据,纬度= Y 轴,经度= X 轴):

city = "Hong Kong"

dtf = pd.read_csv('data_stores.csv')
dtf = dtf[dtf["City"]==city][
        ["City","Street Address","Latitude","Longitude"]
      ].reset_index(drop=True)
dtf = dtf.reset_index().rename(
      columns={"index":"id", "Latitude":"y", "Longitude":"x"})

print("tot:", len(dtf))
dtf.head(3)

图片来源于作者

在这些位置中,我将选择一个作为“仓库”(基地),并计算服务所有其他位置的最佳方式。

# pinpoint your starting location
i = 0
dtf["base"] = dtf["id"].apply(lambda x: 1 if x==i else 0)
start = dtf[dtf["base"]==1][["y","x"]].values[0]

print("start =", start)
dtf.head(3)

图片来源于作者

让我们将起始位置与其他数据点一起绘制:

plt.scatter(y=dtf["y"], x=dtf["x"], color="black")
plt.scatter(y=start[0], x=start[1], color="red")
plt.show()

图片来源于作者

为了使其更具现实感,我将把数据点显示为地图上的位置。你可以使用Folium*,这是一个强大的库,通过 HTML 创建不同类型的互动地图。

# setup
data = dtf.copy()
color = "base"  #color based on this column
lst_colors = ["black","red"]
popup = "id" #popup based on this column

# base map
map_ = folium.Map(location=start, tiles="cartodbpositron", zoom_start=11)

# add colors
lst_elements = sorted(list(data[color].unique()))
data["color"] = data[color].apply(lambda x: 
                  lst_colors[lst_elements.index(x)])

# add popup
data.apply(lambda row: 
    folium.CircleMarker(
            location=[row["y"],row["x"]], popup=row[popup],
            color=row["color"], fill=True, radius=5).add_to(map_), 
    axis=1)

# add full-screen button
plugins.Fullscreen(position="topright", title="Expand", 
      title_cancel="Exit", force_separate_button=True).add_to(map_)

# show
map_

图片来源于作者

基本上,我们需要找到最方便的方式,让红点(仓库)服务所有其他位置(客户)。

# add lines
for i in range(len(dtf)):
    points = [start, dtf[["y","x"]].iloc[i].tolist()]
    folium.PolyLine(points, tooltip="Coast", color="red", 
                    weight=0.5, opacity=0.5).add_to(map_)

map_

图片来源于作者

一个快速提示,如果你想要更改地图样式的选项,请添加以下代码:

layers = ["cartodbpositron", "openstreetmap", "Stamen Terrain", 
          "Stamen Water Color", "Stamen Toner", "cartodbdark_matter"]
for tile in layers:
    folium.TileLayer(tile).add_to(map_)
folium.LayerControl(position='bottomright').add_to(map_)
map_

作者提供的图片

最短路径

对于这种用例,最常见的方法是将道路网络视为图,并找到节点之间的最短路径。

我们已经有了所有节点(数据集中的位置点),但我们缺少链接(连接点的街道)。因此,我们需要使用OSMnx获取街道地图数据,它是一个超级有用的库,查询Open Street Map并将响应转换为NetworkX图,这是标准的 Python 图形库。

# create network graph
G = ox.graph_from_point(start, dist=10000, 
        network_type="drive")  #'drive', 'bike', 'walk'
G = ox.add_edge_speeds(G)
G = ox.add_edge_travel_times(G)

# plot
fig, ax = ox.plot_graph(G, bgcolor="black", node_size=5, 
        node_color="white", figsize=(16,8))

作者提供的图片

图形对象包含从地图提取的节点和链接。所有那些小点都是节点。如果你想只查看链接,可以设置node_size=0

作者提供的图片

节点的形式是这样的…

作者提供的图片

…你可以将它们放入像这样的“地理数据框”中…

# geo-dataframe (nodes)
print("nodes:", len(G.nodes()))
ox.graph_to_gdfs(G, nodes=True, edges=False).reset_index().head(3)

作者提供的图片

…链接也是如此。

# geo-dataframe (links)
print("links:", len(G.edges()))
ox.graph_to_gdfs(G, nodes=False, edges=True).reset_index().head(3)

作者提供的图片

现在我们有了图形,让我们了解如何在节点之间在网络中移动。我们已经有一个起始点,所以让我们选择一个随机的目的地……例如最*的节点:

作者提供的图片

end = dtf[dtf["id"]==68][["y","x"]].values[0]
print("locations: from", start, "--> to", end)

我们有 2 个位置,但为了使用图形,我们必须获得等效的节点。

start_node = ox.distance.nearest_nodes(G, start[1], start[0])
end_node = ox.distance.nearest_nodes(G, end[1], end[0])
print("nodes: from", start_node, "--> to", end_node)

因此,我们可以通过Dijkstra算法找到两个节点之间的最短路径。基本上,它通过逐步从一个“邻里”找到另一个的最短路径来计算整体路线。

作者提供的图片

在 Python 中,我们可以直接使用NetworkX应用算法。例如,可以指定优化的属性权重,例如我们可以优先考虑距离或旅行时间 最短加权路径是使权重最小化的路径。

# calculate shortest path
path_lenght = nx.shortest_path(G, source=start_node, target=end_node, 
                                method='dijkstra', weight='lenght')     
print(path_lenght)

# plot on the graph
fig, ax = ox.plot_graph_route(G, path_lenght, route_color="red", 
                              route_linewidth=5, node_size=1, 
                              bgcolor='black', node_color="white", 
                              figsize=(16,8))

作者提供的图片

如果我们改为优化时间:

# calculate shortest path
path_time = nx.shortest_path(G, source=start_node, target=end_node, 
                              method='dijkstra', weight='travel_time')   
print(path_time)

# plot on the graph
fig, ax = ox.plot_graph_route(G, path_time, route_color="blue", 
                              route_linewidth=5, node_size=1, 
                              bgcolor='black', node_color="white", 
                              figsize=(16,8))

作者提供的图片

我们可以在图上比较路径…

# plot on the graph
fig, ax = ox.plot_graph_routes(G, routes=[path_lenght, path_time], 
                              route_colors=["red","blue"], 
                              route_linewidth=5, node_size=1, 
                              bgcolor='black', node_color="white", 
                              figsize=(16,8))

作者提供的图片

…或者更好地使用组合OSMnxFolium在地图上:

# plot on the map
ox.plot_route_folium(G, route=path_lenght, route_map=map_, 
                     color="red", weight=1)
ox.plot_route_folium(G, route=path_time, route_map=map_, 
                     color="blue", weight=1)
map_

作者提供的图片

最后,我们可以选择一条路径并模拟驾驶员从一个节点到另一个节点。我们将使用Plotly,这是一个著名的互动图表库,以及Mapbox,一个为知名网站(如 Lonely Planet 和 Financial Times)提供定制在线地图的供应商。首先,我们必须准备包含路线信息的数据框,然后创建一个Plotly*动画。

lst_start, lst_end = [],[]
start_x, start_y = [],[]
end_x, end_y = [],[]
lst_length, lst_time = [],[]

for a,b in zip(route_time[:-1], route_time[1:]):
    lst_start.append(a)
    lst_end.append(b)
    lst_length.append(round(G.edges[(a,b,0)]['length']))
    lst_time.append(round(G.edges[(a,b,0)]['travel_time']))
    start_x.append(G.nodes[a]['x'])
    start_y.append(G.nodes[a]['y'])
    end_x.append(G.nodes[b]['x'])
    end_y.append(G.nodes[b]['y'])

df = pd.DataFrame(list(zip(lst_start, lst_end, 
                           start_x, start_y, end_x, end_y, 
                           lst_length, lst_time)), 
                   columns=["start","end","start_x","start_y",
                            "end_x","end_y","length","travel_time"]
                  ).reset_index().rename(columns={"index":"id"})

df.head()

作者提供的图片

## create start/end df 
df_start = df[df["start"] == start_node]
df_end = df[df["end"] == end_node]

## create basic map
fig = px.scatter_mapbox(data_frame=df, lon="start_x", lat="start_y", 
                        zoom=15, width=1000, height=800, 
                        animation_frame="id", 
                        mapbox_style="carto-positron")
## add driver
fig.data[0].marker = {"size":12}
## add start point
fig.add_trace(px.scatter_mapbox(data_frame=df_start, 
                                lon="start_x", lat="start_y").data[0])
fig.data[1].marker = {"size":15, "color":"red"}
## add end point
fig.add_trace(px.scatter_mapbox(data_frame=df_end, 
                                lon="start_x", lat="start_y").data[0])
fig.data[2].marker = {"size":15, "color":"green"}
## add route
fig.add_trace(px.line_mapbox(data_frame=df, 
                             lon="start_x", lat="start_y").data[0])
fig

作者提供的图片

预处理

这只是一个热身,学习如何从一个节点找到另一个节点的路径。我们仍需计算一个访问所有位置的路线。这些问题通常遵循一个固定的公式:生成所有位置之间最短路径成本的距离矩阵,构建初始解,并通过一系列迭代改进它。

## get the node for each location
dtf["node"] = dtf[["y","x"]].apply(lambda x: 
                           ox.distance.nearest_nodes(G, x[1], x[0]), 
                        axis=1)
dtf = dtf.drop_duplicates("node", keep='first')
dtf.head()

作者提供的图片

因此,在应用任何模型之前,首要任务是计算我们数据集中所有位置之间的距离矩阵。我们可以通过找到我们感兴趣的每个节点之间的最短路径距离来完成这项任务。

## distance length function
def f(a,b):
    try:
        d = nx.shortest_path_length(G, source=a, target=b, 
                                    method='dijkstra', 
                                    weight='travel_time')
    except:
        d = np.nan
    return d

## apply the function
distance_matrix = np.asarray([[f(a,b) for b in dtf["node"].tolist()] 
                               for a in dtf["node"].tolist()])
distance_matrix = pd.DataFrame(distance_matrix, 
                               columns=dtf["node"].values, 
                               index=dtf["node"].values)
distance_matrix.head()

作者提供的图片

检查是否有NaN0Inf 值是至关重要的:

heatmap = distance_matrix.copy()
for col in heatmap.columns:
    heatmap[col] = heatmap[col].apply(lambda x: 
                       0.3 if pd.isnull(x) else  #nan -> purple
                      (0.7 if np.isinf(x) else   #inf -> orange
                      (0 if x!=0 else 1) ))      # 0  -> white  

fig, ax = plt.subplots(figsize=(10,5))
sns.heatmap(heatmap, vmin=0, vmax=1, cbar=False, ax=ax)
plt.show()

作者提供的图片

我们在正确的位置(对角线)有0,且没有Inf,虽然看到了一些NaNs……所以我们得处理它们。这一步非常关键,因为距离矩阵会影响任何可以使用的路由模型。通常,我会用行的*均距离来替代缺失值。

# fillna with row average
distance_matrix = distance_matrix.T.fillna(distance_matrix.mean(axis=1)).T

# fillna with overall average
distance_matrix = distance_matrix.fillna(distance_matrix.mean().mean())

作者提供的图片

我们现在将开始处理路由优化模型。

旅行商问题

如果我们仅考虑单个驾驶员的距离,最优路线是从一个节点到另一个节点的最短路径集合,所以基本上它就是最短路线。

## Business parameters
drivers = 1

lst_nodes = dtf["node"].tolist()
print("start:", start_node, "| tot locations to visit:", 
     len(lst_nodes)-1, "| drivers:", drivers)

最先进的 Python 库是OR-Tools,由 Google 开发,用于解决线性编程和相关的优化问题。这是一个非常强大的工具,因为它使用了许多技术,其中之一是冲突驱动子句学习,这与强化学习算法类似。简单来说,它从寻找满足解的过程中的冲突中学习,并尝试避免重复相同的冲突。

首先,你必须定义索引管理器,它跟踪节点索引,以及路由模型,这是主要的OR-Tools对象。

manager = pywrapcp.RoutingIndexManager(len(lst_nodes), 
                                       drivers, 
                                       lst_nodes.index(start_node))
model = pywrapcp.RoutingModel(manager)

然后,我们需要为每一步添加成本函数,目标是将其最小化。在我们的案例中,就是距离……

def get_distance(from_index, to_index):
    return distance_matrix.iloc[from_index,to_index]

distance = model.RegisterTransitCallback(get_distance)
model.SetArcCostEvaluatorOfAllVehicles(distance)

… 并指定策略。

parameters = pywrapcp.DefaultRoutingSearchParameters()
parameters.first_solution_strategy = (
          routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
)

最后,解决问题并打印解决方案:

solution = model.SolveWithParameters(parameters)

index = model.Start(0)
print('Route for driver:')
route_idx, route_distance = [], 0
while not model.IsEnd(index):
    route_idx.append( manager.IndexToNode(index) ) 
    previous_index = index
    index = solution.Value( model.NextVar(index) )
    ### update distance
    try:
        route_distance += get_distance(previous_index, index)
    except:
        route_distance += model.GetArcCostForVehicle(
                              from_index=previous_index, 
                              to_index=index, 
                              vehicle=0)

print(route_idx)
print(f'Total distance: {round(route_distance/1000,2)} km')
print(f'Nodes visited: {len(route_idx)}')

让我们将路线从索引序列转换为节点序列:

print("Route for driver (nodes):")
lst_route = [lst_nodes[i] for i in route_idx]
print(lst_route)

由于此原因,我们可以在地图上绘制路线:

# Get path between nodes
def get_path_between_nodes(lst_route):
    lst_paths = []
    for i in range(len(lst_route)):
        try:
            a, b = lst_nodes[i], lst_nodes[i+1]
        except:
            break
        try:
            path = nx.shortest_path(G, source=a, target=b, 
                                    method='dijkstra', 
                                    weight='travel_time')
            if len(path) > 1:
                lst_paths.append(path)
        except:
            continue
    return lst_paths

lst_paths = get_path_between_nodes(lst_route)

# Add paths on the map
for path in lst_paths:
    ox.plot_route_folium(G, route=path, route_map=map_, 
                         color="blue", weight=1)
map_

图片由作者提供

车辆路线问题

不幸的是,现实世界并不简单,因为公司有更多的业务约束。因此,车辆路线问题有许多变体:

  • 有容量限制的车辆路线问题:车辆对必须交付的货物有有限的承载能力。

  • 带时间窗口的车辆路线问题:送货地点有时间窗口,必须在这些时间窗口内完成送货。

  • 带取货和送货的车辆路线问题:货物需要从某些取货地点运送到其他送货地点。

  • 带利润的车辆路线问题:车辆不必访问所有节点,目标是最大化收集到的利润总和。

对于这个用例,我将引入司机在承载能力和可覆盖距离方面的限制。

## Business parameters
drivers = 3
driver_capacities = [20,20,20]
demands = [0] + [1]*(len(lst_nodes)-1)
max_distance = 1000

就像之前一样,我们需要创建管理器、模型,并添加距离函数:

## model
manager = pywrapcp.RoutingIndexManager(len(lst_nodes), 
                                       drivers, 
                                       lst_nodes.index(start_node))
model = pywrapcp.RoutingModel(manager)

## add distance (cost)
def get_distance(from_index, to_index):
    return distance_matrix.iloc[from_index,to_index]

distance = model.RegisterTransitCallback(get_distance)
model.SetArcCostEvaluatorOfAllVehicles(distance)

然而,这次我们必须包含新的业务约束:

## add capacity (costraint)
def get_demand(from_index):
    return demands[from_index]

demand = model.RegisterUnaryTransitCallback(get_demand)
model.AddDimensionWithVehicleCapacity(demand, slack_max=0, 
                                     vehicle_capacities=driver_capacities, 
                                     fix_start_cumul_to_zero=True,
                                     name='Capacity')

## add limited distance (costraint)
name = 'Distance'
model.AddDimension(distance, slack_max=0, capacity=max_distance, 
                   fix_start_cumul_to_zero=True, name=name)
distance_dimension = model.GetDimensionOrDie(name)
distance_dimension.SetGlobalSpanCostCoefficient(100)

## set strategy to minimize cost
parameters = pywrapcp.DefaultRoutingSearchParameters()
parameters.first_solution_strategy = (
          routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
)
solution = model.SolveWithParameters(parameters)

最后,解决问题并打印解决方案:

solution = model.SolveWithParameters(parameters)

dic_routes_idx, total_distance, total_load = {}, 0, 0
for driver in range(drivers):
    print(f'Route for driver {driver}:')
    index = model.Start(driver)
    route_idx, route_distance, route_load = [], 0, 0
    while not model.IsEnd(index):
        node_index = manager.IndexToNode(index)
        route_idx.append( manager.IndexToNode(index) )
        previous_index = index
        index = solution.Value( model.NextVar(index) )
        ### update distance
        try:
            route_distance += get_distance(previous_index, index)
        except:
            route_distance += model.GetArcCostForVehicle(
                                from_index=previous_index, 
                                to_index=index, 
                                vehicle=driver)
        ### update load
        route_load += demands[node_index]

    route_idx.append( manager.IndexToNode(index) )
    print(route_idx)
    dic_routes_idx[driver] = route_idx
    print(f'distance: {round(route_distance/1000,2)} km')
    print(f'load: {round(route_load,2)}', "\n")
    total_distance += route_distance
    total_load += route_load

print(f'Total distance: {round(total_distance/1000,2)} km')
print(f'Total load: {total_load}')

# Convert from idx to nodes
dic_route = {}
for k,v in dic_routes_idx.items():
    print(f"Route for driver {k} (nodes):")
    dic_route[k] = [lst_nodes[i] for i in v]
    print(dic_route[k], "\n")

让我们在地图上可视化这些路线:

# Get path between nodes
dic_paths = {k:get_path_between_nodes(v) for k,v in dic_route.items()}

# Add paths on the map
lst_colors = ["red","green","blue"]
for k,v in dic_paths.items():
    for path in v:
        ox.plot_route_folium(G, route=path, route_map=map_, 
                             color=lst_colors[k], weight=1)
map_

图片由作者提供

最后,为了以风格结束这篇文章,让我们运行模拟以查看我们的司机开始工作。首先,获取适当的数据框……

def df_animation_multiple_path(G, lst_paths, parallel=True):
    df = pd.DataFrame()
    for path in lst_paths:
        lst_start, lst_end = [],[]
        start_x, start_y = [],[]
        end_x, end_y = [],[]
        lst_length, lst_time = [],[]

        for a,b in zip(path[:-1], path[1:]):
            lst_start.append(a)
            lst_end.append(b)
            lst_length.append(round(G.edges[(a,b,0)]['length']))
            lst_time.append(round(G.edges[(a,b,0)]['travel_time']))
            start_x.append(G.nodes[a]['x'])
            start_y.append(G.nodes[a]['y'])
            end_x.append(G.nodes[b]['x'])
            end_y.append(G.nodes[b]['y'])

        tmp = pd.DataFrame(list(zip(lst_start, lst_end, 
                                    start_x, start_y, 
                                    end_x, end_y, 
                                    lst_length, lst_time)), 
                           columns=["start","end","start_x","start_y",
                                    "end_x","end_y","length","travel_time"]
                          )
        df = pd.concat([df,tmp], ignore_index=(not parallel))

    df = df.reset_index().rename(columns={"index":"id"})
    return df

……其次,绘制动画。

df = pd.DataFrame()
for driver,lst_paths in dic_paths.items():
    tmp = df_animation_multiple_path(G, lst_paths, parallel=False)
    df = pd.concat([df,tmp], axis=0)

first_node, last_node = lst_paths[0][0], lst_paths[-1][-1]
plot_animation(df, first_node, last_node)

图片由作者提供

结论

本文是一个教程,展示了如何使用 Python 进行路线优化。首先,我们学习如何在地图上可视化地理数据集并构建网络图。然后,我展示了如何处理旅行商问题,通过找到司机的最短路线,以及车辆路线问题,通过找到多个司机的最便宜路线。

此外,现在你知道如何使用地理空间数据生成交互式图表和酷炫动画。

希望你喜欢这篇文章!如有问题或反馈,或只是想分享你的有趣项目,请随时联系我。

👉 让我们联系 👈

本文是使用 Python 进行机器学习系列的一部分,请参见:

[## 使用 Python 进行机器学习:分类(完整教程)

数据分析与可视化、特征工程与选择、模型设计与测试、评估与解释

使用 Python 进行机器学习:回归(完整教程) [## 使用 Python 进行机器学习:回归(完整教程)

数据分析与可视化、特征工程与选择、模型设计与测试、评估与解释

使用 Python 进行机器学习:回归(完整教程) [## 聚类地理空间数据

使用交互式地图绘制机器学习和深度学习聚类

使用 Python 进行深度学习:神经网络(完整教程) [## 使用 Python 进行深度学习:神经网络(完整教程)

使用 TensorFlow 构建、绘制并解释人工神经网络

使用 Python 进行深度学习:神经网络(完整教程) [## 现代推荐系统与神经网络

使用 Python 和 TensorFlow 构建混合模型

现代推荐系统与神经网络

现代图像语义搜索

原文:towardsdatascience.com/modern-semantic-search-for-images-cb1a3242631d?source=collection_archive---------7-----------------------#2023-11-14

一篇利用 Python、Pinecone、Hugging Face 和 Open AI CLIP 模型来创建云照片语义搜索应用程序的操作指南。

Josh PoduskaTowards Data Science Josh Poduska

·

关注 发表在 Towards Data Science ·6 min read·2023 年 11 月 14 日

--

作者提供的图片

你想找到“那张几年前的照片”。你记得一些场景细节,想根据特定的短语进行搜索。Apple Photos 不提供语义搜索,而 Google Photos 仅限于少数预定的项目分类器。两者在这类搜索中表现都不佳。我将用两个不寻常的 Google Photos 查询来演示这个问题:“甜甜圈生日蛋糕”和“被雪仗打破的嘴唇”。然后我会分享如何构建自己的语义图像搜索应用程序。

演示:当前局限性与现代语义图像搜索的比较

示例 #1

我喜欢生日蛋糕。我也喜欢甜甜圈。去年,我有一个绝妙的主意,把两者结合起来,用一堆甜甜圈作为我的生日蛋糕。让我们尝试找到它。

Google Photos 查询:“甜甜圈生日蛋糕”

结果: 六张没有甜甜圈的蛋糕图片,接着是一张我想要的图片。

作者提供的图片

语义搜索应用查询:“甜甜圈生日蛋糕”

结果: 两张和一个视频完全符合我想要的。

作者提供的图片

示例 #2

我和我的青少年儿子及他的大群朋友一起去滑雪。他们爬上了一个废弃的火车隧道。 “一次性扔雪球,我会拍慢动作视频!”,我喊道。那并不是我最聪明的时刻,因为我没有预见到一个明显的结论,即我最终会成为二十个拥有强壮臂膀的青少年作为目标练习的对象。

Google Photos 查询:“雪仗中的破裂嘴唇”

结果:

作者创建的图片

当前的 Google 图像分类模型仅限于它所训练过的词汇。

语义搜索应用查询:“雪仗中的破裂嘴唇”

结果: 破裂的嘴唇照片(未展示)和破裂嘴唇之前的视频分别是结果一和二。

作者提供的图片

OpenAI CLIP 模型和应用架构

CLIP 使模型能够学习如何将图像像素与文本关联,并赋予它寻找“甜甜圈蛋糕”和“破裂的嘴唇”等内容的灵活性——这些内容在训练图像分类器时你可能从未考虑过。它代表了对比语言-图像预训练。它是一个开源的多模态零样本模型。它在数百万张带有描述性字幕的图像上进行了训练。

给定图像和文本描述,模型可以预测该图像最相关的文本描述,而无需针对特定任务进行优化。

来源: Nikos Karfitsas, Towards Data Science

大多数在线教程中的 CLIP 架构对于概念验证来说足够好,但不适合企业级应用。在这些教程中,CLIP 和 Hugging Face 处理器将嵌入保持在内存中,以作为运行相似度评分和检索的向量存储。

作者创建的图片

像 Pinecone 这样的向量数据库是扩展此类应用程序的关键组件。它提供了简化、稳健、企业级的功能,如图像的批处理和流处理、嵌入的企业管理、低延迟检索和元数据过滤。

作者创建的图片

构建应用程序

此应用程序的代码和支持文件可在 GitHub 上找到,链接为 github.com/joshpoduska/llm-image-caption-semantic-search。使用它们构建一个用于云照片的语义搜索应用程序。

该应用程序在配备足够内存的笔记本电脑上本地运行。我在 MacBook Pro 上进行了测试。

构建应用程序所需的组件。

  • 松果 或类似的向量数据库用于嵌入存储和语义搜索(本教程使用免费版松果已足够)。

  • Hugging Face 模型和管道。

  • OpenAI CLIP 模型用于图像和查询文本嵌入的创建(从 Hugging Face 可访问)。

  • Google Photos API 用于访问您的个人 Google Photos。

在开始之前提供有用信息。

访问您的图像。

Google Photos API 具有几个关键的数据字段值得注意。更多细节请参阅 API 参考文档

  • Id 是不可变的。

  • baseUrl 允许您访问媒体项的字节。它们有效期为 60 分钟。

您可以简单地使用 pandas、JSON 和 requests 库来加载包含图像 ID、URL 和日期的 DataFrame。

生成图像嵌入。

使用 Hugging Face 和 OpenAI CLIP 模型,这一步是整个应用程序中最简单的步骤。

from sentence_transformers import SentenceTransformer
img_model = SentenceTransformer('clip-ViT-B-32')
embeddings = img_model.encode(images)

创建元数据。

语义搜索通常通过元数据过滤得到增强。在此应用程序中,我使用照片的日期来提取年、月和日。这些信息存储为 DataFrame 字段中的字典。松果查询可以使用此字典通过字典中的元数据来过滤搜索。

这是我 pandas DataFrame 的第一行,包含图像字段、向量和元数据字典字段。

图像由作者提供。

加载嵌入。

松果有关异步和并行加载的优化。基本加载功能如下所示。

index.upsert(vectors=ids_vectors_chunk, async_req=True)

查询嵌入。

要使用 CLIP 模型查询图像,我们需要传递语义查询文本。通过加载 CLIP 文本嵌入模型来简化此过程。

text_model = SentenceTransformer(‘sentence-transformers/clip-ViT-B-32-multilingual-v1’)

现在我们可以为我们的搜索短语创建一个嵌入,并将其与存储在 Pinecone 中的图像嵌入进行比较。

# create the query vector
xq = text_model.encode(query).tolist()

# now query
xc = index.query(xq,
                filter= {
                "year": {"$in":years_filter},
                "month": {"$in":months_filter}
                        },
                top_k= top_k,
                include_metadata=True)

结论

CLIP 模型非常了不起。它是一个通用知识的零样本模型,已经学会了将图像与文本关联,从而摆脱了在预定义类别上训练图像分类器的限制。当我们将这一点与像 Pinecone 这样的企业级向量数据库的强大功能结合时,我们可以创建具有低延迟和高保真度的语义图像搜索应用。这只是生成性 AI 每天涌现出的令人兴奋的应用之一。

监控机器学习模型:数据科学家和机器学习工程师的基础实践

原文:towardsdatascience.com/monitoring-machine-learning-models-a-tried-and-true-cure-for-a-data-scientists-insomnia-c45b0979a878?source=collection_archive---------10-----------------------#2023-02-06

初学者监控机器学习模型指南

Saurav PawarTowards Data Science Saurav Pawar

·

关注 发表在 Towards Data Science ·15 分钟阅读·2023 年 2 月 6 日

--

图片由 Nathan Dumlao 提供,来源于 Unsplash

机器学习属于人工智能的范畴。它专注于创建和开发能够分析数据、得出结论和进行预测的算法。银行和金融服务行业的组织可以利用机器学习技术获得的各种见解来帮助它们在未来做出决策。例如,一种机器学习模型可以通过利用各种数据源对车贷申请者进行风险评分。这些算法然后可以轻松预测哪些客户有违约风险,从而帮助银行重新考虑或调整每个客户的贷款条款。

为什么监控贷款违约者至关重要?

当借款人未按贷款条款进行还款时,称之为“坏账”。这会对银行的盈利能力产生不利影响,并可能导致贷款人承担信用损失。大量次级贷款可能会对银行的资本充足率产生负面影响,最坏的情况下可能导致违约。坏账还可能阻碍长期经济增长,并增加银行系统的不确定性,这些都会增加金融稳定性的风险。

为什么机器学习模型监控对这些用例至关重要?

设想你有一个机器学习模型来确定候选人是否符合贷款条件,你相信你的模型几乎可以正确预测所有情况。这样,你可以利用模型的预测避免很多猜测,并且幸运的是,它成功了。人们容易相信模型将继续正常运作,但这并不一定正确。

由于机器学习模型周围的世界不断变化,这些模型会随着时间的推移而恶化。如果你不经常向模型提供相关和更新的数据,它将产生大量错误的预测,而你甚至不会意识到这些问题。随着时间的推移,这些错误可能会迅速累积,给你的银行带来重大损失。如果你想保护公司和自己免受这些问题的影响,关键是开始并持续检查你的机器学习模型。

大多数模型使用数据集来生成预测,模型的环境随时间变化。如果用于训练模型的数据过时,你的模型将无法产生准确的预测。除了系统漏洞和技术问题,这些问题超出了你通常对机器的期望。

机器学习模型可能会以哪些方式失败?

尽管模型失败可能有多种原因,但最常见的原因是模型漂移

什么是模型漂移?

模型漂移是指由于数据和输入输出变量之间关系的变化,模型的准确性逐渐下降的现象。

作者创作的图像

造成模型漂移的原因是什么?

尽管模型漂移有多种原因,但它们可以分为两大基本类别:

训练数据不良:

  • 样本选择偏倚——当使用偏倚或无效的策略来收集或准备训练数据时,就会发生这种情况。模型将使用的操作环境没有得到准确的表示。

  • 隐变量的变化——虽然隐变量不能直接测量,但它们对一些观察到的变量有显著影响。从本质上讲,如果隐性因素发生变化,那么我们可以看到的数据也会发生变化。例如:我们可能考虑汽车的价值、借款人的收入、他们以往的汽车贷款记录、首付的金额等。然而,一个人的整体情况可能有更大的影响,比如,他/她的家庭成员正在与疾病作斗争,需要紧急资金用于治疗。虽然这些因素无法直接测量,但它们对贷款违约者(我们的目标)数量有着至关重要的影响。

环境变化:

  • 动态环境——这是不稳定性的最直接和明显的例子,其中数据和关系的变化超出了我们的控制。例如,规则和法规可能会发生变化,用户兴趣可能会发生变化,更好的竞争者可能会出现等等。

  • 技术问题——故障的数据管道、特征参数值的变化,甚至是一个漏洞都可能是这些问题的原因。

  • 领域的变化——这指的是概念或价值的意义发生了修改。例如,随着通货膨胀使货币贬值,一件物品的价格或一个人的工资会随着时间而产生不同的后果。

变化模式

如果我们考虑一下“漂移”这个词的定义,它本质上指的是随时间推移的渐进性变化。同样,机器学习中的漂移也会以不同的速度发生。以下是漂移的不同类型:

  • 渐进性变化——随着时间的推移,当新的想法被实施时,会发生渐进的变化。例如,考虑一个在 2015 年开发的汽车价格预测模型,当时的准确度很高。由于汽车价格随时间上升,几年后,预测的有效性和准确性开始下降。这是有道理的,因为正如我们所知道的,汽车价格一般是逐渐上升的。然而,如果没有考虑到这一点,它可能会对模型的准确性产生负面影响。

渐进性变化(作者创作的图像)

  • 突发的变化——变化可能会突然发生。例如:在疫情期间及之后,购买模式和消费者行为的突然调整。

突发变化(作者创作的图像)

  • 重复出现——在这种情况下,变化在初始观察后会重新出现,或者我们可以说它会周期性地发生。以冬季服装购物为例。

递归漂移(图像由作者创建)

  • 峰值— 这些是可能对模型产生影响的特殊情况。例如:战争、疫情、经济衰退等引起的变化

峰值漂移(图像由作者创建)

模型漂移可以进一步分为两个主要类别:

数据漂移

数据漂移的特征是数据分布的变化。这是指实时生产数据与基准或参考数据集(通常是训练集)之间的差异,在生产机器学习模型的情况下,这些数据集代表了模型设计要执行的任务。由于现实世界的变化,生产数据可能会随着时间的推移偏离基准数据。数据漂移可以进一步分为两个类别:

  • 协变量/特征漂移

协变量漂移发生在用于训练算法的数据与用于应用算法的数据之间发生分歧时。这表明尽管特征的分布没有变化,但特征与目标变量之间的关系发生了变化。当这种输入数据的统计属性发生变化时,之前构建的模型将不能提供无偏结果,从而导致不准确的预测。例如,医疗设备制造商可能会使用来自大型城市医院的信息来创建基于机器学习的系统。然而,一旦产品进入市场,由医疗专业人员在农村地区输入到系统中的医疗数据可能与开发数据不相符。在某些社会人口群体中,城市医院的患者可能比农村医院的患者更有可能存在潜在的医疗问题。这些差异可能直到产品发布市场后并开始比测试期间更频繁地发生故障时才会被注意到。

  • 标签漂移

这种漂移发生在类变量(y)、模型输出或标签分布的分布发生变化时。例如,疫情导致汽车成本大幅上升,这使得汽车价格分布向更高的默认值偏移。疫情前开发的汽车价格预测模型在疫情后无法以足够的准确性预测输入值。

标签漂移(图像由作者创建)

概念漂移

当 p(y|X) 发生变化而 p(X) 保持不变时,就发生了概念漂移。在这里,p(X) 和 p(y) 分别表示观察到的汽车特征 X 和汽车价格 y 的概率,而 p(y|X) 表示给定汽车特征的价格的条件分布。在汽车价格预测的例子中,给定汽车属性的价格的条件概率 p(y|X) 可能会发生波动。考虑到汽车的座位容量分布没有变化。如今,客户想要更大的汽车,因此其价格上涨了。特别是对于较大的汽车,给定座位容量的汽车价格的条件概率可能会发生变化。

概念漂移(图像由作者创建)

如何检测漂移?

检测漂移的方法有几种,其中最常见的方法是:

跟踪模型性能:

  • 跟踪模型性能指标是检测漂移的最简单方法。混淆矩阵、准确率、召回率、F1 分数和 ROC-AUC 是一些最受欢迎的机器学习模型性能指标。其他模型行为测量指标也可能至关重要,这取决于我们如何使用模型。

跟踪描述性统计数据:

既然我们已经了解了一些潜在的机器学习模型失败模式及其识别方法,让我们来看看如何使用 NannyML 来预防这些问题!

如何使用 NannyML 避免模型失败?

什么是 NannyML?

NannyML 是一个开源的 Python 库,它允许你估算部署后模型性能(无需访问目标),检测数据漂移,并智能地将数据漂移警报与模型性能变化关联起来。

在接下来的示例中,我们将演示如何使用 NannyML 以及必要的代码来防止模型失败。

让我们安装和导入必要的包:

既然我们已经有了所有必要的包,现在让我们加载数据。这是我将使用的 数据集

在继续之前,让我们先看看这个用例!

这个数据集集中于汽车贷款,我们的目标是确定借款人是否能够偿还他们的债务。数据集中的每一行代表一个客户,我们有关于每个客户贷款申请的详细信息,比如汽车的价值、预期月收入、是否还清了之前的贷款等。最重要的是,这个数据集的一部分(“repaid”列中有 0 和 1 的数据)包含了客户是否能够偿还汽车贷款的信息,因此这些数据被用来训练机器学习模型。我们还可以评估这个模型的性能,因为除了预测输出(“y_pred_proba”列),我们还有实际输出(“repaid”列)。我们还有一个名为“partition”的列,用于区分“reference”和“analysis”数据。机器学习模型部署后提取的数据被称为“analysis”数据。实际进行预测的日期和时间列在“timestamp”列中。

正如我们所见,在这种特定的使用案例中,我们只能在特定时间后获得目标/预测数据,因此我们只能在一年或几个月后确定客户是否能够偿还债务。这使得评估机器学习模型的有效性相当具有挑战性。而这正是我们将利用 NannyML 来做的,以查看我们的模型在部署后没有目标数据的情况下表现如何。

让我们将数据划分为“reference”和“analysis”,因为 NannyML 需要先从参考数据集中学习模型,然后才能监控实际分析的数据,这些数据被提供为分析(部署后)数据:

使用训练(“reference”)数据,我们将现在估计部署后数据(“analysis”)的性能。

在上面的代码中,我们用各种必要的参数初始化了估计器,将其拟合到参考数据中,然后用它来计算分析(部署后)数据的性能估计。

输出:

性能估计(图片由作者创建)

在没有目标数据的情况下,NannyML 使用一种称为 CBPE(基于置信度的性能估计)的方法来计算正在使用的机器学习模型的性能。如果您希望完全理解这种算法,这里有一个精彩的解释在他们的官方文档中。

CBPE 基本上利用测试数据(2018 年 1 月至 2018 年 7 月用蓝色虚线表示的数据)作为模型输出或概率分数,以便得出分析数据(紫色阴影区域)的性能。我们可以看到,模型部署后性能显著下降,因此我们将其标记为稍微红色阴影区域。这是模型未按计划运行的主要指示,因此需要进一步调查以识别问题。

注意:

因为我们还不知道目标数据,这个性能是不准确的,实际的估计性能可能低于 NannyML 使用 CBPE 预测的性能。

如前所述,模型漂移是 ML 模型失败的最常见原因,因此让我们估计单变量多变量漂移。

当变量检测到分布中的显著变化时,就会发生单变量漂移。NannyML 用于检测数据漂移的单变量策略分别检查每个变量,并将分析数据期间产生的块与参考期间进行对比。为了识别漂移,NannyML 除了距离测量之外,还提供了统计测试。这些方法被称为方法。一些方法只能用于分类数据,其他只能用于连续数据,而一些方法可以同时用于两者。我们可以使用 NannyML 选择应用哪些方法。以下是使用 NannyML 估计单变量漂移的代码:

现在让我们拟合“参考”数据并估计“分析”数据的单变量漂移:

注意:

通过使用to_df()方法,我们可以将结果转换为 DataFrame。

参考数据作为衡量分析数据的标准,必须提供给fit()方法。然后,使用提供的数据,calculate()方法将计算漂移的结果。

注意:

由于空间限制,我无法在此显示输出,但你可以参考这个

下一步是可视化结果。对于给定的列,NannyML 可能会同时绘制漂移和分布。jensen-shannon方法对每个连续列的结果将首先被绘制,然后是每个分类列的chi2

注意:

为了保持文章简短,我不会显示每个特征的可视化,但你可以通过这个访问它们。

loan_length 的 Jensen-Shannon 距离(图像由作者创建)

car_value 的 Jensen-Shannon 距离(图像由作者创建)

y_pred_proba 的 Jensen-Shannon 距离(图像由作者创建)

chi2结果用于分类列:

salary_range 的 Chi2 统计量(图像由作者创建)

repaid_loan_on_prev_car 的 Chi2 统计量(图像由作者创建)

size_of_downpayment 的 Chi2 统计量(图像由作者创建)

连续变量的分布

使用 NannyML,我们还可以获得连续变量和类别变量的分布详情。在处理连续变量时,NannyML 创建了一个名为 joyplot 的图形,显示了每个区块中变量的估计概率分布。发现漂移的部分被高亮显示:

car_value 随时间的分布(图像由作者创建)

当我们从 2019 年 1 月到 2019 年 7 月时,可以看到第一四分位数发生了变化。

loan_length 随时间的分布(图像由作者创建)

随着我们从 2019 年 1 月移动到 2019 年 7 月,我们可以从上图中看到,接到一些请求的汽车贷款也有所增加。

y_pred_proba 随时间的分布(图像由作者创建)

在上图中,最大预期概率略微向下移动,最低水*略微向上移动。这是数据接*决策边界的明显迹象。这也被称为模型输出漂移

类别变量的分布

NannyML 创建堆叠条形图,以显示每个区块中类别变量的分布。为了使图表更易于检查,如果一个变量有超过五个类别,只显示前四个。使用下面的代码,我们可以为模型中的类别变量创建条形图:

salary_range 随时间的分布(图像由作者创建)

最低收入范围在上图中略微上移(白色圆圈高亮区域)。此外,最高薪资范围有所降低(紫色圆圈高亮区域)。

repaid_loan_on_prev_car 随时间的分布(图像由作者创建)

上述所有图表有一个共同点:每个图表都包括一个时间参数。因此,值得考虑是否可以在没有数据集中的时间特征的情况下执行等效的分析?

NannyML 的机器学习模型监控服务关注实际随时间的演变,这使其成为其独特的功能之一。然而,通过简单地将数据划分为参考集和分析集,NannyML 也可以用于非时间序列分析。

注意: 建议在分析中包含时间因素,因为它可以产生比非时间分析更准确的结果。

多变量漂移

当输入数据之间的关系发生变化时,会出现多变量漂移。多变量变化检测的解释可能具有挑战性,但它是克服单变量变化检测限制的必要手段。

为什么有必要检查多变量漂移?

多变量数据漂移检测解决了 单变量数据检测技术的缺陷。减少了误警报的风险,发现了单变量技术遗漏的数据结构中更细微的变化。它还提供了一个单一的总结值。

为了发现这种变化,NannyML 使用了 主成分分析数据重建。该技术测量重建误差,并返回一个单一值。这个值的变化反映了模型输入结构的变化。NannyML 会随时间计算监控模型的重建误差,如果结果偏离了参考数据期的方差设定范围,则会触发警报。以下是使用 NannyML 估计多变量漂移的代码:

在上述代码中,我们已使用适当的参数初始化了 DataReconstructionDriftCalculator 模块。参考数据必须传递给 fit() 方法,其中的发现将用于未来的分析。然后,calculate() 方法将使用提供的数据来确定多变量漂移的结果。缺失数据是一个需要解决的关键问题。NannyML 的默认插补方法为连续特征分配均值,为分类特征分配最常见值。

注意: SimpleImputer 类的实例可以覆盖这些默认设置,此时 NannyML 将按照指示执行插补。

让我们使用以下代码可视化我们的多变量漂移结果:

数据重建漂移(图片由作者创建)

以下是对上述图表的分析:

  1. 参考区域使用蓝色阶梯图显示。图上的厚方块点标记显示了块的中点。

  2. 紫色阶梯图描绘了分析期间每个区间的重建误差。

  3. 采样误差可见为围绕重建误差的低饱和紫色阴影区域。

  4. 上限和下限警报阈值由水*红色虚线指示。

  5. 如果重建误差超过了上限或下限阈值,就会触发警告,并显示在整个相关块的宽度上带有红色低饱和度背景。块中间的红色钻石形指针也进一步表示这一点。

排名

特征是由 NannyML 根据它们在所有方法中收到的警报总数进行排名的。预计的性能和单变量特征漂移与此相关。

排名(图片由作者创建)

我希望你能学到一些有用的技巧,了解如何在机器学习模型运行时进行监控,并且现在你知道为什么部署绝不是最后一步。我也希望你现在对如何开始监控生产中的模型有足够的清晰认识!

感谢你阅读到这里。如果你有任何问题,请告诉我。我很乐意帮助你! 🙂

监控机器学习模型的生产:为什么和如何?

原文:towardsdatascience.com/monitoring-machine-learning-models-in-production-why-and-how-13d07a5ff0c6?source=collection_archive---------7-----------------------#2023-09-05

我们的模型在不断发展的世界中受到什么影响?分析重点是漂移示例,以及实施基于 Python 的监控策略

John LeungTowards Data Science John Leung

·

关注 发表在 Towards Data Science · 12 分钟阅读 · 2023 年 9 月 5 日

--

机器学习(ML)模型开发通常需要时间和技术专长。作为数据科学爱好者,当我们获取一个数据集进行探索和分析时,我们迫不及待地使用各种最先进的模型以数据为中心的策略进行训练和验证。当我们优化模型性能时,感觉所有的任务仿佛都已完成,令人非常满足。

然而,将模型部署到生产环境后,有许多原因可能导致模型性能下降或退化。

照片由Adrien Delforge拍摄,来源于Unsplash

#1 训练数据是通过模拟生成的

数据科学家经常面临限制访问生产数据,这导致模型使用模拟或样本数据进行训练。尽管数据工程师负责确保训练数据在规模和复杂性方面的代表性,但训练数据仍然在某种程度上与生产数据有所偏差。上游数据处理中的系统性缺陷,如数据收集和标记,也存在风险。这些因素可能影响额外有用输入特征的提取或阻碍模型的良好泛化能力。

例如: 财务行业中的投资者数据或医疗行业中的患者信息通常由于安全和隐私问题而被模拟。

#2 新的生产数据展示了新的数据分布

随着时间的推移,输入特征的特征也可能发生变化,例如年龄组、收入范围或其他客户人口统计数据的变化。数据源本身甚至可能由于各种情况完全被替代。在模型开发过程中,优化依赖于学习和捕捉训练数据中多数群体的模式。然而,随着时间的推移,之前的多数群体可能会转变为生产数据中的少数群体,从而使原有的静态模型无法满足最新的生产需求。

例如: 模型最初使用亚洲地区的客户数据进行了良好的训练。最*,由于业务扩展到美国,相同的模型直接根据具有变化的输入特征进行预测。

数据漂移(图片由作者提供)

#3 我们预测的模式在不断演变

除了输入特征分布的变化外,特征与目标变量之间的关系在不断发展的环境中也可能发生变化。这些变化可能会以意想不到的方式发生,导致原始模型逐渐失效。

  • 突发的概念变化

这些变化可能会突然发生,有时在几周内,由于不可预见的情况。

示例: 公共 COVID-19 封锁期间对虚拟会议服务需求的激增。

  • 渐进的概念变化

这种变化需要较长时间才能显现,通常是自然进程。

示例: 由于长期通货膨胀,乳制品价格的逐步上涨。

  • 反复出现的概念变化

这些变化可能会周期性发生,通常在一年中的特定时间。

示例: 在诸如黑色星期五和圣诞节前的星期六等特殊日子,电子商务销售的快速增长。

概念漂移(图片由作者提供)

模型开发只是生产就绪的机器学习系统中的一小部分

许多公司通过使用机器学习应用程序强调数据驱动的决策制定。假设您的机器学习模型用于关键应用,例如医疗诊断,以帮助医疗专业人员识别疾病和病症。任何模型退化都意味着影响诊断的准确性,可能导致错误的治疗方案和患者结果的妥协。现实世界中有大量重要的案例,因此,实施客观和持续的监控以检测任何可能的变化变得至关重要。

在接下来的部分中,我们将深入探讨各种监控层级,并提供示例 Python 代码来演示其实现。

图片来源于 Nathy dogUnsplash

#1 监控模型性能指标

为了检测模型退化的任何迹象,直接且有效的方法之一是跟踪性能指标的变化。

  • 回归模型的性能指标:决定系数(R-squared)、均方根误差(RMSE)和*均绝对误差(MAE)

  • 分类模型的性能指标:精确度、召回率和 F1 值

从初始部署中收集的这些性能指标作为持续监控和评估的基准。在收集新的地面真实数据时,例如在营销活动结束后,定期重新评估这些指标至关重要。如果误差指标超过预定义的阈值,或者诸如 R-squared 这样的指标低于阈值,则需要考虑重新执行数据工程过程和重新训练模型。

尽管这种监控方法提供了关于漂移的有价值的见解,但它往往滞后。我们可以采取更积极的方法来监控最新的输入数据。

#2 检测数据分布的变化

我们可以应用统计方法来比较两个数据集的数据分布,而不是等待足够的输入数据来可靠地评估模型性能。在我们的情况下,我们可以确定训练数据集的分布是否与最新的生产数据集相同。如果不能统计上自信这两个分布相同,则表明模型出现了漂移。这作为性能变化的代理。

  • Kolmogorov-Smirnov 检验(K-S Test):非参数检验(即对基础数据分布没有假设)用于数值特征,其在分布中心比在尾部更敏感。

解释: 当 p 值< 0.05 时,触发表示存在漂移的警报。

  • 人口稳定指数(PSI):一种可以应用于数值变量和分类变量的统计检验。它是一个指标,显示每个变量如何与基准值独立地偏离。当评估特征分布而不是目标变量时,有时被称为特征稳定指数(CSI)。

解释: 值为 0~0.1 表示没有显著的分布变化;值为 0.1~0.2 表示中等分布变化;值大于 0.2 则解释为显著的分布变化。

其他著名的统计检验包括 Kullback-Leibler 散度、Jensen-Shannon 散度 和 Wasserstein 距离。

#3 使用滑动窗口方法监控漂移

检测数据或模式漂移的任何延迟都会导致时间差,这可能导致地面真实数据与模型预测之间的差异。为了弥补这一差距,是否有进一步的进展?一种有前景的想法是利用流数据而不是批数据。

自适应窗口(ADWIN)算法利用滑动窗口方法有效检测概念漂移。与传统的固定大小窗口不同,ADWIN 通过在不同点截断统计窗口来决定窗口的大小。每当新数据到来时,ADWIN 分析统计数据并识别出两个子窗口在均值上显著不同的点。

解释: 当两个均值之间的绝对差异超过预定义的阈值时,会触发一个警报,表示存在漂移。

示例演示

让我们探讨一个示例,展示上述监控策略的实现。

我们将利用从 Kaggle 获得的 数据集。该数据集包含 10 万条记录,涵盖 28 个描述客户人口统计和信用相关历史的特征。

我们的目标是将全球金融公司中的客户按信用评分区间进行分类。目标变量是Credit_Score,这是一个分类测量,分为“差”、“标准”和“好”。

特征的示例包括:

  • Occupation: 客户的职业(例如科学家、教师、工程师等)

  • Annual_Income: 客户的年收入

  • Credit_History_Age: 客户信用历史的年龄

  • Payment_Behaviour: 基于支出频率(低/高)和支付金额(小/中/大)的 6 组支付行为

图片由 CardMapr.nl 提供,来自 Unsplash

模型性能指标

我们开始应用数据清洗和转换技术,包括但不限于:

  • 修正/删除缺失值或不正确数据(例如年龄 < 0)或重复记录

  • 检测和处理数据异常值

  • 对数值变量进行最小-最大缩放

  • 对分类变量应用标签编码

这一部分需要深入的探索性数据分析。然而,由于重点在于分享监控策略,这里未详细讨论所执行的转换。

随后,我们拆分数据集并开发了高级梯度提升算法 LightGBM

import numpy as np
import pandas as pd
from sklearn import metrics
from sklearn.metrics import f1_score, precision_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from lightgbm import LGBMClassifier

# Convert target variable to numeric
df['Credit_Score'] = df['Credit_Score'].str.replace('Good', '3', n=-1)
df['Credit_Score'] = df['Credit_Score'].str.replace('Standard', '2', n=-1)
df['Credit_Score'] = df['Credit_Score'].str.replace('Poor', '1', n=-1)
df['Credit_Score'] = df[['Credit_Score']].apply(pd.to_numeric)

# Split the dataset
X=df.loc[:, df.columns != 'Credit_Score']
Y=df['Credit_Score']
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.25)

# Train the LightGBM model
lgbm = LGBMClassifier()
lgbm.fit(x_train, y_train)
y_pred = lgbm.predict(x_test)

# Print performance metrics
print('F1 score: %.3f' % f1_score(y_test, y_pred, average='weighted'))
print('Precision: %.3f' % precision_score(y_test, y_pred, average='weighted'))
print('Recall: %.3f' % recall_score(y_test, y_pred, average='weighted'))

在我们的模型开发过程中,我们收集了多个性能指标,包括 0.810 的 F1 分数、0.818 的精确度和 0.807 的召回率。后续监控可以类似地进行评估。例如,如果 F1 分数低于 0.75,则会触发警报,提示我们立即采取措施来缓解问题。

K-S 检验

为了展示 K-S 检验的工作原理,我选择了数值特征Credit_History_Age。我们将通过创建三个不同的数据集来检查这个统计检验的敏感性:一个包含 1000 个样本,另一个包含 5000 个样本,还有一个包含大约 64000 个样本,这对应于清理后的训练样本的总量。Credit_History_Age特征中的每个数据点都是从训练数据中随机挑选,并通过随机浮点数进行了改变。

‘Credit_History_Age’在原始数据和漂移数据中的数据分布(图像来源:作者)

我们获得了以下结果:

  • 对于 1000 个样本的数据集,K-S 检验的 p 值为 0.093。

  • 对于 5000 个样本的数据集,K-S 检验的 p 值为 0.002。

  • 对于大约 64000 个样本的数据集,K-S 检验的 p 值为 0.000。

当使用 1000 个样本检查数据漂移场景时,p 值超过了 0.05。因此,我们可以得出结论,两种分布仍然相似。然而,随着样本量增加到 5000 及以上,K-S 检验表现出卓越的性能,p 值明显低于 0.05。这清楚地表明了数据分布的漂移,作为明确的警报。

from scipy import stats

# Create new datasets with different no. of samples
original_df = x_train[['Credit_History_Age', 'Payment_Behaviour']].reset_index(drop=True)
new_df = x_train[['Credit_History_Age', 'Payment_Behaviour']].reset_index(drop=True)
new_df1 = new_df.sample(n = 1000).reset_index(drop=True)
new_df2 = new_df.sample(n = 5000).reset_index(drop=True)
new_df3 = new_df.sample(n = len(x_train)).reset_index(drop=True)

# Prepare drifted data for numeric feature
def drift_numeric_col(df, numeric_col, drift_range):
    df[numeric_col] = df[numeric_col] + np.random.uniform(0, drift_range, size=(df.shape[0], ))

drift_numeric_col(new_df1, 'Credit_History_Age', 2)
drift_numeric_col(new_df2, 'Credit_History_Age', 2)
drift_numeric_col(new_df3, 'Credit_History_Age', 2)

# K-S Test
def ks_test(original_df, new_df, numeric_col):
    test = stats.ks_2samp(original_df[numeric_col], new_df[numeric_col])
    print("Column : %s , p-value : %1.3f" % (numeric_col, test[1]))

# Conduct K-S Test for numeric feature
ks_test(original_df, new_df1, 'Credit_History_Age')
ks_test(original_df, new_df2, 'Credit_History_Age')
ks_test(original_df, new_df3, 'Credit_History_Age')

PSI

除了使用 K-S 检验外,我们还将利用 PSI(人口稳定性指数)来评估数值特征Credit_History_Age以及类别特征Payment_Behaviour。为了表示漂移效果,我们随机将Payment_Behaviour特征的 80%数据替换为特定标签值。

# Prepare drifted data for categorical column
def drift_cat_col(df, cat_col, drift_ratio):
    no_of_drift = round(len(df)*drift_ratio)
    random_numbers = [random.randint(0, 1) for _ in range(no_of_drift)]
    indices = random.sample(range(len(df[cat_col])), no_of_drift)
    df.loc[indices, cat_col] = random_numbers

drift_cat_col(new_df1, 'Payment_Behaviour', 0.8)
drift_cat_col(new_df2, 'Payment_Behaviour', 0.8)
drift_cat_col(new_df3, 'Payment_Behaviour', 0.8)

‘Payment_Behaviour’在原始数据和漂移数据中的数据分布(图像来源:作者)

  • 数值特征Credit_History_Age

在 1000 个样本的情况下,PSI 值为 0.023。

在 5000 个样本的情况下,PSI 值为 0.015。

在大约 64000 个样本的情况下,PSI 值为 0.021。

  • 类别特征Payment_Behaviour

在 1000 个样本的情况下,PSI 值为 0.108。

在 5000 个样本的情况下,PSI 值为 0.111。

在大约 64000 个样本的情况下,PSI 值为 0.112。

所有Credit_History_Age特征的 PSI 值明显低于 0.1,这表明没有显著的分布变化。通过将这些结果与 K-S 检验的结果进行比较,我们观察到 K-S 检验在检测分布变化方面比 PSI 具有更高的敏感性。

另一方面,Payment_Behaviour特征的 PSI 值大约为 0.11,表示中等程度的分布变化。有趣的是,三个 PSI 值保持相对一致,暗示着 PSI 的有效性不太依赖于样本量。此外,PSI 具有监测各种特征类型的灵活性,因此在漂移检测中仍然是一种有价值的方法。

以下是 PSI 的实现代码:

def psi(data_base, data_new, num_bins = 10):
    # Sort the data
    data_base = sorted(data_base)
    data_new = sorted(data_new)

    # Prepare the bins
    min_val = min(data_base[0], data_new[0])
    max_val = max(data_base[-1], data_new[-1])
    bins = [min_val + (max_val - min_val)*(i)/num_bins for i in range(num_bins+1)]
    bins[0] = min_val - 0.0001
    bins[-1] = max_val + 0.0001

    # Bucketize the baseline data and count the samples
    bins_base = pd.cut(data_base, bins = bins, labels = range(1,num_bins+1))
    df_base = pd.DataFrame({'base': data_base, 'bin': bins_base})
    grp_base = df_base.groupby('bin').count()
    grp_base['percent_base'] = grp_base['base'] / grp_base['base'].sum() 

    # Bucketize the new data and count the samples
    bins_new = pd.cut(data_new, bins = bins, labels = range(1,num_bins+1))
    df_new = pd.DataFrame({'new': data_new, 'bin': bins_new})
    grp_new = df_new.groupby('bin').count()
    grp_new['percent_new'] = grp_new['new'] / grp_new['new'].sum()

    # Compare the bins
    psi_df = grp_base.join(grp_new, on = "bin", how = "inner")

    # Calculate the PSI
    psi_df['percent_base'] = psi_df['percent_base'].replace(0, 0.0001)
    psi_df['percent_new'] = psi_df['percent_new'].replace(0, 0.0001)
    psi_df['psi'] = (psi_df['percent_base'] - psi_df['percent_new']) * np.log(psi_df['percent_base'] / psi_df['percent_new'])

    # Return the total PSI value
    return np.sum(psi_df['psi'].values)

# Conduct K-S Test for numeric feature
psi(original_df['Credit_History_Age'], new_df1['Credit_History_Age'])
psi(original_df['Credit_History_Age'], new_df2['Credit_History_Age'])
psi(original_df['Credit_History_Age'], new_df3['Credit_History_Age'])

# Conduct K-S Test for categorical feature
psi(original_df['Payment_Behaviour'], new_df1['Payment_Behaviour'])
psi(original_df['Payment_Behaviour'], new_df2['Payment_Behaviour'])
psi(original_df['Payment_Behaviour'], new_df3['Payment_Behaviour'])

ADWIN 算法

最后,我们测试了 ADWIN 在检测数值特征 Credit_History_Age 变化方面的能力。数据流 包含训练数据,随后是漂移数据。我们期望算法在检查完原始数据后,能够迅速识别出漂移。

为了直观地展示情况,创建了一个散点图来展示原始数据的最后 500 个点(以蓝色表示),以及漂移数据的前 500 个点(以绿色表示)。漂移数据显示出略高的*均值。

散点图(作者提供的图像)

通过不断添加流元素,ADWIN 在索引 64457 处识别出了变化,这是漂移数据中的第 637 个数据点。相比之下,K-S 测试和 PSI 需要更多的数据点来自信地得出漂移的存在。ADWIN 更好的性能证明了其在速度和简便性方面监控多样特征的能力。

以下是 ADWIN 的实现代码:

from skmultiflow.drift_detection import ADWIN

adwin = ADWIN()
data_stream=[]
data_stream = np.concatenate((original_df['Credit_History_Age'],new_df3['Credit_History_Age']))

# Add stream elements to ADWIN and verify if drift occurred
for i in range(len(data_stream)):
  adwin.add_element(data_stream[i])
  if adwin.detected_change():
  print('Change detected at index {}'.format(i))
  adwin.reset()

总结一下

我们深入探讨了数据漂移和模型漂移这两个关键概念,它们可能导致生产中的模型衰退。我们可以通过模型性能指标、统计测试和自适应窗口技术来主动监测和检测漂移条件。与参与一次性的 Kaggle 比赛不同,构建一个生产就绪的 ML 系统是一个迭代的过程。这需要一种将全面的模型监控整合进来以确保强大且持续高性能服务空间的心态转变。

在你离开之前

如果你喜欢这篇文章,我邀请你关注 我的 Medium 页面。通过这样做,你可以保持更新,了解与数据科学副项目、机器学习操作(MLOps)演示和项目管理方法相关的精彩内容。

## 超越 A/B 测试的策略优化

用通俗易懂的方式深入解释经典 A/B 测试的优化:Epsilon-贪婪、汤普森采样、上下文……

towardsdatascience.com [## 如何将数据中心化的 AI 思维方式应用于文本分类问题?

使用各种以数据为中心的 Python 包和 AI 技巧掌握客户投诉分类

medium.com

监控生产环境中的 NLP 模型

原文:towardsdatascience.com/monitoring-nlp-models-in-production-ac65745772cf?source=collection_archive---------4-----------------------#2023-02-20

关于检测文本数据漂移的代码教程

Elena SamuylovaTowards Data Science Elena Samuylova

·

关注 发布于 Towards Data Science ·13 min read·2023 年 2 月 20 日

--

图片由作者提供。

所有生产环境中的机器学习模型都需要监控。NLP 模型也不例外。但是,监控使用文本数据的模型可能与监控基于表格数据的模型有所不同。

在本教程中,我们将深入探讨一个具体的例子。 我们将探索影响生产环境中 NLP 模型性能的问题,使用一个示例玩具数据集模拟这些问题,并展示如何监控和调试它们。

我们将使用一个药品评论数据集,并按照以下步骤进行操作:

  • 训练一个简单的评论分类模型,并在验证数据集上评估其质量;

  • 模拟数据质量问题,测试它们对模型准确性的影响,并探索如何提前识别这些问题;

  • 将模型应用于新数据,并探索如何检测和调试模型质量衰退问题,当模型应用于之前未见过的输入时。

我们将使用 Evidently 开源 Python 库 来评估和调试模型问题。

你可以在 示例 Colab 笔记本中重现这些步骤并探索更多细节。

用例

图片由作者提供。

让我们假设你想要对药品评论进行分类

这个自然语言处理用例在电子商务中很常见。例如,用户可能会在在线药房网站上留下评论。你可能希望根据内容将每条评论分配到一个类别,如“副作用”、“使用难易”或“效果”。一旦你创建了模型,你可以自动对每个新提交的评论进行分类。标签将改善用户体验,帮助更快地找到相关内容。

你可能会在其他场景中使用类似的分类模型。例如,在以医疗保健为重点的聊天应用中,挖掘相关信息并丰富用户体验。在这种情况下,你可能会批量分类评论并将其存储在某个数据库中。你可以按需检索这些评论,将内容展示给用户。

让我们以这个用例作为灵感,从一个更简单的分类模型开始。我们的目标是预测整体评论情感是否高度积极或消极。

创建一个模型

要解决这个问题,你首先需要一个标注数据集。

图片由作者提供。

为了说明,我们将使用 UCI 存储库中的药品评论数据集

免责声明: 这个创建的模型仅用于研究和教育目的,以展示机器学习模型评估和监控过程。它不应以任何其他形式或目的使用,也不应用于通知任何实际决策。

数据集相当大。我们将从一个特定的子集开始:止痛药的评论。我们将其分为两部分。60% 用于“训练”部分。另 40% 是“验证”部分。

我们将训练一个模型来区分“1”(负面评论)和“10”(正面评论)的评分,使其成为一个简单的二分类问题。

在实际操作中,你通常会面临有限的标注数据。以仅表示模型最终可能应用的数据子集的数据集开始并不罕见。

一旦我们训练了模型,我们可以在验证数据集上评估其准确性。我们得到的结果是:验证数据集上的准确率为 0.836。我们认为这对于我们的演示目的已经足够好。

我们可以期待在类似数据中生产中的质量相似。如果准确性大幅低于这一水*,我们应该做出反应并深入探讨发生了什么。

注意: 这是一个简单的演示。如果你在处理真实用例时,不要忘记交叉验证,以便对模型质量做出更有依据的预期。

生产中的模型

一旦我们将模型投入生产,就会将其应用于新的、未见过的数据。

在电子商务示例中,我们可能会将模型封装为 API。我们将在网站上提交新评论时调用模型,并根据模型的响应分配一个类别进行显示。在聊天应用场景中,我们可能会执行批量评分。我们将新的预测与分配的标签一起写入数据库。

在这两种情况下,你通常不会得到即时反馈。 没有快速的方法来知道预测标签是否正确。然而,你确实需要某种东西来监控模型的表现,以确保其按预期工作。

图片来源:作者。

了解模型表现良好的方式有很多:

  • 你可以在网站界面中直接设置反馈机制。 例如,你可以允许评论作者或读者报告错误分配的类别并建议更好的类别。如果收到大量报告或修正,你可以对此做出反应并进行调查。

  • 手动标注作为质量控制。 在最简单的形式下,模型创建者可以查看一些模型预测,以查看其是否按预期行为。你也可以不时地邀请外部标注者标注一部分数据。通过这种方式,你可以直接评估模型预测的质量与专家分配的标签之间的差异。

在这两种情况下,模型检查是被动的:你只能在获得标签并评估准确性之后才注意到并解决模型质量问题。

虽然你可能经常接受一些延迟或甚至质量下降(因为错误的成本是可以接受的),但尝试提前检测问题是一个好习惯。

模型质量下降的两个常见原因是数据质量问题和输入数据分布的变化。让我们探讨一下如何检测这些问题!

示例 1:数据质量和完整性

图片来源:作者。

数据质量问题千差万别。 例如,你可能在输入数据处理过程中有一些漏洞,将 HTML 标签泄露到评论文本中。数据还可能因为编码错误、特殊符号的存在、不同语言的文本、表情符号等而被损坏。在作为评分流程的一部分运行的特征转换代码、后处理或清理步骤中可能存在错误。

在我们的例子中,我们人为地改变了数据集。 我们使用了相同的验证数据集,并做了一些更改:注入了随机的 HTML 标签,并将一些评论翻译成法语。目标是“破坏”数据集,模拟数据质量问题。

你可以在随附的 notebook* 中查看完整的代码。*

现在,让我们检查一下这个修改后的数据上的模型质量:

图片由作者提供。Evidently 库的截图。

模型质量低于在“干净”数据集上的初始验证结果。准确率仅为 0.747。

我们如何解决这种下降问题? 如果这种情况在实际中发生,我们的下一步是深入研究模型的表现和数据,以了解发生了什么。让我们来看看!

我们将使用 Evidently Python library。它包含各种评估指标和测试,并帮助生成适用于不同场景的交互式报告。

在这种情况下,我们将通过结合几个我们想运行的评估来创建一个自定义报告,以了解数据的变化。

要应用 Evidently,我们首先需要准备数据并映射架构,以便 Evidently 能够正确解析。这称为“列映射”。我们在所有评估中重用它,因为数据架构保持不变。

这里是我们如何指向包含预测值、目标值的列,并指定评论列应被视为文本列:

column_mapping = ColumnMapping() 
column_mapping.target = 'is_positive' 
column_mapping.prediction = 'predict_proba' 
column_mapping.text_features = ['review']

接下来,我们生成报告。为此,我们需要执行以下操作:

  • 将我们的原始验证数据作为“参考”(比较基线)传入,并将修改后的验证数据作为“当前”,

  • 指定我们想在报告中包含的评估类型(“指标”),

  • 调用可视化报告以在 Jupyter notebook 或 Colab 中进行探索。

在我们的例子中,我们选择评估目标、预测和数据漂移。首先,我们想查看模型输出是否发生了变化。其次,我们想查看输入文本是否发生了变化。

有几种方法可以评估文本数据集之间的相似性。 一种方法是比较文本数据的描述性统计(例如文本长度、词汇外单词的比例以及非字母符号的比例),并探索它们是否在两个数据集之间发生了变化。这个选项在 Evidently 中作为 Text Descriptors Drift 指标提供。除了评估模型预测和目标的漂移之外,我们将其包含在综合报告中。

这是你可以调用它的方式:

data_drift_report = Report( 
    metrics=[ 
        ColumnDriftMetric('is_positive'), 
        ColumnDriftMetric('predict_proba'), 
        TextDescriptorsDriftMetric(column_name='review'), 
    ] 
) 
data_drift_report.run(reference_data=reference, 
                      current_data=valid_disturbed, 
                      column_mapping=column_mapping) 
data_drift_report

一旦我们展示报告,可以看到真实标签或预测概率没有漂移。

图片由作者提供。Evidently 库的截图。

但有些输入文本属性是不同的!

图片由作者提供。Evidently 库的截图。

在后台,Evidently 计算这些描述符并应用不同的统计测试和距离度量,以检查两个数据集之间是否存在显著的变化。

特别是,它指出了文本长度分布的变化。如果我们扩展报告中的细节,我们可以看到一些附加的图表,这些图表有助于理解这种变化。

一些评论现在疑似变得很长:

作者提供的图片。截图来自 Evidently 库。

词汇量也发生了变化。多个评论包含超过 30%的词汇外单词:

作者提供的图片。截图来自 Evidently 库。

这些发现可以帮助我们找到变化的例子,以了解发生了什么。例如,我们可以在数据集中查询所有超过 1000 字的长评论和超过 30%为词汇外单词的评论。

一旦我们显示出这些例子,我们可以迅速了解发生了什么:

  • 含有 HTML 标签的文本直接传递给模型

  • 评论使用了新的、意想不到的语言

这是查询结果之一:

作者提供的图片。截图来自示例笔记本。

知道发生了什么之后,我们可以与数据工程团队(以整理管道)和产品团队(以确保法语评论是预期的,并且是时候为这些评论创建一个单独的模型)解决问题。

示例 2:数据分布变化

作者提供的图片。

生产中可能发生的另一种变化是内容的变化,即模型需要分析的文本内容发生变化。这种变化最终可能导致模型质量下降或模型漂移。它可以以不同的形式出现。

其中一种是概念漂移,当模型学习到的一些概念发生演变时。例如,一些词语或符号的意义可能会逐渐改变。也许之前代表“积极”评论的某些表情符号现在经常用于相反的意图。或者市场上出现了另一种具有相同活性成分的新药,这将一个“概念”转换为两个不同的概念。

另一个是数据漂移,当模型应用于与训练数据不同的新数据时。模型学到的关系依然成立,但它没有见过最新数据中的模式,因此无法很好地评分。例如,如果你将训练用于分类医学评论的模型应用于其他产品,你将会观察到数据漂移。

理解数据漂移和概念漂移的区别对于解释变化很有用。然而,要检测这些变化,我们通常会使用相同的方法。如果你已经有了标签,真实模型质量(例如准确率)是衡量模型漂移的最佳指标。如果你没有标签或者想要调试质量下降,你可以查看输入数据和预测的变化,然后利用你对领域的理解来解释。

让我们回到我们的示例数据集,看看模型漂移在实践中可能是什么样的。

我们现在将模型应用于一个新的、未见过的数据集。 我们将使用不同类别的药物评论:这些评论不再与止痛药有关,而是与抗抑郁药有关。我们可能仍然会期望合理的质量:评论者可能会使用重叠的词汇来描述某种药物是否有效。

模型没有完全失败,但准确率只有 0.779。

作者提供的图片。截图来自 Evidently 库。

这低于预期。我们来调查一下吧!

我们可以再次生成漂移报告,并立即注意到一些变化。特别是,标签的分布已经发生漂移。

作者提供的图片。截图来自 Evidently 库。

当前数据集中的评论也更长,OOV 词汇出现的频率更高。但没有像上述情况那样明显的变化。

作者提供的图片。截图来自 Evidently 库。

我们可以尝试其他方法来调试发生了什么:除了比较文本统计数据,看看是否数据集的内容发生了变化。

检测数据漂移的方法有很多。对于表格数据,你通常会查看数据集中各个特征的分布。对于文本数据,这种方法就不太方便了:你可能不想计算数据集中每个词的分布。词汇实在太多了,结果也难以解释。

Evidently 使用了不同的文本漂移检测方法: 一个领域分类器 它训练一个背景模型来区分参考数据集和当前数据集。二分类器的 ROC AUC 显示漂移是否被检测到。如果一个模型可以可靠地区分当前或参考数据集中的评论,那么这两个数据集可能足够不同。

作者提供的图片。

这种方法和其他方法在论文“ Failing loudly: An Empirical Study of Methods for Detecting Dataset Shift”中进行了描述。

这并非没有警告。如果你在新数据集中有一些时间信息(例如,每条评论包括日期),模型可能会很快学会区分数据集。这可能只是因为其中一个数据集包含“March”一词,而另一个包含“February”,或者提到了黑色星期五促销。然而,我们可以通过查看领域分类器模型的顶级特征和一些示例来评估这一点。

如果检测到文本数据漂移,Evidently 将自动提供一些有用的信息:

  • 当前数据集和参考数据集中的典型词汇。这些词汇在预测特定评论属于哪个数据集时最具指示性。

  • 文本示例来自当前和参考数据集,这些文本对分类器来说最容易正确标记(预测概率非常接* 0 或 1)。

为了使用这种方法,我们将创建一个新报告,并在给定列中包含有助于检测漂移的度量。对于包含文本数据的列,领域分类器是默认方法。

data_drift_dataset_report = Report(metrics=[ 
    ColumnDriftMetric(column_name='review') 
]) 

data_drift_dataset_report.run(reference_data=reference, 
                              current_data=new_content, 
                              column_mapping=column_mapping) 

data_drift_dataset_report

这是它对我们的数据集的显示情况。

首先,它确实检测到了分布漂移。 分类器模型非常自信,ROC AUC 为 0.94。其次,最具辨识性的特征非常明确地指向了文本内容的可能变化。

参考数据集包含“pain”和“migraine”这些词汇。

作者提供的图片。来自 Evidently 库的截图。

当前数据集包含“depression”和“antidepressant”这些词汇。

作者提供的图片。来自 Evidently 库的截图。

从具体的示例评论中也可以看出这一点。它们指的是不同的药物类别,作者使用不同的词汇来描述某种药物是否有效。例如,“improve mood”与“relieve pain”不同,使得模型更难以分类评论的情感。

一旦我们确定了模型漂移的原因,我们可以制定解决方案:通常是使用新标记的数据重新训练模型。

如何在生产环境中应用这一点?

在这个玩具示例中,我们展示了调试工作流程。我们测量了模型的实际准确性,并深入挖掘以确定质量下降的原因。

在实践中,你可以主动进行数据质量检查。 例如,你可以在批量评分流程中实现这个早期质量控制步骤。你可以测试你的数据,以便在获得实际标签或甚至评分模型之前发现潜在问题。

如果你发现评论正文中有 HTML 标签等问题,你可以立即采取行动解决它们:通过更新和重新运行预处理流程。

你可以对数据漂移检查做同样的事情。每次获得新一批数据时,你可以评估其关键特征以及它与前一批数据的相似程度。

如果你检测到漂移并发现它确实是由于新类型的内容或话题出现,你也可以采取主动措施。在这种情况下,这很可能意味着启动新的标记过程和随后的模型重新训练。

Evidently 是一个开源 Python 库,帮助评估、测试和监控生产中的机器学习模型。你可以使用它来检测数据漂移、数据质量问题或跟踪表格和文本数据的模型性能。

在 GitHub 上查看 ⟶

接下来做什么?

评估文本数据漂移可能涉及其他挑战。例如,你可能需要监控嵌入中的漂移,而不是原始文本数据。你还可以进行额外的测试和评估,例如与模型的鲁棒性和公*性相关的测试。

点击这里注册 以获取有关新手动教程和新功能发布的更新。

最初发表于 https://www.evidentlyai.com 于 2023 年 2 月 20 日,并与 Natalia Tarasova 合著。

使用 GEE 在全球范围内监测海表温度

原文:towardsdatascience.com/monitoring-sea-surface-temperature-at-the-global-level-with-gee-1d7349c7da6

如何使用 Python 创建一个用于海洋监测的 Streamlit 应用

Bryan R. VallejoTowards Data Science Bryan R. Vallejo

·发表于 Towards Data Science ·7 分钟阅读·2023 年 3 月 8 日

--

图片由作者提供。SST 监测器 使用 GEE 于 2023 年 1 月 14 日

介绍

过去十年,地球观测及其技术基础设施发展迅速。许多卫星星座提供开放和便捷的数据访问,研究人员可以轻松获取。例如,Google Earth Engine 是一个云基础设施,提供来自多个提供商的数据,如 Modis、NOAA、ASTER、Landsat 等,你可以直接探索和分析这些数据。在 GEE 中了解更多。如果你想在 Python 中使用 API,可以使用由 Qiusheng Wu 开发的 geemap Python 库,它具有很棒的功能,可以像本教程中那样直接使用。

基础设施模型几乎实时运行,因此可以获取最新数据以帮助监测陆地和海洋。对于本教程,我们将使用 HYCOM 数据集,该数据集包含一个数据同化混合模型,展示了全球范围内的海表温度和盐度。该模型包含每个时间层的深度值,因此用户可以在特定的深度和日期可视化海洋。混合坐标海洋模型已在许多出版物中出现,网站上提供了大量有关数据集的文档。如果你想了解更多,推荐你查看 官方网站

应用程序

通过下一个链接获取应用程序访问权限:

🌡️ SST 全球监测 — 应用程序

作者提供的图片。应用程序的简要视图。2023 年 1 月 15 日的 100 米深度

目标

我希望通过这个教程创建一个易于使用的应用程序,能够在全球范围内显示海表温度。应用程序的功能应包括:

  • 时间选择

  • 深度选择

  • 图例配置

一旦互动完成,我们将把应用程序部署到 Streamlit 云端。

数据许可证

HYCOM 数据集以 公共领域标记 1.0 发布。用户可以复制、修改和重新分发,甚至用于商业目的。HYCOM 官方分发为 US DOD 分发 A,表示公开发布和无限分发。

编码教程

希望在开始这个教程之前,你对 Python 编程有所了解。如果没有,那也没关系,只需在教程中请求帮助,我可以建议一些教育材料。一般来说,Python 可以在 Anaconda 中很好地使用。我建议你先安装并熟悉包的安装,然后再开始。在任何情况下,你都能成功部署这个应用程序。

让我们开始创建一个 Python 文件并导入所需的库。

import json
from datetime import datetime, timedelta, date

import ee
import geemap.colormaps as cm
import geemap.foliumap as geemap
import streamlit as st

要在 Streamlit 中使其工作,你可能需要使用密钥进行 GEE 认证。如何做的说明已经写在 Mykola Kozyr 的这篇清晰且出色的教程中。

[## 在 Streamlit 应用程序中使用 Google Earth Engine

我最*发布了一个 Streamlit 应用程序,用于可视化欧洲河流流域的地表温度数据……

medium.com](https://medium.com/@mykolakozyr/using-google-earth-engine-in-a-streamlit-app-62c729793007?source=post_page-----1d7349c7da6--------------------------------)

仔细按照之前提供的说明进行操作,并将自己的凭据添加到 app.py 文件中。它可能看起来像这样。

# ______ GEE Authenthication ______

# _____ STREAMLIT _______

# Secrets
json_data = st.secrets["json_data"]
service_account = st.secrets["service_account"]

# Preparing values
json_object = json.loads(json_data, strict=False)
json_object = json.dumps(json_object)

# Authorising the app
credentials = ee.ServiceAccountCredentials(service_account, key_data=json_object)
ee.Initialize(credentials)

环境

要在 Streamlit 中进行部署,你应该添加一个 requirements.txt 文件,其中包含部署所需的包。在这种情况下,我们将添加要求和一个额外的 packages.txt 文件。这两个文件来自 gishub 的 geemap。

向 repo 中添加一个 requirements.txt 文件,内容包括:

--find-links=https://girder.github.io/large_image_wheels GDAL
# cartopy
geemap
geopandas
ee
jupyter-server-proxy
leafmap
localtileserver
nbserverproxy
owslib
palettable
plotly
streamlit==1.12.2
streamlit-bokeh-events
streamlit-folium
tropycal

另附一个 packages.txt 文件,内容包括:

ffmpeg
gifsicle
build-essential
python3-dev
gdal-bin
libgdal-dev
libproj-dev
libgeos-dev
proj-bin

布局配置

在这里,我们将为页面添加标题,定义 Streamlit 的宽布局,并对画布施加紧密的限制,以使地图看起来更好。

# _______________________ LAYOUT CONFIGURATION __________________________

st.set_page_config(page_title='SST monitor', layout="wide")

# shape the map
st.markdown(
        f"""
<style>
    .appview-container .main .block-container{{

        padding-top: {3}rem;
        padding-right: {2}rem;
        padding-left: {0}rem;
        padding-bottom: {0}rem;
    }}

</style>
""",
        unsafe_allow_html=True,
    )

侧边栏配置和参数输入

在这里,我们将添加一个form,用于输入地图参数,如深度和日期。当前地图始终显示最后可用的图层,即当前日期前的 2 天。帮助中添加了一些注释。

检查描述代码的内联注释。

## ___________________ SIDEBAR PARAMETERS ___________________________

st.sidebar.info('### ***Welcome***\n###### ***Sea Surface Temperature (SST) monitor*** 🧐🌊🌡️')

form = st.sidebar.form('Ocean data')

with form:

    # depths in slider
    depth_options = [0, 2, 4, 6, 8, 10, 12, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100, 125, 150, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1250, 1500, 2000, 2500, 3000, 4000, 5000]

    depth = st.select_slider('Depth (m)', options = depth_options, value = 0, help = 'Depth in ocean to fetch temperature layer' )

    # dates of sst
    ocean_date = st.date_input('Date', min_value=datetime.strptime('1992-10-02', '%Y-%m-%d'), max_value = datetime.now(), help = 'Selected date of temperature to be displayed')

    # conditions to get the available layer 2 days before today
    if str(ocean_date) == str(date.today()):
        ocean_date = ocean_date - timedelta(2)

    if str(ocean_date) == str(date.today() - timedelta(1)):
        ocean_date = ocean_date - timedelta(1)

    # visualization threshold
    min, max = st.slider('Min and Max (°C)', 0, 40, value=(10, 32), help='Threshold of visualization in Celsius')

    # button to update visualization
    update_depth = st.form_submit_button('Update')

定义地图实例

在地图实例中,我们将添加一个海洋基础地图并调用 ImageCollection,然后从集合中的第一个日期选择温度图层。查看image变量。

为了正确显示温度,我们使用乘法和加法。这些参数在 GEE 中默认存在。最后,我们添加图层和带有颜色条的图例。深度和日期将显示在图例中。

# __________________ MAP INSTANCE _________________

# add a map instance
Map = geemap.Map(zoom=3, center=(-10, -55))

Map.add_basemap('Esri.OceanBasemap') # "HYBRID"

# get the layer with current date
sst_thumb = ee.ImageCollection('HYCOM/sea_temp_salinity').filterDate(str(ocean_date)) #('2022-01-10', '2022-01-15')

# get fist date just in case, and select the depth, and transform the values
image = sst_thumb.limit(1, 'system:time_start', False).first().select(f'water_temp_{depth}').multiply(0.001).add(20)

vis_param = {'min': min,
             'max': max, 'alpha': 0.4, 
             'palette': cm.palettes.jet,
             }

# add image
Map.addLayer(image, vis_param)

# add color bar with depth and date info
Map.add_colorbar(vis_param, label = f'Sea Surface Temperature (C°) at {depth} depth on {ocean_date}', layer_name = f"SST at {depth} depth", discrete=False)

地图到 Streamlit

然后,我们将地图实例发送到 Streamlit。

# _______ DISPLAY ON STREAMLIT _______
Map.to_streamlit(height=600,  responsive=True, scrolling=False)

添加额外信息

我们可以添加更多信息,例如此教程的链接、作者、数据集的引用或其他任何您想要的额外信息。

# _______ ADDITIONAL INFORMATION AND LINKS ________

st.sidebar.success('##### **Author and coding tutorial**\n###### ***Bryan R. Vallejo***\n##### ***[How to create a web app for ocean monitoring?](https://medium.com/@bryanvallejo16/monitoring-sea-surface-temperature-at-the-global-level-with-gee-1d7349c7da6)***\n###### ***[Get ful access to more tutorials](https://bryanvallejo16.medium.com/membership)***')

st.sidebar.warning('##### **Dataset**\n###### ***J. A. Cummings and O. M. Smedstad. 2013: Variational Data Assimilation for the Global Ocean. Data Assimilation for Atmospheric, Oceanic and Hydrologic Applications vol II, chapter 13, 303-343***\n###### ***In [Google Earth Engine](https://developers.google.com/earth-engine/datasets/catalog/HYCOM_sea_temp_salinity#description)***')

st.sidebar.error('##### Notes: \n###### ***The last STT layer availabe is 2 days before current date***')

st.sidebar.error('##### Related stories: \n###### ***[Ocean currents seasonality in the Galapagos Islands, Ecuador](https://towardsdatascience.com/ocean-currents-seasonality-in-the-galapagos-islands-ecuador-9197f0b721c0)***\n###### ***[Earth observation and biologging data for marine conservation](https://medium.com/gis4-wildlife-tracking/earth-observation-and-biologging-data-for-marine-conservation-989f2b3dc71d)***')

限制

实时海洋监测的限制在于 HYCOM 的最后一层数据可用时间为当前日期前的 2 天。实际上这并不是一个限制,因为 2 天对于海洋温度的季节性变化来说差异并不大。

一个常见的限制是快速显示全球图层,但得益于 geemap 及其 folium 的集成,实现了快速和轻量化的可视化。

未来发展

我设想未来的发展可能包含更多应用功能,例如添加更多图层如盐度。如果您有建议或对您自己工作的需求,请随时留下评论。

结论

通过简单配置输入和 GEE,可以监测海洋的季节性。GEE API 在 Python 中的发展使得展示变得更加容易,并且可以支持季节监测。此外,它还可以用于其他目的,例如用户可以查看哪个月份的温度最适合他们的下一个度假海滩。

编写者:

Bryan R. Vallejo - LinkedIn 个人资料

监控无结构数据以用于 LLM 和 NLP

原文:towardsdatascience.com/monitoring-unstructured-data-for-llm-and-nlp-efff42704e5b?source=collection_archive---------10-----------------------#2023-06-27

使用文本描述符的代码教程

Elena Samuylova数据科学前沿 Elena Samuylova

·

关注 发表在 数据科学前沿 · 14 分钟阅读 · 2023 年 6 月 27 日

--

图片由作者提供。

一旦你部署了基于 NLP 或 LLM 的解决方案,你需要一种方法来监控它。但是你如何监控无结构数据以理解大量文本呢?

这里有几种方法,从 检测原始文本数据中的漂移嵌入漂移 到使用正则表达式进行规则检查。

在本教程中,我们将深入探讨一种特定的方法——跟踪可解释的文本描述符,这有助于为每段文本分配特定属性。

首先,我们将讨论一些理论:

  • 什么是文本描述符,以及何时使用它们。

  • 文本描述符示例。

  • 如何选择自定义描述符。

接下来,开始编码吧! 你将使用电子商务评论数据并完成以下步骤:

  • 获取文本数据的概览。

  • 使用标准描述符评估文本数据漂移。

  • 使用外部预训练模型添加自定义文本描述符。

  • 实施管道测试以监控数据变化。

我们将使用Evidently 开源 Python 库来生成文本描述符并评估数据变化。

代码示例: 如果你更喜欢直接查看代码,这里是 示例笔记本

什么是文本描述符?

文本描述符是描述文本数据集中对象的任何特征或属性。例如,文本长度或其中的符号数量。

你可能已经拥有有用的元数据来伴随你的文本,作为描述符。例如,电子商务用户评论可能会附带用户分配的评级或主题标签。

否则,你可以生成自己的描述符!你可以通过向文本数据添加“虚拟特征”来做到这一点。每个描述符都有助于使用一些有意义的标准描述或分类你的文本。

作者提供的图片。

通过创建这些描述符,你基本上提出了自己的简单“嵌入”,并将每个文本映射到几个可解释的维度。这有助于理解那些原本无结构的数据。

然后你可以使用这些文本描述符:

  • 监控生产 NLP 模型。 你可以跟踪数据的属性,并检测它们何时发生变化。例如,描述符有助于检测文本长度的突增或情感的漂移。

  • 在更新期间测试模型。 当你迭代模型时,可以比较评估数据集的属性和模型响应。例如,你可以检查 LLM 生成的答案的长度是否保持一致,并且它们是否始终包含你期望看到的单词。

  • 调试数据漂移或模型衰退。 如果你检测到嵌入漂移或直接观察到模型质量下降,你可以使用文本描述符来探索其来源。

文本描述符示例

以下是我们认为良好的默认文本描述符:

文本长度

作者提供的图片。使用 Evidently Python 库的文本概览度量可视化。

一个很好的起点是简单的文本统计。例如,你可以查看文本长度,按单词、符号或句子来度量。你可以评估*均长度、最小值和最大值,并查看分布情况。

你可以根据你的使用案例设定期望。例如,产品评论通常在 5 到 100 个单词之间。如果评论更短或更长,这可能表示上下文发生了变化。如果固定长度评论的数量激增,这可能表示垃圾邮件攻击。如果你知道负面评论通常较长,你可以跟踪超过某一长度的评论比例。

还有一些快速的合理性检查:如果你运行聊天机器人,你可能期望有非零响应或者有一些有意义输出的最小长度。

词汇表外的单词

作者提供的图片。示例参考数据集中 OOV 的*均比例为 5.378%。

评估超出定义词汇范围的单词比例是一个很好的“粗略”数据质量衡量标准。你的用户是否开始用新的语言撰写评论?用户是否用 Python 而非英语与聊天机器人对话?用户是否用“ggg”代替实际单词填写响应?

这是检测各种变化的一个实际措施。一旦发现变化,你可以进行更深入的调试。

你可以根据长期积累的“良好”生产数据示例来设定对 OOV 单词比例的期望。例如,如果你查看以往产品评论的语料库,你可能会期望 OOV 在 10%以下,并监控其值是否超过这个阈值。

非字母字符

相关,但有个小变化:这个描述符将统计所有非字母或数字的特殊符号,包括逗号、括号、井号等。

有时你会期望出现相当多的特殊符号:你的文本可能包含代码或以 JSON 格式结构化。有时,你只期望在人类可读的文本中出现标点符号。

检测非字母字符的变化可以揭示数据质量问题,比如 HTML 代码泄漏到评论文本中、垃圾邮件攻击、意外的使用场景等。

情感

作者提供的图片。示例电子商务评论数据集中“基准”情感分布。

文本情感是另一个指标。它在各种场景中都很有用:从聊天机器人对话到用户评论和营销文案编写。你通常可以设定你处理的文本的情感预期。

即使情感“无关”,这可能意味着期望的主要是中立的语调。负面或正面的语调的潜在出现是值得跟踪和调查的。这可能表明意外的使用场景:用户是否将你的虚拟抵押贷款顾问用作投诉渠道?

你也可能期望某种*衡:例如,总会有一部分对话或评论带有负面语调,但你会期望它不会超过某个阈值,或者评论情感的整体分布保持稳定。

触发词

作者提供的图片。评论一致提到连衣裙:未检测到分布漂移。

你还可以检查文本是否包含特定列表中的单词,并将其视为二进制特征。

这是一种强大的方式来编码对你文本的多个期望。你需要一些努力来手动策划列表,但你可以通过这种方式设计许多有用的检查。例如,你可以创建触发词列表,如:

  • 提及产品或品牌。

  • 提及竞争对手。

  • 提及地点、城市、地方等。

  • 提及代表特定主题的单词。

你可以策划(并不断扩展)类似的列表,这些列表是特定于你的用例的。

例如,如果一个顾问聊天机器人帮助选择公司提供的产品之一,你可能会期望大多数响应中包含该列表中某个产品的名称。

RegExp 匹配

包含特定单词的例子是你可以制定的正则表达式模式之一。你可以想出其他模式:你是否期望你的文本以“hello”开头并以“thank you”结尾?包含电子邮件?包含已知的命名元素?

如果你期望模型输入或输出符合特定格式,你可以使用正则表达式匹配作为另一种描述符。

自定义描述符

你可以进一步扩展这个想法。例如:

  • 评估其他文本属性:毒性、主观性、语气的正式程度、可读性评分等。你通常可以找到开放的预训练模型来完成这项任务。

  • 计数特定组件: 电子邮件、URL、表情符号、日期和词性。你可以使用外部模型或甚至简单的正则表达式。

  • 深入统计数据: 如果这些数据对你的用例有意义,你可以追踪非常详细的文本统计数据,例如,追踪单词的*均长度,它们是大写还是小写,唯一单词的比例等。

  • 监控个人可识别信息:例如,当你不期望它出现在聊天机器人对话中时。

  • 使用命名实体识别: 提取特定实体并将其视为标签。

  • 使用主题建模 来建立主题监控系统。这是最繁琐的方法,但做得好时非常强大。当你期望文本保持在主要话题上并且拥有之前的示例语料库来训练模型时,它非常有用。你可以使用无监督主题聚类并创建一个模型来将新文本分配到已知的簇中。然后,你可以将分配的类别作为描述符来监控新数据中主题分布的变化。

作者提供的图片。多个描述符的总结漂移报告示例。

设计描述符时要记住的几点:

  • 最好保持专注,尽量找出少量适合的质量指标,这些指标与用例匹配,而不是监控所有可能的维度。将描述符视为模型特征。你希望找到一些强大的特征,而不是生成大量弱的或无用的特征。许多特征往往是相关的:语言和 OOV 单词的比例、句子和符号的长度等。选择你最喜欢的!

  • 使用探索性数据分析 评估现有数据中的文本属性(例如,以前对话的日志),以测试你的假设,然后再将它们添加到模型监控中。

  • 从模型失败中学习。每当你遇到一个你期望再次出现的生产模型质量问题(例如,外语文本),考虑如何开发一个测试用例或描述符,以便在未来检测到它。

  • 注意计算成本。 使用外部模型按每个可能的维度评分文本是很诱人的,但这会带来额外的成本。考虑一下在处理较大的数据集时:每个外部分类器都是一个额外的模型。通常可以通过更少或更简单的检查来解决问题。

分步教程

为了说明这个想法,让我们通过以下场景来演示:你正在构建一个分类器模型,以对用户在电子商务网站上留下的评论进行评分并按主题标记。一旦模型投入生产,你希望检测数据和模型环境的变化,但你没有真实标签。你需要运行一个单独的标记过程来获取它们。

没有标签的情况下你如何跟踪变化?

让我们以一个示例数据集为例,按照以下步骤进行操作:

代码示例: 请前往 示例笔记本 以查看所有步骤。

💻 1. 安装 Evidently

首先,安装 Evidently。使用 Python 包管理器在你的环境中安装它。如果你在 Colab 中工作,运行 !pip install。在 Jupyter Notebook 中,你还需要安装 nbextension。查看 安装说明 以获取你的环境信息。

你还需要导入一些其他库,如 pandas 和特定的 Evidently 组件。按照笔记本中的说明进行操作。

🔡 2. 准备数据

一旦你设置好了所有内容,让我们来看看数据吧!你将使用一个来自电子商务评论的开放数据集。

这就是数据集的样子:

图片由作者提供。

我们将重点关注“Review_Text”列进行演示。在生产中,我们想要监控评论文本的变化。

你需要使用列映射指定包含文本的列:

column_mapping = ColumnMapping( 
  numerical_features=['Age', 'Positive_Feedback_Count'], 
  categorical_features=['Division_Name', 'Department_Name', 'Class_Name'], 
  text_features=['Review_Text', 'Title']
)

你还应该将数据分成两部分:参考数据和当前数据。假设“参考”数据是某个代表性过去时期的数据(例如,上个月),而“当前”是当前生产数据(例如,本月)。这两个数据集是你将使用描述符进行比较的数据集。

注意: 建立合适的历史基线很重要。选择一个反映你对未来数据预期的时期。

我们为每个样本选择了 5000 个例子。为了增加趣味性,我们通过选择负面评论来人为地引入了一些偏差。

reviews_ref = reviews[reviews.Rating > 3].sample(n=5000, replace=True, ignore_index=True, random_state=42)
reviews_cur = reviews[reviews.Rating < 3].sample(n=5000, replace=True, ignore_index=True, random_state=42)

📊 3. 探索性数据分析

为了更好地理解数据,你可以使用 Evidently 生成可视化报告。这里有一个预设的文本概览预设,帮助快速比较两个文本数据集。它结合了各种描述性检查,并评估整体数据漂移(在这种情况下,使用基于模型的漂移检测方法)。

此报告还包括一些标准描述符,并允许你使用触发词列表添加描述符。我们将在报告中查看以下描述符:

  • 文本长度

  • OOV 词汇的比例

  • 非字母符号的比例

  • 评论的情感

  • 包含“dress”或“gown”这两个词的评论

  • 包含“blouse”或“shirt”这两个词的评论

查看 Evidently 有关描述符的文档以获取详细信息

这是运行此报告所需的代码。你可以为每个描述符分配自定义名称。

text_overview_report = Report(metrics=[ 
  TextOverviewPreset(column_name="Review_Text", descriptors={ 
    "Review texts - OOV %" : OOV(), 
    "Review texts - Non Letter %" : NonLetterCharacterPercentage(), 
    "Review texts - Symbol Length" : TextLength(), 
    "Review texts - Sentence Count" : SentenceCount(), 
    "Review texts - Word Count" : WordCount(), 
    "Review texts - Sentiment" : Sentiment(), 
    "Reviews about Dress" : TriggerWordsPresence(words_list=['dress', 'gown']), 
    "Reviews about Blouses" : TriggerWordsPresence(words_list=['blouse', 'shirt']), 
  }) 
]) 
text_overview_report.run(reference_data=reviews_ref, current_data=reviews_cur, column_mapping=column_mapping) 
text_overview_report

运行这样的报告有助于探索模式,并形成对特定属性的期望,例如文本长度分布。

“情感”描述符的分布迅速揭示了我们在拆分数据时所做的操作。我们将排名高于 3 的评论放入“参考”数据集中,而将更多负面评论放入“当前”数据集中。结果一目了然:

作者提供的图片。

默认报告非常全面,可以一次查看多个文本属性。可以探索描述符与数据集中其他列之间的相关性!

你可以在探索阶段使用它,但这可能不是你需要一直使用的工具。

幸运的是,它很容易自定义。

Evidently 预设和指标。Evidently 有报告预设,可以快速生成报告。不过,还有很多个别指标可供选择!你可以将它们组合起来创建自定义报告。浏览一下* 预设 和指标以了解有哪些。

📈 4. 监控描述符漂移

假设根据探索性分析和你对业务问题的理解,你决定仅跟踪少量属性:

你希望注意到何时有统计变化:这些属性的分布与参考期不同。为了检测它,你可以使用 Evidently 中实现的漂移检测方法。例如,对于“情感”这样的数值特征,它将默认使用 Wasserstein 距离监控漂移。你也可以选择其他方法。

这是你如何创建一个简单的漂移报告以跟踪三个描述符变化的方法。

descriptors_report = Report(metrics=[ 
  ColumnDriftMetric(WordCount().for_column("Review_Text")), 
  ColumnDriftMetric(Sentiment().for_column("Review_Text")), 
  ColumnDriftMetric(TriggerWordsPresence(words_list=['dress', 'gown']).for_column("Review_Text")), 
]) 
descriptors_report.run(reference_data=reviews_ref, current_data=reviews_cur, column_mapping=column_mapping) 
descriptors_report

一旦你运行报告,你将得到所有选定描述符的综合可视化。这里是一个示例:

作者提供的图片。

深绿色线条是参考数据集中情感的*均值。绿色区域覆盖了*均值的一个标准差。你可以注意到当前的分布(红色)明显更为负面。

注意: 在这种情况下,监控输出漂移也是有意义的:通过跟踪预测类别的变化。你可以使用分类的 数据漂移检测方法,如 JS 散度。我们在教程中没有涵盖这一点,因为我们仅关注输入而不生成预测。在实际应用中,预测漂移通常是反应的第一个信号。

😍 5. 添加一个“情感”描述符

假设你决定跟踪一个更有意义的属性:评论中表达的情感。整体情感是一个方面,但它也有助于区分“伤心”和“愤怒”的评论,例如。

让我们添加这个自定义描述符吧!你可以找到一个合适的外部开源模型来评分你的数据集。然后,你将把这个属性作为额外的列进行处理。

我们将使用来自 Huggingface 的Distilbert 模型,它通过五种情感对文本进行分类。

你可以考虑为你的用例使用任何其他模型,如命名实体识别、语言检测、毒性检测等。

你必须安装 transformers 才能运行模型。有关更多细节,请查看安装说明。然后,将其应用于评论数据集:

from transformers import pipeline 
classifier = pipeline("text-classification", model='bhadresh-savani/distilbert-base-uncased-emotion', top_k=1) 
prediction = classifier("I love using evidently! It's easy to use", ) 
print(prediction)

注意: 这一步将使用外部模型对数据集进行评分。根据你的环境,这可能需要一些时间才能执行完毕。要在不等待的情况下了解原理,请参考示例笔记本中的“简单示例”部分。

在将新列“emotion”添加到数据集后,你必须在列映射中反映这一点。你应该指定这是数据集中的一个新分类变量。

column_mapping = ColumnMapping( 
  numerical_features=['Age', 'Positive_Feedback_Count'], 
  categorical_features=['Division_Name', 'Department_Name', 'Class_Name', 'emotion'], 
  text_features=['Review_Text', 'Title'] )

现在,你可以将“emotion”分布漂移监控添加到报告中。

descriptors_report = Report(metrics=[ 
  ColumnDriftMetric(WordCount().for_column("Review_Text")), 
  ColumnDriftMetric(Sentiment().for_column("Review_Text")), 
  ColumnDriftMetric(TriggerWordsPresence(words_list=['dress', 'gown']).for_column("Review_Text")), 
  ColumnDriftMetric('emotion'), ]) 
descriptors_report.run(reference_data=reviews_ref, current_data=reviews_cur, column_mapping=column_mapping) 
descriptors_report

这是你得到的结果!

图片来源:作者。

你可以看到“sad”评价显著增加,而“joy”评价减少。

追踪随时间变化是否有帮助?你可以继续通过对新数据进行评分来运行此检查。

🏗️ 6. 运行管道测试

为了定期分析数据输入,将评估打包成测试是有意义的。在这种情况下,你会得到一个明确的“通过”或“失败”结果。如果所有测试都通过,你可能不需要查看图表。你只对变化感兴趣!

Evidently 有一个替代接口,称为测试套件,它以这种方式工作。

这里是如何创建一个测试套件以检查相同四个描述符中的统计分布:

descriptors_test_suite = TestSuite(tests=[ 
  TestColumnDrift(column_name = 'emotion'), 
  TestColumnDrift(column_name = WordCount().for_column("Review_Text")), 
  TestColumnDrift(column_name = Sentiment().for_column("Review_Text")), 
  TestColumnDrift(column_name = TriggerWordsPresence(words_list=['dress', 'gown']).for_column("Review_Text")), 
]) 
descriptors_test_suite.run(reference_data=reviews_ref, current_data=reviews_cur, column_mapping=column_mapping) 
descriptors_test_suite

注意: 我们使用默认设置,但你也可以设置 自定义漂移方法和条件

这是结果。输出结构清晰,你可以看到哪些描述符发生了漂移。

图片来源:作者。

检测统计分布漂移是监控文本属性变化的方法之一。还有其他方法!有时,根据描述符的最小值、最大值或均值运行基于规则的期望也很方便。

假设你想检查所有评价文本是否长于两个词。如果至少有一条评价短于两个词,你希望测试失败并在响应中查看短文本的数量。

你可以选择一个TestNumberOfOutRangeValues() 检查。此次,你应设置一个自定义边界:期望范围的“左”侧是两个词。你还必须设置一个测试条件:eq=0。这意味着你期望这个范围外的对象数量为 0。如果高于此值,你希望测试返回失败。

descriptors_test_suite = TestSuite(tests=[ 
  TestNumberOfOutRangeValues(column_name = WordCount().for_column("Review_Text"), left=2, eq=0), 
]) 
descriptors_test_suite.run(reference_data=reviews_ref, current_data=reviews_cur, column_mapping=column_mapping) 
descriptors_test_suite

这是结果。你还可以看到显示定义期望的测试细节。

图片来源:作者。

你可以遵循这一原则设计其他检查。

支持 Evidently

喜欢这个教程吗?给 Evidently 在 GitHub 上点个星 以回馈支持!这有助于我们继续为社区创建免费的开源工具和内容。 ⭐️ 在 GitHub 上点星 ⟶

总结

文本描述符将文本数据映射到可解释的维度,你可以将其表示为数值或分类属性。它们帮助描述、评估和监控非结构化数据。

在本教程中,你学会了如何使用描述符来监控文本数据。

你可以使用这种方法来监控 NLP 和 LLM 驱动模型的生产行为。你可以将你的描述符与其他方法结合使用,如 监控嵌入漂移

你认为还有其他普遍有用的描述符吗?告诉我们!加入我们的 Discord 社区 分享你的想法。

最初发布于 https://www.evidentlyai.com 于 2023 年 6 月 27 日。感谢 Olga Filippova 共同撰写本文。

单目深度估计预测火星表面高程

原文:towardsdatascience.com/monocular-depth-estimation-to-predict-surface-reliefs-of-mars-1b50aed3361a

单目深度估计模型的另一种应用

Mattia GattiTowards Data Science Mattia Gatti

·发表于Towards Data Science ·阅读时间 5 分钟·2023 年 9 月 4 日

--

摄影:由NASA提供,来自Unsplash

文献中讨论了多种从单张图像估算表面高程的方法。在之前的文章中,我讨论了如何使用单目估计模型预测单张 2D 图像的深度。然而,当模型的输入是特定表面的图像时,预测结果代表的是数字高程模型(DEM)。在我的第一篇研究论文中,我展示了如何利用深度学习方法从 2D 灰度图像中获得火星表面的 DEM。为了更好地理解我将要提出的想法,我建议你首先尝试项目的演示版,这里

介绍

如在另一个故事中更详细地讨论过,表面的数字高程模型(DEM)是一个高程值的网格,每个单元格存储表面上特定点的高程:

DEM 的图形可视化。NSIDCCC BY 2.0,通过 Wikimedia Commons

DEM 通常通过颜色地图图形化表示。在上图中,最高点是红色的,最低点是紫色的。

另一方面,单目深度估计模型用于估算图像中每个像素与拍摄该图像的相机(甚至是卫星相机)之间的距离:

一个卧室的深度预测。输入图像来自NYU-Depth V2

这个想法是,将表面的卫星图像输入到单目深度估计模型中。这样可以预测该表面的 DEM,因为输出的每个点代表一个距离(深度),而表面的高程可以通过使用深度推导出来(稍后会详细讲述)。

本文讨论的方法也可以用于其他表面,而不仅仅是火星。

UAHiRISE

高分辨率成像科学实验(HiRISE)是火星勘测轨道器上的一台相机(该航天器为火星任务提供支持)。HiRISE 拍摄了大量火星表面的灰度图像,每张图像都与一个 DTM 相关联。UAHiRISE是亚利桑那大学的网站,所有这些资源都可以在这里找到。这些文件是地理栅格数据,如果您想了解更多关于这种文件格式的信息,请参见我另一篇文章

火星表面的卫星图像及其相关 DTM。图片来自UAHiRISE

数据集

使用了一个网页抓取工具来下载所有的灰度图像及其相关的 DTM(代码见下)。然而,这些文件的分辨率过大,无法作为神经网络的输入。因此,必须将它们拆分为更小的补丁(我在另一篇文章中讨论了这一过程)。最终的数据集由 150,000 个补丁组成。

为了训练模型,使用了一组火星表面的图块及其相应的深度。然而,原始 DTM 的每个图块都必须进行缩放。绝对高程被转换为相对深度。这意味着如果地面真值的范围是[-3500, -2500],它们会被缩放到[0, 1000],其中 0 是最*的点,1000 是最远的点。这是因为单目深度估计模型不能直接预测绝对高程值。

下面的图像展示了一个训练样本的示例:

一个训练样本。数据集由UAHiRISE的图像和 DTM 组成。

红色表示最接*的点,蓝色表示最远的点。然而,点的距离与其高程成反比,即红色点的高程高于蓝色点。预测深度后,所有点都被转换为高程。

数据集大小约为 1TB,可在Kaggle上获取。

模型

在分析了所有可用的单目深度估计架构后,选择了 GLPN¹模型:

GLPN 架构。图像来自官方论文

该架构使用层次化的变换器编码器从 H × W × 3 输入(RGB 图像)中获得不同分辨率的全局信息。然后解码器将瓶颈特征恢复到 H × W × 1 的大小(输出深度图)。

模型几乎准备好用于实验,唯一缺少的是输入层。UAHiRISE 发布的大多数图像是灰度图像。因此,输入层被修改为接受 1 通道(灰度)图像而非 3 通道(RGB)图像。虽然仍然使用了 ImageNet 为编码器预训练的模型,但没有加载输入层的权重。

最终结果

训练模型后,它已准备好预测 DTM。以下是最终模型做出的预测示例:

输入灰度图像。来自UAHiRISE的图块。

预测的 DTM 与相对高程。

DTM 的 3D 模型。图像使用此生成器制作。

DTM 的 3D 模型。图像使用此生成器制作。

在计算所有测试样本的指标后,结果显示*均绝对误差为10.3 米,这取决于具体情况可能是一个好的值。然而,获得良好结果的主要障碍是所有使用的图像都是灰度图像,因此信息量比 RGB 或多光谱图像少。你可以在我仓库的 README 中找到所有指标。

结论

当我在进行这项研究时,有人建议直接预测高度而不是预测深度。然而,实验表明,通过预测深度然后转换为高度的结果更好。

项目的演示可以在这里查看,你可以预测一个 DTM 并获得一个 3D 模型。所有代码都可以在我的仓库中找到,同时还包括准备数据集和训练模型的所有资源。你也可以直接使用我预训练的模型。

该项目的想法可以应用于其他需要从卫星图像预测 DTM 的场景。

感谢阅读,希望你觉得这有用。

除非另有说明,否则所有图像均由作者提供。

[1] Doyeon K., Woonghyun K., Pyungwhan A., Donggyu J., Sehwan C., Junmo K., Global-Local Path Networks for Monocular Depth Estimation with Vertical CutDepth (2022)

蒙特卡罗*似方法:你应该选择哪一种,何时使用?

原文:towardsdatascience.com/monte-carlo-approximation-methods-which-one-should-you-choose-and-when-886a379fb6b?source=collection_archive---------5-----------------------#2023-08-25

是逆变换、随机游走 Metropolis-Hastings,还是 Gibbs?对每种方法的数学基础、从零开始的 Python 实现以及优缺点的分析

Suyang LiTowards Data Science Suyang Li

·

关注 发表在 Towards Data Science · 12 min read · 2023 年 8 月 25 日

--

图片由Joakim Honkasalo拍摄,发布在Unsplash

*似抽样介绍

对于大多数实际应用中的概率模型,精确推理是不可行的,因此我们必须采用某种形式的*似。

— 模式识别与机器学习¹

由于如刚刚所见,确定性推断在概率模型中通常难以处理,因此我们现在转向基于数值采样的*似方法,这些方法被称为蒙特卡罗技术。我们将研究这些方法的关键问题是给定概率分布p(z)下目标函数f(z)的期望值计算。回顾一下,期望的简单定义是通过积分给出的:

来源:PRML¹ Eq. 11.1

正如我们将看到的,这些积分过于计算复杂,因此我们将在本文中转向采样方法

在本文中,我们将研究三种核心采样方法:逆变换、马尔可夫链蒙特卡罗(MCMC)和Gibbs 采样。通过理解这些方法的基本统计属性和计算要求,我们将了解到:

  • 逆变换采样最适合从已知和简单分布中高精度地模拟数据,特别是在低维情况下。

  • 随机游走 Metropolis-Hastings 最适合复杂、多模态或未知分布,其中全球探索和/或收敛是优先考虑的;具体而言,Metropolis 算法——Metropolis-Hastings 的一个特定实例——可以用于对称分布。

  • Gibbs 采样最适合高维问题,其中条件分布容易采样,并且效率是优先考虑的。

目录

  1. 逆变换采样

    • 算法如何工作?

    • Python 实现

    • 前提条件

    • 优缺点

  2. 马尔可夫链蒙特卡罗

    • Metropolis-Hastings 算法

    • 特殊情况:对称分布的 Metropolis 算法

    • 优缺点

  3. Gibbs

    • 算法

    • 条件

    • Gibbs 与 Metropolis-Hastings 的关系

  4. 比较:转换 vs. Met-Hastings vs. Gibbs 的优缺点

1. 转换方法:逆 CDF

逆变换采样,顾名思义,使用目标分布的逆累积分布函数(CDF)生成符合期望分布的随机数。基本思想是:

  1. 生成一个均匀随机数:我们从 0 到 1 之间的均匀分布中抽取一个数 U

  2. 应用逆 CDF:利用目标分布 CDF 的逆,将 U 转换为符合目标分布的样本。

下面是样本(蓝色)如何从分布(红色)中抽取的简要说明:

逆 CDF 是一种计算上 简单且可推广的方法,用于从已知 CDF 的分布中进行采样,例如正态分布、指数分布、伽马分布或贝塔分布。

PDF、CDF 和逆 CDF

(从左到右):标准正态分布的 PDF、CDF 和逆 CDF

从直观上讲,CDF 是 PDF 的累计值,相当于 PDF 的积分;然后我们取 CDF 函数的逆函数,以获得用于此方法的最终逆 CDF。

正式来说,如果 a 是一个随机变量,那么 a 的 CDF 给出如下:

PRML,第 11.5–11.6 节

CDF F 具有以下关键属性:

  • F 是连续的

  • F 是单调非减的

  • F 的范围是 0 ≤ cdf(a) ≤ 1 对于所有 a ∈ R

逆 CDF 算法是如何工作的?

该算法包含以下要素:

输入:

  • UU 是一个从 0 到 1 之间均匀分布的随机变量。

  • 这作为逆 CDF 的输入概率,并将被转换为来自期望分布的样本。

参数

  • *F:* 目标分布的 CDF。

  • 使用 F,我们可以简单地计算其逆函数 F^-1,并用它将输入值映射到期望的领域。

输出

  • x:从目标分布中抽取的随机样本。

  • 这是通过将逆 CDF 应用到均匀分布的随机数(输入)来生成的。

Python 实现

现在,让我们从头开始实现这个方法。我们将使用指数函数,因为它将容易可视化我们通过逆 CDF 生成的样本,并与精确分布进行比较:

指数函数(目标分布)的 PDF

通过标准微积分积分技术,我们发现目标 CDF F(x) 是:

指数函数的 CDF

这个 CDF 的逆函数是:

指数函数的逆 CDF

我们将使用逆 CDF 方法生成 5000 个样本:

import numpy as np
import matplotlib.pyplot as plt

# Inverse CDF for the exponential distribution with lambda = 1
def inverse_cdf(p):
    return -np.log(1 - p)

# Generate 1000 sample values using inverse CDF
uniform_samples = np.random.uniform(0, 1, 1000)
transformed_samples = inverse_cdf(uniform_samples)

# Generate x values and true PDF
x_values = np.linspace(0, np.max(transformed_samples), 1000)
pdf_values = np.exp(-x_values)

逆 CDF 算法工作的要求

逆 CDF 方法做了一个关键假设:

  • CDF F 是可逆的:CDF F 必须是可逆的,这意味着每个输入到 F 的值必须有唯一的输出。

这个约束排除了一些函数。例如,下面是一些常见但不可逆的函数类型(因此不能与逆 CDF 一起使用):

  1. 常数函数:任何形式为 f(x) = c 的常数函数,其中 c 是常数,都不是可逆的,因为每个输入都映射到相同的输出,因此该函数不是一对一的

红点显示了映射到相同 y 值的多个 x 值中的两个,这使得 f(x) = 5 变得不可逆。

  1. 某些二次函数:例如 f(x) = x² 是不可逆的,因为它是多对一的(考虑 f(x) = 1x 可以是 1-1)。

红点显示了映射到相同 y 值的多个 x 值中的一对,在 f(x) = x² 中。

3. 某些三角函数:例如 f(x) = sin(x) 在其整个域上不可逆,因为它们是周期性的,尽管在限制域上它们可能变得可逆。

红点显示了由于周期性,在 f(x) = sin(x) 给定域上映射到相同 y 值的多个 x 值集合之一。

为什么逆累积分布函数(CDF)有效?

关键思想是,一个在 0 到 1 之间均匀分布的随机变量可以通过应用目标分布 CDF 的逆变换成具有特定分布的随机变量,前提是该分布的 CDF 已知且易于处理。

优势

  1. 算法简单性:对于低维数据,实现起来非常容易,因此在不同领域和任务中有广泛的应用。

  2. 样本准确性:假设 CDF 及其逆函数表示准确的目标分布,该方法相比于其他方法(如稍后会看到的 MCMC),具有相对较高的准确性。

缺点

  1. 计算复杂性:对于一些分布,逆 CDF 可能没有封闭形式表达式,使得计算具有挑战性或昂贵。

  2. 高维度难度:在高维空间中应用可能很困难,尤其是当变量之间存在依赖关系时。

  3. 可逆性限制:任何 CDF 不可逆时,该方法将变得无效。这排除了我们上面看到的一些函数。

  4. 限于已知分布:逆 CDF 需要 CDF 的准确形式,这限制了其仅能应用于已知分布。

考虑到所有这些限制,我们只能将逆 CDF 应用于少数几类分布。实际上,面对大数据和未知分布时,这种方法可能会迅速失效。

记住这些优缺点后,让我们现在来看另一种随机采样框架,马尔可夫链蒙特卡罗(MCMC),它解决了这些限制。

2. 马尔可夫链蒙特卡罗(MCMC)

正如我们刚才看到的,逆 CDF 变换方法高度受限,尤其是在高维样本空间中。另一方面,马尔可夫链蒙特卡罗(MCMC)在维度上表现良好,使我们能够从更大的分布家族中进行采样。

Metropolis-Hastings 例子探索混合高斯分布(左),生成样本(右)

Metropolis-Hastings 算法是如何工作的?

直观地说,算法按以下步骤工作:类似于逆 CDF,我们有一个我们从中采样的目标分布。然而,我们需要一个额外的成分:当前状态 z*,并且 q(z|z)* 依赖于 z*,创建一个具有样本 z¹、z²、z³ 的马尔可夫链。每个样本只有在满足某些标准时才被接受到链中,这些标准将在下文定义,因为这些标准在算法的不同变体中有所不同。

让我们将其形式化为更算法化的结构。

算法以周期性方式运行,每个周期遵循以下步骤:

  1. 从提议分布中生成一个样本 *z**。

  2. 以概率接受样本,然后我们将以接受概率接受该值,在 Metropolis-Hastings 中定义为:

PRML¹ Eq 11.44

其中

  • z* 是当前状态。

  • z^T 是提议的新状态。

  • p(z)* 是根据期望分布的状态 z* 的概率。

  • p(z^T) 是根据期望分布的状态 z^T 的概率。

这个接受阈值背后的逻辑是确保更可能的状态(根据期望分布)更频繁地被访问

现在,这是算法的最通用版本;如果提议分布已知是对称的,即从状态 xx′ 的提议移动的概率与从 x′ 到 x 的提议移动的概率相同,即 q(x′|x) = q(x|x′),那么我们可以使用一个特殊的 Metropolis-Hastings 变体,该变体需要更简单的接受阈值。

对称分布的 Metropolis 算法

这是一个特定的 MCMC 算法,我们选择在提议分布对称时使用,即 q(z⁰ | z¹) = q(z¹ | z⁰) 对于所有值 1 和 0,解释为“从任何状态 A 过渡到状态 B 的概率等于从 B 过渡到 A 的概率”。所以,算法的每一步变成:

  1. 从提议分布中生成一个样本 z*。

  2. 以以下概率接受样本:

Metropolis 算法接受阈值。来源:PRML¹ Eq. 11.33

Metropolis-Hastings 和 Metropolis 算法

让我们并排比较这些算法。如我们之前所见,唯一的区别是接受阈值;算法的其他步骤完全相同:

Metropolis 与 Metropolis-Hastings 算法

优势

  1. 收敛到*衡分布:在某些情况下,随机游走可以收敛到期望的*衡分布,尽管在高维空间中可能需要很长时间。

  2. 低计算成本:随机游走通常比其他复杂的采样方法需要更少的计算资源,因此适用于计算效率为优先的问题。

  3. 应用的多样性:由于与自然发生模式的高度相似性,随机游走在广泛的领域中都有应用:

    • 物理学:液体和气体中分子的布朗运动。

    • 网络分析

    • 金融市场:用于建模股票价格波动

    • 群体遗传学

缺点

  1. 对初始化敏感:算法的性能可能对起始值的选择敏感,特别是当初始化值远离高密度区域时。

  2. 局部陷阱:根据提案分布的复杂性,算法可能会陷入局部最优,并且难以遍历分布的其他区域。

现在,牢记 Metropolis-Hastings 算法,让我们看看它的另一个特殊实例:Gibbs 采样。

3. Gibbs 采样

Gibbs 采样是 Metropolis-Hastings 的一个特殊实例,其中 每一步都总是被接受。让我们首先看看 Gibbs 采样算法本身。

Gibbs 算法是如何工作的?

这个想法相对简单,最好通过首先放大涉及从分布 p(z1, z2, z3) 中采样的微观例子来说明。算法的步骤如下:

  1. 在时间步 T,将起始值初始化为:

PRML¹

2. 为 z1 生成新值:

PRML¹ Eq 11.46

3. 从条件分布中为第二个位置 z2 生成一个新值:

PRML¹ Eq 11.47

4. 最后为最后一个位置 z3 生成一个新值:

PRML¹ Eq 11.48

5. 重复这一过程,在三个变量 z1…z3 之间循环,直到达到某个令人满意的阈值。

广义算法

从形式上看,算法首先初始化起始位置,然后采取 T 个连续步骤

图片来源:PRML¹ Ch11.3 Gibbs 采样

Gibbs 从目标分布中正确采样的条件

  1. 不变性。目标分布 p(z) 在每个 Gibbs 步骤中是不变的,因此 p(z) 对整个马尔可夫链是不变的。

  2. 遍历性。如果条件分布都是非零的,则意味着遍历性,因为 z 空间中的任何点都可以在有限步数内到达。

  3. 充分的烧入时间。正如我们在任何需要随机初始化的方法中看到的那样,前几个样本依赖于初始化,随着迭代次数的增加,这种依赖性会减弱。

这与 Metropolis-Hastings 有什么关系?

在 Metropolis-Hastings 中,我们定义接受阈值为:

因此,Metropolis-Hastings 提案步骤总是被接受的,正如我们在 Gibbs 算法中所看到的那样。

变体

由于 Gibbs 方法一次更新一个变量,因此连续样本之间存在较强的依赖性。为克服这一点,我们可以使用一种中间策略来从变量组而非单个变量中采样,称为阻塞 Gibbs

同样,由于马尔可夫链的性质,连续抽取的样本将会相关。为了生成独立样本,我们可以在序列中使用子采样。

4. 优缺点:逆 CDF vs Metropolis-Hastings vs Gibbs

现在我们已经详细了解了每种算法的工作原理及其应用领域,让我们总结一下每种方法的定义特征。

1. 逆变换采样

  • 数据大小:适合中等大小的数据集。

  • 时间:通常对单变量分布有效率。

  • 数据复杂性:适用于已知并易于计算的累积分布函数(CDF)及其逆函数的简单分布。

  • 如果:采样高维变量/分布。

  • 最大优点:如果 CDF 准确反映目标分布,则准确性高。

  • 要求:CDF 必须已知且可逆。

2. Metropolis-Hastings (MCMC)

  • 数据大小:可扩展且适合大型数据集。

  • 时间:可能计算量大,取决于目标分布的复杂性。

  • 数据复杂性:适用于复杂或多模态分布。

  • 最大优点

    • 可以在不知道其归一化常数(完整形式)的情况下从分布中采样

    • 适合探索分布的全球结构并保证收敛

  • 缺点:可能会出现非常慢的收敛速度

    • 复杂或多峰目标分布,因为算法可能陷入局部模式并难以在它们之间转换;

    • 变量高度相关;

    • 高维空间;

    • 不良的初始值或步长选择

3. Gibbs 采样

  • 数据大小:适用于小型和大型数据集。

  • 时间:通常比随机游走 Metropolis-Hastings 更有效,因为它不需要接受/拒绝步骤。

  • 数据复杂性:当处理高维分布时,最佳用于可以从每个变量的条件分布中采样的情况。

  • 最大优点

    • 可以轻松计算条件分布;

    • 相比于随机游走,较少陷入局部极小值。

  • 要求

    • 马尔可夫链遍历性

    • 必须已知且可处理完整的条件分布

总结:

逆 CDF、Metropolis-Hastings 和 Gibbs 的优缺点汇总表

结论

感谢你一直陪伴到现在!在这篇文章中,我们探讨了 3 种关键的*似采样方法:逆 CDF、Metropolis Hastings MCMC 和 Gibbs Sampling MCMC。我们了解了每种算法的工作原理、各自的优缺点以及典型的应用场景。

逆 CDF 提供了一种直接从已知分布中采样的方法,当其 CDF 是可逆时。这种方法计算效率高,但不太适用于高维或复杂的分布。

Metropolis Hastings MCMC 提供了一种更通用的方法,允许从难以处理的分布中进行采样。然而,它需要更多的计算资源,并且可能对像提议分布这样的调参敏感。

Gibbs Sampling MCMC 在联合分布复杂但可以分解为更简单的条件分布时特别高效。它在机器学习中广泛使用,尽管对于高维问题可能收敛缓慢且占用内存。

[1] Bishop, C. M. (2016). 模式识别与机器学习(原版第 1 版 2006 年软封面再版(2009 年第 8 次印刷校订版))。Springer New York.

图片由作者提供,除非另有说明,

蒙特卡洛方法

原文:towardsdatascience.com/monte-carlo-methods-b2504976c415

《婴儿机器人强化学习指南》

强化学习简介:第四部分

Steve RobertsTowards Data Science Steve Roberts

·发表于Towards Data Science ·阅读时间 26 分钟·2023 年 8 月 26 日

--

所有图片由作者提供

介绍

我们再次前往赌场,这次它位于阳光明媚的蒙特卡洛,这里因在经典电影马达加斯加 3:欧洲大追捕中的出现而闻名(虽然它可能早已出名)。

在我们上次访问赌场时,我们研究了多臂赌博机,并将其作为一个视觉化的方式来解决如何在面对多种可能行动时选择最佳行动的问题。

强化学习的角度来看,赌博机问题可以视为代表一个单一状态以及该状态下可用的行动。蒙特卡洛方法将这一概念扩展到覆盖多个相互关联的状态。

此外,在我们之前研究的问题中,我们总是得到了环境的完整模型。这个模型定义了转移概率,即描述从一个状态转移到下一个状态的机会,以及为此转移所获得的奖励。

蒙特卡洛方法中情况并非如此。没有给定模型,代理必须通过探索来发现环境的属性,随着从一个状态转移到下一个状态而收集信息。换句话说,蒙特卡洛方法从经验中学习

本文中的例子使用了自定义的Baby Robot Gym 环境,与本文相关的所有代码可以在Github上找到。

此外,您可以在notebook格式中找到这篇文章的互动版本,在那里您可以实际运行下面描述的所有代码片段。

本系列之前的所有文章可以在这里找到:小机器人的强化学习指南.

并且,为了快速回顾本文中使用的理论和术语,请查看状态值和策略评估 5 分钟

蒙特卡洛预测

在预测问题中,我们想要找到在环境中某一特定状态下的好坏。这种“好坏”由状态值表示,状态值定义为在该状态下开始,然后按照当前策略继续进行所有后续状态时可以获得的期望奖励。

当我们对环境有充分了解,知道转移概率和奖励时,我们可以简单地使用动态规划来迭代计算每个状态的值。

实际上,系统的转移概率通常是未知的。因此,为了估计从一个状态转移到另一个状态的可能性,可以观察多个回合,然后取*均值。这种通过随机样本计算估计值的方法被称为蒙特卡洛采样

请考虑下面的图 1中显示的关卡,小机器人目前正处于其中:

图 1:包含玻璃墙的关卡以及该关卡上的坐标。

初看之下,这个关卡似乎相当简单,从关卡的开始到出口有一条短路径。然而,有两个值得注意的障碍:

  • 在顶部中间的方格(坐标[1,0])有一个大水坑。如我们之前所见,小机器人不喜欢水坑。它们移动起来耗时更长,会带来-4 的负面奖励,并可能导致小机器人打滑。

    当发生打滑时,小机器人不会到达目标状态。通常这将导致他移动到其他可能的状态之一,但在这种情况下没有其他可能的状态,因此他将停留在原地并再次收到-4 的惩罚。

    如果小机器人进入这个水坑,有很大的可能性他会被困住几个时间段,并且会收到一个大的负面奖励。最好避免这个水坑!

  • 在[1,1]和[1,2]之间的粗蓝线表示一面玻璃墙。这是一种小机器人以前未遇到过的新类型挑战。

    与标准墙不同,Baby Robot 看不见玻璃墙,因此可能会选择一个使他撞上墙的动作。当这种情况发生时,他会从墙上弹开,而不是到达目标状态,反而会到达相对的状态。此外,他还会因为需要额外的时间来移动而受到-1 的负奖励惩罚。

    在这个关卡中,有 2 个可能会碰到玻璃墙的机会:

    • 如果他从单元[1,1]向南移动,他将最终到达[1,0]的水坑,并获得-5 的奖励(-4 因进入水坑而受到的惩罚和-1 因撞上玻璃墙而受到的惩罚)。

    • 如果他从[1,2]向北移动,而不是到达[1,1],他实际上会撞上墙并最终到达出口。在这种情况下,他将获得-2 的奖励(-1 墙壁惩罚和-1 移动到干燥区域的奖励)。

如上所述,当我们对系统拥有完整信息,并且知道所有的转移概率和奖励时,我们可以使用策略评估来计算状态值。

对于这个随机选择每个状态下可用动作的环境,策略评估给出了以下状态值:

图 2:使用策略评估计算的状态值,针对一个随机策略。

每个状态值代表了从该状态开始的期望回报。因此,在时间‘t’时,如果我们从状态‘s’开始,策略‘π’下的状态值由下式给出:

方程 1:策略π下的状态值函数

其中回报Gₜ,即从时间‘t’开始在一个回合中累积的总奖励,由下式给出:

方程 2:时间‘t’的折扣回报

在随机策略下,对于这个简单的关卡,我们知道最终 Baby Robot 会到达出口,回合将结束。因此,我们可以将折扣因子‘γ’设置为 1。在这些条件下,状态值表示从该状态开始时可以预期的未来总奖励的*均值。

换句话说,状态值是从该状态开始,到回合结束时所有未来奖励的总和,*均过无限回合。

因此,为了得到状态值的简单估计,我们可以简单地取从该状态开始的多个回合的*均回报。运行的回合越多,我们的估计就会越好。这正是蒙特卡洛 方法所采用的方式。

图 3:一个从起始状态直接到达出口的回合示例。

例如,从起始状态开始,如果 Baby Robot 足够幸运地沿着一条直接到达出口的路径(如上面图 3所示),他的轨迹将具有以下状态和奖励:

[([1,1], -1), ([2,1], -1), ([2,2], -1), ([1,2], -1), ([1,3], -1)]

The moves on this trajectory are:

[0,1]->[1,1] - reward = -1
[1,1]->[2,1] - reward = -1
[2,1]->[2,2] - reward = -1
[2,2]->[1,2] - reward = -1
[1,2]-> Exit - reward = -1

因此,对于这个回合,回报值为 -5。如果我们再运行一次实验,使用产生不同随机路径的随机策略,我们会得到不同的回报值。通过将许多这样的回合的回报相加并取*均,我们将得到起始状态 [0,1] 的状态值估计。我们运行的回合越多,这个估计就越接*真实的状态值。

对于这个层级的初始状态,如果我们运行多个回合并取*均回报,我们得到的估计状态值大约为 -102,这与使用策略评估计算的状态值相同。

然而,与策略评估不同,我们现在完全通过探索计算了这个状态的值,而不知道环境的模型。此外,通过跟随随机轨迹直到回合结束,我们实际上进行了第一次蒙特卡洛搜索。

要查看实际效果,请查看附带的 Jupyter notebook: 第四部分 — 蒙特卡洛方法.ipynb

计算轨迹上其他状态的回报值

在上述示例中,我们看到,通过反复从层级的起始状态运行回合,我们可以生成该初始状态的状态值估计。然而,每个回合在最终到达终端状态之前会经过许多状态,但我们只考虑了从起始状态获得的回报值,而忽略了回合中收集的所有其他信息。这非常低效。

更好的方法是计算回合中看到的每个状态的回报值。然后,通过多次随机回合,我们可以为每个已访问状态创建一个估计。

正如你在上面提供的样本轨迹中看到的(图 3),在前往出口的过程中,Baby Robot 还访问了状态 [1,1]、[2,1]、[2,2] 和 [1,2]。因此,在这个回合中,我们还可以收集这些状态的信息。对于每个访问过的状态,我们可以将从该状态获得的未来奖励视为该状态的回报。这就给出了轨迹中所有看到的状态的估计回报值,而不仅仅是起始状态。

对于这个样本轨迹,我们得到以下已访问状态的回报值:

[0,1] - return = -5
[1,1] - return = -4
[2,1] - return = -3
[2,2] - return = -2
[1,2] - return = -1

如果我们在多个回合中*均这些值,我们将得到一个越来越准确的已访问状态的状态值估计。

首次访问蒙特卡洛

当每个状态在一个回合中只访问一次时,如上例所示,那么计算每个状态的回报很简单——你只需将该状态的所有未来奖励相加。但如果一个状态被多次看到会发生什么呢?

对于一个在单个剧集中可以重复访问的状态,计算其价值的最简单方法是从第一次访问该状态开始,一直到剧集结束。将这些回报在多个剧集中*均,我们可以建立每个已访问状态的价值估计。不出所料,这种方法被称为首次访问蒙特卡洛

示例:

从样本级别的入口到出口的一个可能路径如图 4 所示:

图 4:具有重复状态访问的轨迹

这条路径包含以下步骤和奖励:

1\. [0,1]->[1,1] - reward = -1
2\. [1,1]->[2,1] - reward = -1
3\. [2,1]->[2,2] - reward = -1
4\. [2,2]->[2,1] - reward = -1
5\. [2,1]->[2,2] - reward = -1
6\. [2,2]->[1,2] - reward = -1
7\. [1,2]-> Exit - reward = -1

在这条轨迹中,Baby Robot 没有直接前往出口,而是在第 4 步向后移动到状态[2,1]。之后,他沿着直接路径前往出口。由于这一步向后移动,他访问了状态[2,1]和[2,2]两次。

使用首次访问蒙特卡洛方法,我们从第一次看到状态到剧集结束时累加获得的奖励,以给出该状态的价值估计。当一个已被见过的状态再次被访问时,其奖励仍然作为回报的一部分被使用,但我们不将此视为从该状态的新轨迹。

对于这个轨迹,首次访问回报值如下所示。请注意,我们不会计算步骤 5 和 6 的回报,对于那些已经被见过的状态:

1\. [0,1] - return = -7
2\. [1,1] - return = -6
3\. [2,1] - return = -5
4\. [2,2] - return = -4
5\. [2,1]
6\. [2,2]
7\. [1,2] - return = -1

我们上面描述的方法的步骤在以下 Python 伪代码中详细列出,用于首次访问蒙特卡洛算法:

''' calculate state values using First-Visit Monte Carlo '''

# keep a count of the visits to each state                            
total_state_visits = np.zeros((env.height,env.width))                    ❶

# the total returns for each state
total_state_returns = np.zeros((env.height,env.width))

for episode in range(max_episodes):                                      ❷

  state_rewards = single_episode(env)                                    ❸
  state_returns = rewards_to_returns(state_rewards)                      ❹
  episode_returns,episode_visits = get_first_visit_return(state_returns) ❺

  # add the states that were visited to the total visits
  total_state_visits += episode_visits

  # add the return for each state visited to the total for each state
  total_state_returns += episode_returns

returns = (total_state_returns/total_state_visits)                       ❻

上述首次访问蒙特卡洛代码中的要点如下:

  1. 我们想要跟踪剧集回报和每个状态的访问次数,因此我们开始为每个状态创建零初始化的 Numpy 数组。这些数组的维度设置为环境的宽度和高度。

  2. 运行多个剧集以获取在多个轨迹中生成的回报。在这里,运行次数由‘max_episodes’定义。

  3. 获取单个剧集的奖励。这将返回一个从剧集开始到结束之间访问的所有状态及其获得的奖励的列表。

  4. 将奖励转换为回报。这是通过简单地反向累加奖励来完成的。

  5. 获取首次访问回报值,并将该值添加到‘total_state_returns’数组中。此外,对于每个访问过的状态,我们增加该状态的访问总次数(即我们仅统计首次访问的次数)。

  6. 最终,在所有剧集完成后,我们将总的首次访问奖励除以总的访问次数,以得到每个状态的*均回报。

图 5:10000 次剧集计算的首次访问蒙特卡洛状态值和状态首次访问计数。

使用首次访问蒙特卡洛计算的状态值,在这个级别上进行了 10000 次实验,如上所示。将这些值与我们之前使用策略评估计算的值(见图 2)进行比较,我们可以看到,对于这个非常简单的级别,值是相同的。

这显示了蒙特卡洛方法的威力。通过简单地观察一组随机实验的结果,我们能够计算出状态值的非常好的估计,并且完全不需要了解基础环境属性。

样本*均估计

在上述示例中,为了计算均值状态值,我们跟踪了每个状态的总回报和首次访问总次数。当所有实验完成后,将总回报除以访问次数,以获得估计的状态值。然而,由于总回报不断增大,这可能会导致问题。

更好的方法是使用运行均值,在获得每个新的回报值时,我们基于最后的估计来更新估计的状态值。这不仅有助于避免存储和计算时间的问题,还使我们能够在每一步都获得状态值的估计。我们不需要等到过程结束后才能计算*均状态值。

计算新状态值的公式,基于之前的估计和最新实验中获得的回报,如下所示(其中‘n’是访问状态的次数):

方程 3:计算状态‘s’的估计值,基于之前的值和新的回报。

要验证这个方程式,可以查看Bandits article,我们使用类似的方法来计算行动值的样本*均估计。

探索开始

图 5 上的第二张图片显示了状态首次访问计数。这个数字代表了状态至少被访问过一次的实验次数,而不是该状态的总访问次数。它也等于经过这个状态的所有轨迹的数量。

在这个简单的环境中,从入口到出口只有一条单一的路线(这就是我们在图 2中看到的直接路径)。要到达出口,Baby Robot 必须经过这条路径上的每一个状态。因此,这条轨迹上的所有状态在一次实验中都必须被访问至少一次,因此,所有状态都有 10,000 次首次访问,等于总实验次数。

相比之下,包含水坑的网格方块(在网格位置 [1,0])并不位于到出口的直接路径上。在移动到出口的过程中不需要访问这个状态,因此只有当策略随机选择的动作使 Baby Robot 移动到这里时,这个状态才会被访问。因此,它仅在 10,000 次实验中的 8,737 次中被访问。

从婴儿机器人的角度来看,这是件好事,因为它阻止了他变湿,但从我们状态估计的准确度来看,这并不理想,因为这个状态被采样的次数少于其他状态。一般来说,访问最频繁的状态会比不常见的状态具有更准确的状态值估计。

为了弥补这一点,我们可以选择在不同的状态下开始每一集,从而获得更均衡的状态值估计的准确性。此外,如果我们的策略导致一些状态从未被访问,我们可以在这些状态下开始一些集,从而估计这些状态的值。

这种从总状态空间中选择不同状态开始每集的技术被称为探索性起始

图 6:带有探索性起始的首次访问 MC。计算的状态值和状态首次访问计数,针对 10,000 集。

使用探索性起始进行 10,000 集时的状态值和首次访问计数如图 6所示。你可以看到,在这种情况下,大多数状态的首次访问次数实际上减少了。这是因为*均轨迹更短,因为在大多数情况下,起始状态会更接*出口。对于像这样的简单环境,探索性起始并不会带来实际的好处。事实上,虽然探索性起始可以帮助*衡对每个状态的访问,但这可能并不总是可行或甚至可取。

当我们仅仅是在运行环境的模拟时,选择每集的开始位置非常简单。另一方面,如果这是一个实际的、真实生活中的迷宫,而婴儿机器人正在导航,则选择起始状态将变得相当困难,并且对于某些环境,可能实际上是不可能的。

此外,尽管计算出的每个状态的值应该具有类似的准确度,但这不一定总是有益的。没有探索性起始的情况下,访问最频繁的状态将具有比那些不常访问的状态更高的准确度。这可能是件好事,因为我们集中精力寻找最常见状态的状态值,而不是浪费时间在那些不常见或从未访问的状态上。然而,不使用探索性起始则有可能错过那些可能会带来高回报的状态。正如强化学习中总是要面对的情况一样,这是探索与开发之间的*衡。

每次访问的蒙特卡洛

到目前为止,无论是我们从关卡起点开始的实验,还是使用探索性起始,我们都在使用首次访问蒙特卡洛,其中返回值是从状态第一次被访问起计算到本集结束。

然而,在任何一集游戏中,一个状态可能会被多次访问(例如,参见图 4中给出的样本轨迹,其中状态[2,1]和[2,2]都被访问了两次)。因此,另一种计算状态值估计的方法是将每次访问状态视为一个单独轨迹的开始,并记录该访问的回报,直到本集游戏终止。这种估计状态值的方法被称为每次访问蒙特卡洛

对我们样本关卡的结果,在记录每次访问状态时,如下图 7所示。尽管测试进行了标准的 10,000 次试验,但通过记录每次访问状态,实际上记录到每个状态的访问次数在大多数情况下要高得多。

无论是首次访问还是每次访问蒙特卡洛,只要时间足够,最终都能收敛到真实的状态值。然而,尽管每次访问蒙特卡洛记录了更多对每个状态的访问,但并不清楚它是否比首次访问蒙特卡洛实际能提供更好的结果。这可能是因为每次访问蒙特卡洛记录的额外轨迹信息已经包含在首次访问轨迹的回报中。

图 7:每次访问蒙特卡洛,计算出的状态值和 10,000 次试验的状态访问计数。

蒙特卡洛控制

使用上述任何蒙特卡洛方法,我们能够为任何给定的策略生成相当准确的状态值估计。然而,虽然这使我们能够看到相对于其他状态的相对优劣,但这实际上并不能帮助我们导航关卡。

当我们掌握了有关状态转移和奖励的完整信息时,可以使用动态规划贝尔曼方程转化为一组更新规则,以计算当前策略下的状态值。然后,我们可以从这些状态值中选择出能够带来最大预期回报的动作。

这由以下方程总结,针对状态s的贪婪策略π

方程 4:关于状态值的贪婪策略更新。

其中:

  • p(s’,r|s,a) 是在状态s下采取动作a后,转移到下一个状态s’并获得奖励r的概率。

  • r 是采取该动作后获得的奖励。

  • γ 是折扣因子。

  • V(s’) 是下一个状态的值。

不幸的是,由于我们没有提供环境模型,我们不知道p(s’,r|s,a),因此无法利用下一个状态值V(s’)。 由于没有环境模型,我们不知道采取特定动作后会到达何处,也不知道采取该动作会获得什么奖励。

作为说明,考虑一下小机器人正在探索的区域,以及我们计算的首次访问状态值,再次显示在图 8下方:

图 8:状态值和坐标。

当小机器人到达网格方格[1,1]时,如上所示,如果我们仅仅依据计算出的状态值贪婪地行动,那么最优动作似乎是向南移动到网格方格[1,2],因为这是与当前状态具有最高状态值的相邻状态。然而,由于我们没有环境模型,我们对采取该动作所得到的转移概率或奖励一无所知。

如果小机器人采取这个动作,他到达目标状态[1,2]的概率实际上为零,而是会撞到玻璃墙,最终落入[1,0]的水坑中。此外,他还会因采取这个动作而收到-5 的大负奖励。因此,实际上,这并不是一个好的选择。

因此,对于蒙特卡洛 控制,我们需要计算的是动作值,而不是每个状态的值。作为提醒,这些值被称为Q-值,表示从采取单个动作中可以期待的回报。然后,我们可以利用这些值来找到最优策略,并在迷宫中找到最佳路径。

一旦我们知道状态-动作值(或其*似值),我们就可以通过在这些值上贪婪地行动来进行策略改进,即在每个状态中简单地选择具有最大值的动作。我们不需要环境模型,也不需要引导(即不使用潜在下一个状态的值)。

该策略更新由方程 5描述,对于状态‘s’,贪婪策略‘π’简单地选择具有最大状态-动作值‘q’的动作‘a’

方程 5:关于最大状态-动作值的贪婪策略更新

使用蒙特卡洛方法计算状态-动作值的代码与计算状态值的代码几乎相同,只不过现在我们分析的是这些状态中的动作访问情况,而不是单个状态的访问情况。

使用首次访问蒙特卡洛方法计算状态-动作值的 Python 伪代码如下所示:

''' calculate state-action values using First-Visit Monte Carlo '''

# keep a count of the visits to each action
visits = np.zeros((env.height,env.width,len(Actions)))                    ❶

# the average returns for each action
returns = np.zeros((env.height,env.width,len(Actions)))

for episode in tqdm(range(max_episodes)):                                 ❷

  action_rewards = single_episode(env,policy)                             ❸
  action_returns = rewards_to_returns(action_rewards)                     ❹
  episode_returns,episode_visits = get_first_visit_return(action_returns) ❺

  # add the episode returns to the total returns
  returns += episode_returns

  # increment the count of any states that have been visited
  visits += episode_visits

avg_returns = (returns/visits)                                            ❻

如果你将这段代码与我们用来计算状态值的蒙特卡罗首次访问代码进行比较,你会发现它们几乎完全相同。唯一真正的区别是,现在我们使用一个第三维度来计算Q 值,而不是使用环境中的二维状态数组。

上面代码中的重点,对于首次访问蒙特卡罗方法,如下:

  1. 在所有集的过程中,我们希望跟踪在每个动作首次访问后的回报和每个动作被访问的集数(对于首次访问蒙特卡罗)。因此,我们首先为每个动作创建零初始化的 Numpy 数组。与之前一样,数组维度设置为环境的宽度和高度,但现在增加了一个维度,用于每个状态中的可能动作数量。

  2. 正如所有蒙特卡罗方法中一样,我们希望对多个集获得的奖励进行采样。

  3. 对于每一集,获取访问的状态动作列表以及每个状态动作收到的奖励。

  4. 反向求和奖励以计算每个动作的回报。

  5. 计算在集期间采取的动作的首次访问回报值,然后将其添加到总回报中。这更新了每个在集期间出现的动作的总回报,并增加了这些动作的首次访问计数。

  6. 最后,在所有集完成后,将总首次访问奖励除以访问总数,以获得每个状态-动作的估计 Q 值。

运行这段代码 1000 集后,得到如下状态-动作值和动作访问计数:

图 9:使用蒙特卡罗计算的动作值和计数。

图 9中,我们可以看到,从入口到关卡出口必须采取的所有动作都被采样了 1000 次,等于集的数量。同样,由于宝宝机器人足够聪明,不会走进标准墙,因此会导致撞墙的动作从未被采取,因此访问计数为零。由于使用了随机策略,所有其他可能的动作在所有集上都被访问过。

如果我们现在根据这些动作值采取贪婪行动,我们得到的政策如下,对于这个非常简单的关卡,这也是最优政策。按照这个政策,宝宝机器人将以最短时间到达出口:

图 10:通过对估计的动作值采取贪婪行动计算出的最优政策。

使用蒙特卡罗方法的广义策略迭代(GPI)

对于我们用作示例的简单关卡,在任何状态下,每个动作被选择的概率相等,我们使用蒙特卡洛评估了策略以估计动作值,然后通过在每个状态中选择具有最大值的动作来改进策略(即,我们对动作值采取了贪婪行为)。

这代表了广义策略迭代(GPI)的单次迭代,其中重复的评估和改进步骤使策略越来越接*最优策略,如图 11所示。但现在,我们没有使用动态规划来评估每个策略,而是使用蒙特卡洛方法来逼*价值函数。

图 11:广义策略迭代(GPI)

在我们简单的关卡中,我们仅经过一次迭代就达到了最优策略,但在实践中,对于更复杂的环境,可能需要多次迭代。

例如,考虑下面显示的更复杂的关卡及其相关的确定性策略:

图 12:稍微复杂一点的关卡及其对应的确定性策略

我们可以对该确定性策略进行单次蒙特卡洛策略评估,这会给我们以下状态-动作值:

图 13:确定性策略下 MC 策略评估单次迭代后的状态-动作值

如果我们对这些值采取贪婪行为,我们将得到以下更新的策略:

图 14:在对估计的动作值采取贪婪行为后的更新确定性策略

我们似乎遇到了问题!不仅关卡的大部分未被访问,而且宝宝机器人还被困在循环中,永远无法到达出口(如果我们没有设置回合长度限制,这将永远继续下去)。

很明显,当使用确定性策略时,我们还需要添加一些形式的探索,否则我们将无法估计未访问过的动作的状态-动作值,因此,我们将无法改进策略。

我们可以通过使用我们在状态值中看到的探索起始思想来实现这种探索。在这种情况下,我们不仅在每个回合开始时进入一个新的状态,还将其扩展到覆盖所有动作。然而,除非在简单的环境模拟中,这将是不可行的。更好的方法是考虑在每个状态中有非零选择所有动作的机会的策略。

Epsilon-贪婪算法(ε-贪婪)

遵循确定性策略时,每个状态只会采取一个动作。然而,为了让我们选择最佳动作,我们需要对所有动作的值进行估计,因此需要加入某种形式的探索。

实现这一目标的最简单但非常有效的方法之一是Epsilon-Greedy算法(我们在研究 Bandit 问题时见过)。在这种方法中,在任何状态下,我们通常选择具有最大估计动作值的动作,但以概率‘ε’(epsilon)随机选择一个动作。

因此,对于当前状态,我们可以选择如下所示的动作:

# probability of selecting a random action
p = np.random.random()

# if the probability is less than epsilon then a random action 
# is chosen from the state's available actions
if p < epsilon:
    action = env.action_space.sample()
else:                
    # get the policy's action in the current state
    action = policy.get_action(env.x,env.y) 

现在,当遵循epsilon-greedy策略时,我们可以发现其他状态和动作,这些状态和动作在纯确定性策略下是不会被发现的。因此,在一个回合中,我们会收集到比仅遵循确定性策略时更多的信息。

例如,对于上述使用的相同确定性策略,但增加了以 0.1 的概率选择的随机动作(即大约每第 10 个动作会随机选择),在一个回合中,我们得到如下所示的动作值:

图 15:一个回合的 Epsilon-Greedy 动作值(回合中的允许步数增加到 2000)

当我们对这些动作值贪婪地采取行动时,会得到如下策略,正如你所看到的,这些策略基于一个更大的状态集合定义了动作:

图 16:从贪婪地执行基于动作值的策略中形成的策略。

现在我们有了一种可以探索环境的方法,我们可以回到最初使用 GPI 来寻找最优策略的问题。

在上述示例中,我们只运行了一个蒙特卡洛回合来评估状态-动作值,然后基于这些值改进策略。相反,在策略改进步骤之前,我们本可以在多个回合中评估策略,以形成对动作值的更准确的估计。

这两种方法类似于价值迭代,只在策略改进前完成了一步策略评估,以及策略迭代,其中策略在策略改进步骤之前进行收敛评估。在任何情况下,对于蒙特卡洛方法,我们需要在策略评估步骤中运行完整的回合,然后再进行策略改进。

图 17:一个初始的确定性策略,该策略确实可以达到出口。

确定性策略,如上图图 17所示,定义了一条从起点到达关卡出口的轨迹。但正如你可能已经注意到的,它并不是最优的。

因此,我们希望通过运行多轮 MC 策略评估和策略改进来改进这个策略,使我们逐步接*最优策略。设置ε为 0.5,以提供较高的探索度,并在每次策略改进之前运行一次 MC 策略评估,我们得到了下面显示的动作价值估计和相关策略:

图 18:运行 GPI 时,ε=0.5 的动作价值估计和相关策略改进

delta 值,如图 18所示,每次 GPI 迭代中都有显示,并且在下面的图表中,代表了迭代开始和结束时状态动作值之间的*均差异。正如你所看到的,它最初以一个较高的值开始,并在估计的动作值变得更接*真实动作值时趋*于零。

图 19:delta 值的变化,表示每次迭代中所有动作值的均值差异

在这次运行中,我们实际上使用了 delta 值来控制 GPI 过程的停止(当 delta 小于 0.04 时认为已经收敛)。最终策略如下面的图 20所示:

图 20:GPI 最终迭代中形成的策略,使用最小 delta 值 0.04 作为停止标准。

这个最终策略定义了从入口到出口的更短路线。此外,起始状态的单一动作价值从初始策略中的-45 降低到最终策略中的-27。由于这个值代表了在采取此动作时可以预期的回报,并且在这个环境中,每个动作都会因为采取该动作而产生-1 的惩罚,你可以看到我们大大减少了宝宝机器人到达出口所需的时间。

然而,情况并不完美,这并不是这个关卡的最优策略。例如,如果你考虑左下角的状态,你会发现最终的策略定义了一个将宝宝机器人向北移动的动作,而更短的通向出口的路线是向东移动。

由于在此状态下动作采样不足,最佳动作尚未找到。如果你查看每个动作的首次访问次数,如下图 21所示,你可以看到北方动作在用于策略评估的 23 个回合中仅出现了 3 次。

图 21:在 23 轮 GPI 迭代中访问的动作的首次访问计数。

这个动作几乎从未被访问的原因有几个:

  • 我们定义了一个最小的 delta 值,在找到最优策略之前停止了 GPI。

  • 我们每次策略评估步骤仅运行一个回合。增加回合数将提高动作值估计的准确性,并增加每个动作的访问次数。

  • 默认情况下,Epsilon-贪婪算法遵循当前的确定性策略。虽然其他动作是随机选择的,以增加对环境的探索,但远离的状态和动作较少被访问。

如前所述,这种状态及其动作被访问得不频繁可能并不是一件坏事。与其均匀地访问所有状态和动作,不如集中在从初始状态到出口的路径上,这样我们就减少了寻找迷宫中最优轨迹所需的时间。像往常一样,这涉及到探索\利用的权衡。

摘要

蒙特卡洛方法使用采样从经验中学习。在多个回合中,它们逐渐建立对环境的越来越准确的描述。因此,它们可以在没有环境模型的情况下估计状态和动作值。

广义策略迭代(GPI)的策略评估步骤中,通过使用当前策略运行完整的回合,可以使用蒙特卡洛方法形成访问的状态-动作值的*均值。

为了确保所有状态和动作都被访问,可以通过多种方式引入探索,例如探索开始,在这种情况下,每个新回合从不同的状态或动作开始,或epsilon-贪婪策略,其中,默认情况下,采取由策略指定的动作,但以‘epsilon’的概率选择一个不同的随机动作。

如果策略通过对这些估计值采取贪婪行动来改进,那么策略改进定理保证新策略将与之前的策略一样好或更好。通过重复这些策略评估和改进步骤,蒙特卡洛方法可以用来找到最优策略。

下一步

到目前为止,我们只考虑了‘策略内’方法,其中评估和改进的是单一策略。接下来,我们将查看‘策略外’方法,这些方法使用多个策略。例如,一种策略可以用于探索和收集信息,而第二种策略,即实际用于与环境互动的策略,则根据这些信息创建。

我们还看到,与动态规划不同,蒙特卡洛方法在更新其值估计时不会使用其他状态的值。换句话说,它们不进行‘自举’。在下一篇文章中,我们将探讨时序差分 (TD) 学习,这种方法像蒙特卡洛一样从经验中学习,但在计算其值估计时也采用自举方法。

参考文献:

强化学习: 介绍”,Sutton & Barto (2018)

讲座 4: 无模型预测”,David Silver

**< Part 3: Policy and Value Iteration**

关于道路网络匹配的更多内容

原文:towardsdatascience.com/more-on-road-network-matching-c2ea0728c415?source=collection_archive---------16-----------------------#2023-02-10

道路网络匹配的恶作剧

João Paulo FigueiraTowards Data Science João Paulo Figueira

·

关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 2 月 10 日

--

图片由 Denys Nevozhai 提供,来源于 Unsplash

本文的目的是对之前关于同一主题的文章进行补充和修正。在那篇文章中,我介绍了一种从扩展车辆能量数据集¹(EVED)重建丢失地图匹配数据的方法。我的技术探索了三角形的数学特性,以便快速找到道路网络数据库中的丢失数据。不幸的是,代码中的一个细微错误使得在某些情况下失败。

在这里,我展示了对代码的修正,并讨论了应用这些技术重建三万两千多个轨迹的结果。

## 使用三角形进行道路网络边缘匹配

三角形在地理空间查询中具有强大的属性

towardsdatascience.com

网络的细微差别

在上一篇文章中,我展示了两种方法来将地图匹配的位置拟合到道路网络段上,使用了三角形的性质。支持该思想的几何概念是适当的,但代码需要修正。问题出现在节点过滤部分,影响了两种匹配方法,即基于比例和基于距离的方法。让我们再次查看使用基于比例的函数的原始代码:

def get_matching_edge(self, latitude, longitude, bearing=None):
    loc = np.array([latitude, longitude])
    _, r = self.geo_spoke.query_knn(loc, 1)
    radius = self.max_edge_length + r[0]
    node_idx, dists = self.geo_spoke.query_radius(loc, radius)
    nodes = self.ids[node_idx]
    distances = dict(zip(nodes, dists))
    adjacent_set = set()
    graph = self.graph

    best_edge = None
    for node in nodes:
        if node not in adjacent_set:
            adjacent_nodes = np.intersect1d(np.array(graph.adj[node]),
                                            nodes, assume_unique=True)

            adjacent_set.update(adjacent_nodes)
            for adjacent in adjacent_nodes:
                edge_length = graph[node][adjacent][0]['length']
                ratio = (distances[node] + distances[adjacent]) / \
                        edge_length
                if best_edge is None or ratio < best_edge[2]:
                    best_edge = (node, adjacent, ratio)

        if bearing is not None:
            best_edge = fix_edge_bearing(best_edge, bearing, graph)
    return best_edge

该函数使用一个集合来存储所有处理过的相邻节点,以避免重复。遗憾的是,它确实避免了重复。这就是错误所在,因为通过排除一个节点,函数也阻止了所有其他相邻边缘的测试。这种情况在具有多个链接的节点中特别普遍。修正代码需要一个概念上的变化,即移除处理过的道路网络边缘而不是处理过的节点

但是在使用代码处理几个不同轨迹后出现了另一个问题,即“黑天鹅”事件。当输入的 GPS 位置接*某个节点时会发生什么?在一些罕见情况下,采样的 GPS 位置与某个道路网络节点非常接*,这会引起微妙的浮点不稳定性。结果是选择了一个不合理的边缘,破坏了路径重建。为了解决这个问题,我添加了一个默认参数,设置从输入的 GPS 位置到最*节点的最小距离。当这个距离小于一米(三英尺)时,函数拒绝选择边缘并返回一个空值。这种解决方案比较温和,因为这种情况相对不常发生,而且随后的重建机制可以补偿这一点。

你可以看到下面的修正代码,其中包含了测试过的道路网络边缘集和最小距离检查。

def get_matching_edge(self, latitude, longitude,
                      bearing=None, min_r=1.0):
    best_edge = None
    loc = np.array([latitude, longitude])
    _, r = self.geo_spoke.query_knn(loc, 1)
    if r > min_r:
        radius = self.max_edge_length + r[0]
        node_idx, dists = self.geo_spoke.query_radius(loc, radius)
        nodes = self.ids[node_idx]
        distances = dict(zip(nodes, dists))
        tested_edges = set()
        graph = self.graph
        node_set = set(nodes)

        for node in nodes:
            adjacent_nodes = node_set & set(graph.adj[node])

            for adjacent in adjacent_nodes:
                if (node, adjacent) not in tested_edges:
                    edge_length = graph[node][adjacent][0]['length']
                    ratio = edge_length / (distances[node] + distances[adjacent])

                    if best_edge is None or ratio > best_edge[2]:
                        best_edge = (node, adjacent, ratio)
                    tested_edges.add((node, adjacent))
                    tested_edges.add((adjacent, node))

        if bearing is not None:
            best_edge = fix_edge_bearing(best_edge, bearing, graph)
    return best_edge

请注意,我还更改了速率公式,使其范围在零到一之间,较大的值对应于更好的拟合。有趣的是,基于速率的函数比基于距离的函数更有效且更快速。

重建旅行

路径重建的第一部分意味着将上述函数应用于输入轨迹的每一点,只保留独特的道路网络边缘。下面的代码展示了一个实现这一点的 Python 函数。

def match_edges(road_network, trajectory):
    edges = []
    unique_locations = set()
    edge_set = set()
    for p in trajectory:
        if p not in unique_locations:
            e = road_network.get_matching_edge(*p, min_r=1.0)
            if e is not None:
                n0, n1, _ = e
                edge = (n0, n1)
                if edge not in edge_set:
                    edge_set.add(edge)
                    edges.append(edge)
                unique_locations.add(p)
    return edges

该函数为每个独特的轨迹位置分配一个道路网络边缘。图 1 显示了一个匹配样本。

图 1 — 上面的地图显示了红色的输入轨迹位置和蓝色的匹配道路网络边。(图片来源:作者使用 Folium 和 OpenStreetMap 图像)

如您所见,由于 GPS 采样频率,匹配的链接之间存在一些间隙。重建路径要求我们填补这些间隙,我们通过在道路网络边之间找到最短路径来实现。下面的代码通过保持连接的链接在一起,并使用 OSMnx 的服务填补其他所有间隙来完成这一任务 [2]。

def build_path(rn, edges):
    path = []
    for e0, e1 in pairwise(edges):
        if not len(path):
            path.append(e0[0])
        if e0[0] != e1[0] and e0[1] != e1[1]:
            if e0[1] == e1[0]:
                path.extend([e0[1], e1[1]])
            else:
                n0, n1 = int(e0[1]), int(e1[0])
                sp = ox.distance.shortest_path(rn, n0, n1)
                if sp is not None:
                    path.extend(sp[1:])
    return path

在对先前的匹配调用此函数后,我们应该将轨迹完全重建为一系列道路网络边。通过将轨迹位置叠加到修补后的道路网络边上,我们可以看到一个连续的结果,如下方的图 2所示。

图 2 — 上面的地图显示了重建的链接序列与原始轨迹位置叠加在一起。(图片来源:作者使用 Folium 和 OpenStreetMap 图像)

最小距离的案例

在审查这个解决方案时,我发现了一个在迭代各个轨迹时的问题。下方的图 3详细描述了这个问题。

图 3 — 上面的图像显示了一个角落案例,在这个道路网络边匹配系统崩溃时发生。轨迹的一个位置正好落在某个节点上,从而欺骗了匹配过程。(图片来源:作者使用 Folium 和 OpenStreetMap 图像)

路由中的虚假循环源于修补过程中的输入数据中的意外值。我们可以通过返回显示算法如何将这些 GPS 位置匹配到道路网络边的视图来深入了解发生了什么。

下方的图 4说明了在叉路口的情况,其中匹配的 GPS 位置与道路网络节点重合(或非常接*)。这可能会导致一些数值不稳定性,产生错误结果。一个稳健的算法应该避免这些情况,但如何做到呢?

图 4 — 一旦看到位置分配,匹配问题变得明显。显然,在上述情况下,GPS 位置非常接*某个道路网络节点,这导致了边选择过程中的不稳定性。(图片来源:作者使用 Folium 和 OpenStreetMap 图像)

考虑后,我决定更改两个匹配算法,并使用与最*的道路网络节点的距离作为包含或排除输入位置的标准。如果这个距离小于给定的阈值(默认为一米或三英尺),匹配函数返回一个空值,通知调用代码无法安全地分配一个链接。这对接下来的步骤是可以的,因为它应该推断缺失的链接,因为该算法仅依赖于道路网络边,而不是输入位置。

现在我们已经对整个算法进行了清理,让我们看看它在整个数据集上的表现。

性能评估

为了评估算法在整个 EVED 上的表现,我创建了一个新的独立脚本,它遍历所有轨迹,匹配边缘,生成路径,并比较原始轨迹长度与重建路径的长度。Python 脚本记录了所有的差异,以便我们可以使用标准统计工具集进行后续分析。

脚本的核心在于其主循环,详见下面的代码。

def process_trajectories():
    rn = download_network()
    road_network = RoadNetwork(rn)

    state = load_state()
    if state is None:
        state = {
            "trajectories": get_trajectories(),
            "errors": []
        }

    save_counter = 0
    trajectories = state["trajectories"]
    while len(trajectories) > 0:
        trajectory_id = trajectories[0]
        trajectory = load_trajectory_points(trajectory_id,
                                            unique=True)
        if len(trajectory) > 3:
            edges = match_edges(road_network, trajectory)
            path = build_path(rn, edges)

            if len(path) > 0:
                diff = calculate_difference(rn, path, trajectory)
                print(f"Trajectory: {trajectory_id}, Difference: {diff}")
                state["errors"].append((trajectory_id, diff))

        trajectories = trajectories[1:]
        state["trajectories"] = trajectories

        save_counter += 1
        if save_counter % 100 == 0:
            save_state(state)

    save_state(state)

该函数首先从 OpenStreetMap 下载并预处理道路网络数据。由于这是一个长时间运行的脚本,我添加了一个持久化状态,序列化为 JSON 格式的文本文件,并保存了每 100 条轨迹。这一功能允许用户中断 Python 脚本而不会丢失已处理轨迹的所有数据。计算原始输入轨迹与重建路径之间长度差异的函数如下所示。

def calculate_difference(rn, path, trajectory):
    p_loc = np.array([(rn.nodes[n]['y'], rn.nodes[n]['x']) for n in path])
    t_loc = np.array([(t[0], t[1]) for t in trajectory])

    p_length = vec_haversine(p_loc[1:, 0], p_loc[1:, 1], 
                             p_loc[:-1, 0], p_loc[:-1, 1]).sum()
    t_length = vec_haversine(t_loc[1:, 0], t_loc[1:, 1], 
                             t_loc[:-1, 0], t_loc[:-1, 1]).sum()
    return p_length - t_length

该代码将两个输入转换为地理位置序列,计算它们的累积长度,并最终计算它们的差异。当脚本完成时,我们可以加载状态数据并使用 Pandas 进行分析。

我们可以在下面的图 5中看到两个距离之间的差异分布(标记为"error")。

图 5 — 上面的直方图展示了原始输入轨迹与重建路径之间的距离差异的分布。横轴显示距离(以米为单位)。(图像来源:作者)

为了更详细地查看零附*的情况,我裁剪了直方图,并创建了图 6

图 6 — 上面的直方图详细说明了差异分布,突出了负差异和大多数正差异。大多数差异很小,暗示匹配算法表现良好。(图像来源:作者)

如您所见,光谱的负部分存在一些差异。不过,它主要是正的,这意味着重建路径的长度通常比轨迹更长,这是可以预期的。请记住,轨迹点很少与道路网络节点匹配,这意味着最常见的情况应该是在一个段的中间匹配。

我们还可以通过绘制箱线图来检查差异的中间五十个百分点,如下图图 7所示。

图 7 — 上面的箱线图展示了分布中间 50%的位置。25%、50%和 75%的百分位数分别为 13.0、50.2 和 156.5 米。我们还可以看到,内点带相对较窄,范围从-202.3 到 371.8 米。(图像来源:作者)

上图包含了有关差异分布和异常值数量的关键信息。使用Tukey’s fences方法,我们可以将 32,528 条轨迹中的 5,332²条标记为异常值。这一比例代表了相对较高的 16.4%值,这意味着可能有更好的重建方法,或者存在大量处理不当的轨迹。我们可以查看一些最剧烈的距离差异异常值,试图理解这些结果。

异常值分析

如前所述,有超过五千条轨迹匹配效果较差。我们可以快速检查其中的一些,来确定采样轨迹与重建之间的差异为何如此显著。我们从一个揭示性的案例开始,即图 8中的轨迹编号 59。

图 8 — 上图所示的轨迹展示了一个地图匹配错误,导致重建过程生成了一条比必要的路径更长的路径。(图片来源:作者使用 Folium 和 OpenStreetMap 图像)

上述案例表明,地图匹配过程错误地将三个 GPS 样本放置在了一条*行的道路上,迫使重建过程找到一个虚假的绕行路径。这种错误可能由于地图匹配过程的参数设置不当造成,需在后续文章中进一步研究。

最后,我们可以看到下方的图 9中的一个非常极端的案例。采样的 GPS 位置距离道路如此之远,以至于地图匹配过程放弃了。正如你所见,重建过程分配了与预期轨迹无关的道路网络链接。

图 9 — 上图中的轨迹包含许多错误,初始的地图匹配过程无法消除这些错误。(图片来源:作者使用 Folium 和 OpenStreetMap 图像)

结论

在本文中,我修正了先前发布的道路网络边缘匹配代码,并使用 EVED 数据集评估了其性能。我使用专门的 Python 脚本收集了性能评估数据,揭示了匹配轨迹中测量距离似乎异常偏差较高的情况。为了了解为何结果可以更好,我将接下来转向原始的地图匹配软件,Valhalla,以尝试修复上述问题。

注释

  1. 原论文作者将数据集以 Apache 2.0 许可证授权(请参阅VEDEVED GitHub 仓库)。请注意,这也适用于衍生作品。

  2. 所有图像和计算的来源均在一个 Jupyter notebook 中,来自公共 GitHub 仓库

参考文献

[1] Zhang, S., Fatih, D., Abdulqadir, F., Schwarz, T., & Ma, X. (2022). 扩展车辆能源数据集(eVED):用于深度学习的车辆旅行能耗的大规模增强数据集。ArXiv. doi.org/10.48550/arXiv.2203.08630

[2] Boeing, G. 2017. OSMnx:获取、构建、分析和可视化复杂街道网络的新方法Computers, Environment and Urban Systems 65, 126–139. doi:10.1016/j.compenvurbsys.2017.05.004

João Paulo Figueira 在葡萄牙里斯本的 tb.lx by Daimler Trucks and Buses 担任数据科学家。

带有仿真的形态学操作(CV-05)

原文:towardsdatascience.com/morphological-operations-a-way-to-remove-image-distortion-513d162e7d05

图像处理中的形态学操作最简单解释

Md. ZubairTowards Data Science Md. Zubair

·发表于 Towards Data Science ·8 分钟阅读·2023 年 5 月 10 日

--

照片由 Katharina Matt 提供,发布于 Unsplash

动机

我们每天处理大量的图像。这些图像具有不同的强度和分辨率。有时,由于质量原因,我们无法从图像中提取适当的信息。各种图像处理技术在许多方面对我们有帮助。形态学操作是一种重要的技术,通过它我们可以减少图像的失真并对形状进行操作。请看下面的图像。

左侧是应用形态学操作前的图像,右侧是操作后的图像(作者提供的图像)

另一个例子 —

应用形态学操作前后的对比(作者提供的图像)

结果很有趣。但形态学操作的使用案例不仅限于这两种任务。我将在本文中讨论你需要了解的所有形态学操作。

*[注意:主要的形态学分析适用于二值图像。如果你不知道如何创建二值图像,请阅读我之前的文章* [*图像阈值化*](https://medium.com/towards-data-science/thresholding-a-way-to-make-images-more-visible-b3e314b5215c)*。]*

目录

  1. **形态学操作详解**

  2. **不同类型的形态学操作及其实现**

2.1\. 侵蚀

2.2\. 膨胀

2.3\. 复合操作

3. **结论**

形态学操作详解

形态学操作是一种基于图像形状处理图像的技术。它通过比较邻*像素来构建图像。该过程适用于二值图像({0,1} 或 {0,255})

这个过程是如何工作的?

在熟悉形态学操作之前,我们需要了解一些基本术语——结构元素、错过、命中和适合。

图 1:形态学操作的元素(图像由作者提供)

结构元素

它是一个小的形状或模板,分析图像的每个像素与元素下的邻域像素。上图中标记为蓝色的结构元素。

不同的结构元素——

图 2:不同的结构元素(图像由作者提供)

结构元素是根据图像的形状设计的。结构元素的大小可以有不同的尺寸(2x2, 3x3, 5x1, 5x5, 等)。结构元素包含前景和背景的强度值(即 0 或 1)。它还可以包含无所谓的值。结构元素的一个像素被视为原点。在上面的图像中,我用黑点标记了原点像素。定义原点没有硬性规定,这取决于你。但通常,原点被认为是中心像素。

形态学操作是通过在图像上传播结构元素来完成的。通过比较结构元素下的像素,改变图像中**原点**位置的像素值。

错过: 如果图像中没有像素与结构元素匹配,则称为错过。见于图 1

命中: 当结构元素的至少一个像素与图像像素重叠时,称为命中。见于图 1

适合: 如果结构元素的所有像素与图像匹配,则称为适合。见于图 1

基本上,有两种类型的形态学操作——

  1. *腐蚀*

  2. *膨胀*

从这两种操作中可以导出另外两种复合操作——闭合和开操作

不同类型的形态学操作及其实现

二值图像适合应用形态学操作。

腐蚀

操作很简单。结构元素卷积对象的每个像素。如果结构元素的所有像素都与对象图像像素重叠(满足适合条件),对象图像像素将填充前景像素强度值。否则,它将填充背景像素强度值。

假设背景为 0,前景为 1。

**if Fit -> 1
else -> 0** 

我创建了一个模拟以便更好地理解。为了演示目的,我使用了一个强度为 1 的 2x2 结构元素。

图 3:结构元素(作者图像)

我已经拍摄了一张 6x6 像素的图像。白色元素被视为 0 强度值,天蓝色像素被视为 1 的强度值。现在仔细观察下面的模拟。

侵蚀操作(作者 Gif)

结构元素会卷积到给定图像的每个像素上。如果它满足漏检或击中条件,它将把像素更改为原始位置的 0。在模拟中,我用红色显示了像素从 1 变为 0 的地方。最后,我们得到如下结果。

图 4:侵蚀结果(作者图像)

因此,这表明主要图像像素通过侵蚀减少了。

使用 OpenCV 的实际实现

**我们将使用*OpenCV*库来实现形态学操作。在 OpenCV 库中,结构元素被称为*内核*。**

首先,我们导入必要的库。

我将展示图像如何随着不同的结构元素/内核大小而变化。

  • 加载图像 —

我们的图像在白色背景上是黑色的。但是 [**OpenCV**](https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html) 期望黑色背景与白色图像对象进行形态学操作。 所以,我们将白色背景转换为黑色,并将黑色对象转换为白色,使用逆二值化。接下来,我们在逆二值化图像上应用5x5、9x9 和 11x11内核。由于我们对逆二值化图像应用了形态学操作,在形态学操作后,我们再次应用逆二值化以保留白色背景和黑色对象。最后,我们绘制了这些图像。

下面是带有注释的代码。

我们还可以对图像应用多个迭代,使用相同的内核大小。结果显示为 5x5 内核大小下的 1、2 和 3 次迭代。

侵蚀的属性与编码示例

  1. 这可以用来去除对象的额外噪声。

加载图像 —

去除外部噪声后的结果 —

步骤类似于之前的代码*(逆二值化 → 形态学操作 → 逆阈值化以保留原始图像背景和前景色)*

2. 将连接的图像分开。

编码示例 —

加载连接图像 —

它将图像分开。

膨胀

在膨胀操作中,结构元素会卷积到对象图像上。如果结构元素的像素值与对象图像重叠(满足击中条件),则对象图像像素将被填充为前景像素强度值。否则,像素强度值将保持不变。

我们假设背景为 0,前景为 1。

**If Hit -> 1
else -> 0**

为了更好地理解,我创建了膨胀操作的模拟。作为结构元素,我们使用了图-3中的先前结构元素,并保留了与腐蚀操作中显示的相同图像。

现在,仔细观察下面展示的膨胀操作。

膨胀操作(作者提供的 Gif)

结构元素在对象图像的每个像素上从左到右、从上到下进行卷积。当满足击中或未击中条件时,结构元素原点位置的像素从 0 变为 1。否则,它保持不变。完成操作后,产生如下结果。

图-5: 膨胀结果(作者提供的图片)

所以,膨胀增加了对象图像的像素。

使用 OpenCv 的实际应用

我使用了腐蚀部分中显示的相同图像。代码也与前一部分相同。在这一部分,我们将应用膨胀操作而不是腐蚀操作,并展示不同内核大小和迭代次数的结果。

使用不同的迭代值和相同的 5x5 内核大小,我们可以看到对象图像的形状如何变化。对于 OpenCV 的实现,我们遵循了与腐蚀部分相同的步骤*(反向二值化阈值 → 形态学操作 → 反向阈值以保留原始图像背景和前景颜色)*

膨胀操作的属性及示例

  1. 通过膨胀,我们可以减少/修复图像的断裂。

编码示例 —

我们加载了一个带有断裂的‘H’图像。

接下来,我们应用膨胀来修复断裂。

我们已经成功完成了任务。

2. 我们可以通过膨胀去除图像的内部噪声。

编码示例 —

加载带有内部噪声的图像。

在上述图像上应用膨胀后,我们可以轻松得到没有噪声的输出图像。

复合操作

还有一些其他的复合形态学操作。其中,**开操作和闭操作**是两种广泛使用的操作。下图一览无余地展示了这些操作。

图-6: 开操作和闭操作(作者提供的图片)[1]

开操作通过先进行腐蚀然后进行膨胀来完成。它去除对象上的连接,同时保持形状与主对象相同。见图 6。它有助于去除背景噪声[2]。

我们可以使用 OpenCV 应用开操作,语法如下。

opening = cv.morphologyEx(img, cv.MORPH_OPEN, kernel)

如果我们先对下一步应用膨胀和腐蚀,那么这个过程称为闭操作。这个过程在图-6 中展示。它有助于去除前景图像的噪声[2]。使用 OpenCV 进行闭操作的语法如下。

closing = cv.morphologyEx(img, cv.MORPH_CLOSE, kernel)

还有其他一些复合操作,例如形态学梯度、顶帽和黑帽。

结论

尽管形态学操作的过程很简单,但它可以用于边界提取、填充孔洞、检测食物中的异物、加厚图像对象、提取图像骨架、清除边界等。

如果你认真阅读这篇文章,我相信你将有足够的信心在解决实际问题时应用这些技术。

参考文献

  1. Gonzalez, R. C. (2009). 数字图像处理. Pearson education india.

  2. docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html

通过以下链接加入 Medium 支持我的写作。

mzh706.medium.com/membership

计算机视觉系列的其他文章 —

towardsdatascience.com/getting-started-with-numpy-and-opencv-for-computer-vision-555f88536f68?source=post_page-----513d162e7d05-------------------------------- [## 开始使用 NumPy 和 OpenCV 进行计算机视觉 (CV-01)

用 Python 开始你的计算机视觉编程

towardsdatascience.com/getting-started-with-numpy-and-opencv-for-computer-vision-555f88536f68?source=post_page-----513d162e7d05-------------------------------- towardsdatascience.com/how-color-is-represented-and-viewed-in-computer-vision-b1cc97681b68?source=post_page-----513d162e7d05-------------------------------- [## 计算机视觉中颜色表示的全面指南 (CV-02)

颜色空间和颜色模型的详细解释

towardsdatascience.com/how-color-is-represented-and-viewed-in-computer-vision-b1cc97681b68?source=post_page-----513d162e7d05-------------------------------- towardsdatascience.com/blend-images-and-create-watermark-with-opencv-d24381b81bd0?source=post_page-----513d162e7d05-------------------------------- [## 图像混合的最简单指南 (CV-03)

计算机视觉中图像混合和粘贴的最简单指南

towardsdatascience.com/blend-images-and-create-watermark-with-opencv-d24381b81bd0?source=post_page-----513d162e7d05-------------------------------- towardsdatascience.com/thresholding-a-way-to-make-images-more-visible-b3e314b5215c?source=post_page-----513d162e7d05-------------------------------- [## 阈值化 — 使图像更清晰的方式 (CV-04)

通过阈值化从图像中提取更多信息

towardsdatascience.com/thresholding-a-way-to-make-images-more-visible-b3e314b5215c?source=post_page-----513d162e7d05--------------------------------

激励自注意力

原文:towardsdatascience.com/motivating-self-attention-aead09a02f70?source=collection_archive---------2-----------------------#2023-06-10

为什么我们需要查询、键和值?

Ryan XuTowards Data Science Ryan Xu

·

关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 6 月 10 日

--

…自注意力?

本文的目标是提供一个解释,解释不是自注意力机制如何在变压器中工作,而是为什么它是这样设计的。

我们将从讨论我们希望语言理解模型具备的能力开始,接着进行自注意力机制的互动构建。在这个过程中,我们将发现为什么我们需要查询、键和值来以自然的方式建模单词之间的关系,并且 QKV 注意力是实现这一点的最简单方法之一。

本文对那些曾经接触过变压器和自注意力的读者将最有启发性,但应该对任何熟悉一些基础线性代数的人也能理解。对于那些希望更好地理解变压器的人,我会很高兴地推荐你们阅读这篇博客文章

所有图片均由作者提供。

变压器通常在序列到序列建模任务中呈现,如语言翻译或更明显的句子完成。然而,我认为从思考序列建模问题和具体的语言理解问题开始更容易。

所以,这里有一个我们想要理解的句子:

让我们稍微思考一下我们如何理解这个句子。

  • Evan 的狗 Riley… 从中我们知道 Riley 是狗的名字,而 Evan 拥有 Riley。

  • …如此超级… 简单来说,“hyper”指的是狗 Riley,影响了我们对 Riley 的印象。

  • …她从不停下 这很有趣。“她”指的是 Riley,因为狗是第一句话的主语。这告诉我们,Evan 的狗 Riley 实际上是母狗,这在之前由于狗名“Riley”常用性别中性而模糊不清。“从不停下”是一组稍复杂的词汇,进一步阐述了“hyper”。

关键是要建立对句子的理解,我们不断考虑词与词之间的关系来增强它们的含义。

在机器学习社区中,通过另一个词 b 的存在来增强词 a 的含义的过程被口语化地称为“a 关注 b”,即词 a 关注词 b

一个箭头 a => b 表示“a 关注 b

因此,如果我们希望机器学习模型理解语言,我们可能会合理地希望模型能够让一个词关注另一个词,并以某种方式更新其含义。

这正是我们希望在构建恰如其分地命名的(自)注意力机制的三个部分时模仿的能力。

接下来,我将提出一些斜体的问题。我强烈鼓励读者在继续之前停下来考虑这些问题一分钟。

第一部分

现在,让我们关注词“dog”和“Riley”之间的关系。词“dog”强烈影响词“Riley”的含义,因此我们希望“Riley”能够关注“dog”,因此这里的目标是以某种方式更新词“Riley”的含义。

为了使这个例子更具体,假设我们以每个词的向量表示开始,每个向量的长度为n,基于对词语的无上下文理解。我们将假设这个向量空间相当有序,即在空间中,意义更相似的词与更接*的向量相关联。

所以,我们有两个向量,v_dogv_Riley,它们捕捉了这两个词的意义。

我们如何使用 v_dog 来更新 v_Riley 的值,从而获得一个包含“狗”意义的新词“Riley”的值?

我们不想完全用v_dog 替代v_Riley 的值,所以我们可以将v_Rileyv_dog 的线性组合作为v_Riley 的新值:

v_Riley = get_value('Riley')
v_dog = get_value('dog')

ratio = .75
v_Riley = (ratio * v_Riley) + ((1-ratio) * v_dog)

这似乎还不错,我们已经将“狗”这个词的一部分意义嵌入到了“Riley”这个词中。

现在我们希望尝试将这种形式的注意力应用于整个句子,通过每个词的向量表示更新每个其他词的向量表示。

这里出了什么问题?

核心问题是我们不知道哪些词应该承担其他词的意义。我们还希望有一些衡量标准来确定每个词的值应该对其他词贡献多少。

第二部分

好的。所以我们需要知道两个词之间的相关程度。

进行第二次尝试。

我已经重新设计了我们的向量数据库,使每个词实际上都有两个关联向量。第一个是我们之前拥有的相同值向量,仍然用v 表示。此外,我们现在有单位向量,记作k,它们存储一些关于词语关系的概念。具体来说,如果两个k 向量接*,这意味着这些词的值可能会相互影响。

使用我们新的 k v 向量,我们如何修改之前的方案,以便用 v_dog 更新 v_Riley 的值,并且尊重两个词之间的关系?

我们继续使用之前的线性组合方法,但仅当这两个向量的 k 维接*时。更好的是,我们可以利用这两个 k 向量的点积(它们的范围是 0-1,因为它们是单位向量)来告诉我们应该如何用v_Riley 更新v_dog

v_Riley, v_dog = get_value('Riley'), get_value('dog')
k_Riley, k_dog = get_key('Riley'), get_key('dog')

relevance = k_Riley · k_dog # dot product

v_Riley = (relevance) * v_Riley + (1 - relevance) * v_dog

这有点奇怪,因为如果相关性为 1,v_Riley 会完全被v_dog 替代,但我们先忽略这一点。

我们接下来考虑将这种想法应用于整个序列。当我们通过k的点积来计算每个词的相关性值时,“Riley”将与其他每个词都有一个相关性值。因此,也许我们可以根据点积的值按比例更新每个词的值。为了简单起见,我们还可以将其与自身的点积包含在内,以保持其自身的值。

sentence = "Evan's dog Riley is so hyper, she never stops moving"
words = sentence.split()

# obtain a list of values
values = get_values(words)

# oh yeah, that's what k stands for by the way
keys = get_keys(words)

# get riley's relevance key
riley_index = words.index('Riley')
riley_key = keys[riley_index]

# generate relevance of "Riley" to each other word
relevances = [riley_key · key for key in keys] #still pretending python has ·

# normalize relevances to sum to 1
relevances /= sum(relevances)

# takes a linear combination of values, weighted by relevances
v_Riley = relevances · values

好了,目前为止这些就足够了。

但我再次声明,这种方法存在问题。并不是说我们的任何想法实现不正确,而是这种方法与我们实际思考单词之间关系的方式存在根本性差异。

如果在本文中有任何一点我 非常非常 认为你应该停下来思考,那就是这里。即使是那些认为自己完全理解注意力机制的人。我们的方法有什么问题?

一个提示

单词之间的关系本质上是非对称的!“Riley”关注“dog”的方式与“dog”关注“Riley”的方式不同。“Riley”指的是一只狗,而不是人类,这比狗的名字更重要。

相比之下,点积是对称操作,这意味着在我们当前的设置中,如果 a 关注 b,那么 b 也会同样强烈地关注 a!实际上,这有些不准确,因为我们在规范化相关性分数,但重点是单词应该有机会以非对称的方式进行关注,即使其他标记保持不变。

第三部分

我们快到了!最后,问题变成了:

我们如何最自然地扩展当前设置以允许非对称关系?

那么,我们可以用另一种向量类型做什么呢?我们仍然有值向量 v 和关系向量 k。现在每个标记还多了一个向量 q

我们如何修改我们的设置并使用 q 来实现我们想要的非对称关系?

那些熟悉自注意力工作原理的人可能会在这一点上露出会心的微笑。

当“dog”关注“Riley”时,我们计算相关性 k_dog · k_Riley,我们可以改为用 query q_Rileykey k_dog 的点积来进行计算。当反向计算时,我们将得到 q_dog · k_Riley —— 非对称相关性!

这就是整体内容,一次性计算每个值的更新!

sentence = "Evan's dog Riley is so hyper, she never stops moving"
words = sentence.split()
seq_len = len(words)

# obtain arrays of queries, keys, and values, each of shape (seq_len, n)
Q = array(get_queries(words))
K = array(get_keys(words))
V = array(get_values(words))

relevances = Q @ K.T
normalized_relevances = relevances / relevances.sum(axis=1)

new_V = normalized_relevances @ V

这基本上就是自注意力机制!

我遗漏了一些细节,但重要的概念都在这里。

总结一下,我们开始使用值向量 (v) 来表示每个单词的意义,但很快发现需要键向量 (k) 来考虑单词之间的关系。最后,为了正确建模单词关系的非对称性质,我们引入了查询向量 (q)。感觉如果我们只能使用点积等操作,3 是每个单词建模关系所需的最小向量数量。

本文的目的是以一种比传统算法优先方法更少让人感到压倒的方式揭示自注意力机制。我希望通过这种更具语言动机的视角,查询-键-值设计的优雅和简洁能够展现出来。

我遗漏的一些细节:

  • 我们不再为每个 token 存储 3 个向量,而是存储一个单一的嵌入向量,从中可以提取我们的 q-k-v 向量。提取过程只是一个线性投影。

  • 从技术上讲,在这个整体设置中,每个词都不知道其他词在句子中的位置。自注意力实际上是一种集合操作。因此,我们需要嵌入位置信息,通常是通过将位置向量添加到嵌入向量来完成的。这并不是完全简单的,因为 transformers 应该允许任意长度的序列。具体如何在实践中运作超出了本文的范围。

  • 单个自注意力层只能表示两个词之间的关系。但是通过组合自注意力层,我们可以建模更高级别的词之间关系。由于自注意力层的输出与原始序列的长度相同,这意味着我们可以对它们进行组合。实际上,transformer 模块就是由自注意力层和逐位置前馈模块组成。堆叠几百个这样的模块,花费几百万美元,你就拥有了一个 LLM!😃

OpenAI 显示出理解第二部分需要 512 个 transformer 模块。

Moto、Pytest 和 AWS 数据库:质量与数据工程的交汇点

原文:towardsdatascience.com/moto-pytest-and-aws-databases-a-quality-and-data-engineering-crossroads-ae58f9e7b265?source=collection_archive---------3-----------------------#2023-01-03

Moto 和 Pytest 如何与 AWS 数据库协同工作

Taylor WagnerTowards Data Science Taylor Wagner

·

关注 发布在 Towards Data Science ·9 分钟阅读·2023 年 1 月 3 日

--

图片由Christina @ wocintechchat.com提供,发布在Unsplash

概述

我最*花了很多时间使用 Pytest 来测试 AWS 服务,通过 Boto3。Boto3 是一个非常强大的 AWS SDK,用于处理 AWS 服务。我的研究和实践经验让我发现了一个叫做 Moto 的补充工具!Moto 与 Boto3 配合使用,成为我在最*项目中的测试策略规划以及维护干净生产数据的首选工具。在本文中,我将分享如何以及为何使用 Moto(一个虚假的 Boto3)和 Pytest 来模拟测试 AWS 数据库。我甚至会一步步带你通过使用 Moto 和 Pytest 测试 AWS NoSQL 数据库服务——Amazon DynamoDB 的过程。

请注意:除非另有说明,所有图像均由作者提供。

Moto

Moto是一个能够模拟 AWS 服务编程使用的 Python 库。简单来说 — Boto3是一个面向对象的 API,用于创建、配置和管理真实的 AWS 服务和账户,而Moto 是 Boto3 的模拟版本。虽然 Moto 没有包括 Boto3 提供的每一个方法,Moto 确实提供了相当一部分的 Boto3 方法,以便在本地运行测试。在使用 Moto 时,我强烈建议结合使用 Boto3 文档和 Moto 文档,以获得更详细的方法解释。

Moto 的优点:

  • 不需要 AWS 账户

  • 避免在 AWS 生产账户中更改实际数据或资源的风险

  • 可轻松将 Moto 模拟设置转换为 Boto3 以用于实际应用场景

  • 测试运行迅速 — 无延迟问题

  • 易于学习和上手

  • 免费!— 不会产生 AWS 费用

Moto 的一个缺点:

  • 不包括 Boto3 的所有方法 — Boto3 提供了对 AWS 服务的更广泛覆盖

Moto 文档截图:可用于 Amazon DynamoDB 的 Boto3 方法的完整列表,并标记了 Moto “X”可用的方法。欲查看完整列表,请点击这里

Moto 文档提供了哪些 Boto3 方法可以使用的洞察。例如,Boto3 中略多于一半的DynamoDB 方法可用于 Moto 进行模拟。

Pytest

Pytest是一个用于编写简洁易读测试的 Python 框架。此框架使用详细的断言检查,使得普通的assert语句非常用户友好。Pytest 具有强大的功能,可以扩展以支持应用程序和库的复杂功能测试。

终端截图 1:执行pytest命令

测试可以通过一个简单的pytest命令来执行。pytest命令会运行所有测试,但可以通过在pytest后面添加文件路径来运行单个测试文件,如:pytest tests/test_dynamodb.py。运行pytest会给出准确的结果,每个测试会显示绿色的“.”表示通过,红色的“F”表示失败,或黄色的“S”表示跳过(如适用)。有两个非常有用的标志我将分享并强烈建议在使用 Pytest 执行测试时使用。请注意:在本节的屏幕截图中,这些不同命令运行之间的代码没有任何变化。唯一的区别是标志。

终端截图 2:使用-s 标志执行pytest命令

首先是-s(stdout/stderr 输出)标志。运行pytest -s会在测试运行时在终端显示所有打印语句。如果没有添加-s标志,即使代码中有打印语句,终端中也不会出现打印语句。这对于调试非常有用。

终端截图 3:使用-v 标志执行pytest命令

另一个有用的标志是-v(详细)标志。使用pytest -v会在测试运行时在终端提供更多详细信息。这个标志将提供每个测试的类和方法名称,以及所有待运行测试的每个测试完成的累积百分比,以及每个测试的绿色“通过”、红色“失败”或黄色“跳过”指示器。注意,这个标志不会显示打印语句。

终端截图 4:使用组合的-sv 标志执行pytest命令

专业提示:在运行测试执行命令时,可以组合标志!在使用 Pytest 时,我通常运行pytest -sv-s-v标志的组合提供了更具可读性的细节,并且对打印语句有更多的洞察力。

开始使用

Pytest 要求使用 Python3.7 或更高版本。您可以在这里下载最新版本的 Python。然后,需要三个库才能开始。打开您选择的 IDE,在一个全新的目录中,运行以下终端命令以下载所需的依赖项:

pip install boto3 moto pytest

接下来,我们设置“客户端连接”。设置 Moto 连接的过程与 Boto3 非常相似。对于 Boto3,要求提供 AWS 凭证。为了使用 Moto 模拟这个过程,我们将使用虚假的凭证。可以随意设置值,只要类型是字符串即可。

请注意:在实际使用中,保护机密凭证值非常重要,不应将其硬编码到应用程序中。可以通过命令行导出变量或将其保存在环境文件中以限制访问性。 此外,任何区域都可以用于连接,但“us-east-1”通常用于此区域,因为该区域包括所有 AWS 服务和产品。

Pytest 在名为 conftest.py 的文件中利用夹具在模块之间共享。在你的应用程序根目录中,通过运行以下命令创建该文件:

touch conftest.py

并添加以下内容以配置客户端连接:

import boto3
import os
import pytest

from moto import mock_dynamodb

@pytest.fixture
def aws_credentials():
    """Mocked AWS Credentials for moto."""
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"

@pytest.fixture
def dynamodb_client(aws_credentials):
    """DynamoDB mock client."""
    with mock_dynamodb():
        conn = boto3.client("dynamodb", region_name="us-east-1")
        yield conn

AWS 凭证函数和模拟 DynamoDB 客户端函数都实现了 Pytest 夹具装饰器,以便可以在应用程序的其他文件中使用。模拟 DynamoDB 客户端函数使用 Moto 创建与 AWS 的虚假客户端连接,类似于在 Boto3 中的操作。为了本文的目的,我们将以 DynamoDB 为示例 AWS 服务尝试 Moto。如果你有兴趣将 Moto 用于其他 AWS 服务,请查看 Moto 文档。

使用 Moto 测试 DynamoDB

使用以下命令在项目根目录中创建另一个名为 test_dynamodb.py 的文件:

touch test_dynamodb.py

测试文件的第一步是创建一个 DynamoDB 表。由于 Moto 中的许多方法需要已经创建的表,我们将创建一个非常基础的测试表,在使用各种方法时会用到,以避免在每个测试用例中都需要创建一个表。创建此测试表有几种策略,但我将向你展示如何通过实现一个上下文管理器来创建该表。好消息是,上下文管理器来自 Python 的本地库,无需额外下载软件包。测试表可以这样创建:

在单个函数中使用上下文管理器创建测试表后,我们可以继续测试这个测试表是否确实已创建。测试表的表名将是许多 Moto DynamoDB 方法所需的参数,因此我会将其创建为整个测试类的变量,以避免重复并提高可读性。此步骤不是必需的,但有助于节省时间,并减少因在调用不同方法时需要不断重新编写表名值而产生的语法错误的机会。

在相同的 test_dynamodb.py 文件中,在上下文管理器下方创建一个测试类,以便将所有相关测试(“my-test-table”的测试)分组在一起。

如上所示,dynamodb_client 参数作为参数传递给测试方法。这来自于 conftest.py 文件,在该文件中我们已经建立了与 Amazon DynamoDB 的模拟连接。第 4 行显示了表名变量的可选声明。然后在第 9 行,我们调用上下文管理器进入 test_create_table 方法,以便访问“my-test-table”。

从那里,我们可以编写一个测试来验证表是否成功创建,通过调用 Moto 的 .describe_table().list_tables() 方法,并断言表名是否存在于响应输出中。要运行测试,只需执行以下命令(如果需要,您可以添加额外的标志):

pytest

这个命令应该会返回测试通过的结果:

终端截图 5:执行 pytest 命令以测试表的创建

使用 Moto 进一步测试 DynamoDB

现在,您的测试文件已准备好进行任何额外的 DynamoDB 测试方法,您可以使用 Moto 探索,例如向“my-test-table”添加项:

你看到的测试可以直接放在 TestDynamoDB 类中 test_create_table 方法下。test_put_item 方法也使用了上下文管理器,以使用最初在文件顶部创建的相同测试表。在每个方法开始时,以及在每个测试套件中调用上下文管理器时,“my-test-table” 都处于初次创建时的原始状态。请注意:状态在测试方法之间不会持久化。

我创建了一个 GitHub 存储库,其中包含了本文中的示例测试,并且还增加了更多内容!我的示例代码扩展了文章中的测试,包括以下 DynamoDB 测试:

  • 创建表

  • 将项添加到表中

  • 从表中删除项

  • 删除表

  • 添加表标签

  • 移除表标签

[## GitHub - taylorwagner/moto-aws-data

通过在 GitHub 上创建帐户来贡献 taylorwagner/moto-aws-data 的开发。

github.com](https://github.com/taylorwagner/moto-aws-data?source=post_page-----ae58f9e7b265--------------------------------)

这个存储库还包括一些 AWS RDS 的测试。在我的示例代码中,我将测试文件组织到自己的目录中,并对测试进行了分组以确保测试的一致性。我挑选了每个服务的不同方法来展示 Moto 提供的范围。Moto 还有许多其他方法未包含在我的示例代码中。请务必查看 Moto 的文档!

最终想法

将 Pytest 固件和 Python 上下文管理器结合使用,Pytest 的易用性和可扩展性框架与 Moto 的结合是验证 AWS 数据库服务使用情况的绝佳组合。尽管 Moto/Pytest 的组合可能不适合测试真实的 AWS 服务和账户,但它是练习和更好地理解如何测试真实 AWS 账户的足够选项,且不会带来安全漏洞、篡改重要数据或产生昂贵的、不必要的账单。只需记住,与 Boto3 相比,Moto 的方法范围有限。我发现,与 Moto 的模拟响应一起工作会为你提供利用 Boto3 处理真实 AWS 用例所需的理解。我希望我的文章和示例代码能帮助你满足 AWS 数据库的需求。

资源

  • Moto 的 GitHub 仓库:

[## GitHub - spulec/moto: 一个允许你轻松模拟基于 AWS 的测试的库…]

Moto 是一个库,使你的测试可以轻松模拟 AWS 服务。假设你有以下的 Python 代码…

GitHub 页面

  • 我的示例代码托管在 GitHub 上。你可以查看测试并在你的机器上运行。我还包括了 AWS RDS 测试:

[## GitHub - taylorwagner/moto-aws-data]

通过在 GitHub 上创建账户来为 taylorwagner/moto-aws-data 的开发做出贡献。

GitHub 页面

使用 Neo4j 的电影推荐

原文:towardsdatascience.com/movie-recommendations-with-neo4j-adaad7c9bf2b

使用 Python 和 Neo4j 构建一个简单的电影推荐系统

Dimitris PanagopoulosTowards Data Science Dimitris Panagopoulos

·发布在 Towards Data Science ·阅读时间 7 分钟·2023 年 2 月 19 日

--

图片由作者使用稳定扩散和bytexd.com/get-started-with-stable-diffusion-google-colab-for-ai-generated-art/中的代码创建

介绍

创建推荐是机器学习的一个常见用例。在这篇文章中,我们将演示如何使用图数据库创建一个简单的电影推荐系统。所提出的方法不是最先进的。但使用图数据库的实现简单且易于解释。它们可以成为一个简单推荐系统的起点,该系统可以快速提供结果和/或作为评估更复杂系统的基准。

如果读者希望进行实验,他/她可以使用 Neo4j 的沙盒Google 的 Colab 在一两分钟内准备好系统。对于本文,我们将使用 GroupLens.org(即 “1M 数据集”)中的数据。我们还将使用一个小数据集来创建一个包含少量节点的最小图,以便可以轻松检查计算。所有用于最小图的代码和数据可以在作者的 GitHub 中找到。

请注意:

  1. Neo4j Graph Data Science 插件应安装在 Neo4j 中(它已在 Neo4j 的沙盒中安装)

  2. 在 Python 中,应安装“neo4j-driver”和“graphdatascience”库。

要在(2)中安装 Python 库,可以使用 pip

!pip install neo4j-driver
!pip install graphdatascience

连接到 Neo4j

在加载必要的库后,第一步是连接到 Neo4j。可以使用以下代码片段完成此操作

DB_ULR = 'bolt://xxxxx:xxxx'
DB_USER = 'neo4j'
DB_PASS = 'xxxxx'
gds = GraphDataScience(DB_ULR, auth=(DB_USER, DB_PASS))

如果我们使用 Neo4j 的沙盒,我们可以在“通过驱动程序连接”选项卡中找到 URL 和密码。

Neo4j 沙盒的连接详细信息(作者提供的 Neo4j 沙盒屏幕截图)

将数据加载到 Neo4j

如介绍中所提到的,我们将使用电影评分数据。特别是,我们将使用 MovieLens 1M 数据集。该数据集包含来自 6000 部电影的 1 百万条评分。它由三个独立的文本文件组成:

  1. movies.dat:电影数据,格式为 MovieID::Title::Genres

  2. users.dat:用户数据,格式为 UserID::Gender::Age::Occupation::Zip-code

  3. ratings.dat:评分数据,格式为 UserID::MovieID::Rating::Timestamp

电影、用户和评分文本文件的前五行(图像由作者提供)

我们将创建两种类型的节点。一种代表用户,另一种代表电影。我们还将创建用户节点和电影节点之间的关系,以表示用户对电影的评分。作为该关系的属性,我们将使用评分分数。图数据库的模式如下所示。

图数据库的模式(作者提供的屏幕截图)

使用图数据科学库将 pandas 数据框加载到 Neo4j 是相当简单的。例如,下面的代码加载 users.dat

gds.run_cypher('create constraint if not exists for (n:User) require (n.id) is node key')
create_customer_res = gds.run_cypher('''
    unwind $data as row
    merge (n:User{id: row.UserID})
    set n.Gender = row.Gender
    set n.Age =  row.Age 
    return count(*) as custmers_created
''', params = {'data': users.to_dict('records')}) 

Ratings.dat 文件相当大,不能一次性加载。因此,我们需要将数据框分割并分块加载。

for chunk in np.array_split(ratings,200):
  if i%10 == 0:
    print(i)
  create_rated = gds.run_cypher('''
    unwind $data as row
    match (u:User{id: row.UserID}), (m:Movie{id: row.MovieID})
    merge (u)-[r:RATED]->(m)
    set r.Rating = row.Rating
    return count(*) as create_rated
    ''', params = {'data': chunk.to_dict('records')})
  i = i+1

最小示例图

为了帮助读者理解我们将要使用的方法,我们将使用以下最小图作为示例。它包含:

  • 三个用户节点,编号为 1、2 和 3

  • 四个电影节点

  • 七条评分关系,括号中可以看到实际评分

最小示例图(图像由作者提供)

使用 cypher 查找相似电影

使用 cypher,找到与给定电影相似的电影非常简单。给定一部电影 m1,可以找到所有以最高评分(5)对其进行评分的用户,然后返回这些用户也评分为优秀的其他所有电影。通过计算连接 m1 和每部其他电影的路径数量,我们可以计算相似度得分。

例如,查找与“玩具总动员(1995)”相似电影的 cypher 查询如下。

# Check similar movies
similar_movies = gds.run_cypher('''
    MATCH(m1:Movie)-[r1]-(u:User)-[r2]-(m2:Movie)
    WHERE m1.Title CONTAINS 'Toy Story (1995)'
      AND m2.Title<>'Toy Story (1995)'
      AND r1.Rating=5 AND r2.Rating=5
    RETURN m2.Title,m2.Genres,count(DISTINCT(u)) as number_of_paths
    ORDER BY common_users DESC
''')
similar_movies.head()

在我们的最小图示例中,这将返回“勇敢者游戏(1995)”,它与“玩具总动员(1995)”通过两条路径连接。一条路径经过用户 1,另一条路径经过用户 2。

当我们使用我们常规的包含一百万条评分的图时,与“玩具总动员(1995)”最相似的五部电影是

  1. 《星球大战:第四集 — 新希望(1977)》

  2. 《玩具总动员 2(1999)》

  3. 《夺宝奇兵(1981)》

  4. 《星球大战:第五集 — 帝国反击战……》

  5. 《肖申克的救赎(1994)》

而对于“黑客帝国(1999)”,前五名是

  1. 《星球大战:第四集 — 新希望(1977)》

  2. 星球大战:帝国反击战……

  3. 夺宝奇兵(1981)

  4. 美国美人(1999)

  5. 第六感(1999)

一些读者可能对这些结果感到复杂,这是可以理解的。使用这种方法时,受欢迎的电影和高评分的电影往往占据主导地位。事实是,一百万条评分还不足以建立一个推荐系统。使用 MovieLens 和各种推荐方法的经验表明,增加评分数量可以改善推荐。然而,应该注意的是,我们仅通过简单查询就能利用图找到相似的电影。更复杂的方法是使用类似于下一节描述的查找相似用户的方法。

向用户推荐电影

使用 Neo4j,我们可以应用协同过滤来向用户推荐电影。协同过滤方法的高层次描述是,向用户推荐新电影的过程分为两个步骤:

  1. 我们找到与我们的用户相似的用户,

  2. 我们使用步骤(1)中找到的用户评分来建议新电影。

计算用户相似度

我们将使用 Jaccard 相似度来检测相似用户。在我们的图论设置中,两个节点之间的 Jaccard 相似度是它们都连接的节点数量与连接到至少一个节点的节点数量的比率(不包括我们计算相似度的两个节点)。

在我们的最简图示例中,用户节点 1 和 2:

  • 都连接到“玩具总动员(1995)”和“勇敢的心(1995)”

  • 连接到“玩具总动员(1995)”、“勇敢的心(1995)”和“呼吸等待(1995)”

因此,用户 1 和用户 2 的 Jaccard 相似度为 2/3。

同样,用户 1 和用户 3:

  • 都连接到“玩具总动员(1995)”

  • 连接到“玩具总动员(1995)”、“勇敢的心(1995)”、“呼吸等待(1995)”和“黄金眼(1995)”

因此用户 1 和用户 3 的 Jaccard 相似度为 1/4。

Neo4j 的图数据科学库可以计算 Jaccard 相似度。首先,我们需要创建一个子图(或者 Neo4j 称之为投影),包含我们在计算 Jaccard 相似度时需要考虑的节点和关系。

# Create projection
create_projection = gds.run_cypher('''
   CALL gds.graph.project(
    'myGraph',
    ['User', 'Movie'],
    {
        RATED: {properties:  'Rating'}
            } 
);
''')

然后,我们计算 Jaccard 相似度并将结果存储在 pandas 数据框中。

# Get user similarity
users_similarity = gds.run_cypher('''
CALL gds.nodeSimilarity.stream('myGraph')
YIELD node1, node2, similarity
RETURN gds.util.asNode(node1).id AS UserID1, gds.util.asNode(node2).id AS UserID2, similarity
ORDER BY similarity DESCENDING, UserID1, UserID2
''')

pandas 的前五行包含我们最简图示例图的用户相似度(图像由作者提供)

最后,我们在用户节点之间创建一个新的关系,作为它们之间计算出的相似度的属性。

# Create Similar relationship
i=1
for chunk in np.array_split(users_similarity.query('UserID1>UserID2'),10):
  print(i)
  create_similar = gds.run_cypher('''
    unwind $data as row
    match (u1:User{id: row.UserID1}), (u2:User{id: row.UserID2})
    merge (u1)-[r:SIMILAR]->(u2)
    set r.Similarity=row.similarity
    return count(*) as create_rated
    ''', params = {'data': chunk.to_dict('records')})
  i = i+1

为了向用户(user1)推荐电影,我们计算用户尚未评分的电影的排名,使用其他用户观看过的电影的加权*均评分,其中权重为 Jaccard 相似度。

计算加权*均评分的公式(图像由作者提供)

我们还添加了连接用户(user1)到电影的路径数量的对数。这是因为我们想要提升与(user1)连接的有多个用户的电影。对应的 cypher 查询是

# Check similar movies
similar_movies_for_user = gds.run_cypher('''
    MATCH (u1:User)-[r1:SIMILAR]-(u2)-[r2:RATED]-(m:Movie)
    WHERE id(u1)=$id
    AND NOT ( (u1)-[]-(m))
    RETURN m.Title,m.Genres,Sum(r1.Similarity*r2.Rating)/sum(r1.Similarity)+log(count(r2)) as score
    ORDER BY score DESC
''',params = {'id':2})

对于我们的最小图示例,用户 3 的结果是:

  • 《勇敢者游戏》(1995 年),评分为 5.69

  • 《呼吸等待》(1995 年),评分为 3.00

用户的前 10 部电影推荐(左侧)和用户的前 10 部评分最高的电影(右侧)(图片由作者提供)

结论

希望这篇文章已经展示了使用图数据库来快速创建推荐引擎的好处。虽然这不是最先进的技术,但它易于实现和维护。作为附加 bonus,我希望这篇文章也提供了一些将 Python 与 Neo4j 结合的有用技巧。

使用的数据集引用:

F. Maxwell Harper 和 Joseph A. Konstan. 2015. 《MovieLens 数据集:历史与背景》。ACM 互动智能系统交易(TiiS)5, 4, 文章 19(2015 年 12 月),19 页。DOI=dx.doi.org/10.1145/2827872

多臂老丨虎丨机应用于执行算法中的订单分配

原文:towardsdatascience.com/multi-armed-bandits-applied-to-order-allocation-among-execution-algorithms-fff21dedc927?source=collection_archive---------10-----------------------#2023-03-02

找到利用探索之间的正确*衡

Lars ter BraakTowards Data Science Lars ter Braak

·

关注 发表在 Towards Data Science · 6 min 阅读 · 2023 年 3 月 2 日

--

困惑的机器人以毕加索风格观察三台单臂老丨虎丨机。来源:DALL-E 2。

在不确定性下做出决策是各个领域的专业人士面临的常见挑战,包括数据科学和资产管理。资产经理在选择多个执行算法来完成交易时会遇到这个问题。算法之间的订单分配类似于赌徒面临的多臂老丨虎丨机问题,他们必须决定玩每台老丨虎丨机的次数、顺序以及是否继续当前机器或切换到另一台。在这篇文章中,我们描述了资产经理如何根据实际执行成本最佳地分配订单到可用算法中。

示例

对于每个订单,我们采取行动a,将其分配给K个算法中的一个

Eq. 1: 分配订单到 K 个算法中的一个的可能行动集合。

行动a的价值是该算法的预期执行成本

Eq. 2: (未观察的) 行动 a 的预期执行成本,即选择某个算法。

假设K = 3,并且算法的预期执行成本为

Eq. 3: (未观察的) 三个算法的预期执行成本。

如果你事先知道行动值,解决问题会非常简单。你将始终选择预期执行成本最低的算法。现在假设我们开始按照图 1 中所示在三个算法之间分配订单。

图 1: 在三个算法之间分配订单及其相关执行成本的示例。来源:作者。

我们仍然不确定行动值,但在一段时间t后,我们有了估计:

Eq. 4: (观察的) 基于截至时间 t 的信息,行动 a 的预期执行成本。

例如,我们可以构建每个算法的执行成本¹的经验分布,如图 2 所示。

图 2: 一段时间 t 后每个算法的执行成本的经验分布。来源:作者。

将所有订单分配给预期执行成本最低的算法可能看起来是最佳方法。然而,这样做会阻止我们收集其他算法性能的信息。这体现了经典的多臂老丨虎丨机困境:

  • 利用已经学到的信息

  • 探索以了解哪些行动能获得最佳结果

目标是最小化*均执行成本,在分配P个订单之后:

Eq. 5: 订单分配问题的目标函数。

使用策略解决问题

为了解决这个问题,我们需要一个动作选择策略,它告诉我们如何基于当前信息S分配每个订单。我们可以将策略定义为从Sa的映射:

Eq. 6: 动作选择策略的定义。

我们讨论了最著名的多臂赌博机问题策略²,这些策略可以分为以下几类:

  • 半均匀策略: 贪婪 & ε-贪婪

  • 概率匹配策略: 上置信界限 & 汤普森采样

贪婪

贪婪方法将所有订单分配给估计值最低的动作。该策略总是利用当前知识以最大化即时奖励:

Eq. 7: 贪婪方法的动作选择策略。

ϵ-贪婪

ε-贪婪方法大多数时候表现得很贪婪,但以概率ε随机选择次优动作:

Eq. 8: ϵ-贪婪方法的动作选择策略。

该策略的一个优点是,它在极限情况下会收敛到最优动作。

上置信界限

上置信界限(UCB)方法选择具有最低动作值减去一个与交易算法使用次数成反比的项,即Nt(a)。该方法在非贪婪动作中选择,依据其实际最优潜力及这些估计的相关不确定性:

Eq. 9: 上置信界限(UCB)方法的动作选择策略。

汤普森采样

汤普森采样方法,如汤普森(1933)所提,假设对动作值有一个已知的初始分布,并在每次订单分配后更新分布。该方法根据动作的后验概率选择最佳动作:

Eq. 10: 汤普森采样方法的动作选择策略。

策略评估

在实际应用中,策略通常通过遗憾来评估,遗憾是与最优解的偏差:

Eq. 11: 遗憾作为一系列动作的函数的定义。

其中μ是最小执行成本均值:

Eq. 12: 选择最优动作的期望执行成本。

动作是策略的直接结果,因此我们也可以将遗憾定义为所选策略的函数:

Eq. 13: 遗憾作为动作选择策略π的函数的定义。

在图 3 中,我们在示例中模拟了上述策略的遗憾。我们观察到上置信界限方法汤普森采样方法表现最佳。

图 3:针对虚拟订单分配问题的不同动作选择策略的模拟遗憾。来源:作者。

订单分配?拥抱不确定性!

虚拟示例模拟结果强烈表明,单纯依赖贪婪方法可能无法获得最佳结果。因此,在制定订单分配策略时,融入并衡量执行成本估算的不确定性至关重要。

脚注

¹ 为确保执行成本的经验分布的可比性,我们需要分配类似的订单或使用无关订单的成本指标进行评估。

² 在算法的执行成本依赖于顺序特征的情况下,上下文强盗算法是更合适的选择。要了解更多关于这种方法的信息,我们推荐 Barto & Sutton (2018) 的第 2.9 章作为入门。

³ 我们强烈建议阅读 Russo 等 (2018) 作为学习 Thompson 采样的杰出资源。

额外资源

以下教程 / 讲座对我理解多臂强盗问题非常有帮助。

行业

  1. 研究科学家 Robert Schapire @ Microsoft

  2. 研究科学家 Hado van Hasselt @ Deepmind

学术界

  1. 助理教授 Christina Lee Yu @ Cornell

  2. 助理教授 Emma Brunskill @ MIT

参考文献

[1] Sutton, R. S., & Barto, A. G. (2018). 强化学习:导论。麻省理工学院出版社

[2] Russo, D. J., Van Roy, B., Kazerouni, A., Osband, I., & Wen, Z. (2018). 关于 Thompson 采样的教程。机器学习基础与趋势®11(1),1–96。

[3] Thompson, W. 1933. 在两个样本的证据下,一个未知概率超过另一个的可能性。生物统计学。25(3/4): 285–294。

[4] Thompson, W. R. 1935. 关于配额理论。美国数学杂志。57(2): 450–456。

[5] Eckles, D. 和 M. Kaptein. 2014. 在线自助法的 Thompson 采样。arXiv 预印本 arXiv:1410.4009

如果你对更多内容感兴趣,可以查看我下面的一些文章:

[## VWAP 执行算法的成本分解:买方视角

用于 VWAP 执行算法的线性成本分解,允许更快且更细致的算法交易……

medium.com ## 引言:概率分类的机器学习视角

从预测标签到预测概率的指南

towardsdatascience.com [## 超越传统资产回报建模:拥抱厚尾。

针对预渐*性的统计推断指南。

medium.com

多维探索是可能的!

原文:towardsdatascience.com/multi-dimensional-exploration-is-possible-212b99171706

(至少在数学上)

Diego ManfreTowards Data Science Diego Manfre

·发表于 Towards Data Science ·13 分钟阅读·2023 年 10 月 5 日

--

图片由作者使用Midjourney制作

探索多维世界是科幻故事和电影中的一个常见主题。虽然穿越这些世界仍然是一种幻想,但数学可以帮助我们接*如此荒诞的想法。如果你曾经想过,“又一天没有计算矩阵的行列式”,那就请稍等。线性代数可能比你想象的更接*现实!在你的手机、电脑和流媒体应用中,复杂到简单的数据结构的转换每天都在发生。这些操作是真实的、数学上的多维门户。本文解释了主成分分析—它是什么,为什么重要,以及它是如何工作的。

Carl Sagan 解释了……

有一个视频,由Carl Sagan在 YouTube 上讲解,他解释了来自高维世界的访客会如何看待他们的低维对等物。这个美丽的课程源自 Sagan 著名的系列节目宇宙。正如他节目中的许多摘录一样,Sagan 巧妙地解释了在“*面世界”中的二维个体如何体验来自三维苹果的访问。实际上,从高维到低维空间的数学并不超出线性代数课程的内容。正如 Sagan 在视频结束时所说的,“虽然我们无法想象四维世界,但我们完全可以思考它。”

一个类似于卡尔·萨根在视频中展示的苹果投影。图片由作者使用MidJourney制作。

投影到 2D 空间中的苹果可能看起来不像真正的苹果,但它可以给我们苹果的形状和大小的概念。下面的图片展示了一个苹果和一个胡萝卜投影到 2 维表面上的情况。我们可能仅通过任何一个投影就能识别出苹果。另一方面,可能很难意识到下方的投影对应的是一个胡萝卜。这意味着每次我们将高维对象投影到低维空间时,我们都会丢失一些信息。然而,这些投影帮助我们思考一个高维对象在我们世界中的样子。

图 2. 胡萝卜和苹果的 2D 投影。图片由作者制作。

根据我们用来投影对象的*面不同,我们会对它们的形状和特征有不同的理解。因此,虽然投影可以帮助我们思考更高的维度,但投影的方式对我们的理解至关重要。再看看胡萝卜的下方投影。仅通过这个投影,有办法知道这就是胡萝卜吗?可能没有。选择其他两个投影中的任何一个可能更好。但是,我们如何选择它们呢?如果我们试图思考一个 10 维的对象,我们如何选择出我们的大脑能够处理的最佳三个维度?

超越苹果和胡萝卜

要理解这一点,我们首先需要意识到,多维空间不仅与胡萝卜和苹果的世界相关。每个维度可以表示数据点集合中的某个特定参数或特征。如果我们取一组人并测量他们的五个特征,我们可以将这些个体绘制在一个 5 维空间中。再次强调,我们无法想象这个空间,但我们可以通过将这些 5 维点投影到一个我们可以可视化的 3 维或 2 维空间来思考它。在下面的示例中,生活在 2 维世界中的人只能看到投影到下方(xy)*面的三个球体。否则,他们只能看到两个,这并不是很好地思考这组 3 维球体的方式。

图 3. 空间中点的 2D 投影。图片由作者制作。

投影数据到最佳维度的问题可以通过主成分分析 (PCA)来解决。这个名字可能看起来与我们目前讨论的内容无关,但它源于我们在此分析中寻找数据集的主要成分。在这种情况下,成分意味着可以投影原始数据的维度或*面。PCA 如何找到最佳成分或维度?它查看每对参数的变异性。差异越大,对其真实行为的良好投影就越容易。我们无法识别胡萝卜的原因是,因为我们无法看到胡萝卜的主要特征。通过查看这种投影,我们无法知道它是长的、底部较细以及顶部有一些绿色叶子。我们只看到一个圆形的模糊物体,可能代表其他任何东西。PCA 帮助我们以一种可以看到主要特征的方式观察一个物体或数据点组。

降维投影

矩阵乘法是一种将点投影到低维空间的方法。这意味着,从数学上讲,从 3 维降到 2 维,或者从 10 维降到 2 维。下图展示了一组二维中的点如何被投影到一维线上。此图取自一个包含有关 PCA 的额外细节的Python 笔记本。图中的线可以在任何地方定义。将原始点与包含定义每条线的单位向量的矩阵相乘,可以得到原始点在这些线上的投影。因此,红色和绿色的交叉点表示来自二维世界的蓝色点在一维中会是什么样子。注意红色交叉点的变化比绿色交叉点的变化更大。这意味着在绿色交叉点的世界中,原始点看起来不会像在红色交叉点的世界中那样有所不同。这里要注意的主要思想是,即使我们有方法在不同的维度之间移动(至少在数学上是这样!),我们如何投影原始数据会影响我们观察和思考高维对象的方式。

图 4. 将二维中的点投影到一维线上的示意图。图像由作者制作。

在一个类似于我们在图片中看到的胡萝卜和苹果的例子中,我们可以想象一组 3 维的点及其在不同*面上的投影。再次根据我们选择的投影*面,我们将得到不同的结果。这时 PCA 就派上用场了。通过 PCA,我们可以定义最佳的投影组合,从而观察到最大的变异。因此,PCA 帮助我们获得一个与红色交叉点更相似的投影,而不是绿色交叉点,这反过来会更好地代表在更高维空间中的点集。

PCA 依赖于许多你可能在线性代数课程中学习过的内容。应用 PCA 的步骤可以总结如下:

协方差矩阵

生成一个协方差矩阵:协方差测量两个变量之间关系的强度。如果我们有一组属于 10 维空间的点,那么我们将会有 45 种不同的维度对组合。如果我们计算这些组合的方差,就可以了解两个变量之间关系的方向和强度。这个计算结果写在协方差矩阵中,如图片所示。注意每个值出现了两次,因为每对变量在矩阵中会出现两次。请注意,在计算协方差之前,数据通常会被标准化,因为每个维度可能包含不同量纲的值。具有较大量纲的变量会主导主成分。标准化数据可以确保所有变量具有相同的量纲,从而使 PCA 对每个变量给予相等的权重。

图 5. 协方差矩阵。图像由作者制作。

特征向量和特征值

计算协方差矩阵的特征向量和特征值:是的!你可能在某次线性代数讲座中听到过这个名字!而且,是的!这可能听起来非常陌生!然而,理解的关键在于特征向量和特征值告诉我们关于特定变换的一些特殊信息。记住矩阵乘法本质上是一个变换,我们通过求和和乘法来改变一些点。特征向量代表一个特定的方向,在执行变换后方向保持不变。另一方面,特征值代表特征向量在变换过程中被拉伸或压缩的程度,而方向不变。因此,特征向量和特征值非常重要,因为它们告诉我们在变换后哪些方向保持不变。

下图显示了对同一组数据应用的三种不同线性变换。这种线性变换是水*剪切。在图中显示的所有情况下,特征向量始终是相同的:(1, 0)。这意味着任何位于单位向量(1,0)上的点在变换后将保持不变。注意点(1,0)和(-1,0)没有被变换改变,任何位于特征向量(1,0)上的点,如(0.25, 0),(-0.75, 0)等,也没有被改变。

图 6. 特征向量的图示解释。图片由作者制作。

转换

使用特征向量将原始数据转换到较低维空间。特征值会告诉你哪些特征向量最重要,应该投影到这些向量上。因此,在像图 4 所示的图中,红色交叉点对应的特征向量的特征值高于绿色交叉点的特征向量。这是因为红色交叉点的变异性大于绿色交叉点。如果我们想从一维视角对二维点有一个良好的了解,使用第一个向量来转换点比使用第二个向量更好。你可以看到,一个对应于两个维度的(nx2)数组如果乘以形状为(2x1)的特征向量,就会被转换为一维。如果我们想将一个 10 维的原始数据集转换为 3 维或 2 维,我们可以遵循相同的过程。

投影

通常,将转换后的点投影到原始维度上是有用的。对于这个例子,这意味着将转换后的 1D 点重新绘制到 2D 空间中,这就是图 4 所示的内容。要将点投影到原始维度上,你可以将转换后的数组乘以特征向量数组的转置。在这种情况下,这意味着将(nx1)乘以(1x2)。这将得到图 7 所示的(nx2)数组。在这个图中,特征向量 1 和特征向量 2 的特征值分别为 39.2 和 5.3。这意味着特征向量 1 比特征向量 2 代表了更好的投影,可以从图中看到,因为红色交叉点的分布比绿色交叉点的分布更大。

图 7. 二维点投影到主成分上。图片由作者制作。

数值胡萝卜

让我们将之前的步骤应用到一个类似于本文开头展示的问题中。下图展示了一个 3D 点分布,看起来像胡萝卜(有关如何构建这个胡萝卜的更多信息,请参见这个Python 笔记本)。它还展示了每个*面的投影。我们可以看到,观察 XY *面上的投影的人会发现很难意识到这些点实际上代表了胡萝卜,而 ZX 和*面 Z 的投影有更好的表示效果。因此,在这种情况下,如果我们想在 2D 中可视化这个 3D 对象,最好将其投影到 ZX 或 YZ *面上。

图 8. 旋转胡萝卜形状的 3D 点投影到不同*面上。图像由作者制作。

如果最佳投影*面不是那么明显呢?假设现在我们要投影的点群如下图所示。注意这与之前的胡萝卜相同,但已旋转。如果我们将这些点投影到之前看到的三个*面上,我们将得到胡萝卜的旋转版本。最佳投影是通过胡萝卜中间的*面,而这正是我们可以通过对原始点应用 PCA 来获得的投影。通过找到点之间变化最大的方向,PCA 可以帮助我们在高维空间中可视化数据点。尽管 2D 胡萝卜图像与 3D 胡萝卜不完全相同,但它仍然是一个很好的表示,胜过其他任何*面。有关如何使用 scikit-learn 在 Python 中应用 PCA 的更多信息,请查看这个 Python 笔记本

图 9. 旋转胡萝卜形状的 3D 点投影到不同*面上。图像由作者制作。

图 10. 左侧:旋转胡萝卜形状的 3D 点投影到由 2 个主成分定义的*面上。右侧:根据不同*面对 3D 胡萝卜进行的 2D 转换。彩色图像表示根据主成分的转换。图像由作者制作。

PCA 的应用

PCA 最重要的应用之一与图像处理相关。这意味着对图像进行分析以分类或识别其所属类别。图像识别算法如今在我们的手机和相机中广泛使用。要了解 PCA 如何应用于图像处理,我们应首先了解图像如何存储和处理。以下是一个非常简单的示例,帮助我们理解 PCA 在图像处理中的应用。

假设我们有一张灰度图像需要处理。处理这种图像的多种方法之一是将这张图像转换为一个数组,该数组有多行多列,分别对应图像中的像素数量(请参阅这个Python 笔记本以了解如何加载图像并对其应用 PCA)。因此,在每个像素上,我们将有一个通常范围从 0 到 255 的数字,代表强度值。零表示黑色,255 表示白色,任何中间值是不同的灰度。这意味着以下 50x50 像素的灰度图像可以表示为一个 50x50 的数组。

图 11. 分辨率为 50x50 像素的章鱼灰度图(此图像来自Pixilart

50x50 的数组也可以理解为在 50 维空间中绘制的 50 个点。虽然我们无法想象这样的空间,但我们可以思考一下!这 50 个点每个都有 50 个不同的特征,这些特征最终形成了我们在图 11 中看到的图片。与之前类似,我们可以尝试找到这些数据的主成分。这可以帮助我们将点绘制在较低维空间中。下图展示了如果我们使用不同数量的主成分,投影会是什么样子。注意主成分越少,图像越模糊。此外,注意我们可以使用 25 个主成分得到非常相似的图像。

图 12. 根据用于转换原始图片的主成分数量得到的章鱼灰度图。

我们对这张图片所做的与之前在空间和*面上的点所做的没有不同。在将图片转换为 25 个主成分的情况下,我们所做的事情是:

  1. 计算原始数据集的主成分。这意味着我们构建了一个 50x50 的协方差矩阵,并从该矩阵中计算了特征向量和特征值。

  2. 我们提取了具有最大特征值的 25 个特征向量。然后我们将原始数据集(nx50)与特征向量数组(50x25)相乘。这将原始数据转换为 25 维空间。在我们之前的例子中,这就像是从 3D 胡萝卜变为 2D 胡萝卜。

  3. 由于我们对通过这种降维方式丢失了多少信息感兴趣,我们可以将新的数据集投影到 50 维空间中。这就像是将 2D 中的点投影到原始 3D 空间中。

  4. 图 13 展示了使用 25 个主成分的原始图像与投影图像之间的比较。它还展示了如果我们使用 50x25 而不是 50x50 的分辨率所得到的转换图像。

图 13. 左:章鱼的原始图片。中:使用 25 个主成分转换后的图像。右:投影到 50 维空间的转换图像。

现在我们有了一个降维后的图像,可以考虑这有多么有用。我们不再需要存储和分析 50x50=2500 个数字的数组,而是使用了 50x25=1250 个数字的数组。这可以帮助我们加快多个过程的速度,并更好地理解数据。

以下图片展示了对高细节度肖像进行的降维处理。较少的主成分使图像变得模糊,更难以辨认。然而,我们可以看到,即使使用原始尺寸的一半,我们仍然可以得到适用于许多应用的图像。

图 14. 使用不同主成分的灰度肖像(原始图像是使用Midjourney制作的)

结论

尽管我们的日常体验根植于三维世界,但我们已经看到,可以在更高维度中思考数据结构。尽管听起来很离奇,但数学是连接我们与不同维度世界的门户。然而,更重要的是,从低维到高维的转换不仅仅与科幻和大片有关。数据在多个维度中的转换和投影在我们与手机、应用程序和计算机的日常互动中无缝发生。在高维空间中处理和分析数据使我们能够理解模式并得出属于我们日常生活的结论。尽管本文所解释的过程无疑是对更复杂过程的简化,但主要思想依然有效。因此,下次你看到主角通过魔法门户进入不同维度的电影时,请记住,至少我们有了数值基础来思考这样的旅程。正如卡尔·萨根所说:“虽然我们无法想象四维世界,但我们可以很好地思考它。”

用于神经退行性疾病分类的多层神经网络

原文:towardsdatascience.com/multi-layer-neural-network-for-neurodegenerative-disorder-classification-27c0a5efc766

照片由 Jon Tyson 拍摄,发布于 Unsplash

关于开发多层神经网络以分类阿尔茨海默病磁共振图像的逐步指南

Luca ZammataroTowards Data Science Luca Zammataro

·发表于 Towards Data Science ·阅读时间 23 分钟·2023 年 3 月 3 日

--

在 2019 年 12 月,在MediumToward Data Science的支持下,我开始撰写一系列文章和 Python 教程,解释机器学习(ML)是如何工作的,以及它应如何应用于生物医学数据。从零开始构建机器学习算法的想法,没有像TensorFlowScikit-learn这样的框架的支持,这是有意为之的,目的是解释机器学习的基础,特别是对那些对所有概念有深入理解的读者有益。此外,过度引用的框架是强大的工具,需要特别关注参数设置。因此,深入研究它们的基础可以向读者解释如何充分利用它们的潜力。

随着时间的推移,我意识到诊断成像可能代表了生物医学领域主要受益于人工智能帮助的部分。如果没有其他因素,这个领域为优化机器学习算法提供了许多线索。确实,诊断成像是一个医学领域,其中变异性和定义难度与需要抽象化病理过程的本质相碰撞。

今天,更高效的人工智能系统辅助医生和科学家完成挑战性任务,例如基于影像的早期疾病诊断。人工智能的贡献不仅限于帮助快速诊断疾病,还可以熟练揭示病理过程的根本原因以及组织或器官何时开始退化。以阿尔茨海默病(AD)为例,这是一种导致神经退化和脑组织丧失的慢性神经疾病(McKhann 等人,1984 年):照顾 AD 患者的成本趋于上升,因此及时诊断变得越来越必要。

在这篇文章中,我希望引起您的注意,我们将构建一个多层感知器(MLP 神经网络),作为阿尔茨海默病磁共振成像数据(ADMRI)图像分类系统,数据来自 Kaggle。在我之前的文章中,我描述了基于逻辑回归(LR)的图像检测和结果预测的其他直接方法。在那些文章中,我们看到如何成功地对 MNIST 手写数字或 Zalando 数据进行分类,准确率达到 97%。然而,当训练集的结构达到一定复杂性时,例如 ADMRI,这些方法可能不会那么高效。

对于本文中实现的代码,我们将使用 Python。我建议使用Jupyter notebook和 Python 版本≥3.8。代码需要最低限度的优化计算,特别是涉及线性代数的部分。我们将使用如PandasNumPymatplotlibSciPy等包,这些包是 SciPy.org的一部分,是一个基于 Python 的数学、科学和工程的开源软件生态系统。此外,我们还将导入matplotlib,一个 Python 数据可视化库。此外,还将创建一个opt对象,来自scipy.optimize,用于对梯度进行优化。最后一个库是“pickle”,它对于打开 pickle 格式的文件至关重要。

建议:

为了更好地理解概念,例如 sigmoidal 函数、成本函数和线性与逻辑回归中的梯度下降,我建议读者首先阅读我之前关于这些主题的文章:

1. 一元或多元线性回归;

2. 用于癌症恶性预测的逻辑回归

3. 使用 One-vs-All 检测黑色素瘤;

4. Python 中用于图像识别的 One-vs-All 逻辑回归。

这些是这里将理所当然地使用的基本准备概念。此外,许多概念将在这里简要提及,因此在阅读这些建议的数字顺序的其他文章之前,请等待开始阅读本文。

数据集。

阿尔茨海默病磁共振成像数据(ADMRI)可以从Kaggle下载。数据包含两个文件夹:第一个包含 33984 张增强的 MRI 图像,第二个包含原始图像。数据有四类图片在训练和测试集中:1) 非痴呆,2) 中度痴呆,3) 轻度痴呆,4) 极轻度痴呆。每张图片包含约 200 x 200 像素,对应许多特征(40000)。

对于这个练习,我提供了相同数据集的转换版本,你可以从以下链接下载。这“轻量”版本包含了缩减到 50 x 50 分辨率的整个增强数据集。解压后得到的 pickle 文件是一个pandas dataframe,由两列组成,一列包含每张图片的 2500 个特征,另一列包含相应的标签,定义如下:

Label 1: Non-Demented
Label 2: Moderate Demented
Label 3: Mild Demented
Label 4: Very Mild Demented

首先,我们需要上传所有必要的库来打开数据集和显示图像:

import pandas as pd
import numpy as np 
from matplotlib.pyplot import imshow
import pickle

然后,打开 pickle 文件,发现包含的数据框,输入以下代码到 Jupyter notebook 单元格中:

with open('AugmentedAlzheimer.50X50.training.dataset.pickle', 'rb') as handle:
    ALZ = pickle.load(handle)

我们在这里称之为“ALZ”的数据框包含了所有图片及其相应标签。在新单元格中输入“ALZ”将显示数据结构:

图 1. 训练数据集结构(图片由作者提供)

现在,将每个数据框的列转换为两个不同的numpy对象,X为训练数据集,y为标签向量。(将数据转换为 numpy 对象是线性代数计算所必需的*):

X = np.stack(ALZ['X'])
y = np.int64(ALZ['y']) 

X向量包含 33,984 个项目,每个项目包含一个 2500 灰度值的向量。numpy 的shape方法显示 X 的结构:

操作X时,我们可以通过索引访问单个项目;例如,要访问第一张图片(索引=0)的前 250 个值,输入:

图 2. 显示 X[0]内容(前 250 个值)(图片由作者提供)

每个像素对应 0–255 范围内的特定灰度值,其中 0 是黑色,255 是白色。现在我们可以访问 X 向量,matplot函数imshow()将显示一个 2500 像素的灰度图像,框架为 50 x 50 像素的 2D 表示:

'''
Reshape a 2500-values vector extracted from one of the images
stored in the vector X (i.e.: #12848).
Use the NumPy method .reshape, specifiying the double argument '50'
then show the image with the function imshow, specifying the argument 
cmap='gray'
'''

image = X[12848].reshape(50, 50)
print('image:', 12848)
print('label:', y[12848])

imshow(image, cmap='gray')

运行代码时,图像编号#12848 显示如下:

图 3. 来自增强阿尔茨海默 MRI 数据集的灰度图像,表示图像#12848 对应于标签 2:中度痴呆。(图片来源于www.kaggle.com/datasets/uraninjo/augmented-alzheimer-mri-dataset

我们更愿意使用一个简单的函数随机显示从数据集中挑选的五十张图像,而不是逐一操作。以下是代码:

'''
plotSamplesRandomly
Function for visualizing fifty randomly picked images, from the dataset
'''

def plotSamplesRandomly(X, y):

    from random import randint
    import matplotlib.pyplot as plt
    %matplotlib inline

    # create a list of randomly picked indexes.
    # the function randint creates the list, picking numbers in a 
    # range 0-33983, which is the length of X

    randomSelect = [randint(0, len(X)) for i in range(0, 51)]

    # reshape all the pictures on the n X n pixels, 
    # where n = sqrt(size of X), in this case 50 = sqrt(2500)
    w, h =int(np.sqrt(X.shape[1])), int(np.sqrt(X.shape[1]))
    fig=plt.figure(figsize=(int(np.sqrt(X.shape[1])), int(np.sqrt(X.shape[1]))))

    # Define a grid of 10 X 10 for the big plot. 
    columns = 10
    rows = 10

    # The for loop
    for i in range(1, 51):

        # create the 2-dimensional picture
        image = X[randomSelect[i]].reshape(w,h)
        ax = fig.add_subplot(rows, columns, i)

        # create a title for each pictures, containing #index and label
        title = "#"+str(randomSelect[i])+"; "+"y:"+str(y[randomSelect[i]])

        # set the title font size
        ax.set_title(title, fontsize=np.int(np.sqrt(X.shape[1])/2))                

        # don't display the axis
        ax.set_axis_off()

        # plot the image in grayscale
        plt.imshow(image, cmap='gray')

    # Show some sample randomly
    print('\nShow samples randomly:')
    plt.show()

运行plotSampleRandomly()函数,传入参数 X 和 y:

plotSamplesRandomly(X, y)

输出见图 4:

图 4. 可视化从阿尔茨海默病磁共振成像数据集中随机挑选的图像。每个瓦片代表一幅磁共振图像,附有其识别编号和标签(例如,#10902; y:2,表示图像编号 10902 对应于“中度痴呆”诊断),这些图像来自www.kaggle.com/datasets/uraninjo/augmented-alzheimer-mri-dataset

此外,我还制作了原始阿尔茨海默病磁共振成像数据集的简化版本,可以从这个 Kaggle link下载。可以使用此数据集进行测试。

重要的是:本练习的唯一目的是展示将 MLP 神经网络应用于阿尔茨海默病磁共振图像的基本概念,并且仅用于实验目的,不用于临床使用。

Logistic Unit 模型

让我们从一些历史定义开始:在神经网络(NN)中,我们定义了一类从生物神经细胞网络中汲取灵感的 AI 算法。它们诞生的具体目的是试图模拟人脑功能。其运作假设的基础,正是在八十年前由著名神经科学家进行的研究中建立的。1943 年,沃伦·麦卡洛克和沃尔特·皮茨发表了一项具有重大意义的工作,题为"神经活动中固有思想的逻辑演算",首次描述了生物神经网络如何协同工作以完成复杂任务的计算模型。

从那一年开始,许多 NN 模型接踵而至。例如,1957 年,弗兰克·罗森布拉特定义了感知器的概念,这是一种能够对输入和输出之间的线性关系进行分类的人工神经元。我们可以毫无疑问地说,人工智能在那一刻诞生了。然而,尽管对这些模型的兴趣大幅增长,但当时的巨大硬件限制使得人工智能仍停留在希望和梦想的世界中,而非现实世界。我们不得不等到 1986 年,D. E. Rumelhart 发表了一篇论文,引入了反向传播训练的概念,从而提出了一种能够找到成本函数最小值的新算法,自动化计算成本函数的导数,简而言之,Rumelhart 发明了梯度下降

当我们实现神经网络时,以现代意义上的神经网络,我们指的是对一组生物神经元所做的简化模型。在这种简化中,神经元只是一个逻辑单元,通过各种树突连接到其他类似的输入单元,并通过其轴突产生输出值。

图 5. 逻辑单元模型(作者提供的图片)

逻辑单元模型简化了生物神经元。体部(图 5 中的蓝色圆圈)整合来自输入神经元(X 向量)的值,包括基础神经元(红色),通过电缆连接到逻辑单元,并通过其轴突产生输出值。该输出对应于假设模型。表示的一种方式是g(z)函数,它是一个 sigmoid 函数(或逻辑函数),因此是非线性的,其公式可以写成这样:

g(z)函数使用翻译后的θ 向量X 向量的乘积(我们称这个乘积为z)作为参数,可以定义为:

假设 X 向量取值为 0 或 1,g(z) 当 z = [θ₀X₀ + θ₁X₁ + θ₂X₂] 时,计算输出可能为 0 或 1 的概率。Sigmoid 逻辑函数也称为激活函数,这个定义源于每个生物神经元的生理功能,它根据接收到的输入类型被激活。然后,这个输入与θ 向量(逻辑单元中的权重向量)代数地相乘。对于所有回归模型(线性或逻辑),逻辑单元都有一个成本函数,它可以记录我们距离的最小值有多远,并帮助我们找到最佳的 θ。

但在理解神经网络的成本函数之前,让我们通过一些直觉和两个实际示例来了解神经网络是如何工作的。我们将描述的这个神经网络是故意简单的;它没有隐藏层,仅由一个逻辑单元和几个输入神经元组成。

简单神经网络直觉 I:AND 门

例如,假设我们希望我们的神经网络学习逻辑门 AND:

图 6a. 将 AND 门应用于简单神经网络(作者提供的图片)

图 6a 展示了将 AND 门实现到逻辑单元中的示例,它利用了我们故意指定的三个θ值:θ向量确实等于三元组θ₀=-3,θ₁=+2,θ₂=+2(绿色值)。红色的表格部分表示 AND 真值表。两个 AND 操作数是 X1 和 X2,它们可以取 0 或 1 的值,而 X0 是表示偏置的输入神经元,默认值为 1。输出函数 Y 对应于假设模型 hθ,由函数 g(z)实现。根据z的不同,函数g(z)返回接* 0 或 1 的值,这取决于z位置映射到的 sigmoid。

图 6b:AND 门逻辑函数的放大图(作者提供的图像)

例如,对于 X1 = 0 和 X2 = 0,g(z)将等于 0.05,因为 X 向量中唯一不等于 0 的值是 X0,即存储在偏置神经元中的值(-3)。实际上,g(-3)=0.05 映射到 sigmoid 时接*于零。相反,当 X1 = 1 和 X2 = 1(唯一使 X1 AND X2 为真的条件)时,g(z)等于 g(-3+2+2) = g(1),映射到 sigmoid 时返回 0.73,这个值远远超过 0.5 的阈值,接* 1。在其他两个剩余情况下,即 X1=0,X2=1 和 X1=1,X2=0,g(z)等于 g(-1)=0.27,这个值再次接* 0(图 6b)。所有四种情况下的 g(z)输出(图 6a 中的黄色)返回了 AND 门的预期结果。sigmoid 逻辑函数的优点是它可以*滑地计算输入神经元信号组合的 0/1 概率,像生物神经元一样生理地激活。此外,sigmoid 函数将有助于保证梯度下降算法寻求θ值时收敛到全局最小值,避免处理具有多个局部最优的非凸成本函数的问题(详见癌症恶性预测的逻辑回归以获取有关非凸成本函数问题的详细讨论)。sigmoid 逻辑函数的 Python 实现如下:

# Sigomid Logistic (Activation) Function
def sigmoid(z): 
    g = 1.0 / (1.0 + np.exp(-z))
    return g

请复制并粘贴到新的笔记本单元中。你可以运行该函数,将不同的z值作为参数传递给它,并查看它产生的输出,例如:

或者是否可以实现一个出色的函数来可视化 g(z)及其输出,如下所示:

# Plot the Sigmoid function, and its output:

def plotSigmoid(z):
    import numpy as np
    from matplotlib import pyplot as plt

    plt.rcParams["figure.figsize"] = [7.50, 3.50]
    plt.rcParams["figure.autolayout"] = True

    x = np.linspace(-10, 10, 100)

    # setting the axes at the centre
    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    ax.spines['left'].set_position('center')
    ax.spines['bottom'].set_position('zero')
    ax.spines['right'].set_color('none')
    ax.spines['top'].set_color('none')
    ax.xaxis.set_ticks_position('bottom')
    ax.yaxis.set_ticks_position('left')

    plt.plot(x, sigmoid(x), color='blue')

    plt.plot(0, sigmoid(0), marker="o", markersize=5, 
             markeredgecolor="black", markerfacecolor="black")    
    if(z!=0):
        plt.text(0, sigmoid(0), "  0.5")

    plt.plot(z, sigmoid(z), marker="o", markersize=5,
             markeredgecolor="red", markerfacecolor="red")

    value = "{:5.3f}".format(sigmoid(z))
    plt.text(z, sigmoid(z), str("  z="+str(z)+"; value="+str(value)))

    plt.show()

例如,运行 plotSigmoid,对于 z = 1,即 0.73,将显示如下:

(作者提供的图像)

简单神经网络直观 II:OR 门

关于 AND 门,我们可以用一个涉及 OR 门的其他示例来进行实验。

图 7a:OR 门在逻辑单元中的应用(图片由作者提供)

通过将偏置神经元 X0 的θ值从 -3 更改为 -1 并重新进行所有计算,输出现在反映了 OR 门:我们有三个 X1 和 X2 输入对,它们被映射到 g(z) sigmoid 函数中,产生接* > 0.5、接* 1 的值。

图 7b:OR 门的逻辑函数放大图(图片由作者提供)

多层感知机(MLP)模型

多层神经网络由一个输入层、一个或多个隐藏层以及一个输出层组成。所有层(但输出层除外)都包含一个偏置神经元,所有单元完全连接。隐藏层的数量区别于经典的 MLP 和深度学习神经网络。由于其架构的特性,神经网络可以帮助我们提出复杂的非线性假设。

图 8:多层神经网络(图片由作者提供)

观察 MLP 的模型表示,我们发现 MLP 运作的基础计算与逻辑回归中的计算类似。这一观察确认了 MLP 实质上是一个“超级”逻辑回归器。结构(图 8)显示了三个神经层。第一层是输入层,由X 向量组成。第二层由基于X 乘以 θ 的结果激活的隐藏节点组成。这些激活的结果成为第三层,即输出层的输入。第三层也包括激活单元。唯一的区别是,它的结果应对应于假设模型 。在这里,我们必须使用条件动词,因为神经网络的核心问题正是尝试最小化假设与来自 X 乘以 θ 产品的预测结果之间的误差。

图 9:多层神经网络的模型表示。每个激活单元的计算根据图 8 中表示隐藏层单元的彩色圆盘进行颜色编码。(图片由作者提供)

最小化过程体现在所谓的训练阶段。它包括找到那些θ值(权重),使得能够通过梯度下降符合的要求。我们必须将一个维度为Sj乘以(n + 1)的θ矩阵相乘,其中SjJ层的激活节点数量:

(图片由作者提供)

和,

(图片由作者提供)

实现 MLP 神经网络以增强阿尔茨海默病 MRI 分类

我们将创建一个单层 MLP。隐藏层的单元数计算为输入单元的 2/3 加上输出单元的数量。这个设置保证了训练成功,尽管其他架构在隐藏层使用了更多或更少的激活单元。输入单元的数量取决于数据集中单个图像的像素数量,而输出单元的数量对应于类别标签。整个算法分为三个主要函数:

1\. init
2\. training
3\. testing

这三个主要函数包含其他函数,我们将在适当的时候进行描述。

这里介绍的 NN 是一个 前向传播 MLP;因此,输入数据通过网络向前传播以生成输出。隐藏层处理数据并移动到下一个层。在前向传播过程中,激活发生在每个隐藏层和输出层节点上。激活函数是加权和的计算。基于加权和,激活函数被应用于使神经网络通过使用偏置非线性流动。

初始化

上传数据集后,定义 Xy 向量:

with open('AugmentedAlzheimer.50X50.training.dataset.pickle', 'rb') as handle:
    ALZ = pickle.load(handle) 
X = np.stack(ALZ['X'])
y = np.int64(ALZ['y']) 

X 向量将包含 33,984 个项目,每个项目包含一个 2500 灰度值的向量。init 函数使用方法 X.shape 根据 X 的大小初始化 NN 架构:

'''
function "init"
Neural Network initialization and parameter setup
'''
def init(X, y):
    I  = X.shape[1]  # n x n MRI input size (2500)
    H1 = int(X.shape[1]*2/3) + max(set(y)) # hidden units size: 
                        # 2/3 of the input units + the number of output units (1670)
    O = max(set(y)) # output units size or labels (4)
    m = X.shape[0] 

    ini_Th1 = thetasRandomInit(I, H1)
    ini_Th2 = thetasRandomInit(H1, O)

    # Unroll parameters 
    ini_nn_params = np.concatenate((ini_Th1.T.flatten(), ini_Th2.T.flatten()))
    ini_nn_params = ini_nn_params.reshape(len(ini_nn_params), 1)

    print('\nNeural Network Parameters initialized!\n')
    print('\nNeural Network structure:\n')
    print('Input layer neurons:', I)
    print('1st hidden layer neurons:', H1)
    print('Output layer neurons:', O)

    return ini_nn_params, I, H1, O

参数 I, H1, 和 O 对应于输入层、隐藏层和输出层的大小;ini_nn_params 包含 θ 向量的“展开”版本(Th1 对于输入层,Th2 对于隐藏层)。使用展开的扁*化向量将减少处理过多向量的复杂性,使代码更易读。init 函数接受向量 Xy 作为参数,但需要子函数 thetasRandomInit 嵌套在内部,用于随机化 Th1Th2。(这些必须在训练阶段之前随机生成)。

thetasRandomInit 函数可以如下实现:

'''
function "thetasRandomInit"
Random initialization of thetas
'''
def thetasRandomInit(L_in, L_out):
    e_init = 0.12
    W = np.random.rand(L_out, 1 + L_in) * 2 * e_init - e_init
    return W

W 矩阵包含一个随机值向量,它是基于一个 e_init 值实现的,默认设置为 0.12。请将之前的代码复制粘贴到一个新单元格中并运行。调用 init 函数并定义 NN 参数 ini_nn_params, I, H1, O 类型如下:

ini_nn_params, I, H1, O = init(X, y)

init 函数将初始化这四个变量,并生成输出:

Neural Network Parameters initialized!

Neural Network structure:

Input layer neurons: 2500
1st hidden layer neurons: 1670
Output layer neurons: 4

该函数描述了 NN 的结构,包括 2500 个输入单元,1670 个隐藏单元,以及四个输出单元。

训练

对于使用我们上传的 Alzheimer’s MRIs 数据集(Xy 向量)训练 NN,我们需要创建一个 training 函数,这只是一个将 nnCostFunction 传递给 opt 对象的“包装”代码:

'''
function "training"
Neural Network training
'''
def training(ini_nn_params, I, H1, O, X, y):
    import scipy.optimize as opt

    lambd = 1
    print('\nTraining Neural Network... \n')

    result = opt.minimize(fun=nnCostFunction, 
                          x0=ini_nn_params,
                          args=(I, H1, O, X, y, lambd),
                          method='TNC', 
                          jac=True, 
                          options={'maxiter': 1000, "disp": True})

    params_trained = result.x

    Th1_trained = np.reshape(params_trained[0:(H1 * (I + 1)), ],
                                 (H1, I + 1))
    Th2_trained = np.reshape(params_trained[(H1 * (I + 1)):, ],
                                 (O, H1 + 1))

    print('\nTrained.\n')

    return Th1_trained, Th2_trained

函数 training 使用 scipy 对象 opt 来最小化神经网络成本函数 (nnCostFunction) 的 梯度下降。该函数通过 “maxiter” 参数确定实现最小化目标所需的迭代次数。正如 opt.minimize 的参数列表中所指定的,这里使用的方法是 TNC(截断牛顿法),它可以最小化标量函数,如 nnCostFunction,无论是单变量还是多变量。最后,training 将返回两个经过训练并准备好用于测试阶段的 θ 向量,Th1Th2

nnCostFunction 代表了这里展示的最复杂的例程,是整个算法的核心。现在我们将更详细地解释它的作用。我建议在学习 nnCostFunction 代码时考虑图 8 中的模式和图 9 中的计算。

'''
nnCostFunction
Implements the Neural Network Cost Function
'''
def nnCostFunction(nn_params, I, H1, O, X, y, lambd):

    # 1\. RESHAPING
    # Reshape nn_params back into the parameters Th1 and Th2, 
    # the weight matrices

    Th1 = np.reshape(nn_params[0:(H1 * (I + 1)), ],
                         (H1, I + 1))
    Th2 = np.reshape(nn_params[(H1 * (I + 1)):, ],
                         (O, H1 + 1))

    # 2\. SETUP OF Y
    # Setup the output (y) layout
    m = X.shape[0] 

    Y = np.zeros((m, O))
    for i in range(m):
        Y[i, y[i] - 1] = 1   

    # 3\. INITIALIZE J, and THETAS
    J = 0
    Th1_grad = np.zeros(Th1.shape) 
    Th2_grad = np.zeros(Th2.shape) 

    # 4\. PREPARE ALL THE VECTORS FOR THE FORWARD PROPAGATION
    # Six new vectors are generated here: a1, z2, a2, z3, a3, and h.
    # The vector a1 equals X (the input matrix), 
    # with a column of 1's added (bias units) as the first column.

    a1 = np.hstack((np.ones((m, 1)), X))  

    # z2 equals the product of a1 and Th1
    z2 = np.dot(a1, Th1.T) 

    # The vector a2 is created by adding a column of bias units 
    # after applying the sigmoid function to z2.
    a2 = np.hstack((np.ones((m, 1)), sigmoid(z2))) 

    # z3 equals the product of a2 and Th2
    z3 = np.dot(a2, Th2.T) 
    a3 = sigmoid(z3) 

    # The Hypotheses h is = a3
    h = a3 

    # 5\. MAKE REGUKARIZED COST FUNCTION.
    # calculate P
    p = np.sum([
        np.sum(np.sum([np.power(Th1[:, 1:], 2)], 2)),
        np.sum(np.sum([np.power(Th2[:, 1:], 2)], 2))
    ])

    # Calculate Cost Function
    J = np.sum([
        np.divide(np.sum(np.sum([np.subtract(np.multiply((-Y), np.log(h)), 
                                             np.multiply((1-Y), np.log(1-h)))], 2)), m),
        np.divide(np.dot(lambd, p), np.dot(2, m))
    ])    

    # 6\. FORWARD PROPAGATION
    # d3 is the difference between a3 and y. 
    d3 = np.subtract(a3, Y)

    z2 = np.hstack((np.ones((m, 1)), z2))
    d2 = d3.dot(Th2) * gradientDescent(z2)    
    d2 = d2[:, 1:]

    # GRADIENTS
    # Delta1 is the product of d2 and a1\. 
    delta_1 = np.dot(d2.T, a1)

    # Delta2 is the product of d3 and a2\. 
    delta_2 = np.dot(d3.T, a2)

    # Regularized Gradients.
    P1 = (lambd/m) * np.hstack([np.zeros((Th1.shape[0], 1)), Th1[:, 1:]])
    P2 = (lambd/m) * np.hstack([np.zeros((Th2.shape[0], 1)), Th2[:, 1:]])

    Theta_1_grad = (delta_1/m) + P1
    Theta_2_grad = (delta_2/m) + P2

    grad = np.hstack((Theta_1_grad.ravel(), Theta_2_grad.ravel()))

    return J, grad

函数 nnCostFunction() 接受参数 nn_params,即我们之前创建的 ini_nn_params,它包含了 θ 向量的展开版本;I, H1, O(分别是输入层、隐藏层和输出层的大小);两个向量 X, y;以及对应于正则化参数 lambdalambd。该函数返回 J 和 θ 梯度向量

  1. 首先,nnCostFunctionini_nn_params 重新调整为参数 Th1Th2,考虑到这两个向量的原始大小。

  2. 代码需要创建输出层的设置,首先生成一个由 n-label-columns * n-X_samples 组成的向量 Y,初始值为零,然后用 1 填充,以这种方式对输出进行编码:

Structure of Y (the output layer):

1,0,0,0 for label 1
0,1,0,0 for label 2
0,0,1,0 for label 3
0,0,0,1 for lable 4

新的编码随后被赋值给 y 向量。

3. 用零初始化 成本 Jθ 向量,用于 梯度下降

4. 为 “前向传播” 准备所有向量。这里生成了六个新向量:a1, z2, a2, z3, a3h。向量 a1 等于 X(输入矩阵),在第一列添加了一列 1(偏置单元)。向量 z2 等于 a1θ1 (Th1) 的乘积。向量 a2 是在对 z2 应用 sigmoid 函数后添加了一列偏置单元创建的。向量 z3 等于 a2θ1 (Th2) 的乘积。最后,向量 z3 被复制到向量 h 中,表示假设 。这个步骤表面上看似不重要,但为了更好的可读性,复制 z3h 是必要的。

5. 该部分函数实现了我们希望被正则化的 成本函数。请回顾逻辑回归成本函数的公式:

图 10:逻辑回归成本函数。(作者提供的图片)

正则化版本是:

图 11:正则化逻辑回归成本函数。(作者提供的图片)

该实现通过将 lambda 乘以 probability p(在公式中未出现)来优化正则化;p 计算为 θ1θ2 的幂的总和。

6. 这部分代码将执行前向传播。在执行此任务时,代码必须创建一个新的向量 d3,用于收集 a3y 之间的差异;在这里,z2 被处理,添加了一列 1,而新的向量 d2 将包含 d3Th2 的乘积,以及 Gradient Descent 的输出,应用于 z2

Gradient Descent 函数的实现如下:

'''
gradientDescent
returns the gradient of the sigmoid function

'''
def gradientDescent(z):
    g  = np.multiply(sigmoid(z), (1-sigmoid(z)))
    return g

要运行 training 函数,请调用它,并指定 ini_nn_paramsIH1OXy 作为参数:

Th1_trained, Th2_trained = training(ini_nn_params, I, H1, O, X, y)

注意:

训练可能需要十二小时的计算。两个训练好的 θ 向量可以通过这两个链接获得:Theta1Theta2

下载后,你可以通过输入以下命令上传这两个文件:

# Upload thetas from the two files
with open('ALZ.50.df_theta_1.pickle', 'rb') as handle:
        Th1_trained = pickle.load(handle).values
with open('ALZ.50.df_theta_2.pickle', 'rb') as handle:
        Th2_trained = pickle.load(handle).values 

测试

测试函数用于测试神经网络训练:

'''
testing()
'''
def testing(Th1, Th2, X, y):

    m, n = X.shape
    X = np.hstack((np.ones((m, 1)), X))
    a2 = sigmoid(X.dot(Th1.T))
    a2 = np.hstack((np.ones((m, 1)), a2))
    a3 = sigmoid(a2.dot(Th2.T))

    pred = np.argmax(a3, axis=1)
    pred += 1  
    print('Training Set Accuracy:', np.mean(pred == y) * 100)
    return pred

该函数非常简单,并且在概念上重新追溯前向传播路径。在将 1 的偏置列应用于 X 后,a2 加载 X 乘以 θ1 的 sigmoid 值,并且只有在将 1 的偏置列添加到 a2 后,a3 才加载 X 乘以 θ2 的 sigmoid 值。参数 pred(预测)等于 a3 的最大值。最后,函数返回 predy 向量之间成功结果匹配的百分比。

在运行代码之前,上传测试版本的相同数据集:

# LOAD TESTING DATASET
with open('Alz.original.50X50.testing.dataset.pickle', 'rb') as handle:
    ALZ = pickle.load(handle) 
X = np.stack(ALZ['X'])
y = np.int64(ALZ['y']) 

要运行 testing,请调用它,并指定两个训练好的 θ 向量、XY 作为参数:

pred = testing(Th1_trained, Th2_trained, X, y)

该神经网络架构在阿尔茨海默 MRI 数据集上的准确率为 95%

看到神经网络的实际运行效果将非常棒,展示一些具体结果。以下函数表示 plotSampleRandomly 的修改版,即我们用来随机显示数据集中 MRI 图像的函数,适合我们。它接受向量 Xy 作为参数,还有在测试阶段创建的向量 pred

'''
plotSamplesRandomlyWithPrediction
Function for visualizing fifty randomly picked images with their prediction.
'''

def plotSamplesRandomlyWithPrediction(X, y, pred):
    from random import randint
    from matplotlib import pyplot as plt

    # create a list of randomly picked indexes.
    # the function randint creates the list, picking numbers in a 
    # range 0-Xn, which is the length of X

    randomSelect = [randint(0, len(X)) for i in range(0, 51)]

    # reshape all the pictures on the n X n pixels, 
    w, h =int(np.sqrt(X.shape[1])), int(np.sqrt(X.shape[1]))
    fig=plt.figure(figsize=(int(np.sqrt(X.shape[1])), int(np.sqrt(X.shape[1]))))

    # Define a grid of 10 X 10 for the big plot. 
    columns = 10
    rows = 10

    # The for loop
    for i in range(1, 51):

        # create the 2-dimensional picture
        image = X[randomSelect[i]].reshape(w,h)
        ax = fig.add_subplot(rows, columns, i)

        # create a title for each pictures, containing #index and label
        #title = "#"+str(randomSelect[i])+"; "+"y="+str(y[randomSelect[i]])
        title = "#"+str(randomSelect[i])+"; "+"y:"+str(y[randomSelect[i]])+"; "+"p:"+str(pred[randomSelect[i]])

        # set the title font size
        ax.set_title(title, fontsize=np.int(np.sqrt(X.shape[1])/2))        

        # don't display the axis
        ax.set_axis_off()

        # plot the image in grayscale
        plt.imshow(image, cmap='gray')

    plt.show()

并按如下方式运行函数:

plotSamplesRandomlyWithPrediction(X, y, pred)

plotSamplesRandomlyWithPrediction 的一个可能输出如下:

图 12. 可视化原始阿尔茨海默数据集中随机挑选的预测结果。图像来源于 www.kaggle.com/datasets/uraninjo/augmented-alzheimer-mri-dataset

结论

神经网络是分类复杂图像的绝佳解决方案,尤其是当这些图像来自神经成像时。在其他专门讨论类似问题的文章中,我们看到对比度较差的图像可能在分类中遇到困难。Kaggle 提供的增强型阿尔茨海默病 MRI 数据集展示了一些优势,因为每张图像的对比度良好。然而,各病理类别特征的模式多样性所带来的复杂性很高,并且仅在偶尔的情况下才能一眼识别。

这篇文章旨在确定一种计算快速且相对高效的分类和模式预测技术,测试准确率约为 ~ 95%。此类工具为快速诊断提供了新的机会,并且在病理学中提供了一种巧妙的方法来提取关键特征。

这篇文章再次提供了详细解释构建和训练多层神经网络机制的机会。揭示了一些关于神经网络在逻辑门中的计算过程及其与逻辑回归的强相关性的直觉,展示了神经网络实际上是一个超级逻辑回归模型。

扩展文章中描述的代码可能会增加隐藏层的数量。由于编辑原因,我没有分享这些代码扩展,增加此特定应用中的隐藏层不会提高准确性百分比;相反,会使其变得更差。无论如何,有兴趣的人可以获得两层和三层隐藏层的代码。

参考文献

  1. 来自 Kaggle 的增强型阿尔茨海默病 MRI 数据集采用 GNU 较小公共许可证。它代表了 Kaggle 阿尔茨海默病数据集(4 类图像) 的一个分支,采用 开放数据公共开放数据库许可证(ODbL)v1.0

  2. 卢卡·扎马塔罗,一个或多个变量的线性回归,《数据科学前沿》,2019

  3. 卢卡·扎马塔罗,癌症恶性预测的逻辑回归,《数据科学前沿》,2019

  4. 卢卡·扎马塔罗,通过一对多检测黑色素瘤,《数据科学前沿》,2020

  5. 卢卡·扎马塔罗,用于图像识别的 Python 中的一对多逻辑回归,《数据科学前沿》,2022

  6. 安德鲁·恩吉,机器学习 | Coursera

  7. 克里斯·阿尔邦,Python 机器学习烹饪书,O'Reilly,ISBN-13:978–1491989388。

  8. Aurélien Géron, 《动手学机器学习:使用 Scikit-Learn、Keras 和 TensorFlow 构建智能系统的概念、工具和技术》 第 2 版,O’Reilly,ISBN-10: 1492032646

  9. Wen, J. 等人:用于阿尔茨海默病分类的卷积神经网络:概述与可重复评估

多层感知器的解释与说明

原文:towardsdatascience.com/multi-layer-perceptrons-8d76972afa2b

理解神经网络的第一个完全功能模型

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

·发表于 Towards Data Science ·13 分钟阅读·2023 年 4 月 2 日

--

上一篇文章 中,我们讨论了感知器作为最早的神经网络模型之一。正如我们所见,单个感知器在计算能力上有限,因为它们只能解决线性可分的问题。

在本文中,我们将讨论多层感知器(MLP),这些网络由多个感知器层组成,比单层感知器强大得多。我们将探讨这些网络如何运作,以及如何利用它们解决复杂任务,例如图像分类。

定义与符号

多层感知器(MLP)是一个至少有三层的神经网络:输入层、隐藏层和输出层。每一层都处理其前一层的输出:

MLP 架构

我们将使用以下符号:

  • aᵢˡ 是层 l 中神经元 i 的激活值(输出)

  • wᵢⱼˡ 是从层 l-1 中神经元 j 到层 l 中神经元 i 的连接权重

  • bᵢˡ 是层 l 中神经元 i 的偏置项

输入层和输出层之间的中间层称为 隐藏层,因为它们在网络之外不可见(它们构成了网络的“内部大脑”)。

输入层通常不算作网络中的层数。例如,一个 3 层网络有一个输入层、两个隐藏层和一个输出层。

前向传播

前向传播是将输入数据逐层通过网络的过程,直到生成输出。

在前向传播阶段,神经元的激活值计算类似于单个感知器的激活值计算方式。

例如,让我们看一下第l层的神经元 i。该神经元的激活值是通过两个步骤计算得出的:

  1. 我们首先计算神经元的净输入,作为其输入的加权和加上偏置:

l 层的神经元 i 的净输入

2. 我们现在对净输入应用激活函数以获得神经元的激活值:

l 层神经元 i 的激活值

根据定义,输入层神经元的激活值等于当前呈现给网络的示例的特征值,即,

输入神经元的激活值

其中 m 是数据集中的特征数量。

向量化形式

为了提高计算效率(特别是在使用像 NumPy 这样的数值库时),我们通常使用上述方程的向量化形式。

我们首先定义向量 aˡ 为包含第 l 层所有神经元激活值的向量,以及向量 bˡ 为包含第 l 层所有神经元偏置的向量。

我们还定义 为从第 l 层到第 l 层 - 1 的所有神经元的连接权重矩阵。例如,W¹₂₃ 是层 0(输入层)中神经元编号 2 与层 1(第一隐藏层)中神经元编号 3 之间连接的权重。

我们现在可以将前向传播方程写成向量形式。对于每一层 l,我们计算:

前向传播方程的向量化形式

解决 XOR 问题

多层感知器(MLP)相对于单层感知器的首次演示表明,它们能够解决 XOR 问题。XOR 问题是非线性可分的,因此单层感知器无法解决:

XOR 问题

然而,具有单层隐藏层的 MLP 可以轻松解决这个问题:

解决 XOR 问题的多层感知器(MLP)。偏置项写在节点内部。

让我们分析一下这个 MLP 是如何工作的。这个 MLP 除了两个输入神经元外,还有三个隐藏神经元和一个输出神经元。我们在这里假设所有的神经元都使用阶跃激活函数(即,对于所有非负输入,函数值为 1,而对于所有负输入,函数值为 0)。

顶层隐藏神经元仅与第一个输入 x₁ 连接,连接权重为 1,且有一个偏置 -1。因此,这个神经元仅在 x₁ = 1 时激活(此时其净输入为 1 × 1 + (-1) = 0,f(0) = 1,其中 f 是阶跃函数)。

中间隐藏神经元与两个输入相连,连接权重为 1,且有一个偏置 -2。因此,这个神经元仅在两个输入都为 1 时激活。

底部隐藏神经元仅与第二输入 x₂ 连接,连接权重为 1,且偏置为 -1。因此,这个神经元仅在 x₂ = 1 时被激活。

输出神经元与顶部和底部隐藏神经元的权重为 1,且与中间隐藏神经元的权重为 -2,偏置为 -1。因此,它仅在顶部或底部隐藏神经元激活时被激活,而在两者同时激活时不被激活。换句话说,它仅在 x₁ = 1 或 x₂ = 1 时被激活,而在两个输入都为 1 时不会被激活,这正是我们对 XOR 函数输出的预期。

例如,计算这个多层感知机(MLP)在输入 x₁ = 1 和 x₂ = 0 时的前向传播。此时隐藏神经元的激活情况是:

x1 = 1 和 x2 = 0 时隐藏神经元的激活情况

我们可以看到在这种情况下只有顶部的隐藏神经元被激活。

输出神经元的激活情况是:

MLP 在 x1 = 1 和 x2 = 0 时的输出

在这种情况下,输出神经元被激活,这正是我们对输入 x₁ = 1 和 x₂ = 0 时 XOR 输出的预期。

验证你是否理解 MLP 如何计算 XOR 函数的其他三个情况!

MLP 构建练习

作为另一个例子,考虑以下数据集,其中包含来自三个不同类别的点:

构建一个 MLP,正确分类数据集中所有点。

提示:使用隐藏神经元来识别三个分类区域。

解决方案可以在本文底部找到。

通用逼*定理

关于 MLP 的一个显著事实是它们可以计算任何任意的函数(尽管网络中的每个神经元计算的是非常简单的函数,如阶跃函数)。

通用逼*定理指出,一个具有足够数量神经元的单隐层 MLP 可以任意精确地逼*任何连续的输入函数。具有两个隐层的 MLP 甚至可以逼*不连续的函数。这意味着即使是非常简单的网络架构也可以非常强大。

不幸的是,定理的证明是非构造性的,即它没有告诉我们如何构建一个网络来计算特定的函数,只是展示了这样的网络存在。

MLP 中的学习:反向传播

尽管 MLP 已被证明在计算上非常强大,但很长一段时间内还不清楚如何在特定的数据集上训练它们。虽然单层感知器有一个简单的权重更新规则,但不清楚如何将此规则应用于隐藏层的权重,因为这些权重不会直接影响网络的输出(因此也不会直接影响训练损失)。

当 1986 年 Rumelhart 等人引入其突破性的反向传播算法用于训练 MLP 时,AI 社区花费了超过 30 年的时间才解决了这个问题。

反向传播的主要思想是首先计算网络误差函数相对于每个权重的梯度,然后使用梯度下降来最小化误差。之所以称之为反向传播,是因为我们利用导数链式法则将误差的梯度从输出层传播回输入层。

反向传播算法在 这篇文章 中有详细解释。

激活函数

在单层感知器中,我们使用了步进函数或符号函数作为神经元的激活函数。这些函数的问题在于它们的梯度几乎为 0(因为它们在 x > 0 和 x < 0 时等于常数值)。这意味着我们无法在梯度下降中使用它们来找到网络的最小误差。

因此,在 MLP 中我们需要使用其他激活函数。这些函数应该既可微分又是非线性的(如果 MLP 中所有神经元使用线性激活函数,则 MLP 的行为类似于单层感知器)。

对于隐藏层,最常见的三种激活函数是:

  1. Sigmoid 函数

2. 双曲正切函数

3. ReLU(修正线性单元)函数

输出层的激活函数取决于网络试图解决的问题:

  1. 对于回归问题,我们使用恒等函数 f(x) = x

  2. 对于二分类问题,我们使用 Sigmoid 函数(如上所示)。

  3. 对于多类分类问题,我们使用 softmax 函数,它将 k 个实数的向量转换为 k 种可能结果的概率分布:

Softmax 函数

在这篇文章中深入解释了为什么我们在多类问题中使用 Softmax 函数:

## 深入了解 Softmax 回归

理解 Softmax 回归背后的数学原理以及如何使用它来解决图像分类任务

towardsdatascience.com

Scikit-Learn 中的 MLP

Scikit-Learn 提供了两个实现 MLP 的类,位于 sklearn.neural_network 模块中:

  1. MLPClassifier 用于分类问题。

  2. MLPRegressor 用于回归问题。

这些类中的重要超参数有:

  • hidden_layer_sizes — 定义每个隐藏层中神经元数量的元组。默认值是 (100,),即一个包含 100 个神经元的隐藏层。对于许多问题,使用一两个隐藏层应该足够。对于更复杂的问题,你可以逐渐增加隐藏层的数量,直到网络开始过拟合训练集。

  • activation — 在隐藏层中使用的激活函数。选项有 ‘identity’,‘logistic’,‘tanh’,和 ‘relu’(默认)。

  • solver — 用于权重优化的求解器。默认值是 ‘adam’,它在大多数数据集上表现良好。各种优化器的行为将在未来的文章中解释。

  • alpha — L2 正则化系数(默认为 0.0001)

  • batch_size — 用于训练的迷你批次的大小(默认为 200)。

  • learning_rate — 权重更新的学习率调度(默认为 ‘constant’)。

  • learning_rate_init — 使用的初始学习率(默认为 0.001)。

  • early_stopping — 是否在验证得分没有改善时停止训练(默认为 False)。

  • validation_fraction — 从训练集中留出用于验证的比例(默认为 0.1)。

我们通常使用网格搜索和交叉验证来调整这些超参数。

在 MNIST 上训练 MLP

例如,让我们在 MNIST 数据集 上训练一个 MLP,这是一个广泛用于图像分类任务的数据集。

数据集包含 60,000 张训练图像和 10,000 张测试图像,每张图像为 28 × 28 像素,通常用一个包含 784 个数字的向量表示,范围在 [0, 255] 之间。任务是将这些图像分类到十个数字(0-9)之一。

我们首先使用 fetch_openml() 函数获取 MNIST 数据集:

from sklearn.datasets import fetch_openml

X, y = fetch_openml('mnist_784', return_X_y=True, as_frame=False)

as_frame 参数指定我们希望以 NumPy 数组而不是 DataFrame 的形式获取数据和标签(这个参数的默认值在 Scikit-Learn 0.24 中从 False 改为 ‘auto’)。

让我们检查 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 个数字

让我们检查每个数字的样本数量:

np.unique(y, return_counts=True)
(array(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], dtype=object),
 array([6903, 7877, 6990, 7141, 6824, 6313, 6876, 7293, 6825, 6958],
       dtype=int64))

数据集在 10 个类别之间比较均衡。

我们现在将输入缩放到 [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:]

我们现在创建一个具有 300 个神经元的单隐藏层 MLP 分类器。我们将保持所有其他超参数的默认值,除了early_stopping,我们将其更改为 True。我们还将设置 verbose=True,以便跟踪训练进度:

from sklearn.neural_network import MLPClassifier

mlp = MLPClassifier(hidden_layer_sizes=(300,), early_stopping=True, 
                    verbose=True)

让我们将分类器拟合到训练集上:

mlp.fit(X_train, y_train)

训练期间得到的输出是:

Iteration 1, loss = 0.35415292
Validation score: 0.950167
Iteration 2, loss = 0.15504686
Validation score: 0.964833
Iteration 3, loss = 0.10840875
Validation score: 0.969833
Iteration 4, loss = 0.08041958
Validation score: 0.972333
Iteration 5, loss = 0.06253450
Validation score: 0.973167
...
Iteration 31, loss = 0.00285821
Validation score: 0.980500
Validation score did not improve more than tol=0.000100 for 10 consecutive epochs. Stopping.

训练在 31 次迭代后停止,因为在前 10 次迭代中验证分数没有改善。

让我们检查一下 MLP 在训练集和测试集上的准确性:

print('Accuracy on training set:', mlp.score(X_train, y_train))
print('Accuracy on test set:', mlp.score(X_test, y_test))
Accuracy on training set: 0.998
Accuracy on test set: 0.9795

这些是很好的结果,但具有更复杂架构的网络,如卷积神经网络(CNNs),可以在这个数据集上获得更好的结果(在测试集上的准确率高达 99.91%!)。你可以在这里找到关于 MNIST 的最先进结果及相关论文的链接。

为了更好地理解我们模型的错误,让我们显示其混淆矩阵:

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

y_pred = mlp.predict(X_test)
cm = confusion_matrix(y_test, y_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=mlp.classes_)
disp.plot(cmap='Blues')

测试集上的混淆矩阵

我们可以看到,模型的主要混淆发生在数字 4⇔9、7⇔9 和 2⇔8 之间。这是有道理的,因为这些数字在手写时经常彼此相似。为了帮助我们的模型区分这些数字,我们可以增加这些数字的样本(例如,通过数据增强)或从图像中提取额外的特征(例如,数字中的闭合环数)。

可视化 MLP 权重

尽管神经网络通常被认为是“黑箱”模型,但在由一到两个隐藏层组成的简单网络中,我们可以可视化学到的权重,并偶尔对这些网络如何在内部工作有所了解。

例如,我们可以绘制 MLP 分类器的输入层和隐藏层之间的权重。权重矩阵的形状为 (784, 300),并存储在一个名为 mlp.coefs_[0] 的变量中:

print(mlp.coefs_[0].shape)
(784, 300)

这个矩阵的 i 列表示输入到隐藏神经元 i 的权重。我们可以将这一列显示为一个 28 × 28 像素的图像,以检查哪些输入神经元对该神经元的激活有较强的影响。

以下图展示了前 20 个隐藏神经元的权重:

fig, axes = plt.subplots(4, 5)

for coef, ax in zip(mlp.coefs_[0].T, axes.flat):
    im = ax.imshow(coef.reshape(28, 28), cmap='gray')
    ax.axis('off')

fig.colorbar(im, ax=axes.flat)

前 20 个隐藏神经元的权重

我们可以看到每个隐藏神经元关注图像的不同部分。

其他库中的 MLP

尽管 Scikit-Learn 中的 MLP 分类器易于使用,但在实际应用中,你更可能使用如 TensorFlow 或 PyTorch 等深度学习库来构建 MLP。这些库可以利用更快的 GPU 处理速度,还提供许多附加选项,如额外的激活函数和优化器。你可以在这篇文章中找到如何使用这些库的示例。

MLP 构建练习的解决方案

以下 MLP 正确分类了数据集中的所有点:

MLP 用于解决分类问题。偏置项被写在节点内部。

解释:

左侧隐藏神经元只有在x₁ ≤ 3 时才会激活,中间隐藏神经元只有在x₂ ≥ 4 时才会激活,右侧隐藏神经元只有在x₂ ≤ 0 时才会激活。

左侧输出神经元对左侧和中间隐藏神经元执行 OR 操作,因此只有在x₁ ≤ 3 OR x₂ ≥ 4 时才会激活,即只有当点是蓝色时。

中间输出神经元对所有隐藏神经元执行 NOR(非 OR)操作,因此只有在 NOT(x₁ ≤ 3 OR x₂ ≥ 4 OR x₂ ≤ 0)时才会激活。换句话说,只有当x₁ > 3 AND 0 < x₂ < 4 时,它才会激活,即只有当点是红色时。

只有当右侧隐藏神经元激活时,右侧输出神经元才会激活,即只有当x₂ ≤ 0 时,才会发生这种情况,这仅对紫色点有效。

最终说明

你可以在我的 GitHub 上找到本文的代码示例:github.com/roiyeho/medium/tree/main/mlp

除非另有说明,否则所有图片均由作者提供。

MNIST 数据集信息:

  • 引用: 邓丽君,2012。用于机器学习研究的手写数字图像的 MNIST 数据库。IEEE 信号处理杂志,29(6),第 141–142 页。

  • 许可证: Yann LeCun 和 Corinna Cortes 拥有 MNIST 数据集的版权,该数据集根据知识共享署名-相同方式共享 4.0 国际许可证CC BY-SA)提供。

感谢阅读!

多区域数据源

原文:towardsdatascience.com/multi-regional-source-of-truth-d43e1cc9e098?source=collection_archive---------17-----------------------#2023-03-14

以 BigQuery 作为数据仓库的多区域 BI 解决方案

Andrey ShalitkinTowards Data Science Andrey Shalitkin

·

关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 3 月 14 日

--

图片由 Lars Kienle 提供,来源于 Unsplash

我在一年前创建了一篇关于从 Postgres 迁移到 Snowflake 的文章,现在这里有另一个迁移案例。这一次我将集中于业务案例、架构和设计,而不是技术方面,但我也会尝试分享一些可能有用的技巧。

业务案例

这是一个 B2B(企业对企业)业务。公司是一个 AaaS(应用即服务)提供商。它的软件下载作为在线服务,供全球多个国家的客户使用。它主要需要一个 BI(商业智能)系统来为客户提供数据访问。以下是最有趣的要求:

  • 一些客户需要访问现成的仪表板和可视化,而有些客户希望将数据提取到他们的 DWH 中。

  • 由于客户在不同国家运营,数据应保持在最初创建的区域内。

  • 客户应该只能看到他们的信息。对于相同的区域,数据存储在同一数据库和相同表中,因此必须应用行级安全。

  • 在大多数情况下,客户希望实时数据。

让我先介绍一下现有架构的样子:

作者提供的图表

关键特性是读取只读副本以提取数据,没有 DWH,并且在其上使用 Looker。显而易见的优点是这是设置最快的解决方案。副本通常具有实时数据。Looker 负责数据转换、语义层和可视化。Looker 还通过不同的过滤器实现行级安全。不幸的是,这可能是唯一的优点,它还有许多缺点。

  • 应用任何现代数据工程技术的能力非常有限。我们不能保存转换后的数据,不能使用适当的工具进行转换、实现管道、自动测试等。基本上,唯一可用的工具是 Looker Derived Tables 功能。

  • 由于没有 DWH,无法从其他来源(如 CRM)提取数据,也无法混合来自不同服务的数据或上传任何静态字典。BI 系统仅限于由特定软件产品保存的数据。

  • 如我之前提到的,一些客户需要提取数据,并希望获得尽可能低级和原始的数据。Looker 在提供报告方面做得很好,但它在其语义层之上工作,语义层已经有一些联接,并且并非所有字段都可用,每个 Looker 探查都是为特定业务流程设计的。结构并未针对导出进行优化。

  • 性能。这可能是最大的问题,因为 OLTP 数据库不适合分析用途,并且由于我们使用的是只读副本,无法控制索引。这不仅导致初始性能缓慢,还会导致性能下降,因为没有索引时,数据越多查询速度越慢。

解决方案

解决这些缺点的最直接方法是引入 DWH 层,我们确实这样做了。新的设计如下所示:

作者提供的图表

流数据

市场上有多种流媒体提供商可供选择,我们选择了 Fivetran,但也可以看看 Stitch、Airbyte 或其他开源框架,如 Meltano 或 Singer。我们也考虑过 Google Storage Transfer Service,但它还处于原始状态,灵活性不足。

流媒体设置的整个过程当然不是没有问题的。以下是其中一些问题:

  • OLTP 数据库没有公共访问权限,因此必须为 Fivetran 设置 VPN 以拉取数据。这涉及一些网络配置,并需要反复讨论以决定既适用于 Fivetran 又适用于我们的解决方案。

  • Fivetran 增加了数据库的负载,因此我们必须设置额外的监控,确保流媒体不会影响主要功能。

  • 最初的计划是每 5 分钟同步一次数据,但由于更改量巨大,导致连接超时并在日志中抛出错误。Fivetran 能够恢复,因此没有数据丢失,但生成了混乱,使得很难区分实际错误和这些过载错误。没有简单的解决方案,只能放宽对实时数据的要求,减少数据拉取的频率。

转换数据

选择 BigQuery 主要是因为它满足了我们所有的要求,并且大多数现有基础设施也在 Google 云中,因此过渡本应顺利。

由于我们现在有了可读写的 DWH,我们能够利用 DBT 工具进行数据转换,并将来自不同来源的数据混合在一起。即使没有任何转换,通过 Looker 查询之前相同的数据,但从 BigQuery 中获取,性能也有了显著提高,有时对重表的提升超过了100 倍

我不会详细讨论 Looker 的数据准备,因为它非常领域特定。更有趣的是我们如何解决数据导出问题。总的来说,这很简单,我们只是给了客户对 BigQuery 的直接访问权限,但细节中的问题需要关注,所以让我分享一些细节。

实现的最简单要求是将数据保存在不同的区域。幸运的是,BigQuery 中每个数据集的区域是可配置的。Fivetran 将数据流入不同的数据集,而 DBT 模型则针对这些数据集分别运行,因此来自不同区域的数据完全不会混合。

实现行级安全性以确保客户仅能访问他们的数据要复杂一些。BigQuery 支持行级安全性,但由于我们使用 DBT,因此选择了不同的方法,以更好地控制数据暴露和更多自动化选项。

我们创建了一组 DBT 模型,这些模型根据当前连接用户的信息在 SQL 代码中应用安全性,并在 BigQuery 中创建视图。更准确地说:

  • 需要访问的每个客户都提供用户/服务帐户。IAM 角色具有对 BigQuery 的通用访问权限,但没有对任何特定数据集的访问权限,因此,初始状态下,新用户无法查询数据

  • 新用户电子邮件与客户识别信息一起添加到 DBT 种子文件中。不同地区的文件分别保存。这提供了用户-客户映射以及用户应该访问的数据集的信息。种子文件存储在 GitHub 中,因此我们可以在此文件的每次更改时运行 GitHub 操作

  • GitHub Action 在不同地区运行参数化的 DBT 项目。地区和数据不同,但结构相同,因此我们只需为每个视图编写一次 SQL 代码。DBT 上传种子文件,创建视图,最后一步是为各个用户提供对所需数据集的只读访问权限

  • 视图通过session_user()函数识别当前登录的用户电子邮件,将其与用户-客户映射连接,并筛选出与客户无关的所有内容。

这里有一个我想分享的非常具体的情况,即授予 oAuth 用户访问权限。在 Google IAM 中有两种类型的用户:服务帐户和 oAuth 用户。处理第一种用户比较简单,但并非所有下游系统都支持这种访问方式。例如,Tableau online 无法使用服务帐户。第二种类型是普通用户登录 Google 帐户的方式。有关如何确保这种用户仅访问特定数据集的文档非常有限,因此这里是一些提示:

  • 用户应该拥有BigQuery Job用户的所有权限,并额外具有bigquery.jobs.create权限

  • 数据集级权限可以像服务帐户一样授予,例如使用 SQL GRANT 命令

  • 用户不一定需要由相同的 Google 帐户创建。例如,来自其他组织的人可以获得访问权限,只要他们的 IAM 权限在我们的项目中配置正确

  • 小心组电子邮件。虽然数据集级别的访问权限正常工作,但session_user()返回的是用户的个人电子邮件,因此行级安全性无法与组电子邮件一起使用

最后一个要提到的提示是,由于我们客户端只能访问视图而不能访问表格,因此视图应该能够从其他数据集查询数据,而无需将原始数据暴露给客户端。BigQuery 具备这样的能力,可以允许一个数据集中的所有视图查询另一个数据集中的数据,而不受最终用户权限的限制。

BigQuery 监控

由于客户可以直接访问 DWH,因此了解每个客户如何使用这些功能非常重要。由于每个客户都与用户绑定,因此在用户级别的监控应该能够回答这个问题。我遇到了一篇关于如何做到这一点的有趣文章,我们刚刚在 Looker 中重新实现了相同的方法。

基础设施的另一个部分是 Github,更准确地说是 GitHub Actions。此功能允许创建执行工作流并设置计划。像这个这样的开源操作可以帮助实现这一点,唯一需要支付的就是 GitHub 的使用时间。

单一真实数据源

我最后要提到的是解决方案的灵活性。我已经描述了主要和最有趣的案例,但公司还有其他服务,凭借当前的解决方案,我们能够将所有这些服务的分析转移到我们的 DWH 系统中,并将其用作下游系统的真实数据源。换句话说,它也作为数据馈送存在。DBT 被用来为所有业务案例生成视图和表格,而数据集级别的访问允许在数据级别分离下游系统和用户。

DBT 的灵活性允许在一个项目中为不同的下游系统创建模型,这一过程通过前面提到的 GitHub 实现自动化。

多阶段推荐系统构建方法

原文:towardsdatascience.com/multi-stage-approach-to-building-recommender-systems-71a31e58ecb4?source=collection_archive---------4-----------------------#2023-01-01

发现构建推荐系统的最先进方法

Nathan ChengTowards Data Science Nathan Cheng

·

关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 1 月 1 日

--

多阶段推荐系统。图像来源 作者

# 1 信息超载的问题

我们都对搜索引擎和推荐系统并不陌生。没有它们,我们会被每秒钟产生的大量信息所淹没。这些信息可能有不同的数据格式 —— 文本图像音频视频 等。

从根本上讲,这些系统可以在拥有大量信息目录的情况下,根据用户的查询或个人资料,呈现、过滤和排序相关项目,使我们能够在信息的海洋中进行导航,否则用户会因信息过载而挣扎。

如本帖标题所提,我们将重点讨论推荐系统。我可能会在未来的文章中单独介绍搜索系统设计,请关注。

# 2 个行业推荐系统的用例

你需要一个推荐系统吗?

公*地说,80% 的公司不会有建立复杂推荐系统的需求。对于小型目录和少量产品类别,使用动态 SQL 查询就足够了。

如果你想例如,在项目描述页面上建立一个 “类似的项目…”“你可能也喜欢…” 面板,你会编写一个 SQL 查询,以检索当前 类别top xx items

如果你想在网站首页上建立一个 “为你推荐…” 面板,或在营销邮件中发送推荐项目列表,同样适用。只需检索用户在历史时间段内查看的顶级项目类别,并编写相同的动态 SQL 查询以基于用户的历史互动数据检索推荐项目。

探索你的网站的用户也能够扫描整个目录,以寻找他们需要的内容,而不会感到不知所措。

影响是否需要推荐系统的因素

项目目录大小:

  • 随着目录规模的增加,通过手动过程标记每个项目的元数据变得更加繁琐和昂贵。

  • 在市场等商业模式中,其中项目列表完全由用户众包,确保高质量的项目元数据标签几乎是不可能的。

  • 我们必须寻找生成推荐的替代方法,而不仅仅是使用简单的 SQL 查询。

缺乏结构化数据:

  • 对于某些公司,表格格式的结构化数据可能甚至没有被收集。相反,他们可能拥有一个包含视频、图像、自由文本、文章的存储库,这些可以被挖掘用于生成推荐。

  • 使用非结构化数据作为该想法的基础需要采用深度神经网络方法,将非结构化数据编码为结构化的数值数据,以学习有意义的表示。

  • 结构化 — 表格格式的数据,具有明确定义的模式。

  • 非结构化 — 文本图像音频视频

可重复的业务指标改进过程:

  • 如果没有可重复的实验过程,就无法测试新技术来改进对用户的推荐。此外,也无法监控系统更改是否对关键业务指标产生了实际影响。

  • 推荐系统已被证明能够改善业务指标,例如点击率、转化率、加购物车购买、新项添加到购物车、收听/观看时间等。

  • 实施第一个推荐系统并不一定能保证你会有一个可重复的实验过程开始,但它是迈向这一目标的第一步。

推荐系统案例研究

以下是一些社会中普遍存在的推荐系统示例用例,按行业分类:

行业中的推荐系统使用案例:

在线教育、在线银行、保险也是我期望找到成功案例的行业,但信息很少。

构建和设计推荐系统

在现实世界中,IT 系统通常是多个服务共同工作的集合。构建推荐系统是一种分层、多阶段的过程,每个阶段使用适当类型的模型。

让我们来看看机器学习工程师在构建推荐系统时所经历的阶段。

# 3 推荐系统设计

多阶段推荐系统。图片由 作者 提供。

上图展示了构建推荐系统的事实标准。最先进(SOTA)的推荐系统将系统工程分解为这个三阶段的过程。

关于模型服务的说明: 在每个阶段,我们还需要考虑如何暴露或服务已经为下游消费者构建的机器学习模型。

我们将在后续部分中探讨每个阶段。

候选生成;检索

候选生成在一些推荐系统文献中也被称为检索阶段。项目目录可能会增长到数十万、数百万或数十亿项。

候选生成方法能够通过高效比较项目之间的相似性,使用距离度量选择前一百个最相关的项目。

首要的想法是学习用户和项目的数值向量表示,即输入数据。这些向量,称为嵌入,可以被写入数据库或最*邻索引中,后续系统可以从中检索它们。

一旦我们有了用户和项目的嵌入,就可以使用距离度量如欧氏距离、点积、余弦相似度等计算它们在嵌入空间中的相似性。

候选生成旨在通过一种高效的方法来缩小潜在候选的数量,该方法返回数百个候选结果,并在下一阶段进行排序。通常,会建立多个候选生成器,这些生成器使用不同的方法进行训练,每个生成器利用其特定的数据集。

为什么不直接进入评分阶段?

一个常见且合理的问题是,为什么不直接进入评分阶段?为什么要经过生成嵌入的过程来进行候选生成?

这里有几个需要考虑的要点:

  1. 生成嵌入是一个关键的特征工程步骤。这些向量值可以在下游作为输入特征值重复使用,以进一步提高下游评分模型的准确性。

  2. 候选生成模型可以在非结构化数据上进行训练,并使用多维向量表示项或用户的非结构化数据属性。这些有用的信息,如文本图像音频视频,否则将被忽视。

  3. 嵌入空间中每个项或用户的语义视觉理解。你可以使用如TensorBoard Embedding Projector这样的工具在二维空间中可视化高维向量。这样,你也可以观察,并应用聚类算法发现相似对象的簇。

  4. 候选生成模型的输出旨在建立项或用户之间的相似性。这个想法在大多数推荐系统产品中是基础的。例如“类似于…”“与你相似的用户也喜欢…”

  5. 高效服务模型结果。使用*似最*邻(ANN)引擎来服务候选生成模型也很高效。有关 ANN 算法的更多信息,请参阅视频:Approximate Nearest Neighbors: Data Science Concepts与评分阶段对比——在评分模型训练过程中,你还需计算所有工程特征,API 才能返回模型结果。

如前所述,服务这些嵌入是通过暴露 ANN 引擎来完成的。市场上有许多选择,包括开源和公共云,以下是一些选项:

只需使用这些服务中的一个,选择你的距离相似性度量,下游消费者可以调用 API 以检索对象嵌入。

另一种方法是将对象嵌入写入数据库,下游系统查询数据库以检索这些嵌入。这本质上就是Spotify 使用 Cloud Bigtable所做的。

评分;排名

评分在一些推荐系统文献中也被称为排名阶段。它是在候选生成之后发生的阶段。下图展示了这一过程的可视化表示:

多候选生成模型。图片由作者提供。

在从多个候选生成模型中检索出前一百个候选项后,每个候选项的嵌入可以与有关项目和用户的其他输入数据(如日期时间、类别、设备类型等)一起组织成结构化的表格格式。评分是一个监督学习问题,因此我们必须确保传递相关业务指标的标记训练数据。

评分是一个可选的阶段

根据你是否有标记的训练数据,你的公司可能没有评分阶段。这完全没问题,有些公司仅依靠候选生成阶段来支持它们的推荐系统产品。为内部数据工程过程建立正确的遥测以生成标记的训练数据集可能需要更多的时间和精力。

评分是一个分类问题。分类模型的输出是一个概率评分,能够让你按从最可能提升业务指标的候选项到最不可能提升的候选项进行排名。这些业务指标的示例包括点击率、视频观看时间、项目添加到购物车等。

为了服务于评分模型的输出,你可以发布一个带有模型版本的 API 端点。下游系统需要调用 API,使用基本输入特征(我们必须小心不要引入数据泄漏),然后评分模型将计算工程特征,并返回一个按概率评分排序的项目列表。

在所有 3 个公共云提供商上部署这样的端点有简单的选项:

同样,若通过批处理端点检索的排名结果可以缓存到数据库中,以便稍后由下游应用程序检索。

重新排序

在通过分配概率评分对项目进行排序后,系统还可以重新排序这些有序的项目列表。重新排序有几个好处,即:

  1. 项目质量——去除恶意项目,如虚假/盗版/诈骗产品、低质量的项目列表、点击诱饵等,这些可能会对用户信心产生负面影响,阻止他们使用*台。

  2. 项目多样性——我们可以推荐用户以前未见过的项目,或者推荐用户通常查看的不同类别的项目,以鼓励*台上的发现和探索。

当你针对业务指标进行优化时,你可能会遇到指标腐败——这会鼓励不良行为者采取某些恶意行为。重新排序阶段旨在通过考虑与评分阶段优化的主要指标互补的其他指标来减轻这一问题。

重新排名的过程可以是简单的业务规则,以确保填充项目的强制字段,或者可以训练一个独立的模型来学习恶意行为者或具有恶意意图的项目的特征。服务这些模型将类似于在评分阶段描述的方法。

为了促进*台上新项目的发现并鼓励多样性,公司在向最终用户提供排名项目列表时,也利用多臂老丨虎丨机算法的变体,以在探索与开发之间取得*衡,但这是一个需要专门博客文章讨论的主题。

# 4 特别感谢 / 参考文献

特别感谢以下资源的作者和创作者,他们帮助我写了这篇文章:

最初发表于 https://natworkeffects.com 于 2023 年 2 月 10 日。

多任务架构:综合指南

原文:towardsdatascience.com/multi-task-architectures-9bee2e080456?source=collection_archive---------13-----------------------#2023-07-18

实时多任务推理的轻量级模型

Isaac BerriosTowards Data Science Isaac Berrios

·

关注 发表于 Towards Data Science ·10 分钟阅读·2023 年 7 月 18 日

--

图片由Julien DuduogluUnsplash提供

介绍

你是否曾经想过如何训练一个深度神经网络来完成多项任务?这样的模型被称为多任务架构,相较于使用单独模型来处理每个任务的传统方法,它具有一定的优势。多任务架构是多任务学习的一个子集,这是一种训练模型或模型集合以同时执行多个任务的通用方法。

在这篇文章中,我们将学习如何训练一个模型同时执行分类和回归任务。本文的代码可以在GitHub找到。以下是概述:

  • 动机 — 我们为什么要这样做?

  • 方法 — 我们将如何实现这一目标?

  • 模型架构

  • 训练方法

  • 推理 — 检查性能并从一个 有趣的失败 中学习

  • 结论

动机

我们为什么要使用轻量级模型?这不会降低性能吗?如果我们不部署到边缘,难道不应该使用尽可能大的模型吗?

边缘应用需要轻量级模型以进行实时推理并减少功耗。其他应用也可以从中受益,但具体如何受益呢?轻量级模型的一个被忽视的好处是它们对计算的要求较低。通常,这可以降低服务器使用,从而减少功耗。这总体上具有降低成本减少碳排放的效果,后者可能在未来的 AI 中成为一个主要的问题

轻量级模型有助于降低成本和减少碳排放,因为它们消耗更少的电力

话虽如此,多任务架构只是工具箱中的一个工具,所有项目需求应在决定使用哪些工具之前加以考虑。现在让我们深入探讨一下如何训练其中一个模型吧!

方法

为了构建我们的多任务架构,我们将大致涵盖这篇论文中的方法,该论文中训练了一个模型以同时进行分割和深度估计。其基本目标是以快速高效的方式完成这些任务,同时可以接受性能上的损失。在多任务学习中,我们通常将相似的任务组合在一起。在训练过程中,我们还可以添加一个辅助任务,这可能有助于模型的学习,但我们可能会选择在推理过程中不使用它[1, 2]。为简化起见,我们在训练过程中不会使用任何辅助任务。

深度和分割都是密集预测任务,并且有相似之处。例如,单个物体的深度在物体的所有区域内可能是一致的,形成一个非常狭窄的分布。主要的想法是每个物体应该有其自己的深度值,我们应该能够仅通过查看深度图来识别单个物体。以同样的方式,我们应该能够通过查看分割图来识别相同的单个物体。虽然可能会有一些异常值,但我们将假设这种关系是成立的。

数据集

我们将使用City Scapes 数据集提供(左侧摄像头)输入图像分割掩码和深度图。对于分割图,我们选择使用标准训练标签,共 19 类+ 1 类未标记类别。

深度图准备—默认视差

使用SteroSGBM创建的视差图可以从 CityScapes 网站上轻松获取。视差描述了从每个立体摄像头的角度观察物体的像素差异,它与深度成反比,可以通过以下公式计算:

City Scapes 深度计算,单位见括号。来源:作者。

然而,默认的视差图存在噪声,许多孔对应于无限深度,以及一个总是显示自车的区域。清理这些视差图的常见方法包括:

  1. 裁剪底部 20%以及左边和顶部边缘的部分

  2. 调整到原始比例

  3. 应用*滑滤波器

  4. 执行inpainting

一旦我们清理了视差图,就可以计算深度,结果如下:

图 1. 来自 City Scapes 的深度数据。来源:作者。

这种方法的详细信息超出了本文的范围,但如果你感兴趣,这里有一个YouTube视频解释。

裁剪和调整步骤意味着视差(以及深度)图将不会完全与输入图像对齐。虽然我们可以对输入图像进行相同的裁剪和调整以纠正此问题,但我们选择探索一种新方法。

深度图准备—CreStereo 视差

我们探索了使用CreStereo从左右图像生成高质量视差图。CreStereo 是一个先进的模型,能够从立体图像对中预测*滑的视差图。这种方法引入了一个称为知识蒸馏的范式,其中 CreStereo 是教师网络,我们的模型将是学生网络(至少在深度估计方面)。这种方法的细节超出了本文的范围,但如果你感兴趣,这里有一个YouTube链接。

通常,CreStereo 深度图的噪声最小,因此不需要裁剪和调整。然而,分割掩码中存在的自车可能会导致泛化问题,因此我们在所有训练图像中去除了底部 20%。训练样本如下所示:

图 2. 训练样本。来源:作者。

现在我们有了数据,让我们来看看架构。

模型架构

根据[1],架构将包括一个 MobileNet 主干/编码器,一个LightWeight RefineNet 解码器,以及用于每个单独任务的头部。整体架构如图 3 所示。

图 3. 模型架构。

对于编码器/主干,我们将使用一个MobileNetV3并将 1/4、1/8、1/16 和 1/32 分辨率的跳跃连接传递到 Light Weight Refine Net。最后,输出将传递到每个负责不同任务的头部。请注意,如果需要,我们甚至可以向该架构中添加更多任务。

图 4.(左)详细的编码器-解码器多任务架构。(右)LightWeight RefineNet 块的详细信息。修改自

为了实现编码器,我们使用预训练的 MobileNetV3 编码器,将 MobileNetV3 编码器传递给自定义 PyTorch 模块。其前向函数的输出是一个ParameterDict的跳跃连接,用于输入到 LightWeight Refine Net。下面的代码片段展示了如何实现这一点。

class MobileNetV3Backbone(nn.Module):
    def __init__(self, backbone):
        super().__init__()
        self.backbone = backbone

    def forward(self, x):
        """ Passes input theough MobileNetV3 backbone feature extraction layers
            layers to add connections to
                - 1:  1/4 res
                - 3:  1/8 res
                - 7, 8:  1/16 res
                - 10, 11: 1/32 res
           """
        skips = nn.ParameterDict()
        for i in range(len(self.backbone) - 1):
            x = self.backbonei
            # add skip connection outputs
            if i in [1, 3, 7, 8, 10, 11]:
                skips.update({f"l{i}_out" : x})

        return skips

LightWeight RefineNet 解码器与[1]中实现的解码器非常相似,只是进行了少许修改以使其与MobileNetV3兼容,而不是 MobileNetV2。我们还注意到解码器部分包含了分割和深度头。该模型的完整代码可以在GitHub上找到。我们可以按如下方式组装模型:

from torchvision.models import mobilenet_v3_small

mobilenet = mobilenet_v3_small(weights='IMAGENET1K_V1')

encoder = MobileNetV3Backbone(mobilenet.features)
decoder = LightWeightRefineNet(num_seg_classes)
model = MultiTaskNetwork(encoder, freeze_encoder=False).to(device)

训练方法

我们将训练分为三个阶段,第一阶段为 1/4 分辨率,第二阶段为 1/2 分辨率,最后阶段为全分辨率。所有权重都会更新,因为冻结编码器权重似乎效果不佳。

转换

在每个阶段,我们执行随机裁剪调整大小、颜色抖动、随机翻转和归一化。左侧输入图像使用标准图像网均值和标准差进行归一化。

深度转换

通常深度图包含大多数较小的值,因为深度图中包含的大部分信息都是相机附*的物体和表面。由于深度图的大部分深度集中在较低的值附*(见下图 4 的左侧),因此需要对其进行变换,以便神经网络能够有效学习。深度图被裁剪在 0 到 250 之间,这是因为大距离的立体视差/深度数据通常不可靠,在这种情况下,我们希望丢弃它。然后,我们取自然对数并除以 5,以便将分布浓缩到较小范围的数字上。有关更多细节,请参见这个 笔记本

图 4. 左 — 裁剪后的深度分布。右 — 变换后的深度分布。深度是从 64 个随机全尺寸训练深度掩模中采样的。来源作者。

老实说,我不确定最佳的深度数据变换方法。如果有更好的方法或您会以不同的方式进行,我很想在评论中了解更多 😃.

损失函数

我们保持损失函数简单,分割使用交叉熵损失,深度估计使用均方误差。我们将它们加在一起,不加权,并联合优化。

学习率

我们使用了一个周期余弦退火学习率,最大值为 5e-4,并在 1/4 分辨率下训练 150 个周期。用于训练的笔记本位于 这里

图 5. 一个周期余弦退火学习率。来源作者。

我们在 1/2 分辨率下微调了 25 个周期,然后在全分辨率下再次微调了 25 个周期,学习率为 5e-6。请注意,每次我们在增加分辨率时微调时,都需要减少批量大小。

推断

在推断过程中,我们对输入图像进行了归一化,并通过模型进行了前向传播。图 6 显示了来自验证和测试数据的训练结果。

图 6. 推断结果(前两个是来自测试集,后两个是来自验证集)。来源作者。

通常情况下,当图像中存在较大的物体时,模型似乎能够进行分割和深度估计。当出现更精细的物体,如行人时,模型往往难以完全分割它们。模型能够在一定程度上准确估计它们的深度。

一个有趣的失败

图 6 的底部展示了一个有趣的失败案例,即未能完全分割图像左侧的灯柱。分割仅覆盖了灯柱的下半部分,而深度图显示灯柱的下半部分比上半部分更*。深度的失败可能是由于下部像素通常对应于较*的深度;注意到像素 500 附*的地*线,存在明显的分界线,将较*的像素和较远的像素区分开来。似乎这种偏差可能泄漏到了模型的分割任务中。这种任务泄漏应在训练多任务模型时考虑。

在多任务学习中,一个任务的训练数据可以影响另一个任务的表现。

深度分布

让我们检查预测的深度与真实深度的分布情况。为了简化,我们将只使用 94 对真实/预测的全分辨率深度图样本。

图 8. 真实(左)和预测(右)深度图分布,每个分布有 1000 个区间。来源作者。

似乎模型学习到了两个分布,一个在 4 附*有峰值,另一个在 30 附*有峰值。注意到剪切伪影似乎没有产生影响。总体分布包含了一个长尾,这表明只有图像的一小部分包含远处的深度数据。

预测的深度分布比真实值更*滑。真实值分布的粗糙度可能是由于每个物体包含相似的深度值。可能可以利用这些信息应用某种正则化来强制模型遵循这一范式,但那将是另一个问题。

额外内容:推理速度

由于这是一个旨在追求速度的轻量级模型,让我们看看它在 GPU 上的推理速度。下面的代码已从这篇文章中修改。在这个测试中,输入图像被缩小到 400x1024。

# find optimal backend for performing convolutions 
torch.backends.cudnn.benchmark = True 

# rescale to half size
rescaled_sample = Rescale(400, 1024)(sample)
rescaled_left = rescaled_sample['left'].to(DEVICE)

# INIT LOGGERS
starter, ender = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True)
repetitions = 300
timings=np.zeros((repetitions,1))
#GPU-WARM-UP
for _ in range(10):
    _, _ = model(rescaled_left.unsqueeze(0))
# MEASURE PERFORMANCE
with torch.no_grad():
    for rep in range(repetitions):
        starter.record()
        _, _ = model(rescaled_left.unsqueeze(0))
        ender.record()
        # WAIT FOR GPU SYNC
        torch.cuda.synchronize()
        curr_time = starter.elapsed_time(ender)
        timings[rep] = curr_time

mean_syn = np.sum(timings) / repetitions
std_syn = np.std(timings)
print(mean_syn, std_syn)

推理测试表明,该模型可以以 18.69+/-0.44 毫秒或约 55Hz 的速度运行。需要注意的是,这只是一个在搭载 NVIDIA RTX 3060 GPU 的笔记本电脑上运行的 Python 原型,不同的硬件会改变推理速度。我们还应该注意,如果在 NVIDIA GPU 上部署像Torch-TensorRt这样的 SDK,可以显著提高速度。

结论

在这篇文章中,我们了解了多任务学习如何节省成本和减少碳排放。我们学习了如何构建一个轻量级的多任务架构,能够在 CityScapes 数据集上同时执行分类和回归。我们还利用了 CreStereo 和知识蒸馏来帮助我们的模型更好地预测深度图。

这个轻量级模型在速度和效率之间做出了权衡。即使有这个权衡,训练后的模型仍然能够在测试数据上预测出合理的深度和分割结果。此外,它还能够学习预测与真实深度图类似的深度分布。

参考文献

[1] Nekrasov, Vladimir 等人. ‘实时联合语义分割和深度估计使用不对称注释’. CoRR, vol. abs/1809.04766, 2018, arxiv.org/abs/1809.04766

[2] Standley, Trevor 等人. ‘在多任务学习中应该一起学习哪些任务?’ CoRR, vol. abs/1905.07553, 2019, arxiv.org/abs/1905.07553

[3] Cordts, M., Omran, M., Ramos, S., Rehfeld, T., Enzweiler, M., Benenson, R., Franke, U., Roth, S., & Schiele, B. (2016). 城市景观数据集用于语义城市场景理解. 2016 IEEE 计算机视觉与模式识别会议 (CVPR). doi.org/10.1109/cvpr.2016.350

多任务机器学习:同时解决多个问题

原文:towardsdatascience.com/multi-task-learning-4531eb32d77b

在自然语言处理和计算机视觉中,有些是监督的,有些是无监督的,有些是自监督的。

Arun JagotaTowards Data Science Arun Jagota

·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 4 月 27 日

--

图片由 Gerd Altmann 提供,来源于 Pixabay

单任务学习是从标记数据集中学习预测单一结果(二元、多类别或连续)的过程。

相比之下,多任务学习是指在相同模态的输入上联合学习以预测多个结果的过程。例如图像或文本。

显而易见的问题是,为什么要联合学习?为什么不独立学习单任务模型来预测各种结果?

答案是,联合学习可以学习到在多个任务之间更好地泛化的特征。那些能够作为多个任务的良好预测器的特征会比那些不能的特征更受青睐。这些学习到的特征甚至可能在相同领域的新预测任务中进行泛化。

将无监督学习添加到其中

到目前为止,我们假设多任务设置中的所有选择任务都是监督性质的。让我们放宽这一假设,允许一些任务为无监督任务。

为什么?因为我们可能有更多的数据可以进行训练。一些是用于各种结果的标记数据,还有很多未标记数据。将无监督任务添加到联合学习中可以让我们从更大规模的数据集中进行学习。

我们实际上可以通过实证测试这个假设。我们可以保留一部分标记数据集,并进行两个实验:一个是在其余标记数据上训练,另一个是在其余标记数据和大量未标记数据的组合上训练。然后,我们可以比较这两个实验中学习到的模型在保留(标记)测试集上的质量。

事实上,从标记和未标记数据的混合中学习的概念已经有了一个名称,称为半监督学习。

将无监督学习精细化为自监督学习

虽然允许一些任务为无监督任务使我们能够使用潜在的大量未标记数据,但我们能否更好地利用未标记数据?一般的回答是肯定的。使用自监督。

在自监督中,我们定义监督任务,以从其余数据中预测数据的某些特征。例如,我们可能会预测从先前看到的单词中出现的下一个单词。

自监督比无监督学习更强大,因为它是有监督的。此外,它从所有未标记的数据中学习,因为它不需要人工标记。最后,自监督是获得大量多样化标记数据的强大机制,无需人工工作。在计算机视觉和自然语言处理部分,我们将提供简洁且现实的例子,揭示我们如何构建多样化的标记数据集,并且这种方法如何提高学习能力,相较于完全不进行自监督。

在本文中,当我们说多任务学习时,我们真正指的是至少有两个监督学习任务的学习,可能还有额外的无监督学习任务,以及可能的额外自监督任务。

我们在下面以视觉方式展示了这一点。

图 1:(作者提供)某种模态的数据的维恩图。实心圆表示用于各种监督任务的标记子集。虚线圆圈表示用于各种自监督任务的自标记子集。

请注意,此图仅描绘了输入数据的子集,涵盖了各种监督和自监督任务,而不是不同任务标签之间的关系。

图 1 也描绘了

  • 标记用于各种监督任务的数据子集往往较小。

  • 添加自监督任务可以覆盖更多的数据。这些子集通常可以做得更大。

  • 监督任务的子集可以相互重叠,但通常不完全相同。

放宽本文中“联合学习”一词的定义

到目前为止,我们给人的印象是联合学习意味着从所有任务的所有可用数据中同时学习一个模型。

实际上,我们希望放宽这一点以允许更灵活的训练策略。这是因为在实践中,发现首先从未标记数据中学习自监督(以及可能的无监督)任务,然后进一步调整这些模型以进行特定的下游监督任务是有效的。这种方法的一种更灵活的方式是,它自然允许在实时定义新的监督任务并根据需要微调现有模型。

关于多任务学习的数据

我们需要多个标记数据集,每个数据集用于预测一个结果。这些数据集中的输入通常来自相同领域。实际输入在数据集中可能会有所不同,因为它们通常来自不同来源。

我们可能还有未标记的数据。只要我们合理地相信这些数据会有帮助,就应该将其用于适当的无监督任务。如前一节所述,我们可以在决定之前通过实证测试我们的信念。如果适当的话,我们还应定义适合的自我监督任务,以便利用和学习从其他数据中预测未标记数据的某些特征。

多任务学习在自然语言处理和计算机视觉中尤其有用。我们将逐一讨论这两个领域。

计算机视觉中的多任务学习

想象一下,我们有手写数字、汽车、人脸图像、宠物图像等标签图像。还假设这些数据集中有些是小的。

为每个任务单独构建单任务模型的风险在于我们可能没有足够丰富的标记数据。

将所有数据集结合起来,学习一个单一的多任务模型,可能有助于缓解上述特定任务数据稀疏的问题,只要一些共同学习的特征也有助于这些任务。

思路是图像就是图像,在某些足够低的层次上,某些特征可能对多个任务的结果具有预测性,比如边缘或循环。

在这种情况下无监督任务的价值

我们之前提到,在多任务学习中加入适当的无监督任务是有益的。我们将在图像的设置中进一步阐明这一点。

网络上有数十亿张图像,大多数是未标记的。假设我们有一些特定任务的标记图像,比如预测图像中是否有建筑物。

直觉上,使用大量未标记图像进行无监督学习可以帮助发现比仅使用标记图像更好的特征,比如更好的边缘和更好的循环。

未标记数据上的自监督学习任务

如前一节所述,通常有大量的未标记图像数据。数十亿张图像。在前一节中,我们关注了半监督学习。即除了这些未标记图像外,我们还拥有一些用于特定任务的标记图像。我们希望从未标记图像中以无监督方式学习,从标记图像中以监督方式学习。

引入自监督学习可以大大提升这种方法。这是因为合适的自监督任务迫使模型学习从其他特征预测图像的一些特征。这通常可以轻松地产生大量标记数据,而不需要人工付出任何努力。

一些特定形式的自监督

以下是一些通常适用于图像数据的自监督的具体形式。

掩盖某些区域

取一张图像并模糊掉某些整个区域。创建一个新实例,其中模糊的图像是输入,原始实例是其标签。

为了激发我们对这种方法如何帮助的想象,假设我们有一些人的图像,有些人戴着帽子,有些人没有。如果我们以某种方式掩盖所有人的脸,我们可能会学到帽子下方是人的头,但反过来就不一定,即不是所有头上都有帽子。

这引出了一个问题,我们如何确定要掩盖哪些内容?我们将以不同的角度来看待这个问题。我们不尝试优化掩盖,而是建议考虑将图像分割成一定尺寸的矩形网格,并创建每个实例中掩盖一个单一网格的情况。通过选择网格的细粒度,我们可以控制从任何一个图像中获得多少被破坏的实例。

然后我们可以自动化发现哪些被破坏的图像可以被很好地重建。我们甚至可以检验这种自监督是否提高了一个或多个下游监督任务的准确性。

去色图像

通过将彩色图像变为灰色,我们可以迫使模型学习从剩余属性中尝试重建颜色。例如,它可能会学到,停牌通常是红色的,因为它们有独特的形状,甚至上面还写着Stop

降低分辨率

我们可以降低图像的分辨率,并让模型尝试重建原始图像。这可能迫使模型在可能的情况下学习填补细节。作为一个想象中的生动例子,模型可能会学会填补人脸的某些细节,比如睫毛。

NLP 中的多任务学习

考虑一个文本句子。我们可能会对标记每个词的词性、标记某些段落为命名实体以及标记某些段落为名词短语或动词短语感兴趣。

在单任务学习中,每个任务都会被独立处理。

在多任务学习中,我们会学习一个具有共享特征的模型,同时处理所有这些任务。当任务之间有协同作用时,这可以产生更好的模型。例如,学习预测句子中每个词的词性可能有助于检测命名实体、名词短语和动词短语。

在未标记数据上添加合适的任务

可能有很多句子没有标签。没有标签意味着没有词性标注,没有命名实体标注,也没有名词或动词短语标注。

我们应该考虑定义自监督学习任务,即根据目前为止看到的词来预测句子中的下一个词。这个任务可以以强大的方式利用未标记的数据。预测下一个词可以促使网络学习前面词的复杂表示。

更一般地说,定义自监督学习任务来预测句子中剩余的某些被屏蔽的词可能会很有用。我之所以说“更一般地”,是因为这些词可以出现在句子的任何地方。

这是一个例子。考虑

阳光暴露会导致皮肤癌。

我们可能考虑预测的三个被屏蔽的实例在下面两个填空场景中展示。

_____ 导致皮肤癌。

阳光暴露会导致 ______。

_ 到 _ 导致皮肤癌

在第一个场景中,我们根据其余部分预测句子的左尾。在第二个场景中,我们根据其余部分预测句子的右尾。第三个场景展示了可以有多个不连续的被屏蔽子序列需要预测。

预测被屏蔽词背后的直觉

为什么要从预测下一个词扩展到预测任何被屏蔽的词的子序列?简短的回答是:(i)这大大扩展了可用于训练的标记数据集,(ii)并且有助于在新标记的实例中呈现新类型的场景。

为了提高我们对(i)的直觉,想象一下我们为由 n 个词组成的句子的所有可能屏蔽生成被屏蔽的标记实例。屏蔽的数量大约是 2^n。相比之下,将句子切分以预测未来的词的方式大约是 n 种。

为了详细说明(ii),通过允许屏蔽出现在任何地方,我们允许模型发现潜在地比从左到右限制的关系更一般的预测关系。为了想象这一点,假设我们在语料库中有很多 X 导致 Y 的实例。同时,假设只有 X 导致 Y。如果 X 被屏蔽,模型可以从数据中学习到只有 X 导致 Y 的关系。而从左到右的语言模型不能学习到只有 X 导致 Y

第二个非常简单的例子

想象一下我们有一个汽车名称的列表。例如

本田思域,丰田凯美瑞,福特野马,吉普牧马人。

预测下一个词肯定会帮助模型学习特定品牌的模型。例如CherokeeWrangler(等)对于Jeep。然而,以屏蔽任何词的形式进行额外的自监督将帮助模型学习到品牌名称往往能够强烈预测品牌。例如,CelicaToyota 的,MustangFord 的,WranglerJeep 的,等等。

特定案例

这种类型的自监督的两个特殊案例是连续词袋模型(CBOW)和跳字模型[3]。前者对应于在某个数量的左侧和右侧上下文中掩盖中间词。后者则相反——掩盖左侧和右侧的上下文,同时保持中间的词不变。

下面是我们示例中展示的两个。

exposure to sunlight causes skin cancer.    # Original
exposure to sunlight _ skin cancer.         # Masking for CBOW
_ _ _ causes _ _                            # Masking for Skip-gram.

另一个被发现有用的自监督任务是下句预测[2]。特别是对于问答系统。在[2]中,这个任务被表述为一个二分类问题。输入(X1,X2)被解释为“句子 X1 后面跟随句子 X2”。正实例是通过语料库中相邻句子的对生成的。负实例是通过将语料库中的一个句子 X1 与语料库中的随机句子 X2 配对生成的。

在上述段落的背景下,“句子”一词指的是任何连续的文本序列[2]。因此,从当前段落预测下一个段落也会被视为下句预测问题。

利用深度架构

多任务学习可以显著利用深度架构。直觉是这样的。深度架构在靠*输入的层中学习低级特征,而在靠*输出的层中学习高级特征。直觉上认为,学习低级特征的较深层次恰恰是那些可以在任务之间共享的层。

考虑我们的多任务图像分类场景。可以合理推测,像边缘或循环这样的低级特征将在这些任务中的多个任务中成为有用的预测因子。

下面展示了一个深度架构,其中输入层靠*的层由所有监督任务共享,而靠*任务的层则是任务特定的。

图 2:一个具有共享层的多任务深度架构。(作者提供。)

训练可以通过常规的反向传播算法进行。训练集中的特定实例可能只有一个任务的标签。这个任务的预测与目标之间的误差通过任务特定层反向传播,然后传递到共享层。因此,如果我们混入来自不同任务的实例,共享层会学习到在任务间通用的表示。

总结

在这篇文章中,我们涵盖了多任务学习的话题。我们解释了为什么多任务学习可以产生理解和泛化能力比单独学习每个任务的模型更好的模型。当我们将无监督和自监督任务加入混合时,这种方法尤为强大。这些任务使我们能够从未标记的数据中学习很多。

我们还涵盖了计算机视觉和自然语言处理中的多任务学习。在这些设置中,我们讨论了具体的监督任务、可用数据的性质以及具体的自监督方法。

进一步阅读

  1. ai.facebook.com/blog/self-supervised-learning-the-dark-matter-of-intelligence/

  2. arxiv.org/pdf/1810.04805.pdf

  3. arxiv.org/pdf/1301.3781.pdf

  4. arxiv.org/pdf/2103.01988.pdf?fbclid=IwAR2pqhYda6MV9r2b3Afx_0eKUiZhX-Es6Pa_FbLOqH8fglQzO2kY3yKxZE8

推荐系统中的多任务学习:基础知识

原文:towardsdatascience.com/multi-task-learning-in-recommender-systems-a-primer-508e661a2029

试图做到这一切的算法背后的科学和工程

Samuel FlenderTowards Data Science Samuel Flender

·发布于 Towards Data Science ·阅读时长 8 分钟·2023 年 7 月 25 日

--

Mike KononovUnsplash 的照片

尽管多任务学习在计算机视觉和自然语言处理领域已经得到很好的应用,但在现代 推荐系统 中的使用仍然相对较新,因此理解还不够充分。

在这篇文章中,我们将深入探讨多任务推荐系统中的一些重要设计考虑因素和最新研究突破。我们将涵盖

  • 为什么我们首先需要多任务推荐系统,

  • 正向迁移和负向迁移:多任务学习者的关键挑战,

  • 硬参数共享和专家建模,以及

  • 辅助学习:为了提高主任务的性能而添加新任务的想法。

让我们开始吧。

为什么要使用多任务推荐系统?

多任务推荐系统的关键优势在于其能够同时解决多个业务目标。例如,在视频推荐系统中,我们可能希望优化点击量,但同时也要优化观看时长、点赞、分享、评论或其他形式的用户互动。在这种情况下,单一的多任务模型不仅比多个单任务模型在计算上更便宜,而且每个任务的预测准确性也可能更高。

即使在我们只想预测一个事件的情况下,比如电子商务推荐系统中的“购买”行为,我们仍然可以添加额外的任务,其唯一目的是提高主要任务的性能。我们称这些额外的任务为“辅助任务”,这种学习形式为“辅助学习”。在电子商务的例子中,除了“购买”之外,还可以学习“加入购物车”和“添加到列表”,因为这些事件彼此紧密相关:它们表示购物意图。

哪些任务可以很好地一起学习?

从高层次来看,预测第二个任务要么有助于第一个任务,要么产生相反的效果:使第一个任务的预测变得更差。我们称前者为“正向迁移”,后者为“负向迁移”。多任务学习的挑战在于仅学习那些具有正向迁移的任务,并避免负向迁移,因为后者可能会对模型性能产生不利影响。

多任务学习中的一个关键问题是哪些任务可以很好地一起学习。在许多情况下,我们可以凭借领域知识做出合理的猜测。我们在上面已经看到一个例子:“购买”和“加入购物车”都表示购物意图,因此应该在多任务学习者中表现良好(实际上,它们确实如此)。

然而,如果任务数量变得很大,我们可能需要算法上确定哪些任务应该一起学习,哪些应该分开学习。值得注意的是,这是一个 NP 难题,因为可能的任务分组数量随着任务数量的增加而呈指数级增长。这并不容易,但可以做到:在 2020 年的一篇 论文 中,斯坦福大学的作者使用“分支限界”算法解决了计算机视觉数据集上的任务分组问题,得出的解决方案优于多个单任务学习者和单一的多任务学习者。

3 种不同的多任务建模范式。图源自 Ma et al 2018

硬参数共享:多任务学习的幕后

构建多任务神经网络的最简单方法是一个被称为“硬参数共享”或“共享底部”的技术,在这种方法中,我们将一个共享底部模块与任务特定的顶部模块结合在一起。通过这种方式,底部模块可以学习任务通用的模式,而顶部模块则可以学习任务特定的模式。(“模块”在这里指的是具有特定激活函数的多层感知机(MLP))。

在最简单的情况下,任务特定模块可以是一个单一的输出神经元,用于进行预测。然而,在实践中,我们通常可以通过为每个任务添加一个专用模块来实现更好的性能,该模块可以学习数据的任务特定内部表示。

多任务学习者的输出将是一个预测列表,我们可以将其合并为最终损失,如

其中 p 是预测值,y 是标签,w 是(可选的)任务特定权重,控制每个任务的相对重要性。

专家建模

硬参数共享也许是解决多任务学习问题最常见且最简单的方法,但它有一个主要缺点:我们必须提前决定网络的哪些部分应该共享,哪些不应该。这要求我们知道哪些任务能很好地一起学习,哪些不能,但实际上,我们可能无法提前获得这些信息。

进入“专家建模”,也称为“专家混合”(MoE),追溯到 1991 年一些 AI 大咖,如 Robert Jacobs、Michael Jordan、Steven Nolan 和 Geoffrey Hinton 的论文。MoE 的关键思想是通过门控网络结合 N 个专家,根据输入数据选择最佳专家。在这里,

  • “专家”是一个处理数据的 MLP,结果是一个嵌入或预测,并且

  • “门控网络”只是一个 softmax 函数:

其中 x 是输入数据,W 是一个可学习的矩阵。换句话说,W(因此门控)学习根据输入数据选择正确的专家。

然而,MoE 只使用一个门控,这在有多个任务的情况下可能效果不好,每个任务都需要自己的专家集。因此,多任务推荐系统的一个突破是用多个门控替代 MoE 中的单一门控,每个任务一个,结果就是“MMoE”,即“多门控专家混合”,在 2018 年由 Google 提出的论文中介绍。

作者展示了 MMoE 在合成数据、普查数据以及大规模生产推荐数据集上均优于硬参数共享和 MoE。MMoE 特别适用于(相比于 MoE)任务相关性较低的情况,突显了拥有多个门控的优势。

辅助学习

MetaBalance 的关键思想是将辅助梯度缩放到与主要任务的梯度相匹配,如从左到右的图表过渡所示。图源自He et al 2022

在许多推荐问题中,通过联合学习辅助任务可以改善主要任务的预测性能。例如,

  • 在预测转化率时,当联合学习预测点击率的辅助任务时,预测性能会提高(Ma et al 2018),

  • 当尝试预测用户评分时,预测性能在联合学习辅助任务预测项目元数据(如类别和标签)时会得到改善(班萨尔等 2016)。

  • 当尝试预测新闻推送中的阅读时长时,预测性能会在联合学习辅助任务预测点击率时得到改善(赵等 2021),仅举几个例子。在所有这些情况下,辅助任务的目的是不是在推理时使用预测,而仅仅是提升我们试图学习的主要任务的预测性能。

辅助学习之所以有效,是因为我们添加了梯度信号,这有助于模型在参数空间中找到最佳潜在最小值,而当主任务的梯度信号稀疏时,这种额外的信号尤为有用。例如,转换率远低于点击率,因此预计后者的梯度信号更丰富,可以补充前者。

然而,已显示辅助学习中的梯度可能高度不*衡,以至于辅助梯度要么主导学习,要么根本无关紧要。这是一个问题,假设“MetaBalance”的作者,2022 年提出的高级辅助学习算法论文来自 Meta。MetaBalance 的关键思想是将辅助梯度缩放到与主要任务梯度相同的数量级。

形式化地说:

g_aux <-- g_aux * r * |g_main|/|g_aux|,

其中 g_aux 是来自辅助任务的梯度,g_main 是来自主要任务的梯度,r 是作者通过经验确定的超参数。论文中 r~0.7 似乎效果最佳:换句话说,当辅助任务的梯度与主要任务的梯度接*但不完全相同时,辅助任务帮助最大。

事实上,MetaBalance 展示了令人鼓舞的结果。作者考虑了两个电子商务购物数据集,其中目标是“购买”,辅助任务包括“点击”、“添加到购物车”和“添加到列表”。相较于单任务和普通(共享底层)多任务建模,MetaBalance 的改进是显著的。在一个问题中,他们将 NDGC@10 从 0.82(普通多任务)提高到 0.99(MetaBalance),提升了 17%!

总结

回顾一下:

  • 多任务学习很重要,因为现代推荐系统通常需要同时优化多个业务目标。

  • 不是所有任务都能很好地一起学习。任务可以互相帮助,产生正迁移,也可以相反——彼此对抗——产生负迁移。确定哪些任务可以一起学习是一个 NP-hard 问题!

  • 硬参数共享(即“共享底层”)是解决多任务学习最简单和最常见的方法。这是建立稳固基线时应该首先尝试的方法。

  • 专家建模,特别是 MMoE,是当前解决多任务学习问题的最先进技术,同时能够减轻负迁移。

  • 在辅助学习中,我们增加了额外的任务,其唯一目的是提高主要任务的性能。辅助学习在多个应用场景中已被证明能带来显著的建模改进,并且通过扩展辅助梯度可以进一步提升效果。

而这仅仅是冰山一角。在这个领域还有很多未解之谜:如何设计良好的辅助任务?辅助任务与主要任务的最佳比例是多少?最佳的专家数量是多少?它如何随着任务数量的增加而扩展?专家模块应该有多大?

请继续关注这个领域。新的突破无疑就在前方。

对现代推荐系统仍感到好奇?请继续阅读:

多层次回归模型与辛普森悖论

原文:towardsdatascience.com/multilevel-regression-models-and-simpsons-paradox-acb9820e836d?source=collection_archive---------2-----------------------#2023-08-08

使用适当的工具避免错误结论

Dorian DrostTowards Data Science Dorian Drost

·

关注 发布于 Towards Data Science ·10 分钟阅读·2023 年 8 月 8 日

--

数据分析会影响我们的结论,但我们应使用适当的工具来走上正确的道路。照片由Brendan Church拍摄,发布在Unsplash

数据分析——顾名思义——是数据科学家工作的重要组成部分,从描述性统计和简单的回归模型到复杂的机器学习方法。然而,这些方法需要小心处理,选择正确的方法远非简单。复杂的数据通常包含隐藏的结构,如果未得到适当考虑,可能会导致谬误,最终得出无效的结论。

在这篇文章中,我想举一个 辛普森悖论 的例子,并展示如何通过简单但目光短浅的分析导致看似有数据支持的错误结论,尽管这些结论不过是误解。通过这种方式,我演示了 多层回归模型 作为分析层次结构数据(即嵌套数据)的适当方法。

问题

我们现在就开始吧!假设我们已经发布了一个智能手机应用,并且想了解更多关于我们的用户及其满意度的信息。因此,我们进行了一项小调查,询问一些用户在 1(非常不满意)到 4(非常满意)的范围内对我们应用的满意度评分。此外,我们还测量了他们在过去一周内在应用中花费的时间,为了获取丰富的样本,我们询问了不同国家的用户。然后我们的数据可能会像这样(本文使用了生成的数据):

 Satisfaction  Time_spent  Country
0      2.140440    1.585295        0
1      2.053545    0.636235        0
2      1.589258    1.468033        1
3      1.853545    0.968651        2
4      1.449286    0.967104        2
.      .            .              .
.      .            .              .
.      .            .              .

我们对我们应用中花费时间与报告的满意度之间的关系感兴趣。准确地说,我们想知道在我们的应用中花费更多时间是否与更高或更低的满意度相关,并且我们想量化这种关联,即我们想做出类似于“在我们的应用中多花一小时与满意度提高 x 倍/降低 x 倍相关”的陈述。当我们查看数据时,我们可能已经有了第一直觉,即在应用中花费更多时间与较低的满意度相关:

线性回归

让我们做一个线性回归来验证我们的猜测。通过线性回归,我们尝试预测满意度,前提是时间花费作为形式为 satisfaction = intercept + regression_coefficient * time_spent 的线性函数。我们可以使用 statsmodels 包中的 OLS(普通最小二乘法)函数轻松完成这一点。

import statsmodels.api as sm
result = sm.OLS(df["Satisfaction"], sm.add_constant(df["Time_spent"])).fit()
print(result.params)

add_constant 方法只是我们用来告诉模型我们希望在方程中有一个截距的技术细节(这在数据未标准化时是必需的)。result.params 给出了两个值,即 interceptconst)和 regression_coefficient 对应的 Time_spent 变量。

const         3.229412
Time_spent   -0.655470

也就是说,我们的模型告诉我们,满意度可以预测为3.229 –0.655time_spent。换句话说,在应用程序中多花一小时的时间会导致满意度下降 0.655 分(由于负号)。然而,满意度并非从零开始,而是一个人从第一印象(即time_spent=0*)开始的*均满意度为 3.229。我们还可以将其以截距为 3.229 和斜率为-0.665 的线表示出来:

当然,这个预测并不完美,但至少它给出了一个趋势。好的,情况已经清楚了,对吧?在应用程序中花费更多时间会导致满意度下降,我们甚至可以量化这种下降。我们现在可以从中得出结论,并考虑如何改进应用程序(当然,我们希望用户在使用应用程序时更满意),或者进行更详细的调查,以找出用户不满意的原因。

慢一点!

按国家分组

记得我们从不同国家的用户那里收集了数据吗?如果我们分国家查看这些数据会发生什么呢?在下面的图表中,我们看到的是之前相同的数据点,但现在我们将每个国家用不同的颜色突出显示。

从这个图表中我们可以观察到两点。首先,各国在满意度和在应用程序中花费的时间上似乎有所不同。来自蓝色国家的受访者在应用程序中花费的时间更多,但相比其他国家的受访者,他们的满意度更低。更进一步,当我们将三个国家分开来看时,我们可能会认为应用程序使用时间与满意度之间的关系确实是正向的。这不是与我们之前的分析相矛盾吗?

辛普森悖论

实际上,这个名字并不是以那些辛普森家族命名的……照片由Stefan Grage提供,来源于Unsplash

我们刚刚看到的效果被称为辛普森悖论。当数据中的相关性在组之间与组内不同的时候,就会出现这种情况。尽管这一点非常违反直觉,但确实可能发生(正如我们刚刚看到的),其原因在于混杂变量。让我们用上述例子来解释。当孤立地看每个国家时,我们会看到一个正趋势:在应用程序中花费更多的时间与更高的满意度相关。然而,正如我们已经看到的,各国的*均满意度和应用程序使用时间不同。在蓝色国家,*均满意度较低,但在应用程序中花费的时间比橙色或绿色国家要长;这种趋势与国家内部的趋势相反。然而,可能还有其他变量导致这种情况。例如,可以想象,在蓝色国家,更多的人经常感到无聊,导致总体满意度较低(因此对我们的应用程序的态度较为消极),但在应用程序中花费更多时间。当然,这只是一个可能的解释,可能还有许多其他解释。然而,目前正确的解释并不是特别重要。对我们来说,重要的是理解不同国家之间存在系统性的差异。

那么,我们为什么在之前的分析中没有发现这一点呢?我们在进行线性回归时是否犯了错误?确实,因为完全进行线性回归是不对的,因为线性回归的一个核心假设被违反了:线性回归假设所有数据点是独立采样的,并且来自相同的分布。然而,在我们的例子中并非如此!显然,不同国家之间的应用程序使用时间和满意度的分布是不同的。现在,如果线性回归的假设被违反,线性回归就不是数据分析的合适工具。

层次模型

我们现在可以做什么,以便以更合适的方式分析我们的数据?幸运的是,有统计模型可以将线性回归的理念扩展到层次数据。我们说数据是层次数据,是指我们采样的数据点嵌套在一个层次结构中,就像在我们的案例中,受访者被嵌套在各个国家中。这些统计模型被称为层次线性模型多级模型线性混合效应模型。这些模型通过引入所谓的固定效应随机效应来考虑组结构。在一个简单的例子中,当我们想根据一个变量预测另一个变量时(比如我们想预测在应用程序中花费的时间与满意度的关系),固定效应包括一个截距和所有组共同的一个斜率。这与线性回归中的情况完全一样。

现在,随机效应可以引入每组单独的截距偏差。例如,蓝色国家的截距可能比固定截距略低,而绿色国家的截距可能比固定截距略高。这将解释国家间满意度均值水*的差异。

此外,随机效应可以引入每组的斜率偏差。例如,在橙色组中,斜率可能高于固定斜率(即,满意度与时间花费之间的关联更强),而在绿色国家中,斜率可能较低。

层次模型的实际效果

让我们通过实际操作来理解发生了什么。我们进行了一项新的分析,但这次我们使用了 statsmodels 的mixedlm函数。我们明确表示希望根据时间花费来预测满意度(而不是相反),使用公式“满意度 ~ 时间花费”,并指出数据框中的“国家”列用于确定不同的组。此外,参数re_formula="时间花费”告知模型我们希望每个组有一个单独的斜率。如果没有这一点,随机效应只会考虑组特定的截距,而不会考虑组特定的斜率。

import statsmodels.formula.api as smf

result = smf.mixedlm("Satisfaction ~ Time_spent", data=df, groups=df["Country"], re_formula="Time_spent").fit()
print(result.fe_params)
print(result.random_effects)

如果我们打印出固定效应fe_params)和随机效应,我们会得到如下值:

 Fixed effects
  Intercept     2.286638
  Time_spent    0.497657
Random Effects
  {0: Group -0.958805, Time_spent -0.018178,
   1: Group 0.155233,  Time_spent 0.274222,
   2: Group 0.803572,  Time_spent -0.256044}

那么,这意味着什么?对于固定效应,我们有一个截距值和一个变量时间花费的值。然而,对于随机效应,我们有每个国家(0,1,2)的两个值:一个是截距值(),另一个是我们变量的斜率值(时间花费)。正如我们上面看到的,随机效应描述了每组的偏差。对于我们的三个组,我们可以通过将随机效应加到固定效应的截距和斜率中来构造三个不同的线性方程。

satisfaction_0 = (2.286638 - 0.958805) + (0.497657 - 0.018178) * time_spent = 1.327833 + 0.479479 * time_spent
satisfaction_1 = (2.286638 + 0.155233) + (0.497657 + 0.274222) * time_spent = 2.441871 + 0.771879 * time_spent
satisfaction_2 = (2.286638 + 0.803572) + (0.497657 - 0.256044) * time_spent = 3.090210 + 0.241613 * time_spent

我们看到组 0 的随机截距为负值(-0.958),组 2 的随机截距为正值(0.803),所以组 0 低于固定截距,而组 2 高于固定截距。因此,组 0 在其线性函数中的截距最低(1.327),而组 2 最高(3.090)。换句话说,在国家 0 中,满意度的起始水*低于国家 2。

我们还看到组间的斜率存在差异。在组 1 中,斜率最高为 0.771,而在组 2 中仅为 0.241。这意味着在应用中的满意度与时间花费之间的关联在国家 1 中远高于国家 2。换句话说,在国家 1 中,每增加一个小时的应用时间,满意度会增加 0.771 分(均值),而在国家 2 中仅增加 0.241 分。此外,所有斜率都是正值,这与我们从上图预期的一致,但与我们一开始进行的线性回归的负斜率相矛盾。

现在我们可以为每个国家绘制一条回归线:

现在我们清楚地看到每个国家的积极趋势和不同的截距(即,当time_spent=0 时,线条的位置)。

结论

在上述示例中,我们看到短视的分析很容易导致虚假的结论。忽视数据的嵌套结构,即来自不同国家的用户,我们本可以在进行线性回归后就停止,得出更多时间花费在应用中与较低的满意度相关的结论。只有通过理解我们的数据不符合线性回归的核心假设,因为数据点不是从同一分布中抽取的,我们才有动机进行进一步分析,揭示了实际情况正好相反:更多时间花费在应用中确实与更高的满意度相关。

那么,让我们从这个例子中总结一些要点:

  • 在使用统计方法进行分析之前,应该验证其假设是否符合数据。

  • 对于嵌套数据,假设所有数据点都来自同一分布的情况可能并不总是成立。

  • 可能发生的情况是,整体数据的趋势与构成该数据的单个组内部的趋势不同。这被称为辛普森悖论。

  • 多级线性模型是应对嵌套数据结构并避免因辛普森悖论产生虚假结论的一种方法。

进一步阅读

我们使用了在statsmodels中实现层次模型的以下方法:

我使用了以下统计学教材(遗憾的是,这本书仅有德文版)。

  • Eid, M., Gollwitzer, M., & Schmitt, M. (2017). 统计与研究方法

关于多级模型的背景信息也可以在这里找到:

如果你想重现结果,数据是这样生成的:

import numpy as np
import pandas as pd
group_1_x = np.random.uniform(0.5, 1.8, 25)
group_1_y = (1 + 0.3 * group_1_x) + np.random.rand(len(group_1_x))

#start_2, end_2, step_2 = 0.3, 1.3, 0.04
group_2_x = np.random.uniform(0.3, 1.3, 22)
group_2_y = (2 + 0.7*group_2_x) + np.random.rand(len(group_2_x))

#start_3, end_3, step_3 = 0, 1, 0.04
group_3_x = np.random.uniform(0, 1, 32)
group_3_y = (2.5 + 0.3*group_3_x) + np.random.rand(len(group_3_x))

all_x = np.concatenate([group_1_x, group_2_x, group_3_x])
all_y = np.concatenate([group_1_y, group_2_y, group_3_y])
df = pd.DataFrame({"Satisfaction": all_y, "Time_spent":all_x, "Country":[0]*len(group_1_x) + [1]*len(group_2_x) + [2]*len(group_3_x)})

喜欢这篇文章吗? 关注我 以便接收我未来的帖子。

用 R 进行的多层回归

原文:towardsdatascience.com/multilevel-regression-with-r-eb3eb7d8de88

通过这个简单的解释和示例来理解层级线性模型

Gustavo SantosTowards Data Science Gustavo Santos

·发布在 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 15 日

--

图片由 Lidya Nada 提供,来源于 Unsplash

介绍

回归模型已经存在很长时间了,远早于机器学习的出现。统计学家们早在 1900 年代之前就开始使用这些模型来理解变量之间的关系,当时 Sir Francis Galton(1885)提出了这一思想。

幸运的是,自那时起,理论、计算机和技术都有了很大的发展,以至于我们可以说,现如今创建这样的模型是简单的(如果不是最简单的话)。

但不要被它的实现简单性所迷惑。它确实很简单,但也不是那么简单。有不止一种回归模型可用,不仅仅是普通最小二乘法(OLS)。

我们在这篇文章中讨论的是层级线性模型(HLM),或者简单地说,就是多层回归

准备工作

在深入内容之前,让我们先了解一下本例中将使用的数据集以及我们在编码时需要的库。

加载以下库。如果你没有安装其中任何一个,只需使用 install.packages("library_name") 进行安装。

library(nlme)
library(tidyverse)
library(ggridges)

数据集汽油在许多 R 库中均可用。在这个例子中,它是通过 R 的 nlme 库访问的,许可证为 GPL 2.0。它最初来源于 Prater, N. H. (1955), Estimate gasoline yields from crudes, Petroleum Refiner, 35

这个数据框包含以下变量:

  • **yield**:原油转化为汽油的百分比

  • **endpoint**:所有汽油蒸发的温度(华氏度)

  • **Sample**:原油样本编号

  • **API**:原油重力(API 度数)

  • **vapor**:原油的蒸汽压(lbf/in2)

  • **ASTM**:10%的原油变成蒸汽的温度。

# Dataset
data("Gasoline")

目标:我们的目标是预测yield数值,即从一个样本中转化为汽油的原油量。

关于 OLS 的小回顾

在 OLS 回归模型中,我们取一个或多个解释变量[X],并尝试使用这些数字来解释由响应变量[y]测量的主题行为。

对于 OLS,最佳的特征选择统计测试是相关性。毕竟,最常用的衡量线如何拟合数据的指标是 R²,这只是相关性*方(附注:确实还有其他模型性能指标,应该与 R²一起使用)。

因此,我们的目标是绘制一条最佳拟合数据的线。换句话说,我们希望这条线与数据点一致,绘制出响应变量的*均值。此外,我们希望这条线与实际数据点之间的差异尽可能小。

更技术一点,响应变量将由一个方程确定,我们希望优化 y 轴上的截距点alpha和这条线的斜率,而斜率由beta系数决定,正如下文所述。

简单线性回归方程。作者提供的图片。

如果我们查看我们的数据集,这就是 OLS 模型将会做的事情:

从汽油数据集创建的 OLS 回归模型。作者提供的图片。

这是一个不错的拟合,但不是最佳模型。它可以在某种程度上解释yield的方差,但我们可以做得更好,如下所示。

层次线性模型 [HLM]

HLM 与传统 OLS 模型的不同之处在于它考虑了数据的自然分组。

多层回归就像为数据中的每个组创建一个不同的回归模型。

从我们的数据来看,首先引起我注意的是存在不同的Sample。所以,这引出了问题:

  • 不同样本会影响产量的估计吗?

  • 如果我们通过样本添加多个回归层次,结果会更好吗?

汽油数据集摘录。作者提供的图片。

组件

HLM 模型由固定效应和随机效应组件组成。固定效应可以估计 X 变量和 y 之间的关系,而随机效应组件将为每个组确定不同的截距和斜率系数,帮助创建对每个组更合适的估计。

请看下面的多层回归模型的样子。

多层次模型 HLM2,一级=观察,二级=样本。图像由作者提供。

实际上,会创建一个回归模型来计算固定组件,这对于每个组的每个观察值都是 y = a + bx。然后,顺序地计算随机组件,这是对更多或更少的调整,根据该组的最佳截距和/或斜率。

HLM 模型可以有随机组件,仅改变截距、仅改变斜率或两者都有。在前面的图中,随机组件是截距和斜率,因为每个样本的回归线不*行且有不同的截距点。

编码

现在让我们创建一个 OLS 模型和一个 2 级的 HLM,然后比较结果。

我们可以先进行相关性测试,以了解回归中使用的最佳变量。我们会发现 endpoint 是与 yield 相关性最高的变量。

# Check for the best correlations
cor(Gasoline[,-3])

              yield   endpoint        API      vapor       ASTM
yield     1.0000000  0.7115262  0.2463260  0.3840706 -0.3150243
endpoint  0.7115262  1.0000000 -0.3216782 -0.2979843  0.4122466
API       0.2463260 -0.3216782  1.0000000  0.6205867 -0.7001539
vapor     0.3840706 -0.2979843  0.6205867  1.0000000 -0.9062248
ASTM     -0.3150243  0.4122466 -0.7001539 -0.9062248  1.0000000

在 R 中创建 OLS 模型非常简单。原生函数 lm() 可以轻松处理。让我们看看。

# OLS model
model_ols <- lm(yield ~ endpoint, data=Gasoline)
summary(model_ols)

[OUT]
Call:
lm(formula = yield ~ endpoint, data = Gasoline)

Residuals:
     Min       1Q   Median       3Q      Max 
-14.7584  -6.2783   0.0525   5.1624  17.8481 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -16.66206    6.68721  -2.492   0.0185 *  
endpoint      0.10937    0.01972   5.546 4.98e-06 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 7.659 on 30 degrees of freedom
Multiple R-squared:  0.5063, Adjusted R-squared:  0.4898 
F-statistic: 30.76 on 1 and 30 DF,  p-value: 4.983e-06

# Calculate Log Likelihood
logLik(model_ols)
'log Lik.' -109.5206 (df=3)

结果:模型在统计上显著(F 检验的 p 值<0.05),系数也显著。

  • R²: 50%

  • Loglik: -109.52

  • MAE: 5.95(yield *均值 = 19.66)

  • RMSE: 7.41(标准差:10.72)

我们的主要绝对误差约为 y *均值的 30%。这太多了。正如我们在单一回归线的图中看到的那样,这条线位置良好,但无法完全捕捉方差,因为有许多点离*均值太远。

现在让我们尝试运行一个 HLM。

# Multilevel Model with fixed slope and random intercept
model_multi <- lme(fixed = yield ~ endpoint,
                       random = ~ 1 | Sample,
                       data = Gasoline,
                       method = "REML")

[OUT]
Linear mixed-effects model fit by REML
  Data: Gasoline 
       AIC      BIC   logLik
  183.4306 189.0354 -87.7153

Random effects:
 Formula: ~1 | Sample
        (Intercept) Residual
StdDev:    8.387862  1.88046

Fixed effects:  yield ~ endpoint 
                Value Std.Error DF   t-value p-value
(Intercept) -33.30626  3.287182 21 -10.13216       0
endpoint      0.15757  0.005703 21  27.62678       0
 Correlation: 
         (Intr)
endpoint -0.582

Standardized Within-Group Residuals:
       Min         Q1        Med         Q3        Max 
-1.6759908 -0.4857902 -0.0397993  0.5128182  1.8518205 

Number of Observations: 32
Number of Groups: 10 

这是代码中发生的情况。我们使用 nlme 库中的 lme 函数运行了一个多层次模型。固定组件是回归 yield ~ endpoint随机组件仅针对截距。注意 random = ~1 | Sample~1 意味着固定斜率——即按组的*行回归线——和按 Sample 计算的随机截距。

与简单的 y = a + bx 相比,这个模型变成了:

  • 固定: [截距 + b*endpoint] +

  • 随机: [ 由样本随机效应 + 错误]

与 OLS 模型相比,注意它如何改进了。

  • Loglik: -87.7153(数字越高越好)。

  • MAE: 1.16

  • RMSE: 1.52

哇,已经改进了很多:LogLik 测量值提高了大约 20%。

我们还可以进一步改进吗? 我们可以尝试向模型中添加另一个变量。我们将选择 vapor,因为它与响应变量的相关性是下一个最强的。

这次,我们将运行一个带有随机斜率和截距的 HLM,这意味着我们将允许模型在交叉 y 轴的位置和拟合线的倾斜度上变化,而不是将其静态为*行线。

在下面的代码片段中,我们正在计算yieldendpointvapor函数中的一个固定部分(按观察、按样本)。随机部分将根据endpoint值计算回归的斜率,并根据Sample计算截距。下一个模型将是:

  • 固定:[截距 + (b1 * 终点) + (b2 * 蒸汽)] +

  • 随机:[截距随机效应由样本 + 斜率随机效应 * 终点 + 误差]

# Multilevel Model with random slope and intercept
model_multi2 <- lme(fixed = yield ~ endpoint + vapor,
                   random = ~ endpoint | Sample,
                   data = Gasoline,
                   method = "REML")

# Look how the fitted values are presented
# It is divided in Fixed and the result with the levels
# "Sample" is the final prediction, with the random effects considered
model_multi2$fitted

        fixed     Sample
1   9.9034036  6.2878876
2  17.4490277 16.9089759
3   6.2864457  8.2047743
4  13.2051879 10.0773886
5  -0.3240537  6.1951732

为了公*起见,我还使用了蒸汽变量运行了另一个模型。接下来,我们将比较所有模型。

比较创建的 4 个模型。图像由作者提供。

尽管 OLS 模型endpoint+vapor给出了一个有趣的 LogLik 值,但我们还是更仔细地看看预测,以了解哪个模型更合适。

# Results table
results <- data.frame(
   yield = Gasoline$yield,
   pred_ols = model_ols$fitted.values,
   pred_ols2 = model_ols2$fitted.values,
   pred_multi = model_multi$fitted[,'Sample'],
   pred_multi2 = model_multi2$fitted[,'Sample']

   yield  pred_ols  pred_ols2 pred_multi pred_multi2
1    6.9  9.040133 11.2681120  5.8234477   6.2878876
2   14.4 16.914846 17.8195910 17.5516334  16.9089759
3    7.4  6.524599  8.0634010  8.6490752   8.2047743
4    8.5 23.258365 13.5848551  9.9282663  10.0773886
5    8.0  7.180825  1.9380928  6.1502045   6.1951732
6    2.8  9.040133 -0.2448399 -0.2263362   0.6677289
7    5.0 14.508684  5.1154648  5.7778750   5.0504530
8   12.2  5.759002 13.7816308 12.3278463  11.9321042
9   10.0 12.540005 13.3171528 10.1678972  10.3006218
10  15.2 16.149249 20.3249040 16.0654477  16.3795097
11  26.8 23.477107 26.1797067 27.0057873  27.2203084
12  14.0 21.727171 17.5245089 13.0730720  13.5617889
13  14.7 24.789559 15.5355488 12.1342356  12.1958142
14   6.4 13.414973  5.3285706  6.0764330   6.4725490
15  17.6 23.258365 16.2622858 18.3834134  18.1474338
16  22.3 13.414973 23.5350992 23.3576925  23.3378940

OLS 模型 2 做得非常好,但多层回归更加一致。例如,查看第 4、5、9、10、12 行的拟合值,两个多层模型将提供更好的估计。然而,尽管存在这些误差,OLS2 对这些数据来说并不是一个糟糕的模型。

比较创建的 4 个模型。图像由作者提供。

这个最终的视觉效果可以给我们一个模型的比较视图。毫无疑问,最佳拟合模型是右下角的最后一个 HLM 模型。

离开前

在本教程中,我想为你介绍层次线性模型或多层回归模型。当你有数据自然嵌套在不同组中时,这些模型会非常有用,并且这些组会影响估计的方式。

使用多层模型,你可以允许回归线根据组变化其斜率和/或截距。

在 R 中进行 HLM 建模所用的包是nlme

这是这个练习的代码,在 GitHub 上。

[## Studying/R/Multilevel Regression at master · gurezende/Studying

你目前无法执行该操作。你在另一个标签或窗口中登录。你在另一个标签或窗口中注销了……

github.com](https://github.com/gurezende/Studying/tree/master/R/Multilevel Regression?source=post_page-----eb3eb7d8de88--------------------------------)

如果你喜欢这个内容,不要忘记关注我。

[## Gustavo Santos - Medium

阅读 Gustavo Santos 在 Medium 上的文章。数据科学家。我从数据中提取见解,以帮助人们和公司……

gustavorsantos.medium.com](https://gustavorsantos.medium.com/?source=post_page-----eb3eb7d8de88--------------------------------)

此外,如果你考虑加入 Medium 成为会员,以便访问我的文章以及其他成千上万的优质内容,这是我的推荐码

你可以在LinkedIn找到我,也可以通过TopMate 预订与我进行快速聊天

参考文献

Fávero, L. & Belfiore, P. 2022. Manual de Análise de Dados. 第 1 版. LTC,里约热内卢,巴西。

使用 Azure ML 和 MONAI 的多模态 3D 脑肿瘤分割

原文:towardsdatascience.com/multimodal-3d-brain-tumor-segmentation-with-azure-ml-and-monai-4a721e42f5f7?source=collection_archive---------6-----------------------#2023-03-21

在企业级 ML *台上大规模运行医学影像框架

Andreas KoppTowards Data Science Andreas Kopp

·

关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 3 月 21 日

--

作者:Harmke AlkemadeAndreas Kopp

3D 脑肿瘤分割(图片来源于 Shutterstock,授权给 Andreas Kopp

我们感谢来自 NVIDIA 和 MONAI 团队的 Brad Genereaux、Prerna Dogra、Kristopher Kersten、Ahmed Harouni 和 Wenqi Li,他们在该资产的开发中给予了积极支持。

自 2021 年 12 月以来,我们发布了多个示例以支持使用 Azure 机器学习的医学影像,我们收到了热烈的反馈。兴趣和咨询进一步证明了 AI 如何迅速成为现代医学实践的关键方面。

选定的医学影像库用例(图片 5,6 通过 Shutterstock 获得 Andreas Kopp 授权

3D 脑肿瘤分割应用案例

今天,我们介绍一个新的医学影像资产,用于3D 脑肿瘤分割,这是一个解决肿瘤学领域中的挑战性用例的解决方案。我们的解决方案利用来自多个 MRI 模态的体积视觉输入和不同的 3D 胶质瘤肿瘤分割,以产生准确的肿瘤边界和子区域预测。为了处理涉及的大量影像数据,我们的资产采用了在 Azure ML 上使用可扩展 GPU 资源的并行训练。此外,我们利用了医学人工智能开放网络(MONAI),这是一个领域特定的框架,提供了最先进的工具和方法用于医学影像分析。

结合 Azure 机器学习和 MONAI,为可扩展的机器学习开发和操作提供了宝贵的协同效应,特别是在医学影像方面的创新。

Azure 机器学习是一个基于云的*台,为数据科学家和机器学习工程师提供了一个协作环境,用于大规模开发和部署机器学习模型。它提供了多种创作选项,从无需编码/低编码的自动化机器学习到使用 VSCode 等流行工具的代码优先。用户可以访问可扩展的计算资源进行训练和部署,使其在处理大型数据集和复杂模型时非常理想。此外,它包括 MLOps 功能,可以实现机器学习资产的管理、维护和版本控制,并允许自动化的训练和部署管道。该*台包括负责任的 AI 工具,可以帮助解释模型并减轻潜在的偏见,使其成为希望开发和部署伦理 AI 解决方案的企业的理想选择。

医学人工智能开放网络(MONAI)是一个基于 PyTorch 的开源项目,旨在用于医学影像。它提供了一套全面的工具,用于构建和部署医疗影像的 AI 模型:

  • MONAI 标签用于 AI 辅助标记医学影像数据

  • MONAI 核心用于训练具有领域特定功能的 AI 模型

  • MONAI 部署用于打包、测试和部署医学 AI 应用程序。

MONAI Core 是本文描述的解决方案的重点。它原生支持常用的医学成像格式,如 Nifty 和 DICOM。它还包括字典变换等功能,以确保在复杂的变换管道中图像和分割之间的一致性。此外,MONAI Core 提供了一系列网络架构,包括像 UNETR 这样的最先进的基于 Transformer 的 3D 分割算法。

欢迎探索我们的演示,并将其作为您自己 3D 分割用例的模板(无论是医学领域还是其他领域)。

我们使用的是 2021 版本的 脑肿瘤分割 (BraTS) 挑战 数据集。它是来自不同机构的专家标注的多模态 3D 磁共振成像 (MRI) 扫描图像的集合。MRI 是一种医学成像技术,利用磁场和无线电波的组合来可视化体内结构。通过改变成像参数可以生成不同的 MRI 模态,从而产生不同的组织对比度,使得某些特征在图像中更为突出。BraTS 数据集使用了以下模态:

  1. 原生 T1 (T1):可用于区分各种组织类型和病理状态。原生 T1 通常用于提取有关组织特性的定量信息。

  2. T1 加权对比剂钆 (T1Gd):这种模态可用于划分肿瘤边界和识别活跃肿瘤生长区域。

  3. T2 加权 (T2):这些图像对检测水肿(液体积聚)、炎症和其他可能与肿瘤相关的脑组织变化非常有用。

  4. T2 加权液体衰减反转恢复 (T2-FLAIR):T2-FLAIR 图像对于识别侵袭性肿瘤边缘和非增强肿瘤成分非常有用。

以下插图提供了 BraTS 数据集中专家标注的肿瘤分割示例:肿瘤核心、整体肿瘤和增强结构。左上角的图像结合了这三种分割。

从 BraTS 2021 数据集中提取的脑肿瘤分割(作者提供的图像)

能够区分这些结构对于诊断、预后和治疗规划至关重要。在 BraTS 数据集中,整体肿瘤指的是肿瘤的全部体积,包括核心部分和任何周围的水肿或肿胀。核心是肿瘤内部含有最具侵袭性的癌细胞的区域,即坏死和活跃的肿瘤。增强结构是指肿瘤区域在 MRI 扫描中出现明亮并在对比剂作用下增强的部分。这一区域与最活跃和侵袭性的癌细胞相关。

付诸实践

现在,我们已经对使用案例有了概述,让我们来看一下如何使用 Azure Machine Learning 和 MONAI 来训练和部署一个机器学习模型以完成这个有趣的任务。

这个使用案例的端到端工作流程在我们的 编排笔记本 中实现。这些是主要步骤及其相关输出:

脑肿瘤分割工作流程及输出(图片由作者提供)

第一步涉及下载和可视化数据,然后提交训练作业到 Azure ML 计算集群。模型训练完成并注册后,它被部署为 Azure ML 管理的端点。笔记本最后通过可视化验证集中的图像体上的模型预测来结束。训练和推理脚本单独存储在仓库中。

在 itkwidgets 3D 查看器中可视化肿瘤核心(图片由作者提供)

为了在我们的工作流程中管理机器学习资产,如数据集、计算资源、实验和结果模型,我们使用 Azure ML SDK 创建与 Azure ML 工作区的连接。这个连接可以在任何 Python 环境中建立,无需运行 Azure ML 计算实例。通过使用 Azure ML 管理资产,它们可以在 Azure ML Studio 中找到以备将来参考。这有助于轻松比较不同的实验,并通过引用相关资产(数据集版本、训练代码、环境等)创建结果模型的沿袭。

数据是通过 Kaggle API 从 Kaggle 下载的,并注册为 Azure ML 管理的数据集。这有助于对训练数据进行版本控制,这在训练数据可能随时间变化的迭代模型开发场景中尤为重要。例如,BraTS 数据集自 2012 年以来持续更新。将数据版本与模型关联可以确保良好的治理。

训练脚本 使用 MONAI 框架以及原生 PyTorch 类。数据使用 MONAI 框架提供的多个转换进行预处理,主要使用基于字典的包装器。训练图像的不同 MRI 模态被存储为单独的文件,分割掩膜则有一个文件。基于字典的包装器允许对所有训练数据使用单一的预处理管道,其中管道中的每个转换都可以指定应用于数据的哪一部分。

以下示例说明了 MONAI 字典变换在确保图像与相关分割标签之间的一致性方面的好处。假设我们使用一个随机变换的管道来进行数据增强,这些变换会影响图像的形状或位置(如调整大小、翻转、旋转、透视调整等)。我们必须确保对分割图像应用相同的变换,以确保图像和标签之间的一致性,便于后续的训练过程。在这种情况下,MONAI 字典变换是一个便利的工具:我们只需指定变换应应用于图像、标签还是两者。在这个例子中,我们将变换应用于两个对象。MONAI 还确保在随机变换的情况下保持一致性。

随机水*翻转的字典变换示例(图像由作者提供)

MONAI 提供了一系列基于 PyTorch 的网络架构。我们选择了 SegResNet(一个为分割任务适配的 ResNet 架构变体)来完成我们的任务。它基于编码器-解码器框架,其中编码器从输入图像中提取特征,解码器重建输出分割掩模。

# Specify SegResNet for 3D segmentation
segresnet = SegResNet(
    blocks_down=[1, 2, 2, 4],
    blocks_up=[1, 1, 1],
    init_filters=16,
    in_channels=4,
    out_channels=3,
    dropout_prob=0.2,
).to(device)

# Wrap network for DDP
model = torch.nn.parallel.DistributedDataParallel(module=segresnet, device_ids=[local_rank])

# Specify Dice loss, optimizer and LR scheduler
loss_function = DiceLoss(smooth_nr=0, smooth_dr=1e-5, squared_pred=True, to_onehot_y=False, sigmoid=True)
optimizer = torch.optim.Adam(model.parameters(), initial_lr, weight_decay=1e-5)
lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=max_epochs)

用于下采样的编码器块数量由 blocks_down 参数指定。因此,解码器部分的上采样块由 blocks_up 参数定义。第一个卷积层使用了 16 个滤波器。随着网络的深入,数量通常会翻倍,以便学习更复杂的特征。网络接受 4 个通道的输入,由 in_channels=4 指定。这代表了我们的多模态 MRI 数据,每个通道对应不同的 MRI 模态:T1、T1Gd、T2 和 T2-FLAIR。网络的输出具有 3 个通道,由 out_channels=3 指定。这对应于我们分割任务的类别:整个肿瘤、肿瘤核心和增强结构。

使用 PyTorch 原生特性以支持多 GPU 的分布式数据并行(DDP)训练。

对于损失函数,我们利用了 MONAI 的 Dice 损失。它是用来衡量预测分割与真实标签之间相似度的常用损失函数。它在类分布不均的情况下特别有用,例如医学图像分割,其中目标区域(如肿瘤)通常比背景小得多。

Adam 优化器与余弦退火学习率调度器(CosineAnnealingLR)一起使用。该调度器使学习率在初始值和指定的最小值之间振荡。CosineAnnealingLR 通过使模型在训练的初期阶段能够逃离局部最小值,并在后期阶段进行精确调整,从而帮助提高模型性能。

训练脚本通过命令作业提交到 Azure ML 计算集群,使用以下定义:

job= command(
    inputs= {"input_data": Input(type=AssetTypes.URI_FOLDER, path= dataset_asset.path)},
    code= 'src/',
    command= "python train-brats21.py --epochs 50 --initial_lr 0.00025 --train_batch_size 1 --val_batch_size 1 --input_data ${{inputs.input_data}} --best_model_name BRATS21",
    environment= "monai-multigpu-azureml@latest", 
    compute= train_target,
    experiment_name= experiment,
    display_name= f"3d brain tumor segmentation based on BRATS21",
    description= "## Brain tumor segmentation on 3D MRI brain scans",
    shm_size= '300g',
    resources= dict(instance_count= 1), # cluster nodes 
    distribution= dict(type="PyTorch", process_count_per_instance= 4), # GPUs per node
    environment_variables= dict(AZUREML_ARTIFACTS_DEFAULT_TIMEOUT = 1000),
    services= {
    "My_jupyterlab": JobService(job_service_type="jupyter_lab"),
    "My_vscode": JobService(job_service_type="vs_code",),
    "My_tensorboard": JobService(job_service_type="tensor_board",),
        })

returned_job= ml_client.create_or_update(job)

当我们提交训练作业时,会在计算集群中启动一个容器,使用我们之前在笔记本中定义的环境“monai-multi-gpu”。这个环境是从包含训练脚本要求的 conda 规范文件构建的。生成的 Docker 镜像被缓存到链接到 Azure ML 工作区的 Azure 容器注册表中。

通过使用distribution 参数,我们启动了分布式数据并行训练的进程数量。由于我们在这次实验中使用了一台四 GPU 的机器,因此启动了四个进程,每个进程访问自己的 GPU 实例。此外,我们还可以通过使用resources 参数在计算集群的不同节点上并行化训练运行。

了解 GPU 利用率对于优化资源使用和防止在长时间的 PyTorch 训练过程中出现的内存溢出(OOM)错误至关重要。Azure ML 提供了一系列指标来跟踪网络和磁盘 I/O,以及 CPU 和 GPU 内存和处理器利用率。以下示例演示了在 Standard_NC24rs_v3 类型的多 GPU 集群节点上训练运行开始时的 GPU 内存和能耗情况。在这种情况下,我们可以看到四个 GPU 上每个 GPU 的 16 GB 内存正在有效利用。剩余的头部空间很小,这清楚地表明增加批量大小可能会提高遇到 OOM 错误的风险。此外,Azure ML 还允许我们监控每个 GPU 的能耗,以千焦耳为单位进行测量。

监控训练期间的多 GPU 资源消耗(图像由作者提供)

使用大量数据集进行训练可能需要相当长的时间,可能需要几个小时甚至几天才能完成。使用像 Azure ML 这样的基于云的数据科学和机器学习*台的好处在于能够根据需要按需访问强大的资源。

对于我们的最终模型,我们使用了 NC96ads_A100_v4 集群节点,配备 96 核心、880 GB CPU 内存以及每个四个 NVIDIA A100 GPU 上 80 GB 内存,以训练超过 150 个 epoch。这种设置的主要优势是资源按需可用:一旦训练完成,集群会自动关闭,消除了任何进一步的费用。

在我们的案例中,我们选择了“低优先级” SKU,相比专用计算资源,它提供了更具成本效益的解决方案。然而,这种选择存在风险,即如果计算资源被用于其他任务,正在运行的训练作业可能会被抢占。

在训练作业运行时,JupyterLab、VSCode 和 Tensorboard 在容器中运行,使其可以进行监控和调试。这在 services 参数中指定。我们在训练脚本中使用 Tensorboard 和 MLFlow 记录训练指标,并使用 MLFlow 注册最终模型。由于 Azure ML 中对 MLFlow 的本机支持,指标和模型都可以在 Azure ML Studio 的实验部分找到。通过访问运行在容器中的 Tensorboard 实例,我们可以在模型训练时查看训练和验证指标。

使用 Tensorboard 进行交互式作业的训练性能监控(图像由作者提供)

Dice 系数用于评估模型性能,是一种常用于对象分割模型的度量指标。它衡量预测分割掩膜与真实数据掩膜之间的相似性。Dice 系数为 1.0 表示预测掩膜与真实数据掩膜完全重叠。对数据集中不同类别的 Dice 指标进行跟踪:肿瘤核心 (val_dice_tc)、整体肿瘤 (val_dice_wt) 和增强结构 (val_dice_et)。还跟踪不同类别的*均 Dice 指标 (val_mean_dice)。进行 150 个 epochs 的实验得到以下指标:

在 Azure ML Studio 中进行 150 个 epoch 训练期间的验证 DICE 指标(图像由作者提供)

注册模型后,我们进入部署阶段,这是使模型在生产环境中可用于预测新 MRI 图像的分割掩膜的一种方式。一个可能的脑肿瘤分割部署场景是将其与 MRI 图像查看器集成,以提供诊断辅助。在笔记本中,使用了 Azure ML 管理端点来进行部署。首先,创建一个管理在线端点,它提供了发送请求和接收模型推断输出的接口,并提供认证和监控功能。然后,我们通过定义模型、评分脚本、环境和目标计算类型来创建一个部署。我们参考了工作区中最新注册的 BraTS 模型进行此部署。

在将四种模态的二进制编码版本的 MRI 图像堆栈发送到我们的端点后,我们会收到作为 JSON 响应的一部分的预测分割结果。编排笔记本的最后部分包含了可视化这些预测的代码。我们的图像滑块将每个切片的预测分割结果与真实数据并排显示,以便进行比较。

比较专家标注的(真实数据)与预测的肿瘤结构(图像由作者提供)

人工智能在医疗保健中的负责任使用

将机器学习融入临床实践,例如肿瘤检测和分析,带来了诸多好处。例如,算法可以通过标记每日生成的海量图像中的可疑区域来辅助放射科医生。另一个应用是对肿瘤进展进行持续监测,以评估癌症治疗的效果。从每位患者的研究中计算出超过一百张切片的肿瘤体积可能非常耗时,但 AI 辅助测量可以显著加快这一过程。

尽管 AI 辅助图像分析取得了显著进展,但仍可能出现错误。未被检测出的肿瘤是一个重大问题,训练数据中稀有肿瘤或代表性不足的患者特征可能导致此类假阴性。因此,必须通过纳入多样化的数据集和不断完善算法来解决这些限制。

医生必须继续对诊断和治疗决策负责。机器学习预测应被视为有价值的支持工具,而不是可以替代人类专业知识的绝对替代品。医生始终处于整个过程的前沿,确保患者护理同时受到技术和专业判断的指导。

结论

在这篇文章中,我们探讨了 Azure 机器学习和 MONAI 在 3D 脑肿瘤分割中的应用,这是肿瘤学领域中的一个挑战性用例。我们讨论了 MRI 图像的各种模态以及区分肿瘤核心、整个肿瘤和增强结构在诊断、预后和治疗计划中的重要性。我们还概述了这一用例的端到端工作流程,包括数据可视化、模型训练和注册、部署以及模型预测的可视化。使用 Azure 机器学习和 MONAI 使我们能够利用可扩展的 GPU 资源进行并行训练,并为医疗成像中的 AI 模型训练提供了特定领域的能力。

我们希望 我们的资产 能作为未来医学领域 3D 分割用例的模板,并为现代医学实践中 AI 的日益重要性做出贡献。

该资产的训练代码基于 MONAI 3D 脑肿瘤分割教程 的早期版本。

参考文献

[1]: Hatamizadeh, A., Nath, V., Tang, Y., Yang, D., Roth, H. and Xu, D., 2022. Swin UNETR: Swin Transformers for Semantic Segmentation of Brain Tumors in MRI Images. arXiv preprint arXiv:2201.01266.

[2] Tang, Y., Yang, D., Li, W., Roth, H.R., Landman, B., Xu, D., Nath, V. 和 Hatamizadeh, A., 2022. 自监督预训练的 swin 变换器用于三维医学图像分析。见于 IEEE/CVF 计算机视觉与模式识别会议论文集(第 20730–20740 页)。

[3] U.Baid 等人,RSNA-ASNR-MICCAI BraTS 2021 脑肿瘤分割和放射基因组分类基准,arXiv:2107.02314,2021。

[4] B. H. Menze, A. Jakab, S. Bauer, J. Kalpathy-Cramer, K. Farahani, J. Kirby 等人,“多模态脑肿瘤图像分割基准(BRATS)”,《IEEE 医学影像学报》34(10),1993–2024(2015)DOI: 10.1109/TMI.2014.2377694

[5] S. Bakas, H. Akbari, A. Sotiras, M. Bilello, M. Rozycki, J.S. Kirby 等人,“通过专家分割标签和放射组学特征推动癌症基因组图谱胶质瘤 MRI 收集”,《自然科学数据》,4:170117(2017)DOI: 10.1038/sdata.2017.117

[6] S. Bakas, H. Akbari, A. Sotiras, M. Bilello, M. Rozycki, J. Kirby 等人,“TCGA-GBM 收集的术前扫描的分割标签和放射组学特征”,癌症影像档案,2017. DOI: 10.7937/K9/TCIA.2017.KLXWJJ1Q

多模态思维链:在多模态世界中解决问题

原文:towardsdatascience.com/multimodal-chain-of-thoughts-solving-problems-in-a-multimodal-world-961a8ab9d0fa

NLP | 多模态性 | 思维链 |

世界不仅仅是文字:如何将思维链扩展到图像和文字?

Salvatore RaieliTowards Data Science Salvatore Raieli

·发表于 Towards Data Science ·14 分钟阅读·2023 年 3 月 13 日

--

图片由 Giulio Magnifico 提供,来源于 Unsplash

有时得出答案并不容易,特别是当问题需要推理时。模型并不总是在其参数中隐藏答案,但可以通过正确的上下文和方法得出答案。什么是思维链?为什么这种方法使得解决多步骤推理任务成为可能?它可以扩展到多模态问题(即包含图像和文字的问题)吗?只有大型模型才能做到这一点吗?

本文讨论了如何回答这些问题。

思维链(CoT):它是什么?

图片由 Todd Cravens 提供,来源于 Unsplash

*年来,我们见证了模型参数数量的增长(超过 1000 亿个参数)。这受到扩展法则的推动:随着参数数量的增加,误差减少。

## 无监督数据剪枝:用更少的数据更好地学习

更多的数据并不总是意味着更准确的模型,但如何选择你的数据呢?

towardsdatascience.com

虽然这对于如情感分析和机器翻译等任务是正确的(即使在零-shot少-shot 学习的情况下),即使是拥有数十亿参数的模型在需要多步推理的任务中(例如数学问题或常识推理)也会遇到困难。

如何让模型在这些任务中取得成功?

大型模型可以针对特定任务进行微调,这也是最初尝试的系统。正如这个想法的作者解释的那样,如果你问一个模型鲸鱼是否有肚脐,模型将错误地回答“没有”。这是因为模型的参数中没有存储这条信息。作者建议可以通过提供隐性知识的提示来帮助模型:“鲸鱼是哺乳动物”。

来源:这里

提供隐性知识的想法为系统通过与用户交互来改进自己铺*了道路。用户可以识别错误并向模型提供信息,允许其自我纠正。或者更准确地说,正如作者所定义的:

这可以视为一种“单次学习”的形式,可以在不进行进一步训练的情况下即时改进模型,这与大多数当前依赖于数据收集和重新训练来修正模型错误的工作方式不同。

从概念上讲,这个想法是一个模型可以通过利用中间步骤来解决其确切答案并不直接知道的问题。

正如谷歌所指出的,提示使得上下文少样本学习成为可能。换句话说,除了对特定任务进行微调外,可以通过一些输入-输出示例来提示语言模型。这种方法被证明非常有效,尤其在问答系统中。此外,正如在上下文学习中所示,它对于大型模型特别有效

“对于所有 42 个准确度基准的整体表现,虽然零-shot 性能随着模型规模的增加而稳步提升,但少-shot 性能则更快增加,证明了更大的模型在上下文学习方面更为高效。” 来源:这里

谷歌随后提出,通过仅包含几个思维链的示例,可以让模型解决多步推理问题。为了更好地理解,以下是经典提示和思维链提示之间的变化示例:

“思维链提示使大型语言模型能够处理复杂的算术、常识和符号推理任务。思维链推理过程被突出强调。” 来源:这里

这种方法的优点是它既不需要改变语言模型的权重,也不需要大规模的训练数据集。

简而言之,我们可以说,这个理念是将复杂的问题分解为一系列可以单独解决的中间步骤。

这可能看起来是小事,但实际上意味着这种方法可以应用于任何你可以用语言解决的问题。

谷歌的作者表示这是模型的一个突现特性,它在达到一定的模型容量时(他们估计约 100 B 参数)出现。作者评估了增加模型以解决数学问题:

“: 思维链提示使大型语言模型能够解决具有挑战性的数学问题”。来源:这里

此外,作者指出,模型的改进并非来自增加参数,而是通过使用“思维链提示,增加模型规模会带来显著优于标准提示的大模型性能提升。”

这对于常识推理(“在假设一般背景知识的情况下推理关于物理和人类互动”)也是成立的。

“: 算术、常识和符号推理基准的输入、思维链、输出三元组示例。” 来源:这里

在这种情况下,模型也显示了相同的行为:“性能随着模型规模的扩大而提高,采用思维链提示也带来了额外的小幅改进。” 最大的改进出现在运动理解领域(令人惊讶)。

总的来说,我们已经看到 CoT 有两种技术,微调或使用提示(上下文学习)。关于第二种范式,我们可以进一步细分为:

  • 零样本 CoT。Kojima 证明了语言模型在零样本 CoT(仅仅添加“让我们一步步来思考”)方面表现不错,这足以显著提高零样本 LLM 的复杂推理能力。

  • 少样本 CoT。少量的逐步推理示例用于模型推理中的条件设置。每个示例同时提供一个问题和一个解释模型如何得出最终答案的推理链(这些示例可以是手工制作的或使用自动生成)。

来源:这里

少样本 CoT 已被证明更有效且结果更好(前提是示例编写得很好)。因此,大多数后续研究都集中在这种方法上。

“典型的 CoT 技术(FT: 微调;KD: 知识蒸馏)。第一部分:上下文学习技术;第二部分:微调技术。根据我们所知,我们的工作是首次研究不同模态下的 CoT 推理。此外,我们专注于 1B 模型,不依赖于 LLMs 的输出。” 来源:这里

多模态链式思维具有挑战性

图片来源于 airfocus 在 Unsplash 上

正如我们所见,链式思维(CoT)在需要复杂推理的问题上证明了非常有用。许多问题不仅是文本的,还有多模态的。例如,解决一个问题我们可能需要查看图片。正如我们所说,CoT 仅适用于可以用文本形式表达的问题。我们如何处理多模态问题?

想象一下阅读一本没有图形或表格的教科书。我们通过联合建模不同的数据模态(如视觉、语言和音频)来极大地增强知识获取能力。 (来源)

最*一篇文章正好提出了这个问题,并试图将 CoT 扩展到多模态问题中:

[## 多模态链式思维推理在语言模型中的应用]

大型语言模型(LLMs)通过利用链式思维在复杂推理上表现出色…

arxiv.org

如前所述,参数少于 100 亿的模型往往会产生不合逻辑的 CoT,从而导致错误的答案。一个 多模态模型 不仅要处理文本输入,还要处理其他模态。这使得创建一个参数少于 100 亿的模型变得困难。

另一方面,META 的 LLaMA 显示出,参数少于 100 亿的模型可以达到与更大模型相当的结果。

[## META 的 LLaMA:一个击败巨头的小型语言模型]

META 开源模型将帮助我们理解语言模型的偏见如何产生

medium.com](https://medium.com/mlearning-ai/metas-llama-a-small-language-model-beating-giants-5065948e0b7f?source=post_page-----961a8ab9d0fa--------------------------------)

此外,正如其他研究所示,文本模型在训练过程中没有看到图片,因此没有关于视觉元素或如何利用视觉特征的信息。

在多模态环境中进行 CoT 推理要求模型考虑不同的模态:给定不同模态的输入,模型将一个多步骤的问题分解为一系列中间步骤,然后推断出答案。

多模态 CoT 任务示例。 来源: 这里

执行多模态 CoT 的最直接方法是将不同模态的输入转换为一种模态,并提示大型语言模型执行 CoT。 (source)

例如,可以取一张图像,并将其作为输入用于字幕生成模型。一旦获得字幕,就可以将其与文本提示结合起来,然后提供给大型语言模型。

然而,这种方法有一个严重的缺陷,即字幕与视觉特征相比丢失了大量信息,因此不同模态间的信息协同作用丧失了。

此外,之前的研究表明,预训练的单模态模型的跨模态对齐并不容易。例如,在 BLIP-2 中,为了使视觉变换器和语言模型能够互相交流,他们需要在两者之间增加一个额外的变换器。

## BLIP-2: when ChatGPT meets images

BLIP-2 是一种新的视觉语言模型,能够进行关于图像的对话。

levelup.gitconnected.com

考虑到这些挑战,作者决定研究是否可以训练一个具有 10 亿参数的多模态 CoT 模型。

这项工作集中在 10 亿模型上,因为它们可以用消费级 GPU(例如 32G 内存)进行微调和部署。在这一部分,我们将研究为什么 10 亿模型在 CoT 推理中失败,并研究如何设计有效的方法以克服这一挑战。 (source)

为什么小模型在 CoT 中失败,如何设计以克服这一挑战?

图片由 Jason Leung 提供,来源于 Unsplash

实际上,训练小模型进行推理的方法已经被尝试过。然而,之前的尝试中使用了一个大型模型作为教师和一个小型模型作为学生。

例如,作者为教师模型提供了一个提示,并使用了“让我们一步一步思考”的方法来获得解释推理的答案。然后,将提示加上演示提供给较小的模型。

“我们考虑一种由多个阶段组成的方法。首先,使用多步骤推理提示一个大型教师模型回答问题,而不依赖于正确的示例。也就是说,教师采用零-shot 链式思维推理生成输出。然后,我们使用生成的推理样本(包括问题和教师输出)对更小的学生模型进行微调。” 图像来源 (here)

然而,这种方法仍然需要使用大型语言模型及其所有缺点。

作者决定探索小型模型可以针对多模态-CoT 进行微调的可能性。简而言之,融合多模态特征允许模型架构更加灵活地调整(与提示相关)。然而,主要问题仍然存在:“关键挑战在于,参数少于 100 亿的语言模型往往生成幻觉理由,从而误导答案推断。”

首先,为什么小模型在 CoT 推理中会出现幻觉?

作者提出了相同的问题:调查为什么 1-B 模型在 CoT 推理中失败。一旦理解了这一点,研究有效的方法。

作者首先对文本-only 基线模型进行了 CoT 推理的微调。在这种情况下,问题被建模为文本生成问题。基线包括问题(Q)、上下文(C)和多个选项(O),模型必须预测答案(A)。作者将基线与在答案前预测理由(R)(QCM→RA)以及理由用于解释答案(QCM→AR)进行了比较。

(source)

结果令人惊讶,如果模型首先预测理由,准确率下降超过 10%:“结果表明,理由可能不一定有助于预测正确答案。”换句话说,似乎推理反而对答案有害。

但为什么呢?

为了理解这一点,作者决定将问题分成两个阶段。首先,生成理由,然后利用这些理由回答问题。模型在生成高质量理由方面成功了(RougeL是一种用于自动摘要和机器翻译的度量),但同时,似乎对准确性推断(问题的答案)产生了不利影响。

(source)

理由并没有帮助提高答案的准确性。因此,作者选择了 50 个随机错误案例并手动检查。发现模型在生成理由时经常出现幻觉,因为缺乏对视觉内容的参考。

(source)

这是最常见的错误,超过 60%的错误归因于这一因素。

(来源)

那么为什么不提供关于图像内部内容的信息呢? 作者使用了一个管道来生成标题并将其提供给模型(将标题附加到输入中)。然而,这导致边际准确度的增加(0.59 个百分点,在表 3 中)。

作者随后测试了另一种方法,将图像作为输入传递给DETR模型,目的是提取视觉特征。他们将这些视觉特征与编码后的语言表示结合在一起。换句话说,文本由 LM 编码器编码,图像由视觉模型编码。这两个输出结合在一起,成为 LM 解码器的输入。

结果显示(见表 3),这不仅改善了理由生成,还提高了回答的准确性。换句话说,拥有更好的理由“幻觉现象得到了缓解。”视觉特征对更好的回答有益,但这些有用的信息可能在生成标题的过程中丢失了。

理解了模型为什么会出现幻觉之后,我们可以使用什么框架来进行高效的多模态-CoT?

作者建议将语言(文本)和视觉(图像)模态结合到一个两阶段框架中:首先生成理由,然后生成回答。

模型架构在两个步骤中是相同的;然而,输入和输出有所变化。在第一步中,模型接受语言和视觉输入以生成理由。在第二步中,提供了原始语言输入,并将其附加到第一阶段生成的理由中。这经过第二模型的编码器,然后添加视觉特征并使用解码器得到最终答案。

“我们多模态-CoT 框架的概述。Multimodal-CoT 由两个阶段组成:(i)理由生成和(ii)答案推断。两个阶段共享相同的模型架构,但在输入和输出上有所不同。在第一阶段,我们将语言和视觉输入提供给模型以生成理由。在第二阶段,我们将原始语言输入与第一阶段生成的理由附加在一起。然后,我们将更新后的语言输入与原始视觉输入一起提供给模型以推断答案。” (来源)

小模型能有竞争力吗?

照片由Steven Lelham拍摄,来自 Unsplash

我们已经了解了为什么小模型在 CoT 过程中会产生幻觉,如何解决这个问题,现在还需要了解这种方法是否在与大型模型和其他方法相比时具有竞争力。

作者决定使用 ScienceQA 基准:

ScienceQA 是首个大规模的多模态科学问题数据集,注释了详细的讲解和解释。它包含 21,000 个多模态选择题,涵盖了 3 个学科、26 个主题、127 个类别和 379 项技能的丰富领域多样性。 (来源)

为了使用视觉特征,他们需要一个使用编码器-解码器的模型,因此选择了T5。此外,为了更好地研究该方法是否对其他模型具有通用性,他们还选择了FLAN-T5。他们还决定将其与多个模型和人类进行比较。

结果表明,他们的方法优于 GPT-3.5,并且在各种问题类别中也超越了人类(无论是*均水*还是在不同类别中)。UnifiedQA 和 GPT-3.5 使用了字幕,结果表明视觉特征更为有效。

(来源)

消融研究表明,使用两阶段方法能够充分发挥视觉特征的优势。

(来源)

此外,作者指出,多模态性能提升收敛性。实际上,这种两阶段模型从训练开始时就能实现更高的准确性。

“No-CoT 基准和 MultimodalCoT 变体在各个时期的准确性曲线。” (来源)

作者表示,该方法具有广泛的模型通用性来提取视觉特征,他们选择了 DETR,因为它提供了最佳的准确性。

(来源)

选择的文本模型也是具有通用性的。也就是说,该方法即使在使用不同的语言模型时也能有效。

(来源)

然后,作者检查了 50 个答案正确的例子和 50 个答案错误的例子,以更好地理解机制。结果表明,CoT 并不总是对答案有利,但模型非常稳健,在某些情况下即使推理错误也能正确回答。此外,当答案错误时,大多数错误是由于常识性错误。

(来源)

当问题需要常识知识时,该模型在大多数情况下会出现常识错误:例如,理解地图或计算图像中的数字,或使用字母表。错误示例:

(source)

作者表示,这些结果为模型的未来修改提供了线索:

可以通过以下方式改进 MultimodalCoT:(i)结合更有信息量的视觉特征,改善语言-视觉交互,以便理解地图和计数;(ii)注入常识知识;(iii)应用过滤机制,例如,只使用有效的 CoT 来推断答案,并去除无关的 CoT。(source)

作者已将模型、代码和数据集上传到 GitHub,供希望测试或了解更多的人使用:

[## GitHub - amazon-science/mm-cot: “Multimodal Chain-of-Thought Reasoning”的官方实现…]

“想象一下学习一本没有图表的教科书。”Multimodal-CoT 在解耦的方式中融入了视觉特征…

github.com](https://github.com/amazon-science/mm-cot?source=post_page-----961a8ab9d0fa--------------------------------)

告别思考

本研究中的作者正式研究了多模态 CoT。他们分析了为什么小模型在 CoT 期间会产生幻觉,并展示了小模型在多模态 CoT 中能够超越大型模型(甚至超越人类表现)的能力。关键是能够最佳地结合文本和视觉模态。

这是通过使用两阶段方法实现的,第一阶段使用视觉特征创建推理,然后利用这一最佳推理来获得答案。作者进行的分析给出了如何获得更好模型的建议。

简而言之,这篇论文的结果表明,即使是一个小模型也能解决复杂问题。此外,为模型提供正确的多模态特征是至关重要的。无需一个拥有数十亿参数的大型语言模型,因为对视觉特征有了解的小模型在图像描述方面表现得更好。

如果你觉得这些内容有趣:

你可以查看我的其他文章,也可以订阅以便在我发布新文章时获得通知,你还可以在LinkedIn上联系我

这是我的 GitHub 仓库的链接,我计划在这里收集与机器学习、人工智能等相关的代码和许多资源。

[## GitHub - SalvatoreRa/tutorial: 机器学习、人工智能、数据科学教程…]

机器学习、人工智能、数据科学的教程,包含数学解释和可重用的代码(使用 Python…)

GitHub 教程

或许你对我最*的一篇文章感兴趣:

[## PCA:生物信息学家最喜爱的工具可能会产生误导

一项新研究评估了一个最常用的技术可能带来的问题

PCA:生物信息学家最喜爱的工具可能会产生误导 [## 稳定扩散与大脑:AI 如何读取我们的思想

研究人员能够利用 fMRI 数据重建图像。

稳定扩散与大脑:AI 如何读取我们的思想 [## 微软 BioGPT:走向生命科学领域的 ChatGPT

BioGPT 在不同的生物医学 NLP 任务中达到了 SOTA

微软 BioGPT:走向生命科学领域的 ChatGPT [## 稳定扩散填补医学图像数据中的空白

一项新研究显示,稳定扩散可以帮助医学图像分析和罕见疾病。怎么做?

稳定扩散填补医学图像数据中的空白

R 中的多项逻辑回归

原文:towardsdatascience.com/multinomial-logistic-regression-in-r-428d9bb7dc70

R 中的统计系列

Md Sohel MahmoodTowards Data Science Md Sohel Mahmood

·发布于 Towards Data Science ·6 分钟阅读·2023 年 1 月 29 日

--

图片由 Edge2Edge Media 提供,来源于 Unsplash

介绍

如果你阅读了我之前关于“R 中的统计系列”的文章,你可能对 R 中的逻辑回归实现有了较好的理解,也对不同类型的逻辑回归模型有了基本了解。我们在回归领域已经走了很长一段路,覆盖了二项、比例优势 (PO)、广义以及部分比例优势 (PPO) 模型。在这篇文章中,我将讨论多项逻辑回归,它是特定应用的,并且可能对理解具有多个无序响应变量的逻辑回归模型至关重要。例如,可能出现的回归类型是人们对政党的归属,例如共和党、民主党、独立党等。

简要回顾

首先,我们来回顾一下我们已经覆盖的内容。起初,我们介绍了二项逻辑回归,它没有有序响应。在这里,我们只有两个类别,例如健康或不健康。接着,我们深入探讨了具有多个有序响应的有序逻辑回归模型。在这里,我们对健康状态进行了更多有序分类,并将其输入到有序模型中。该模型的一个基本假设是系数独立于类别,并且它们在响应之间不变化。例如,如果我们有四个健康状态类别,并按 1、2、3、4 排列。我们假设每个独立变量的系数在这四个类别中保持不变。这被称为比例优势 (PO) 假设。

后来我们进入了广义逻辑模型,并允许所有系数在各类别之间变化。例如,当我们考虑健康状态从 1 变为 2 时,某个特定自变量的系数将与健康状态从 2 变为 3 时的系数值不同。

之前讨论过的另一种模型是部分比例奇数(PPO)模型。在这种模型中,仅允许违反比例奇数假设的变量在各类别之间变化。例如,如果我们将健康状态作为响应变量,教育年限、婚姻状态和家庭收入作为预测变量,如果我们发现只有教育状态违反了比例奇数假设,我们可以为此提出 PPO 模型,而不是广义有序回归模型。我知道这可能听起来有点复杂,但如果你查看我之前关于“R 语言统计系列”的文章,希望事情会变得更加易懂。目前,下面的表格展示了我们在模型定义和执行方面所涵盖的内容。

什么是多项式回归?

简单来说,多项式回归模型通过对数比或对数赔率方法估计个体落入特定类别的可能性,相对于基准类别。它类似于当名义响应变量有超过 2 个结果时对二项分布的扩展。在多项式回归中,我们需要定义一个参考类别,模型将确定相对于参考类别的多个二项分布参数。

在我们的示例中,我们将设置一个特定的健康状态作为基准类别,并以此进行多项式逻辑回归。简言之,多项式回归就像一次性执行多个二项回归模型。

数据集

UCI 仓库中的成人数据集将再次用于多项式逻辑回归的实现。下面是数据集的快速查看。

成人数据集来自 UCI 仓库

  • 教育:数值型和连续型

  • 婚姻状态:二元(0 为未婚,1 为已婚)

  • 性别:二元(0 为女性,1 为男性)

  • 家庭收入:二元(0 为*均或低于*均,1 为高于*均)

  • 健康状态:有序(1 为差,2 为一般,3 为好,4 为优秀)

在这里,尽管健康状态是有序的,但我们实际上不需要它,因为我们将执行多个二项逻辑回归。

R 语言中的实现

要实现多项式逻辑回归,我将使用 VGAM 包中的 vglm() 命令。代码片段如下。

如前所述,我们需要定义一个参考类别。在这里,我将健康状态水*=1 定义为参考类别。

结果解释

模型总结

好的。我们已经总结了上述定义的模型。这是一个具有多个结果类别的预测模型。预测变量是教育年限(连续变量),结果是类别变量,实际上是有序的,但我们不需要类似的有序响应。我们可以简单地将数值分配给这些类别。

总结与二元逻辑回归类似,但我们有多个二元回归模型,所有模型都与参考类别进行比较。让我们参考上述系数块。

第一个模型使用 intercept1 和 educ1 比较类别 2 与参考类别 1(模型定义中已定义)。第二个模型使用 intercept2 和 educ2 比较类别 3 与参考类别 1。第三个模型使用 intercept3 和 educ3 比较类别 4 与参考类别 1。由于有四个类别,因此只能进行三种二元比较。

model1:

model2:

model3:

相同的二元逻辑回归解释也适用于此。例如,在 model1 中,我们可以说,每增加一个单位的 educ 变量,类别 2 相对于类别 1 的对数几率增加 0.08969,其余两个模型也是如此。所有情况下的斜率都是正的,因为随着教育水*的提高,具有更好健康状态的对数几率也会增加。这是显而易见的,但我们通过给定的数据集量化了这一事实。

R 还对每个预测变量进行了假设检验。原假设声明预测变量在预测对数几率中不是显著变量。educ1 的相关 p 值为 0.00853,小于 0.01。因此,我们可以拒绝原假设,并得出结论,educ1 在确定类别 2 相对于参考类别 1 的对数几率中发挥了显著作用。

我们还可以在 R 中确定奇数比,如下所示。

奇数比

这基本上是系数的指数值。这告诉我们,每增加一个单位的 educ,处于类别 2 的几率相比于类别 1 增加了 1.09350 倍。换句话说,每增加一个单位的 educ,处于类别 2 的几率增加了 9.35%。

对于其他模型,百分比变得显著。每增加一个单位的教育,处于类别 3 的概率增加 25.23%,而每增加一个单位的教育,处于类别 4 的概率增加 35.82%,相对于参考类别 1。

结论

在这篇文章中,我讨论了多项逻辑回归模型的必要性,并在 R 中进行了实现。这种回归类型类似于二项回归,只是我们进行的是多个二元比较。当我们有像政党归属这样的无序类别时,可以进行多项逻辑回归。

数据集致谢

Dua, D. 和 Graff, C. (2019). UCI 机器学习库 [http://archive.ics.uci.edu/ml]。加州尔湾:加州大学信息与计算机科学学院 (CC BY 4.0)

[## 使用我的推荐链接加入 Medium - Md Sohel Mahmood

阅读 Md Sohel Mahmood(以及 Medium 上的其他成千上万名作者)的每一篇故事。您的会员费用将直接...

获取 Md Sohel Mahmood 发布时的电子邮件通知 [## 订阅 Md Sohel Mahmood 的所有故事

每当 Md Sohel Mahmood 发布新内容时,你将收到电子邮件通知。通过注册,如果你还没有 Medium 帐户,将创建一个...

订阅 Medium 使用我的推荐链接 - Md Sohel Mahmood

PyTorch 中的多 GPU 训练及其替代方案:梯度累积

原文:towardsdatascience.com/multiple-gpu-training-in-pytorch-and-gradient-accumulation-as-an-alternative-to-it-e578b3fc5b91

代码与理论

Alexey KravetsTowards Data Science Alexey Kravets

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 7 月 24 日

--

unsplash.com/photos/vBzJ0UFOA70

在这篇文章中,我们首先将探讨数据并行(DP)和分布式数据并行(DDP)算法的区别,然后解释什么是梯度累积(GA),最后展示 DDP 和 GA 如何在 PyTorch 中实现,并且它们如何导致相同的结果。

引言

在训练深度神经网络(DNN)时,一个重要的超参数是批量大小。通常,批量大小不应过大,因为网络会倾向于过拟合,也不应过小,因为这会导致收敛速度缓慢。

当处理高分辨率图像或其他占用大量内存的数据时,假设如今大多数大 DNN 模型的训练都是在 GPU 上进行的,根据可用 GPU 的内存,适配小批量大小可能会有问题。因为,如前所述,小批量大小会导致收敛缓慢,我们可以使用三种主要方法来增加有效批量大小:

  1. 使用多个小型 GPU 并行运行模型于小批量 — DP 或 DDP 算法

  2. 使用更大的 GPU(昂贵)

  3. 在多次步骤中累积梯度

现在让我们详细探讨 1. 和 3. — 如果你幸运地拥有一张可以容纳所有数据的大 GPU,你可以阅读 DDP 部分,了解它是如何在 PyTorch 中实现的,跳过其他部分。

假设我们想要有效的批量大小为 30,但每个 GPU 上只能容纳 10 个数据点(小批量大小)。我们有两个选择:数据并行或分布式数据并行:

数据并行(DP)

首先,我们定义主 GPU。然后,执行以下步骤:

  1. 将 10 个数据点(小批量)和模型的副本从 Master GPU 移动到其他 2 个 GPU。

  2. 在每个 GPU 上进行前向传播,并将输出传递给 Master GPU。

  3. 在 Master GPU 上计算总损失,然后将损失返回到每个 GPU,以计算参数的梯度。

  4. 将梯度(这些是所有训练样本的梯度*均值)返回到 Master GPU,将它们求和以获得整个 30 个数据点批次的*均梯度。

  5. 更新 Master GPU 上的参数,并将这些更新发送到其他两个 GPU 以进行下一次迭代。

这个过程存在一些问题和低效之处:

  • 数据从 Master GPU 传递,然后在其他 GPU 之间分割。此外,Master GPU 的利用率高于其他 GPU,因为总损失的计算和参数更新都发生在 Master GPU 上。

  • 我们需要在每次迭代时同步其他 GPU 上的模型,这可能会减慢训练速度。

分布式数据并行(DDP)

分布式数据并行(Distributed Data Parallel)旨在改善数据并行(Data Parallel)算法的低效问题。我们仍然使用之前的设置——每批次 30 个数据点,3 个 GPU。区别如下:

  1. 它没有 Master GPU。

  2. 由于我们不再拥有 Master GPU,我们直接从磁盘/RAM 上以 非重叠 的方式并行加载每个 GPU 上的数据——DistributedSampler 负责这项工作。在底层,它使用本地排名(GPU id)将数据分配到各个 GPU 上——给定 30 个数据点,第一个 GPU 将使用点 [0, 3, 6, … , 27],第二个 GPU [1, 4, 7, .., 28],第三个 GPU [2, 5, 8, .. , 29]。

n_gpu = 3
for i in range(n_gpu):
  print(np.arange(30)[i:30:n_gpu])

前向传播、损失计算和反向传播在每个 GPU 上独立执行,梯度异步减少,计算均值,然后所有 GPU 上的更新进行。

由于 DDP 相对于 DP 的优势,现如今更倾向于使用 DDP,因此我们仅展示 DDP 实现。

梯度累积

如果我们只有一个 GPU,但仍想使用更大的批次大小,可以选择在一定步数内累积梯度,有效地累积一定数量的小批次的梯度,从而增加有效批次大小。以上述示例为例,我们可以累积 10 个数据点的梯度进行 3 次迭代,以实现与 DDP 训练中有效批次大小为 30 相同的结果。

DDP 过程 代码

下面我将仅讲述实现 DDP 与单 GPU 代码相比的区别。完整代码可以在下几节中找到。首先,我们初始化进程组,以允许不同进程之间进行通信。通过 int(os.environ[“LOCAL_RANK”]),我们可以检索给定进程中使用的 GPU。

init_process_group(backend="nccl")
device = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(device)

然后,我们需要将模型包装在 DistributedDataParallel 中,以启用多 GPU 训练。

model = NeuralNetwork(args.data_size) 
model = model.to(device) 

if args.distributed:
  model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[device])

最后一部分是定义 DistributedSampler,我在 DDP 部分提到过。

sampler = torch.utils.data.DistributedSampler(dataset)

其余的训练保持不变——我将在文章的末尾包括完整代码。

梯度累积代码

当发生反向传播时,在调用loss.backward()后,梯度会存储在各自的 Tensor 中。实际更新发生在调用optimizer.step()时,然后存储在 Tensor 中的梯度会被optimizer.zero_grad()设置为零,以便进行下一次反向传播和参数更新。因此,为了累积梯度,我们调用loss.backward()进行所需的梯度累积次数,而不将梯度设置为零,以便它们在多个迭代中累积,然后我们对累积梯度迭代的*均梯度进行*均loss = loss/ACC_STEPS)。之后,我们调用optimizer.step()并将梯度置零,以开始下一次梯度累积。在代码中:

ACC_STEPS = dist.get_world_size() # == number of GPUs
# iterate through the data
for i, (idxs, row) in enumerate(loader):
  loss = model(row)  
  # scale loss according to accumulation steps
  loss = loss/ACC_STEPS
  loss.backward()
  # keep accumualting gradients for ACC_STEPS
  if ((i + 1) % ACC_STEPS == 0):
    optimizer.step()  
    optimizer.zero_grad()

完整代码

import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"
print(os.environ["CUDA_VISIBLE_DEVICES"])

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset, Sampler
import argparse
import torch.optim as optim 
import numpy as np
import random
import torch.backends.cudnn as cudnn
import torch.nn.functional as F

from torch.distributed import init_process_group
import torch.distributed as dist

class data_set(Dataset):

    def __init__(self, df):
        self.df = df

    def __len__(self):
        return len(self.df)

    def __getitem__(self, index):    

        sample = self.df[index]
        return index, sample

class NeuralNetwork(nn.Module):
    def __init__(self, dsize):
        super().__init__()
        self.linear =  nn.Linear(dsize, 1, bias=False)
        self.linear.weight.data[:] = 1.

    def forward(self, x):
        x = self.linear(x)
        loss = x.sum()
        return loss

class DummySampler(Sampler):
    def __init__(self, data, batch_size, n_gpus=2):
        self.num_samples = len(data)
        self.b_size = batch_size
        self.n_gpus = n_gpus

    def __iter__(self):
        ids = []
        for i in range(0, self.num_samples, self.b_size * self.n_gpus):
            ids.append(np.arange(self.num_samples)[i: i + self.b_size*self.n_gpus :self.n_gpus])
            ids.append(np.arange(self.num_samples)[i+1: (i+1) + self.b_size*self.n_gpus :self.n_gpus])
        return iter(np.concatenate(ids))

    def __len__(self):
        # print ('\tcalling Sampler:__len__')
        return self.num_samples

def main(args=None):

    d_size = args.data_size

    if args.distributed:
        init_process_group(backend="nccl")
        device = int(os.environ["LOCAL_RANK"])
        torch.cuda.set_device(device)
    else:
        device = "cuda:0"

    # fix the seed for reproducibility
    seed = args.seed

    torch.manual_seed(seed)
    np.random.seed(seed)
    random.seed(seed)
    cudnn.benchmark = True

    # generate data
    data = torch.rand(d_size, d_size)

    model = NeuralNetwork(args.data_size)    
    model = model.to(device)  

    if args.distributed:
        model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[device])

    optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
    dataset = data_set(data)

    if args.distributed:
        sampler = torch.utils.data.DistributedSampler(dataset, shuffle=False)
    else:
        # we define `DummySampler` for exact reproducibility with `DistributedSampler`
        # which splits the data as described in the article. 
        sampler = DummySampler(dataset, args.batch_size)

    loader = DataLoader(
                dataset,
                batch_size=args.batch_size,
                num_workers=0,
                pin_memory=True,
                sampler=sampler,
                shuffle=False,
                collate_fn=None,
            )          

    if not args.distributed:
        grads = []

    # ACC_STEPS same as GPU as we need to divide the loss by this number
    # to obtain the same gradient as from multiple GPUs that are 
    # averaged together
    ACC_STEPS = args.acc_steps 
    optimizer.zero_grad()

    for epoch in range(args.epochs):

        if args.distributed:
            loader.sampler.set_epoch(epoch)

        for i, (idxs, row) in enumerate(loader):

            if args.distributed:
                optimizer.zero_grad()

            row = row.to(device, non_blocking=True) 

            if args.distributed:
                rank = dist.get_rank() == 0
            else:
                rank = True

            loss = model(row)  

            if args.distributed:
                # does average gradients automatically thanks to model wrapper into 
                # `DistributedDataParallel`
                loss.backward()
            else:
                # scale loss according to accumulation steps
                loss = loss/ACC_STEPS
                loss.backward()

            if i == 0 and rank:
                print(f"Epoch {epoch} {100 * '='}")

            if not args.distributed:
                if (i + 1) % ACC_STEPS == 0: # only step when we have done ACC_STEPS
                    # acumulate grads for entire epoch
                    optimizer.step()  
                    optimizer.zero_grad()
            else:
                optimizer.step() 

        if not args.distributed and args.verbose:
            print(100 * "=")
            print("Model weights : ", model.linear.weight)
            print(100 * "=")
        elif args.distributed and args.verbose and rank:
            print(100 * "=")
            print("Model weights : ", model.module.linear.weight)
            print(100 * "=")

if __name__ == "__main__":

    parser = argparse.ArgumentParser()
    parser.add_argument('--distributed', action='store_true',)
    parser.add_argument('--seed', default=0, type=int) 
    parser.add_argument('--epochs', default=2, type=int) 
    parser.add_argument('--batch_size', default=4, type=int) 
    parser.add_argument('--data_size', default=16, type=int) 
    parser.add_argument('--acc_steps', default=3, type=int) 
    parser.add_argument('--verbose', action='store_true',)

    args = parser.parse_args()

    print(args)

    main(args)

现在,如果我们运行这两个脚本:

  • python3 ddp.py — epochs 2 — batch_size 4 — data_size 8 — verbose — acc_steps 2

  • torchrun — standalone — nproc_per_node=2 ddp.py — epochs 2 — distributed — batch_size 4 — data_size 8 — verbose

我们会看到获得完全相同的最终模型参数:

# From Gradient Accumulator
Model weights :  Parameter containing:
tensor([[0.9472, 0.9440, 0.9527, 0.9687, 0.9570, 0.9343, 0.9411, 0.9186]],
       device='cuda:0', requires_grad=True)

# From DDP:
Model weights :  Parameter containing:
tensor([[0.9472, 0.9440, 0.9527, 0.9687, 0.9570, 0.9343, 0.9411, 0.9186]],
       device='cuda:0', requires_grad=True)

结论

在这篇文章中,我们简要介绍了 DP、DDP 算法以及梯度累积的基本概念,并展示了如何在没有多个 GPU 的情况下增加有效批量大小。需要注意的是,即使我们获得了相同的最终结果,使用多个 GPU 的训练速度也远快于使用梯度累积,因此如果训练速度很重要,那么多个 GPU 是加速训练的唯一途径。

结构方程模型中的多组分析

原文:towardsdatascience.com/multiple-group-analysis-in-structural-equation-modeling-2b2c7eef329?source=collection_archive---------7-----------------------#2023-06-16

测试子群体之间的效应

Laura Castro-SchiloTowards Data Science Laura Castro-Schilo

·

关注 发表在 Towards Data Science ·6 分钟阅读·2023 年 6 月 16 日

--

多组分析(MGA)是一种统计技术,允许研究人员通过指定具有组特定估计值或在各组之间具有相等估计值的结构方程模型(SEMs),来研究子群体或人口统计细分中的差异。

可以使用 MGA 来研究均值、回归、载荷、方差和变量的协方差之间的差异,因为所有这些参数都可以在 SEM 中建模。因此,即使其他建模技术(例如方差分析或带有交互效应的回归)也可以调查分组变量的作用,但这些技术在 SEM 中的灵活性不如 MGA。

图 1. 多组分析的一般概述以及推断策略。图像由作者提供。

多组分析的常见用途

每当有兴趣探索组间差异时,MGA 可以成为一个有用的工具。当数据是关于个人时,组通常基于具有少量层级的因素(例如,性别、民族、职业、家庭状态、健康状态等)来定义,但也可以根据不同的领域、数据和分析背景来定义其他各种因素。以下是一些可以在几个不同领域使用 MGA 回答的问题示例:

消费者研究

  • 满意度(或质量)在不同的人口统计群体中是否有所不同?

人力分析

  • 员工表现(或动机)在公司各分支机构或部门之间是否相等?

医疗保健

  • 患者报告的结果是否因药物制造商而异?

营销

  • 新的营销活动在不同地理区域提高品牌声誉的效果如何?

心理学

  • 情感体验是否存在跨文化差异?

教育

  • 学业成就的增长在女性和男性之间是否相等?

在多个组中测量未观察到的变量

上述所有问题涉及的变量都是未观察到的(例如,满意度、表现等),也称为潜在变量。由于这些变量无法直接观察,因此测量起来比较困难。

图 2. 比较未观察(潜在)变量与观察变量的测量。图像由作者提供。

这样一个困难是,不同的组可能对这些变量有不同的概念化。问问自己:

什么满意度?

什么良好的表现?

你的回答是否可能与那些具有不同生活经历的人的回答不同?

很多时候,答案是肯定的。

幸运的是,我们可以通过实证测试不同组是否以相似的方式概念化潜在变量。这个测试在 SEM 框架下通过 MGA 进行,称为因子不变性(也称为测量不变性)。因子不变性测试对于确保跨组比较的有效性至关重要;因此,如果存在潜在变量,这些测试必须在比较组间的回归或均值(即结构参数)之前进行。

图 3. 模型化未观察变量的挑战在于,它们可能在子群体之间测量的内容不相同。图片由作者提供。

参数差异测试

为了测试组间参数的差异,研究人员通常会拟合有无组间等式约束的结构方程模型。然后,使用似然比检验(等效于卡方差异检验)和其他拟合统计量(例如比较拟合指数和均方根误差)比较这两个模型,以评估施加约束是否导致模型拟合的统计学显著恶化。如果模型的拟合没有显著恶化,则保留具有等式约束的模型,并得出结论认为考虑的总体在测试的参数上没有显著差异。相反,如果模型的拟合显著恶化,则保留没有约束的模型(即允许每个组具有自己的估计),并得出结论认为考虑的总体在测试的参数上存在显著差异。

下图展示了在进行简单线性回归的双组示例中 MGA 背后的策略。此图显示了对一个参数施加的等式约束。模型 1 具有零自由度(即,完全饱和),而模型 2 由于等式约束而具有一个自由度。这些模型通过它们的卡方差异进行比较,该差异也服从自由度为一的卡方分布(模型之间的自由度差异)。可以通过对多个参数同时施加等式约束来进行更不特定的测试。

图 4. 具有简单线性回归的双组示例中 MGA 背后的策略。图片由作者提供。

结构方程模型(SEMs)作为确认性模型开发而来。 也就是说,人们提出假设,将其转化为可检验的统计模型,然后通过推断来确定数据是否支持这些假设。这种方法也适用于 MGA,并且对于避免大规模的 I 型错误率至关重要,这种错误会导致发现数据中实际上不存在的统计效应。因此,不推荐进行所有可能的组间比较。

MGA 估计的直观理解

免责声明: 以下段落适用于希望深入了解 MGA 的方法论者。本节假设读者了解全信息最大似然估计器。此外,这里列出的步骤仅用于解释 MGA 背后的逻辑。实际上,按照这些步骤进行 MGA 会低效,因为统计软件应利用简化此过程的算法。

MGA 的估计与具有缺失数据的简单 SEM 没有不同。在 MGA-SEM 的标准实施中,用户提交他们想要分析的数据及一个分组变量,该变量指示每个观察值所属的组。需要一个简单的数据处理步骤——使用分组变量——来为多个组设置分析。下面的图示出了用于分析的数据和用于 MGA 的数据重组。

图 5. 用户输入的数据与进行多组分析后的数据重组。图像由作者提供。

现在可以使用重组数据与完全信息最大似然法作为估计量,确保数据中的所有行都提交用于分析,即使存在缺失数据。重组数据的一些方便结果包括:

  • 任何给定行的对数似然仅受非缺失单元格的影响,因此将所有‘Group 0’行的对数似然相加会得到该组的对数似然。同样,将所有‘Group 1’行的对数似然相加会得到组 1 的对数似然。每组的对数似然用于估计整体模型的卡方统计量,该统计量量化了每组的拟合度。

  • 缺失值的模式禁止在各组变量间估计任何参数(例如,Var1_0 和 Var1_1 的协方差无法估计),这并不重要,因为 MGA 关注的是跨组效果的比较,而不是组间估计。

  • ‘Vanilla SEM’ 允许对参数设置等式约束。因此,使用重组后的数据在 SEM 中,可以指定两个相同的模型,每个模型的变量子集不同,并且可以对各组间等效参数设置等式约束。再重复一遍,所有这些都可以在标准 SEM 中完成,而无需明确要求软件进行 MGA。

幸运的是,想进行 MGA-SEM 的用户无需执行这些步骤!SEM 软件通过允许用户指定分组变量,使拟合多组模型变得非常简单。然而,进行数据处理(见图 5)并使用标准 SEM 进行 MGA-SEM 将加深你对这个主题的理解。要了解更多信息,请查看以下引用的资源。

JMP 中应用的多组分析的逐步示例

有关因子(测量)不变性的多组分析的章节

Widaman, K. F., & Olivera-Aguilar, M. (2022). 使用确认性因素分析调查测量不变性。结构方程建模手册, 367.

期刊文章 关于使用替代适配度指数测试不变性:

Chen, F. F. (2007). 对适配度指数对测量不变性的敏感性。结构方程建模:多学科期刊14(3),464–504。

本文最初发表于 JMP 用户社区 ,发布时间为 2023 年 2 月 27 日。

多元线性回归:深入探讨

原文:towardsdatascience.com/multiple-linear-regression-a-deep-dive-f104c8ede236

从零开始的多元线性回归:深入理解

Md. ZubairTowards Data Science Md. Zubair

·发表于 Towards Data Science ·10 分钟阅读·2023 年 3 月 3 日

--

两个特征(x1 和 x2)的多元回归(作者图片)

动机

我们人类已经尝试了很长时间去创建智能系统。因为如果我们能自动化一个系统,它将使我们的生活更轻松,并且可以作为我们的助手。机器学习使这一切成为可能。解决不同问题的机器学习算法有很多。本文将介绍一种可以用多个变量解决回归问题(连续值预测)的机器学习算法。假设你在经营房地产生意。 作为业务负责人,你应该对建筑物、土地等的价格有一个合理的了解,以便使你的业务盈利。对于一个人来说,跟踪广泛区域的价格是相当困难的。一个高效的机器学习回归模型可以大有帮助。想象一下你输入的位置、大小和其他相关信息,系统会自动显示价格。多元线性回归正是可以做到这一点。难道不很有趣吗!

我将解释多元线性回归的过程,并展示从零开始的实现。

*注意 — 如果你对简单线性回归没有清晰的概念,我建议你先阅读* [***这篇文章****再深入了解多元线性回归。]*

目录

  1. **什么是多元线性回归?**

  2. **我们什么时候使用多元线性回归?**

  3. **多元线性回归的详细介绍**

  4. **线性回归的向量化方法**

  5. **用 Python 的动手实现**

什么是多元线性回归问题?

在简单线性回归中,只能存在一个特征(自变量)。但在多重线性回归中,特征的数量多于一个。它们都用于预测连续值。

简单线性回归问题(作者提供的图像)

看看表格。表中是产品价格与重量的关系。通过线性回归,如果我们可以为给定的值拟合一条线,我们可以通过将产品的重量插入模型来轻松预测价格。相同的过程也适用于多重线性回归。不同的是,特征不止一个,而是多个(自变量)。

多重线性回归问题(作者提供的图像)

上述示例有两个特征 “**年龄**”“**收入**”。我们需要预测这两个新特征的每月 “**支出**”。这就是多重线性回归的一个示例。

多重线性回归不限于两个特征。它可以有两个以上的特征。

何时使用多重线性回归?

简单或单变量线性回归仅适用于从一个自变量预测连续值。

简单线性回归的过程不适用于多个特征。如果我们需要从多个特征(变量)中预测连续值,我们必须应用多重线性回归。值得一提的是,数据必须是线性分布的。非线性数据不适合线性回归。

多重线性回归的详细信息

让我们尝试以可视化的方式表示多重线性回归。我试图保持模型简单,只包含两个自变量(特征)。

两个特征(x1 和 x2)的多重线性回归(作者提供的图像)

*x1 和 x2* 是两个特征(自变量)。假设 x1=4x2=5。如果将这些值投影到 x1-x2 *面上,我们将得到点 **A**。在多重回归模型中,我们需要从数据集中创建一个回归*面,如图所示。现在,在回归*面上画一条垂直线会将*面划分为一个特定的点。通过从交点向 y 轴画一条水*线,我们可以得到预测值。预测值是 y 轴 的交点。

*[注 — 我仅使用两个特征可视化多重线性回归以作演示,因为可视化超过两个特征是不可能的。在特征更多的情况下,过程是相同的。]*

让我们尝试更深入地探讨

在简单线性回归中,我们根据一个自变量预测一个因变量。 *[阅读* [*上一篇文章*](https://medium.com/towards-data-science/deep-understanding-of-simple-linear-regression-3776afe34473) *以获取关于简单线性回归的更详细解释。]*

例如,一个简单的线性回归方程,***yi=mxi + c***。这里,‘m’是回归线的斜率,而‘c’y轴截距值。

在有多个自变量的情况下,我们需要扩展回归方程,如下所示。

其中,

y表示因变量(预测回归值)。

x1,x2, ……,xn是不同的自变量。

m1, m2, m3,…….,mn象征着不同自变量的斜率系数。

m0y轴的截距点值。

现在,我们将从数据集中获取自变量。我们面临的主要挑战是找出斜率的系数值(m0,m2,……., mn )

让我们考虑第一部分中显示的多元线性回归问题数据集。

如果我们有*m0, m1 和 m2*的最佳值,预测个人的‘支出’就很容易了。我们可以通过输入年龄收入值来轻松获得‘支出’

但没有直接的方法来找到系数的最佳值。为此,我们需要通过梯度下降来最小化代价(损失)函数。

关于梯度下降的详细信息 —

在深入梯度下降之前,我们应该对代价函数有一个清晰的了解。代价函数就是一个误差函数。它衡量预测模型的准确性。我们将使用以下误差函数作为代价函数。

在这里,y̅i是预测值,yi是实际值。

**梯度下降**是一个优化算法。我们使用这个算法通过优化回归方程的系数来最小化代价函数。

梯度下降(图片作者提供)

红色曲线是代价函数的导数。为了优化系数,我们随机分配系数的权重。现在,我们将计算代价函数的导数。为了简化起见,我们将考虑简单的线性回归方程。

  1. 让我们用*(mxi+c)*替换***y̅i***。这意味着方程如下 —*

2. 对**m****c**的偏导数。

*[注意 — 你可能会发现一些代价函数是乘以 1/2n 而不是 1/n。这没什么大不了的。如果你使用 1/2n,导数会中和它,结果将是 1/n 而不是 2/n。在实现部分,我们也使用 1/2n。]*

3. 现在,我们将通过以下方程迭代更新mc的值。

α 是学习率,指示我们在每一步中移动的距离以最小化成本函数(如图所示)。迭代将继续进行,直到成本函数显著最小化。

对于多重线性回归,整个过程是相同的。让我们再次考虑多重线性回归的方程。

如果我们像简单线性方程一样计算系数的导数,我们将得到一种通用形式(如上所示)。

其中 ***j*** 取值为 ***1,2,…..,n,*** 代表特征。

对于 *m0*,导数将是 —

我们将使用以下公式同时更新所有系数。

对于 m0, 我们将使用以下方程。

我们将不断更新所有系数以拟合模型并计算成本。如果成本显著降低,我们将停止更新系数。

但这个过程在计算上是昂贵且耗时的。向量化使实现变得简单。

向量化线性回归方法

让我们再次考虑多重线性回归。

我们添加了一个常数 *xi0=1* 以方便计算。它不会影响前面的方程。让我们看看方程的向量化表示。

线性回归方程的向量化实现(图像由作者提供)

这里,yi=1…..z, **z** 是总数据集实例的数量。X 存储了所有特征值,直到 z 实例。

简而言之,向量化方程是 —

现在,向量化成本函数的 导数如下(详细解释)。

现在是时候使用下面给出的公式更新权重了。

是的!我们已经完成了理论过程。现在是时候将整个过程编写成代码了。

从头开始的 Python 实现

现在是时候开始动手编写代码了。我将逐步展示指南。

*[附注 — 我们使用* [*波士顿房价*](https://www.cs.toronto.edu/~delve/data/boston/bostonDetail.html) *数据集进行演示。* *它注册在公共领域。可从*[***这里***](https://www.kaggle.com/code/prasadperera/the-boston-housing-dataset/data)***]***

导入库

读取数据集

我们看到主数据集中没有列名。在下一步中,我们将根据文档设置列名。

设置列名

我们已经成功地将列添加到 Dataframe 中。

在本文中,我们的主要重点是理解多元线性回归的内部过程。因此,我们将主要关注实现过程,而不是项目的有效性。为了保持模型的简单性,我们将考虑高度相关的特征。

让我们找到与目标列的相关性 ***‘MEDV’***

为了方便起见,我们选择了三个特征,**‘RM’, ‘DIS’, 和 ‘B’**,以及目标值 **‘MEDV’**

规范化特征

归一化减少了模型的计算复杂度。因此,我们将对特征进行归一化。

将数据集拆分为测试集和训练集

我们必须将数据集拆分为训练集和测试集,以便进行评估。使用训练集,我们将训练模型,并用测试集评估我们的模型。

在这里,我们将 75% 的数据用于训练,其余 25% 用于测试。

带有向量化实现的梯度下降优化函数

让我们调用函数找出系数的最佳值。

该函数返回两个值,系数(w)和一个损失值列表。

优化过程的迭代可视化

创建预测函数以预测新值

让我们对测试特征进行预测。

将我们的自定义模型与标准 scikit-learn 库进行比较

在进行比较之前,我们需要创建一个用于多元线性回归的预测模型,使用 **scikit-learn**

使用 scikit-learn 的线性回归

从 MSE 的角度比较模型

两个 MSE 值非常相似。即使我们的模型的 MSE 稍低于 scikit-learn 模型。

这两个模型的预测差异有多大?

结果表明我们的优化非常完美,并且与 scikit-learn 的基准模型效果类似。

结论

我们主要使用多元线性回归来解决从多个特征预测连续值的问题。我们可以使用许多基准库和工具来实现线性回归。但我们无法了解算法的内部工作原理。通过这篇文章,我尝试从最基本的方面解释多元线性回归的过程。我相信这将为您提供对算法的清晰理解。

*您可以从* [***这里***](https://github.com/Zubair063/ML_articles/blob/main/Multiple%20Linear%20Regression/Multiple%20Linear%20Regression%20from%20scratch%20.ipynb) *下载完整的笔记本。*

[这篇 文章 指导我如何展示多元线性回归的概念。]

***关于 ML 算法从头实现的其他文章正在筹备中。***

之前关于 从头开始学习机器学习系列 的文章如下。

《从头开始的机器学习系列》***Machine Learning from Scratch Series***的先前文章如下。

## 深入理解简单线性回归

从零开始的线性回归:详细解释

towardsdatascience.com ## 从零开始的 K-means 聚类

K-means:用于数据聚类的最佳 ML 算法

towardsdatascience.com ## 从零开始的 KNN 算法

KNN 算法的实现和细节解释

towardsdatascience.com

**如果你是初学者,不要错过阅读以下系列。**

## 数据科学的统计学终极指南

数据科学中的统计学一览:标准指南

towardsdatascience.com

多变量分析 — 超越一次一个变量

原文:towardsdatascience.com/multivariate-analysis-going-beyond-one-variable-at-a-time-5d341bd4daca

Python 中的多变量分析与可视化

Farzad MahmoodinobarTowards Data Science Farzad Mahmoodinobar

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 1 月 12 日

--

一只反思数据的猫头鹰,来自 DALL.E 2

现在,企业和公司收集尽可能多的信息已成为一种常见做法,即使在收集时这些数据的使用场景尚不明确——希望是将来能够理解和利用这些数据。一旦这些数据集可用,数据驱动的个人将深入数据中,寻找其中隐藏的模式和关系。发现这些隐藏模式的工具之一就是多变量分析。

多变量分析涉及分析多个变量(即多变量数据)之间的关系,并理解它们如何相互影响。这是一个重要的工具,帮助我们更好地理解复杂的数据集,从而做出基于数据的明智决策。如果你仅对一次分析一个变量的影响感兴趣,可以通过单变量分析来实现,我在这篇文章中做了介绍。

## 单变量分析 — 介绍与实现

使用 seaborn 进行的单变量分析:统计数据可视化

towardsdatascience.com

现在我们已经熟悉了多变量数据,我们可以将单变量数据定义为多变量数据的一种特殊情况,其中数据只包含一个变量。类似地,双变量数据包含两个变量,依此类推。

在这篇文章中,我们将讨论数值和分类变量的双变量/多变量分析。因此,让我们先快速复习一下这两种变量的区别,然后再进入分析部分。

  • 数值变量: 代表一个可测量的量,可以是连续变量或离散变量。连续变量可以取某个范围内的任何值(例如身高、体重等),而离散数值变量只能取范围内的特定值(例如孩子的数量、停车场的汽车数量等)。

  • 分类变量: 代表一个组(或类别),可以取有限数量的值,例如汽车品牌、狗的品种等。

现在我们已经了解了这两种变量的区别,我们可以进入实际的分析部分。

我已经将这篇文章组织成一系列问答的格式,这是我个人认为的有效学习方法。我还在文末提供了一个链接,指向我用来创建这个练习的笔记本。阅读完这篇文章后,随时下载并练习!

让我们开始吧!

(除非另有说明,所有图像均由作者提供。)

[## 通过我的推荐链接加入 Medium - Farzad Mahmoodinobar

阅读 Farzad(以及 Medium 上其他作者)的每一个故事。您的会员费将直接支持 Farzad 和其他人……

medium.com](https://medium.com/@fmnobar/membership?source=post_page-----5d341bd4daca--------------------------------)

数据集

为了练习多变量分析,我们将使用来自 UCI 机器学习库(CC BY 4.0)的数据集,该数据集包括汽车价格及与每个汽车价格相关的一组汽车属性。为了简化过程,我已经清理和筛选了数据,可以从这个链接下载。

让我们先导入今天将要使用的库,然后将数据集读入数据框,并查看数据框的前 5 行,以熟悉数据。

# Import libraries
import numpy as np
import pandas as pd
import seaborn as sns
from scipy import stats
import matplotlib.pyplot as plt
%matplotlib inline

# Show all columns/rows of the dataframe
pd.set_option("display.max_columns", None)
pd.set_option("display.max_rows", None)

# Read the data
df = pd.read_csv('auto-cleaned.csv')

# Return top 5 rows of the dataframe
df.head()

结果:

在这篇文章中使用的列是自解释的,因此目前无需担心理解所有列。

让我们继续分析吧!

数值双变量分析

让我们从一个包含两个变量的双变量数据集开始。双变量分析的目标是理解两个变量之间的关系。有多种统计技术可以用来分析双变量数据,而散点图是其中最常见的一种。让我们看看散点图是如何工作的。

问题 1:

价格与发动机大小之间的关系是什么?直观上,我们可能会预期发动机较大的汽车价格较高(其他条件相同),但让我们看看数据是否支持这一点。创建一个以价格为 x 轴、发动机大小为 y 轴的散点图。

回答:

# Create the scatterplot
sns.regplot(data = df, x = 'price', y = 'engine-size', fit_reg = False)
plt.show()

结果:

价格与发动机大小的散点图

正如我们所见,数据中价格与发动机大小之间似乎存在正相关关系。需要注意的是,这并不意味着存在因果关系(无论这种关系是否正确),只是展示了两者之间的正相关性。让我们添加相关值,以便为参考提供量化的测量。

问题 2:

返回价格与其他变量之间的相关性,并按降序排列。

回答:

# Create the overall correlation
corr = np.round(df.corr(numeric_only = True), 2)

# Return correlation only with price
price_corr = corr['price'].sort_values(ascending = False)
price_corr

结果:

结果确认了我们在散点图中观察到的价格与发动机大小之间的正相关性。让我们尝试更深入地了解数据中的变异情况。

异质性与分层

数据中的异质性指的是数据集中的变异。例如,我们的数据集包含了不同的车身风格,如轿车、掀背车、旅行车、敞篷车等。我们是否期望这些车身风格中价格与发动机大小之间的相关性是相似的?例如,客户对敞篷车大发动机的增量支付意愿可能会高于对主要用于家庭的旅行车的支付意愿。让我们检验这个假设,看看不同车身风格之间是否存在这样的变异,通过对数据进行分层分析。

问题 3:

数据集包括具有不同车身风格的汽车价格,如“body-style”列所示。数据集中每个类别有多少行?

回答:

# Apply value_counts to the df['class'] column
df['body-style'].value_counts()

结果:

根据结果,共有五类。

问题 4:

创建每种车身风格的价格与发动机大小的散点图,以展示车身风格之间是否存在视觉上的差异。

回答:

sns.FacetGrid(data = df, col = 'body-style').map(plt.scatter, 'price', 'engine-size').add_legend()
plt.show()

结果:

价格与发动机大小的散点图,按车身风格分类

这确实很有趣!这些分布与我们在问题 1 中观察到的总体分布相差甚远,并展示了这五种车身风格之间的视觉差异。所有五种车身风格都如预期般展示了价格与发动机大小之间的正相关性,但坡度似乎在敞篷车上最高(尽管数据点较少),而在旅行车上较低。让我们查看相关数字来量化这些差异。

问题 5:

对于每种车身风格,价格与发动机大小之间的相关性是什么?

回答:

bodies = df['body-style'].unique()

for body in bodies:
    print(body)
    print(df.loc[df['body-style'] == body, ['price', 'engine-size']].corr())
    print()

结果确认了我们的视觉检查——所有车身类型的价格与发动机尺寸的相关性都是正的,其中敞篷车的相关性最高,旅行车的相关性最低,这正如我们直观上所预期的。接下来,我们将查看分类双变量分析。

分类双变量分析

在本节中,我们将创建一个类似的双变量分析,但用于分类变量。在统计学中,这种分析通常通过“列联表”(也称为交叉表或 crosstab)来可视化,它显示了两个(对于双变量)或更多(对于多变量)分类变量的观察频率或计数。让我们看一个例子,以便更好地理解列联表。

问题 6:

创建一个显示汽车车身类型和气缸数量的列联表。你在结果中看到模式了吗?

答案:

crosstab = pd.crosstab(df['body-style'], df['num-of-cylinders'])
crosstab

结果:

如果你对汽车有所了解,最常见的气缸数量是 4、6 和 8,这也是我们在表中看到的频率最多的地方。我们还可以看到,我们的数据集中大多数汽车是四缸的,车身类型为轿车和掀背车,其次是旅行车。你是否注意到我们在进行心理计算,以计算每种气缸数量和车身类型组合的百分比?列联表可以被规范化来解决这个问题。有三种方法可以规范化这样的表格:

  1. 每行的条目总和为 1

  2. 每列的条目总和为 1

  3. 整个表格的条目总和为 1

让我们在下一个问题中尝试其中一个。

问题 7:

创建一个类似于前一个问题的交叉表,规范化方式是每行的条目总和等于 1,四舍五入到两位小数。

答案:

我将在这里演示两种不同的方法以供学习。第一种方法使用 Pandas 的 crosstab,第二种方法使用 groupby。

# Approach 1

# Create the crosstab (similar to previous question)
crosstab = pd.crosstab(df['body-style'], df['num-of-cylinders'])

# Normalize the crosstab by row
crosstab_normalized = crosstab.apply(lambda x: x/x.sum(), axis = 1)

# Round the results to two decimal places
round(crosstab_normalized, 2)

结果:

# Approach 2

# Group by and count occurences using size method
grouped_table = df.groupby(['body-style', 'num-of-cylinders']).size()

# Pivot the results using unstack and apply the row normalization
grouped_table_normalized = grouped_table.unstack().fillna(0).apply(lambda x: x/x.sum(), axis = 1)

# Round the results to two decimal places
round(grouped_table_normalized, 2)

结果:

混合数值和分类数据的双变量分析

我们经常需要分析混合了数值和分类变量的数据,所以让我们看看如何在我们已经知道如何分别处理每种数据类型的情况下来完成这项工作。

问题 8:

创建一系列箱线图,展示不同车身类型(x 轴的分类变量)下价格(y 轴的数值变量)的分布。

答案:

# Set the figure size
plt.figure(figsize = (10, 5))

# Create the boxplots
sns.boxplot(x = df['body-style'], y = df['price'])
plt.show()

结果:

按车身类型分层的汽车价格箱线图

我个人认为这个可视化非常有用。例如,我们可以看到,相比硬顶车或敞篷车,掀背车的价格范围相对较小。与其他车身风格相比,敞篷车的起始价格较高,并且根据车辆的各种特征,价格范围似乎也很广泛。

如果我们只关注轿车,并查看价格范围如何随气缸数量变化?让我们创建箱线图。

# Set the figure size
plt.figure(figsize = (10, 5))

# Create the boxplots
sns.boxplot(x = df[df['body-style'] == 'sedan']['num-of-cylinders'], y = df[df['body-style'] == 'sedan']['price'])
plt.show()

结果:

按气缸数量分组的轿车价格箱线图

正如预期的那样,气缸数量增加时,价格也随之上升。

包含练习题的笔记本

下面是包含问题和答案的笔记本,供参考和练习。

结论

在这篇文章中,我们介绍了多变量分析作为发现数据中隐藏模式的工具,然后讲解了如何实现对数值变量、分类变量以及两者混合的分析。我们利用了散点图和箱线图等可视化工具来展示变量之间的关系,并在一些情况下量化了这些相关性。

感谢阅读!

如果你觉得这篇文章有帮助,请 关注我在 Medium 上 并订阅以接收我的最新文章!

主成分分析中的多变量过程控制,使用 T² 和 Q 误差

原文:towardsdatascience.com/multivariate-process-control-by-principal-component-analysis-using-t%C2%B2-and-q-errors-c94908d14b04?source=collection_archive---------8-----------------------#2023-04-26

使用和解读 Hotelling 的 T² 和*方预测误差 Q 在异常检测系统中的应用

Davide Massidda Towards Data Science Davide Massidda

·

关注 发表在 Towards Data Science · 12 分钟阅读 · 2023 年 4 月 26 日

[]

图片来自 geraltPixabay

作为数据科学家的工作中,构建制造过程控制的异常检测系统是一个重要部分,主成分分析(以下简称 PCA)在我的工具箱中扮演着关键角色。

科学文献建议使用两种度量来追踪 PCA 中的异常:Hotelling 的 T²和*方预测误差,也称为Q 误差。尽管它们的计算并不复杂,但基础软件包通常忽略它们,其含义仍然相当神秘。

网络上有数百个关于 PCA 的优秀教程。然而,尽管信息量很大,一些关于 PCA 在生产环境中使用的问题仍然需要得到充分解决。在本教程中,我将避免重复之前已有的内容,直截了当地切入主题。

本教程

在对 PCA 进行快速介绍后——仅仅强调一些要点——在本教程中,我们将深入探讨 T²和 Q 误差的使用,拆解它们的公式以理解它们能告诉我们什么。这里你不会找到有关 PCA 及其在异常检测中使用的全部故事,但我将重点讲解 T²和 Q 度量的含义:它们从何而来以及如何解读。

我将使用基础的 Python 代码,直接利用numpy中实现的 SVD 算法。我只会提供获得所描述结果所需的代码,省略生成图表的代码(你可以在 这里 找到)。

如果你至少对 PCA 有一个浅显的了解,请继续阅读。如果你不知道什么是主成分,建议你先阅读关于 PCA 的一般论文,然后再回来这里。

目录

过程控制数据

数据中心化和缩放 通过 PCA 进行过程控制

编码-解码系统

蒸馏 通过 PCA 进行异常检测

*方预测误差 Q

Hotelling 的 T² 实际中的异常检测从数据到主成分…再回来

Q 贡献

T²贡献 总结性意见

过程控制数据

在本教程中,我生成了一些特定的数据,模拟了工业工厂中的一个简单过程控制。在 这个仓库 中,你可以找到数据和一个笔记本,其中包括用于运行分析的完整代码。

让我们开始读取数据并简要探索它。

import pandas as pd

plant = pd.read_csv("plant_generated.csv")
plant.head()
 group  time        var1       var2        var3       var4        var5
0  train     1   83.346705  28.115372  455.898265  12.808663  227.974152
1  train     2   90.594521  33.497319  462.503195  14.079053  228.173486
2  train     3  101.275664  30.396332  492.407791  16.832834  250.212025
3  train     4   90.898109  29.143537  472.162499  16.505277  234.079354
4  train     5   84.898605  29.459506  467.872180  12.801665  238.440786

表格包括一个“group”列,将观察分为训练集(100 条记录)和测试集(30 条记录)。记录了五个变量。

variables = plant.columns[2:].tolist()
print(variables)
['var1', 'var2', 'var3', 'var4', 'var5']

每个变量在时间上被监控。

plant.groupby(["group"]).agg({"time": [len, min, max]})
 time
       len  min  max
group   
 test   30    1   30
train  100    1  100

相关矩阵显示了变量集之间的良好关系。没有出现“孤立社区”或冗余测量。

plant.loc[plant.group=="train", variables].corr()
 var1      var2     var3       var4      var5
var1  1.000000  0.536242  0.773872  0.534615  0.727272
var2  0.536242  1.000000  0.535442  0.451099  0.632337
var3  0.773872  0.535442  1.000000  0.613662  0.709600
var4  0.534615  0.451099  0.613662  1.000000  0.571399
var5  0.727272  0.632337  0.709600  0.571399  1.000000

数据的中心化和缩放

由于数据具有不同的尺度,我们需要在运行 PCA 之前对值进行中心化和缩放,得到一个训练集,其中每个变量的均值为 0,标准差为 1。

为了重新缩放未来数据,我训练一个“缩放器”对象来存储中心和尺度参数,以便我可以转换训练数据。我将原始观察数据称为“X”,将缩放后的数据称为“Z”。

import numpy as np
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

X_train = np.array(plant.loc[plant.group=="train", variables])
Z_train = scaler.fit_transform(X_train)

最后,看看每个自动缩放变量的时间序列记录。

图 1. 自动缩放变量的时间序列 — 图片来源于作者,许可证 CC0。

通过 PCA 进行过程控制

观察 PCA 的一种方式是将其视为一个经过训练的编码-解码系统,该数据集表示一个受控过程。在训练过程中,系统学习监控变量之间的关系规则。随后,依赖于这些规则,系统可以评估新数据,确定过程是否在控制之中。

PCA 将观察到的变量视作一个整体,并重新分配它们的变异性,构建新的正交变量:主成分(PCs)。从* k 个变量开始,PCA 可以得到 k 个 PCs。该算法估计系数(loadings*),用于与观察变量(Z)相乘以获得 PCs。

图 2. PCA 中数据空间与成分空间之间的关系 — 图片来源于作者,许可证 CC0。

实际上,PCA 找到规则(即系数)来数据从观察数据空间投影到主成分空间,从而获得 PC 得分。这种投影可以通过将 PC 得分与转置的系数矩阵相乘来恢复(见图 2)。

我在以下代码块中使用奇异值分解算法来估计我们植物数据的系数。

u, s, vh = np.linalg.svd(Z_train)
loadings = np.transpose(vh)
print(loadings)
array([[ 0.46831293, -0.07447163,  0.50970523,  0.1634115 , -0.69902377],
       [ 0.40387459,  0.76147727, -0.41265167,  0.29133908, -0.04333374],
       [ 0.47556921, -0.21952275,  0.30115426,  0.43988217,  0.66441965],
       [ 0.40644337, -0.58699292, -0.68019685, -0.01720318, -0.16516475],
       [ 0.47561121,  0.14783576,  0.12867607, -0.83344223,  0.20187888]])

系数数组报告变量在行上,PCs 在列上。这是矩阵的常规方向,但软件包的排列方式可能会有所不同。

我们可以通过将观察数据矩阵乘以系数矩阵来获得 PC 得分。

train_scores = Z_train @ loadings

编码-解码系统

PCA 的一个关键概念是每个 PC 都由所有变量提供(尽管根据系数值的不同,程度有所不同)。因此,当我们检查单个 PC 的得分时,我们实际上是在一次性检查所有变量(是的,就是这样!)。

然而,总的数据变异性在 PCs 之间并不均等分配,因为 PCs 是从最大变异性(PC_1)到较少变异性(PC_k)进行排序的。实际上,前面的 PCs 代表数据中的“信号”,而后面的 PCs 代表噪声。因此,我们可以丢弃那些没有显著变异性的 PCs,将信号与噪声分开。

下面的图展示了我们数据的时间序列 PC 得分。显然,随着成分排名的增加(特别是从第一个成分跳到第二个成分),变异性减少。

图 3. 主成分得分的时间序列 — 图片来源于作者,许可证 CC0。

蒸馏

在下面的图像中,我放大了图 2. 我用名为提炼数据的灰色节点表示了所有保留的主成分,而用另一个灰色节点表示了所有被舍弃的主成分。我使用“提炼”这个词是因为每个组件是许多变量的融合精华。

图 4. PCA 作为编码-解码系统 — 图片来源于作者,许可证 CC0。

我们可以通过对 SVD 的奇异值进行*方并除以自由度,计算每个组件吸收的方差量。

n = Z_train.shape[0]
variances = s**2 / (n - 1)

让我们来看看结果。

pd.DataFrame({
    "Component": [*range(1, len(s)+1)],
    "Variance": variances,
    "Proportion": variances/np.sum(variances),
    "Cumulative": np.cumsum(variances/np.sum(variances))
})
 Component  Variance  Proportion  Cumulative
0          1  3.485770    0.690182    0.690182
1          2  0.573965    0.113645    0.803828
2          3  0.498244    0.098652    0.902480
3          4  0.276403    0.054728    0.957208
4          5  0.216122    0.042792    1.000000

第一个组件占数据总方差的 69%。加上第二个组件,我们可以解释 80%。如果我们假设剩下的 20%是噪声,那么这两个组件就足以概括整个数据。

仅使用五个组件中的两个,就像在数据压缩中丢失信息。然而,由于丢失的部分理论上是噪声,我们通过舍弃最后几个组件获得了优势。

让我们隔离系统中的载荷和方差。

n_comp = 2
pca_loadings = loadings[:,:n_comp]
pca_variances = variances[:n_comp]

当新数据到达训练好的 PCA 时,它们会被投影到组件空间中(编码)。之后,它们会被重建以逆转过程,仅使用最初的主成分(解码)。

通过 PCA 检测异常

现在,我们深入探讨通过 Q 和 T²误差度量检测异常。首先,让我们了解它们代表什么。稍后我们将直接看到公式应用于数据。

*方预测误差 Q

由于我们在编码步骤中丢弃了噪声,重建的数据不能与观察到的数据完全一致。这种差异产生了误差 Q。

当过程控制系统返回 Q 类型的异常时,某些东西破坏了相关结构:一个或多个变量不再与其他变量和谐变化(“和谐”这一概念由相关矩阵定义)。

我们可以区分两种极端情况。

  • 一个变量出现了意外的值(不一定超出范围),并且不再能从其他变量中“预测”。如果期望两个变量有某种相关性,但其中一个的方向突然变化,就不再观察到它们的预期关系,从而发出警报。

  • 变量表现正常,但其他所有变量都偏离了预期。如果之前的信息显示,当其他变量偏离时,我们的变量也应该偏离,但实际没有,这个过程就出现了异常。

第二种情况可能显得矛盾。因此,我们需要考虑另一种类型的误差:Hotelling 的 T²。

Hotelling 的 T²

与 Q 不同,T²与观察数据的度量更相关:超出预期范围的值会产生极端的 T²。

T² 统计量与马哈拉诺比斯距离紧密相关。我们可以将 T² 视为基于观测数据阈值的多变量异常检测系统,但有一些相关的差异。

  1. 观测数据已从编码系统中去噪。

  2. 异常在 PC 级别被搜索,只有在此之后它们才会在观测数据级别进行解码。

第二点很有趣,因为 PCs 综合了整个变量组。使用 T²,我们不是逐个变量地从单变量角度寻找异常,而是一次性分析数据。

在训练过程中,我们存储了 PCs 的方差。现在,我们可以使用这些信息,将观测到的 PCs 方差与期望的方差进行比较,为每个 PC 计算“异常分数”。之后,我们可以解码这个异常分数以返回数据空间,从而获取每个变量对 PCs 变化的贡献。

实践中的异常检测

我们之前构建了 PCA 系统。现在,让我们开始使用它,将新数据投影到 PC 空间中。我们首先进行数据的居中和标准化,然后可视化变量的时间序列。

X_test = np.array(plant.loc[plant.group=="test", variables])
Z_test = scaler.transform(X_test)

在下图中,表示了新数据中每个变量的时间序列。

图 5. 居中和标准化测试数据的时间序列 — 图片来自作者,许可证 CC0。

疯狂的数据,不是吗?正如你所见,我模拟了一个疯狂的过程。四个变量从期望均值剧烈偏移到高值,而第五个变量在初始偏移后逐渐恢复到正常值。

从数据到 PCs……再回到数据

我们可以编码数据,计算 PC 分数:

test_scores = Z_train @ pca_loadings

随后,我们可以通过计算期望数据来解码 PCs:

test_expect = test_scores @ np.transpose(pca_loadings)

现在,让我们可视化重建数据的时间序列:

图 6. 使用 PC 子集构建的期望数据时间序列 — 图片来自作者,许可证 CC0。

好的,这里我们终于揭示了多变量过程控制的一个根本问题。如果仔细查看上面的图表,你会注意到重建的数据与观测数据(图 5)相比显示了两个主要缺陷。

  1. 前四个变量的观测数据在高峰处达到稳定状态。不同的是,它们的期望值在达到高峰后逐渐减少。

  2. 第五个变量的观测数据在峰值处迅速增加并迅速减少。不同的是,其期望值在高峰后略微减少。从本质上讲,其趋势类似于其他变量之一。

前四个变量的重建受到了第五个变量情况的影响,而第五个变量的重建受到了前四个变量情况的影响。因此,每个重建的变量都是整个数据集的混合体。

这种不准确性发生因为我们通过丢弃最后的 PCs 来解码。根据我们的 PCA,新数据的“疯狂”是噪声,因此它从最后的 PC 中被吸收,而这些 PC 吸收了训练数据中不存在的新数据的“奇异性”。

在生产环境中,Q 和 T²误差可以用来监控这些异常并理解其来源。对于这两种统计量,我们可以计算以下值:

  • 每个变量对每种误差度量的贡献;

  • 每种误差度量的*方累积贡献,生成 Q 和 T²。

你可以在这里找到详细的数学解释。在这篇文章中,我将专注于贡献,因为数据监控的主要兴趣在于定位异常的来源。

Q 贡献

每个变量对 Q 误差的贡献是观察到的缩放数据与其预期(重建)值之间的距离。

contrib_Q = Z_test - test_expect

在下面的图片中,你可以可视化新数据的 Q 贡献时间序列。

图 7. Q 贡献的时间序列 — 图片来自作者,许可证 CC0。

粗略来说,自监控周期的一半以来,Q 贡献对所有变量的影响接*零。在周期的后半部分,Q 贡献开始从零漂移,尤其是对第五个变量。

这不应让我们感到惊讶。正如我们所见,直到周期中期,变量是异常的,但异常以同步的方式(与训练集中观察到的相关性一致)。在周期的后半部分,第五个变量返回到均值附*,趋向于“正常”值(接*训练集中的值),但打破了相关性结构。

T²贡献

获取 T²贡献的公式类似于将 PC 解码到数据层面的公式,但有一个重要的区别:缩放因子

PCs 都以均值为中心,因此所有 PC 的期望值为零。然而,由于每个 PC 吸收的数据方差不同(见图 3),每个 PC 具有其尺度。相同的分数可能在一个 PC 上代表小的变化,而在另一个 PC 上代表大的变化!

因此,在解码之前,每个 PC 都通过除以其标准差进行缩放。这样,PCs 在解码前被置于相同的尺度上。

pc_scaling = np.diag(1/np.sqrt(pca_variances))
contrib_T2 = test_scores @ pc_scaling @ np.transpose(pca_loadings)

下面的图片展示了 T²贡献的时间序列。

图 8. T²贡献的时间序列 — 图片来自作者,许可证 CC0。

总结

使用 PCA 的过程控制将被监控变量视为一个应当一致移动的系统,期望其元素在一定范围内(Q 统计量)和一致地变化(T²统计量)。

Q 和 T²异常之间的主要区别在于它们的来源:第一个是在数据层面,第二个是在PC 层面

Q 警告我们编码解码系统在新数据上由于变量间的观察关系未按预期工作。因此,Q 贡献使我们能够根据观察数据识别不可预测的变量。

T² 警告我们编码系统的 PC 分数与整个数据中心的距离过远。T² 贡献解码错误,“反向传播”到数据层级,允许我们识别具有异常观测值的变量。

核心思想是每条数据记录由信号和噪声组成。PCA 去除噪声并评估信号,如果变量的信号与预期(T²)过远或被噪声压倒(Q),则发出警报。尽管它们可以同时发生,但一个并不一定意味着另一个。

MusicGen 重新构想:Meta 在 AI 音乐领域的低调进展

原文:towardsdatascience.com/musicgen-reimagined-metas-under-the-radar-advances-in-ai-music-36c1adfd13b7?source=collection_archive---------6-----------------------#2023-11-21

探索被忽视但非凡的 MusicGen 进展

Max HilsdorfTowards Data Science Max Hilsdorf

·

关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 11 月 21 日

--

一张象征 Music AI 产品如何提升音乐创作的图像。图像通过与 ChatGPT 和 DALL-E-3 的对话生成。

事情是怎么开始的……

在 2023 年 2 月,谷歌推出了其生成音乐 AI MusicLM,引起了轰动。那时,两个事实变得很清楚:

  1. 2023 年将成为基于 AI 的音乐生成突破年

  2. 一个新模型很快就会超越 MusicLM

许多人预期下一个突破性模型在模型参数和训练数据方面将是 MusicLM 的十倍。这也会引发相同的伦理问题,包括对源代码的限制访问以及使用受版权保护的训练材料。

今天,我们知道这些说法只有一半是正确的。

发布于 2023 年 6 月,Meta 的 MusicGen 模型带来了一些巨大的改进,包括…

  1. 更高质量的音乐输出(24kHz → 32kHz)

  2. 更自然的乐器声音

  3. 可以根据任何旋律进行生成的选项(我写了一篇关于此的 博客文章

…同时使用更少的训练数据,开源代码和模型权重,仅使用商业授权的训练材料。

六个月后,炒作逐渐*息。然而,Meta 的研究团队FAIR继续发表论文并更新代码,以逐步改进 MusicGen。

…当前的进展

自发布以来,Meta 在两个关键方面升级了 MusicGen:

  1. 使用多频段扩散进行更高质量的生成

  2. 由于立体声生成,输出更加生动

尽管这听起来像是两个小改进,但差别很大。自己听听吧!以下是使用原始 MusicGen 模型(3.3B 参数)生成的 10 秒钟片段:

从官方 MusicGen 演示页面 提取的生成曲目。

使用的提示是:

具有自然质感、环保意识、尤克里里融入、和谐、轻快、随意、自然乐器、柔和的节奏

现在,这里有一个基于相同提示生成的 MusicGen 输出示例,展示了六个月后的效果:

使用 MusicGen 3.3B 立体声生成的曲目由作者创作。

如果你通过智能手机扬声器收听,可能不会很明显。使用其他设备时,你应该能够听出整体声音更加清晰自然,立体声使得作品更加生动和令人兴奋。

在这篇博客文章中,我想展示这些改进,解释它们的重要性和工作原理,并提供一些示例生成。

多频段扩散——这有什么用?

要了解多频段扩散是什么及其重要性,让我们看看原始 MusicGen 模型 [1] 是如何生成其输出的。

34kHz 采样率的 30 秒音频在计算机中表示为* 100 万个数字。逐样生成这样的音频相当于使用 ChatGPT 生成 10 部完整的小说。

相反,Meta 依赖于神经音频压缩技术。他们的压缩模型 EnCodec [2] 可以将音乐从 34kHz 压缩到大约 0.05kHz,同时保持重建到原始采样率所需的相关信息。EnCodec 包括一个编码器,用于压缩音频,以及一个解码器,用于重建原始声音(图 1)。

图 1 — Encodec:Meta 的神经音频压缩模型。图片作者提供。

现在回到 MusicGen。它不是以全采样率生成音乐,而是以 0.05kHz 生成,并让 EnCodec“重建”,从而在最小的计算时间和成本下实现高保真输出(图 2)。

图 2 — MusicGen:用户提示(文本)被转换为编码音频信号,然后解码以产生最终结果。图片作者提供。

尽管 EnCodec 是一项令人印象深刻的技术,但其压缩并非无损。与原始音频相比,重建后的音频中存在明显的伪影。请亲自聆听!

原始音频

EnCodec 音乐示例取自官方的EnCodec 演示页面

重建音频

EnCodec 音乐示例取自官方的EnCodec 演示页面

由于 MusicGen 完全依赖于 EnCodec,因此它是生成音乐质量的主要瓶颈。这就是为什么 Meta 决定改进 EnCodec 的解码部分。2023 年 8 月,他们开发了一个更新的解码器,利用了多频带扩散[3]

Meta 发现 EnCodec 的原始解码器有一个问题,就是它倾向于先生成低频,然后生成高频。不幸的是,这意味着低频中的任何错误/伪影也会扭曲高频,从而大幅降低输出质量。

多频带扩散通过在组合之前独立生成频谱的不同部分来解决这个问题。研究人员发现,这一过程显著改善了生成的输出。从我的角度来看,差异非常明显。请用原始 EnCodec 解码器和多频带扩散解码器聆听相同的曲目:

原始解码器

生成的曲目取自多频带扩散演示页面

多频带扩散解码器

生成的曲目取自多频带扩散演示页面

当前文本到音乐系统的核心问题之一是其生成的声音总是有一种不自然的质量,尤其是对于声学乐器。多频带扩散使输出的声音更加干净自然,将 MusicGen 提升到了一个新的水*。

为什么立体声如此重要?

到目前为止,大多数生成音乐模型生成的是单声道声音。这意味着 MusicGen 不会将声音或乐器放置在左侧或右侧,从而导致混音较少生动和兴奋。立体声被大多忽视的原因是生成立体声并非一项简单的任务。

作为音乐家,当我们制作立体声信号时,我们可以访问混音中的各个乐器轨道,并将它们放置在我们想要的位置。MusicGen 并不是分别生成所有乐器,而是生成一个合成的音频信号。在没有这些乐器源的情况下,创建立体声音效是困难的。不幸的是,将音频信号拆分成各个源是一个棘手的问题(我已经发布了一篇 博客文章 讨论了这个问题),而且技术仍然未完全成熟。

因此,Meta 决定将立体声生成直接集成到 MusicGen 模型中。他们使用包含立体声音乐的新数据集,训练 MusicGen 生成立体声输出。研究人员声称,生成立体声与单声道相比没有额外的计算成本。

尽管我觉得论文中对立体声过程的描述并不十分清楚,但我的理解是这样的(图 3):MusicGen 已经学会生成两个压缩音频信号(左声道和右声道),而不是一个单声道信号。这些压缩信号必须分别解码,然后再结合起来生成最终的立体声输出。这个过程不会花费两倍的时间,因为 MusicGen 现在可以在大约生成一个信号的时间内同时生成两个压缩音频信号。

图 3 — MusicGen 立体声更新。请注意,由于论文中对该过程的记录不够充分,我不能 100% 确定这一点。请将其视为一个有根据的猜测。图片由作者提供。

能够生成令人信服的立体声效果确实使 MusicGen 从其他先进模型如 MusicLM 或 Stable Audio 中脱颖而出。从我的角度来看,这个“微小”的增加在生成音乐的生动性方面产生了巨大的差异。自己听听吧(在智能手机扬声器上可能很难听到):

单声道

立体声

结论

自发布之日起,MusicGen 就给人留下了深刻的印象。然而,从那时起,Meta 的 FAIR 团队不断改进他们的产品,提供了更高质量、更逼真的效果。在生成音频信号(而非 MIDI 等)的文本到音乐模型方面,我认为 MusicGen 在竞争对手中处于领先地位(截至 2023 年 11 月)。

此外,由于 MusicGen 及其相关产品(EnCodec、AudioGen)都是开源的,它们成为了令人难以置信的灵感来源和有志于成为 AI 音频工程师的首选框架。如果我们看看 MusicGen 在短短 6 个月内取得的进步,我只能想象 2024 年将是一个激动人心的年份。

另一个重要的点是,Meta 通过其透明的方法,也为希望将这项技术集成到音乐软件中的开发人员做出了基础工作。生成样本、头脑风暴音乐创意或者改变现有作品的风格 —— 这些是我们已经开始看到的一些令人兴奋的应用。通过足够的透明度,我们可以确保我们正在构建一个未来,使得人工智能不仅仅是对人类音乐能力的威胁,而是让音乐创作变得更加令人兴奋的未来。

注意:虽然 MusicGen 是开源的,但预训练模型可能不允许商业使用!访问 audiocraft 的GitHub 仓库以获取有关其所有组件预期使用的更详细信息。

参考文献

[1] Copet 等人(2023)。简单可控的音乐生成。arxiv.org/pdf/2306.05284.pdf

[2] Défossez 等人(2022)。高保真神经音频压缩。arxiv.org/pdf/2210.13438.pdf

[3] Roman 等人(2023)。从离散标记到高保真音频使用多频带扩散。arxiv.org/abs/2308.02560

关于我

嗨!我是一名音乐学家和数据科学家,分享我对人工智能和音乐当前主题的见解。这里是我之前与本文相关的一些工作:

MediumLinkedin找到我!

MusicLM — 谷歌是否解决了 AI 音乐生成问题?

原文:towardsdatascience.com/musiclm-has-google-solved-ai-music-generation-c6859e76bc3c

论文及其重要性解释

Max HilsdorfTowards Data Science Max Hilsdorf

·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 2 月 2 日

--

图片来源:Placidplace

介绍

在 2023 年 1 月 25 日的一个 PowerPoint 演示中,我描述了生成高质量长序列音乐作为音频 AI 领域在不久的将来需要解决的主要挑战之一。一天之后,我的幻灯片就过时了。

MusicLM 由 Google Research 开发,可以根据自然语言中的简单文本查询生成风格和流派各异的高质量音乐,时长为一分钟。

最好还是亲自体验一下,查看充满音乐示例的演示页面。如果你对细节感兴趣,也可以查阅研究论文,尽管本文也会涵盖所有相关主题。

来自 MusicLM 演示页面的摘录。图片由作者提供。

那么,是什么让 MusicLM 成为如此巨大的技术飞跃?它解决了过去十年困扰 AI 研究人员的哪些问题?为什么我仍然认为 MusicLM 是一种过渡技术——通向音乐制作不同世界的桥梁?这些问题以及更多内容将在这里解答,而不会让你感到枯燥的数学或过多的技术术语。

挑战 1:将文本转换为音乐

图片来源:Debby Hudson 供图于Unsplash

MusicLM 利用了最*发布的一种模型,将音乐和文本都映射到同一个“地图”上。就像计算从伦敦到斯德哥尔摩的距离一样,MusicLM 可以计算音频-文本对之间的“相似性”。

音乐难以描述

将文本转化为音乐是一项复杂的任务,因为音乐是一种多维度的艺术形式,不仅涉及音乐的旋律和和声,还包括节奏、速度、音色等等。为了将文本转化为音乐,机器学习模型需要能够理解和解释文本的含义,然后利用这种理解来创作出准确表现文本的音乐作品。

将文本转化为音乐的另一个问题是,音乐是一种高度主观的艺术形式。一个人认为是“快乐”的音乐可能会被另一个人听成是“苦涩”或“*静”。这使得机器学习模型很难创作出被普遍认为是“快乐”的作品。尽管音乐常被(在我看来是错误地)描述为一种普遍语言,但从口语到音乐的客观翻译似乎是不可能的。

MusicLM 的方法

考虑到这一点,你可能会惊讶地发现,将文本转化为音乐并不是 MusicLM 的主要贡献。将文本与音频、图像与文本或音频与图像关联起来的机器学习模型(我们称之为“跨模态模型”)在过去 2-3 年里在学术界和工业界变得相当成熟。当然,最著名的跨模态模型之一是DALL-E 2,它根据输入文本生成高分辨率图像。

在 MusicLM 中,研究人员并没有自己训练跨模态部分。相反,他们利用了一个名为“MuLan”的预训练模型,该模型于 2022 年发布(详见论文这里)。MuLan 通过一种称为“对比学习”的方法训练,以将音乐与文本关联起来。在这里,训练数据通常包括成千上万对音乐及其描述文本的配对。学习目标是,当呈现任何一对音乐和文本(不一定是关联的)时,模型能够判断文本是否属于音乐。一旦实现了这一点,模型就能够计算音频-音频、文本-音频或文本-文本对之间的相似度。

挑战 2:减少生成音乐的时间和资源

图片来源:Agê BarrosUnsplash

MusicLM 利用最先进的音频压缩工具,显著减少了生成高质量音频信号所需的信息量。

目前,该模型能够判断它生成的音乐是否符合给定的文本输入。然而,音频生成过程本身存在一些挑战,主要是创建一段音乐所需的时间和资源。

维度问题

尽管音乐对我们人耳来说很容易处理,但对于数据科学家来说,它是一种相当复杂的数据类型。普通的流行歌曲(3:30 min)在 CD 质量下以几乎 1000 万个数字存储在计算机中。相比之下,高清质量的图片(1280 x 720 像素)存储和处理的值甚至不到 100 万。*年来,已经开发了许多方法,将音乐压缩成计算上更便宜的格式,同时保持高质量的声音。

使用传统的方法,生成 1 分钟的 CD 质量音乐(44100 Hz)需要机器学习模型生成大约 260 万个数字——一个接一个。如果生成一个数字仅需 0.01 秒,那么这个过程仍然需要超过 7 小时来完成。不难想象,如果你请一位专业音乐家作曲和录制音乐,他们会更快地完成任务。关键点是:到目前为止,在快速音频生成和输出质量之间存在巨大的权衡。

以往的方法

为解决这个问题,已经尝试了许多方法。一种较新的方法是间接生成音频,即首先生成音频信号的图像表示(例如,“频谱图”),然后将此图像转换为“真实”音频(如在 “Riffusion” 中所做)。另一种方法是通过创建符号表示来避免直接生成音频。音乐的最广泛知晓的符号表示是乐谱。如你所知,乐谱并不是一个真实的音频事件,但音乐家能够将其转换为真实的音频。过去,我们已经看到机器学习模型在符号 MIDI 格式下生成音乐的相当成功(例如,参见 Magenta 的 Chamber Ensemble Generator)。然而,这两种方法都有其弱点,主要是因为生成“真实的东西”非常困难。

MusicLM 的 Approach

最后,让我们讨论 MusicLM 采用的方法。MusicLM 并不是生成音频的代理(如图像或 MIDI),而是应用了一种最先进的音频压缩算法 “SoundStream”,该算法于 2021 年发布。使用 SoundStream,模型能够以 24 kHz(每秒 24000 个数字)的速度生成音频,同时实际计算仅 600 个数字。每秒从 600 个值映射到 24000 个值的工作由 SoundStream 处理。换句话说,模型需要生成 97.5%更少的信息,同时实现大致相同的结果。虽然过去也有其他优秀的压缩算法,但 SoundStream 明显优于所有这些算法。

挑战 3:生成连贯且真实的音乐

图片由 Brantley Neal 提供,来自 Unsplash

通过将文本与音乐的关联任务与实际音频生成部分分开,MusicLM 可以在数十万小时的未标注音频数据上进行训练。这有助于其生成音乐的丰富性。

术语

对于“连贯”和“真实”音乐究竟是什么,确实值得讨论。然而,在 AI 生成音乐的背景下,可以说,我们甚至会考虑将 MusicLM 的作品称为“连贯”和“真实”,这一点已经足够说明问题。对于一个宽松的工作定义,假设连贯的音乐具有一个通过不同部分和/或通过音乐创意的重复、变化或引用来表现的基础结构。这里的“真实”指的是,AI 生成的音乐呈现出一种能够让我们相信是人类有意创作的方式。

音乐的“记忆”

生成连贯的音乐并不是 MusicLM 的突破。早在 2018 年,Google Magenta 的 “音乐变换器” 就能够创作具有清晰旋律和和声序列的 MIDI 音乐,其中音乐创意会重复或被改变。音乐变换器能够跟踪过去超过 45 秒的音乐事件。然而,由于原始音频远比符号 MIDI 表示复杂,这样的大“记忆”对于生成原始音频的模型来说长期以来都是难以实现的。MusicLM 拥有 30 秒的“记忆”,这比我所知道的任何类似模型都要多(尽管我可能错了——发布的模型实在是太多了……)。虽然这并不允许 MusicLM 创作出长达 15 分钟的史诗作品,但仍足以维持基本的音乐结构,如节奏、韵律、和声和音色,较长时间内不会丧失。

真实输出

更重要的是,在我看来,MusicLM 创作的音乐听起来出奇地真实。对此的技术解释可能是,MusicLM 找到了一种巧妙的方法,利用数千小时的未标注音乐(即没有文本描述的音乐)来训练文本到音乐的模型。通过使用预训练的“MuLan”模型来关联文本和音乐,他们设计了模型架构,使其可以将音频生成部分与未标注音频数据分开学习。其基本假设是,将音乐与文本关联并不像创作真实音乐那么困难。将问题重新定义并调整架构的这一“技巧”可能是 MusicLM 成功的关键因素。

从某种意义上说,结果不言而喻。首次出现的 AI 模型不仅仅是介于作曲和音乐之间的中间产品,或者是任何 4 岁儿童都能区分开的人造音乐。它这次确实感觉有所不同。感觉类似于我第一次阅读 GPT-3 写的文本时。类似于我第一次看到 DALL-E-2 生成的图像时。MusicLM 可能正是那个将在历史上留下印记的突破性 AI 音乐模型。

MusicLM 的不足与未来展望

Kenny Eliason拍摄于Unsplash

定量不足

尽管 MusicLM 具备所有这些令人惊叹的特点,但该模型绝非完美。我甚至会说,与文本的 GPT-3 或图像的 DALL-E-2 等模型相比,MusicLM 似乎更加有限。一个原因是,生成的音乐只有机器学习社区认为是高质量的。如果没有有效的方法将 24 kHz 的音乐上采样到 44.1 kHz,生成的作品将永远无法在现实世界中使用,因为仔细聆听时,即使是非专家也能注意到 CD 录音和 MusicLM 输出之间的质量差异。而一个 1024 x 1024 的图像(如 DALL-E-2 生成的)已经可以用于网站、博客等,而 24kHz 的音乐始终会被视为不合格。

类似地,虽然 30 秒的“记忆”对于一个音频机器学习模型来说令人印象深刻,但一个训练有素的作曲家可以创作数小时的连贯音乐,训练有素的音乐家也可以轻松演奏这些音乐。机器学习模型在这方面要赶上人类还有很长的路要走。然而,随着计算资源的增加,模型的采样率和“记忆”无疑会增加。此外,音频压缩和机器学习方法的改进可能会进一步加速这一过程。看到生成式 AI 模型在过去 2-3 年里提高得如此迅速,我相信这些问题在今年年底之前或多或少会得到缓解。

质量/伦理不足

然而,也有一些问题无法仅通过规模解决:知识产权问题。在最*的过去,许多大型生成模型已遭遇版权诉讼(GitHub Copilot & StableDiffusion,仅举其中两个)。在大多数情况下,这些模型是在未经商业用途许可的数据上进行训练的。虽然模型的创作是“新的”,但你可以认为它仍然“商业性地使用”了训练数据。MusicLM 也是如此。此外,总是有可能生成“窃取”了整个旋律或和弦序列的版权保护作品的内容。

在 MusicLM 论文中,生成与训练数据中的一段音乐“完全匹配”的概率不到 0.2%。虽然这个概率听起来很低,但请记住——假设 0.2%的生成率——每 500 个生成的曲目中可能有 1 个是版权声明的安全嫌疑对象。更大的数据集、更多的多样性以及改进的模型架构或训练算法几乎肯定能帮助降低这一比率,但核心问题依然存在,就像图像或文本等其他领域一样:如果我们计划使用在版权保护数据上训练的生成型 AI 模型,我们不能在大规模上生成输出而不冒重大法律风险。然而,这不仅仅是财务风险,也是一个重大的伦理问题。

此外,MusicLM 及其训练数据都没有公开发布。这引发了关于 AI 系统透明度和问责制的伦理问题。由于像 MusicLM 这样的 AI 模型有可能颠覆整个行业,因此开发过程和方法论应当接受审查。这将使研究人员能够了解模型的训练方式,评估其偏见,并识别可能影响其输出的任何局限性。如果没有模型的访问权限,很难评估其对社会的影响以及可能带来的风险。

最后,目前尚不清楚 MusicLM 或未来模型的商业应用场景。世界上已经有数以百万计的人在有效地免费制作优秀音乐。因此,通过用机器替代人类来降低音乐创作成本既在经济上没有效益,更不用说在伦理上是不可取的。尽管 MusicLM 仍有一定的盈利潜力,我认为生成型 AI 作为人类作曲家的助手,帮助他们快速原型化音乐创意并专注于为世界创造艺术价值,具有更大的潜力和价值。

未来展望

很难预测未来在音乐生成 AI 方面会带我们走向何方。可以确定的一点是:MusicLM 将被更大规模的模型取代,这些模型将使用更大的数据集和更智能的算法。这些模型无疑能够克服许多 MusicLM 的不足之处。似乎像这样的技术会不可避免地彻底改变音乐市场——而且这种改变可能会比我们预期的更早。然而,我认为将全部注意力集中在黑箱模型上是一个错误。总体而言,世界并不需要完全依赖机器进行音乐制作。我们有的是人类来完成这一任务。真正重要的是我们应当利用 AI 技术,通过开辟新的发明、创造和享受音乐的方式,将更多的艺术价值带入这个世界。

感谢阅读本文! 我主要撰写有关 AI 和音乐交集的内容,如果你对此感兴趣,你可能还会喜欢我的其他作品。

穆布里奇德比:利用人工智能使动物运动照片栩栩如生

原文:towardsdatascience.com/muybridge-derby-bringing-animal-locomotion-photographs-to-life-with-ai-b1918e6622ec

我如何使用 Midjourney 和 RunwayML 将埃德瓦德·穆布里奇的照片序列转化为高分辨率视频

Robert A. GonsalvesTowards Data Science 罗伯特·A·贡萨尔维斯

·发表于 Towards Data Science ·阅读时间 16 分钟·2023 年 7 月 25 日

--

第 2 帧 第 626 号板,奔跑 由埃德瓦德·穆布里奇(左),基于 Midjourney 参考图像的 RunwayML Gen-1 视频生成器的转换(中间和右侧),这些图像由 AI 图像创建程序生成

背景

我相信你一定见过 19 世纪英国摄影师埃德瓦德·穆布里奇拍摄的奔跑的马的系列图片。为了帮助回忆,这里有一个 GIF 动画展示了他更著名的照片系列之一。

第 626 号板,奔跑 由埃德瓦德·穆布里奇创作,动画 GIF 由作者制作

这是穆布里奇的肖像,附有他为拍摄该系列照片而制作的设备插图。

埃德瓦德·穆布里奇的肖像(左) 图片来自 维基媒体穆布里奇的设备(右),图片来自 维基媒体

埃德瓦德·穆布里奇

穆布里奇是自然摄影师,由加州州长利兰·斯坦福委托拍摄他的豪宅和财物。斯坦福给穆布里奇提出了一个令人兴奋的挑战:他能否拍摄到一匹奔跑的马的清晰照片?

1872 年是马依布里奇开始热衷于运动摄影的年份。他受加州州长利兰·斯坦福委托,拍摄了他的赛马“西方”的运动步态。在此之前,马的运动步态仍是个谜。马蹄何时触地?四只脚是否曾同时离开地面?绘制奔跑中的马蹄一直是艺术家的难题。... [他使用] 12 台相机,每台相机连接到一个电器装置,当马奔跑经过时,装置会触发快门。 … 马依布里奇于 1879 年发明了 zoopraxiscope,这是一台可以将多达两百张单独图像投影到屏幕上的机器。1880 年,他首次向加州美术学院的一群人展示了投影移动图像,从而成为电影之父。- 维·惠特迈尔 [1]

马依布里奇不仅拍摄了移动的马。他还拍摄了移动的猫、狗、水牛、鸵鸟、人物等类似序列。

马依布里奇德比

对于这个项目,我想看看是否可以使用 AI 系统将马依布里奇的动物运动照片转换为高清晰度全彩视频。在尝试了各种技术后,我使用Midjourney通过文本提示生成参考帧,并结合 RunwayML 的 Gen-1 视频生成器,使原始序列变得更为逼真。为了好玩,我制作了一个短动画,“马依布里奇德比”,展示了这一工作。这里就是。

“马依布里奇德比,”基于爱德华·马依布里奇的动物运动照片, 作者制作的视频

在接下来的部分,我将描述如何转换运动序列、生成背景滚动以及将这些元素结合起来创建动画。

使用 Midjourney 生成参考帧

作为将马依布里奇照片系列转换为高清晰度视频的前提,我使用了原始系列的一张照片和 Midjourney 中的文本提示生成了高分辨率参考帧。

例如,这里是我用于生成马和骑手参考帧的提示,“一个戴着蓝色帽子、穿着蓝色夹克、白色裤子和黑色靴子的男人骑着一匹棕色马,背景为白色 -- ar 4:3。”请注意,--ar 4:3 参数表示 4:3 的宽高比。

第 626 号板画的 第二帧(左),Midjourney 缩略图(中),精选图像,修饰后,作者提供的图像(右)

我将穆布里奇第 2 帧的图像链接和提示粘贴到 Midjourney 中,它生成了四个缩略图。所有四张生成的图像都很不错。我喜欢这些图像的细节和质感,包括骑师的衣服和马的毛发光泽。虽然它们都没有完全匹配原始马的姿势,但我发现当对视频进行风格化时,这并不重要。RunwayML 中的视频风格化器仅捕捉图像的一般外观。我选择了右下角(以绿色轮廓标出)的缩略图,并在 Photoshop 中进行了一些编辑;我将图像水*翻转,将马的颜色更改为棕色,并改变了骑师帽子的风格。

我对动画中的其他四种动物重复了这个过程,包括一只猫、一只水牛、一只大象和一只鸵鸟。以下是结果。你可以在下面的左列看到穆布里奇照片系列中的一张图片。中间列显示了使用穆布里奇图像和文本(如“跑步中的猫的全彩照片,侧面视图,-- ar 4:3”)从 Midjourney 得到的结果。所选缩略图以绿色轮廓标出。右列展示了经过稍微清理和在 Photoshop 中水*翻转(如有需要)的所选图像。

运动照片的 水牛 大象** 和** 鸵鸟由伊德华德·穆布里奇(左),Midjourney 缩略图(中),所选 Midjourney 图像,(作者)

Midjourney 系统在生成参考图像方面表现出色。动物的细节令人惊叹。你可以点击任何一张图片来放大查看。虽然它没有完全匹配参考图像中的姿势,但整体渲染质量非常优秀。有关 Midjourney 的更多信息,你可以查看我之前的文章这里。

接下来,我将展示如何利用参考图像使用 RunwayML 转换照片系列。

RunwayML

Runway 是一家位于纽约市的初创公司,研究并提供使用机器学习(ML)的媒体创建和编辑服务。他们因其网站的 URLrunwayml.com而被称为 RunwayML。他们提供不同价格点的订阅层级: 免费、每月$12、每月$28 等。

这是他们提供的一些服务的列表:

  1. 超慢动作 - 将视频转换为超*滑的运动

  2. 视频对视频编辑 - 使用文本或图像更改视频风格

  3. 移除背景 - 移除、模糊或替换视频背景

  4. 文本对视频生成 - 使用文本提示生成视频

  5. 图像对图像编辑 - 使用文本提示转换图像

我使用了前三个来为我的视频中的 Muybridge 序列进行风格化。

626 号牌,奔马由 Eadweard Muybridge 创作,由 RunwayML 风格化,作者动画

RunwayML 的视频对视频编辑模型

RunwayML 的视频对视频编辑服务允许用户上传输入视频并提供文本或参考图像作为提示。然后,机器学习模型将通过施加提示中指定的风格来“编辑”镜头,同时保持输入视频的主要元素完整。该过程在他们的论文中有详细描述,结构和内容引导的视频合成与扩散模型 [2]。

在这项工作中,我们展示了一种结构和内容引导的视频扩散模型,该模型根据对期望输出的视觉或文本描述编辑视频。由于两个方面之间的解耦不足,用户提供的内容编辑与结构表示之间会发生冲突。作为解决方案,我们展示了在不同细节水*的单眼深度估计上进行训练提供了对结构和内容保真的控制。……我们发现从输入视频帧提取的深度估计提供了所需的属性,因为它们编码的内容信息显著少于更简单的结构表示。—— P. Esser 等人,RunwayML

注意“单眼深度估计”指的是深度图,其中像素的值表示从相机到场景中物体表面的距离。为了获得深度估计,他们使用了一组欧洲研究人员的另一个机器学习模型[3]。这个模型叫做 MiDaS(我猜,这是单眼深度估计器的倒编词?)MiDaS 系统在 3D 电影场景的数据集上进行了训练,如下图所示。

来自 3D 电影数据集的样本图像,来自MiDaS 论文的图像

你可以看到深度图中浅黄色显示了场景中较*的点,而深蓝色则显示了背景中较远的点。训练过的 MiDaS 模型可以从任何输入模型估计深度图。以下是论文中的一些结果。

使用 MiDaS 的预测深度图,图片来自MiDaS 论文

你可以看到 MiDaS 模型在深度估计方面表现出色。例如,你可以看到狗的尾巴非常清晰地突显出它在后面的水流中。

RunwayML 的视频到视频模型使用输入视频的预测深度图来条件化一个由文本提示或参考图像指导的扩散视频生成模型。

我们的潜在视频扩散模型根据结构和内容信息合成新的视频。我们通过根据深度估计进行条件处理来确保结构一致性,同时内容由图像或自然语言控制。通过模型中的额外时间连接和图像与视频的联合训练,实现了时间稳定的结果。— P. Esser 等人,RunwayML

你可以看到一些来自论文的文本提示的视频编辑结果。

RunwayML 的图像到视频编辑与文本的结果,图片来自RunwayML 的论文

各种风格,包括铅笔素描、动漫和低多边形渲染,转化了输入视频以创建输出。你可以看到应用风格在每一帧中的一致性。以下是一些论文中的例子,使用图像提示来美化视频。

RunwayML 的图像到视频编辑与参考图像的结果,图片来自RunwayML 的论文

再次,你可以看到颜色调色板和提示图像的外观如何将视频转变为指定的风格。生成帧的细节也在最终视频中保持一致。

使用 RunwayML 的视频到视频编辑服务

为了使用该服务,我创建了一个账户并登录了。如上所述,你可以使用有限制的免费版本,比如生成的视频最长只能为四秒。我选择每月支付 12 美元,这允许我创建最长 15 秒的视频并享受其他好处

我拍摄了一段关于兔子的简短影像以测试系统,用编辑系统进行了清理,然后上传到 RunwayML。我选择了 Gen-1: Video to Video 工具。我加载了剪辑,输入了提示语“在田野中的逼真兔子,耳朵下垂”,然后点击了预览风格按钮。系统思考了一会儿,渲染了四个缩略图。你可以在下面的截图底部看到它们。

RunwayML Gen-1: 视频到视频屏幕,图像由作者提供

四个缩略图看起来都不错。它们都遵循了影子木偶的形式,但画面中出现了一只逼真的兔子。我选择了第三个,然后点击 生成视频。视频渲染大约花了 20 分钟。我还用提示“在田野里有垂耳的 2D 动画兔子”制作了一个视频。你可以在下面看到结果,原始影子木偶视频,以及我的清理版本供参考。

原始影子木偶视频(左上),清理后的影子木偶视频(右上),使用提示“逼真 …”的 RunwayML 风格化视频(左下),和 使用提示“2D 动画 …”的 RunwayML 风格化视频(右下),视频由作者提供

生成的视频效果很好!左下角的逼真视频效果最好,兔子的眼睛、耳朵和鼻子细节非常漂亮。2D 动画渲染效果有些偏差。系统似乎对耳朵的识别有些困惑,背景也不太有趣。接下来,我用 Midjourney 生成的两个参考图像尝试了同样的实验。

使用提示“逼真 …”的 Midjourney 图像(左上),使用提示“2D 动画 …”的 Midjourney 图像(右上),使用逼真参考的 RunwayML 风格化视频(左下),和 使用动画参考的 RunwayML 风格化视频(右下),视频由作者提供

这些也做得很好。它们都从参考图像中拾取了风格,同时遵循了原始影子木偶视频中的形状和动作。然而,右边的那个视频有一个奇怪的效果从右侧出现,几乎看起来像是太阳光晕。注意到两个生成的动画都显示了参考帧中的背景细节,比如右侧漂亮的云朵。但参考帧中的前景形状却缺失了,比如左侧的麦粒和右侧的树。这可能是由于 RunwayML 使用的训练数据中包含了深度图像。它展示了我的手部动作被转换成兔子作为前景图像,但保留了参考图像中的背景元素,比如田野和天空。

使穆伊布里奇的照片栩栩如生

我使用了上述描述的 RunwayML 技术,并进行了小幅变更,将穆伊布里奇的原始图像序列转换为高分辨率版本。

超慢动作

由于穆伊布里奇的实验中动物动作很快,帧间存在大量的运动。例如,这里是马匹序列的三个帧。

帧 2、3 和 4 的 编号 626,疾驰 由 Eadweard Muybridge 制作

注意马腿间帧的运动量。我在尝试快速移动动画的视频到视频风格化时,结果并不理想。我的解决方案是先使用 RunwayML 的超慢动作功能将运动减慢两倍,然后应用转换,最后将结果视频的速度提高两倍。

这是减速视频的效果。

帧 2(左)和 3(右) 编号 626,疾驰 由 Eadweard Muybridge 制作,RunwayML 帧插值(中)由作者

你可以看到帧间的运动减少,尤其是马的腿部。这是原始马匹序列与 50% 慢动作版本的对比。

编号 626,疾驰由 Eadweard Muybridge 制作,作者动画(左),RunwayML 的 50% 超慢动作,作者动画

系统在运动插值方面表现出色。总体而言,使用 RunwayML 的超慢动作时运动更为流畅。当序列重置时,动作有一点小的卡顿,但当我将转换后的视频速度提高两倍时,这种情况会被掩盖。

视频到视频转换

我首先将减速的马匹动画上传到 RunwayML 来创建转换后的视频,然后选择了Gen-1: 视频到视频工具。我选择了图像风格 参考,并上传了我使用 Midjourney 创建的马匹参考帧。转换有多种设置,包括以下内容。

  • 风格:结构一致性 - 更高的值使输出与输入视频在结构上更有差异。

  • 风格:权重 - 更高的值强调匹配风格而非输入视频。

  • 帧一致性 - 值低于 1 会减少时间上的一致性;值高于 1 会增加帧与前一帧的相关性。

你可以在 RunwayML 的 帮助页面 上看到这些设置的变体示例。我试验了这些设置,但使用了默认值,即结构一致性 2、权重 8.5 和帧一致性 1。

然后我点击了预览样式,它在底部显示了四个选项。

RunwayML 的 Gen-1:视频到视频功能的屏幕截图,图片由作者提供

我选择了第三个预览,并点击了生成视频按钮。这里是参考图像、原始马匹序列和经过风格化的动画,速度加快了两倍以匹配初始速度。

Midjoruney 参考图像(左),626 号版图,飞跑(中),使用 RunwayML 风格化的动画(右)

这做得很好!你可以看到参考图像的风格如何被施加到原始的梅布里奇动画上,同时保持了马匹和骑手的动作完整。系统还进行了基于机器学习的视频缩放,将最终视频调整到 640x480,这带来了不少细节。请注意,系统有一个“放大”设置,它会将分辨率水*和垂直方向都翻倍。

我对另外四个图像序列执行了相同的操作。你可以在下面看到结果,包括 Midjourney 的参考帧、梅布里奇的原始动物照片序列和 RunwayML 的风格化视频。

Midjoruney 参考图像(左),伊德华·梅布里奇的动物运动研究(中),使用 RunwayML 风格化的动画(右)

这些看起来也很棒!就像马匹动画一样,RunwayML 模型从参考图像中提取了纹理和颜色,并将其应用于原始动画,同时保持运动的完整性。然而,新动画中的背景并没有从右到左滚动。但这不是问题。你可以在下一部分看到我如何创建一个“alpha mask”来保留前景图像,并将动物合成到新的背景上。

移除背景图像

我使用了 RunwayML 的 Remove Background 功能来替换奔跑动物片段的背景。我加载了来自 Muybridge 照片的原始视频片段,并用光标选择了两个点,即马匹和骑师的腿。系统思考了一会儿,然后显示了所选区域为绿色,如下面的截图所示。

RunwayML 背景移除功能截图,图像由作者提供

系统展示了它如何选择视频中所有帧的前景,我可以将其作为预览播放。它做得非常出色,没有花费我太多的工作。然后,我将 alpha matte 保存为视频以供合成应用程序使用。

我在 Midjourney 中创建了一个赛马场的静态图像,并将其用作动画的滚动背景。凡是 matte 为黑色的地方,会显示背景(赛马场);凡是 matte 为白色的地方,会显示前景(马匹和骑师)。这里是马匹的风格化剪辑、alpha matte 和最终结果。

使用 RunwayML 风格化的动画(左),Alpha Matte(中),和 最终结果(右),动画由作者提供

在我的合成程序中,我需要稍微清理一下 alpha matte。例如,我模糊了尾部,使其看起来更像头发而不是固体物体。你可以看到滚动背景如何帮助展示马匹向前奔跑的效果,而在原始风格化动画中并没有这个效果。

这里是最终的动画,这次稍微大一点,以便你查看细节。

“Muybridge Derby”,基于 Eadweard Muybridge 的动物运动照片,视频由作者提供

如果你想在大屏幕上观看动画,它将于 2023 年 8 月 5 日至 9 月 30 日在加州卡马里奥的 Studio Channel Islands Art Center 展出,地址是 The Next Big Thing

最终想法

我很享受使用 Muybridge 图片以及利用 Midjourney 和 RunwayML 的工具生成和修改媒体。如果你熟悉我在 Medium 上的写作,你知道我喜欢尝试新的制作方法,但我并不总是能创作出完整的作品。因此,将多个元素结合在一起让我感到很满意。作为一个“深度切入”,我使用了我为之前的文章生成的 AI 歌曲作为片尾音乐。这首歌叫做“我会在到达时到达”,这对一场德比比赛来说有点合适。😄

## AI-Tunes: 使用人工智能创作新歌曲

如何微调 OpenAI 的 GPT-3 来生成具有全球结构的音乐

towardsdatascience.com

输入和生成媒体的所有权

Midjourney 和 RunwayML 对于用于提示的图像和文本及生成的图像有不同的政策。Midjourney 区分付费用户和免费用户,而 RunwayML 对两种用户使用相同的政策。

Midjourney 条款

根据 Midjourney 的 服务条款,免费服务层的用户不拥有他们生成的图片。Midjourney 拥有这些图片。这些图片根据知识共享署名非商业性 4.0 国际许可证授权给非付费用户用于非商业用途。付费服务的用户拥有他们生成的图片,这些图片可以用于商业用途。

如果你在一家大公司工作,还有额外的限制。

如果你是年收入超过 1,000,000 美元的公司的员工或所有者,并且你是代表你的雇主使用这些服务,你必须为每一个代表你访问服务的个人购买“Pro”或“Mega”会员,以便拥有你创建的资产。如果你不确定你的使用是否代表你的雇主,请假设是。— Midjourney 服务条款

仅供参考,Pro 计划每人每月 60 美元,Mega 计划每月 120 美元。

此外,根据条款,所有用户授予 Midjourney 许可,以任何目的使用任何文本提示、上传的作为提示的图片以及生成的图片,包括用于训练未来版本的模型。

RunwayML 条款

根据 RunwayML 的 使用条款,所有用户拥有并可以商业化使用其生成的内容。然而,所有用户都授予 RunwayML 许可,以任何目的使用他们的输入和输出,包括训练未来的模型版本。

图片和动画的许可条款

我将为此项目发布的图片和动画采用知识共享署名相同方式共享许可证。

知识共享署名-相同方式共享

致谢

我想感谢 Jennifer Lim 对文章的审阅和反馈。

参考文献

[1] V. Whitmire,《国际摄影名人堂:伊德华·迈布里奇》(2017)

[2] P. Esser 等人,结构与内容引导的视频合成与扩散模型 (2023)

[3] R. Ranftl 等人,迈向鲁棒的单目深度估计:混合数据集进行零样本跨数据集迁移 (2020)

我的亚马逊经济学家面试

原文:towardsdatascience.com/my-amazon-economist-interview-186e52e58a27

问题、准备和建议

Nazlı Alagöz数据科学前沿 Nazlı Alagöz

·发表于 数据科学前沿 ·阅读时间 8 分钟·2023 年 12 月 21 日

--

图片来源于 Christian WiedigerUnsplash

亚马逊是经济学及相关领域博士毕业生的主要雇主之一,提供与数据科学和机器学习(ML/DS)特别是因果推断领域密切相关的职位。它是少数几个提供明确称为‘经济学家’职位的公司之一,但这些角色往往与其他科技公司中因果推断数据科学家职位的职责和技能要求相似。作为准备进入经济学和数据科学交叉领域的人员,我发现亚马逊经济学家面试过程提供了丰富的洞察,适用于这两个领域。在本文中,我将分享我的亚马逊经济学家面试经验和准备过程,旨在为那些希望在亚马逊或其他数据科学职位上导航的人员提供资源。我的目标是揭示面试过程,提供对那些希望成为亚马逊经济学家的求职者以及对科技行业中专注于因果推断的数据科学家角色感兴趣的 ML/DS 求职者的相关见解和建议。

既然已经介绍完毕,我们来谈谈我的面试经历。我在 LinkedIn 上看到一个亚马逊经济学家职位:“经济学家——长期实习(10 个月),经济决策科学(EDS),”位于伦敦。这引起了我的兴趣,因此我决定申请。值得注意的是,这是一少数几次我在没有依赖个人关系如推荐信的情况下获得面试邀请的情况。面试发生在一段时间之前,我认为已经过去了足够的时间,所以现在分享我的准备过程和面试经历是合适的。

为了提供一些背景信息,我来介绍一下这个职位。这个角色的重点是应用计量经济学模型和使用大型数据集来解决亚马逊面临的实际商业问题。这个角色属于 EDS 团队,该团队在欧盟商店业务内运作,旨在优化和自动化决策。这个职位要求具备坚实的计量经济学基础,并且对统计编程工具如 Stata、R 或 Python 有基本的了解。

让我们继续讨论我如何准备面试以及面试内容。面试之前,我了解到其形式。面试持续一个小时,分为行为和技术两个部分。行为部分重点关注亚马逊的价值观,特别是他们的领导原则,你可以在亚马逊的领导原则找到这些原则。技术部分则围绕与角色相关的业务问题。在我的情况下,这个问题涉及到将计量经济学模型应用于亚马逊零售部门的一个场景。

作为提示,亚马逊的面试是在其自有的视频通话应用 Chime 上进行的。提前下载并检查其功能,例如背景模糊,或确保麦克风和音频配置正确,是个不错的主意。此外,招聘人员会告诉你面试官是谁。提前了解他们的工作是个好主意,这样你可以更好地与他们沟通。对我来说,面试官是一位经济学家,我很感激。

LinkedIn Sales Solutions拍摄,Unsplash

1 — 行为部分。

面试的前 15 分钟左右用于行为问题。有 2 到 3 个与领导原则相关的情境问题以及一些后续问题。

我确保为潜在的面试问题做好准备,为每个关键原则准备了 1-2 个故事,并使用 STAR(情况、任务、行动、结果)或 CAR(背景、行动、结果)框架大声练习这些故事。为了获得更多指导,你可以回顾每个原则的常见问题或示例,以了解面试中可能出现的问题类型。

我被问到了这样的问题:

  • 讲述一个你为复杂问题提出简单解决方案的经历。

  • 讲述一个你在存在分歧时影响决策的经历。

如果你了解领导力原则,你会发现第一个问题与“创新和简化”原则相关,而第二个问题与“有骨气;反对并承诺”或“经常正确”相关。面试官可能会要求对你的故事进行进一步的细节了解,所以要做好准备。例如,我被问到第一个问题的后续问题,关于简单解决方案的潜在风险以及我如何处理这些风险。

我保持了回答的简洁,并让面试官在需要更多细节时提出后续问题。使用“我”作为主语而非“我们”也很重要,并且在讲述这些故事时不要对任何你曾经合作过的人发表负面评论。

这些故事对我在其他公司的面试也很有帮助。因此,虽然这些是亚马逊的领导力原则,但许多公司都有类似的价值观,他们会问类似的情境问题。总的来说,为这次面试做准备对我在其他公司的面试非常有用。

第二部分 — 技术部分。

我通过回顾因果推断方法来准备,包括 差分中的差分法、匹配、合成控制、工具变量、回归不连续性和 A/B 测试(实验)。具体来说,我回顾了这些方法所需的假设、如何验证这些假设、可能存在的陷阱以及如何编写这些方程式或计算标准误差。

诚然,这些是经济学或相关领域博士的基础知识,但要能够当场提出答案并清晰沟通,仍需要复习。

我还练习了一些商业案例问题。有效沟通与选择适当的方法同样重要。在案例面试中,有几个关键方面需要注意。首先,必须要大声思考。这种方法让面试官了解你的思维过程和选择方法的理由。其次,如果问题的任何部分不清楚,不要犹豫去请求澄清。第三,与面试官进行对话,并对他们的建议持开放态度。在我的经验中,面试官在面试过程中提供了有用的提示和指导。

最终,我遇到的技术问题涉及评估亚马逊上一个假设的价格促销的影响。这个情景描述了一个捆绑定价交易:对购买任何四种价格在 10 美元以下的产品的顾客提供 10%的折扣。这项促销活动在所有地区同时实施。提出的挑战是:“你如何评估这项促销对亚马逊盈利能力的影响?”

Marques Thomas拍摄的照片,来自Unsplash

评估此次促销活动的影响需要考虑多个因素。一方面,这项促销可能是有益的,因为它鼓励顾客购买更多商品,可能通过捆绑减少运输成本。另一方面,10%的折扣可能会对利润产生负面影响。

在面试过程中,我询问了分析这种情况所需的数据可用性。面试官表示,我可以访问产品级销售数据、交易级数据和运输成本数据。根据这些信息,我提出了几种分析方法,包括进行一个事件研究和运用回归不连续性。我还解释了如何利用这些数据来进行这些分析。

还有一些后续问题。例如,这次促销活动并没有同时在所有地方推出,而是进行了一次实验,这次促销在一些州实施而没有随机化。提出的问题是:“你如何使用这个实验来衡量其影响?”对此问题,按我记得,我建议了一种合成控制方法。

面试官询问了我对几个其他假设情景的建议。其他一些跟进问题包括:“假设利润下降但收入增加。那你会建议什么?你会更广泛地实施促销还是不?”我认为在这些类型的问题中,你需要回到一些领导原则,例如“客户至上”,但我不太确定。不过,这些问题确实值得思考。

第三部分:问答环节。

我的面试官在会议结束时留了 5–10 分钟的时间供我提问。我准备了一些问题,例如团队是如何运作的,或者一天的工作是什么样的。所以,我问了这些问题。面试官非常详细地回答了这些问题,我确实很感激。如果你正在为这种面试做准备,你也应该准备一些问题。

回顾

对于行为类问题,我觉得回答起来并不难,但我不确定我的故事有多有力。因此,回过头来看,我可能会尝试准备更有说服力的故事。找到特别与公司文化和挑战相关的故事可能会更好。

对于技术部分,我发现了一些挑战。其中之一是要明确写出你的计量经济模型的方程。在整个面试过程中,你需要口头传达你的解决方案。我没有问题用这种方式沟通总体方法和假设,但大声说明回归方程却很具有挑战性。你还需要指定下标等。我习惯于把方程写下来,但大声说出来并不擅长。我还很难决定回归的最佳结果指标是什么,例如销售额还是利润。

起初,亚马逊计划了两轮面试,但我的旅程在第一轮就结束了。因此,我分享的见解基于我在亚马逊面试过程中的首次也是唯一一次经历。回顾这点,我想强调对于准备类似面试的人,无论是在亚马逊还是专注于因果推断的数据科学角色,重要的不仅仅是专注于行为问题和技术方法。同样关键的是能够解决业务问题,尤其是那些特定于角色背景的问题。在我的情况下,重点是零售行业。回头看,花更多时间审视零售特定场景会更有利。由于我*时的工作不涉及零售数据,深入研究这个领域会有助于迅速识别最佳指标并更清晰地表达我的想法。这种方法不仅对亚马逊的经济学家角色有益,也是任何进入行业特定角色的数据科学专业人士的宝贵策略,其中上下文理解对展示专业知识和解决问题能力至关重要。

感谢阅读!

如果你喜欢这篇文章并想查看更多我的文章,考虑 关注我

免责声明:我写作是为了学习,所以你可能会发现文章中的错误。如果发现,请告知我。

我对 DevOps 和 DataOps 的经验

原文:towardsdatascience.com/my-experience-with-devops-and-dataops-baf3471fd68c

这两种数据角色如何相似又如何不同

Madison SchottTowards Data Science Madison Schott

·发布于Towards Data Science ·阅读时间 6 分钟·2023 年 11 月 4 日

--

图片由Pawel Czerwinski提供,来源于Unsplash

当我首次成为数据工程师时,我在一个专注于 DevOps 的团队中工作。虽然这并不是我在第一个职位上想做的工作,但它教会了我很多。现在回头看,如果当时我没有在那种角色中工作,那么我今天作为分析工程师的经验可能就不会如此丰富。

现在,作为分析工程师,我专注于一种叫做 DataOps 的工作。虽然这可能听起来与 DevOps 类似,但它们却非常不同。DevOps 关注的是软件作为产品,而 DataOps 则关注于生产高质量的数据。对于专注于 DataOps 的人来说,数据就是产品!

在担任 DevOps 数据工程师期间,我支持软件工程师对我们的网页应用程序进行代码更改。我专注于每次部署后测试 UI 的更改,而不是检查数据的具体情况。我从未检查过表中的行数或字段中的值是否已填充。相反,我确保后端没有出现错误。

作为分析工程师,每次我进行代码更改或将内容推送到生产环境时,我需要关注元数据——即关于数据的数据。这涉及编写验证查询,以确保行数、列数和值的分布与我推送更改前的情况一致。或者,如果我希望它们看起来不同于之前的样子,它们反映了这些变化!

尽管 DevOps 和 DataOps 听起来相似,但它们服务于两个不同的目的。在这篇文章中,我们将深入探讨它们之间的差异,涉及它们旨在服务的产品以及成功的不同指标。

图片来源:Y42

什么是 DevOps?

DevOps 涉及软件代码更改的部署和测试。当我担任 DevOps 工程师时,这通常涉及长时间的部署夜晚,在许多不同环境中测试代码更改,并与进行更改的软件工程师验证这些更改。

软件作为产品

DevOps 团队的工作是服务于软件工程师。他们帮助管理更改发生的环境,确保最小化停机时间,并使*台能够相应地扩展。DevOps 数据工程师不会编写他们部署的代码,而是管理支持代码的基础设施。

我通常会与软件工程师合作,以确定何时应该推送更改,推送到哪个环境,然后验证我们的 AWS 资源是否能够妥善处理这些更改。我从不阅读拉取请求或检查某人的 JavaScript 编写方式。我则确保选择了正确的集群大小并相应地扩展了云资源。重要的是要记住,DevOps 专注于软件代码更改的可交付性。

为了确保你能够作为 DevOps 团队进行交付,你需要建立像 CI/CD 管道这样的流程,并利用这些流程不断迭代。如果在将内容推送到生产环境之前没有进行适当的测试,交付通常会失败。为了防止代码出现问题,主动测试代码在新环境中的表现至关重要。

DevOps 成功的关键指标

DevOps 团队将停机时间作为其核心成功指标。这是我们团队用来查看自己与之前几个月或其他团队的表现对比情况的关键指标。如果我们的停机时间增加,我们知道需要进行某些更改。这也是系统改进是否有效的一个很好的指标。

如果你做得很好,停机时间通常会被限制,因为部署过程中的一切都顺利进行。这表明你已经做好了充分的准备,并知道如何解决遇到的潜在问题。

然而,如果停机时间较长,这通常是部署过程出现问题或系统未能根据变化进行相应扩展的良好指标。这在 DevOps 中至关重要,因为客户通常是受到停机时间影响的对象。这也是为什么你通常会看到由 DevOps 团队引发的 SEV 1 和 SEV 2 事件。这些被认为是关键事件,因为它们阻止了客户使用你的产品。

什么是 DataOps?

DataOps 将数据视为产品。它不支持核心软件功能,而是专注于高质量的元数据。作为一名分析工程师,我每天都专注于 DataOps。如果我不能生成新鲜、准确且可靠的数据,那还有什么意义呢?

数据作为产品

DataOps 是任何数据角色的核心部分。与 DevOps 不同,它不是一个独立的团队来监督变化,而是每个数据从业者必须执行的任务。分析工程师和数据工程师应验证他们在模型中使用的数据模型以及模型生成的数据。许多人像我一样会进行自己的测试,然后监控这些测试以确保一切顺利进行。没有单独的团队来质量检查新数据如何融入现有的数据环境。

在 DataOps 中,不一定有部署计划或流程,因为它是持续进行的。分析师和数据工程师需要不断监控数据库、数据仓库和任何其他生成数据的系统。与其说有一个严格的流程,不如说是持续地进行更改,并确保你在过程中进行测试和监控。

然而,与 DevOps 类似,DataOps 也可能涉及构建 CI/CD 管道等功能,以便使代码更改更容易、更可靠。DataOps 还可能涉及设置不同的环境,以测试源数据或数据模型的变化如何影响下游数据。

DataOps 成功的关键指标

由于 DataOps 关注数据质量,成功的关键指标包括数据的新鲜度、计算值与实际值的接*程度,以及数据在需要时的可用频率。可观察性中强调的相同指标也被认为是 DataOps 中的成功指标。

归根结底,产生的数据需要被业务部门使用。如果业务无法利用这些数据来做出决策,那么 DataOps 的执行就不够好。其目的是引导业务走向成功并获得最大收入。高质量的数据会帮助你实现这一目标,而低质量的数据则会产生相反的效果。

结论

虽然 DevOps 和 DataOps 在服务的产品和测量的指标上有所不同,但归根结底,它们都是为了服务最终用户并提供最佳体验。对 DevOps 而言,这个最终用户是客户;对 DataOps 而言,则是业务利益相关者。

DevOps 和 DataOps 都关注产品的健康,并获得可能出现问题的洞察。他们通过在实施更改前进行适当的测试,尽量防止对客户或利益相关者产生负面影响。从根本上说,它们的存在是为了减少业务面临的问题数量。

尽管作为分析工程师和 DevOps 数据工程师的日常工作非常不同,但我的两个团队都位于组织的核心。他们的目标是服务他人并确保变更顺利进行。他们共享最佳实践,如在组织内实施严格的数据治理、使用版本控制和实施 CI/CD。从本质上讲,DevOps 和 DataOps 是以服务为中心的学科,没有它们,许多公司将会非常迷失。

如果你正在考虑寻找一个 DevOps 或 DataOps 的职位,我建议你先考虑一下你想要从事的产品类型。你对软件工程还是数据工程更感兴趣?如果你更倾向于数据工程,你可能需要了解一下 DataOps。

还要考虑你的生活方式——在 DevOps 中,你经常需要在半夜进行奇怪的部署。如果你是一个重视早睡和睡眠的人,这可能不是适合你的角色。

每个领域都有优缺点,你需要深入了解一下。练习我在本文中提到的一些关键技能,看看你更喜欢哪个。测试这些领域的唯一方法就是尝试各自的职责!

我与 ChatGPT 的第一次探索性数据分析

原文:towardsdatascience.com/my-first-exploratory-data-analysis-with-chatgpt-7f100005efdc

发掘 ChatGPT 的力量:深入探讨探索性数据分析和未来机会

Jye Sawtell-RicksonTowards Data Science Jye Sawtell-Rickson

·发布于 Towards Data Science ·15 分钟阅读·2023 年 5 月 10 日

--

“一个 AI 探索广阔的数据世界。数字艺术。生动的色彩。”(作者通过 DALL-E 2 生成)

ChatGPT 是一个非凡的工具,可以提高工作效率,这不仅仅局限于数据分析。在本文中,我们将通过一个 ChatGPT 执行的探索性数据分析(EDA)示例进行介绍。我们将覆盖 EDA 的各个阶段,看到一些令人印象深刻的输出(词云!),并指出 ChatGPT 表现好的地方(和不太好的地方)。最后,我们将讨论 LLM 在分析中的未来以及我们对此的兴奋之情。

用于分析的数据集是来自 Common Crawl 的样本,该数据集 免费供任何人访问和分析。Common Crawl 数据集是一个庞大的网络爬虫数据集合,包含来自互联网的数十亿网页。该数据集包括各种网页内容类型,并且定期更新。它作为训练语言模型(如 LLMs)的重要资源,占 ChatGPT 训练数据的 60%。你可以在 Kaggle 上找到由作者策划的数据集样本 这里

在整个文章中,内容将会被截断,所以可以直接查看 用于运行此分析的 Google Colab

我们将分析分为五个部分:

  • 🤔 总体结构

  • 🧹 数据清理

  • 💯 基本分析

  • 🇬🇧 NLP 方法

  • 🔑 关键发现

以上内容介绍完毕,让我们开始分析吧!

结构化工作

在开始分析工作之前,制定一个计划是很好的。EDA 通常遵循一个标准结构,开始时的步骤是固定的,然后在分析中发现有趣的见解时再进行扩展。一个这样的模板是这个,它详细介绍了核心组件:预览数据、清理数据、绘制数据。我们可以以此为起点,但假设我们一无所知,看看 ChatGPT 会提出什么。

Prompt:

让我们一起对 Common Crawl 文本数据集进行 EDA。数据集是从互联网上许多网页抓取的。列包括'text'、'WARC-Type'、'WARC-Target-URI'、'WARC-Date'、'WARC-Record-ID'、'WARC-Refers-To'、'WARC-Block-Digest'、'WARC-Identified-Content-Language'、'Content-Type'和'Content-Length'。数据集保存在一个名为 common_crawl_text 的 csv 文件中。

你应该给我一个整体计划,然后我们可以一步一步进行。在每一步中,你应该写一些代码,我会在终端中运行它,然后将结果反馈给你。

Response:

当然,这里是对 Common Crawl 文本数据集进行 EDA 的高层次计划:

  1. 将数据集加载到一个 pandas DataFrame 中。

  2. 检查每列中的缺失值,并适当地处理它们。

  3. 探索目标变量'WARC-Type'的分布。

  4. 检查'WARC-Identified-Content-Language'的分布。

  5. 检查'Content-Type'的分布。

  6. 探索'text'列,包括文本长度的分布和最常见的单词。

  7. 检查不同列之间的相关性。

  8. 总结发现并识别任何有趣的模式或见解。

让我们从第 1 步开始,将数据集加载到一个 pandas DataFrame 中。

ChatGPT 在这里做得相当好,覆盖了标准步骤。注意,它并不要求我们深入分析所有列,而是对特定子集感兴趣。这很合理,但我觉得它遗漏了一些有价值的数据点。例如,“WARC-Target-URI”值得探索,“WARC-Date”列将提供数据收集时间的见解。

我用这个提示了 ChatGPT,它很高兴地将这些内容纳入了计划。同时,它还添加了创建 Wordcloud 的步骤,这是我一直在考虑的。

我喜欢这种互动方式,感觉像是合作,而不仅仅是阅读一本书。

数据清理

按照结构中的每个步骤,我们可以要求 ChatGPT 扩展该步骤,并给我们提供精确的代码。首先,它建议:

我们可以从加载 Common Crawl 文本数据集并查看其结构开始。以下是将数据集加载到 Pandas DataFrame 中的代码:

import pandas as pd

df = pd.read_csv("common_crawl_text.csv")

一旦加载了数据集,你可以使用以下代码检查 DataFrame 的结构:

df.head()

这将显示数据框的前五行。你也可以使用 df.info() 方法来获取列及其数据类型的概述,以及每列中非空值的数量。

在提供结果后,它能够分享一些基本信息:

根据提供的数据,似乎我们有一个包含不同属性的网页档案记录的数据集,例如记录的类型、记录的 URI(统一资源标识符)、记录的日期、内容的长度和内容本身。内容以不同的语言和格式存在,例如纯文本,内容长度也各不相同。数据集包含 37832 条记录,其中一些记录在内容长度列中有空值。

我们可以进一步要求它给出每一列的详细描述:

提示:

你能提供一个详细的列列表和预期含义吗?

响应:

当然!这是数据框中列的详细列表及其预期含义:

  1. WARC-Type: WARC 记录的类型。WARC(Web ARChive)记录是一种用于存储互联网资源(如网页及其相关元数据)的格式。

  2. WARC-Target-URI: 被存档网页的目标 URI(统一资源标识符)。

  3. WARC-Date: 网页被存档的日期和时间。

  4. WARC-Record-ID: WARC 记录的唯一标识符。

  5. WARC-Refers-To: 此记录所引用的相关 WARC 记录的标识符。

  6. WARC-Block-Digest: 对 WARC 记录内容计算的加密哈希值。

  7. WARC-Identified-Content-Language: 在网页内容中识别的语言。

  8. Content-Type: 内容的媒体类型,例如 text/plain 或 text/html。

  9. Content-Length: 内容的字节长度。

  10. text: 网页提取的文本内容。

请注意,其中一些列是特定于 WARC 格式的,可能在其他类型的数据中不存在。

它在猜测这些列的含义方面做得相当不错。虽然有些可能不正确(例如 Content-Length),但这是一个有用的起点。它可能确实知道这些特定名称,因为它的训练数据,但我已经尝试了任意列名,它通常可以从名称和返回的数据的组合中猜测列将包含什么。

现在,它注意到有缺失值,我们来看看如何处理这些问题:

提示:

我们应该对缺失值做些什么吗?

响应:

是的,处理 Content-Length 列中的缺失值是个好主意。一种选择是删除那些缺失值的行,如果它们不是数据的重要部分。另一种选择是使用一些统计方法(例如均值、中位数或众数)来填补缺失值。然而,最佳方法将取决于数据的性质和所执行的分析。

这些可能是我处理它的方式。在这种情况下,我很乐意直接删除这些条目,因为数量不是很多。

提示:

我们就将这些删除,因为数量非常少。我该怎么做呢?

响应:

你可以使用 pandas 中的 dropna() 函数删除包含缺失值的行。以下是一个示例:

import pandas as pd

# 创建一个数据框

df = pd.DataFrame({'A': [1, 2, None, 4], 'B': [5, None, 7, 8]})

# 删除缺失值的行

df.dropna(inplace=True) print(df)

输出:

A B 0 1.0 5.0 3 4.0 8.0

在这个示例中,dropna() 函数会删除第二行,该行包含缺失值。inplace=True 参数会修改原始数据框,因此结果数据框只包含没有缺失值的行。

注意它如何提供有效的代码,并对每一行代码进行注释。它还在最后提供了发生了什么的解释。这很好,因为我们可以了解代码的运行情况,如果有需要修改的地方,也会变得容易得多。

我们现在可以开始分析的第一部分。

基本分析

步骤 3–5 大致相同,绘制一些数据分布图并查看是否有任何有趣的发现。

当提示开始步骤 3 时:

Sure, let's explore the distribution of the target variable 'WARC-Type'. We can do this by creating a histogram or a bar chart of the variable to see the count or percentage of each value in the dataset. If you are using Python, you can use libraries like Matplotlib or Seaborn to create visualizations. Here is an example code snippet to create a bar chart using Matplotlib: python import matplotlib.pyplot as plt
# Count the number of occurrences of each value in the 'WARC-Type' column
value_counts = df['WARC-Type'].value_counts()
# Create a bar chart of the value counts
plt.bar(value_counts.index, value_counts.values)
# Set the title and axis labels
plt.title('Distribution of WARC-Type')
plt.xlabel('WARC-Type')
plt.ylabel('Count')
# Show the plot plt.show() This code will create a bar chart showing the count of each value in the 'WARC-Type' column. You can modify the code to show the percentage of each value instead of the count.

这样会为我们输出一个图表。在这种情况下,图表并不特别有趣,因为实际上只有一个“WARC-Type”,但这是个不错的主意。图像的问题在于很难将结果反馈给 ChatGPT。我发现,建议 ChatGPT 返回数据表以解决这个问题会更好,同时在有意义的地方附上额外的图表。

了解列中只有一个值后,ChatGPT 很乐意继续进行下一阶段。当我们探索“WARC-Content-Language”时,我们开始看到一些更复杂的东西。返回的结果是:

eng 15903

rus 1551

...

hrv,nno,bel 1

zul,eng 1

从中学到的是:

似乎“WARC-Identified-Content-Language”列有 1187 个独特的值,最常见的语言是英语('eng'),其次是俄语('rus')、德语('deu')和中文('zho')。由于有如此多的独特值,分析变量的分布可能会很困难。简化分析的一种方法可能是将语言分为更广泛的类别,例如“英语”、“罗曼语系语言”、“斯拉夫语言”等,具体取决于研究问题。

关于分组的观点非常好,但这里有遗漏——每行可以包含多种语言。当多次询问这个问题时,它提出了一个很好的方法。

Here's an updated code that splits the rows with multiple languages and weighs them accordingly: import pandas as pd

# Split the rows with multiple languages
df['WARC-Identified-Content-Language'] = df['WARC-Identified-Content-Language'].str.split(',')

# Create a new DataFrame to store the split rows
df_split = pd.DataFrame({ 'WARC-Identified-Content-Language': [lang for langs in df['WARC-Identified-Content-Language'].tolist() for lang in langs], 'count': [1/len(langs) for langs in df['WARC-Identified-Content-Language'].tolist() for lang in langs] })

# Aggregate by language
df_agg = df_split.groupby('WARC-Identified-Content-Language')['count'].sum().reset_index()

# Plot the distribution of languages
plt.figure(figsize=(10,6))
sns.barplot(data=df_agg, x='WARC-Identified-Content-Language', y='count', order=df_agg.sort_values('count', ascending=False)['WARC-Identified-Content-Language'])
plt.title('Distribution of WARC-Identified-Content-Language') plt.xticks(rotation=90)
plt.show() 

This code splits the rows with multiple languages into separate rows, and weights them by the number of languages in each row. Then it aggregates the rows by language and plots the distribution of languages.

将这与一些关于语言分组的提示结合后,我们可以得到一个不错的图表:

我特别喜欢的一点是它给图表添加了标签。每次都写标签可能有点麻烦,但确实会有区别,所以看到它足够聪明地提供合理的标签是很不错的。

看起来英语仍然是主导语言——主导程度如何?

提示:

看起来主要是英语。你能计算一下汇总数据中英语的百分比吗?

Response:

当然,这里有一些代码来计算汇总数据中英语的百分比:english_percentage = (agg_data.loc['eng', 'weight'] / agg_data['weight'].sum()) * 100

print(f"The percentage of the aggregated data that is English is: {english_percentage:.2f}%")

这应该输出汇总数据中英语的百分比,保留到小数点后两位。

(作者创建的图像)

自然语言处理

现在我们可以进入第 6 步,这涉及对文本列进行一些探索。这将更注重分析,并需要自然语言处理(NLP)技术。让我们看看 ChatGPT 的表现。

Prompt:

很好。我们进入第 6 步:

"探索“text”列,包括文本长度的分布和最常见的词汇。"

Response:

Sure! To explore the 'text' column, we can start by calculating the length of each text and creating a histogram to visualize the distribution of text lengths. Here's the code to do that:

# Calculate the length of each text
df['text_length'] = df['text'].str.len()

# Create a histogram of text lengths
plt.hist(df['text_length'], bins=50)

This will create a histogram that shows the distribution of text lengths in the 'text' column. We can then look at the most common words in the 'text' column by first removing any HTML tags, punctuation, and stop words. Here's the code to do that: import re

import nltk from nltk.corpus
import stopwords

# Remove HTML tags from text
df['text'] = df['text'].apply(lambda x: re.sub('<.*?>', '', x))
# Remove punctuation from text
df['text'] = df['text'].str.replace('[^\\w\\s]','')
# Convert text to lowercase
df['text'] = df['text'].str.lower()
# Remove stop words from text
stop_words = set(stopwords.words('english'))
df['text'] = df['text'].apply(lambda x: ' '.join([word for word in x.split() if word not in stop_words]))
# Tokenize text into individual words
df['text'] = df['text'].apply(nltk.word_tokenize)
# Flatten list of words into a single list
word_list = [word for sublist in df['text'] for word in sublist]

# Calculate frequency distribution of words
freq_dist = nltk.FreqDist(word_list)

# Print the 20 most common words
print(freq_dist.most_common(20)) 

This code will remove HTML tags and punctuation from the 'text' column, convert all text to lowercase, remove stop words, tokenize the text into individual words, and then calculate the frequency distribution of words. Finally, it will print the 20 most common words in the 'text' column.

结果如下:

[('nan', 36847), ('0', 3467), ('1', 2809), ('keresőoptimalizálás', 2498), ('idősgondozás', 1852), ('2', 1740), ('3', 1559), ('szerződés', 1529), ('eltartási', 1527), ('brands', 1442), ('5', 1362), ('4', 1350), ('usd', 1245), ('us', 1205), ('2023', 1204), ('new', 1178), ('sex', 1165), ('2022', 1031), ('home', 919), ('cookies', 904)]

我对此不是很满意,因为我们可以看到很多“常见词汇”只是数字,或者是其他语言的停用词(它只使用了英语停用词)。我能够在这方面进行调整,通过过滤掉非字母字符的词汇和包括其他语言的停用词列表来帮助清理数据。现在让我们在词云中可视化这些数据。

Prompt:

你能制作一个词云吗?

Response:

Sure! To create a wordcloud from the top 50 most frequent words, you can use the WordCloud library in Python as follows: from wordcloud import WordCloud

import matplotlib.pyplot as plt

# Convert the top 50 most frequent words to a dictionary
freq_dict = dict(top_words)

# Generate the wordcloud
wordcloud = WordCloud(width=800, height=400, background_color='white').generate_from_frequencies(freq_dict)

# Display the wordcloud
plt.figure(figsize=(12, 8))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off')
plt.show()
This code will generate a wordcloud with the top 50 most frequent words in the text column. You can adjust the parameters of the WordCloud function to customize the appearance of the wordcloud.

警告:包含明显的语言。对于那些好奇的人,“keresőoptimalizálás”是匈牙利语中的“搜索引擎优化”(作者创建的图像)。

我不确定从这个图中能得到什么。似乎有一些词可能关联有用的内容(例如“信息”、“博客”、“品牌”),但也有很多不需要的内容。我将这些常见词汇分享给 ChatGPT 并询问它的看法:

Prompt:

我们对此可以说些什么?

Response:

根据“text”列中发现的常见词汇,数据可能包含很多垃圾信息或不相关的信息,因为许多常见词汇与在线约会、评论、贷款和陪玩相关。此外,像“nan”这样的词汇表明“text”列中可能有缺失或不完整的数据。进一步调查“text”列的数据性质和质量对于确定其在后续分析中的有用性是重要的。

这是一个合理的结论,实际上,与 Common Crawl 数据集合作的公司确实表示,他们投入了大量资源来清理数据集,以使其适合训练 AI 模型。

Wrapping Up

这就结束了 ChatGPT 概述的计划,所以我们将在这里结束分析,并查看它有什么发现。

提示:

很好。让我们继续第 8 步。你能总结一下我们迄今为止的所有分析结果吗?

回复:

当然,基于我们目前进行的分析,以下是一些关键发现:语言分布:数据集中最常见的语言是英语,其次是俄语、德语、中文和西班牙语。内容类型分布:数据集中大多数内容类型为"text/plain"。文本长度:数据集中,文本长度范围从 0 到 8,542,524,*均长度为 20,566。常见词:数据集中最频繁的词汇并不特别有信息量,因为它们包含许多停用词和其他非信息性术语。相关性:文本长度与内容长度之间存在强正相关。检测到的语言数量与内容长度之间也存在弱正相关。

如果有数据分析师带着这些内容来找我,我会感到非常失望。正如开始时提到的,有一些列数据可以分析得更详细,并且通常更好的是提出一些下一步的建议。然而,我们从未给它设定分析的目标,也没告诉它给出建议,所以我不会太过苛责。

讨论

如你所见,整个过程中使用的提示相对简单。考虑到这一点,ChatGPT 能够很好地理解我想要的并给出有用的回答,这一点令人印象深刻。这些提示可以通过在每个提示中提供更多上下文以及对我们希望返回的内容更严格来改进。例如,每个提示可以包含对其专注的具体任务的引用,以及额外的文本以确保其做我们想要的:

不要回应多余的文本。假设 pandas、numpy 和 matplotlib 已按标准方式导入。

这些可以保存在你自己的提示模板中以加快这类工作的进度,或者使用诸如LangChain这样的工具来完成。

我们还可以定义自己的总体模板。我让 ChatGPT 提出一个计划,但它并不完美。我们可以定义一个总体结构供其遵循,以及分析每个变量的标准方法。通过模板,ChatGPT 在这种分析中遗漏洞察的可能性较小。

虽然与 ChatGPT 反复交流以获取数据输出很有趣,但很快变得疲惫。ChatGPT 在能够直接运行代码时更为强大。ChatGPT 可以通过使用Python API连接到 Python 运行时。在这种情况下,代码可以自动运行,但要将人从循环中剔除,我们还需要一个额外的工具。

在过去一个月里,AutoGPT 作为 ChatGPT 的增强工具非常受欢迎,它有效地为 ChatGPT 代理提供了指南,允许他们持续朝着某个目标执行。AutoGPT 可以在这种情况下取代我,让 ChatGPT 代理设计代码,然后执行它,将结果反馈给 ChatGPT,直到它得到详细的分析。它还将与一个内存数据库接口,从而允许它执行更大规模的分析。

使用像 AutoGPT 这样的工具,我们可以设定一个明确的目标,包括分析的细节和期望的结论风格。在这种情况下,我们可以较少地检查结果,并最终很少需要工作即可得到一个不错的分析。

最后,我们应该指出 ChatGPT 远未达到“完美”,即便在这次模拟分析中,我也需要调整提示,以获得接*我想要的答案。虽然这比我预期的要容易得多,但仍然值得注意。它创建了一些有错误的代码,尽管每次被告知后都能修复这些错误。有时它创建的代码是我不想运行的,我需要建议它采用不同的方法,但再次提示后,它能够提出一个不错的解决方案。

结论

在本文中,我们已经看到 ChatGPT 如何用于支持探索性数据分析(EDA)的运行。我们发现,与系统合作时能够获得意想不到的良好结果,几乎不需要外部帮助。我们还注意到,已经存在一些工具可以扩展这一想法,例如 AutoGPT,它可能成为一个更强大的助手。

作为一名数据分析师,我已经在使用 ChatGPT 帮助我的分析,尽管我很少使用它进行本文中详细描述的端到端分析。随着像 AutoGPT 这样的工具的更多集成,以及使用摩擦的减少,我预计会越来越多地使用它,并且对此非常期待(虽然我不会被取代 😉 )。

我的#30DayMapChallange 第一周

原文:towardsdatascience.com/my-first-week-of-the-30daymapchallange-1bef0646c88e

作者提供的图像。

我对#30DayMapChallange 第一周的个人看法,这是一个旨在每年 11 月每天设计主题地图的社会挑战。

Milan JanosovTowards Data Science Milan Janosov

·发表于 Towards Data Science ·5 分钟阅读·2023 年 11 月 7 日

--

自 2019 年以来,每年 11 月地理信息系统(GIS)和空间分析社区都会变得非常忙碌——这要归功于一个名为#30DayMapChallange的有趣挑战。每年,这个挑战都有一个主题安排,提出一个应该作为当天地图可视化主要方向的主题。虽然这些问题确实意味着某种限制,但它们也帮助参与者找到共同兴趣,分享数据源,并在视觉和技术上表达个人风格。

在这里,我想简要概述一下我参加这个挑战的第一周,详细展示我创建的不同地图——通常使用 Python。

在本文中,所有图像均由作者创建。

第一天——点

这个挑战的第一天通常涉及所有几何形状中最简单的——点。为了创建我的 POI 地图,我使用了NASA 的开放数据门户的陨石着陆数据。这个数据集包含大约 40k 条观察数据,当这些数据放置在地图上时,与人口密度显示出显著的相关性。因此,陨石要么落在有人居住的土地上,要么我们在更多人居住的地方有更多关于陨石的数据,对吧?

为了创建这个(交互式)地图,我使用了 Python,特别是 Folium。

在可视化中,我根据每颗陨石的记录质量(以克为单位,范围从 0.01 克到 60,000 千克或 60 吨)来调整每个点标记的大小。顺便说一下,这颗 60 吨重的巨型 Hoba 是在 1920 年在纳米比亚的 Grootfonteinn 发现的。然后,我根据发现时间为每个标记着色。有趣的事实是:第一颗记录在案的陨石 Nōgata(472 克)是在 861 年在日本福冈县发现的,发现后不久。经过这次观察,数据库中几个世纪没有数据。然后,最后,Elbogen 在 1399 年(107000.0)出现,接着是 1490 年的 Rivolta de Bassi(103.3 克)和 1491 年的 Ensisheim(127000.0 克)。查看数据集中的后期,结果表明 35%的陨石是在 2000 年后记录的,98%是在 1899 年后记录的。

第 1 天——点

第 2 天——线

在点之后,自然地,就到了线(或者更准确地说,是线字符串)。作为一名网络科学家和地理空间数据科学家,我的选择显而易见:使用从 OpenStreetMap 收集的数据,通过OSMNx包来可视化我家乡布达佩斯的道路网络。

关于这个网络的有趣事实:它有 115,539 个节点和 316,096 条边,而所有道路段的总长度为 1,879 公里!

第 2 天——线

第 3 天——多边形

首先是点,然后是线——现在,轮到多边形了!在这张地图上,我将世界上的每个国家视为一个单独的多边形,基于Natural Earth数据进行构建。为了添加一个漂亮的渐变颜色,我根据每个国家距离赤道的距离来着色。所有计算都在 Python 中完成,视觉效果则使用 Matplotlib 制作。

这张地图形状相当奇特,因为我使用了所谓的 Larrivee 投影,这种投影由 Léo Larrivée 于 1988 年为加拿大开发。你可以在这里了解更多关于不同地图投影的信息。

第 3 天——多边形

第 4 天——一张糟糕的地图

地图挑战的第四天非常不寻常——当每个人都忙着制作自己一生中最好的地图时,我们必须制作一张差的地图。然而,这张地图如何糟糕则取决于解释。

为了确保我的地图足够差,我做了一些数学计算,以得出一张毫无意义、绝对糟糕的德国地图。首先,我使用 OSMNx 包从 OpenStreetMap 下载了德国的行政边界。其次,使用 Uber 的H3库将其拆分成六边形,使用六边形级别 6,得到了 12122 个六边形。第三,对于每个六边形,我计算了其质心的经纬度坐标(使用 epsg:4326),精确到 14 位小数。然后,我将经纬度坐标中的每个数字相加,得到用于为每个六边形单元着色的‘分数’。完成!毫无意义!

第四天 — 一张糟糕的地图

第五天 — 模拟地图

在经过四天的密集数据处理后,第五天我们被带到荒野中,并要求我们制作模拟地图。由于当时离万圣节还很*,我决定将其与地图挑战结合起来,并在上面雕刻布达佩斯的地图。

可怕的部分有点隐藏 — 我使用了 EPSG:23700,即匈牙利的本地投影系统,这在布达佩斯看起来非常好,但在其他任何国家的几何图形上使用时很可能效果糟糕。如果你敢试试的话!

第五天 — 模拟地图

第六天 — 亚洲

第六天,我回到了之前介绍的自然地球数据库,并下载了包含全球时区的 GIS 文件。由于第六天的主题是亚洲,我需要进行一些过滤,找出 48 个亚洲国家,然后得出了如下地图 — 使用了 Python 和 Matplotlib。

第六天 — 亚洲

第七天 — 导航

最*,我在哈佛数据集上发现了一个有趣的数据集,名为罗马道路网络(2008 版):这是一个完美的 GIS 格式的罗马帝国历史道路网络。我将其转化为网络分析项目,并发布在数据科学之路上。正如这个项目所展示的,确实,昔日的所有道路都通向罗马;我相信这是一个— 相对简单但仍然 — 的导航地图,引导每一个踏上这些道路的人前往中心。

第七天 — 导航

这是我进行#30DayMapChallenge 的第一周总结 — 还有三周的挑战,准备迎接更多的地图吧!现在,我还在我的Patreon频道上提供视频和代码教程。

我成为更好的数据科学家的五个关键学习心得

原文:towardsdatascience.com/my-five-key-learnings-to-be-a-better-data-scientist-39a99658c89e

对我六年数据科学职业生涯的反思

Yu DongTowards Data Science Yu Dong

·发表于 Towards Data Science ·阅读时长 9 分钟·2023 年 1 月 2 日

--

我在 2018 年 1 月开始了我的第一份全职数据科学工作,担任助理产品分析师。这是我数据科学职业生涯的第七年,现在是高级数据科学家。六年过得很快(特别是疫情期间)。我为两家公司工作,支持了至少 10 个不同的团队,与合作伙伴在产品、工程、设计、财务和运营方面合作,并涉及各种数据科学领域,包括数据管道、仪表板、指标审查、深入分析、A/B 测试和建模。

因此,我觉得是时候对这段旅程进行反思了。以下是我五个关键的学习心得。

GPT 生成的图像

I. 技术技能很重要,但…

当我在学校时,我对技术技能非常着迷。我花了很多时间学习和探索不同的机器学习模型,并参加了 Kaggle 比赛。然而,自从我开始进入行业后,我意识到需要关注的事项远不止编写代码和构建华丽模型,有些事情实际上更为重要。

我想特别强调的一点是理解业务背景,无论你更侧重于分析还是机器学习。我们被公司聘用是为了创造(货币)价值,因此,归根结底,公司最关心的是你所做的分析或建立的模型是否能够带来更多用户、增加收入或降低成本。而理解业务背景是关键。我们都听说过“可操作的洞察”这一术语——最好的洞察是那些你的业务利益相关者可以采取行动的。但如果你没有足够的领域知识,你将无法知道哪些洞察是可操作的。

此外,我们在学校里通常会得到干净的数据集。但实际上,数据总是很混乱。你可能会花费至少 30%的时间来收集数据、清理数据,甚至设置数据管道,以确保你有高质量的数据来进行分析或构建模型。这需要对业务背景有深入理解,以便识别什么是‘高质量数据’。例如,良好的商业感觉使你更容易注意到当你计算的商业指标明显偏离正常范围时;不同的缺失值填补技术有不同的影响,商业理解可以帮助你选择最佳解决方案。

更不用说,如今 AI 工具如 ChatGPT 降低了成为数据科学家的技术门槛,并将继续这样做。在这篇文章中,我评估了 ChatGPT 的数据分析能力。你可以看到它能完成多少初级数据分析师/科学家的工作,但显然缺少的是对你特定业务问题的知识。

总之,技术技能是获得数据科学工作最低要求。但它们不是唯一区分你(与 ChatGPT)的因素,也不是让你成为优秀数据科学家的因素

那么,获取业务背景的最佳方法是什么呢?

  1. 如果可以的话,亲自使用产品。我记得在我在 Ancestry 工作时,至少有五个测试账户。而且我自己也经常使用 Brex。尝试不同的用户路径和在 UI 中点击是理解参与模型和识别潜在的流失点及痛点的非常有帮助的。

  2. 与跨职能的合作伙伴交谈,包括 PM、工程师、设计师等,并定期与他们会面,分享你的发现。他们通常从不同的角度对产品有大量的理解,可以帮助你了解当前的产品目标、过去尝试过的内容和所得到的经验,以及什么是可行的,什么是不行的。

II. 不要低估报告和仪表板工作

这可能不是一个受欢迎的观点。

如今,成为数据科学家意味着很多不同的事情。特别是如果你在一家初创公司工作,你几乎被要求具备“全栈”能力。在所有这些不同的任务类型中,我们都喜欢进行深度分析、运行 A/B 测试和构建复杂模型,同时,报告和仪表板工作通常不受欢迎。我大多数时候也是这样感觉的,我们数据科学家通常根据那些更技术性的项目进行评估。不过,我想说这些工作有其价值,并且对于过去数据支持不足的新产品功能或新的业务单元至关重要。我相信它们有三个主要价值:

  1. 它有助于建立基准。在进行任何高级分析之前,我们总是需要问自己我们的目标是什么,或者我们尝试改进的是什么。然而,如果我们没有跟踪关键指标的仪表板,或者我们甚至没有就关键指标达成一致,这就是一个困难的问题。想象一下,我们花了大量的努力通过媒体组合建模来优化营销预算,以获取大量新用户,但这些用户每月的流失率达到 90%(但由于我们没有跟踪留存率,我们并不知道),那么这只是浪费钱。

  2. 它可以带来快速的洞察。在产品开发的初期阶段,事情可能会发生快速变化,这意味着指标也会不断变化。因此,密切监控指标并快速诊断任何趋势变化是很重要的。即使是成熟的产品,你也可能会不时看到指标异常,这可能是由于功能变化、用户组合变化或竞争环境变化等原因。通过例行报告和指标审查,及早发现这些异常是很重要的。

  3. 最后但同样重要的是,这是一种赢得信任的好方法。报告和仪表板对理解产品和衡量其影响至关重要,因此业务利益相关者通常非常重视它。能够构建准确的报告,讨论指标,并根据这些指标提供业务建议,对建立与跨职能合作伙伴的关系非常有帮助,尤其是在初期阶段。

但当然,当我们有一组一致同意的关键指标来监控时,我们应该确保自动化这些报告工作,这样我们可以腾出更多时间去做我们更喜欢的工作,并最终释放更多价值。

III. 有效的沟通与合作

如果你问我离开学校后最大的惊讶是什么,我会说——会议。我从没想到数据科学家每天需要参加如此多的会议。有时候,会议占据了我一天的一半时间。然而,其中大多数是必要的。今天,我将数据科学项目视为一个迭代过程——我参加跨职能会议以收集业务痛点和问题,然后将它们转化为数据问题。接下来,我使用数据科学工具进行分析,并将其转化为业务语言。最后,我将洞察带回给利益相关者,我们讨论想法和下一步,这可能会发展成我们想要运行的实验、我们想要构建的模型或其他分析。这些步骤不断重复,有效的沟通和合作是持续提供业务价值的关键。

说实话,这对我来说一直很困难,因为我非常内向(根据我的观察,很多数据人员也是如此),而且英语不是我的母语。以下是一些我发现对提高沟通技能有用的方法。

首先,听取别人会议中的发言。许多时候,我在会议上过于害羞,特别是当我刚加入团队时。在这种情况下,我会利用机会向他人学习,特别是当我的经理或高级团队成员在这些会议中时。我学到的内容包括如何与不同的利益相关者沟通,如何用简单的英语讨论数据解决方案,如何设定正确的期望,如何进行小谈话,以及如何提出反对意见 😃

其次,打磨演讲技巧。演讲是与利益相关者分享分析工作和产生影响的最常见方式之一。打磨演讲包括两个部分——制作最佳的幻灯片和最有效地传达数据见解。网上有许多文章教你如何制作幻灯片。在我看来,一些关键要点包括:

  1. 限制幻灯片上的文本数量。在分析幻灯片上,用一句话总结见解,并将其放在幻灯片标题中,以配合图表。

  2. 选择合适的可视化类型,并使图表和标签足够大,以便人们能够轻松理解。

  3. 我特别喜欢以这样的结构来组织我的演示文稿:背景 -> 关键见解 -> 建议 -> 详细分析 -> 总结(重复关键见解和建议以加深印象)。这样可以帮助观众更容易捕捉你想传达的主要信息。

至于口头沟通,我 95%确信我的业务利益相关者无法准确解释什么是“统计显著”。但这没关系。我是说,我也需要几秒钟才能用科学术语表达出来。然而,我们的工作是进行严谨的统计分析,并以易于理解的方式传达我们的发现。如果你不够自信,正式演讲前的排练总是有帮助的。

当然,随着在这一领域的经验增加,我开始独立支持产品团队并领导分析项目。这些机会非常宝贵,因为它们迫使我跨部门合作并迅速成长。

IV. 成为一个团队成员

作为一名资深数据科学家,我不仅需要成为一个优秀的独立贡献者,还要能够为团队做出贡献并帮助他人成长。我感受到成为一个优秀团队成员的重要性。那么,如何成为一个优秀的团队成员呢?

首先想到的是编写良好的文档。这虽然小(且繁琐),但却非常重要。人们在入职时通常会遇到的问题是理解所有的表格、指标和过去的项目。当我遇到这种情况时,我通常会首先在 Slack、Confluence 和 Google Drive 中搜索,然后向团队成员询问。这时,我希望有更好的文档。因此,为了让他人的工作更轻松(并在你需要三个月后返回某些内容时让自己的工作更轻松),添加详细的表格文档、编写项目总结和模型方法文档是至关重要的。

为了拓宽范围,我认为当我们看到机会时,应该总是考虑为团队建立最佳实践。以文档为例,看到有多少表格缺少描述,也许这是提出全团队范围内增加文档覆盖率和为新表格设置要求的好时机。我已经启动了一项 Looker 培训,帮助队员和利益相关者快速上手 Looker,并起草了一个 SQL 查询优化课程,以倡导 SQL 最佳实践,节省查询时间和成本。

最后,尝试指导你的初级队员。这是我新接触的领域,所以我仍在学习。在过去六年中,我遇到了许多优秀的经理和导师,因此我完全理解良好导师对新人的价值。我还发现这种导师过程对双方都有益,因为我了解了更多他人的项目,并学习了新的领域知识和技术。

V. 持续学习并做好准备

我想分享的最后一个收获是持续学习。我们都知道数据科学是一个快速发展的领域。每天都有新的模型和技术。例如,许多 AI 工具每天涌现,使我们的数据科学家生活更轻松。我每周五和周日晚上都会抽出时间阅读 Medium 上的有趣博客(可以在这里查看我的双月阅读笔记)。并不是每一篇博客都能立即对工作有所帮助,但它们至少让我了解了现有的东西,当我遇到新问题时,我知道自己可以潜在地使用什么。

此外,学习不应仅限于数据科学领域。如今数据科学角色高度跨功能,因此我发现深入了解工程框架、数据管道工具、产品设计知识等也很有帮助。这使得与利益相关者的对话更加顺畅,并为未来的挑战做好准备。

我没想到在开始写这篇文章时,它会成为一篇 2000 字的长文,而且这篇文章也没有涵盖我过去几年的所有学习内容。然而,我希望这对刚刚开始数据科学职业生涯或有意进入这个领域的人有所帮助。

这篇文章最初发布在我的博客上,时间是 2023 年 1 月,并在 2024 年 6 月更新。

我的#30DayMapChallange 第四周

原文:towardsdatascience.com/my-fourth-week-of-the-30daymapchallange-c2c79c09a38c

我对#30DayMapChallange 第四周的个人看法,这是一个旨在每年 11 月每天设计主题地图的社交挑战。

Milan JanosovTowards Data Science Milan Janosov

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 11 月 29 日

--

自 2019 年以来,地理信息系统(GIS)和空间分析社区每年 11 月都很忙碌——得益于一个有趣的挑战,称为#30DayMapChallange。每年,这个挑战都有一个主题安排,提出一个应该作为地图可视化主要指导方针的主题,并在那一天发布。虽然预设的每日主题确实对创意思维造成了约束,但它们也帮助参与者找到共同的兴趣,分享数据源,并在视觉和技术上表达个人风格。

在这里,我想简要回顾一下我在这个挑战的第四周——也是最后一周——详细描述并展示我创建的不同地图——通常使用 Python 及各种空间分析和地理空间数据工具。

在这篇文章中,所有图像均由作者创建。

第 22 天 — 𝐍𝐨𝐫𝐭𝐡 𝐢𝐬 𝐍𝐨𝐭 𝐀𝐥𝐰𝐚𝐲𝐬 𝐔𝐩

在这个部分,我在主题和视觉效果上都进行了大量的探索。最后,我依靠我的物理学背景,决定绘制地球磁场及其主磁场倾角线。这些线条作为磁极,可以是正的也可以是负的。地球的磁北极由这些线条定义——而且并不总是向上的。它在移动!*年来,它开始从加拿大北极迁移到俄罗斯,速度每年几公里。

第 22 天 — 北极并非总是向上

第 23 天 — 3𝐃

我的第一个 3D 地图——所以我保持了数据的相对简单,选择了布达佩斯市区,特别是佩斯区的第 V 区和第 VI 区,并基于ATLO的布达佩斯开放数据地图可视化了其建筑高度轮廓。至于技术部分,我像往常一样使用了 Python,并最终学习了 Pydeck 的基础知识来创建这一作品。享受交互式版本这里,每栋建筑的高度与其实际高度成比例,该信息也通过颜色共享进行编码:

对于当地人——看到国会大厦和大教堂作为最高的建筑物令人安心!

第 23 天——3D

第 24 天——黑白

我终于在我的黑白地图中创建了我的第一个山脊图,参考了几个美丽的 Joy-division 专辑封面风格的地图示例。我使用了Colin Carroll 实现的 Python 版本。技术方面,山脊图使用的高程数据

美丽的工具和结果;享受这里意大利的风景:

第 24 天——黑白

第 25 天——南极洲

这是一个棘手的任务——可用的数据非常少。我对其他人的帖子异常好奇!至于我自己,我最终只是可视化了由国家雪冰数据中心提供的 125 米分辨率的整个大陆 SAR 图像。虚假颜色的色调对应于不同的形态特征,正如文档所述:

“𝘛𝘩𝘦 25 𝘮 𝘪𝘮𝘢𝘨𝘦 𝘵𝘪𝘭𝘦𝘴 𝘱𝘳𝘦𝘴𝘦𝘳𝘷𝘦 𝘢 𝘵𝘳𝘶𝘦 𝘲𝘶𝘢𝘯𝘵𝘪𝘵𝘢𝘵𝘪𝘷𝘦 𝘮𝘦𝘢𝘴𝘶𝘳𝘦 𝘰𝘧 𝘣𝘢𝘤𝘬𝘴𝘤𝘢𝘵𝘵𝘦𝘳 𝘸𝘩𝘪𝘤𝘩 𝘮𝘢𝘺 𝘣𝘦 𝘥𝘪𝘳𝘦𝘤𝘵𝘭𝘺 𝘳𝘦𝘭𝘢𝘵𝘦𝘥 𝘵𝘰 𝘴𝘪𝘨𝘮𝘢-𝘯𝘢𝘶𝘨𝘩𝘵. 𝘍𝘰𝘳 𝘵𝘩𝘦 𝘰𝘵𝘩𝘦𝘳 𝘱𝘳𝘰𝘥𝘶𝘤𝘁𝘀, 𝘦𝘢𝘤𝘩 𝘱𝘪𝘹𝘦𝘭’𝘀 𝘪𝘯𝘵𝘦𝘯𝘴𝘪𝘵𝘺 𝘲𝘶𝘢𝘭𝘪𝘵𝘢𝘵𝘪𝘷𝘦𝘭𝘺 𝘳𝘦𝘱𝘳𝘦𝘴𝘦𝘯𝘵𝘴 𝘪𝘵𝘀 𝘳𝘢𝘥𝘢𝘳 𝘣𝘢𝘤𝘬𝘴𝘤𝘢𝘵𝘵𝘦𝘳 𝘪𝘯𝘵𝘦𝘯𝘴𝘪𝘵𝘺, 𝘣𝘶𝘵 𝘢𝘤𝘵𝘶𝘢𝘭 𝘣𝘢𝘤𝘬𝘴𝘤𝘢𝘵𝘵𝘦𝘳 𝘷𝘢𝘭𝘶𝘦𝘴 𝘩𝘢𝘷𝘦 𝘣𝘦𝘦𝘯 𝘢𝘳𝘣𝘪𝘵𝘳𝘢𝘳𝘪𝘭𝘺 𝘢𝘥𝘫𝘶𝘴𝘵𝘦𝘥 𝘵𝘰 𝘪𝘮𝘱𝘳𝘰𝘷𝘦 𝘮𝘰𝘴𝘢𝘪𝘤 𝘪𝘮𝘢𝘨𝘦 𝘲𝘶𝘢𝘭𝘪𝘵𝘺. 𝘝𝘢𝘳𝘪𝘢𝘣𝘭𝘦𝘴 𝘢𝘧𝘧𝘦𝘤𝘵𝘪𝘯𝘨 𝘳𝘢𝘥𝘢𝘳 𝘣𝘢𝘤𝘪𝘯𝘨 𝘪𝘯𝘤𝘭𝘶𝘥𝘦 𝘴𝘶𝘳𝘧𝘢𝘤𝘦 𝘳𝘰𝘶𝘨𝘩𝘯𝘦𝘴𝘴, 𝘵𝘩𝘦 𝘴𝘶𝘳𝘧𝘢𝘤𝘦 𝘮𝘢𝘵𝘦𝘳𝘪𝘢𝘭’𝘀 𝘥𝘪𝘦𝘭𝘦𝘤𝘵𝘳𝘪𝘤 𝘱𝘳𝘰𝘱𝘦𝘳𝘵𝘪𝘦𝘴, 𝘢𝘯𝘥 𝘵𝘩𝘦 𝘨𝘦𝘰𝘮𝘦𝘵𝘳𝘺 𝘣𝘦𝘁𝘸𝘦𝘦𝘯 𝘵𝘩𝘦 𝘴𝘱𝘢𝘤𝘦𝘤𝘳𝘢𝘧𝘁 𝘢𝘯𝘥 𝘵𝘢𝘳𝘨𝘦𝘁. 𝘍𝘰𝘳 𝘮𝘰𝘳𝘦 𝘪𝘯𝘧𝘰𝘳𝘮𝘢𝘵𝘪𝘰𝘯, 𝘴𝘦𝘦 [𝘛𝘦𝘤𝘩𝘯𝘪𝘤𝘢𝘭 𝘙𝘦𝘧𝘦𝘳𝘦𝘯𝘤𝘦 𝘰𝘯 𝘚𝘈𝘙 𝘛𝘩𝘦𝘰𝘳𝘺/𝘐𝘯𝘵𝘦𝘳𝘱𝘳𝘦𝘵𝘪𝘯𝘨 𝘐𝘮𝘢𝘨𝘦𝘴](https://daacdata.apps.nsidc.org/pub/DATASETS/nsidc0103_radarsat_sar/geoTIF_V2/ https://nsidc.org/sites/default/files/nsidc-0103-v002-userguide_0.pdf)。”

第 25 天——南极洲

第 26 天——极简

所以我们选择了极简——而我最喜欢的极简地图是布达佩斯,尤其是从 Budapest Open Data Portal收集的其海拔等高线。地图清晰地展示了多瑙河如何将城市分成两半,右侧的佩斯区是多么的*坦(大约在海*面上 100 米),而布达山丘则从 500 米的高空俯视它!

第 26 天 — 极简

第 27 天 — 点

对于我的点地图,我再次选择了Budapest Open Data Portal,它提供了布达佩斯的一个很好的栅格化人口地图(关于人口栅格数据的更多信息,也可以查看我关于 TDS 的教程)!然后,我将每个网格单元转化为其多边形的 POI,并用一个大小与相应网格单元中的居民数量成比例的标记绘制每个 POI。随后,我随机将每个点涂成红色、蓝色和白色,赋予它这种略显复古的 3D 效果(我也要承认,今年我成了霓虹红蓝色调的最大粉丝,这在某种程度上受到《星球大战》的启发)。

第 27 天 — 点

第 28 天 — 这是图表还是地图?

毫无疑问,这是最奇怪的主题。我应该制作一个地图吗?最后,我决定重新创建我最喜欢的地图衍生可视化之一,这个可视化最初是由Geoff Boeing设计的。这个地图本质上显示了一个特定城市——这里是一系列欧洲城市——的扩展程度。这可以通过测量道路段的总长度来轻松捕捉,这些道路段的方向落在某个范围内(例如,0 到 5 度之间)。然后,将这些转化为极坐标条形图,我们得到了城市道路网络的有趣数字足迹:

第 28 天 — 这是图表还是地图?

第 29 天 — 人口

在这里,我回顾了我之前的文章,𝐄𝐱𝐩𝐥𝐨𝐫𝐢𝐧𝐠 𝐋𝐚𝐫𝐠𝐞-𝐬𝐜𝐚𝐥𝐞 𝐑𝐚𝐬𝐭𝐞𝐫 𝐏𝐨𝐩𝐮𝐥𝐚𝐭𝐢𝐨𝐧 𝐃𝐚𝐭𝐚,发布在 Towards Data Science,在文章中我探讨了两组全球人口数据,这些数据以栅格格式提供,并展示了如何在全球、国家和城市层面进行可视化和处理。

“𝘐 𝘩𝘢𝘷𝘦 𝘰𝘧𝘵𝘦𝘯 𝘴𝘦𝘦𝘯 𝘣𝘦𝘢𝘶𝘵𝘪𝘦𝘶𝘭 𝘱𝘰𝘱𝘶𝘭𝘢𝘵𝘪𝘰𝘯 𝘮𝘢𝘱𝘴 𝘤𝘪𝘳𝘤𝘶𝘭𝘢𝘵𝘪𝘯𝘨 𝘰𝘯𝘭𝘪𝘯𝘦; 𝘩𝘰𝘸𝘦𝘷𝘦𝘳, 𝘐 𝘶𝘴𝘶𝘢𝘭𝘭𝘺 𝘨𝘰𝘵 𝘴𝘵𝘶𝘤𝘬 𝘢𝘵 𝘴𝘰𝘮𝘦 𝘵𝘦𝘤𝘩𝘯𝘪𝘤𝘢𝘭 𝘱𝘢𝘳𝘵𝘴, 𝘭𝘪𝘬𝘦 𝘷𝘪𝘴𝘶𝘢𝘭𝘪𝘇𝘪𝘯𝘨 𝘰𝘵𝘩𝘦𝘳 𝘮𝘢𝘱 𝘴𝘦𝘨𝘮𝘦𝘯𝘵𝘴 𝘵𝘩𝘢𝘯 𝘴𝘩𝘰𝘸𝘯 𝘪𝘯 𝘵𝘩𝘦 𝘵𝘶𝘵𝘰𝘳𝘪𝘢𝘭 𝘰𝘳 𝘵𝘶𝘳𝘯𝘪𝘯𝘨 𝘵𝘩𝘦 𝘭𝘢𝘳𝘨𝘦-𝘴𝘤𝘢𝘭𝘦 𝘳𝘢𝘴𝘵𝘦𝘳 𝘥𝘢𝘁𝘢 𝘪𝘯𝘵𝘰 𝘮𝘰𝘳𝘦 𝘤𝘰𝘮𝘱𝘶𝘵𝘢𝘵𝘪𝘰𝘯-𝘧𝘳𝘪𝘦𝘯𝘥𝘭𝘺 𝘷𝘦𝘤𝘵𝘰𝘳 𝘧𝘰𝘳𝘮𝘢𝘵𝘴. 𝘐 𝘰𝘷𝘦𝘳𝘤𝘰𝘮𝘦 𝘴𝘰𝘮𝘦 𝘰𝘧 𝘵𝘩𝘦𝘴𝘦 𝘴𝘩𝘰𝘳𝘵𝘤𝘰𝘮𝘪𝘯𝘨𝘴 𝘪𝘯 𝘵𝘩𝘪𝘴 𝘢𝘳𝘵𝘪𝘤𝘭𝘦 𝘸𝘪𝘵𝘩 𝘢 𝘩𝘢𝘯𝘥𝘴-𝘰𝘯 𝘨𝘶𝘪𝘥𝘦 𝘵𝘰 𝘵𝘸𝘰 𝘱𝘳𝘪𝘮𝘢𝘳𝘺 𝘨𝘭𝘰𝘣𝘢𝘭 𝘱𝘰𝘱𝘶𝘭𝘢𝘵𝘪𝘰𝘯 𝘥𝘢𝘵𝘢 𝘴𝘰𝘶𝘳𝘤𝘦𝘴.”

还需要注意的是,除了美学价值之外,人口数据显示它们的地图是最基本的信息之一,并且是任何城市发展位置智能任务中可以收集和整合的有价值的信息。它们在规划新设施选址捕捉分析估计城市产品规模描述不同邻里等使用场景中尤为重要。

第 29 天——人口

第 30 天——我最喜欢的

为了结束今年的地图挑战,我决定不挑选我个人的最爱,而是选择人们最喜欢的前 12 个城市,展示它们基于 OpenStreetMap 的路网,使用 OSMNx 包。现在我已经使代码完全可重复使用,最初是在数据可视化协会发布的,文章链接如下,开头是:

“路网是城市的鸟瞰图,放大了的指纹,展示了城市的悠久话题城市规划的基石以及空间数据科学。作为今天城市规划的一个主要目标,即设计宜居的、未来-proof 的城市,通过像15 分钟城市这样的概念,这里我汇总了最宜居城市的顶级列表,并提供了这些城市的路网的可视化概览——以ChatGPT的方式呈现。”

一切都结束了——这就是我上一周做#30DayMapChallenge 的总结。第一年我选择了完整包,学到了很多,玩得很开心,还花了很多时间尝试让地图看起来美观,无论话题是什么!

查看第三周的概览这里!

查看第二周的概览这里!

查看第一周的概览这里!

我在人工智能领域获得博士学位的旅程

原文:towardsdatascience.com/my-journey-to-phd-admission-in-artificial-intelligence-35593adb923f?source=collection_archive---------0-----------------------#2023-06-18

关于在未知领域中导航的个人回忆

Fiona VictoriaTowards Data Science Fiona Victoria

·

关注 发表在 Towards Data Science · 8 分钟阅读 · 2023 年 6 月 18 日

--

照片由 Casey Olsen 提供,来源于 Unsplash

一切就此结束!

在经过 6 个月的紧张大学申请和焦急的倒计时之后,我很高兴地说我将在 2023 年秋季攻读人工智能博士学位。我决定写这篇博客,分享我在这段旅程中学到的宝贵经验和遇到的挑战,这些都让这一切成为可能。

背景

关于我的一点背景,我在印度一所顶级私立学校完成了机器人与自动化学士学位。这所学校在全国排名第 63 位,在该州前 10(大约 500 所大学中)。我选修了机器学习和人工智能课程,这激发了我对这一领域的兴趣。虽然在那里度过的时光塑造了我的学术成功,但我常常疑惑为什么当时没有考虑研究的道路。我遇到了一些幸运的人,他们在本科期间发现了对研究的热情,并积极与教授合作,参与同行评审期刊或会议论文的撰写。显然,早期发表可以成为游戏规则的改变者,我也见证了它如何积极塑造了该领域其他人的档案。

在本科毕业后,我在一家初创公司担任机器学习工程师(MLE)。虽然我的时间相对较短,但我获得了使用流行深度学习框架——TensorFlow 的宝贵经验。我参与了一个集中于神经网络量化的项目,涵盖了训练、评估和推断阶段。通过这个过程,我学会了如何构建神经网络架构,修改图层,并从头开始创建内核,使我对 TensorFlow 的底层源代码有了全面的了解。参与这样的项目(即使是个人项目)将培养深度学习优化技术的坚实基础,增强作为研究员的思维能力。它还将提供不仅是预训练算法的最终用户的信心,还能设计符合研究目标的定制或特定神经网络模型。

尽管有些人跳过了在行业中工作的这一过程(这完全没问题!),但我个人看到了合作并获得实践经验的价值,这让我在继续攻读研究生学业时具备了优势。这个过程教会了我如何有效地与团队成员合作,如何将项目从构思推进到完成,以及如何无畏地深入探究,在过程中制造和打破事物。总的来说,我作为机器学习工程师的短暂经历是一次非常充实的旅程,为我下一阶段的旅程奠定了基础。

无论你在旅程中处于何处,不要担心错失的机会。考虑一下你在接下来的日子里能控制的事物,并把精力投入到下一个最重要的事情上。

作为下一步,我进入了一个硕士项目,旨在深入探索人工智能。在此期间,我享受了创建一个作品集网站、LinkedIn 个人资料和定制简历以展示我的能力的额外挑战。基本上,建立一个专业品牌。展望未来两年,并不断增加对未来目标有意义的内容。但这还不是全部!这段时间还通过各种演讲锻炼了我的沟通技能,有效地让我传达我的想法。与来自不同背景的专业人士合作,帮助我建立了一个能够持续一生的强大网络。我还有机会作为课程的一部分参与令人兴奋的实际项目,这帮助我建立了研究作品集,并深深地培养了对研究的热情。

我相信,对研究的热情源于对研究潜力的好奇心,这种潜力能够推动特定领域的边界,同时对社区产生积极影响。此外,你还会有机会与跨大学的研究人员合作——这教会了你准备研究实验室会议、批判性思维和科学写作的过程。它还为你提供了参加技术会议的机会,在那里你可以与来自全球不同领域的专家互动。这与探索感兴趣领域的自由相结合,就是我所说的理想“研究者的世界”——一个好奇心、韧性、合作和影响力交织的世界。我的研究经验属于负责任的人工智能(Responsible AI)这一大范畴,我希望成为隐私保护的人工智能/机器学习系统的专家。每天 intellectually 刺激我的,是阅读现有文献、制定研究问题以及设计实验以检验我的假设的过程。

在硕士课程的第二年,我有三个选择:课程学习、项目基础和论文基础。从一开始,我就决定选择论文,这确保了通向博士学位的道路始终对我开放。论文是完成硕士学位的最具挑战性的路径,与博士论文非常相似,但规模小得多。我认为自己很幸运,找到了一个与我兴趣紧密相关的论文指导老师。在生成性人工智能(Generative AI)在行业内掀起热潮之前,我就对基于计算机视觉的生成模型产生了兴趣。经过数月的相关文献阅读、多个精心设计实验的迭代和与导师的一对一会议,我成功完成了题为“Phoenix — 一个联邦生成扩散模型”的论文。

下面是我在这个过程中学到的一些关键经验:(1)通过跟踪你感兴趣领域的预印本和同行评审的研究工作,保持相关性;(2)主动识别机会并解决人工智能领域的动态变化中的空白;(3)准备进行严格的实验,以彻底测试你的方法。对于所有打算攻读博士学位的硕士生来说,我的建议是:享受学习的过程,并记住抓住每一个超越课程的机会。

博士申请过程

一旦我决定攻读人工智能领域的博士学位,我意识到还有很多工作要做——完全不知道从哪里开始申请过程。作为家中第一个踏入这个未知领域的人,我通过博客和视频进行了大量自学,每个博客和视频都详细描述了他们申请博士的个人经历。我理解和遵循的核心要点如下,

  • 项目 — 更深入地研究你想攻读的项目(计算机科学、人工智能或电子工程),然后缩小你的研究兴趣范围。这可以是你已经熟悉的领域,或者你可以选择进入一个新的、令人兴奋的领域,进一步探索你热衷的方向。

  • 大学 — 选择大学没有明确的指导方针,但通常人们会参考一些大学排名网站(如 csranking、drafty.cs、usnews),并申请梦校、目标校和保底校的组合。然而,我建议不要完全依赖排名列表来作为决策的唯一标准。研究设施、跨学科合作和资金机会更为重要。记住,作为博士生,你的生活在很大程度上依赖于研究质量和导师的指导,这比大学本身更为重要。

  • 潜在导师 — 这是可能是最重要的考虑因素。阅读你潜在导师的出版物,看看是否有特别吸引你兴趣的内容。建立一个强大且支持性的关系对你接下来 4 年以上的生活至关重要——在这一过程中分配足够的时间来彻底决定。个人而言,我在每所大学选择了两位与我的研究兴趣一致的导师。我还记下了关键点(例如,我对他们的哪些工作印象深刻、我为何适合、我们的共同目标和兴趣等),这些我可以在个人沟通和申请论文中提及。

  • 个人陈述—— 这是展示你的写作技巧和突出你研究成就的最佳机会。请不要以“自从我出生以来,我就想做研究”等通用短语开头。要现实地自豪地描述你的研究背景,包括它的起源时间和地点。你还应详细说明你最显著的研究成果、社区参与和志愿活动。此外,添加一些关键点,说明为什么你会选择与某位导师合作,以及这将如何强化你的研究生涯。这篇文章应该清晰地表达你为什么值得在他们的大学/研究实验室追求博士学位。我见过一些样本文章还突出个人挑战及其克服方式,但这完全取决于你是否愿意在个人陈述中包含这些内容。虽然添加所有这些技术细节很重要,但不要忘记注入最能代表你的个人风格。毕竟,这不是一份报告!

  • 推荐信—— 你需要至少三位教授的强有力推荐信。然而,你也可以请求两位教授和一位在行业内知名人士的推荐信。虽然信件内容是保密的,但你可以请求与你有良好工作关系的个人撰写信件。这样,这些信件更有可能传达出对你性格和独立研究能力的积极评价。

我的检查清单

在整个入学申请过程中,我保持了一个电子表格来保持组织性。这个表格可以跟踪我阅读的博客、任何突发的想法以及所有我打算申请的大学的全面列表。这里列出了一些表格字段,但可以根据需要自定义,因为没有一个模板适合所有人。

** University
* Department/Program
* Links
* Personal Ranking
* Deadline
* Tests GRE/TOEFL
* Application Fee
* Potential Advisors
* Recommendation Letters
* Extra Notes*

大多数入学申请截止日期从 12 月初到 1 月中旬。为了让这一过程更可控、更少压力,我决定每天申请一所大学。申请提交后,进入了等待阶段。这个阶段开始时充满了查看 gradcafe、discord 群组的诱惑,这让集中精力于日常活动变得更加困难。我建议不要像我一样陷入这个循环,因为结果会在 2 月后逐渐公布。在此期间,你还可能会开始收到来自教授的邮件,表达他们对进一步对话(如匹配面试)的兴趣,无论是面对面还是虚拟的。

反思和教训

  • 尽早开始——研究、研究、再研究!不仅是你的发表作品,还有潜在的大学、导师以及工作领域。

  • 建立你的个人资料 — 收集所有本科期间的成果,并考虑建立一个作品集网站以突出你的成就。

  • 建立网络 — 利用 Linkedin 的力量,与那些经历过类似道路的其他人建立联系。他们通常会提供来自个人经验的见解。

好事需要时间,而伟大的事物则需要更长的时间。

希望这对那些想开始申请博士学位的朋友有所帮助。虽然这听起来可能很艰难和具有挑战性,但这个过程是值得的。不要被未选择这条道路的同龄人或身边的人所影响。选择这条少有人走的路,过程会更加激动人心,需要极大的热情和努力!祝好运,我们在另一端见 😃

我最欣赏的 AI 科学家:李飞飞博士、安妮玛·安南库玛博士、达芙妮·科勒博士

作品集Linkedin我作为艺术家

我在#30DayMapChallange 的第二周

原文:towardsdatascience.com/my-second-week-of-the-30daymapchallange-11fbab10cae6

我对#30DayMapChallange 第二周的个人看法,这是一个旨在每年 11 月每天设计主题地图的社交挑战。

米兰·贾诺索夫Towards Data Science 米兰·贾诺索夫

·发表于Towards Data Science ·阅读时间 5 分钟·2023 年 11 月 14 日

--

自 2019 年以来,每年 11 月,地理信息系统(GIS)和空间分析社区都会变得非常忙碌——这要归功于一个有趣的挑战,称为#30DayMapChallange。每年,这个挑战都有一个主题日程,提出一个主题作为当天地图可视化的主要方向。虽然预定义的每日主题确实意味着对创造力的限制,但它们也帮助参与者找到共同的兴趣,分享数据源,并以视觉和技术的方式表达个人风格。

在这里,我想简要概述我在此挑战的第二周,详细说明并展示我创建的不同地图——通常是用 Python 制作的。

在这篇文章中,所有图片均由作者创建。

第 8 天 — 非洲

为了启动第二周,我利用了联合国粮农组织发布的[非洲河流](https://lnkd.in/dZ6rZeGG https://lnkd.in/dUZFPdBN)数据集。它们的 GIS 文件包含* 20 万条线性多边形,属于河流,并具有几个参数,如其主要流域或每段弯曲的 Strahler 流序。我用这些字母来设置每条河流的颜色和宽度;等级越高,河流越暗且越细,从一阶溪流到 8 阶主干河流。

第 8 天 — 非洲

第 9 天 — 六边形

这个图有点复杂,但基于 Uber 的H3 六边形。首先,我从IUCN 红色名录受威胁物种的地理空间数据库收集了所有(大约 5000 种)哺乳动物物种栖息地的多边形格式数据。然后,我通过逐对比较计算每个物种栖息地的空间重叠。由于多边形的复杂性,如果使用简单的 GeoPandas 覆盖,这将耗费大量时间,因此我将每个栖息地分割成六边形,并仅通过共享的六边形 ID 数量捕捉栖息地的重叠。这样,我成功地将网络节点定义为物种,并将它们的网络连接定义为它们的六边形 ID 集合的 Jaccard 相似度指数(它们共享栖息地的相对比例),然后我可以使用 Gephi 可视化它们。

最后,网络节点的大小基于它们的度中心性 — 即它们与之共享栖息地的其他物种的数量。此外,每个节点的颜色 — 依然是物种 — 基于其最大栖息地国家的主要颜色,为这个网络图赋予了最终的世界地图效果。

第 9 天 — 六边形

第 10 天 — 北美

这张地图展示了基于自然地球数据集的北美道路网络,直观地展示了 49,183 段道路,并清晰地显示了大陆的形态和人口密度的变化,北部道路稀疏,而在人口较多的地区则呈现整齐的网格结构。

第 10 天 — 北美

第 11 天 — 复古

在这里,主题是复古的 — 所以我回顾了不久前由 Towards Data Science 发布的一张怀旧地图,展示了布达佩斯和维也纳公开可用除颤器设备的可达性。每个交点的颜色表示可达时间,从绿色到红色,显示从跑步速度到达最*设备所需的时间,范围从 0 分钟(绿色)到 10 分钟(红色),用红色区域捕捉了最关键的区域。

第 11 天 — 复古

第 12 天 — 南美

第 12 天,我使用 OpenStreetMap(OSMNx)和维基百科数据构建了我的地图。特别是,我收集了所谓的经济复杂性指数,这是一个宏观经济指标,捕捉一个国家的生产能力。利用网络理论,这个指标源自国家的国际贸易档案以及进出口模式,衡量国家的经济稳定性和潜力。作为一种以简化方式量化整个国家的测量方法,还允许比较和排名国家,它有着长长的支持者和批评者名单,但这留待另日讨论。至于地图,像往常一样,我在 Python 中构建了它,使用了 OSMNx、Matplotlib、urllib,并且为了给最终效果增添黑暗模式的底图,使用了 contextily。

第 12 天 — 南美洲

第 13 天 — 色斑图

在这张地图和相关的文章中,我提出并解决了一个流行且关键的城市发展问题——绿色*等和绿色区域的可达性。我在这篇文章中使用维也纳的政府开放数据来探索这些概念,通过计算维也纳不同社区的人均绿地面积,并将其与当地的财政状况相关联。原文发布在Towards Data Science

第 13 天 — 色斑图

第 14 天 — 欧洲

这张欧洲地图显示了每个国家的每*方公里人口密度,基于 WorldPop 发布的 100x100 米建模网格数据。正如我在原始文章中所写,我展示了如何创建这样的地图的教程,我经常看到网上流传着漂亮的人口地图;然而,我通常在一些技术细节上卡住,比如如何可视化教程中未显示的地图段,或将大规模栅格数据转换为更易于计算的矢量格式。在这篇文章中,我通过实用指南克服了这些不足,介绍了两个主要的全球人口数据来源。阅读全文教程这里。

第 14 天 — 欧洲

这是我进行#30DayMapChallenge 的第二周总结——还有两周时间,所以准备好迎接更多地图吧!现在,在我的Patreon频道上,还有视频和代码教程。

我的 #30DayMapChallange 第三周

原文:towardsdatascience.com/my-third-week-of-the-30daymapchallange-b42efbe38b51

我对 #30DayMapChallange 第三周的个人看法,这是一项旨在每年 11 月每天设计主题地图的社交挑战。

Milan JanosovTowards Data Science Milan Janosov

·发布于 Towards Data Science ·阅读时间 6 分钟·2023 年 11 月 21 日

--

自 2019 年以来,地理信息系统(GIS)和空间分析社区每年 11 月都非常繁忙——这要归功于一个有趣的挑战,称为 #30DayMapChallange。每年,这个挑战都有一个主题日程,提出一个主题作为当天地图可视化的主要方向。虽然预设的每日主题无疑对创造力构成了限制,但它们也帮助参与者找到共同兴趣,分享数据源,并在视觉和技术上表达个人风格。

在这里,我想简要回顾一下我在这个挑战的第三周,详细介绍并展示我创建的不同地图——通常使用 Python 和各种空间分析及地理空间数据工具。

在这篇文章中,所有图片均由作者创建。

第 15 天 — OSM

OpenStreetMap 是获取地图数据的首选来源之一。虽然有许多库在其基础上进行构建,但我去年最喜欢的发现是 PrettyMaps,我用它来可视化 PlanetWare 精心挑选的一些全球顶级旅游景点,例如巴黎的埃菲尔铁塔。其余的收藏可以在 这里 查看,其中包括罗马的斗兽场(Colosseum)、纽约的自由女神像(Statue of Liberty)、法国的凡尔赛宫(Château de Versailles)、纽约的中央公园(Central Park)、中国的紫禁城(Forbidden City)、捷克共和国的布拉格城堡(Prague Castle)以及悉尼歌剧院(Sydney Opera House)。

第 15 天 — OSM

第 16 天 — 大洋洲

为了可视化澳大利亚及大洋洲众多岛屿,我使用了来自Natural Earth的 10 米分辨率物理矢量数据。也就是说,我在这里想象了澳大利亚和大洋洲的海岸线。对于这个线图,我使用了我以前的一种技巧——创建这些看起来像‘光剑’、发光、受《星球大战》启发的线条——这些线条完全使用 Python 中的 Matplotlib 创建。此外,找到合适的边界框有点棘手,所以我只是参考了一些在线地图。

第 16 天 — 大洋洲

第 17 天 — 流动

当我试图搞清楚这张地图时,主题是“流动”,我有点卡住了——然后我想,好吧,来点卡路里,继续前进。

tl;dr

我决定可视化巧克力(在协调系统中其标准代码为 HS 1806)的出口流网络,使用来自Comtrade的国际贸易数据。这个数据集包含了哪些国家向哪些国家出口了什么数量、什么价值的信息。我利用这些信息创建了一个非地理但具有拓扑结构的地图,一个网络可视化。在这个网络中,每个国家是一个节点,而国家 A 与国家 B 相连,如果 A 向 B 出口巧克力产品,其中链接的大小与 2022 年交易商品的总值成比例。节点颜色对应网络社区——那些看起来比其他国家内部交易更多的国家簇,而节点大小则衡量了每个国家通过出口这些甜品所获得的总价值。未出口的国家标记为深灰色。有趣的是:我是在布达佩斯的#Flow Specialty Coffee Bar & Bistro 喝咖啡时创建了这张地图的。

第 17 天 — 流动

第 18 天 — 大气层

在这个可视化中,我创建了显示全球月度最高气温值的实际热图,使用了World Climate data

“这是 WorldClim 2.1 版的 1970–2000 年气候数据。该版本于 2020 年 1 月发布。包括了最低、*均和最高气温、降水量、太阳辐射、风速、水汽压和总降水量的月度气候数据。此外,还有 19 个‘生物气候’变量。”

因此,在我的地图中,每一帧对应一个月,使用了 WorldClim 2.1 栅格数据集的最高气温。

第 18 天 — 大气层

第 19 天 — 5 分钟地图

没错——我们只有 5 分钟时间来制作一个地图可视化。时间不多,所以我决定使用我经常做的事情——将网络数据和地理空间数据结合起来,展示另一个道路网络,这次是关于曼哈顿及其美丽的、完全人工的方格状道路系统。与其重新运行以前的代码,我从头开始写了这个笔记本,仅用大约 5 分钟,几乎没有查找资料(除了找到正确的十六进制代码)。

第 19 天——5 分钟地图

第 20 天——户外

为了在户外冒险——同时创建地图——我决定在线查看美丽的地标,让我感觉就像身临其境——当我坐在 Python 终端前时。因此,在我的户外地图中,我结合了可视化气候变化和宏大的ESA 的 Sentinel 数据的新更新,制作了这段乌普萨拉冰川,阿根廷的动画。这段影像基于查询了 2016 年到今天每半年最少云层的真实色彩图像。由于文件大小问题,这里提供了一个静态快照,您可以在这里找到动画。

第 20 天——户外

第 21 天——栅格

在前一天的卫星影像之后,我回到了栅格遥感数据。具体来说,我重新发布了我以前的一个作品,其中使用了旧的 Sentinel API 来计算苏黎世市的 NDVI 指数,展示了卫星图像波段的一个基本但非常实用的转换。根据#Wikipedia上的理论:

“𝘕𝘰𝘳𝘮𝘢𝘭𝘪𝘻𝘦𝘥 𝘥𝘪𝘧𝘧𝘦𝘳𝘦𝘯𝘤𝘦 𝘷𝘦𝘨𝘦𝘵𝘢𝘵𝘪𝘰𝘯 𝘪𝘯𝘥𝘦𝘹:𝘛𝘩𝘦 𝘯𝘰𝘳𝘮𝘢𝘭𝘪𝘻𝘦𝘥 𝘥𝘪𝘧𝘧𝘦𝘳𝘦𝘯𝘤𝘦 𝘷𝘦𝘨𝘦𝘵𝘢𝘵𝘪𝘰𝘯 𝘪𝘯𝘥𝘦𝘹 𝘪𝘴 𝘢 𝘴𝘪𝘮𝘱𝘭𝘦 𝘨𝘳𝘢𝘱𝘩𝘪𝘤𝘢𝘭 𝘪𝘯𝘥𝘪𝘤𝘢𝘵𝘰𝘳 𝘵𝘩𝘢𝘵 𝘤𝘢𝘯 𝘣𝘦 𝘶𝘴𝘦𝘥 𝘵𝘰 𝘢𝘯𝘢𝘭𝘺𝘻𝘦 𝘳𝘦𝘮𝘰𝘵𝘦 𝘴𝘦𝘯𝘴𝘪𝘯𝘨 𝘮𝘦𝘢𝘴𝘶𝘳𝘦𝘮𝘦𝘯𝘵𝘴, 𝘰𝘧𝘵𝘦𝘯 𝘧𝘳𝘰𝘮 𝘢 𝘴𝘱𝘢𝘤𝘦 𝘱𝘭𝘢𝘵𝘧𝘰𝘳𝘮, 𝘢𝘴𝘴𝘦𝘴𝘴𝘪𝘯𝘨 𝘸𝘩𝘦𝘵𝘩𝘦𝘳 𝘰𝘳 𝘯𝘰𝘁 𝘵𝘩𝘦 𝘵𝘢𝘳𝘨𝘦𝘵 𝘣𝘦𝘪𝘯𝘨 𝘰𝘵𝘩𝘦𝘳 𝘵𝘩𝘢𝘯 𝘢𝘯𝘺 𝘵𝘢𝘳𝘨𝘦𝘵 𝘣𝘦𝘪𝘯𝘨 𝘰𝘣𝘴𝘦𝘳𝘷𝘦𝘥 𝘤𝘰𝘯𝘵𝘢𝘪𝘯𝘴 𝘭𝘪𝘷𝘦 𝘨𝘳𝘦𝘦𝘯 𝘷𝘦𝘨𝘦𝘵𝘢𝘵𝘪𝘰𝘯。”

实际上,NDVI 是一个+1.0 到-1.0 的浮动值变量,我们可以通过结合卫星图像的红色和*红外波段来计算。由于绿色表面的光吸收特性,这样的结果会使得绿色的像素值更高。

由于我使用了免费的#sentinel卫星图像,我的像素大小为 10x10 米。在可视化中,我使用了绿色-黄色-红色的颜色图来展示从+1.0 到 0 的 NDVI 值。负值通常表示水体,接*零的正值是建筑区域的良好参考,而值在 0.1 到 0.5 之间表示稀疏,以上则表示密集的绿色植被。

第 21 天——栅格

这是我第三周参与#30DayMapChallenge 的总结——还有 10 天就结束了,所以准备好迎接更多地图吧!

第二周的概况请点击这里查看!

第一周的概况请点击这里查看!

我的(非常)个人数据仓库

原文:towardsdatascience.com/my-very-personal-data-warehouse-fitbit-activity-analysis-with-duckdb-8d1193046133?source=collection_archive---------3-----------------------#2023-05-31

使用 DuckDB 分析 Fitbit 活动数据

Simon AuburyTowards Data Science Simon Aubury

·

关注 发表在 Towards Data Science · 13 分钟阅读 · 2023 年 5 月 31 日

--

图片由Jake Hills提供,发布在Unsplash

可穿戴健身追踪器已成为我们生活中不可或缺的一部分,收集并跟踪我们日常活动、睡眠模式、位置、心率等数据。我已经使用 Fitbit 设备 6 年来监测我的健康。然而,我一直觉得数据分析功能不足 — 尤其是在我想跟踪长期健身目标的进展时。我的个人健身活动数据存档中埋藏了哪些见解?要开始探索,我需要一种有效的方法来对成千上万的记录不完善的 JSON 和 CSV 文件进行数据分析……额外加分的是分析过程中不需要将我的数据从笔记本电脑上移走。

进入 DuckDB — 一个轻量级、免费但强大的分析数据库,旨在简化数据分析工作流 — 它在本地运行。在这篇博客中,我想使用 DuckDB 来探索我的 Fitbit 数据,并分享使用 Seaborn 数据可视化的各种数据格式分析方法以及绘制我的健康和健身目标的途径。

导出 Fitbit 数据存档

首先,我需要获取我所有的历史健身数据。通过遵循 导出您的账户存档 的说明,Fitbit 使得导出您账户生命周期中的 Fitbit 数据变得相当简单。

使用导出 Fitbit 数据存档的说明 — 作者截图。

您需要确认您的请求……并保持耐心。我的存档创建了超过三天 — 但我最终收到了含有下载 ZIP 文件说明的电子邮件。该文件应包含由我的 Fitbit 或相关服务记录的所有个人健身活动。解压存档后会显示出大量的文件 — 例如,我在解压 79MB 文件后总共有 7,921 个文件。

数以千计的嵌套文件中的一小部分 — 作者截图。

让我们开始查看存档中可用的数据种类。

为什么选择 DuckDB?

有许多优秀的博客 (1,2,3) 描述了 DuckDB — TL;DR 摘要是 DuckDB 是一个开源的内存 OLAP 数据库,专为分析查询而构建。它本地运行,支持广泛的 SQL,并能直接在 Pandas 数据、Parquet、JSON 数据上运行查询。额外加分的是它与 Python 和 R 的无缝集成。它的极速处理能力和大部分内存处理使其成为构建个人数据仓库的好选择。

Fitbit 活动数据

我查看的第一个文件集合是活动数据。物理活动和广泛的锻炼信息似乎存储在编号的文件中,例如 Physical Activity/exercise-1700.json

我无法弄清楚文件编号实际意味着什么,我猜这些编号只是用于一组锻炼文件的递增整数。在我的数据导出中,最早的文件从 0 开始,经过 6 年的时间到达文件编号 1700。里面是一个记录数组,每个记录都有一个活动的描述。记录似乎会根据活动的不同而变化——这是一个“步行”的示例。

"activityName" : "Walk",
  "averageHeartRate" : 79,
  "calories" : 122,
  "duration" : 1280000,
  "steps" : 1548,
  "startTime" : "01/06/23 01:08:57",
  "elevationGain" : 67.056,
  "hasGps" : false,
  : : : :
  "activityLevel" : [
    { "minutes" : 1, "name" : "sedentary"},
    { "minutes" : 2, "name" : "lightly"},
    { "minutes" : 6, "name" : "fairly"},
    { "minutes" : 6, "name" : "very"
  }]

这些物理活动数据是我笔记本电脑上 7,921 个文件中的一个。幸运的是,DuckDB 可以使用 read_json 函数从 JSON 文件中读取(并自动检测模式),让我可以通过一个 SQL 语句将所有锻炼文件加载到 physical_activity 表中。值得注意的是,我需要指定日期格式掩码,因为 Fitbit 导出的日期格式非常 美国风格 😕。

CREATE OR REPLACE TABLE physical_activity
as
SELECT 
  startTime + INTERVAL 11 hours as activityTime
, activityName
, activityLevel
, averageHeartRate
, calories
, duration / 60000 as duration_minutes
, steps
, distance
, distanceUnit
, tcxLink
, source
FROM read_json('./Physical Activity/exercise-*.json'
, format='array'
, timestampformat='%m/%d/%y %H:%M:%S');

这个 SQL 命令从磁盘读取物理活动数据,将活动和持续时间转换并加载到内存中的 DuckDB 表中。

将物理活动数据加载到数据框中

我想了解我每个月的时间花费情况。由于活动数据存储在非常细粒度的级别上,我使用了 DuckDB SQL time_bucket 函数将 activityTime 时间戳截断为每月的桶。将分组的物理活动数据加载到数据框中可以通过这个汇总 SQL 完成,查询结果可以通过 << 运算符导入 Pandas 数据框中。

activity_df <<
  select time_bucket(interval '1 month', activityTime) as activity_day
  , activityName
  , sum(duration_minutes) as duration
  from physical_activity
  where activityTime between '2022-09-01' and '2023-05-01'
  group by 1, 2
  order by 1;

这个 SQL 查询将我的活动数据(骑车、步行、跑步等)分组到每月的桶中,让我可以真实地反映我投入到物理活动中的时间。

绘制每月活动分钟数

我现在想要通过视觉方式探索我的活动数据——所以我们来获取 Fitbit 数据并制作一些统计图形。我将使用 Python 的 Seaborn 数据可视化库直接从 activity_df 数据框中创建一个每月活动分钟数的条形图。

import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.dates import DateFormatter
plt.figure(figsize=(15, 6))
plt.xticks(rotation=45)

myplot =sns.barplot(data=activity_df, x="activity_day", y="duration", hue="activityName")
myplot.set(xlabel='Month of', ylabel='Duration (min)', title='Monthly Activity Minutes')
plt.legend(loc="upper right", title='Activity') 
plt.show()

执行此操作会生成这个条形图。

锻炼活动细分——作者截图。

看起来我的主要活动仍然是步行,而且我在 2023 年的新年决心是更频繁地跑步,但实际上并没有发生(还?)。

睡眠

关于 三分之一的成年人睡眠不足,所以我想探索我的长期睡眠模式。在我的 Fitbit 档案中,睡眠数据似乎被记录在以日期命名的文件中,例如Sleep/sleep-2022-12-28.json。每个文件包含一个月的数据,但混淆的是,文件的日期为事件发生前的月份。例如,文件sleep-2022-12-28.json似乎包含了 2023 年 1 月 2 日至 2023 年 1 月 27 日的数据。不管怎样 — 文件命名的奇怪之处暂且不提,我们可以探讨文件的内容。在记录中有一个扩展的“levels”块,详细描述了睡眠类型(清醒、浅睡、快速眼动、深睡)。

"logId" : 39958970367,
  "startTime" : "2023-01-26T22:47:30.000",
  "duration" : 26040000,
  :: :: ::
  "levels": 
    "summary" : {
      {
      "light": { "count": 30, "minutes": 275},
      "rem": { "count": 4, "minutes": 48 },
      "wake": { "count" : 29, "minutes" : 42 },
      "deep" : { "count" : 12, "minutes" : 75}
      }
    }

如果查看一些较旧的文件(可能是用我以前的 Fitbit Surge 设备创建的),会发现睡眠类型的分类有所不同(躁动、不清醒、睡眠)。

"logId" : 18841054316,
  "startTime" : "2018-07-12T22:42:00.000",
  "duration" : 25440000,
  :: :: ::
  "levels" : {
    "summary" : {
      "restless" : {"count" : 9, "minutes" : 20 },
      "awake" : { "count" : 2, "minutes" : 5 },
      "asleep" : { "count" : 0,   "minutes" : 399}
    }
  }

无论数据模式如何,我们都可以使用 DuckDB JSON 读取器将记录读入一个表格中。

CREATE OR REPLACE TABLE sleep_log
as
select dateOfSleep 
, levels
from read_json('./Sleep/sleep*.json'
, columns={dateOfSleep: 'DATE', levels: 'JSON'}
, format='array') ;

睡眠数据的模式变化

我想处理我所有的睡眠数据,并处理记录睡眠的模式变化(很可能是因为我更换了 Fitbit 设备的型号)。一些记录的时间标记在$.awake上,这与$.wake类似(但不完全相同)。

我使用了 SQL 中的 coalesce 函数 — 它返回第一个计算结果为非 NULL 值的表达式,以结合类似类型的睡眠阶段。

sleep_log_df <<
  select dateOfSleep
  , cast(coalesce(json_extract(levels, '$.summary.awake.minutes'), json_extract(levels, '$.summary.wake.minutes')) as int) as min_wake
  , cast(coalesce(json_extract(levels, '$.summary.deep.minutes'), json_extract(levels, '$.summary.asleep.minutes')) as int) as min_deep
  , cast(coalesce(json_extract(levels, '$.summary.light.minutes'), json_extract(levels, '$.summary.restless.minutes')) as int) as min_light
  , cast(coalesce(json_extract(levels, '$.summary.rem.minutes'), 0) as int) as min_rem
  from sleep_log
  where dateOfSleep between '2023-04-01' and '2023-04-30'
  order by 1;

使用 DuckDB,我可以通过 json_extract 提取嵌套 JSON 中的时长阶段,以生成一个 sleep_log_df 数据框,将所有历史睡眠阶段进行分组。

绘制睡眠活动图

我们可以将每日睡眠日志制作成堆叠条形图,显示每晚清醒、浅睡、深睡和 REM 睡眠的分类。

import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.dates as mdates

#create stacked bar chart
fig, axes = plt.subplots(figsize=(15,6))
myplot = sleep_log_df.set_index('dateOfSleep').plot(ax=axes, kind='bar', stacked=True, color=['chocolate', 'palegreen', 'green', 'darkblue'])
myplot.set(xlabel='Date', ylabel='Duration (min)', title='Sleep')
axes.xaxis.set_major_locator(mdates.DayLocator(interval=7))
plt.legend(loc="upper right", labels = ['Awake', 'Deep', 'Light', 'REM']) 
plt.xticks(rotation=45)
plt.show()

加载一个月的睡眠数据让我能够进行更广泛的睡眠时长分析。

每晚的睡眠周期时长 — 作者截图。

将多晚的睡眠数据绘制在一个图表上,使我能够开始理解星期几和周期性事件如何影响我的睡眠时长和质量。

心率

心率数据被非常频繁地捕捉(每10–15 秒一次),存储在名为Physical Activity/heart_rate-2023-01-26.json的每日文件中。这些文件非常大 — 每天约有 70,000 行 — 所有数据都包装在一个数组中。

[{{"dateTime": "01/25/25 13:00:07", "value": {"bpm": 54, "confidence": 2}},
  {"dateTime": "01/25/25 13:00:22", "value": {"bpm": 54, "confidence": 2}},
  {"dateTime": "01/25/25 13:00:37", "value": {"bpm": 55, "confidence": 2}},
  : : : : : :
  {"dateTime": "01/26/26 12:59:57", "value": {"bpm": 55, "confidence": 3}
}]

我的理论是文件名表示用户的时区。例如,在我的时区(GMT+11),命名为heart_rate-2023-01-26.json的数据覆盖了 26 日 00:00(AEST)至 23:59(AEST) - 如果文件中的日期为 GMT,则逻辑上是合理的。

转换 JSON 文件

到目前为止,我已经成功处理了包含 DuckDB 函数的 Fitbit 数据。然而,在处理这些巨大的心率文件时,我遇到了问题。当尝试处理 JSON 文件中的大数组记录时,DuckDB 给出了这个错误。

(duckdb.InvalidInputException) “INTERNAL Error: Unexpected yyjson tag in ValTypeToString”

我认为这个错误信息是一个突如其来的提醒,告诉我期望一个 JSON 数组有这么多元素是不合理的。解决办法是预处理文件,使其不是 JSON 记录数组,而是转换为换行符分隔的 JSON 或 ndjson

{"dateTime": "01/25/23 13:00:07", "value": {"bpm": 54, "confidence": 2}
{"dateTime": "01/25/23 13:00:22", "value": {"bpm": 54, "confidence": 2}
{"dateTime": "01/25/23 13:00:37", "value": {"bpm": 55, "confidence": 2}
  : : : : : :
{"dateTime": "01/26/23 12:59:57", "value": {"bpm": 55, "confidence": 3}

为了将心率数组记录转换为换行符分隔的 JSON,我使用了一点小巧的 Python 代码来转换每个文件。

import glob
import json
import ndjson
import re

for json_src_file in sorted(glob.glob('./Physical Activity/steps-*.json')):
  json_dst_file = re.sub('\.[a-z]*$', '.ndjson', json_src_file)
  print(f'{json_src_file} -->  {json_dst_file}')
  with open(json_src_file) as f_json_src_file:
    json_dict =json.load(f_json_src_file) 
    with open(json_dst_file, 'w') as outfile:
      ndjson.dump(json_dict, outfile)

这将查找每个 .json 文件,读取内容并将其转换为换行符分隔的 JSON,并用 .ndjson 文件扩展名创建新文件。这将一个包含 70,000 条记录的数组转换为一个包含 70,000 行的文件——每条 JSON 记录现在存储在新的一行上。

将心率数据加载到表中

使用新转换的 ndjson 文件,我现在准备将心率数据加载到 DuckDB 表中。注意使用 timestampformat='%m/%d/%y %H:%M:%S'); 来描述日期中的前导月份(例如 "01/25/23 13:00:07")

CREATE OR REPLACE TABLE heart_rate
as
SELECT dateTime + INTERVAL 11 hours as hr_date_time
, cast(value->'$.bpm' as integer) as bpm
FROM read_json('./Physical Activity/*.ndjson'
, columns={dateTime: 'TIMESTAMP', value: 'JSON'}
, format='newline_delimited'
, timestampformat='%m/%d/%y %H:%M:%S');

我们可以通过将格式设置为 ’newline_delimited’ 来加载所有 .ndjson 文件。注意我们可以通过 JSON 提取来提取 BPM(每分钟心跳次数)并将其转换为整数。

DuckDB 在处理 JSON 时非常快速 — 作者截图。

值得在这里强调 DuckDB 的速度是多么惊人——加载 1200 万条记录仅用了 2.8 秒!

将心率加载到数据框中

在加载了 1200 万条心率测量记录后,我们将 5 月 21 日的一天数据加载到数据框中。

hr_df << 
  SELECT time_bucket(interval '1 minutes', hr_date_time) as created_day
  ,  min(bpm) as bpm_min
  ,  avg(bpm) as bpm_avg
  ,  max(bpm) as bpm_max
  FROM heart_rate
  where hr_date_time between '2023-05-21 00:00' and '2023-05-21 23:59'
  group by 1;

这个 DuckDB 查询将心率的变异性聚合到 1 分钟的时间块中;在每个周期内进行最小值、*均值和最大值的分类。

绘制心率图

我可以使用这样的图来绘制心率(顺便炫耀一下,我确实在早上 6 点去跑步了)

import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter
plt.figure(figsize=(15, 6))
plt.xticks(rotation=45)

myplot = sns.lineplot(data=hr_df, x="created_day", y="bpm_min")
myplot = sns.lineplot(data=hr_df, x="created_day", y="bpm_avg")
myplot = sns.lineplot(data=hr_df, x="created_day", y="bpm_max")
myFmt = DateFormatter("%H:%M")
myplot.xaxis.set_major_formatter(myFmt)
myplot.set(xlabel='Time of day', ylabel='Heart BPM', title='Heart rate')
plt.show()

一天中的心率 — 作者截图。

以细粒度探索心率使我能够跟踪我的健身目标——特别是如果我坚持我的常规跑步计划。

步骤

步骤记录在名为 Physical Activity/steps-2023-02-26.json 的每日文件中。这似乎是对一天中每个时间段块(每 5 到 10 分钟)内的步骤的详细计数。

[{
  "dateTime" : "02/25/23 13:17:00",
  "value" : "0"
},{
  "dateTime" : "02/25/23 13:52:00",
  "value" : "5"
},{
  "dateTime" : "02/25/23 14:00:00",
  "value" : "0"
},{
:: :: ::
},{
  "dateTime" : "03/24/23 08:45:00",
  "value" : "15"
}]

为了将步骤聚合为每日统计,我需要将 GMT 转换为我的本地时区(GMT+11)。

steps_df <<
select cast(time_bucket(interval '1 day', dateTime + INTERVAL 11 hours	) as DATE) as activity_day
, sum(value) as steps
from read_json('./Physical Activity/steps-2023-04-27.ndjson'
, auto_detect=True
, format='newline_delimited'
, timestampformat='%m/%d/%y %H:%M:%S') 
group by 1;

将每日步骤聚合到 steps_df 数据框中,使我能够探索长期的活动趋势,因为我努力超越 10,000 步以实现健康益处的提升

绘制每日步骤

现在我们可以将数据框绘制为每日步数

import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter
plt.figure(figsize=(15, 6))
plt.xticks(rotation=45)

myplot = sns.barplot(data=steps_df, x="activity_day", y="steps")
myplot.set(xlabel='Day', ylabel='Steps', title='Daily steps')
plt.show()

每日步数统计——作者截图。

这显示我还需要努力达到我的每日步数目标——这对我的新年健身决心来说又是一次打击。

GPS 映射

Fitbit 将 GPS 记录的活动存储为TCX (训练中心 XML) 文件。这些 XML 文件在下载的 ZIP 文件中没有,但我们在身体活动文件中有其位置的参考,我可以像这样查询。

select tcxLink 
from physical_activity
where tcxLink is not null;

tcxLink 字段是对身体活动文件中位置的 URL 参考。

每个 TCX 文件的 URL——作者截图。

我们可以直接在浏览器中使用这个 URL(登录 Fitbit 网站后)来下载 GPS XML 文件。查看 TCX 文件内部,我们会发现每隔几秒钟就有低级别的 GPS 位置数据。

TCX GPS XML 文件样本内容——作者截图。

好消息是数据中有一些明显的字段,如纬度、经度和时间。不太好的消息是数据是 XML 格式的,因此我们需要在加载到 DuckDB 之前预处理这些文件,因为目前 XML 格式不被文件读取器支持。我们可以通过另一段 Python 代码将 XML 文件转换为 JSON 文件,循环遍历每个 .tcx 文件。

这里有一些复杂的 XML 嵌套,位置数据位于 TrainingCenterDatabase/Activities/Activity/Lap 下。

import glob
import json
import ndjson
import xmltodict
import re

for xml_src_file in sorted(glob.glob('MyFitbitData/tcx/*.tcx')):
    json_dst_file = re.sub('\.[a-z]*$', '.ndjson', xml_src_file)
    print(f'{xml_src_file} -->  {json_dst_file}')
    with open(xml_src_file) as f_xml_src_file:
        # erase file if it exists
        open(json_dst_file, 'w') 
        data_dict = xmltodict.parse(f_xml_src_file.read())
        # Loop over the "laps" in the file; roughly every 1km
        for lap in data_dict['TrainingCenterDatabase']['Activities']['Activity']['Lap']:
            data_dict_inner = lap['Track']['Trackpoint']
            # append file
            with open(json_dst_file, 'a') as outfile:
                ndjson.dump(data_dict_inner, outfile)
                outfile.write('\n')

加载 GPS 地理空间数据

我们可以这样加载地理空间数据

route_df <<
SELECT time
, position
, cast(json_extract_string(position, '$.LatitudeDegrees') as float) as latitude
, cast(json_extract_string(position, '$.LongitudeDegrees') as float) as longitude
FROM read_json('MyFitbitData/tcx/54939192717.ndjson'
, columns={Time: 'TIMESTAMP', Position: 'JSON', AltitudeMeters: 'FLOAT', DistanceMeters: 'FLOAT', HeartRateBpm: 'JSON'}
, format='newline_delimited'
, timestampformat='%Y-%m-%dT%H:%M:%S.%f%z');

这个 DuckDB 查询扁*化了 JSON,将纬度、经度和时间转换为正确的数据类型,并加载到 route_df 数据框中。

使用 Folium 可视化 GPS 路线

拥有一个位置信息的表格并不够直观,因此我想开始在交互式地图上绘制我的跑步路线。我使用了这篇博客来帮助使用 Folium 可视化路线。修改代码帮助我绘制了自己的跑步路线,例如这是我在堪培拉度假时的一次跑步路线图。

import folium

route_map = folium.Map(
    location=[-35.275, 149.129],
    zoom_start=13,
    tiles='openstreetmap',
    width=1024,
    height=600
)

coordinates = [tuple(x) for x in route_df[['latitude', 'longitude']].to_numpy()]
folium.PolyLine(coordinates, weight=8, color='red').add_to(route_map)
display(route_map)

跑步的 Folium 地图图示——作者截图。

这生成了一个使用开放街图 瓦片的跑步图,给我提供了一个很好的交互式详细跑步地图。

数据目标和健身目标总结

我是否更接*分析我的 Fitbit 设备数据的目标——绝对是的!DuckDB 证明是一个理想的灵活轻量级分析工具,能够快速处理我的大量混乱的 Fitbit 数据。通过广泛的 SQL 支持和灵活的文件解析选项,DuckDB 能够在几秒钟内处理数百万条记录,将数据本地导入数据框,这使得 DuckDB 成为构建个人数据仓库的理想选择。

至于我的健身目标——我还有一些工作要做。我觉得我现在应该离开这个博客,因为我今天的步数目标还差一点。

Code

🛠️用于 Fitbit 活动分析的代码——github.com/saubury/duckdb-fitbit

朴素贝叶斯分类

原文:towardsdatascience.com/naive-bayes-classification-41d1fe802a1e

朴素贝叶斯分类器家族的深入解释,包括一个 Python 中的文本分类示例

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

·发表于 Towards Data Science ·阅读时间 23 分钟·2023 年 6 月 1 日

--

图片由 Mediocre Studio 提供,来源于 Unsplash

朴素贝叶斯分类器是一类基于应用贝叶斯定理并假设特征之间独立性的概率分类器。

这些分类器在训练和预测中都非常快速,并且具有很高的可扩展性和可解释性。尽管它们的假设过于简化,但它们在复杂的实际问题上通常表现良好,尤其是在如垃圾邮件过滤和情感分析等文本分类任务中,其朴素假设通常成立。

朴素贝叶斯也是最早的生成模型之一(早于 ChatGPT……),它学习每个类别中的输入分布。这些模型不仅可用于预测,还可以用于生成新样本(有关生成模型与判别模型的深入讨论,请参见 这篇文章)。

在这篇文章中,我们将深入探讨朴素贝叶斯模型及其变体,然后展示如何使用 Scikit-Learn 中的实现来解决文档分类任务。

背景:贝叶斯定理

贝叶斯定理(或贝叶斯规则)是概率中的一个重要定理,它允许我们基于与事件相关的先验知识计算事件的条件概率。

从数学上讲,定理指出,对于任何事件 AB

贝叶斯规则

  • P(A|B)是后验概率,即给定B的情况下事件A发生的概率。

  • P(B|A)是似然性,即给定A的情况下事件B发生的概率。

  • P(A)是先验概率,即A的概率,没有任何先验条件。

  • P(B)是边际概率,即B的概率,没有任何先验条件。

贝叶斯定理特别适用于从结果推断原因,因为通常更容易辨别在原因存在或不存在的情况下结果的概率,而不是相反。例如,估计一个患有脑膜炎的患者会出现头痛的概率比估计一个头痛的患者是否患有脑膜炎要容易得多(因为许多其他疾病也可能导致头痛)。在这种情况下,我们可以应用贝叶斯规则如下:

示例

已知大约 25%的肺癌患者会出现胸痛。假设肺癌的发生率为每 10 万人中 50 人,全球胸痛的发生率为每 10 万人中 1,500 人。那么,一个有胸痛的患者患肺癌的概率是多少?

让我们将给定的输入以概率的形式表示。设L为患肺癌的事件,C为胸痛的事件。根据我们拥有的数据,我们知道:

  • P(C|L) = 0.25

  • P(L) = 50 / 100,000 = 0.0005

  • P(C) = 1,500 / 100,000 = 0.015

使用贝叶斯规则,给定胸痛的情况下患肺癌的后验概率是:

即,患者患肺癌的概率只有 0.833%。

朴素贝叶斯模型

朴素贝叶斯模型是概率分类器,即它们不仅为给定样本分配一个类别标签,还提供该样本属于该类别的概率估计。例如,朴素贝叶斯模型可以预测某个电子邮件有 80%的概率是垃圾邮件,20%的概率是正常邮件。

回顾一下在监督学习问题中,我们会得到一个包含n个标记样本的训练集:D = {(x₁, y₁), (x₂, y₂), … , (xₙ, yₙ)},其中x是一个m维向量,包含样本i特征,而yᵢ表示该样本的标签。在分类问题中,标签可以取任何一个K类别,即,y ∈ {1, …, K}。

我们区分两种类型的分类器:

  1. 确定性分类器为每个样本输出一个硬标签,而不提供类别的概率估计。这类分类器的例子包括 K-最*邻决策树 和 SVM。

  2. 概率分类器输出对 k 类的概率估计,然后根据这些概率给样本分配标签(通常是概率最高的类别的标签)。这种分类器的例子包括朴素贝叶斯分类器,逻辑回归和使用逻辑/softmax 输出层的神经网络

给定一个样本 (x, y),朴素贝叶斯分类器使用贝叶斯规则计算它属于类别 k 的概率(即 y = k):

方程右侧的概率是从训练集估计得到的。

首先,类别先验概率 P(y = k) 可以通过类别 k 在训练样本中的相对频率来估计:

其中 nₖ 是属于类别 k 的样本数量,n 是训练集中的样本总数。

其次,边际概率 P(x) 可以通过对贝叶斯规则中所有类别的分子部分进行求和来计算:

由于边际概率不依赖于类别,如果我们只对分配硬标签给样本感兴趣(而不提供概率估计),则不需要计算它。

最后,我们需要估计在给定类别下特征的可能性,即 P(x|y = k)。估计这些概率的主要问题是数量太多,可能在训练集中没有足够的数据来估计所有的概率。

例如,假设 xm 个二进制特征组成,例如,每个特征表示某个词是否出现在文本中。在这种情况下,为了建模 P(x|y),我们需要从训练集中为每个类别估计 2 个条件概率(每个 x₁, …, xₘ 的所有可能组合),因此总共有 2ᵐK 个概率。在大多数情况下,我们在训练集中没有足够的样本来估计所有这些概率,即使有,也需要耗费指数时间。

朴素贝叶斯假设

为了减少需要估计的参数数量,朴素贝叶斯模型做出了以下假设:特征在给定类别变量的情况下是相互独立的

这个假设允许我们将概率 P(x|y = k) 写作每个个体特征在给定类别下的条件概率的乘积:

朴素贝叶斯假设

例如,在垃圾邮件过滤任务中,朴素贝叶斯假设意味着诸如“rich”和“prince”这样的词独立地对预测邮件是否是垃圾邮件作出贡献,无论这些词之间是否存在任何可能的关联。

朴素假设在应用领域如文本分类和推荐系统中大致成立,在这些领域中,特征通常彼此独立。

朴素贝叶斯假设显著减少了需要从数据中估计的参数数量。例如,在输入x包含m个二元特征的情况下,它将模型的参数数量从 2减少到每个类别m

MAP(最大后验)

基于朴素贝叶斯假设,我们现在可以将类别后验概率写作如下:

如果我们只关心将类别标签分配给给定样本(而不关心概率),我们可以忽略分母 P(x),并使用以下分类规则:

这称为MAP(最大后验)决策规则,因为它选择最大化后验概率的假设。

只要正确的类别被预测为比其他类别更可能,朴素贝叶斯将做出正确的 MAP 决策,即使概率估计不准确。这为模型提供了一些对基础朴素独立假设缺陷的鲁棒性。

请注意,如果我们假设所有先验 P(y) 是等可能的(例如,当我们没有关于哪个假设更可能的先验信息时),那么 MAP 决策规则等同于 MLE(最大似然估计)决策规则,它选择最大化给定模型 P(x|y) 的数据似然性的模型。(你可以在这篇文章中了解更多关于最大似然的信息。)

参数估计

我们现在剩下的任务是估计每个特征 j 和每个类别 k 的条件概率 P(xⱼ|y = k)。这个估计依赖于特征的类型(例如,离散或连续)以及我们假设其具有的概率分布。

对特征分布的假设称为事件模型。每个事件模型导致不同类型的朴素贝叶斯分类器。在接下来的部分,我们将讨论不同的事件模型以及如何在每种模型中估计模型参数。

伯努利朴素贝叶斯

在伯努利事件模型中,特征xⱼ被建模为具有伯努利分布的独立二元变量,即每个特征xⱼ在给定样本中出现的概率为pⱼ,不出现的概率为 1 − pⱼ

例如,在文本分类任务中,每个特征xⱼ可能代表文本中词汇表中第j个单词的出现或缺失。

在伯努利事件模型中,概率P(xⱼ|y = k)是通过特征j在类别k的样本中出现的频率来估计的:

其中nⱼₖ是类别k中包含特征xⱼ的样本数量,nₖ是类别k中样本的总数。

分类朴素贝叶斯

分类事件模型是伯努利事件模型对V个类别(而不是仅两个类别)的扩展。在这个模型中,我们假设每个特征是一个类别(离散)变量,可以取V个可能的类别中的一个,其概率为pᵢ,其中所有概率的总和为 1。

在这个事件模型中,我们需要估计每个特征xⱼ和每个类别v的概率P(xⱼ = v|y = k)。与之前的模型类似,我们通过特征j在类别k的样本中取值v的频率来估计这个概率:

其中nⱼᵥₖ是类别k中特征xⱼ取值v的样本数量,nₖ是类别k中样本的总数。

示例:顾客购买预测

假设我们有一个包含商店顾客过去购买数据的表格:

训练集

表中的每一行包含顾客的年龄、是否是学生、收入水*、信用评级以及是否购买了产品。

一个具有以下属性的新顾客到达商店:

<Age = Young, Student = Yes, Income = Low, Credit = Excellent>

你需要预测这个顾客是否会购买产品。

我们首先通过计算 Buys = Yes(10 行中的 6 行)和 Buys = No(10 行中的 4 行)的行数来计算类别先验概率。

然后,我们计算每个类别中特征的可能性:

因此,类别后验概率为:

α是归一化因子(α = 1 / P(x))。

由于P(Buys = Yes|x) > P(Buys = No|x),我们的预测是顾客会购买产品。

如果我们想得到顾客购买产品的实际概率,我们可以首先使用两个后验概率之和为 1 的事实来找到归一化因子:

然后,我们可以将其代入“Buy = Yes”的后验概率中:

客户购买该产品的概率是 54.21%。

多项式朴素贝叶斯

在多项式事件模型中,我们假设数据集只有一个类别特征x,它可以取m个类别中的一个,每个特征向量(x₁, …, xₘ)是一个直方图,其中xⱼ计算了x在特定实例中取值j的次数。

这个事件模型在处理文本文件时特别有用,其中m是词汇表中的词数,每个特征xⱼ表示词汇表中第j个词在文档中出现的次数。这种表示方法称为词袋模型:

词袋模型

在这个事件模型中,我们估计概率P(x = v|y = k)为特征x在类别k的样本中取得值v的频率:

其中nᵥₖ是类别kx取值v的样本数量,nₖ是类别k中样本的总数量。

示例:垃圾邮件过滤器

我们希望基于一个包含 100 封邮件的训练集构建一个垃圾邮件过滤器:其中 80 封为正常邮件,20 封为垃圾邮件。每种类型邮件中的词语计数在以下表格中给出:

正常邮件和垃圾邮件中的词语计数

一封新的邮件到达,内容为“rich friend need money”。这是正常邮件还是垃圾邮件?

首先计算类别的先验概率:

接下来,我们估计每种类型邮件中词语的可能性。正常邮件中的词语总数为:80 + 5 + 5 + 20 + 50 + 40 = 200,而垃圾邮件中的词语总数为:20 + 20 + 30 + 25 + 25 = 120。因此,词语的可能性为:

因此,类别的后验概率是:

因此,我们的预测是该邮件是垃圾邮件。

拉普拉斯*滑

如果某个特征和某个类别在训练集中从未一起出现,那么它的可能性估计将为零。由于特征的可能性是相乘的,这将抹去我们从其他特征中获得的所有信息。

例如,由于垃圾邮件中缺少“movie”这个词,如果我们收到一封包含“movie”的邮件,它将被自动认为是正常邮件,即使邮件中的其他词语非常“垃圾”。

作为一个具体的例子,考虑一封内容为“movie rich rich prince”的邮件。这封邮件将被分类为正常邮件,尽管“rich”和“prince”这两个词与垃圾邮件高度相关,因为垃圾邮件的后验概率为零:

为了处理这个问题,我们在所有概率估计中加入一个小的样本修正(称为伪计数),以确保概率不会被设定为零。

在多项式朴素贝叶斯中,修正如下应用:

其中α是*滑参数,n是训练集中的样本总数。设置α = 1 称为拉普拉斯*滑(最常见的),而α < 1 称为利德斯通*滑。

同样,在分类朴素贝叶斯中,修正如下应用:

其中nⱼ是特征j的可能类别数。

重新审视我们的垃圾邮件过滤器示例,让我们通过为所有词汇添加伪计数 1 来应用拉普拉斯*滑:

对词频应用拉普拉斯*滑

这次,包含文本“movie rich rich prince”的电子邮件将被分类为垃圾邮件,因为:

高斯朴素贝叶斯

直到现在我们假设所有特征都是离散的。朴素贝叶斯模型如何处理连续特征?

处理连续特征的主要方法有三种:

  1. 将特征值离散化,并获得伯努利或分类分布特征。

  2. 假设特征按照某个已知概率分布(通常是正态分布)分布,并从训练集中估计该分布的参数(例如,正态分布的均值和方差)。

  3. 使用KDE(核密度估计)来估计特征的概率密度函数,利用给定的样本点作为核。

在高斯朴素贝叶斯中,我们采用第二种方法,假设特征的似然性是高斯分布的:

其中μⱼₖ是类别k中所有样本的xⱼ值的均值,σⱼₖ是这些值的标准差(这些是分布真实参数的最大似然估计)。

上述事件模型也可以组合使用,以处理异质数据集,即包含不同类型特征的数据集(例如,既有分类特征也有连续特征)。

Scikit-Learn 中的朴素贝叶斯分类器

模块 sklearn.naive_bayes 提供了上述提到的四种朴素贝叶斯分类器的实现:

  1. BernoulliNB实现了伯努利朴素贝叶斯模型。

  2. CategoricalNB实现了分类朴素贝叶斯模型。

  3. MultinomialNB 实现了多项式朴素贝叶斯模型。

  4. GaussianNB 实现了高斯朴素贝叶斯模型。

前三种类别接受一个名为alpha的参数,该参数定义了*滑参数(默认为 1.0)。

文档分类示例

在接下来的演示中,我们将使用MultinomialNB来解决文档分类任务。我们将使用的数据集是20 newsgroups dataset,该数据集包含 18,846 篇新闻组帖子,几乎均匀地划分为 20 个不同主题。这个数据集在机器学习中的文本应用研究中被广泛使用,包括文档分类和聚类。

加载数据集

你可以使用函数fetch_20newsgroups()在 Scikit-Learn 中下载带标签的文本文档。你可以选择将所有文档作为一个组下载,或者分别下载训练集和测试集(使用subset参数)。训练集和测试集的划分基于在特定日期之前或之后发布的消息。

默认情况下,文本文档包含一些元数据,如标题(例如,帖子的日期)、页脚(签名)和其他帖子的引用。由于这些特征与文本分类任务无关,我们将通过使用remove参数将它们剥离掉:

from sklearn.datasets import fetch_20newsgroups

train_set = fetch_20newsgroups(subset='train', remove=('headers', 'footers', 'quotes'))
test_set = fetch_20newsgroups(subset='test', remove=('headers', 'footers', 'quotes'))

请注意,第一次调用此函数时,可能需要几分钟时间下载所有文档,之后它们将被缓存到本地文件夹 ~/scikit_learn_data* 中。

函数的输出是一个包含以下属性的字典:

  • data — 文档集合

  • target — 目标标签

  • target_names — 文档类别名称

让我们将文档及其标签存储到适当的变量中:

X_train, y_train = train_set.data, train_set.target
X_test, y_test = test_set.data, test_set.target

数据探索

让我们对数据进行一些基本的探索。我们在训练集和测试集中拥有的文档数量是:

print('Documents in training set:', len(X_train))
print('Documents in test set:', len(X_test))
Documents in training set: 11314
Documents in test set: 7532

简单计算显示,60%的文档属于训练集,40%属于测试集。

让我们打印类别列表:

categories = train_set.target_names
categories
['alt.atheism',
 'comp.graphics',
 'comp.os.ms-windows.misc',
 'comp.sys.ibm.pc.hardware',
 'comp.sys.mac.hardware',
 'comp.windows.x',
 'misc.forsale',
 'rec.autos',
 'rec.motorcycles',
 'rec.sport.baseball',
 'rec.sport.hockey',
 'sci.crypt',
 'sci.electronics',
 'sci.med',
 'sci.space',
 'soc.religion.christian',
 'talk.politics.guns',
 'talk.politics.mideast',
 'talk.politics.misc',
 'talk.religion.misc']

很明显,某些类别彼此紧密相关(例如,comp.sys.mac.hardwarecomp.sys.ibm.pc.hardware),而其他类别则高度不相关(例如,sci.electronicssoc.religion.christian)。

最后,让我们查看训练集中其中一份文档(例如,第一个):

print(X_train[0])
I was wondering if anyone out there could enlighten me on this car I saw
the other day. It was a 2-door sports car, looked to be from the late 60s/
early 70s. It was called a Bricklin. The doors were really small. In addition,
the front bumper was separate from the rest of the body. This is 
all I know. If anyone can tellme a model name, engine specs, years
of production, where this car is made, history, or whatever info you
have on this funky looking car, please e-mail.

不出所料,该文档的标签是:

categories[y_train[0]]
'rec.autos'

将文本转换为向量

为了将文本文档输入到机器学习模型中,我们首先需要将它们转换为数值向量(即向量化文本)。这个过程通常涉及文本的预处理和清理,然后选择合适的数值表示来表示文本中的词。

文本预处理包括多个步骤,其中最常见的包括:

  1. 清理和标准化文本。这包括去除标点符号和特殊字符,并将文本转换为小写。

  2. 文本分词,即将文本拆分成单个词或术语。

  3. 停用词的去除。停用词是特定语言中常用的词。例如,英语中的停用词包括“the”、“a”、“is”、“and”。这些词通常被过滤掉,因为它们不携带有用的信息。

  4. 词干提取或词形还原。词干提取通过去除或替换词缀将词还原为其词汇根,而词形还原将词还原为其规范形式(词元),并考虑词的上下文(词性)。例如,词computers的词元是computer,但其词汇根是comput

以下示例演示了这些步骤在给定句子上的应用:

文本预处理示例

清理文本后,我们需要选择如何将其向量化为数值向量。最常见的方法有:

  1. 词袋(BOW)模型。在这个模型中,每个文档通过词频向量表示(类似于我们在垃圾邮件过滤器示例中使用的)。

  2. TF-IDF(词频与逆文档频率的乘积)通过乘以两个指标来衡量一个词对文档的相关性:

    (a) TF(词频)——单词在文档中出现的次数。

    (b) IDF(逆文档频率)——词在整个语料库中出现的频率的倒数。

    其思想是减少在语料库中频繁出现的词的权重,同时增加稀有词的权重(从而更能指示文档的类别)。

  3. 词嵌入。在这种方法中,词被映射到实值向量中,以便具有相似意义的词在向量空间中具有接*的表示。这个模型通常用于深度学习,将在未来的帖子中讨论。

Scikit-Learn 提供了以下两种变换器,支持文本预处理和向量化:

  1. CountVectorizer使用词袋模型。

  2. TfIdfVectorizer使用 TF-IDF 表示。

这些变换器的重要超参数包括:

  • lowercase — 是否在标记化之前将所有字符转换为小写(默认为 True)。

  • token_pattern — 用于定义什么是令牌的正则表达式(默认正则表达式选择两个或更多字母数字字符的令牌)。

  • stop_words — 如果为‘english’,则使用内置的英语停用词列表。如果为 None(默认值),则不会使用停用词。你也可以提供自己定制的停用词列表。

  • max_features — 如果不为 None,则构建一个仅包含在训练语料库中具有最高术语频率的前max_features个词汇的词汇表。否则,将使用所有特征(这是默认值)。

请注意,这些变换器不提供诸如词干提取或词形还原之类的高级预处理技术。要应用这些技术,你将需要使用其他库,如NLTK(自然语言工具包)或spaCy

由于朴素贝叶斯模型在 TF-IDF 表示上表现更好,我们将使用 TfidfVectorizer 将训练集中的文档转换为 TF-IDF 向量:

from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(stop_words='english')
X_train_vec = vectorizer.fit_transform(X_train)

提取的 TF-IDF 向量的形状是:

print(X_train_vec.shape)
(11314, 101322)

也就是说,语料库的词汇表中有 101,322 个独特的令牌。我们可以通过调用向量化器的get_feature_names_out()方法来检查这些令牌:

vocab = vectorizer.get_feature_names_out()
print(vocab[50000:50010]) # pick a subset of the tokens
['innacurate' 'innappropriate' 'innards' 'innate' 'innately' 'inneficient'
 'inner' 'innermost' 'innertubes' 'innervation']

显然,在 90 年代没有自动拼写检查器 😃

TF-IDF 向量非常稀疏,*均有 67 个非零组件,超过 100,000 个:

print(X_train_vec.nnz / X_train_vec.shape[0])
66.802987449178

让我们也将测试集中的文档向量化(请注意,在测试集上我们调用transform方法而不是fit_transform):

X_test_vec = vectorizer.transform(X_test)

构建模型

现在,让我们构建一个多项式朴素贝叶斯分类器,并将其拟合到训练集上:

from sklearn.naive_bayes import MultinomialNB

clf = MultinomialNB(alpha=0.01)
clf.fit(X_train_vec, y_train)

请注意,我们需要将*滑参数α设置为一个非常小的数字,因为 TF-IDF 值被缩放到 0 和 1 之间,因此默认的α = 1 会导致值的剧烈变化。

评估模型

接下来,让我们在训练集和测试集上评估模型。

模型在训练集上的准确率和 F1 得分为:

from sklearn.metrics import f1_score

accuracy_train = clf.score(X_train_vec, y_train)
y_train_pred = clf.predict(X_train_vec)
f1_train = f1_score(y_train, y_train_pred, average='macro')

print(f'Accuracy (train): {accuracy_train:.4f}')
print(f'F1 score (train): {f1_train:.4f}')
Accuracy (train): 0.9595
F1 score (train): 0.9622

测试集上的准确率和 F1 得分为:

accuracy_test = clf.score(X_test_vec, y_test)
y_test_pred = clf.predict(X_test_vec)
f1_test = f1_score(y_test, y_test_pred, average='macro')

print(f'Accuracy (test): {accuracy_test:.4f}')
print(f'F1 score (test): {f1_test:.4f}')
Accuracy (test): 0.7010
F1 score (test): 0.6844

与训练集相比,测试集上的得分相对较低。为了调查错误来源,让我们绘制测试文档的混淆矩阵:

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

cm = confusion_matrix(y_test, y_test_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=clf.classes_)
fig, ax = plt.subplots(figsize=(10, 8))
disp.plot(ax=ax, cmap='Blues')

测试集上的混淆矩阵

如我们所见,大多数混淆发生在高度相关的主题之间,例如:

  • 主题 0(alt.atheism)和主题 15(soc.religion.christian)之间有 74 个混淆

  • 主题 18(talk.politics.misc)和主题 16(talk.politics.guns)之间有 92 个混淆

  • 主题 19(talk.religion.misc)和主题 15(soc.religion.christian)之间有 89 个混淆

根据这些发现,朴素贝叶斯分类器表现得相当不错。让我们看看它与其他标准分类算法的比较。

基准测试

我们将把朴素贝叶斯模型与另外四个分类器进行基准测试:逻辑回归KNN随机森林AdaBoost

首先编写一个函数,该函数获取一组分类器,并在给定的数据集上评估它们,同时测量它们的训练时间:

import time

def benchmark(classifiers, names, X_train, y_train, X_test, y_test, verbose=True):
    evaluations = []

    for clf, name in zip(classifiers, names):
        evaluation = {}
        evaluation['classifier'] = name

        start_time = time.time()
        clf.fit(X_train, y_train)
        evaluation['training_time'] = time.time() - start_time

        evaluation['accuracy'] = clf.score(X_test, y_test)
        y_test_pred = clf.predict(X_test)
        evaluation['f1_score'] = f1_score(y_test, y_test_pred, average='macro')

        if verbose:
            print(evaluation)
        evaluations.append(evaluation)
    return evaluations

我们现在将使用这五个分类器调用这个函数:

from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier

classifiers = [clf, LogisticRegression(), KNeighborsClassifier(), RandomForestClassifier(), AdaBoostClassifier()]
names = ['Multinomial NB', 'Logistic Regression', 'KNN', 'Random Forest', 'AdaBoost']

evaluations = benchmark(classifiers, names, X_train_vec, y_train, X_test_vec, y_test)

我们得到的输出是:

{'classifier': 'Multinomial NB', 'training_time': 0.06482672691345215, 'accuracy': 0.7010090281465746, 'f1_score': 0.6844389919212164}
{'classifier': 'Logistic Regression', 'training_time': 39.38498568534851, 'accuracy': 0.6909187466808284, 'f1_score': 0.6778246092753284}
{'classifier': 'KNN', 'training_time': 0.003989696502685547, 'accuracy': 0.08218268720127456, 'f1_score': 0.07567337211476842}
{'classifier': 'Random Forest', 'training_time': 43.847145318984985, 'accuracy': 0.6233404142326076, 'f1_score': 0.6062667217793061}
{'classifier': 'AdaBoost', 'training_time': 6.09197473526001, 'accuracy': 0.36563993627190655, 'f1_score': 0.40123307742451064}

让我们绘制分类器的准确率和 F1 分数:

df = pd.DataFrame(evaluations).set_index('classifier')

df['accuracy'].plot.barh()
plt.xlabel('Accuracy (test)')
plt.ylabel('Classifier')

测试集上的准确率

df['f1_score'].plot.barh(color='purple')
plt.xlabel('F1 score (test)')

测试集上的 F1 分数

多项式 NB 在准确率和 F1 分数上都表现最佳。注意,分类器使用的是默认参数,未经过任何调整。为了更公*的比较,应在微调其超参数后比较算法。此外,一些算法如 KNN 会遭遇维度诅咒,需要进行降维才能使其有效工作。

让我们还绘制分类器的训练时间:

df['training_time'].plot.barh(color='green')
plt.xlabel('Training time (sec)')
plt.ylabel('Classifier')

不同分类器的训练时间

多项式 NB 的训练速度如此之快,以至于我们在图中看不到它的时间!通过查看上面的函数输出,我们可以看到其训练时间仅为 0.064 秒。请注意,KNN 的训练速度也非常快(因为实际上没有构建模型),但其预测时间(未显示)非常慢。

总之,多项式 NB 在所有检查的标准中显示出优越性。

查找最具信息量的特征

朴素贝叶斯模型还允许我们获取每个类别的最具信息量的特征,即具有最高可能性的特征 P(xⱼ|y)。

MultinomialNB 类有一个名为 feature_log_prob_ 的属性,它提供了每个类别的特征的对数概率,矩阵形状为 (n_classes, n_features)。

使用这个属性,编写一个函数来找到每个类别中 10 个最有信息量的特征(词项):

def show_top_n_features(clf, vectorizer, categories, n=10):
    feature_names = vectorizer.get_feature_names_out()

    for i, category in enumerate(categories):       
        top_n = np.argsort(clf.feature_log_prob_[i])[-n:]
        print(f"{category}: {' '.join(feature_names[top_n])}")
show_top_n_features(clf, vectorizer, categories) 

我们得到的输出是:

alt.atheism: islam atheists say just religion atheism think don people god
comp.graphics: looking format 3d know program file files thanks image graphics
comp.os.ms-windows.misc: card problem thanks driver drivers use files dos file windows
comp.sys.ibm.pc.hardware: monitor disk thanks pc ide controller bus card scsi drive
comp.sys.mac.hardware: know monitor does quadra simms thanks problem drive apple mac
comp.windows.x: using windows x11r5 use application thanks widget server motif window
misc.forsale: asking email sell price condition new shipping offer 00 sale
rec.autos: don ford new good dealer just engine like cars car
rec.motorcycles: don just helmet riding like motorcycle ride bikes dod bike
rec.sport.baseball: braves players pitching hit runs games game baseball team year
rec.sport.hockey: league year nhl games season players play hockey team game
sci.crypt: people use escrow nsa keys government chip clipper encryption key
sci.electronics: don thanks voltage used know does like circuit power use
sci.med: skepticism cadre dsl banks chastity n3jxp pitt gordon geb msg
sci.space: just lunar earth shuttle like moon launch orbit nasa space
soc.religion.christian: believe faith christian christ bible people christians church jesus god
talk.politics.guns: just law firearms government fbi don weapons people guns gun
talk.politics.mideast: said arabs arab turkish people armenians armenian jews israeli israel
talk.politics.misc: know state clinton president just think tax don government people
talk.religion.misc: think don koresh objective christians bible people christian jesus god

大多数词似乎与其对应的类别有很强的相关性。然而,也有一些像“just”和“does”这样的通用词不提供有价值的信息。这表明我们的模型可能通过更好的停用词列表来改进。实际上,Scikit-Learn 建议不要使用其默认列表,并引用其文档:“‘english’存在一些已知问题,你应考虑其他替代方案。” 😲

总结

让我们总结一下朴素贝叶斯与其他分类模型的优缺点:

优点

  • 训练和预测速度极快

  • 提供类别概率估计

  • 可用于二分类和多分类问题

  • 需要少量的训练数据来估计其参数

  • 高度可解释

  • 高度可扩展(参数数量与特征数量线性相关)

  • 在高维数据上表现良好

  • 对噪声具有鲁棒性(噪声样本在估计条件概率时被*均处理)

  • 可以处理缺失值(计算特征的似然时忽略缺失值)

  • 没有超参数需要调整(除了*滑参数,通常不做更改)

缺点:

  • 依赖于朴素贝叶斯假设,而该假设在许多实际领域并不成立

  • 特征之间的相关性可能会降低模型的性能

  • 通常被更复杂的模型超越

  • 零频率问题:如果一个分类特征在训练集中未出现过,其类别将被模型赋予零概率。*滑处理可以缓解这一问题,但不能完全解决。

  • 无法处理连续属性,除非进行离散化或对其分布做出假设

  • 仅能用于分类任务

最终说明

这是我在 Medium 上写的最长的一篇文章。希望你阅读时的感受至少与我写作时一样愉快。如果有任何不清楚的地方,请在评论中告知我。

你可以在我的 GitHub 上找到这篇文章的代码示例: github.com/roiyeho/medium/tree/main/naive_bayes

除非另有说明,否则所有图片均由作者提供。

20 个新闻组数据集的信息:

  • 引用: Mitchell, Tom (1999). Twenty Newsgroups. UCI 机器学习库。 doi.org/10.24432/C5C323.

  • 许可证: Creative Commons CC BY 4.0。

从头开始的朴素贝叶斯分类器,使用 Python

原文:towardsdatascience.com/naive-bayes-classifier-from-scratch-with-python-942708211470

从理论到实践,运用贝叶斯定理

Piero PaialungaTowards Data Science Piero Paialunga

·发表于Towards Data Science ·阅读时间 10 分钟·2023 年 1 月 4 日

--

Joel Abraham拍摄,图片来源于Unsplash

数学和物理充满了定理、方程、原理、公理和推论。当我开始学习物理时,我记得我达到了所有课程都具有相同结构的阶段:

A. 定义基本假设

B. 使用数学构建下一个“砖块

C. 一块块地叠加,直到所有部分汇聚成一个优雅、美丽的世界模型

让我们从我学习过的第一门物理课程开始:微积分

1. 你从集合数字的基本假设开始。你开始定义自然数、整数、实数和复数。

2. 从这里开始,你定义的函数不过是从空间 A(假设是 N 维实数空间)到空间 B(假设是 1 维实数空间)的映射

3. 然后你开始研究函数。于是你开始分析它们的最小值、最大值和鞍点。你偶然(哦!)了解了“导数”的概念。

4. 然后你看看如何积分一个函数,那是导数的反过程。

5. 然后你将这些与微分方程结合起来。

不要误解我:这个过程是惊人的。我喜欢看到人类逻辑能带你多远。我喜欢看到非常复杂的自然事件可以通过从非常简单的概念开始逐步推导出更深的含义。

另一个宝贵的科学课程是,最初应用于“A”的定理,也可以应用于“B”、“C”、“D”和“E”。

令人着迷的方面在于,领域“A”和其他领域(B、C、D 和 E)不必相关。

可能最好的例子就是贝叶斯定理

贝叶斯定理在某种程度上是一个基础且显而易见的概念。令人难以置信的酷事是,我们可以通过强调贝叶斯定理背后的理念并将其应用于机器学习来构建一些非常有趣的算法。这个工具被称为朴素贝叶斯分类器

在这篇文章中:

  1. 我们将简要介绍贝叶斯定理。我们将解释它是什么,为什么重要,以及它如何应用于机器学习

  2. 我们将看到贝叶斯定理在一个虚构的分类任务中的应用。

  3. 我们将看到一个升级版的贝叶斯定理,使用所谓的高斯朴素贝叶斯分类器

我非常兴奋。让我们开始吧!

1. 关于贝叶斯定理

有一个关于贝叶斯定理的定义一直深深地印在我的脑海里:

“贝叶斯定理是这样的定理,它证明了仅仅因为你的车是蓝色的,并不意味着全世界的车都是蓝色的如果全世界的车都是蓝色的那么你的车必须是蓝色的。”

如果你阅读了这篇文章关于贝叶斯神经网络,你可能会认识到这与之前的定义相同,但我发誓我没有因为懒惰而重复使用同一定义!我的意思是……我确实很懒,但这不是原因。

我使用那个定义是因为它帮助我们理解事件 A 在事件 B 的条件下的发生概率与事件 B 在事件 A 的条件下的发生概率是不一样的。

让我做一个新的例子来克服我的懒惰。

假设我们有两个碗

现在,假设这两个碗里满是篮球足球(称为足球,不是足球

作者图片

现在,假设我问你这个问题:

知道我从蓝色碗里挑了一个球,挑到一个足球球的概率是多少。”

好吧,答案很简单。概率是1,因为在蓝色碗里,我只有足球球。现在假设我随机挑选一个碗(挑选白色碗和挑选蓝色碗的概率都是 0.5)。让我问你这个问题:

“知道我挑了一个足球,选择这个球来自蓝色碗的概率是多少?”

如你所见,问题有点类似,但事件 A(我挑了一个足球)和事件 B(我挑了蓝色碗)的顺序正好相反。

现在,我想你可能已经猜到答案并不相同,因为我可能从白色碗中而不是蓝色碗中提取了足球。我也认为你可能猜到这个概率仍然很高,因为我在白色碗中只有一个足球。

让我们在Python中进行实验。

我可以逐行详细解释,但我觉得那样很无聊。我所做的仅仅是设置碗和球的情况,就像之前一样,然后运行概率实验N次。在这 N 次迭代结束时,我们将得到 N 个结果。然后我们需要将概率计算为频率主义概率。在这种情况下,我们将计算:

图片由作者提供

现在我们知道,如果我们在相同的实验中运行 N = 无限次,我们将收敛到分析结果,因此我们将迭代地增加 N,看看它是否收敛到任何结果。

如你所见,我们进行了 20,50,70,…, 1M 次迭代。

这个结果是什么?

图片由作者提供

嗯,看来确实存在一个分析结果,对吗?让我们来看看。

所以,贝叶斯定理告诉我们:

图片由作者提供

因此,提取到的足球来自 蓝色碗的概率等于提取一个足球从蓝色碗(注意区别!这是在你选择了蓝色碗的前提下提取一个足球的概率)的概率乘以从蓝色碗提取的概率,除以提取足球的概率。

我们知道:

图片由作者提供

因为蓝色碗中只有足球。我们还假设我们以相等的概率选择两个碗中的一个。

所以我们有:

图片由作者提供

现在,提取足球的概率是从白色碗中提取足球的概率和从蓝色碗中提取足球的概率之和。例如:

图片由作者提供

所以我们有:

图片由作者提供

因为:

图片由作者提供

所以我们有:

图片由作者提供

如果我们在之前绘制的图上绘制 5/6,我们会得到:

图片由作者提供

我们的数学计算是正确的 😄

好吧,这时我相信你们都在想:

“这与机器学习有什么关系?”

让我们来看看 😏

2. 朴素贝叶斯分类器

朴素贝叶斯分类器是朴素地将贝叶斯定理应用于机器学习分类器的过程:就是这么简单。

假设我们有一个特定的二分类问题(类别 1 和类别 2)。你有 N 个实例,每个实例都有其标签 Y。

所谓的先验概率定义如下:

图片由作者提供

现在我们真正想知道的是:给定一个特定的实例,那个实例属于类别 1 的概率是多少?属于类别 2 的概率是多少?

所以我们感兴趣的是,给定一个实例 x:

图片由作者提供

在二元数据集中,我们知道这两个数量的总和是 1,因此实际上我们只需要其中一个。现在这个数量可能看起来有些神秘,但另一个:

图片由作者提供

可以非常容易地计算(记住碗/球的例子!)。

计算 P(x)的概率也非常简单,就像我们计算 P(class 1)和 P(class 2)一样。

所以我们实际上可以计算:

图片由作者提供

这一切看起来都很好。现在我想你可能已经理解了我们为什么称之为朴素。它之所以朴素,是因为它不过是计数出现次数并使用贝叶斯逻辑来推断预测。

让我们将其应用到一个玩具数据集上。我们有这两个类别和两个特征,是使用sklearn.datasets库生成的。

太棒了。

所以:

图片由作者提供

因为我们有两个维度。

给定类别 0,我们可以轻松计算给定区域(2D 点周围的小区域)的概率。

所以:

  1. 我们知道如何计算后验概率,即 P(Y|X)

  2. 我们有我们的 Y 和 X 集合

  3. 我们可以将规则应用于训练集,并且可以预测测试集的结果。

让我们开始吧!

  1. 训练-测试分割

2. 导入拟合朴素贝叶斯分类器

请注意,我们对数据(x)添加了一个偏置。这是因为我们使用的贝叶斯分类器将 x_1 特征视为分类特征,而我们将这些特征视为数值特征。一种更严格的方法是使用LabelEncoder()特征,但在这个特定情况下,它们是完全等价的。

3. 测试性能

在这个非常简单的玩具示例中,性能显然是(几乎)完美的。

3. 高斯朴素贝叶斯分类器

你可能已经听够了我讲的内容,我对此感到抱歉。

我只是想通过谈论高斯朴素贝叶斯来结束这篇文章。

在前面的例子中,我们将特征视为分类的。但如果它们不是呢?

好吧,我们必须假设一个特定的可能性分布:P(Y|X)

图片由作者提供

所以它叫做高斯朴素贝叶斯,因为如上所见,似然被认为是高斯的。均值(mu_y)和方差(sigma_y *方)是通过计算类别的均值和方差得到的。

请注意,现在我们不再需要在数据中添加偏差。

这些是结果

4. 总结

在这篇文章中,我们:

  1. 承认贝叶斯定理的存在。我们对它进行了直观的介绍,并进行了一个非常简单的受控案例来查看它是如何工作的。

  2. 了解贝叶斯定理如何应用于机器学习。什么是 Y,什么是 X,我们如何将它们放入贝叶斯公式中以在分类任务中进行一些预测。

  3. 我们自己创建了一个数据集,并使用Categorial Naive Bayes类(来自sklearn)进行了一项玩具分类任务。它几乎完美地工作,但有一个问题,就是只能处理分类特征。

  4. 我们了解了如何将分类朴素贝叶斯扩展为Gaussian Naive Bayes。正如我们所见,Gaussian Naive Bayes 使用高斯似然来工作。这种高斯似然也适用于非分类特征。

贝叶斯定理在机器学习中以多种方式应用,如贝叶斯神经网络或贝叶斯岭回归。我觉得这是一个很酷的入门示例,展示了贝叶斯定理在分类问题中的应用。我写这篇文章非常开心,希望你们喜欢它!🥰

5. 结论

如果你喜欢这篇文章,想了解更多关于机器学习的内容,或者只是想问我一些问题,你可以:

A. 在 Linkedin 上关注我,我会发布我的所有故事。

B. 订阅我的 新闻通讯。它将让你了解新故事,并给你机会通过短信与我联系,获取你可能有的所有更正或疑问。

C. 成为 会员,这样你就没有“每月最大故事数”的限制,可以阅读我(和其他数千名机器学习和数据科学顶级作者)关于最新技术的所有文章。

从零开始的朴素贝叶斯与 TensorFlow

原文:towardsdatascience.com/naive-bayes-from-scratch-with-tensorflow-6e04c5a25947

概率深度学习

路易斯·罗克Towards Data Science 路易斯·罗克

·发表于 Towards Data Science ·10 分钟阅读·2023 年 1 月 18 日

--

介绍

本文属于“概率深度学习”系列。该系列每周涵盖深度学习的概率方法。主要目标是扩展深度学习模型以量化不确定性,即了解它们不知道的内容。

在本文中,我们对使用葡萄酒样本数据集的朴素贝叶斯分类算法进行了考察。朴素贝叶斯算法是一种基于贝叶斯定理的概率机器学习技术,假设在给定目标标签的情况下特征之间是独立的。为了便于可视化类别的分离,我们将模型限制为仅使用两个特征。

我们的目标是基于选定的特征对葡萄酒样本进行分类。为实现这一目标,我们首先探索数据并选择有效区分各类别的特征。然后,我们构建类别先验分布和类别条件密度,从而能够预测具有最高概率的类别。该研究使用的数据集包含葡萄酒的各种特征,如色调、酒精、类黄酮以及一个目标类别,并且数据集来自 scikit-learn 库 [1]。

迄今为止发表的文章:

  1. TensorFlow Probability 的温和介绍:分布对象

  2. TensorFlow Probability 的温和介绍:可训练参数

  3. 从零开始的最大似然估计,使用 TensorFlow Probability

  4. 从零开始的概率线性回归,使用 TensorFlow

  5. Tensorflow 中的概率性与确定性回归

  6. 频率学派与贝叶斯统计在 Tensorflow 中的应用

  7. 确定性与概率性深度学习

  8. 从头开始使用 TensorFlow 实现朴素贝叶斯

图 1:今天的格言:在葡萄酒分类方面要保持天真? (source)

我们使用 TensorFlow 和 TensorFlow Probability 开发我们的模型。TensorFlow Probability 是一个建立在 TensorFlow 之上的 Python 库。我们将从 TensorFlow Probability 中的基本对象开始,理解如何操作它们。接下来的几周里,我们将逐步增加复杂性,并将我们的概率模型与现代硬件(例如 GPU)上的深度学习结合起来。

如常,代码可以在我的GitHub上找到。

探索性数据

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

import tensorflow as tf
import tensorflow_probability as tfp
tfd = tfp.distributions

from sklearn.metrics import accuracy_score
from sklearn import datasets, model_selection
from sklearn.datasets import load_wine

我们的目标是调查使用朴素贝叶斯算法根据选择的特征对葡萄酒样本进行分类。为了实现这一目标,我们首先进行数据的探索性分析。让我们开始识别两个有效区分目标变量的特征,并利用它们预测葡萄酒的类别。

dataset = load_wine(as_frame=True)
dataset = pd.concat((dataset['data'], dataset['target']), axis=1)
sns.pairplot(dataset[['alcohol','alcalinity_of_ash', 'flavanoids', 'color_intensity', 'hue', 'target']],hue='target');

图 2:分析葡萄酒数据集中的特征对。

酒精度和色调是有效区分类别的特征。因此,这些就是我们将用于构建朴素贝叶斯模型的两个特征。

sns.jointplot(x='alcohol',y='hue', hue='target', data=dataset);

图 3:按酒精度和色调分布的目标样本。

我们现在可以将数据分成训练集和测试集。

data = dataset[['alcohol', 'hue']].to_numpy()
targets = dataset[['target']].to_numpy()

label_colors = ['darkred', 'peachpuff', 'black']
x_train, x_test, y_train, y_test = model_selection.train_test_split(data, targets, test_size=0.2)

朴素贝叶斯分类器

朴素贝叶斯是一种广泛使用的概率机器学习算法,基于贝叶斯定理。它特别适用于分类任务,并以其简单性和高效性著称。尽管名字中有“朴素”,但特征之间的“朴素”独立性假设并不总是限制,并且在实践中通常能取得良好结果。在这篇文章中,我们将全面回顾朴素贝叶斯算法及其变体,并从基本原理上实现它。

我们首先简要介绍贝叶斯定理,这是朴素贝叶斯算法的基础。贝叶斯定理表明,在给定一些证据(E)的情况下,假设(H)的概率与假设的先验概率乘以证据在给定假设下的似然性成正比。朴素贝叶斯算法使用这个定理通过计算每个类别的后验概率来分类新实例,然后选择概率最高的类别。

朴素贝叶斯算法的基本原理是假设给定实例的特征在给定类别标签的情况下是条件独立的。这一假设,也称为“朴素”假设,使得算法在计算上更为高效,因为它减少了需要估计的参数数量。然而,当特征实际上并非独立时,这也可能导致准确率下降。

朴素贝叶斯算法有几种变体,每种都适用于不同类型的数据。例如,高斯朴素贝叶斯用于连续数据,而多项式朴素贝叶斯用于离散数据。伯努利朴素贝叶斯用于二元数据。在这种情况下,我们将专注于实现高斯朴素贝叶斯。

朴素贝叶斯算法已被应用于广泛的领域,包括自然语言处理、计算机视觉和生物信息学。在自然语言处理领域,它通常用于文本分类,例如垃圾邮件检测和情感分析。在计算机视觉中,它用于图像分类和目标检测。在生物信息学中,它用于蛋白质分类和基因预测。

正如我们上述所述,朴素贝叶斯分类器基于贝叶斯规则:

其中 𝑋 是输入特征,𝑌 是输出类别,𝐾 是类别的数量。更具体地说,𝑃(𝑌) 表示类别先验分布,𝑃(𝑋|𝑌) 是输入的类别条件分布,而 𝑃(𝑌|𝑋) 是给定输入特征的类别概率。

独立性假设大大简化了算法,因为我们不需要估计完整的联合分布 𝑃(𝑋|𝑌=𝑦𝑘)。相反,类别条件分布可以写作:

其中 𝑓 表示特征的数量。

先验

在朴素贝叶斯算法中,类别先验分布是一个概率分布,描述了训练数据中每个类别的概率。它是算法的一个基本组成部分,因为它用于计算给定一些证据的类别后验概率。

类别先验分布定义为给定训练数据中实例总数的类别概率。它通常表示为 𝑃(𝑌=𝑦𝑘), 其中 𝑘 是类别标签。类别先验分布通过训练数据中每个类别的相对频率来估计。例如,如果训练数据中有 100 个实例,其中 60 个属于类别 A,那么类别 A 的先验概率估计为 P(Y=A) = 0.6。

类别先验分布在朴素贝叶斯算法中起着至关重要的作用,因为它用于计算在给定一些证据的情况下某一类别的后验概率。后验概率的计算是将类别先验和在给定类别下证据的似然度相乘,并通过证据的边际似然度进行归一化。换句话说,类别先验分布作为一个权重因子,调整似然函数的相对重要性。

然而,如果类别先验分布是从有偏的训练数据中估计的,它可能会导致算法性能不佳,特别是当测试数据来自不同的分布时。这被称为类别不*衡问题,可以通过使用过采样、欠采样或合成数据生成等技术来缓解。

类别先验分布是属于类别 𝑘 的数据示例的比例。我们可以将其写成以下形式:

其中,𝑛 表示第 𝑛 个数据集示例,𝑁 是数据集中示例的总数,𝛿 是克罗内克δ函数(当类别匹配时返回 1,否则返回 0)。它返回一个与 𝑃(𝑌=𝑦𝑘) 相对应的分类分布。

def prior_fn(y):
    n_classes = np.unique(y).shape[0]
    counts = np.zeros(n_classes)
    for c_k in range(n_classes):
        counts[c_k] = np.sum(np.where(y==c_k, 1, 0))
        priors = counts/np.sum(counts)
    dist = tfd.Categorical(probs=priors)
    return dist

prior = prior_fn(y_train)
prior

<tfp.distributions.Categorical 'Categorical' batch_shape=[] event_shape=[] dtype=int32>

让我们绘制我们的先验分布。

plt.bar([0, 1, 2], prior.probs.numpy(), color=label_colors)
plt.xlabel("Class")
plt.ylabel("Prior probability")
plt.title("Class prior distribution")
plt.xticks([0, 1, 2],)
plt.show()

图 4:每个目标类别的先验概率。

似然性

在朴素贝叶斯算法中,类别条件密度是描述给定类别标签下每个特征的似然性的概率分布。它们用于计算在给定一些证据的情况下某一类别的后验概率,是算法的一个基本组成部分。类别条件密度定义为给定类别标签下每个特征的概率密度函数(pdf)。它们通常表示为 𝑃(𝑋𝑖|𝑌=𝑦𝑘), 其中 𝑋𝑖 是一个特征,𝑘 是类别标签。类别条件密度是通过各种技术从训练数据中估计出来的,具体取决于数据的类型。例如,对于连续数据,类别条件密度可以使用高斯分布估计,而对于离散数据,可以使用多项式分布或伯努利分布进行估计。正如我们之前所述,在我们的案例中,我们有连续特征,因此我们将探索高斯方法。

类别条件密度在朴素贝叶斯算法中起着关键作用,因为它们用于计算给定类别标签下证据的似然性。这一似然性是通过评估证据的每个特征的类别条件密度来计算的,然后将它们相乘。类别条件密度作为一个权重因子,调整每个特征在分类任务中的相对重要性。

现在是定义𝑃(𝑋|𝑌)——输入的类别条件分布的时候了。在这种情况下,我们使用单变量高斯分布(请记住独立性假设):

其中𝜇𝑖𝑘𝜎𝑖𝑘是需要估计的参数。使用最大似然估计,估计值就是每个类别样本数据点的均值和方差:

def class_conditionals_fn(x, y):
    n_classes = np.unique(y).shape[0]
    n_features = x.shape[1]
    counts = np.zeros(n_classes)
    mean_feature_given_class = []
    std_feature_given_class = []
    for c_k in range(n_classes):
        mean_feature_given_class.append(np.mean(x[np.squeeze(y==c_k)], axis=0))
        std_feature_given_class.append(np.std(x[np.squeeze(y==c_k)], axis=0))

    class_cond = tfd.MultivariateNormalDiag(loc = np.asarray(mean_feature_given_class).reshape(n_classes, n_features),
                             scale_diag=np.asarray(std_feature_given_class).reshape(n_classes, n_features))

    return class_cond

class_conditionals = class_conditionals_fn(x_train, y_train)
class_conditionals

<tfp.distributions.MultivariateNormalDiag 'MultivariateNormalDiag' batch_shape=[3] event_shape=[2] dtype=float64>

下方的等高线图展示了类别条件密度。请注意每个分布的等高线如何对应于具有对角协方差矩阵的高斯分布,因为模型假设在给定类别的情况下每个特征是独立的。

def contour_plot(x0_range, x1_range, prob_fn, batch_shape, colors, levels=None, num_points=100):
    x0 = np.linspace(x0_range[0], x0_range[1], num_points)
    x1 = np.linspace(x1_range[0], x1_range[1], num_points)
    X0, X1= np.meshgrid(x0, x1)
    Z = prob_fn(np.expand_dims(np.array([X0.ravel(), X1.ravel()]).T, 1))
    Z = np.array(Z).T.reshape(batch_shape, *X0.shape)
    for batch in np.arange(batch_shape):
        if levels:
            plt.contourf(X0, X1, Z[batch], alpha=0.2, colors=colors, levels=levels)
        else:
            plt.contour(X0, X1, Z[batch], colors=colors[batch], alpha=0.3)

plt.figure(figsize=(10, 6))
plot_data(x_train, y_train, alpha=0.3)
x0_min, x0_max = x_train[:, 0].min()-0.2, x_train[:, 0].max()+0.2
x1_min, x1_max = x_train[:, 1].min()-0.2, x_train[:, 1].max()+0.2
contour_plot((x0_min, x0_max), (x1_min, x1_max), class_conditionals.prob, 3, label_colors)
plt.title("Training set with class-conditional density contours")
plt.show()

图 5:带有类别条件密度等高线的训练集。

在执行上述计算后,算法的最后一步是预测新的数据输入𝑋̃ :=(𝑋̃ 1,…,𝑋̃ 𝑓)的类别𝑌̂。可以通过以下方式完成:

def predict_class(prior, class_conditionals, x):
    log_prob_list = []
    for sample in x:
        cond_probs = class_conditionals.log_prob(sample)
        joint_likelihood = tf.add(prior.probs.numpy(), cond_probs)
        norm_factor = tf.math.reduce_logsumexp(joint_likelihood, axis=-1, keepdims=True)
        log_prob = joint_likelihood - norm_factor
        log_prob_list.append(log_prob)
    return np.argmax(np.asarray(log_prob_list), axis=-1)

predictions = predict_class(prior, class_conditionals, x_test)

结果

在这篇文章中,我们应用了朴素贝叶斯算法来根据选定的特征对葡萄酒样本进行分类。具体来说,我们使用了两个特征:色调和酒精,来预测葡萄酒的类别。我们的结果表明,该模型在这项任务中的准确率超过了 91%。

accuracy = accuracy_score(y_test, predictions)
print("Test accuracy: {:.4f}".format(accuracy))

Test accuracy: 0.9167

为了进一步分析模型的性能,我们还绘制了模型的决策区域,即分隔不同类别的边界。决策区域有助于可视化算法执行的类别分离。如我们所见,模型能够相当有效地分隔数据集中的三个类别。

值得注意的是,朴素贝叶斯算法假设特征之间是独立的,这在实际场景中可能不成立。特征之间的相关性可以帮助提高模型的准确性。因此,考虑在模型特征之间引入相关性可能有助于提升性能。此外,还可以考虑允许特征之间相关性的其他算法来改善结果。

plt.figure(figsize=(10, 6))
plot_data(x_train, y_train)
x0_min, x0_max = x_train[:, 0].min()-0.2, x_train[:, 0].max()+0.2
x1_min, x1_max = x_train[:, 1].min()-0.2, x_train[:, 1].max()+0.2
contour_plot((x0_min, x0_max), (x1_min, x1_max), 
             lambda x: predict_class(prior, class_conditionals, x), 
             1, label_colors, levels=[-0.5, 0.5, 1.5, 2.5, 3.5],
             num_points=200)
plt.title("Training set with decision regions")
plt.show()

图 6:训练集决策区域。

结论

在这篇文章中,我们使用 TensorFlow Probability 从零开始实现了朴素贝叶斯算法。我们将其应用于使用葡萄酒样本数据集的分类任务。我们选择了两个特征,色调和酒精,来预测葡萄酒的类别,并且达到了超过 91%的准确率。我们还可视化了模型的决策区域,这有助于理解算法执行的类别分离。

这个简单的例子展示了朴素贝叶斯算法在分类任务中的简单性和有效性。然而,朴素贝叶斯算法假设特征之间是独立的,这在实际场景中可能并不成立。

参考文献和资料

[1] — 葡萄酒数据集

[2] — Coursera: 深度学习专业课程

[3] — Coursera: TensorFlow 2 深度学习 专业课程

[4] — TensorFlow 概率指南与教程

[5] — TensorFlow 博客中的 TensorFlow 概率帖子

命名实体与新闻

原文:towardsdatascience.com/named-entities-and-the-news-ac9009601095?source=collection_archive---------5-----------------------#2023-07-04

探索命名实体在荷兰新闻数据集中的应用

Felix van DeelenTowards Data Science Felix van Deelen

·

关注 发表在 Towards Data Science ·10 min read·Jul 4, 2023

--

一个新闻推荐系统的例子,确实可以从命名实体识别(NER)中受益。来源:文章NOS提供,照片由Rick LUnsplash(左),作者创建的图像(中),文章NOS,照片由Cristina Anne CostelloUnsplash(右)。

在 NOS——荷兰公共广播基金会——我们的编辑团队每天撰写数百篇新闻文章。这些文章向荷兰公民提供新闻,同时从自然语言处理的角度来看,也形成了一个有趣且高质量的数据集。在这篇博客中,我作为 NOS 的数据科学家,报告了通过将命名实体识别(NER)应用于我们荷兰新闻文章的数据集所进行的几个实验,并提出了在新闻背景下应用 NER 的几个想法。

什么是命名实体?

命名实体(NE)是一种特殊的词,指代具有专有名称的现实世界对象,例如人物地点组织。存在自动识别这些类型词语的模型,这些模型称为命名实体识别(NER)模型。右侧图示展示了一个应用于我们文章摘录的 NER 模型,其中 NE 被突出显示并标注了 NE 类型。

在荷兰,有一些预训练模型可用,如spaCy [1]、Flair [2] 或NTLK [3]。我们对这三种模型进行了定性评估,通过将它们应用于我们文章的随机样本并手动检查结果。由此我们决定在剩余实验中使用 spaCy。此模型可能识别的所有 NE 类型的概述见下图 1 左侧。

图 1:由spaCy提供的 NER 模型中的 NE 类型概述(左)。应用于从荷兰语翻译的新闻文章摘录的 NER 示例(右)。

使用来自 spaCy 的预训练模型,我们对数据集的几个子集应用了 NER。我们首先收集了一个月(2023 年 2 月)的所有文章,将数据分为新闻体育(分别为 1,030 篇和 596 篇),然后应用 NER 以获得每种 NE 类型的总频率计数。新闻体育的结果显示在图 2 中,立即展示了 NE 在新闻中的重要性。可以看出,仅在一个月的文章中,就提到了数万条 NE。为提供一个视角,*均每篇文章包含 404 个词,大约 10%的词是 NE。下图也显示了新闻体育中最常提到的 NE 类型有所不同。对于新闻,大多数 NE 类型是国家,其次是组织和人物。而对于体育,最常提到的 NE 类型是人物,其次是国家和数字。这可能是因为体育涉及比分(基数)和个人运动员(人物),而新闻报道事件时,通常需要提到地点(gpe)。

图 2:新闻文章(左)和体育文章(右)中检测到的 NE 类型频率计数。

NER 为我们的数据提供了新的视角

我们对 2022 年世界杯足球赛的所有文章进行了案例研究,总共包含 482 篇文章。NER 被应用于数据集,以检测所有类型为Person的 NE。发现 2,171 个独特的 NE,其中 1,296 个只被提到过一次。在图 3A 中,我们展示了这一事件中最常提到的人物的概述。此外,对于最常提到的人物,我们创建了一个流图,以展示提及频率随时间的发展,见图 3B。这表明,如范加尔在整个比赛中频繁被提到,而其他人物则主要在特定日期被提到。这类图表可能为我们的编辑团队提供新的洞察,因为它们是 NOS 报道内容的定量反映。这些洞察是通过 NER 高效提供的。目前我们特别应用于世界杯 22,但可以想到许多不同的设置,这些图表可能会很有趣。例如,想一想在选举期间提到哪些政治家或政党,或更普遍地,某个较长时间范围内国家、城市、组织等的提及频率。

图 3A & B:通过 NER 获得的世界杯 22 中提到的人物频率计数。左侧显示总数,右侧显示随时间的发展。

关于[您的命名实体这里]

我们将案例研究扩展到使用所有 2022 年世界杯的文章,并提出了“我们能否利用 NER 为命名实体生成摘要?”的问题。我们首先开发了一个模块,该模块收集所有提到给定 NE 的文章,这可以作为用户特别感兴趣的 NE 的所有信息集合。但更有趣的是,该模块收集了所有提到 NE 的句子,从而生成了该集合的摘要。例如,我们将该模块应用于安德里斯·诺普特,荷兰国家队的守门员。从图 3 中可以看出,诺普特在赛事中被提及的频率相当高。应用该模块后,生成了一个很好的概述,展示了我们守门员的非凡故事,如下所示,翻译自荷兰语。

-------------------------------------------------- -------------------------------------------------- --------------------
2022-11-11
   - Noppert joining as a penalty killer?
-------------------------------------------------- -------------------------------------------------- --------------------
2022-11-16
   - sc Heerenveen goalkeeper Andries Noppert is the nineteenth premier league player in Qatar.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-11-20
   - 'Don't worry about Qatar and Ecuador' and 'Failure on goal is a gamble' Analysts Leonne Stentler and Pierre van Hooijdonk agree.
   - Van Gaal does not say anything about Noppert's base place, but hints at Gakpo 'at 10' According to various media, 28-year-old Andries Noppert, who plays for sc Heerenveen, would make his debut for the Orange squad against Senegal on Monday.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-11-21
   - Is Noppert the base goalkeeper now?
   - Noppert: 'This is what you dream of as a boy' Goalkeeper Andries Noppert turned out not to suffer from stage fright against Senegal.
   - Will Noppert succeed first World Cup debutant Schoenaker?
   - Goalkeeper Andries Noppert makes his debut in Orange and can look back on a successful first international match.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-11-22
   - 'Disarming' Noppert takes the stage: 'In the Netherlands we are all whining' The 28-year-old goalkeeper of sc Heerenveen made his debut on Monday in the World Cup match against Senegal in the Dutch national team.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-11-23
   - Noppert?
-------------------------------------------------- -------------------------------------------------- --------------------
2022-11-24
   - The Foggia episode of Orange keeper Noppert: 'He smoked like a chimney' Andries Noppert is suddenly a well-known Dutchman after the World Cup match of the Netherlands against Senegal.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-11-25
   - Jurriën Timber, Virgil van Dijk and Nathan Aké had their defenses well organized and Andries Noppert once again proved to be a reliable goalkeeper.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-12-03
   - View the reactions of Virgil van Dijk and Andries Noppert here: In that team, one of the important players is just back in his familiar spot in the attack.
   - Andries Noppert made a good save with his left leg.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-12-07
   - Noppert lives soberly towards Argentina: 'Messi can also miss penalties, can't he?'
-------------------------------------------------- -------------------------------------------------- --------------------
2022-12-09
   - So yes..." Noppert's fairy tale ended It could have been so beautiful for sc Heerenveen goalkeeper Andries Noppert, but the keeper on the other side, Emiliano Martinez, became the great hero.
   - The Argentinian wingback Molina ran away from the back of his Dutch colleague Blind, Virgil van Dijk was just too late to correct and Molina passed Andries Noppert.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-12-16
   - Six striking World Cup facts: Amrabat conquers, Modric dribbles, Noppert saves Remarkable statistics everywhere during the World Cup in Qatar.
-------------------------------------------------- -------------------------------------------------- --------------------
2022-12-18
   - Andries Noppert (Netherlands) Vermeulen: "The same goes for Noppert, of course.

一个 NE 感知的推荐系统

到目前为止,我们已经看到 NE 在新闻文章中非常普遍,应用 NER 可以提供一些有趣的见解。我们认为还有一个实验很值得在此博客中分享,涉及研究问题“我们能否利用 NER 改进基于内容的推荐系统?”。之前我们开发了一个基于内容的推荐系统,该系统最*已集成到我们的新闻应用中。通过在线和离线测试,我们比较了各种模型和优化,现在我们观察到应用中的点击率有所增加。这一切都是好消息,但我们一直在寻找进一步改进推荐系统的方法。我们收到编辑团队的反馈,称推荐系统对包含人名或地点名的文章感到困惑,这些名字在荷兰语中也是常见词汇。在接下来的部分中,我们报告了一个使用 NER 解决这种歧义的实验。

实验

我们当前的推荐系统基于使用 TF-IDF 进行文本向量化的余弦相似度。这基本上意味着它在识别相似文章时严重依赖词语重叠,但对稀有词语赋予更高的相关性。可以想象,当词语具有多重含义时,这种方法的效果可能不佳,这在 NE 的情况下尤为如此。举个例子,考虑一篇关于高尔夫球手泰格·伍兹的文章:一个基本的推荐系统可能会找到提到动物老虎或关于树林的文章。这些显然不会是有用的推荐。我们假设通过在推荐系统中引入 NE 感知,即通过按类型标注文本中的 NE,可以解决这个问题。在这种情况下,令牌将不再重叠,如图 5 所示。

图 5:当前推荐系统(基础)与 NE 感知系统的示例对比。当前系统因两篇文章中都提到了“tiger”一词而将这两篇文章关联起来,而 NE 感知系统则解决了这一歧义。来源:文章NOS提供,照片由Rick LUnsplash拍摄(左),文章NOS提供,照片由Cristina Anne CostelloUnsplash拍摄(右)。

我们使用 NE 类型personlocationorganisation及其组合来实现 NE 感知。我们使用由编辑团队手动标注的测试集来评估各种模型,该测试集包含了哪些文章相关的信息。该测试集包含 14,541 篇独特的文章,*均每篇文章链接到约 2 篇其他文章。作为评估指标,我们计算了在排序推荐中的策划链接文章的*均排名。

图 6 显示了我们的基础模型与各种 NE 感知模型的结果。可以看出,实际上我们的基础模型优于所有类型的 NE 感知模型。理论上,引入 NE 感知应能改善推荐系统,但实际上我们发现它引入了比解决的歧义更多的歧义。我们详细检查了各种模型的输出,发现我们受限于 NER 模型的性能。spaCy NER 模型在其自身测试集上的 F-score 为 0.77,但在应用于其他数据集时,该得分可能更低,因此模型偶尔可能不准确。从对一些 NE 感知模型输出的手动检查中,我们看到,结合 TF-IDF 时,错误检测到的 NE 的影响相当强。对于错误检测到的 NE 的文章,输出推荐经常包含相同的错误分类 NE。例如,我们看到一篇包含词汇hindsight的文章被错误分类为 Person 类型的 NE,导致推荐中包含相同错误分类的 NEhindsight。虽然在这种情况下 NER 并未启用,但推荐是有意义的,因为 TF-IDF 会给像hindsight_Person这样在语料库中非常稀有的标记分配更高的相关性。我们的结论是,目前预训练的荷兰 NER 模型不够准确,无法融入我们的推荐系统。

图 6:我们当前推荐系统(基础版)与 NE 感知系统的性能比较。

我们未来可能会从自己微调预训练模型中受益。目前,我们探索了另一种解决 NE 模糊性的方法,使用诸如类别和关键词等元数据作为无噪音但关联性较弱的 NE 测量,这显著改善了我们的推荐系统。

结论

在本博客中,我们探讨了将命名实体识别应用于荷兰新闻数据集时可以做些什么。我们发现它在获取数据集的一般见解方面效果良好,例如构建 NE 频率图和流图。然而,当应用于我们的推荐系统时,我们发现模型的准确性不够。虽然引入 NE 感知解决了一些 NE 模糊性问题,但同时也引入了 NE 检测错误的新模糊性。未来我们可能会尝试微调预训练模型或从头开始训练自己的模型,或者如果你有任何建议,请在评论中告诉我们!

除非另有说明,否则所有图片均为作者提供。

参考文献 [1] spaCy NER 模型:spacy.io/models/nl#nl_core_news_lg

[2] Flair NER 模型:huggingface.co/flair/ner-dutch-large

[3] NLTK NER 模型:www.nltk.org/book/ch07.html

关于 NOS NOS 是荷兰的一个独立公共媒体组织,通过电视、广播、网站和移动应用等*台报道新闻和体育。我们有专门的专业团队为多个品牌创建数字服务。本博客中描述的研究是在 NOS 数据团队的成员身份下进行的,该团队负责探索新闻背景下新兴数据科学和 AI 技术的应用。

Python 标准库中的 NaN 值

原文:towardsdatascience.com/nan-values-in-the-python-standard-library-798d9ed946c0

PYTHON 编程

NaN 意味着 Not-a-Number。你可以在数值库中使用它——但也可以在 Python 标准库中使用。

Marcin KozakTowards Data Science Marcin Kozak

·发布在 Towards Data Science ·11 分钟阅读·2023 年 10 月 7 日

--

照片由 cyrus gomez 提供,来源于 Unsplash

NaN 代表 Not-a-Number。因此,NaN 对象表示它的名字所传达的意思——某物不是一个数字。它可以是一个缺失值,也可以是数值变量中的非数值值。由于我们不应该在纯数值容器中使用非数值值,因此我们将这样的值标记为 not-a-number,即 NaN。换句话说,我们可以说 NaN 代表一个 缺失的数值

在本文中,我们将讨论 Python 标准库中可用的 NaN 对象。

NaN 值在数值数据中经常出现。如果你对这个值的细节感兴趣,你可以在这里找到它们:

[## NaN - 维基百科

在计算中,NaN(代表 Not a Number)是数字数据类型的一种特定值(通常是浮点数)……

en.wikipedia.org](https://en.wikipedia.org/wiki/NaN?source=post_page-----798d9ed946c0--------------------------------)

在本文中,我们不会讨论 NaN 值的所有细节。¹ 相反,我们将讨论一些如何在 Python 中处理 NaN 值的示例。

每种编程语言对 NaN 值有自己独特的方法。在以计算为重点的编程语言中,NaN 值是基本的。例如,在 R 语言中,你有 NULL(相当于 Python 的 None)、NA(表示 not available)和 NaN(表示 not-a-number):

来自 R 会话的截图。图片由作者提供。

在 Python 中,你有None和多个表示NaN的对象。值得注意的是,Pandas 区分NaNNaT,后者代表缺失的时间。本文将讨论标准库中的NaN值;主流数值 Python 框架中的NaN(以及NaT)——例如 NumPy 和 Pandas——将在未来的文章中讨论。

如果你没有在 Python 中处理过数值数据,可能根本没有遇到过NaN。然而,NaN值在 Python 编程中无处不在,因此了解如何处理它们非常重要。

Python 中NaN的介绍

当你处理一个list对象时,可以使用数字值和非数字值。因此,它可以是[1, 2, "three"][1, 2, None]等。你甚至可以这样做:

>>> [1, "two", list, list(), list.sort]
[1, 'two', <class 'list'>, [], <method 'sort' of 'list' objects>]

所以,列表可以接受任何对象。如果你想对这些列表进行数字计算,可以做到,但需要调整代码:

>>> x = [1, 2, "three"]
>>> sum(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> sum(xi for xi in x if isinstance(xi, (float, int)))
3

如你所见,非数字对象在这里可以保持原样,你仍然可以进行数字计算——这不是简单或简洁的代码,但它有效。

NaN——非数字——代表一个缺失的数值

然而,对于只接受特定数字类型对象的容器类型来说情况并非如此。这包括array.arraynumpy.arraypandas系列(以及pandas数据框中的数值列)。如果定义为数字,它们不接受非数字值,唯一的例外是:NaN值。

NaN表示一个非数字值,但正如你将很快看到的,它的类型是数字类型——准确地说是float。为什么?原因很简单,因为这样你可以在数字容器中使用NaN

为什么不去掉它们呢?

为什么不干脆去掉所有这些值呢?干嘛要费这个劲呢?

NaN值的一个常见用例是数据分析和可视化。例如,考虑一个数据集,其中几列有某些行的缺失值。你不能从数据框中删除一个单元格,因此你可以选择保留所有这些行并以某种方式处理缺失值,或者删除所有包含一个或多个NaN值的行。删除缺失值的行是一个常见的做法,但它有代价:它会删除某些列中的非NaN值,通常不明智地丢弃我们已经拥有的信息。

NaN 值的另一个用途是在错误处理。例如,如果一个函数期望一个数值输入,但接收到一个字符串或其他非数值的值,它可能会返回一个 NaN 值以表示输入无效。我们很快会看到一个例子。这使得调用代码可以优雅地处理错误,而不是引发异常或返回意外结果。通过使用 NaN 值来表示错误或缺失的数据,可以对可能包含无效或缺失值的数据集进行计算和处理。你可以返回 None,但在 Python 中,None 可能表示各种不同的含义,而 NaN 传达了更具体的信息,直接与值的数值特性相关——这不是一个数字

当你处理数值和工具时,你应该知道如何使用 NaN。然而,当你的应用是通用性质的,因此不需要数值框架(如 NumPy 或 Pandas)时,你会发现 NaN 常常可以被忽略,或者用 None 表示。如果这样可以使代码更简单而不牺牲任何功能,考虑这样做。

示例

NaN 值可以意味着各种事情:

  • 一个普通的缺失值——它没有提供,未传输,等等。在你的笔记本中,你会将其标记为“NA”或“N/A”:不适用。不适用,即你无法应用它。它是缺失的,因此我们需要将其标记为缺失。你可以使用 NaN

  • 一个函数的结果得到了数值类型参数的错误值。函数返回 NaN,而不是抛出错误。

  • 一个错误。这可能是输入错误;信不信由你,输入错误比我们大多数人想象的要频繁得多,它们可能会对后续分析产生很大影响。例如,许多人仍然认为菠菜是铁的良好来源。实际上并不是,那为什么这么多人认为如此呢?这源于一个输入错误——一个错误的十进制点。你可以使用 NaN 来表示有错误的数据元素——除非你确定可以纠正这个错误。

  • 一个注释。这是一个被错误地输入到数值变量中的字符串值。当输入数据的人想解释某个特定值缺失的原因时,比如“读取不清”或“我睡过头了”。虽然这些仍然是缺失的数据,但它们提供的信息比单纯的空白值要多。有时这些信息很重要,但有时则不然。然而,对于数值计算来说,这样的注释值通常是微不足道的。因此,如果你需要使用数值容器来表示这个变量,你可以使用 NaN 来表示注释。

这些是四个例子,但也有其他情况。虽然每种情况略有不同,但从数值计算的角度来看,它们都是相同的:这个值不是一个数字。我们需要对它做点什么,使用 NaN 是一个常见的选择。

标准库中的 NaN

Python 提供了几种类型的 NaN 值,我们将在下文中讨论它们。本文重点关注标准库,但请注意,如果你使用数值框架,它很可能有自己对 NaN 的实现(或说是表示)以及与之配套的函数/方法。²

尽管 Python 标准库不是进行数值计算的最佳工具,但它确实提供了数值容器和专用工具。一个数值容器的例子是 array 模块及其 array.array 容器类型。虽然它不是直接使用的最佳工具,但它使你可以高效地使用 Cython,而无需使用像 NumPy 这样的非标准库工具。一个标准库中的专用数值工具的例子是 math 模块:

[## math — 数学函数 — Python 3.10.8 文档

该模块提供了 C 标准定义的数学函数的访问。这些函数不能被使用…

docs.python.org

在 Python 标准库中使用 NaN 值有两种方式:float("nan")math.nan。我读过许多关于 Python 的书,但我不记得看到过这两个值的提及。我的记忆并不完美,但我怀疑即使这些值在一些书中被提到,也没有得到足够的关注。因此,我认为许多数据科学家,甚至是数据科学领域之外的 Python 开发者,可能对 float("nan")math.nan 并不知晓,尽管他们可能熟悉 np.nan,这是在 NumPy 数组和 pandas DataFrame 中表示 NaN 值的标准方式(见下文)。一个可能的原因是这两个值不像 np.nan 那样广泛使用。

这两个 NaN 对象都是值,都是 float 类型:

float(“nan”) 和 math.nan 的类型都是 float。图像由作者提供。

顺便提一下,现在你不应该感到惊讶,了解到 np.nan 的类型也是 float

记住两个 NaN 值的比较是很重要的:

>>> float("nan") is float("nan")
False
>>> float("nan") == float("nan")
False
>>> math.nan is math.nan
False
>>> math.nan == math.nan
False

这是因为我们只知道 NaN 不是一个数字,但我们不知道它是什么类型的值。在一种情况下,它可以是一个字符串;在另一种情况下,它可以是一个不同的字符串;还有一种,它可以是一个长字典;再一种,它可以是一个缺失值,因为 NaN 经常用于表示 NA。因此,我们不能假设两个 NaN 值彼此相等。这在处理数值向量和矩阵时可能会产生相当大的差异:

>>> [1, 2, 3] == [1, 2, 3]
True
>>> [1, 2, float("nan")] == [1, 2, float("nan")]
False

然而,如果我们创建一个新的NaN对象,我们将看到:

>>> NaN = float("nan")
>>> NaN is NaN
True
>>> NaN == NaN
False
>>> NaN = math.nan
>>> NaNmath = math.nan
>>> NaNmath is NaNmath
True
>>> NaNmath == NaNmath
False

你有没有注意到,即使is比较返回True==比较却返回False?所以,这个对象是它自己,但它却不等于自己……

记住使用新定义的哨兵值如NaNNaNmath时的这种行为。我知道这很诱人,我自己也做过不止一次。因此,只有当这种行为是你想要实现的时,才这样做。

让我们回到这个例子:

>>> x = [1, 2, "three"]
>>> sum(xi for xi in x if isinstance(xi, (float, int)))
3

让我们看看NaN值的实际应用。我们可以用NaN值替换掉"three",而不是调整sum()函数。为此,我们可以使用以下函数:³

from collections.abc import Sequence
from typing import Any

def use_nan(__x: Sequence[float | Any]) -> Sequence[float]:
    """Replace non-numerical values with float("nan").

    >>> NaN = float("nan")
    >>> use_nan([1, 2, 3])
    [1, 2, 3]
    >>> use_nan([1., 2., 3.])
    [1.0, 2.0, 3.0]
    >>> use_nan([1, 2., 3.])
    [1, 2.0, 3.0]
    >>> use_nan([1, 2, "str"])
    [1, 2, nan]
    >>> use_nan((1, 2, str))
    (1, 2, nan)
    >>> use_nan(1., 2, Any, str, (1, 2,)))
    (1.0, 2, nan, nan, nan)
    """
    return type(__x)([xi
                      if isinstance(xi, (float, int))
                      else float("nan")
                      for xi in __x])

现在,让我们在使用sum()函数之前使用该函数,如上所述,它不接受非数值型的值:

>>> x = use_nan([1, 2, float("nan")])
>>> sum(x)
nan
>>> import math
>>> sum([1, 2, math.nan])
nan

哈?发生了什么?我们使用了NaN值来使sum()工作,确实它不像之前那样抛出错误。但它只是返回了nan……

从数学角度来看,这完全合理:将一个数字加到不是数字的东西上不会得到一个数字,对吧?这就是我们上面得到nan的原因。但这真的是我们想要实现的结果吗?

这要视情况而定。通常,我们可以选择如何处理NaN值。最典型的方法是删除它们。这通过从数据框中删除整行或整列,或从变量中删除单元格来完成。另一种方法——在统计学中常用——是用其他值填充缺失值,这称为插补。

本文并不打算详细讨论这些方法。你可以在许多统计学书籍中阅读这些方法,也可以在各种文章中找到它们;下面的两篇文章描述了在 Python 中使用这些方法:

[## 3 种处理 Python 中缺失值的终极方法]

在插补和数据删除之间明智地选择

towardsdatascience.com](/3-ultimate-ways-to-deal-with-missing-values-in-python-ac5a17c53787?source=post_page-----798d9ed946c0--------------------------------) [## 如何处理 Python 中的缺失数据? [5 个简单步骤说明]]

学习如何处理 Python 中的缺失数据。识别、评估并处理缺失数据,以便你能最大限度地发挥……

www.analyticsvidhya.com](https://www.analyticsvidhya.com/blog/2021/05/dealing-with-missing-values-in-python-a-complete-guide/?source=post_page-----798d9ed946c0--------------------------------)

如上所述,标准库中的方法确实能处理 NaN 值,但它们会简单地返回 nan,这是 float("nan")reprstr 表示。因此,我们需要手动从容器中移除不是数字的值。不幸的是,鉴于 NaN 值的比较方式,以下方法将无效

>>> x = use_nan([1, 2, "three"])
>>> sum(xi for xi in x if xi is not float("nan"))
nan
>>> sum(xi for xi in x if xi != float("nan"))
nan

那么,nan 又来了。这是怎么回事?

我们已经知道发生了什么:NaN 在与其他 NaN 值进行比较时返回 False,这发生在 if xi is not NaNif xi != NaN 中。因此,我们需要一个专门的函数来检查 NaN 值。标准库提供了这样的一个函数,在 math 模块中:

>>> sum(xi for xi in x if not math.isnan(xi))
3

结论

我们讨论了在 Python 标准库中使用 NaN 值。这些知识应该足够你在标准库工具中处理 NaN 值。然而,数值框架可以实现自己的 NaN 值。例如 NumPy 的 np.nan

正如之前提到的,我不认为作为 Python 程序员,你会频繁使用标准库中的这两个 NaN 哨兵。然而,你应该知道如何处理它们,因为有时你可能需要使用它们,即使你在使用像 NumPy 这样的数值框架。此外,我认为仅为了使用 np.nan 而安装 NumPy 并不明智。我希望这篇文章能帮助你处理这样的情况。

脚注

¹ 值得注意的是,像 None 一样,NaN 值也是哨兵值:

[## 哨兵值 — 维基百科

在计算机编程中,哨兵值(也称为标志值、三值、恶意值、信号值或…

en.wikipedia.org

² 一个例子是 np.nan 和处理 NaN 值数据的 Numpy 函数,如 np.nansum()np.nanmean()np.nanmax()np.nanmin()np.nanstd()

³ 函数的文档字符串包含多个 doctest;你可以在以下文章中阅读关于这个出色的文档测试工具的信息,它也可以用于单元测试:

## 使用 doctest 进行 Python 文档测试:简便方法

doctest 允许进行文档、单元和集成测试,以及测试驱动开发。

[towardsdatascience.com

感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在 这里 查看。如果你想加入 Medium,请使用我下面的推荐链接:

[## 使用我的推荐链接加入 Medium - Marcin Kozak

作为 Medium 会员,你的一部分会员费会转给你阅读的作者,并且你可以完全访问所有故事……

medium.com

学习 Transformers 代码优先:第一部分 — 设置

原文:towardsdatascience.com/nanogpt-learning-transformers-code-first-part-1-f2044cf5bca0

使用 nanoGPT 作为起点的 4 部分 Transformers 探索

Lily Hughes-RobinsonTowards Data Science Lily Hughes-Robinson

·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 7 月 7 日

--

图片由 Josh Riemer 提供,来源于 Unsplash

不知道你是否和我一样,有时查看代码比阅读论文更简单。当我在开发 AdventureGPT 时,我首先阅读了 BabyAGI 的源代码,这是一种用大约 600 行 Python 实现的 ReAct 论文

最*,我通过优秀的 Cognitive Revolution Podcast第 33 集 了解到了一篇名为 TinyStories 的最新论文。TinyStories 尝试展示经过数百万(而不是数十亿)参数训练的模型,在足够高质量的数据下也能有效。在论文中的微软研究人员的案例中,他们使用了从 GPT-3.5 和 GPT-4 生成的合成数据,这些数据生成的零售成本大约为 $10k。数据集和模型可以从作者的 HuggingFace repo 获取。

听到一个模型可以在 30M 及更少参数上进行训练,我感到很着迷。作为参考,我在搭载 GTX 1660 Ti 的 Lenovo Legion 5 笔记本上进行所有模型训练和推理。即使仅仅是推理,大多数拥有超过 3B 参数的模型也太大,无法在我的机器上运行。我知道有付费的云计算资源,但我是在空闲时间学习这些内容,实际上只能负担得起通过 API 调用产生的适度 OpenAI 账单。因此,能够在我的普通硬件上训练模型的想法让我立刻振奋起来。

我开始阅读 TinyStories 论文,很快意识到他们在模型训练中使用了已经停用的GPT Neo模型。我开始深入研究代码,看看是否能理解它,并意识到我需要从更小的东西开始。为了提供背景,我主要是一名后端软件工程师,具有足够的机器学习经验以便在听到别人谈论神经网络时不会完全迷失。我离一个真正的 ML 工程师还很远,这使我在搜索引擎中输入了“gpt from scratch”以寻找更温和的入门介绍。我找到下面的视频,一切都发生了变化。

这是我一直在寻找的。除了视频中链接的基本代码库,还有一个名为nanoGPT的精简版本,目前仍在积极开发中。更重要的是,训练代码和模型代码每个大约有 300 行 python。对我来说,这比视频更令人兴奋。我关闭了视频,开始仔细研究源代码。nanoGPT 利用了我从未使用过的 PyTorch。它还涉及了足够的数学和机器学习术语,让我这位新手感到紧张。这将是一个比我预期更大的工程。

理解某件事的最佳方法之一是写下来。因此,我计划深入研究 nanoGPT 代码库中的代码,阅读著名的“Attention is All You Need”论文,并以自下而上的实践方式学习变换器。无论我在这个过程中学到什么,我都希望在这个系列中写下来。如果你想跟随学习,可以将 nanoGPT 代码库克隆到你的机器上(模型甚至可以在 CPU 上训练,所以没有硬件借口),并进行跟随。

克隆代码库后,我做的第一件事是按照 README 的指示训练最简单的模型,即使用tiny_shakespeare 数据集的字符级生成模型。这里有一个脚本用于准备训练数据,一个脚本用于实际训练,还有一个采样脚本用于输出生成的文本。通过几个终端命令和一个多小时的训练,我得到一个能够输出类似莎士比亚风格文本的简单模型。

遵循说明是好的,但我直到将其修改为适合自己的用例后才真正理解某些东西。我的目标是使用 TinyStories 数据集训练一个类似的字符级模型。这需要创建我自己的数据准备脚本,以使数据集准备好进行训练。让我们深入探讨一下这个问题。

nanoGPT 有两种类型的数据准备脚本:一种用于 GPT-2 风格模型,一种用于字符级模型。我从 HuggingFace 仓库下载的 GPT-2 模型中提取了一些代码,其余的都来自 tiny_shakespeare 字符级脚本。这里一个重要的点是,tiny_shakespeare 仅有 1MB 多一点,仅包含 40k 行莎士比亚的作品。而 TinyStories 压缩后超过 3GB,包含 39.7M 个故事。对 tiny_shakespeare 进行分词和切片的方法不能直接转移,至少在我的笔记本电脑上(拥有 32GB RAM)是这样。我在尝试 pythonic、易读的数据准备方法时多次崩溃了我的机器。最终脚本使用了一些技巧,我将在下面详细说明。

首先,我处理数据列表的首选方案是 列表推导式,这是一种从现有列表生成新列表并进行修改的语法。在这种情况下,列表推导式的问题是,3GB 的压缩文本在 RAM 中变得接* 10GB。现在,列表推导式需要在 RAM 中多次复制列表。对于小数据不是问题,但对 TinyStories 来说不可行。

数据准备脚本的输出是一个压缩的 NumPy 数组,包含训练和验证数据的字符级编码,以及一个包含唯一字符完整列表和编码/解码映射的元数据 pickle,以将这些字符转换为数字。以此为参考,一旦找到并映射到数字,我们不需要其他任何东西,除了最终的编码数字数组。最有效的内存使用方法是通过简单的 for 循环迭代数据,同时分段构建这些输出。为此,在循环前初始化一个变量,然后在每次交互中更新。这样可以防止在 RAM 中保存数据集的多个版本,只输出我们需要的内容。最终的词汇生成代码如下:

chars_dataset = set([])
len_dataset = 0

# get all the unique characters that occur in this text as well as total length for training data
desc = "Enumerate characters in training set"
for story in tqdm(dataset['train']['text'], desc):
    chars = list(set(story))

    for char in chars:
        chars_dataset.add(char)

    len_dataset += len(story)

也就是说,将 30.7M 个故事(超过 40 亿字符)编码为数字的数组仍然占用大量 RAM,因为 Python 是动态存储整数的。这里引入 NumPy,它具有更高效的数组存储,可以指定整数的确切大小。除了高效的存储,NumPy 还有内存高效的数组连接,可以用于逐步构建最终的编码数组,而不是一次性完成。

我对脚本的最后修饰是使用 tqdm 为每一步添加一个进度条,最后我准备好运行脚本。所以,我让它过夜运行,早上回来时,脚本仍在运行,估计剩余计算时间超过 100 小时。

这时我真正意识到:30.7M 的故事对于语言模型来说虽然小巧,但绝对不是一个可以在单线程上处理的玩具数据集。是时候引入大招:并行化了。并行化带来了许多复杂性和开销,但性能提升是值得的。幸运的是,有许多方法可以并行化 Python 代码。许多解决方案需要对串行执行的脚本进行重大重写或复杂的抽象。经过一番挖掘,我找到了一种方法,让我可以保持大部分脚本不变,但仍然运行多个进程以利用所有线程。

Ray 是一个可以轻松地将 Python 方法并行化的库,可以在本地或集群中运行。它处理任务队列中的任务并启动工作进程来处理队列。如果这引起了你的兴趣,下面有一个很好的 ray 指南。

## 现代并行和分布式 Python:Ray 的快速教程

Ray 是一个用于并行和分布式 Python 的开源项目。

towardsdatascience.com

在选择并行化内容时,encode 函数似乎是一个不错的候选者。它有明确的输入和输出,对这些输入没有副作用,而且是计算时间中最大的一部分之一。将现有代码适配 ray 变得非常简单:通过装饰器使函数对 ray 可用,功能调用稍微更改以添加远程属性,并有一个函数来启动所有数据的执行。以下是最初在我的代码库中呈现的样子:

import ray

ray.init()

…

# given all the unique characters within a dataset, 
# create a unique mapping of characters to ints
stoi = { ch:i for i,ch in enumerate(chars_dataset) }

@ray.remote
def encode(s):
    return [stoi[c] for c in s]

…

encoded_stories = []
for story in dataset[‘train’][‘text’]:
    encoded_stories.append(encode.remote(story))

ray.get(encoded_stories)

…

拥有了所有 CPU 的力量,我继续前进,却立即导致了我的笔记本电脑崩溃。由于 ray 使用的本地分布式调用栈,整个数据集在内存中存在了几次。仅仅将整个数据集放入队列就导致了内存不足错误。我感到恼火,借此机会买了更多的 RAM(64GB,来了!),但在 RAM 发货期间继续调整代码。

下一个合乎逻辑的步骤是将 ray 处理的请求批量化成适合合理内存的大小。添加批量逻辑相对简单,并且在最终的代码库中有体现,链接将在文章末尾提供。实际上,令人感兴趣的是实验批量大小。一开始,我选择了一个随机的批量大小(5000),效果不错,但我很快意识到在每个批次中,单线程代码占用了相当多的时间。

实际上,在观察我首选的系统监视器时,我看到一个核心被占用数分钟,最后我所有笔记本的核心都亮起了几秒钟,然后又回到只有一个核心被使用。这使我尝试调整批量大小,希望能更快地喂养那些饥饿的 CPU 核心,并让它们保持更长时间的活动。减少批量大小没有帮助,因为每批中的同步代码用于从完整数据集中切分和准备一个批次。这些代码无法并行化,因此每个批次生成数据块的启动成本时间较大。这使我尝试了相反的方法,即增加数据块大小,以使核心保持更长时间的活跃。这有效,因为数据块生成的时间不论数据块大小如何都相同,但每个数据块处理了更多的数据。结合将编码后处理移到 ray 函数中,我能够在短短几个小时内处理 30%的训练数据集,全部在一台笔记本电脑上完成。

最终,在再过几个小时后,我拥有了一个完全准备好的自定义数据集,以供字符级模型使用。我很高兴我不需要求助于昂贵的云计算来处理训练集,如果 RAM 增加不起作用,这将是我的下一个步骤。更重要的是,我深入了解了为字符级模型创建/处理数据集的含义。

在本系列的下一篇文章中,我将检查实际的模型代码,尽我所能地进行解释,并链接大量外部资源以提供额外的信息,以弥补我的知识不足。一旦文章写好,我将回来在这里提供一个链接。与此同时,我已经在下面链接了我数据集准备脚本的最终版本,您可以跟随并了解在有限计算*台上处理较大数据集所需的内容。

[## nanoGPT/data/tinystories_char/prepare.py at master · oaguy1/nanoGPT

最简单、最快速的中型 GPT 训练/微调的代码库。 - nanoGPT/data/tinystories_char/prepare.py…

github.com](https://github.com/oaguy1/nanoGPT/blob/master/data/tinystories_char/prepare.py?source=post_page-----f2044cf5bca0--------------------------------)

此外,系列的第二部分已经上线!点击下面阅读!

## 学习变换器代码第一部分 — GPT *距离了解

深入研究通过 nanoGPT 生成预训练变换器

[towardsdatascience.com

XGBoost 如何原生支持类别特征?

原文:towardsdatascience.com/native-support-of-categories-in-xgboost-how-does-it-work-d359096bd003

Saupin GuillaumeTowards Data Science Saupin Guillaume

·发表于Towards Data Science ·6 分钟阅读·2023 年 3 月 22 日

--

图片由Jon Tyson提供,来源于Unsplash

XGBoost 和其他基于决策树的方法通过梯度提升进行训练,使用比较来做出决策。为类别定义一个比较操作符在数学上并不简单。

在这篇文章中,我们将解释可用的选项,详细说明它们的优缺点,并重点关注最*在 XGBoost(以及 LightGBM)中引入的对分类特征的原生支持。

如果你对梯度提升及其在决策树训练中的应用感兴趣,请考虑我的书:

[## 实用梯度提升:深入了解 Python 中的梯度提升

这本关于梯度提升方法的书面向希望深入了解的学生、学者、工程师和数据科学家……

amzn.to](https://amzn.to/3LDmbKM?source=post_page-----d359096bd003--------------------------------)

决策树

如下图所示,决策树基于比较来做出决策:

一个简单的决策树。图由作者提供。

例如,在这个简单的例子中,如果输入是一行数据,包含两列A=21B=111,则输出值将是权重4

这种决策树的局限性在于它只能处理数字作为特征。

处理分类特征的标准方法

然而,在数据科学中,经常会遇到分类特征。让我们看看现有的选项。

一热编码

处理类别值的一种常见方法是使用 独热编码。这里的想法是通过为每个可能的类别创建一列,将非数值类别值转换为数值值。

当类别适用于数据集的当前行时,相应的列被设置为 1,否则为 0。

以下代码片段展示了使用 pandas 进行独热编码的标准方法:

使用 Pandas 进行独热编码。由作者编写的代码。

独热编码的主要限制之一是,你将为数据集中每个不同的类别值添加相应的列。

同样重要的是,在训练和预测时必须使用相同的离散值,否则在预测过程中会缺少某些列。

GLMM 编码

另一种选择是使用编码,更具体地说是 GLMM 编码。这里的想法是通过模型将非数值类别转换为数字。

转换是通过 GLMM 完成的,即 广义线性混合模型

这里 广义 表示 GLMM 只是线性模型的一种推广,其中目标变量可以通过对数等函数进行变换。因此,利用相同的数学框架,可以对标准线性回归和逻辑回归进行建模。

混合 表示这些模型可以整合固定效应和随机效应,即预测变量的均值在一组观测中要么是固定的,要么是随机的。

应用于类别编码的情况时,GLMM 将捕捉每个类别的随机效应,并适配这种模型:

混合模型预测类别 I 的 Y。由作者提供的公式。

其中 Y_i 是 Mixel 模型为目标 Y 和类别 i 预测的值。μ 是 Y 的全局均值,表示固定效应,而 RE_ci 是由于类别 i 引起的随机效应。

因此 RE_ci 捕捉了类别的效应,这就是类别的编码值。

使用 pandascategorical_encoders 库,所有这些理论可以归纳为以下几行代码:

GLMM 编码与 Pandas。由作者编写的代码。

如果你感兴趣,categorical_encoders 库中的重要代码行是:

由作者编写的代码。

这正好训练了一个由上面的公式描述的模型。

XGBoost 原生支持

如上所述,可以通过将类别值转换为实际值(glmm 编码)或通过更改数据集结构并为每个可能的类别引入一列(独热编码)来改变类别值的属性。

但这似乎不是在标准的梯度提升结构中整合类别支持的最佳方式:决策树。

包括而非比较

确实,似乎自然地将比较替换为包含作为决策操作。即,与检查一个值是否大于或小于给定阈值不同,我们可以检查该值是否在给定的类别集合中。

这意味着我们可以用一个阈值来替代比较。

作者公式

作者

作者公式

基于我上一篇文章中的代码:

## DIY XGBoost 库,少于 200 行 Python

XGBoost 解释以及梯度提升方法和超参数调整,通过构建你自己的梯度提升库来实现...

towardsdatascience.com

这可以通过更新创建节点条件函数的方法来实现,以支持包含而不是比较:

使用类别时创建一个包含条件。作者代码。

穷尽生成可能的类别列表

在标准方法中,使用值作为分割条件,通过对考虑的列中的值进行排序并保持唯一值来生成候选分割。

根据这个分割值,评估左节点和右节点的增益。

当使用类别时,排序不再相关,候选分割条件不再是单一值,而是类别列表。

由于我们对优化增益的最佳类别分组方式没有先验知识,一种选择是生成所有可能的类别组合。

因此,根据我上一篇文章中的代码,我们提取了创建基于阈值的数值分割的_numerical_split代码,并引入了一个新方法_categorical_split来生成可能的组合:

为数值和类别类型生成候选分割。作者代码。

请注意,pandas 掩码是一种非常方便的方式来类似地处理这两种情况。

将所有内容汇总

完整代码,从头实现了决策树的梯度提升,并支持类别数据和数值数据,见下文:

作者的完整代码。

除了上述两个主要更改外,代码基本与我上一篇文章中的相同,但稍微更通用,以便同时支持数值数据和类别数据。

在提供的基本示例中,使用类别分割或数值分割得到相同的结果。当使用类别支持时,代码只会稍微慢一些,因为探索的组合数量很重要:

探索的组合。作者代码。

请注意,我们使用内部 pandas 代码 category 来区分数值数据和分类数据。

性能问题

上述实现按预期工作,并支持分类值。

然而,由于我们正在全面探索所有可能的类别组合,使用 python itertools.combinations 时,当唯一类别超过几个时,代码会变得非常慢。

实际上,对于一组 n 个分类值,k 的可能组合数由以下公式给出:

可能的组合数。公式由作者提供。

这就是为什么 XGBoost 和 LightGBM 使用启发式方法来大幅减少这个数量。更多细节可以在这里找到:

[## 分类数据 - xgboost 1.7.4 文档

注意:截至 XGBoost 1.6,此功能仍为实验性,功能有限。从 1.5 版本开始,XGBoost 具备了...

xgboost.readthedocs.io](https://xgboost.readthedocs.io/en/stable/tutorials/categorical.html?source=post_page-----d359096bd003--------------------------------#optimal-partitioning)

www.buymeacoffee.com/guillaumes0

结论

我强烈建议在使用 XGBoost 或 LightGBM 时使用对分类数据的内部支持。这不仅是一种更高效的内存和计算方法,而且提供了良好的精度。

它还简化了数据准备流程,并在训练和预测集不重叠时原生支持缺失值。

如果你对梯度提升及其在决策树中的应用感兴趣,请考虑阅读我的书:

[## 实用梯度提升:深入探讨 Python 中的梯度提升

这本关于梯度提升方法的书旨在为希望...

amzn.to](https://amzn.to/3LDmbKM?source=post_page-----d359096bd003--------------------------------) [## Guillaume Saupin

Guillaume Saupin,博士学位持有者及应用数学专家,在数学界因其...

www.buymeacoffee.com](https://www.buymeacoffee.com/guillaumes0?source=post_page-----d359096bd003--------------------------------)

使用 Spotify 的 Pedalboard 进行自然音频数据增强

原文:towardsdatascience.com/natural-audio-data-augmentation-using-spotifys-pedalboard-212ea59d39ce

包含现成的 Python 代码和预设

Max HilsdorfTowards Data Science Max Hilsdorf

·发表于Towards Data Science ·10 分钟阅读·2023 年 1 月 9 日

--

图片改编自户山 神奈.

作为数据科学家,处理音频数据常常是一项艰巨的任务。一个常见的问题是数据缺乏,这会影响构建有效模型(如语音识别、音乐推荐和语音转文本)的能力。通常,音频数据是敏感的,可能包含人声或受版权保护的音乐。最后,相比其他数据类型,音频数据是高维的,通常需要巨大的计算资源。因此,充分利用现有数据至关重要。音频数据增强是一种很好的方法来实现这一目标!

在本文中,我们将讨论以下主题:

  1. (音频)数据增强基础

  2. Spotify Pedalboard 简介

  3. 音频数据增强的最佳实践

  4. 利用现成代码和预设的高级用法

数据增强

数据增强是通过轻微修改数据集中部分或所有示例来增加训练数据总量的过程。如果做得正确,增强数据集可以有效地防止过拟合。这意味着,即使数据量相对较小,你的机器学习模型也很可能会产生更具普遍适用性的结果。数据增强通常仅应用于训练数据,因为如果将增强混入验证和测试数据中,度量指标将变得无效。

最重要的是,增强示例必须仍然足够类似于原始示例,以便被视为“同一类别”。这就是我们所说的自然分布内增强。以下是图像识别中的一个示例,数据增强被广泛应用:如果你旋转和*移猫的图像,你可以生成各种经过修改的图像,这些图像在技术上是“不同”的,但仍然明显是猫。

为数据增强而对猫的图像进行*移和旋转,这一灵感来源于 Suki Lau 的介绍文章。猫的图像由Alexander London提供。

如果你的增强示例看起来在模型部署时可能会遇到“现实世界”的示例,那就可以了。

这一经验法则很有用,但这对音频数据意味着什么呢?

音频数据增强

我有个好消息要告诉你:在机器学习中,音频数据通常表现为图像,例如谱图。那么它与猫的例子有多大不同呢?嗯,这里也有一个坏消息:*移或旋转这些图像都没有用。只需看看下面的图像:

应用在谱图上的旋转和*移。图像由作者提供。

你不需要任何信号处理方面的专业知识就能看出,*移和旋转后的谱图不是自然增强。你的模型在“现实生活”中永远不会看到*移/旋转的谱图,因此你不应使用这些进行训练。

有三种音频数据增强方法被广泛采用:

  1. 在将音频转换为图像表示后,使用遮罩来“隐藏”图像中的某些部分。

  2. 如果你的音频文件比你想用作模型输入的长度要长,你可以从单个音频文件中提取多个片段

  3. 对你的示例应用信号修改,如音频效果,以稍微改变它们。

在这三种方法中,我认为第三种选项最好,它能够可靠地产生所有类型音频数据的自然增强。然而,它需要大量的努力和细致的思考才能正确完成。这就是为什么我把它作为本文的主题。

无论是语音还是音乐,声音文件通常可以用音频效果如压缩器、均衡器或混响来编辑。实际上,这些是任何声音工程师的“基本功”。好消息是,这些效果在我们处理的数据中非常固有,因此特别适合数据增强。如果我增加了摇滚歌曲的整体混响,它仍然是一首摇滚歌曲,但生成的谱图可能看起来明显不同。

踏板板

吉他踏板板。图像由Frankie Lopez提供。

Pedalboard 于 2021 年 9 月相对较新发布,附有 介绍文章github 仓库。术语 “pedalboard” 源于电吉他手,他们通常使用多个吉他效果器,比如失真或混响,通常以脚踏板的形式连接到效果板上。尽管 pedalboard 显然是为音乐应用而构建的,但它也非常适合语音增强。

安装

你可以在终端中运行以下命令来安装 pedalboard。

pip install pedalboard

要使用 Pedalboard 的效果,你需要将音频文件加载到 Python 中,作为其波形的数组表示形式。“librosa”和“soundfile” 在这方面尤其有用。

pip install librosa
pip install soundfile

为了使 librosa 正常工作,你还需要下载并安装 FFmpeg将其添加到你的 PATH 环境变量** 中**。

加载音频

要加载音频文件,只需使用:

import librosa

audio, sr = librosa.load("path/to/file")

“librosa.load” 返回波形数组和音频采样率。我们将使用以下短音乐示例贯穿本文,以演示 pedalboard 的功能:

这是我在不久前制作的一段短小的放克风格的器乐片段。如果你想跟随操作,可以直接从 SoundCloud 下载音频文件。

应用单个效果踏板

首先,我们从 pedalboard 库中导入 “Pedalboard” 和 “Reverb”。即使我们只想应用一个效果,也需要首先实例化一个 “Pedalboard”,然后将我们的效果添加到其中。作为效果,我们将使用具有默认 pedalboard 设置的混响效果。

from pedalboard import Pedalboard, Reverb

# Create a pedalboard with one pedal
board = Pedalboard()
board.append(Reverb()) # instantiate with default parameters

# Apply pedal to audio signal
audio_reverbed = board(audio, sr)", audio_reverbed, sr)

有许多方法可以将音频波形导出为 .mp3 或 .wav 文件。这里是一种常见的方法:

import soundfile as sf

sf.write("path/to/file.wav", audio_reverbed, sr)

这是应用标准混响效果后的示例声音:

很容易听出,尽管混响效果已经相当强烈,但我们保留了相关的音乐参数和整体的曲目表现。

如果我们想要不同类型的混响效果怎么办?Pedalboard 允许我们为每个效果踏板使用自定义参数。例如,这里是如何创建一个比之前使用的混响效果更为突出的效果:

board = Pedalboard()
board.append(Reverb(room_size=0.15, damping=0.8, wet_level=0.9))

audio_reverbed = board(audio, sr)

如果你听下面的示例,你会发现混响效果非常突出,以至于掩盖了基础音乐。这是一个不自然增强的例子,至少对于大多数使用情况来说是这样。

应用多个效果踏板

我们可以使用更多效果并将它们组合成一个效果链。例如,PitchShift 会将整个音频的音高提高或降低,比如提升两个半音。Compressor 会将信号中较响的部分降低,从而使声音的动态范围减少,更加紧凑。除此之外,我们还可以再次添加具有自定义参数的混响效果。

from pedalboard import Pedalboard, Compressor, PitchShift, Reverb

# Create board with multiple pedals
board = Pedalboard(
    [
        PitchShift(semitones=2),
        Compressor(threshold_db=-20, ratio=2),
        Reverb(wet_level=0.3)
    ]
)

# Apply board
audio_multieffect = board(audio, sr)

这是结果音频的声音示例:

我们是否已经对效果处理过度了?当然,这需要在机器学习任务的背景下讨论。如果我想检测歌曲的调性,显然我们不应该改变音高,以保持相同的音高标签。如果我们尝试检测一首曲目的风格,它仍然可以明显被识别为——制作不佳的——放克曲目。

问题和最佳实践

图片改编自 Olia Danilevich

到此为止,应该可以清楚地看到,踏板板非常适合自然音频数据增强。然而,在实际使用案例中,还有一些额外的事项需要考虑。

“好”的预设

如果我们想保持增强效果自然,我们需要找到一个(或多个)预设,能够可靠地创建自然的增强效果,适用于我们数据集中的所有示例。例如,为重金属曲目添加更多失真可能是一个可行的选择,但将古典音乐中的小提琴失真处理真的自然吗?

增强多样性

当然,我们可以对所有我们想增强的示例使用相同的效果链。然而,对每个示例在效果链中引入一些随机性有两个显著的优点:

  1. 你可以创建相同示例的多个增强效果,它们会有所不同。

  2. 模型学习处理更多样化的效果,这可以提高其鲁棒性。

在保持增强自然性的同时使用随机效果链是这里的关键挑战

速度

如果我们想处理成千上万的音频示例,我们需要考虑一个高效的实现方式。我们当然可以增强每个文件并将增强后的示例存储为音频文件。然而,我们真的有兴趣将增强效果存储在某个地方吗?如果没有,我们可以在数据处理脚本或管道中计算增强效果。例如,如果我们想计算每个音频示例的梅尔谱图,我们可以

  1. 将音频加载到 numpy 数组中。

  2. 随机生成一个踏板板并将其应用于数组。

  3. 计算原始音频和增强示例的梅尔谱图。

  4. 将两个梅尔谱图都存储在训练数据集中。

这样,不会浪费时间在导出和加载我们不会再使用的音频文件上。

踏板板增强 GitHub 仓库

截图由作者提供。

为了帮助你开始进行音频增强,我写了一些代码并将其发布在这个 GitHub 仓库中。该仓库包括

  • 一个实现随机、自然效果链的音频增强模块。

  • 示例脚本用于将这些随机、自然的增强应用于单个文件或作为梅尔谱图特征提取管道的一部分

  • 从 GTZAN 数据集中获得了一些示例音频文件,你可以用来玩弄增强效果。

虽然我无法在这篇文章中详细讲解代码,但我仍然想展示如何将随机效果链应用于单个音频文件。有关进一步的指导,请查看仓库中的“README.md”文件。

构建随机效果链

首先,我们将音频增强模块(库中的“code/audio_augmentation.py”)的路径添加到 sys.path 中,并导入相关功能。

import sys
sys.path.append("../code/")
from audio_augmentation import roll_pedal, pedal_dict, process_track

接下来,我们通过指定正确顺序中希望使用的效果来定义效果链。此外,我们为每个踏板分配一个概率值,表示每次踏板板滚动时该踏板被激活的可能性。如果你希望每次都应用所有踏板,请将所有概率设置为 1。

pedal_config = [("compressor", .3),
                ("chorus", .3),
                ("reverb", .3),
                ("distortion", .3),
                ("lowpassfilter", .3),
                ("highpassfilter", .3),
                ("pitchshift", .3)
                ]

接下来,我们定义一个备用效果链。这在没有踏板通过随机滚动的情况下需要,即没有效果被选择。在这种情况下,模块将使用这个备用效果链作为默认设置。

from pedalboard import Compressor, PitchShift, HighpassFilter

fallback = [HighpassFilter(cutoff_frequency_hz=300),
            PitchShift(semitones=1),
            Compressor(threshold_db=-10, ratio=1.3)]

滚动踏板板

要“滚动”我们的踏板板,我们可以使用这段代码,它调用了增强模块中的“roll_pedal”函数:

pedal_rolls = [roll_pedal(pedal=pedal_dict[pedal]["pedal"],
                          param_dict=pedal_dict[pedal]["param_dict"],
                          pedal_prob=prob,
                          random_seed=None)
               for pedal, prob in pedal_config]

请注意,“pedal_dict”是从增强模块导入的。它保存了参数范围的信息,并为每个踏板应用了随机设置。

应用随机踏板板

现在,最后,我们可以加载我们的音频文件,并使用增强模块中的“process_track”函数将随机踏板板应用于它。

audio, sr = librosa.load("path/to_file")
audio_processed = process_track(
  audio, sample_rate=sr,
  pedals=pedal_rolls, fallback=fallback
)

让我们听听这段代码会对我们的示例音轨做什么!

这个效果有轻微的合唱效果以及一些轻微的失真。它听起来明显不同于原始版本,但仍然非常接*。在此期间,让我们再尝试另一种随机增强滚动!

这里,我们对信号应用了压缩器、低通滤波器、高通滤波器和音高移位。这次差异更为明显。然而,音轨的自然性仍然得到保持。

结论

在这篇文章中,我们

  • 学习了数据增强是什么以及为何使用它。

  • 讨论了自然增强为何如此重要。

  • 看到音频数据增强与其他数据类型的增强有所不同。

  • 获得了将踏板板库应用于音频信号的实际经验。

  • 概述了音频数据增强的最佳实践。

  • 初步了解了一个基于踏板板的音频增强模块。

希望你学到了一些新知识,并觉得这篇文章有用!

如果你对其他形式的音频数据增强感兴趣,可以查看我写的 这篇文章,讲述了如何使用“分而治之”方法进行音乐类型分类。

自然语言基础——情感分析、机器翻译和命名实体识别的介绍与语言模型实现

原文:towardsdatascience.com/natural-language-fundamentals-intro-implementation-of-sentiment-analysis-machine-translation-c79662e52624

自然语言领域的多语言建模

Farzad MahmoodinobarTowards Data Science Farzad Mahmoodinobar

·发布在 Towards Data Science ·阅读时间 9 分钟·2023 年 3 月 20 日

--

图片由 RetroSupply 提供,来自 Unsplash

我们人类使用语言、声音、手势和符号,以不同的形式(如演讲、写作和标志)传达复杂的概念和抽象。随着计算机的出现,为了利用这些强大的机器,我们不得不想出让计算机理解人类交流和现有知识库的方法。因此,自然语言处理(NLP)、理解(NLU)和生成(NLG)这三个人工智能的分支应运而生。这三个领域的界限有时并不清晰,而整体自然语言空间涵盖了当今计算机和数据科学世界中的各种应用。最常见的应用包括(I)情感分析,(II)机器翻译和(III)命名实体识别(NER),我们将在本文中对这些应用进行定义和实现。

[## 通过我的推荐链接加入 Medium

阅读 Farzad(以及 Medium 上其他作者)的每一个故事。您的会员费用直接支持 Farzad 和其他人……

medium.com](https://medium.com/@fmnobar/membership?source=post_page-----c79662e52624--------------------------------)

为了实现这三项任务,我们将利用现有的“预训练语言模型”。因此,让我们首先了解什么是语言建模。我建议浏览一下“语言建模”部分,但如果你主要对应用和/或实现感兴趣,可以跳过这部分。

语言建模

语言建模包括使用统计和概率的方法,结合机器学习架构(如特别是深度神经网络)来确定一串文字中词语序列出现的可能性,例如一个句子。基于计算出的概率,可以做出某些决策——例如,一个模型可以生成对用户提供的提示的字符串/响应(如 ChatGPT),执行文本分类以确定某个词是否为名词、动词等。由于如今我们周围有大量的文本数据,这些语言模型通常在大量文本数据上进行训练。因此,这些模型也被称为大语言模型。此时你可能会想这与我们的帖子有何关联——我们正要到达这里。这些预训练的语言模型可以进一步训练(即微调)以执行特定任务,如情感分析、机器翻译和命名实体识别,我们今天将详细探讨这些任务。深入了解语言模型的架构和训练策略超出了本文的意图,但如果你对这一主题感兴趣,可以访问以下帖子:

## 预训练模型在自然语言处理中的应用

预训练模型基础及其下游任务。

medium.com

既然我们对自然语言领域和语言建模有所了解,接下来就进入使用这些模型的有趣部分吧!

1. 情感分析

识别文本情感的任务,例如判断它是积极的、消极的还是中立的,称为情感分析。它被用于社交媒体监控、客户反馈分析和产品评论分析等应用中。可以想象,这对许多公司来说非常有用。例如,一家大型在线零售公司无法分配足够的人力资源来手动阅读关于各种产品的所有评论。相反,他们可以对评论运行情感分析模型,并分析结果,从而节省时间和成本。接下来,让我们看看如何实现这一点。

1.1. 情感分析 — 实现

在这个示例中,我们首先从 Transformers 库中加载一个预训练的语言模型。接着,我们使用模型从输入句子中生成情感分析。最后,我们在两个不同的句子上测试它,一个是积极的,另一个是消极的,以验证模型的性能。以下是我们将使用的两个句子:

  • I loved this movie!,我们期望模型将其分类为“积极”情感

  • I did not like this movie.,我们期望模型将其分类为“消极”情感

让我们看看它是如何工作的!

# Import libraries
from transformers import pipeline

# Load the pre-trained model
nlp = pipeline('sentiment-analysis', model='distilbert-base-uncased-finetuned-sst-2-english')

# Define the function to perform sentiment analysis
def sentiment_analyzer(input_text):
    # Perform sentiment analysis
    result = nlp(input_text)[0]

    # Return results
    return f"'{input_text}' has a {result['label']} sentiment, with a score of {round(result['score'], 4)}!\n"

# Define example sentences
sentence_1 = "I loved this movie!"
sentence_2 = "I did not like this movie."
sentence_list = [sentence_1, sentence_2]

# Analyze the sentiment of each sentence
for sentence in sentence_list:
    print(sentiment_analyzer(sentence))

结果:

结果看起来非常好,如我们所期待的。如果你对情感分析有更深入的兴趣,以下帖子适合你:

情感分析——简介与实现

使用 NLTK、scikit-learn 和 TextBlob 进行情感分析

towardsdatascience.com

接下来,让我们讨论机器翻译。

2. 机器翻译

使用计算机将文本从一种语言翻译到另一种语言的任务称为机器翻译。对大多数用户来说,最著名的例子是 Google 翻译——Google 翻译所做的就是机器翻译!应用非常广泛。例如,人们可以阅读和理解其他语言的信息。

2.1. 机器翻译——实现

要实现机器翻译,我们将使用由Facebook AI开发的 mBART-50,它来自Transformers库,这是一个用于机器翻译的预训练语言模型。步骤与我们在情感分析中做的非常相似,具体如下:

  1. 按如下方式安装 Transformers:pip install transformers

  2. 导入库

  3. 加载预训练模型

  4. 通过模型运行示例句子并返回结果

mBART-50 的有趣之处在于它是一个多语言机器翻译模型,这意味着它可以进行不同语言之间的翻译。让我们测试一下这个功能,将一个句子翻译成 6 种不同的语言,使用一个模型!

提示: 我强调了这个语言模型的多语言特性,因为历史上,神经机器翻译模型只能从一种特定语言翻译到另一种特定语言。例如,我们需要为从英语到法语的翻译训练一个特定的模型,并为从英语到德语的翻译训练另一个模型。另一方面,这些多语言机器翻译模型可以使用一个模型在多种语言之间进行翻译,这非常令人印象深刻!

在下面的代码块中,我们导入库,加载模型,然后创建 translator 函数,该函数接受三个参数:(1)一个句子(source_sentence),(2)提供的句子的语言(source_language)以及(3)我们希望翻译成的语言(target_language)。然后 translator 按照指示返回翻译结果。

# Import library
from transformers import MBartForConditionalGeneration, MBart50TokenizerFast

# Load model and tokenizer
model = MBartForConditionalGeneration.from_pretrained("facebook/mbart-large-50-many-to-many-mmt")
tokenizer = MBart50TokenizerFast.from_pretrained("facebook/mbart-large-50-many-to-many-mmt")

def translator(source_sentence, source_language, target_language):
    # Encode sentence
    tokenizer.src_lang = source_language
    input_ids = tokenizer(source_sentence, return_tensors="pt").input_ids

    # Translate sentence
    output_ids = model.generate(input_ids, forced_bos_token_id=tokenizer.lang_code_to_id[target_language])
    translation = tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0]

    # return translation
    return translation

接下来,让我们通过将 Multilingual machine translation is impressive! 翻译成法语、西班牙语、意大利语、德语、简体中文和日语来测试我们的函数。

# Define sentence to be translated
original_sentence = 'Multilingual machine translation is impressive!'

# Define source language
english = "en_XX"

# Define target languages
french = "fr_XX"
spanish = "es_XX"
italian = "it_IT"
german = "de_DE"
simplified_chinese = "zh_CN"
japanese = "ja_XX"

# Create a list of target languages
target_list = [french, spanish, italian, german, simplified_chinese, japanese]

# Create a prompt list of lists
prompt_list = []

for target in target_list:
    prompt_list.append([original_sentence, english, target])

# Create translations
print(f"Generating machine translations for: \n'{original_sentence}'\n")

for i in enumerate(prompt_list):
    translation = translator(source_sentence=i[1][0], source_language=i[1][1], target_language=i[1][2])
    print(f"{i[1][2]}:")
    print(f"{translation}\n")

结果:

我们可以在结果中看到目标语言的翻译!我个人不讲这些语言,但我使用 Google 翻译验证了这些翻译,结果似乎是准确的!

最后但同样重要的是,让我们看一下命名实体识别。

3. 命名实体识别(NER)

识别和分类文本中的命名实体,并将其分类到预定义类别中的任务称为命名实体识别,简称 NER。这些类别的一些示例包括:人名、地点、日期、组织、数字等。你可能会想,为什么我们需要这样的模型。NER 在自然语言领域有许多应用。例如,Visa、美国运通、亚马逊等可以使用 NER 识别和屏蔽客户通信中的敏感信息,以保护客户的敏感信息,如出生日期和信用卡信息。另一种应用是社交媒体公司(如 Meta)识别评论/帖子中的地点和个人姓名,并将其用于内容推荐。

现在我们了解了 NER 的基本概念,让我们实现它并查看结果。

3.1. NER — 实现

在这个例子中,我们将使用 spaCy 的预训练模型进行命名实体识别(NER)。实现过程非常简单。我们将按照以下步骤进行:

  1. 安装并下载所需的数据

  2. 导入库

  3. 加载预训练任务,包括 NER

  4. 通过模型运行一个示例句子并返回结果

如果你需要安装 spaCy 并下载数据,你可以使用以下命令 (source):

pip3 install spacy
python -m spacy download en_core_web_sm

我通过命令行界面完成了上述安装。操作步骤很简单:(I)打开终端,然后(II)运行上述两行命令。如果你需要更详细的命令行教程,请随时查看以下内容:

## 命令行界面(CLI)教程 — 高级用户如何与计算机互动

CLI 入门以提高与计算机互动的生产力

towardsdatascience.com

接下来,让我们对以下句子实施和应用命名实体识别(NER):Farzad wrote this Medium article in March 2023, using an Apple laptop, on a Jupyter notebook!

# Import library
import spacy

# Load English tokenizer, tagger, parser and NER
nlp = spacy.load("en_core_web_sm")

# Define example sentence
sentence = "Farzad wrote this Medium article in March 2023, using an Apple laptop, on a Jupyter notebook!"

# Apply NER
doc = nlp(sentence)

# Analyze syntax
print(f"Noun phrases:{[chunk.text for chunk in doc.noun_chunks]}\n")
print("Verbs:", [token.lemma_ for token in doc if token.pos_ == "VERB"])
print("")

# Find named entities, phrases and concepts
for entity in doc.ents:
    print(entity.text, entity.label_)

结果:

这非常有趣!让我们来谈谈结果。第一行识别了名词——我个人不完全同意所有这些,但仍然,这非常令人印象深刻!第二行正确地识别了“write”和“use”作为动词,第三块识别了“Medium”作为地点,“March 2023”作为日期,“Apple”作为组织(这一点很有趣,因为 apple 也可能是水果的名称,但模型根据句子的上下文识别了公司名称)和“Jupyter”作为一个人(这一点需要一些改进)。有方法进一步训练这些预训练模型,以确保 NER 在每个使用场景中更准确,但我们想要表达的重点是展示这些预训练语言模型如何以合理的准确性完成复杂任务,如 NER。

结论

在这篇文章中,我们简要地介绍了自然语言处理(NLP)、理解(NLU)和生成(NLG)的世界,并通过引入和实施自然语言领域中的一些常见任务,使用语言建模来理解它们的重要性。然后,我们介绍了(I)情感分析,(II)机器翻译和(III)命名实体识别(NER)的语言模型实现,并查看了这些强大的预训练语言模型在多种语言中的令人印象深刻的结果。

感谢阅读!

如果你觉得这篇文章对你有帮助,请关注我在 Medium并订阅以接收我最新的文章!

(所有图片,除非另有说明,均由作者提供。)

自然语言处理初学者指南

原文:towardsdatascience.com/natural-language-processing-for-absolute-beginners-a195549a3164

用 10 行 Python 代码解决复杂的 NLP 任务

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 9 月 2 日

--

作者提供的图片(使用 Craiyon 生成)

NLP(自然语言处理)通常被认为是计算机科学中的复杂领域。像 SpaCy 或 NLTK 这样的框架体积庞大,通常需要一些学习。但借助开源的大型语言模型(LLMs)和现代 Python 库,许多任务可以更容易地解决。而且,甚至在几年前只有科学论文中才能找到的结果,现在也可以通过仅仅 10 行 Python 代码实现。

话不多说,我们开始吧。

1. 语言翻译

你是否曾想过 Google 翻译是如何工作的?Google 使用一个在大量文本上训练的深度学习模型。现在,借助 Transformers 库,不仅在 Google 实验室,还能在普通 PC 上完成这项工作。在此示例中,我将使用一个预训练的 T5-base(文本到文本转换器)模型。该模型最初在原始文本数据上训练,然后在诸如(“将英语翻译成德语:这所房子很棒”,“Das Haus ist Wunderbar”)的源-目标对上微调。在这里,“将英语翻译成德语”是一个前缀,告诉模型该做什么,而短语是模型应该学习的实际上下文。

重要警告。大型语言模型的体积确实很大。此示例中使用的 T5ForConditionalGeneration 类将自动下载约 900 MB 大小的“t5-base”模型。在运行代码之前,请确保有足够的磁盘空间,并且流量没有限制。

可以在 Python 中使用预训练的 T5 模型:

from transformers import T5Tokenizer, T5ForConditionalGeneration

preprocessed_text = "translate English to German: the weather is good"
tokenizer = T5Tokenizer.from_pretrained('t5-base',
                                        max_length=64,
                                        model_max_length=512,
                                        legacy=False)
tokens = tokenizer.encode(preprocessed_text,
                          return_tensors="pt",
                          max_length=512,
                          truncation=True)

model = T5ForConditionalGeneration.from_pretrained('t5-base')
outputs = model.generate(tokens, min_length=4, max_length=32)

print("Result:", tokenizer.decode(outputs[0], skip_special_tokens=True))

#> Result: Das Wetter ist gut.

在这里,一个 T5Tokenizer 类将源字符串转换为数字形式;这个过程叫做 分词。在我们的例子中,一个“将英语翻译成德语:天气很好”的文本将被转换为 [13959, 1566, 12, 2968, 10, 8, 1969, 19, 207, 1] 数组。“生成”方法实际执行任务,最后,分词器进行反向转换。作为输出,我们将得到结果“Das Wetter ist gut”。

我们能否使这段代码更简洁?实际上,我们可以。借助 Transformer 的 pipeline 类,我们可以创建一个抽象管道,只需 2 行 Python 代码即可完成这个任务:

from transformers import pipeline

translator = pipeline("translation_en_to_de", model="t5-base")
print(translator("the weather is good"))

#> [{'translation_text': 'Das Wetter ist gut.'}] 

出于自学的目的,我通常更喜欢第一种方法,因为它更容易理解“幕后”的情况。但对于“生产”目的,第二种方法更具灵活性,它还允许在不更改代码的情况下使用不同的模型。

2. 摘要生成

文本摘要的目标是将文档转换为简短版本,这显然如果手动完成会花费时间。令人惊讶的是,T5 模型也可以做到这一点;我们唯一需要更改的是前缀:

body = '''Obviously, the lunar surface is covered with craters, left 
from previous collisions of meteorites with the Moon. Where does math go? 
While a meteorite collision is a random event, its frequency 
obeys probability theory laws. There is no atmosphere on the Moon's 
surface, no erosion, and no wind. Therefore the lunar surface is an 
ideal "book" in which the events of the last tens of thousands of 
years are recorded. By studying the Moon, we can calculate how often 
such objects fall on its surface.

A study of the lunar surface with high-resolution cameras is ongoing. 
It has been estimated that at least 220 new craters have formed on the 
Moon over the past 7 years. This check is also vital because 
these calculations can help assess the danger to the Earth.'''

preprocessed_text = f"summarize: {body}"

tokenizer = T5Tokenizer.from_pretrained('t5-base', 
                                        max_length=256,
                                        model_max_length=512,
                                        legacy=False)
tokens = tokenizer.encode(preprocessed_text,
                          return_tensors="pt",
                          max_length=256,
                          truncation=True)

model = T5ForConditionalGeneration.from_pretrained('t5-base')
outputs = model.generate(tokens,
                         min_length=4,
                         max_length=64)

print("Result:", tokenizer.decode(outputs[0], skip_special_tokens=True))

#> the lunar surface is an ideal "book" in which the events of the last 
#> tens of thousands of years are recorded. by studying the Moon, we can 
#> calculate how often such objects are falling on its surface.

正如我们所见,结果非常准确。

与第一个例子一样,使用管道可以为相同的任务生成更简短的代码:

summarizer = pipeline("summarization", model="t5-base", tokenizer="t5-base")
summarizer(body, min_length=4, max_length=64)

#> [{'summary_text': 'the lunar surface is an ideal "book" in which the 
#>  events of the last tens of thousands of years are recorded . it has been
#>  estimated that at least 220 new craters have formed on the Moon over the 
#>  past 7 years .'}]

读者可能对使用 “t5-base” 模型可以完成的其他任务感兴趣。我们可以轻松地将它们全部打印出来:

for prefix in model.config.task_specific_params:
    print(f"{prefix}: {model.config.task_specific_params[prefix]}")

#> summarization: 
#>   {'early_stopping': True, 'length_penalty': 2.0, 'max_length': 200, 'min_length': 30, 'no_repeat_ngram_size': 3, 'num_beams': 4, 'prefix': 'summarize: '}
#> translation_en_to_de: 
#>   {'early_stopping': True, 'max_length': 300, 'num_beams': 4, 'prefix': 'translate English to German: '}
#> translation_en_to_fr: 
#>   {'early_stopping': True, 'max_length': 300, 'num_beams': 4, 'prefix': 'translate English to French: '}
#> translation_en_to_ro: 
#>   {'early_stopping': True, 'max_length': 300, 'num_beams': 4, 'prefix': 'translate English to Romanian: '}

3. 问答

大型语言模型还可以提供另一种有趣的功能,即在给定的上下文中回答问题。我将使用与之前示例相同的文本:

body = '''Obviously, the lunar surface is covered with craters, left 
from previous collisions of meteorites with the Moon. Where does math go? 
While a meteorite collision is a random event, its frequency 
obeys probability theory laws. There is no atmosphere on the Moon's 
surface, no erosion, and no wind. Therefore the lunar surface is an 
ideal "book" in which the events of the last tens of thousands of 
years are recorded. By studying the Moon, we can calculate how often 
such objects fall on its surface.

A study of the lunar surface with high-resolution cameras is ongoing. 
It has been estimated that at least 220 new craters have formed on the 
Moon over the past 7 years. This check is also vital because 
these calculations can help assess the danger to the Earth.'''

question_answerer = pipeline("question-answering",
                             model='distilbert-base-cased-distilled-squad')

result = question_answerer(question="Which surface has collision with meteorites",
                           context=body)
print(f"Answer: '{result['answer']}', score: {round(result['score'], 4)}, start: {result['start']}, end: {result['end']}")

#> Answer: 'the Moon', score: 0.4401, start: 93, end: 101

result = question_answerer(question="How many craters were formed",
                           context=body)
print(f"Answer: '{result['answer']}', score: {round(result['score'], 4)}, start: {result['start']}, end: {result['end']}")

#> Answer: 'at least 220', score: 0.5302, start: 600, end: 612

result = question_answerer(question="Is there atmosphere on the moon",
                           context=body)
print(f"Answer: '{result['answer']}', score: {round(result['score'], 4)}, start: {result['start']}, end: {result['end']}")

#> Answer: 'There is no', score: 0.2468, start: 220, end: 231

在这种情况下,我使用了一个 distilbert-base-cased-distilled-squad 模型,模型大小为 261 MB。正如我们所见,该模型不仅提供了答案,还从原始文本中检索了位置,这对验证结果很有帮助。

4. 语言生成

另一个有趣的过程是语言生成。对于这个例子,我将使用一个 GPT-2 模型。这显然不是我们今天拥有的最新 GPT 模型,但 GPT-2 是免费提供的,并且其文件大小为 548 MB,足够在普通 PC 上运行。

让我们看看它是如何工作的:

from transformers import GPT2Config, GPT2Model,
                         TFGPT2LMHeadModel, GPT2Tokenizer

tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
input_ids = tokenizer.encode("I am going to say",
                             return_tensors='tf')
model = TFGPT2LMHeadModel.from_pretrained("gpt2",
                                  pad_token_id=tokenizer.eos_token_id)
output = model.generate(input_ids,
                        max_length=128,
                        early_stopping=True,
                        do_sample=True,
                        top_k=20)
print("Output:", tokenizer.decode(output[0], skip_special_tokens=True))

#> Output: I am going to say something. It is very hard for people to 
#> believe me because I do not have to speak English. But the fact that 
#> you are writing a book on it is one thing. You have to have this 
#> knowledge which is not from what you read; it is from an educated...

同样,可以使用更简短的代码通过管道完成:

from transformers import pipeline

generator = pipeline('text-generation', model='gpt2')
generator("I am going to say", max_length=128, num_return_sequences=1)

#> I am going to say this: I have had several bad experiences with the 
#> internet in the years since my first connection. While there ...

实际上,我在这些文本中看不到太多意义,但从语法角度来看,它们足够好,对于某种自动化或单元测试,它们可能是有用的。有趣的是,GPT-2 模型于 2019 年发布。为了好玩,我向 GPT-3.5(发布于 2022 年)提出了相同的问题,“请继续我将要说的短语”,并得到了以下答案:

Am going to say that communication is a vital skill in today's interconnected
world. Whether it's expressing your thoughts and ideas, building relationships,
or resolving conflicts, effective communication plays a central role in almost
every aspect of our lives. It's not just about the words we use but also our
tone, body language, and the context in which we communicate.

这个结果要好得多;这三年取得了巨大的进步。但显然,即使 GPT-3.5 模型在今天发布于公共领域,也不可能在普通电脑上运行。

5. 情感分析

之前的示例主要是为了娱乐,但情感分析对于商业来说更为重要。情感分析是分析给定文本中的情感并找出主观意见的过程。对于网络商店、流媒体*台和其他许多用户可以发布评论的服务,这可能特别重要。

对于这个测试,我将使用一个distilbert-base-uncased-finetuned-sst-2-english模型:

from transformers import pipeline

sentiment_pipeline = pipeline("sentiment-analysis",
                         model='distilbert-base-uncased-finetuned-sst-2-english')
data = ["It was not bad",
        "I expected to love it but I was wrong"]
sentiment_pipeline(data)

#> [{'label': 'POSITIVE', 'score': 0.9995607733726501},
#>  {'label': 'NEGATIVE', 'score': 0.997614860534668}]

我故意尝试使用不易分析的短语,但模型给出了正确的答案。显然,自然语言是非常灵活的,仍然可能出现导致错误结果的文本。例如,这个模型对短语“我期待它很糟糕,但我错了”给出了错误的回答。同时,(体积大得多的)GPT-3.5 模型能够正确解析。另一方面,考虑到 DistilBERT 模型的大小仅为 268 MB 且可以免费使用(模型拥有 Apache 2.0 许可证),结果还是相当不错的。读者也可以尝试其他开源模型,并选择最适合他们需求的模型。

6. 命名实体识别(NER)

自然语言处理的另一个有趣部分是“命名实体识别”。这是从非结构化文本中提取实体,如名称、地点、日期等的过程。对于这个测试,我将使用一个bert-base-NER 模型(文件大小为 433 MB)。

让我们考虑一个例子:

from transformers import pipeline

body = "Hi, my name is Dmitrii. I am in London, I work in Super Company, " \
       "I have a question about the hotel reservation."

ner = pipeline("ner",
               model="dslim/bert-base-NER",
               aggregation_strategy='average')
ner(body)

#> [{'entity_group': 'PER',
#>  'score': 0.7563127,
#>  'word': 'Dmitrii',
#>  'start': 15,
#>  'end': 22},
#> {'entity_group': 'LOC',
#>  'score': 0.99956125,
#>  'word': 'London',
#>  'start': 32,
#>  'end': 38},
#> {'entity_group': 'ORG',
#>  'score': 0.99759734,
#>  'word': 'Super Company',
#>  'start': 50,
#>  'end': 63}]

正如我们所见,该模型能够正确确定文本中提到的主要实体,如名称、地点和公司。

7. 关键词提取

在最后一个例子中,我们测试了 NER,但不是所有的参数都从文本中提取出来了。一个单独的关键词提取算法对于同一任务也可能很有用。对于这个例子,我将使用KeyBERT

from keybert import KeyBERT

body = "Hi, my name is Dmitrii. I am in London, I work in Super Company, " \
       "I have a question about the hotel reservation."

kw_model = KeyBERT()
keywords = kw_model.extract_keywords(body,
                                     keyphrase_ngram_range=(1, 1),
                                     diversity=0.8,
                                     stop_words=None)
print(keywords)

#> [('reservation', 0.5935), ('hotel', 0.5729), ('london', 0.2705), 
#>  ('dmitrii', 0.2), ('company', 0.1817)]

正如我们所见,关键词提取作为 NER 的补充也很有用,它可以从相同的短语中提取一些额外的数据。

结论

在这篇文章中,我测试了不同的自然语言处理(NLP)算法。正如我在文章开头承诺的那样,通过现代库的帮助,复杂的任务可以在 10 行 Python 代码中解决。还要提到的是,这些代码都可以在本地运行,无需任何 API 订阅或密钥。最后但同样重要的是,我希望读者能看到 NLP 也可以很有趣。

感谢阅读。如果你喜欢这个故事,可以随时订阅Medium,你将会在我的新文章发布时收到通知,并且可以全面访问其他作者的成千上万篇故事。

自然语言处理不仅仅是聊天机器人

原文:towardsdatascience.com/natural-language-processing-is-about-more-than-chatbots-4ff7faf1363c?source=collection_archive---------8-----------------------#2023-03-02

TDS EditorsTowards Data Science TDS 编辑

·

关注 发表在 Towards Data Science · 以 新闻通讯 形式发送 · 3 分钟阅读 · 2023 年 3 月 2 日

--

在我们集体注意力仍然集中在聊天机器人上时,很容易忘记自然语言处理(NLP)领域的广阔和多样性。从翻译到文本分类及其他,数据科学家和机器学习工程师们正在进行(并且仍在进行)一些令人兴奋的项目,这些项目的名称既不以 C 开头,也不以 T 结尾。

为了让这些子领域重新引起关注,我们很高兴分享我们*期的一些 NLP 收藏。它们将吸引任何喜欢处理文本数据的人——也包括那些对学习和实验感兴趣的数据从业者。

  • 深入挖掘 NLP 项目的关键要素Erwin van Crasbeek的详尽讲解是初学者和资深专家的完美起点。它涵盖了自然语言处理的历史和基本概念,然后解释了 Erwin 的荷兰语问答机器学习模型的内部工作原理。

  • 语言检测是如何工作的? 如果你曾经使用过在线翻译工具,你可能体验过那种神奇的瞬间,工具识别了你输入文本的语言。Katherine Munro 将 Python、NLTK(自然语言工具包)*台和一点统计学结合在一起,揭示了这一魔法背后的过程。

图片由 Ross SneddonUnsplash 提供

  • 不要被文本分类器的复杂性吓到。学习如何构建文本分类器“可能会有点棘手,”Lucy Dickinson 说。这正是 Lucy 的 10 步指南如此有用的原因:它将一个潜在的复杂过程分解为明确和清晰的任务(以及你开始自己动手所需的所有代码)。

  • 为特定 NLP 任务找到合适数据的艺术。在大多数机器学习管道中,成功与失败的关键在于你使用的数据质量。Benjamin Marie 指出,对于机器翻译项目,我们不仅要考虑质量,还要考虑适用性;此外,真正知道如何从现有数据中挤出尽可能多的价值也很有帮助。

二月飞逝得如此之快;这里有一些我们还没来得及突出显示的亮点,但它们值得你花时间了解,因为我们正轻轻迈入三月。

  • 如果你既不是深度学习新手也不是专家,千万不要错过Leonie Monigatti的新中级模型微调指南。

  • 对于语言模型局限性的清晰探讨,尤其是它们取代搜索引擎的潜力,我们热情推荐Noble Ackerson的首篇 TDS 文章。

  • 想要深入了解去噪扩散模型的工作原理?Wei Yi的新深度探讨是你所需的完整资源。

  • 如果你刚开始处理地理空间数据,Eugenia Anello的最新贡献是 GIS 实用入门(以及关键术语和概念的实用指南)。

  • 是否厌倦了一遍遍使用相同的软件包?Miriam Santos邀请你在可视化和 EDA 工作流中尝试五个开源 Python 包。

感谢你本周的时间和支持!如果你喜欢我们发布的工作(并且想要访问所有内容),请考虑成为 Medium 会员

直到下一个 Variable,

TDS 编辑

行业特定 AI 的导航:从过渡性英雄到长期解决方案

原文:towardsdatascience.com/navigating-industry-specific-ai-from-transitional-heroes-to-long-term-solutions-67e41a84b207

该图像由 Midjourney 支持制作。

策略、洞察与行业特定大语言模型的演变

Serena Xu数据科学前沿 Serena Xu

·发表于数据科学前沿 ·7 分钟阅读·2023 年 9 月 4 日

--

随着人工智能(AI)领域的不断发展,我们正目睹一个日益增长的趋势:专为特定行业量身定制的大语言模型(LLMs)的兴起。这些行业特定的 LLMs 不仅适应了特定领域的专业术语和背景,还提供了定制的 AI 解决方案,以应对该行业内的独特挑战。例如,在医疗领域,一个专业的 LLM 可以加速药物研究与发现,而在金融领域,相应的模型可以迅速解码复杂的投资策略。

在这种背景下,所谓的“行业大模型”可以基本理解为“应用于特定行业的一般大模型的扩展”。这里有两个核心概念需要强调:第一个是“一般大模型”,第二个是“行业特定数据”。

一般大模型的真正价值不仅在于其庞大的参数数量,更在于其在多个领域的广泛适用性。这种跨领域的普遍性不仅增强了模型的适应性,还在模型向更“通用”的方向发展时产生了独特的能力。因此,单纯使用行业特定数据来训练模型是一种短视的方法,这与一般大模型的核心理念“普遍性”基本相悖。

对于行业特定的数据,主要有两种应用方式。第一种是直接使用这些数据对一般的大型模型进行微调或继续训练。第二种方法利用提示或外部数据库,利用一般大型模型的“上下文学习”能力来解决特定行业问题。这两种方法各有优缺点,但它们都有一个共同的目标,即利用一般大型模型的能力,更准确地解决行业特定的挑战。

*衡即时利益与长期愿景

一般的大型语言模型在两个方面表现出显著的优势:“知识”和“能力”,其中知识可以类比为经验,能力则类似于智商(IQ)。

能力如智商: 在经过大量数据训练后,一般的大型模型表现得像智商极高的实体,具备迅速理解、学习和解决问题的能力。以数学问题解决为例:大型模型只需简要了解数学公式和概念,就能轻松解决复杂问题,这种情况被称为“零样本学习”。在提供几个示例问题的情况下,它甚至可以应对更复杂的数学挑战,这被称为“少样本学习”。相比之下,行业特定的大型模型深入研究各种专业书籍和问题集。当两个大型模型之间存在明显的智商差异时,例如 GPT-4 和 GPT-3.5,智商较高的模型即使经验较少,也会有更高的准确率。

知识如经验: 一般大型模型的知识来源于它们接触过的数据。这意味着,对于某些特定问题或领域,如果模型没有接触过相关数据,它就无法提供准确的答案。例如,当询问“费马最后定理”的证明时,除非模型接触过相关的学术文献,否则它无法准确阐述定理的证明。

在这种背景下,当通用大型模型在特定领域的能力不足时,行业特定的大型模型可以被视为“过渡性产品”。例如,在法律领域,ChatLaw 模型在法律问题和信息检索任务中表现出色,因为它经过大量专业法律数据的训练。然而,随着通用大型模型能力的持续提升,对这种专业训练的需求可能会减少。从长远来看,通过整合外部知识库或上下文信息,通用大型模型具有运用其高级推理能力来应对复杂行业挑战的潜力,逐渐取代行业特定的大型模型。这一趋势不仅在通用人工智能(AGI)的发展中不可避免,也代表了解决行业挑战的更根本的方法。

行业模型的策略

使用大型模型解决行业挑战可以大致分为三种主要方法:

基于混合数据从零开始构建

这种方法涉及使用通用数据和领域特定数据的组合,从零开始创建一个新的模型。显著的例子包括 BloombergGPT。然而,需要注意的是,这种方法可能需要大量的数据和计算资源。

调整通用模型的权重

这一类别包括两种策略:

  • 在通用模型上继续预训练: 像 LawGPT 这样的模型就是这种策略的例子,通过继续预训练建立在通用模型的基础上。这一迭代过程增强了模型对特定领域的适应性。

  • 指令调优(SFT): 这种方法在通用模型框架的基础上利用指令调优(SFT)。通过微调通用模型的权重,它可以更好地解决特定行业的问题。

这两种方法都旨在通过调整权重来适应行业特定需求。虽然这些方法通常比完全重训练消耗的资源少,但可能无法始终达到最佳性能水*。此外,与重训练行业特定大型模型相比,实现显著性能提升可能具有挑战性。

结合通用模型与领域知识

这一类别包括两种策略:

  • 利用外部领域知识: 这种方法涉及将领域特定知识(例如利用向量数据库)增强通用大型模型。通过整合领域知识数据库中的信息,可以提高通用模型在应对行业特定问题时的效果。

  • 利用上下文学习进行领域响应: 通过“上下文学习”,通过创建具有上下文相关的提示来解决特定行业的挑战。然后,通用的大型模型直接生成响应。随着对更广泛上下文窗口的趋势日益增强,提示可以涵盖更多领域特定的知识。因此,即使使用通用的大型模型,也可以有效地回应行业特定的问题。

总结来说,所有这些方法都有一个共同的目标,即解决行业特定的挑战,但它们在方法和资源需求上有所不同。选择合适的方法应考虑可用数据、计算能力和预期的性能提升。在评估行业特定的大型模型时,必须区分这些不同的方法,防止对工作负荷和成本不同的方法进行误导性的比较。

实际实施指南

在行业特定的大型模型领域,一个关键但经常被忽视的问题是数据比例。具体而言,实践者经常发现,使用大量行业特定数据进行模型微调可能会悖论地降低模型的能力。一种可能的解释是行业特定数据与一般数据之间的质量差异。此外,数据比例——专门数据与一般数据的混合比例——可能并不理想。例如,一些模型如 BloombergGPT 采用简单的 1:1 数据比例,但这种方法在预训练、持续预训练或微调中的有效性仍然是一个悬而未决的问题。

我在跨越不同领域(从电子商务和办公室管理到智能家居技术)开发领域特定语言模型的经验为我提供了宝贵的见解。通常,这些模型的规模为 13B,利用 20M 到 100M 的领域特定数据。我发现,在持续预训练阶段,约 10%到 20%的领域特定数据与总数据集的比例通常能提供最佳结果。超过这个比例往往会妨碍模型的通用能力,如总结和问答。

另一个有趣的观察是,在持续预训练过程中,同时融入用于监督微调(SFT)的数据是有益的。这使得模型即使在预训练阶段也能获得更多领域特定的知识。

但是,值得注意的是,这个 10%到 20%的比例并不是一种通用的解决方案。它可能会根据所使用的特定预训练模型以及模型大小和原始数据比例等其他因素而有所不同。调整这些比例通常需要通过实证调整,以实现通用能力和领域特定能力之间的理想*衡。

对于监督微调(SFT),数据比例可以更加灵活,有时甚至可以达到 1:1 的*衡以获得良好的结果。但这种灵活性取决于 SFT 中使用的数据总量。如果数据集较小,这种混合数据方法的影响可能会很小。

在制定应对行业特定挑战的策略时,必须根据任务复杂性量身定制方法。对于较简单的任务,精心制作的提示通常更有效,特别是当与向量数据库结合使用时。这种方法可以解决大多数行业问题。对于中等复杂度的任务,监督微调(SFT)通常更有效,尤其是当混入一些通用数据时。这可以解决大多数剩余问题。对于更复杂的挑战,可能需要等待通用大模型进一步提升其能力。

结论

随着人工智能领域的不断拓展,行业特定的大模型在满足专业需求方面变得越来越重要。然而,必须理解这些专业化模型不是独立存在的构件。它们本质上是针对特定行业需求定制的通用大模型的专门应用。这些行业特定的迭代版本充当了过渡性解决方案,弥补了当前通用模型无法填补的空白。然而,随着通用模型的持续演进,这种专业化适配的需求可能会减少,更多的通用解决方案可能会取而代之。

选择适当的策略来应对行业特定的挑战是一个复杂的过程。其中的复杂性包括常被低估的数据比例因素,这可以深刻影响模型的有效性。对于简单的任务,使用精心设计的提示可以产生优异的结果,而对于更复杂的问题,可能需要不断发展的通用大模型的增强能力。在这个不断变化的环境中,开发有影响力的行业解决方案的关键在于对数据类型、模型架构和任务复杂度的精细*衡。

导航制图挑战:#30 天地图挑战中途进行中

原文:towardsdatascience.com/navigating-the-cartographic-challenge-halfway-through-the-30daymapchallenge-ee96e02aaf95?source=collection_archive---------13-----------------------#2023-11-17

Deepsha MenghaniDeepsha MenghaniTowards Data Science

·

关注 发表在Towards Data Science ·7 min read·Nov 17, 2023

--

作者提供的图片

每张地图都讲述一个故事,每个故事都让我们更接*理解我们的世界。

视觉化世界的广阔是一项了不起的壮举。但为了更接*它,今年十一月,我参加了#30 天地图挑战。我一直迷恋远处的地理可视化,并自己尝试过一些,但我想更深入地学习如何制作能讲述故事的美丽地图。因此,我参加了#30 天地图挑战。

什么是#30 天地图挑战?

30DayMapChallenge 是一个由社区驱动的活动,每年 11 月举行。这个活动的想法是围绕不同的每日主题创建地图,并使用标签#30DayMapChallenge。对于制作地图所用的工具、技术或数据没有限制。

这个挑战为我提供了一个完美的机会,让我深入探索制图的世界(双关语)。我们已经进入了十一月中旬,我最喜欢的部分是从社区中学习并受到启发,同时每天挑战自己的创造力。

在这篇文章中,我将分享一些我至今最喜欢的地理可视化图,这些图是我使用Observable Plot这一 JavaScript 库为探索性数据可视化而创作的。你可以在我的#30DayMapChallenge集合中找到所有可视化图以及数据源和代码。我选择使用 Observable Plot,因为我在参加今年早些时候的#30DayChartChallenge时对其简洁性和易用性爱不释手。

本文中的所有图片均由作者创作。

第 1 天 — 点位

第 1 天 — 点位

十月已经过去,但灵异事件仍在继续。对于这个主题,我选择了在美国绘制鬼魂地点。我从数据世界网站获取数据,这是我最喜欢的数据集发现来源之一。这个数据集包括了每个报告的鬼魂地点的纬度和经度,非常适合绘制成“点”。

我使用了 Albers 投影来绘制美国地图,并给它设置了黑色背景以形成鲜明的对比效果。这个图的主要目标不仅是绘制每个位置的点位,还要创造一种发光效果,以契合我的“鬼魂”主题。为此,我将每个报告的地点用三个点层叠在一起。首先是一个大“红色”点,透明度最低;然后是一个“橙色”点,半径较小且透明度较高;最后是一个“白色”点,半径最小且透明度最高。这些层叠的点位产生了一个带有火焰光环和发光效果的白色点。我不认为这种组合在每个图上都会产生类似的效果,但所有点位的紧密度创造了我想要达到的完美效果。

这个图确实把许多东海岸的地点加入了我未来旅行的清单,也许在这一生中,或者也可能作为一个幽灵。

第 7 天 — 导航

第 7 天 — 导航

我最喜欢的导航方式是 Amtrak 系统,因此在导航主题中,我决定绘制加州的 Amtrak 车站,它是车站最多的州,数据来源于data.world

在可视化加州的 Amtrak 车站网络时,我旨在突出铁路旅行在金州内的连通性和覆盖范围。我选择了墨卡托投影以准确表示州级网络,背景使用从太*洋到西南的颜色渐变。每个车站都用一个‘点’标记,主要枢纽用更大、更显眼的标记区分。为了进一步展示铁路系统的扩展和覆盖范围,我叠加了 Voronoi 网络图。这张地图不仅作为潜在旅行者的工具,还展示了支持在这个广阔州内移动的基础设施。

第 8 天 — 非洲

第 8 天 — 非洲

处理非洲的主题时,我聚焦于乌干达的水源数据,数据来源于水点数据交换通过TidyTuesday

数据提供了一个全面的水点列表,我使用墨卡托投影绘制了这些水点,以准确表示它们的分布。地图的深色背景使水源点突出,吸引注意力到水源密度较高的区域。我将这些位置绘制为栅格地图,以添加纹理层。这张地图不仅展示位置;它讲述了获取水的故事,点的聚集可以指示潜在的水资源压力或丰富区域。

第 10 天 — 北美

第 10 天 — 北美

美国众议院选举结果地图是对美国五十多年来政治潮流的紧凑可视化。我从麻省理工学院选举数据与科学实验室通过TidyTuesday获取了数据,提供了按州详细的投票统计信息。

挑战在于以一种既有信息性又引人入胜的方式展示这个庞大的数据集。我选择了网格布局,每个州的投票趋势通过迷你条形图表示,允许立即在年份和州之间进行视觉比较。颜色编码非常简单——蓝色代表民主党,红色代表共和党,灰色代表其他——以便一眼就能清晰地洞察政治格局。这个地图作为美国政治的视觉历史,展示了可能无法仅从数字中看出的变化和模式。

第 12 天 — 南美

第 12 天 — 南美

对于这个主题,我找到了一组强大的数据集,来自于movebank.org,通过data.world关于南美土耳其秃鹫迁徙的数据。

这幅数据可视化图旨在捕捉鸟类移动的动态特性。数据包括标记秃鹫的时间戳位置,使我能够绘制它们随时间的旅程。我选择了暗色背景来象征覆盖的广阔区域,迁徙路径则用颜色渐变突出,便于眼睛跟踪模式。这幅地图并非静态的;它设计为代表迁徙的涨落,更集中的点可能显示重要的休息地。它提供了自然模式的一瞥,并作为保护主义者追踪这些迁徙路线健康状况的工具。

这绝对是我创建的最喜爱的动画可视化之一,因为它如此轻松地促进了清晰的迁徙模式的观察。这些数据还有很多可以做的,而我感觉自己只是刚刚触及表面。

第 13 天 — 分级地图

第 13 天 — 分级地图

2020 年非洲政治权利的区域分级地图,深刻描绘了这个大陆多样的政治景观。使用来自Freedom House通过TidyTuesday的数据,我旨在展示大陆各地政治权利不同层次的差异。

这幅地图使用绿色调来表示政治权利的渐变,较深的色调表示更大的自由。这种颜色选择是象征性的,在许多文化中,绿色被视为‘继续前进’,表明政治权利在进步的地区。除了其美学价值外,这幅地图还作为分析工具,呈现了复杂且不断发展的政治气候的快照。

中途反思

当我们达到#30daymapchallenge 的中点时,这正是停下来反思迄今为止旅程的完美时刻。每天制作地图,而不是图表,是一种不同类型的探索 —— 将数据和地理,故事和符号融合在一起。

在继续挑战的下半程中,我很兴奋地深入探索地图制作艺术,运用迄今为止学到的教训。对未来的挑战者们:让每个主题激发灵感,向社区学习,并将每一幅地图视为数据可视化旅程中的一步。

完成整个挑战后,我期待分享更丰富的可视化内容及其给予我的洞察。在此之前,我鼓励读者深入挖掘这些数据集,创造并分享他们自己的解读,以及参与这一全球地图挑战。

您可以在我的Observable collection中找到本文中所有可视化的代码和数据复现。

如果您愿意,可以在Linkedin上找到我。

探索聚类领域

原文:towardsdatascience.com/navigating-the-clustering-landscape-b7930ac44147

聚类 | 机器学习 | Python

主要聚类算法的比较及实用 Python 示例

David FarrugiaTowards Data Science David Farrugia

·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 5 月 29 日

--

照片来源:Aleks DorohovichUnsplash

机器学习领域不断发展,*年来引起了极大的兴趣。尤其是在当前 AI 热潮的影响下,数据科学和机器学习的神秘世界无疑受益于广泛的公众曝光。

在本文中,我们将把焦点放在机器学习的一个迷人子领域——聚类上。

让我们尝试描绘一个图景,好吗?

假设你正站在一个繁忙的城市中,周围是成千上万的人。在那一刻,每个人都是 100%独特的。他们是各自独立的个体——然而,你们都有共同的特征或属性,将你们作为一个群体连接在一起。也许是你们的时尚感、职业、政治或宗教信仰,甚至是你们喜欢的食物。

这就是聚类的本质——试图在大量数据点中发现隐藏的模式和分组。在机器学习的世界里,聚类技术是数据宇宙的制图师,描绘出原本看似随机或脱节的信息星座。

在本文中,我们将深入探讨主要的聚类技术类型,揭示它们的独特特性、优势以及应用。不论你是经验丰富的数据专业人士还是好奇的读者,我邀请你加入我这次激动人心的探索之旅。

K 均值聚类

让我们假设一下,我们是养蜂人。我们有一群蜜蜂在嗡嗡作响,我们的目标是将它们分成 K 个不同的蜂巢(即我们的聚类)。

首先,我们随机选择 K 只蜜蜂。这些蜜蜂将作为我们的簇质心。

就像蜜蜂被吸引到蜜糖一样,每只蜜蜂(数据点)都会向最*的蜂巢(质心)靠拢。

当所有的蜜蜂都找到一个蜂巢后,我们将确定每个蜂巢的新质心(更新质心)。我们会不断重复这个过程,直到蜜蜂安定下来,不再交换蜂巢。

啪嗒,那就是 K-means 聚类的本质!

K-means 聚类流程图。图片由作者提供。

优点

  1. 简单易懂

  2. 易于扩展

  3. 在计算成本方面效率高

缺点

  1. 需要提前指定 K

  2. 对初始质心选择敏感

  3. 假设簇是球形且大小相等(这可能并不总是如此)

完美的应用场景

  1. 市场细分

  2. 文档聚类

  3. 图像分割

  4. 异常检测

Python 示例

from sklearn.cluster import KMeans
import numpy as np

# Let's assume we have some data
X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]])

# We initialize KMeans with the number of clusters we want
kmeans = KMeans(n_clusters=2, random_state=0).fit(X)

# We can get the labels of the data points
print(kmeans.labels_)

# And we can predict the clusters for new data points
print(kmeans.predict([[0, 0], [4, 4]]))

# The cluster centers (the mean of all the points in that cluster) can be accessed with
print(kmeans.cluster_centers_)

层次聚类(凝聚型聚类)

假设你正在参加一个大家庭的婚礼,亲属关系不明确。

你的第一个任务是识别直系亲属,如兄弟姐妹或父母和子女,并将他们聚在一起。

接下来,你寻找与这些已建立的群体有密切关系的其他亲属,并将他们纳入其中。

你继续这个过程,逐渐拼凑出整个家庭和朋友的网络,直到每个人都互相连接。

啪嗒,那就是层次聚类的本质!

层次聚类流程图。图片由作者提供。

优点

  1. 无需指定簇的数量

  2. 提供一个簇的层次结构,这可能是有用的

缺点

  1. 对大型数据集计算成本高

  2. 对距离度量的选择敏感

完美的应用场景

  1. 基因测序

  2. 社会网络分析

  3. 构建分类树

Python 示例

from sklearn.cluster import AgglomerativeClustering
import numpy as np

# Let's assume we have some data
X = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]])

# We initialize AgglomerativeClustering with the number of clusters we want
clustering = AgglomerativeClustering(n_clusters=2).fit(X)

# We can get the labels of the data points
print(clustering.labels_)

基于密度的空间聚类算法(DBSCAN)

想象一下你是一个狂热的鸟类观察者。

拿着你可靠的双筒望远镜,你四处扫描地*线,寻找空中鸟群。每当你发现一只鸟时,你会环顾四周,看看是否在给定半径内(数学上定义为 epsilon)有特定数量的鸟(我们称这个概念为最小点数或 MinPts)。

如果你的观察结果验证了,恭喜!你已识别出一个鸟群!

接下来,你寻找更多在这个簇范围内的鸟,并将它们纳入群体。这个模式会持续,直到所有的鸟都被标记为群体的一部分或被确定为孤立飞行者(异常值)。

啪嗒,那就是 DBSCAN 的本质!

DBSCAN 聚类流程图。图片由作者提供。

优点

  1. 无需指定簇的数量。

  2. 可以发现任意形状的簇。

  3. 擅长将高密度簇与低密度噪声分开。

缺点

  1. 不适合具有不同密度簇的数据。

  2. 对 epsilon 和 MinPts 的选择敏感。

完美的用例

  1. 空间数据分析

  2. 异常检测

  3. 图像分割。

Python 示例

from sklearn.cluster import DBSCAN
import numpy as np

# Let's assume we have some data
X = np.array([[1, 2], [2, 2], [2, 3], [8, 7], [8, 8], [25, 80]])

# We initialize DBSCAN with the epsilon and minPts we want
clustering = DBSCAN(eps=3, min_samples=2).fit(X)

# We can get the labels of the data points
print(clustering.labels_)

均值漂移聚类

想象一下你身处一个漆黑的房间,只能用手电筒,你的目标是找到你的朋友们。

一旦你打开手电筒,它会照亮附*的一些人。然后你评估他们的*均(均值)位置,并将手电筒的光束调整到那个方向。

你继续重复这个过程,每次都将光调整到计算出的均值位置,直到你发现自己在朋友们中间。

而这就是均值漂移聚类的精髓!

均值漂移聚类流程图。图片由作者提供。

优点

  1. 无需指定簇的数量

  2. 可以发现任意形状的簇

缺点

  1. 对大数据集来说计算开销较大。

  2. 带宽参数可以极大地影响结果。

完美的用例

  1. 图像分割

  2. 视频追踪

  3. 计算机视觉

Python 示例

from sklearn.cluster import MeanShift
import numpy as np

# Let's assume we have some data
X = np.array([[1, 1], [2, 1], [1, 0], [4, 7], [3, 5], [3, 6]])

# We initialize MeanShift with the bandwidth we want
clustering = MeanShift(bandwidth=2).fit(X)

# We can get the labels of the data points
print(clustering.labels_)

# The cluster centers can be accessed with
print(clustering.cluster_centers_)

谱聚类

想象你是一个音乐指挥,任务是从一群音乐家中组建一个乐团。

你首先听每位音乐家的表演。你记录他们音乐风格的相似性,创建一种“音乐兼容性图表”——你的相似性矩阵。

这个图表作为模板(和谐蓝图——使用拉普拉斯方法计算),你可以根据它来识别出相似的不同簇。从数学上讲,这是通过特征值分解实现的。

最终,每位音乐家根据这种分析(使用 K 均值聚类)找到自己在特定组中的位置。

而这就是谱聚类的精髓!

谱聚类流程图。图片由作者提供。

优点

  1. 可以发现任意形状的簇。

  2. 对于小数量的簇效果良好。

缺点

  1. 对大数据集来说计算开销较大。

  2. 相似性度量和簇的数量选择会极大地影响结果。

完美的用例

  1. 图像分割

  2. 社交网络分析

  3. 基因表达分析

Python 示例

from sklearn.cluster import SpectralClustering
import numpy as np

# Let's assume we have some data
X = np.array([[1, 1], [2, 1], [1, 0], [4, 7], [3, 5], [3, 6]])

# We initialize SpectralClustering with the number of clusters we want
clustering = SpectralClustering(n_clusters=2, assign_labels="discretize", random_state=0).fit(X)

# We can get the labels of the data points
print(clustering.labels_)

结论

在这篇文章中,我们深入探讨了一些在机器学习领域至关重要的聚类算法。我们审视了这些方法如何实现目标,讨论了它们的优缺点,并强调了它们最有效的场景。

选择合适的聚类技术就像选择适当的工具来完成特定任务一样。重要的是要考虑你处理的数据类型和你要解决的具体问题。例如,处理层次数据时,使用基于密度的聚类方法是不明智的。

机器学习中的聚类领域是一个充满活力的景观,充满了探索、发现和突破性想法的可能性。

当我们持续深入探索和掌握这一领域时,我们不仅仅是在使数据更易理解;我们还在为开创性的进步奠定基础,这些进步可能会重塑我们与周围世界的互动方式。因此,请保持探索的精神,继续丰富你的理解,永远不要停止聚类!

你喜欢这篇文章吗?每月$5,你可以成为会员,解锁对 Medium 的无限访问权限。这将直接支持我和你在 Medium 上喜欢的其他作者。非常感谢!

## 通过我的推荐链接加入 Medium - David Farrugia

获得我所有⚡优质⚡内容和 Medium 上的无限访问权限。通过请我喝一杯咖啡来支持我的工作…

david-farrugia.medium.com

想联系我吗?

我很想听听你对这个话题,或者对任何 AI 和数据的看法。

如果你希望联系我,请发邮件到davidfarrugia53@gmail.com

Linkedin

探索大语言模型的领域

原文:towardsdatascience.com/navigating-the-large-language-model-landscape-f56bd67e48d9?source=collection_archive---------9-----------------------#2023-07-24

对于前瞻性领导者和企业家而言的选择和考虑

大卫·科尔布Towards Data Science 大卫·科尔布

·

关注 发布于 Towards Data Science ·6 分钟阅读·2023 年 7 月 24 日

--

图片: 大卫·科尔布

微软和 Meta 最*推出了 Llama 2,这是一种下一代开源大语言模型(LLM)。有了 Llama 2 的广泛预训练和微调 LLM 的集合,企业现在面临一个关键问题。

这一点对寻求采用大语言模型的前瞻性领导者和企业家有什么影响?

随着大语言模型(LLM)市场变得越来越复杂,公司面临五种选择:商业模型、开源模型、微调模型、定制模型,或者与 AI 提供商/研究人员合作。

针对 LLM 市场日益复杂的情况,本文旨在总结企业可用的五种主要选项。

商业模型

商业模型,如 ChatGPT、Google Bard 和 Microsoft Bing,为有远见的领导者和企业家提供了直接、高效的解决方案。这些模型已经在多样化的数据集上进行了广泛的训练,具备文本生成、语言翻译和问答能力。它们的关键优势在于即时可用性。通过正确的策略、程序和流程,企业可以快速部署这些模型,迅速利用其能力。

然而,必须记住,尽管这些模型设计为多用途,服务于广泛的应用,它们可能不擅长于特定于您业务的任务。因此,应根据您的独特业务需求考虑它们的适用性。

图片 : David Kolb

开源模型

开源模型是考虑实施大型语言模型(LLM)解决方案的企业的一个经济选择。这些免费提供的模型具有先进的语言能力,同时能够降低成本。然而,需要注意的是,开源模型可能无法提供与专有选项相同的控制级别,特别是对于需要广泛定制的组织。

在某些情况下,它们使用的数据集比商业模型更小。开源 LLM 仍然在文本生成、翻译和问答任务中提供多样性。开源模型的主要优势在于其成本效益。若干开源供应商提供微调服务,以满足特定业务需求,提供更为量身定制的方案。

一个需要考虑的因素是开源模型的维护和支持。公共云服务提供商通常会更新和改进其商业模型,而开源模型可能缺乏持续的维护。必须评估所选开源模型的可靠性和持续发展,以确保长期适用性。

微调模型

微调模型使企业能够在特定业务任务上实现最佳性能。这些模型结合了商业模型的优势,通过使用组织的数据进行额外训练来提升性能。

一个希望改善客户支持聊天机器人的公司可以从能够理解和生成自然语言的商业模型开始。他们可以使用历史客户支持聊天记录对该模型进行微调,以训练它处理特定的客户查询、回应和上下文。

微调的优势在于能够根据特定需求调整模型,同时受益于商业模型提供的易用性。这对行业特定的术语、独特的要求或专业的用例尤其有价值。然而,微调可能资源密集,要求有一个准确代表目标领域或任务的合适数据集。获取和准备这个数据集可能涉及额外的成本和时间。

在精心执行的情况下,微调使企业能够将大型语言模型适应其独特需求,提升性能和任务特定的相关性。尽管涉及规划和投资,但其好处使微调模型对旨在提升语言处理能力的组织具有吸引力。

图片 : David Kolb

构建定制模型

从头开始构建定制的 LLM 为企业提供了无与伦比的控制和定制性,但成本更高。此选项复杂,需要机器学习和自然语言处理方面的专业知识。定制 LLM 的优势在于其量身定制的特性。它可以根据你的业务的独特需求进行设计,确保性能最佳且与目标一致。

使用定制的 LLM,你可以控制模型的架构、训练数据和微调参数。然而,构建定制的 LLM 既耗时又昂贵。它需要一个熟练的团队、硬件、大量研究、数据收集和注释以及严格的测试。还需要进行持续的维护和更新,以保持模型的有效性。

构建定制的 LLM 是寻求绝对控制和高性能组织的终极选择。虽然这需要投入,但它为你的语言处理需求提供了高度量身定制的解决方案。

混合方法

混合方法结合了不同策略的优势,提供了*衡的解决方案。企业可以通过结合商业模型和微调或定制模型来实现定制化和高效的语言模型策略。

该方法经过优化,以满足任务特定的要求和行业细节。例如,当新客户请求进来时,商业模型可以处理文本并提取相关信息。这一初步互动受益于商业模型的一般语言理解和知识。经过微调或定制的模型,专门针对企业的客户互动和对话数据进行训练,将接管分析处理过的信息,提供量身定制和有针对性的响应,利用其在客户评价和类似互动上的训练。

通过采用混合方法,企业可以实现一种适应性强且高效的策略,提供量身定制的解决方案,同时利用商业模型中的知识。这种策略在既定语言模型的背景下,为满足业务特定需求提供了一种实用而有效的方法。

图片 : 大卫·科尔布

与 AI 供应商的合作

与 AI 供应商合作是实施 LLM 的企业的一个可行选择。这些供应商提供专业知识和资源来构建和部署量身定制的语言模型。与 AI 供应商合作的优势在于可以获得他们的专业知识和支持。他们拥有深厚的机器学习和自然语言处理知识,有效指导企业。他们提供见解、推荐模型,并在开发和部署过程中提供支持。请考虑,与 AI 供应商合作可能会涉及额外费用。评估其财务影响。

通过与 AI 供应商合作,企业可以受益于专业知识,确保 LLM 的更顺利集成。虽然需要考虑费用,但与 AI 供应商合作的优势,特别是在专业指导和支持方面,可能会超过费用支出。

结论

在快速发展的生成式 AI 领域,做出正确选择不仅需要了解可用的模型,还需要知道每个模型如何与您的独特业务目标对齐。

这里有一些关键要点

  • 大型语言模型有可能彻底改变业务运作和客户互动,但利用这一潜力需要一个与您特定需求相一致的策略。

  • 成功实施这些模型并非偶然——这是一个选择。这取决于您是否能够采纳整体视角,在即时需求与未来趋势和机会之间取得*衡。

  • 没有一种适合所有人的解决方案。最佳策略将是为您的业务量身定制的方案。

在考虑这些见解时,请思考一下:在复杂的生成式 AI 领域,最大的挑战往往不是技术本身,而是确定合适的策略以释放其潜力。有时,困惑与清晰、停滞与进步之间的差距,仅在于是否得到正确的指导。

欲了解更多信息,请访问 David Kolb Consultancy

使用 PySpark 的 NBA 分析

原文:towardsdatascience.com/nba-analytics-using-pyspark-a1699ae1117a

背靠背比赛的胜率、比赛得分的均值和标准差,以及更多的 Python 代码

Jin CuiTowards Data Science Jin Cui

·发表于 Towards Data Science ·7 min read·2023 年 4 月 14 日

--

图片由 Emanuel Ekström 提供,来源于 Unsplash

动机

一周多前,我观看了一场 NBA 比赛,密尔沃基雄鹿对阵波士顿凯尔特人。这是联赛前两名球队的对决,许多人认为这场比赛是东部决赛的前奏。作为一个篮球和 NBA 的狂热粉丝,这场比赛令人相当失望,因为密尔沃基雄鹿以 99–140 输给了波士顿凯尔特人,这是密尔沃基在 2022–2023 赛季中为数不多的惨败之一,而密尔沃基在常规赛中持有最佳战绩。

尽管这对于密尔沃基来说显得有些不寻常,尤其是考虑到这是在主场的惨败,但比赛的评论员提醒我他们实际上是在打背靠背比赛,即在前一天比赛之后进行的比赛(在这种情况下,是前一天在印第安纳的客场比赛)。换句话说,疲劳可能在他们的失利中发挥了作用,因为背靠背比赛对运动员身体要求很高,加上比赛间的旅行(从印第安纳回到密尔沃基)可能加剧了这一点。

查看球队赛程,在一个赛季的 80 多场比赛中,NBA 球队确实会进行多场背靠背比赛。你是否曾经好奇这些比赛中球队的表现如何,以及当球队在客场或主场比赛时这种情况是否有所变化?本文展示了使用 PySpark — 一个即用型的 Python Apache Spark 接口 — 来获取这些通常在公共领域中不可用的统计数据的一种方法。

数据

为了确定背靠背比赛的胜率,我们需要每支 NBA 球队背靠背比赛的历史记录及其结果。虽然这些统计数据可以在官方 NBA 网站和其他社区网站上找到,但它们不允许商业使用,因此我模拟了一个包含以下字段的合成数据集。

  • 比赛进行的日期

  • 主队名称

  • 客队名称,以及

  • 比赛分数,以及主客队的相应结果

下表显示了合成数据集的一个片段。你应该能够通过官方 NBA 比赛时间表核实这些并不是实际的比赛。

表 1:合成比赛数据。作者提供的表格。

数据转换

本节提供了一个逐步的 Python 指南,讲解如何将上述数据集转换为识别某场比赛是否为背靠背比赛的数据集,并随后计算每支球队这些比赛的胜率。

步骤 1:加载包和数据

#Load required Python packages

import numpy as np 
import pandas as pd 

!pip install pyspark #Install PySpark
import pyspark
from pyspark.sql.window import Window  #For use of Window Function
from pyspark.sql import functions as F #For use of Window Function
from pyspark.sql import SparkSession   #For initiating PySpark API in Python
#Read in game.csv

path_games = "/directory/game_synthetic.csv"  #Replace with your own directory and data
data_raw_games = pd.read_csv(path_games, encoding = 'ISO-8859-1')

步骤 2:格式化并创建日期列

#Format the 'game_date' column (if it was defaulted to string at ingestion)
#into Date format

data_raw_games['GAME_DATE'] = pd.to_datetime(data_raw_games['game_date'], \
                              format='%Y-%m-%d')
#Create a 'GAME_DATE_minus_ONE' column for each row

data_raw_games['GAME_DATE_minus_ONE'] = pd.DatetimeIndex(data_raw_games['GAME_DATE']) \
                              + pd.DateOffset(-1)

上述创建的‘GAME_DATE_minus_ONE’列表示数据集中每场比赛的前一个日历日期。这将在稍后(步骤 4)详细讨论,并用于识别是否为背靠背比赛。

步骤 3:按队伍分割数据集

由于数据集的每一行都处于比赛级别(即显示两队之间的比赛结果),因此需要进行分割以在队伍级别上表示结果(即将每一行分割为两个,表示每队的比赛结果)。可以使用下面的 Python 代码实现这一点。

#Create two dataframes, one for results of home teams and 
#one for results of away teams, and merge at the end

data_games_frame_1 = data_raw_games.sort_values(['game_id'])
data_games_frame_2 = data_raw_games.sort_values(['game_id'])

data_games_frame_1['TEAM_ID'] = data_games_frame_1['team_id_home']
data_games_frame_2['TEAM_ID'] = data_games_frame_2['team_id_away']

data_games_frame_1['WIN_FLAG'] = (data_games_frame_1['win_loss_home'] == 'W')
data_games_frame_2['WIN_FLAG'] = (data_games_frame_1['win_loss_home'] != 'W')

data_games_frame_1['TEAM_NAME'] = data_games_frame_1['team_name_home']
data_games_frame_2['TEAM_NAME'] = data_games_frame_2['team_name_away']

data_games_frame_1['TEAM_NAME_OPP'] = data_games_frame_1['team_name_away']
data_games_frame_2['TEAM_NAME_OPP'] = data_games_frame_2['team_name_home']

data_games_frame_1['HOME_FLAG'] = 'Home'
data_games_frame_2['HOME_FLAG'] = 'Away'

#Merge the two dataframes above
data_games = pd.concat([data_games_frame_1, data_games_frame_2], axis = 0).drop(['team_id_home', 'team_id_away'], axis = 1)\
                              .sort_values(['game_id']).reset_index(drop = True)

步骤 4:返回每场比赛的日期,标记出该队上一次比赛的日期

这时,PySpark 就显得特别有用。特别是,我们将利用 PySpark 中窗口函数下的lag函数。实际上,如下面的 表 2 所示,lag函数提供了对所选列的偏移值的访问。在这种情况下,它返回亚特兰大老鹰队相对于当前比赛的上一场比赛的日期,通过一个窗口,该窗口显示了亚特兰大老鹰队所有的比赛记录。

例如,在索引 1 的行中,亚特兰大老鹰队在 23/10/2021 与克利夫兰骑士队进行了比赛(“当前比赛”),如‘GAME_DATE’列所示,其上一场比赛是在 21/10/2021 对阵达拉斯小牛队,如‘GAME_DATE’列所示,该数据通过lag函数在当前比赛所在的行中返回,显示在“GAME_DATE_PREV_GAME”列。

表 2:lag函数演示。作者提供的表格

上面返回的‘GAME_DATE_PREV_GAME’列,当其等于在步骤 2中创建的‘GAME_DATE_minus_ONE’列时,表示这是一场连续比赛(即上场比赛的日期等于当前比赛的前一个日历日)。这在表 1 中索引 8(和 14)的行中是这样的,因为亚特兰大老鹰队在 2021 年 4 月 11 日对阵犹他爵士队——在 2021 年 3 月 11 日对布鲁克林篮网队之后的一天。

返回‘GAME_DATE_PREV_GAME’列以及标记所有球队的连续比赛的 Python 代码如下所示。

#Select relevant columns from the dataset

col_spark = [

               'GAME_DATE'
              ,'GAME_DATE_minus_ONE'
              ,'TEAM_ID'
              ,'TEAM_NAME'
              ,'TEAM_NAME_OPP'
              ,'HOME_FLAG'
              ,'WIN_FLAG'
              ,'SCORE'
              ,'season_id'             

              ]

df_spark_feed = data_games[col_spark]
#Initiate PySpark session

spark_1= SparkSession.builder.appName('app_1').getOrCreate()
df_1 = spark_1.createDataFrame(df_spark_feed)

#Create window by each team
Window_Team_by_Date = Window.partitionBy("TEAM_ID").orderBy("GAME_DATE")

#Return date of previous game using the lag function
df_spark = df_1.withColumn("GAME_DATE_PREV_GAME", F.lag("GAME_DATE", 1).over(Window_Team_by_Date)) \
#Flag back-to-back games using a when statement  
               .withColumn("Back_to_Back_FLAG", F.when(F.col("GAME_DATE_minus_ONE") == F.col("GAME_DATE_PREV_GAME"), 1) \
               .otherwise(0)) 

#Convert Spark dataframe to Pandas dataframe
df = df_spark.toPandas()

步骤 5:计算连续比赛的胜率

#Select relevant columns

col = [
         'TEAM_NAME'
        ,'TEAM_NAME_OPP'
        ,'GAME_DATE'
        ,'HOME_FLAG'
        ,'WIN_FLAG'

      ]

#Filter for back-to-back games
df_b2b_interim = df[df['Back_to_Back_FLAG'] == 1]

#Show selected columns only
df_b2b = df_b2b_interim[col].sort_values(['TEAM_NAME', 'GAME_DATE']).reset_index(drop = True)

各队连续比赛的胜率是多少?

表 3:按球队的连续比赛胜率。表格由作者提供

基于合成数据集,似乎连续比赛的胜率因球队而异。休斯顿火箭队在连续比赛中的胜率最低(12.5%),其次是奥兰多魔术队(14.8%)。

连续比赛是在客场还是主场进行的重要吗?

表 4:按球队和主客场的连续比赛胜率。表格由作者提供

基于合成数据集,似乎在表 4中的大多数球队,球队在主场赢得连续比赛的可能性更大,而不是在客场(这是一个合理的观察)。布鲁克林篮网队、芝加哥公牛队和底特律活塞队是这一观察结果的少数例外。

其他拆分也可以计算,例如使用下面的 Python 代码计算连续比赛与非连续比赛的胜率。输出的片段表明,球队更有可能赢得非连续比赛(这仍然是一个合理的观察,尽管有一些例外)。

表 5:连续比赛与其他比赛的胜率。表格由作者提供

其他统计

步骤 4中使用的 PySpark 会话及相关窗口函数可以进一步自定义,以返回其他比赛统计数据。

例如,如果我们想按赛季查询胜率(无论是连续比赛还是非连续比赛),只需按球队和赛季 ID 引入一个窗口,并像下面这样对其进行分区。

#Create window by season ID

Window_Team_by_Season = Window.partitionBy("TEAM_ID").orderBy("season_id")

此外,我们都知道 NBA 比赛的得分波动性很大,但究竟有多大?这可以通过得分的标准差来衡量,而这在公共领域中可能不可用。我们可以通过引入数据集中可用的得分,并应用 avgstddev 窗口函数来轻松校准,这将返回预定义窗口上的标准差。

举例来说,如果 NBA 比赛的标准差约为 20 分,那么有 70% 的概率得分会在 NBA 比赛*均得分线的 +/- 20 分以内(假设正态分布)。

返回该统计数据的示例 Python 代码如下所示。

 spark_1= SparkSession.builder.appName('app_1').getOrCreate()
df_1 = spark_1.createDataFrame(df_spark_feed)

Window_Team = Window.partitionBy("TEAM_ID").orderBy("HOME_FLAG")
df_spark = df_1.withColumn("SCORE_AVG", F.avg("SCORE").over(Window_Team)) \
               .withColumn("SCORE_STD", F.stddev("SCORE").over(Window_Team))

df = df_spark.toPandas()
df.groupby(['TEAM_NAME', 'HOME_FLAG'])["SCORE_AVG", "SCORE_STD"].mean()

当我乘风破浪于 AI/ML 领域时,我喜欢以通俗易懂的语言编写和分享逐步指导和操作教程,并附有可运行的代码。如果你想访问我所有的文章(以及来自其他实践者/作者在 Medium 上的文章),你可以通过 这个链接 进行注册!

最*邻回归器 — 可视化指南

原文:towardsdatascience.com/nearest-neighbors-regressors-a-visual-guide-78595b78072e

模型的视觉理解及超参数的影响

Angela 和 Kezhan ShiTowards Data Science Angela 和 Kezhan Shi

·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 3 月 31 日

--

K 最*邻(KNN)是机器学习中最简单的模型之一。实际上,在某种程度上,它没有模型,因为对于新观测的预测,它将使用整个训练数据集来根据距离(通常是欧几里得距离)找到“最*邻居”。然后在回归任务中,预测值是通过对这些邻居的目标变量值取*均来计算的。

由于我们使用的是距离的概念,所以应该只使用数值特征。当然,你可以通过独热编码或标签编码转换分类特征,距离计算算法仍然可以工作,但距离可能会变得没有意义。

还值得注意的是,目标变量的值并未用于寻找邻居。

在本文中,我们将使用一些简单的数据集来可视化 KNN 回归器的工作原理,以及超参数 k 如何影响预测。我们还将讨论特征缩放的影响。我们还将探索一个较少为人知的邻*版本,即半径最*邻。最后,我们将讨论更自定义的距离版本。

一个连续特征

我们将使用一个具有非线性行为的简单数据集,因为我们知道 KNN 能够处理这种情况。

最*邻回归器数据集 — 作者提供的图片

对于那些读过我关于 决策树回归器 可视化文章的人,你可以注意到这就是相同的数据。我们将与决策树回归器模型进行快速比较。

我们可以创建并拟合一个 KNeighborsRegressor 模型,使用 KNeighborsRegressor(n_neighbors = 3),然后用 model.fit(X, y) 来“拟合”模型。

为了使所有模型的拟合过程相同,你可以注意到模型是通过经典的拟合方法“拟合”的。但对于 KNeighborsRegressor,拟合过程仅仅是保存数据集 X 和 y,没有其他内容。是的,这是最快的拟合!模型也是最大的一次!

现在,我们可以测试一个观测值的“模型”。在以下代码中,我们将使用一个单点。经典预测方法是计算预测值,而 kneighbors 方法允许我们获取邻居。

然后我们可以绘制邻居。我将展示 x = 10 和 x = 20 的图形。请随意进行更多测试。

最*邻回归器与 kneighbors — 图片作者

现在,我们也可以使用一系列 x 值来获取所有预测结果。

这里是前述代码生成的结果图。对于红色段上的每一个点,y 值代表 k 个最*邻的*均值(这里 k = 3)

带预测的最*邻回归器 — 图片作者

现在,让我们为不同的 k 值创建模型预测。

不同 k 值下的最*邻回归器 — 图片作者

我们还可以与决策树回归模型进行比较

最*邻回归器与决策树回归器 — 图片作者

我们可以注意到,对于决策树回归器,边界总是干净利落的,而对于 k 最*邻回归器,边界则更加细腻。

两个连续特征

我们将使用以下数据集,具有两个连续特征,来创建一个 KNN 模型。对于测试数据集,我们将使用 meshgrid 生成网格。

然后我们可以使用 plotly 创建交互式 3D 图。在下图中,我们可以看到不同 k 值的 3D 图。

带有两个特征的最*邻回归器 — 图片作者

在这里,我们可以再次与决策树回归模型进行比较。我们可以到并感受到这两个模型的行为差异。

最*邻回归器与决策树回归器 — 图片作者

缩放的影响

与决策树不同,特征的缩放对模型有直接影响。

例如,对于两个特征的情况,我们可以进行以下变换。值得注意的是,对于一个连续特征,缩放没有影响,因为相对距离没有变化。

不同特征尺度下的最*邻回归器 — 图片作者

我们可以直观地得出这两组模型非常不同。我们可以计算通常的模型性能来比较它们。但在这里,我的方法确实是通过视觉上展示模型如何表现不同。你能感受到吗?距离发生了变化,因为特征的尺度发生了变化。最终,邻居也发生了变化。

有人可能会说我们应该使用标准化或最小-最大缩放。但你可以看到,上面的图像中,某些情况可能是标准化(或最小-最大缩放)数据集。而你无法事先判断标准化是否有助于模型性能的提升。

实际上,为了考虑每个特征在距离计算中的相对重要性,我们应该为不同的特征赋予不同的权重。但这会使调优过程变得过于复杂。

想象一下在线性回归模型中,关键是为每个特征找到系数。在 k NN 的距离计算中,所有特征被视为同等重要。从直觉上讲,我们可以感受到这个 kNN 模型的表现不会太好!

半径邻居

在 scikit-learn 的 [neighbors](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.neighbors) 模块中,有一个鲜为人知的模型叫做 RadiusNeighborsRegressor,你可以从它的名字轻易理解到,与 K *邻模型不同,我们使用一个固定半径的圆圈来围绕新的观察点找到它的邻居。

现在,何时半径邻居模型可能会更有趣?让我们以一个包含异常值的数据集为例。我们可以看到这两种模型的表现不同。由于这个异常值“远”离其他点,在 KNN 的情况下,邻居数量是固定的,因此邻居之间也相距较远。但对于半径邻居,异常值的影响更为重要。

最*邻回归器 KNN vs. 半径 NN — 图片由作者提供

在这里请注意,当我使用“异常值”一词时,并不一定意味着我们应该去除这个异常值。我只是想展示一些数据点远离其他点的情况。

影响对于非异常值也同样显著。因为我们无法满足这两种情况。

当半径过小时,你可能会注意到上图中有些奇怪的现象。是的,在这种情况下,对于某些点,没有邻居。然后,分配了一个巨大的负值。事实上,在这种情况下没有解决方案。但我仍然认为这是一个应该在 scikit-learn 中纠正的错误。产生错误总比默认给出这个值要好。

在完成半径邻居之前,什么时候它会真正有趣?想象一下,当你在城市的一个区域有大量数据,而在附*的另一个区域数据较少,但你知道你本可以收集更多数据。此时半径邻居可能更相关。

类似地,如果你为一个区域有一个值(将分配给该区域的所有地址),那么我们可以使用邻居来*滑该值。这里再次,半径邻居将更相关。下面是使用最*邻模型进行值*滑的示例。

地理邻居*滑 — 作者图像

关于距离的更多信息

关于距离的最后一个有趣讨论,你可能在可视化前述制图时已经考虑过。距离的概念可能非常具体,因为如果你用经纬度计算欧几里得距离,那么这种距离可能无法正确反映地理邻域(这是你可能希望使用的距离)。

你可能已经知道,但看到总是更好。从下面的图像中,红色圆圈是中央位置的“真实圆”,由与中央位置具有相等地理距离的位置形成的红色区域。蓝色“圆圈”是通过计算经纬度的欧几里得距离得到的。在赤道附*,这两个圆圈几乎相同。但在远离赤道的地方,它们会有很大不同。因此,下次你在数据集中使用经纬度并使用最*邻模型时,你必须考虑这一点。

地球上的真实圆形与经纬度的“圆形” — 作者图像

现在,你可以想象在其他情况下,更自定义的距离可能是必要的。因此,这种简单模型可能会变得更高效。

邻居的加权也是可能的。你可以使用weights参数来实现。以下是来自官方文档对该参数的描述:

weights : {‘uniform’, ‘distance’}, callable 或 None, 默认=‘uniform’

预测中使用的权重函数。可能的值:

  • ‘uniform’ : 均匀权重。每个邻域中的所有点都被赋予相等的权重。

  • ‘distance’ : 通过距离的倒数来加权点。在这种情况下,查询点的较*邻居将比较远的邻居具有更大的影响。

  • [callable] : 用户定义的函数,该函数接受一个距离数组,并返回一个包含相同形状的权重数组。

默认使用均匀权重。

K 最*邻回归器,权重 = “distance” — 作者图像

然而,距离设计可能变得非常复杂,这时采用其他方法如决策树和基于数学函数的模型可能更为简单。

结论

我正在撰写一系列类似的文章,演示如何通过可视化帮助我们更好地理解机器学习模型的工作原理而无需数学。请通过下面的链接关注我,获取我文章的全部访问权限:medium.com/@angela.shi/membership

如果你想获取生成本文图形的代码,你可以在这里支持我:ko-fi.com/s/4cc6555852

因此,在本文中,我们展示了最*邻“模型”对于简单数据集来说,借助可视化是相当直观的,因为邻居的概念是直观且简单的。

超参数 K 或半径的选择对最*邻模型的性能有重要影响。如果 K(或半径)太小,模型可能会过拟合数据中的噪声;而如果 K(或半径)太大,模型可能会欠拟合,无法捕捉数据中的潜在模式。

当使用最*邻模型时,数据的缩放也很重要,因为该算法对输入特征的缩放非常敏感。

需要速度:将 Pandas 2.0 与四个 Python 加速库进行比较(附代码)

原文:towardsdatascience.com/need-for-speed-comparing-pandas-2-0-with-four-python-speed-up-libs-with-code-b287e0d9b836

PolarsDaskRAPIDS.ai cuDFNumba 与使用 pyarrowPandas 2.0 在后台处理、向量化以及 itertuples()apply() 方法上进行了比较。

Theophano Mitsa 博士Towards Data Science Theophano Mitsa 博士

·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 19 日

--

图片由 alan9187 提供,来源于 Pixabay

到 2023 年为止,这一年给机器学习领域带来了显著的进展,先进的大语言模型LLMs)得到了开发和全球分布。然而,机器学习的专业能力不仅限于对LLLMs的微调和提示工程。一位机器学习专家了解系统内部的运作,能够解释/优化各种系统行为,并最终负责机器学习解决方案的整体质量、业务需求适配性和性能。

除了性能比较,文章还旨在通过提供代码示例来教育读者如何实现各种 Python 加速操作,突出关键点。

说到了解系统内部运作和性能,本文将对比新发布的 Pandas 2.0 与四个其他加速 Python 库和操作:PolarsRAPIDS.ai cuDFDaskNumba。代码在 ASUS Rig Strix Scar(2022 款)游戏笔记本电脑上执行,配置为 NVIDIA GeForce RTX 3080 Ti Laptop GPUIntel Core i9 12900Hz 处理器。除了性能比较,文章还旨在通过提供代码示例来教育读者如何实现各种 Python 加速操作,突出关键点。

在我们的代码示例中,我们将使用一个合成的.csv文件,该文件包含 500K 行,模拟一个超级商店的两地商品价格。文件有三列;第一列,称为Store1,包含第一个超级商店位置的库存价格,第二列,称为Store2,包含第二个超级商店位置的库存价格。Store2的价格略高(最高可高 20%)。最后,第三列,称为Discountability,仅包含 0 和 1,指示一个项目是否可以打折。0 表示该项目不能打折,1 表示可以打折。创建Pandas 2.0 DataFrame和合成.csv文件的代码如下。

关键点:注意到col2Store2的价格)是通过使用矢量化操作从numpy数组col1var创建的,而无需使用循环。我们之所以能这样做,是因为 NumPy支持矢量化操作。

A. Dataframe 创建性能比较

我们将比较不同方法性能的第一个操作是DataFrame的创建。具体来说,我们将比较Pandas 2.0DaskPolars的性能。在接下来的两个部分中,我们将讨论代码实现,然后在 A.3 节中,我们将比较性能。

A.1. 使用 Pandas 2.0

Pandas 2.0中的一个重要新特性是添加了 Apache Arrow (pyarrow)支持的内存格式。Apache Arrow的主要优点是它可以执行更快、更节省内存的操作。下面的代码展示了使用和不使用pyarrow构建Pandas DataFrame的方式。

关键点: 注意,一旦我们将后端类型(dtype_backend)指定为pyarrow,我们还需要将engine指定为pyarrow

A.2 使用 Polars 和 Dask

Polars是一个加速的 Python DataFrame库,实施了Apache Arrow内存模型,并用Rust编写。与Apache Arrow类似,它既快速又内存高效,特别适合数据密集型计算。为了创建Polars DataFrame,我们只需导入Polars库并调用其read_csv()方法。

Dask是一个开源框架,支持 Python 中的并行和分布式计算,因此特别适合数据密集型应用程序。为了创建一个Dask DataFrame,我们必须首先导入dask.dataframe库,然后调用其read_csv()方法。

关键点。 安装Dask库时要小心,因为Pandas是其依赖项之一,它会安装它。在撰写本文时,Dask的安装不会安装Pandas 2.0,而是一个较旧的版本。因此,如果你想使用Dask而不覆盖你的Pandas 2.0安装,请创建一个虚拟环境并在其中安装Dask

A.3 Dataframe 创建执行时间比较

操作时间使用timeit库进行实现,如下所示。

下方展示了从最快到最慢的DataFrame创建执行时间。最快的是PolarsDask,紧随其后的是具有pyarrow后端的Pandas 2.0。至于传统的Pandas DataFrame创建,它比前者慢 4.37 倍。因此,即使从创建Pandas DataFrame的第一步开始,pyarrow也能发挥作用。

  • 使用Dask:0.020656200009398162

  • 使用Polars:0.027874399966094643

  • 使用pyarrow支持的Pandas 2.0:0.04491699999198317

  • 使用Pandas 2.0(不使用pyarrow):0.196299700008239

除了测量执行时间,我还进行了一些内存使用的估算。cuDF DataFrame的内存占用最小(0.12MB),而Polars DataFrame的内存占用约为 7.63MB,Pandas DataFrame的内存占用约为 11.44MB。注意,cuDF DataFrame的创建时间未提供,因为.csv文件存储在 CPU 中,因此我们无法利用cuDF的 GPU 能力进行此操作。我们将在文章后面讨论这一点。

B. DataFrame操作的性能比较

为了比较上述库在执行DataFrame操作时的性能,假设如下情况:所有Store1的合格商品将折扣 20%,所有Store2的合格商品将折扣 30%,折扣后的价格将保存在一个新的数据框中。我们使用“合格”这个词,因为如上所述,Discountability为 0 的项目不能享受折扣。因此,我们将对应用函数到DataFrame的行上进行执行时间比较,这是一个常见任务。

代码实现展示在 B.1-B.8 节,性能比较展示在 B.9 节。

B.1 使用 pyarrow 的 Pandas

在我们对DataFrame操作的第一次实验中,我们将利用Apache Arrow的能力,鉴于它最*与Pandas 2.0的互操作性。如下面代码的第一行所示,我们将一个Pandas DataFrame转换为pyarrow Table,这是一种在内存中高效表示列数据的方式。每一列单独存储,这允许高效的压缩和数据查询。然后,将Store1Store2Discountability列传递给scale_columns()函数,该函数通过适当的折扣(0.2 或 0.3)和Discountability列的掩码值(0 或 1)来缩放这些列。缩放后的列作为元组由函数返回。最后,将表result_table转换为Pandas DataFrame

关键点: 在函数scale_columns()中,乘法操作是通过pyarrow.compute* 函数multiply() 实现的。注意每一个乘法操作都必须使用multiply() 实现。例如,如果我们将上面的pc.multiply(0.2,mask_col) 替换为pc.multiply(0.2mask_col),我们将会遇到错误。最后,我们使用pyarrow.compute* 的subtract()函数进行减法。

B2. 使用 Pandas 方法 apply()

我们的第二个实验将使用Pandas DataFrameapply()方法来执行逐行操作。函数scale_columns()在下面的代码中执行缩放。请注意,实际的缩放是在嵌套函数discount_store()中完成的。

关键点: 嵌套函数返回一个包含字典的Pandas Series,其中key是列名,value是缩放后的商店价格。你可能会好奇为什么我们要返回这种类型的对象。原因是,Pandas apply() 方法返回一个Series,其索引是列名。因此,将结构类似于其返回值的对象传递给apply()有助于计算。为了教育目的,在我的代码的github目录中(文章末尾有链接),我提供了两个使用apply()的实现。这里展示的是其中一个,还有一个实现,其中嵌套函数将缩放值作为元组返回。这个实现计算上更为密集,因为返回的类型与apply()需要返回的结构不同。

B.3. 使用 Pandas itertuples()

itertuples() 是一个快速的Pandas迭代器,生成namedtuples(列名,相应行的值)。实现商店价格缩放并使用itertuples() 计算折扣价格的代码如下所示。

关键点: Pandas itertuples() 方法通常比 Pandas apply() 更快,特别是对于较大的数据集,就像我们的代码示例一样。原因是apply()为每一行调用一个函数,而itertuples()则不会,并且可以利用向量化操作,同时返回一个轻量级迭代器,这不会创建新对象,且内存效率高。

B.4 Pandas 向量化操作

现在,我们已经到了计算折扣商店价格的最优雅和最快的方法:向量化!这是一种允许对整个数组一次性应用所需操作的方法,而不是使用循环进行迭代。实现如下所示。

B.5 使用 Numba

Numba 是一个即时(JIT)编译器,用于将 Python 代码在运行时翻译成优化的机器代码。它在优化涉及循环和NumPy 数组的操作中非常有用。

@numba.njit 符号是一个装饰器,它告诉Numba 随后的函数要编译成机器代码。

B.6 使用 Dask

类似于NumPyDask提供了向量化操作,额外的优势是这些操作可以以并行和分布的方式应用。

Dask的额外优势包括:(a) 懒惰计算,即Dask数组和操作建立在任务图上,仅在请求结果时执行,例如调用compute()。(b) 超出内存处理,即可以处理不适合内存的数据集。(c) 与Pandas的集成。

B.7 使用 Polars

Polars提供了利用Rust语言的速度和内存效率的向量化操作。类似于Dask,它提供了懒惰计算、超出内存处理和与Pandas的集成。实现如下所示。

B.8 使用 RAPIDS.ai cuDF

最后,我们使用cuDF通过向量化操作实现了价格折扣功能。cuDF库建立在CUDA之上,因此其向量化操作利用了 GPU 的计算能力以加快处理速度。cuDF的一个方便功能是提供CUDA kernels。这些是优化的函数,利用了 GPU 的并行特性。cuDF的实现如下所示。

B.9 函数应用的执行时间比较

类似于DataFrame的创建时间,timeit被用来测量价格折扣功能的执行时间及其在新DataFrame中的节省。下面是执行时间,按从最快到最慢排序。

  • 使用RAPIDS.ai, cuDF: 0.008894977000011295

  • 使用Polars: 0.009557300014421344

  • 使用pyarrow Table: 0.028865800006315112

  • 使用Pandas 2.0向量化操作: 0.0536642000079155

  • 使用Dask: 0.6413181999814697

  • 使用Numba: 0.9497981000000095

  • 使用Pandas 2.0 itertuples(): 1.0882243000087328

  • 使用Pandas 2.0 apply(): 197.63155489997007

不出所料,支持 GPU 的Rapids.ai cuDF取得了最快的时间,其次是闪电般快速的Polars库。pyarrow TablePandas 2.0的向量化操作的执行时间也紧随其后;cuDF的执行时间*均比后两者快约 4.64 倍。DaskNumbaPandasitertuples()方法的表现较差但仍合理(分别比Polars慢约 72、107 和 122 倍)。最后,Pandasapply()相对于所有其他方法表现极其,这也不令人惊讶,因为此方法在处理大型数据集时以逐行方式操作,速度相对较慢。

关键点:(a)一般来说,apply()是对小到中型数据集上的Pandas DataFrames应用函数的优秀方法。但是,当处理大型数据集时,如我们所例,最好考虑其他实现函数的方法。(b)如果你决定在Pandas 2.0中使用apply(),请确保这在标准创建的DataFrame上进行,而不是在后台使用pyarrow的。原因是,显著的数据类型转换开销会大大减慢你的计算速度。

C. 结论

在这篇文章中,我描述了几种加速应用于大型数据集的 Python 代码的方法,特别关注新发布的Pandas 2.0pyarrow后端。不同的加速技术在两个任务中进行了性能比较:(a)DataFrame创建和(b)对DataFrame行应用函数。对于Pandas 2.0用户来说,确实是好消息; Pandas DataFrame的向量化操作和pyarrow Table(从Pandas DataFrame中提取)实现了Rapids.ai cuDFPolars库相当的性能

整个代码在github目录下:link

感谢阅读!

posted @ 2024-10-12 19:54  绝不原创的飞龙  阅读(487)  评论(0)    收藏  举报