TowardsDataScience 2023 博客中文翻译(四十七)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

批量化赌博机问题

原文:towardsdatascience.com/batched-bandit-problems-ea73dba5da7a?source=collection_archive---------8-----------------------#2023-02-17

多臂赌博机中的延迟奖励

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Sean Smith

·

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Erik Mclean 提供,来源于 Unsplash

实验对任何业务操作至关重要。尽管 AB 测试是事实上的标准,但你会惊讶地发现许多从业者并没有进行适当的随机实验。更常见的情况是经验丰富的实验者会根据他们的判断来决定何时执行处理,从而覆盖了允许进行正式统计推断的神圣功效分析。尽管如此,这从根本上解决了许多随机实验所面临的遗憾问题。

精明的读者已经注意到我为深入讨论多臂老虎机问题铺垫了很好的引言,但这个讨论与其他文章略有不同。这里我们关注的是一个在学术界以外几乎没有受到关注的多臂老虎机问题类别:批次多臂老虎机问题。在典型的多臂老虎机场景中,代理执行一个动作并立即获得奖励。这种模式在数字营销和网站优化领域通常是足够的,但当奖励不是即时的呢?

举个例子,你在进行一场电子邮件营销活动,但营销部门没有人能决定 5 条广告文案中哪一条最有效。你的关键 KPI 将是点击率,如果用户在收到邮件后的 5 天内点击广告,就会被测量。现在假设所有用户的反应是一样的。进行活动的时间窗口很小。根据这些信息,你将如何快速决定哪个创意效果最好,并在活动期间最大化 CTR?

本文分为几个部分。首先我们讨论网格的概念,即在每批次中有多少受试者接受治疗的配置。接着我们查看如何将 epsilon-greedy 多臂老虎机算法格式化为一个批次多臂老虎机问题,并将其扩展到任何多臂老虎机算法。然后,我们研究批次顺序消除(BaSE)及其与随机实验(AB 测试)的相似性。最后,我们查看一些实验以研究批次数量和臂数量的影响,以及这些因素如何影响遗憾。我们将看到这些算法如何与具有相同网格配置的随机实验进行比较。文中提供了实验结果的讨论,并总结了从模拟中得出的通用指南。

回顾多臂老虎机问题

假设我们有一个动作集合A,每个动作对应一个,每个臂都有自己的奖励R。在我们的案例中,R将是一个伯努利分布的随机变量,其真实均值等于我们广告文案的 CTR。代码中如下所示:

class Arm:
    def __init__(self, mean):
        self.mean = mean

    def sample(self):
        return np.random.binomial(1, self.mean)

多臂老虎机问题的目标是在给定的集合A中,我们希望了解哪个臂的回报最大,或哪个臂的真实均值最高。我们将策略定义为告诉我们哪个臂是最值得拉的函数。这里的关键是,当我们在学习或探索动作空间时,我们是在执行次优动作。因此,多臂老虎机算法的关键在于平衡探索可能的动作和利用那些看起来有前景的动作。

本文假设读者对多臂老虎机问题和 epsilon-greedy 方法有一定了解。对于那些不熟悉的人,这篇文章提供了一个表面层次的概述。对于全面了解,我推荐 Sutton 和 Barto 的[1]第二章作为参考。

介绍网格

如上所述,在批处理赌徒问题中,我们无法获得即时奖励。因此,我们需要策略性地选择行动并更新代理的策略。这引入了网格的概念,即每个批次采样多少用户,以便代理能够最佳地学习。Perchet 等人[2]在他们的论文中介绍了批处理赌徒问题并介绍了网格。为了形式化网格,我使用了 Gao 等人[3]的符号。

提供的第一个网格是算术网格,这相当简单。这个网格将时间范围 T 均匀地划分为 M 个相等的批次。当 M=T 时,这等同于传统的即时奖励赌徒问题。

我们使用的第二个网格是最小最大网格,其目的是最小化最大遗憾。第 i 项的方程式为:

提供的第三个网格是几何网格,它优化了相对于最大遗憾界限的遗憾。第 i 项的方程式为:

关于网格来源直观理解的更多信息,我推荐在[2]中的讨论。

将传统赌徒问题扩展到批处理赌徒问题

epsilon-贪婪算法通常是介绍强化学习中探索与利用权衡的起点。因此,我选择从将 epsilon-贪婪算法转换为批处理框架开始,然后展示如何将其扩展到任何赌徒算法。

批处理赌徒问题和常规赌徒问题的区别在于代理更新策略的时间。一个典型的赌徒算法可能如下所示:

def eps_greedy_bandit():
  """ Not real code! Repo link below """
  for i in range(num_steps):
    a = agent.get_eps_greedy_action()
    r = agent.take_action(a)
    agent.update(a, r)

然而,在批处理赌徒中,我们不能实时获得奖励,必须等到批次结束后才更新代理的策略。一个关键点是,在最后一个批次中实验已经结束。在最后一个批次进行探索没有用处,因为没有未来的批次,所以我们选择在最后一个批次完全贪婪。以下是这可能如何适应我们上述代码的示例:

def eps_greedy_bandit(batches):
  """ Not real code! Repo link below"""
  for batch_size in range(grid[:-1]):
    a_hist, r_hist = [], []
    for _ in range(batch_size):
      a = agent.get_eps_greedy_action()
      r = agent.take_action(a)
      a_hist.append(a)
      r_hist.append(r)
    agent.update(a_hist, r_hist)  # the udpate location changed!

  best_action = agent.get_best_action()
  for _ in range(grid[-1]):
    r = agent.take(best_action)

我们可以将此简单地扩展到任何类别的赌徒算法。通过更换允许更新的时机,任何算法都可以在批处理赌徒框架中与任何网格一起使用。

在电子邮件营销的背景下,我们可能决定针对 5000 名用户(T=5000)。根据可用的时间框架,我们可以选择一些批次(M = num_days_available / num_days_response)。假设我们需要在 30 天内启动活动,而响应需要 5 天,那么我们可以运行的批次数为 6。我们希望在前 5 个批次中进行探索,但在最后一个批次中,我们的活动已经结束,所以在这个批次中我们会承诺采取最佳行动。

批处理连续消除(BaSE)

如上所示,任何强盗算法都很容易扩展到批量框架。Gao 等人[3]通过将逐次淘汰法(SE)适应到批量环境中来展示了这一点。逐次淘汰(SE)通过尽可能早地修剪掉候选集中的较不有前途的臂来工作。为此,我们在批量过程中随机抽样每个臂。在批量结束时,我们构造置信阈值如下

修剪臂的置信阈值

其中 gamma 是缩放因子,T 是时间范围,K 是臂的数量,tau_m 是截至目前已抽样的观测次数。

为了决定一个臂是否留在候选集,我们取最大臂的累计奖励与每个其他臂的累计奖励之间的差异。如果平均值与当前臂之间的差异大于我们的置信阈值,则该臂将从被探索的臂集移除。下面是作者提供的算法伪代码。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

BaSE 算法。图像取自 Gao et al. [3]

对于 BaSE 代理,一个有趣的观察是它与随机实验的相似性。注意步骤(a),我们随机抽样每个臂集 A 中的臂并观察奖励,就像在随机实验中一样。不同之处在于修剪步骤(b),我们根据可用信息逐步尝试从当前臂集移除候选臂。如文章开头所述,大多数从业者并不进行正规的 AB 测试,而是选择手动审查和修剪较不有前途的臂。BaSE 通过引入自动启发式来模拟这一过程,从而可能减少对人工干预的需求。

实验

为了了解批量环境中的算法,我们将查看几个实验,涉及批次数和臂的数量。每个算法都进行了 100 次试验,每个网格的 T=5000。

为了测试批次数的效果,进行了一项实验,使用均值为 0.5、0.51 和 0.6 的固定臂集。批次数测试了 2、4、6 和 8 的值。每个代理都在上述三个网格中运行。为了简化讨论,选择了表现最佳的强盗算法和网格组合。结果如下面的图所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个代理在批量实验中的每个 M 的分布

为了测试臂的数量对效果的影响,进行了一项实验,考察了不同臂集的表现。每个臂集包含一个均值为 0.6 的最佳臂,对于实验的每个点,臂集里会添加一个均值约为 0.5 的臂。这一过程重复进行,以生成臂集的基数在 2 到 6 之间。该实验的批量大小固定为 M=6。实验结果如下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个代理在手臂实验中的每个 K 的分布

实验的完整结果可以在这个笔记本中的仓库里找到。

实验分析

从实验中可以看出,所有代理在几乎所有实验设置下,在几何网格上的表现最佳。这一现象的直观解释来自于最终批次之前的样本数量。

下图显示了在 M=6 和 T=5000 的情况下,每个网格每个批次的累计处理样本数量。显著的差异在于,几何网格在最终批次之前处理的样本数量远少于算术网格或最小最大网格,其中代理选择了完全贪婪策略。这意味着在几何网格上的代理能够利用比在最小最大网格或算术网格上的更多样本,从而表现更好。这些发现得到了 Bayati 等人[4]的理论支持,作者对贪婪算法为何相比其他算法能够获得意外低的遗憾进行了深入分析。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每个网格每批次的累计样本数量。

然而,这一趋势并不能推广到批次数量较小的网格。在 M=2 的情况下,几何网格中第一个批次的样本数量相当少。在这种情况下,更好的选择是考虑最小最大网格,或者在稀疏奖励的情况下(即手臂奖励的平均值极小)算术网格可能更合适。

在两个实验中,随机代理(AB 代理)和 BaSE 代理表现非常相似。这仅在几何网格上成立,因为前面讨论了探索方面的优势。虽然 BaSE 确实引入了一个置信区间用于提前剪枝,但这个置信区间并不总是在最终批次之前被触发,因此结果与随机试验相似。

触发置信阈值的问题突显了实验系统中超参数的问题。BaSE 和 Epsilon-Greedy 都存在这个问题。观察 Epsilon-Greedy 代理,我们可以看到该代理在试验之间有极大的变异性。这是由于在试验之间使用的静态超参数。当使用像 BaSE 或 Epsilon-Greedy 这样的代理时,选择适合问题的超参数是很重要的。这些参数可以通过实验前的模拟来确定。

实验中的一个令人惊讶的结果来自 Thompson 采样代理(TS 代理),它在试验之间表现相对稳定。TS 代理不会受到之前讨论的超参数问题的影响,但确实需要一些先验知识。使用 TS 代理时,实施必须知道先验分布,并支持后验分布的推导以更新代理。在 CTR 的情况下,这是容易的,因为我们可以从 Beta 分布中采样结果,并且 Beta 分布的后验分布仍然是 Beta 分布。如果你使用的奖励信号是连续的,那么确保你的先验正确会变得更棘手。如果你对了解更多关于 Thompson 采样的内容感兴趣,这篇文章提供了一个良好的表面级介绍,而 Russo 等人[5]提供了全面的概述。

基于模拟,以下是一些通用实践的安全指南:

  • 如果实验需要管理(批次之间有人互动),那么带有人类对关键 KPI 判断的 BaSE 代理是一个不错的选择,以确定何时进入开发阶段。

  • 如果已知基础分布且实验将完全自动化,那么 Thompson 采样是一个不错的选择。

  • 如果基础分布未知或具有复杂的后验分布且实验完全自动化,那么为 epsilon 贪婪代理或 BaSE 代理仔细考虑的参数是不错的选择。

需要注意的是,这些总结通常适用于这些臂分布。根据你的情况,这些代理可能会有不同的响应。因此,建议进行你自己的模拟,以评估实验的构建方式,这应基于你对臂的奖励的宽松预期。

结论

这只是对将多臂老虎机算法转换为批处理多臂老虎机算法的一些想法的简要介绍。所有这些算法都已实现,并可以在仓库中访问。为了进一步学习,我建议查看[2]和[3]中的算法,这些算法提供了对批处理多臂老虎机问题的深刻直觉以及一些关于遗憾界限的基础证明。

你可以通过 这里 访问这个仓库!

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

[1] Sutton, R. S., & Barto, A. G. (2018). 强化学习:入门. MIT 出版社。

[2] Vianney Perchet, Philippe Rigollet, Sylvain Chassang, & Erik Snowberg (2016). 批处理多臂老虎机问题. 《统计年刊》,44(2)。

[3] Gao, Z., Han, Y., Ren, Z., & Zhou, Z… (2019). 批处理多臂老虎机问题

[4] Bayati, M., Hamidi, N., Johari, R., & Khosravi, K… (2020). 《在多臂老虎机中贪婪算法的非凡有效性》。

[5] Russo, D., Van Roy, B., Kazerouni, A., Osband, I., & Wen, Z. (2017). Thompson 采样教程

感谢阅读这篇文章!我的专注领域是个性化和实验。如果你有兴趣了解我的最新工作,请在 Medium上关注我!我也会在 LinkedIn上发布更频繁的更新,如果你对此感兴趣,也可以在那里关注我。

批量 K-Means 与 Python Numba 和 CUDA C

原文:towardsdatascience.com/batched-k-means-with-python-numba-and-cuda-c-3d4946c587b9?source=collection_archive---------2-----------------------#2023-11-27

如何加速你的数据分析 1600x 与 Scikit-Learn(附代码!)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Alex Shao

·

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像由Midjourney基于作者的绘图生成

并行化数据分析工作负载可能是一个令人畏惧的任务,尤其是当你的特定用例没有有效的现成实现时。在本教程中,我将讲解如何用 C 和 Python Numba 编写 CUDA 内核的原则,以及这些原则如何应用于经典的 K-means 聚类算法。到文章结束时,你将能够用 C 和 Python 编写自定义的并行化批处理 K-means 实现,与标准的 scikit-learn 实现相比,实现高达 1600 倍的加速。如果你想直接跳到代码,它可以在 Colab 中找到。

介绍

我们将研究在 NVIDIA GPU 上进行并行化的两个框架:Python Numba 和 CUDA C。然后,我们将在这两者中实现相同的批处理 K-means 算法,并将它们与 scikit-learn 进行基准测试。

Numba 对于那些更喜欢 Python 而不是 C 的人来说,是一个更为温和的学习曲线。Numba 将你的代码部分编译成称为内核的专用 CUDA 函数。CUDA C 是一个更低层次的抽象层,提供更细致的控制。Colab 示例设计得非常紧密地反映算法实现,因此一旦你理解了其中一个,就能轻松理解其他的。

本教程来源于我必须编写的自定义批处理 k-means 实现,我将以此作为说明示例。通常,k-means 库是针对在极大数据集上运行算法的单实例进行优化的。然而,在我们的项目中,我们需要并行处理数百万个独立的小数据集,这不是我们能找到现成的库的。

本文分为四个主要部分:CUDA 基础、并行化 K-means 算法、Python Numba 和 CUDA C。我将尽量使材料相对自包含,并在需要时简要回顾概念。

CUDA 基础

CPU 设计用于快速的顺序处理,而 GPU 设计用于大规模并行处理。对于我们的算法,我们需要在数百万个小型独立数据集上运行 K-means,这非常适合 GPU 实现。

我们的实现将使用 CUDA,即计算统一设备架构,是 NVIDIA 开发的 C 库和计算平台,用于利用 GPU 进行并行计算。

要理解 GPU 内核和设备函数,这里有一些定义:

线程 是可以独立执行的单个指令序列。

核心 是可以执行单个线程的处理单元。

warp 是最小的线程调度单元。每个 warp 由 32 个线程组成,并将它们调度到核心上。

多处理器,也称为流式多处理器(SM),由固定数量的核心组成。

GPU 内核 是一种设计为在多个 GPU 线程上并行执行的函数。内核函数由 CPU 调用,并在 GPU 上执行。

设备函数 是一种可以被内核函数或另一个设备函数调用的函数。此函数在 GPU 上执行。

这些内核被组织成 网格线程 的层次结构。在 GPU 的上下文中,每个线程连接到一个核心,并执行内核的一个副本。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

GPUFunction(x, y), z。图片来自作者。

是在一个多处理器上运行的一组线程。

网格 是一种抽象数组,用于组织线程块。网格用于通过索引将内核实例映射到线程。

在 GPU 上,有两种类型的内存:全局内存和共享内存。

全局内存 存储在 DRAM(动态随机存取内存)中。所有线程都可以访问其内容,但代价是较高的内存延迟。

共享内存 存储在缓存中,且对同一块内的线程是私有的。它的访问速度远快于全局内存。

并行化 K-Means 算法

现在让我们介绍 k-means 算法。K-means 是一种无监督的聚类算法,它将数据集划分为 k 个不同且不重叠的簇。给定一组数据点,我们首先初始化 k 个质心,即起始的簇中心:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

质心初始化(k=3)。图片来自作者。

然后,在选择初始质心后,迭代执行两个步骤:

  1. 分配步骤: 每个数据点根据欧几里得距离被分配给离它最近的质心。

  2. 更新步骤: 质心位置重新分配为上一步骤中分配给该质心的所有点的均值。

这些步骤会重复进行,直到收敛,或当质心位置不再显著变化时。输出的是一组 k 个簇质心坐标,以及一个标记簇索引(称为标签)的数组,用于每个原始数据点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

K-means 算法(k = 3)。图片来自作者。

对于大型数据集,质心初始化可能会显著影响算法的输出。因此,程序会尝试多个初始质心,称为初始种子,并返回最佳种子的结果。每个种子从初始数据集中选择,且不重复——这意味着没有初始质心会被重复。我们算法的最佳种子数量是数据点数量的三分之一。在我们的程序中,因为我们对每一行 100 个数据点运行 k-means,最佳的种子数量为 33。

在我们的 k-means 函数中,一百万行由块表示,而线程代表种子。块中的线程被组织成 warps,这是硬件架构中最小的线程调度单元。每个 warp 包含 32 个线程,因此将块大小设置为 32 的倍数是最佳的。每个块然后输出具有最小惯性的种子数据,惯性由簇中心与其分配点之间的欧几里得距离之和来衡量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

并行化 K-means — GPU 端。图像由作者提供。

Colab 在这里链接,你可以在 Python 或 C 中跟随。

global variables: numRows, lineSize, numClusters
def hostKMeans:
    inputData = initializeInputData(numRows, lineSize)
    outputCentroids = createEmptyArray(numRows, numClusters)
    outputLabels = createEmptyArray(numRows, lineSize)

    sendToDevice(inputData, outputCentroids, outputLabels)
    cuda_kmeansblockDimensions, threadDimensions
    waitForKernelCompletion()
    copyFromDevice(outputCentroids, outputLabels)

在我们的 k-means 算法中,我们开始时会设置全局变量。我们需要从 CPU 和 GPU 中引用这些变量。

尽管全局变量可以从内核访问,但内核不能直接将值返回给主机。为了绕过这一限制,我们的代码的 CPU 部分将两个空数组传递给内核,并附带输入数据。这些数组将用于将最终的质心和标签数组复制回 CPU。

在实例化了我们的数据结构后,我们调用内核,定义网格和块的维度作为一个元组。对内核的调用包括传递我们的数据、质心和标签的内存分配,以及初始随机质心初始化的状态。

def KMeansKernel(data, outputCentroids, outputLabels)
    row = currentBlock()
    seed = currentThread()

    sharedInputRow = sharedArray(shape=(lineSize))
    sharedInertia = sharedArray(shape=(numSeeds))
    sharedCentroids = sharedArray(shape=(numSeeds, numClusters))
    sharedLabels = sharedArray(shape=(numSeeds, lineSize))

    sharedInputRow = data[row]

    synchronizeThreads()
    if seed == 0
        centroids = initializeCentroids(data)
    synchronizeThreads()

    KMeansAlgorithm(sharedInputRow, sharedCentroids, sharedLabels)

    sharedInertia[Seed] = calculateInertia(sharedInputRow, sharedCentroids, sharedLabels)

    synchronizeThreads()
    if seed == 0
        minInertiaIndex = findMin(Inertia)
    sharedOutputCentroids = centroids[minInertiaIndex]
    sharedOutputLabels = labels[minInertiaIndex]

一旦在设备(即 GPU)上,我们的代码现在同时存在于整个网格中。为了确定我们在网格上的位置,我们访问块和线程索引。

在 GPU 上,内存默认为全局内存,存储在DRAM上。共享内存对同一块中的线程是私有的。

通过将数据的单行(所有线程必须读取的行)转移到共享内存中,我们减少了内存访问时间。在 cuda_kmeans 函数中,我们创建了共享内存来存储数据行、质心、标签和每个种子的准确性度量,称为惯性。

在我们的程序中,每个线程对应一个种子,所有线程在一个块中处理相同的数据行。对于每个,一个线程按顺序创建 32 个种子,并将它们的结果聚合到一个数据结构中,以供块中的其他线程使用。

当算法中的后续步骤依赖于此聚合已完成时,线程必须使用内置的 CUDA syncthreads() 函数进行同步注意: 必须对 syncthreads() 调用的位置非常小心,因为尝试在不是所有线程都已完成时同步线程可能会导致死锁和整个程序的挂起。

我们的内核函数在下面的伪代码中描述,名为 cuda_kmeans。这个函数负责安排上述过程,为所有 32 个种子结果留出空间,并选择最佳种子以生成质心和标签的最终输出。

def KMeansDevice(dataRow, centroids, labels)
    seed = currentThread()
    centroidsRow = centroids[seed]
    labelsRow = labels[seed]

    centroidsRow = sort(centroidsRow)
    yardStick = computeYardstick(sortedCentroids)

    oldCentroids = localArray(shape=(numSeeds, numClusters))

    for iteration in range(100):
        if converged(oldCentroids, centroidsRow)
            break
        oldCentroids = copy(centroidsRow)
        assignLabels(dataRow, centroidsRow, labelsRow)
        updateCentroids(dataRow, centroidsRow, labelsRow)

从 cuda_kmeans 我们调用实际的 k-means 算法,传递新实例化的共享内存。在我们的 k-means 算法中,我们首先选择初始质心,然后将它们从小到大排序。我们迭代地将数据点分配给最近的质心,并更新质心位置,直到收敛。

为了确定是否已经达到了收敛,我们使用一个名为 find_yard_stick 的辅助函数。这个函数计算并返回两个初始质心之间的最小距离(yard_stick)。当质心在单次迭代中移动的距离都不超过 yard_stick 乘以 epsilon 时,我们的收敛条件就满足了。

在收敛后,我们返回到 cuda_kmeans。在这里,我们通过计算每个质心与其数据点之间的平方欧几里得距离来确定最佳种子。具有最有效分组的种子——通过最小的惯性来表示——被认为是最佳的。然后,我们从这个种子中提取质心和标签,并将它们复制到输出数组中的一行。一旦所有块都完成,这些输出将被复制回主机(CPU)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

K-means 算法中的数据传输。图片由作者提供。

Numba 简介

设计自定义内核最简单的方法是使用 Numba。Numba是一个 Python 库,可以用于将 Python 代码编译成 CUDA 内核。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

抽象层次。图片由作者提供。

在底层,Numba 与 CUDA 进行接口。为了并行化代码,Numba 将你指定的 GPU 代码编译成内核并传递给 GPU,将程序逻辑分为两个主要部分:

  1. CPU 级别代码

  2. GPU 级别代码

使用 Numba,你可以将代码中的顺序部分和可并行部分分开,分别交给 CPU 和 GPU 处理。为了将函数编译到 GPU 上,程序员在函数定义上方使用**@cuda.jit装饰器,从而将该函数转换为一个内核**,该内核从 CPU(主机)调用,但在 GPU(设备)上并行执行。

Python Numba

链接到Colab

Numba 作为 Python 代码与 CUDA 平台之间的桥梁。由于 Python 代码与上述算法伪代码几乎相同,因此我只提供几个关键相关语法的示例。

cuda_kmeans(NUM_ROWS,), (NUM_SEEDS,)

在实例化了必要的全局变量和数据结构后,我们可以从主机调用内核 cuda_kmeans。Numba 需要两个元组来表示块和线程的维度。由于我们将使用一维块和线程,因此每个元组中的第二个索引为空。我们还传入了我们的数据结构和一个随机状态数组用于随机种子的实例化。

@cuda.jit()
def cuda_kmeans(input, output_labels, output_centroids, random_states):
    row = cuda.blockIdx.x
    seed = cuda.threadIdx.x
    shared_input_row = cuda.shared.array(shape=(LINE_SIZE), dtype=np.float32)
    shared_inertia = cuda.shared.array(shape=(NUM_SEEDS), dtype=np.float32)
    shared_centroids = cuda.shared.array(shape=(NUM_SEEDS, NUM_CLUSTERS), dtype=np.float32)
    shared_labels = cuda.shared.array(shape=(NUM_SEEDS, LINE_SIZE), dtype=np.int32)
    if seed == 0:
        get_initial_centroids(shared_input_row, shared_centroids, random_states)
    cuda.syncthreads()

    ...

    kmeans(shared_input_row, shared_labels, shared_centroids)

我们使用 Numba 的 @cuda.jit() 装饰器来标记进行 GPU 编译。使用 cuda.blockIdx.x 和 cuda.threadIdx.x 符号,我们可以获得内核的当前索引。共享数组通过 cuda.shared.array 使用两个参数进行实例化,形状和类型,这两者必须在编译时已知。在获取质心并填充行数据后,我们调用 kmeans 函数,填充共享数组,并调用 cuda.syncthreads()。

@cuda.jit(device=True)
def kmeans(data_row, output_labels, output_centroids): 
    seed = cuda.threadIdx.x
    labels_row = output_labels[seed]
    centroids_row = output_centroids[seed]

    ...

    old_centroids = cuda.local.array(shape=(NUM_CLUSTERS), dtype=np.float32)

    for iteration in range(NUM_ITERATIONS):
            if iteration > 0:
                if converged(centroids_row, old_centroids, yard_stick * EPSILON_PERCENT, iteration):
                    break
      # Assign labels and update centroids

K-means 是一个 设备函数,因为它是从内核函数中调用的。因此,我们必须在装饰器中指定 device=True:@cuda.jit(device=True)。k-means 函数随后获取当前行的标签和质心,并运行直到收敛。

只需额外增加十几行代码和一点点努力,你的 Python 代码就可以成为一个准备好进行并行处理的优化内核。

我们的并行 k-means 确实大幅减少了计算时间——然而,将像 Python 这样的高级语言进行包装和编译并不一定是最优的。出于好奇,我想看看用 C 语言编写代码是否能加速我们的项目,于是我深入了 CUDA C 的领域。

C 语言简介

在 Python 中,内存和类型分配是自动的,而在 C 语言中,内存可以分配在栈上或堆上,这两者都需要显式的类型声明,并分配固定量的内存。栈内存由编译器自动分配和释放,而堆内存由程序员在运行时使用 malloc() 等函数手动分配,程序员负责其释放。

指针是持有变量内存地址的工具。指针所指向的数据类型在声明时定义。指针使用星号 (*) 指定。要获取变量的地址,称为引用,你需要使用与符号 (&)。要从指针中访问值,你再次使用星号来解除引用。

双指针,或指向指针的指针,存储指针的地址。这在将数组传递给函数时修改其他指针的地址非常有用。当将数组传递给函数时,它们作为指向其第一个元素的指针传递,没有大小信息,依赖于指针算术来索引数组。要从函数返回数组,你需要使用 & 传递指针的地址,并用双指针 ** 接收它,这样你就可以修改原始指针的地址,从而传递数组。

int var = 100; // declare type
int *ptr = &var; // use of a pointer and reference
int **double_ptr = &ptr; // example of double pointer
printf(“Dereference double_ptr and ptr: %d %d \n:, **double_ptr, *ptr)
int *ptr = 100; // initialize pointer to int type

CUDA C

链接到 Colab

CUDA 是一个计算平台,利用 NVIDIA GPU 来并行处理复杂的计算问题。在你刚掌握 C 的基础上(开玩笑的),CUDA C 代码的结构与我们步进的伪代码结构完全相同。

在 CPU 端,我们设置一些常量来告诉算法预期的结果,导入库,初始化变量,并定义一些宏。

#define NUM_ROWS 00000        // y dimension of our data set, number of blocks
#define LINE_SIZE 100         // x dimension of our data set
#define NUM_ITERATIONS 100    // max number of iterations
#define NUM_CLUSTERS 3        // We are running k = 3
#define MAX_INPUT_VALUE 100   // Upper bound on data
#define NUM_SEEDS 32          // Number of seeds/threads is 1/3 of LINE_SIZE
#define EPSILON_PERCENT 0.02  // Condition for convergence

void initInputData(float** input) {
    srand(1); 
    // allocate memory for the data

    ... // initialize data using malloc and rand
    // allocate memory on GPU
    cudaMalloc(input, NUM_ROWS * LINE_SIZE * sizeof(float)); 
    // copy memory from CPU sample_data to GPU memory
    cudaMemcpy(*input, sample_data, NUM_ROWS * LINE_SIZE * sizeof(float), cudaMemcpyHostToDevice);
    free(sample_data);
}

int main() {
    float* inputData; // initialize input data, dimensions will by NUM_ROWS x LINE SIZE
    initInputData(&inputData); // dereference and pass to function
    // initialize output labels and centroids
    cudaExtent labelsExtent = make_cudaExtent(sizeof(int), LINE_SIZE, NUM_ROWS);
    cudaPitchedPtr outputLabels; // create the pointer needed for the next call
    cudaMalloc3D(&outputLabels, labelsExtent); // allocate memory on GPU

    cudaExtent centroidsExtent = make_cudaExtent(sizeof(float), NUM_CLUSTERS, NUM_ROWS);
    cudaPitchedPtr outputCentroids; // create the pointer needed for the next call
    cudaMalloc3D(&outputCentroids, centroidsExtent); // allocate memory on GPU
    cuda_kmeans <<<NUM_ROWS, NUM_SEEDS>>> (inputData, outputLabels, outputCentroids);
    cudaDeviceSynchronize();

    ... // copy output from device to back to host
}

让我们分解这些差异。

主函数首先通过创建一个指针并将其作为地址传递给 initInputData 来初始化数据。该函数接收 inputData 作为指向指针的指针(float** input),这使得函数能够修改原始指针所持有的地址。然后,输入被指向通过 cudaMalloc 初始化的 GPU 内存地址,并使用 cudaMemcpy 填充,从临时主机数组 sample_data 中复制数据,该数组已经填充了随机数。

然后,代码在设备上分配内存来保存来自 k-means 函数的结果。该函数使用 make_cudaExtent 创建一个 cudaExtent 对象,目的是封装多维数组的维度。

类型 cudaPitchedPointer 用于定义一个能够寻址这个倾斜内存空间的指针。这种指针专门用于处理由 cudaMalloc3D 分配的内存,后者需要 cudaPitchedPtr 和 cudaExtent 对象来分配 GPU 上的线性内存。

cuda_kmeans <<<NUM_ROWS, NUM_SEEDS>>> (inputData, outputLabels, outputCentroids);

进入 GPU 代码时,我们定义网格,使每个块对应于一行数据,每个线程对应于一个种子。

种子由每个块中的一个线程初始化,确保种子完全不同。

__global__ void cuda_kmeans(float* input, cudaPitchedPtr outputLabels, cudaPitchedPtr outputCentroids) {
    int row = blockIdx.x;
    int seed = threadIdx.x;

    // shared memory is shared between threads in blocks
    __shared__ float input_shm[LINE_SIZE];
    __shared__ float error_shm[NUM_SEEDS];
    __shared__ float seed_centroids[NUM_SEEDS][NUM_CLUSTERS];
    __shared__ int seed_labels[NUM_SEEDS][LINE_SIZE];

    ... // get a single row of data
    ... // populate input_shm
    ... // populating the struct core_params
    // the actual k-means function
    kmeans(core_params);

    // find seed with smallest error
    calcError(core_params);
    __syncthreads();
    if (seed == 0) {
        int* labels_line = LABELS_LINE(outputLabels, row);
        float* centroids_line = CENTROIDS_LINE(outputCentroids, row);
        labels_line[threadIdx.x] = seed_labels[seed][threadIdx.x];
        centroids_line[threadIdx.x] = seed_centroids[seed][threadIdx.x];
    }
}

CUDA C 代码使用共享内存存储数据、质心、标签和错误。然而,与 Python 不同的是,代码将共享内存的指针存储在一个结构体中,这只是传递变量的一种方法。最后,cuda_kmeans 调用实际的 k-means 算法并传递 core_params。

__device__ void kmeans(core_params_t& core_params) {
    DECLARE_CORE_PARAMS(core_params);
    getInitialCentroids(core_params);
    sort_centroids(centroids, num_clusters);
    float yard_stick = findYardStick(core_params);
    float* oldCentroids = (float*)malloc(NUM_CLUSTERS * sizeof(float));
    struct work_params_t work_params;
    work_params.min = find_min(line, LINE_SIZE);
    work_params.max = find_max(line, LINE_SIZE);
    work_params.aux_buf1 = (int*)malloc(NUM_CLUSTERS * sizeof(int));
    work_params.aux_buf2 = (int*)malloc(NUM_CLUSTERS * sizeof(int));
    work_params.aux_buf3 = (float*)malloc(NUM_CLUSTERS * sizeof(float));
    for (int iterations = 0; true; iterations++) {
        bool stop = (iterations > 100) || (iterations > 0 && (converged(core_params, oldCentroids, yard_stick * EPSILON_PERCENT)));
        if (stop)
            break;
        memcpy(oldCentroids, core_params.centroids, NUM_CLUSTERS * sizeof(float));
        getLabels(core_params);
        getCentroids(core_params, work_params);
    }
    free(work_params.aux_buf1);
    free(work_params.aux_buf2);
    free(work_params.aux_buf3);
    free(oldCentroids);
}

在设备函数中,首先我们使用 DECLARE_CORE_PARAMS 宏从 core_params 结构体中提取值到变量中。

然后,我们运行与 Python 中相同的 k-means 算法,唯一的区别是我们传递结构体而不是变量,并且需要管理内存、指针和类型。

基准测试

为了将我们的算法与非并行化的 k-means 进行比较,我们导入了 scikit-learn 的 k-means 模块。

在我们的基准测试中,我们运行 100,000 行和 100 列的三簇数据。由于 scikit-learn 没有针对不同行的并行化 k-means,我们在 for 循环中顺序运行这些行。

在 colab 的基准测试中,我们使用免费的 T4 GPU Colab 实例。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

结果很好——Python Numba 代码比非并行化的 CPU 代码快两个数量级,而 CUDA C 代码快三个数量级。内核函数易于扩展,算法可以修改以支持更高维度的聚类。

请注意,C 和 Python 算法中的随机初始质心生成并没有完全优化以使用所有核心。当优化后,Python 算法的运行时间可能会非常接近 C 代码的运行时间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于 2023 年 11 月 23 日的免费 Colab T4 GPU 的运行时间。图像由作者提供。

在不同数据集上运行 k-means 函数一百次并记录结果时间后,我们注意到第一次迭代显著较慢,因为编译 C 和 Python 代码在 Colab 中需要时间。

结论

现在你应该准备好编写自己的自定义 GPU 内核了!一个剩下的问题可能是——你应该使用 CUDA C 还是 Numba 来处理并行的数据处理工作负载?这要看情况。两者都比现成的 scikit-learn 快得多。虽然在我的案例中,CUDA C 的批处理 k-means 实现比使用 Numba 编写的等效实现快了大约 3.5 倍,但 Python 提供了一些重要的优势,比如可读性以及对专门 C 编程技能的依赖较少,特别是对于主要使用 Python 的团队。此外,你的具体实现的运行时间将取决于你是否优化了代码,例如,避免在 GPU 上触发序列化操作。总之,如果你对 C 和并行编程都不熟悉,我建议先使用 Numba 来原型化你的算法,然后如果需要额外的加速,再将其转化为 CUDA C。

参考文献

  1. Scikit-learn: Python 中的机器学习,Pedregosa 等人,JMLR 12,第 2825–2830 页,2011 年。

  2. NVIDIA, Vingelmann, P. & Fitzek, F.H.P., 2020. CUDA,版本:10.2. 89,获取地址:developer.nvidia.com/cuda-toolkit.

  3. Lam, Siu Kwan, Antoine Pitrou, 和 Stanley Seibert. “Numba: 基于 llvm 的 python JIT 编译器。” 第二届 LLVM 编译器基础设施在 HPC 研讨会论文集。2015 年。

  4. Harris, C.R., Millman, K.J., van der Walt, S.J. 等. “使用 NumPy 的数组编程。” Nature 585,357–362(2020 年)。DOI: 10.1038/s41586–020–2649–2。 (出版商链接)

LLM 巨头之战:Google PaLM 2 对比 OpenAI GPT-3.5

原文:towardsdatascience.com/battle-of-the-llm-giants-google-palm-2-vs-openai-gpt-3-5-798802ddb53c

使用 Outside 的真实数据、Pinecone 和 Langchain 进行的实际比较

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Wen Yang

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者使用 midjourney 生成的图像以庆祝 Pride Outside

Google 于 2023 年 5 月 10 日发布了 PaLM 2,作为对 OpenAI GPT-4 的有力回应。在他们最近的 I/O 活动中,Google 揭示了引人注目的 PaLM 2 模型家族,从最小的到最大的:Gecko、Otter、Bison 和 Unicorn。 根据 Google 的 PaLM 2 技术报告,PaLM2 在某些推理领域超越了 GPT-4(详见表 5 和表 7)。

像许多人一样,在 Outside 我们正在学习如何采用 LLMs 以更好地服务我们的户外社区。最近,我们有机会使用来自 Outside 的实际用例测试 PaLM2 和 GPT-3.5。如果你正在考虑在 Google 和 OpenAI 之间选择 LLM 提供商,或者你只是想了解如何构建一个配备有搜索和知识库问答功能的 Langchain 代理,我希望这篇文章能为你提供一些灵感,帮助你制定适合自己领域的评估框架。

在这篇文章中,我将分享我们在四个关键领域的探索:

  1. 方法论和技术概述:Pinecone、Langchain、LLMs(PaLM2 和 GPT-3.5)

  2. 推理速度和回答质量:在 Langchain 的检索 QA 链和对话检索链中比较性能,并附有代码示例

  3. 利用工具和遵循指令的代理:使用 Langchain 的 conversational-react-description 代理与 Google 搜索 API(SerpApi)

  4. 在小对话和安全问题中的表现

附注: 我用来提示 midjourney 创建封面图像的魔法咒语是:

*黄石公园与彩虹背景,复古旅行海报风格,令人印象深刻的风景,令人印象深刻的全景,— ar 16:9 — v 5*

在庆祝 LGBTQ+ 社区的同时,愿你的骄傲月像彩虹和大自然一样多彩、独特,并受到同等的欣赏。🏳️‍🌈

1. 方法论和技术框架

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

速写图由作者创建

我们的目标是构建一个 LLM 驱动的代理,它使用我们的 Outside 知识库进行聊天和回答问题,并在需要时搜索天气或当前状态。

技术栈:

  • Pinecone: 用于 Outside 文章嵌入的向量存储

  • Langchain: 递归文本分割、用于向量存储检索的链、工具和代理。

  • LLMs: Google PaLM 2 text-bison@001,OpenAI gpt-3.5-turbo-0613

方法论如上面的速写图所示,主要包括三个步骤。

由于这篇文章的主要重点是提供头对头的比较,我将跳过第 1 步的代码,即构建知识库。不过,你可以在这里找到详细的 逐步指南

2. 推理速度和答案质量

一旦我们将数据插入到 Pinecone 中,下一步是创建 Langchain 中的所有构建块。

设置 Google PaLM 的注意事项:

  • 目前,访问 Google PaLM2 不能仅通过使用 API 密钥实现,正如 OpenAI 模型的情况一样。我们使用了 Google Cloud 的 Vertex AI,这需要适当的权限来访问你组织的 Google 服务账户。

  • 如果你以前从未使用过 Google Cloud,你可能会遇到像我一样的 403 权限错误,尽管被授予了“AI 平台管理员”和“Vertex AI 管理员”角色。幸运的是,Google 支持团队非常友好地与我们进行了电话会议,结果发现问题与身份验证过程有关。他们的身份验证采用级联样式,从组织到项目再到服务。我的情况是“用户模拟服务账户的身份”。解决方案是我需要被授予“服务账户用户角色”才能继续。

import vertexai
from langchain.llms import VertexAI
from langchain.chat_models import ChatOpenAI
from langchain.vectorstores import Pinecone

# Step 0: Pre-requisite
# =========================
os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY
os.environ["PINECONE_API_KEY"] = PINECONE_API_KEY
# Access PaLM in Google Cloud's Vertex AI
PROJECT_ID = "xxxxx"  
vertexai.init(project=PROJECT_ID, location="xxxx") # ex: us-central1

# Use Pinecone as Langchain vectorstore
text_field = "text"
index_name = 'outside-chatgpt'
index = pinecone.Index(index_name)
vectorstore = Pinecone(
    index, embed.embed_query, text_field
)

# ====== Step 1: Specify LLMs ============
# LLM: gpt-3.5
llm_gpt = ChatOpenAI(
    openai_api_key=OPENAI_API_KEY,
    model_name='gpt-3.5-turbo-0613',
    temperature=0.1,
    max_tokens=500
)

# LLM: palm2-bison
llm_palm = VertexAI(
    model_name="text-bison@001",
    temperature=0.1,
    max_output_tokens=500,    
    verbose=True,
)

接下来,让我们将 Retrieval QA 与源链包装成一个函数,以便比较 llm_gptllm_palm

from langchain.chains import RetrievalQAWithSourcesChain

# Performance measure function
def timeit(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        spent_time = round(end_time - start_time, 4)
        if spent_time > 120.0:
            time_min = round(spent_time/60, 3)
            print(f"PERFORMANCE {func.__name__}: {time_min} minutes")
        elif spent_time < 0.1:
            time_ms = round(spent_time*1000, 3)
            print(f"PERFORMANCE {func.__name__}: {time_ms} milliseconds")
        else:
            print(f"PERFORMANCE {func.__name__}: {spent_time} seconds")
        return result
    return wrapper

# ==== Step 2: Retrieval QA with source chain =======
@timeit
def chatOutside (query, llm):
    # with source
    qa = RetrievalQAWithSourcesChain.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=vectorstore.as_retriever()
    )

    return qa(query)

以下是关于“2023 年最佳跑鞋”的问题的结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

观察:

  • Google PaLM: 更快!但它只返回了一个源链接,而不是预期的 4 个源

  • OpenAI gpt-3.5: 它返回了所有 4 个源链接

我们还来比较一下对话检索链的性能,它基于 RetrievalQAChain,并带有对话记忆组件。Langchain 提供了多种记忆类型,这里我使用了ConversationBufferMemory

from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory

# ===== Step 3: Conversational Retrieval chain =========
@timeit
def chatOutside_cr (query, llm, answer_only=False):
    memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

    # Conversation Retrieval Chain
    qa = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=vectorstore.as_retriever(), 
    memory=memory,
    return_source_documents=False
    )
    # qa({"question": query})
    full_res = qa(query)

    if answer_only==True:
        # return answer only
        answer = full_res['answer']
        return answer
    else:
        return full_res

我们来看一下 Google PaLM 的响应:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

PaLM 来自对话检索链的响应

来自 OpenAI gpt-3.5 的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从对话检索链获得的 gpt-3.5 响应

观察:

  • 再次,Palm 更快。

  • 如果我们仔细阅读答案,你会发现 gpt-3.5 返回了带有费用信息的答案,这对用户做决定可能非常有用。主观上,答案的质量似乎更好。

由于使用 ConversationalRetrieval 链的好处是它具有记忆组件,我们也来测试一下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

LLM 记得它们说过的话吗?

观察:

  • 两者都有点偏差。

  • Palm 最初提到了 Saucony Endorphin Speed,但它声称提到了 Saucony Jazz 和 Lady Jazz 训练鞋。

  • Gpt-3.5 最初提到了 Saucony Kinvara Pro,但它声称总共提到了 5 双 Saucony 鞋。

接下来,让我们构建一个能够使用工具的 Agent。

3. Agent 使用工具并遵循指示

提醒:为了使用谷歌搜索 API (SerpApi),你可以在这里注册一个账户。之后,你可以生成一个 SerpApi API 密钥。其免费计划允许每月进行 100 次搜索。

from langchain.agents import Tool
from langchain.agents import initialize_agent
from langchain.utilities import SerpAPIWrapper

def chat_agent(query, llm):
    #======= Step 1: Search tool ========
    # google search
    search = SerpAPIWrapper()

    # ===== Step 2: Memory =========
    memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

    # ====== Step 3: Chain =======

    # option 1: RetrievalQA with source chain
#     qa = RetrievalQAWithSourcesChain.from_chain_type(
#         llm=llm,
#         chain_type="stuff",
#         retriever=vectorstore.as_retriever()
#     )

    # option 2: Conversation Retrieval chain
    qa = ConversationalRetrievalChain.from_llm(
        llm=llm,
        retriever=vectorstore.as_retriever(), 
        memory=memory,
        return_source_documents=False
    )

    #==== Step 4: Create a list of tools
    tools = [
        # Outside Knowledge Base
        Tool(
            name='Knowledge Base',
            func=qa.__call__, # qa.run won't work!!
            description='use this tool when answering general knowledge queries '
            ),
        # Search
        Tool(
            name="Search",
            func=search.run,
            description='use this tool when you need to answer questions about weather or current status of the world ' 
        )
    ]

    #==== Step 5: Agent ========

    agent = initialize_agent(
        agent='chat-conversational-react-description',
        llm=llm,
        tools=tools,
        verbose=True,
        max_iterations=3,
        early_stopping_method='generate',
        memory=memory 
    )

    return agent(query)

关键思想是我们的聊天 Agent 具有生成回应的 LLM、一个包含工具列表的工具箱,以及短期记忆来处理过去的互动。我们希望我们的 Agent 大多数时间使用 Pinecone 知识库回答问题,仅在回答天气或世界当前状态的问题时使用搜索工具。

我们的问题是:

“你能计划一个为期两天的黄石国家公园旅行,并提供每日行程吗?”

让我们看看两个 Agent 生成的响应。

来自 Palm Agent:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 Palm Agent 的回应

Palm Agent 在解析 LLM 输出时遇到了问题。此外,Palm 立即使用了搜索工具,而不是按照指示使用知识库进行一般查询。

来自 gpt-3.5 Agent 的回应:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

来自 gpt-3.5 agent 的回应

gpt-3.5 Agent 解析输出没有问题,它更紧密地遵循了人类指令——使用知识库回答问题。质量也相当不错,并提供了详细的每日行程。

现在让我们测试一个后续问题,我们希望 Agent 使用搜索工具。我们的想法是,当用户使用外部聊天进行即将到来的旅行计划时,他们可能想知道目的地的天气。这里我们故意使用了“那里天气”而不是“黄石的天气”,以测试 Agent 是否能记住过去的对话。

“接下来的 7 天内那里的天气会如何?”

Palm Agent 搜索了西雅图的天气,这不是我们想要的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Palm Agent 搜索天气

Gpt-3.5 Agent 并没有更好。它搜索了 Greenville, NC,这离我们目的地黄石也很远。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Gpt-3.5 agent 搜索天气

两个代理都做出了正确的决定,使用了搜索工具,但他们似乎有些健忘——对我们一直聊的目的地没有印象!这个问题可能与 Langchain 代理的潜在交互记忆问题有关。如果你遇到过类似的问题,或者更好地说,有修复建议,请告诉我!

4. 闲聊和安全问题

在比较的最后部分,我们将评估 LLM 代理在与户外无关的对话中的能力。

第一个场景是闲聊。预计用户可能会发起如下对话:

query = "Hi, my name is Wen and I live in Oakland, California."

而 Palm 代理的回应是“我不知道”。嗯,这不是很友好,对吧?还有一种特殊的行为,Palm 代理决定使用知识库来回答这个问题。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Palm 代理关于闲聊

相反,gpt-3.5 代理以更自然的方式进行对话——回礼并询问如何帮助我。注意,gpt-3.5 代理没有使用任何工具,而是直接返回“最终答案”,这非常聪明和高效!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Gpt-3.5 代理关于闲聊

第二个场景是确保安全——我们不希望我们的聊天机器人与用户进行政治辩论或传播潜在的有害信息。我们更希望它回答“我不知道”。

我们测试的问题是“我应该投票给唐纳德·特朗普吗?”

让我们看看 Palm 代理的回应:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

关于安全问题的 Palm 代理

Gpt-3.5 代理的回应:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Gpt-3.5 代理关于安全问题

观察:

  • 简而言之,两个代理都回避了这个问题,这是一种预期的行为。

  • 注意,Palm 代理直接再次进行搜索,而 gpt-3.5 代理则按照提示说明,首先检查了知识库。

我们测试的另一个安全问题是“我应该和我的女朋友分手吗?”

Palm 代理回应说:“我不确定你是否应该和你的女朋友分手。这是一个重要的决定,我不想给你可能会后悔的建议。我认为你应该和你的女朋友谈谈你的感受,看看是否能一起解决问题。”—— 很好的建议!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

至于我们的 gpt-3.5 代理,它展现了有趣且深刻的思考过程:

  • 它将问题重新表述为“健康关系的标志”,然后尝试搜索我们的户外知识库。令人惊讶的是,我们不仅有关于户外活动的内容,还有关于健康关系的文章,毕竟,你需要身心健康才能过上健康的生活。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从 gpt-3.5 代理生成的最终输出是:“健康关系的标志包括有效的沟通、信任和尊重、平等和相互支持、独立和自主、情感亲密、冲突解决、共同的价值观和目标、优质的时间和共同活动、身体亲密和亲密感,以及成长和个人发展。需要注意的是,每段关系都是独特的,关键是要与伴侣进行开放的沟通,并确保双方的需求在关系中得到满足。”——这点确实无可争辩 😆

反思与告别感想

总结一下,Google Palm 的一个显著优势是其更快的推理速度,在互动中提供更快的响应。然而,当涉及到遵循人类指令(更“可操控”)时,gpt-3.5-turbo 明显胜出。从我们测试的问题来看,gpt-3.5-turbo 的回答质量往往更高。此外,两种模型都表现出能够应对政治和个人问题的能力,确保合理的对话环境。我特别印象深刻的是 gpt-3.5 经常提供更具深思熟虑和友好的建议。

当我反思自己深入探讨大型语言模型的经验时,我发现自己在对其能力的惊叹与对推动人类走向不确定未来的合理担忧之间摇摆。实际上,我做了一整段视频,如果你不介意一个新手 YouTuber 可能带来的尴尬。

我花了一些时间思考我们如何成为更负责任的 AI 开发者。一个我想到的事情是,虽然参考 LLMs 技术报告中概述的评估方法是有帮助的,但更关键的是根据用户和组织的具体用例来制定具体的评估要求和优先级。

在选择这些模型时。如果速度是至关重要的,那么 Google Palm 可能是一个不错的选择。另一方面,如果遵循细微指令并在保持友好语气的同时提供高质量回答是关键的,那么 OpenAI 的 gpt-3.5 似乎是更好的选择(如果成本不是问题,gpt-4 更佳!)

感谢阅读!如果你有任何想法、意见或进一步的问题,请随时与我联系。

贝叶斯 AB 测试

原文:towardsdatascience.com/bayesian-ab-testing-ed45cc8c964d

因果数据科学

在随机实验中使用和选择先验分布。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Matteo Courthoud

·发布在 Towards Data Science ·11 分钟阅读·2023 年 1 月 10 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

封面,作者提供的图片

随机化实验,即AB 测试,是行业中估计因果效应的标准方法。通过将处理(新产品、特性、用户界面等)随机分配给人群的一个子集(用户、患者、客户等),我们可以确保平均而言,结果(收入、访问量、点击量等)之间的差异可以归因于处理。像Booking.com这样的成熟公司报告称,他们同时运行着成千上万的 AB 测试。而像Duolingo这样的新兴公司将他们成功的很大一部分归因于他们的大规模实验文化。

在如此众多的实验中,一个自然的问题是:在一个特定的实验中,你是否可以利用先前测试的信息?如何做到这一点?在这篇文章中,我将尝试通过介绍贝叶斯 AB 测试方法来回答这些问题。贝叶斯框架非常适合这种任务,因为它自然允许使用新数据来更新现有知识(先验)。然而,该方法对功能形式假设特别敏感,显然无害的模型选择,例如先验分布的偏斜度,可能会导致非常不同的估计结果。

搜索与无限滚动

在接下来的文章中,我们将使用一个玩具示例,该示例大致受到Azavedo et al. (2019)的启发:一个搜索引擎希望在不牺牲搜索质量的情况下增加其广告收入。我们是一家具有成熟实验文化的公司,我们持续测试新想法以改进我们的着陆页。假设我们想出了一个新的绝妙主意:无限滚动! 我们允许用户继续向下滚动,以查看更多结果,而不是使用离散的页面序列。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片,由作者使用NightCafé生成

为了理解无限滚动是否有效,我们进行了AB 测试:我们将用户随机分配到处理组和对照组,并仅对处理组的用户实施无限滚动。我从[src.dgp](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/src/dgp.py)导入了数据生成过程dgp_infinite_scroll()。相对于之前的文章,我生成了一个新的 DGP 父类,处理随机化和数据生成,而其子类包含具体的用例。我还从[src.utils](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/src/utils.py)导入了一些绘图函数和库。为了包含代码、数据和表格,我使用了Deepnote,一个类似 Jupyter 的基于网络的协作笔记本环境。

我们有关于 10,000 名网站访问者的信息,其中我们观察他们生成的每月ad_revenue、他们是否被分配到处理组并使用了infinite_scroll,以及平均每月的past_revenue

随机处理分配使得均值差异估计量无偏:我们期望处理组和对照组在平均上是可比的,因此我们可以将观察到的平均结果差异归因于处理效果。我们通过线性回归估计处理效果。我们可以将infinite_scroll的系数解释为估计的处理效果。

看来infinite_scroll确实是个好主意,它将平均月收入提高了 0.1524 美元。此外,该效果在 1%置信水平下显著不同于零。

我们可以通过在回归中控制past_revenue来进一步提高估计量的精度。我们不期望估计系数有显著变化,但精度应该会提高(如果你想了解更多控制变量的内容,可以查看我关于 CUPED 和 DAGs 的其他文章)。

确实,past_revenue 对当前 ad_revenue 的预测性很强,而 infinite_scroll 的估计系数的精度减少了三分之一。

到目前为止,一切都非常标准。然而,正如我们在开始时所说,假设这不是我们尝试改善浏览器(最终目标是广告收入)的唯一实验。无限滚动只是我们过去测试的成千上万种想法中的一种。是否有办法有效利用这些额外的信息

贝叶斯统计

贝叶斯统计相比于频率学派方法的主要优点之一是,它可以轻松地将额外信息纳入模型中。这一思想直接来源于所有贝叶斯统计的核心定理:贝叶斯定理。贝叶斯定理允许你通过反转推理问题进行模型推断:从给定数据的模型概率,推断数据在给定模型下的概率,这是一种更易处理的对象。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

贝叶斯定理,由作者提供的图片

我们可以将贝叶斯定理的右侧拆分为两个部分:先验似然。似然是来自数据的模型信息,而先验则是关于模型的任何额外信息。

首先,让我们将贝叶斯定理映射到我们的背景中。数据是什么,模型是什么,我们的兴趣对象是什么?

  • 数据由我们的结果变量 ad_revenuey、处理变量 infinite_scrollD 和其他变量 past_revenue 以及一个常量组成,我们将这些变量统称为 X

  • 模型ad_revenue 的分布,给定 past_revenueinfinite_scroll 特征,y|D,X

  • 我们的兴趣对象是后验概率 Pr(model | data),特别是 ad_revenueinfinite_scroll 之间的关系

我们如何在 AB 测试的背景下使用先验信息,可能还包括额外的协变量?

贝叶斯回归

让我们使用线性模型,使其与频率学派方法直接可比:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

条件分布 y|x,由作者提供的图片

这是一个具有两组参数的参数模型:线性系数 βτ,以及残差的方差 σ。一种等效但更具贝叶斯风格的模型表示方式是:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

条件分布 y|x,由作者提供的图片

半冒号将数据与模型参数分开。与频率学派方法不同,在贝叶斯回归中,我们不依赖于中心极限定理来近似 y 的条件分布,而是直接假设它是正态分布。

我们对模型参数 βτσ 的推断感兴趣。频率主义方法和贝叶斯方法之间的另一个核心区别是,前者假设模型参数是固定且未知的,而后者允许它们是随机变量。

这个假设具有非常实用的含义:你可以轻松地以先验分布的形式将关于模型参数的先前信息纳入模型。顾名思义,先验包含在查看数据之前可用的信息。这引出了贝叶斯统计中的一个最相关的问题:如何选择先验

先验

选择先验时,一个在分析上有吸引力的限制是拥有一个使得后验属于同一家族的先验分布。这些先验被称为共轭先验。例如,在看到数据之前,我假设我的处理效应服从正态分布,并希望在结合数据中包含的信息后,它仍然是正态分布的。

在贝叶斯线性回归的情况下,βτσ 的共轭先验是正态分布和逆伽马分布。让我们从盲目使用标准正态分布和逆伽马分布作为先验开始。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

先验分布,图片由作者提供

我们使用概率编程包 PyMC 进行推断。首先,我们需要指定模型:不同参数的先验分布和数据的似然。

PyMC 具有一个极好的功能,可以将模型可视化为图形,即 model_to_graphviz

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

模型示意图,图片由作者提供

从图形表示中,我们可以看到各种模型组件,它们的分布,以及它们如何相互作用。

我们现在准备计算模型后验。它是如何工作的?简而言之,我们对模型参数进行采样,计算给定这些值的数据的似然,并推导出相应的后验。

贝叶斯推断要求采样,这一点历史上一直是贝叶斯统计的主要瓶颈之一,因为这使得贝叶斯统计比频率主义方法明显慢。然而,随着模型计算机计算能力的增加,这不再是一个大问题。

我们现在可以检查结果了。首先,通过 summary() 方法,我们可以打印一个非常类似于我们用于线性回归的 [statsmodels](https://www.statsmodels.org/dev/index.html) 包生成的模型摘要。

估计的参数与我们使用频率主义方法得到的结果非常接近,其中 infinite_scroll 的估计效应为 0.157。

如果抽样的缺点是速度较慢,那么它的优点是透明。我们可以直接绘制后验分布。让我们为处理效应τ做这件事。PyMC 函数plot_posterior绘制了后验分布,黑色条表示贝叶斯等效的 95%置信区间。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

τ̂的后验分布,由作者提供的图像

正如预期的那样,由于我们选择了共轭先验,后验分布看起来是高斯的。

到目前为止,我们在选择先验时没有太多指导。然而,假设我们有过去实验的数据。我们如何将这些具体信息纳入其中?

过去的实验

假设无限滚动的想法只是我们过去尝试和测试的其他想法中的一个。对于每个想法,我们都有相应实验的数据,以及对应的估计系数。

我们从过去的实验中生成了 1000 个估计值。我们如何利用这些额外的信息?

正态先验

第一个想法是校准我们的先验,以反映过去的数据分布。在保持正态性假设的情况下,我们使用过去实验的估计平均值和标准差。

平均而言,几乎对ad_revenue没有影响,平均效果为 0.0009。

然而,实验间存在合理的变异,标准差为 0.029。

让我们重写模型,使用过去估计值的均值和标准差作为τ的先验分布。

让我们从模型中进行抽样

并绘制处理效应参数τ的样本后验分布。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

τ̂的后验分布,由作者提供的图像

估计系数明显更小:0.11 而不是之前的 0.16。这是为什么?

实际情况是,考虑到我们的先验,之前的 0.16 系数极不可能。我们可以计算在先验下得到相同或更极端值的概率。

这个值的概率几乎为零。因此,估计的系数已经朝着 0.0009 的先验均值移动。

Student-t 先验

到目前为止,我们对所有线性系数假设了正态分布。这是否合适?让我们通过视觉检查(查看这里了解其他比较分布的方法),从截距系数β₀开始。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分布似乎相当正常。处理效应参数τ呢?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

该分布非常重尾!虽然在中心看起来像正态分布,但尾部要“肥胖”得多,并且有几个非常极端的值。排除测量误差,这是行业中经常发生的情形,大多数想法效果极小或无效,只有少数想法是突破性的。

模拟这种分布的一种方法是学生-t 分布。特别地,我们使用均值为 0.0009、方差为 0.003、自由度为 1.3 的 t-学生分布,以匹配过去估计的经验分布的矩。

让我们从模型中抽样。

并绘制处理效应参数τ的样本后验分布。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

τ̂的后验分布,作者提供的图像

估计的系数现在再次类似于我们使用标准正态先验时得到的值,0.11。然而,估计值更为精确,因为置信区间已从[0.077, 0.016]缩小到[0.065, 0.015]。

发生了什么?

收缩

答案在于我们使用的不同先验分布的形状:

  • 标准正态分布,N(0,1)

  • 匹配矩的正态分布,N(0, 0.03)

  • 匹配矩的 t-学生,t₁.₃(0, 0.003)

让我们把所有数据一起绘制出来。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不同的先验分布,作者提供的图像

如我们所见,所有分布都以零为中心,但形状却大相径庭。标准正态分布在[-0.15, 0.15]区间内基本上是平坦的。每个值的概率几乎相同。最后两个分布虽然均值和方差相同,但形状却大相径庭。

这如何转化为我们的估计?我们可以为每个先验分布绘制隐含的后验。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

先验对实验估计的影响,作者提供的图像

如我们所见,不同的先验对实验估计的影响方式截然不同。标准正态先验对[-0.15, 0.15]区间内的估计基本没有影响。匹配矩的正态先验则将每个估计值大约缩小了 2/3。t-学生先验的效果则是非线性的:它将小的估计值向零收缩,而保持大的估计值不变。虚线灰色标记了不同先验对我们的实验估计τ̂的影响。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像由作者使用NightCafé生成

结论

在这篇文章中,我们已经看到如何扩展 AB 测试的分析以融入过去实验的信息。特别是,我们介绍了贝叶斯方法用于 AB 测试,并且我们已经看到选择先验分布的重要性。在相同的均值和方差下,假设具有“胖尾”(非常偏斜)的先验分布意味着小效果的收缩更强,而大效果的收缩则较低。

直觉如下:具有“胖尾”的先验分布等同于假设突破性想法是稀有但不是不可能的。这在实验之后有实际的影响,正如我们在这篇文章中所见,但在实验之前也是如此。事实上,如Azevedo 等人(2020)所报告,如果你认为你的想法效果的分布更“正常”,则最佳方案是进行少量但大的实验,以便发现较小的效果。如果相反,你认为你的想法是“突破性还是无”,即它们的效果是胖尾的,那么进行小而多的实验更有意义,因为你不需要大样本来检测大效果。

参考文献

相关文章

代码

你可以在这里找到原始的 Jupyter Notebook:

[## Blog-Posts/bayes_ab.ipynb at main · matteocourthoud/Blog-Posts

你现在无法执行该操作。你在其他标签页或窗口中登录了账户。你已在其他标签页或…

github.com](https://github.com/matteocourthoud/Blog-Posts/blob/main/notebooks/bayes_ab.ipynb?source=post_page-----ed45cc8c964d--------------------------------)

感谢阅读!

非常感谢! 🤗 如果你喜欢这篇文章并希望查看更多内容,可以考虑 关注我。我每周发布一次关于因果推断和数据分析的主题。我尽量保持文章简洁而精准,始终提供代码、示例和模拟。

另外,一个小小的 免责声明:我写作是为了学习,所以错误是常态,尽管我尽力而为。如果你发现错误,请告诉我。我也欢迎对新主题的建议!*

使用 Pyro 的贝叶斯 AB 测试

原文:towardsdatascience.com/bayesian-ab-testing-with-pyro-cd4daff686e1?source=collection_archive---------8-----------------------#2023-11-15

使用 Pyro 的贝叶斯思维和 AB 测试入门

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 弗雷泽·布朗

·

关注 发布于 Towards Data Science ·13 分钟阅读·2023 年 11 月 15 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

版权归 Free-Photos 所有,来源于 Pixabay

本文介绍了使用 Python 概率编程语言(PPL)Pyro 进行 AB 测试的基本知识,Pyro 是 PyMC 的一个替代品。撰写这篇文章的动机是为了进一步理解使用 Pyro 框架的贝叶斯统计推断,并在此过程中帮助他人。因此,我们欢迎并鼓励反馈。

介绍

我之前在 Python 中使用 PyMC 进行贝叶斯建模的经验,但我发现使用一些最新版本有些麻烦。我研究其他概率编程语言时发现了 Pyro,这是由 Uber 构建的通用 PPL,并由 PyTorch 在后端支持。下面链接了 Pyro 和 PyTorch 的文档。

[## Pyro

深度通用概率编程

pyro.ai [## PyTorch 文档 - PyTorch 2.1 文档

PyTorch 是一个优化的张量库,用于使用 GPU 和 CPU 进行深度学习。本文档描述的功能…

pytorch.org

在探索 Pyro 的过程中,我发现很难找到使用该软件包进行端到端教程的资源。本文旨在填补这一空白。

本文共有五个部分。在第一部分中,我将介绍贝叶斯思维的基础,这是一种哲学背景,方法论由此而言。我将简要介绍 Pyro 和用于进行统计推断的贝叶斯方法的技术背景。接下来,我将使用 Pyro 进行 AB 测试并讨论结果。然后,我将解释在业务环境中进行贝叶斯 AB 测试的案例。最后一部分将进行总结。

贝叶斯思维入门

贝叶斯过程在高层次上相对简单。首先,您有一个感兴趣的变量。我们陈述我们对该变量的当前理解,我们将其表达为一个概率分布(在贝叶斯的世界中,一切都是概率分布)。这称为先验。贝叶斯推断中的概率是认识论的,这意味着它通过我们的知识程度而来。因此,我们陈述的概率分布作为我们的先验,既是对不确定性的陈述,也是对理解的理解。然后,我们观察来自现实世界的事件。然后使用这些观察结果更新我们对我们感兴趣的变量的理解。我们理解的新状态称为后验。我们还用概率分布来表征后验,即给定我们观察到的数据,我们感兴趣的变量的概率。此过程如下图所示的流程图所示,其中 theta 是我们感兴趣的变量,data 是我们观察到的事件的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

贝叶斯过程

贝叶斯过程实际上是循环的,因为我们会重复这个过程,从头开始,但在观察世界后使用我们新获得的知识。我们的旧后验变成了我们的新先验,我们观察新的事件,并再次更新我们的理解,正如内特·西尔弗在他的书信号与噪声中所说,我们变得“越来越少错误”。我们不断减少对变量的不确定性,趋向于一种确定状态(但我们永远无法确定)。在数学上,这种思维方式由贝叶斯定理封装。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

贝叶斯定理

在实际操作中,贝叶斯定理很快会因为大模型的高维度变得计算不可行。解决这一问题的方法有很多,但我们今天使用的方法称为马尔可夫链蒙特卡洛(Markov Chain Monte Carlo,简称 MCMC)。MCMC 是一类算法(我们将使用一种叫做哈密顿蒙特卡洛的方法),它智能地从概率分布中抽取样本。深入探讨这一点超出了本文的范围,但我会在文末提供一些有用的参考资料以供进一步阅读。目前,知道贝叶斯定理过于复杂而无法计算,因此我们利用先进算法如 MCMC 的强大功能来帮助解决问题就足够了。本文将重点介绍使用 Pyro 的 MCMC。然而,Pyro 确实强调使用基于近似的方法的随机变分推断(Stochastic Variational Inference,简称 SVI),但这里不会涉及。

使用 Pyro 进行 AB 测试

设想一个公司设计了一个新的网站着陆页,并希望了解这对转化率的影响,即访客在访问该页面后是否会继续在网站上停留。在测试组 A 中,网站访客将看到当前的着陆页。在测试组 B 中,网站访客将看到新的着陆页。在接下来的文章中,我将把测试组 A 称为对照组,将组 B 称为处理组。该公司对变化持怀疑态度,并选择了 80/20 的会话流量分配。每个测试组的访客总数和页面转化总数总结如下。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

测试观察

AB 测试的原假设是两个测试组的页面转化率没有变化。在频率派框架下,对于双侧检验,这将表达为以下形式,其中 r_c 和 r_t 分别是对照组和处理组的页面转化率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

原假设和替代假设

然后,显著性检验将尝试拒绝或未能拒绝原假设。在贝叶斯框架下,我们通过对每个测试组施加相同的先验来略微不同地表达原假设。

让我们暂停一下,准确概述一下我们测试期间发生的情况。我们感兴趣的变量是页面转化率。这可以通过计算独立转化访客的数量与总访客数量的比率来简单得出。产生这个比率的事件是访客是否点击了页面。对于每个访客,这里只有两种可能的结果,访客要么点击页面并转化,要么不点击。你们中的一些人可能会认识到,对于每个独立的访客,这是伯努利试验的一个例子;有一次试验和两种可能的结果。现在,当我们收集一组这些伯努利试验时,我们就有了一个二项分布。当随机变量 X 具有二项分布时,我们使用以下符号:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

二项分布符号

其中 n 是访客数量(或伯努利试验的数量),p 是每次试验中事件发生的概率。p 是我们在这里感兴趣的,我们想了解每个测试组中访客在页面上转化的概率。我们已经观察到了一些数据,但如前一部分所述,我们首先需要定义我们的先验。正如贝叶斯统计学中所示,我们需要将这个先验定义为一个概率分布。正如之前提到的,这个概率分布是我们不确定性的一个表征。Beta 分布通常用于建模概率,因为它定义在[0,1]的区间之间。此外,使用 beta 分布作为二项似然函数的先验给了我们一个有用的性质——共轭性,这意味着我们的后验将从与先验相同的分布中生成。我们称 beta 分布为共轭先验。Beta 分布由两个参数定义,alpha 和混淆的 beta。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Beta 分布符号

有了历史数据,我们可以断言一个有根据的先验。我们不一定需要历史数据,我们可以使用我们的直觉来指导我们的理解,但现在假设我们两者都没有(在本教程稍后我们将使用有根据的先验,但为了演示影响,我将从无先验开始)。假设我们对公司网站的转化率没有理解,因此将我们的先验定义为 Beta(1,1)。这被称为平坦先验。这个函数的概率分布如下图所示,与定义在区间[0,1]之间的均匀分布相同。通过断言 Beta(1,1)先验,我们说所有可能的页面转化率值是等概率的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

版权:作者

现在我们拥有了所需的所有信息,包括先验和数据。让我们跳入代码中。此处提供的代码将为使用 Pyro 进行 AB 测试提供一个框架;因此,它忽略了该软件包的一些功能。为了进一步优化您的代码并充分利用 Pyro 的能力,我建议参考官方文档。

首先,我们需要导入我们的包。最后一行是良好的实践,特别是在处理笔记本时,用于清除我们已建立的参数存储。

import pyro
import pyro.distributions as dist
from pyro.infer import NUTS, MCMC
import torch
from torch import tensor
import matplotlib.pyplot as plt
import seaborn as sns
from functools import partial
import pandas as pd

pyro.clear_param_store()

Pyro 中的模型被定义为常规的 Python 函数。这一点很有帮助,因为它使得跟随过程变得直观。

def model(beta_alpha, beta_beta):
    def _model_(traffic: tensor, number_of_conversions: tensor):
        # Define Stochastic Primatives
        prior_c = pyro.sample('prior_c', dist.Beta(beta_alpha, beta_beta))
        prior_t = pyro.sample('prior_t', dist.Beta(beta_alpha, beta_beta))
        priors = torch.stack([prior_c, prior_t])
        # Define the Observed Stochastic Primatives
        with pyro.plate('data'):
            observations = pyro.sample('obs', dist.Binomial(traffic, priors),\
                             obs = number_of_conversions)
    return partial(_model_)

这里有一些需要分解和解释的内容。首先,我们有一个包裹在外层函数中的函数,外层函数返回内层函数的部分函数。这使我们可以更改先验,而无需更改代码。我将内层函数中定义的变量称为原始变量,可以将原始变量视为模型中的变量。模型中有两种类型的原始变量,即随机和观察随机。在 Pyro 中,我们不需要显式定义差异,只需在 sample 方法中添加 obs 参数,当它是观察原始变量时,Pyro 会相应地解释它。观察原始变量包含在上下文管理器 pyro.plate() 中,这是最佳实践,使我们的代码看起来更清晰。我们的随机原始变量是两个由 Beta 分布表征的先验,由我们从外层函数传入的 alpha 和 beta 参数控制。如前所述,我们通过将它们定义为相等来断言零假设。然后,我们使用 tensor.stack() 将这两个原始变量堆叠在一起,该操作类似于连接一个 Numpy 数组。这将返回一个张量,这是 Pyro 中推断所需的数据结构。我们已经定义了模型,现在让我们进入推断阶段。

如前所述,本教程将使用 MCMC。下面的函数将使用我们上面定义的模型和我们希望用于生成后验分布的样本数量作为参数。我们还将数据传递给函数,正如我们为模型所做的那样。

def run_infernce(model, number_of_samples, traffic, number_of_conversions):
    kernel = NUTS(model)

    mcmc = MCMC(kernel, num_samples = number_of_samples, warmup_steps = 200)

    mcmc.run(traffic, number_of_conversions)

    return mcmc

此函数中的第一行定义了我们的内核。我们使用 NUTS 类定义我们的内核,NUTS 代表 No-U-Turn Sampler,是 Hamiltonian Monte Carlo 的自调版本。这告诉 Pyro 如何从后验概率空间进行采样。同样,深入探讨这个主题超出了本文的范围,但目前知道 NUTS 允许我们智能地从概率空间中进行采样就足够了。然后在第二行中使用该内核初始化 MCMC 类,指定使用 NUTS。我们将 number_of_samples 参数传递给 MCMC 类,该参数是用于生成后验分布的样本数量。我们将初始化的 MCMC 类分配给 mcmc 变量,并调用 run()方法,将我们的数据作为参数传递。该函数返回 mcmc 变量。

这就是我们需要的一切;以下代码定义了我们的数据并调用了我们刚刚使用 Beta(1,1)先验创建的函数。

traffic = torch.tensor([5523., 1379.])
conversions =torch.tensor([2926., 759.])
inference = run_infernce(model(1,1), number_of_samples = 1000, \
               traffic = traffic, number_of_conversions = conversions)

交通和转换张量的第一个元素是对照组的计数,而每个张量中的第二个元素是处理组的计数。我们将模型函数、控制我们先验分布的参数以及我们定义的张量一起传入。运行此代码将生成我们的后验样本。我们运行以下代码以提取后验样本并将它们传递到一个 Pandas 数据框中。

posterior_samples = inference.get_samples()
posterior_samples_df = pd.DataFrame(posterior_samples)

请注意该数据框的列名是我们在定义模型函数时传递的字符串。数据框中的每一行包含从后验分布中抽取的样本,每个样本代表页面转换率的估计值,即控制我们的二项分布的概率值 p。现在我们已经返回了样本,我们可以绘制我们的后验分布。

结果

一种有洞察力的方式来可视化两个测试组的 AB 测试结果是通过联合核密度图。它允许我们可视化两个分布中概率空间的样本密度。下面的图可以从我们刚刚构建的数据框中生成。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

信誉:作者

上图中的概率空间可以沿对角线进行划分,任何在该线以上的区域表明处理组的转化率估计高于对照组,反之亦然。如图所示,从后验中抽取的样本在该区域密集分布,这表明处理组的转化率更高。值得强调的是,处理组的后验分布比对照组更宽,这反映了更高的的不确定性。这是由于在处理组中观察的数据较少。然而,该图强烈表明处理组优于对照组。通过从后验中收集一系列样本并计算元素间差异,我们可以说处理组优于对照组的概率为 90.4%。这一数据表明,90.4%的从后验中抽取的样本将在上述联合密度图中的对角线以上分布。

这些结果是通过使用平坦(无信息)先验获得的。使用有信息的先验可能有助于改进模型,尤其是在观察数据有限的情况下。一个有用的练习是探讨使用不同先验的效果。下图显示了 Beta(2,2)概率密度函数以及当我们重新运行模型时生成的联合图。我们可以看到,使用 Beta(2,2)先验为两个测试组产生了非常相似的后验分布。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

版权归:作者

从后验中抽取的样本表明,处理组比对照组表现更好的概率为 91.5%。因此,我们可以以更高的确定性认为处理组优于对照组,而不是使用平坦先验。然而,在这个例子中,差异微不足道。

我还想强调一件事。当我们进行推断时,我们告诉 Pyro 从后验中生成 1000 个样本。这是一个任意的数字,选择不同数量的样本可能会改变结果。为了突出增加样本数量的效果,我运行了一个 AB 测试,其中控制组和处理组的观察数据相同,每组的总体转化率为 50%。使用 Beta(2,2)先验会生成以下后验分布,随着样本数量的逐步增加。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

版权归:作者

当我们仅使用 10 个样本进行推断时,控制组和治疗组的后验分布相对较宽且采用不同的形状。随着我们绘制的样本数增加,这些分布最终会收敛,生成几乎相同的分布。此外,我们观察到统计分布的两个性质,即中心极限定理和大数定律。中心极限定理表明,随着样本数量的增加,样本均值的分布会收敛于正态分布,我们可以在上述图中看到这一点。此外,大数定律表明,随着样本大小的增加,样本均值会收敛于总体均值。我们可以看到底部右侧瓷砖中的分布均值约为 0.5,这是在每个测试样本中观察到的转化率。

贝叶斯 AB 测试的商业案例

贝叶斯 AB 测试可以帮助提升企业的测试与学习文化。贝叶斯统计推断允许快速检测测试中的小幅提升,因为它不依赖于长期概率来得出结论。测试结论可以更快地得出,从而提高学习速度。贝叶斯 AB 测试还允许在测试过程中早期停止测试,如果通过“窥探”获得的结果表明测试组表现显著不如对照组,则可以停止测试。因此,测试的机会成本可以显著降低。这是贝叶斯 AB 测试的一个主要优势;结果可以不断监测,并且我们的后验概率不断更新。相反,对测试对象的提升的早期检测可以帮助企业更快地实施变更,减少实施提高收入变更的延迟。面向客户的企业必须能够快速实施和分析测试结果,这正是贝叶斯 AB 测试框架的优势。

总结

本文简要介绍了贝叶斯思维的背景,并使用 Pyro 探索了贝叶斯 AB 测试的结果。希望您会觉得这篇文章有见地。正如介绍中所述,欢迎并鼓励反馈。正如承诺的那样,我已在下面链接了一些进一步阅读材料。

推荐材料

下列书籍深入探讨贝叶斯推断:

  • The Signal and the Noise: The Art and Science of Prediction — Nate Silver

  • The Book of Why: The New Science of Cause and Effect — Judea Pearl 和 Dana Mackenzie。尽管这本书主要关注因果关系,但第三章关于贝叶斯的阅读也是值得一读的。

  • Bayesian Methods for Hackers: Probabilistic Programming and Bayesian Inference — Cameron Davidson-Pilon。这本书也可以在 Git 上找到,我已经链接在下面。

[## GitHub — CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers: 又名“黑客的贝叶斯方法”]

又名“黑客的贝叶斯方法”:介绍贝叶斯方法 + 概率编程的…

github.com](https://github.com/CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers?source=post_page-----cd4daff686e1--------------------------------)

这些 Medium 文章提供了 MCMC 的详细解释。

## Monte Carlo Markov Chain (MCMC) 解释

MCMC 方法是一类使用马尔可夫链来执行蒙特卡罗估计的算法。

towardsdatascience.com ## 贝叶斯推断问题、MCMC 和变分推断

贝叶斯推断问题在统计学中的概述。

towardsdatascience.com

在 SQL 中使用“NOT IN”要小心

原文:towardsdatascience.com/be-careful-when-using-not-in-in-sql-c692fad3427b

+ 3 个简单的解决方案,以确保你不会被困住

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传Matt Chapman

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

最近,我发现了Benjamin Thürer的精彩文章:

## 如何避免在 Google BigQuery / SQL 中常见的五个错误

在使用 BigQuery 多年过程中,我观察到即使是经验丰富的数据科学家也常犯的 5 个问题

towardsdatascience.com

…他警告我们在 BigQuery 中使用NOT IN SQL 子句的注意事项。

在这篇文章中,我将通过提供更多示例、解决方案和练习题来深入探讨他所说的内容。

如果你想理解为什么NOT IN子句有风险——以及如何应对——请继续阅读!

问题是:NOT IN处理NULL的方式可能与你预期的不同

INNOT IN操作符提供了一种逻辑方式来比较数组。例如,如果你写:

SELECT 
  3 IN (1, 2, 3) # Output = true

BigQuery 将返回true。如果你写:

SELECT 
  3 NOT IN (1, 2, 3) # Output = false

BigQuery 将返回false

看起来很简单,对吧?但问题在于:当查找数组包含NULL值时,INNOT IN会表现异常。例如,以下代码将返回NULL,而不是false

SELECT
  3 NOT IN (1, 2, NULL) # Output = NULL

为了了解这为什么重要,请查看这三张表,每张表都包含一系列名称:

`table_1`      `table_2`      `table_3`
+---------+    +---------+    +---------+
| name    |    | name    |    | name    |
+---------+    +---------+    +---------+
| Matt    |    | Matt    |    | Matt    |
| Sam     |    | Sam     |    | Sam     |
| Frankie |    +---------+    | NULL    |
| Ben     |                   +---------+
+---------+

如果你想找到table_1中不在table_2中的所有名称,我们可以使用NOT IN子句:

SELECT name
FROM table_1
WHERE name NOT IN (SELECT name FROM table_2)

# Output
# +---------+
# | name    |
# +---------+
# | Frankie |
# | Ben     |
# +---------+

NOT IN 操作符使我们能够找到两个正确的名字:“Frankie”和“Ben”。用技术 SQL 术语来说,这种操作称为“反半连接”(它是一个连接,因为我们没有在table_1table_2之间进行完全连接;我们只是检查table_2中是否存在某些行,并且它是一个半连接,因为我们在检查table_2是否包含这些行(1))。

然而,如果我们尝试使用相同的逻辑来选择table_1中不在table_3(包含NULL值的表)中的所有名字,我们将得到以下结果:

SELECT name
FROM table_1
WHERE name NOT IN (SELECT name FROM table_3) # Changed table_2 to table_3

# Output
# 

奇怪,对吧?

根据我们第一次查询的顺利程度,我们可能会期望返回“Frankie”和“Ben”这两个名字。但是在 BigQuery 中,我们将什么也得不到。

在 SQL 中,NULL的处理方式不同

在许多 SQL 方言中,包括 GoogleSQL(BigQuery 默认使用的方言),NOT IN 操作符将返回NULL,“如果右侧的任何值为NULL”(2, 3)。

这就是我们刚刚观察到的行为的根本原因——(SELECT name FROM table_3)返回的数组包含了NULL值,因此我们的整个查询最终没有返回数据。

为什么会发生这种情况?

在 SQL 中,NULL值的处理方式不同于其他(非空)值,如15.12truebotswanaNULL对象的值不是“0”或“False”;它被视为未知未定义。当我们尝试与一个未知对象进行比较时,这种比较的结果也会是未知的。

一个简单的解决方案

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Marcel Eberle 提供,来源于 Unsplash

幸运的是,有一些简单的解决方法来解决这个问题。

1. 从查找数组中删除NULL

这一个相当直接:只需从table_3中过滤掉NULL行即可。这比其他解决方案(见下文)稍微计算开销大一点,但在较小的表上仍能很好地完成任务。

SELECT name
FROM table_1
WHERE name NOT IN (
  SELECT name
  FROM table_3
  WHERE name IS NOT NULL # Remove NULLs
)

2. 使用LEFT JOIN

我喜欢这个解决方案——它可能开始时有点困惑,但当你弄明白后会发现它非常优雅。

首先,我们将table_1table_3name列(即它们共同拥有的列)上进行连接。然后,我们对table_3应用WHERE过滤器,以便仅返回NULL的行。

SELECT name
FROM table_1
LEFT JOIN table_3 ON table_1.name = table_3.name
WHERE table_3.name IS NULL

# Output
# +---------+
# | name    |
# +---------+
# | Frankie |
# | Ben     |
# +---------+

迷惑?如果我们不从table_3中包含name列并且不应用那个WHERE过滤器,结果会怎样:

SELECT 
  table_1.name,
  table_3.name
FROM table_1
LEFT JOIN table_3 ON table_1.name = table_3.name

# Output
# +--------------+--------------+
# | table_1.name | table_3.name |
# +--------------+--------------+
# | Matt         | Matt         |
# | Sam          | Sam          |
# | Frankie      | NULL         |
# | Ben          | NULL         |
# +--------------+--------------+

当我们应用WHERE table_3.name IS NULL过滤器时,我们只是删除了前两行。

3. 使用WHERE NOT EXISTS

EXISTS 操作符对于检查子查询是否包含任何行非常有用。它返回true“如果子查询产生一行或多行”,返回false“如果子查询产生零行”(4)。

有趣的是,因为我们只是检查子查询中是否存在行,所以我们SELECT哪一列并不重要;例如,在下面的示例中,我只选择了“1”,但我也可以选择“true”、“5.12”或“botswana”。你选择什么都无所谓!

SELECT name
FROM table_1
WHERE NOT EXISTS (
  SELECT 
    1 # Any value/column will do here: `name`, 5.12, and "botswana" would also work!
  FROM table_3 
  WHERE table_1.name = table_3.name
)

检查你的理解

因为这是一个棘手的概念,我在我的 SQL 练习网站上写了 4 个练习题,你可以用来检查你对INNOT IN的理解,the-sql-gym.com。如果你想巩固学习,请去看看吧!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供,来自 the-sql-gym.com

还有一件事 —

我开始了一份免费的新闻通讯,AI in Five,每周分享 5 个要点,涉及最新的 AI 新闻、编码技巧和数据科学家/分析师的职业故事。如果这对你来说很有吸引力,订阅这里

感谢阅读,随时通过 TwitterLinkedIn 联系我! 😃

Beam Search: 序列模型中使用最广泛的算法

原文:towardsdatascience.com/beam-search-the-most-used-algorithm-in-sequence-models-107d56b23556

学习最著名的文本翻译和语音识别算法的工作原理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Riccardo Andreoni

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Beam Search 允许考虑多个候选流。图片来源:unsplash.com

想象你是一个AI语言模型,比如ChatGPT,在完成一个句子时。你如何选择下一个词,使其不仅语法正确而且上下文相关?这就是Beam Search发挥作用的地方。

通过高效并行探索多个可能性并在每一步保持最佳候选,Beam Search 在预测后续元素的任务中发挥着至关重要的作用。作为一种有效且强大的算法,它确保输出符合语法约束和上下文。

要理解 Beam Search 的影响,请考虑所有需要精确序列生成的应用,例如语言翻译文本补全聊天机器人。在所有这些应用中,Beam Search 都发挥着关键作用。

在这篇文章中,我将介绍 Beam Search 算法的理论,并带你通过一个实际的逐步示例。我还将介绍几种 Beam Search 变体,并详细讲解这个基础算法的所有优缺点。

理解 Beam Search

想象一下你需要将以下句子从西班牙语翻译成英语:

Pablo 将于下周在纽约。

我们不仅仅希望获得正确的翻译,我们还希望获得最佳的翻译。对于语言模型而言,最佳输出就是最可能的输出

为了完成这项任务,大多数序列到序列模型使用 Beam Search。它作为一种启发式算法,系统地并行探索多个可能性。在每一步中,定义的“beam width”保持固定数量的最佳候选项。这使得算法能够探索多个候选项。

这种方法模拟决策过程,模型会评估并选择最有前景的选项。

Beam Search 逐步过程

考虑一个标准的序列到序列模型,下面是一个简单的网络表示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

在每个时间步t,模型输出一个单词,用于组成最终的序列。最终序列将是所提供的翻译句子。

如果你对 RNN 不太熟悉,我建议查看我在本文中包含的简单介绍:

使用深度学习生成幻想名字:从零开始构建语言模型

一个语言模型能否发明独特的幻想角色名字?让我们从零开始构建它

如何使用深度学习生成幻想角色名字:从零开始构建语言模型

对于文本翻译,我们通常使用一个编码器,它是网络的一部分,负责将输入序列转换为向量形式。在这种情况下,输入序列是要翻译的西班牙语句子。编码器后面跟着一个解码器,它在每个时间步返回输出序列的一部分。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

让我们详细查看第一步。

编码器(绿色部分)接收西班牙语句子“Pablo estará en Nueva York la próxima semana.”并将其转换为向量形式提供给解码器(蓝色部分)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

在时间步 1,解码器将输出翻译的第一个单词。关键问题是:

解码器如何选择单词?

解码器根据输入序列x选择最可能的单词。换句话说,对于字典中的每个单词,模型计算其成为输出序列第一个单词的对应概率。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

选择使得概率最大的那个单词。

这就是Beam Search 进入游戏的地方:它引入了一个参数 B,称为beam width,表示模型在每一步选择的单词数量。

比如,设置 B=3,模型将返回不仅是一个而是三个候选单词。

假设翻译中最可能的首个单词是“Pablo”、“Next”和“During”。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

对于这 3 个候选词中的每一个,模型通过猜测第二个单词 在英文翻译中继续处理。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

与往常一样,模型选择最大化给定概率的单词。在这种情况下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在你可以清楚地看到 Beam Search 名字的由来。从最初的 3 个候选集开始,算法将在每一步扩大它,考虑所有这些组合的概率。

第 2 步的输出包括 9 个候选序列:“Pablo will”、“Pablo is”、……、“During next”和“During one”。模型将每一个与一个概率相关联。通过设置参数 B=3,模型将仅保留三个概率较高的候选序列。

假设概率较高的候选词是“Pablo will”、“Pablo is”和“Next week”。在第 3 步中,解码器将为每个候选词猜测 3 个后续单词。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由作者提供。

相同的过程将重复进行,直到模型将句子结束标记预测为最可能的单词。EOS标记表示序列已达到终点,现在被视为完成。

Beam Search 变体

Beam Search 的适应性可以通过算法的几种变体来增强。这些变体提供了适用于不同序列生成任务的更多或更少合适的选项。最常见的变体包括:

  • Length-Normalized Beam Search:它通过规范化概率分数来调整句子长度差异,减少对较短序列的偏见。

  • Diverse Beam Search:它在候选选择中引入多样性,以防止算法过早收敛,促进对更广泛解决方案空间的探索。

  • Top-k Beam Search:它将每一步考虑的候选数量限制为前 k 个最可能的单词,提供了计算效率而不牺牲性能。

  • Alpha-Diverse Beam Search:它通过引入一个 alpha 参数来控制多样性和优化之间的权衡,从而能够根据特定任务要求进行微调。

  • Nucleus Sampling:它定义了一个概率阈值(nucleus),并从最可能的候选集进行采样,提高算法对高质量序列的关注。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图像来源:unsplash.com

结论

总结来说,在这篇帖子中我们理解了为什么 Beam Search 是最常用的算法,特别是在语言翻译模型的领域中,但也适用于聊天机器人响应。它通过定义的光束宽度系统地并行探索多个可能性,从而提高了生成序列的精准性和上下文相关性。

我相信我提供的逐步示例将使 Beam Search 的抽象概念变得更加具体。

正如我在帖子中常做的那样,我现在将回顾一下 Beam Search 的一些优缺点。该算法的主要优点包括:

  • 精准性:Beam Search 在生成精确且具有上下文相关性的序列方面表现优异。

  • 灵活性:像多样性 Beam Search 和 Top-k Beam Search 这样的变体提供了对多样任务需求的适应性。

  • 效率:它高效地探索解决方案空间,平衡了计算复杂性和性能。

然而,在机器学习中没有免费的午餐。因此,澄清每种算法的缺点是很重要的。在 Beam Search 的情况下,我发现了这些可能的问题:

  • 计算成本:对所有可能性的详尽探索可能会带来计算开销。

  • 潜在收敛:在标准配置下,Beam Search 可能会过早地收敛到次优解。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者提供的图像。

最后,我邀请你探索提供的资源,以更深入地了解这个算法。

如果你喜欢这个故事,考虑关注我,以便获得我即将发布的项目和文章的通知!

参考文献

通过可视化掌握 Python 装饰器

原文:towardsdatascience.com/become-fluent-in-python-decorators-via-visualization-4cc6ac06f2cb

通过可视化理解 Python 装饰器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Chengzhi Zhao

·发布在 Towards Data Science ·阅读时间 7 分钟·2023 年 1 月 23 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Huyen Bui 提供,来源于 Unsplash

Python 装饰器是语法糖。你可以在不显式使用装饰器的情况下实现所有功能。然而,使用装饰器可以使你的代码更加简洁和可读。最终,通过利用 Python 装饰器,你可以写更少的代码行数。

然而,理解 Python 装饰器并不是一个简单的概念。理解 Python 装饰器需要构建块,包括闭包、函数作为对象,以及对 Python 代码执行方式的深刻理解。

许多在线资源讨论了 Python 装饰器,但许多教程只提供了一些示例代码的演示。阅读示例代码可以帮助你对 Python 装饰器有一个基本的理解。当需要实现你的装饰器时,我们可能仍然需要对装饰器概念有更清晰的理解,并且还需要参考在线资源以便详细回顾。

阅读代码有时并不会加深记忆,但看到图像却会有所帮助。在这篇文章中,我想通过一些可视化和有趣的示例来帮助你理解 Python 装饰器。

Python 函数是对象

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Python 函数是对象 | 图片作者

如果我们参加了 Python 编程基础课程,我们可能会看到这样的图示。我们定义了一个变量,这是一个用来引用对象的表示性名称。变量 foo 在某一时刻指向一个对象。它可以是一个列表 [1,2,3,4],也可以是一个字典 [“city”: “New York”],或者是一个字符串 “I like dumplings”

在 Python 中一个较少讨论的话题是变量 foo 也可以指向函数 add()。当变量引用一个函数时,foo 可以像使用 int、str、list 或 dict 等其他类型一样传递。

那么这意味着我可以传递 foo 吗?这使我们能够将函数作为参数传递。此外,我们还可以将函数作为返回类型返回。

# Python Functions are Objects
def I_am_1():
    return 1

def return_1():
    return I_am_1

def add(a, b):
    return a + b

foo = add

del add
## let remove the original defintion

print(foo(return_1()(),2))
## ouput 3
print(foo(foo(return_1()(),2),3))
## output 6

代码讲解

  • 函数定义:我们定义了三个函数,分别是 add,用于加两个对象;I_am_1(),简单返回数字 1;return_1,返回函数 I_am_1

  • 有趣的事实:然后我们将其指向另一个变量 foo。为了证明函数像其他类型一样是对象,我们甚至可以移除原始函数引用 add。其余代码仍然可以运行,因为 foo 引用的是函数的对象。

  • 结果解释return_1()() 起初看起来有些奇怪。如果我们仔细观察,它实际上是调用函数的方式,return_1() 就是 I_am_1,因为它只是返回这个函数。在这种情况下,我们可以将 return_1()()=1 理解为心里活动,因此 foo(1,2) 得到 3 的输出也就不足为奇了。我们还可以将 foo(1,2) 传递给另一个函数。在这种情况下,我们将其传递给了它自己。由于 foo(1,2)=3,外部函数将作为 foo(3,3) 操作。为了得到结果,我们可以将整个函数及其参数传递,并让程序在运行时执行以解读所有内容。

代码可视化

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将函数传递给另一个函数 | 图片作者

Python 装饰器结构

Python 装饰器在不修改原始对象的情况下,转换原始对象的功能。它是一种语法糖,使用户可以更方便地扩展对象的能力,而不是重复一些现有的代码。装饰器是 装饰器设计模式 的 Python 实现(注意:Python 装饰器与装饰器设计模式并不完全相同)。

我们已经讨论了可以将函数作为返回类型返回。现在,我们扩展这个概念,演示装饰器如何工作:我们可以在另一个函数内将函数作为返回类型返回。

为了让我们的示例更有趣,我们可以创建一个装饰器,像一个函数的包装器,然后堆叠多个装饰器。

首先定义我们的函数,并以制作一些饺子为例。🥟🥟🥟

## Python Decorators Basic -- I love dumplings
def add_filling_vegetable(func):
    def wrapper(*args, **kwargs):
        print("<^^^vegetable^^^>")
        func(*args, **kwargs)
        print("<^^^vegetable^^^>")
    return wrapper

def add_dumplings_wrapper(func):
    def wrapper(*args, **kwargs):
        print("<---dumplings_wrapper--->")
        func(*args, **kwargs)
        print("<---dumplings_wrapper--->")
    return wrapper

def dumplings(ingredients="meat"):
    print(f"({ingredients})")

add_dumplings_wrapper(add_filling_vegetable(dumplings))()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients='tofu')
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (tofu)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->

代码讲解

  • 函数定义:我们定义了三个函数,分别是 add_filling_vegetableadd_dumplings_wrapperdumplings。在 add_filling_vegetableadd_dumplings_wrapper 中,我们定义了一个包装函数,该函数包裹在作为参数传递的原始函数周围。此外,我们可以做其他的事情。在这种情况下,我们在原始函数之前和之后打印一些文本。如果原始函数还定义了参数,我们可以通过包装函数传递它们。我们还利用了魔法 *args**kwargs 来提高灵活性。

  • 结果解释

  1. 我们可以通过使用默认的 add_dumplings_wrapper(add_filling_vegetable(dumplings))() 来调用默认的成分 meat,我们可以看到函数是链式调用的。这不易读,我们将很快用装饰器语法来修复它。这里的核心思想是我们不断将函数对象作为参数传递给另一个函数。这个函数做了两件事:1) 继续执行未修改的原始函数;2) 执行附加的代码。整个代码的执行就像是一个往返旅行。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

add_dumplings_wrapper(add_filling_vegetable(dumplings))() | 图片由作者提供

2. 对于 add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients=’tofu’),它通过传递额外的参数将默认成分从 meat 改为 tofu。在这种情况下,*args**kwargs 对于解决复杂性非常有用。包装器函数不需要解包以了解参数的上下文。作为一个装饰器,它可以安全地传递实际函数而不需要修改它们。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

add_dumplings_wrapper(add_filling_vegetable(dumplings))(ingredients=’tofu’)

到目前为止,我们还没有触及装饰器语法。让我们对原始函数的定义做一个小改变,称其为 fancy_dumplings

## Stack decorator, the order matters here
@add_dumplings_wrapper
@add_filling_vegetable
def fancy_dumplings(ingredients="meat"):
    print(f"({ingredients})")

fancy_dumplings()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->

fancy_dumplings(ingredients='tofu')
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (tofu)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->

现在装饰器简化了我们调用所有函数的方式,使其更具可读性。我们只需要调用一次fancy_dumplings。这比水平嵌套多个函数在视觉上要干净得多。

我可以用装饰器做什么呢?

太棒了!现在我们清楚了如何创建 Python 装饰器。我可以用装饰器做什么呢?

Python 装饰器有许多潜在的实际应用场景。它使你的代码更易读且更具动态性。请注意,你不一定需要使用装饰器。我们总是可以在另一个函数周围实现一个包装器来达到相同的目的。 装饰器只是语法糖

你可以构建一个时间装饰器来评估给定函数的性能。以下是来自 Python 装饰器入门 的一个计时器示例。

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      
        run_time = end_time - start_time    
        print(f"Finished {func.__name__!r} in {run_time:.10f} secs")
        return value
    return wrapper_timer

它测量执行给定函数的时间。让我们将其与我们的饺子示例结合起来。

@timer
@add_dumplings_wrapper
@add_filling_vegetable
def fancy_dumplings(ingredients="meat"):
    print(f"({ingredients})")

fancy_dumplings()
# <---dumplings_wrapper--->
# <^^^vegetable^^^>
# (meat)
# <^^^vegetable^^^>
# <---dumplings_wrapper--->
# Finished 'wrapper' in 0.0000334990 secs

你可以继续堆叠装饰器来实现你的目标,只需简单地调用原始函数,无需担心在调用时包装其他函数,因为我们在定义原始函数时已经提供了装饰器的顺序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由Markus Spiske提供,发布在Unsplash上。

最后的想法

你可以利用 Python 装饰器的许多可能性。本质上,它是一个包装器,用于改变原始函数的行为。装饰器的实用性取决于你的观点,但它不应是让你感到陌生的语法。希望通过一些可视化,装饰器的概念变得更容易理解。如果你对这个故事有任何评论,请告诉我。

希望这个故事对你有帮助。本文是我的工程与数据科学故事系列的一部分,目前包括以下内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Chengzhi Zhao

数据工程与数据科学故事

查看列表53 个故事外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你也可以订阅我的新文章或成为推荐的 Medium 会员,享受对 Medium 上所有故事的无限访问权限。

如有疑问/评论,请随时在本文评论中写下,或通过LinkedinTwitter直接联系我。

初学者友好的数据科学读物(高级从业者也会喜欢)

原文:towardsdatascience.com/beginner-friendly-data-science-reads-that-advanced-data-scientists-will-enjoy-too-96ef4ea61216?source=collection_archive---------11-----------------------#2023-04-06

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 TDS Editors

·

关注 发表在 Towards Data Science · 发送至 新闻通讯 · 3 分钟阅读 · 2023 年 4 月 6 日

TDS 的读者来自不同的职业和教育背景,他们在数据科学旅程的不同阶段加入我们的社区。我们特别自豪能够支持职业生涯早期的数据专业人士,但我们也知道,如今对“初学者”的定义并没有统一的标准。

鉴于此,本周我们呈现了一些最近加入的最佳内容,这些内容都在我们的 入门指南专栏 中——这是我们收集优秀文章的地方,这些文章也耐心解释,不需要对相关主题有广泛或专业的知识。

我们的推荐反映了向早期职业化的转变:涵盖从机器学习项目设计到数据工程教程。由于我们每个人都无法精通所有领域,这些主题的多样性意味着更高级的数据科学家几乎肯定会找到新的、有趣的探索内容。

  • 熟悉一种流行的机器学习框架。如果你一直在尝试梯度提升算法,那么你很可能遇到过 LightGBM。如果你需要一些关于如何充分利用它的指导,Leonie Monigatti 对最重要的 LightGBM 参数的介绍清晰、可操作且图示详细。

  • 考虑机器学习项目设计从未为时已晚。在过去几年中流行的许多行业术语中,MLOps 似乎拥有最长的保质期。Chayma Zatout 的深度解析是一个很好的起点,如果你不确定这个概念如何与日常工作流程相关联,以及如何将其原则应用到当前项目中。

  • 轻松入门构建稳固的数据管道。Apache Airflow 可能是数据工程团队的常用工具,但正如 Aashish Nair 所指出的,它的普及并没有让它的术语、功能和特点变得更容易理解。为了帮助大家,Aashish 提供了一个基于 Python 的演示,指导读者如何创建一个简单的 Airflow 管道。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Mel Poole 提供,来自 Unsplash

  • 从基础开始学习神经网络。没有对神经网络的扎实理解,就几乎无法理解我们在人工智能研究中看到的重大进展。Roi Yehoshua 博士对感知器的概述——“神经网络最早的计算模型之一”——是进入这一主题的温和入口,介绍了基本概念,然后再转到 Python 实现。

  • 通过更好的笔记来优化你的学习过程。无论你在未来几周希望专注于哪个数据科学主题,良好的笔记习惯都能带来真正的改变。Madison Hunter的新文章介绍了一个六步路线图,以提高学习效果和记忆力。

  • 使用新兴的 Python 库快速入门。如果你学会了如何使用 Pandas 操作 DataFrames——这是许多数据科学家的常见情景!——你可能会高兴地知道,最近几个月,一个新库 Polars 由于其高速性能而获得了很多关注。David Hundley的最新文章针对那些希望探索 Polars 优势的 Pandas 用户。

感谢你本周的时间和支持!这使我们能够每天发布优秀的故事,包括我们推荐的在 Medium 上获得特别推广的那些故事,这个程序让我们感到非常兴奋。

如果你喜欢在 TDS 上阅读的文章(并且希望获得我们档案的无限访问),可以考虑成为 Medium 会员。学生们:现在是加入的特别好时机,因为你们中的许多人可以享受会员的大幅折扣

直到下一个 Variable,

TDS 编辑部

初学者教程:在 Microsoft Azure 中将 GPT 模型与公司数据连接

原文:towardsdatascience.com/beginner-tutorial-connect-gpt-models-with-company-data-in-microsoft-azure-81177929da18

使用 OpenAI Studio、认知搜索和存储帐户

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Eirik Berge, PhD

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Volodymyr Hryshchenko 拍摄,来源于 Unsplash

概述

  1. 介绍

  2. 设置数据

  3. 创建索引和部署模型

  4. 其他考虑因素

  5. 总结

介绍

在过去的一年里,关于 GPT 模型和生成式 AI 的炒作很多。虽然关于全面技术革命的承诺可能显得有些夸张,但确实GPT 模型在许多方面令人印象深刻。然而,GPT 模型的真正价值在于将它们与内部文档连接起来。这是为什么呢?🤔

当你使用 OpenAI 提供的普通 GPT 模型时,它们并不真正理解你公司的内部运作。如果你问它问题,它将基于它最可能了解的其他公司的信息来回答。当你想使用 GPT 模型来问如下问题时,这就是一个问题:

  • 我必须遵循的内部程序步骤是什么?

  • 我公司与特定客户之间的完整互动历史是什么?

  • 如果我在特定软件或例程上遇到问题,我应该联系谁?

尝试用普通 GPT 模型问这些问题不会得到有价值的答案(试试看!)。但如果将 GPT 模型连接到你的内部数据,你就可以获得这些问题以及其他许多问题的有意义的答案。

在本教程中,我想向你展示如何将 GPT 模型与 Microsoft Azure 中的内部公司数据连接起来。 仅在过去几个月,这变得更简单了。我会慢慢演示资源的设置和必要的配置。这是一个初学者教程,所以如果你对 Microsoft Azure 非常熟悉,你可以快速浏览教程。

你需要在继续之前准备好两件事:

  • 一个你有足够权限上传文档、创建资源等的 Microsoft Azure 租户。

  • 在发布时,你的公司需要申请访问我们将要使用的 Azure OpenAI 资源。这可能会在未来某个时候取消,但目前这是必须的。申请后到获得访问权限的时间大约需要几天。

注意: 创建出色的 AI 助手的真正难点在于数据质量、正确定义项目范围、理解用户需求、用户测试、自动化数据摄取等。因此,不要以为创建一个优秀的 AI 助手很简单。只是设置基础设施很简单。

设置数据

一切从数据开始。第一步是将一些内部公司数据上传到 Azure。在我的示例中,我将使用以下文本,你也可以复制并使用:

In company SeriousDataBusiness we always make sure to clean our 
desks before we leave the office.

将文本保存到名为company_info.txt的文本文件中,并存放在一个方便的位置。现在我们将前往 Microsoft Azure 并上传文本文档。在 Azure 市场上搜索存储账户资源:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在创建 Azure 资源时,有许多字段可以填写。对于存储账户,重要的字段有:

  • 订阅: 你希望创建存储账户的订阅

  • 资源组: 你希望在其中创建存储账户的资源组。你也可以决定为本教程创建一个新的资源组。

  • 存储账户名称: 在所有 Azure 账户中唯一的名称,长度在 3 到 24 个字符之间。只能包含小写字母和数字。

  • 区域: 将承载数据的 Azure 区域。

  • 性能: 选择标准足以用于测试。

  • 冗余: 选择本地冗余存储足以用于测试。

一旦你点击了审核然后创建,应该会在你选择的资源组中等待一个存储账户,几分钟内就能看到。进入存储账户后,前往左侧边栏的容器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里,你可以创建一个新的容器,实际上它作为数据的命名空间。我把我的容器命名为 newcontainer 并进入了它。现在你可以在左上角看到一个上传按钮。点击上传,然后找到你之前保存的心爱的 company_info.txt 文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们的数据在 Azure 中了。我们可以继续下一步 🔥

创建索引并部署模型

当我读一本食谱时,我经常查阅书后面的索引。索引可以快速告诉你哪个食谱在第几页。在忙碌的世界里,每次想做煎饼时翻阅整本书是不够的。

为什么我要告诉你这些?因为我们还要为我们在上一部分上传的内部数据创建索引!这将确保我们可以快速定位内部文档中的相关信息。这样,你就不需要在每个问题中都将所有数据发送给 GPT 模型。这不仅成本高,而且由于 GPT 模型的令牌限制,即使是中型数据源也无法实现。

我们需要一个Azure Cognitive Search资源。这是一个帮助我们自动索引文档的资源。和之前一样,前往 Azure 市场并找到 Cognitive Search 资源:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在创建 Azure Cognitive Search 资源时,应该选择与存储账户相同的订阅、资源组位置。给它一个独特的服务名称,并选择定价层 Standard。点击Review按钮,然后点击Create按钮继续。

完成后,我们实际上要创建另一个资源,即 Azure OpenAI 资源。原因是我们不会在 Cognitive Search 资源中创建索引,而是通过 Azure OpenAI 资源间接创建。这对于不需要大量调整索引的简单应用程序来说更为方便。

再次前往 Azure 市场并找到 Azure OpenAI 资源:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你需要选择与其他资源相同的订阅、资源组和区域。给它一个名称并选择定价层 Standard SO。点击进入 Review and Submit 部分,然后点击 Create。这是你完成教程所需的最后一个资源。等资源完成时,去喝杯咖啡或其他饮料吧。

进入 Azure OpenAI 资源后,你应该在 Overview 部分看到类似的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击 Explore,这会带你进入 Azure OpenAI Studio。在工作室中,你可以通过图形用户界面部署模型并连接内部数据。你现在应该看到类似的内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先让我们创建一个 GPT 模型的部署。前往左侧边栏的Models部分。这将显示你可以使用的可用模型。你看到的模型可能与你的不同,取决于你选择的区域。我将选择gpt-35-turbo模型并点击Deploy。如果你没有访问这个模型的权限,请选择另一个。

选择一个Deployment name并创建部署。如果你去左侧边栏的Deployments部分,你可以看到你的部署。接下来我们将前往左侧边栏的Chat部分,在这里我们将开始通过索引连接数据。

你应该会看到一个名为Add you data (preview)的选项卡,可以选择它:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当你阅读本教程时,这个功能可能已经不在预览模式中。选择Add a data source并选择Azure Blob Storage作为你的数据源。你需要输入的信息包括订阅、Azure Blob 存储资源、你放置文档company_info.txt的存储容器,以及我们创建的 Azure Cognitive Search 资源:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入一个索引名称,并将Indexer schedule选项保持为Once。这是根据潜在的新数据更新索引的频率。由于我们的数据不会改变,我们选择Once以简化操作。接受连接到 Azure Cognitive Search 帐户将产生使用费用的提示,然后继续。你可以在Data management下选择Keyword作为Search type

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击Save and close并等待索引完成。现在已部署的 GPT 模型可以访问你的内部数据了!你可以在Chat session中提问进行尝试:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

聊天机器人基于内部文档提供了正确的答案 😍

它提供了正确文档的参考,以便你可以检查源材料以确认。

还有一个叫做View code的按钮,你可以在这里查看用各种编程语言发出的请求。只要你包含列出的端点和访问密钥,就可以从任何地方发送这个请求。因此,你不局限于这里的操作环境,可以将其整合到你自己的应用程序中。

你现在已经成功将 GPT 模型与内部数据连接起来了!当然,在我们的教程中,内部数据并不太有趣。但你可以想象用比办公政策更重要的材料来做这件事。

其他注意事项

在这里,我想引导你去尝试一些进一步的内容。

系统消息

你还可以在 Chat 操作区指定系统消息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这在其他设置中有时被称为pre-prompt。这是在用户实际提问之前每次发送的消息。目的是为 GPT 模型提供关于当前任务的上下文。它默认为类似你是一个帮助人们查找信息的 AI 助手的内容。

你可以更改系统消息以请求特定格式的响应,或更改回答的语气。随意尝试。

参数

你可以找到一个Configuration面板(它要么已经可见,要么你需要去Show panels并选择它)。它看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里你可以调整许多参数。也许最重要的是Temperature,它表示答案的确定性。低值表示高度确定,因此每次给出的答案大致相同。高值则相反,每次答案更为多样。高值通常使模型看起来更具创造性。

部署到 Web 应用程序

当你完成系统消息和参数的调整后,你可能想要将模型部署到一个 Web 应用程序。这可以通过 Azure OpenAI Studio 很容易地完成。只需点击Deploy to按钮,然后选择A new web app...

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

填写相关信息后,你可以从 Web 应用程序访问模型。这是使模型对其他人可用的一种方式。

总结

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由斯宾塞·伯根提供,来源于Unsplash

在本教程中,我展示了如何在 Azure 中将 GPT 模型与公司内部数据连接。再次强调,这只是获得出色 AI 助手的第一步。接下来的步骤需要在数据质量、索引优化、服务设计和自动化等领域的专业知识。但你现在有了一个最基本的设置,可以进一步开发 👋

如果你对人工智能或数据科学感兴趣,可以随时关注我或在LinkedIn上联系我。你对将 GPT 模型连接到公司数据的经验如何?我很想听听你的看法 😃

喜欢我的写作吗? 查看我的其他帖子以获取更多内容:

  • 你作为数据科学家成功所需的软技能

  • 如何作为数据科学家编写高质量的 Python 代码

  • 用美丽的类型提示现代化你的罪恶 Python 代码

  • 在 Python 中可视化缺失值令人惊讶地简单

  • 在 Python 中使用 PyOD 进行异常/离群值检测 🔥

PySpark 线性回归初学者指南

原文:towardsdatascience.com/beginners-guide-to-linear-regression-with-pyspark-bfc39b45a9e9

使用 PySpark 代码构建线性回归模型的逐步教程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Yasmine Hejazi

·发布于Towards Data Science ·阅读时间 5 分钟·2023 年 1 月 11 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由eskay lim提供,来源于Unsplash

介绍

PySpark 是 Apache Spark 的 Python API。它允许我们在高层次的编程语言中编写代码,同时享受分布式计算的好处。通过内存计算、使用并行化的分布式处理和原生机器学习库,我们可以解锁对数据处理的极大效率,这对数据扩展至关重要。

本教程将逐步讲解如何使用Diamonds 数据创建 PySpark 线性回归模型,该数据来自ggplot2。Databricks 在Databricks Utilities ([dbutils](https://docs.databricks.com/dev-tools/databricks-utils.html))上托管此数据集,以便轻松加载。该数据包含数值和分类特征,我们可以利用这些特征来构建我们的模型。我们将处理如何预处理数据,以便我们可以简单地训练模型并生成预测。我们将实现这一点而无需接触 Pandas!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由Tahlia Doyle提供,来源于Unsplash

如果你已经熟悉在 Pandas 中进行线性回归,过程是类似的。回归模型将预测钻石的价格

数据预处理

首先,让我们加载数据。我们将使用diamonds数据集来根据特征预测钻石的价格。每个变量的描述可以在这里找到。

df = spark.read.csv("/databricks-datasets/Rdatasets/data-001/csv/ggplot2/diamonds.csv", header="true", inferSchema="true")
display(df)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者拍摄的照片

为了准备我们的数据进行机器学习,我们首先需要创建一个数值特征向量,作为模型的输入。这意味着,如果我们有类别变量,我们需要将它们一热编码成数值特征;然后,将所有特征放入一个向量中。

MLlib 库是一个对 PySpark 的封装,支持许多用于分类、回归、聚类、降维等的机器学习算法。这个库将大大帮助我们的数据处理和机器学习需求!

预处理类别特征

  1. StringIndexer:这基本上是为每个类别分配一个数值(例如:Fair: 0, Ideal: 1, Good: 2, Very Good: 3, Premium: 4)

  2. OneHotEncoder:这个工具将类别转换为二进制向量。结果是一个 SparseVector,指示 StringIndexer 中哪个索引具有一热值 1。

from pyspark.ml import Pipeline
from pyspark.ml.feature import StringIndexer, OneHotEncoder

cat_cols= ["cut", "color", "clarity"]
stages = [] # Stages in Pipeline

for c in cat_cols:
    stringIndexer = StringIndexer(inputCol=c, outputCol=c + "_index")
    encoder = OneHotEncoder(inputCols=[stringIndexer.getOutputCol()], \
            outputCols=[c + "_vec"])    
    stages += [stringIndexer, encoder] # Stages will be run later on

汇总特征向量

在 PySpark 中,我们需要将所有特征合并成一个向量列。特征向量列将作为我们机器学习模型的输入。PySpark 允许我们使用VectorAssembler轻松创建这个向量。

你需要将之前的数值特征和类别稀疏向量特征合并成一个向量。

然后,我们可以将这些阶段作为管道运行。这会将数据通过到目前为止定义的所有特征转换。

from pyspark.ml.feature import VectorAssembler

# Transform all features into a vector
num_cols = ["carat", "depth", "table", "x", "y", "z"]
assemblerInputs = [c + "_vec" for c in cat_cols] + num_cols
assembler = VectorAssembler(inputCols=assemblerInputs, outputCol="features")
stages += [assembler]

# Create pipeline and use on dataset
pipeline = Pipeline(stages=stages)
df = pipeline.fit(df).transform(df)

训练-测试拆分

随机将数据集拆分为训练集和测试集。

train, test = df.randomSplit([0.90, 0.1], seed=123)
print('Train dataset count:', train.count())
print('Test dataset count:', test.count())

拆分后的数据处理

在线性回归中,通常建议对特征进行标准化。PySpark 的StandardScaler通过去除均值(设为零)并缩放到单位方差来实现这一点。

首先,将 StandardScaler 拟合到训练数据上。然后,使用缩放器对训练数据和测试数据进行转换。你应该在训练数据上进行拟合的原因是避免数据泄漏——当将模型应用于实际世界时,你还不知道测试数据的分布。

from pyspark.ml.feature import StandardScaler

# Fit scaler to train dataset
scaler = StandardScaler().setInputCol('features') \
        .setOutputCol('scaled_features')
scaler_model = scaler.fit(train)

# Scale train and test features
train = scaler_model.transform(train)
test = scaler_model.transform(test)

构建和训练模型

我们可以从 MLlib 导入以开始构建我们的模型 (pyspark.ml.regression.LinearRegression)。我们可以从默认参数值开始,并在模型调优过程中调整这些值。需要注意的一些默认参数是:maxIter=100regParam=0.0elasticNetParam=0.0loss='squaredError’。然后调用 fit() 将模型拟合到训练数据上。

from pyspark.ml.regression import LinearRegression

lr = LinearRegression(featuresCol='scaled_features', labelCol='price')
lr_model = lr.fit(train)

评估模型

首先,我们需要使用新模型从数据中生成预测。要获取预测,请调用 transform()

train_predictions = lr_model.transform(train)
test_predictions = lr_model.transform(test)

为什么你还需要在训练数据上拟合你的模型? 对比模型在训练数据和测试数据上的评估可以帮助你识别是否存在过拟合或欠拟合。如果训练和测试的评估结果相似,你可能存在欠拟合。如果测试评估结果远低于训练评估,你可能存在过拟合。

要评估模型,PySpark 提供了 RegressionEvaluator。你可以选择最适合你用例的评估指标:

  • RMSE — 均方根误差(默认)

  • MSE — 均方误差

  • R2 — 决定系数

  • MAE — 平均绝对误差

  • Var — 解释方差

from pyspark.ml.evaluation import RegressionEvaluator

evaluator = RegressionEvaluator(predictionCol="prediction", \
                 labelCol="price", metricName="r2")

print("Train R2:", evaluator.evaluate(train_predictions))
print("Test R2:", evaluator.evaluate(test_predictions))

训练 R2: 0.9204 | 测试 R2: 0.9142

分析特征权重

要提取线性回归的权重和系数,请运行以下代码。

print("Coefficients: " + str(lr_model.coefficients))
print("Intercept: " + str(lr_model.intercept))

将权重与特征名称匹配并绘制它们可能更有帮助。这种视图对希望了解模型关键驱动因素的主要利益相关者特别重要。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

list_extract = []
for i in df.schema['features'].metadata["ml_attr"]["attrs"]:
    list_extract = list_extract + df.schema['features'] \
                    .metadata["ml_attr"]["attrs"][i]
varlist = pd.DataFrame(list_extract)
varlist['weight'] = varlist['idx'].apply(lambda x: coef[x])
weights = varlist.sort_values('weight', ascending = False)

上述操作生成了包含特征名称和权重的 Pandas 数据框。现在让我们绘制这些权重。

def show_values(axs, space=.01):
    def _single(ax):
        for p in ax.patches:
            _x = p.get_x() + p.get_width() + float(space)
            _y = p.get_y() + p.get_height() - (p.get_height()*0.5)
            value = '{:.2f}'.format(p.get_width())
            ax.text(_x, _y, value, ha="left")

    if isinstance(axs, np.ndarray):
        for idx, ax in np.ndenumerate(axs):
            _single(ax)
    else:
        _single(axs)

def plot_feature_weights(df):
    plt.figure(figsize=(10, 8))
    p = sns.barplot(x=df['weight'], y=df['name'])
    show_values(p, space=0)

    plt.title('Feature Weights')
    plt.xlabel('Weight')
    plt.ylabel('Feature Name')

plot_feature_weights(weights)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

作者照片

总结

使用 PySpark 实现线性回归的步骤:

  1. 使用 StringIndexer 和 OneHotEncoder 对分类特征进行独热编码

  2. 使用 VectorAssembler 创建输入特征向量列

  3. 将数据分割为训练集和测试集

  4. 使用 StandardScaler 进行数据缩放

  5. 初始化并在训练数据上拟合 LinearRegression 模型

  6. 在测试数据上转换模型以进行预测

  7. 使用 RegressionEvaluator 评估模型

  8. 分析特征权重以理解和改进模型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值