TowardsDataScience-2023-博客中文翻译-二十二-
TowardsDataScience 2023 博客中文翻译(二十二)
探索解决股票切割问题的贪婪算法
原文:
towardsdatascience.com/how-bad-is-being-greedy-cdec75ce52a6
为股票切割问题创建快速估算
·发表于 Towards Data Science ·7 分钟阅读·2023 年 8 月 3 日
--
目录
-
股票切割问题的动机
-
NP-Hard 问题的快速概述
-
将股票切割问题编码到 Python 中
-
贪婪算法
-
与低维空间中的穷举搜索的比较
-
与随机搜索在更高维空间中的比较
-
结论
股票切割问题的动机
我是一名数据科学家。虽然我的数据科学技能在工作中非常重要(这很自然),但我发现数据科学概念也帮助解决了许多工作之外的问题!
我的数据科学技能在我的 DIY 爱好中派上了用场。一个常见的挑战是知道如何规划材料的切割。我有一个需要从多个同尺寸材料中切割的清单。如何规划这些切割以尽可能少浪费呢?这个挑战被称为“股票切割问题”。事实证明,这个问题很难解决,实际上是 NP-Hard 的!
在本文中,我将探索一种“捷径”(双关语)来解决这个问题,并分析这种方法与长途解决方案的比较。

我去年制作的白橡木桌面,钢制底座 — 作者图片
NP-Hard 问题的快速概述
我不会在这里深入探讨 NP-Hard 问题,但我确实想给出一些直观的理解。使优化问题变得困难的通常是其解决方案空间的大小。也就是说,你需要探索多少可能的解决方案才能找到最佳方案?一个问题的难度通常通过问题规模增长时解决方案空间增长的速度来评估。
对于“股票切割问题”,当你增加更多切割时,问题变得更大。看看解决方案空间增长的速度吧!

作者图片
解空间呈阶乘增长,这意味着必须搜索的解决方案总数是 n!,正如你所看到的,这个数字增长非常快。
NP 代表‘非确定性多项式’,这意味着问题的增长速度快于任何多项式函数。有很多资源深入探讨 NP/NP-hard 问题。这里我只会讨论到这些。
将库存切割问题编码为 Python
库存切割问题本质上是一个排序问题。你按照特定顺序进行切割,当某块库存的长度用完时,你开始在下一块库存上进行切割(仍然按照顺序)。
可视化最能解释这一点。假设我们有这个切割顺序:[4', 2', 1', 2', 2', 1'],并且我们有 5' 的库存。浪费计算如下:

图片作者
这里我们总共浪费了 4'。
但是,如果我们改变顺序(保持所有相同的切割),可能会得到不同的浪费水平。让我们尝试 [4', 1', 2', 2', 1', 2']:

图片作者
在这里,我们只有 3' 的浪费——简单地改变顺序就减少了我们的浪费!这就是这个优化问题的核心思想。我们想找出哪种切割顺序最好。
现在,为了将其编码为 Python 脚本,我们需要 (1) 用于计算每个订单浪费的函数,和 (2) 将列表排序为最优顺序的算法。
计算浪费的函数只是复制了上述逻辑。以下是 Python 代码:
def total_waste(stock_size, solution):
'''
Calculates cutoff waste give a specific solution
inputs
stock_size (float) : the dimension of the stock
available for purchase
solution (list) : list of floats depicting the
order to make the cuts
output
cut_off_waste (float) : the total cutoff waste of
the given solution
'''
# set up variable to keep track of the total lengths of
# cuts on current piece of stock
temp_cut_length = 0
# start with no waste
cut_off_waste = 0
for cut in solution:
# if next cut doesn't fit on current stock,
# calculate new waste and reset for another piece of stock
if temp_cut_length + cut > stock_size:
# calculate cutoff waste
cut_off_waste += stock_size - temp_cut_length
# reset cut length
temp_cut_length = cut
else:
# add to cumulative length of cuts on stock
temp_cut_length += cut
# add in last cut waste -- it is not captured in the loop
cut_off_waste += stock_size - temp_cut_length
return cut_off_waste
我们将在下一节中介绍所需的算法。
贪婪算法
贪婪算法非常简单,只需找到适合当前库存剩余部分的最大块。
使用之前的示例,我们将假设我们想进行这些切割 [4', 2', 1', 2', 2', 1'],并且我们需要一个贪婪算法来优化顺序。
我们首先从适合当前库存的最长切割开始。由于我们还没有进行任何切割,我们当前的库存长度是 5'。4' 是我们拥有的最长切割,它适合在 5' 的剩余库存中。下一个最长的切割是 2',由于我们只剩下 1' 的库存,这个切割太长了。我们转到下一个最长的切割,即 1'。它适合在剩余库存中,因此我们下一个切割是 1'。该算法按照这种模式进行,直到没有剩余的切割。
以下是该算法在 Python 中的实现:
def greedy_search(cuts, stock_size):
'''
Calculates a greedy optimal solution
inputs:
cuts (list) : cuts that need to be made
stock_size (float) : size of stock available for purchase
outputs:
cut_plan (list) : sequence of cuts to obtain greedy
optimal results
waste (float) : amount of material wasted by solution
'''
# empty cut off plan, to be populated
cut_plan = []
# start with cutoff size equal to stock size
cut_off = stock_size
# copy cuts list to avoid modifying original list
cuts_copy = copy(cuts)
# sort cuts in desc order
cuts = list(np.sort(cuts)[::-1])
# continue ordering cuts until
# all cuts have been ordered
while len(cuts_copy) > 0:
for cut_size in cuts_copy:
# if cut size is smaller than remaining stock,
# assign the cut now
if cut_size < cut_off:
# add cut to plan
cut_plan.append(cut_size)
# update the leftover amount
cut_off -= cut_size
# remove cut from list of cuts still needing
# to be ordered
cuts_copy.remove(cut_size)
# reset cut_off to be the full stock size
cut_off = stock_size
# calculate waste using total_waste function
waste = total_waste(stock_size, cut_plan)
return cut_plan, waste
与低维空间穷举搜索的比较
通过使用贪婪算法的*似解,我们节省了大量时间,但这种*似解有多好?对解空间进行穷举搜索可以得到全局最优解——这是我们的金标准。让我们将贪婪算法的解与仅有少量切割的全局最优解进行比较(记住,找到大量切割的全局最优解确实很困难)。
我随机创建了 250 个切割列表,切割尺寸范围从 2 到 10。每个切割的范围是 0 到 1,库存尺寸设置为 1.1。以下是性能表现:

图片由作者提供
正如你所见,随着'n'的增加,贪婪算法的性能相对于全局最优解变得更差,但仍然相对接*且高度相关。
与高维空间中的随机搜索比较
不幸的是,我们有一个重大的盲点……也就是说,我们不知道在高维空间中的全局最优解。现在,我们进入比较不同类型启发式算法(旨在*似全局最优解的算法)的领域。贪婪算法与随机搜索解空间的效果如何?

图片由作者提供
我们可以看到,随着切割数量的增加,随机搜索的效果变得越来越差。这是有道理的,因为随机搜索是在随机选择 500 个解并挑选最佳的一个——随着解空间的爆炸性增长,我们随机搜索到的解空间的概率百分比越来越小。我们现在可以看到,贪婪解法明显比随机查看潜在解决方案要好。
结论
看起来,对于股票切割问题,贪婪解法是一种合理且非常快速的找到相当好解的方法。对我来说,这就足够了。我每年只做几个项目,通常规模较小。然而,对于制造厂等应用,更复杂且密集的方法可能对公司有显著的经济影响。你可以研究一下“混合整数线性规划”(MILP),这是一种寻找更优解的方法。
如果我要深入研究这个问题,我会将贪婪方法与更好的元启发式算法(随机搜索可能是最差的)进行比较,例如各种版本的爬山算法、禁忌搜索或模拟退火。不过现在我就先放到这里,我还有另一个表格要做!
有关本文中使用的所有代码,请参见这个 repo。
人工智能如何使旧视频看起来更流畅?
原文:
towardsdatascience.com/how-can-ai-make-old-videos-look-smoother-5ad8b70fdd64
视频帧插值技术已经取得了长足的进展,而人工智能正在将其引向一些奇怪的方向。
·发表于 Towards Data Science ·阅读时长 7 分钟·2023 年 1 月 10 日
--
那是在 2015 年,我正在为与我的博士生导师委员会的会议创建一些数据可视化。这是在我研究生阶段接*尾声的时候,我运行了一些超级计算机模拟,模拟了一个在星际气体云中形成的大质量恒星。
我认为展示这些模拟结果的最佳方式是创建各种数据动画。我的分析代码处理了数吉字节的模拟数据,并在特定时间间隔生成了图像文件。因此,我使用 ffmpeg 将它们拼接成动画:

这是一个展示原恒星盘演化过程的天体物理学模拟视频。作者自制。请参阅 Klassen et al. (2016)。
我一直希望能拥有更高的帧率,以便获得更流畅、更美观的动画,但我受限于模拟中获得的数据。
有时我会花几个小时尝试生成中间帧,以使我的影片在编译时看起来更流畅。那时我的尝试笨拙且最终被放弃,但今天已经有许多技术效果非常好。事实上,大多数现代电视都包括(默认开启的)选项,以人工增强视频的帧率。
这项技术的正式名称是视频帧插值(VFI),结果发现这一领域已有相当多的研究。
什么是视频帧插值?
视频画面通常是由单独的静态帧组装而成的。如果帧率较低,视频可能会显得“跳跃”。为了让视频看起来更流畅,已经开发了各种技术来人工插入原始帧之间的中间帧。
在下面的示例中,一个短动漫剪辑的帧率通过 AI 技术提升了 8 倍。结果是视频更加平滑。
让我们深入探讨这些技术。我们将从一些不需要人工智能的方法开始,最后介绍一些最先进的方法。
作为我们的源材料,我们将使用由电影制片人Pat Whelen拍摄的 6 秒钟火车机车剪辑,上传到免费的股票照片和视频网站Pexels。

原始 23 fps 的火车机车剪辑。感谢:Pat Whelen, Pexels。免费使用。
原始视频以 23 帧每秒的高分辨率录制。一般来说,我们的目标是在插值过程中将帧率翻倍。
帧插值技术
帧平均
添加更多帧的最简单方法之一是将两个连续的帧混合,创建这两帧的平均值。这很容易做到。我使用了ffmpeg来执行插值,使用了minterpolate滤镜:
ffmpeg \
-i input/train.mp4 \
-crf 10 \
-vf "minterpolate=fps=46:mi_mode=blend" \
output/train_blended.mp4
有关滤镜设置的更多细节可以在这里找到。结果如下,转换为动画 gif。

源视频帧率通过视频帧混合技术得到了提升。
如果仔细观察,你可以从结果视频中看到,任何运动的地方都会出现伪影。如果物体在帧之间移动过多,那么混合的中间帧会非常明显地显示物体出现在两个位置,并且在运动的地方呈半透明状。
为了克服这个问题,研究人员开发了运动估计技术,以提高插值的质量。
运动估计
为了创建看起来不仅仅是两个其他帧模糊平均值的中间帧,开发了运动补偿帧插值。这种方法通过识别连续帧中的物体,从类似的视觉特征计算这些物体的运动路径。这样就创建了一个向插值算法可以利用的向量“运动场”。

来自Choi & Ko (2010)的图示,显示了覆盖在视频帧上的运动场。
多年来,多个运动估计算法已被实现到ffmpeg中,我们将使用一种称为“重叠块运动补偿”(OBMC)的算法。结果如下:

使用名为重叠块运动补偿的运动插值技术增强了源视频的帧率。
尽管生成的视频质量要好得多,但视频中仍可能出现微小的伪影。为了实现上述视频,我再次依赖于ffmpeg中的minterpolate滤镜,但使用了一些不同的设置:
ffmpeg \
-i input/train.mp4 \
-crf 10 \
-vf "minterpolate=fps=46:mi_mode=mci:mc_mode=obmc:me_mode=bidir" \
output/train_obmc.mp4
使用人工智能的帧插值
计算机视觉领域正在通过新人工智能技术的应用迅速发展。深度神经网络可以执行高度准确的图像分割、物体检测甚至 3D 深度估计。
之前的技术依赖于估计帧之间物体的运动。这些物体是通过视觉特征检测的。卷积神经网络(CNNs)在从图像中提取视觉特征方面表现出色,因此在这里应用它们似乎是合理的。
接下来,如果我们能更多地了解图像所表示的世界的 3D 特性,那么我们可以更好地估计图像中不同部分的运动,从而生成更准确的中间帧。
这里的真正改变者是增加了深度估计。一种训练有素的神经网络可以预测图像中各部分的距离。
下方的动画展示了一篇名为“深入探讨自监督单目深度估计”(ICCV 2019, 代码)的论文中的结果。深度神经网络准确地逐帧估计图像中哪些部分代表了较*的物体与较远的物体。
这种技术的良好效果并不令人感到意外。这是我们的大脑本能地做的事情,即使是在闭上一只眼睛去除视差信息时也是如此。使用双眼可以进一步提高准确性。

深度感知视频帧插值技术,如 2019 年由 Bao 等人提出的DAIN 算法,比以前的方法表现更好,因为它们可以考虑遮挡情况,即当物体互相经过时。
最*,黄等人(ECCV 2022)提出的“实时中间流估计”(RIFE)技术取得了卓越的基准成绩,同时运行速度也快得多。这可能代表了目前的最先进技术。

来自 RIFE 论文的一个示例视频片段,展示了 2 张输入图像的 16 倍插值:github.com/megvii-research/ECCV2022-RIFE

RIFE 对相同输入进行的深度估计。
让我们通过将 RIFE 算法应用于纽约市场景来总结我们的练*。我将GitHub 仓库克隆到我的笔记本电脑上,下载了 repo 的 ReadMe 文件中描述的预训练模型参数,并对requirements.txt文件进行了几处调整。我在没有 GPU 帮助的情况下在笔记本电脑上运行了代码。处理 6 秒的视频素材大约花费了 15 分钟。以下是结果:

使用新颖的深度 CNN 技术进行视频插值,“实时中间流估计”(RIFE)。
在 RIFE 创建的高分辨率 mp4 视频中效果更佳。对于本文,我将该视频转换成了动画 gif。
最终想法
这个领域发展非常迅速。训练于互联网上数十亿图像的图像生成 AI 已经能够从一系列输入图像或基于文本的提示中生成高度一致的、新颖的图像。
其他技术将深度估计方法扩展到从多个摄像机角度创建点云,然后使用称为神经渲染的技术合成新的人工视角。举例来说,Rückert 等(2021)及其神经渲染方法:

Rückert 等(2021)中的神经渲染。左侧的图像是从一系列真实图像(右下角)生成的。
随着这一领域的持续发展,我们可以期待中间帧的准确性更高,伪影更少,即使更多的帧本质上是由非常大的预训练人工神经网络“梦到”或幻觉出来的。相对较少的输入帧即可在它们之间产生高度逼真、流畅的过渡。
这打开了很多创造性的可能性,比如恢复旧档案视频、为静态图像制作动画,以及从少量二维照片中创建虚拟现实世界。
这也可能导致世界上充斥着大量合成图像,使得区分什么是实际真相变得困难。同时,期待你的视频看起来非常流畅。

我的原始研究数据可视化(见上文),经过了 RIFE 算法处理。
如果你喜欢阅读这样的故事并希望支持我作为作者,可以考虑注册成为 Medium 会员。每月 $5,你可以访问我和其他成千上万名作家的所有作品。如果你通过我的链接注册,我将获得一小笔佣金,而你无需额外付费。
[## 通过我的推荐链接加入 Medium — Mikhail Klassen
阅读米哈伊尔·克拉森(Mikhail Klassen)以及在 Medium 上的其他数千名作家的每一个故事。你的会员费直接支持…
机器学*如何应用于音频分析?
了解机器学*如何用于分析音频信号,并为分类和回归任务生成预测
·
关注 发表在 Towards Data Science ·5 分钟阅读·2023 年 1 月 10 日
--
图片由 Jarrod Reed 提供,来源于 Unsplash
机器学*在*十年来迅速发展。实际上,它被广泛应用于医疗保健、农业和制造业等多个行业。随着技术和计算能力的进步,机器学*的潜在应用不断增加。由于数据以各种格式大量存在,现在正是利用机器学*和数据科学从数据中提取各种见解并进行预测的最佳时机。
机器学*最有趣的应用之一是在音频分析中,理解不同音频格式的质量。因此,使用各种机器学*和深度学*算法可以确保对音频数据进行预测并理解其结果。

Med Badr Chemmaoui拍摄的照片,来源于 Unsplash
在进行音频分析之前,必须单独采样并分析信号。我们采样的频率也称为采样率或奈奎斯特率。将时域信号转换为频域信号将非常有助于对信号进行逻辑理解,并计算如功率和能量等有用的组件。所有这些特征都可以提供给我们的机器学*模型,用于进行预测。
有一种流行的将音频信号转换为谱图(图像)的方法,这样可以将其输入到卷积神经网络(CNNs)中进行预测。谱图可以捕捉音频信号的重要特征,并以 2D 形式呈现,从而可以与基于图像的网络一起使用。
许多机器学*模型在给定图像的情况下能很好地预测输出标签。因此,包含幅度和不同频率单位的音频信号也可以转换为图像,并用于强大的机器学*预测。
在这篇文章中,我们将通过一个随机示例来讨论如何读取音频文件,并绘制它以了解其图形表示。随后,我们将对图像数据进行特征工程,并在将音频转换为图像后执行卷积操作。最后,我们将获得样本预测以用于未见过的数据。请注意,这段代码仅用于演示,未考虑具体数据集。
读取数据
我们将导入用于读取音频文件的必要库,这些文件通常以‘.wav’格式存在。在读取文件后,我们会得到一个数组表示,如上面的代码单元所示。最后,我们将绘制输出,以便使用 matplotlib 查看它的样子。
特征工程
现在数据已经被绘制和可视化以查看‘.wav’文件中的异常,我们将使用一个流行的库‘librosa’,它可以用来计算音频数据的短时傅里叶变换。这是为了确保信号被分解成其组成频率,这是一种在众多行业中广泛使用的技术。
模型训练
现在我们已经使用librosa获取了频率成分,我们将使用机器学*模型进行预测。需要注意的是,这是一项分类问题,因此,我们选择使用随机森林分类器。不过,也可以使用任何其他适合你的需求和业务的机器学*模型。
我们现在将使用相同的代码,但用于回归任务,其中输出是连续的而不是离散的。下面是关于如何进行训练和使用随机森林回归器监控性能的代码单元格。
超参数调优
在模型(随机森林)可以实时部署之前,确定正确的超参数非常重要。在深度神经网络中,有很多超参数需要搜索。由于我们使用的是随机森林作为基准模型,因此我们应该能够在最小的搜索空间内找到正确的超参数。让我们看看如何在通用数据集上进行超参数调优。
在代码单元格中,我们指定估计器数量和树的最大深度,以便在测试集上获得最佳结果。我们最终监控得分,并观察超参数的变化如何导致模型性能的提升。
模型部署
现在我们已经进行了超参数调优以提供最准确的预测,是时候保存提供最佳结果的机器学*模型了。因此,我们将使用 python 中的pickle库,以便能够保存可以用于服务的机器学*模型。
保存模型后,我们将在构建生产就绪代码时再次加载它,并用它来对 incoming batches 或数据流进行预测。需要注意的是,在训练数据期间使用的特征化步骤也必须在测试集上执行,以避免数据偏差。
持续监控
我们知道模型在处理接收用户数据时表现良好,但一个重要且常被忽视的步骤是监控模型的预测质量。模型的表现可能会与训练期间有所不同。这可能是因为训练数据和服务数据之间存在差异。例如,可能会出现概念漂移或数据漂移,这些情况会对投入生产的推理模型的性能产生显著影响。
持续监控确保采取措施检查预测模型,并了解其在变化数据下的行为。如果预测的准确性最低,并导致企业收入损失,则应采取措施用这些异常数据重新训练模型,以确保模型行为不会出现意外变化。
结论
在阅读完这篇文章后,你可能对如何进行音频数据的机器学*以及整体工作流程有了很好的了解。我们见识了读取数据、特征工程、模型训练、超参数调整、模型部署及持续监控等步骤。应用这些步骤并确保在开发管道时没有错误,将导致一个稳健的机器学*生产系统。
以下是你可以联系我或查看我的工作的方式。
GitHub:suhasmaddali (Suhas Maddali ) (github.com)
YouTube:https://www.youtube.com/channel/UCymdyoyJBC_i7QVfbrIs-4Q
LinkedIn:(1) Suhas Maddali, Northeastern University, Data Science | LinkedIn
如何在没有复杂算法的情况下改进预测指标和流程?
开发描述性分析可以帮助识别改进需求规划 KPI 和流程效率的重点领域
·发表于数据科学前沿 ·阅读时间 10 分钟·2023 年 8 月 26 日
--

图片由Lukas Blazek提供,来源于Unsplash
引言
需求规划是集成业务规划(IBP)流程的核心阶段。此阶段的结果构成了其他阶段(例如供应规划、生产规划)的基础。此阶段通常以基于历史销售生成的统计预测开始,并将其与销售输入结合以得出初步基线预测。随后,这一预测将经过审查并与销售和营销相关者对齐,从而形成共识预测。我们通常在 IBP 的这一阶段追踪预测准确性和偏差作为关键绩效指标(KPI)。供应链、销售和营销团队可能会投入大量时间、精力(和金钱)来准确预测,往往还会投资于昂贵的预测工具。虽然拥有一套先进的算法确实有助于提高预测准确性,但通过历史销售和预测数据得出的描述性分析也可以帮助简化需求规划过程并提升 KPI。
我们为什么需要这样的分析?
大部分情况下,分析有两个主要目的:
-
它们有助于识别提升 KPI 的机会
-
它们有助于识别流程中存在的低效环节
例如,将每个产品和客户的销售预测准确性与统计预测准确性进行比较,可以帮助了解在哪些方面投入时间收集销售输入会比系统生成的统计预测更准确。销售团队可以专注于这些特定项目,从而节省时间和精力,同时生成更好的预测。
分析还帮助向高级领导层传达明确的信息。与其尝试解读晦涩的 KPI 数据,不如利用分析突出需要改进的具体领域,以及可以从整个业务中借鉴的高绩效实体。这在获取管理层对特定、具体目标的认可时非常有效。
设计分析
一种更直观的方法是查看需求规划过程的各个组成部分(图 1),并为这些子过程开发描述性分析。

图 1. 需求规划过程
- 本文中的所有图像,除非另有说明,均由作者提供
首先,我们分析历史销售数据并清理异常值。在统计预测生成步骤中,使用调整了异常值的历史销售数据来生成基于系统的预测。在下一步骤中,销售团队收集客户反馈或提供其需求估计,这成为销售预测。需求经理使用分析仪表板分析统计和销售预测、历史销售数据以及特殊事件信息,以制定基准预测。基准预测由销售、市场营销和供应规划团队的利益相关者在定期会议上(通常是每月或每周)审查,以达成共识预测。在此会议上,所有利益相关者都审查分析,以确定重点行动项。讨论后,将供应限制(如果有的话)纳入预测,以得出最终预测。
在实施分析之前,必须决定其粒度。组织内的不同业务单元可能会在不同的产品和客户层级上规划其需求、供应和物流。产品规划可能会根据制造设置在 SKU、产品系列(或其他)层级进行。客户规划可能会根据账户设置在客户级别或市场细分级别进行。与其为每个单独的实体开发和展示描述性分析,不如开发选择分析和洞察的层级的灵活性。分析设计也受我们希望改进的 KPI 的影响。在本文中,我们将重点讨论基于销量的描述性分析,这有助于改善加权平均绝对百分比误差(MAPE),作为预测准确性的指标。
分析模板
在本节中,我们将讨论使用预测和历史销售(实际数据)的描述性分析。请注意,所有的预测和实际数据均为所选细化层级的销售量。下面的可视化中所选择的细化层级(在产品和客户层级、历史时间段、预测类型等方面)只是示例,可以根据业务设置进行更新。每个可视化顶部的蓝色框是下拉菜单,用户可以选择多个项目(显示实体除外,在该选项中我们只能选择一个实体)。显示实体是分析可视化的层级。时间段表示分析所用的最*过去的时间段(在本节所示示例中为月份)。
I. 需求规模和变异性

图 2. 需求规模和变异性
在图 2 中,我们展示了过去 12 个月 SKU 层级的实际销售数据。这些数据以箱线图的形式展示,突出显示了销售的中位数、第 25 百分位、第 75 百分位、最小值和最大值。
洞察:图 2 中的箱线图提供了历史时间段内需求规模(由中位数表示)以及需求的变异性(通过四分位距(IQR)表示)。我们可以选择按中位数或 IQR 排序,以识别最大销售量或最大变异性的项目。
行动:通常,我们将预测工作重点放在高销售量和高变异性的项目上,同时对低销售量或低变异性的项目使用统计预测。
II. 历史销售帕累托图

图 3. 历史销售帕累托图
在图 3 中,我们绘制了过去 6 个月每个客户提升的实际数据。
洞察:图 3 中的图表显示了按 6 个月提升量降序排列的客户列表。此视图还使我们能够计算这一时期内一组客户的累计销售额。
行动:通常,少数客户负责了大部分需求(80-20 法则)。集中精力预测这些项目,将有助于获得更高的时间投资回报。
III. 持续表现不佳的项目

图 4A. 预测偏差(原始值)

图 4B. 预测偏差(绝对值)
在图 4A 和图 4B 中,我们分别查看了过去 6 个月 SKU 层级的预测与实际数据的原始和绝对偏差。
洞察:图 4A 中的正偏差显示了我们持续高估的区域,而负偏差突出了持续低估的项目。第二张图(图 4B)显示了我们对某些项目预测时机的错误(间歇性地高估和低估)。
行动:理想情况下,我们会降低有持续正偏差的项目的预测,并增加那些持续被低估的项目的预测,除非业务条件发生了变化。对于那些我们无法准确把握时机的项目,我们可能需要与客户合作,了解根本原因,并更好地把握时机。
IV. 基于预测准确度的项目分段

图 5. 销售预测误差与统计预测误差之间的偏差
在图 5 中,我们展示了 SKU-客户组合在过去 6 个月的销售预测误差与统计预测误差之间的偏差。
洞察:图 5 中的正偏差显示了销售预测在 6 个月内的累计误差高于相应的统计预测。负偏差显示了销售调整使得预测优于统计预测(即累计销售预测误差低于所选时间段内的累计统计预测误差)。
行动:为了改进指标,我们将对有正偏差的项目使用统计预测,对有负偏差的实体使用销售预测。这再次假设业务条件保持稳定。
V. 基于*期销售的异常值

图 6. 从*期销售的预测偏差
在图 6 中,我们调查了下个月销售预测与过去 3 个月平均值之间的偏差。
洞察:图 6 中的正误差显示了我们相对于*期销售的过度预测,而负偏差显示了我们预计销售将低于*期历史。
行动:我们希望仔细审查异常值,并验证预测是否偏离实际,或最*销售是否存在异常,如有必要进行调整。
VI. 基于增长和季节性的异常值

图 7A. 基于增长率的预测偏差

图 7B. 实际与预测增长率
对于这些视觉图表,我们通常选择产品系列或更高层次的属性,因为较低层次的属性(例如 SKU)可能会导致增长率的噪音。出于同样的原因,我们倾向于按季度查看异常值,而不是按月。在图 7A 中,我们查看了销售预测偏差的产品系列与预期第二季度销售额的偏差。第二季度的预期销售额仅仅是最后可用的第二季度销售额与多个年份的平均第二季度销售增长率的乘积。在图 7B 中,我们深入分析了过去 3 年每季度的增长率(%)。
洞察:图 7A 中的正偏差显示了我们在预测销售时的过度预测情况,相较于考虑了年均增长和季节性的预期销售,而负偏差显示了我们预计销售低于调整后的年均增长的季节性预期。深入研究图 7B 中的一个产品系列(例如,PF21),我们发现明年第二季度的估计增长率远低于过去 3 年的平均第二季度增长率,这需要进一步审查。
行动:需要审查最大的偏差(正负),以了解为什么预测与预期增长和季节性不一致,并相应调整。
VII. 价格-销量异常值

图 8.(标准化)价格与销量
在图 8 中,我们将产品系列 PF23 的标准化价格与销量进行对比。虽然价格使用了相关饲料‘Feed3’的价格进行标准化,但销量未进行标准化,因为我们可能没有关于该产品系列或类似产品系列的整体行业需求数据。在历史期间,使用历史价格和销量生成散点图,而使用预测价格和销量生成前瞻性的标准化价格和销量预测。
洞察:散点图显示了在不同粒度下价格(标准化)和销量(标准化)之间的相关性。虽然这大致说明了销量如何随价格变化而变化(注意标准化实体本身是*似值),但该图表可以帮助识别预测异常值(例如,在与(标准化)历史价格与销量趋势比较时,对于给定的预测(标准化)价格,预测(标准化)销量过高)。
行动:我们在价格与销量图表中探索预测异常值,并审查历史业务背景以及当前市场需求和供应条件,以评估异常值是否合理。如果不合理,则调整销量或价格以使预测与历史趋势一致。
VIII. 随时间变化的一致预测恶化或改善的实体

图 9A. 预测与实际(原始)的偏差

图 9B. 选定预测中误差最小的项目

图 9C. 滞后预测与实际数据的对比
在图 9A 中,我们分析了过去 6 个月Lag2Final Forecast(实际数据分析前 2 个月最终确定的预测)在 SKU 级别的表现。进一步地,我们还调查了图 9B 中显示的在 6 个月内具有最小Lag2Final Forecast绝对误差的产品系列项。为了理解这些项目的趋势,我们在图 9C 中绘制了不同滞后预测与实际数据的对比图。
洞察:从图 9A 中,我们可以根据误差是正还是负来识别那些持续被高估或低估的实体。图 9B 显示了在一段时间内具有最高准确性的滞后预测项。我们深入分析(如图 9C 所示),调查图 9B 表中的每个项目在过去一段时间内相对于其他滞后预测的表现。
行动:我们希望减少所有滞后预测的误差,因此会重点关注所有这些预测中的表现不佳者。另一个可行的事项是从任何粒度上表现最佳的滞后预测中学*,并将其用于未来参考。例如,如果我们发现Lag3Final到Lag2Final再到Lag1Final的预测效果越来越差,我们可能首先想要了解根本原因——如果被认为是与业务异常无关的糟糕预测更新,我们可能会在特定项目的Lag3Final后停止更新该预测。
摘要
虽然我们讨论了一些关键的描述性需求规划分析,但这些并不是详尽无遗的。例如,详细列表可以在 这里 找到。然而,考虑分析数量的权衡是重要的。分析过少无法提供足够的洞察,而分析过多则消耗大量时间和精力,违背了例外管理预测的初衷。目标是从多个分析中寻找洞察的一致性。最终,无论分析的数量如何,我们都希望利用这些分析来识别需要额外关注的领域,以改善组织的需求规划过程和预测准确性。
感谢阅读。希望你觉得有用。随时向我发送评论到 rkumar5680@gmail.com。让我们在 LinkedIn 上联系。
ChatGPT 的工作原理:聊天机器人背后的模型
原文:
towardsdatascience.com/how-chatgpt-works-the-models-behind-the-bot-1ce5fca96286
对你听说过的聊天机器人背后的直觉和方法的简要介绍。
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 1 月 30 日
--

这篇关于驱动 ChatGPT 的机器学*模型的温和介绍,将从大语言模型的介绍开始,深入探讨使 GPT-3 能够训练的革命性自注意力机制,然后深入挖掘使 ChatGPT 异常出色的强化学*技术。
大语言模型
ChatGPT 是一种自然语言处理模型的扩展,称为大语言模型(LLMs)。LLMs 消化大量的文本数据,并推断文本中的单词之间的关系。随着计算能力的进步,这些模型在过去几年里得到了发展。LLMs 随着输入数据集和参数空间的增大而提高其能力。
语言模型的最基本训练涉及在单词序列中预测一个单词。最常见的是下一词预测和掩码语言建模。

作者生成的下一词预测和掩码语言建模的任意示例。
在这种基本的序列技术中,通常通过长短期记忆(LSTM)模型实现,模型根据周围的上下文填入最有统计概率的单词。这种序列建模结构有两个主要的限制。
-
模型无法对一些周围的单词赋予比其他单词更多的权重。在上述示例中,虽然‘reading’通常会与‘hates’相关联,但在数据库中‘Jacob’可能是如此热衷于阅读,以至于模型应给予‘Jacob’比‘reading’更多的权重,从而选择‘love’而不是‘hates’。
-
输入数据是单独且顺序处理的,而不是作为整个语料库处理。这意味着,当 LSTM 被训练时,上下文窗口是固定的,仅在序列中的几个步骤之外扩展。这限制了单词之间的关系的复杂性和可以推导出的意义。
针对这个问题,2017 年 Google Brain 团队引入了变换器。与 LSTM 不同,变换器可以同时处理所有输入数据。通过使用自注意力机制,模型可以根据语言序列的任何位置对输入数据的不同部分给予不同的权重。这一特性显著提升了将意义注入 LLM 的能力,并使得处理显著更大的数据集成为可能。
GPT 和自注意力
生成预训练变换器(GPT)模型最早由 openAI 在 2018 年推出,命名为 GPT-1。之后,模型在 2019 年演变为 GPT-2,2020 年演变为 GPT-3,最*在 2022 年推出了 InstructGPT 和 ChatGPT。在将人类反馈整合到系统之前,GPT 模型演变的最大进展是由计算效率的成就驱动的,这使得 GPT-3 能够在比 GPT-2 上训练更多的数据,从而拥有更广泛的知识基础和执行更广泛任务的能力。

GPT-2(左)和 GPT-3(右)的比较。由作者生成。
所有 GPT 模型大体上遵循在“Attention is All You Need”(Vaswani et al., 2017)中建立的变换器架构,该架构包括一个编码器来处理输入序列和一个解码器来生成输出序列。在原始变换器中,编码器和解码器都有一个多头自注意力机制,使得模型能够对序列的不同部分赋予不同的权重,从而推断意义和上下文。作为对原始变换器的演变,GPT 模型利用了 Radford 等人(2018)建立的仅解码器的变换器,采用了掩蔽自注意力头。该架构通过 Radford 等人(2019)和 Brown 等人(2020)的工作进一步微调。采用仅解码器的框架是因为 GPT 的主要目标是生成连贯且上下文相关的文本。自回归解码由解码器处理,使得模型能够保持上下文,并逐个标记生成序列。
驱动 GPT 的自注意力机制通过将标记(文本片段,可以是一个单词、句子或其他文本组)转换为向量,来表示标记在输入序列中的重要性。
-
为输入序列中的每个标记创建查询、键和值向量。
-
通过计算步骤一中的查询向量与每个其他标记的键向量之间的点积,计算它们之间的相似性。
-
通过将步骤 2 的输出输入到softmax 函数中生成标准化权重。
-
生成最终向量,通过将步骤 3 中生成的权重与每个令牌的值向量相乘,来表示令牌在序列中的重要性。
GPT 使用的‘多头’注意机制是自注意力的进化。模型并非仅执行步骤 1-4 一次,而是并行地多次迭代此机制,每次生成查询、键和值向量的新线性投影。通过这种方式扩展自注意力,模型能够理解输入数据中的子含义和更复杂的关系。

作者生成的 ChatGPT 截图。
尽管 GPT-3 在自然语言处理方面引入了显著的进展,但其在与用户意图对齐的能力上仍有限。例如,GPT-3 可能会生成
-
缺乏帮助性,意味着它们不遵循用户的明确指示。
-
包含幻觉,即反映不存在或不正确的事实。
-
缺乏可解释性,使得人们难以理解模型如何得出特定的决定或预测。
-
包含有害或偏见内容,即有害或冒犯性内容,并传播虚假信息。
在 ChatGPT 中引入了创新的训练方法,以应对标准 LLM 的一些固有问题。
ChatGPT
ChatGPT 是 InstructGPT 的衍生产品,它引入了一种新方法,将人类反馈纳入训练过程,以更好地使模型输出符合用户意图。有关强化学*从人类反馈(RLHF)的详细描述,请参阅openAI 的 2022论文训练语言模型以遵循人类反馈的指示,下面进行了简化。
步骤 1:监督微调(SFT)模型
第一个开发涉及对 GPT-3 模型进行微调,通过聘请 40 名承包商创建一个监督训练数据集,其中输入具有已知输出供模型学*。输入或提示收集自实际用户输入到 Open API 中。标签员随后为每个提示编写了适当的回应,从而为每个输入创建了已知输出。然后使用这个新的监督数据集对 GPT-3 模型进行微调,创建了 GPT-3.5,也称为 SFT 模型。
为了最大化提示数据集的多样性,每个用户 ID 最多只能有 200 个提示,任何共享长前缀的提示都被删除。最后,所有包含个人可识别信息(PII)的提示都被删除。
在汇总来自 OpenAI API 的提示后,标签员还被要求创建样本提示,以填补仅有少量真实样本数据的类别。感兴趣的类别包括
-
简单提示: 任何任意请求。
-
少样本提示: 包含多个查询/响应对的指令。
-
基于用户的提示: 对应于 OpenAI API 请求的特定用例。
在生成响应时,要求标注者尽力推断用户的指令是什么。论文描述了提示请求信息的三种主要方式。
-
直接: “告诉我关于……”
-
少样本: 给出这两个故事的例子,写另一个关于相同主题的故事。
-
续写: 给出一个故事的开头,完成它。
从 OpenAI API 和标注者手工编写的提示汇编得到了 13,000 个输入/输出样本,用于监督模型。

图片(左侧)摘自《训练语言模型以遵循人类反馈的指令》 OpenAI 等,2022 arxiv.org/pdf/2203.02155.pdf。额外的上下文由作者在右侧红色部分添加。
第 2 步:奖励模型
在第 1 步中训练好 SFT 模型后,模型生成了更符合用户提示的响应。下一步的改进形式是训练奖励模型,其中模型输入是一系列提示和响应,而输出是一个缩放值,称为奖励。奖励模型是为了利用强化学*,在这种学*方式中,模型学会生成输出以最大化其奖励(参见第 3 步)。
为了训练奖励模型,标注者会看到 4 到 9 个 SFT 模型的输出,这些输出都是针对一个输入提示的。他们需要将这些输出从最好到最差进行排名,生成输出排名的组合如下。

响应排名组合的示例。由作者生成。
将每个组合作为单独的数据点纳入模型导致了过拟合(无法超越已见数据进行推断)。为了解决这个问题,模型是通过将每组排名作为一个单独的批次数据点来构建的。

图片(左侧)摘自《训练语言模型以遵循人类反馈的指令》 OpenAI 等,2022 arxiv.org/pdf/2203.02155.pdf。额外的上下文由作者在右侧红色部分添加。
第 3 步:强化学*模型
在最终阶段,模型会接收到一个随机提示并返回一个响应。这个响应是使用模型在第 2 步中学到的“策略”生成的。策略代表了机器为了实现目标而学会使用的策略;在这个案例中,目标是最大化奖励。基于在第 2 步中开发的奖励模型,会为提示和响应对确定一个缩放奖励值。然后,奖励反馈到模型中,以进化策略。
2017 年,Schulman et al. 介绍了 Proximal Policy Optimization (PPO),这一方法用于更新每次生成回应时的模型策略。PPO 结合了来自 SFT 模型的每个令牌 Kullback–Leibler (KL) 惩罚。KL 散度测量两个分布函数的相似性,并惩罚极端距离。在这种情况下,使用 KL 惩罚减少了回应与在步骤 1 中训练的 SFT 模型输出之间的距离,以避免过度优化奖励模型,并过于偏离人类意图数据集。

从 Training language models to follow instructions with human feedback OpenAI et al., 2022 arxiv.org/pdf/2203.02155.pdf 插入的图片(左)。作者添加的额外背景信息(右)以红色标注。
过程的第 2 和第 3 步可以重复迭代,尽管在实践中尚未广泛实施。

由作者生成的 ChatGPT 截图。
模型评估
模型评估通过在训练期间留出一个模型未见过的测试集来进行。在测试集上进行一系列评估,以确定模型是否比其前身 GPT-3 更好地对齐。
有用性:模型推断和遵循用户指令的能力。标注者 85 ± 3% 的时间更喜欢 InstructGPT 的输出而不是 GPT-3。
真实度:模型出现幻觉的倾向。PPO 模型在使用 TruthfulQA 数据集进行评估时,显示出真实度和信息量的轻微增加。
无害性:模型避免不适当、贬低和诋毁内容的能力。无害性通过使用 RealToxicityPrompts 数据集进行了测试。测试在三种条件下进行。
-
指示提供尊重的回应:结果显著减少了有害回应。
-
指示提供回应,没有任何尊重设置:毒性没有显著变化。
-
指示提供有害回应:回应实际上比 GPT-3 模型显著更有害。
要了解更多关于创建 ChatGPT 和 InstructGPT 的方法,请阅读由 OpenAI 发布的原始论文 Training language models to follow instructions with human feedback,2022 arxiv.org/pdf/2203.02155.pdf。

由作者生成的 ChatGPT 截图。
祝学*愉快!
来源
-
medium.com/r/?url=https%3A%2F%2Fdeepai.org%2Fmachine-learning-glossary-and-terms%2Fsoftmax-layer -
medium.com/r/?url=https%3A%2F%2Ftowardsdatascience.com%2Fproximal-policy-optimization-ppo-explained-abed1952457b
关于计算机视觉中颜色表示的全面指南 (CV-02)
原文:
towardsdatascience.com/how-color-is-represented-and-viewed-in-computer-vision-b1cc97681b68
颜色空间和颜色模型的详细解释
·发布于 Towards Data Science ·7 min read·2023 年 4 月 3 日
--

照片由 Lucas Kapla 拍摄,来源于 Unsplash
动机
我们如何感知一个物体?人类和计算机视觉系统的工作方式相同吗?有多少种颜色?我们是否知道或想知道这些问题的所有答案?如果“是”,那么这篇文章将是最适合你的。
眼睛是造物主创造的美丽杰作,它能以美观和和谐的方式感知物体的颜色。颜色是一种基于电磁波谱的视觉感知 [1]。动物视觉系统探测物体波长的吸收情况,不同的吸收产生不同的颜色。颜色可能是无限的,但我们大致认为存在 1000 万种颜色 [2]。光是产生颜色的必需条件。在没有光的情况下,一切都显得黑暗。想象一下你被锁在一个黑暗的房间里。感觉如何?所以,没有光,世界是没有意义的。它也为我们的生活带来了魅力。
计算机不能像人类一样感知物体。计算机通过一些标准的颜色空间(颜色的表示方式)来生成和识别颜色。
*在这篇文章中,我将讨论不同的颜色模型,以便更好地了解颜色在计算机视觉系统中的表示和感知。*
目录
-
**人类与计算机视觉系统** -
**什么是颜色空间?** -
**不同的颜色模型**
人类与计算机视觉系统
人类视觉系统是复杂的。首先,眼睛感知环境/物体,然后不同的器官和神经将信号传递给大脑。最后,大脑进行识别并做出相应的反应。
[注:我只是简单地表示了这个过程,因为它超出了本文的范围。实际上,过程要复杂得多。如果你仍然对了解更多关于人类视觉系统的信息感兴趣,请阅读 文章。]

人类与计算机视觉系统(图像来源于作者)
计算机是一台在没有人类隐性或显性指令的情况下无法执行任何任务的机器。对于计算机视觉系统,摄像头拍摄图像以感知环境,摄像头输入进一步传递给计算机。最后,不同的计算机视觉模型识别图像并显示输出。
什么是颜色空间?
颜色是计算机视觉系统的一个重要组成部分。颜色空间是以有组织的方式组合的颜色,主要用于在不同介质中再现颜色。例如,你有一个笔记本电脑屏幕和一个 52 英寸的大型显示器。显然,总像素和颜色组合会有所不同。不同的颜色空间帮助我们使其与屏幕分辨率兼容,以提供更好的颜色组合体验。

CIE 1931 xy 色度图(根据 Wikimedia Commons 自由许可证 CC BY-SA 3.0)
上面的色度图表示了颜色空间。它展示了不同 x 和 y 轴值及各种波长的波段。这个空间使所有可能的颜色组合对人眼可见。像 RGB、sRGB 等有很多颜色空间。每个颜色空间下的颜色组合(用三角形标记)包含了指定的颜色。阅读 文章以获得详细解释。
不同的颜色模型
颜色模型是我们可以在数学上表示不同颜色的方式。例如,RGB 颜色模型结合了红色、绿色和蓝色。不同强度值的混合产生不同的颜色。
我们经常将颜色模型与颜色空间搞混。 让我们来澄清一下。颜色模型在数学上表示不同的颜色,而“颜色”一词出现在我们在不同介质中再现颜色时。例如,sRGB 颜色空间是红色、绿色和蓝色不同范围的颜色强度的混合。阅读答案如果你还想了解更多区别。
常见颜色模型有 *RGB, CMYK, HSL, 和 HSV*。
- RGB 颜色模型
RGB 是基本的颜色模型组合,包括不同强度的红色、绿色和蓝色。强度值范围从 0 到 255。这个模型使用三个颜色通道(红色、绿色和蓝色)来表示颜色。

RGB 加法颜色(来自Wikimedia Commons 公共领域许可证)
可能的颜色组合有(255 x 255 x 255)16,581,375 种。它也被称为加法颜色模型,因为它通过结合 RGB 颜色来产生不同的颜色。大多数显示器使用 RGB 颜色模型。
我创建了一个立方体 RGB 颜色模型的 gif,以便更好地理解。

如何通过 RGB 值改变颜色(gif 由作者创建)
立方体上的黑色圆圈表示不同 RGB 强度值的确切颜色。
*[注:我使用了* [***网站***](https://programmingdesignsystems.com/color/color-models-and-color-spaces/index.html) *来模拟颜色模型。]*
- CMYK 颜色模型
CMYK 颜色模型是由四种颜色青色、品红色、黄色和黑色(Key)组成的。它是 RGB 颜色模型的修改版本,因为我们通过连续减去红色、绿色和蓝色来获得青色、品红色和黄色。
Cayan = 255- Red
Magenta = 255- Green
Yellow = 255- Blue

CMYK 颜色模型(图片由作者提供)
我们无法通过青色、品红色和黄色的组合来产生纯黑色。因此,额外的黑色(Key)用于完成模型。
这种颜色模型最常用于打印。
- HSL 和 HSV 颜色模型
这两种颜色模型是基于人类对 RGB 颜色的感知创建的。尽管这些模型与人类感知不完全相同,但它们是兼容的。在讨论颜色模型之前,我想解释一下主要的颜色属性***色相、饱和度和亮度/明度***。

RGB/HSL/HSV 色相,饱和度 100%和亮度 50%(值 100%)(来自 wikimedia commons 的免费许可证CC BY-SA 4.0)
色相 — 上面的颜色轮表示从 0 到 360 度的多种颜色。轮子上的任何特定位置都被认为是色相。下图清楚地描绘了这一情况。

色相范围 0–360°(来自 wikimedia commons 的免费许可证CC BY-SA 4.0)
饱和度 — 它是颜色(色相)的强度。下图表示了红色的饱和度范围。红色的灰度是低饱和度,而纯红色是高饱和度。

饱和度范围(左侧为 0%)(Wikimedia commons 免费许可证CC0)
明度 / 亮度 — 它是测量单个颜色亮度的指标。 其中,暗色是低亮度,白色是高亮度。蓝色的亮度如下所示。

蓝色的亮度/明度(图片由作者提供)
👉 HSL 颜色模型由色调、饱和度和亮度组成。请看下面的图像。

HSL 颜色模型(来自 Wikimedia Commons 免费许可 CC BY-SA 3.0)
颜色模型与实心圆柱进行比较。色调表示从 0 到 360 度的不同角度的颜色。在圆柱的顶部,所有颜色的亮度都很高。这就是为什么它看起来是白色的原因。从上到下,亮度逐渐降低;在底部,没有光线。饱和度值从圆柱的中心到外侧逐渐从低到高。HSL 颜色模型在不同色调、饱和度和亮度下的模拟如下所示。

HSL 颜色模型(GIF 由作者创建)
圆柱上的圆圈表示不同色调、饱和度和亮度值在左侧刻度中显示的特定颜色。
👉HSV 颜色模型类似于 HSL 颜色模型。在这里,“亮度”一词被“值”取代。在 HSL 中,最大亮度时,颜色为纯白色。另一方面,在 HSV 中,最大值时,颜色不是纯白色,而是类似于在特定颜色上照射光线。

HSV 颜色模型(来自 Wikimedia Commons 免费许可 CC BY-SA 3.0)
让我们通过模拟更好地理解该模型。

HSV 颜色模型(GIF 由作者创建)
[我用来模拟模型的 网站 ]
结论
除了这些颜色模型,还有一些其他颜色模型。但这些模型在计算机视觉中广泛使用。详细来说,RGB 颜色模型通过混合红色、绿色和蓝色来表示颜色。CMYK 是青色、品红色、黄色和关键(黑色)颜色的颜色模型组合。而最后两个颜色模型,HSL 和 HSV,是用于更直观地表示 RGB 颜色的颜色模型,以反映人类对 RGB 颜色的感知。
参考文献
我为你选择了一些文章列表。
## 开始使用 NumPy 和 OpenCV 进行计算机视觉
用 Python 开始你的计算机视觉编程
towardsdatascience.com 
机器学*基础
查看列表6 个故事



数据科学统计
查看列表13 个故事



数据可视化完整指南
查看列表4 个故事


企业如何停止在人工智能和数据驱动决策中失败
四个杠杆可以帮助商业领袖成功利用数据
·
关注 发表在 Towards Data Science ·7 min read·2023 年 2 月 8 日
--
图片由 Towfiqu barbhuiya 提供,来自 Unsplash
数据已经成为一个热门话题十多年了。它随着大数据的兴起而发展,*年来,AI 和机器学*也成为了前沿。过去十年也是一个极度增长的十年。资本充裕,公司有足够的时间来学*和完善他们的技术。然而,研究指出组织在有效利用数据方面的失败。经济环境现在发生了变化,公司现在更加关注盈利能力。失败的成本现在太高了,那些能够有效利用数据的公司将拥有竞争优势。AI 和数据驱动的决策可以在这里提供帮助,通过清晰化讨论、将事实与意见分开,并加速市场推出的时间。
然而,大多数公司尚未建立一个适合 AI 和数据驱动决策的组织模型。随着公司调整战略以应对新的经济现实,以下四个关键要素可以帮助建立基于数据的决策文化:
明确数据需要如何使用的原因和方式
-
目标、目标和指标的一致性。 现代劳动力高度以目的为驱动。对他们来说,效率、盈利能力、增长等是实现目标的手段,而不是目标本身。因此,领导层需要对公司所追求的目的非常清楚。这个目的需要被转化为具体的目标,然后进一步细化为需要衡量的准确指标。我的一个咨询客户,一家道路救援公司,特别有效地做到了这一点。他们的目的在于确保没有客户在道路上被困超过特定时间,这取决于服务区域。然后,这被转化为每个组织的目标,例如,财务预算分配被优化以实现这一目标,客户服务部门将致力于第一次呼叫解决问题,销售部门不仅依据目标完成情况获得奖励,还依据 NPS 进行激励,以确保他们将正确的产品卖给正确的客户。最后,执行仪表板中包含了所有这些指标,并且在年度规划过程中为这些 P0 指标设定了目标。
-
明确的数据导向问题陈述在整个组织中层层传递。 拥有一套清晰的问题陈述可以带来整个组织的一致性。这些陈述在公司层面需要简单明了,例如:“将运营利润率提高 50 个基点”。这有助于每个人对需要解决的问题达成一致,并且为其提供一个数据点。总体目标可以进一步细分到各个职能部门,他们可以决定想要采取的措施。例如,市场营销可能会制定一个问题陈述:“改变媒体投放模型,以在减少营销支出$xx 的同时保持 MQLs。”
-
高层的“使用”数据,而不仅仅是“认可”。 即使高层同意基于数据的决策是必要的,他们仍然会依赖直觉。高层应当做出基于数据的决策,即使这有时与他们的直觉相悖。他们也应当传达这一点,以便整个组织感受到自己是一个数据驱动的组织。我曾与一位非常有效的销售高管合作,他理解数据的力量,并确保他的团队充分利用公司的数据能力。他的两个主要方法是:在做出决定之前,确保至少有一些支持数据,并且向数据团队提供非常明确的陈述,例如——“我想了解 5%和 10%的价格上涨对我们销售量和利润的影响。”
拥有对数据的共同理解
-
什么正在被测量? 我们常常认为数据是客观的,但实际上,它可能非常主观。组织往往对一些基本指标如增长、流失率和服务成本有模糊甚至不一致的定义。必须对用于衡量组织的指标及其确切定义有共同的理解。如果缺乏这种理解,数据就会成为争论的引发者,而不是推动决策的工具。我发现,最有效的方法之一是建立一个公司级的数据词典,提供精确的定义和数据来源。
-
谁负责测量? 我们生活在矩阵型组织的时代,多个团队跨职能合作,常常有些任务重叠。如果多个个人或团队负责一项事物,最终没有人真正拥有它。强有力的责任心和流程至关重要。每个人都应明确谁负责和提供每个指标。虽然关于如何定义指标可能会有争论,但一旦争论完毕,指标所有者提供的数字即为事实。类似于前述,确定数据词典中的负责人也至关重要。
-
什么是黄金真理来源? 在大型组织中,找到十几个真理来源并不罕见。每个团队都会创建自己版本的真理,以满足其用例。这会在组织中造成混乱。这通常被视为战术数据话题,而实际上它是一个高层领导话题。拥有一组核心数据表,例如财务数据、市场推广指标和人员指标,是至关重要的。这是分析效率丧失的最大原因之一,分析师花费无数时间试图协调数据,而高层则花费宝贵时间争论数据,而不是制定成功的市场策略。
建立基础要素
-
简化技术栈。 技术越简单,维护越容易,解释和使用也越容易。工程团队倾向于追逐炫目的技术,但业务目标和用例应该定义需要实施的技术,而不是相反。如今技术发展迅速,创新层出不穷。这导致了我们有时会看到寻找钉子的锤子的情况。我见过许多组织拥有多个复杂的数据栈,且几乎没有互操作性。最好保持技术栈的简单,有时 Power Pivot 运行在数据库之上就足够了。
-
自助分析。 如今数据如此庞大,除非加以情境化,否则很难有效利用。这种情境通常存在于业务和具体职能中。数据基础设施需要使分析师甚至业务人员能够利用数据。像 Alteryx 这样的工具可以方便非技术团队构建 ETL,而像 Tableau 这样的工具可以帮助他们使报告变得可重复。这不仅仅是技术问题,还涉及文化和组织如何使用数据的哲学。
-
治理和流程。 这些通常是组织中最后投资的领域。这是一个既不受高管也不受数据团队热爱的主题。然而,随着时间的推移,它成为了速度的限制因素。例如,新聘请的分析师常常需要几个月时间来适应,因为他们要在混乱的数据中摸索;仍然需要花费数周时间查找和清理数据,而这些分析应该只需要几个小时。
创建一个重视和使用数据的文化
-
授权决策。 决策不是领导者的唯一特权,也不应该是。微观决策最终会累积。组织中每天可能发生数百个决策。例如:小额交易的定价决策、过程工作流的软件插件选择、待办事项的优先级排序等。明确的决策权和利用数据来提供信息的文化可以带来巨大的效率。员工感到被授权,可以迅速行动。但这很难实现,根据我的经验,这需要领导推动并且具有高度跨职能的组织转型主题。
-
定义仪式和文档。 文化变革需要通过行动来实践和推动。培养小的变化可以产生大的影响。一个最有效的方法是创建仪式并将其融入工作方式中。一个简单但强大的做法是,如果在会议中需要做出决定,请求者需要在会议前发送经过分析和对齐的支持数据作为预读。同样,我观察到,开始运营评审时查看一组指标在决策中非常有效。亚马逊的 备忘录和新闻稿 是这种文档的绝佳例子。
-
连接数据与业务。 每当数据与职能团队之间存在交易关系时,他们在翻译过程中会失去洞察力。那些朝着相同问题陈述工作的联合业务+数据团队最为高效。明确的问题陈述,例如“我们如何在相同的营销预算下提高转化率?” 可以帮助团队对齐到同一个目标。然后,业务团队需要向数据团队传授背景知识,而数据团队则需要向业务团队传授可能性艺术及分析技术的局限性。数据和技术团队需要有一个席位才能发挥有效作用。那些将其作为后端功能的组织在挖掘数据价值方面会有所欠缺。
任何形式的组织变革都很困难,而成为一个人工智能和数据驱动决策的组织更是如此,因为这需要将业务、技术和数据分析元素结合在一起。然而,这是可能的,使用本文讨论的一些策略可以提供一个良好的开端。
关注我在 LinkedIn 和 Medium 的动态,以获取有关人工智能与数据驱动决策和人机团队合作的更多见解。
Cypher 在 Neo4j v5 中的变化
在 Neo4j 最新发布版本中,Cypher 有哪些新功能和被弃用的特性
·
关注 发表于 Towards Data Science ·9 分钟阅读·2023 年 1 月 17 日
--
几周前,Neo4j 5 发布了。如果你像我一样,在较新的 Neo4j 4 版本中忽略了所有弃用警告,你可能需要更新你的 Cypher 查询以适配最新的 Neo4j 版本。幸运的是,新 Cypher 语法没有重大变化,因此更新过程应该不会太困难。然而,我决定写这篇博客文章来帮助你过渡。此外,我还会介绍一些新的 Cypher 语法,这些语法可以简化你的查询。
本博客文章中的 Cypher 示例可作为 GitHub 上的 Jupyter Notebook 下载。
Neo4j 环境设置
你需要设置一个 Neo4j 环境来跟随这篇文章中的代码示例。像往常一样,你可以使用Neo4j Desktop应用程序来设置本地环境。另一方面,你可以使用作为Neo4j Aura提供的免费云实例。如果你选择 Neo4j Aura 的路径,使用免费的永久实例并启动一个没有预填充图形的空项目。
附注:免费的 Neo4j Aura 实例不提供 GDS 支持。由于这是一个以 Cypher 为重点的博客文章,你不会使用 GDS 库中的任何算法。
数据集
我对奇幻或超级英雄数据集有特别的喜爱。这次,我找到了一个描述《沙丘》系列角色的数据集。
## Frank Herbert's DUNE Characters
在奇幻世界中的社交网络....帮助构建数据集!
沙丘数据集在 Kaggle 上以 CC0 许可证提供。为了方便你,我已经将数据集复制到 GitHub 仓库中,这样你就不必下载数据集,可以轻松地在你的本地或云实例的 Neo4j 中导入它。此外,我将关系类型 Parent-Child 和 Other Family 重命名为 Family,并删除了 Unknown 关系。
图形模型

沙丘图形模型。作者提供的图片。
图形模型围绕角色展开。角色节点具有多个属性,如名字、文化、以及他们的出生或去世时间。此外,他们的家族忠诚度被表示为次要节点标签。使用次要节点标签而不是单独的节点的主要目的是展示 Cypher 中新节点标签过滤选项。角色之间有四种关系类型:
-
同盟
-
敌人,
-
家庭
-
婚姻
数据集导入
与大多数导入情况一样,你首先需要在 Neo4j 中定义唯一约束。唯一约束确保每个具有特定标签的节点的给定属性是唯一的。
在 Neo4j v5 中,定义唯一约束的语法略有变化。

UNIQUE CONSTRAINTS 的语法更改。作者提供的图片。
新的唯一约束语法中有两个关键词被替换。ON 关键词被替换为FOR,而ASSERT 被更改为REQUIRE。
以下 Cypher 语句定义了Character节点的name属性的唯一约束。
CREATE CONSTRAINT IF NOT EXISTS FOR (c:Character) REQUIRE c.name IS UNIQUE;
接下来,你需要导入 CSV 文件。尽管 Dune CSV 文件只有 1000 行,但你将假设你正在处理一个具有数千行的大型 CSV 文件。因此,你希望将批量导入分成多个事务。由于 Neo4j v5 中USING PERIODIC COMMIT子句已被弃用,你需要使用新的批处理事务语法。

导入大型 CSV 文件的语法更改。图片由作者提供。
子查询已经在 Neo4j v4 中引入,但在 v5 中发挥了更重要的作用。子查询通过CALL子句实例化,并用花括号{}包裹。它们非常适合各种功能,如后联合处理、限制每行结果、以及条件执行。此外,Cypher 子查询现在是唯一的原生 Cypher 语法(不包括 APOC),可以将单个 Cypher 语句批处理成多个事务。为了指定 Cypher 子查询应拆分成多个事务,你需要添加IN TRANSACTIONS OF x ROWS,其中 x 代表每个批次的行数。
以下 Cypher 语句包含两个嵌套的子查询。顶层子查询用于将每 10 行的导入批处理到一个新事务中。另一方面,你可以将嵌套的 Cypher 子查询用作条件执行功能。
LOAD CSV WITH HEADERS FROM "https://bit.ly/3kbU21V" AS row
CALL {
WITH row // Explicit import of variables
MERGE (c:Character {name:row.Character})
SET c += apoc.map.clean(row,
["name", "Detail", "to", "relationship_type", "House_Allegiance"], [])
WITH c, row
CALL apoc.create.addLabels(c, [row.House_Allegiance]) YIELD node
// nested subquery for conditional execution
CALL {
WITH row, c // Explicit import of variables
WITH row, c
WHERE row.to IS NOT NULL
MERGE (c1:Character {name: row.to})
WITH row, c, c1
CALL apoc.merge.relationship(c, toUpper(row.relationship_type), {}, {}, c1)
YIELD rel
RETURN count(*) AS count
}
RETURN count(*) AS finalCount
} IN TRANSACTIONS OF 10 ROWS // Define batched transaction for every 10 rows
RETURN count(*)
导入查询稍微复杂一些,因为我希望在一个 Cypher 语句中完成导入。它以你可能见过的LOAD CSV子句开始。为了将导入批处理成多个事务,你需要使用CALL子句来启动 Cypher 子查询。顶层子查询以IN TRANSACTIONS OF x ROWS子句结束,该子句指定了事务批处理。
如果你想在 Cypher 语句中使用任何变量,你必须通过WITH子句显式导入它。首先,顶层子查询从外部查询中导入row变量。接着,它合并 Character 节点并设置几个属性。由于纯 Cypher 中不支持动态的次级标签,你可以使用 APOC 的过程。
一些《沙丘》角色在 CSV 的 to 和 relationship_type 列中定义了额外的关系。另一方面,to 和 relationship_type 列在几个行中是空的。 FOREACH 条件执行技巧 是我博客文章的主要选择。然而,由于这篇博客文章的主题是 Cypher 子查询,我决定展示如何使用 Cypher 子查询进行条件执行。首先,通过嵌套的 Cypher 子查询,你需要导入 row 和 c 变量。接下来,你需要过滤出 to 列不为空的行。然而,你不能在用于导入变量的同一 WITH 子句中过滤变量。因此,你需要添加第二个 WITH 子句来过滤行。最后,你使用 APOC 的合并关系过程,因为纯 Cypher 语法不支持创建具有动态关系类型的属性。
如果这个示例对你来说有点复杂,你可以查看 文档。
新的内联过滤选项
Neo4j v5 中有许多新的内联过滤选项。那么什么是内联过滤呢?本质上,它是直接在 MATCH 子句中过滤图模式的能力。
你将开始学*用于过滤节点标签的新语法。在 Neo4j v5 中,引入了以下逻辑表达式,以允许更灵活的节点过滤。
-
& — AND 表达式
-
| — OR 表达式
-
! — NOT 表达式
例如,如果你想匹配所有具有 Fremen 或 Harkonnen 标签的节点,你可以使用 | 表达式。
MATCH (c:Fremen|Harkonnen)
RETURN count(*)
另一方面,如果你需要找到同时具有 Character 和 Harkonnen 标签的所有节点,你可以使用 & 表达式。
MATCH (c:Character&Harkonnen)
RETURN count(*)
! 表达式允许你否定节点标签。例如,假设你想匹配所有具有 Character 标签但没有 Harkonnen 标签的节点。
MATCH (c:Character&!Harkonnen)
RETURN count(*)
现在 Cypher 还支持用于过滤节点标签的嵌套表达式。例如,以下 Cypher 语句匹配所有不具有 Fremen 或 Harkonnen 标签的节点。
MATCH (c:!(Fremen|Harkonnen))
RETURN count(*)
这些表达式使用括号 () 嵌套。
Cypher 的另一个新特性是可以在 MATCH 子句中包含 WHERE 子句。这使你能够在 MATCH 语句中直接应用 Cypher 按节点属性过滤的所有灵活性。
例如,以下 Cypher 语句匹配所有Culture属性以Z开头且Died属性不为空的字符。
MATCH (c:Character WHERE c.Culture STARTS WITH "Z" AND c.Died IS NOT NULL)
RETURN c.name AS character
用于过滤节点的相同逻辑表达式也可以用于过滤关系类型。例如,| 表达式(表示逻辑 OR)在 Cypher 中已经存在了一段时间。
以下 Cypher 语句匹配所有Character节点,其Culture属性值为Bene Gesserit,并展开它们的ALLIES或FAMILY关系。
MATCH p=(c:Character WHERE c.Culture = "Bene Gesserit")-[r:ALLIES|FAMILY]-()
RETURN [n in nodes(p) | n.name] AS pairs, type(r) AS results
仅使用&表达式实际上没有什么意义,因为单个关系不能有多个类型。然而,与否定!表达式结合使用时,&可能会很有用。
例如,以下 Cypher 语句展开所有不是ALLIES或FAMILY的关系。
MATCH p=(c:Character WHERE c.Culture = "Fremen")-[r:!ALLIES&!FAMILY]-()
RETURN [n in nodes(p) | n.name] AS pairs, type(r) AS results
存在性子查询
接下来,你将了解存在性子查询的语法如何更改。存在性子查询可以用来查找属于指定模式的图模式。

存在性过滤器的语法更改。图片由作者提供。
在这个例子中,存在性子查询用于查找具有进或出FAMILY关系的Character节点。我喜欢存在性子查询,因为它们提供了根据任何图模式过滤节点的灵活性,而无需在 MATCH 子句中展开它们并担心查询基数(行数)。在 Neo4j v5 中,你必须添加EXISTS子句,并用大括号括起用于过滤的指定图模式以执行存在性子查询。
MATCH (c:Character)
WHERE exists { (c)-[:FAMILY]-() }
RETURN count(*) AS count
此外,你现在可以在存在性子查询中引入新的引用变量。例如,以下存在性子查询过滤具有FAMILY关系的Character节点。然而,仅考虑FAMILY关系的终节点同时具有ALLIES和MARRIAGE关系的模式。
MATCH (c:Character)
WHERE exists { (c)-[:FAMILY]-(t)
WHERE (t)-[:ALLIES]-() AND (t)-[:MARRIAGE]-() }
RETURN count(*) AS count
你还可能看到在存在性子查询中使用的MATCH子句。根据我的经验,MATCH子句完全是可选的。然而,它可能使存在性子查询的语法更具可读性和直观性。
MATCH (c:Character)
WHERE exists { MATCH (c)-[:FAMILY]-(t)
WHERE (t)-[:ALLIES]-() AND (t)-[:MARRIAGE]-() }
RETURN count(*) AS count
查看文档以获取更多存在性子查询的示例。
计数子查询
本文最后一类子查询是所谓的计数子查询。它们用于计数定义的图模式数量。例如,我在以前的博客文章中经常使用它们来计数一个节点拥有的关系数量。

计数子查询的语法更改。图片由作者提供。
之前,你可以使用size()函数来计数特定模式的数量。这是一种方便的语法,可以计数图模式而不影响主查询的基数。此外,它可能比其他计数关系数量的方法表现更好。在 Neo4j v5 中,你需要用count{}替代size()操作符。
以下 Cypher 语句返回按其度数(关系计数)排序的前五个 Character 节点。
MATCH (c:Character)
RETURN c.name AS character,
count{ (c)--() } AS degree
ORDER BY degree DESC
LIMIT 5
如之前一样,您可以使用WHERE子句中的计数子查询来过滤节点。在这个例子中,Cypher 语句过滤了具有多个关系的节点。
MATCH (c:Character)
WHERE count{ (c)--() } > 2
RETURN count(*) AS count
摘要
Neo4j 的新版本提供了更快的性能和更多的 Cypher 灵活性。然而,您可能需要重构一些现有的 Cypher 语句,以便与 Neo4j v5 兼容。希望这篇博客文章能帮助您完成升级过程。
一如既往,代码可以在GitHub上找到。
数据泄漏如何影响模型性能声明
原文:
towardsdatascience.com/how-data-leakage-affects-model-performance-claims-841eb01276bb
·发布于数据科学之路 ·4 分钟阅读·2023 年 1 月 2 日
--
今年出现了几项 重要 科学进展,这些进展得益于机器学*驱动的研究。随着热情的增长,也带来了与基于 ML 的科学中遇到的可重复性问题相关的一些担忧。已经发现了几种方法论问题,其中数据泄漏似乎是最普遍的。通常,数据泄漏会扭曲结果,并导致过于乐观的结论。
数据泄漏可能通过多种方式发生。本帖的目的是介绍一些最常遇到的类型,并提供一些关于如何识别和缓解它们的提示。

图像由作者使用 dreamstudio.ai 生成
数据泄漏 可以定义为目标变量与其预测变量之间通过数据收集方法或预处理策略无意中引入的人工关系。
我将尝试举例说明的主要数据泄漏来源有:
-
训练数据集和测试数据集之间的分离不当
-
使用不合法的特征(代理变量)
-
测试集并非从感兴趣的分布中抽取
1. 训练数据集和测试数据集之间的分离不当
数据科学家知道他们需要将输入数据划分为训练集和测试集,仅使用训练集来训练模型,并且只在测试集上计算评估指标。这是一个大多数人都知道要避免的教科书错误。然而,初步的探索性分析通常是在完整的数据集上进行的。如果这初步分析也涉及预处理和数据清理步骤,那么它可能成为数据泄露的来源。
可能引入数据泄露的预处理步骤:
-
在拆分两个数据集之前进行缺失值插补或缩放。通过使用完整的数据集来计算插补参数(均值、标准差等),一些在训练过程中不应对模型可用的信息被引入到训练集中。
-
在拆分两个数据集之前进行过采样/欠采样 也会导致训练集和测试集之间的不正确分离(训练集中的过采样数据可能会出现在测试集中,导致过于乐观的结论)
-
在拆分之前未删除数据集中的重复项。在这种情况下,相同的值可能会在拆分后成为训练集和测试集的一部分,导致过于乐观的评估指标。
2. 使用不合法的特征
当数据集包含不应在建模中合法使用的特征时,也被视为数据泄露。一个直观的例子是,如果其中一个特征是结果变量的代理。
西雅图建筑能源基准数据集 包含了这样一个变量的例子。西雅图的目标是根据已经公开的特征(如建筑表面、建筑类型、物业用途、建造日期等)来预测建筑的能源性能。它们的数据集中还包含电力和天然气消耗值,以及目标变量场地能源使用和温室气体排放。电力和天然气消耗值与目标变量高度相关,在构建预测模型时包含这些特征将产生非常准确的结果。

某些特征与目标变量之间的相关性(作者图片)
然而,这些特征只是结果变量的代理。它们实际上并没有解释任何常识没有告诉我们的事情:使用大量电力的建筑总体能源使用会很高。
如果电力使用值在预测时是可用的,那么预测场地能源使用变成了一项微不足道的任务,实际上没有必要建立模型。
这里给出的例子很简单,但通常,是否使用特定特征的判断需要领域知识,并且可能是特定于问题的。
3. 测试集不是从感兴趣的分布中抽取的
这种特定的数据泄漏源可能更难举例,但可以直观地解释。我们可以将其分为几个子类别:
-
时间泄漏:如果模型用于对未来进行预测,那么测试集不应包含任何早于训练集的数据(模型将基于未来的数据进行构建)
-
训练和测试样本之间的非独立性:这个问题在医学领域更为常见,其中有多个样本是从同一个患者身上在一段时间内收集的。这个问题可以通过使用特定的方法如 块交叉验证来处理,但在通用情况下是一个困难的问题,因为数据中所有的潜在依赖关系可能都不为人知。
-
采样偏差:选择数据集中一个不具代表性的子集进行评估。一个这样的偏差例子是仅选择极端抑郁的病例来评估抗抑郁药物的有效性,并就该药物对一般抑郁症的治疗效果做出声明。
结论
数据泄漏可以在建模流程的各个阶段引入,检测它可能并不明显。预处理步骤和测试/训练分割方法将取决于数据集的特性,并可能需要特定领域的知识。一般来说,如果获得的结果好得令人难以置信,那么数据泄漏的可能性很高。
数据科学如何创造价值
原文:
towardsdatascience.com/how-data-science-can-deliver-value-f2d4a1ea3a49
对价值主张进行分类可以更容易地传达你们团队的工作内容
·发表在Towards Data Science ·阅读时间 4 分钟·2023 年 10 月 11 日
--

照片由Shubham Dhage提供,来源于Unsplash
大多数科技工作者会同意,目前的经济环境不太乐观。今年早些时候许多公司裁员,包括我所在的公司。更紧张的现金流通常意味着对团队的绩效和对组织的价值贡献进行更严格的审查。因此,你和你的团队能够清晰地阐明如何为业务目标做出贡献变得更为重要。
不过,团队——尤其是数据科学团队——可以通过不同的方式创造价值。这些方式包括指标与测量、产品特性、战略洞察以及操作决策产品。
为什么这四个类别?它们并非完全互斥——我预期一些团队会偶尔跨越这些类别。但一般来说,这些类别需要不同的技能集,产生不同的成果,并专注于特定的问题。了解哪个标签适用于你的团队或项目,可以帮助你确保拥有实现成果所需的正确技能,并更容易地传达你们的工作内容。也许,你会发现组织中还有一些未开发的领域。
1) 衡量指标与测量
不要低估公司中良好指标的价值。负责指标和测量这些指标的数据团队在评估业务表现方面发挥了关键作用。他们帮助提醒业务领导者可能需要调整的性能下降情况。他们帮助定义关键绩效指标,以便团队朝着正确的目标努力。他们创建计划和测量绩效的预测。本质上,他们将运营业务的复杂性简化为一些关键统计数据,以简化许多决策。这是一项非常有价值的工作。
此外,我还想强调的是,指标和测量还包括实验,这是许多数据科学家的常见职责。一个新功能是否对指标产生了提升?最新的营销活动是否增加了预订量?这些分析都属于这一类别。
所需的典型技能:商业智能、领域知识、实验、统计、SQL、数据可视化、仪表板管理、管道管理、有时是机器学*
示例:
-
定义新的业务指标
-
预测年度公司关键结果
-
分析产品实验的提升
-
创建客户终身价值数据集
2) AI/ML 产品或产品功能
数据科学的第二个常见领域是构建面向外部的产品功能。这些功能通常由机器学*提供支持,尽管这不是必需的。这项工作创建了一个服务/功能或改善了客户的用户体验。例如,一个推荐引擎在流媒体平台上提供电视节目建议。因此,交付物是一个可以嵌入产品或网站的工作(且通常一个关键要求是可扩展的)算法。
所需的典型技能:软件工程、机器学*、深度学*、API 开发
示例:
-
搜索
-
流媒体平台或电子商务网站上的推荐引擎
-
垃圾邮件检测对于电子邮件客户端
-
面部识别功能解锁手机
3) 战略洞察
这是可能是最模糊的领域,关于实际交付的内容,但战略洞察包括指导业务决策的研究或分析。它可以利用数据分析、统计建模、模拟等。它可以从简单到非常先进,但交付物通常是一个建议。业务应如何进行以及为什么?这些项目通常与业务利益相关者紧密合作。它们通常关注特定的临时问题,而不是需要定期做出的决策(见下一个类别)。
所需的典型技能:分析、统计推断、仪表板管理、模拟、概率、领域知识
示例:
-
推荐新分销中心的位置
-
使用聚类创建营销人物画像
-
模拟可能影响购买的杠杆
-
零基预算
4) 操作决策产品
最后,这一类别关注的是用于内部业务决策的工具。通过做出更好的和/或更快的决策,业务如何提高运营效率?例如,构建一个帮助营销部门竞标 Google 关键词的工具是一个可以通过运营决策产品解决的问题。
这一类别与战略洞察的区别在于,首先,决策产品应是生产级应用。其次,决策产品回答的是具体的、重复出现的决策问题,而不是一次性的业务问题。因此,决定在哪里开设新的配送中心可能是一个战略洞察问题,而自动将库存分配到数千个商店则是一个运营决策产品。
这一类别与 AI/ML 产品或产品功能不同。区别在于它如何创造价值,而不是这些工具是如何构建的。运营决策产品是通过更高效或更有效地运营来增加收入或降低成本。因此,我会将一个动态调整电子商务网站价格的定价工具归类为运营决策产品,而不是 AI/ML 产品功能。
典型所需技能:优化、运筹学、实验、软件工程
示例:
-
Google 广告竞价模型
-
邮件个性化
-
商店库存采购或分配决策
-
劳动力排班
那么你的工作属于哪一类?度量与测量、产品功能、战略洞察还是运营决策产品? 所有这些工作都可以为业务提供有价值的知识、工具、功能和方向。主要区别在于它们如何贡献价值。了解你工作的价值主张并向利益相关者阐明这一点至关重要。
此外,我希望领导者能够利用这些分类来明确数据科学如何支持业务。当领导层不知道如何使用数据科学时,往往会出现数据科学团队被未充分利用的情况。团队不被视为必要时,可能会发生裁员。但是,如果高管们清楚了解数据科学如何提供帮助,他们可能会突然意识到团队应该处理的项目清单。然后,就看你如何交付了。
有什么想法?请留下评论分享你的经验。
数据科学如何帮助福尔摩斯找到凶手
原文:
towardsdatascience.com/how-data-science-helped-sherlock-holmes-find-a-murderer-5cff72a5c98a
使用图论、约束编程和混合整数线性规划解决“谁杀了丹斯莫尔公爵”的谜团
·发表于 数据科学的前沿 ·14 分钟阅读·2023 年 4 月 28 日
--

图片由 弗拉基米尔·赫里申科 提供,来源于 Unsplash
库存管理、投资组合优化、机器调度、车辆路线以及许多其他现实生活中的问题都是数据科学和分析技术应用的绝佳例子。这些问题在大学里通常是我们最早学*的内容。然而,我对可以被表示为抽象实体的众多其他问题感到着迷,这些实体可以是图、计划或一组数学方程,并利用适当的技术加以解决。一旦你掌握了建模技能,将这些技术应用于供应链问题或简单的谜题都是很直接的。
在本文中,我将展示如何使用 图论、约束编程 和 混合整数线性规划 解决丹斯莫尔公爵谋杀案的谜团。
克劳德·贝尔热的《谁杀了丹斯莫尔公爵》是一部悬疑小说,专注于找出在怀特岛的丹斯莫尔城堡安放炸弹导致公爵死亡的人。尽管过去了十年,这个案件仍然未能破解,直到福尔摩斯和华生决定深入探讨此事。
当邓斯摩尔公爵因爆炸事件在怀特岛的城堡中遇难时,他的六位前妻——安、贝蒂、夏洛特、伊迪丝、费利西亚、乔治亚和海伦——成为了主要嫌疑人。问题进一步复杂化的是公爵的遗嘱在爆炸中被毁,据说其中对某位前妻的态度非常严厉。然而,揭示了所有六位女性在悲剧发生前都曾收到过城堡的邀请。
他所有的前妻都发誓她们只去过一次城堡。她们也不记得自己在那里待了多久,但知道在访问期间遇见了谁。
-
安遇见了贝蒂、夏洛特、费利西亚和乔治亚。
-
贝蒂遇见了安、夏洛特、伊迪丝、费利西亚和海伦。
-
夏洛特遇见了安、贝蒂和伊迪丝。
-
伊迪丝遇见了贝蒂、夏洛特和费利西亚。
-
费利西亚遇见了安、贝蒂、伊迪丝和海伦。
-
乔治亚遇见了安和海伦。
-
海伦遇见了贝蒂、费利西亚和乔治亚。
根据提供的信息,福尔摩斯和华生认为凶手多次访问了城堡,这与前妻们只访问过一次的说法相矛盾。侦探怀疑其中一位前妻在谎报她访问城堡的次数。确定哪位前妻对她们访问的频率撒了谎,是解开谜团的关键。
我将展示三种解决这个难题的方法:使用图论、约束编程和混合整数线性编程。
图论
这就是贝尔热最终解决谜团的方法,这似乎是最直接的办法。每个前妻在城堡的停留时间都是特定的,有明确的到达和离开时间。虽然我们不知道她们的确切停留时间以及到达和离开的顺序,但从审问中我们知道她们的某些访问时间是重叠的。
为了简化问题,我们集中关注海伦及其在城堡访问期间与贝蒂、费利西亚和乔治亚的互动。为了说明她们的访问,我们可以使用水平线,端点表示她们的到达和离开时间,如下所示:

时间段和重叠示例
如果两个时间段有重叠,这意味着对应的人在城堡里同时出现过,因此一定见过面。我们可以将时间段表示为图,其中每个节点对应一个人。如果两个时间段相交,我们就在相应的节点之间画一条边。对于海伦、贝蒂、费利西亚和乔治亚,最终得到的图如下所示:

时间段图示例
这种图称为时间段图,可以用来建模任何时间段集合。然而,并非所有图都可以表示为时间段。例如,考虑下图:

周期图 C4
它不能被分解为区间,因为无法找到四个区间,使得 A-B, B-C, C-D 和 D-A 相交。这个图称为 C4。如果一个图有 C4 作为子图,肯定不是一个区间图。
因此,如果图不是一个区间图,我们知道至少有一个人撒谎关于他们是否访问过城堡。我们可以利用这一点通过从审问中构建图并检查它是否为区间图来识别真正的罪犯。

基于审问的图。如果两个人见过面,它们之间就有一条边。
让我们在 Python 中使用库networkx并创建图:
people = ['Ann', 'Betty', 'Charlotte', 'Edith', 'Felicia', 'Georgia', 'Helen']
people_met = {'Ann': ['Betty', 'Charlotte', 'Felicia', 'Georgia'],
'Betty': ['Ann', 'Charlotte', 'Edith', 'Felicia', 'Helen'],
'Charlotte': ['Ann', 'Betty', 'Edith'],
'Edith': ['Betty', 'Charlotte', 'Felicia'],
'Felicia': ['Ann', 'Betty', 'Edith', 'Helen'],
'Georgia': ['Ann', 'Helen'],
'Helen': ['Betty', 'Felicia', 'Georgia']}
def create_graph(plot_graph=False):
"""
Generate graph based on interrogation, and draw it it plot_graph = True
:param plot_graph: True if graph is plotted
:return: Graph
"""
G = nx.Graph()
for person in people:
G.add_node(person)
for p1 in people_met.keys():
for p2 in people_met[p1]:
G.add_edge(p1, p2)
if plot_graph:
nx.draw_spectral(G, node_size=500, node_color=['red', 'green', 'pink', 'brown', 'yellow', 'orange', 'skyblue'],
with_labels=True)
plt.show()
return G
既然图已经定义好了,接下来需要进行以下两个步骤:
-
使用 networkx 的 simple_cycles 函数生成图的所有简单周期(见文档此处)。将生成所有长度的基本周期。
-
对于所有长度为 4 的周期,使用 subgraph 函数生成子图。并检查它是否不是chordal。换句话说,检查该周期是否为 C4 —— 一个长度为 4 的周期,其中所有节点的度数均为 2(见此处)。
def check_cycles(G):
"""
Print all the chordless cycles with length = 4
:param G: Input graph
"""
list_cycles = list(sorted(nx.simple_cycles(G.to_directed())))
for l in list_cycles:
if len(l) == 4:
H = G.subgraph(l)
if not nx.is_chordal(H):
print(l, len(l))
G = create_graph(False)
check_cycles(G)
C4 子图是:
-
安, 夏洛特, 伊迪斯, 费莉西亚
-
安, 费莉西亚, 伊迪斯, 夏洛特
-
乔治亚, 海伦, 贝蒂, 安
-
乔治亚, 安, 费莉西亚, 海伦
-
乔治亚, 海伦, 贝蒂, 安
-
乔治亚, 海伦, 费莉西亚, 安
安 出现在所有周期中,如果将她从集合中去除,可以完美地生成一个区间图,这表明她可能在撒谎,并可能被认为是凶手。
约束编程
约束编程(CP)是一种解决问题的方法,旨在通过关注寻找满足给定约束的可行解来解决组合和逻辑问题。CP 算法通过逐步减少可行解集来实现收敛。由于其处理复杂逻辑语句和约束的能力,CP 在解决当前问题时特别有效。
为了解决我们的问题,我们可以使用一个 CP 模型,其中 N 是人们的集合,s_i 是个体 i 到达城堡的时间,d_i 是他们访问的持续时间,e_i 是他们访问的结束时间。我们还可以引入一个逻辑变量 v_i,如果个体 i 不能被包含在访问计划中,则 v_i 为 TRUE,否则为 FALSE。此外,met_i 是个体 i 在访问期间遇到的人的集合。整数变量 s、d 和 e 的上限很高(1000),并且必须是非负的。
我们将使用 ortools库来解决这个问题:
from ortools.sat.python import cp_model
import pandas as pd
from matplotlib import pyplot as plt
people = ['Ann', 'Betty', 'Charlotte', 'Edith', 'Felicia', 'Georgia', 'Helen']
people_met = {'Ann': ['Betty', 'Charlotte', 'Felicia', 'Georgia'],
'Betty': ['Ann', 'Charlotte', 'Edith', 'Felicia', 'Helen'],
'Charlotte': ['Ann', 'Betty', 'Edith'],
'Edith': ['Betty', 'Charlotte', 'Felicia'],
'Felicia': ['Ann', 'Betty', 'Edith', 'Helen'],
'Georgia': ['Ann', 'Helen'],
'Helen': ['Betty', 'Felicia', 'Georgia']}
model = cp_model.CpModel()
num_vals = 1000
# Arrival time of person i in the castle
s = {i: model.NewIntVar(0, num_vals, 'start_' + i) for i in people}
# Duration of the visit of person i in the castle
e = {i: model.NewIntVar(0, num_vals, 'end_' + i) for i in people}
# Departure time of person i from the castle
d = {i: model.NewIntVar(0, num_vals, 'duration_' + i) for i in people}
# Logic variable that is True (1) is person cannot be included in the visit schedule. False otherwise
v = {i: model.NewBoolVar('v_' + i) for i in people}
初始步骤是确定两个个体是否重叠,这意味着验证区间 (s_i, e_i) 和 (s_j, e_j) 是否在任何时刻相交。这可以通过满足以下两个条件之一来实现:
-
s_j < e_i AND s_i < e_j
-
s_i < e_j AND s_i > s_j
还需要定义当两个区间不重叠时的情况。它应满足以下两个条件之一:
-
e_i ≤ s_j
-
s_i ≥ e_j
本质上,为了让两个人见面,它们的区间必须重叠,否则它们必须不重叠。然而,如果一个人不能被包含在计划中呢?这就是变量 v_i 发挥作用的地方。
最后,对于每一对人 i 和 j:

当两个个体 i 和 j 见面时,它们必须遵守以下两个重叠条件之一:(s_j < e_i AND s_i < e_j) 或者 (s_i < e_j AND s_i > s_j)。如果它们未能遵守其中一个条件,那么一个或两个个体将不会被包含在计划中 (v_i = True 或者 v_j = True)。
另一方面,如果两个个体 i 和 j 不见面,它们应遵循非重叠条件 (e_i ≤ s_j 或者 s_i ≥ e_j)。如果它们不遵守这个条件,一个或两个个体将不会被包含在计划中 (v_i = True 或者 v_j = True)。
为了表示这些条件,我们将使用布尔变量 b1、b2、b3 和 b4。
-
b1 = s_j < e_i AND s_i < e_j
-
b2 = s_i < e_j AND s_i > s_j
-
b3 = e_i ≤ s_j
-
b4 = s_i ≥ e_j
# Auxiliary Boolean variables
b1 = {(p, p2): model.NewBoolVar(name='b1_' + p + "_" + p2) for p in people for p2 in people if p != p2}
b2 = {(p, p2): model.NewBoolVar(name='b2_' + p + "_" + p2) for p in people for p2 in people if p != p2}
b3 = {(p, p2): model.NewBoolVar(name='b3_' + p + "_" + p2) for p in people for p2 in people if p != p2}
b4 = {(p, p2): model.NewBoolVar(name='b4_' + p + "_" + p2) for p in people for p2 in people if p != p2}
for i in people:
for j in people:
if i != j:
if j in people_met[i]:
# Overlap condition 1
model.Add(e[i] > s[j]).OnlyEnforceIf(b1[(i, j)])
model.Add(s[i] < e[j]).OnlyEnforceIf(b1[(i, j)])
# Overlap condition 2
model.Add(s[i] < e[j]).OnlyEnforceIf(b2[(i, j)])
model.Add(s[i] > s[j]).OnlyEnforceIf(b2[(i, j)])
# At least one of the overlap conditions
model.AddAtLeastOne([b1[(i, j)], b2[(i, j)], v[i], v[j]])
else:
# No overlap condition 1
model.Add(e[i] <= s[j]).OnlyEnforceIf(b3[(i, j)])
# No overlap condition 2
model.Add(s[i] >= e[j]).OnlyEnforceIf(b4[(i, j)])
# At least one of the non overlap conditions
model.AddAtLeastOne([b3[(i, j)], b4[(i, j)], v[i], v[j]])
如果一个人的访问持续时间 (d_i) 为 0,则 v_i 的值为 True。这意味着该人不被包含。如果 v_i 为 False,则 d_i 必须是严格正的。

# Duration of person (d_i) is 0 is value v_i is True. If v_i is False, d_i must be strictly positive.
for i in people:
model.Add(d[i] == 0).OnlyEnforceIf(v[i])
model.Add(d[i] > 0).OnlyEnforceIf(v[i].Not())
最后,最后一个约束确保每个人的到达时间加上持续时间等于结束时间

# Departure time is equal to arrival time plus duration
for i in people:
model.Add(s[i] + d[i] == e[i])
我们的目标函数是最大化包含的人员数量,这等同于最小化变量 v_i 的总和。

# Objective function is maximizing the number of people included - minimizing the sum of variable v_i.
var_obj = 0
for i in people:
var_obj += v[i]
model.Minimize(var_obj)Running the model we can plot the gantt chart generated:
通过解决模型并绘制甘特图,我们得到如下结果:
solver = cp_model.CpSolver()
status = solver.Solve(model)
if status == cp_model.OPTIMAL:
for p in people:
print(p, solver.Value(s[p]), solver.Value(e[p]), solver.Value(d[p]))
gantt_result_dict = {'Person': [], 'Start': [], 'End': [], 'Duration': []}
for i in people:
gantt_result_dict['Person'].append(i)
gantt_result_dict['Start'].append(solver.Value(s[i]))
gantt_result_dict['End'].append(solver.Value(e[i]))
gantt_result_dict['Duration'].append(solver.Value(d[i]))
gantt_result_df = pd.DataFrame.from_dict(gantt_result_dict)
print(gantt_result_df)
fig, ax = plt.subplots(1, figsize=(16, 6))
ax.barh(gantt_result_df['Person'], gantt_result_df['Duration'], left=gantt_result_df['Start'])
plt.grid()
plt.show()

运行 CP 模型生成的甘特图
谁是那个不能被包含且持续时间等于 0 的人?安!
混合整数线性规划
虽然这可能不是最简单的方法,但我最喜欢的解决这个难题的方法是使用 MILP。要了解更多关于如何建模和解决 MILP 的信息,可以参考我下面的文章:
我们仍然有集合 N,表示参与的人,以及 met_i,表示人员 i 遇到的人。在这种情况下,我们将采用时间索引方法,事先定义一组时间段 T(假设 T = 6)。变量如下:
-
y_i:二进制。若人员 i 被包含,则为 1。否则为 0
-
x_(i, t):二进制。若人员 i 在 t 在城堡中,则为 1。否则为 0
-
z_(i, j, t):二进制。若人员 i 和 j 在 t 同时在城堡中,则为 1。否则为 0
-
w_(i, t):二进制。若人员 i 在 t 到达城堡,则为 1。否则为 0
让我们还使用 ortools 来解决它:
import pandas as pd
from matplotlib import pyplot as plt
from ortools.linear_solver import pywraplp
people = ['Ann', 'Betty', 'Charlotte', 'Edith', 'Felicia', 'Georgia', 'Helen']
people_met = {'Ann': ['Betty', 'Charlotte', 'Felicia', 'Georgia'],
'Betty': ['Ann', 'Charlotte', 'Edith', 'Felicia', 'Helen'],
'Charlotte': ['Ann', 'Betty', 'Edith'],
'Edith': ['Betty', 'Charlotte', 'Felicia'],
'Felicia': ['Ann', 'Betty', 'Edith', 'Helen'],
'Georgia': ['Ann', 'Helen'],
'Helen': ['Betty', 'Felicia', 'Georgia']}
periods = 6
T = [p for p in range(0, periods)]
solver = pywraplp.Solver('puzzle', pywraplp.Solver.CBC_MIXED_INTEGER_PROGRAMMING)
# 1 if person i is included. 0 otherwise
y = {(i): solver.IntVar(0, 1, "y_{0}".format(i)) for i in people}
# 1 if person i is in the castle at t. 0 otherwise
x = {(i, t): solver.IntVar(0, 1, "x_{0}_{1}".format(i, t)) for i in people for t in T}
# 1 if person i and j are in the castle simultaneously at t. 0 otherwise
z = {(i, j, t): solver.IntVar(0, 1, "z_{0}_{1}_{2}".format(i, j, t))
for i in people for j in people for t in T if i != j}
# 1 if person i arrives in the castle at t. 0 otherwise
w = {(i, t): solver.IntVar(0, 1, "w_{0}_{1}".format(i, t)) for i in people for t in range(0, periods - 1)}
约束条件如下所示:

(1) 保证任何包含在计划中的个人必须至少在城堡中出现过一次。(2) 确保如果一个人被包含,他们必须与其他他们遇到的个体在同一时间在城堡中。(3) 和 (4) 确保两个个体仅当且仅当他们都被包含且在某时刻都在城堡中时才同时存在。(5) 确保未见过的两个人不能同时在城堡中。(6) 定义每个人的到达时间。(7) 不允许任何人在时间段 0 和 T(为了简单起见)期间在城堡中。最后,(8) 保证任何包含在计划中的人只有一个记录的到达时间。
# If a person is included she must have been in the castle at least once
c1 = {i: solver.Add(solver.Sum(x[(i, t)] for t in T) >= y[i]) for i in people}
# If a person is included she must have been in the castle simultaneously with other people she met at least once
c2 = {(i, j): solver.Add(solver.Sum(z[(i, j, t)] for t in T) >= y[i])
for i in people for j in people_met[i]}
# Two people were simultaneously at t in the castle only if they are both there at t
c3 = {(i, j, t): solver.Add((z[(i, j, t)] >= x[(i, t)] + x[(j, t)] - 1))
for i in people for j in people for t in T if i != j}
# Two people were simultaneously at t in the castle only if they are both there at t and if they are included
c4 = {(i, j, t): solver.Add(
(x[(i, t)] + x[(j, t)] + periods * (1 - z[(i, j, t)]) >= y[i] + y[j]))
for i in people for j in people for t in T if i != j}
# Two people that did not meet cannot be in the castle simultaneously
c5 = {(i, j): solver.Add(solver.Sum(z[(i, j, t)] for t in T) == 0)
for i in people for j in people if i != j and j not in people_met[i]}
# Defines the arrival time of person
c6 = {(i, t): solver.Add((w[(i, t)] >= x[(i, t + 1)] - x[(i, t)]))
for i in people for t in range(0, periods - 1)}
# Person cannot be in the castle in period 0 and period T
c7 = {(i): solver.Add(x[(i, 0)] + x[(i, periods - 1)] == 0)
for i in people}
# If a person was included it has just one arrival time
c8 = {(i): solver.Add(solver.Sum(w[(i, t)] for t in range(0, periods - 1)) == y[i])
for i in people}
目标函数是最大化包含的人数

number_of_people = solver.Sum(y[i] for i in people)
solver.Maximize(number_of_people)
解决模型并绘制甘特图,我们得到如下结果:
gantt_result_dict = {'Person': [], 'Start': [], 'End': [], 'Duration': []}
duration = {i: 0 for i in people}
start = {i: 0 for i in people}
end = {i: 0 for i in people}
for i in people:
for t in T:
if x[(i, t)].solution_value() >= 0.1:
duration[i] += 1
if x[(i, t - 1)].solution_value() == 0:
start[i] = t
end[i] = start[i] + duration[i]
gantt_result_dict['Person'].append(i)
gantt_result_dict['Start'].append(start[i])
gantt_result_dict['End'].append(end[i])
gantt_result_dict['Duration'].append(duration[i])
gantt_result_df = pd.DataFrame.from_dict(gantt_result_dict)
print(gantt_result_df)
fig, ax = plt.subplots(1, figsize=(16, 6))
ax.barh(gantt_result_df['Person'], gantt_result_df['Duration'], left=gantt_result_df['Start'])
plt.grid()
plt.show()

通过运行 MIP 模型生成的甘特图
再次没有包括在内的是谁?安!
这个例子演示了如何使用不同技术以各种方式处理和解决问题,但所有方法都产生相同的结果。你自己尝试解决问题时,可能会开发出替代的方法或模型。
尽管在考虑这个特定问题时,图论 似乎是最简单的方法,但由于它是一种特定的方法,因此不能普遍适用于所有问题。数学规划(MILP)与 约束编程 的差异可能是整篇文章或甚至一本书的主题。不过,主要的差异是:
-
数学编程 模型支持离散和连续决策变量,而 约束编程 仅支持离散决策变量(整数或布尔值)。
-
约束编程 支持逻辑约束以及算术表达式,包括模运算、整数除法、最小值、最大值和一个通过决策变量索引值数组的表达式。它还可以利用专门的约束,例如 全不同 和 无重叠 约束,这可以简化建模,而 数学编程 仅支持 ≥、≤ 和 = 约束。
参考文献
[1] B. Claude 和 W. Ian, 《谁杀了丹斯莫尔公爵》(1994),Biblioteque Oulipienne #67
除非另有说明,所有图片均为作者提供
数据科学家如何节省时间
·
关注 发布于 Towards Data Science · 发送为 通讯 · 3 分钟阅读 · 2023 年 6 月 1 日
--
据说我们中间有些人拥有所有需要的自由时间;如果你是其中四位幸运的个体之一,你可能正悠闲地在迷雾森林中冥想,而不是阅读这些文字。
我们中的其他人则与时间、效率以及平衡竞争需求的紧张关系有着更复杂的关系。几小时之间可能会影响到一个机器学*项目按时交付与错过截止日期的差别。它们可能决定你是否能和朋友共度一个下午、从零开始制作一个奶酪蛋糕、重温继承大结局,或者……无法做任何这些事情。
互联网充满了生产力技巧和节省时间的窍门;我们相信你可以自己找到它们。本周,我们提供的是务实的、动手的方式,以加速数据专业人员每天执行的工作流。从选择合适的工具到简化数据清理过程,通过学*我们的作者的经验,你有望节省宝贵的时间,所以我们希望你好好利用这些时间——或者不利用!这是 你的 时间。
-
分析地理空间数据可能是一个缓慢而复杂的过程。作为他 TDS 的首次亮相,Markus Hubrich 突出了 R-trees 在 显著提高空间搜索速度 方面的强大功能,并使用(令人愉快的)树映射示例来说明他的观点。
-
许多库和工具承诺改善 Python 著名的缓慢性能。Theophano Mitsa 对新*发布的 Pandas 2.0 进行基准测试,与 Polars 和 Dask 等竞争者进行比较,这样你可以在设计下一个项目时做出最明智的决定。
-
继续讨论加速以 Python 为中心的工作流,Isaac Harris-Holt 的最新教程展示了如何通过 将 Rust 嵌入到你的 Python 代码中 利用 Rust 的灵活性,借助 PyO3 和 maturin 等工具。(保持我们的主题,这也是一篇快速而简洁的文章。)

由 Annie Spratt 拍摄,图片来自 Unsplash
-
大型语言模型在执行复杂、细微的过程时效果如何?结论可能还未揭晓,但 它们在文本摘要方面已经显示出潜力— Andrea Valenzuela 的最新指南解释了如何使用它们快速且一致地生成高质量的摘要。
-
对于 Vicky Yu —— 我们猜测也包括许多人——数据清洗可能会很快变得乏味。为了顺利完成项目的这个阶段,Vicky 推荐创建用户定义函数(UDFs),这些函数可以简化你的 SQL 查询,避免在表中的多个列上重复编写相同的逻辑。
-
CPU 还是 GPU?如果你处理大量数据,你可能已经知道硬件配置的选择可能对你所需的时间和资源产生巨大的影响。 Robert Kwiatkowski 的有用入门指南涵盖了多个使用案例,并对两种处理器选项的优缺点进行了比较。
如果你还有几分钟空闲时间,我们希望你能花时间阅读我们其他一些*期的优秀文章:
-
机器学*模型是如何产生的,它们是如何随着时间变化的? Valeria Fonseca Diaz 探讨了模型的生命周期。
-
在他们的新深度分析中,Marco Tulio Ribeiro 和 Scott Lundberg 提倡使用与其他软件相同的原则来测试 LLM 构建的应用程序。
-
Nazlı Alagöz 最*分享了一篇关于学术界与行业数据科学实践之间惊人相似性的深刻反思。
-
如果你想要对逻辑回归的直观和易懂的介绍,不要错过 Igor Šegota 的初学者友好解释。
感谢你支持我们的作者!如果你喜欢在 TDS 上阅读的文章,可以考虑成为 Medium 会员 —— 这将解锁我们所有的档案(以及 Medium 上的其他所有文章)。
直到下一个 Variable,
TDS 编辑
从损失函数的角度来看,决策树如何分裂节点
原文:
towardsdatascience.com/how-decision-trees-split-nodes-from-loss-function-perspective-60a2f2124b4e
了解决策树如何通过分裂节点来最小化其损失函数
·发表于 Towards Data Science ·阅读时间 12 分钟 ·2023 年 5 月 15 日
--

图片由 Ernest Brillo 提供,来源于 Unsplash
在谈论决策树节点分裂时,我经常听到诸如“方差减少”和“信息增益最大化”的术语。我总是对这些术语感到恐惧。这些并不是我日常用词中熟悉的内容,直到我意识到这些术语是“最小化决策树损失函数”的同义词。
哪种损失函数?嗯,每个机器学*模型都需要一个损失函数。损失函数量化了模型预测与实际值之间的差异。另一种说法是模型预测中的误差。决策树模型也不例外。
对我来说,理解损失函数是理解机器学*模型的最重要一步。这是因为损失函数通过一个单一的数字编码了我们希望模型在各个方面表现良好的标准。就像我们用一个单一的数字来量化一个人的优秀程度;我记得那是某种长度的度量,对吧,眨眼,眨眼?
由于损失函数衡量的是树的预测与实际值之间的差异,为了理解损失函数,我们首先需要了解决策树是如何进行预测的。我们从回归树开始,然后再转到分类树。
回归树如何进行预测
回归决策树的预测形式是一个连续值。例如,一个回归树根据一个人的年龄和性别来预测其薪资。这里有一个小数据集:

这是一个用于回归树的示例数据集,以薪资作为目标变量,作者提供
我们希望我们的回归树预测目标变量薪资,它是一个连续数量,因此得名“回归树”,由两个特征组成,年龄是连续的,性别是分类的。对于薪资,我使用y₁到y₇而不是实际数字,以突出我们不应知道其他人的薪资。让sₒ={y₁, y₂, y₃, y₄, y₅, y₆, y₇}表示所有薪资数据条目。
回归树使用一些条件元组(特征,值,比较)将数据分成两部分。请注意,决策树总是进行二分拆分。
以下树使用拆分条件元组(年龄,41,<)将完整数据集s₀拆分成两个子集s1={y₁, y₃, y₅}和s2={y₂, y₄, y₆, y₇}。元组(年龄,41,<)表示树使用条件“年龄 < 41”将当前根节点(目前是完整数据集)中的所有数据点拆分成两个不重叠的子集。
我可视化了这个拆分,将子集s1放在左支,将s2放在右支。注意,拆分条件元组(年龄,41,<)仅为说明目的,可能并非示例数据集的最佳选择。

作者提供的回归树,拆分条件为年龄 < 41
回归树通过取平均值来进行预测
这个小的回归树通过一个拆分为一个数据点的薪资预测如下:
如果该人年龄小于 41 岁,他将进入左支,即子集s1;模型随后预测的数量是ŷₛ₁,即子集s1中薪资的平均值:

如果该人年龄大于或等于 41 岁,他将进入右支,即子集s2;模型预测的数量是ŷₛ₂,即子集s2中薪资的平均值:

换句话说,对于一个数据点,回归树的预测方法是:
-
首先让数据点通过树,直到到达叶节点
-
收集目标变量值,以便用于位于该叶节点的训练数据子集
-
使用这些目标值的平均值作为预测。
回归树的损失函数
由于回归树的预测是一个连续数量,最明显的损失函数选择是测量该预测与实际值之间误差的平方损失,或等效的均方误差(MSE)。由于有两个分支,我们分别定义它们的损失项。
对于子集s1:

对于子集s2:

其中 |s1| 表示子集 s1 的大小,即 3,|s2| 表示子集 s2 的大小,即 4。⅓和¼的标准化项将两个子集的大小考虑到损失函数中。因此,Lₛ₁ 和 Lₛ₂ 是 每数据点 的平均损失。
整棵树的总体损失是这两个损失项的总和:

Lₛ₁ 和 Lₛ₂ 每数据点损失的事实意味着我们不需要在总损失 L 前面加上系数来考虑 s1 和 s2 的大小差异。子集大小差异已由标准化常数⅓和¼考虑。
为决策树寻找分裂条件
为了从数据中学*模型,或者说,优化模型,我们改变模型参数,使得模型的预测误差尽可能小。换句话说,模型的预测应该最小化上述损失函数 L。
在回归树中,每次分支时,模型参数是分裂条件元组,如(年龄,40,<)。决定分裂树的元组的算法包括以下三个步骤:
1. 确定适合的分裂条件元组子集
观察数据中的特征,并在所有可能的分裂条件中找到候选分裂条件:
-
如果特征是连续的,比如年龄,则查看此特征的范围并决定一个短列表的分裂值。例如,数据中的年龄范围从 20 到 70,则短列表的分裂值可以是 30、40、50、60。这给出以下四个元组(年龄,30,<)、(年龄,40,<)、(年龄,50,<)和(年龄,60,<)。对于连续特征,可以有无限多个分裂值,算法只能使用启发式方法选择一个小子集。这些子集可能不是最优的,但这是模型精度和计算之间的折衷。
-
如果特征是类别型的,比如性别,则使用它的值作为分裂值。这给出两个元组(性别,男,=),(性别,女,=),对于这个二元特征,简化为一个元组(性别,男,=)。
所以我们总共有五个分裂条件元组。
2. 使用分裂条件元组对数据进行分区并评估损失
根据每个分裂条件元组对数据集进行分区,并评估损失函数 L。在这个例子中,我们有五个元组,因此我们根据每个元组将数据分为五次,评估五次损失函数,每次基于数据的不同分区。
3. 选择最小损失的分裂条件元组
现在,算法选择能产生最小损失的分裂条件元组,并将这个条件下的两个分区数据子集放入由分裂创建的两个叶节点中。
然后它将每个叶节点视为新树的根,并对每棵新树应用相同的三个步骤进行进一步分裂。
量化模型预测的改进
这就是回归树如何拆分节点——它拆分节点以最小化其损失函数,即均方损失。拆分后,模型可以在预测产生更小误差的意义上进行更好的预测。模型之所以能做到这一点,是因为它从数据集中提取了信息。为了量化拆分前后提取的信息量,请计算整个数据集 s₀ 上的损失,公式为

然后对两者进行差分:

方差减少
你有时会在回归树拆分的讨论中听到“方差减少”这个短语。这是因为如果我们查看树的预测和相应的损失,如下所示,仅为左分支:
预测:

损失:

你会发现,对于一系列数字 y₁, y₃, y₅,回归树的预测 ŷₛ₁ 是这些数字的均值,通常记作 ȳ。均方误差 Lₛ₁ 具有与计算该数字列表的方差相同的结构:

在我们的例子中,n=3。因此,上述拆分算法通过最小化损失,或者说,减少方差来实现目标。这就是为什么我们说回归树通过减少方差来拆分节点。
现在转到分类树,它以分类值的形式进行预测,而不是连续值。你知道怎么做的。首先我们展示分类树如何进行预测,然后定义损失函数,最后看看上述拆分算法如何像应用于回归树一样应用于分类树。
分类树如何进行预测
首先通过交换之前数据集中性别和薪资列来创建一个分类任务的数据集:

用于分类树的示例数据集,目标变量为性别,作者
在这里,目标变量是性别(Gender),它是一个分类变量,因此我们需要一个分类树。两个特征是年龄(Age)和薪资(Salary)。
假设我们有以下分类树:

拥有拆分条件 Age<41 的分类树,作者
这个树需要将一个数据点分类到女性类别(Female)或男性类别(Male)。假设这个树仍然使用条件 Age < 41 来将数据集 s₀ 拆分成 s1={y₁F, y₃F, y₅M} 和 s2={y₂M, y₄F, y₆M, y₇F}。我在每个数据点旁边附上性别符号“F”或“M”,例如“y1F”,以使每个数据点的性别在达到叶节点时更加清晰。
所以在分裂后,到达左叶节点的⅔数据点是女性,用P(F)=⅔表示,这说明该节点中女性数据点的发生概率是⅔。类似地,对于男性,它是P(M)=⅓。
分类树通过多数投票来做出预测
我们需要一种不同于平均的方法(如回归树中的情况),因为⅔的女性加上⅓的男性不是一个实际的情况,尽管*年来 LGBT 运动积极推进。
更好的选择是多数投票——预测发生概率最高的类别。对于左节点,由于女性数据点多于男性数据点,模型将预测女性类别。
那右侧叶节点呢,其中女性和男性数据点的概率相等,都是½?在这种情况下,当两个类别的概率相等时,模型可以随机打破平局,例如预测男性类别。实际上,分裂算法会防止平局情况的发生,正如我们稍后将看到的那样。
分类树的损失函数
让我们来处理量化树的错误的损失函数。直观地说,在一个叶节点中,如果主要类别的发生概率更大,则模型对该节点的预测更好:
-
我们的树对左节点预测为女性,这一预测在⅔的情况下是正确的,而在⅓的情况下是错误的。换句话说,如果一个节点中不同类别的分布不够均匀,预测会更好。
-
它对右节点预测为男性,这一预测仅在½的情况下是正确的。换句话说,如果节点中不同类别的分布更均匀,预测效果会更差。
所以我们需要一个衡量数据点类别标签均匀性的指标。存在许多度量方法。常见的选择是熵:

其中求和是对所有类别c,在我们的案例中是女性和男性。P(c) 是类别c的发生概率,而log(P(c)) 是该发生概率的对数。
请注意,熵是衡量均匀性的一个指标;还有其他常见的衡量方法,如卡方和基尼 impurity。回想一下,在经济学中,基尼指数用于量化社会中财富在不同人群之间分配的不均等性——它是一个不均匀性的度量。
如果你想知道为什么熵可以衡量均匀性,请参阅其他资源。在本文中,只需知道熵可以衡量均匀性就足够了。为了好玩,我们可以插入一些数字来看看熵是否告诉我们左分支比右分支更均匀。
对于左侧叶节点:

对于右侧叶节点:

我们可以看到,左叶节点的损失Hₛ₁=0.637确实小于右叶节点的损失Hₛ₂=0.693,这表明模型对左节点的数据点的预测更好。
现在,就像回归树的情况一样,我们需要将Hₛ₁和Hₛ₂结合起来,以表示整个树的总体损失。
在回归树的情况下,我们将Lₛ₁和Lₛ₂相加,因为Lₛ₁和Lₛ₂代表每个数据点的损失,并且不同子集的大小s1和s2经过标准化。但是在分类树的情况下,上述基于熵的损失项并非如此。如果你查看Hₛ₁和Hₛ₂的定义,子集 s1 和 s2 的大小并没有提到。
从直观上看,当模型对大量数据子集的预测产生错误时,即使这个错误很小,对整体模型性能指标的影响也可能很大,因为数据子集的规模很大。
为了考虑数据子集的大小,我们将总损失 H 定义为加权和,权重根据数据子集的比例确定:

寻找分类树的分裂条件
使用为分类树定义的损失函数H,我们可以应用用于回归树的相同算法来分裂节点。我们只需要使用分类的新损失H,而不是回归的损失L。
这很有意义,因为加权熵H表示我们分类树的错误——越均匀,错误越大;越不均匀,错误越小。上述分裂算法会选择最小化这种错误的分裂条件,从而鼓励每个叶节点的数据点变得更少均匀。因此,树可以减少错误。同时,通过在分裂叶节点中鼓励较少均匀的数据子集,不太可能出现两个类别之间的平局。
太神奇了!
量化模型的预测改进
就像回归树的情况一样,你可以通过计算分裂前的根节点Hₛ₀上的损失来衡量数据在分裂前后的信息提取,然后计算H-Hₛ₀。
信息增益
在结束之前,我想提到信息增益,因为你会在讨论分类树时到处看到它。

你可以看到信息增益实际上是熵的负值。因此,我们可以等效地找到一个最大化信息增益的分裂条件,而不是最小化熵损失的分裂条件。信息增益公式中的常数 1 在分裂算法中没有影响。如果算法找到一个最大化-熵的分裂条件,那么这个条件也会最大化信息增益,即1-熵。
结论
本文解释了决策树,包括回归树和分类树,从损失函数的角度如何进行节点分裂。这种视角使得如方差减少、信息增益等概念变得更加直观易懂。
我们如何知道一篇文本是 AI 生成的?
原文:
towardsdatascience.com/how-do-we-know-if-a-text-is-ai-generated-82e710ea7b51
检测 AI 生成文本的不同统计方法。
·发表在Towards Data Science ·阅读时间 6 分钟·2023 年 5 月 26 日
--

图片来源:Andreas Fickl 在Unsplash
在人工智能这一迷人且迅速发展的领域中,最令人兴奋的进展之一就是 AI 文本生成的出现。像GPT-3、Bloom、BERT、AlexaTM以及其他大型语言模型,能够生成极其类似人类的文本。这既令人兴奋又让人担忧。这些技术进步使我们能够以之前无法实现的方式进行创造,但同时也打开了欺骗的大门。随着这些模型的不断改进,区分人类编写的文本和 AI 生成的文本将变得越来越具有挑战性。
自ChatGPT发布以来,全球范围内的人们一直在测试这些 AI 模型的极限,利用它们获取知识,同时也有一些学生用它们来完成作业和考试,这对这种技术的伦理影响提出了挑战。尤其是当这些模型变得足够复杂以模仿人类写作风格并在多个段落中保持上下文时,即便是微小的错误也需要修正。
这引发了一个重要的问题,这是我经常被朋友和家人问到的问题(自从 ChatGPT 发布以来,我被问到过很多次……)
我们怎么知道一篇文本是人类编写的还是 AI 生成的?
准确的评估是性能改进的唯一途径
towardsdatascience.com
这个问题在研究界并不新鲜;检测 AI 生成的文本,我们称之为“深度伪造文本检测”。今天,你可以使用不同的工具来检测文本是由人类撰写还是 AI 生成的,例如 OpenAI 的 GPT-2。那么这些工具是如何工作的呢?
当前有不同的方法用于检测 AI 生成的文本;随着用于生成这些文本的模型变得更加先进,新的技术也在被研究和实施,以检测这种文本。
本文将探讨 5 种不同的统计方法,用于检测 AI 生成的文本。
让我们直接进入主题……
1. N-gram 分析:
N-gram 是从给定文本样本中提取的 N 个单词或标记的序列。N-gram 中的“N”代表序列中的单词数。例如:
-
纽约(2-gram)。
-
三剑客(3-gram)。
-
这个小组定期会面(4-gram)。
分析文本中不同 N-gram 的频率可以确定模式。例如,在我们刚刚经历的三个 N-gram 示例中,第一个是最常见的,第三个是最少见的。通过跟踪不同的 N-gram,我们可以决定它们在 AI 生成的文本中比在人工撰写的文本中更常见或更少见。例如,AI 可能会比人类作者更频繁地使用特定的短语或词汇组合。通过训练我们的模型来处理人类和 AI 生成的数据,我们可以找到 AI 和人类使用的 N-gram 频率之间的关系。
2. 困惑度:
如果你在英文词典中查找“perplexed”这个词,它会被定义为惊讶或震惊。然而,在 AI 和自然语言处理(NLP)的背景下,特别是,perplexity(困惑度)用来衡量语言模型预测文本的信心程度。估计模型的困惑度是通过量化模型对新文本的响应时间,换句话说,就是模型对新文本的“惊讶”程度。例如,AI 生成的文本可能会降低模型的困惑度;模型对文本的预测越准确,困惑度就越低。困惑度计算速度快,这使得它在其他方法中具有优势。
3. 突发性:
在自然语言处理(NLP)中,Slava Katz 将 burstiness(突发性)定义为在文档或一组文档中某些词汇出现“突发”的现象。这个概念是,当一个词在文档中出现一次时,它很可能会在同一文档中再次出现。AI 生成的文本表现出不同于人类写作的突发性模式,因为它们没有选择其他同义词所需的认知过程。
利用这些工具加快数据标注速度并获得更好的结果。
towardsdatascience.com
4. 文体分析:
文体分析是对语言风格的研究,可以用来识别作者,或者在这种情况下,识别文本的来源(人类与 AI)。每个人使用语言的方式不同,有些人喜欢短句,而有些人则喜欢长且连贯的句子。人们使用分号和破折号(以及其他独特的标点符号)的方式也各不相同。此外,有些人更频繁使用被动语态而不是主动语态,或使用更复杂的词汇。AI 生成的文本可能会展现不同的风格特征,即使是关于同一主题的内容也可能有所不同。而且由于 AI 没有自身的风格,这些不同的风格可以用来检测是否由 AI 编写文本。
5. 一致性和连贯性分析:
延续文体分析,由于 AI 模型没有自己的风格,它们生成的文本有时需要更多的一致性和长期的连贯性。例如,AI 可能会自我矛盾或在文本中途突然改变话题和风格,从而导致思想流动更难以跟随。
我们可以使用 Python 实现一个非常简化的版本,以理解每种方法的工作原理。
import nltk
from collections import Counter
from sklearn.feature_extraction.text import CountVectorizer
from textblob import TextBlob
import numpy as np
text = "This is some sample text to detect. This is a text written by a human? or is it?!"
# 1\. N-gram Analysis
def ngram_analysis(text, n=2):
n_grams = nltk.ngrams(text.split(), n)
freq_dist = nltk.FreqDist(n_grams)
print(freq_dist)
ngram_analysis(text)
# 2\. Perplexity
# Perplexity calculation typically requires a language model
# This is just an example of how perplexity might be calculated given a probability distribution
def perplexity(text, model=None):
# This is a placeholder. In practice, we would use the model to get the probability of each word
prob_dist = nltk.FreqDist(text.split())
entropy = -1 * sum([p * np.log(p) for p in prob_dist.values()])
return np.exp(entropy)
print(perplexity(text))
# 3\. Burstiness
def burstiness(text):
word_counts = Counter(text.split())
burstiness = len(word_counts) / np.std(list(word_counts.values()))
return burstiness
print(burstiness(text))
# 4\. Stylometry
def stylometry(text):
blob = TextBlob(text)
avg_sentence_length = sum(len(sentence.words) for sentence in blob.sentences) / len(blob.sentences)
passive_voice = text.lower().count('was') + text.lower().count('were')
vocabulary_richness = len(set(text.split())) / len(text.split())
return avg_sentence_length, passive_voice, vocabulary_richness
print(stylometry(text))
# 5\. Consistency and Coherence Analysis
# Again, this is a simple way to explain the idea of calculating the consistency of a text. In reality, more complex algorithms are used.
def consistency(text):
sentences = text.split(".")
topics = [sentence.split()[0] for sentence in sentences if sentence]
topic_changes = len(set(topics))
return topic_changes
print(consistency(text))
如果你想深入探讨如何实施这些方法,请查看这些教程:
结论
随着我们向人工智能领域的深入,我们将需要更先进和复杂的工具来检测 AI 生成的文本,以避免误信息和欺骗。虽然这是一个非常活跃的研究领域,但研究人员已经开发了检测 AI 写作文本的工具,举例来说,爱德华·田来自普林斯顿大学完成了这样的工作。田开发了一种名为GPTZero的实验工具,该工具使用“困惑度”和“突发性”来估计 AI 生成内容的可能性。另一个例子是诺亚·史密斯,他是华盛顿大学的教授和自然语言处理研究员,他的研究集中在人工写作文本的独特质量和意图上。AI 通常生成的文本缺乏意图和一致性,这可能会随着语言模型的改进而改变。本文中探讨的任何方法都不是万无一失的;通常需要结合不同的技术和广泛的训练集来构建现实生活中的 AI 生成文本分类器。
科技始终是我们生活中令人兴奋的补充,能够提升我们的生活质量。然而,它也可能给我们带来新的挑战和障碍。然而,这些障碍从未成为放弃使用新技术的理由,而是一个发展协议和规则的机会,让我们能够以伦理的方式利用这些伟大的技术。
决策树如何知道从数据中询问下一个最佳问题?
从零开始构建你自己的决策树分类器(使用 Python),并理解它如何使用熵来分裂节点
·发布于Towards Data Science ·14 分钟阅读·2023 年 11 月 17 日
--

图片由Daniele Levis Pelusi拍摄,来自Unsplash
介绍
决策树是多用途的机器学*算法,能够执行分类和回归任务。它们通过询问有关数据的特征的问题,使用 IF-ELSE 结构来跟随路径,最终得出预测结果。挑战在于确定在每一步决策过程中需要询问什么问题,这也等同于确定在每个决策节点上最佳分裂的方式。
在这篇文章中,我们将尝试构建一个用于简单二分类任务的决策树。本文的目的是理解在每个节点上如何使用不纯度度量(例如熵)来确定最佳分裂,最终构建一个基于规则的树状结构以获得最终预测结果。
为了获得关于熵和基尼不纯度(用于测量随机性并确定决策树中分裂质量的另一种度量)的直觉,可以快速查看这篇文章。
问题定义和数据
问题: 根据鱼的长度和重量预测它是金枪鱼还是鲑鱼。
挑战在于根据鱼的重量和长度预测鱼的类型(目标变量)。这是一个二分类任务的示例,因为我们的目标变量类型有两个可能的值,即tuna和salmon。
你可以从这里下载数据集。
强烈建议你在阅读本文时进行编码,以获得最大理解 😃
代码跟随前提条件
让我们确保你有一切准备好开始(我敢打赌你已经准备好了,但以防万一)。
-
任何允许你使用 Python (.ipynb 扩展名) 笔记本的代码编辑器,例如 Visual Studio Code、Jupyter Notebook 和 Google Colab 等等。
-
库: pandas 和 numpy 用于数据处理;plotly 用于可视化。(如果你愿意,可以使用任何其他的数据可视化库。)
-
数据,当然。
这就是我们需要的一切,很可能你已经准备好了。现在让我们开始编码吧!
一步步解决方案
创建一个新的 .ipynb 文件并首先导入库。
import pandas as pd
import numpy as np
import plotly.graph_objects as go
将数据读入 pandas 数据框。
# read the csv file
df = pd.read_csv("fish.csv")
# how many rows and columns?
print(df.shape)
# print column names
print(df.columns)
# print class distribution
print(df["type"].value_counts())
单元输出:

我们的数据框中有 1000 行和 3 列。‘length’ 和 ‘weight’ 是特征,‘type’ 是目标变量。由于‘type’ 列有值——‘tuna’ 和 ‘salmon’,让我们对它们进行标签编码。
对目标列进行标签编码:
df["type"] = df["type"].apply(lambda x: 1 if x=="tuna" else 0)
我们将类别标记为:{salmon: 0 和 tuna: 1}
现在,让我们绘制一个散点图来可视化我们的类别。
# create a Figure
fig = go.Figure()
# specify custom colors for the plot
color_map = {
0: "red",
1: "blue",
}
# apply the color map to 'type' column
colors = df["type"].map(color_map)
# add a scatter trace to the figure
fig.add_trace(go.Scatter(x=df["length"],
y=df["weight"],
mode="markers",
marker=dict(color=colors, size=8)))
# add x-label, y-label and title
fig.update_layout(
width=800,
height=600,
title_text="Scatter Plot of Data",
xaxis=dict(title="length",
tickvals=[i for i in range(10)]),
yaxis=dict(title="weight",
tickvals=[i for i in range(10)])
)
单元输出:

图片由作者提供
我们现在可以清楚地看到用红色和蓝色标记的两个类别。这一切都是关于数据准备的,让我们进入决策树的部分。将特征列名称分配给一个列表,我们稍后会使用到。
features = ["length", "weight"]
找到我们的第一个划分
在决策树中的划分指的是将数据分为两个(或更多)分区的 (特征, 值) 对。
在数值特征的情况下,划分将导致数据的两个分区——一个是特征 ≤ 值,另一个是特征 > 值。
在分类特征的情况下,划分将导致数据的两个分区——一个是特征=值,另一个是特征不等于值。
# Finding the first split:
# initialize best_params which is a dictionary that will keep track
# of best feature and split value at each node.
best_params = {"feature": None, "impurity": np.inf, "split_value": None}
# for each feature in features, do the following:
### for val in all feature values
### (starting from the min possible value of the feature until max possible value,
### incrementing by 'step_size'), check the following:
###### if impurity at this feature val is less than previously recorded impurity,
###### then update best_params
# Following is the code for above interpretation
for feature in features:
curr_val = df[feature].min()
step_size = 0.1
while curr_val <= df[feature].max():
curr_feature_split_impurity = compute_impurity(df, feature, curr_val)
if curr_feature_split_impurity < best_params["impurity"]:
best_params["impurity"] = curr_feature_split_impurity
best_params["feature"] = feature
best_params["split_value"] = curr_val
curr_val += step_size
运行此单元将产生错误,因为我们还没有定义函数 compute_impurity。我们需要这个函数来比较在划分之前和之后数据的杂质。结果在划分后杂质最低的 (特征, 值) 对将被选为当前节点的最佳划分,我们将相应地更新best_params。
定义函数如下:
def compute_impurity(df, feature, val, criterion):
"""
Inputs:
df: dataframe before splitting
feature: colname to test for best split
val: value of 'feature' to test for best split
Output: float
Returns the entropy after split
"""
# Make the split at (feature, val)
left = df[df[feature]<=val]["type"]
right = df[df[feature]>val]["type"]
# calculate impurity of both partitions
if criterion=="entropy":
left_impurity = compute_entropy(left)
right_impurity = compute_entropy(right)
else:
left_impurity = compute_gini(left)
right_impurity = compute_gini(right)
# return weighted entropy
n = len(df) # total number of data points
left_n = len(left) # number of data points in left partition
right_n = len(right) # number of data points in right partition
return (left_n/n)*left_impurity + (right_n/n)*right_impurity
此函数使用另一个辅助函数 compute_entropy,它为给定类别列表提供熵。让我们也定义这个函数:
def compute_entropy(vals):
"""
Input:
vals: list of 0s and 1s corresponding to two classes
Output:
entropy: float"""
# probability of class labeled as 1
# will be equal to the average of vals
p1 = np.mean(vals)
p0 = (1-p1)
# it means data is homoegeneous
# entropy is 0 in this case
if p1==0 or p0==0:
return 0
return - (p0*np.log2(p0) + p1*np.log2(p1)) # formula of entropy for two classes
现在我们已经定义了两个辅助函数,再次运行之前导致错误的单元格,现在应该可以成功运行了。打印 best_params 字典以检查它是否已更新。
print(best_params)
单元格输出:

完美!我们在 length = 3 处得到了第一个最佳分割,这意味着我们现在将数据分为两个分区——data['length']<=3 和 data['length']>3。
注意: 在这里,我们选择结果熵最小的分割。决策树可以在此分割标准上有所不同,例如, ID3 使用信息增益 (即分割前数据的熵与分割后分支熵加权和的差值),而 CART 使用基尼指数 作为它们各自的分割标准。
下面是我们当前决策树的样子。由于左侧分支的数据只包含一个类别,我们将其设为叶节点;因此,任何 length<=3 的数据点都将被预测为 金枪鱼。
(提醒: 根据我们的 color_map,我们将蓝色分配给金枪鱼,将红色分配给鲑鱼):
color_map = {
0: "red", # salmon
1: "blue", # tuna
}
对于右侧分支的数据,我们可以递归地遵循相同的过程,找到最佳分割,并对后续分支重复此过程,直到达到最大深度或不再可能进行进一步分割。

在第一次分割后可视化决策树(图像作者提供)
这个过程会对每个分区重复,直到满足停止条件(例如,树的最大深度达到,或者叶节点中的样本数量低于阈值等)。这就是当我们使用如 scikit-learn 等包中的分类器或回归器时,超参数 允许我们定义的内容。
使用递归来泛化代码
我们将把上述代码封装到一个函数 build_tree 中,该函数可以递归调用来构建决策树。
辅助函数:
compute_entropy: 返回具有两个类别的数据集的熵。上述定义。
compute_gini: 返回具有两个类别的数据集的基尼指数。我们可以选择使用熵或基尼指数作为我们的 impurity 衡量标准。
def compute_gini(vals):
"""
Input: vals is a list of 0s and 1s
Output:
gini: float
"""
# probability of '1' will be equal to the average of vals
p1 = np.mean(vals)
p0 = (1-p1) # since there are just two classes and p0+p1 = 1
if p1==0 or p0==0:
return 0
return 1 - p1**2 - p0**2
compute_impurity: 是上述 compute_impurity 函数的扩展,它返回数据集的 impurity。它使用 compute_entropy 和 compute_gini 函数根据指定的标准计算给定分割点的熵和基尼指数。
def compute_impurity(df, feature, val, criterion):
"""
Inputs:
df: dataframe before splitting
feature: colname to test for best split
val: value of 'feature' to test for best split
Output: float
Returns the entropy after split
"""
# Make the split at (feature, val)
left = df[df[feature]<=val]["type"]
right = df[df[feature]>val]["type"]
# calculate impurity of both partitions
if criterion=="entropy":
left_impurity = compute_entropy(left)
right_impurity = compute_entropy(right)
else:
left_impurity = compute_gini(left)
right_impurity = compute_gini(right)
# return weighted entropy
n = len(df) # total number of data points
left_n = len(left) # number of data points in left partition
right_n = len(right) # number of data points in right partition
return (left_n/n)*left_impurity + (right_n/n)*right_impurity
get_best_params: 返回包含当前节点分割所用特征和值的 best_params 字典。
def get_best_params(df, features, criterion):
"""
A function to determine the best split at a node
Input:
df: dataframe before split
features: list of features
criterion: impurity measure to use (gini or entropy)
Output:
best_params: dict
"""
# Initialize best_params
best_params = {"feature": None, "val": None, "impurity": np.inf}
# iterate for all features
for feature in features:
curr_val = df[feature].min()
step_size = 0.1
# iterate for all values for a feature (according to step_size)
while curr_val<=df[feature].max():
# calculate impurity (gini or entropy) for the current value of feature
impurity = compute_impurity(df, feature, curr_val, criterion)
# update best_params if impurity is less than previous impurity
if impurity <= best_params["impurity"]:
best_params["feature"] = feature
best_params["val"] = curr_val
best_params["impurity"] = impurity
curr_val += step_size
best_params["val"] = np.round(best_params["val"], 2)
best_params["impurity"] = np.round(best_params["impurity"], 2)
return best_params
主函数
build_tree: 这是主要的驱动函数,它利用辅助函数递归地为给定的数据构建决策树。我还添加了一些额外的语句,试图在创建过程中以可解释的格式打印决策树。
def build_tree(data, features, curr_depth=0, max_depth=3, criterion="entropy"):
"""A function to buil the decision tree recursively.
Input:
data: dataframe with columns length, weight, type
features: ['length', 'weight']
curr_depth: Keep track of depth at current node
max_depth: Decision tree will stop growing if max_depth reached
criterion: "gini" or "entropy"
"""
# Base case: max depth reached
if curr_depth >= max_depth:
classes, counts = np.unique(data['type'].values, return_counts=True)
predicted_class = classes[np.argmax(counts)]
print(("--" * curr_depth) + f"Predict: {predicted_class}")
return
# Get the best feature and value to split the data
best_params = get_best_params(data, features, criterion)
# Base case: pure node, single class case
if best_params["impurity"] == 0:
predicted_class = data['type'].iloc[0]
print(("--" * curr_depth) + f"Predict: {predicted_class}")
return
# If there's no feature that can improve the purity (not possible to split)
if best_params["feature"] is None:
predicted_class = data['type'].iloc[0]
print(("--" * curr_depth) + f"Predict: {predicted_class}")
return
# Print the current question (decision rule)
best_feature = best_params["feature"]
best_split_val = best_params["val"]
question = f"Is {best_feature} <= {best_split_val}?"
print(("--" * (curr_depth*2)) + ">" + f"{question}")
# Split the dataset
left_df = data[data[best_feature] <= best_split_val]
right_df = data[data[best_feature] > best_split_val]
# Recursive calls for left and right subtrees
if not left_df.empty:
print(("--" * curr_depth) + f"Yes ->")
build_tree(left_df, features, curr_depth + 1, max_depth, criterion)
if not right_df.empty:
print(("--" * curr_depth) + f"No ->")
build_tree(right_df, features, curr_depth + 1, max_depth, criterion)
现在,我们可以传递我们的鱼数据集并测试代码的输出,如下所示。
build_tree(df, ["length", "weight"], max_depth=4, criterion="entropy")
单元格输出:

以下是我们最终决策树的样子:

图片来源:作者
这对应于以下决策边界:

图片来源:作者
注意: 如果叶节点不是纯净的,即叶节点分区中有多个类别,会发生什么?只需选择多数类别。
代码链接
你可以从这里获取最终的代码笔记本。
主要收获
如果你已经读到这里,你会对许多与决策树相关的重要问题有更清晰的认识,比如可解释性作为优点,以及过拟合作为主要缺点——你可能在之前接触决策树时已经遇到过。我们不能在这里忽略这些话题,所以简单提及一下。
让我们先来看看优点。还有更多,但我们只列出最重要的几个。
决策树的(主要)优点是什么?
- 可解释性: 决策树的预测相较于其他机器学*模型更易于解释,因为我们可以直观地查看达到最终预测所遵循的路径。
决策树直观且易于解释,即使对非技术人员也是如此。
例如,假设一家银行使用决策树预测是否应根据客户的属性(如收入、银行余额、年龄、职业等)向客户授予贷款。如果分类系统建议银行不应向客户提供贷款,那么银行需要制定适当的回应,说明拒绝的理由。否则,这可能会损害他们的业务和声誉。
- 无需重度预处理: 与其他一些机器学*模型不同,决策树不要求数据进行归一化(或标准化)。
你只需进行最少的数据预处理,决策树也不会太在意。
此外,它可以自然地处理分类特征,我们不需要担心独热编码(或其他解决方案),因为不同的类别在分裂时会被视为不同的分支。
主要缺点——> 过拟合
过拟合是指我们的模型好得不真实,即模型对训练数据的适应过于紧密,以至于丧失了泛化能力,无法在面对测试数据时表现出类似的准确度。
决策树如果控制不当,非常容易过拟合。 注意在我们上述的例子中,如果我们不断地分裂训练数据而不设定任何限制,那么决策树将不断创建更多的决策边界,学*训练数据中的噪声。
为了保持模型的泛化能力,采取措施避免过拟合是非常重要的。在决策树的情况下,我们可以采取以下防止过拟合的步骤:
- 预剪枝: 剪枝指的是防止决策树达到其最大容量。可以通过以下方式主动进行:
-
设置max_depth: 不允许树的深度超过预定义的深度。
-
设置min_samples_split: 如果决策节点处的样本数量低于此值,则不允许分裂发生。
-
设置min_samples_leaf: 如果任何结果叶节点处的样本数量低于此值,则不允许分裂发生。
这些(以及许多其他的)是我们在通过诸如 scikit-learn 这样的包实现决策树时可以根据需要调整的超参数。你可以在这个文档中找到所有超参数及其定义。
2. 后剪枝: 指的是让决策树在其最大容量下生长,然后丢弃一些看起来不必要或导致高方差的部分/分支。
3. 另一种可能的解决方案是:不要使用决策树! 而是选择它们的高级版本——比如随机森林或梯度提升树 😃 这仍然需要你对前者有基本了解,因此阅读这篇文章绝对不会浪费时间!
奖励积分
还有一些决策树的其他特性值得注意:
-
非参数:决策树是非参数机器学*模型,这意味着它们对训练数据的分布、特征的独立性等不做假设。
-
贪心算法:决策树遵循贪心算法,这意味着它们选择在给定节点处认为最好的分裂(即,局部最优解),且无法回溯,从而可能导致次优解。
结论
在这篇文章中,我们学*了如何在没有使用任何包的情况下为二分类任务构建决策树,以在概念层面上打下坚实的基础。我们通过逐步过程理解了如何使用数据不纯度度量(如熵)在每个节点生成决策规则,然后在 Python 中实现了递归算法以输出最终的决策树。这篇文章的目的是通过深入了解决策树的基本原理。
实际上,当处理现实世界的数据和面临挑战时,我们通常不需要从零开始做这些工作,因为有许多现成的包可以使事情变得更方便和稳健。但拥有扎实的基础会帮助我们更好地利用这些包,同时在使用时也会更有信心。
希望下次当我们构建下一个决策树或随机森林(这是一组决策树的集成)时,我们能在配置模型时更加周到(并且更容易理解超参数的真正含义 😺)。
希望这些内容对你有所帮助。欢迎提供任何反馈或建议。
我想感谢 Ritvik Kharkar 的精彩 YouTube 视频,帮助我更好地理解了决策树的概念。我从他的录像中获得了灵感,写了这篇文章,使用了他用的相同例子,并通过添加递归实现和打印决策树的逻辑将解决方案推进了一步。他的视频链接在下面的参考文献中。
相关阅读
- 想要理解 impurity measures 背后的直觉? 请查看这篇关于熵和基尼指数的文章:
理解这些指标如何帮助我们量化数据集中的不确定性
- 如何评估决策树分类器? 请查看下面的文章,了解不同的分类模型评价指标:
展开混淆矩阵、精准率、召回率、F1 分数和 ROC 曲线
参考文献
[1] Aurélien Géron, (2019). Hands-on machine learning with Scikit-Learn, Keras and TensorFlow: concepts, tools, and techniques to build intelligent systems (第 2 版). O’Reilly.
[2] ritvikmath 的 YouTube 视频
[3] StatQuest
AI 如何看待你的国家?
原文:
towardsdatascience.com/how-does-ai-see-your-country-3899e4057735
不拘一格的揭秘者
让我们带着 Midjourney 环游世界
·发表于 Towards Data Science ·19 分钟阅读·2023 年 1 月 29 日
--
欢迎!这不仅仅是一篇博客文章,更是一个由 Midjourney 制作的展览,展示了 AI 艺术的美丽和 偏见。我的灵感来自于想知道 AI 系统如何反映国家身份,因此我使用相同的提示但不同的国家名称生成了一些艺术作品,结果非常引人入胜!
AI 系统如何反映国家身份?
我在下面粘贴了 200 张图片,没有评论,留给你自己得出结论。
但首先,一些背景信息。如果你对这些图片的来源不感兴趣,只需滚动过所有文字即可查看它们。

这里的提示是“未来世界的动画科学节目的插图。”
什么是 AI 艺术?
开始阅读 这里 或在下面的视频中观看我使用 OpenAI 的 DALL·E 2 制作一些 AI 艺术。
Midjourney 是什么?
类似于 DALL·E 2,Midjourney 是一个 AI 艺术生成器,它以文本提示作为输入,并为每个提示生成四张图片。要使用它,你需要一个与 Midjourney bot 连接的 Discord 账号。虽然这有点麻烦,并且花了我 10 分钟来 设置,但我认为这是值得的。

Midjourney 新手频道 的截图。
要尝试,你需要输入命令/imagine,然后将你的提示直接输入到Midjourney 新人频道中,免费试用。免费版的缺点是你只能获得 25 个提示,而你的创作会在屏幕上闪过,混杂在与其他人共享的频道的结果中。你制作的任何艺术作品也会在他们的网站上公开发布在你的用户名下,你可以在注明 Midjourney 作为来源的前提下使用这些图像,使用的是创意共享许可证。
你可以从 25 个免费提示中获得很多乐趣,但还是花一点时间看看其他人的提示结果,以便你掌握一些“提示工程”(如何措辞提示以获得你想要的结果),否则你可能会浪费这 25 个机会。
如果你购买像我正在使用的高级账户,你将获得无限提示、一个没有其他人图像干扰的私人频道,并且你拥有你制作的任何图像的所有版权。
强大但不完美
我不得不承认,AI 艺术生成器可能是我新的最爱玩具。
作为一位有品位但没有才华的艺术爱好者,我突然有能力以前所未有的方式进行视觉表达。我一直知道我喜欢什么艺术,但不知道如何创造我喜欢的东西。直到现在。
当我使用 AI 艺术工具时,我觉得我可以理解为什么有些人追求赌博的刺激。
作为一名统计学家,我从未特别喜欢赌场;我无法对负期望值的提议产生任何兴奋感。但是,当我使用 AI 艺术工具时,我觉得我可以理解为什么有些人追求赌博的刺激。每次提交一个提示时,总会有一种兴奋的感觉:我会得到美丽的东西吗?
有时候输出的确是美丽的。或者搞笑的。或者完全怪异的。
AI 艺术反映了我们的价值观。
不过,有时候,输出结果令人失望。不仅仅是像那些视觉上乏味的作品(这种作品在 Midjourney 的赌博游戏中有很多)那样的轻微失望,也不仅仅是像那些被太多手指(是的,这里手指数量的正确性是例外而非规则)毁坏的几乎完美的图片那样的失望。是那种让我对人类感到有点悲伤的失望。
我们选择捕捉的图像揭示了我们当时认为重要的东西。
AI 艺术生成器并不是凭空创造东西,而是通过使用标记图像数据中的模式。任何基于它们的 AI 艺术只是将我们的物种反映给我们。我们最好和最糟的一面。我们选择捕捉的图像首先揭示了我们当时认为重要的东西。不幸的是,我们的价值观一直充斥着偏见。(我有一整篇关于偏见和责任的文章在这里)
AI 艺术仅仅是捕捉和存储最多图像的人的价值观的重混。任何输出都是这些图像的汇聚。一些是美丽的,一些是糟糕的。但也许我们可以通过审视它们来了解自己,并在未来做得更好。我将让你们自己判断。
本次展览的解释
当我决定创建这个收藏时,我事先制定了一些规则。首先,我计划每个提示只提交一次,然后允许自己对最差的结果进行 3 次重试……但我必须告诉你哪些国家被重试了。*
你看到的每一个结果——除了 3 次重试——都是第一次生成的结果,因为如果我多次运行每个提示并选择我最喜欢的结果,结果会更多地告诉你关于我而不是关于 Midjourney 或底层训练数据的事情。(我坚持认为,当一个人调整提示并从多个提示中选择自己喜欢的艺术作品时,艺术家绝对是人类,而不是 AI。)这些输出将反映我、我的品味以及我的世界观。但这不是这个收藏的重点。作为艺术家的参与仅限于概念。除了 3 次重试,其他没有挑选单独作品。

我打开的两个标签页的截图。左侧的标签页是 Discord/Midjourney 界面,上面是南非的提示(我来自那里),右侧的标签页是我使用的国家列表的维基百科来源。
其次,每个提示都会采用这种相同的格式:“设定在[国家]的关于科学和进步的未来动画节目插图。”
为什么这个提示?我想要一些有抱负的东西。无论今天世界上存在什么问题,我们都可以渴望进步和更光明的未来。
为什么选择动画节目的插图?啊,那就是提示工程!(或者,按照我喜欢的说法,文本调整。)在我开始这个项目之前,我调整了措辞,以获得我最喜欢的图形风格。别对着你的奶昔哼笑,但有一刻我真的考虑过使用“1980 年代的关于科学和进步的卡通,设定在[国家]”。
为什么选择科学与进步,而不仅仅是进步?科学似乎是对提示的一个有趣补充,不仅因为科学的图像通常关注人类的最佳面貌,还因为我受过科学训练(让我们检验一下个人偏见!),而且它给人工智能生成器提供了大量机会,来发掘一些令人愉快的奇异之处,为未来发明新的科学。(我也是个怪人——双重偏见!)
不要期望人工智能以与人类相同的方式从模式中学*。
第三,我会让维基百科决定国家名单。每一个现任或曾任联合国成员国或联合国特别委员会的国家都在展览中占有一席之地,我按照这个列表中的常用名称按字母顺序输入。唯一的快速编辑更改是将“格鲁吉亚(欧洲)”而不是仅仅“格鲁吉亚”和“乍得共和国”而不是仅仅“乍得”作为提示,因为 Midjourney 并不像人类那样从模式中学*。如果你是我的插画师,我给你所有这些提示,你会意识到我打算你将乍得视为一个国家,因为其他的都是,但我有预感 Midjourney 会将乍得解释为某个叫查尔斯的人的随意日子。至于其他的,我只是随便用了列表中第一列的内容。
现在,事不宜迟,结果来了!
收藏
提示: “为设定在[国家]的未来科学与进步主题的动画节目制作插图。”




















































































”。
如果 AI 代表“自动化灵感”而非“人工智能”,那将更好。
我对刚果民主共和国使用了第一次重投,因为这是 Midjourney 唯一认为应该将科学家描绘成穿实验室大褂的动物的国家。一个我们都成了动物的未来,对我来说有点太过进步了。很难说初始图像的原因是统计随机性、过多的野生动物宣传册、不公平的讽刺画,还是完全其他的原因。
虽然我不了解 Midjourney 的训练集的详细信息,但可以合理地假设,Midjourney 的图像来源与全球互联网设备的分布高度相关,因此你获得的图像更可能反映出拥有广泛互联网使用的国家对其他国家的部分性讽刺,而不是一个国家对自身的看法。
Midjourney 反映了谁的观点?它将与全球互联网设备的分布高度相关,因此并非所有国家在训练数据中的贡献都相同。
我将剩余的两次重投用于叙利亚和伊拉克,因为最初的结果显示了太多破坏的建筑……这不是我的提示所要求的,对吧?我要求的是“科学和进步,发生在未来”。无论一个国家的过去如何,它的未来没有理由不能辉煌——我最初设计提示的形式就是为了展现一个更美好的明天。一次重投稍微改善了这些图像。尽管展览排除了那些原始图像,但这里仍然有一个可教的时刻:AI 反映了我们的过去,如果我们用它来做出自动化的最终决定,而不让人类参与其中,它将会让我们陷入过去,并使我们更难以向更好的方向改变。
AI 反映了我们的过去,如果我们用它来做出自动化的最终决定,而不让人类参与其中,它将会让我们陷入过去,并使我们更难以向更好的方向改变。
感谢阅读!来一门课程如何?
如果你在这里玩得开心,并且在寻找一个有趣的领导力导向课程,旨在让 AI 初学者和专家都感到愉快,这是我为你制作的小东西:

课程链接: bit.ly/funaicourse
想磨练决策技能而不是构建 AI 能力?你可以通过这个 免费的课程链接 向我学*决策智能
[## 你生活的方向盘——决策智能视频教程 | LinkedIn Learning…
决策是你可以学*的最有价值的技能。你的人生归结为两件事:你所做的决策的质量…
bit.ly](https://bit.ly/decisioncourse?source=post_page-----3899e4057735--------------------------------)
P.S. 你是否尝试过在 Medium 上多次点击点赞按钮,看看会发生什么? ❤️
喜欢这位作者?与 Cassie Kozyrkov 取得联系
成为朋友吧!你可以在 Twitter、YouTube、Substack 和 LinkedIn 上找到我。想让我在你的活动中演讲?请使用 这个表单 与我联系。
阅读 Cassie Kozyrkov 的每个故事(以及 Medium 上其他数千名作者的故事)。你的会员费用直接支持…
PPO 的剪切如何工作?
原文:
towardsdatascience.com/how-does-ppo-with-clipping-work-eff71a7a974a
直观 + 数学 + 代码,针对实践者
·发表于 Towards Data Science ·9 min 阅读·2023 年 10 月 7 日
--

图片由 Tamanna Rumee 提供,Unsplash
在强化学*中,*端策略优化(PPO)通常被引用作为策略方法的例子,相对于 DQN(基于价值的方法)和包括 TD3 和 SAC 在内的大量 actor-critic 方法。
我回想起之前第一次学*时,我感到不信服。许多老师采用了一种模糊的方法。我不接受这种做法,你也不应如此。
在这篇文章中,我将尝试解释 PPO 如何工作,通过直观和代码来支持数学。你可以尝试不同的场景,亲自验证它不仅在原则上有效,而且在实践中也有效,并且没有 cherry picking。
为什么要费心?
PPO 和其他 SOTA 模型可以在几分钟内使用 stable-baselines3(sb3)实现。任何遵循文档的人都可以运行它,而无需了解底层模型。
然而,无论你是实践者还是理论家,基础知识确实重要。如果你仅仅把 PPO(或任何模型)当作一个黑箱,你怎么指望你的用户对你提供的结果有信心呢?
我将在本月晚些时候进行详细的代码讲解,编写一个包装器,使任何环境,无论是来自 Gymnasium 还是你自己的环境,都能与任何 sb3 模型兼容,无论空间是‘Discrete’还是‘Box’。(上个月,我展示了如何从 TD(λ)派生出 Monte Carlo、SARSA 和 Q-learning,所有这些都是通过一套代码实现的。)
够了,明天再谈,现在就让我们在这里吧!
PPO 的前身
Vanilla 策略梯度是基于策略方法中的最基本情况,其中策略是直接学*和更新的,而不是从某些价值函数推导出来的。缺点是策略更新的方差很高,这对收敛性是一个问题,特别是在奖励稀疏的环境中。
TRPO 的数学
TRPO(Trust Region Policy Optimization)确保新的策略(其中“新”指的是更新后)不会偏离旧策略太远。这是通过施加一个约束来实现的,即新策略相对于旧策略的 KL 散度不超过某个阈值δ。

PPO 的目标。公式由作者输入,参考OpenAI Spinning Up。
注意每个策略π{θ}本身是一个分布。*D*,带有“帽子”,是相对于旧策略下访问的状态的 KL 散度的(加权)平均值。KL 散度本身是概率比的对数的平均值,根据第一个分布加权。

KL 散度公式用于两个离散分布p和 q 之间。对于连续分布,我们将有概率密度和积分替代求和。公式由作者提供。
目标函数是替代优势,它是一个比率(新策略下的动作概率除以对应于旧策略的概率)乘以优势。这个优势是相对于某些基准的期望回报,基准可以简单地是相应状态值的移动平均。
我们有一个约束优化问题,使用拉格朗日乘子和共轭梯度法来解决。目标和约束通过泰勒展开线性化,因此解决方案是*似的。为了确保满足约束,使用回溯线搜索来调整策略更新中的步长。
(这是我设定的界限,不再深入探讨。上述内容对应于 Level 7xx 课程,实践者可以跳过;了解描述的方向在我看来已经足够了。)
TRPO 的直觉
-
如果一个动作是有利的,即A > 0,我们希望增加其概率。更新策略后,它应该被更频繁地选择。
-
如果一个动作是不利的,即A < 0,我们希望降低其概率。L是负的,我们希望它更不负,以最大化目标。
-
小的 KL 散度意味着在旧政策和新政策下每个动作的概率保持接*。在极端情况下,我们有 log(1),这使得 KL 散度为零。
过渡到 PPO
解决约束优化问题涉及回溯线搜索(即每次用较小的步长重复计算,直到满足约束)以及计算包含二阶偏导数的 Hessian 矩阵。我们可以简化这一过程吗?
TRPO 论文的第一作者在 2015 年提出了改进模型,并在 2017 年作为第一作者发表了 PPO。即便在今天(2023 年),PPO 仍然被广泛使用。
PPO 的数学
我将减少数学内容,所以请继续跟随我!有不同的 PPO 变体,我们将在这里讨论‘clip’版本,这也是 sb3 使用的版本。
不通过对 KL 散度施加约束,而是修改目标函数,使其不受政策大幅变化的影响。我们仍然尝试增加有利动作的概率(并减少不利动作的概率),但概率比大幅高于(或低于)1 的影响被裁剪所限制。
ϵ 通常选择 0.2。这个值也在原始论文中使用。

TRPO 的目标。作者编写的方程,参考 OpenAI Spinning Up。
论文的图 1(第 3 页)显示了 (1 + ϵ) 或 (1 — ϵ) 的因子,根据优势是正还是负,二者都对目标函数设置了上界。下面的图示更直观:

简化方程,取决于优势 A 的值。
不要被‘min’运算符搞混。我们希望优化目标 L 使其尽可能高(积极)。‘min’运算符仅用于 (1 + ϵ) 或 (1 — ϵ) 的作用形成上限。
PPO 的直观理解
-
如果A > 0,当概率比高于 1 时,我们的目标函数 L 较大(更积极),即新策略更频繁地选择有利的动作。然而,由于 (1 + ϵ),存在一个上界。
-
如果A < 0,当概率比低于 1 时,我们的目标函数 L 较大(较少负值),即新策略更频繁地选择不利的动作。然而,由于 (1 - ϵ),存在一个上界。
-
梯度更新可以使用常规的 pytorch 或 tensorflow 轻松完成。
代码
首先让我们说服大家,梯度更新是有效的,不涉及任何裁剪。
1 层
我们建立了一个简单的单层神经网络,输入维度为 1 的状态,输出在 3 个可能选择之间的动作概率。
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
torch.manual_seed(0)
class PolicyNetwork(nn.Module):
def __init__(self, n_in=1, n_out=3):
super(PolicyNetwork, self).__init__()
self.fc = nn.Linear(n_in, n_out)
self.softmax = nn.Softmax(dim=1)
def forward(self, x):
x = self.fc(x)
return self.softmax(x)
你可以有多维状态,甚至是图像,概念依然有效——对于某个给定状态,神经网络给出总和为 1 的概率。
无裁剪
现在,让我们指定一个状态。对于本练*,它可以是任何常数。在实际操作中,这个状态是通过对环境的观察获得的,并且完全独立于参数θ,即在计算梯度时将其视为常数。
优势可以通过广义优势估计(如有兴趣,请参见上述引用的 Schulman et al. 2017 年论文的公式 11)来计算。它结合了来自多个时间区间的信息,试图平衡偏差和方差之间的权衡,类似于 TD(λ)。
policy = PolicyNetwork()
optimizer = optim.Adam(policy.parameters(), lr=0.01)
state = torch.tensor([[1.]])
pi = policy(state)
print("Before: ", pi)
advantages = torch.tensor([[-2., 0., 0.]], requires_grad=True)
loss = -torch.sum(pi * advantages)
optimizer.zero_grad()
loss.backward()
optimizer.step()
pi = policy(state)
print("After: ", pi)
对于本练*,我们任意指定了第一行动的负面优势,模拟了第一次行动带来负面优势的情况。这是为了验证参数是否更新,使得第一行动的概率减少。

上面代码单元的输出,其中第一次行动导致负面收益
实际上,在梯度更新后,策略输出了较低的第一行动概率和较高的其他行动概率。
使用 Clip
现在,我们将实现剪切方面。为了首先检查正确性,将进行一次更新。这次,假设第二行动被执行,估计的优势为 1.8。
注意,在求和乘积时添加了负号,以得到损失(在此基础上将进行反向传播)。这是因为我们希望最大化目标函数,并进行梯度下降。
epsilon = 0.2
old_policy = PolicyNetwork()
old_policy.load_state_dict(policy.state_dict())
pi_old = old_policy(state)
r = pi / pi_old
advantages = torch.tensor([[0., 1.8, 0.]], requires_grad=True)
clipped_ratio = torch.clamp(r, 1 - epsilon, 1 + epsilon)
ppo_objective = torch.min(r * advantages, clipped_ratio * advantages)
loss = -torch.sum(ppo_objective)
print("Probabilities before update:", policy(state))
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("Probabilities after update:", policy(state))

上面代码单元的输出,其中第二行动导致了正面收益
到目前为止,一切顺利。当采取一种有利的行动时,更新策略会增加该行动的概率。现在,我们将考虑许多迭代的净效果,其中每种不同的行动都被多次执行。
你可能会说,在现实中,优势是有噪声的,使用常数是不公平的。你说得对!让我们通过np.random.randn()添加随机噪声。
num_iterations = 500
advatange_dict = {
0: [-2., 0., 0.],
1: [0., 1.8, 0.],
2: [0., 0., 0.1],
}
np.random.seed(2023)
for i in range(num_iterations):
pi = policy(state)
pi_old = old_policy(state)
r = pi / pi_old
old_policy.load_state_dict(policy.state_dict())
noisy_advantange = [
x+np.random.randn() for x in advatange_dict[i%3] if x != 0
]
advantage = torch.tensor(noisy_advantange, requires_grad=True)
ppo_objective = torch.min(
r * advantage,
torch.clamp(r, 1 - epsilon, 1 + epsilon) * advantage
)
loss = -torch.sum(ppo_objective)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print("After: ", pi)
经过 500 次迭代,包括噪声,我们在给定状态下得到了以下行动概率。(当然,我们可以轻松扩展这个环境以考虑不同的状态。)

上面代码单元的输出,其中多次执行行动,并添加了噪声
我们可以看到,所有更新的净效果使得第二种行动的概率增加,而其他行动的概率减少。(我们开始时的概率为[0.2149, 0.5257, 0.2594]。)
尽管最后一个动作具有正的均值优势,这种情况仍然存在。这是因为梯度不仅依赖于优势的符号,还依赖于其大小。每次执行最后一个动作时,概率在更新后会增加,但在执行第二个动作时,更新的影响会抵消这一增加。
最后,让我们看看剪切的实际效果。重复上述单元(其中epsilon ϵ 设置为 0.2),但这次将其设置为更小的值,比如 0.02。

以较小的 epsilon 值重新运行代码时的输出
很好!我们看到剪切确实有效!通过使用更小的ϵ,策略的变化幅度确实较小,尽管仍朝着期望的方向变化。
结论
在这篇文章中,我们探讨了 PPO 的起源。数学部分仅保留了最基本的内容——足以让从业者向其他利益相关者展示这些思想,而不会被详细的推导所淹没。我们从数学中获得了关键的直觉,以理解 PPO 的工作原理。
此外,你现在拥有了代码来证明 PPO 的基本组件确实实现了其承诺。我们看到,即使添加了噪声,最有利的动作的概率也有所增加。此外,我们发现,通过收紧剪切,得到的策略变化的幅度较小。
社会教育指数如何影响学校离校结果? — 用 R 和 brms 进行的贝叶斯分析
ANCOVA — 贝叶斯风格
·发布于 Towards Data Science ·阅读时间 11 分钟·2023 年 9 月 12 日
--
在上一篇文章对学校部门和高等教育结果之间的比例差异进行了解释并得到非常积极的反馈之后,让我们进一步探讨一下。我们指出该模型并不旨在展示因果关系,仅仅是描述性的。我们预先说明了在因果模型中可能需要考虑的众多因素。事实证明,确实存在一个代理指标,即 ICSEA 或社区社会教育优势指数。
ANOVA — 贝叶斯风格
towardsdatascience.com
在上一篇文章的基础上,我们将探讨 ICSEA 与各部门比例结果之间是否存在因果关系,进行一个贝叶斯 ANCOVA 工作流。

图片来源:Vasily Koloda 在 Unsplash
背景
在开始建模之前,让我们发展我们的因果模型和理解。ICSEA 是一个识别学校社会教育优势的尺度。这是由ACARA(澳大利亚课程评估报告局)计算的指标,考虑了父母的教育背景和职业、学生的土著身份以及学校的地理位置。ICSEA 的平均值设定为 1000,标准差为 100,因此较高的 ICSEA 值表示具有更高社会教育优势的学校。
有许多因素可能影响学生的表现和成果。我们将其简化为下面更具体的内容。从左侧开始,我们可以合理地预期父母的教育水平会影响他们孩子的个人抱负,父母的教育水平将影响他们的薪资,从而影响他们能够负担的社区,使他们接*某些部门的学校。我们将合理假设富裕地区的独立学校和天主教学校的比例高于政府学校。这些是许多需要整理、总结并创建回归模型以测试因果假设的领域和数据点。幸运的是,ICSEA 是大多数这些输入变量的魔法代理测量,因此右侧是我们的简化因果模型。

学生成果的因果图 — 作者提供的图片
学术文献对此进行了广泛探讨。下文中的综述论文,由 Alyahyan & Düştegör 提供,概述了一系列可能影响学术成功的因素。我们可以看到这是一个极其复杂的关系网络,涉及心理、社会、经济和环境因素。
学生成功在教育机构中扮演着至关重要的角色,因为它通常被用作机构的衡量标准…
educationaltechnologyjournal.springeropen.com 
影响学生学术成功预测的因素总结 — 图片来源于参考文献 [1],采用 CC BY 4.0 许可证
让我们不要弄混水,因为成功是一个主观的术语,源于一些期望的结果。
我们的研究问题是关于理解不同部门之间 ICSEA 水平在结果上的差异,这是一种微妙但重要的区别。
加载库和数据集
ACARA 是澳大利亚课程评估报告机构,他们为全国所有学校发布 ICSEA 和其他绩效指标,点击此处 查看。这一数据集在 CC BY 4.0 许可下提供。与之前一样,我们将从 维多利亚州政府的 On Track 调查 中加载数据,该调查为 2021 年毕业生的离校去向调查,同样在 CC BY 4.0 许可下提供。
下面我们进行一些基本清理,然后将两个数据集 profile 和 df_long 合并,创建 df_longb,其中包含我们的部门、结果、比例以及每个学校对应的 ICSEA 分数。两个学校没有 ICSEA 分数,因此已被移除。
library(tidyverse)
library(brms)
library(tidybayes)
library(readxl)
df <- read_excel("DestinationData2022.xlsx",
sheet = "SCHOOL PUBLICATION TABLE 2022",
skip = 3)
colnames(df) <- c('vcaa_code', 'school_name', 'sector', 'locality', 'total_completed_year12', 'on_track_consenters', 'respondants', 'bachelors', 'deferred', 'tafe', 'apprentice_trainee', 'employed', 'looking_for_work', 'other')
df <- drop_na(df)
df2 <- df |>
mutate(across(5:14, ~ as.numeric(.x)), #convert all numeric fields captured as characters
across(8:14, ~ .x/100 * respondants), #calculate counts from proportions
across(8:14, ~ round(.x, 0)), #round to whole integers
respondants = bachelors + deferred + tafe + apprentice_trainee + employed + looking_for_work + other, #recalculate total respondents
filter(sector != 'A') #remove A schools (low volume)
df_long <- df2 |>
select(sector, school_name, 7:14) |>
pivot_longer(c(-1, -2, -3), names_to = 'outcome', values_to = 'proportion') |>
mutate(proportion = proportion/respondants)
profile <- read_excel("school-profile-2022.xlsx",
sheet = "SchoolProfile 2022") |> #From ACARA
filter(State == 'VIC' & `School Type` %in% c('Secondary', 'Combined')) |>
select(`School Sector`, `School Name`, ICSEA, `ICSEA Percentile`) |>
drop_na() |>
mutate(`School Sector` = substr(`School Sector`, 1, 1),
ICSEA_s = ((ICSEA - min(ICSEA-1)) / (max(ICSEA+1) - min(ICSEA-1))))
colnames(profile) <- c('sector', 'school_name', 'ICSEA', 'Percentile', 'ICSEA_s', 'percentile_d', 'percentile_c')
df_longb <-
df_long |>
mutate(school_name = str_to_upper(school_name)) |>
left_join(profile |> mutate(school_name = str_to_upper(school_name))) |>
drop_na()
探索性数据分析
我们的数据集已经准备好,可以开始可视化感兴趣的特征了。
profile |>
ggplot(aes(y = sector, x = ICSEA, fill = sector)) +
stat_halfeye(alpha = 0.5) +
theme_ggdist() +
scale_fill_viridis_d(begin = 0.4, end = 0.8) +
labs(title = 'Distribution of ICSEA Values By Sector', y = 'Sector')

各学校部门的 ICSEA 分数分布 — 图片由作者提供
有趣的是,可能并不意外的是公立学校的方差最大,而独立学校的中位数最高。
贝叶斯方差分析 — 估计 ICSEA 后验均值的差异
首先,我们检查 ICSEA 各部门之间的差异是否真实,如同前一篇文章,我们将进行贝叶斯方差分析。这也开始解决我们 DAG 的第一部分,即 ICSEA 与部门之间的关系。我们已指示模型提供每个部门层级的 mu 和 phi 参数值。

m4e 模型的数学公式 — 图片由作者提供
m4e <-
brm(bf(ICSEA_s ~ sector + 0,
phi ~ sector + 0),
family = Beta,
data = profile,
prior = c(prior(normal(-1, 2), class = 'b'),
prior(gamma(6, 2), class = b, dpar = phi, lb = 1)),
seed = 246, warmup = 1000, iter = 6000, cores = 4, chains = 4, save_pars = save_pars(all = T)) |>
add_criterion(c('waic', 'loo'), moment_match = T)
summary(m4e)
我们决定使用最小-最大缩放 ICSEA,以便我们可以通过 Beta 似然函数进行建模,因此 mu 和 phi 参数分别使用对数几率和对数链接函数。
实质上,均值的贝塔项将是每个部门均值百分位的对数值,基于重新缩放的 ICSEA 值。

m4e 模型的总结输出 — 图片由作者提供
让我们简要地对每个部门进行后验预测检查,以查看模型是否合理地捕捉了观察到的数据。
alt_df <- profile |>
select(sector, ICSEA) |>
mutate(y = 'y',
draw = 99)
sim_df <- tibble(sector = c('C', 'I', 'G')) |>
add_predicted_draws(m4e, ndraws = 1200, seed = 246) |> #generates 1200 posterior draws for each sector
mutate(ICSEA = (.prediction * (1211 - 808) + 808)) |> #rescale ICSEA back
ungroup() |>
mutate(y = 'y_rep',
draw = rep(seq(from = 1, to = 20, length.out = 20), times = 180)) |> #draws
select(sector, ICSEA, draw, y) |>
bind_rows(alt_df)
sim_df |>
ggplot(aes(ICSEA, group = draw)) +
geom_density(aes(color = y)) +
facet_grid(~ sector) +
scale_color_manual(name = '',
values = c('y' = "darkblue",
'y_rep' = "grey")) +
theme_ggdist() +
labs(y = 'Density', x = 'y', title = 'Distribution of Observed and Replicated Proportions by Sector', subtitle = 'Model = m4e - Non-Pooled Phi')

m4e 模型的后验预测检查 — 图片由作者提供
我们的模型合理地捕捉了各部门 ICSEA 分布的形状。最后,让我们请模型告诉我们使用贝叶斯方差分析的各部门间均值 ICSEA 差异。
new_df <- tibble(sector = c('I', 'G', 'C'))
epred_draws(m4d, newdata = new_df) |>
mutate(ICSEA = .epred * (1211 - 808) + 808) |>
compare_levels(ICSEA, by = sector, comparison = rlang::exprs(C - G, I - G, C - I)) |>
mutate(sector = fct_inorder(sector),
sector = fct_shift(sector, -1),
sector = fct_rev(sector)) |>
ggplot(aes(x = ICSEA, y = sector, fill = sector)) +
stat_halfeye() +
geom_vline(xintercept = 0, lty = 2) +
theme_ggdist() +
theme(legend.position = 'bottom') +
scale_fill_viridis_d(begin = 0.4, end = 0.8) +
labs(x = 'Difference in Mean Posterior ICSEAS', y = 'Sector Comparison', title = 'Sector Comparison of Posterior Distributions of Mean ICSEAS')

贝叶斯方差分析 — 部门间均值 ICSEA 差异 — 图片由作者提供
鉴于我们在探索性数据分析中观察到的情况,这些差异并不令人惊讶。天主教学校和政府学校之间的平均差异为 46 个 ICSEA 分数点,而独立学校和政府学校之间的差异为 80 个分数点。后者几乎相当于一个标准差。独立学校的平均分数比天主教学校高 30 个点。这些结果都是真实的。
贝叶斯 ANCOVA——ICSEA 对不同部门的比例结果的影响。
由于我们希望理解 ICSEA 在各部门中结果比例的变异性,我们需要使用层次模型。由于我们的目标变量是一个比例,我们将继续使用 Beta 似然函数。
我们希望建模 ICSEA 在各部门和比例结果中的变异性。这个模型在我的机器上运行大约需要 78 分钟,使用了链内线程,因此请注意。我们不会假装理解模型中的每一个参数,我们设定了广泛的一般先验,更重要的是,我们在构建一个能够回答问题的模型。
m5d <-
brm(bf(proportion ~ 1 + (ICSEA_s|sector:outcome),
phi ~ 1 + (ICSEA_s|sector:outcome)),
family = Beta,
data = df_longb |> mutate(proportion = proportion + 0.01),
prior = c(prior(cauchy(0, 2), class = sd),
prior(cauchy(0, 2), class = Intercept)),
seed = 246, chains = 4, cores = 4, iter = 8000, warmup = 1000, save_pars = save_pars(all = T),
control = list(adapt_delta = 0.99, max_treedepth = 15), threads = threading(2)) |>
add_criterion(c('waic', 'loo'), moment_match = T)
summary(m5d)

模型输出 m5d——作者提供的图像
让我们检查曲线与观察数据的拟合情况。
new_df <- expand_grid(sector = c('C', 'I', 'G'),
outcome = unique(df_long$outcome),
ICSEA_s = seq(from = 0, to = 1, by = 0.05))
m5d |>
epred_draws(new_df, ndraws = 200) |>
filter(outcome != 'other') |>
mutate(ICSEA = ICSEA_s * ((max(df_longb$ICSEA) + 1) - (min(df_longb$ICSEA) - 1)) + min(df_longb$ICSEA) - 1) |>
ggplot(aes(ICSEA, .epred, color = sector)) +
stat_lineribbon() +
geom_point(data = df_longb |> filter(outcome != 'other'), aes(y = proportion, x = ICSEA), alpha = 0.3) +
facet_grid(sector ~ outcome) +
theme_ggdist() +
theme(legend.position = 'bottom') +
scale_color_viridis_d(begin = 0.2, end = 0.8) +
scale_fill_brewer(palette = "Greys") +
labs(y = 'Proportion', x = 'ICSEA', title = 'Beta Regression Curve Fits of Proportion & ICSEA by Sector and Outcome', subtitle = 'm5d Non-Pooled Phi Term')

按 ICSEA 的比例回归曲线拟合——作者提供的图像
上述内容讲述了一个有趣的故事。首先,大学就读学生的比例与 ICSEA 呈正相关,几乎在所有部门中均是如此。与此相反,高中毕业后就业的学生比例与 ICSEA 的增加呈负相关。ICSEA 的影响现在变得更加明显,因此让我们理解 ICSEA 在各部门之间的比例结果的预期差异,ANCOVA。
new_df <- expand_grid(sector = c('C', 'I', 'G'),
outcome = c('apprentice_trainee', 'bachelors', 'deferred', 'employed', 'looking_for_work', 'tafe'),
ICSEA_s = seq(from = 0, to = 1, by = 0.05))
m5d |>
epred_draws(new_df, ndraws = 2000, seed = 246) |>
compare_levels(variable = .epred, by = sector, comparison = rlang::exprs(C - G, I - G, C - I)) |>
mutate(ICSEA = ICSEA_s * ((max(df_longb$ICSEA) + 1) - (min(df_longb$ICSEA) - 1)) + min(df_longb$ICSEA) - 1,
sector = fct_inorder(sector),
sector = fct_shift(sector, -1),
sector = fct_rev(sector)) |>
ggplot(aes(y = .epred, x = ICSEA, color = sector)) +
stat_lineribbon() +
geom_hline(yintercept = c(0, -0.05, 0.05), lty = c(2)) +
scale_fill_brewer(palette = "Greys") +
scale_color_viridis_d(begin = 0.2, end = 0.8) +
facet_grid(outcome ~ sector, scales = 'free_y') +
theme_ggdist() +
theme(legend.position = 'bottom') +
scale_y_continuous(labels = scales::label_percent()) +
labs(x = 'ICSEA', y = 'Difference in Posterior Means', title = 'Sector Comparison of Outcomes by ICSEA')

ICSEA 和部门的均值结果比例比较——作者提供的图像
上述内容讲述了一个有趣的故事,也突显了贝叶斯方法的好处——测量不确定性。
让我们深入探讨学士学位的结果。在 ICSEA 水平低于~1000 时,政府学校的本科教育就读学生比例通常高于独立学校和天主教学校。

各部门和 ICSEA 的学士比例比较——作者提供的图像
读者会被 1000–1100 ICSEA 之间后验均值差异的缩小所吸引。我们的估计更为准确,因为在这一范围内学校有较大的重叠。两侧的结果反映了不确定性的扩大,因为各部门的人口减少,这一现象在我们的密度图中得到了加强。
一般而言,当根据 ICSEA 条件对学士学位结果进行分析时,后验密度的 50% 大致位于 ±5% 之间。这加强了 ICSEA 与学校离校生接受某种形式大学教育之间的正相关关系,同时对非政府学校有小的偏向。
如果我们应用一个 ROPE(实际等效区域),设定为 ±5% 的边际差异范围,我们可以说差异确实是微不足道的。

Pipe DAG — 图片由作者提供
从 ICSEA 预期比例结果的差异来看,根据 ICSEA 条件进行分析时,部门间的差异是微不足道的。参考我们上面的因果 DAG,我们可以有效地将其从一个碰撞器重新框架为一个管道。
当根据 ICSEA 条件对学科进行分析时,结果比例在部门之间是条件独立的。换句话说,一旦我们知道一所学校的 ICSEA,部门的影响在 ±5% 的均值后验差异的 ROPE 范围内是微不足道的。
结果比例 || 部门 | ICSEA
摘要与结论
在这篇文章中,我们继续探索中学毕业生的结果,这次将社区社会教育优势指数(ICSEA)作为衡量学校社会教育优势的替代指标。我们进行了初步的贝叶斯 ANOVA 来识别部门间 ICSEA 后验均值的差异。然后,我们将其与结果数据集合并,进行贝叶斯 ANCOVA 以回答在根据 ICSEA 条件分析时结果比例是否因部门而异。
这项工作开始揭示一个可能的因果模型,我们通过 DAG 进行了说明。我们的结论是,结果比例 || 部门 | ICSEA 并不令人意外,这与我们对 ICSEA 的理解是一致的。ICSEA 被设计为捕捉一系列可能的混杂因素,以单一指标来描述学校或人口水平的影响。在这个练*中,它使我们能够量化部门间的均值差异,并发现这些差异在 ±5% 的 ROPE 范围内总体上是微不足道的。
参考文献
- Alyahyan, E., Düştegör, D. 预测高等教育中的学术成功:文献综述及最佳实践。Int J Educ Technol High Educ 17, 3 (2020).
doi.org/10.1186/s41239-020-0177-7
不确定性原理如何限制时间序列分析?
为什么我们不能从时间序列中互相提取精确的时间和频率信息,以及小波分析如何解决这一限制
·
关注 发布于 Towards Data Science · 6 分钟阅读 · 2023 年 8 月 31 日
--
照片由 Jamie Street 通过 Unsplash 提供
1. 引言
傅里叶变换、不确定性原理和时间序列分析之间的联系揭示了一个引人入胜的相互作用,这种相互作用塑造了同时提取时间和频率信息的方式。要理解这种关系,首先需要简要了解傅里叶变换(FT)和不确定性原理作为结果的基本概念。然后,我们探索小波变换(WT)作为克服这一限制的有希望的工具,以足够清晰地揭示特定的频率时间发生情况。
1.1 傅里叶变换
傅里叶变换(FT)作为函数时间域和频率域之间的数学桥梁。FT 可以描述为:

我不会详细讲解这个积分,但重要的是FT 将函数 f(x) 变换为频率空间中的另一个函数 g(ω)。 记住这个信息,它会很重要。(为了更好地理解 FT,我强烈推荐 3Blue1Brow 的视频。)
1.2 不确定性原理作为傅里叶变换的结果
1927 年,物理学家沃纳·海森堡提出了可能是量子力学中最著名的概念之一——不确定性原理[1]。该原理基本上是关于傅里叶变换的定理,当两个函数是彼此的傅里叶变换时,不确定性原理就会发挥作用。
尽管我们暂时绕开了其复杂的物理学,但仅考虑其本质:位置x和动量p的不确定性的乘积始终受到限制。 这一限制突显了以无限精度测量这些量的固有边界(如果你感兴趣,可以查看这个视频)。

物理学中的不确定性原理。
这发生是因为位置和动量是彼此的傅里叶变换! 在时间序列分析的情况下,位置 x 和动量 p 的类比分别是时间序列中的时间 t 和频率 ω 空间。

信号处理中的不确定性原理。
2. 使用 FT 在时间序列中生成频率
傅里叶变换在时间序列分析中的一个非常重要的应用。考虑一个场景,我们需要找到时间序列中的固有频率。例如,想想识别出人们重看给定内容的常见频率。因此,我们希望将 f(t)(时间序列) 变换为 g(ω),一个频率函数,使用 FT。
2.1 示例
为了举例说明,我们使用 Mauna Loa 每周大气 CO2 数据集[2]。

Mauna Loa 每周大气 CO2 时间序列。图像由作者提供。
去除线性趋势以消除虚假的低频贡献:

时间序列已去趋势。图片来源:作者。
使用快速傅里叶变换算法(FFT)计算傅里叶变换:

时间序列的傅里叶变换。图片来源:作者。
从中可以得到组成序列的频率,在这种情况下,可以明显看到两个不同的峰,一个是年度的,另一个是半年期的。这是数据集清晰季节性的可视化。
然而,最有趣的方面发生在时间序列中出现扰动时,这可能是事件或外部变量(例如,火山喷发临*)的影响。为了模拟这一点,我们可以在相同的序列中加上一个随机正弦波:

马纳罗亚每周大气 CO2 去趋势时间序列加上扰动。图片来源:作者。
应用快速傅里叶变换(FFT)来获取序列的傅里叶变换:

扰动时间序列的傅里叶变换。图片来源:作者。
现在,扰动造成的峰值在 0.5 到 0.75 之间出现。
2.2 限制
有时,我们想知道扰动和频率变化发生的时间,或只是序列中每个频率的时间位置。在我们的模拟中,有和没有扰动的序列明显表明,大部分影响发生在序列的开始处。然而,仅凭对序列的视觉检查和傅里叶变换信息还不足以知道扰动发生的确切位置。因此,我们需要找到另一种工具来帮助我们。
然而,傅里叶变换(FT)有一个权衡:它会剥离时间信息,如不确定性原理所示,使我们无法知道这些频率在序列中出现的具体时间。 这就是不确定性原理的作用所在。我们可以利用不确定性原理,在减少分辨率的情况下获得对这两种量的洞察,同时保持平衡。
3. 小波变换作为时间和频率权衡的工具
小波变换(WT)作为一种分辨率平衡的手段,将我们的函数 f(t) 转换为 F(t,ω),即时间和频率的组合。 我不会详细介绍小波变换的工作原理,但总的来说,变换过程使用一系列不同的小波(具有已知频率和形状的信号)通过两个函数的点积来与时间序列匹配时间同步。因此,可以对频率和时间发生有一定了解,但两者的分辨率都有限。

莫尔雷特小波信号的例子(这是一个复杂信号,因此有虚部和实部)。图片来源:作者。
要可视化信号的小波变换,通常将 x 轴表示为时间尺度,y 轴表示为频率尺度,颜色尺度表示为频率的功率。
对于没有扰动的去趋势时间序列,

小波变换的毛纳罗阿每周大气 CO2 时间序列。图片由作者提供。
从视觉检查可以清楚地看到季节性按预期重复。然而,FT 中 1 年季节性的巨大峰值现在变宽了,因此我们对该值的确定性降低了。
最有趣的分析是对于模拟场景的。

扰动时间序列的小波变换。图片由作者提供。
WT 显示,在 FT 中观察到的低频扰动发生在系列的早期,并且在 20-25 年后产生了显著的后果。尽管 WT 在时间和频率上都失去了分辨率,但关于频率出现的新信息是有价值的,并且能够回答各种问题。
4. 结论
简而言之,不确定性原理对时间序列分析施加了基本限制,如傅里叶变换的约束所揭示的。虽然傅里叶变换有效地从时间序列数据中提取频率信息,但它牺牲了关于这些频率出现时间的任何知识。因此,小波变换是一个有用的工具,使我们能够在接受不确定性原理固有的程度的同时,在时间和频率分辨率之间进行权衡,从而获得频率分量的时间发生的洞察。
致谢
这个来自Artem Kirsanov的精彩视频激发了我撰写本文。如果你想深入了解这个话题,我也推荐观看这个视频。
备注
-
傅里叶变换(FT)不仅对量子力学和时间序列分析非常重要。它也正被用于此时将本文的数据存储在云服务中。历史上最著名和使用最广泛的算法之一称为快速傅里叶变换(FFT),它基本上支持了当今的每一个数据压缩。
-
为了用数学上描述亚原子粒子特性,傅里叶变换自然地出现在量子力学中。这反映了不确定性原理对我们理解自然的根本性视角转变。测量粒子的位置会让你失去关于同一粒子速度的信息,反之亦然。
本文的笔记本可在 这里获得。
参考文献
[1] en.wikipedia.org/wiki/Uncertainty_principle
[2] www.statsmodels.org/dev/datasets/generated/co2.html (公有领域)
[3] 小波变换 (维基百科)
XGBoost 如何处理多类分类?
原文:
towardsdatascience.com/how-does-xgboost-handle-multiclass-classification-6c76ba71f6f0
理解使用这种模型的分类机制至关重要,因为它影响到性能。
·发表于 数据科学前沿 ·阅读时间 7 分钟·2023 年 1 月 7 日
--

图片来源于 Andrew Coop 在 Unsplash
在本文中,我们将了解使用梯度提升库如 XGBoost、LightGBM 和 CatBoost 训练的决策树集成如何进行多类分类。
确实,决策树的集成将一个实数值关联到一组特征,因此问题是:决策树集成如何将一个标量值转换为多类标签?
理解使用这种模型的分类机制至关重要,因为它影响到性能。
我们将按照以下计划逐步进入主题:
-
Python 中二分类的提醒和示例
-
首先,使用 XGBoost 作为回归器进行二分类
-
第二次,使用 XGBoost 作为分类器进行二分类
-
使用 XGBoost 进行多类分类
基于决策树的集成模型的多样性
XGBoost、LightGBM 或 CatBoost 是共享(默认情况下)相同类型底层模型的库:决策树。
这些决策树通过梯度提升迭代组合。即 向当前树中添加新节点,以便优化非线性目标,通常是平方误差。为了处理非线性,目标使用其梯度和海森矩阵进行线性化。
因此叫做梯度提升。更多细节请参见我之前的论文:
## 在不到 200 行 Python 代码中实现 DIY XGBoost 库
XGBoost 的解释以及梯度提升方法和超参数调优,通过构建自己的梯度提升库来实现……
towardsdatascience.com
使用决策树集成进行预测
提醒一下,预测过程相对简单:给定一行数据,每棵决策树都会被遍历。
根据特征的值,每棵树会将一个唯一值关联到最终的叶节点上。
然后,每棵树的唯一预测值被简单地加总起来,得到总体预测结果。
下面的图例在一个简单的示例中说明了这一点,其中一个决策树的集成模型了一个在 1 到 4 之间的整数的身份函数:

一个简单的决策树集成。图示由作者提供。
例如,当输入为 1 时,第一个树生成 8,第二个树生成-6,最后一个树生成-1。将这三个值加总得到 1,这是期望的输出。
这个例子摘自我的书籍,实用梯度提升,关于梯度提升的内容:
这本关于梯度提升方法的书籍是为那些希望……](https://amzn.to/3QmRqtO?source=post_page-----6c76ba71f6f0--------------------------------)
使用单一标量值时,我们所能做的最好是进行二分类,将负预测标记为一个类别,将正预测标记为另一个类别。
没有 XGBoost 的二分类
在发现第一个选项,即将 XGBoost 作为回归器进行二分类之前,让我们详细展示二分类是如何完成的。
我们在这里尝试解决的问题很简单:我们想要根据学生花费在学*科目上的小时数来确定其成功的概率。
下面的图展示了收集的数据,即工作小时数和结果:通过 或 失败。

成功的概率取决于学*时间。图示由作者提供。
用于分类的标准模型是 Logistic 函数。该函数类似于线性回归,不同之处在于它生成的值仅在[0, 1]范围内,而不是ℝ范围内。其公式值得了解:

Logistic 函数。公式由作者提供。
和机器学*中的情况一样,为模型(这里是逻辑函数)找到最佳参数是通过最小化误差来完成的。面对一个二分类问题,其中正输出可以用 1 建模,负输出可以用 0 建模,可以将两种误差结合成一个单一的表达式:

简单的误差函数。由作者提供的公式。
其中y_k是观察到的样本,而f(x_k)是模型f做出的预测。
这个基本误差的问题在于,根据逻辑函数的二元性质,它主要取两个值:零和一,因此相对于模型参数m的误差也主要取两个值。因此,在最佳参数的邻域之外,误差将是平坦的。

直接使用误差时的饱和现象。由作者提供的图。
我们可以使用这个公式,这样可以工作,只要我们提供一个相当好的最佳参数估计。如果不是这样,我们可能会陷入一个误差几乎恒定的平坦区域。在这个区域,梯度将几乎为零,最陡下降法的收敛速度将非常缓慢。
我们需要一种方法来处理误差输出,将其从给定样本的范围[0, 1]扩展到ℝ+,以避免饱和现象。
额外的约束是零误差在变换后必须仍然是零误差。
诀窍是认识到 log(1)为零,而 log(0)为-∞。
因此使用对数损失:

对数损失。由作者提供的公式。
其中y_k是观察到的样本,而f(x_k)是模型f做出的预测。注意加法运算符前的负号和1-f(x_k)的取反与f(x_k)的交换。这是因为log(1)=0。
使用对数损失,误差不再饱和:

使用对数损失的误差图。由作者提供的图。
最简单的最小化这个误差的方法是使用最陡下降法,这只需要计算误差的梯度。可以有多种方法来实现这一点。这里我们将使用sympy进行符号微分:
逻辑回归。由作者提供的代码。
算法找到了预期值 14.77,这个值非常接*理论值。
现在让我们回到主题,使用决策树和梯度提升进行二分类。
使用 XGBoost 进行二分类
让我们从一个简单的例子开始,使用Cleveland Heart Disease Dataset(CC BY 4.0),其中分类是通过回归完成的。由于我们进行的是二分类,可以使用简单回归,因为我们可以将正标签附加一个正值 1.0,将负标签附加一个负值-1:
使用回归器进行分类。作者提供的代码。
XGBoost 使用的默认错误是平方误差。预测被四舍五入为整数,正如混淆矩阵所示,模型的预测没有误差。
直接使用 XGBoost 分类器也能实现类似的结果:
使用分类器进行分类。作者提供的代码。
在这种情况下,无需将预测值四舍五入以获取相应的类别。所有的工作都是由 XGBClassifier 原生完成的。让我们看看 XGBoost 是如何处理的。
XGBClassifier 训练多个模型
实际上,当你使用 XGBoost 进行分类时,使用 XGBClassifier(或用适当参数进行分类的 xgb.train),XGBoost 实际上训练了多个模型,每个类别一个。
下面的代码片段展示了如何深入了解 XGBoost 的内部实现。
获取每个类别的单独概率。作者提供的代码。
更具体地说,predict_proba 方法允许访问内部模型生成的原始数据。这清楚地揭示了,在进行分类时,XGBoost 为每个类别做出概率预测。
预测的类别是具有最高概率的那个。
查看将 XGBoost 集成到 sklearn 的代码,我们确认 XGBoost 进行多个预测:
提取自 XGBoost 的开源代码。参见 github.com/dmlc/xgboost/blob/master/python-package/xgboost/sklearn.py#L1541
如可见,第 25 行中使用了 argmax 来检索使用 softprob 时具有最高概率的类别索引。在使用 softmax 作为目标的情况下,预测值直接转换为整数。
XGBoost 如何执行多类分类?
通常,关于 XGBoost 如何处理多类分类的解释说明它训练了多个树,每个类别一个。
实际情况并非如此。事实上,所有的树是同时构建的,使用的是向量目标函数而不是标量目标函数。即 每个类别都有一个目标。
XGBoost 的 文档 提供了这种目标的示例:
提取自 XGBoost 文档。参见 xgboost.readthedocs.io/en/stable/python/examples/custom_softmax.html
这个代码片段中有两件非常有趣的事情:
- 当使用 XGBoost 中的集成目标时,目标名称是
multi:softprob。这很令人困惑,因为实际目标并非softprob,而是softmax的对数损失。这在代码中表现得很清楚,因为梯度直接是softmax。但softmax不是softmax的梯度,而是其对数损失的梯度:

softmax 的对数损失的梯度。由作者提供的公式。
2. 另一个点是代码使用了一个变量hess,代表海森矩阵。然而,从数学角度来看,这实际上不是海森矩阵,而是二阶导数。因此,这个变量的正确名称应该是拉普拉斯算子。
www.buymeacoffee.com/guillaumes0
结论
我们在这篇论文中展示了 XGBoost 如何处理分类问题。
理解分类n个类别会生成n倍复杂的树是至关重要的。
同样重要的是注意,XGBoost API 中暴露的目标函数名称并不总是非常明确。例如,在进行分类时,优化的目标不是softmax或softprob,而是它们的对数损失。
如果你想了解更多关于梯度提升方法的详细信息,请查看我的书:
这本关于梯度提升方法的书籍是为希望深入了解该领域的学生、学者、工程师和数据科学家们准备的 梯度提升方法。
企业如何构建类似于 OpenAI 的 ChatGPT 的大型语言模型
想要构建你自己的 ChatGPT 吗?这里有三种方法可以实现
·
关注 发表在 Towards Data Science · 10 分钟阅读 · 2023 年 7 月 1 日
--
图 1:构建自定义 LLM 的三种方式(图像来源:作者)
TL;DR。企业应该构建自己的定制 LLM,因为它提供了定制、控制、数据隐私和透明度等各种好处。为了简化构建定制 LLM 的过程,建议遵循三个级别的方法——L1、L2 和 L3。这些级别从低模型复杂度、准确性和成本(L1)到高模型复杂度、准确性和成本(L3)。企业必须在这些折衷中取得平衡,以满足他们的需求并从 LLM 计划中获得投资回报。
介绍
语言模型*年来获得了显著关注,彻底改变了自然语言处理、内容生成和虚拟助手等多个领域。一个最突出的例子是 OpenAI 的 ChatGPT,这是一种大型语言模型,可以生成类人文本并进行互动对话。这引发了企业的好奇心,促使他们探索构建自己大型语言模型(LLM)的想法。
然而,决定是否开始构建 LLM 需要仔细审视。它需要大量的资源,包括计算能力和数据可用性。企业必须权衡收益与成本,评估所需的技术专长,并判断这是否符合他们的长期目标。
在这篇文章中,我们向你展示了构建自己 LLM 的三种方法,类似于 OpenAI 的 ChatGPT。在文章结束时,你将对构建自己的大型语言模型相关的挑战、需求和潜在回报有更清晰的了解。让我们深入探讨吧!
企业应该构建自己的 LLM 吗?
要了解企业是否应该构建自己的 LLM,让我们探索他们可以利用这些模型的三种主要方式。

图 2:利用大型语言模型的不同方式(作者提供的图像)
1. 闭源 LLM: 企业可以利用现有的 LLM 服务,如 OpenAI 的 ChatGPT、Google 的 Bard,或其他提供商的类似服务。这些服务提供了现成的解决方案,使企业能够利用 LLM 的强大功能,而无需进行大量基础设施投资或具备技术专长。
优势:
-
快速、简便的部署,节省时间和精力。
-
在一般文本生成任务上的良好表现。
缺点:
-
对模型行为和响应的控制有限
-
在特定领域或企业特定数据上的准确性较低
-
数据隐私问题,因为数据被发送到托管服务的第三方
-
依赖第三方供应商和可能的价格波动。
2. 使用领域特定 LLM: 另一种方法是使用领域特定的语言模型,例如用于金融的 BloombergGPT、用于生物医学应用的 BioMedLM、用于营销应用的 MarketingGPT、用于电子商务应用的 CommerceGPT 等。这些模型在领域特定的数据上进行训练,从而在各自领域中提供更准确和量身定制的响应。
优势:
-
由于在相关数据上进行训练,特定领域的准确性得到了提升。
-
具有针对特定行业量身定制的预训练模型的可用性。
缺点:
-
在超出指定领域的适应性方面灵活性有限。
-
依赖于供应商的更新和领域特定模型的可用性。
-
稍微提高了准确性,但仍受限于非特定于您企业数据的情况
-
数据隐私问题,因为数据被发送到托管服务的第三方
3. 构建和托管自定义 LLM:最全面的选项是企业使用其特定数据构建和托管自己的 LLM。这种方法提供了最高级别的定制和隐私控制,确保与其品牌声音的一致性和领域特定的准确性。
优点:
-
完全定制和控制:自定义模型使企业能够生成与其品牌声音、行业特定术语和独特要求完全一致的响应。
-
成本效益:如果设置得当(微调成本在几百美元左右)
-
透明:整个数据和模型对企业是已知的
-
最佳准确性:通过在企业特定数据和要求上训练模型,它可以更好地理解和回应企业特定的查询,从而产生更准确和上下文相关的输出。
-
隐私友好:数据和模型保留在您的环境中。拥有自定义模型可以使企业控制其敏感数据,从而减少与数据隐私和安全漏洞相关的担忧。
-
竞争优势:在个性化和准确语言处理起着关键作用的行业中,自定义大型语言模型可以成为显著的区分因素。
缺点:
- 需要显著的机器学*和 LLM 专业知识来构建自定义大型语言模型
需要注意的是,自定义 LLM 的方法取决于各种因素,包括企业的预算、时间限制、所需准确性和期望的控制级别。然而,正如您从上述内容中看到的,基于企业特定数据构建自定义 LLM 提供了许多好处。
自定义大型语言模型为特定领域、使用案例和企业需求提供了无与伦比的定制、控制和准确性。因此,企业应考虑构建自己的企业特定自定义大型语言模型,以解锁量身定制的无限可能,满足其需求、行业和客户群体。
构建自定义大型语言模型的三种方式
您可以通过三种方式构建自定义 LLM,这些方式的复杂性从低到高,如下图所示。

图 3:构建自定义 LLM 的三种方式(图片来源:作者)
L1. 利用调整的 LLM
利用预训练 LLM 的一种常见方法是制定有效的提示技术以应对各种任务。一个常见的提示方法是 上下文学*(ICL),其包括用自然语言文本表达任务描述和/或示例。此外,通过在提示中加入一系列中间推理步骤,思维链(CoT) 的使用可以增强上下文学*。要构建一个 L1 LLM,

图 4:ICL 和 CoT 提示的比较示意图。(图片来自论文《大型语言模型调查》——参考文献 №7)
要构建一个 L1 LLM,
-
首先选择一个合适的预训练 LLM(可以在 Hugging Face 模型库 或其他在线资源中找到),通过查看许可证确保其适用于商业用途。
-
接下来,确定与你的特定领域或用例相关的数据源,组建一个多样化且全面的数据集,涵盖广泛的主题和语言变体。对于 L1 LLM,不需要标记数据。
-
在定制过程中,所选预训练 LLM 的模型参数保持不变。相反,采用提示工程技术来调整 LLM 的响应以适应数据集。
-
如上所述,上下文学*和思维链提示是两种流行的提示工程方法。这些技术统称为资源高效调优(RET),提供了一种简化的方式来获得回应,而无需大量基础设施资源。
L2. 指令调优 LLM
指令调优是对预训练 LLM 进行微调的一种方法,其形式为自然语言中的格式化实例,这与监督微调和多任务提示训练高度相关。通过指令调优,LLM 能够在没有显式示例的情况下(类似于零-shot 能力)遵循任务指令,从而具备更好的泛化能力。要构建这种指令调优的 L2 LLM,
-
首先选择一个合适的预训练 LLM(可以在 Hugging Face 模型库 或其他在线资源中找到),通过查看许可证确保其适用于商业用途。
-
接下来,确定与你的目标领域或用例相关的数据源。需要一个包含与你的领域或用例相关的各种指令的标记数据集。例如,你可以参考 Databricks 提供的dolly-15k 数据集,它提供了不同格式的指令,如闭合问答、开放问答、分类、信息检索等。这个数据集可以作为构建你自己指令数据集的模板。
-
进入监督微调过程,我们向第 1 步中选择的原始基础 LLM 引入新的模型参数。通过添加这些参数,我们可以训练模型进行特定的训练轮次,以微调其对给定指令的响应。这个方法的优点在于它避免了更新基础 LLM 中数十亿个参数的需要,而是集中在较少数量的附加参数(数千或数百万)上,同时仍能在所需任务中取得准确的结果。这种方法还帮助减少了成本。
-
下一步是进行微调。各种微调技术,如前缀调优、适配器、低秩注意力等,将在未来的文章中详细阐述。上述第 3 点中讨论的添加新模型参数的过程也依赖于这些技术。有关更多详细信息,请参见参考文献部分。这些技术属于参数高效微调(PEFT)类别,因为它们能够在不更新基础 LLM 的所有参数的情况下进行定制。
L3. 对齐调优的 LLM
由于 LLM(大语言模型)是通过捕捉预训练语料库的数据特征进行训练的(包括高质量和低质量的数据),它们可能会生成有毒、偏见甚至对人类有害的内容。因此,可能有必要将 LLM 与人类价值观对齐,例如,有帮助、诚实和无害。为了这一对齐目的,我们使用了与人类反馈的强化学*(RLHF)技术,这是一种有效的调优方法,可以使 LLM 遵循预期的指令。它通过精心设计的标记策略将人类纳入训练循环。要构建这个对齐调优的 L3 LLM,
-
首先选择一个开源的预训练 LLM(可以在Hugging Face 模型库或其他在线资源中找到),或者选择你的 L2 LLM 作为基础模型。
-
构建对齐调整 LLM 的主要技术是 RLHF,它结合了监督学*和强化学*。首先,从第 1 步的特定领域或指令语料库的微调 LLM 开始,使用它生成回应。然后,这些回应由人类注释,用于训练监督奖励模型(通常使用另一个预训练 LLM 作为基础模型)。最后,通过使用奖励模型进行强化学*(PPO),对 LLM(来自第 1 步)进行再次微调,以生成最终回应。
-
因此,训练了两个 LLM:一个用于奖励模型,另一个用于微调 LLM 以生成最终回应。在这两种情况下,基本模型参数可以有选择地更新,取决于所需的响应准确性。例如,在一些 RLHF 方法中,仅更新涉及强化学*的特定层或组件的参数,以避免过拟合,并保留预训练 LLM 捕获的一般知识。
这个过程的一个有趣的现象是,到目前为止,成功的 RLHF 系统使用了相对于文本生成的不同大小的奖励语言模型(例如,OpenAI 175B LM,6B 奖励模型,Anthropic 使用从 10B 到 52B 的 LM 和奖励模型,DeepMind 使用 70B Chinchilla 模型作为 LM 和奖励模型)。一种直觉是,这些偏好模型需要有类似于生成文本所需的模型的能力,以理解所给定的文本。
还有 RLAIF(带有 AI 反馈的强化学*),可以替代 RLHF。主要区别在于,AI 模型作为评估者或评论者,在强化学*过程中向 AI 代理提供反馈,而不是人类反馈。
结论
企业可以利用定制 LLM 的非凡潜力,实现与其特定领域、用例和组织需求相符的卓越定制、控制和准确性。建立一个特定于企业的定制 LLM,使企业能够解锁大量量身定制的机会,完全符合其独特需求、行业动态和客户基础。
建立自定义 LLM 的过程有三个层级,从低模型复杂性、准确性和成本到高模型复杂性、准确性和成本。企业必须在这些权衡之间找到平衡,以最适合其需求并从 LLM 计划中获取投资回报。

图 5:三个 LLM 层级之间的权衡(图像由作者提供)
参考文献
-
In-Context Learning (ICL) — Q. Dong, L. Li, D. Dai, C. Zheng, Z. Wu, B. Chang, X. Sun, J. Xu, L. Li, 和 Z. Sui, “关于上下文学*的调查,” CoRR, 卷. abs/2301.00234, 2023。
-
思维链提示 — J. Wei、X. Wang、D. Schuurmans、M. Bosma、E. H. Chi、Q. Le 和 D. Zhou,“思维链提示在大型语言模型中引发推理,”CoRR,第 abs/2201.11903 卷,2022 年。
-
指令调整 — J. Wei、M. Bosma、V. Y. Zhao、K. Guu、A. W. Yu、B. Lester、N. Du、A. M. Dai 和 Q. V. Le,“微调语言模型是零样本学*者,”发表于第十届国际学*表征会议,ICLR 2022,虚拟会议,2022 年 4 月 25–29 日. OpenReview.net,2022 年。
-
一项关于大型语言模型的调查 — Wayne Xin Zhao、Kun Zhou、Junyi Li、Tianyi Tang、Xiaolei Wang、Yupeng Hou、Yingqian Min、Beichen Zhang、Junjie Zhang、Zican Dong、Yifan Du、Chen Yang、Yushuo Chen、Zhipeng Chen、Jinhao Jiang、Ruiyang Ren、Yifan Li、Xinyu Tang、Zikang Liu、Peiyu Liu、Jian-Yun Nie 和 Ji-Rong Wen,arXiv:2303.18223v4 [cs.CL],2023 年 4 月 12 日
决策树如何解决回归问题?
原文:
towardsdatascience.com/how-exactly-does-a-decision-tree-solve-a-regression-problem-fbb908cf548b
从头开始构建你自己的决策树回归器(用 Python),揭示其内部机制
·发布于 Towards Data Science ·阅读时长 12 分钟 ·2023 年 11 月 25 日
--

图片来源于 Chris Lawton 在 Unsplash
在本文中,我将通过一个简单的示例、流程图和代码展示决策树回归器(即回归树)内部实现的整个逻辑。阅读后,你将对回归树的工作原理有一个清晰的理解,并在下一次回归挑战中更加深思熟虑和自信地使用和调整它们。
我们将涵盖以下内容:
-
对决策树的精彩介绍
-
生成一个用于训练回归树的玩具数据集
-
绘制回归树逻辑的流程图
-
参考流程图编写代码,使用 NumPy 和 Pandas 进行首次分裂
-
使用 Plotly 在首次分裂后可视化决策树
-
使用递归将代码泛化以构建整个回归树
-
使用 scikit-learn 执行相同任务并比较结果(剧透:你将为自己与 scikit-learn 得到相同的输出感到非常自豪!)
介绍
决策树是可以用来解决分类和回归问题的机器学*算法。尽管分类和回归本质上是不同的,但决策树试图以优雅的方式解决这两种问题,其最终目标是在给定节点找到最佳分裂。确定最佳分裂的方式就是使分类树和回归树彼此不同的原因。
在我之前的文章中,我介绍了决策树如何解决分类问题的基础知识。我使用了一个两类数据集来演示如何逐步理解如何通过数据不纯度度量(如熵)在每个节点生成决策规则,然后在 Python 中实现了一个递归算法以输出最终的决策树。不确定是否应该将这篇文章添加到你的阅读列表中?让我们使用决策树来找出答案吧!

来源:作者提供的图片
重要提示: 这只是一个展示决策树是什么的示例,无论它是否这样说,你都很棒。
如上所示,决策树分类器旨在预测离散标签(或类别),在我们的案例中是你很棒!和去阅读它!。在这种情况下,决策树会查看每个分裂点的类别概率分布,以计算熵等指标,从而决定最佳的特征和分裂值。
然而,在回归问题中,目标变量是连续的,我们不能在这种情况下使用熵(或基尼指数)作为分裂标准。因此,回归树使用均方误差(MSE)并选择在每个节点上导致最小 MSE 的特征和数值。
均方误差: 它被定义为真实值与预测值之间的平方差之和。

来源:作者提供的图片
构建回归树
为了演示回归树是如何学*的,我将使用 NumPy 生成一个类似于二次函数的玩具数据集,用作训练数据。可以暂停并打开一个新的 Python 笔记本,以便在阅读时进行编码。
首先让我们导入库。
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
生成数据集
在下面的代码中,我们将生成训练数据,其中我们的目标,即因变量 y 是自变量 X 的二次函数。为了简单起见,我考虑了一个单一特征 X,但可以很容易地扩展到多个特征(包括连续特征和分类特征)。
在这里,X 和 y 都是连续的(y 必须是连续的,因为这是回归,而 X 可以是分类的或连续的)。
# Set a random seed for reproducibility
np.random.seed(0)
# Constants for the quadratic equation
a, b, c = 1, 2, 3
# Create a DataFrame with a single feature
n = 100 # number of samples
x = np.linspace(-10, 10, n) # feature values from -10 to 10
noise = np.random.normal(0, 10, n) # some random noise
y = a * x**2 + b * x + c + noise # quadratic equation with noise
# create a pandas dataframe
df = pd.DataFrame({'X': x, 'y': y})
数据可视化
我们已经创建了 100 个数据点。接下来我们将使用 Plotly 以如下方式可视化我们的数据。
df = data.copy()
# Create the figure
fig = go.Figure()
# Add scatter plot trace
fig.add_trace(
go.Scatter(
x=df["X"],
y=df["y"],
mode="markers",
marker=dict(opacity=0.7, size=6, color="red",
line=dict(color='Black', width=1))
)
)
# Update layout
fig.update_layout(
title={
'text': "Scatter plot of our sample data",
'y':0.9,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top'},
xaxis=dict(
title="X",
showline=True, # Show the X axis line
tickmode='linear', # Tick mode to be linear
# tick0=-10, # Start tick at 0
dtick=1, # Tick at every 1 unit
linecolor='black', # X axis line color
),
yaxis=dict(
title="y",
showline=True, # Show the Y axis line
linecolor='black', # Y axis line color
),
plot_bgcolor='white', # Background color
width=900,
height=500
)
# Show the figure
fig.show()
单元格输出:

来源:作者提供的图片
准备好进行第一次分裂了吗?
在这之前,让我们首先概述一下回归树所采取的步骤,然后我们将参考这些步骤来编写我们的代码。 花一点时间研究下面的流程图就足以理解回归树的基本逻辑,以在给定节点找到最佳划分。

流程图展示了回归树逻辑(来源:作者提供的图片)
一旦上述逻辑清楚,我们就可以使用以下代码来见证回归树进行第一次最佳划分。
我们需要首先定义一个辅助函数来计算 MSE。
def mean_squared_error(y, y_hat):
"""
Returns mean squared error for
given actual values and predicted values
"""
y = np.array(y)
y_hat = np.array(y_hat)
return np.mean((y-y_hat)**2)
以下代码是如上流程图所示的核心逻辑。
df = data.copy()
# list of all features (only one i.e., 'X' in our case)
features = ["X"]
# iterate over all features
for feature in features:
# initialize best_params dict
best_params = {"feature": None, # best feature
"split_value": None, # best split value
"weighted_mse": np.inf, # weighted mse of two branches
"curr_mse": None, # mse at current node
"right_yhat": None, # prediction at right branch
"left_yhat": None # prediction at left branch
}
# sort the df by current feature
df = df.sort_values(by=feature)
# compute the mse at current node
curr_yhat = np.mean(df['y'].mean())
curr_mse = mean_squared_error(df['y'], curr_yhat)
curr_mse = np.round(curr_mse, 3)
# iterate over all rows of sorted df
for i in range(1, len(df)):
# compute average of two consecutive rows
split_val = (df.iloc[i][feature] + df.iloc[i-1][feature]) / 2
split_val = np.round(split_val, 3)
# split the df into two partitions
left_branch = df[df[feature]<=split_val]
right_branch = df[df[feature]>split_val]
# compute the MSE of both partitions:
left_yhat = np.mean(left_branch["y"]) # prediction will be average of target
left_yhat = np.round(left_yhat, 3)
left_mse = mean_squared_error(left_branch["y"], left_yhat) # mse of left
left_mse = np.round(left_mse, 3)
right_yhat = np.mean(right_branch["y"]) # prediction will be average of target
right_yhat = np.round(right_yhat, 3)
right_mse = mean_squared_error(right_branch["y"], right_yhat) # mse of right
right_mse = np.round(right_mse, 3)
# compute weighted MSE
weighted_mse = ((len(left_branch) * left_mse) + (len(right_branch) * right_mse))/len(df)
weighted_mse = np.round(weighted_mse, 3)
# update best_params if weighted_mse is less than previously best_mse
if weighted_mse <= best_params["weighted_mse"]:
best_params["weighted_mse"] = weighted_mse
best_params["split_value"] = split_val
best_params["feature"] = feature
best_params["curr_mse"] = curr_mse
best_params["right_yhat"] = right_yhat
best_params["left_yhat"] = left_yhat
best_params
Cell Output:

太棒了!我们在X=6.869得到了第一个最佳划分。让我们如下可视化它:
# add vertical line to existing figure
fig.add_vline(x=best_params["split_value"], line_width=3, line_color="black")
# plot right branch split
fig.add_shape(type="line",
x0=best_params["split_value"], y0=best_params["right_yhat"],
x1=10, y1=best_params["right_yhat"],
line=dict(color="gray", width=3))
# plot left branch split
fig.add_shape(type="line",
x0=-10, y0=best_params["left_yhat"],
x1=best_params["split_value"], y1=best_params["left_yhat"],
line=dict(color="gray", width=3))
# fig.update_layout(title=
fig.update_layout(
title={
'text': f"X<={np.round(best_params["split_value"],2)}, MSE={np.round(best_params["curr_mse"],2)}, samples={len(df)}, value={np.round(df["y"].mean(),2)}",
'y':0.9,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top'},
)
fig.show()
Cell Output:

来源:作者提供的图片
上述结果的解释:
-
X<=6.869对应划分值。这意味着当前节点的数据将被划分为两部分——一部分是X<=6.869,另一部分是X>6.869。这就是黑色垂直线所表示的。 -
samples=100对应当前节点的总数据点数。最初,根节点包含所有数据点。 -
MSE=1381.82对应当前节点的均方误差值。我们知道 MSE 需要真实值和预测值来计算;我们已经有了真实值,预测值是什么呢?那就是value。 -
value=37.605对应当前节点的预测值。任何节点的预测值都是该节点数据的所有真实值的平均值。最初,根节点的预测值将仅仅是所有 100 行y值的平均值。 -
划分两侧的水平灰线表示左分区和右分区的预测值。
可选练*:复制我们上面使用的两个单元格,并将原始数据框替换为左分区或右分区数据框。你可以看到后续的划分是如何以完全相同的方式进行的。只是输入数据框改变了,其他(核心逻辑)保持不变。
下面是当前回归树的状态,经过对左分支和右分支重复代码后的样子:

来源:作者提供的图片
我希望这使得解读如下回归树变得更容易:

来源:作者提供的图片
上述过程会对后续的分区重复进行,直到满足停止准则(例如达到最大深度、无法进一步划分等)。
使用递归来概括代码
让我们将上述代码封装到一个函数build_tree中,我们可以递归调用它来构建回归树。
辅助函数:
mean_squared_error: 返回给定实际值和预测值的均方误差。如上所定义。
get_best_params: 返回包含特征和用于在当前节点拆分数据的值的best_params字典,并且包含当前节点的预测值。
def get_best_params(df):
"""Function to return best split"""
for feature in features:
# initialize best_params dict
best_params = {"feature": None, # best feature
"split_value": None, # best split value
"weighted_mse": np.inf, # weighted mse of two branches
"curr_mse": None, # mse at current node
"right_yhat": None, # prediction at right branch
"left_yhat": None # prediction at left branch
}
# sort the df by current feature
df = df.sort_values(by=feature)
# iterate over all rows of sorted df
for i in range(1, len(df)):
# compute average of two consecutive rows
split_val = (df.iloc[i][feature] + df.iloc[i-1][feature]) / 2
split_val = np.round(split_val, 3)
# split the df into two partitions
left_branch = df[df[feature]<=split_val]
right_branch = df[df[feature]>split_val]
# compute the MSE of both partitions:
left_yhat = np.mean(left_branch["y"]) # prediction will be average of target
left_yhat = np.round(left_yhat, 3)
left_mse = mean_squared_error(left_branch["y"], left_yhat) # mse of left
left_mse = np.round(left_mse, 3)
right_yhat = np.mean(right_branch["y"]) # prediction will be average of target
right_yhat = np.round(right_yhat, 3)
right_mse = mean_squared_error(right_branch["y"], right_yhat) # mse of right
right_mse = np.round(right_mse, 3)
# compute weighted MSE
weighted_mse = ((len(left_branch) * left_mse) + (len(right_branch) * right_mse))/len(df)
weighted_mse = np.round(weighted_mse, 3)
# update best_params if weighted_mse is less than previously best_mse
if weighted_mse <= best_params["weighted_mse"]:
best_params["left_yhat"] = left_yhat
best_params["right_yhat"] = right_yhat
best_params["weighted_mse"] = weighted_mse
best_params["split_value"] = split_val
best_params["feature"] = feature
best_params["curr_mse"] = curr_mse
return best_params
build_tree: 它是一个主要的驱动函数,利用辅助函数递归地构建回归树。我添加了如max_depth等参数,以便可以控制回归树的生长,以避免过拟合,并作为停止准则。
def build_tree(df, max_depth=3, curr_depth=0):
"""Function to build the regression tree recursively"""
if curr_depth>=max_depth:
prediction = np.round(np.mean(df["y"]), 3)
print(("--" * curr_depth) + f"Predict: {prediction}")
return
best_params = get_best_params(df)
if best_params["feature"] is None or best_params["split_value"] is None:
prediction = np.round(np.mean(df["y"]), 2)
print(("--" * curr_depth) + f"Predict: {prediction}")
return
feature = best_params["feature"]
split_val = best_params["split_value"]
# Print the current question (decision rule)
question = f"{feature} <= {split_val}"
mse = mean_squared_error(df["y"], df["y"].mean())
mse = np.round(mse, 3)
samples = len(df)
print(("--" * (curr_depth*2)) + ">" + f"{question}, mse: {mse}, samples: {samples}, value: {np.round(df["y"].mean(),3)}")
left_branch = df[df[feature]<=split_val]
right_branch = df[df[feature]>split_val]
# recursive calls for left and right subtrees
if not left_branch.empty:
print(("--" * curr_depth) + f"Yes ->")
build_tree(left_branch, curr_depth=curr_depth+1)
if not right_branch.empty:
print(("--" * curr_depth) + f"No ->")
build_tree(right_branch, curr_depth=curr_depth+1)
现在让我们在训练数据上调用上述函数。
build_tree(data)
Cell Output:

上述输出对应于设置了max_depth=3的回归树。为了更清晰,请参阅下图中的回归树。

最终回归树(来源:作者图片)
上述回归树中的叶子节点对应于预测值。根据我们的最终回归树绘制预测值和决策边界时,我们会得到如下结果:

最终预测和决策边界(来源:作者图片)
代码链接
你可以在这里找到完整的笔记本。
关键时刻 — 使用 scikit-learn 实现回归树,并将最终树与我们的进行比较
现在我们需要知道我们对回归树工作原理的理解是否准确。为此,让我们使用 scikit-learn 在相同的数据上训练回归树,并查看最终结果如下。
from sklearn.tree import DecisionTreeRegressor
from sklearn import tree
import matplotlib.pyplot as plt
# Fit the regression tree
regressor = DecisionTreeRegressor(max_depth=3)
X = np.array(data["X"]).reshape(100, 1)
y = np.array(data["y"])
regressor.fit(X, y)
# Plot the tree
plt.figure(figsize=(15, 8))
tree.plot_tree(regressor, feature_names=['X'], filled=True)
plt.show()
Cell Output:

来源:作者图片
scikit-learn 的 DecisionTreeRegressor 返回的回归树与我们之前创建的完全相同。
不再是谜!
我希望到目前为止,本文对你有所帮助,希望这行代码regressor.fit(X, y)不再是一个谜。既然我们现在对底层算法的构造有了充分了解,我们可以在未来调整基于树的回归模型时更加深思熟虑。
这还不是结束
本文的目的是仅仅演示决策树回归器在节点处做出最佳分割所使用的逻辑。然而,这不是关于决策树的全面指南,因为在建模时还有许多其他重要方面需要关注。如果我们完全不讨论这些方面是不公平的,因此在这里提供一些要点的简要总结。
决策树的优点:
-
决策树是可解释的和直观的,因为人们可以清楚地了解导致特定预测的原因。
-
它们不需要对训练数据进行大量预处理,并且可以内在处理类别特征和缺失值。
决策树的缺点:
-
决策树对数据的小变化非常敏感。
-
如果控制不当,决策树非常容易过拟合,需要采取适当措施以防止树的过度生长,从而使其具有良好的泛化能力。
结论
在这篇文章中,我们从零开始构建了一个回归树,以全面理解其背后的逻辑。我们生成了一个包含一个连续特征 X 和目标 y 的简单数据集,该数据集用于训练决策树回归器。接着,我们通过流程图传达了回归树在每个节点所采取的方法,以确定最佳分裂,使我们能够更清晰地编写代码并了解步骤的顺序。我们可视化了代码返回的第一个最佳分裂,并讨论了如何将相同的逻辑扩展到构建整个树。
我们还使用 scikit-learn 在相同的数据上训练了一个决策树回归器,并注意到它产生的结果与我们之前从零开始得到的结果相同。本文的目标是了解在调用 .fit() 来训练 scikit-learn 的 DecisionTreeRegressor 模型时,后台究竟发生了什么。
希望拥有这些知识将帮助我们在未来更好地调整、优化和解读基于树的回归模型。
感谢阅读 😃
欢迎任何反馈或建议!
想了解更多关于决策树的信息? 请查看以下文章:
从零开始构建自己的决策树(使用 Python),并了解它如何利用熵来进行分裂
towardsdatascience.com ## 熵与基尼指数
理解这些度量如何帮助我们量化数据集中的不确定性
towardsdatascience.com
少样本学*如何自动化文档标记
原文:
towardsdatascience.com/how-few-shot-learning-is-automating-document-labeling-43f9868c0f74
利用 GPT 模型
·发布于数据科学前沿 ·阅读时间 5 分钟·2023 年 4 月 7 日
--

手动文档标记是一个耗时且繁琐的过程,通常需要大量资源且容易出错。然而,最*在机器学*方面的进展,特别是所谓的少样本学*技术,使得自动化标记过程变得更加容易。特别是大语言模型(LLMs)由于其在上下文学*中的新兴能力,是优秀的少样本学*者。
在这篇文章中,我们将详细探讨少样本学*如何改变文档标记,特别是对文档处理中最重要的任务——命名实体识别(NER)的影响。我们将展示UBIAI的平台如何通过少样本标记技术使自动化这一关键任务变得比以往更容易。
什么是少样本学*?
少样本学*是一种机器学*技术,使得模型能够仅用少量标记示例来学*给定的任务。在不修改其权重的情况下,模型可以通过在输入中包含这些任务的连接训练示例,并要求模型预测目标文本的输出,从而调整以执行特定任务。以下是使用 3 个示例进行命名实体识别(NER)任务的少样本学*示例:
###Prompt
Extract entities from the following sentences without changing original words.
###
Sentence: " and storage components. 5+ years of experience deliver
ing scalable and resilient services at large enterprise scale, including experience in data platforms including large-scale analytics on relational, structured and unstructured data. 3+ years of experien
ce as a SWE/Dev/Technical lead in an agile environment including 1+ years of experience operating in a DevOps model. 2+ years of experience designing secure, scalable and cost-efficient PaaS services on
the Microsoft Azure (or similar) platform. Expert understanding of"
DIPLOMA: none
DIPLOMA_MAJOR: none
EXPERIENCE: 3+ years, 5+ years, 5+ years, 5+ years, 3+ years, 1+ years, 2+ years
SKILLS: designing, delivering scalable and resilient services, data platforms, large-scale analytics on relational, structured and unstructured data, SWE/Dev/Technical, DevOps, designing, PaaS services, Microsoft Azure
###
Sentence: "8+ years demonstrated experience in designing and developing enterprise-level scale services/solutions. 3+ years of leadership and people management experience. 5+ years of Agile Experie
nce Bachelors degree in Computer Science or Engineering, or a related field, or equivalent alternative education, skills, and/or practical experience Other 5+ years of full-stack software development exp
erience to include C# (or similar) experience with the ability to contribute to technical architecture across web, mobile, middle tier, data pipeline"
DIPLOMA: Bachelors\nDIPLOMA_MAJOR: Computer Science
EXPERIENCE: 8+ years, 3+ years, 5+ years, 5+ years, 5+ years, 3+ years
SKILLS: designing, developing enterprise-level scale services/solutions, leadership and people management experience, Agile Experience, full-stack software development, C#, designing
###
Sentence: "5+ years of experience in software development. 3+ years of experience in designing and developing enterprise-level scale services/solutions. 3+ years of experience in leading and managing
teams. 5+ years of experience in Agile Experience. Bachelors degree in Computer Science or Engineering, or a related field, or equivalent alternative education, skills, and/or practical experience."
提示通常以指示模型执行特定任务开始,例如“从以下句子中提取实体而不改变原始词语。”请注意,我们添加了指示“无更改原始词语”以防止 LLM 产生随机文本,这是其著名的特性。这在获得一致的模型响应中至关重要。
这种现象已在这篇文章中得到了广泛研究,我强烈推荐。实质上,论文表明,在温和的假设下,模型的预训练分布是潜在任务的混合,这些任务可以通过上下文学*高效地学*。在这种情况下,上下文学*更多的是关于识别任务,而不是通过调整模型权重来学*任务。
Few-shot 标注
Few-shot 学*在数据标注领域具有出色的实际应用,通常被称为少量标注。在这种情况下,我们向模型提供少量已标注的示例,并要求它预测后续文档的标签。然而,将这一能力集成到功能齐全的数据标注平台中,难度远超想象,以下是一些挑战:
-
LLM 本质上是文本生成器,倾向于生成可变的输出。提示工程对于使其生成可预测的输出至关重要,这些输出可以用于自动标注数据。
-
Token 限制:如 OpenAI 的 GPT-3 这样的 LLM 每次请求的 token 数量限制为 4000 个,这限制了可以一次发送的文档长度。在发送请求之前,分块和拆分数据变得至关重要。
-
Span 偏移计算:在从模型接收输出后,我们需要在文档中搜索其出现位置并正确标注。
使用 UBIAI 进行 Few-shot 标注
我们最*通过将OpenAI 的 GPT-3 Davinci与UBIAI 标注工具集成,新增了少量标注能力。该工具目前支持对未结构化和半结构化文档(如 PDF 和扫描图像)的 few-shot NER 任务。
开始使用:
-
只需标注 1–5 个示例
-
启用 few-shot GPT 模型
-
在新的未标注文档上运行预测
这是在提供 5 个示例的工作描述上进行 few-shot NER 的一个示例:

图片作者:非结构化文本上的 Few Shot NER
GPT 模型仅凭五个上下文示例就能准确预测大多数实体。由于 LLM 在大量数据上进行训练,这种 few-shot 学*方法可以应用于各种领域,如法律、医疗、HR、保险文档等,使其成为一个极其强大的工具。
然而,少量样本学*最令人惊讶的方面是它对上下文有限的半结构化文档的适应能力。在下面的示例中,我仅提供了一个标记的 OCR 发票示例,并要求它标记下一个。模型出乎意料地准确地预测了许多实体。即使有更多的示例,模型在对半结构化文档的泛化方面也表现得非常出色。

作者提供的图像:PDF 上的少量样本命名实体识别(NER)
有关少量样本标记功能的详细教程,请查看下面的视频:
UBIAI 的少量样本标记教程
结论:
少量样本学*正在彻底改变文档标记过程。通过将少量样本标记功能集成到功能性数据标记平台中,如 UBIAI 的注释工具,现在可以自动化诸如命名实体识别(NER)等关键任务,无论是在非结构化还是半结构化文档中。这并不意味着大型语言模型(LLMs)会很快取代人工标注员。相反,它们通过提高效率来增强他们的能力。凭借少量样本学*的力量,大型语言模型可以标记大量数据,并应用于法律、医疗、HR 和保险等多个领域,从而训练出更小、更准确的专业化模型,这些模型可以高效部署。
我们目前正在添加对少量样本关系提取和文档分类的支持,请继续关注!
在 Twitter 上关注我们 @UBIAI5 或 点击这里订阅!
如何通过 GenAI 解决方案彻底改变商业自动化:解读 LLM 应用的高级管理层
公司如何利用大型语言模型(LLMs)的力量来自动化工作流程并提高成本效率
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 9 月 6 日
--
照片由 Gerard Siderius 提供,来源于 Unsplash
介绍
在最*与一家生物制药公司制造高管的合作中,我们深入探讨了生成性 AI 的世界,特别是大型语言模型(LLMs),以探索它们如何用于加速质量调查。质量调查在产品制造或测试中发现偏差时触发。由于潜在的患者健康风险和监管要求,批次被暂停,根据影响,生产甚至可能会被暂停。加速调查以进行根本原因分析,并尽快实施纠正和预防措施计划至关重要。我们的目标是尽可能利用 GenAI 加速这一过程。
当我们开始思考最低可行产品(MVP)时,面临了几个选项,关于 GenAI 如何自动化流程的不同阶段,以提高周期时间并尽快解冻批次。高管们是各自领域的专家,并且接受了 GenAI 培训。然而,有必要深入了解 LLM 的能力和各种 GenAI 解决方案模式,以确定在质量调查过程中优先考虑哪个阶段作为 MVP,在短期解决方案可行性和预期周期时间改进之间取得平衡。
尽管在我们的案例中,讨论集中在一个特定的过程上,但相同的解决方案模式正被各行业和职能利用,以提取成本效率并加速成果。那么,GenAI 解决方案如何帮助这样的过程呢?
LLMs 的独特能力
在最* GenAI 人气激增之前,企业界的自动化解决方案主要针对常规的、基于规则的任务,或依赖于机器人流程自动化(RPA)。机器学*应用主要围绕分析展开,例如使用回归模型预测销售量等结果。然而,最新的 LLMs 由于其卓越的特性而脱颖而出:
-
内容理解: LLMs 能够“理解”文本的含义
-
即时训练: LLMs 能够执行其原始训练中未涉及的新任务(即零样本学*),通过自然语言指令和可选的少量示例(少样本学*)
-
推理: LLMs 能够在一定程度上“思考”和“推理”潜在的行动(尽管存在一些限制和风险)
在“传统”机器学*中,构建和使用模型的过程通常涉及收集数据、手动定义“目标”,并训练模型以预测给定其他属性的“目标”。因此,模型可以执行一个特定的任务或回答一个特定类型的问题。相反,你可以要求一个经过预训练的 LLM 评估客户评论中对你的业务重要的特定方面,这些方面是 LLM 从未见过的,也没有在评论中明确提到。
LLM-based 解决方案的机制
行业内的许多 LLM 解决方案集中于设计和提供详细的指令,以使 LLM 执行特定任务(这被称为提示工程)。一种增强 LLM 影响力的有效方式是通过自动化方式使其访问公司的专有信息。检索增强生成(RAG)已成为实现这一目标的最常见解决方案模式之一。
概述 — 10,000 英尺视角

GenAI 解决方案工作流的高级概览(来源:作者提供的图像)
简而言之,解决方案有两个阶段:
-
搜索: 检索与用户请求相关的公司数据。例如,如果要求以特定格式或风格撰写报告,则会提取并将以前报告的文本发送给 LLM 作为示例。
-
生成: 将在先前阶段检索到的指令和示例(或任何其他相关信息)编译成文本提示,并将其发送给 LLM 以生成所需的输出。以报告为例,提示可以是,
请将以下信息编写成报告,使用提供示例的格式和风格。
这里是内容:[报告内容…. ]。
这是示例:之前的报告标题
第一部分 …
第二部分 …
结论
RAG 工作流 — 1,000 英尺视角
让我们深入了解解决方案模式中的搜索和生成两个阶段。

使用Cohere 的 embed-english-v2.0 模型的示例句子的向量嵌入(即数值表示)(来源:作者提供的图像)
这些数值表示如何映射到二维空间中是很有趣的。可以看到,相似主题的句子被映射得很*。

示例句子的向量嵌入绘制在二维空间中(来源:作者提供的图像,灵感来自课程具有语义搜索的大型语言模型中的演示,课程由Deeplearning.ai提供)
2. 创建 知识库: 解决方案的搜索组件接收一个问题,并进行语义搜索以寻找知识库中最相似的信息。那么,这个知识库是如何创建的呢?需要放入知识库的文件会由嵌入模型处理,从而创建数值表示。这些数值表示会被加载到一个专门的数据库中——通常是为高效存储和快速检索这种信息而特别设计的向量数据库。
3. 在知识库中检索相似信息(即检索): 当用户提交问题或任务到解决方案时,解决方案使用嵌入模型将问题文本转换为数值表示。这个问题向量会与知识库进行匹配,以寻找最相似的信息。可能会返回一个或多个搜索结果,这些结果可以传递到下一阶段以生成回应或输出。
4. 使用 LLM 生成输出(即生成): 现在,解决方案已经成功找到可以帮助 LLM 生成有意义输出的相关信息,整个包,即“提示”,可以发送到 LLM。这个提示包括一个或多个标准的指令集,这些指令引导 LLM,还有实际的用户问题,最后是检索阶段得到的信息。LLM 生成的结果可以在必要时进行处理(例如,将输出加载到特定格式的 Word 文档中),然后再交付给用户。
深入了解解决方案组件
让我们更深入地探讨解决方案的组件

深入探讨 RAG 解决方案工作流的组件(紫色块显示可选组件)。(来源:作者提供的图片)
1. 创建知识库: 将相关文档加载到知识库中有一些细节和考虑因素。
-
文档加载: 可能需要将不同的相关文档(pdf、word、在线资源等)导入到数据存储库中。根据用例,某些文档的只有特定部分可能是相关的。例如,对于为金融分析师设计的查询公司 10-K 报告的解决方案,标题页、目录、标准合规信息和一些附录可能与财务分析无关。因此,这些部分可以从知识库中省略。避免知识库中的冗余信息至关重要,以确保 LLM 模型提供多样化和高质量的回应。
-
文档拆分: 一旦确定了需要包含在知识库中的相关文档部分,下一步是确定如何拆分这些信息并将其加载到向量数据库中。选择方式可能因使用场景而异。一种有效的方法是按段落拆分并留有一定重叠。这涉及到为保留完整段落设置字数(或“令牌”,LLM 用于文本处理的单位)限制。如果段落超过此限制,则应将其拆分为多个记录以存储在向量数据库中。通常会故意保留一些词汇重叠以保持上下文。例如,使用每个向量 1,000 字的限制,并有 40 字的重叠。
-
附加元数据: 增强知识库中的信息涉及用有意义的元数据标记每条记录。基本的元数据示例包括提取信息的原始文档标题和章节层级。附加元数据可以进一步提升搜索和检索的质量。例如,从 10-K 报告中提取的资产负债表数据可以用以下元数据进行标记:
原始文档标题:公司 XYZ 10-K
年份:2022
部分:财务报表及附加数据 | 资产负债表
- 存储: 存储信息的选项有很多。可以使用如 Chroma 或 Faiss 之类的向量数据库解决方案,或在 Postgres / MySQL 上使用这些解决方案。然而,SQL 数据库、NoSQL 数据库、文档存储、图数据库也可以使用。此外,还可以考虑使用内存存储以减少延迟,以及横向扩展以提高可伸缩性、可用性、负载均衡等。
2. 从知识库中检索相似信息: 对于简单的使用案例,基于在知识库中搜索相似向量的检索方法,如前一部分所述,应该足够。一种常见的两阶段方法可以平衡搜索速度与准确性:
-
密集检索: 最初,通过快速*似邻居搜索对广泛的知识库进行快速扫描,以处理搜索查询。这将产生几十个或几百个结果供进一步评估。
-
重新排序: 在获取的候选项中,可以使用更计算密集的算法来区分更相关和不太相关的结果。可以通过对密集检索阶段获取的候选项进行二次处理,或使用其他特征(如指向每个搜索结果的链接数量(表示可信度或主题权威)、TF-IDF 分数,或直接请求 LLM 审查所有候选项并对其相关性进行排名)来计算额外的相关性分数。
对于高级功能,例如通过选择多样化的信息、基于自然语言用户提示应用过滤器等来提高搜索结果的质量,可能需要更复杂的方法。例如,在财务查询中,用户问:“XYZ 公司在 2020 年的净利润是多少?”解决方案必须过滤出关于 XYZ 公司的文件以及 2020 年的数据。一个可能的解决方案是使用 LLM 将请求拆分为过滤组件,通过使用元数据按年份 2020 进行过滤,从而缩小语义搜索的目标范围。然后,执行语义搜索以在知识库中定位“XYZ 公司的净利润”。
3. 使用 LLM 生成输出(生成): 过程的最后一步涉及使用 LLM 生成输出。
-
直接方法: 直接方法是将从搜索阶段检索到的所有信息连同人工提示和指令传递给 LLM。然而,对于 LLM 能处理的信息量存在限制。例如,Azure OpenAI 基础 GPT-4 模型的上下文大小为 1024 个令牌,大约相当于 2 页文本。根据使用情况,可能需要对这一限制进行变通处理。
-
链式方法: 为了绕过上下文大小限制,一种方法是逐步向语言模型提供信息,并指示它在每次迭代中构建和完善答案。LangChain 框架提供了如“refine”、“map_reduce”和“map_rerank”等方法,以帮助生成多个答案部分,并最终通过另一个 LLM 调用将它们组合起来。
结论
在数据生成不断增加的时代,利用 GenAI,我们的上下文感知和可训练助手,比以往任何时候都更具影响力。正如文章中所述,这一解决方案模式无缝地解决了自动化数据处理的挑战,并释放出人力资源以处理更复杂的任务。随着大型语言模型(LLM)的日益商品化和解决方案组件的标准化,可以预见,这些解决方案将很快变得普遍。
本文涉及了基础 RAG 概念。下一篇文章将通过构建一个能够回答基于任何指定网站信息的问题的聊天机器人来对这些概念进行实际探索。正如所说,最好的学*方式就是实践!
通过了解 LLM 基于产品的内部机制来做出更好的产品决策。
towardsdatascience.com
常见问题解答(FAQs)
-
生成的内容会成为 LLM 的记忆并影响未来的输出吗?例如,经验不足的用户生成的糟糕输出会影响其他用户的输出质量吗?
不会。在这种解决方案方法中,LLM 对其生成的内容没有“记忆”——每个请求都从头开始。除非 LLM 进行进一步的微调(训练)或生成的输出也被添加到知识库中,否则未来的输出不会受到影响。
-
LLM 是否会随着使用而学*并变得更好? 不会自动如此。RAG 解决方案模式并非一种强化学*系统。然而,可以设计解决方案,使用户能够对输出质量提供反馈,从而用以微调模型。更新知识库或使用升级的 LLM 也可以提高解决方案输出的质量。
-
向量嵌入会保存在源数据仓库中吗? 一般不会。虽然文档切块的向量可以在技术上存储在源数据仓库中,但源数据仓库和向量数据库(或者为了这个解决方案专门用于存储向量的 SQL 数据库)的目的不同。将向量添加到源数据库可能会创建操作依赖性和额外开销,这可能是不必要的或没有任何奖励的。
-
解决方案如何用新数据进行更新? 数据加载流程(识别文档、处理、切块、向量化、加载到向量数据库)需要在新数据可用时运行。这可以是一个定期批处理过程。更新频率可以根据使用案例进行调整。
-
我们如何确保知识库中存储的文档中的敏感信息不会被公众或 LLM 供应商访问?
企业可以使用 Azure OpenAI 服务作为单租户解决方案,配备私有实例的 OpenAI LLM。这可以确保数据隐私和安全。另一种解决方案是将 Hugging Face LLM 部署到公司的私有基础设施上,以确保数据不会离开公司的安全边界(与使用公开托管的 LLM 不同)。
推荐资源
探索这些资源以深入了解 LLM 及其应用:
想更深入了解 RAG 解决方案模式:
生成性人工智能如何支持食品行业企业
原文:
towardsdatascience.com/how-generative-ai-can-support-food-industry-businesses-993872b4a6ce
从过去的错误中学*,并利用 ChatGPT 为食品行业公司构建更好的机器学*模型
·发表于Towards Data Science ·12 分钟阅读·2023 年 7 月 19 日
--

介绍
我将带你踏上的旅程有两个重要原因。
-
它将展示如何使用 ChatGPT 来支持食品行业的公司。
-
可以说最重要的原因,我将详细讲解我在将*两年前发表的一篇文章,指出那篇文章中的问题,并尝试解决它们。
是的,我认为第二个原因更为重要。为什么?回顾过去的方法和过程,分析数据是重要的,因为它可以让你学*如何修正失败,这最终会导致成功。我绝非完美,我个人寻找过去做错的事情,希望从错误中学*,并为我支持的客户开发出更强大的模型。
原始出版物
我第一次发布《机器学*不仅仅是大科技的专利》是在 2021 年 7 月。
使用自然语言处理来支持小企业。
towardsdatascience.com
本文的目的是展示食品行业中的公司如何通过各种机器学*(ML)应用得到支持。我使用了自然语言处理(NLP)技术来处理关于公司的网络评论。我使用的一些 NLP 方法包括主题建模分析,以更好地了解客户在谈论什么,以及情感分析,以创建一个可以帮助预测未来评论情感并向公司提供反馈的模型。分析显示这两种方法都能够在小型数据集上进行。
啊!大错误。
我的数据并不理想。数据集不仅小,而且倾向于正面评论。这导致模型几乎总是预测评论为正面(对公司没有帮助),并且存在过拟合。
解决方案?我考虑使用生成对抗网络(GAN)来创建新的合成评论,但随后我想,能不能直接问 ChatGPT?轰。我原始工作中的第一个错误被解决了。我能够利用 ChatGPT 创建人工的正面和负面评论,这最终平衡了我的意大利食品公司评论数据集!
数据集
幸运的是,我在训练任何模型或进行任何分析之前,已经仔细验证了我创建的数据的可用性。请查看下面的帖子,提供对真实数据和人工数据的更深入分析。
调查食品行业公司如何通过 ChatGPT 生成的数据来支持评论和调查收集。
[towardsdatascience.com
正面评论创建
对于数据集,我希望正面和负面评论的比例均衡。首先,使用 ChatGPT,我查询它来创建正面评论。
创建 500 条关于不同意大利食品和产品的正面评论,并将其放入 CSV 文件中。
我至少这样做了 5 次,原因有二。一是 ChatGPT 不断超时。二是我想确保获得足够不同的评论。此外,为了增加生成数据的多样性,我每次都会更改查询。例如,我会将意大利食品和产品改为意大利甜点或意大利葡萄酒。让我们看看 ChatGPT 生成的一个虚假正面评论。
“我购买的佩科里诺托斯卡纳奶酪味道浓郁美味。它的质地坚实且易碎,带有一丝草味,非常适合刨丝、刮片或直接享用。
如果你问我,不错!
负面评论创建
为了创建负面评论,我遵循了相同的过程。我确实需要明确告诉 ChatGPT 我制作负面评论并不是为了伤害任何人,这确实是事实!我只是想要一个能泛化到模型可能遇到的所有数据类型(正面和负面评论)的分类模型和分析。
创建 100 条关于不同意大利食品的负面评论,并将它们放入 CSV 文件中。
生成的负面评论示例:
“我尝试的意大利火腿和芝麻菜比萨上有枯萎的芝麻菜,意大利火腿也很硬。它并不令人垂涎。”
如果你问我,结果还不错!
分析
第一个错误:
无法从‘keras.preprocessing.sequence’导入名称‘pad_sequences’ (/usr/local/lib/python3.10/dist-packages/keras/preprocessing/sequence.py)
解决方案:
→ 替代导入
from keras.preprocessing.sequence import pad_sequences
→ 使用导入:
from keras.utils import pad_sequences
大多数代码如以前一样工作,这很令人惊讶。由于 Keras 3.0 的最新发布,部分代码可能会被弃用,这取决于你使用的包和 IDE。
数据清理
在模型训练之前,还需要对评论进行一些额外的清理。幸运的是,可以在情感列中将“0”用于负面评论,将“1”用于正面评论,附加到每条评论中。
原始清理
虽然如果我添加新列,以下代码是可以实施的,但我决定使用 lambda 函数来清理数据集。
#Original Line from the first analysis
df['Label'] = [1 if x >=3 else 0 for x in df['Rating']]
此外,我决定将3的评分视为负面评分,这幸运地帮助平衡了数据集。
orig_reviews['Rating']=orig_reviews['Rating'].apply(lambda x: 0 if x < 4else 1)
我在清理过程中注意到原始帖子中有未定义的变量,这是我以前经常遇到的问题(说实话,我现在仍然有)。目前,我尝试通过重置内核并在将工作集成到我的帖子之前仔细检查来减轻这个问题。
原始数据集的问题

数据集不平衡(图像来自作者)
数据集最大的问题是正面评论和负面评论之间的巨大不平衡。最初有 512 条正面评论和 132 条负面评论。这为什么是个问题? 使用这个数据集训练模型很可能会导致一个大多数(几乎总是)将评论预测为正面的模型。我在原始帖子中确实忽视了这一点,应该更好地解决这个问题。虽然我不回避不平衡的数据集,但在我用尽所有努力并尝试各种技术(包括进一步的数据收集!)来平衡它们之前,我不会最初使用它们。
如果我们完全不训练模型而仅使用 BERT 会怎样?
如我们所见,目前数据集不支持强大的情感分析模型。BERT 在没有训练的情况下能够准确预测评论的情感吗?还是需要采取其他方法?
在没有训练的情况下,BERT 的准确率为68.27%。由于许多评论使用了非结构化和模糊的语言,BERT 无法理解,因此可能未能实现更高的准确率。

BERT 预测混淆矩阵(图片来自作者)
让我们通过混淆矩阵来深入了解 BERT 的预测。对于消极评论,BERT 正确了 76 次。56 个标签为积极,但应该是消极的。这可能是由于评论在给定状态下可能更为中立,但倾向于积极情感。例如,“这个地方以前不错,但今天的比萨饼不太好。” 作为人类,我们阅读这句话会理解其表达了主要的消极情感。另一方面,BERT 可能会将句子中连续使用的“好” 作为预测该句子为积极的依据。
对于积极评论,BERT 实现了更准确的分类。BERT 正确预测了 363 次评论为积极。然而,一个失败是,BERT 将 149 个预测为积极的评论误认为是消极的。
我想概述并讨论混淆矩阵中的信息,因为它显示了算法理解人类语言的困难,特别是当不同的语言特征被纳入时,例如讽刺和说话者希望传达的情感。使用 BERT 而不进行训练的目的是查看是否需要对公司进行模型微调,还是可以使用现成的模型。在准确率低于 80%的情况下,我建议微调一个模型,以更好地与公司的数据对齐。
模型创建
我注意到在我最初的帖子中,我使用了卷积神经网络(CNN)来开发我的模型。根据我的记忆,我这样做是因为我在已发布的研究中大量使用 CNN(使用混合生成对抗网络图像来增强分类模型训练数据集的好处*)。虽然使用 CNN(在这种情况下是 1-D)并没有错,但我还想看看其他可能对数据集提供更好预测的模型。
D = 20
i = Input(shape = (T,))
x = Embedding(V +1 , D)(i)
x = Conv1D(16,2,activation='relu',)(x)
x = MaxPooling1D(2)(x)
x = Conv1D(32,2,activation='relu')(x)
x = GlobalMaxPooling1D()(x)
x = Dense(1,activation='sigmoid')(x)
model = Model(i,x)
新模型
虽然创建自己的模型对我的学*很有帮助,但我决定进行迁移学*,并在一个知名模型BERT上训练我的新情感分析(SA)模型。
请参见我使用的完整 BERT 代码。
为什么 迁移学*? 为什么要重新发明轮子,当有强大的模型可以根据你的问题进行调整时? 每当遇到问题时,总是要做研究,看看其他人在过去完成了什么。你可能会惊讶地发现,很多人遇到过相同的或类似的问题,并且已经为你解决了!
结果
总体而言,我没有准确描述(或者说,根本没有概述)我原始数据集如何导致模型偏向和严重过拟合数据集。
训练:无数据增强
如前所述,原始模型因正负标签的极端不平衡而过拟合数据集。导致“更好”模型的一些变化包括将训练和测试集的划分从 80/20 改为 70/30,以及在 BERT 模型中添加更多的 dropout(我使用了 0.5)。

训练/测试损失与准确度:原始数据集(图片来自作者)
如你所见,在模型训练过程中,损失稳定在 50%(这是可以预期的)。当我使用 70/30 的划分进行训练时,最大的变化是训练集的损失值从 ~80% 降至 ~60%。总体而言,模型在不平衡的数据集上表现不佳。
增强数据集
BERT 如何解读虚假评论?
BERT 的表现不佳,准确率为 38.22%。我不把全部责任归咎于 BERT 模型。ChatGPT 生成的数据在区分正面和负面评论方面缺乏明确性,这清楚地表明 ChatGPT 生成的数据很大程度上基于过去的模式,而不具备意识。

BERT 混淆矩阵与生成数据(图片来自作者)
在总体混淆矩阵中,最显著的问题是 BERT 将许多评论错误分类为正面,而实际上它们是负面的。为什么?首先,ChatGPT 需要生成的负面评论比正面评论更多。显然,ChatGPT 用于创建负面评论的模式与正面评论过于相似,这突显了数据生成的一个缺陷,以及 ChatGPT 无法在不同类别的数据中产生多样性,这些数据与现实世界的信息不一致。
使用增强数据集对 BERT 进行微调

平衡数据集(图片来自作者)
使用 ChatGPT 后,数据集被平衡为包含 1,126 条正面评论和 1,124 条负面评论(额外的 614 条正面评论和 992 条负面评论)。使用生成式 AI 算法的一个好处是它们能够平衡数据集,特别是像这样的巨大不平衡数据集。 缺点?新生成的数据可能无法代表原始数据,这需要考虑到。
一旦数据集平衡,我们可以尝试再次微调 BERT 进行情感分析,使用之前相同的过程。一个明显的变化是,由于生成的评论(150 → 60)导致每个嵌入的单词数量减少了。

训练/测试损失与准确率:增强数据集(图片来自作者)
成功!数据集增强改善了 BERT 模型的微调。在 5 个周期后,准确率达到了99.67%,损失很小。测试损失始终维持在约 13.3%左右,第五周期的准确率为 97.3%。
这个模型如何惠及使用它的食品行业公司? 虽然使用公司内部的数据可能会导致一个更个性化、与其流程对齐的模型,但使用外部数据可以帮助公司提供一个更通用、适应现实世界变化的模型。这些变化几乎是不可避免的,模型可能会对此感到陌生,拥有一个能够适应现实世界不可预测性的模型可以减少失败,并作为对模型做出不良决策的缓冲。
结论
生成式 AI 最*迅猛发展,找到适合并对不同产业公司有积极和有益的用例非常重要。对于食品行业的公司,或任何拥有产品及其评论的公司,ChatGPT 可以帮助支持能够将评论标记为正面和负面的模型,以支持业务运营和产品开发。虽然 ChatGPT 可以用于自动化,但我们应该警惕其强大功能,采取人类参与的方法,评估生成的数据。不论你使用何种生成式 AI 技术进行数据集开发,确保数据真实反映现实世界信息,以期创造出表现最强的模型。
从个人角度来看,今天展示了我们如何不断努力变得更好,我们必须从过去的不佳表现中学*以取得进步。从错误和弱点中学*是数据科学家最重要的部分之一,最终将促成一个以卓越和持续发展为核心的职业生涯。
如果你喜欢今天的阅读,请关注我,并告诉我是否有其他主题你希望我深入探讨!如果你没有 Medium 账号,通过我的链接 这里 (这样我会获得少量佣金)注册吧!此外,可以在 LinkedIn 上添加我,或者随时联系我!感谢阅读!
来源
- 数据使用经 Altomontes Inc.批准。
代码
# Set the model name
MODEL_NAME = 'bert-base-cased'
# Build a BERT based tokenizer
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)
# Store length of each review
token_lens = []
# Iterate through the content slide
for txt in df.content:
tokens = tokenizer.encode(txt, max_length=512)
token_lens.append(len(tokens))
MAX_LEN = 160
class GPReviewDataset(Dataset):
# Constructor Function
def __init__(self, reviews, targets, tokenizer, max_len):
self.reviews = reviews
self.targets = targets
self.tokenizer = tokenizer
self.max_len = max_len
# Length magic method
def __len__(self):
return len(self.reviews)
# get item magic method
def __getitem__(self, item):
review = str(self.reviews[item])
target = self.targets[item]
# Encoded format to be returned
encoding = self.tokenizer.encode_plus(
review,
add_special_tokens=True,
max_length=self.max_len,
return_token_type_ids=False,
pad_to_max_length=True,
return_attention_mask=True,
return_tensors='pt',
)
return {
'review_text': review,
'input_ids': encoding['input_ids'].flatten(),
'attention_mask': encoding['attention_mask'].flatten(),
'targets': torch.tensor(target, dtype=torch.long)
df_train, df_test = train_test_split(df, test_size=0.2, random_state=RANDOM_SEED,stratify=df.sentiment)
def create_data_loader(df, tokenizer, max_len, batch_size):
ds = GPReviewDataset(
reviews=df.content.to_numpy(),
targets=df.sentiment.to_numpy(),
tokenizer=tokenizer,
max_len=max_len
)
return DataLoader(
ds,
batch_size=batch_size,
num_workers=0
)
BATCH_SIZE = 16
train_data_loader = create_data_loader(df_train, tokenizer, MAX_LEN, BATCH_SIZE)
test_data_loader = create_data_loader(df_test, tokenizer, MAX_LEN, BATCH_SIZE)
print(df_train.shape, df_test.shape)
bert_model = BertModel.from_pretrained(MODEL_NAME,return_dict=False)
# Build the Sentiment Classifier class
class SentimentClassifier(nn.Module):
# Constructor class
def __init__(self, n_classes):
super(SentimentClassifier, self).__init__()
self.bert = BertModel.from_pretrained(MODEL_NAME,return_dict=False)
self.drop = nn.Dropout(p=0.5)
self.out = nn.Linear(self.bert.config.hidden_size, n_classes)
# Forward propagaion class
def forward(self, input_ids, attention_mask,return_dict):
_, pooled_output = self.bert(
input_ids=input_ids,
attention_mask=attention_mask,
return_dict=False
)
# Add a dropout layer
output = self.drop(pooled_output)
return self.out(output)
# Instantiate the model and move to classifier
model = SentimentClassifier(2)
model = model.to(device)
# Number of iterations
EPOCHS = 10
# Optimizer Adam
optimizer = AdamW(model.parameters(), lr=2e-5, correct_bias=False)
total_steps = len(train_data_loader) * EPOCHS
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=0,
num_training_steps=total_steps
)
# Set the loss function
loss_fn = nn.CrossEntropyLoss().to(device)
# Function for a single training iteration
def train_epoch(model, data_loader, loss_fn, optimizer, device, scheduler, n_examples):
model = model.train()
losses = []
correct_predictions = 0
for d in data_loader:
input_ids = d["input_ids"].to(device)
attention_mask = d["attention_mask"].to(device)
targets = d["targets"].to(device)
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
return_dict=True)
_, preds = torch.max(outputs, dim=1)
loss = loss_fn(outputs, targets)
correct_predictions += torch.sum(preds == targets)
losses.append(loss.item())
# Backward prop
loss.backward()
# Gradient Descent
nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
optimizer.zero_grad()
return correct_predictions.double() / n_examples, np.mean(losses)
def eval_model(model, data_loader, loss_fn, device, n_examples):
model = model.eval()
losses = []
correct_predictions = 0
with torch.no_grad():
for d in data_loader:
input_ids = d["input_ids"].to(device)
attention_mask = d["attention_mask"].to(device)
targets = d["targets"].to(device)
# Get model ouptuts
outputs = model(
input_ids=input_ids,
attention_mask=attention_mask,
return_dict=True)
_, preds = torch.max(outputs, dim=1)
loss = loss_fn(outputs, targets)
correct_predictions += torch.sum(preds == targets)
losses.append(loss.item())
return correct_predictions.double() / n_examples, np.mean(losses)
生成式 AI 将如何影响产品工程团队
原文:
towardsdatascience.com/how-generative-ai-will-impact-product-engineering-teams-83a5eaa8fc60

作者使用 Midjourney 制作的图像
生成式 AI 编码和生产力工具对构建软件产品的团队会产生什么影响?
·发表于 Towards Data Science ·阅读时间 7 分钟·2023 年 7 月 25 日
--
我认为我们现在可以安全地假设,关于生成式 AI 工具 —— 如 ChatGPT、Midjourney 和 Dall-E —— 将对我们的工作方式产生影响,已经没有多少争议。剩下的问题实际上是“影响会有多大?”和“何时发生?”
在过去几个月里,我经常发现,当我考虑技术团队的人员配置时,我最终会质疑一个我多年来一直可以依赖的相当稳定的模型。随着生成式 AI 工具的演变速度和投入资金的数量不断增加,有一个想法一直困扰着我:
下一代产品工程团队可能会比现在拥有更少的工程师。
在这六篇文章的系列中,我将探讨当前构建数字产品的团队的现状和结构,最*几个月显著的变化,以及新一代 AI 编码工具对团队本身可能产生的影响。到文章结束时,技术、数据和产品领导者,以及工程师们,应该能够理解这一变化的范围,并有一个基础来考虑他们将如何应对。
这个系列主要针对对产品开发有一定理解的技术、数据和产品领导者,但我尽量在上下文中解释任何更技术性的概念。这不是对 AI 工具的详细探索或比较,而是它们可能对团队产生的影响。
今天我们所处的位置 — 5:1 比例
五比一。这是我对构建数字产品的团队中每个产品经理所需工程师数量的一个非常粗略的经验法则。
我的日常工作是作为顾问首席技术官(CTO),为高管团队提供建议,或直接与产品和技术领导者一起工作,帮助制定他们的战略和产品与技术团队的结构。当有人询问如何构建他们的产品工程团队时,我的回答通常是‘由五到九人组成的跨职能团队,具备交付业务成果所需的所有技能’。
无论公司规模如何,这些建议保持惊人的一致。当初创公司处于发展的初期阶段,工程团队迅速壮大时,十到十二人的团队通常是团队规模开始造成拖延和缺乏清晰度的点。在这个关键的拐点,当工作量超过单一团队的承载能力时,就该进行扩展了。一个团队分裂成两个团队,两个团队分裂成三个团队,每个团队有五到九人,如同有丝分裂一般。
在规模较大的公司中,随着多个团队部落的扩展,这种模型保持一致。我负责过的最大技术团队大约有 300 名工程师,属于一个约 450 人的产品和技术团队。即便在这样的整体规模下,团队层级的比例仍然主导着 5:1——大约五名工程师和一名产品经理,通常还有一些额外角色的独特变化,如数据科学家、机器学*工程师、设计师、用户体验(UX)专家、敏捷教练、质量保证(QA)人员和开发运维(dev ops),以填补人数需求。
什么构成了一个团队?
每个产品工程团队和每个首席技术官(CTO)都是不同的。虽然我个人偏好产品经理了解他们的客户,对商业结果负责,扫描创新前沿,并直接与团队合作以优先处理工作,但一些公司选择将产品负责人和产品经理角色分开。我倾向于选择跨团队工作的敏捷教练,但有些人更喜欢在团队内嵌的敏捷教练。有些人坚信测试和质量保证(QA)作为团队内的具体角色的重要性,但我更倾向于将质量责任推给编写代码的人,并尽可能地‘左移’。产品设计和用户体验研究可能在团队内,或者作为对团队的服务提供。有成千上万种配置,没有特定的对错答案,且始终需要根据组织的背景来调整。
如果有人让我创建一个理想的团队,我会要求一个产品负责人和一个技术负责人,他们具有相当的资历,并且彼此之间存在创意上的紧张关系。这两个角色的重要性在于代表(并对其不同观点进行争论)。客户满意度。成本。收益。风险。价值。正如我经常强调的:“产品决定构建什么,技术决定如何构建”。
过去 20 年左右,自从敏捷方法成为软件开发的主要方法以来,产品工程团队通常会有一个产品负责人和一些不同资历的工程师。在今天的世界中,工程师是产品交付的实际单位——没有工程师将需求转化为结果,我们就无法交付任何产品。
工程师不仅仅是构建新产品和功能;他们还负责保持现有产品的适用性和健康状态。工程师一周中的很大一部分时间可能会花在维护现有产品和系统上;修复漏洞、升级版本、回应小的变更请求和处理安全问题。他们剩余的时间可能会用来解决更有创意的问题,构建新能力或实施新的技术解决方案。此外,大多数工程团队都有需要时间学*和培训的初级工程师,还有更资深的工程师在不断变化的技术面前维持现状。
团队中产品经理的角色至关重要。如果工程师是我们价值交付的单位,产品经理也肩负着同样重要的责任;引导客户和利益相关者的需求,帮助团队以创意方式将挑战转化为解决方案,分析并展示结果的价值,确定价值交付的优先顺序,然后证明所有努力的投资回报。
在我们有五名工程师编写代码的团队中,“一个”几乎是理想的产品经理数量,可以保持工程师的参与度、生产力以及提供优质工作的供应。
直到现在为止。
产品工程中的生成式 AI 崛起
过去几个月我与许多 CEO 交谈时,他们专注于如何将“AI”硬生生地融入他们的产品中,表现出一种介于歇斯底里和绝望之间的热情。我对他们的建议是,应该将“真正”的 AI 工作留给那些拥有风险投资资金的人,自己则只需集成 OpenAI、微软、Salesforce、亚马逊和谷歌在未来几个月内提供的服务。
暂时搁置生成式 AI(我将用来泛指 GPT、LLMs 等)的新产品应用,我认为有一个更有趣且战略性的讨论——当生成式 AI 真正准备好投入生产时,我们的团队将会是什么样子。
面对美国的SAG-AFTRA 和 WGA 演员及编剧罢工,回顾麦肯锡在 2017 年对 AI 对劳动力影响的预测具有启示性。经过大量的研究和分析,麦肯锡对可能受到 AI 影响的职业进行了排序。麦肯锡提出,“创意工作者” 这一小而不断增长的艺术家、表演者和娱乐工作者类别将受到的影响最小,因为随着收入的增加,这类工作者的需求也会增加。麦肯锡认为,娱乐工作者就像现在罢工的编剧和演员一样,不仅不会避免 AI 的影响,反而会从中受益。 对于任何关于 AI 影响的预测(包括你正在阅读的这篇文章),这里有一个警示。
当然,麦肯锡的水晶球和其他人的一样并不神奇,我引用六年前的研究也有些古板。尽管如此,看到 AI 的影响没有被世界上最大的创意产业忽视,反而成为创意工会对制片厂和制作人的主要投诉之一,这还是令人感到振奋的。从对漫威《秘密入侵》中使用生成 AI 艺术的抗议到(有争议的)演员可能需要将其数字肖像权利永久转让给 AI 效果的说法,有一点非常明确:创意产业在不久的将来无疑将受到 AI 的影响。
为什么在一篇关于产品工程团队的文章中要绕道谈论创意产业呢?好吧,猜猜麦肯锡在其“安全”职业的大名单中,接下来最不容易受到 AI 影响的是什么?根据麦肯锡,接下来最不容易受到 AI 影响的群体将是“IT 专业人员和其他技术专家、管理人员和高管,他们的工作不容易被机器替代”。
麦肯锡早在 2017 年的观点是,“自动化对涉及管理人员、应用专业知识和社会互动的工作影响较小,因为目前机器无法与人类的表现相匹敌。” [link]。问题是,GPT 完全颠覆了算法在这些领域会挣扎的合理假设。2023 年的大型语言模型在广泛的话题中,包括软件开发,极具能力地展示了‘应用专业知识’的可信外观。
在第二部分中,我们探讨:
-
人工智能工具如 ChatGPT 如何彻底改变产品工程团队的编码方式——从生成用户故事到编写实际代码的过程。
-
开发者通常认为繁琐的任务,如编写测试和文档,现在可以被 AI 轻松高效地处理,使整个编码过程更加流畅。
-
深入探讨测试的重要性以及可能由深思熟虑的测试设计开始的提示工程应用的未来。
-
一种未来的愿景,即生成 AI 工具如何重新定义产品工程团队中的角色,以及对技术团队相比于产品管理的深远影响。
-
敬请关注这些发展将如何影响软件工程的世界以及成为开发者的本质。
本系列的其他文章:
附言:如果你喜欢这些关于团队的文章,可以看看我的Teamcraft 播客,在节目中我和我的共同主持人安德鲁·麦克拉伦与嘉宾讨论如何让团队更高效。
地理围栏如何塑造你周围的世界
地理围栏是一种基于位置的技术,为我们打开了新的可能性
·
关注 发布于 Towards Data Science ·11 分钟阅读·2023 年 2 月 7 日
--
DALL-E 所描绘的地理围栏概念(作者提供的图像)
随着我们迎接一个更加互联的世界,位置数据的使用日益重要。几乎地球上的每一个服务提供商或平台都在尝试通过了解用户的行为模式、参与度等来接触人们(和潜在客户)。而地理围栏技术在这一领域创造了一波新潮流。它允许组织向进入或离开特定地理区域的用户发送消息或警报。
*年来,地理围栏已成为企业、政府和个人越来越受欢迎的工具,它正在以多种方式改变我们周围的世界。凭借简单的实施过程和几乎可以覆盖全球所有电子设备的潜力,让我们来看看地理围栏如何塑造你周围的世界。
如果你想了解更多关于地理空间数据及其如何改变数据分析领域的信息,可以查看我关于该主题的博客 这里。还可以阅读我关于为什么需要使用地理包而不是 shapefile 或 geojson 的博客 这里。
什么是地理围栏?
地理围栏的工作原理基于空间关系。该服务部署在应用程序或软件中,通过 GPS、RFID、Wi-Fi 或蜂窝数据收集位置信息。它在任何给定的时间点跟踪移动设备或 RFID 标签的位置是否在围绕地理位置设置的虚拟边界内。这个虚拟边界被称为地理围栏(因其类似于围绕房地产的围栏)。一旦有设备进入或离开地理围栏的触发,服务会运行预先编程的操作,包括向设备发送优惠券、通知、短信、安全警报,并启用某些参与功能以进行定向广告等。
地理围栏可以用于各种目的。它允许监控安全区域的活动。
地理围栏是如何工作的?
利用地理围栏技术的第一步是建立一个虚拟边界,围绕应用程序或软件中的指定位置。
跟踪模式
地理围栏使用多种技术来跟踪设备的位置并确定它们是否在虚拟边界内。最常见的跟踪模式包括:
a. GPS(全球定位系统): GPS 是一种基于卫星的导航系统,可以确定设备的精确位置。
b. 蜂窝网络: 蜂窝技术可以根据设备与基站的接*程度来跟踪设备的位置。
c. Wi-Fi: Wi-Fi 技术可以根据设备与 Wi-Fi 网络的接*程度来跟踪设备的位置。
d. 蓝牙: 蓝牙技术可以根据设备与蓝牙设备的接*程度来跟踪设备的位置。
e. RFID(射频识别): RFID 是一种利用无线电波与设备或物体上的标签进行通信的技术。
f. IP 地址: 可以通过设备的 IP 地址来确定其位置。
目标模式
一旦确定了设备的位置,地理围栏可以用来触发各种目标模式。一些最常见的目标模式包括:
a. 品牌应用: 品牌可以利用地理围栏向已安装其应用并位于特定地理区域的用户发送通知或促销信息。
b. 网络广告: 网络广告可以针对位于特定地理区域的用户,基于其设备的位置进行定向投放。
c. 短信: 短信可以发送给位于特定地理区域的用户。
d. 第三方应用: 第三方应用可以利用地理围栏向用户提供基于位置的信息和服务。
e. 社交媒体广告: 社交媒体广告可以针对位于特定地理区域的用户,基于其设备的位置进行定向投放。
地理围栏的应用
车队管理
-
车辆追踪: 地理围栏可以用于实时追踪卡车、出租车和其他车辆的位置,使车队管理者能够监控车队的移动情况,并确保车辆按照预期使用。
-
路线优化: 通过使用地理围栏,车队管理者可以优化车辆的路线,以减少燃油消耗,提高安全性,并提高效率。这可以通过在禁入或限制区域周围创建地理围栏,以及利用交通模式和道路状况数据来制定最有效的路线来实现。
-
驾驶行为监控: 地理围栏可以用于监控驾驶员的行为,包括速度、加速度和刹车。这些数据可以用于改善驾驶*惯,减少燃油消耗,提高整体安全性。
-
合规监控: 地理围栏可以用于确保车辆在合规规定范围内运行,如卡车运输规定和服务时间规则。这可以帮助车队管理者避免处罚和罚款,并保持良好的声誉。
-
客户位置追踪: 对于共享出行和出租车公司,地理围栏可以用于追踪客户的位置,并将其与最*的可用车辆匹配。这可以帮助改善客户体验并减少等待时间。
-
资产管理: 地理围栏可以用于管理车队的资产,包括拖车和货物集装箱。通过使用地理围栏,车队管理者可以跟踪这些资产的位置,并确保它们按照预期使用。
营销与广告
-
基于位置的广告: 地理围栏可以用于根据用户的位置投放定向广告。例如,当用户进入一个购物中心的地理围栏时,他们可能会收到该购物中心内商店的广告。
-
事件推广: 地理围栏可以用于通过根据用户的位置投放定向广告来推广活动。例如,当用户进入一个活动场地的地理围栏时,他们可能会收到该活动的广告。
-
人流量分析: 可以使用地理围栏跟踪和分析特定地点的人流量,如零售店或购物中心,以更好地了解消费者行为并改善营销策略。
-
客户重定向: 可以使用地理围栏根据客户的过去行为(如位置历史)来投放定向广告,从而提高广告的相关性和有效性。
-
基于位置的优惠: 可以使用地理围栏根据用户的位置提供基于位置的优惠,如折扣和优惠券。
-
客户细分: 可以使用地理围栏根据客户的地理位置(如城市、州或区域)进行客户细分,以提高广告的相关性和有效性。
安全
-
无人机管理: 可以使用地理围栏来管理无人机的操作,例如,通过在无人机飞行禁区周围创建虚拟边界。这可以帮助防止无人机干扰其他飞行器或进入受限空域,从而提高整体安全性。
-
执法: 可以被执法机构用来监控高犯罪率区域并提高公共安全。通过在已知犯罪高发区域创建地理围栏,执法部门可以实时接收到犯罪发生的警报,并更迅速地做出回应。
-
物理安全: 可以使用地理围栏来监控高价值地点的物理安全,如政府大楼、核电站和其他关键基础设施。通过在这些地点周围创建地理围栏,安保人员可以实时接收到未经授权人员进入区域的警报,并更迅速地做出回应。
-
活动安全: 可以使用地理围栏来提升大型活动的安全性,如音乐会、体育赛事和政治集会。通过在活动场地周围创建地理围栏,安保人员可以实时监控人群移动,并在出现安全威胁时迅速做出反应。
区域划分
-
旅游: 可以使用地理围栏来促进特定区域的旅游活动,通过在热门旅游景点(如国家公园、历史遗址和文化景点)周围创建地理围栏。当游客进入地理围栏时,可以接收到定向广告、促销信息及其他营销消息。
-
国家公园: 可以使用地理围栏通过在特定区域(如徒步旅行小径、野餐区和野生动物保护区)周围创建地理围栏来提升国家公园的游客体验。游客可以实时获取关于公园及其景点的信息,如小径地图、野生动物 sightings 和公园规定。
-
度假村:地理围栏可以通过在度假村区域内创建地理围栏来提升度假体验,例如酒店、餐厅和水疗中心。客人可以实时获取有关度假村及其设施的信息,例如客房服务菜单、水疗项目和每日活动。
-
养老院:地理围栏可以通过在设施周围创建地理围栏来增强养老院居民的安全性和生活质量。如果居民离开设施,护理人员可以实时接收警报,这有助于防止漫游和脱逃。
自动驾驶
-
车辆安全:地理围栏可以用于通过在车辆不应进入的区域创建虚拟边界来确保自动驾驶车辆的安全操作。例如,可以在危险区域设置地理围栏,如施工现场、步行专用区和敏感环境。
-
自动驾驶区:地理围栏可以用于指定仅限自动驾驶车辆的区域,如机场、港口和工业园区。这使得自动驾驶车辆能够在没有人驾车干扰的情况下运行,并可以改善交通流量和减少事故风险。
-
基于地理位置的服务:地理围栏可以用于向自动驾驶车辆中的乘客提供基于位置的服务,例如导航、天气信息和交通更新。乘客可以实时获取有关车辆位置、目的地和预计到达时间的信息,以及有关周围地区的信息,例如交通状况、天气和兴趣点。
-
紧急响应:地理围栏可以通过在事故现场和其他紧急地点创建地理围栏来支持紧急响应工作。自动驾驶车辆可以被派往现场提供即时援助,例如运输医疗用品、设备和人员。
资产管理
-
设备追踪:地理围栏可以通过在工地、仓库和车间周围创建地理围栏来追踪设备,如建筑机械和车辆。这使得管理者能够实时监控设备的移动,减少盗窃和损坏的风险,并提高设备利用效率。
-
库存管理:地理围栏可以用于通过在仓库和存储区域周围创建地理围栏来管理库存水平。这使得管理者能够实时监控库存的移动,减少盗窃和损失的风险,并提高库存管理的效率。
-
工人安全:地理围栏可以通过在危险区域周围创建地理围栏来增强工人的安全,例如施工现场和矿区。这使得管理者能够实时监控工人的移动,减少事故风险,并在紧急情况下提高响应速度。
-
实时监控: 地理围栏可以用来实时监控资产的状态和健康,比如建筑机械和车辆。这使得管理者能够迅速应对任何问题,如故障和维护需求,减少停机时间并提高操作效率。
-
合规监控: 地理围栏可以用来监控法规遵守情况,如健康和安全法规,通过在受监管区域周围创建地理围栏。这使得管理者能够实时监控合规情况,减少罚款风险并提高整体合规性。
家庭自动化
-
家庭安全: 地理围栏可以用来增强家庭安全,通过在家周围创建地理围栏。这使得房主能够实时监控个人和车辆的移动,降低入侵和盗窃的风险。
-
智能家居控制: 地理围栏可以用来控制智能家居设备,如灯光、恒温器和家电,通过在家周围创建地理围栏。这使得房主可以自动化日常操作,比如离家时关灯,或到家时调整温度。
-
车辆追踪: 地理围栏可以用来追踪车辆的位置,比如汽车、摩托车和自行车,通过在家、工作地点或常去的地方创建地理围栏。这使得房主能够实时监控车辆的移动,降低盗窃风险并提高车辆利用效率。
-
提醒: 地理围栏可以用来根据位置设置提醒,比如到家时提醒服药,或离开工作地点时提醒去买杂货。
-
物联网集成: 地理围栏可以用来集成物联网(IoT)设备,如智能锁、摄像头和传感器,通过在家周围创建地理围栏。这使得房主能够实时监控设备的状态和健康,减少问题风险并提高操作效率。
社交网络
-
社交网络: 地理围栏可以用来提升社交网络体验,通过基于位置让用户找到并与他人连接。例如,约会应用可能会利用地理围栏根据用户之间的距离进行匹配。
-
基于位置的游戏: 地理围栏可以用来增强基于位置的游戏,如宝可梦 GO,通过让玩家根据位置与虚拟物体互动。
-
基于位置的奖励: 地理围栏可以用来根据位置向用户提供奖励。例如,奖励计划可能会利用地理围栏在用户进入参与商店周围的地理围栏时提供积分或优惠券。
设置地理围栏
-
定义地理围栏: 设置地理围栏的第一步是定义虚拟边界或地理围栏。这可以通过在地图上绘制一个圆圈或多边形,或使用一组 GPS 坐标来定义边界来完成。
-
选择跟踪方法: 一旦定义了地理围栏,下一步是选择跟踪方法。这将取决于具体的使用案例和需要跟踪的设备。例如,GPS 可能是跟踪户外位置的最佳选择,而 Wi-Fi 或蓝牙可能更适合室内跟踪。
-
与地理围栏平台集成: 要使用地理围栏,需要将其与地理围栏平台集成。该平台通常包括一个用于管理地理围栏、跟踪设备和目标营销活动的仪表板。
-
安装跟踪软件: 如果使用 GPS、Wi-Fi、蓝牙或其他跟踪模式,跟踪软件需要安装在需要跟踪的设备上。
-
测试和调整: 在设置好地理围栏平台后,重要的是测试系统以确保其正常运作。这可能包括调整地理围栏的大小和位置,或细化目标营销活动。
-
启动营销活动: 一旦地理围栏平台经过测试和调整,就可以启动目标营销活动。这可能包括向进入地理围栏的用户发送通知或促销信息,或向地理围栏内的用户展示基于位置的广告。
隐私问题
地理围栏是一项强大的技术,但也引发了诸多隐私问题,包括:
-
跟踪你的定位: 地理围栏依赖于跟踪用户的位置,以便提供定向广告、优惠和其他类型的内容。这种跟踪可以揭示用户的位置信息和日常*惯,可能会被用于恶意目的,例如身份盗窃或监视。
-
个人数据收集: 地理围栏需要收集个人数据,例如用户的位置,以便正常运作。这些个人数据可能容易受到泄露或未经授权的访问,从而导致敏感信息落入不当之手。
-
垃圾邮件: 地理围栏可能会导致用户被大量不必要的广告和优惠轰炸,这可能会造成烦扰并侵害他们的隐私。此外,恶意行为者可以利用地理围栏来发送垃圾邮件或钓鱼内容,这可能使用户面临身份盗窃或其他类型的诈骗风险。
-
Strava 热图揭示美国军事位置: 2018 年,揭露了健身追踪应用 Strava 通过其热图功能暴露了关于美国军事位置和行动的敏感信息。这突显了使用地理围栏和其他基于位置的技术来揭示敏感信息的风险。
-
在频繁访问医院时对疾病的预测:地理围栏技术在用于预测疾病或健康状况时也可能引发隐私问题。例如,如果一个人频繁访问医院,地理围栏技术可能预测他们有某种特定的健康状况,这可能是他们不希望公开的敏感信息。
用户了解这些隐私风险非常重要,公司需要实施强有力的隐私保护措施,以减轻这些风险,保护用户的个人数据和隐私。
结论
总之,地理围栏技术正在以多种方式改变我们周围的世界。从营销和广告到公共安全和农业,地理围栏对多个行业产生了重大影响。随着技术的不断发展,我们可能会看到未来更多创新和激动人心的应用。无论你是企业、政府还是个人,地理围栏都是一个可以帮助你充分利用地理位置并改善日常生活的工具。
谷歌如何利用虚假数据集来训练生成音乐 AI
原文:
towardsdatascience.com/how-google-used-fake-datasets-to-train-generative-music-ai-def6f3f71f19
·发表于 Towards Data Science ·6 分钟阅读·2023 年 5 月 28 日
--

图片由 James Stamler 提供,来源于 Unsplash
在这篇文章中,我们探讨了谷歌训练其杰出的文本到音乐模型(包括 MusicLM 和 Noise2Music)的创新方法。我们将深入了解“虚假”数据集的概念以及这些突破性模型如何利用它们。如果你对这些技术的内部运作及其对推动音乐 AI 的影响感到好奇,那么你来对地方了。
标签化音乐数据的缺乏
像 ChatGPT 或 Bard 这样的语言模型(LLMs)在大量非结构化文本数据上进行训练。虽然收集数百万个网站的内容可能会耗费大量计算资源,但公共网络上有丰富的训练数据。相比之下,像 DALL-E 2 这样的文本到图像模型需要完全不同类型的数据集,即配有相应描述的图像对。
同样地,文本到音乐模型依赖于带有音乐内容描述的歌曲。然而,与图片不同,互联网上很难找到标签化的音乐。有时,像乐器、风格或情绪这样的元数据是可以获得的,但完整的深入描述则极其难以获取。这对研究人员和公司收集数据以训练生成音乐模型构成了严重问题。
2023 年初,谷歌研究人员通过突破性的模型 MusicLM 和 Noise2Music 引起了音乐 AI 领域的广泛关注。然而,在音乐家中,对于这些模型的数据如何收集知之甚少。让我们一起深入探讨这个话题,了解谷歌音乐 AI 研究中使用的一些技巧。
谷歌如何克服数据稀缺
弱关联标签
对于 MusicLM 和 Noise2Music,谷歌依赖于另一个叫做 MuLan 的模型,该模型经过训练可以计算任何音乐片段和任何文本描述之间的相似性。为了训练 MuLan,谷歌使用了我们所说的“弱关联标签”。他们没有仔细策划一个高质量文本描述的音乐数据集,而是故意采取了不同的方法。
首先,他们从 YouTube 上的 4400 万个音乐视频中提取了 30 秒的片段,得到 37 万小时的音频。然后,音乐被标记上与视频相关的各种文本:视频标题和描述、评论、播放列表的名称等。为了减少数据集中的噪音,他们使用了一个大型语言模型来识别哪些关联文本信息包含与音乐相关的内容,并丢弃所有不相关的内容。
在我看来,弱关联标签不能被视为“伪造”数据集,因为文本信息仍然是由真实的人撰写的,并且与音乐在某种程度上确实有关。然而,这种方法确实优先考虑了数量而非质量,这在过去曾引起大多数机器学*研究人员的关注。而谷歌才刚刚开始……
伪标签
Noise2Music 是一个基于扩散技术的生成音乐 AI,这种技术也用于像 DALL-E 或 Midjourney 这样的图像生成模型。
为了训练 Noise2Music,谷歌将之前的方法推向极致,从弱关联标签过渡到完全人工标签。在他们称之为“伪标签”的方法中,作者采用了一种出色的方式来收集音乐描述文本。他们让一个大型语言模型(LaMDA)为 15 万首热门歌曲写多个描述,结果得到了 400 万个描述。以下是一个描述的示例:
皇后乐队的《Don’t Stop Me Now》:这首充满活力的摇滚歌曲由钢琴、贝斯吉他和鼓构成。歌手们充满激情,准备好出发,充满了振奋的感觉。
随后,研究人员移除了歌曲和艺术家的名字,生成的描述理论上可以适用于其他歌曲。然而,即使有了这些描述,研究人员仍然需要将它们与合适的歌曲匹配,以获得一个大规模标注的数据集。这时,MuLan,这个经过弱关联标签训练的模型,发挥了作用。
研究人员收集了一个大规模的未标记音乐数据集,总计 34 万小时的音乐。对于这些音乐曲目中的每一个,他们利用 MuLan 来识别最匹配的人工生成的歌曲描述。本质上,每段音乐并不是映射到描述歌曲本身的文本,而是映射到描述类似音乐的文本。
为什么这有效?
问题
在传统的机器学*中,分配给每个观察(在这种情况下是音乐片段)的标签理想情况下应该代表客观的真相。然而,音乐描述本质上缺乏客观性,这提出了第一个问题。此外,通过使用音频到文本映射技术,标签不再反映对歌曲发生的事情的“真实”表示。它们没有提供对音乐的准确描述。鉴于这些明显的缺陷,人们可能会怀疑为什么这种方法仍然能够产生有用的结果。
偏差 vs. 噪声
当数据集的标签分配不准确时,可能有两个主要原因:偏差和噪声。偏差指的是标签在某种特定方式上的一致性倾向不真实。例如,如果数据集经常将器乐曲标记为歌曲,但从不将歌曲标记为器乐曲,这就显示了对预测存在人声的偏向。
另一方面,噪声表示标签的一般变异性,无论方向如何。例如,如果每一首曲目都标记为“悲伤的钢琴曲”,那么数据集就会严重偏向,因为它对许多歌曲提供了一致的不准确标签。然而,由于它对每个曲目应用相同的标签,因此数据集中没有变异性,也就没有噪声。
通过将曲目映射到为其他曲目写的描述文本,我们引入了噪声。这是因为,对于大多数曲目而言,数据集中不太可能存在完美的描述。因此,大多数标签都稍有偏差,即不真实,这就产生了噪声。然而,这些标签是否有偏见?
由于现有的描述是为流行歌曲生成的,因此可以合理地假设这些描述池倾向于(西方)流行音乐。尽管如此,基于 150k 首独特歌曲的 400 万条描述,仍然可以期望有多样化的描述可供选择。此外,大多数标记的音乐数据集也表现出相同的偏向,因此这并不是这种方法相较于其他方法的独特劣势。真正使这种方法与众不同的是引入了额外的噪声。
噪声在机器学*中可以是可以接受的
在偏向的数据集上训练机器学*模型通常不是一种理想的方法,因为这会导致模型学*和复制对任务的偏见理解。然而,在无偏但有噪声的数据上训练机器学*模型仍然可以获得令人印象深刻的结果。让我用一个例子来说明。
请考虑下图,其中展示了两个数据集,由橙色和蓝色点组成。在没有噪声的数据集中,蓝色和橙色点是完全可分的。然而,在有噪声的数据集中,一些橙色点已移动到蓝色点簇中,反之亦然。尽管有了这些附加噪声,如果我们检查经过训练的模型,会发现两者识别的模式大致相同。这是因为即使在存在噪声的情况下,AI 也会学*识别最优模式,以尽可能减少错误。

一个 AI 模型在无噪声和有噪声数据上训练的例子。图像生成使用了 Tensorflow Neural Network Playground。
这个例子表明,AI 确实可以从噪声数据集中学*,例如谷歌生成的数据集。然而,主要的挑战在于数据集的噪声越大,所需的训练数据量就越大,以有效地训练模型。这一理由得到了支持,因为噪声数据集相对于大小相同的无噪声数据集,固有地包含较少有价值的信息。
结论
总之,谷歌采用了创新技术来应对训练其生成音乐 AI 模型时有限的标记音乐数据的问题。他们为 MuLan 使用了弱相关标签,利用了来自各种音乐视频相关来源的文本信息,并使用语言模型来过滤掉无关数据。在开发 Noise2Music 时,他们通过为热门歌曲生成多个描述并使用预训练模型将其映射到合适的曲目,从而引入了伪标签。
虽然这些方法可能偏离了传统的标记方法,但它们仍然证明了有效性。尽管引入了噪声,模型仍然能够学*并识别最佳模式。尽管使用伪造数据集可能被认为是不寻常的,但它突显了现代语言模型在创建大规模和有价值的数据集方面的巨大潜力。
我写了很多关于音乐和 AI 的文章。以下是你可能喜欢的三篇文章:
-
MusicLM — 谷歌解决了 AI 音乐生成的问题了吗?
-
AudioGPT — 未来音乐创作的瞥见
参考文献
[1] Huang 等人 (2022)。Mulan:音乐音频与自然语言的联合嵌入。 arxiv.org/abs/2208.12415
[2] Agostinelli 等人 (2023)。MusicLM:从文本生成音乐。 arxiv.org/abs/2301.11325
GPT 模型的工作原理
了解 OpenAI 的 GPT 模型背后的核心概念
·发表于 Towards Data Science ·14 分钟阅读·2023 年 5 月 20 日
--

介绍
我在 2021 年写了第一段使用 GPT 模型的代码,那时我意识到文本生成已经达到了一个拐点。在那之前,我在研究生阶段从零开始编写过语言模型,也有使用其他文本生成系统的经验,所以我知道让它们产生有用的结果有多么困难。我有幸在 Azure OpenAI 服务发布时获得了 GPT-3 的早期访问权限,并在其发布前进行了尝试。我要求 GPT-3 总结一份长文档,并尝试了一些少样本提示。我发现结果远远超出了之前模型的水平,这让我对这项技术感到兴奋,并渴望了解它是如何实现的。现在,后续的 GPT-3.5、ChatGPT 和 GPT-4 模型正迅速获得广泛应用,更多领域的人也对它们的工作原理充满好奇。尽管它们的内部工作细节是专有且复杂的,但所有 GPT 模型共享一些基本概念,这些概念并不难理解。我的目标是解释语言模型的一般核心概念,特别是 GPT 模型的核心概念,解释面向数据科学家和机器学*工程师。
生成式语言模型的工作原理
让我们开始探索生成式语言模型的工作原理。最基本的想法如下:它们将n个标记作为输入,生成一个标记作为输出。

这看似是一个相当简单的概念,但为了真正理解它,我们需要知道什么是标记。
一个标记是文本的一部分。在 OpenAI GPT 模型的上下文中,常见且简短的词通常对应一个标记,例如下图中的“我们”一词。较长且不常用的词通常会被拆分为多个标记。例如,下图中的“人性化”一词被拆分为三个标记。像“ChatGPT”这样的缩写可能被表示为一个标记或拆分为多个标记,这取决于这些字母出现在一起的频率。你可以访问 OpenAI 的 Tokenizer page,输入你的文本,并查看它如何被拆分成标记。你可以在“GPT-3”分词(用于文本)和“Codex”分词(用于代码)之间进行选择。我们将保持默认的“GPT-3”设置。

你还可以使用 OpenAI 的开源 tiktoken 库通过 Python 代码进行分词。OpenAI 提供了几种不同的分词器,每种分词器的行为略有不同。在下面的代码中,我们使用了用于“davinci”(一个 GPT-3 模型)的分词器,以匹配你在 UI 中看到的行为。
import tiktoken
# Get the encoding for the davinci GPT3 model, which is the "r50k_base" encoding.
encoding = tiktoken.encoding_for_model("davinci")
text = "We need to stop anthropomorphizing ChatGPT."
print(f"text: {text}")
token_integers = encoding.encode(text)
print(f"total number of tokens: {encoding.n_vocab}")
print(f"token integers: {token_integers}")
token_strings = [encoding.decode_single_token_bytes(token) for token in token_integers]
print(f"token strings: {token_strings}")
print(f"number of tokens in text: {len(token_integers)}")
encoded_decoded_text = encoding.decode(token_integers)
print(f"encoded-decoded text: {encoded_decoded_text}")
text: We need to stop anthropomorphizing ChatGPT.
total number of tokens: 50257
token integers: [1135, 761, 284, 2245, 17911, 25831, 2890, 24101, 38, 11571, 13]
token strings: [b'We', b' need', b' to', b' stop', b' anthrop', b'omorph', b'izing', b' Chat', b'G', b'PT', b'.']
number of tokens in text: 11
encoded-decoded text: We need to stop anthropomorphizing ChatGPT.
你可以在代码的输出中看到,这个分词器包含 50,257 个不同的标记,每个标记在内部映射为一个整数索引。给定一个字符串,我们可以将其拆分为整数标记,并将这些整数转换为它们对应的字符序列。对字符串进行编码和解码应始终返回原始字符串。
这给你一个关于 OpenAI 分词器如何工作的良好直觉,但你可能会想知道他们为何选择这些标记长度。让我们考虑一些其他的分词选项。假设我们尝试最简单的实现,即每个字母都是一个标记。这使得将文本拆分为标记变得容易,并且保持不同标记的总数较小。然而,我们无法编码像 OpenAI 方法那样多的信息。如果我们在上述示例中使用基于字母的标记,11 个标记只能编码“我们需要”,而 11 个 OpenAI 的标记可以编码整个句子。事实证明,当前的语言模型对它们可以接收的最大标记数量有限。因此,我们希望在每个标记中尽可能地打包更多信息。
现在让我们考虑一种情况,每个单词都是一个标记。与 OpenAI 的方法相比,我们只需七个标记即可表示相同的句子,这似乎更高效。而且按单词拆分也很容易实现。然而,语言模型需要一个完整的标记列表,以便处理它们可能遇到的标记,这对于整个单词来说是不可行的——不仅因为词典中的单词太多,还因为难以跟上特定领域的术语和任何新发明的词汇。
因此,OpenAI 选择了这两种极端之间的某种解决方案也就不足为奇了。其他公司也发布了采用类似方法的分词器,例如 Google 的 Sentence Piece。
现在我们对令牌有了更好的理解,让我们回到原始图示,看看是否可以更好地理解它。生成模型接受 n 个令牌,这些令牌可以是几个词、几个段落或几页内容。它们输出一个单一的令牌,这可能是一个短词或词的一部分。

现在这就更有意义了。
但如果你使用过 OpenAI 的 ChatGPT,你会知道它产生了许多令牌,而不仅仅是一个令牌。这是因为这个基本思想以扩展窗口模式应用。你给它 n 个令牌,它输出一个令牌,然后将这个输出令牌作为下一次迭代的输入的一部分,产生一个新的令牌,依此类推。这个模式不断重复,直到达到停止条件,表明它已经生成了你需要的所有文本。
例如,如果我向我的模型输入“我们需要”,算法可能会产生如下结果:

在使用 ChatGPT 的过程中,你可能也注意到模型不是确定性的:如果你问它完全相同的问题两次,你可能会得到两个不同的答案。这是因为模型实际上并不会产生一个单一的预测令牌;相反,它返回的是所有可能令牌的概率分布。换句话说,它返回一个向量,其中每个条目表示选择特定令牌的概率。然后,模型从该分布中采样以生成输出令牌。

模型是如何产生概率分布的?这就是训练阶段的目的。在训练过程中,模型接触了大量文本,并且其权重被调整以预测好的概率分布,给定一系列输入令牌。GPT 模型是用互联网上的大量数据进行训练的,因此它们的预测反映了它们见过的信息的混合。
你现在对生成模型背后的理念有了很好的理解。请注意,我只是解释了这个理念,尚未给出具体的算法。事实上,这个理念已经存在了几十年,并且多年来已经用几种不同的算法实现了。接下来我们将看看其中的一些算法。
生成语言模型的简史
隐马尔可夫模型(HMMs)在 1970 年代变得流行。它们的内部表示编码了句子的语法结构(名词、动词等),并在预测新单词时使用这些知识。然而,由于它们是马尔可夫过程,它们在生成新标记时只考虑最新的标记。因此,它们实现了“n 个标记输入,一个标记输出”想法的非常简单版本,其中 n = 1. 结果是,它们不会生成非常复杂的输出。我们来看以下示例:

如果我们将“ The quick brown fox jumps over the”输入到语言模型中,我们会期待它返回“lazy”。然而,HMM 只会看到最后一个标记“the”,而且只有这么少的信息,它不太可能给出我们期望的预测。随着人们对 HMMs 的实验,变得明显的是,语言模型需要支持多个输入标记以生成好的输出。
N-gram 在 1990 年代变得流行,因为它通过使用多个标记作为输入解决了隐马尔可夫模型(HMMs)的主要局限性。一个 n-gram 模型可能在预测前一个示例中的单词“lazy”时表现得非常好。
最简单的 n-gram 实现是基于字符的二元模型(bi-gram),它能够根据一个字符预测序列中的下一个字符。你可以用几行代码创建一个这样的模型,我鼓励你试试。首先,计算训练文本中不同字符的数量(我们称之为 n),并创建一个 n x n 的二维矩阵,初始值为零。每对输入字符可以用来在这个矩阵中定位一个特定的条目,通过选择对应第一个字符的行和第二个字符的列。当你解析训练数据时,对于每对字符,你只需将对应矩阵单元的值加一。例如,如果你的训练数据包含单词“car”,你会在“c”行和“a”列的单元格中加一,然后在“a”行和“r”列的单元格中加一。一旦你积累了所有训练数据的计数,将每一行转换为概率分布,方法是将每个单元格的值除以该行的总和。

然后,为了做出预测,你需要提供一个起始字符,例如“c”。你查找与“c”行对应的概率分布,并从中采样以生成下一个字符。然后,你取出你生成的字符,重复这个过程,直到达到停止条件。高阶 n-gram 遵循相同的基本思想,但它们能够通过使用 n 维张量查看更长的输入标记序列。
N-gram 简单易实现。然而,由于矩阵的大小随着输入标记数量的增加而指数级增长,它们在处理大量标记时的扩展性较差。并且对于仅有少量输入标记的情况,它们无法生成良好的结果。因此,需要一种新技术来继续在这一领域取得进展。
在 2000 年代,循环神经网络(RNNs)变得相当流行,因为它们能够接受比以前技术更多的输入标记。特别是,LSTMs 和 GRUs,这些是 RNNs 的类型,变得广泛使用并证明能够生成相当不错的结果。
RNNs 是一种神经网络,但与传统的前馈神经网络不同,它们的架构可以适应任何数量的输入并生成任何数量的输出。例如,如果我们给 RNN 输入标记“We”,“need”和“to”,并希望它生成更多标记直到达到完整的句点,RNN 可能具有以下结构:

上述结构中的每个节点具有相同的权重。你可以把它看作是一个单一的节点,连接到自身并重复执行(因此叫“递归”),或者可以按照上图中的扩展形式来理解。LSTMs 和 GRUs 相较于基础 RNNs 增加的一个关键能力是存在一个从一个节点传递到下一个节点的内部记忆单元。这使得后续节点能够记住前面节点的某些方面,这对于生成良好的文本预测是必不可少的。
然而,RNNs 在处理非常长的文本序列时存在不稳定问题。模型中的梯度往往会呈指数级增长(称为“梯度爆炸”)或减小为零(称为“梯度消失”),这阻碍了模型继续从训练数据中学*。LSTMs 和 GRUs 缓解了梯度消失问题,但并没有完全消除。因此,即使在理论上它们的架构允许任意长度的输入,但在实践中存在长度限制。文本生成的质量再次受到算法支持的输入标记数量的限制,需要新的突破。
在 2017 年,论文 介绍了 Transformers,谷歌发布了这一技术,我们进入了文本生成的新纪元。Transformers 的架构允许输入标记数量的大幅增加,消除了 RNNs 中出现的梯度不稳定问题,并且高度可并行化,这意味着它能够利用 GPU 的强大性能。如今,Transformers 被广泛使用,它们是 OpenAI 为其最新的 GPT 文本生成模型所选择的技术。
Transformers 基于“注意力机制”,它允许模型对某些输入给予比其他输入更多的关注,无论这些输入在输入序列中的位置如何。例如,我们来考虑以下句子:

在这种情况下,当模型预测动词“bought”时,它需要与动词“went”的过去式进行匹配。为了实现这一点,它必须非常关注“went”这个词。实际上,它可能会比对“went”这个词的注意力更强,即使“went”在输入序列中出现得更早。
GPT 模型中的这种选择性注意行为得益于 2017 年论文中的一个新颖概念:使用“掩码多头注意力”层。让我们来解析这个术语,并深入探讨它的每个子术语:
注意力: “注意力”层包含一个权重矩阵,表示输入句子中所有标记位置对之间的关系强度。这些权重在训练过程中学*得到。如果对应一对位置的权重大,则这两个位置上的标记会相互影响很大。这是使得 Transformer 能够对一些标记给予更多关注的机制,无论这些标记在句子中出现的位置如何。
掩码: 如果注意力层的矩阵仅限于每个标记位置与输入中的早期位置之间的关系,则称为“掩码”。这就是 GPT 模型在文本生成中使用的方式,因为一个输出标记只能依赖于它之前的标记。
多头: Transformer 使用了一个带有掩码的“多头”注意力层,因为它包含多个并行工作的掩码注意力层。
LSTM 和 GRU 的记忆单元也使得后续的标记能够记住一些早期标记的方面。然而,如果两个相关的标记距离非常远,梯度问题可能会成为障碍。Transformers 没有这个问题,因为每个标记与所有在它之前的其他标记都有直接连接。
现在你已经了解了 GPT 模型中使用的 Transformer 架构的主要思想,让我们来看一下目前可用的各种 GPT 模型之间的区别。
不同 GPT 模型的实现方式
在撰写时,OpenAI 发布的三款最新文本生成模型是 GPT-3.5、ChatGPT 和 GPT-4,它们都基于 Transformer 架构。实际上,“GPT”代表“生成预训练变换器”。
GPT-3.5 是一个训练为完成式模型的 Transformer,这意味着如果我们给它几个词作为输入,它能够生成几个可能跟随这些词的词。
另一方面,ChatGPT 被训练为对话风格的模型,这意味着当我们像进行对话一样与它交流时,它的表现最佳。它基于与 GPT-3.5 相同的 transformer 基础模型,但经过对话数据的微调。然后,通过使用人类反馈的强化学*(RLHF)进一步微调,这是一种 OpenAI 在其 2022 InstructGPT 论文 中引入的技术。在这一技术中,我们将相同的输入提供给模型两次,得到两个不同的输出,并询问人类评分者哪个输出更好。这个选择然后用于通过微调来改进模型。这项技术使模型的输出与人类期望保持一致,并且对 OpenAI 最新模型的成功至关重要。
另一方面,GPT-4 可用于完成任务和对话,并且拥有全新的基础模型。该基础模型也经过 RLHF 微调,以更好地符合人类期望。
编写使用 GPT 模型的代码
你有两个选项来编写使用 GPT 模型的代码:你可以直接使用 OpenAI API,或者使用 Azure 上的 OpenAI API。无论哪种方式,你都使用相同的 API 调用,你可以在 OpenAI 的 API 参考 页面中了解这些调用。
两者之间的主要区别在于 Azure 提供了以下附加功能:
-
自动化的负责任 AI 过滤器,减轻 API 的不道德使用
-
Azure 的安全功能,如私人网络
-
区域可用性,以在与 API 交互时获得最佳性能
如果你在编写使用这些模型的代码,你需要选择你想使用的具体版本。以下是 Azure OpenAI 服务中当前可用版本的快速备忘单:
-
GPT-3.5: text-davinci-002, text-davinci-003
-
ChatGPT: gpt-35-turbo
-
GPT-4: gpt-4, gpt-4–32k
两个 GPT-4 版本的主要区别在于它们支持的 token 数量:gpt-4 支持 8,000 个 token,而 gpt-4–32k 支持 32,000 个 token。相比之下,GPT-3.5 模型仅支持 4,000 个 token。
由于 GPT-4 目前是最昂贵的选项,因此最好从其他模型之一开始,只有在需要时才升级。有关这些模型的更多详细信息,请查看 文档。
结论
在这篇文章中,我们涵盖了所有生成性语言模型的基本原则,以及特别是 OpenAI 最新 GPT 模型的独特方面。
在过程中,我们强调了语言模型的核心理念:“n 个令牌输入,一个令牌输出。”我们探讨了令牌如何被拆分以及为何如此拆分。我们追溯了语言模型从早期的隐马尔可夫模型到*期的基于 Transformer 的模型的数十年演变。最后,我们描述了 OpenAI 最新的三个基于 Transformer 的 GPT 模型,每个模型的实现方式,以及如何编写代码利用这些模型。
到现在为止,你应该已经能够对 GPT 模型进行有深度的讨论,并在自己的编码项目中开始使用它们。我计划撰写更多关于语言模型的解释文章,请关注我,并告诉我你希望看到哪些话题!感谢阅读!
注意
所有图片均由作者提供,除非另有说明。你可以在本博客文章中出于任何目的使用原始图片,但需注明出处(链接到本文)。
GPT 的工作原理:使用一个药水的故事对注意力中的键、值、查询进行隐喻性的解释

来源:由 Midjourney 生成。
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 6 月 17 日
--
!更新:该帖子现在有一个 10 分钟的视频版本 可用!
ChatGPT 的核心是 GPT 模型,它是使用变换器架构构建的。变换器的核心是注意力机制。对许多人来说,理解注意力中最难的概念是键、值和查询。在这篇文章中,我将用药水的类比来帮助理解这些概念。即使你已经机械地理解了变换器的数学部分,我希望通过这篇文章,你能从头到尾更直观地理解 GPT 的内部工作原理。
这个解释不需要数学背景。对于技术性强的读者,我在[…]中添加了更多的技术解释。你也可以安全地跳过[方括号]中的注释和像这样在引号块中的附注。在我的写作过程中,我编造了一些可读的人类解释,用于说明变换器模型的中间状态,以帮助解释,但 GPT 并不完全是这样思考的。
[当我谈论“注意力”时,我专指“自注意力”,因为这正是 GPT 背后的机制。但同样的类比也可以很好地解释“注意力”的一般概念。]
设置
GPT 可以输出连贯的段落内容,因为它非常擅长执行一个任务:“给定一个文本,下一个词是什么?”让我们角色扮演 GPT:“Sarah 躺在床上,感觉 ____”。你能填上这个空白吗?
其中一个合理的答案是“疲倦”。在接下来的内容中,我将详细说明 GPT 是如何得出这个答案的。(为了好玩,我将这个提示放入 ChatGPT 中,它从中写了一个简短的故事)。
类比:(键、值、查询),或(标签、药水、配方)
你将上述提示输入 GPT。在 GPT 中,每个词都配备了三样东西:键(Key)、值(Value)、查询(Query),这些值是在 GPT 模型训练期间从整个互联网的文本中学*到的。正是这三种成分的相互作用使得 GPT 能够在文本的上下文中理解一个词。那么它们究竟是做什么的呢?

来源:由作者创建。
让我们设立炼金术的类比。对于每一个词,我们有:
-
药水(即“值”):药水包含了关于词的丰富信息。为了说明目的,假设词“lies”的药水包含了如“疲倦;不诚实;如果是善意的谎言可能有积极的含义;……”的信息。词“lies”可以有多重含义,例如“说谎”(与不诚实相关)或“躺下”(与疲倦相关)。只有在文本的上下文中,你才能真正了解其含义。目前,药水包含了两种含义的信息,因为它没有文本的上下文。
-
炼金术师的配方(也称为“查询”):给定单词的炼金术师,例如“谎言”,会查看所有附*的单词。他找到了一些与自己单词“谎言”相关的单词,他的任务是用这些单词的药水填满一个空药瓶。炼金术师有一个配方,列出了确定他应该关注哪些药水的具体标准。
-
标签(也称为“关键字”):每种药水(值)都有一个标签(关键字)。如果标签(关键字)与炼金术师的配方(查询)匹配得很好,炼金术师将会关注这种药水。
注意:炼金术师的药水调配

带标签的药水。来源:作者创作。
在第一步(注意),所有单词的炼金术师各自出发,去从相关单词中获取药水填满他们的药瓶。
以单词“谎言”的炼金术师为例。他知道根据之前的经验——经过对整个互联网文本的预训练——有助于解释句子中“谎言”的单词通常是:“一些平面表面、与不诚实相关的词、与休息相关的词”。他将这些标准写在他的配方(查询)中,并寻找其他单词药水上的标签(关键字)。如果标签与标准非常相似,他会将大量这种药水倒入他的药瓶中;如果标签不相似,他则会倒入少量或不倒入。
所以他发现“床”的标签上写着“一个平面的家具”。这与他配方中的“一些平面表面”类似!他将“床”的药水倒入药瓶中。“床”的药水(值)包含的信息有“疲惫、安静、昏昏欲睡、生病”。
单词“谎言”的炼金术师继续搜索。他发现“still”的标签上写着“与休息相关”(以及“still”的其他含义)。这与他的标准“休息”相关,因此他将一部分“still”的药水倒入药瓶中,包含的信息有“休息、安静、静止”。
他查看了“on”,“Sarah”,“the”,“feeling”的标签,发现它们不相关。因此,他没有将这些药水倒入他的药瓶。
记住,他也需要检查自己的药水。自己的药水“谎言”的标签上写着“与休息相关的动词”,这与他的配方“休息”相匹配。因此,他也将自己药水的一部分倒入药瓶中,包含的信息有“疲惫;不诚实;如果是善意的谎言,可能有积极的含义;……”。
在他完成检查文本中的单词任务时,他的药瓶已经满了。

来源:作者创作。
与原始的“谎言”药水不同,这种混合药水现在考虑了这个特定句子的上下文。也就是说,它包含了很多“疲惫,精疲力竭”的元素,只有一点点“不诚实”。
在这个任务中,炼金术师知道如何关注正确的词,并结合那些相关词的值。这是“注意力”的隐喻步骤。我们刚刚解释了 Transformer 的最重要方程,这是 GPT 的底层架构:

Q 是查询;K 是键;V 是值。来源:Attention is All You Need
高级笔记:
1. 每位炼金术师查看每个瓶子,包括他们自己的 [Q·K.transpose()]。
2. 炼金术师可以快速将他的配方(查询)与标签(键)匹配,并做出快速决定。[查询和键之间的相似性通过点积确定,这是一项快速操作。] 此外,所有炼金术师的任务都是并行进行的,这也有助于加快速度。[Q·K.transpose() 是矩阵乘法,可以并行处理。与按顺序计算的前身递归神经网络相比,速度是 Transformer 的优势特性。]
3. 炼金术师很挑剔。他只选择最好的几种药水,而不是混合所有药水。[我们使用 softmax 来压缩 Q·K.transpose()。softmax 将输入值拉向更极端的值,并将许多输入压缩到接*零。]
4. 在这个阶段,炼金术师不考虑单词的顺序。无论是“Sarah lies still on the bed, feeling”还是“still bed the Sarah feeling on lies”,填满的烧瓶(注意力的输出)将是相同的。[在没有“位置编码”的情况下,Attention(Q, K, V) 与单词位置无关。]
5. 烧瓶总是返回 100%满,没有多也没有少。[softmax 被归一化为 1。]
6. 炼金术师的配方和药水的标签必须使用相同的语言。[查询和键必须具有相同的维度才能进行点积运算以进行通信。值可以采用不同的维度,如果你愿意的话。]
7. 技术精明的读者可能会指出我们没有进行掩蔽。我不想用太多细节来混淆类比,但我会在这里解释。在自注意力中,每个词只能看到前面的词。因此,在句子“Sarah lies still on the bed, feeling”中,“lies”只看到“Sarah”;“still”只看到“Sarah”和“lies”。 “still”的炼金术师不能接触到“on”、“the”、“bed”和“feeling”的药水。
Feed Forward: 混合药水中的化学反应
直到此时,炼金术师只是将其他瓶子的药水倒入烧瓶中。换句话说,他将“谎言”——“疲倦;不诚实;…”——作为均匀混合物倒入烧瓶中;他还不能将“疲倦”部分提取出来并丢弃“不诚实”部分。[注意力只是将不同的 V 加在一起,经过 softmax 加权。]

来源:作者创建。
现在进入真正的化学过程(前馈)。炼金术师将一切混合在一起并进行合成。他注意到词语之间的互动,例如“sleepy”和“restful”等。他还发现“dishonesty”只在一个药水中提到。他根据以往的经验知道如何让一些成分相互作用,以及如何丢弃那些一次性的成分。[前馈层是值的线性(然后是非线性)变换。前馈层是神经网络的构建块。你可以将其视为 Transformer 中的“思考”步骤,而早期的混合步骤只是“收集”。]
经过处理后的药水变得更适合预测下一个词。直观上,它在句子上下文中表现出该词的一些更丰富的特性,而与起始药水(值)相比,后者是脱离上下文的。
最终线性和 softmax 层:炼金术师的集会
我们如何从这里得到最终输出,即预测“Sarah lies still on the bed, feeling ___”之后的下一个词是“tired”?
到目前为止,每个炼金术师一直独立工作,只关注自己的词汇。现在,所有不同词汇的炼金术师汇集在一起,将他们的药水瓶按照原始词序排列,并呈现给 Transformer 的最终线性和 softmax 层。这是什么意思呢?这里,我们必须离开比喻。
这个最终线性层综合了不同词汇的信息。基于预训练数据,一个可能的学*是,紧接前一个词对预测下一个词是重要的。例如,线性层可能会重点关注最后一个药水瓶(“feeling”药水瓶)。
然后结合 softmax 层,这一步为词汇表中的每个词分配一个概率,表示在“Sarah lies on the bed, feeling…”之后这个词的可能性。例如,非英语词汇的概率接* 0。像“tired”、 “sleepy”、 “exhausted”这样的词将获得高概率。然后我们选择概率最高的词作为最终答案。

来源:作者创建。
总结
现在你已经构建了一个极简主义的 GPT!
总结一下,在注意力步骤中,你确定每个词应该关注哪些词(包括自身),基于该词的查询(配方)与其他词的键(标签)的匹配程度。你将这些词的值(药水)按该词对它们的注意力进行混合。你处理这种混合物以进行一些“思考”(前馈)。每个词处理后,你将所有其他词的混合物组合在一起,以进行更多的“思考”(线性层)并做出最终预测,确定下一个词应该是什么。

来源:作者创建。
旁注:“解码器”这一术语是原始论文中的残余,因为 Transformer 最初用于机器翻译任务。你“编码”源语言到嵌入中,然后“解码”从嵌入到目标语言。
这是一个很好的停顿点。
如果你渴望了解更多,我们将在上述简约架构的基础上介绍另外两种变体:多头注意力和重复块。
多头注意力:许多炼金术士的集合
到目前为止,每个词只有一个炼金术士的配方、一个标签和一种药水。[对于每个词,每个查询、值、键是一个单一向量,而不是一个矩阵。] 但是,如果我们为每个词配备几组配方、标签、药水,我们可以获得更好的结果。对于参考,GPT 每个词使用 12 组(即 12 个“注意力头”)。也许对于每个词,第一组的炼金术士专注于分析情感,第二组的炼金术士专注于解析引用(“它”指的是什么),等等。
高级说明:情感炼金术士组只研究情感药水;他们不会知道如何处理其他集合的药水,也不会触及那些药水。[来自同一注意力头的 V、K、Q 是联合训练的。来自不同注意力头的 V、K、Q 在多头注意力步骤中不进行交互。]

来源:由作者创建。
每个词的 12 位炼金术士将他们的专门化、填充的瓶子一起[连接不同注意力头的输出]。作为一个群体,他们利用所有这些瓶子进行一次巨大的化学反应,并呈现出一种最终的药水[前馈,即线性层]。
高级说明:就像以前一样,在解码器块中,不同词的瓶子不会混合在一起。[前馈步骤是按位置进行的,这意味着它对每个词独立应用。]
这个比喻解释了原论文中的以下方程式和图表。


堆叠块:并且……重复!

来源:由作者创建。
为了获得更好的效果,我们将解码器块重复 N 次。对于参考,GPT 重复 12 次。直观上,你会希望在收集其他相关词汇的药水(注意力)和自己合成这些药水以获得意义(前馈)之间进行交替:收集,合成;收集,合成……
现在你可以了解炼金术……我的意思是……GPT!
人类劳动如何促进机器学*
技术与人类活动之间的大部分分隔是人为的——人们是如何让我们的工作成为可能的?
·
关注 发布在 Towards Data Science ·8 分钟阅读·2023 年 10 月 31 日
--
图片由 Dominik Scythe 提供,来源于 Unsplash
我们并没有足够谈论我们依赖多少人工工作来实现机器学*领域的激动人心的进展。事实是,技术和人工活动之间的划分是人为的。所有使模型产生的输入都是人类努力的结果,而所有输出以某种方式存在于影响人们。我今天使用这一专栏来谈论我们在某些特定领域忽视了人们对我们所做工作的重要性——不仅仅是那些编写代码的数据科学家。
技术与人工活动之间的划分是人为的,因为所有使模型产生的输入都是人类努力的结果,而所有输出以某种方式存在于影响人们。
生成数据
你几乎肯定已经知道这一点——LLMs 需要巨量的文本数据进行训练。我们通常以硬盘上的数百或数千 GB 的数据来考虑这个问题,但这有点抽象。一些报告指出,GPT-4 的训练数据大约有1 万亿个单词。每一个单词都是由一个人用他们自己的创造能力写出的。作为参考,《冰与火之歌》系列的第一本书大约有 292,727 个单词。因此,GPT-4 的训练数据大约是3,416,152 本书的长度。而这只是文本建模的一个例子——其他类型的模型,例如那些生成或分类多媒体的模型,也使用类似规模的海量数据。
当涉及到这些数据时,有几个方面需要考虑。首先,所有这些数据都是由人生成的,它不会凭空出现在我们的硬盘上。尊重和认可那些创造我们数据的人的工作是重要的,这是伦理问题,因为他们付出了努力,创造了我们正在受益的价值。但我们也有更自私的理由需要了解我们的数据来源。作为数据科学家,我们有责任了解我们向模型提供的材料作为示例,并深入理解它。如果我们忽视数据的来源,我们可能会在面对现实世界时,对我们的模型行为感到不愉快的惊讶。例如,在互联网论坛或社交媒体数据上训练 LLMs 会使这些模型面临复制这些空间最糟糕现象的风险,包括种族主义、仇恨言论等。在稍微不那么极端的例子中,我们知道模型受其训练数据的影响。
如果我们忽视数据的来源,我们可能会在面对现实世界时,对我们的模型行为感到不愉快的惊讶。
标记数据
数据标注需要人工帮助。但是,标签到底是什么?从根本上说,标注数据意味着使用人类的洞察力为我们在数据中发现的内容分配值或判断。无论数据如何收集或创建,大多数机器学*用例都需要某种形式的标注。
这可能意味着简单地决定一个数据点是好是坏,确定词语是积极的还是消极的,创建衍生值,将记录划分为不同类别,确定哪些标签适用于图像或视频,或者其他无尽的任务。一个常见的例子是识别图像或其他多媒体中的文本,以改善字符识别模型。如果你使用过验证码,我敢打赌这听起来很熟悉——你已经做过数据标注工作。
理论上,LLM 本身不需要标注,因为我们从这些文本已经由真实人类生成的事实中推断出人类的质量,因此这些文本在本质上必须尽可能类似于人类输出。基本上,因为是人类写的,那么它在定义上就是一个可接受的示例供模型学*和模仿。这就是我们使用语义嵌入的地方——模型学*人类生成文本中的语言模式如何工作,并将这些模式量化为数学表示。但是正如我之前所描述的,我们仍然在选择哪些文本进入模型的过程,我们有责任理解和评估这些文本。
教学模型
强化学*使用人工干预来调整任务——意味着我们在模型基本上掌握了返回连贯答案的方式后,稍微调整模型对提示的响应,无论是文本、图像、视频还是其他内容。在一些主要的自动化预训练或基础训练之后,许多模型由人类进行微调,做出有时微妙的判断,判断模型是否达到了期望。 这是一项非常艰巨的任务,因为我们实际希望模型做到的细微差别可能非常复杂。这基本上是在大规模上以通过或不通过的方式进行 LLM 的校对。
正如我之前讨论的,许多现代模型寻求生成对人类用户最令人愉悦的内容——那些看起来正确且吸引人的东西。那么,有什么比让人类查看训练的中间阶段结果并决定结果是否符合描述,再告诉模型以便它做出更合适的选择,更好的训练方式呢?这不仅是最有效的方法,也可能是唯一有效的方法。
这基本上是在以通过或不通过的方式进行 LLM 的校对。
为什么这很重要
好吧,那又怎样呢?仅仅意识到真实的人们付出了大量的努力来实现我们的模型,这就够了吗?拍拍他们的背,表示感谢?不完全是,因为我们需要审视人类的影响对我们生成的结果意味着什么。作为数据科学家,我们需要对我们构建的东西与其所在世界之间的互动保持好奇心。
由于所有这些影响领域,人类的选择塑造了模型的能力和判断。我们将人类的偏见嵌入到模型中,因为人类创造、控制并判断所有相关材料。我们决定哪些文本将提供给模型进行训练,或者模型的哪个具体回应比另一个更差,而模型将这些选择固化为可以重复和再利用的数学表示。
这种偏见元素是不可避免的,但并不一定是坏事。试图创造一个完全摆脱人类影响的东西暗示人类的影响和人类本身是需要避免的问题,这在我看来是不公平的评估。同时,我们也应该现实地认识到人类偏见是我们模型的一部分,并抵制将模型视为超越我们人类缺陷的诱惑。例如,我们如何分配标签,导致我们有意识或无意识地赋予数据意义。无论是原创创意内容、数据标签,还是模型输出的判断,我们都在创建的数据中留下了我们思维过程和历史的痕迹。
试图创造一个完全摆脱人类影响的东西暗示人类的影响和人类本身是需要避免的问题,这在我看来是不公平的评估。
此外,在机器学*领域,人类的努力通常被视为服务于“真实”工作的,而不是自身具有意义。那些产生原创作品的人不再被视为独特的创造性个体,而只是被归入服务于模型的“内容生成者”。我们忽视了这些内容存在的本质人性和真实原因,即服务和赋能人类。与之前的观点一样,我们最终贬低了人们而崇拜技术,我认为这是愚蠢的。模型是人们的产物,旨在服务于人们,它们不是独立的目标。如果你构建了一个从未被使用和运行的模型,那还有什么意义呢?
数据是可再生资源吗?
另一个有趣的问题是:人类生成的纯净内容的风险成为模型能力的限制因素。也就是说,当我们的社会开始使用 LLM 生成数据,使用 Dall-E 生成图像,并且我们停止激励真实的人在没有这些技术的情况下进行创作时,我们需要训练新版本这些模型的万亿字词和山岳般的图像将被人为生成的内容所污染。那种内容,当然,来源于人类内容,但并不完全一样。我们尚未拥有很好的方法来区分由没有模型的人生成的内容,所以我们将难以判断我们未来模型的训练数据是否存在这种污染,以及污染的程度。
有人认为这其实不是什么大问题,训练模型时使用至少一部分人工内容不会有问题,但也有其他理论认为,当我们开始以这种方式掠夺人工生成的内容时,训练的基本过程将以称为模型崩溃的形式被根本性改变。在某些方面,这是模型影响了模型所依赖的世界的根本问题的一个例子,因此模型的行为定义上改变了模型本身。数据科学家对此也深有体会。这不仅仅适用于 LLM,任何模型都可能通过影响人们的行为来使自己失业,从而导致由于基础数据关系的变化而性能漂移。
你的模型影响了模型所依赖的世界,因此模型的行为定义上改变了模型本身。
即使我们没有在实际的人工数据上进行训练,还有许多学者在考虑我们的人的组成和创造过程是否会因接触到人工生成的内容而发生变化。如果你大量阅读 LLM 生成的文本,无论是在写作时获取模型的建议还是在互联网的一般环境中,这是否会微妙地改变你的写作方式?在社区层面上现在还为时尚早,但这是一个严重的问题。
人类影响是机器学*的一个事实——这是一个哲学问题。我们将机器学*视为一种纯科学事业,认为它作用于我们,这也是它对某些人来说似乎令人恐惧的原因之一。但实际上,正在创建的系统是人类干预和创造力的产物。创建和策划数据使得所有其他机器学*的可能性成为可能。在某种程度上,这应该让我们感到安慰,因为我们可以控制我们如何使用机器学*以及如何使用它。机器学*的过程是将数据片段之间的关系计算成数学表示,但数据是由人类产生的,并且在我们的控制之下。机器学*和人工智能不是某种外星的、抽象的力量——它们只是我们。
查看更多我的作品请访问 www.stephaniekirmer.com.
上述文章和参考资料,方便访问:
-
www.youtube.com/watch?v=bZQun8Y4L2AGPT 状况,微软 Build 大会 2023,Andrej Karpathy -
www.technologyreview.com%2F2023%2F08%2F07%2F1077324%2Fai-language-models-are-rife-with-political-biases%2F MIT 技术评论,Melissa Heikkilä,2023 年 8 月
如何基于 AWS 构建级联数据管道(第一部分)
原文:
towardsdatascience.com/how-i-built-a-cascading-data-pipeline-based-on-aws-997b212a84d2
自动化、可扩展且强大
·发表于 Towards Data Science ·14 分钟阅读·2023 年 7 月 31 日
--
今天我要分享一些我引以为豪的数据工程项目的经验。你将了解到我为何使用这些工具和 AWS 组件,以及我如何设计架构。

作者提供的图片
免责声明:本文本内容受到我与一个未命名实体的经验启发。然而,某些关键商业利益和细节已故意用虚构数据/代码替代或省略,以保持机密性和隐私。因此,实际涉及的商业利益的完整和准确程度保留。
前提条件
背景
假设你是一个数据工程师,需要不断更新数据仓库中的数据。例如,你需要定期与 Dunder Mifflin Paper Co. 的销售记录同步。(我知道这不是一个现实的场景,但请尽情享受 😃!)数据通过供应商的 API 发送给你,你需要确保分支、员工(实际上只考虑销售人员)和销售的信息是最新的。提供的 API 有以下 3 个路径:
-
/branches,接受分支名称作为查询参数以检索指定分支的元数据; -
/employees,接受分支 ID 作为查询参数以检索某个分支所有员工的信息,响应中包括一个表示员工职业的键值对; -
/sales,接受员工 ID 作为查询参数来检索销售人员的所有销售记录,响应包括一个指示交易完成时间的键值对。
总的来说,API 的返回结果如下:
/branches 路径:
{
"result": [
{
"id": 1,
"branch_name": "Scranton",
"employees": 50,
"location": "Scranton, PA",
...
}
]
}
/employees 路径:
{
"result": {
"branch_id": 1,
"employees": [
{
"id": 1234,
"occupation": "data engineer",
"name": "John Doe",
...
},
{
"id": 1235,
"occupation": "salesperson",
"name": "Jim Doe",
...
},
...
],
...
}
}
/sales 路径:
{
"result": {
"employee_id": 1235,
"sales: [
{
"id": 3972,
"transaction_timestamp": "2023-01-01 23:43:23",
...
},
{
"id": 4002,
"transaction_timestamp": "2023-01-05 12:23:31",
...
},
...
],
...
}
}
期望你的最终工作将使数据分析师能够仅通过 SQL 查询来检索他们分析所需的数据。
思路
最终,我们将有 3 个不同的地方分别存储分支、销售人员和销售的数据,这一点说起来很简单。数据将通过访问特定的 API 路径来导入。然而,由于所有这些实体的标识符大多是自动生成的,实践者不大可能提前获得这些 ID。相反,由于我们通常可以找到分支名称,因此可以使用第一个路径来获取分支的元数据及其员工的 ID。然后,我们可以使用员工 ID 访问/employees路径,/sales路径也是如此。这就是我称这个流程为级联的原因。
为了确保我们的数据库大部分时间是最新的,必须足够频繁地执行这些操作。但另一方面,我们也需要考虑成本和潜在的 API 访问配额。因此,每小时运行一次是适当的,尽管可以说尚未达到最佳。
最后但同样重要的是,让我们讨论 AWS。首先,执行这些操作的代码将由AWS Lambda运行,因为它能够使用 200 多个 AWS 服务和应用程序作为触发器,包括SQS和EventBridge。数据将通过SQS传递,作为 AWS 提供的最成熟的消息服务之一。最后,从 API 抓取的信息将存储在DynamoDB中。对于一些有经验的读者来说,利用DynamoDB作为数据仓库工具可能会令人困惑,因为这是一个 NoSQL 数据库服务,而数据仓库通常与 NoSQL 数据库不兼容。我确实意识到这一点,DynamoDB 表在这里将仅作为临时存储表,因为我可以利用其在键值/文档数据模型模式中的灵活性,然后最终将 JSON 格式的 API 检索数据转换为数据仓库记录。如果你对我实现 DynamoDB-S3 加载的细节感兴趣,可以查看这篇文章。
实现
这是我最终工作的结构。
Cascading-ETL-pipeline
├── LICENSE
├── README.md
├── branches
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── lambda_function.py
│ ├── requirements.txt
│ └── service
│ ├── config.py
│ └── service.py
├── sales
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── lambda_function.py
│ ├── requirements.txt
│ └── service
│ ├── config.py
│ └── service.py
├── salespersons
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── lambda_function.py
│ ├── requirements.txt
│ └── service
│ ├── config.py
│ └── service.py
├── template.yml
└── utils.py
有 3 个文件夹(/branches,/salespersons,/sales),分别包含每个 lambda 函数的代码。Utils.py 是一个多功能文件,其中的函数、变量和类被全局应用。模板.yml 是我们将用来声明和部署 AWS 资源以建立数据管道的 AWS CloudFormation 模板。
每个文件夹中的Lambda_function.py是代码执行的入口函数:
import json
import logging
from pythonjsonlogger import jsonlogger
from service import service, config
# Load environment
ENV = config.load_env()
LOGGER = logging.getLogger()
# Replace the LambdaLoggerHandler formatter :
LOGGER.handlers[0].setFormatter(jsonlogger.JsonFormatter())
# Set default logging level
LOGGING_LEVEL = getattr(logging, ENV["LOGGING_LEVEL"])
LOGGER.setLevel(LOGGING_LEVEL)
def _lambda_context(context):
"""
Extract information relevant from context object.
Args:
context: The context object provided by the Lambda runtime.
Returns:
dict: A dictionary containing relevant information from the context object.
"""
return {
"function_name": context.function_name,
"function_version": context.function_version,
}
# @datadog_lambda_wrapper
def lambda_handler(event, context):
"""
Handle the Lambda event.
Args:
event(dict): The event object containing input data for the Lambda function.
context(dict): The context object provided by the Lambda runtime.
Returns:
dict: A dictionary containing the response for the Lambda function.
"""
LOGGER.info("Starting lambda executing.", extra=_lambda_context(context))
service.main(event, ENV)
LOGGER.info("Successful lambda execution.", extra=_lambda_context(context))
return {"statusCode": 200}
/Service/config.py 返回template.yml中输入的环境变量:
import os
import sys
import logging
LOGGER = logging.getLogger(__name__)
def load_env():
"""Load environment variables.
Returns:
dict: A dictionary containing the loaded environment variables.
Raises:
KeyError: If any required environment variable is missing.
Notes:
- The function attempts to load several environment variables including:
- If any of the required environment variables are missing, a KeyError is raised.
- The function logs an exception message indicating the missing environment variable and exits the program with a status code of 1.
"""
try:
return {
"LOGGING_LEVEL": os.environ["LOGGING_LEVEL"],
"APP_ENV": os.environ["APP_ENV"],
"SQS": os.environ["SQS"],
"DB": os.environ["DB"],
}
except KeyError as error:
LOGGER.exception("Enviroment variable %s is required.", error)
sys.exit(1)
/Service/service.py 是我们实际处理数据的地方。基本上,该功能由一个或两个触发器(时间调度器)调用,在从数据源(API 或 SQS 队列)中检索数据之前。数据将以键值对的形式打包,如有需要,更新到相应的 DynamoDB 表中,然后该功能分发其成员的标识符(即,分支中的所有员工,销售人员的所有销售记录)。
以 /branches/service/service.py 为例。它的功能包括:
-
一旦被唤醒,立即从 API
/branches获取所有数据; -
检查 DynamoDB 数据表中每个销售人员的个人信息是否存在和准确,如果不更新,则插入数据记录;
-
获取所有员工的 ID,并将其连同分支 ID 作为消息通过 SQS 队列传递给尾部函数(/salespersons)。
实际操作中,实施方式如下:
import logging, requests, sys
from utils import *
from boto3.dynamodb.conditions import Key
LOGGER = logging.getLogger(__name__)
def main(event, environment):
"""Process invoking event data and update the DynamoDB table based on specified branches.
Args:
event (dict): A JSON-formatted document that contains data for a Lambda function to process.
environment (dict): A context object that provides methods and properties about the invocation, function and runtime environment.
Returns:
None
Raises:
SystemExit: If an exception occurs during the execution.
Notes:
- If `event` does not contain the 'branches' key, the function will default to processing information for all branches.
- The function retrieves branch-specific information from a URL and updates the DynamoDB table accordingly.
- The updated information is then delivered to an SQS queue for further processing.
"""
LOGGER.info(event)
if not event.get("branches"):
# default to look up all branches if the value is an empty list
branches = [
"Scranton",
"Akron",
"Buffalo",
"Rochester",
"Syracuse",
"Utica",
"Binghamton",
"Albany",
"Nashua",
"Pittsfield",
"Stamford",
"Yonkers",
"New York",
]
else:
branches = event["branches"] # should be an array
queue = environment["SQS"]
table = environment["DB"]
try:
for branch in branches:
# go to a path that allows users to retrieve all information of the specified branch(es) based on input date range
response = requests.get(
url=f"www.dundermifflinpaper.com/branches/?branch={branch}"
)
response = response.json()
branches = response.get("result")
for result in branches:
if not upToDate(
table,
Key("branch_id").eq(str(result["id"])),
result,
"branch_",
):
# only update DynamoDB table when it's NOT complete ingesting
update_info(table, result)
deliver_message(queue, str({"branch": result["branch_id"]}))
LOGGER.info(f"sending branch {result['branch_id']} for the next stage")
except Exception as e:
LOGGER.error(str(e), exc_info=True)
sys.exit(1)
最后,我们需要为构建和部署做准备:
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters: # Type: String
Environment:
Type: String
Resources:
# =========================================================================================
# IAM ROLES, POLICIES, PERMISSIONS
# =========================================================================================
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${AWS::StackName}-lambda-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- events.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSLambdaExecute
- arn:aws:iam::aws:policy/AmazonSQSFullAccess
- arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
Path: '/'
LambdaPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub '${AWS::StackName}-lambda-policy'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: EventBusAccess
Effect: Allow
Action:
- events:PutEvents
Resource: '*'
- Sid: LambdaInvokeAccess
Effect: Allow
Action:
- lambda:InvokeFunction
Resource: "*"
- Sid: LogAccess
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
Roles:
- !Ref LambdaRole
# =========================================================================================
# AWS LAMBDA FUNCTIONS
# =========================================================================================
BranchCollector:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub branch-collector-${Environment}
Handler: lambda_function.lambda_handler
Runtime: python3.9
CodeUri: branches/
Description: updating branch info in our DynamoDB table
MemorySize: 128
Timeout: 900
Role: !GetAtt LambdaRole.Arn
Environment:
Variables:
LOGGING_LEVEL: INFO
APP_ENV: !Ref Environment
SQS: !Ref EmployeeQueue
DB: !Sub branches-${Environment}
DeadLetterQueue:
Type: SQS
TargetArn:
Fn::GetAtt: BranchFunctionDeadLetterQueue.Arn
Events:
StartScheduledEvent:
Type: Schedule
Properties:
Schedule: rate(1 hour)
SalespersonCollector:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub salesperson-collector-${Environment}
Handler: lambda_function.lambda_handler
Runtime: python3.9
CodeUri: salespersons/
Description: updating salesperson info in our DynamoDB table
MemorySize: 128
Timeout: 900
Role: !GetAtt LambdaRole.Arn
ReservedConcurrentExecutions: 5
Environment:
Variables:
LOGGING_LEVEL: INFO
APP_ENV: !Ref Environment
SOURCE_SQS: !Ref EmployeeQueue
TARGET_SQS: !Ref SaleQueue
DB: !Sub salespersons-${Environment}
DeadLetterQueue:
Type: SQS
TargetArn:
Fn::GetAtt: EmployeeFunctionDeadLetterQueue.Arn
Events:
StartScheduledEvent:
Type: Schedule
Properties:
# every minute
Schedule: rate(1 minute)
SaleCollector:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub sale-collector-${Environment}
Handler: lambda_function.lambda_handler
Runtime: python3.9
CodeUri: sales/
Description: updating sales info in our DynamoDB table
MemorySize: 128
Timeout: 900
ReservedConcurrentExecutions: 3
Role:
Fn::GetAtt:
- LambdaRole
- Arn
Environment:
Variables:
LOGGING_LEVEL: INFO
APP_ENV: !Ref Environment
SQS: !Ref SaleQueue
DB: !Sub sales-${Environment}
DeadLetterQueue:
Type: SQS
TargetArn:
Fn::GetAtt: SaleFunctionDeadLetterQueue.Arn
Events:
StartScheduledEvent:
Type: Schedule
Properties:
# every minute
Schedule: rate(1 minute)
# =========================================================================================
# AWS DynamoDB TABLES
# =========================================================================================
BranchDynamoDBTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Delete
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
-
AttributeName: "branch_id"
AttributeType: "S"
KeySchema:
-
AttributeName: "branch_id"
KeyType: "HASH"
StreamSpecification:
StreamViewType: NEW_IMAGE
TableName: !Sub branch-${Environment}
SalespersonDynamoDBTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Delete
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
-
AttributeName: "employee_id"
AttributeType: "S"
-
AttributeName: "branch_id"
AttributeType: "S"
KeySchema:
-
AttributeName: "employee_id"
KeyType: "HASH"
-
AttributeName: "branch_id"
KeyType: "RANGE"
StreamSpecification:
StreamViewType: NEW_IMAGE
TableName: !Sub salesperson-${Environment}
SaleDynamoDBTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Delete
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
-
AttributeName: "sale_id"
AttributeType: "S"
-
AttributeName: "employee_id"
AttributeType: "S"
KeySchema:
-
AttributeName: "sale_id"
KeyType: "HASH"
-
AttributeName: "employee_id"
KeyType: "RANGE"
StreamSpecification:
StreamViewType: NEW_IMAGE
TableName: !Sub sale-${Environment}
# =========================================================================================
# AWS SQS QUEUES
# =========================================================================================
EmployeeQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub employee-queue-${Environment}
VisibilityTimeout: 900
RedrivePolicy:
deadLetterTargetArn:
Fn::GetAtt: EmployeeWorkloadDeadLetterQueue.Arn
maxReceiveCount: 10
EmployeeWorkloadDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub employee-workload-dead-letter-queue-${Environment}
MessageRetentionPeriod: 1209600
BranchFunctionDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub branch-function-dead-letter-queue-${Environment}
MessageRetentionPeriod: 1209600
SaleQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub sale-queue-${Environment}
VisibilityTimeout: 900
RedrivePolicy:
deadLetterTargetArn:
Fn::GetAtt: SaleWorkloadDeadLetterQueue.Arn
maxReceiveCount: 10
SaleWorkloadDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub sale-workload-dead-letter-queue-${Environment}
MessageRetentionPeriod: 1209600
EmployeeFunctionDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub employee-function-dead-letter-queue-${Environment}
MessageRetentionPeriod: 1209600
SaleFunctionDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub sale-function-dead-letter-queue-${Environment}
MessageRetentionPeriod: 1209600
# =========================================================================================
# AWS CLOUDWATCH ALARMS
# =========================================================================================
BranchErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref BranchCollector
EvaluationPeriods: 1
MetricName: Errors
Namespace: AWS/Lambda
Period: 300
Statistic: Sum
Threshold: '1'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
BranchDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref BranchCollector
EvaluationPeriods: 1
MetricName: Duration
Namespace: AWS/Lambda
Period: 60
Statistic: Maximum
Threshold: '750000'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
BranchThrottleAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref BranchCollector
EvaluationPeriods: 1
MetricName: Throttles
Namespace: AWS/Lambda
Period: 300
Statistic: Sum
Threshold: '1'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
SalespersonErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref SalespersonCollector
EvaluationPeriods: 1
MetricName: Errors
Namespace: AWS/Lambda
Period: 300
Statistic: Sum
Threshold: '1'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
SalespersonDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref SalespersonCollector
EvaluationPeriods: 1
MetricName: Duration
Namespace: AWS/Lambda
Period: 60
Statistic: Maximum
Threshold: '750000'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
SalespersonThrottleAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref SalespersonCollector
EvaluationPeriods: 1
MetricName: Throttles
Namespace: AWS/Lambda
Period: 300
Statistic: Sum
Threshold: '1'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
SaleErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref SaleCollector
EvaluationPeriods: 1
MetricName: Errors
Namespace: AWS/Lambda
Period: 300
Statistic: Sum
Threshold: '1'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
SaleDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref SaleCollector
EvaluationPeriods: 1
MetricName: Duration
Namespace: AWS/Lambda
Period: 60
Statistic: Maximum
Threshold: '750000'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
SaleThrottleAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref SaleCollector
EvaluationPeriods: 1
MetricName: Throttles
Namespace: AWS/Lambda
Period: 300
Statistic: Sum
Threshold: '1'
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-action-name}
问答
是的,我正在进行自问自答的环节。在编码时,挑战自己“为什么?”或“怎么做?”通常是有帮助的,这样我会对自己所做的每个决定有更多信心,也能证明我使用的每个工具。
a. 我如何监控这些功能?
我使用CloudWatch 警报。CloudWatch 警报监视任何可用的指标或 AWS CloudWatch 支持的指标计算。它们可以根据给定阈值在指定时间内对指标或计算进行自定义操作。
对我来说,最关键的是在发生错误时立即学*和解决。因此,我为所有 3 个功能设置了以 1 个错误为阈值的警报,也就是说,只要出现错误就会触发警报。我希望在不不断盯着 CloudWatch 仪表板的情况下识别错误,因此警报的操作是将通知推送到 SNS 主题,该主题将警报转发到我的电子邮件收件箱。
如果你在一个协作环境中工作,我建议你通过将其发送到 Slack 频道,分发到分发列表中的所有地址,或将其包含在共享邮箱中,以扩展可见性。
b. 你为什么按现在的方式定义表的键?
这是基于现实情况的。显然,分支之间有所不同,因此它们的 ID 足以作为分支表中的唯一哈希键,符合1NF 约束。相比之下,销售人员和销售表则需要额外的键进行规范化。
因为在现实中,一个分支可能在记录中有多个员工,而员工允许从一个分支转移到另一个分支,从数据模式的角度来看,分支与员工之间的关系是多对多的。而且正因为如此,只有销售记录 ID + 销售人员 ID + 分支 ID(交易发生时的分支)组合才能指向销售表中的一个确切记录。瓶颈在于像 DynamoDB 这样的文档数据库允许最多两个属性作为键,我选择了销售人员 ID 作为排序键,而不是分支 ID,以确保销售记录与销售人员之间的关系。销售和分支之间的差异将在接下来的问题中解释。
c. 我如何建立销售和分支之间的联系?为什么?
数据供应商在销售记录中未能包含分支信息。处理这个问题的万灵药是从最上层(分支收集器函数)到最底层附加分支 ID。然而,这种方式忽略了一些极端情况。例如,吉姆·哈尔普特 在他在斯克兰顿分支的最后一天进行了销售。由于一些技术问题,这条记录没有被添加到他的销售记录列表中,也没有发布到 API 上,直到第二个工作日他被预设为转移到斯坦福德的员工。
没有上下文很难发现标签错误,尤其是当根本原因来自供应商时。从我的经验来看,此阶段的调试非常依赖于我们的反馈。这就是为什么我让销售表中的分支 ID 成为一个宽松的键值对;否则,需要额外的工作来删除和重写该项。
d. 我如何触发销售人员和销售收集器函数?
SQS 队列是 Lambda 函数官方允许的触发操作之一,因此这是唤醒这两个函数的自然选择,因为它们已经设置为监听队列。我绕了一下路,绕过了 API 所有者施加的最大访问限制。如果我让我的函数在消息到达时立即从队列中获取并访问 API,将会有多个函数几乎同时处理消息,这使得管道架构失效,因为它可能很容易超过 API 配额。通过设置每分钟的时间调度器(我为每个函数创建了两个调度器),处理频率从毫秒级下降到秒级。通过这种方式,数据管道中的消息流量得到了缓解。
e. 如何避免重复操作?
几乎不可能在不实际访问 API(真相来源)的情况下判断收集的数据是否是最新的。因此,我们不能减少 API 访问次数,而是可以像我在上一个问题中所做的那样降低超出 API 访问配额的风险。
如果数据流的目标是 DynamoDB,每次从 API 接收时都会完全更新每条记录。令人担忧的是,我们从 DynamoDB 到 S3 的火 hose 流带宽不足,导致运输偶尔中断。鉴于这一情况,我在更新之前插入了一个合理性检查。此检查将记录的每个属性值与最*从 API 提取的值进行比较。除非记录完全没有变化,否则现有记录将被覆盖。
附件是合理性检查函数:
def upToDate(table_name, condition, result, prefix):
"""
Check if a record in a given specified DynamoDB table is up-to-date, which means that it's no different from the API retrieval.
Args:
table_name (str): The name of the DynamoDB table to check.
condition (boto3.dynamodb.conditions.Key): The key condition expression for querying the table.
result (dict): The record to check for ingestion completion.
prefix (str): The prefix used for key matching.
Returns:
bool: True if the ingestion is completed, False otherwise.
Notes:
- The function queries the specified DynamoDB table using the provided condition.
- It retrieves the items matching the condition.
- The function compares the key-value pairs of the result with the retrieved items, accounting for the provided prefix if applicable.
- If all key-value pairs match between the result and the retrieved items, the ingestion is considered completed.
- The function returns True if the ingestion is completed, and False otherwise.
"""
table = dynamodb.Table(table_name)
retrieval = table.query(KeyConditionExpression=condition)["Items"]
existing_items = 0
if len(retrieval) > 0:
for key in retrieval.keys():
if key.upper() not in reserved_words:
if result[key] == retrieval[0].get(key):
existing_items += 1
elif result["key"] == retrieval[0].get(prefix + key):
existing_items += 1
completed = len(retrieval) and existing_items == len(result.items())
# len(retrieval) == 0: the item doesn't exist in DynamoDB at all
# existing_items == len(result.items()): the item exists and all its key-value pairs
# are synced up with API
return completed
部署
在每个文件夹中,执行
pip install -r requirements.txt
然后返回到上一级文件夹:
# copy utils.py to each folder
for d in */; do cp utils.py "$d"; done
# build the cloudformation
sam build -u
# invoke the functions locally for local testing
# event.json should be like:
# {
# "branches": ["Scranton"]
# }
# env.json should be like:
# {
# "Parameters": {
# "Environment": "local"
# }
# }
sam local invoke "BranchCollector" -e branch.json --env-vars env.json
sam local invoke "SalespersonCollector" -e branch.json --env-vars env.json
sam local invoke "SalesCollector" -e branch.json --env-vars env.json
# deploy it onto AWS
sam deploy --parameter-overrides Environment=dev
最终工作
版权信息
-
作为代码的图表提供了所有支持简洁可视化的工具
-
《办公室》(The Office是一个精彩的节目,带来的欢笑和灵感。
如何基于 AWS 构建级联数据管道(第二部分)
原文:
towardsdatascience.com/how-i-built-a-cascading-data-pipeline-based-on-aws-part-2-217622c65ee4
自动化、可扩展和强大
·发布于 Towards Data Science ·10 分钟阅读·2023 年 8 月 25 日
--

照片由Mehmet Ali Peker拍摄,发布在Unsplash
之前,我分享了使用 AWS CloudFormation 技术开发数据管道的经验。然而,这不是一个最优的方法,因为它留下了 3 个待解决的问题:
-
部署必须手动进行,这可能增加错误的机会;
-
所有资源都创建在一个单一的堆栈中,没有适当的边界和层次;随着开发周期的进行,资源堆栈会变得越来越重,管理它将会是一场灾难;
-
许多资源应当在其他项目中持续使用和重用。
简而言之,我们将以敏捷的方式增加该项目的可管理性和重用性。
解决方案
AWS 使用户能够实现 2 种 CloudFormation 结构模式:跨堆栈引用和嵌套堆叠。跨堆栈引用代表了一种单独且通常独立地开发云堆栈的设计风格,而所有堆栈之间的资源可以根据引用关系相互关联。嵌套堆叠指的是由其他堆栈组成的 CloudFormation 堆栈。这是通过使用[AWS::CloudFormation::Stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stack.html)资源实现的。

现实生活中的嵌套堆栈:一个充满巢穴/蛋的巢(照片由 Giorgi Iremadze 拍摄,来源于 Unsplash)
因为我们旨在实现更好的项目管理,所以项目将通过分层分离来拆分,而嵌套堆栈将对此提供帮助。然而,鉴于现有堆栈工件之间的内在关系,我们还需要进行跨堆栈引用。
实现
我们创建了 3 个 Lambda 函数,3 个 DynamoDB 表,1 个 IAM 角色及其附加策略,几个 SQS 队列和几个 Cloudwatch 警报。由于这些功能本身的复杂性,在这个版本中,它们将被定义在不同的模板中,服务仅供自己使用,包括警报和死信队列。除此之外,IAM 资源将是另一个嵌套堆栈,DynamoDB 表也是,为了保持其可重用性。用于在 Lambda 函数之间传递消息的 SQS 队列也将是一个不同的堆栈。这些嵌套堆栈将被放在一个新的目录中,称为 /templates。
Cascading-ETL-pipeline
├── LICENSE
├── README.md
├── branches
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── lambda_function.py
│ ├── requirements.txt
│ └── service
│ ├── config.py
│ └── service.py
├── sales
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── lambda_function.py
│ ├── requirements.txt
│ └── service
│ ├── config.py
│ └── service.py
├── salespersons
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── lambda_function.py
│ ├── requirements.txt
│ └── service
│ ├── config.py
│ └── service.py
├── templates
│ ├── BranchCollector.yml
│ ├── SaleCollector.yml
│ ├── SalespersonCollector.yml
│ ├── iam.yml
│ ├── queues.yml
│ └── tables.yml
├── template.yml
└── utils.py
与之前相比,这一部分的编码工作相对轻松,因为我们主要只需将这些原始代码从一个文件移动到另一个文件。例如,在配置 DynamoDB 表堆栈时,我所做的仅是复制代码片段并粘贴到新文件中:
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters: # Type: String
Environment:
Type: String
Resources:
BranchDynamoDBTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Delete
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
-
AttributeName: "branch_id"
AttributeType: "S"
KeySchema:
-
AttributeName: "branch_id"
KeyType: "HASH"
StreamSpecification:
StreamViewType: NEW_IMAGE
TableName: !Sub branch-${Environment}
SalespersonDynamoDBTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Delete
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
-
AttributeName: "employee_id"
AttributeType: "S"
-
AttributeName: "branch_id"
AttributeType: "S"
KeySchema:
-
AttributeName: "employee_id"
KeyType: "HASH"
-
AttributeName: "branch_id"
KeyType: "RANGE"
StreamSpecification:
StreamViewType: NEW_IMAGE
TableName: !Sub salesperson-${Environment}
SaleDynamoDBTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Delete
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
-
AttributeName: "sale_id"
AttributeType: "S"
-
AttributeName: "employee_id"
AttributeType: "S"
KeySchema:
-
AttributeName: "sale_id"
KeyType: "HASH"
-
AttributeName: "employee_id"
KeyType: "RANGE"
StreamSpecification:
StreamViewType: NEW_IMAGE
TableName: !Sub sale-${Environment}
跨嵌套堆栈引用
值得注意的是,还有一些修改是必要的。如果我们需要确保堆栈内定义的资源可以被堆栈外部的其他资源引用,那么那些导出堆栈需要一个新的部分,称为 Outputs,以输出引用值。在我们的案例中,IAM 角色将被全球使用,因此在其定义之后,我们会输出其 ARN,以便在一定范围内可见。
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters: # Type: String
Environment:
Type: String
Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${Environment}-lambda-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
- events.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSLambdaExecute
- arn:aws:iam::aws:policy/AmazonSQSFullAccess
- arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
Path: '/'
LambdaPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: !Sub '${AWS::StackName}-${Environment}-lambda-policy'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: EventBusAccess
Effect: Allow
Action:
- events:PutEvents
Resource: '*'
- Sid: LambdaInvokeAccess
Effect: Allow
Action:
- lambda:InvokeFunction
Resource: "*"
- Sid: LogAccess
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
Roles:
- !Ref LambdaRole
Outputs:
Role:
Description: The role to be used across the stacks
Value: !GetAtt LambdaRole.Arn
Export:
Name: !Sub ${Environment}-Role
同样,消息队列也以相同的方式导出:
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters: # Type: String
Environment:
Type: String
Resources:
EmployeeQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub employee-queue-${Environment}
VisibilityTimeout: 900
RedrivePolicy:
deadLetterTargetArn:
Fn::GetAtt: EmployeeWorkloadDeadLetterQueue.Arn
maxReceiveCount: 10
EmployeeWorkloadDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub employee-workload-dead-letter-queue-${Environment}
MessageRetentionPeriod: 1209600
SaleQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub sale-queue-${Environment}
VisibilityTimeout: 900
RedrivePolicy:
deadLetterTargetArn:
Fn::GetAtt: SaleWorkloadDeadLetterQueue.Arn
maxReceiveCount: 10
SaleWorkloadDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub sale-workload-dead-letter-queue-${Environment}
MessageRetentionPeriod: 1209600
Outputs:
EmployeeQueue:
Description: The SQS queue that delivers the payloads from branch collector to salesperson collector
Value: !Ref EmployeeQueue
Export:
Name: !Sub ${Environment}-EmployeeQueue
SaleQueue:
Description: The SQS queue that delivers the payloads from salesperson collector to sale collector
Value: !Ref SaleQueue
Export:
Name: !Sub ${Environment}-SaleQueue
与此同时,从其他地方导入资源的堆栈也需要做些许调整。AWS 通过提供一个内置函数 [Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html) 来支持此功能。以“分支收集器”为例。它涉及到 IAM 角色和一个消息队列,这些都不是在同一堆栈中创建的。因此,每当出现上述资源时,我将它们替换为 Fn::ImportValue 函数的值。
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters: # Type: String
Environment:
Type: String
Resources:
BranchCollector:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub branch-collector-${Environment}
Handler: lambda_function.lambda_handler
Runtime: python3.8
CodeUri: ./../branches/
Description: updating branch info in our DynamoDB table
MemorySize: 128
Timeout: 900
Role:
Fn::ImportValue:
!Sub ${Environment}-Role
Environment:
Variables:
LOGGING_LEVEL: INFO
APP_ENV: !Ref Environment
SQS:
Fn::ImportValue:
!Sub ${Environment}-EmployeeQueue
DB: !Sub branches-${Environment}
DeadLetterQueue:
Type: SQS
TargetArn:
Fn::GetAtt: BranchFunctionDeadLetterQueue.Arn
Events:
StartScheduledEvent:
Type: Schedule
Properties:
Schedule: rate(1 hour)
# dead letter queue
BranchFunctionDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub branch-function-dead-letter-queue-${Environment}
MessageRetentionPeriod: 1209600
# alarms
BranchErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref BranchCollector
EvaluationPeriods: 1
MetricName: Errors
Namespace: AWS/Lambda
Period: 300
Statistic: Sum
Threshold: 1
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-name}
BranchDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref BranchCollector
EvaluationPeriods: 1
MetricName: Duration
Namespace: AWS/Lambda
Period: 60
Statistic: Maximum
Threshold: 750000
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-name}
BranchThrottleAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref BranchCollector
EvaluationPeriods: 1
MetricName: Throttles
Namespace: AWS/Lambda
Period: 300
Statistic: Sum
Threshold: 1
AlarmActions:
- arn:aws:sns:us-east-1:{id}:{alarm-name}
根堆栈
现在所有嵌套堆栈都已定义并创建,应该有一个根堆栈将整个基础设施集成在一起。考虑到使用嵌套堆栈风格的基础设施是一个层次结构,根堆栈是所有嵌套堆栈所归属的父级(尽管嵌套堆栈也可以作为其他堆栈的父级)。在我们的案例中,替换定义单个资源的现有片段为对预定义的 template.yml 中嵌套堆栈 CloudFormation 模板的引用非常简单。
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters: # Type: String
Environment:
Type: String
Resources:
# =========================================================================================
# IAM ROLES, POLICIES, PERMISSIONS
# =========================================================================================
IAM:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./templates/iam.yml
Parameters:
Environment: !Ref Environment
# =========================================================================================
# AWS LAMBDA FUNCTIONS
# =========================================================================================
BranchCollector:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./templates/BranchCollector.yml
Parameters:
Environment: !Ref Environment
DependsOn:
- IAM
- Queues
SalespersonCollector:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./templates/SalespersonCollector.yml
Parameters:
Environment: !Ref Environment
DependsOn:
- IAM
- Queues
SaleCollector:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./templates/SaleCollector.yml
Parameters:
Environment: !Ref Environment
DependsOn:
- IAM
- Queues
# =========================================================================================
# AWS DynamoDB TABLES
# =========================================================================================
Tables:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./templates/tables.yml
Parameters:
Environment: !Ref Environment
# =========================================================================================
# AWS SQS QUEUES
# =========================================================================================
Queues:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: ./templates/queues.yml
Parameters:
Environment: !Ref Environment
从这里开始,可以放心地说 CloudFormation 升级已经完成!
等等,这是否意味着我们需要重建和重新部署?遗憾的是,是的,这正是下一节将要解决的问题。
自动部署
由于我总是将我编写的所有代码存储在 GitHub 仓库中,我打算利用 GitHub Actions。GitHub Actions 是 GitHub 的一个功能,用于自动化存储在任何 GitHub 仓库中的工作流,从而无缝支持构建、测试和部署。尽管有预定义的工作流,但由于我们任务的独特性和复杂性,我们需要自定义一个满足我们需求的工作流。
一般来说,工作流应该模拟我们在软件构建和部署阶段在云环境中手动执行的操作。即包括以下步骤:
-
为参数分配值,例如
environment -
设置 AWS 配置,例如输入默认的 AWS 凭证和服务区域代码
-
授予 AWS 角色访问将要使用的服务/资源的权限
-
创建一个 S3 桶作为 CloudFormation 存储位置
-
将通用代码库克隆到每个独立目录
-
构建和部署无服务器应用程序模型(SAM)
为了在工作流文件中将它们全部包装起来,格式如下:
name: A workflow that automates the data pipeline deployment
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- '.gitignore'
- '*.png'
- 'README.md'
pull_request:
paths-ignore:
- '.gitignore'
- '*.png'
- 'README.md'
jobs:
deploy:
container:
image: lambci/lambda:build-python3.8
runs-on: ubuntu-latest
env:
BUCKET_NAME: your-bucket-name
steps:
- name: Set Environment
id: setenv
run: |
echo "Running on branch ${{ github.ref }}"
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "::set-output name=env_name::prod"
else
echo "::set-output name=env_name::dev"
fi
- name: Set Repo
id: setrepo
run: |
echo "::set-output name=repo_name::${{ github.event.repository.name }}"
- name: Set Branch
id: setbranch
run: |
echo "::set-output name=branch_name::${{ github.head_ref}}"
- name: Checkout
uses: actions/checkout@v2
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{secrets.AWS_ACCESS_KEY_ID}}
aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY}}
aws-region: us-east-1
# role-to-assume: arn:aws:iam::807324965916:role/cdk-hnb659fds-deploy-role-807324965916-us-east-1
role-duration-seconds: 900
- name: Install sam cli
run: 'pip3 install aws-sam-cli'
- name: Complete policies
run: |
aws iam attach-user-policy \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess \
--policy-arn arn:aws:iam::aws:policy/CloudWatchEventsFullAccess \
--policy-arn arn:aws:iam::aws:policy/AWSLambda_FullAccess \
--policy-arn arn:aws:iam::aws:policy/IAMFullAccess \
--policy-arn arn:aws:iam::aws:policy/AWSCloudFormationFullAccess \
--policy-arn arn:aws:iam::aws:policy/AmazonSQSFullAccess \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
--user-name Memphis
- name: Create S3 Bucket
run: |
if ! aws s3api head-bucket --bucket "${{env.BUCKET_NAME}}" 2>/dev/null; then
aws s3api create-bucket --bucket "${{env.BUCKET_NAME}}"
else
echo "Bucket ${{env.BUCKET_NAME}} already exists"
fi
- name: Copy utils.py
run: 'for d in */; do cp utils.py "$d"; done'
- name: build
run: sam build && sam package --s3-bucket ${{env.BUCKET_NAME}} --s3-prefix "${{steps.setrepo.outputs.repo_name}}/${{steps.setbranch.outputs.branch_name}}/${{steps.setenv.outputs.env_name}}" --output-template-file packaged.yaml --region us-east-1 || { echo 'my_command failed' ; exit 1; }
- name: deploy
run: sam deploy --template-file packaged.yaml --s3-bucket ${{env.BUCKET_NAME}} --s3-prefix "${{steps.setrepo.outputs.repo_name}}/${{steps.setbranch.outputs.branch_name}}/${{steps.setenv.outputs.env_name}}" --stack-name "${{steps.setrepo.outputs.repo_name}}-${{steps.setenv.outputs.env_name}}-stack" --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND --region us-east-1 --no-fail-on-empty-changeset --parameter-overrides Environment=${{steps.setenv.outputs.env_name}} || { echo 'my_command failed' ; exit 1; }
在这里我包括了一个名为“deploy”的任务。它通过 lambci/lambda:build-python3.8 镜像进行容器化,并在 Ubuntu 系统上运行,这在此任务中进行了定义。创建了一个名为 BUCKET_NAME 的变量,用于存储我们将要命名的 S3 桶的字符串值。
第 1 步是创建一个新的参数 env_name,用于存储工作环境名称的值。它取决于触发工作流的分支:除非工作流在主分支上运行,否则称为 prod;否则为 dev。
第 2 步在另一个输出值中记录仓库名称。这将成为 S3 桶中模板文件位置前缀的一部分。
第 3 步类似于第 2 步,获取将来作为前缀另一部分使用的分支名称。
第 4 步简单地使用 actions/checkout,这是一个软件包,用于在不同的工作区中检出当前的仓库,以便工作流能够访问它。
第 5 步还使用了一个包。在aws-actions/configure-aws-credentials@v1的帮助下,我们可以轻松配置 AWS 工作环境,只需提供 AWS 访问密钥 ID、AWS 秘密访问密钥和区域即可。请注意,建议将你的敏感凭证(AWS 访问密钥 ID 和 AWS 秘密访问密钥)保存在秘密中。
第 6~11 步在 bash 命令行中执行。按照这个顺序,工作流的其余部分安装 SAM CLI;然后将所有必要的策略 ARN 附加到 IAM 用户;如果 S3 桶不存在,则使用作为环境参数提供的桶名称创建一个 S3 桶;最后但同样重要的是,将通过 SAM 命令构建、打包和部署 CloudFormation 堆栈。
使用工作流,开发人员将避免不断重新运行部分步骤(如果不是全部步骤的话)。相反,只要创建了拉取请求,任何新的提交都会触发一个新的工作流运行,代码在非主分支上;当拉取请求合并时,最*的工作流也会被触发,同时处理主分支上的资源。
离开之前
感谢你读到这里。在这篇文章中,我讨论了一个主要的升级,以提高代码的可管理性和重用性,同时介绍了一个自动化堆栈构建和部署的解决方案。如果你对细节感兴趣,随时查看我的仓库!
顺便说一下,我组织资源和工件的方式也可以是启动微服务的一个很好的开端。如果你对微服务感兴趣,别忘了订阅以跟踪我的下一篇文章!
如何在 10 分钟内利用 LlamaIndex 构建定制的 GPT 聊天机器人
Python 的概述和实现
·发表于 Towards Data Science ·6 分钟阅读·2023 年 4 月 12 日
--

照片来源:Miguel Á. Padriñán: www.pexels.com/photo/two-white-message-balloons-1111368/
不久前,我读到了一篇由 Jerry Liu 撰写的文章,介绍了 LlamaIndex,这是一种利用 GPT 来综合响应用户提供信息的接口。
[## 如何利用非结构化数据和 LlamaIndex 将 LLM 的力量带入你的数据
(合著者:LlamaIndex 的创始人 Jerry Liu 和 Unstructured 的 CEO Brian Raymond)
它立刻引起了我的注意,我知道我必须亲自尝试一下。
毕竟,许多在世界上引起轰动的大型语言模型(LLM)由于没有用用户可用的数据进行训练,因此用途有限。
因此,仍然需要可定制的 LLM,以满足用户的需求。简单来说,企业需要能够处理文本总结、问答(Q&A)以及使用他们自己信息生成文本的 LLM。
为了看看 LlamaIndex 是否有潜力满足这一需求,我尝试了它的功能,并对我能够做到的事情感到真正惊讶。我甚至在大约 10 分钟内构建了一个 Q&A 聊天机器人!
在这里,我将带你深入了解我的旅程。
LlamaIndex 的功能是什么?
LlamaIndex 通过将 LLM 连接到用户提供的信息来生成查询响应。
如文档中详细描述的,使用 LlamaIndex 涉及以下步骤:
-
加载文档
-
将文档解析成节点(可选)
-
构建索引
-
在构建的索引上构建索引(可选)
-
查询索引
实质上,LlamaIndex 将数据加载到文档对象中,然后将其转换为索引。当索引接收到查询时,它将查询传递给 GPT 提示,以合成响应。默认情况下,这使用的是 OpenAI 的 text-davinci-003 模型。
虽然这个过程听起来相当复杂,但实际上可以用很少的代码来执行,正如你将要发现的那样。
设置
为了测试 LlamaIndex 的多样性,我最终构建了 3 个不同的聊天机器人,每个机器人都使用不同的数据源。为了简洁起见,之前提到的可选步骤(即第 2 步和第 4 步)将被省略。
首先,让我们处理好前提条件。
可以使用以下命令通过 pip 安装 LlamaIndex 和 OpenAI:
pip install llama-index
pip install openai
用户还需要一个来自 OpenAI 的 API 密钥:
import os
os.environ['OPENAI_API_KEY'] = 'API_KEY'
项目还需要以下导入:
加载数据
数据可以手动加载,也可以通过数据加载器加载。在这个案例中,我加载了 3 种数据:
-
一个本地 .txt 文件,其中写有我最喜欢的水果(在GitHub 仓库中可以访问)
第一个索引将使用本地 .txt 文件创建,该文件位于名为 data 的文件夹中。这些数据将手动加载。
第二个索引将使用来自 Wikipedia 页面关于苹果的数据创建。它可以通过 LlamaIndex 的数据加载器之一进行加载。
第三个索引将使用展示如何制作香草蛋糕的 YouTube 视频构建。这些数据也将通过数据加载器加载。
构建索引
所有数据加载到文档对象中后,我们可以为每个聊天机器人构建索引。
每个索引可以通过一行代码从文档对象构建。
吃惊吗?从加载数据到创建索引,使用 LlamaIndex 只需要几行代码!
查询索引
现在,构建的索引可以为任何给定的查询生成响应。再次强调,这一步也可以用一行代码完成。
- 查询使用 .txt 文件构建的索引

代码输出(作者创建)
如果你在想,这就是正确的答案。
2. 查询使用 Wikipedia 页面(主题:苹果)构建的索引

代码输出(作者创建)
3. 查询使用 YouTube 视频(主题:香草蛋糕食谱)构建的索引

代码输出(由作者创建)
最后,值得注意的是,索引仅在包含所需上下文时才会提供查询的答案。
这是如何使用 YouTube 视频数据创建的相同索引对完全不相关的查询的响应。

代码输出(由作者创建)
幸运的是,LlamaIndex 似乎已采取措施防止幻觉(即模型自信地给出未由数据支持的答案)。
使用 Web 应用部署聊天机器人
最后,我们可以创建一个 Web 应用,以便与最终用户分享构建的索引。
为此,我们需要首先使用save_to_disk方法保存索引。
这些索引将用于 Streamlit 应用。整个应用的底层代码如下:
在应用中,用户可以选择他们希望根据其提问的数据源,并在提供的框中输入他们的查询。
我们可以看到在运行应用后,索引的表现如何:
streamlit run app.py
查询使用.txt 文件(我最喜欢的水果)构建的索引:

Streamlit 应用(由作者创建)
查询使用维基百科页面(苹果)构建的索引:

Streamlit 应用(由作者创建)
查询使用 YouTube 视频(香草蛋糕食谱)构建的索引:

Streamlit 应用(由作者创建)
很酷,对吧?我们在短短 10 分钟内构建了一个功能完整的 Web 应用!
结论

图片由Prateek Katyal提供,来源于Unsplash
到目前为止,我仅实现了 LlamaIndex 接口的基本功能。这个项目中有很多领域尚未探索,例如自定义 LLMs 和使用非默认设置。
欲了解更多信息,请访问文档。
如果你打算自己尝试这个工具,请注意使用 OpenAI 的 API 所产生的费用。这个项目只花了我$1,但这归功于我处理的小文档(定价基于你使用的令牌数量)。如果你过于投入,可能会收到一张令人不快的账单。
最后,创建 Q&A 聊天机器人所用的所有源代码可以在这个 Github 库中访问:
[## GitHub - anair123 的创建自定义 GPT 聊天机器人]
你现在无法执行该操作。你在另一个标签页或窗口中登录了。你在另一个标签页中已退出…
感谢阅读!
如何构建带有 AI 生成曲目的 Lo-fi 音乐网页播放器
使用 Tone.js 和 LSTM 模型制作
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 1 月 12 日
--
图片来源于 rupixen.com 在 Unsplash
介绍
自大学以来,低保真嘻哈音乐一直是我学*时的首选伴侣。它以相对简单的音乐结构营造出舒适和宁静的氛围。一些爵士和弦进行、律动的鼓点、环境声音和怀旧的电影台词可以让我们制作出相当不错的低保真嘻哈曲目。除了音乐方面,动画视觉效果也是低保真美学的关键部分,与水、风和火的自然声音一起营造氛围。
创建我自己的低保真网页播放器的想法是在一个星期天下午,当时我正在学*深度生成模型。我做了一些研究,并在假期期间完成了这个项目。该网页播放器提供两个选项:用户可以选择基于 Tone.js 编码的真实歌曲的低保真曲目,或者选择 AI 生成的独奏曲目。两个选项都会与用户在前一步中选择的鼓循环、环境声音和台词叠加在一起。本文主要讨论如何使用 LSTM 模型生成 midi 曲目,并在最后简要讨论如何使用 Tone.js 制作一首歌曲。
你可以在这里尝试网页版播放器,我建议使用桌面版 Chrome 浏览器以获得最佳体验。
LSTM 模型架构与 Midi 生成
在之前的帖子中,我解释了什么是 LSTM 网络。简要回顾一下,它是一种特殊类型的 RNN,能够更好地处理长期依赖。它还具有递归结构,将来自前一个时间步的输出传递到当前时间步。为了更好地理解它,我们可以展开网络,将 LSTM 单元看作是多个相同网络的副本,每个副本将信息传递给下一个时间步,如下所示。

展开 LSTM;作者创建
每个单元格包含四个主要组件,使其能够更好地处理长期依赖:
-
遗忘门:决定遗忘哪些信息
-
输入门:决定更新和存储哪些信息到我们的单元状态中
-
单元状态更新:进行逐元素操作以更新单元状态
-
输出门:决定输出哪些信息

在 LSTM 单元内部;作者创建
训练数据准备
对于我们训练模型所使用的音乐数据格式,我们有几种选择:原始音频、音频特征(例如 Mel 频谱图)或符号音乐表示(例如 midi 文件)。我们的目标是生成一个独奏曲目(即音符、和弦和休止符的序列),以便在其他组件(如鼓循环)上叠加,因此 midi 文件是实现我们目标的最简单和最有效的格式。原始音频的训练计算开销非常大。换句话说,以 48000kHz 采样的音乐片段意味着每秒音频中有 48000 个数据点。即使我们将其下采样到 8kHz,这仍然是每秒 8000 个数据点。此外,仅有旋律或和弦进行的干净音频极其稀少。然而,如果我们足够努力,仍然可以找到一些只包含和弦进行/旋律的 midi 文件。
在这个项目中,我使用了一些来自 YouTube 创作者Michael Kim-Sheng的 lo-fi midi 样本,他慷慨地允许我使用他的文件。我还利用了一些在这个Cymatics lo-fi toolkit中授权用于商业用途的 midi 文件。为了确保我在质量数据上训练我的模型(适用于 lo-fi 嘻哈的合理和弦进行和节拍),我听了一部分来自每个来源的曲目,并筛选出我的训练数据集。模型架构的灵感来自于经典钢琴作曲家库这里。
加载和编码 midi 文件
一个 Python 包music21可以用于加载 midi 文件。Music21 解析 midi 文件并将每个音乐组件存储到特定的 Python 对象中。换句话说,音符将被保存为 Note 对象,和弦将被保存为 Chord 对象,而休止符将被保存为 Rest 对象。它们的名称、持续时间、音高类别和其他属性可以通过点号表示法访问。Music21 将音乐片段存储在如下面所示的层次结构中,我们可以相应地提取必要的信息。如果你对如何使用这个包感兴趣,包的官方网站有一个适合初学者的用户指南,The Sound of AI 的 Valerio Velardo 也有一个教程来介绍如何使用 music21。
如前所述,music21 将每个音符、休止符和和弦存储为一个 Python 对象,因此下一步是对它们进行编码,并将它们映射到模型可以训练的整数上。模型输出应该包含不仅仅是音符,还包括和弦和休止符,因此我们将分别对每种类型进行编码,并将编码值映射到整数。我们对所有的 midi 文件执行此操作,并将它们连接成一个序列以训练模型。
-
和弦:获取和弦中音符的音高名称,将它们转换为正常顺序,并用点连接成字符串格式,“#.#.#”
-
音符:使用音高名称作为编码
-
休止符:编码为字符串“r”

Midi 编码与映射
创建输入和目标对
现在我们已经得到了适合模型的 midi 数据编码。下一步是准备输入和目标对,以便输入到模型中。在一个简单的监督分类机器学*问题中,存在输入和目标对。例如,一个对狗品种进行分类的模型将以狗的毛色、身高、体重和眼睛颜色作为输入,而标签/目标将是狗所属的具体品种。在我们的情况下,输入将是从时间步 i 开始的长度为 k 的序列,而对应的目标将是时间步 i+k 的数据点。因此,我们将遍历编码后的音符序列,并为模型创建输入和目标对。最后一步,我们将输入的维度转换为适合 keras 的格式,并对输出进行 one-hot 编码。
模型结构
如前所述,我们将使用 LSTM 层作为核心结构。此外,该网络还使用了以下组件:
-
Dropout 层:通过在训练期间以一定频率随机将输入单元设置为 0 来规范化网络并防止过拟合(在我们的例子中,频率为 0.3)
-
密集层:完全连接前一层,并在每个节点执行矩阵-向量乘法。最后一层密集层需要具有与网络中唯一音符/和弦/休止符的总数相同的节点数。
-
激活层:如果用于隐藏层,则为我们的网络添加非线性;如果用于输出层,则帮助网络进行分类。
model = Sequential()
model.add(LSTM(
256,
input_shape=(network_input.shape[1], network_input.shape[2]),
return_sequences=True
))
model.add(Dropout(0.3))
model.add(LSTM(512, return_sequences=True))
model.add(Dropout(0.3))
model.add(LSTM(256))
model.add(Dense(256))
model.add(Dropout(0.3))
model.add(Dense(n_vocab))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='rmsprop')
model.fit(network_input, network_output, epochs=200, batch_size=128)
对于这个示例网络,使用了 3 层 LSTM,每层 LSTM 后面跟有 2 层 dropout 层。接着是 2 层全连接的密集层,最后是一个 softmax 激活函数。由于输出是分类的,我们将使用分类交叉熵作为损失函数。使用了 RMSprop 优化器,这在 RNN 中相当常见。还添加了检查点,以便在不同的训练轮次中定期保存权重,并在模型训练结束之前使用。请随意调整模型结构,并尝试不同的优化器、轮次和批量大小。
输出生成与解码回 Midi 音符
输出生成过程类似于训练过程——我们给模型一个长度为 m 的序列(我们也称之为序列 m 以简化符号表示),并要求它预测下一个数据点。这个序列 m 的起始索引是从输入序列中随机选择的,但如果我们愿意,也可以指定一个特定的起始索引。模型输出是一个来自 softmax 的概率列表,告诉我们每个类别作为下一个数据点的适合程度。我们将选择概率最高的类别。为了生成长度为 j 的序列,我们会通过删除序列 m 的第一个元素并将最*生成的数据点添加到这个序列 m 中来重复这个过程,直到模型生成 j 个新的数据点。
从上一段生成的数据仍然是整数,因此我们使用编码过程中相同的映射将其解码回音符/和弦/休止符。如果它是和弦字符串格式,我们将从字符串“#.#.#.#”中读取整数符号,并创建一个 music21.chord 对象。如果它是音符或休止符,我们将相应地创建一个对应的音符和休止符对象。同时,我们在每个时间步将生成的新数据点追加到预测输出序列中。有关此过程的示例,请参见下面的示例流程,我们使用 3 个数据点的输入序列生成 4 个数据点的序列。

输出生成和 MIDI 解码
现在我们有了一系列的音符、和弦和休止符。我们可以将它们放入一个 music21 流中并写出 MIDI 文件,在这种情况下,所有的音符都会是四分音符。为了保持输出的趣味性,我添加了一个代码片段,随机抽取一个时长来指定每个音符或和弦的时长(默认的概率分布是 0.65 用于八分音符,0.25 用于十六分音符,0.05 用于四分音符和二分音符)。休止符默认为十六分休止符,以避免音符之间的沉默过长。
NOTE_TYPE = {
"eighth": 0.5,
"quarter": 1,
"half": 2,
"16th": 0.25
}
offset = 0
output_notes = []
for pattern in prediction_output:
curr_type = numpy.random.choice(list(NOTE_TYPE.keys()), p=[0.65,0.05,0.05, 0.25])
# pattern is a chord
if ('.' in pattern) or pattern.isdigit():
notes_in_chord = pattern.split('.')
notes = []
for current_note in notes_in_chord:
new_note = note.Note(int(current_note))
new_note.storedInstrument = instrument.Piano()
notes.append(new_note)
new_chord = chord.Chord(notes, type=curr_type)
new_chord.offset = offset
output_notes.append(new_chord)
elif str(pattern).upper() == "R":
curr_type = '16th'
new_rest = note.Rest(type=curr_type)
new_rest.offset = offset
output_notes.append(new_rest)
# pattern is a note
else:
new_note = note.Note(pattern, type=curr_type)
new_note.offset = offset
new_note.storedInstrument = instrument.Piano()
output_notes.append(new_note)
# increase offset each iteration so that notes do not stack
offset += NOTE_TYPE[curr_type]
midi_stream = stream.Stream(output_notes)
midi_stream.write('midi', fp='test_output.mid')
一旦我们用不同的参数运行几次模型,并挑选出我们喜欢的曲目,我们会在任何数字音频工作站(DAW)中选择一种 lofi 风格的乐器效果,以便我们生成的曲目听起来更像真正的音乐。然后,我们转到 JavaScript 来构建我们的网页播放器。
使用Tone.js构建网页播放器
Tone.js 是一个用于在浏览器中创建互动音乐的网络音频框架。你可以使用它来构建很多有趣的互动网站(见演示)。但在我们的案例中,它提供了一个全局传输功能,以确保我们的鼓点、环境声音、引号和旋律同时播放。它还允许我们编写音乐乐谱、采样乐器、添加音效(混响、增益等)以及在 JavaScript 中创建循环。代码框架的感谢归功于Kathryn。如果你想要一个快速有效的 Tone.js 速成课程,我强烈推荐他们网站上的用例示例。最重要的收获是,对于我们创建的每个声音事件,我们需要通过toDestination()将它们连接到 AudioDestinationNode(即我们的扬声器),或者通过samplePlayer.chain(effect1, Tone.Destination)来添加音效。然后,通过Tone.Transport,我们将能够在主输出上启动、暂停和调度事件。
循环音频片段
鼓点、环境声音、引号和预生成的 AI 轨道都是通过 Player 类加载到网页播放器中的音频文件(.mp3 或.wav)。在加载来自网站的用户输入事件后,这些事件会被输入到 Tone.js 的 Part 类中以创建循环。
鼓点每 8 小节循环一次,环境音效每 12 小节循环一次,AI 独奏轨道每 30 小节循环一次。引号部分不循环,从第 5 小节开始。
使用乐器样本创建旋律和和弦进程
Tone.js 不提供我们在 DAW 中看到的软件乐器选项,只有采样器,允许我们通过加载几个音符来采样自己的乐器。然后,采样器会自动重新调整样本的音高,以创建未明确包含的音高。
然后,我们可以通过指定音符和音符出现的时间来写入旋律和和弦进程。我建议使用 TransportTime 来精确编码节拍。TransportTime 的格式为"BARS:QUARTERS:SIXTEENTHS",并使用零基数编号。例如,"0:0:2"表示音符将在第一小节的两个十六分音符后出现。"2:1:0"表示音符将在第三小节,经过一个四分音符后出现。我这样为 3 首现有歌曲编写了旋律和和弦进程:FKJ 的《Ylang Ylang》、Camille 的《La Festin》和 Tyler, the Creator 的《See You Again》。
网页播放器设计
我添加了函数,通过不同的环境声音输入来改变网页播放器的背景,以便每种环境下显示更合适的 gif。同时还有一个与歌曲音符连接的可视化工具,由 p5.js 制作,以增加视觉吸引力。
未来工作
LSTM 模型
-
添加序列开始和序列结束标记,以便模型可以学*音乐模式,当歌曲结束时。
-
融入音符时长的编码,以便启用节拍跟踪。
网络播放器
-
将后端 AI 模型连接到网络播放器,将会非常酷,这样输出可以实时生成。目前的障碍是模型生成输出需要几分钟,但我们可能可以利用预训练模型 API。
-
如果网络播放器允许用户 1) 输入他们自己选择的和弦进行 2) 写下文本并对其进行情感分析并输出匹配情感的和弦进行,那么用户互动将大大改善。
结论
关于代码和训练数据集,请参阅 GitHub 仓库这里。虽然这只是一个简单的 lo-fi 网络播放器,但我在 LSTM 模型和 Tone.js 上玩得很开心。每次看到我们如何将技术融入音乐体验中,总是让我感到惊讶。
我是如何构建一个编程语言的:通往成功的(艰难)之路
坦白说,“艰难”这个词还不足以形容。
·
关注 发表在Towards Data Science ·6 分钟阅读·2023 年 7 月 20 日
--
我的上一篇文章介绍了我的编程语言的语法,并提供了一个关于我如何构建它的总体思路。
但我决定写另一篇文章,专注于我朝最终结果前进的历程,因为说实话,这段旅程确实有更多的挫折而非成功,我必须克服的挑战也非常令人生畏。希望这篇文章也能成为那些正在进行类似项目、面临相似困难的人们的动力,提醒他们不要放弃,知道曾经有其他人也处于相同的位置,并最终达成了他们的目标。
那你为什么应该在意呢?
毕竟,你们都来自不同的背景,无论是数据科学,还是经典的 Python 和 Java。
即使你不感兴趣于构建自己的编程语言,许多我在这次经历中提升的软技能肯定会对你们所有人(尤其是对编程感兴趣的人)有启发作用(希望也是激励作用)。
你写程序时,有多少次每一行看起来都正确,但却得到一个似乎几乎无法调试的神秘错误,这时候你开始失去动力,想着要放弃。

在构建语言的过程中,我遇到了那些非常常见的问题,经过最终克服和坚持,这些经验希望能够传达一个值得铭记的信息,当你继续阅读这篇文章时!
开始动手
这个想法最初是在我上八年级(或者九年级,不太记得了)时产生的,但那时我几乎不了解条件语句和循环,所以我就把它忘了。
但是多年后,同样的想法重新点燃了我的脑海,只是我完全不知道该如何开始。

照片由 Towfiqu barbhuiya 在 Unsplash 提供
就像所有其他我不知道如何做的事情一样,我在谷歌上搜索了一下。
“如何制作自己的编程语言”。
好吧,结果相当有希望。我花了很长时间逐行分析这些代码,抓住了要点:
编程语言有两种类型:解释型和编译型。解释型语言通常构建起来更简单,但较慢。另一方面,编译型语言将你的代码转换为机器代码,然后执行它(编译型语言通常更快)。
构建语言的一般过程是:
-
定义语言的目的和语法: 对我来说这很简单(我知道我想构建一个与数学相关的语言),为了节省工作量并使语言尽可能高级,我决定只保留几个(基本且有用的)函数和两个变量,一个函数和一个浮点数。
-
构建词法分析器和语法分析器: 词法分析器是任何语言的第一个组件,将代码中的每个单词/短语分类(如关键字、操作符、注释等)。但仅靠这些词法单元本身并没有意义,要使其有意义,需要构建一个语法分析器。语法分析器确保词法单元遵循语言的语法,并将其放入抽象语法树(AST)中。
-
执行代码: AST 中的每个节点代表我们原始词法分析器收集的一个标记,包括函数名。因此,运行我们的 AST 允许我们逐行执行编写的代码;这就是解释型编程语言的基本原理。
但编译器需要一些额外的步骤;你需要使用流行的库,如 LLVM 和 libgccjit,将代码转换为编译后的可执行文件。
编译型还是解释型?
一方面,我可以创建一种解释型语言,这相当具有挑战性,需要我投入大量时间和精力。另一方面,我可以创建一种编译型语言,这更具挑战性,需要我投入更多时间和精力。
但构建自己编译型语言的想法实在是太吸引人了,我陷入了这个陷阱。
建议:不要这样做。
尽管有关于使用 LLVM 构建编译器的资源(强烈推荐观看那段 100 秒的视频,既有趣又有些教育意义),但由于文档过于复杂,我很快就迷失了。
我猜测,你可能比学*使用 LLVM 构建编译器的基础知识要更快地学会 Java 的基础知识。

图片由 Emile Perron 提供,来源于 Unsplash
巨大的时间浪费(还是说不是)?
对我来说,不幸的是,我花了两个多月的时间,先是折腾 LLVM,然后是 libgccjit。后者看起来更有希望,我甚至为编译器编写了代码,但随后遇到了大量关于设置 libgccjit 包的问题;我甚至下载了它的全部源代码,并将其放在与我的编译器代码相同的目录中。但仍然没有任何效果。我浏览了无数个 Reddit 和 StackOverflow 的帖子,但没有任何结果。
当 StackOverflow 无法帮助你时,你知道情况非常糟糕。
处于放弃的边缘
此时,我真的陷入了一个新的低谷,因为我很长时间没有取得丝毫进展,开始贬低自己所做的所有工作,比如从头开始构建词法分析器和解析器,而没有使用流行的 lex/yacc 和 flex/Bison 工具。在这时,我问自己:“我是否应该把这一切都扔进(虚拟的)垃圾桶里?”我很想说“是的”,但我不那么容易放弃,特别是在涉及编程相关的项目时。
所以我去散步,清理了思路,然后回到设计图板,这次的目标不同:构建一个解释型语言。
最终完成了它。
一旦我决定要构建一个解释器,我也做出了放弃 C 语言、回到 Python 的艰难决定。在正常情况下,最好使用较低级的语言来构建解释性语言,因为它们的速度弥补了解释器的慢速。
但我当时正处于绝望的时期,那时候需要采取绝望的措施。
回到印度后,我花了接下来的一周处于失眠状态,字面意义上全天 24 小时工作(如果不算早餐、午餐和晚餐的话,大约是 21 小时——我吃得很慢)。
接下来发生的事情是我第一次遇到的,并且可能不会再发生——我的代码第一次尝试就成功了!没有调试,没有更多的困惑。
当我运行我的源代码时,每一个组件:从词法分析器到解析器及 AST,都正常工作,并正确执行了每一行代码。

图片由 the blowup 提供,来源于 Unsplash
反思与最终的备注
上面的引用可能听起来有些陈词滥调,但这并不会削弱其有效性;在构建 AdvAnalysis 的过程中,我经历了比成功更多的失败,但最终,我却脱颖而出。
虽然你可以学*在构建自己的编程语言时需要注意的事项(例如编译型与解释型、应遵循的过程),但我得到的主要经验教训(希望你也能得到)是,无论程序中不断遇到令人困惑的错误多么令人沮丧,只要你能够保持耐心,回到起点并采取不同的方法,最终,它总会如你所期待的那样运作。
说实话,作为一名编程爱好者,你可能早就知道了这一点。 😃
我如何用 ChatGPT 编写了自己的私人法语 tutor
逐步指南,讲述了我如何利用最新的人工智能服务来学*一门新语言,从架构到提示工程
·
关注 发表在 Towards Data Science · 10 分钟阅读 · 2023 年 6 月 30 日
--
讨论中提到的外语 tutor 的代码可以在 [companion](https://github.com/shakedzy/companion) 我的 GitHub 页面上的 repo中找到,您可以在任何非商业用途下自由使用。

使用 Dall-E 制作
所以在推迟了一段时间之后,我决定重新开始我的法语学*。当我报名参加课程时,这个想法突然出现——如果我能编程让 ChatGPT 成为我的个人法语导师会怎么样?如果我能 与它对话 ,而它会回应我呢? 作为一名与 LLMs 合作的数据科学家,这似乎是值得构建的东西。我是说,是的,我可以直接和我的法语妻子对话,但这不如设计一个由 ChatGPT 构建的个人导师来得酷。爱你,亲爱的❤️。
但说真的,这个项目不仅仅是“另一个酷炫的代码玩具”。生成性 AI 正向我们生活的每一个领域进军,而大型语言模型(LLMs)似乎在这里占据主导地位。如今,一个人通过访问这些模型所能做的事情令人瞠目结舌,我认为这个项目值得我投入时间——我相信也值得你的时间——主要有两个原因:
-
使用 ChatGPT 作为知名的在线工具是很强大的,但将 LLM 集成到你的代码中是完全不同的事情。LLMs 仍然有些不可预测,当你的产品依赖于 LLM——或任何其他生成 AI 模型——作为核心产品时,你需要学会真正控制生成 AI。这并不像听起来那么简单。
-
获取第一个工作版本只用了几天工作时间。在生成 AI 和 LLMs 出现之前,这需要几个月,并且可能需要不止一个人。利用这些工具快速创建强大应用的力量是你真正需要自己尝试的——这是未来,至少在我看来,我们不会回头。
而且,这个项目实际上可以做一些好事。我妈妈真的希望找一个可以练*英语的对象。现在她可以做到,而且费用不到每月 3 美元。我的妻子的妈妈想开始学*韩语。情况相同,费用相同。当然,我自己也在使用!这个项目真的帮助了人们,费用比一杯小咖啡还少。如果你问我,这才是真正的生成 AI 革命。
从零开始
从高层次来看,我需要的有 4 个要素:
-
语音转文本,将我的声音转录为文字
-
大型语言模型,最好是一个聊天型 LLM,我可以向它提问并获得回答
-
文本转语音,将 LLM 的回答转换为声音
-
翻译,将我不完全理解的法语文本转换为英语(或希伯来语,我的母语)
幸运的是,现在是 2023 年,以上提到的一切都变得非常非常容易获得。我还选择使用托管服务和 API,而不是在本地运行这些服务,因为这样推理速度会更快。此外,这些 API 的个人使用价格非常低,使得这个决定毫无疑问。
经过尝试几种替代方案后,我选择了 OpenAI 的 Whisper 和 ChatGPT 作为我的语音转文本和大型语言模型,Google 的文本转语音和翻译作为剩余模块。创建 API 密钥并设置这些服务非常简单,我能够通过它们原生的 Python 库在几分钟内与所有服务进行通信。
经过测试这些服务后,我真正震惊的是我正在构建的导师不仅仅是一个英法翻译老师;由于 Whisper、ChatGPT 和 Google Translate & TTS 支持几十种语言,这可以用来学*几乎任何语言,同时使用任何其他语言进行交流。这真是疯狂!

由 Dall-E 制作
架构和线程处理
首先,让我们确保整体流程得到充分理解:(1) 我们从录制用户的声音开始,(2) 该声音发送到 Whisper API,并返回文本。(3) 文本被添加到聊天记录中,并发送到 ChatGPT,(4) ChatGPT 返回书面回复。其回复被(5) 发送到 Google Text-to-speech,Google 返回的音频文件将(6) 被播放。

高级架构
我第一个实际步骤是将其拆解为组件并设计整体架构。我知道我需要一个 UI,最好是 Web UI,因为现在通过浏览器启动应用程序比使用独立的可执行文件更简单。我还需要一个“后端”,即实际的 Python 代码,与所有不同的服务进行通信。但为了提供实时的流畅体验,我意识到我需要将其拆分为不同的线程。
主线程将运行大部分代码:它将把我的录音转录为文本(通过 Whisper),将此文本显示在聊天界面上,然后将导师的书面回复(由 ChatGPT 接收)也显示在聊天屏幕上。但我必须将导师的文本转语音移到一个单独的线程——否则我们将得到:
-
导师的声音只能在从 ChatGPT 接收到整个消息后才会被听到,而且其回复可能会很长
-
它将阻止用户在导师讲话时进行回应
这不是我想要的“流动”行为;我希望导师在消息显示在屏幕上时就开始讲话,并且绝对不要因为音频仍在播放而阻止用户并阻止其回应。
为此,项目的文本转语音部分被拆分为 两个 额外的线程。当从 ChatGPT 收到导师的响应时,每个完整的句子被传递到另一个线程,再从那里发送到文本转语音服务,转换为声音文件。我想在这里强调一下 文件 这个词——由于我将文本逐句发送到 TTS 服务,所以我也有多个音频文件,每个句子一个,需要按正确的顺序播放。这些声音文件然后从另一个线程播放,确保音频播放不会阻塞程序的其余部分运行。
使所有这些工作,以及源于 UI-服务器交互的其他几个问题,是 这个 项目中复杂的部分。令人惊讶,对吧——软件工程才是最难的。
设计用户界面

项目的用户界面
嗯,我知道我需要一个用户界面,我也大致知道我希望它看起来是什么样的——但编写用户界面超出了我的知识范围。所以我决定尝试一种新方法:我让 ChatGPT 为我编写用户界面。
为此,我使用了实际的 ChatGPT 服务(而非 API),并使用了 GPT-4(是的,我是一个自豪的付费用户!)。令人惊讶的是,我的初始提示:
Write a Python web UI for a chatbot application. The text box where
the user enters his prompt is located at the bottom of the screen, and
all previous messages are kept on screen
交付了一个惊人的初步结果,最终得到了一个 Python-Flask 后端、jQuery 代码、HTML 和匹配的 CSS。但这只是我希望获得的所有功能的约 80%,所以我花了大约 10 个小时与 GPT-4 来回沟通,一次一个请求,优化和升级我的用户界面。
如果我让它看起来很简单,我想明确说明其实并非如此。我添加的请求越多,GPT-4 就越困惑,并且交付的代码出现故障,这在某些时候比让它修复要手动纠正更容易。我有很多请求:
-
在每条消息旁边添加一个个人资料图片
-
为每条消息添加一个按钮,用于重新播放其音频
-
为每条法语消息添加一个按钮,将其翻译添加到原始文本下方
-
添加一个保存会话和加载会话的按钮
-
添加一个暗模式选项,使其自动选择正确的模式
-
添加一个“工作中”图标,以便在等待服务响应时显示
-
还有更多……
尽管通常情况下 GPT 的代码无法直接运行,但考虑到我在前端领域知识有限,结果仍然令人惊叹——远远超出了我仅凭谷歌搜索和 StackOverflow 能做到的任何事情。我在学*如何制作更好的提示方面也取得了很大进展。考虑到这一点,也许我应该再写一篇博客,专门讲述从零开始与 LLM 一起构建产品的经验教训……(嗯, 我确实做过)。
提示工程
对于这部分内容,我将假设你对如何通过 API 与聊天 LLM(如 ChatGPT)进行通信有一些基本了解。如果没有,你可能会有些迷失。

由 Dall-E 制作
最后但绝对不是最不重要的 — 我让 GPT 扮演一个私人导师的角色。
作为起点,我在聊天开始时添加了一个 系统提示。由于与 LLM 的聊天基本上是用户和机器人相互发送的消息列表,系统提示通常是聊天的第一条消息,用于向机器人描述它应该如何表现以及期望它做什么。我的系统提示看起来像这样(由花括号包围的参数会被运行时值替代):
You are a {language} teacher named {teacher_name}.
You are on a 1-on-1 session with your student, {user_name}. {user_name}'s
{language} level is: {level}.
Your task is to assist your student in advancing their {language}.
* When the session begins, offer a suitable session for {user_name}, unless
asked for something else.
* {user_name}'s native language is {user_language}. {user_name} might
address you in their own language when felt their {language} is not well
enough. When that happens, first translate their message to {language},
and then reply.
* IMPORTANT: If your student makes any mistakes, be it typo or grammar,
you MUST first correct your student and only then reply.
* You are only allowed to speak {language}.
这实际上产生了相当不错的结果,但似乎随着聊天的进行,我给机器人的行为指令(“当我错了时纠正我”,“始终用法语回应”)的有效性逐渐减弱。
为了应对这种逐渐消失的行为,我想出了一个有趣的解决方案;我在将用户消息发送给 GPT 之前对其进行了处理。不论用户的消息是什么,我都会在其中添加额外的文本:
[User message goes here]
---
IMPORTANT:
* If I replied in {language} and made any mistakes (grammar, typos, etc),
you must correct me before replying
* You must keep the session flow, you're response cannot end the session.
Try to avoid broad questions like "what would you like to do", and prefer
to provide me with related questions and exercises.
* You MUST reply in {language}.
在每个用户消息的末尾添加这些内容确保了 LLM 按照我希望的方式回应。值得一提的是,我添加的长后缀是用英语书写的,而用户的消息可能不是。因此,我在原始消息和我的附加内容之间添加了一个明确的分隔符(---),以结束原始消息的上下文并开始新的上下文。还请注意,由于这个后缀是添加到用户的消息中,因此是用第一人称(“我”,“我自己”等)书写的。这个小技巧显著改善了结果和行为。虽然这可能是显而易见的,但值得强调的是,这个后缀不会显示在聊天界面中,用户不知道它被添加到他们的消息中。它在幕后插入,在与聊天历史记录一起发送给 ChatGPT 之前。
我想要的另一种行为是让导师先发言,也就是说,ChatGPT 将在会话开始时发送第一条消息,而不等待用户启动会话。显然,这不是 ChatGPT 设计时的功能。
当我尝试让 ChatGPT 在仅包含系统提示的消息历史记录中回复时,我发现 ChatGPT “丢失了”,开始与自己进行聊天,扮演用户和机器人。不管我尝试什么,我都无法让它在用户先说话之前正确启动会话。
然后我有了一个想法。当会话初始化时,我代表用户向 ChatGPT 发送以下消息:
Greet me, and then suggest 3 optional subjects for our lesson suiting my
level. You must reply in {language}.
这个请求旨在使 GPT 的响应看起来正如我所认为的那样,应该是机器人的适当会话初始化方式。然后我从聊天中删除了我的消息,让它看起来好像是机器人自行启动了会话。
概述

使用 Dall-E 制作,已编辑
从一个有趣的小念头开始,几乎在瞬间变为现实,这完全是在一个非常忙碌的人空闲时间中完成的。任务变得如此简单,我感到非常惊讶。仅仅一年前,像 ChatGPT 这样的工具还是科幻小说中的内容,而现在我可以在自己个人的笔记本电脑上进行塑造。
这是未来的开始,无论未来会发生什么——至少我知道,我会用一种新的外语做好准备。再见!
如何使用 Python 创造 DALL-E 10000 份积分无法购买的生成艺术
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 7 月 19 日
--
Python 和 Pillow: 如何编写 DALL-E 无法做到的代码
在这篇博客文章中,我将展示一些我使用 Python 编程语言创建的生成艺术作品,具体利用了 Pillow 和 Torch 库。我的艺术创作灵感来源于奥地利音乐作曲家和视觉艺术家罗曼·豪本斯托克-拉马蒂的视觉构图。
在 2021 年初,我经常浏览 Catawiki,因为我想购买一些艺术品来装饰我的家庭办公室。当我在 2021 年初在 Catawiki 上发现 Haubenstock-Ramati 的作品时,我立刻被他复杂而美丽的参数艺术所吸引。我一直想用我的编程技能做一些创意性的工作,因此受到启发开发了可以生成类似输出的代码。下面的图像是激发我灵感的其中一幅图像,由 Roman Haubenstock-Ramati 创作。

Konstellationen,1970/1971 年,由 Roman Haubenstock-Ramati 创作
在 2022 年 4 月 Dall-E 2 发布后,我尝试使用该模型生成应该类似于 Haubenstock-Ramati 作品的艺术作品。要求模型这样做是一个有争议的话题,因为关于 AI 模型能否生成如此类似于艺术家作品的输出,以至于这些输出可能被视为对原作的版权侵权,存在有效的担忧。这一讨论超出了这篇博客的范围,但我想澄清的是,我输入到 Dall-E 的提示并不是为了生成 Haubenstock-Ramati 作品的精确复制品或贬低他的作品。我编写的代码也是如此,它们并不是为了分发他的作品的副本,而仅仅是演示如何使用 Python 创建视觉几何构图。
DALL-E 的输出很有趣,但并没有完全捕捉到他原作的精髓。输出缺乏 Haubenstock-Ramati 艺术作品中存在的精确约束和复杂性。我尝试了许多不同的提示,但始终无法接*我想要的效果。


Dall-E 根据我的提示生成的一些输出:“创建一幅罗曼·豪本斯托克-拉马蒂风格的绘画作品,融入图形符号和实验音乐构图的元素。画作应以黑白色为主,具有大胆的线条和几何形状,并应包含一个中心主题,代表作品的主题。”
为了简化过程,我向 Dall-E 提出了一个更简单的请求:“画一条垂直线连接到一个矩形,再连接一个正方形到这条线,然后用另一条垂直线将正方形连接到另一个矩形,最后用另一条垂直线将矩形连接到一个圆。”出乎意料的是,结果并不如我所愿。尽管提示很简单,Dall-E 仍然难以理解形状之间的预期关系,产生了意外的结果。


Dall-E 根据提示生成的图像:“绘制一个垂直线连接一个矩形,将一个方块连接到这条线,再用另一条垂直线连接方块到另一个矩形,最后用另一条垂直线将矩形连接到一个圆形。”
我清楚地意识到 Dall-E 无法处理几何约束的提示,我尝试了一个更简单的提示:“创建一个仅显示两条正交线的图”。这也被证明过于困难。


Dall-E 生成的图像根据提示:“创建一个仅显示两条正交线的图”
Dall-E 不能完成这一任务让我感到惊讶,但考虑到像 Dall-E 这样的模型如何工作,这并不令人意外。它基于潜在扩散,这本质上是一个噪声过程,不适合基于约束的精确提示。
接下来,我将展示我生成的图像,并详细讨论如何编写类似的代码。

使用我的代码生成的图像的单一示例。

一个展示我代码生成不同图像的 gif,显示了相同参数下图像的多样性。
我使用 Python 和 Pillow 制作了这些图像,不涉及任何机器学*。我的代码生成的图像通过 Torch 引入了随机性,Torch 是一个多功能的软件包,我因为其熟悉和方便而使用。它通常用于机器学*(ML)。但这些图像并不是通过机器学*(ML)生成的。
你可能会想知道图像的多样性来源于哪里,我个人喜欢我的代码能够生成给人类似感觉但仔细看又各不相同的图像。输出的多样性是一个至关重要的特性。我的代码生成的图像的变化源自于对随机变量的复杂使用。在概率论和统计学中,随机变量是其可能值是随机现象的结果的变量。
现在我将描述我代码生成图像的过程,并展示一些 Python 示例,展示这个生成过程从高层次的视角看起来是怎样的。
我们可以将生成过程分为 3 步。
-
步骤 1:生成中心图形。这是通过采样一个矩形、一条线、一种矩形、一个方块、一条线和一个圆形来完成的。这些图形被放置在固定的位置,形状的大小由随机变量确定。
-
步骤 2:从三个不同的分布中采样三个具有线条和相邻的簇。在每个簇中,放置了若干条具有不同起始和结束点的垂直线。
-
步骤 3:在线条的簇中采样并绘制圆形和矩形。

动图显示了单个图像的逐步生成过程。
步骤 1
为了理解随机变量在我的代码中的作用,请考虑我们图像创建过程中的第一步:形成一个纵向矩形,其特点是高度大于宽度。这个矩形,虽然看似简单,却体现了随机变量的作用。
矩形可以分解为四个主要元素:起始的 x 和 y 坐标,以及结束的 x 和 y 坐标。现在,这些点在从特定分布中选择时,转变为随机变量。但我们如何决定这些点的范围,或更具体地说,它们来自的分布呢?答案在于统计学中最常见且关键的分布之一:正态分布。
正态分布由两个参数定义——均值 (μ) 和标准差 (σ),在我们的图像生成过程中发挥着关键作用。均值 μ 表示分布的中心,因此充当随机变量值围绕的点。标准差 σ 量化了分布中的离散程度。它决定了随机变量可能取的值范围。本质上,较大的标准差会导致生成图像的多样性更大。
import torch
canvas_height = 1000
canvas_width = 1500
#loop to show different values
for i in range(5):
#create normal distribution to sample from
start_y_dist = torch.distributions.Normal(canvas_height * 0.8, canvas_height * 0.05)
#sample from distribution
start_y = int(start_y_dist.sample())
#create normal distribution to sample height from
height_dist = torch.distributions.Normal(canvas_height * 0.2, canvas_height * 0.05)
height = int(height_dist.sample())
end_y = start_y + height
#start_x is fixed because of this being centered
start_x = canvas_width // 2
width_dist = torch.distributions.Normal(height * 0.5, height * 0.1)
width = int(width_dist.sample())
end_x = start_x + width
print(f"start_x: {start_x}, end_x: {end_x}, start_y: {start_y}, end_y: {end_y}, width: {width}, height: {height}")
start_x: 750, end_x: 942, start_y: 795, end_y: 1101, width: 192, height: 306
start_x: 750, end_x: 835, start_y: 838, end_y: 1023, width: 85, height: 185
start_x: 750, end_x: 871, start_y: 861, end_y: 1061, width: 121, height: 200
start_x: 750, end_x: 863, start_y: 728, end_y: 962, width: 113, height: 234
start_x: 750, end_x: 853, start_y: 812, end_y: 986, width: 103, height: 174
采样一个正方形看起来非常相似,我们只需采样高度或宽度,因为它们是一样的。采样一个圆形则更简单,因为我们只需采样半径。
在 Python 中绘制矩形是一个简单的过程,特别是当使用 Pillow 库时。以下是如何做到这一点:
from PIL import Image, ImageDraw
# Create a new image with white background
# Loop to draw rectangles
for i in range(5):
img = Image.new('RGB', (canvas_width, canvas_height), 'white')
draw = ImageDraw.Draw(img)
# Creating normal distributions to sample from
start_y_dist = torch.distributions.Normal(canvas_height * 0.8, canvas_height * 0.05)
start_y = int(start_y_dist.sample())
height_dist = torch.distributions.Normal(canvas_height * 0.2, canvas_height * 0.05)
height = int(height_dist.sample())
end_y = start_y + height
start_x = canvas_width // 2
width_dist = torch.distributions.Normal(height * 0.5, height * 0.1)
width = int(width_dist.sample())
end_x = start_x + width
# Drawing the rectangle
draw.rectangle([(start_x, start_y), (end_x, end_y)], outline='black')
img.show()
步骤 2
在这些图像中的垂直线的背景下,我们考虑了三个随机变量,即:
-
线的起始 y 坐标(y_start)
-
线的结束 y 坐标(y_end)
-
线的 x 坐标(x)
由于我们处理的是垂直线,每条线只需要采样一个 x 坐标。线的宽度是恒定的,由画布的大小控制。
需要一些额外的逻辑来确保线条不交叉。为此,我们基本上需要将图像视为一个网格,并跟踪被占用的位置。为了简化问题,我们暂时忽略这一点。
这是一个在 Python 中的示例。
import torch
from PIL import Image, ImageDraw
# Setting the size of the canvas
canvas_size = 1000
# Number of lines
num_lines = 10
# Create distributions for start and end y-coordinates and x-coordinate
y_start_distribution = torch.distributions.Normal(canvas_size / 2, canvas_size / 4)
y_end_distribution = torch.distributions.Normal(canvas_size / 2, canvas_size / 4)
x_distribution = torch.distributions.Normal(canvas_size / 2, canvas_size / 4)
# Sample from the distributions for each line
y_start_points = y_start_distribution.sample((num_lines,))
y_end_points = y_end_distribution.sample((num_lines,))
x_points = x_distribution.sample((num_lines,))
# Create a white canvas
image = Image.new('RGB', (canvas_size, canvas_size), 'white')
draw = ImageDraw.Draw(image)
# Draw the lines
for i in range(num_lines):
draw.line([(x_points[i], y_start_points[i]), (x_points[i], y_end_points[i])], fill='black')
# Display the image
image.show()
然而,这样做只会得到直线。另一部分是线末端的圆形,我称这些为相邻圆。随机变量也决定它们的过程。首先,是否会有一个相邻圆是从伯努利分布中采样的,而形状的位置(左、中、右)是从均匀分布中采样的。
圆形可以完全由一个参数定义:它的半径。我们可以将线段长度视为影响圆形半径的条件。这形成了一个条件概率模型,其中圆形的半径(R)依赖于线段长度(L)。我们使用条件高斯分布。该分布的均值(μ)是线段长度平方根的函数,而标准差(σ)是一个常数。
我们最初建议给定线段长度 L 的半径 R 服从正态分布。这被表示为 R | L ~ N(μ(L), σ²),其中 N 是正态(高斯)分布,σ 是标准差。
然而,这存在一个小问题:正态分布包括了采样负值的可能性。在我们的场景中,这个结果是不可能的,因为半径不能为负。
为了规避这个问题,我们可以使用半正态分布。这个分布与正态分布类似,由一个尺度参数 σ 定义,但关键的是,它被限制为非负值。给定线段长度的半径服从半正态分布:R | L ~ HN(σ),其中 HN 表示半正态分布。这样,σ 由所需均值确定,即 σ = √(2L) / √(2/π),确保所有采样的半径都是非负的,并且分布的均值是 √(2L)。
from PIL import Image, ImageDraw
import numpy as np
import torch
# Define your line length
L = 3000
# Calculate the desired mean for the half-normal distribution
mu = np.sqrt(L * 2)
# Calculate the scale parameter that gives the desired mean
scale = mu / np.sqrt(2 / np.pi)
# Create a half-normal distribution with the calculated scale parameter
dist = torch.distributions.HalfNormal(scale / 3)
# Sample and draw multiple circles
for _ in range(10):
# Create a new image with white background
img_size = (2000, 2000)
img = Image.new('RGB', img_size, (255, 255, 255))
draw = ImageDraw.Draw(img)
# Define the center of the circles
start_x = img_size[0] // 2
start_y = img_size[1] // 2
# Sample a radius from the distribution
r = int(dist.sample())
print(f"Sampled radius: {r}")
# Define the bounding box for the circle
bbox = [start_x - r, start_y - r, start_x + r, start_y + r]
# Draw the circle onto the image
draw.ellipse(bbox, outline ='black',fill=(0, 0, 0))
# Display the image
img.show()
第 3 步
我们过程中的第 3 步是第 1 步和第 2 步元素的结合。在第 1 步中,我们处理了在设定位置采样和绘制矩形的任务。在第 2 步中,我们学会了如何使用正态分布在画布的一部分上绘制线条。此外,我们还掌握了如何采样和绘制圆形的知识。
当我们过渡到第 3 步时,我们将重新利用前面步骤中的技巧。我们的目标是将方形和圆形和谐地分布在我们之前采样的线条周围。正态分布将在这个任务中再次派上用场。
我们将重用用于创建线条集群的参数。然而,为了增强视觉效果并避免重叠,我们对均值(μ)和标准差值引入了一些噪声。
在这一步中,我们的任务不是定位线条,而是放置采样的矩形和圆形。我鼓励你尝试这些技巧,并看看是否能将圆形和矩形添加到你的线条集群中。
在这篇博客文章中,我已经解剖并简化了支撑我的代码的过程,以便更深入地理解其操作方式。我展示了像 Dall-E 这样的生成性 AI 模型在遵循精确约束方面的困难。
编写产生这些图像的代码对我来说是一次极好的体验。看到每写一行代码,图像逐渐变化,真的很酷。我希望这篇博客文章激发了你对艺术与编程交汇点的兴趣。我鼓励你利用你的编程技能,通过代码让你的想象力变为现实。无需耗尽你的 Dall-E 额度,创造的力量就在你的指尖。
我在 2022 年的哭泣情况:对 365 天个人数据的分析
原文:
towardsdatascience.com/how-i-cried-in-2022-an-analysis-of-365-days-of-personal-data-190082a3c65
利用我收集的个人数据对我的哭泣模式进行调查
·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 1 月 6 日
--

本文中一张条形图的 Stable Diffusion 渲染图。由作者生成。
本文最初 发布在我的博客上
我对收集个人数据情有独钟。2022 年的每一天,我都会填写我制作的 Google 表单,记录我是否哭泣、是否锻炼、是否喝咖啡或是否洗头等项目。我还从 Apple Health 和 Google 位置历史中收集数据,以获得我全年模式和行为的更完整图景。在本文中,我通过结合所有这些数据提供了对 2022 年个人经历的洞察。
对我而言,2022 年是一个充满巨大变化和新机会的年份——我搬到了纽约,开始了一份新工作,并且旅行了许多城市。为了以数据科学家的方式反思刚过去的一年,我将所有这些数据结合起来进行分析,以了解我的哭泣模式——我在哪里哭泣,何时哭泣,哭泣的频率,以及一点点关于我为何哭泣的洞察。希望这些洞察能为我在新的一年中面对更多的哭泣日做好准备。(注:这些分析纯粹是为了趣味,并非十分严谨。未做任何统计声明。)
第一部分:分析结构化数据中的哭泣模式
数据来源概述
我结合了以下数据:
-
苹果健康数据,导出为 CSV(按照说明)。包括步行速度、步态不对称和行走/跑步的距离。
-
Garmin(运动手表)数据,导出为 CSV。包括心率、步数和爬楼梯的层数。
-
Flo 的月经健康数据,导出为 CSV。
-
Google 位置历史数据,通过Google Takeout获得。Google 位置数据在地理位置(非常具体的纬度和经度坐标)以及时间(精确到分钟)方面非常详细。我将纬度/经度四舍五入到城市级别,并选择了我每天所在的最常见城市。
-
Google 表单调查数据,导出为 CSV。包括我做了什么样的运动、喝了多少咖啡,以及是否哭泣。
在 2022 年,我在许多不同的地点哭泣。

按地点分解的哭泣天数。图表由作者创建。
在 2022 年,我共哭泣了 48 天。我将这些天数分配到纽约(我住的地方)、多伦多(我伴侣所在的地方)、西雅图(我的工作地点)、探访父母和旅行中。* 36%的哭泣天数是在多伦多。即使考虑到我在多伦多待了 49 天(相比在纽约的 239 天),我在多伦多哭泣的频率仍然比其他地方高得多。

每个地点的哭泣天数分解。图表由作者创建。
我还查看了在特定地点哭泣的频率(在城市中哭泣的天数百分比除以在该城市花费的总天数)。与其他地点相比,我在多伦多花费了*四分之一的时间在哭泣。
那么,为什么我在多伦多哭得这么多呢?一个原因可能是因为我在 2022 年 1 月和 2 月待在那里——那里不仅冷得刺骨,而且多伦多那时仍在封锁中。这可能解释了为什么我在多伦多哭了那么多时间。那里没有其他事情可做。
我等到周末来哭。

按工作日分解的哭泣天数。图表由作者创建。
我超过 50%的哭泣天数是在周末。我在周六和周日哭泣的次数远远超过了工作日。没有什么周末活动能比哭泣更有趣了。
我哭泣时是否运动都无所谓。

每个地点的运动分解。图表由作者创建。
锻炼对心理健康非常有益。我猜测在锻炼的日子里我可能会哭得少一点。上面是我在不同地点做的锻炼类型的分解。我在纽约和多伦多主要做武术(泰拳、拳击和一次咏春拳课程),而在我去西雅图或探望父母期间做了更多的有氧运动(跑步、骑车、徒步)。跳舞(萨尔萨舞、嘻哈舞)、步行(指那些我步行超过 10K 步但没有记录特定锻炼的日子)和其他锻炼(包括瑜伽、家庭锻炼和去普通健身房)则分布在不同地点。

每项练*的哭泣天数分解。图表由作者创建。
我计算了在做特定练*的日子里哭泣的百分比,相对于我做这些练*的总天数。乍一看,似乎我在跳舞的日子哭得更多。但实际上,我在整年里只去过 8 天跳舞,所以我在这 8 天中的 2 天哭泣并不足以强烈说明跳舞导致哭泣(或反之,哭泣导致跳舞?虽然那样的话会很有趣)。
另一方面,我在做武术的日子里哭泣最少。这是合理的,因为我去拳击馆的一个重要原因是发泄我的愤怒和挫折感。即使这种相关性可能是虚假的,这仍然是继续我所做的事情的好理由。
我对那些我完全不锻炼的日子里我哭泣的程度感到惊讶。我想知道,在我没有锻炼的日子里,我并不是把所有额外的时间都用来在家哭泣。
我在月经周期的不同阶段哭泣
我在 2022 年花了大量时间了解女性月经周期的不同阶段如何影响情绪、荷尔蒙健康等。我对自己在月经周期的哪个阶段哭泣更多感到好奇。根据我的经验,在月经开始前我总是哭泣,所以我猜测月经开始前我会看到更多的哭泣。
我查看了每个周期日子的哭泣百分比。我给相应的月经周期的日子上了色:月经期(第 1–5 天)、卵泡期(第 5–14 天)、排卵期(第 14–15 天)、黄体期(第 15–28 天)。

每天的哭泣天数分解,基于我的月经周期。图表由作者创建。
我在月经第 16 天哭泣的情况最严重。这通常是在排卵后,考虑到那时荷尔蒙波动很大,这也是合理的。我在第 21 和 22 天哭泣也很多,这大约是在月经开始前一周,通常是我感觉最糟糕的经前综合症时期。我在第 13 和 15 天(排卵前)以及第 5、8 和 9 天(经期结束后的前几天)哭泣最少。
所以,现实情况并不像“我在月经前哭得很多”那么明确。我在我的 28 天周期的所有阶段都会哭泣,但并不是每一天都一样。我在荷尔蒙波动较大的日子,例如排卵后和月经阶段开始前,会更多地哭泣。但我现在知道要记住,排卵后的时间特别容易流泪。
第二部分:分析非结构化文本中的哭泣模式
我无法在不包括至少一点机器学*的情况下分析我的个人数据。在文章的第二部分,我使用了非结构化数据来进一步分析我的哭泣*惯。
我在 2022 年每天都写日志。我使用了OpenAI 的文本嵌入将每一天的日志映射到文档级嵌入(本质上是捕捉文本片段精髓的一系列数字)。

2022 年日志嵌入的二维表示,经过 PCA 处理。图由作者创建。
这些嵌入的维度非常高,因此我使用 PCA 将嵌入降至 2 维。我绘制了前两个主成分,并根据那天是否哭泣来给每个文档嵌入上色。(注意:前两个主成分只解释了整个方差的 7%,这并不高)。乍一看,哭泣和不哭泣的日子的嵌入之间似乎没有明显的区别。这可能是因为哭泣的原因因每次情况而异,并且我每次哭泣时写作的方式可能不相同。
预测未来的哭泣日
最后,我想看看是否可以预测未来我更有可能哭泣的日子。
对于机器学*的朋友们:我根据时间将数据集划分为训练集和测试集(80%训练,20%测试)。我将数据分为训练数据和测试数据。在测试数据中,只有 12 天有哭泣(总共 72 天)。这是一个类别不平衡的例子,其中不哭泣的天数远多于哭泣的天数。在建模方面,我尽可能保持简单。我使用了一个现成的Gradient Boosting Classifier from sklearn。我尝试了更简单的模型,例如逻辑回归和随机森林,但结果太差没有包括那些。我没有进行任何超参数调优或额外特征工程。
我建立了两个分类器。每个分类器预测在给定的一天我是否哭泣:
-
第一部分基于所有结构化特征(例如 Google 位置、Apple 健康、调查数据)做出了预测。
-
第二部分基于日志嵌入做出了预测

对比使用和未使用嵌入的分类器的混淆矩阵。图由作者创建。
对于机器学*爱好者,我展示了每个分类器结果的混淆矩阵。第一个模型(未经嵌入训练)更容易预测一天是哭泣的,即使实际上不是。第二个模型(经过嵌入训练)并没有错误地将一天预测为哭泣,但也错过了大多数实际的哭泣天数。这两个模型的差异不大。
这一切意味着——这两个模型在实际检测哭泣的天数方面都不太好。由于数据不平衡(不哭泣的天数远多于哭泣的天数),通过简单地预测“未哭泣”每天来获得高准确率是很容易的。然而,(至少在这个早期阶段的建模中,未进行任何复杂处理)得出关于哭泣的明确指标的简单结论是困难的。特别是我的日记条目,并没有给出清晰的哭泣指示。这进一步支持了每次哭泣的原因、类型和本质都是多变的这一观点。预测我在某一天是否会哭泣是相当困难的!
结论
我喜欢新年——这是我最喜欢的节日。我喜欢在新年里立下决心和反思过去的一年。用我的个人数据来回顾我的一年——包括 2022 年的哭泣和锻炼*惯——感觉特别。

饼图展示了三种不同类别中哭泣与不哭泣的百分比。图形由作者创建。
并不是所有的见解都很有用。根据这些饼图,我在洗头、做艺术和喝咖啡的日子里哭泣的频率更高。由于这三种活动都带给我快乐或对我有益,我不会停止做这些活动。
如果我有更多时间,我希望能包含生活中其他方面的数据,例如 Spotify(音乐听取*惯)、Toggl(用于跟踪工作时间)和支出跟踪(我的钱都花到哪里去了?)。此外,我还希望使用 Apple 屏幕数据(目前无法导出)和睡眠数据(没有跟踪)。这些是我希望在明年的分析中包含的内容!
感谢阅读我的文章!如果你有任何反馈,请随时留下评论!如果你对数据清理过程感兴趣,请告诉我,我也可以分享那部分内容。
这篇文章最初发布在我的博客上。如果你喜欢这篇文章,可以在
www.artfish.ai关注更多类似文章。
我是如何在 6 个月内获得数据分析师职位的
原文:
towardsdatascience.com/how-i-got-a-data-analyst-job-in-6-months-cc6180de06c3
以及你如何在 2023 年通过利用 ChatGPT 和生成性 AI 做同样的事情。
·发布于 Towards Data Science ·10 分钟阅读·2023 年 11 月 18 日
--

图片来自 Motion Array
在 2019 年,我在国内一所最负盛名的大学攻读计算机科学学位——希望这能让我在数据行业找到一份工作。
然而,到我大学的第二年时,现实开始显现。
当我意识到我的学位并没有为我在该领域的工作做好准备时,我开始申请技术相关的实*——数据科学、数据分析和网页开发。
我不知道如何编程,缺乏解决商业问题所需的技术专长,也没有可以在简历上展示的市场技能。
在申请实*过程中经历了多次拒绝后,我意识到我不能再依靠我的学位来教会我获得技术行业工作的必要技能。
我花了所有的空闲时间参加在线课程并建立个人项目。
仅仅用了 6 个月的自学时间,我便获得了第一个数据分析师实*机会。
这次实*为我转型进入数据科学领域铺平了道路。现在我正在担任全职数据科学家。
在这篇文章中,我将分享我在 6 个月内自学数据分析的步骤。
想要视频版,请点击 这里。
为什么你应该成为数据分析师?
当我开始时,我的初衷是成为一名数据科学家。
然而,我发现作为数据分析师找工作要容易得多,因为这个角色的入门门槛比数据科学低。
这是因为大多数数据科学职位要求你拥有统计学或计算机科学等定量领域的硕士学位。
如果你没有技术背景,那么进入数据分析领域会比进入数据科学领域容易得多。
此外,大多数数据分析师职位并不要求你有数学或机器学*的背景——这两者都是数据科学家必须具备的技能。
数据分析师到底做什么?
虽然数据分析师的具体角色可能根据公司和行业有所不同,但这里是一般的工作流程:
你提取组织的电子表格或数据库中的数据,分析它,并向业务解释你的发现。
然后,公司会根据你的发现做出业务决策(与销售、营销、财务等相关)。
执行这种分析所用的工具会根据你的角色有所不同。有些角色只要求你掌握 Excel,而其他角色则要求你具备数据库和编程语言的知识。
我是如何在 6 个月内学*数据分析的?
这是我在 2020 年学*数据分析所采取的步骤:
1. SQL
SQL 代表结构化查询语言。它是一种允许你与公司数据库进行沟通的语言。
它让你从大型数据库中提取数据,转换数据,并利用这些数据解决业务问题。
这是数据分析师职位招聘中最受欢迎的技术技能之一。
我在刚开始时参加了 Udacity 上的一个免费的课程,名为SQL for Data Analysis。
这将教会你所有需要知道的概念,以便开始在关系数据库中处理数据——例如数据聚合、联接,甚至一些更复杂的主题,如子查询和窗口函数。
我推荐这个 Udacity 课程而不是互联网上成千上万的其他资源的原因是:
-
这直接切入重点。它教授你成为数据分析师所需的所有 SQL 知识。其他课程深入探讨如 ER 图和创建数据库等主题,这些对数据分析师的工作实际上并不必要。
-
每个主题之后都有练*题。你学*一个概念后,他们会给你一个业务问题。你需要编写 SQL 查询来解决这个问题,使用你刚学到的概念。
在我 3 年的技术经验中,我意识到通过实践你学到的东西要比仅仅从视频内容中获取信息多得多。
自己编写一行代码会比观看其他人输入 100 行程序学到更多东西。
我发现这个课程在理论和实践之间达到了完美的平衡。
2. BI 工具
我做的第二件事是学*使用商业智能(BI)工具。
商业智能(BI)工具让你将原始数据转换为仪表板或人们可以轻松理解的可视化格式。
简单来说,BI 工具将数据转换为可操作的见解。
再次强调,这里有很多东西要学*——数据可视化的基础、创建计算字段、构建仪表板。
而且很容易在互联网上大量资源中迷失方向。
所以,我建议你这样做:
市场上两个最受欢迎的 BI 工具包括 Tableau 和 PowerBI,根据 2022 年的调查,Tableau 的需求更高。
当你刚开始时,我建议你先参加一个 Tableau 在线课程,以学*基础知识。
完成此课程后,你将能够将原始数据转化为互动仪表板。
一旦你完成了这门课程,决定这是否是你想进一步学*的内容。
即使在数据分析领域,也有专门从事不同工作的人员。我认识一些分析师,他们的唯一工作就是使用 BI 工具构建仪表板。
如果你觉得自己更具创造力,喜欢创建可视化的过程,你可以深入研究并专注于仪表板。
你甚至可以获得Tableau 专家认证。
就我个人而言,我更像是一个通才。我做一点所有的工作,知道足够将原始数据转化为功能性仪表板。
3. Python(可选)
最终,我学*了 Python 编程。
我认为这对于数据分析师来说是一个可选技能——它并不是大多数角色的严格前提。
你应该学* Python 如果:
-
你计划将来转型到数据科学或其他更技术性的领域。
-
你有时间。这个过程花费了我大约 4-5 个月的练*,而我已有一些计算机科学学位的编程知识。
-
你想要争取更高的薪水。
对我来说,这绝对是值得的,因为学*编码为我打开了许多机会的大门。
我能够争取到更高的薪水,获得多个自由职业编程角色,并随后过渡到数据科学领域。
我还发现,一旦我学会了 Python,掌握其他编程语言就变得容易得多。
但是,如果你的唯一目标是尽快获得数据分析师的职位,你可以跳过这一步。
如果你确实想学 Python,你可以参加 Freecodecamp 的 Python for Beginners 课程。然后跟着在 Leetcode 和 Hackerrank 上的练*题。
这里有一篇更详细的文章,解释了如何从零开始学*编码。
如果我能在 2023 年重新开始,我会做些什么不同的事情?
现在你知道了我如何在 2023 年学*数据分析,如果我今天必须重新开始,这里是我会做的事情:
1. 我会学* Excel
当我作为数据分析师实*生加入我的第一家公司时,我被期望在 Excel 中进行并展示大量的分析。
这是因为 SQL 和 Python 对许多公司来说有些多余。他们中的很多人只是将数据存储在 Excel 文件中。
利益相关者和决策者也喜欢看到以 Excel 表格呈现的数据,因为他们对这种界面很熟悉。
他们中的大多数人不在乎你那些花哨的 Python 代码。
如果你时间紧迫,需要快速找到工作,或者确实没有精力学*太多工具,我建议你只学* Excel。
一旦你真正掌握了在 Excel 中分析数据的技能,你可以获得许多入门级的数据分析师职位。
当然,这些工作可能比那些需要 SQL 和编程技能的工作薪资低,但这是一个很好的起点。
2. 我会利用 ChatGPT 和 AI 学*数据分析
ChatGPT 对数据分析师来说是一个颠覆性工具。
它可以在几分钟内分析数据。
GPT-4 中的高级数据分析插件允许你上传数据集并自动化大量与 Excel 相关的工作。
它还可以创建可视化,生成 SQL 查询,并编写 Python 代码:

图片由作者提供(数据集由作者创建用于演示目的)
如果你不学*如何使用 ChatGPT 进行数据分析,你将比那些正在使用它的分析师处于劣势。
使用 ChatGPT,我能够将数据分析的速度提高了一倍。
365DataScience 有一个叫做 使用 ChatGPT 进行数据分析 的课程,我强烈推荐。
此外,Microsoft Copilot 也是一个很棒的工具,可以使用简单的文本提示将数据转化为商业洞察。
你只需输入一个问题,例如“哪个客户产生了最高的销售额”,它就会为你创建一个图表——你不需要输入公式或进行任何手动分析。

图片由作者提供
ChatGPT 和 Copilot 在数据分析领域都是颠覆性工具。学会使用它们。
摘要:2023 年如何学*数据分析
好吧,让我们快速回顾一下。
如果你时间紧迫,只能学*一个工具,那么学* Excel 和 用于数据分析的 ChatGPT。这些要求是进入该领域的最低标准。
如果你有时间全职学* 6 个月,学* Excel、SQL、BI 工具、ChatGPT,以及可选的 Python。
求职申请流程
一旦你掌握了我之前列出的技能,你可以开始申请数据分析师的职位列表。
申请实*
我建议从实*开始,因为它们的门槛较低。公司实际上是付钱让你在为他们工作时学*。
如果一切顺利,实*可以迅速转变为全职工作机会。我的 6 个月实*在我大学毕业后变成了一个永久职位。
提高你被录用的机会
当我刚开始申请实*时,遇到了多次拒绝。
大多数公司在我通过 LinkedIn 和 Indeed 申请时根本没有回应。我经常被“幽灵”了。
当我这样做时,我的成功率显著提高:
-
我列出了我所在地区招聘数据分析师的公司。
-
我筛选了我想要工作的公司。
-
我找到了该职位的招聘经理并给他们发送了个性化的邮件。
下面是我在申请数据分析师职位时发送的个性化邮件的一个例子:
Hi *recruiter name*,
I am Natassha, currently a final year computer science student.
I am writing to you because I'd like to seek an internship in *company name* from March 2021, for a period of around 4-5 months. I am available for a full-time internship during this period.
I am currently doing a part time internship with *company name*, and have worked on data analytics projects with clients such as *client names*. I also am a prolific content writer in some of the top Medium publications, and am a top writer in the field of artificial intelligence.
I would love to have the opportunity to utilise my skills in my area of expertise, and learn while working closely with a team of dynamic individuals.
I will link my portfolio, along with my LinkedIn profile below. If you are interested in knowing more about the work I do, please do revert back to me. I will then share my resume with you and be happy to discuss further.
Portfolio site: https://natassha.github.io/natasshaselvaraj/
LinkedIn: https://www.linkedin.com/in/natassha-selvaraj-33430717a/
Thanks for taking your time to read this. I look forward to hearing back from you.
Regards,
Natassha
发送这封邮件后,我获得了面试机会,最终在公司获得了实*职位!
另一个增加你作为数据分析师被聘用机会的方法是创建作品集项目。
这里是我的一个数据分析师作品集项目的例子。
随意从这个项目中汲取灵感并复制它,创造属于你自己的项目。
最后,我想通过谈论一个大多数人在学*困难内容时会遇到的问题来结束本文:深入研究。
如果你试图进入数据行业,互联网上可用的信息量可能令人畏惧。
这在生成式 AI 时代尤其如此——几乎每周都会推出新的 API 和与数据相关的功能。
仅在过去一年中,数据分析领域就因 Microsoft Copilot、Advanced Data Analysis、Noteable 和 Pandas AI 的引入而发生了显著变化。
虽然我总是推荐将 AI 融入你的工作流程,但几乎不可能跟上每个新功能的推出,尤其是如果你是该领域的完全初学者。
鉴于此,我建议使用本文之前提到的步骤来学*数据分析的基础知识。利用像 ChatGPT 这样的 AI 工具学*提示工程的基础,但不要在网上资源的海洋中迷失方向。
一旦你熟悉了分析师的核心工作流程,你可以随时开始引入新工具以最大化效率。
这样,你可以在熟悉基础知识的同时,利用最先进的技术使自己从其他分析师中脱颖而出。
这就是本文的全部内容,感谢阅读!
如何利用开源 LLM 在大型计算项目中实现巨额节省
利用开源 LLM 和 GPU 租赁实现大型计算项目的成本效益。
·发表在 Towards Data Science ·阅读时间 6 分钟·2023 年 8 月 30 日
--

图片由 Alexander Grey 提供,来源于 Unsplash
介绍
在大型语言模型(LLM)的世界中,计算成本可能是一个重大障碍,尤其是在大型项目中。我最*开始了一个需要运行 4,000,000 个提示的项目,平均输入长度为 1000 个 tokens,平均输出长度为 200 个 tokens。这接* 50 亿 tokens!传统的按 token 计费的方法(如 GPT-3.5 和 GPT-4 所用的)会导致高额账单。然而,我发现通过利用开源 LLM,我可以将定价模型转变为按小时计算计算时间,从而节省大量成本。本文将详细说明我采取的方法,并对每种方法进行比较。请注意,虽然我分享了定价经验,但这些费用可能会变化,并可能因您的地区和具体情况而有所不同。关键点是,利用开源 LLM 和按小时租赁 GPU 的成本节省潜力,而不是具体的报价。
ChatGPT API
我在一个小数据子集上使用了 GPT-3.5 和 GPT-4 进行了初步测试。两个模型都表现出色,但 GPT-4 在大多数情况下始终优于 GPT-3.5。为了让你了解成本,通过 Open AI API 运行全部 400 万个提示的成本大致如下:

运行 400 万个提示的总成本,输入长度为 1000 个 tokens,输出长度为 200 个 tokens
虽然 GPT-4 提供了一些性能上的好处,但相较于其对我的输出所增加的增量性能,成本却过高。相比之下,尽管 GPT-3.5 Turbo 更加实惠,但在性能上有所欠缺,在 2–3% 的提示输入中出现明显错误。鉴于这些因素,我不愿意在一个本质上是个人项目的项目上投资 $7,600。
开源模型
开源模型来救援!使用开源模型,定价模式非常不同:你只需按计算时间的小时收费。因此,你的目标是最大化每小时的计算迭代次数。此外,像 Petals.ml 这样的解决方案,你可以免费运行你的计算(有一定限制)!
在尝试了 Hugging Face 上的各种模型后,我发现一个名为 Stable Beluga 2 的 LLama-2 70B 微调版本给了我很好的性能,无需任何微调!它的表现优于 GPT-3.5 Turbo,但略低于 GPT-4。然而,这个模型仍然非常庞大,需要非常强大的 GPU。通常,最佳实践是使用尽可能小的模型来高效完成任务。因此,我尝试了 Stable Beluga 2 的 7B 版本。性能仍然不够好!
微调
为了提升任务性能,我使用了 GPT-4 和 Petals.ml(Stable Beluga 2 70B)来生成微调数据集。我通过 Petals.ml 生成了 25K 个提示完成对,并通过 GPT-4 API 生成了 2K 个。请注意,Petals.ml 允许你使用 bit-torrent 技术免费运行开源 LLM 模型。然而,推理时间并不理想:进行 4mm 迭代需要超过一年。
此外,Petals.ml 免费托管了这个模型。因此,我利用 Petals.ml 生成了 25,000 个提示完成对。 我还使用了 GPT-4 API 生成了额外的 2K 个提示完成对。事后看来,我可能在训练数据上做得过头了。我阅读过报告显示,少至 500 个微调样本就足以提升 LLM 的性能。无论如何,这里是使用 Open AI API 运行 2K 提示的总成本:

运行 2K 个提示完成对的总成本
没错:$84。你能猜到接下来发生了什么吗?使用这 27K 个提示完成对,我对最小的 LLama 2 7B 进行了微调,结果,这个新模型在我的用例中表现极其出色。总微调成本仅为 $6.6,因为我租用了一个 A100 GPU 6 小时。最终,我微调后的模型表现优于 GPT-3.5,略逊于 GPT-4。
推理
现在让我们谈谈推断计算成本。我的模型至少需要 20GB 的 VRAM,因此我们至少需要 RTX 3090。目前,有四个很好的 GPU 租赁选项:AWS、LambdaLabs、RunPod和Vast.AI。根据我的经验,Vast.AI 的价格最好,但可靠性最差。AWS 非常可靠,但比其他选项更贵。RunPod 价格优越,用户界面也很好。我个人想尽量降低成本,所以决定使用 Vast.AI 进行这个项目。Vast.AI 是一个点对点 GPU 租赁服务,因此它可能也是最不安全的。经过在 Vast.AI 上寻找最佳性价比的服务器后,以下是我找到的最佳选项:

Vast.AI 使用微调 LLama2 7b 模型的总项目成本。总成本是通过每秒的迭代次数、GPU 租赁的每小时价格和所需的总迭代次数来计算的。
如上所示,推断的总成本可以低于$99。在我的案例中,我启动了大约 10 台服务器,并计算了每台服务器每秒能产生的迭代次数。对于这个特别的项目,利用了微调的 Llama2–7B 变体,RTX A5000 在性价比方面是一个很好的选择。我还应该提到,如果没有开源库VLLM的极快速度,这一切都不可能实现。我的计算完全依赖于 VLLM 的支持。没有 VLLM,计算时间将增加 20 倍! 你也可以看看总运行时间并发笑——638 小时就是 26 天;然而,你可以轻松地在多个 GPU/服务器之间并行处理任务。
结论
总之,利用开源 LLM 以及将定价模式从按令牌计费改为按计算时间计费,使得在这个大型计算项目中实现了显著的成本节省。总成本,包括提示生成、微调和推断,仅为$189.58。这与使用像 GPT-4 或 GPT-3.5 Turbo 这样的模型的费用形成了鲜明对比,后者的费用分别为$167,810 和$7410.42。
这个项目的主要收获是利用开源 LLM 可以显著降低成本。通过按小时租用 GPU,费用与计算时间挂钩,而不是提示的数量,这可以带来巨大的节省,尤其是对于大型项目。这种方法不仅使这些项目在财务上更具可行性,而且为个人和小型组织提供了承担大型计算项目的机会,而不会产生过高的费用。
这个项目的成功突显了开源模型在大型语言模型和 AI 领域的价值与潜力,证明了创新且具有成本效益的解决方案在解决复杂计算任务中的力量。
定价说明
本文提供的定价信息基于我的个人经验,旨在作为一般比较。价格可能会根据你所在的地区和具体情况有所不同。关键点是利用开源 LLM 和按小时租赁 GPU 的潜在成本节省,而不是具体的报价。
📢 嗨!如果你觉得这篇文章有帮助,请考虑:
👏 鼓掌 50 次(这很有帮助!)
✏️ 留下评论
🌟 突出你觉得有洞察力的部分
👣 关注我
有任何问题吗?🤔 请随时提问。以这种方式支持我是一种免费且简单的方式来感谢我的详细教程文章!😊
GPU 租赁推荐:
最佳选择:RunPod.io GPU 租赁
最便宜:Vast.AI
其他深度学*博客
Andrew Ng 的深度学*专修课程 — 21 节课学到的知识
我如何设置数据科学项目(使用 VS Code 和 DVC)
来源于软件开发的灵感
·
关注 发表在 数据科学前沿 ·6 分钟阅读·2023 年 1 月 26 日
--
介绍
设置开发环境通常是开始任何编码项目时的第一步。一个有效的开发环境可以极大提升生产力,帮助我们产生高质量的工作。然而,与软件开发等其他领域相比,数据科学中的这个过程非常模糊,因为其自身的独特性和挑战。
在这篇文章中,我将解释如何为数据科学项目设置工作环境,包括我的动机以及我和我的团队使用的工具。

动机
当我还是软件工程师时,加入一个团队时我做的第一件事就是设置开发环境。这通常包括检查代码仓库、安装所需的库/框架以及熟悉第三方工具。一旦完成,你就有了一个可以与团队一起使用的工作环境。但当我转向数据科学时,这样的过程在我的公司并不存在。数据科学家在基于云的笔记本服务上工作,代码没有版本控制,数据被上传到云存储却没有任何文档。融入团队完全是一片混乱,更不用说与他人协作了。
为了减少痛苦,我开始设计一个标准的团队工作环境,以软件开发中的环境为灵感。然而,数据科学不同于软件开发。简单地复制和粘贴是不够的。数据科学有其自身的挑战,因此需要一些修改。以下是我在设计数据科学工作环境时考虑的 3 个额外需求:
-
代码和数据跟踪
与软件项目不同,数据科学项目不仅仅是代码的产物,还有数据。一个有效的数据科学工作环境需要对代码和数据进行版本控制。
-
无缝的远程开发体验
处理超出普通机器容量的数据是数据科学项目中的常见情况,这就是为什么人们更喜欢在远程工作站上运行代码。我们设计的环境应该使远程开发体验尽可能无缝。
-
易于使用的实验跟踪
数据科学工作是实验性的。数据科学家可以整天调整超参数,以比较其对模型的影响。因此,易于使用的实验跟踪设施是必不可少的。
既然我们已经明确了需求,下一步就是找到实现这些需求的工具。
工具
Git + DVC
从项目跟踪开始,Git 是多年来软件开发中的通用标准,因此代码版本控制只有一个选项。然而,数据版本控制更为棘手。Git 被设计用于处理小型和基于文本的文件,而我们的大多数机器学*项目包含大量和无结构的数据。我们探索了两个选项:Git LFS 和 DVC。最终我们选择了 DVC,因为它的功能更适合数据科学。

用 Git 和 DVC 跟踪代码和数据(图片由作者提供)。
Visual Studio Code
如前所述,ML 项目通常需要超出本地机器的计算能力。这就是为什么 ML 从业者使用 Google Colab、SageMaker 和 Vertex AI 等基于云的 notebook 服务。但 Jupyter notebook 不具备现代 IDE 的任何编码辅助功能。那些功能如果使用得当,可能会大幅提高生产力。因此,我们从 Jupyter notebook 迁移到了 Visual Studio Code。
对于计算资源,我们使用 VS Code 的远程开发功能连接到云远程服务器上的代码和数据仓库。这样,我们不必在高计算能力和良好的开发体验之间做出选择。我们可以兼得二者。有关 VS Code 远程开发的更多细节可以在这里找到。

在 Jupyter notebook 上编码 VS 在 VS Code 上编码(图片由作者提供)。
DVC VS Code 扩展
数据科学工作流的另一个关键方面是实验跟踪。数据科学家在获得最终模型之前会反复运行不同参数的 ML 实验。跟踪这些实验是一个极其耗费精力的任务。有许多实验跟踪工具,如 TensorBoard、MLFlow 和 WandB 等。我们对这些工具的担忧是,它们要么是需要托管的独立服务,要么是需要将数据发送到其 API 的第三方 SaaS。
我们最终选择了 dvc metrics,这是 DVC 的一个功能。DVC metrics 允许你将指标保存到 JSON 或 YAML 文件(我们在使用 DVC 之前已经这样做了)并指定这是一个指标文件。DVC 会开始类似于其他数据那样跟踪它,只是你可以使用 CLI 绘制、可视化和比较该文件的内容。通过 CLI 生成的实验报告在用户友好性方面明显不如 TensorBoard 和 MLFlow 提供的那些。幸运的是,DVC 提供了一个 VS Code 扩展,这有助于将这个问题缓解到我们认为可以接受的程度。
我们发现使用 DVC 管理实验的一个局限性是,当多个工程师同时在同一个模型上工作并希望比较他们的结果时,他们必须克隆其他人的分支,因为指标是在 Git 上跟踪的。由于我们团队较小,这种情况不常发生。因此,这是我们可以接受的局限性。

DVC VS Code 扩展(图片来自 Code Extension Marketplace)。
工作流程
以下是我们在开发数据科学项目时使用的流程。
-
创建 git 和 DVC 仓库
我们首先创建一个使用 Cookie Cutter Data Science 项目模板的 git 仓库。拥有一个标准项目模板有助于开发人员更顺畅地协作,因为项目已经有了明确的结构。一旦代码设置完成,我们使用 DVC 将数据添加到项目中,并推送到我们的远程存储,以便团队可以访问。
-
设置远程开发环境 接下来是设置一个数据科学家运行实验的开发服务器。我们打开一个 EC2 实例,登录到服务器,从 git 中检出代码,并使用 DVC 从远程存储中拉取数据。
-
实施与实验
现在开发环境已准备就绪,我们可以开始工作。如前所述,我们使用 VS Code 进行开发,需要安装两个扩展:Remote SSH 和 DVC。第一个用于远程连接到上一阶段创建的环境,以便我们可以在高性能机器上运行代码。DVC 扩展主要用作实验跟踪工具。安装和配置完毕后,数据科学家可以开始编写代码、运行实验和调整参数。
-
打开合并请求并进行代码审查
一旦数据科学家获得满意的模型,他们将提交所有更改并向 git 仓库打开合并请求,要求进行代码审查。当拉取请求被提出时,我们的 CI/CD 流水线将创建一个报告,比较新分支与目标分支的指标,以便其他人可以在批准/拒绝请求之前查看新更改对模型的影响(如果你对这部分感兴趣,请查看这篇文章)。

工作流程概述(图由作者提供)。
结论
就是这样。这就是我如何使用 Git、VS Code 和 DVC 为数据科学项目设置开发环境的方法。一旦我的团队开始在我们的团队中使用这个标准,新成员的入职过程变得更加顺利,因为对新成员应该使用的工具和流程没有了模糊之处。此外,团队成员之间的合作变得更加有效,因为他们使用的是相同的语言。
最初发表于 https://thanakornp.com.
我作为全职数据科学家如何跟上最新的 AI 趋势
不,我不会只是让 ChatGPT 告诉我
·发表于Towards Data Science ·阅读时长 8 分钟·2023 年 5 月 1 日
--

图片来源:Werner Du plessis 在Unsplash
数据科学发展迅速。
比如说,真的很快。
五年前,如果你说你对 AI 感兴趣,那大致等于你拥有量子数学的博士学位或背上纹了“我爱π”的纹身。现在,我的妈妈给我发 ChatGPT 的搞笑图片,而你甚至可以购买教婴儿神经网络的图画书。
依靠伪造 AI 对话的日子已经一去不复返了。如今,你必须真正掌握相关知识。那么,如何跟上人工智能和数据科学领域的最新动态呢?
尽管已有大量优秀的建议,但我发现很多其他人推荐的策略并不适合我。作为一名在行业中工作的数据科学家,我很少有时间进行研究,把所有时间都花在阅读学术论文和技术发布文档上是不现实的。我需要那些简洁、明了且应用性的资源——帮助我理解如何在日常工作中使用新技术的资源。
在这本指南中,我将总结对我有效的资源。我的目标是强调那些从应用角度有帮助的资源(而不仅仅是从商业或研究角度)。如果你是数据科学家或有志成为数据科学家,我希望这些资源能帮助你保持相关性,并在日常工作中发现创新机会。
说到这里,我们开始吧。
阅读其他数据科学团队的博客
许多数据科学团队都有博客——我个人喜欢的博客包括Netflix、Tripadvisor、Duolingo、Meta和Spotify。我喜欢这些博客的原因在于它们非常实用。虽然许多学术研究论文关注数据科学和机器学*的理论方面,但公司博客往往强调数据科学如何在实践中解决现实世界的问题。
作为一名在工业界工作的数据科学家,我发现这些内容非常有帮助。我不需要筛选大量的理论术语或解读数百个灰度流程图。我可以直接切入问题的核心,理解数据科学是如何应用于解决我在日常工作中遇到的各种问题的。
比如前几天,我在寻找有关 A/B 测试的信息:这是数据科学中一个我个人经验较少的常见框架。我找到了一些还算可以的文章,然后偶然发现了这个在Netflix 技术博客上的精彩系列文章。它不仅解答了我关于 A/B 测试的问题,还充满了 A/B 测试在 Netflix 中应用的实际例子。这对理解理论和实践都非常有帮助。
开始在线写作

图片由Daniel Thomas提供,发布在Unsplash。
我开始在 Medium 上写作的主要原因之一是为了帮助我学*。
不是为了教学或营销,而是为了学*。
人们常说你记住阅读内容的 10%,看到的 20%,而95%则是你所教授的内容。这些具体数字的准确性是次要的:重点在于,如果你仅仅依赖于阅读他人的博客,你可能无法充分获取或记住你所阅读的所有信息。开设一个数据科学博客是巩固知识和强化学*内容的极好方式。它迫使你逐步思考问题,并帮助你深入理解主题。
如果写作在线上的想法让你感到有些不自在,不用担心。我曾经也有同样的感觉!但问问自己:最糟糕的情况是什么?在我的情况下,我发现最糟糕的结果可能只是同事们的轻微戏弄。总体而言,我认为我可以应对这种情况,而且考虑到我从写作中获得的经验,这完全值得。
如果你不确定写什么,你可能会喜欢我上周写的 这篇 文章,关于如何想出数据科学项目创意:
如何寻找独特的数据科学项目创意,使你的作品集脱颖而出 [## 如何寻找独特的数据科学项目创意,使你的作品集脱颖而出]
忘记 Titanic 和 MNIST:选择一个独特的项目,提升你的技能并帮助你从人群中脱颖而出
或者,看看 Towards Data Science 的 常见问题页面 上的一些建议:这是我在寻找初始创意时查看的地方。
订阅 TLDR AI Newsletter
阅读和撰写博客是跟上数据科学和 AI 最新 技术 发展的好方法,但如果你想了解 商业 方面的内容,我建议订阅 AI 专注的新闻简报,这些简报跟踪初创公司、收购和研究趋势。
我个人最喜欢的是 TLDR AI Newsletter:他们每天发送一封邮件,总结行业中的关键新闻,这比他们更成熟的 科技新闻简报 要专注得多。其他的选择包括 MIT 技术评论的 The Download 和 DeepLearning.AI 的 The Batch。我喜欢这些新闻简报,因为它们让我只需 2 分钟就能阅读,并帮助我快速了解 AI 世界之外发生的事情。
关注‘每周 ML 论文’ GitHub 仓库
DAIR.AI 维护了一个很棒的 GitHub 仓库,每周更新一份包含 10 篇新机器学*论文的列表。对于每篇论文,DAIR 提供了简要总结和链接到一条推文,进一步解释论文的发现。如果你不想‘关注’这个 GitHub 仓库或 订阅邮件更新,你也可以在 Twitter 上关注 DAIR.AI。
就个人而言,我发现这些简短的总结比你从 Google Scholar 或 arXiv 等服务获得的提醒要“更具实用性”。当在 Google Scholar 和 arXiv 上设置提醒时,你很快就会被通知淹没,而且,如果你像我一样,可能会因为信息过载而完全不参与。DAIR.AI 的一个好处是,它们为你完成了繁重的工作,筛选出最有趣和前沿的论文,并给你选择要么阅读更多,要么快速丢弃并继续前进的选项。
订阅 YouTube 上的数据科学频道
Two Minute Papers 是一个完全符合其名字的 YouTube 频道。每周,该频道上传两个新视频,每个视频旨在提炼最*研究论文的发现,其中许多集中在 AI 领域。就在我写这段文字时,他们的 AI and Deep Learning 播放列表中有 495 个视频。
订阅这个频道是跟踪 AI 研究最新进展的好方法。我特别喜欢他们经典的“OpenAI 玩捉迷藏”视频,但说实话,有这么多精彩的视频,很难选择。
我特别喜欢的另外两个频道是 StatQuest with Josh Starmer 和 3Blue1Brown。我喜欢这些频道,因为它们用一种非常直观和易于理解的方式解释统计学和机器学*概念,而不假设大量的先前知识。虽然这些频道更著名的是它们的入门课程,但它们也发布了关于机器学*前沿主题的解释性视频,例如 3Blue1Brown 最*的视频关于卷积:
参加网络研讨会和午餐学*
很多组织会主办免费的网络研讨会,讲解数据科学和 AI 的最新创新。就个人而言,我很喜欢这些,因为预订网络研讨会迫使我腾出时间用于学*和发展,这对确保我有保护的时间来跟上最新进展非常有帮助。
比如说,如果你在日常工作中使用像 Google BigQuery 或 AWS RDS 这样的云数据库系统,你可能会受益于参加 Google 或 AWS 主办的网络研讨会,这些研讨会讲述了如何充分利用这些工具。我最*参加了其中一个(一个非常棒的 BigQuery 网络研讨会),主题是优化 SQL 代码以减少成本和查询运行时间。
在 Twitter 上关注一些 AI 大佬
如果你像我一样,很容易迷失在许多 AI 新闻稿和技术文档的细节中。Twitter 是阅读专家更直接意见和建议的好地方。我特别推荐关注Yann LeCun、Timnit Gebru、Geoffrey Hinton、Andrew Ng和Christopher Manning。像这些 AI 研究人员在 Twitter 上分享他们工作的最新动态,并提供有用的小贴士,这些信息并不总是出现在科学论文中。
然而,我喜欢 Twitter 的主要原因是,它是关注在行业(而不仅仅是研究)中工作的数据科学家和 AI 从业者的好方式。像Chris Albon、Jay Alammar和Cassie Kozyrkof这样的人处理的是与数据科学家在行业中日常工作密切相关的问题,关注他们是了解其他数据科学组织正在做什么的好方法。
还有一件事:Twitter 比 arXiv 更幽默。
结论
如果你想在 AI 和数据科学领域建立职业生涯,紧跟最新趋势是绝对必要的。
然而,根据我的经验,我发现很容易被特定任务的琐碎细节吸引,而忽视了整个行业的动态。在这篇文章中,我的目标是突出那些帮助我保持最新趋势和发掘日常工作中创新机会的策略。如果你认为我遗漏了什么宝贵的见解,请在评论中告诉我——我很想听听对你有效的方法。
还有一件事——你能成为我那 1%的人吗?
在 Medium 上,点击‘关注’按钮的读者不到 1%,所以无论你是在 Medium、Twitter还是LinkedIn,你的关注都非常重要。
如果你想无限访问我的所有故事(以及 Medium.com 的其他内容),你可以通过我的推荐链接注册,每月$5。与通过普通注册页面相比,这不会额外增加你的成本,并且有助于支持我的写作,因为我会获得少量佣金。
我是如何将 ChatGPT 转变为类似 SQL 的图像和视频数据集翻译器
这是一个涉及提示工程、软件工程、试验和错误、以及辛勤工作的过程
·
关注 发表在 Towards Data Science ·21 分钟阅读·2023 年 6 月 8 日
--
VoxelGPT 使用自然语言查询图像数据集。图片由作者提供。
与表格数据不同,计算机视觉任务的数据集是非结构化的——可以想象成一堆像素、大量标签、标签包以及一些有时结构化的元数据。然而,那些从事计算机视觉工作的人仍然需要能够高效地筛选这些数据宝库,以理解数据集,准备训练集和测试集,发现模式,识别边缘案例,并评估模型性能。
当我需要理解视觉数据时(这基本上是一直需要的),我使用开源库 FiftyOne,它定义了一种强大的 Python 语法来查询计算机视觉数据。这有点像计算机视觉数据的 SQL,它允许我以编程方式筛选、排序和语义切片由图像、视频甚至 3D 点云组成的数据集。
几个月前,随着 ChatGPT 热潮的全面爆发,我在 OpenAI 的网站上看到一个例子应用,将自然语言输入转换为 SQL 查询。虽然这个应用程序相当基础,而计算机视觉数据则复杂得多,但这让我思考:是否可以为图像和视频数据集做类似的事情?换句话说:
我们能否利用大型语言模型 (LLM) 的多功能性,将自然语言查询转换为对非结构化计算机视觉数据集的过滤视图?
答案是?是的,我们可以!
将 LLM 的通用语言和推理能力与 FiftyOne 的查询语言相结合,我们在 Voxel51 的团队构建了 VoxelGPT:一个开源 AI 助手,可以让你无需编写一行代码即可全面查询计算机视觉数据集!
我们使用了 langchain、tiktoken 和 fiftyone 完成了这个任务。
你可以在 gpt.fiftyone.ai 免费试用!
这篇文章将带你了解构建领域特定 LLM 应用程序的提示工程、软件工程和大量试错过程。
在这个过程中,我们多次遇到瓶颈,并担心任务是否不可行。如果你正在尝试并挣扎于构建一个 LLM 驱动的应用程序,我希望这篇文章能给你突破自己困境的灵感!
这篇文章的结构如下:
-
类似 SQL 的图像和视频查询
-
定义总体任务
-
为模型提供上下文
-
生成和利用示例
-
拆解问题
-
投入生产
查询语言

VoxelGPT 使用自然语言查询图像数据集。图片由作者提供。
在我们深入讨论如何使用 LLM 生成查询之前,值得花一点时间描述一下我们希望模型翻译的查询语言。以下是你需要的基本信息。有关更全面的概述,请参阅FiftyOne 用户指南。如果你已经熟悉这些语言,可以直接跳到下一部分。
-
数据集:
Dataset是 SQL 中的Table或 pandas 中的DataFrame的计算机视觉类比。它包含与一组媒体文件相关的所有信息。 -
样本:
Sample类似于数据表中的一行。样本是Dataset的基本元素。每个样本都有一个filepath,指向一个媒体文件,并存储与该数据片段相关的所有其他信息。 -
字段:
Field类似于数据表中的列,因为它定义了样本的属性(例如,图像宽度、高度和文件路径)。然而,字段是灵活的,它们可以包含其他字段(见下文的Label)。 -
标签:
Label是一个存储语义真实或预测信息的Field。例如,对象检测存储在Detections标签字段中,而分类存储在Classification标签字段中。
与 SQL 或 pandas 一样,你可以使用查询操作来过滤数据。例如,你可能想要查询数据表以获取:
所有在“A”列中条目大于 0.5 的行
然而,与数据表适合进行数字切片和过滤不同,非结构化计算机视觉数据更适合进行语义切片,例如:
在至少有 3 个非“狗”地面真实检测的图像中,检索高置信度的“大”边界框“狗”预测
对非结构化数据进行语义切片需要更大的灵活性。
为了实现这种灵活性,FiftyOne 定义了一组ViewStage方法,这些方法封装了允许的查询操作,如过滤、匹配、选择、排序和排除。这些方法大致类似于 SQL 中的SELECT、WHERE和ORDER BY,但由于查询的空间更大,方法数量也多得多,每个方法都有许多使用场景。有关详细讨论,请参见这个视图备忘单。
你可以通过将多个ViewStage操作按顺序组合来获取Dataset的任意子集,这个子集称为DatasetView。
下面是查询语言在实际操作中的样子:给定一个名为my_dataset的数据集,如果我们想要获取所有在包含“猫”的 10 张随机图片的"predictions"标签字段中的“狗”检测,我们可以使用以下查询:
view = my_dataset.match(
F("ground_truth.detections.label").contains(["cat"])
).filter_labels(
"predictions",
F("label") == "dog"
).take(10)
match()限制为在"ground_truth"检测字段中包含“猫”的图像,然后filter_labels()深入到"predictions"字段中,仅检索“狗”检测,最后take()随机选择一个子集。
定义任务
当我们开始这个旅程时,我们并不知道 LLMs 能做什么。早在 2022 年 12 月,我曾玩过 ChatGPT,并撰写了一篇文章计算机视觉中的隧道视野:ChatGPT 能看见吗?,探讨了聊天机器人对计算机视觉概念的理解。当时,一个通用语言模型能为基本的图像处理任务编写大部分正确的 Python 代码已经令人印象深刻,但在计算机视觉应用中使用 GPT 生成的代码(没有人工验证)的想法似乎不切实际。
在看到文本到 SQL 的示例后,并阅读了 GPT-4 reportedly 以优异的成绩通过 BAR 考试(以及其他考试)的消息后,我决定再次尝试 GPT-for-CV。我让 GPT-4 生成了一些简单查询的 Python 代码,对其回应感到惊喜。GPT-4 的即用结果的真实性还有待提高,这是可以理解的,因为我没有提供任何关于 FiftyOne 库的信息。但使其有效的前景令人兴奋。
需要统一性
当我们实验不同的提示技术时(我们稍后会讨论),如果我们想将其纳入更大的管道中,立刻就显现出我们需要明确任务。
如果任由其自行运作,GPT 模型的回应在格式上会因提示而异。
这里只是两个例子,我们将很快进行分析:
提示 1:
你的任务是将输入的自然语言查询转换为 Python 代码,以生成计算机视觉库 FiftyOne 的 ViewStages。
这是你的第一个自然语言查询:“仅包含狗的图像”
给我 FiftyOne 代码。
回应 1:
当然,这里是生成仅包含狗的图像的 FiftyOne 代码:
import fiftyone as fo
import fiftyone.zoo as foz
# Load the dataset
dataset = foz.load_zoo_dataset(
"open-images-v6",
split="validation",
label_types=["detections"],
classes=["Dog"],
)
# Create a view stage that filters out images without any dog detections
view_stage = fo.ViewStage("detections", fo.FilterField("detections", "label", "Dog"))
# Apply the view stage to the dataset
dataset = dataset.add_stage(view_stage)
# View the dataset
session = fo.launch_app(dataset)
这段代码加载了 Open Images v6 验证数据集,仅包含“检测”标签类型的狗。然后,它创建了一个视图阶段,过滤掉没有狗检测的图像。最后,将视图阶段应用到数据集中,并在 FiftyOne 应用中启动。
提示 2:
你的任务是将输入的自然语言查询转换为 Python 代码,以生成 FiftyOne 机器学*库的 ViewStages。
这是你的第一个自然语言查询:“最独特的假阳性检测图像”
给我 FiftyOne 代码。
回应 2:
当然,这里是使用 FiftyOne 库在 Python 中生成“最独特的假阳性检测图像”自然语言查询的 FiftyOne 代码:
import fiftyone as fo
import fiftyone.zoo as foz
# Load your dataset
dataset = fo.load_dataset("your_dataset_name")
# Define your false positive detection model
model = foz.load_zoo_model("your_model_name")
# Define your pipeline
pipeline = [
{"$set": {"predictions": {"$predict": model}}},
{"$match": {"predictions.mistakes.false_positive": True}},
{"$group": {"_id": "$filepath"}},
{"$count": "num_images"},
{"$sort": {"num_images": -1}},
]
# Run your pipeline and display the results in a view stage
view = dataset.aggregate(pipeline).limit(10).display()
在这段代码中,我们首先加载 FiftyOne 库和任何必要的依赖项。然后,我们使用 fo.load_dataset()加载你的数据集。
接下来,我们使用 foz.load_zoo_model()定义你的假阳性检测模型。该函数从 FiftyOne Zoo Model Zoo 加载一个预训练模型,其中包括广泛的计算机视觉最先进模型。
之后,我们使用\(set、\)match、\(group、\)count 和\(sort 聚合阶段定义我们的管道。管道首先使用\)set 为数据集中每个样本添加一个包含模型预测的字段。然后,我们使用$match 过滤样本,仅包括那些检测到假阳性的样本。
接下来,我们使用\(group 按文件路径对样本进行分组,并使用\)count 计算每个组中的图像数量。最后,我们使用$sort 按图像数量的降序对组进行排序。
最后,我们在你的数据集上运行管道,并使用 limit()将结果限制为检测到最多假阳性的前 10 张图像。然后,我们使用 display()在视图阶段展示结果。
尽管这两个示例的提示结构相同,但响应在几个关键方面有所不同。响应 1 尝试通过向数据集添加ViewStage来创建DatasetView。响应 2 定义并应用了一个MongoDB 聚合管道,随后使用limit()方法(应用Limit阶段)将视图限制为 10 个样本,并使用了一个不存在(即幻觉)display()方法。此外,尽管响应 1 加载了一个实际的数据集(Open Images V6),响应 2 实际上是模板代码,因为"your_dataset_name"和"your_model_name"需要填写。
这些示例还突出了以下问题:
-
模板代码:一些响应包含了导入模块、实例化数据集(和模型)以及可视化视图(
session = fo.launch_app(dataset))的代码。 -
解释性文本:在许多情况下——包括教育背景——模型解释其“推理”是一个积极的。如果我们希望代表用户执行查询,这些解释性文本就会成为障碍。一些查询甚至导致多个被文本分隔的代码块。
我们真正想要的是让 LLM 回应可以直接复制粘贴到 Python 进程中的代码,而不附带额外的负担。作为对模型进行提示的第一次尝试,我开始将以下文本作为前缀添加到任何自然语言查询中:
Your task is to convert input natural language queries into Python code to generate ViewStages for the computer vision library FiftyOne.
Here are some rules:
- Avoid all header code like importing packages, and all footer code like saving the dataset or launching the FiftyOne App.
- Just give me the final Python code, no intermediate code snippets or explanation.
- always assume the dataset is stored in the Python variable `dataset`
- you can use the following ViewStages to generate your response, in any combination: exclude, exclude_by, exclude_fields, exclude_frames, …
关键是,我定义了一个任务,并设置了规则,指导模型允许和不允许做的事情。
注意:随着响应格式的更加统一,我在这时从 ChatGPT 聊天界面转向使用 OpenAI 的 API 中的 GPT-4。
限制范围
我们的团队还决定,至少在开始时,我们将限制我们要求 LLM 执行的范围。尽管 fiftyone 查询语言本身功能全面,但要求预训练模型在没有任何微调的情况下执行任意复杂的任务是令人失望的。开始时要简单,然后逐步增加复杂性。
对于此实验,我们施加了以下限制:
-
仅限图像和视频:不要指望 LLM 查询 3D 点云或分组数据集。
-
忽略变化的
**ViewStages**:大多数ViewStages遵循相同的基本规则,但有一些例外。Concat是唯一一个接受第二个DatasetView的ViewStages;Mongo使用 MongoDB 聚合语法;GeoNear具有一个query参数,该参数接受一个fiftyone.utils.geojson.geo_within()对象;而GeoWithin需要一个 2D 数组来定义“within”适用的区域。我们决定忽略Concat、Mongo和GeoWithin,并支持所有GeoNear的使用 除了query参数。 -
坚持两个阶段:虽然模型能够组合任意数量的阶段是很好的,但在我见过的大多数工作流程中,一两个
ViewStages就足以创建所需的DatasetView。该项目的目标不是陷入细节,而是为计算机视觉从业者构建一些有用的东西。
提供上下文

VoxelGPT 使用自然语言查询图像数据集。图片由作者提供。
除了给模型一个明确的“任务”和提供清晰的指示外,我们发现通过提供有关 FiftyOne 查询语言如何工作的更多信息,可以提高性能。如果没有这些信息,LLM 就像在黑暗中盲目飞行,只是在抓取和伸手进入黑暗中。
例如,在提示 2 中,当我要求获取假阳性预测时,响应尝试用predictions.mistakes.false_positive来引用这些假阳性。就 ChatGPT 而言,这似乎是存储和访问假阳性信息的合理方法。
模型不知道在 FiftyOne 中,检测预测的真实性/虚假性是通过 dataset.evaluate_detections() 评估的,运行该评估后,你可以通过匹配 eval_fp>0 来检索所有具有假阳性的图像:
images_with_fp = dataset.match(F("eval_fp")>0)
我尝试通过提供额外的规则来澄清任务,例如:
- When a user asks for the most "unique" images, they are referring to the "uniqueness" field stored on samples.
- When a user asks for the most "wrong" or "mistaken" images, they are referring to the "mistakenness" field stored on samples.
- If a user doesn't specify a label field, e.g. "predictions" or "ground_truth" to which to apply certain operations, assume they mean "ground_truth" if a ground_truth field exists on the data.
我还提供了有关标签类型的信息:
- Object detection bounding boxes are in [top-left-x, top-left-y, width, height] format, all relative to the image width and height, in the range [0, 1]
- possible label types include Classification, Classifications, Detection, Detections, Segmentation, Keypoint, Regression, and Polylines
此外,虽然通过提供允许的视图阶段列表,我能够推动模型使用这些阶段,但它并不知道
-
当给定阶段相关时,或者
-
如何以语法正确的方式使用阶段
为了填补这一空白,我想给 LLM 提供有关每个视图阶段的信息。我编写了代码来遍历视图阶段(你可以使用 fiftyone.list_view_stages() 列出这些阶段),存储文档字符串,然后将文档字符串的文本拆分为描述和输入/参数。
然而,我很快遇到了一个问题:上下文长度。
使用 OpenAI API 的基础 GPT-4 模型时,我已经接*了 8,192 令牌的上下文长度。这是在添加示例或任何有关数据集的信息之前!
OpenAI 确实有一个具有 32,768 令牌上下文的 GPT-4 模型,理论上我可以使用,但一个粗略的计算让我相信这可能会很贵。如果我们填满了整个 32k 令牌上下文,根据OpenAI 的定价,每次查询大约需要 2 美元!
相反,我们的团队重新考虑了我们的方法,并进行了以下操作:
-
切换到 GPT-3.5
-
最小化令牌数
-
更加挑剔地选择输入信息
切换到 GPT-3.5
没有什么是免费的——这确实导致了稍微较低的性能,至少在初期。在项目过程中,我们通过提示工程恢复并远远超越了这一点!在我们的案例中,这个努力是值得的。在其他情况下,可能就不是这样了。
最小化令牌数
由于上下文长度成为限制因素,我使用了以下简单的技巧:使用 ChatGPT 优化提示!
一次处理一个ViewStage,我将原始描述和输入列表提供给 ChatGPT,并附上一个要求 LLM 最小化文本的令牌数的提示,同时保留所有语义信息。使用tiktoken来计算原始和压缩版本中的令牌数,我能够将令牌数减少约 30%。
更加挑剔
尽管提供上下文对模型很有帮助,但某些信息比其他信息更有用,具体取决于任务。如果模型仅需要生成涉及两个ViewStages的 Python 查询,它可能不会从其他ViewStages的输入信息中获得太大帮助。
我们知道需要一种方法来根据输入的自然语言查询选择相关信息。然而,这不会像在描述和输入参数上执行相似性搜索那样简单,因为前者通常以与后者非常不同的语言出现。我们需要一种将输入和信息选择联系起来的方法。
结果发现,那个链接是示例。
示例
生成示例
如果你曾经使用过 ChatGPT 或其他 LLM,你可能亲身体验过,即使只提供一个相关的示例,也能大幅提高性能。
作为起点,我想出了 10 个完全合成的示例,并将这些示例通过在任务规则和ViewStage描述下方添加以下内容传递给 GPT-3.5:
以下是 A、B 形式的输入-输出对的一些示例:
A) "Filepath starts with '/Users'"
B) `dataset.match(F("filepath").starts_with("/Users"))`
A) "Predictions with confidence > 0.95"
B) `dataset.filter_labels("predictions", F("confidence") > 0.95)`
…
仅凭这 10 个示例,模型响应的质量有了显著提高,因此我们的团队决定对此采取系统化的方式。
-
首先,我们仔细检查了我们的文档,找出了通过
ViewStages组合创建的所有视图示例。 -
然后,我们查看了
ViewStages的列表,并添加了示例,以便尽可能全面地覆盖用法语法。为此,我们确保每个参数或关键字至少有一个示例,以便为模型提供一个可遵循的模式。 -
在覆盖了用法语法之后,我们更改了示例中字段和类的名称,以防止模型对名称与阶段之间的关联产生错误的假设。例如,我们不希望模型仅仅因为所有
match_labels()的示例都包含“person”类,就强烈关联“person”类与match_labels()方法。
选择类似的示例
在这个示例生成过程结束时,我们已经有了数百个示例——远远超过了可以容纳的上下文长度。幸运的是,这些示例包含了(作为输入)自然语言查询,我们可以直接与用户的自然语言查询进行比较。
为了进行这种比较,我们使用 OpenAI 的text-embedding-ada–002模型预计算了这些示例查询的嵌入。在运行时,用户的查询会使用相同的模型进行嵌入,然后选择与自然语言查询最相似的示例——通过余弦距离来确定。最初,我们使用了ChromaDB来构建内存中的向量数据库。然而,由于我们处理的是数百或数千个向量,而不是数十万或数百万个向量,实际上转而使用精确向量搜索更为合理(此外,我们还减少了依赖)。
管理这些示例和提示的组件变得越来越困难,因此我们在这时开始使用LangChain 的 Prompts 模块。最初,我们能够使用他们的相似性示例选择器来选择最相关的示例,但最终我们不得不编写一个自定义的ExampleSelector,以便对预过滤有更多控制。
过滤适当的示例
在计算机视觉查询语言中,查询的适当语法可能取决于数据集中样本的媒体类型:例如,视频有时需要与图像不同地处理。为了避免通过给出看似矛盾的示例而使模型感到困惑,或通过迫使模型基于媒体类型推断而使任务复杂化,我们决定仅给出对给定数据集语法正确的示例。在向量搜索的上下文中,这被称为预过滤。
这个想法效果很好,以至于我们最终将相同的考虑应用于数据集的其他特性。在某些情况下,差异仅仅是语法上的——在查询标签时,访问Detections标签的语法与访问Classification标签的语法不同。其他过滤器则更具战略性:有时我们不希望模型了解查询语言的某些特性。
例如,我们不想给 LLM 提供使用它无法访问的计算的示例。如果没有为特定数据集构建文本相似性索引,则向模型提供查找与自然语言查询最佳视觉匹配的示例是没有意义的。类似地,如果数据集中没有任何评估运行,则查询真实正例和假阳性将产生错误或空结果。
你可以在 GitHub 仓库中的view_stage_example_selector.py中查看完整的示例预处理管道。
根据示例选择上下文信息
对于给定的自然语言查询,我们然后使用ExampleSelector选择的示例来决定在上下文中提供哪些额外信息。
特别是,我们统计了这些选择示例中每个ViewStage的出现次数,确定了出现频率最高的五个``ViewStages,并在我们的提示中添加了关于这些ViewStages`的描述和输入参数信息。这么做的理由是,如果一个阶段在类似的查询中频繁出现,它很可能(但不能保证)与该查询相关。
如果不相关,那么描述将帮助模型确定它不相关。如果相关,那么关于输入参数的信息将帮助模型生成语法正确的ViewStage操作。
分而治之

VoxelGPT 使用自然语言查询图像数据集。图片由作者提供。
到目前为止,我们专注于将尽可能多的相关信息——而且只是相关信息——挤进一个提示中。但这种方法已接*其极限。
即使不考虑每个数据集都有自己的字段和类名,可能的 Python 查询空间也太大了。
为了取得进展,我们需要将问题分解成更小的部分。借鉴*期的方法,包括链式思考提示和选择推理提示,我们将生成DatasetView的问题分成了四个不同的选择子问题。
-
算法
-
算法运行
-
相关字段
-
相关的类名
然后我们将这些选择“链接”串联起来,并将它们的输出传递给模型,在最终提示中用于DatasetView推理。
对于这些子任务,统一性和简洁性的原则适用。我们尽可能地回收现有示例中的自然语言查询,但明确简化了每个选择任务的所有输入和输出的格式。对一个链接来说最简单的东西可能对另一个链接并不是最简单的!
算法
在 FiftyOne 中,来自数据集计算的信息被存储为一个“运行”(“run”)。这包括诸如 uniqueness 这样的计算,它衡量每张图像相对于数据集中其他图像的独特性,以及 hardness,它量化模型在尝试学*此样本时会遇到的难度。它还包括 similarity 的计算,这涉及为与每个样本相关联的嵌入生成向量索引,甚至包括我们之前提到的 evaluation 计算。
每个这些计算生成不同类型的结果对象,每种结果对象都有自己的 API。此外,ViewStages 和这些计算之间没有一一对应的关系。以独特性(uniqueness)为例。
独特性计算结果存储在每张图像的一个浮点值字段中(默认值为 "uniqueness")。这意味着根据情况,你可能希望按独特性排序:
view = dataset.sort_by("uniqueness")
检索独特性高于某个阈值的样本:
from fiftyone import ViewField as F
view = dataset.match(F("uniqueness") > 0.8)
或者仅显示独特性字段:
view = dataset.select_fields("uniqueness")
在这个选择步骤中,我们任务 LLM 预测可能与用户自然语言查询相关的计算。这个任务的一个示例是:
Query: "most unique images with a false positive"
Algorithms used: ["uniqueness", "evaluation"]
算法运行
一旦识别出可能相关的计算算法,我们会任务 LLM 选择每个计算的最合适运行。这是必需的,因为某些计算可以在同一数据集上使用不同配置运行多次,而 ViewStage 可能仅在正确的“运行”下才有意义。
一个很好的例子是相似度运行。假设你在你的数据上测试两个模型(InceptionV3 和 CLIP),并且你已经为每个模型在数据集中生成了向量相似度索引。当使用 SortBySimilarity 视图阶段时,哪些图像被确定为与哪些其他图像最相似可能会强烈依赖于嵌入模型,因此以下两个查询可能会生成不同的结果:
## query A:
"show me the 10 most similar images to image 1 with CLIP"
## query B:
"show me the 10 most similar images to image 1 with InceptionV3"
这个运行选择过程对每种计算类型分别处理,因为每种计算需要修改后的任务规则和示例。
相关字段
链中的这个链接涉及识别所有与自然语言查询相关的字段名称,这些字段名称 不 与计算运行相关。例如,并非所有包含预测的 数据集 都将这些标签存储在名为 "predictions" 的字段下。根据个人、数据集和应用的不同,预测可能存储在名为 "pred"、"resnet"、"fine-tuned"、"predictions_05_16_2023" 或完全不同的名称的字段中。
此任务的示例包括查询、数据集中所有字段的名称和类型,以及相关字段的名称:
Query: "Exclude model2 predictions from all samples"
Available fields: "[id: string, filepath: string, tags: list, ground_truth: Detections, model1_predictions: Detections, model2_predictions: Detections, model3_predictions: Detections]"
Required fields: "[model2_predictions]"
相关的类名
对于标签字段如分类和检测,将自然语言查询转换为 Python 代码需要使用数据集中实际类的名称。为此,我让 GPT-3.5 执行命名实体识别以识别输入查询中的标签类。
在查询“样本中至少有一个牛预测且没有马”的情况下,模型的任务是识别"horse"和"cow"。这些识别出的名称随后与在之前步骤中选择的标签字段的类名进行比较——首先是区分大小写的,然后是不区分大小写的,最后是不区分复数的。
如果在数据集中未找到命名实体和类名之间的匹配,我们将退回到语义匹配:"people" → "person","table" → "dining table",和"animal" → [“cat”, “dog", “horse", …]。
每当匹配不完全相同时,我们使用匹配的类名来更新传递给最终推断步骤的查询:
query: "20 random images with a table"
## becomes:
query: "20 random images with a dining table"
ViewStage 推断
一旦所有这些选择完成,类似的示例、相关的描述和相关的数据集信息(选择的算法运行、字段和类)将与(可能修改过的)查询一起传递给模型。
与其指示模型以dataset.view1().view2()…viewn()的形式返回代码,我们最终去掉了dataset部分,而是要求模型将ViewStages作为列表返回。当时,我对这种改进性能感到惊讶,但回想起来,它与任务拆分越多,LLM 表现越好的见解相一致。
使其可用
创建一个 LLM 驱动的玩具很酷,但将相同的内核转变为 LLM 驱动的应用程序则更酷。以下是我们如何做到这一点的简要概述。
单元测试
当我们将其从一个原则证明转变为一个稳健的工程系统时,我们使用单元测试来压力测试管道并识别薄弱点。链中链接的模块化特性意味着每一步可以单独进行单元测试、验证和迭代,而无需运行整个链。
这导致了更快的改进,因为在提示工程团队中,不同的个人或小组可以并行处理链中的不同链接。此外,这还导致了成本减少,因为理论上,你只需要运行 LLM 推断的单个步骤来优化链中的单个链接。
评估 LLM 生成的代码
我们使用 Python 的eval()函数将 GPT-3.5 的响应转换为DatasetView。然后,我们设置 FiftyOne App 的session状态以显示该视图。
输入验证
垃圾输入 → 垃圾输出。为了避免这种情况,我们运行验证以确保用户的自然语言查询是合理的。
首先,我们使用 OpenAI 的审核端点。然后,我们将任何提示分类到以下四种情况之一:
1: 合理且完整:该提示可以合理地转换为用于查询数据集的 Python 代码。
所有带狗检测的图像
2: 合理但不完整:该提示合理,但在没有额外信息的情况下无法转换为 DatasetView。例如,如果我们有两个模型对数据进行预测,那么仅提到“我的模型”的提示是不足够的:
检索我的模型的错误预测
3: 超出范围:我们正在构建一个生成查询视图的应用程序,应用于计算机视觉数据集。虽然底层的 GPT-3.5 模型是通用 LLM,但我们的应用不应变成一个与数据集相邻的离线 ChatGPT 会话。如下提示应被拒绝:
像我五岁一样解释量子计算
4: 不合理:给定一个随机字符串,尝试生成数据集视图是不合适的——从哪里开始呢?!
Azlsakjdbiayervbg
在尝试将用户的输入查询转换为视图阶段序列之前,我们将输入传递到带有验证指令和示例的模型中。根据响应,我们要么提示用户提供更多信息或更合理的查询,要么继续进行数据集视图生成流程。
总结
VoxelGPT 实战!视频由作者提供。
一个简单的想法引发了一个疯狂的想法,这段旅程将那个疯狂的想法变成了现实。通过提示工程、一些真正的软件工程、大量的辛勤工作以及健康的黑魔法,我们的小团队创建了一个 LLM 驱动的应用程序,将自然语言查询转换为计算机视觉数据集的过滤视图。
要点很简单:定义任务、规定规则、限制范围、简化、选择一致性、分解问题,并保持相关性。
当我们将所有部分组合在一起时,它看起来像这样:
-
验证查询
-
查找类似的示例
-
检索相关文档
-
确定潜在的算法/计算
-
选择这些算法最可能的运行结果
-
确定在查询中访问/利用的字段(属性)
-
推断每个标签字段的类名称
-
生成查询的视图阶段列表
-
组成视图阶段并返回过滤后的数据集视图
-
胜利
VoxelGPT 远非完美。但这从未是目标。每一步都有改进的空间——更不用说更多的示例了!还可以将其作为构建 AutoGPT 风格的计算机视觉任务委托器的基础。
对 VoxelGPT 的任何贡献都欢迎。它是免费的开源软件!🙂
如何将公司的文档转变为可搜索的数据库,利用 OpenAI
以及你如何用同样的方法处理你的文档
·
关注 发表在 Towards Data Science ·15 分钟阅读·2023 年 4 月 25 日
--
图片来自 Unsplash。
在过去的六个月里,我一直在系列 A 初创公司 Voxel51 工作,并且是开源计算机视觉工具包 FiftyOne的创建者。作为一名机器学*工程师和开发者推广员,我的工作是倾听我们的开源社区,满足他们的需求——新的功能、集成、教程、研讨会,等等。
几周前,我们为 FiftyOne 添加了对向量搜索引擎和文本相似性查询的原生支持,以便用户可以通过简单的自然语言查询在他们(通常是庞大的——包含数百万或数千万样本的)数据集中找到最相关的图像。
这让我们处于一个奇特的位置:现在使用开源 FiftyOne 的人可以轻松地用自然语言查询搜索数据集,但使用我们的文档仍然需要传统的关键字搜索。
我们有大量文档,这有利有弊。作为用户,我有时发现,由于文档量庞大,找到我确切需要的信息比我希望的时间要长。
我不打算就此罢休……所以我在业余时间构建了这个:

从命令行语义化搜索公司文档。图片由作者提供。
所以,这是我如何将我们的文档转换为语义搜索的向量数据库:
-
将所有文档转换为统一格式
-
将文档拆分为块并添加了一些自动清理
-
为每个块计算了嵌入
-
从这些嵌入生成了一个向量索引
-
定义了索引查询
-
将所有内容封装在用户友好的命令行界面和 Python API 中
你可以在 voxel51/fiftyone-docs-search 仓库中找到本帖的所有代码,并且可以通过 pip install -e . 在本地编辑模式中轻松安装包。
更好的是,如果你想使用这种方法为你自己的网站实现语义搜索,你可以跟着做!以下是你需要的材料:
-
安装 openai Python 包并创建一个账户: 你将使用此账户将文档和查询发送到推断端点,该端点会返回每个文本片段的嵌入向量。
-
安装 qdrant-client Python 包并通过 Docker 启动 Qdrant 服务器:你将使用 Qdrant 为文档创建一个本地托管的向量索引,查询将对其进行运行。Qdrant 服务将在 Docker 容器中运行。
将文档转换为统一格式
我公司的文档都托管为 HTML 文档,地址是 docs.voxel51.com。一个自然的起点是使用 Python 的 requests 库下载这些文档,并用 Beautiful Soup 解析文档。
作为开发者(以及我们许多文档的作者),我认为自己可以做得更好。我已经在本地计算机上有一个 GitHub 仓库的工作副本,其中包含生成 HTML 文档所用的所有原始文件。我们的某些文档是用 Sphinx ReStructured Text (RST) 编写的,而其他文档,如教程,则从 Jupyter notebooks 转换为 HTML。
我(错误地)认为,离 RST 和 Jupyter 文件的原始文本越*,事情就会越简单。
RST
在 RST 文档中,部分由仅包含 =、- 或 _ 字符的行来划分。例如,这是 FiftyOne 用户指南中的一个文档,其中包含所有三种划分符:

开源 FiftyOne 文档中的 RST 文档。图片由作者提供。
然后,我可以移除所有的 RST 关键字,如 toctree、code-block 和 button_link(还有许多其他的),以及随关键字、区块开始或区块描述符出现的 :、:: 和 ..。
链接也很容易处理:
no_links_section = re.sub(r"<[^>]+>_?","", section)
当我想从 RST 文件中提取部分锚点时,情况开始变得棘手。我们许多部分明确指定了锚点,而其他部分则在转换为 HTML 时被推断出来。
这里有一个例子:
.. _brain-embeddings-visualization:
Visualizing embeddings
______________________
The FiftyOne Brain provides a powerful
:meth:`compute_visualization() <fiftyone.brain.compute_visualization>` method
that you can use to generate low-dimensional representations of the samples
and/or individual objects in your datasets.
These representations can be visualized natively in the App's
:ref:`Embeddings panel <app-embeddings-panel>`, where you can interactively
select points of interest and view the corresponding samples/labels of interest
in the :ref:`Samples panel <app-samples-panel>`, and vice versa.
.. image:: /images/brain/brain-mnist.png
:alt: mnist
:align: center
There are two primary components to an embedding visualization: the method used
to generate the embeddings, and the dimensionality reduction method used to
compute a low-dimensional representation of the embeddings.
Embedding methods
-----------------
The `embeddings` and `model` parameters of
:meth:`compute_visualization() <fiftyone.brain.compute_visualization>`
support a variety of ways to generate embeddings for your data:
在我们用户指南文档中的 brain.rst 文件(上面展示的部分),Visualizing embeddings 部分有一个由 .. _brain-embeddings-visualization: 指定的锚点 #brain-embeddings-visualization。然而,紧随其后的 Embedding methods 子部分有一个自动生成的锚点。
另一个很快出现的困难是如何处理 RST 中的表格。List tables 相对简单。例如,这是我们 View Stages 备忘单中的一个列表表格:
.. list-table::
* - :meth:`match() <fiftyone.core.collections.SampleCollection.match>`
* - :meth:`match_frames() <fiftyone.core.collections.SampleCollection.match_frames>`
* - :meth:`match_labels() <fiftyone.core.collections.SampleCollection.match_labels>`
* - :meth:`match_tags() <fiftyone.core.collections.SampleCollection.match_tags>`
Grid tables 另一方面,很快就会变得凌乱。它们给文档作者提供了很大的灵活性,但这种灵活性使得解析起来非常麻烦。看看我们 Filtering 备忘单中的这张表格:
+-----------------------------------------+-----------------------------------------------------------------------+
| Operation | Command |
+=========================================+=======================================================================+
| Filepath starts with "/Users" | .. code-block:: |
| | |
| | ds.match(F("filepath").starts_with("/Users")) |
+-----------------------------------------+-----------------------------------------------------------------------+
| Filepath ends with "10.jpg" or "10.png" | .. code-block:: |
| | |
| | ds.match(F("filepath").ends_with(("10.jpg", "10.png")) |
+-----------------------------------------+-----------------------------------------------------------------------+
| Label contains string "be" | .. code-block:: |
| | |
| | ds.filter_labels( |
| | "predictions", |
| | F("label").contains_str("be"), |
| | ) |
+-----------------------------------------+-----------------------------------------------------------------------+
| Filepath contains "088" and is JPEG | .. code-block:: |
| | |
| | ds.match(F("filepath").re_match("088*.jpg")) |
+-----------------------------------------+-----------------------------------------------------------------------+
在表格中,行可以占用任意数量的行,列的宽度也可以变化。网格表格单元格中的代码块也很难解析,因为它们占用多行空间,因此它们的内容与其他列的内容交错在一起。这意味着这些表格中的代码块需要在解析过程中有效地重建。
这不是世界末日,但也不是理想状态。
Jupyter
Jupyter notebooks 解析起来相对简单。我能够将 Jupyter notebook 的内容读取为字符串列表,每个单元格一个字符串:
import json
ifile = "my_notebook.ipynb"
with open(ifile, "r") as f:
contents = f.read()
contents = json.loads(contents)["cells"]
contents = [(" ".join(c["source"]), c['cell_type'] for c in contents]
此外,部分由以 # 开头的 Markdown 单元格划分。
尽管面临 RST 带来的挑战,我决定转向 HTML 并平等对待我们的所有文档。
HTML
我通过bash generate_docs.bash从本地安装生成了 HTML 文档,并开始用 Beautiful Soup 解析它们。然而,我很快意识到,当 RST 代码块和包含内联代码的表格被转换为 HTML 时,虽然渲染正确,但 HTML 本身却非常难以处理。以我们的过滤器备忘单为例。
当在浏览器中渲染时,我们过滤器备忘单中日期和时间部分之前的代码块如下所示:

开源 FiftyOne 文档中的备忘单截图。图片由作者提供。
然而,原始 HTML 看起来是这样的:

RST 备忘单转换为 HTML。图片由作者提供。
这不是不可能解析,但也远非理想。
Markdown
幸运的是,我通过使用 markdownify 将所有 HTML 文件转换为 Markdown 解决了这些问题。Markdown 有几个关键优点,使其成为这个工作的最佳选择。
- 比 HTML 更干净:代码格式从一串杂乱的
span元素简化为用单个`标记的内联代码片段,代码块则用三重引号标记py````py`before and after. This also made it easy to split into text and code. - Still contained anchors: unlike raw RST, this Markdown included section heading anchors, as the implicit anchors had already been generated. This way, I could link not just to the page containing the result, but to the specific section or subsection of that page.
- Standardization: Markdown provided a mostly uniform formatting for the initial RST and Jupyter documents, allowing us to give their content consistent treatment in the vector search application.
Note on LangChain
Some of you may know about the open source library LangChain for building applications with LLMs, and may be wondering why I didn’t just use LangChain’s Document Loaders and Text Splitters. The answer: I needed more control!
Processing the documents
Once the documents had been converted to Markdown, I proceeded to clean the contents and split them into smaller segments.
Cleaning
Cleaning most consisting in removing unnecessary elements, including:
- Headers and footers
- Table row and column scaffolding — e.g. the
|’s in|select()| select_by()| - Extra newlines
- Links
- Images
- Unicode characters
- Bolding — i.e.
**text**→text
I also removed escape characters that were escaping from characters which have special meaning in our docs: _ and *. The former is used in many method names, and the latter, as usual, is used in multiplication, regex patterns, and many other places:
document = document.replace("\_", "_").replace("\*", "*")
```py
## Splitting documents into semantic blocks
With the contents of our docs cleaned, I proceeded to split the docs into bite-sized blocks.
First, I split each document into sections. At first glance, it seems like this can be done by finding any line that starts with a `#` character. In my application, I did not differentiate between h1, h2, h3, and so on (`#` , `##` , `###`), so checking the first character is sufficient. However, this logic gets us in trouble when we realize that `#` is also employed to allow comments in Python code.
To bypass this problem, I split the document into text blocks and code blocks:
text_and_code = page_md.split('```py')
text = text_and_code[::2]
code = text_and_code[1::2]
然后,我用 `#` 标记文本块中的新节的开始。我从这一行中提取了节标题和锚点:
```py
def extract_title_and_anchor(header):
header = " ".join(header.split(" ")[1:])
title = header.split("[")[0]
anchor = header.split("(")[1].split(" ")[0]
return title, anchor
然后将每个文本或代码块分配到适当的部分。
起初,我还尝试将文本块拆分成段落,假设因为一个部分可能包含许多不同主题的信息,该部分的嵌入可能与只涉及其中一个主题的文本提示的嵌入不相似。然而,这种方法导致大多数搜索查询的最佳匹配过于集中在单行段落上,结果显示这些搜索结果并不十分有用。
查看附带的 GitHub 仓库 ,了解这些方法的实现,并尝试在您自己的文档中应用!
使用 OpenAI 嵌入文本和代码块
将文档转换、处理并拆分为字符串后,我为这些块生成了嵌入向量。由于大型语言模型本质上灵活且通常能力强,我决定将文本块和代码块视为相同的文本,并用相同的模型进行嵌入。
我使用了 OpenAI 的 text-embedding-ada-002 模型,因为它易于使用,且在所有 OpenAI 的嵌入模型中(在 BEIR 基准上)表现最佳,并且也是最便宜的。实际上,它的费用非常低($0.0004/1K 标记),以至于为 FiftyOne 文档生成所有嵌入仅花费了几美分!正如 OpenAI 自己所说,“我们建议几乎所有用例都使用 text-embedding-ada-002。它更好、更便宜、更简单。”
使用这个嵌入模型,你可以生成一个 1536 维的向量,表示任何输入提示,最多 8,191 个标记(大约 30,000 个字符)。
要开始使用,你需要创建一个 OpenAI 账户,在 platform.openai.com/account/api-keys 生成 API 密钥,使用以下命令将该 API 密钥导出为环境变量:
export OPENAI_API_KEY="<MY_API_KEY>"
你还需要安装 openai Python 库:
pip install openai
我编写了一个 OpenAI API 的包装器,该包装器接受文本提示并返回嵌入向量:
MODEL = "text-embedding-ada-002"
def embed_text(text):
response = openai.Embedding.create(
input=text,
model=MODEL
)
embeddings = response['data'][0]['embedding']
return embeddings
为了生成所有文档的嵌入,我们只需对所有文档中的每个子部分——文本和代码块——应用此函数。
创建 Qdrant 向量索引
拥有嵌入后,我创建了一个向量索引用于搜索。我选择使用 Qdrant 的原因与我们选择在 FiftyOne 中添加本地 Qdrant 支持的原因相同:它是开源的、免费的,并且易于使用。
要开始使用 Qdrant,你可以拉取一个预构建的 Docker 镜像并运行容器:
docker pull qdrant/qdrant
docker run -d -p 6333:6333 qdrant/qdrant
此外,你还需要安装 Qdrant Python 客户端:
pip install qdrant-client
我创建了 Qdrant 集合:
import qdrant_client as qc
import qdrant_client.http.models as qmodels
client = qc.QdrantClient(url="localhost")
METRIC = qmodels.Distance.DOT
DIMENSION = 1536
COLLECTION_NAME = "fiftyone_docs"
def create_index():
client.recreate_collection(
collection_name=COLLECTION_NAME,
vectors_config = qmodels.VectorParams(
size=DIMENSION,
distance=METRIC,
)
)
然后我为每个子部分(文本或代码块)创建了一个向量:
import uuid
def create_subsection_vector(
subsection_content,
section_anchor,
page_url,
doc_type
):
vector = embed_text(subsection_content)
id = str(uuid.uuid1().int)[:32]
payload = {
"text": subsection_content,
"url": page_url,
"section_anchor": section_anchor,
"doc_type": doc_type,
"block_type": block_type
}
return id, vector, payload
对于每个向量,你可以提供额外的上下文作为 payload 的一部分。在这种情况下,我包括了结果可以找到的 URL(及锚点)、文档的 类型,以便用户可以指定是否希望搜索所有文档,或只是某些类型的文档,以及生成嵌入向量的字符串内容。我还添加了块类型(文本或代码),以便如果用户正在寻找代码片段,他们可以将搜索定制为此目的。
然后我将这些向量一个页面一个页面地添加到索引中:
def add_doc_to_index(subsections, page_url, doc_type, block_type):
ids = []
vectors = []
payloads = []
for section_anchor, section_content in subsections.items():
for subsection in section_content:
id, vector, payload = create_subsection_vector(
subsection,
section_anchor,
page_url,
doc_type,
block_type
)
ids.append(id)
vectors.append(vector)
payloads.append(payload)
## Add vectors to collection
client.upsert(
collection_name=COLLECTION_NAME,
points=qmodels.Batch(
ids = ids,
vectors=vectors,
payloads=payloads
),
)
查询索引
一旦创建了索引,可以通过使用相同的嵌入模型对查询文本进行嵌入,并在索引中搜索相似的嵌入向量来执行对索引文档的搜索。使用 Qdrant 向量索引,可以通过 Qdrant 客户端的 search() 命令执行基本查询。
为了使公司的文档可搜索,我希望允许用户按文档的部分以及编码的块类型进行筛选。在向量搜索术语中,筛选结果同时确保返回预定数量的结果(由top_k参数指定)被称为预筛选。
为了实现这一点,我编写了一个程序化过滤器:
def _generate_query_filter(query, doc_types, block_types):
"""Generates a filter for the query.
Args:
query: A string containing the query.
doc_types: A list of document types to search.
block_types: A list of block types to search.
Returns:
A filter for the query.
"""
doc_types = _parse_doc_types(doc_types)
block_types = _parse_block_types(block_types)
_filter = models.Filter(
must=[
models.Filter(
should= [
models.FieldCondition(
key="doc_type",
match=models.MatchValue(value=dt),
)
for dt in doc_types
],
),
models.Filter(
should= [
models.FieldCondition(
key="block_type",
match=models.MatchValue(value=bt),
)
for bt in block_types
]
)
]
)
return _filter
内部的_parse_doc_types()和_parse_block_types()函数处理参数为字符串、列表或 None 的情况。
然后我写了一个query_index()函数,它接受用户的文本查询,进行预筛选,搜索索引,并从负载中提取相关信息。该函数返回一个元组列表,形式为(url, contents, score),其中得分表示结果与查询文本的匹配程度。
def query_index(query, top_k=10, doc_types=None, block_types=None):
vector = embed_text(query)
_filter = _generate_query_filter(query, doc_types, block_types)
results = CLIENT.search(
collection_name=COLLECTION_NAME,
query_vector=vector,
query_filter=_filter,
limit=top_k,
with_payload=True,
search_params=_search_params,
)
results = [
(
f"{res.payload['url']}#{res.payload['section_anchor']}",
res.payload["text"],
res.score,
)
for res in results
]
return results
编写搜索包装器
最后一步是为用户提供一个干净的界面,以便在这些“向量化”文档中进行语义搜索。
我编写了一个print_results()函数,它接受查询、query_index()的结果和一个score参数(是否打印相似度分数),并以易于理解的方式打印结果。我使用了rich Python 包来格式化终端中的超链接,以便在支持超链接的终端中,点击超链接将打开默认浏览器中的页面。如果需要,我还使用了webbrowser来自动打开最顶端结果的链接。

显示带有丰富超链接的搜索结果。图片由作者提供。
对于基于 Python 的搜索,我创建了一个FiftyOneDocsSearch类来封装文档搜索行为,因此一旦实例化了FiftyOneDocsSearch对象(可能使用默认的搜索参数设置):
from fiftyone.docs_search import FiftyOneDocsSearch
fosearch = FiftyOneDocsSearch(open_url=False, top_k=3, score=True)
你可以通过调用这个对象在 Python 中进行搜索。例如,要查询“如何加载数据集”,只需运行:
fosearch(“How to load a dataset”)

在 Python 进程中对公司的文档进行语义搜索。图片由作者提供。
我还使用了argparse来通过命令行提供此文档搜索功能。当包被安装后,可以通过 CLI 搜索文档:
fiftyone-docs-search query "<my-query>" <args
仅为有趣,因为fiftyone-docs-search query有点繁琐,我在我的.zsrch文件中添加了一个别名:
alias fosearch='fiftyone-docs-search query'
使用这个别名,可以通过命令行搜索文档:
fosearch "<my-query>" args
结论
在开始之前,我已经把自己看作是公司开源 Python 库 FiftyOne 的重度用户。我编写了许多文档,并且每天都在使用(并继续使用)这个库。但将我们的文档转变为可搜索的数据库的过程迫使我以更深刻的方式理解我们的文档。当你为他人构建某些东西时,它也能帮助到你,这总是很棒的!
这是我学到的:
-
Sphinx RST 使用繁琐:它能生成美观的文档,但解析起来有点麻烦
-
不要在预处理上过度:OpenAI 的 text-embeddings-ada-002 模型非常擅长理解文本字符串背后的意义,即使它有稍微不典型的格式。再也不用进行词干提取和费力地去除停用词及各种字符了。
-
小的语义有意义的片段最好:将文档分解成尽可能小的有意义的部分,并保留上下文。对于较长的文本,搜索查询与索引中文本部分的重叠更可能被段落中的不相关文本遮掩。如果你将文档拆分得过小,许多索引条目可能包含很少的语义信息。
-
向量搜索非常强大:只需最少的工作量,不需任何微调,我就能显著提高文档的可搜索性。从初步估计来看,这种改进的文档搜索比旧的关键字搜索方法返回相关结果的可能性提高了两倍以上。此外,这种向量搜索方法的语义特性意味着用户现在可以用任意措辞、任意复杂的查询进行搜索,并且保证得到指定数量的结果。
如果你发现自己(或他人)不断地挖掘或筛选大量文档以寻找特定信息,我鼓励你将这一过程调整到你的用例中。你可以修改它以适用于你的个人文档或公司的档案。如果你这样做,我保证你会从中得到新的启发!
这里有一些方法可以扩展到你自己的文档中!
-
混合搜索: 将向量搜索与传统的关键字搜索结合起来
-
全球化:使用Qdrant Cloud在云中存储和查询集合
-
引入网络数据:使用requests直接从网络下载 HTML
-
自动更新:使用Github Actions在底层文档更改时触发嵌入的重新计算
-
嵌入:将其包装在一个 Javascript 元素中,替代传统的搜索框
构建包所用的所有代码都是开源的,可以在voxel51/fiftyone-docs-search仓库中找到。
我如何通过第一个 #30DayChartChallenge 学* Observable Plot
·
关注 发表在 数据科学前沿 ·6 分钟阅读·2023 年 10 月 13 日
--
图片由 Isaac Smith 提供,出处 Unsplash。
如果你在数据领域工作,你可能熟悉那种有一长串工具列表等待学*的感觉,希望有一天能实现。对我而言,我的列表上有很长时间的一项是Observable Plot,这是一个用于探索性数据可视化的 JavaScript 库。终于在今年,我决定利用#30DayChartChallenge来深入了解这个令人惊叹的库。作为第一次参与这个挑战的人,我想分享我的经验和所获得的见解,并为那些考虑参与类似即将到来的挑战的人提供建议。
什么是 #30DayChartChallenge
30DayChartChallenge 是一个由社区驱动的挑战,每年四月举行。每天有一个主题提示,为当天的可视化提供灵感,数据可视化创作者可以根据这个主题进行解释。对数据来源或工具的使用没有限制。对于数据爱好者、设计师和有志的数据可视化艺术家来说,这是一个迷人的活动。
为什么我参与了 #30DayChartChallenge。
我决定参加挑战是由于几个因素驱动的:
-
探索 Observable Plot 库: 这是我的主要目标。我想通过深入了解所有 JS 基础的Observable Plot库来扩展我的技能并探索新的交互式数据可视化工具。你可以在我的#30DayChartChallenge collection中找到所有我的可视化的代码和数据来源,包括这里提到的那些。
-
提升数据可视化技能: 我迫切希望提高将原始数据转化为视觉上吸引人且信息丰富的图表和图形的能力。
-
激发创造力: 这个挑战提供了一个独特的机会,可以尝试各种数据可视化技术和创意,推动我跳出固有思维。它提供了一种有趣的方式来发挥我的创造力。
-
建立联系: #30DayChartChallenge 社区由来自不同背景的数据爱好者组成,为网络交流和分享创意提供了一个很好的平台。
从我第一次 #30DayChartChallenge 学*新工具的经验中获得的建议
1. 规划 — 你值得信赖的盟友:
由于提示在月份开始前很久就已提供,所以可以提前分配时间进行头脑风暴,寻找数据来源,并草拟粗略设计。挑战开始前的规划允许你专注于可视化过程和学*,而不会每天感到不堪重负。我浏览过的一些最喜欢的数据来源慷慨地来自Our World in Data和Kaggle。
这是我使用 US tornadoes dataset 从 Kaggle 创建的可视化,用于第 7 天的“危险”提示。

第 7 天:危险,来源:#30DayChartChallenge collection
2. 数据重用的艺术:
每天寻找新的数据来激励你,持续 30 天会很快让人感到疲惫。重复使用数据,尝试用新的方式讲述相同的故事。
以下第一个图是第 6 天的“数据日:OWID”提示。我使用了累积土地使用变化数据,涵盖了六个国家一个世纪的时间。在第 28 天的提示是“趋势”,我认为这些数据也很适合。因此,我没有讲述一个新的故事,而是专注于通过动画讲述故事,这样我既可以在使用相同数据的同时学*新东西,又能节省寻找数据的时间。

第 6 天:数据日 — OWID,来源:#30DayChartChallenge collection

第 28 天:趋势,来源:#30DayChartChallenge collection
3. 目标的清晰度。
确定你参加挑战的目标。对我来说,学* Observable Plot 库是主要的重点。这种清晰度帮助我在整个过程中保持专注和积极。有时这意味着我不得不接受一个花费时间较长的“足够好”的图,即使有其他我已经熟悉的工具如 ggplot2,我可以用它们在更短的时间内制作更好的图。
这是我为第 5 天的“斜率”提示创建的图,使用了动物园动物寿命数据。这个简单的折线图和点图花了我太多时间去弄清楚,因为我在学*一个新工具,但完成它时感觉很值得。

第 5 天:斜率,来源:#30DayChartChallenge collection
4. 追求成长,而非完美!
这很困难,因为我们都希望每天都能发布出最好的可视化。拥抱学*过程,并接受不是每个可视化都会完美,这是避免在挑战中感到疲惫的关键。专注于提高你的技能和对数据可视化概念的理解。
这是第 13 天的一个示例,提示是“流行文化”。在一天长时间的工作之后,当我坐下来进行挑战时,我想学*创建一个径向图,以查看超级碗广告类别分布如何随着时间变化。我曾设想为所有类别创建它,我从最感兴趣的 4 个类别开始。虽然这不是最终计划,也很难停在这里,但我决定在 4 个类别时结束,以免疲惫并为第二天的可视化保留精力。

第 13 天:流行文化,来源:#30DayChartChallenge collection
5. 庆祝这段旅程!
庆祝你的成就,无论多小。认可你的进步和成长,最重要的是不要陷入与他人比较的陷阱,而要以此为灵感。记住,这个挑战是关于个人成长和学*的。我发布的前几个图表远不如我看到的其他人的精彩,但在最初的几天里,提醒自己在变得更好之前会很糟糕,这是很重要的。
不必完成所有三十天,参与其中可能会很累,但也很有影响力,所以只要你能参与就足够了,每次出现都应该庆祝。
这是我在最后一周为了好玩制作的一个图表,使用了大脚怪数据,因为谁不喜欢大脚怪呢,而在最后一周,我已经逼自己很辛苦了,需要一些乐趣来继续前进!

来源:#30DayChartChallenge collection
向前迈进,持续进步!
参加#30DayChartChallenge 对我来说是一次变革性的经历。我提高了数据可视化技能,掌握了一个新库,并与许多了不起的创作者进行了交流和学*。现在,我的代码库以及所有数据源充当了个人的 Observable Plot 备忘单,我经常参考。最*,我在与两位非常有才华的数据可视化艺术家 Tanya Shapiro 和 Allison Horst 的小组讨论中分享了我通过#30DayChartChallenge 学* Observable Plot 的经历。
我希望这篇文章鼓励你参与你的第一次#30DayChartChallenge,也许还可以用它来学*一个新工具。今年 11 月还会有#30DayMapChallenge,届时的每日提示已经在这里提供了。所以,现在是时候开始为假期季节做计划了。
你可以在我的Observable collection中找到重现本文所有可视化的代码和数据。
如果你愿意,可以在Linkedin上找到我。
如何赢得新加坡的 GPT-4 提示工程竞赛
原文:
towardsdatascience.com/how-i-won-singapores-gpt-4-prompt-engineering-competition-34c195a93d41
深入探讨我学*到的利用大型语言模型(LLMs)力量的策略
·发表于 Towards Data Science ·阅读时长 23 分钟·2023 年 12 月 29 日
--

庆祝一个里程碑——真正的胜利是无价的学*体验!
上个月,我荣幸地赢得了新加坡首届 GPT-4 提示工程竞赛,该竞赛汇聚了超过 400 位极具才华的参与者,由新加坡政府技术局(GovTech)组织。
提示工程是一门结合了艺术和科学的学科——它既需要技术理解,也需要创造力和战略思维。这是我在过程中学*到的提示工程策略的汇编,这些策略能推动任何 LLM 做你需要的事,甚至更多!
作者注释: 在撰写这篇文章时,我力求避免传统提示工程技术的过度讨论和文献记录。我的目标是通过实验带来新见解,并从个人角度理解和处理某些技术。希望你喜欢阅读这篇文章!
本文涵盖了以下内容, 🔵 指代初学者友好的提示技术,而 🔴 指代高级策略:
1. [🔵] 使用 CO-STAR 框架构建提示
2. [🔵] 使用分隔符对提示进行分段
3. [🔴] 创建带有 LLM 保护措施的系统提示
4. [🔴] 仅使用 LLMs 分析数据集,无需插件或代码 **—
通过实际例子分析真实的 Kaggle 数据集,使用 GPT-4**
1. [🔵] 使用 CO-STAR 框架构建提示
有效的提示结构对于从 LLM 获得最佳响应至关重要。CO-STAR 框架,由新加坡 GovTech 的数据科学与人工智能团队提出,是一个便于结构化提示的实用模板。它考虑了所有影响 LLM 响应效果和相关性的关键方面,从而产生更优的响应。

CO-STAR 框架 — 作者提供的图像
这就是它的工作原理:
(C) 上下文:提供任务的背景信息
这帮助 LLM 理解正在讨论的具体场景,确保其响应相关。
(O) 目标:定义你希望 LLM 执行的任务
明确你的目标有助于 LLM 将响应集中在满足那个特定目标上。
(S) 风格:指定你希望 LLM 使用的写作风格
这可能是某个著名人士的写作风格,或是某个职业领域的专家,比如业务分析专家或首席执行官。这g指导 LLM 以与你的需求一致的方式和措辞作出回应。
(T) 语气:设定响应的态度
这确保了 LLM 的响应能够与所需的情感或情绪背景相契合。示例包括正式、幽默、富有同情心等。
(A) 受众:确定响应的目标人群
针对特定受众调整 LLM 的响应,例如领域专家、初学者、儿童等,可以确保其在所需的上下文中适当且易于理解。
(R) 响应:提供响应格式
这确保了 LLM 的输出格式完全符合你对后续任务的要求。举例来说,包括列表、JSON、专业报告等。对于大多数在 LLM 响应上进行程序化操作以便后续处理的 LLM 应用来说,JSON 输出格式是理想的。
CO-STAR 的实际应用
这是一个 CO-STAR 框架有用的真实世界示例。假设你是一名社交媒体经理,需要帮助撰写一个 Facebook 帖子来宣传公司新产品。
一个没有 CO-STAR 的简单提示可能看起来像这样:
撰写一个 Facebook 帖子来宣传我公司的新产品。我的公司名为 Alpha,产品叫做 Beta,是一种全新的超快速吹风机。
这是来自 GPT-4 的输出:
介绍 Alpha 的革命性 Beta 吹风机!
告别漫长乏味的吹干头发时光!Alpha 自豪地推出 Beta,我们最新的头发护理技术创新。
超快速干发:Beta 设计用来将你的吹干时间缩短一半,得益于其先进而强大的马达。更快准备好,享受更多做你喜欢的事情的时间!
立即购买:[产品页面链接]
尽管足够,但这个输出过于通用,缺乏与公司受众产生共鸣所需的具体细微差别和针对性吸引力。
这是一个使用 CO-STAR 模板的示例,它提醒您在提示中整合任务的其他方面,特别是前一个简洁提示中缺失的STYLE、TONE和AUDIENCE方面:
CONTEXT
我想宣传我公司的新产品。我们公司的名字是 Alpha,产品叫 Beta,是一种全新的超快速吹风机。
OBJECTIVE
为我创建一个 Facebook 帖子,旨在促使人们点击产品链接购买。
STYLE
参考那些成功广告类似产品的公司,例如 Dyson,来模仿写作风格。
TONE
说服力强的
AUDIENCE
我公司在 Facebook 上的受众群体通常是老年人。请根据这个受众的需求调整您的帖子,关注他们通常关注的护发产品特点。
RESPONSE
简洁且有影响力的 Facebook 帖子。
通过应用 CO-STAR,GPT-4 的响应变得更具针对性和有效性:
以 Alpha Beta 为您的选择——智者一代的超快速吹风机。
重新发现简单有效的护发乐趣,尽在 Alpha Beta。我们的最新创新不仅仅是一款吹风机;它承诺迅速、温和地护理您的头发。
可靠的发型护理,适合那些欣赏生活精致事物的人。
- 易于使用且直观:告别复杂的设备。Alpha Beta 是
为舒适和简约而打造,完美适合那些重视简洁明了的人。
无忧技术。
- 节省时间的技术:我们理解您的时间非常宝贵。这就是为什么 Alpha Beta 显著缩短了干发时间,让您有更多时间享受生活的乐趣。
用 Alpha Beta 让每一天都成为美发好日子。体验
今天就拥抱优雅与简约。
[您的产品链接]
CO-STAR 框架引导您以结构化的方式提供有关任务的所有关键信息给 LLM,确保对您的需求进行量身定制和优化的响应。
2. [🔵] 使用分隔符分段提示

由 DALL·E 3 生成的图像
分隔符是特殊标记,帮助 LLM 区分提示中应该作为一个单元处理的部分。这一点很重要,因为您的整个提示以一个长序列的标记到达 LLM。分隔符通过将提示的特定部分围起来,提供结构,让这些部分被单独处理。
值得注意的是,分隔符对于简单任务的 LLM 响应质量可能没有影响。然而,任务越复杂,分隔符在分段上的使用对 LLM 响应的影响就越大。
分隔符作为特殊字符
分隔符可以是任何通常不会一起出现的特殊字符序列,例如:
-
===
就这样,各位。命令从 3.12-rc-bookworm 镜像启动了一个容器,添加了 `-it` 标签后启动了 Python 解释器。现在,你可以在交互式 Shell 中做任何事情。
了解更多信息,请查看官方文档中的[3.12 中的新功能](https://docs.python.org/3.12/whatsnew/3.12.html)文章。我会首先尝试那些改进的错误信息。
> 要退出 shell 和容器,运行 `exit()`。
## 第 4 步:将 VSCode 连接到容器
你难道不觉得我们会在这里运行一堆在傻乎乎的 shell 中的表达式吗?
哦不。我们要将 Python 3.12 的容器与 VSCode 链接,并编写一些脚本以真正测试新版本。
因此,在你机器上的任何目录中,打开 VSCode(我希望你已经[为 Python 和数据科学配置了它](https://code.visualstudio.com/docs/datascience/data-science-tutorial))。
```py
$ cd some_dir
$ code . # Launches VSCode
然后,安装 Remote Development 扩展:

图片由我提供
重新加载 VSCode。然后,跳转到你的 Docker Desktop,点击 Containers 菜单:

图片由我提供
你会看到一个运行中的容器列表。我版本的 Python 3.12 被称为 adoring_dirac。现在,它没有运行,因为我在 CLI 中退出了 Python shell。要运行它,我按下播放按钮来启动容器。
现在,再次打开 VSCode,打开命令面板(Ctrl + Shift + P),搜索“附加到运行中的容器”。这里有一个有用的 GIF:

一旦你点击一个以 Python 3.12 为基础镜像的正在运行的容器实例,一个新的 VSCode 窗口将弹出,并直接链接到容器。
请记住,这个容器与操作系统隔离,因此你创建或编辑的任何文件或启动的终端会话也会被隔离,且不可见。
所以,这就是你尝试在容器内测试 Python 3.12 的绿灯。除了创建脚本,

你还可以安装其他软件,如 Conda、Git、DVC,基本上将容器视为一个只安装了 Python 3.12 的全新空计算机。
结论
你还可以将上述方法应用于安装未来的 Python 版本到其他工具或库中。例如,流行的框架如 TensorFlow 或 PyTorch 在 Docker Hub 上有它们的官方 Docker 镜像。
通过利用这些官方镜像,你可以轻松设置支持 GPU 的框架,消除任何复杂性或挑战。Docker 容器已预配置,确保了无忧的安装体验。
感谢阅读!
喜欢这篇文章和它奇怪的写作风格吗?想象一下,访问到更多类似的文章,所有文章都由一位才华横溢、迷人且风趣的作者(顺便说一句,就是我 😃)。
只需 4.99 美元的会员费,你不仅能访问我的故事,还能从 Medium 上最优秀的头脑那里获得丰富的知识宝藏。如果你使用 我的推荐链接,你将获得我超级感激的心意和一个虚拟的击掌,以支持我的工作。
[## 使用我的推荐链接加入 Medium — Bex T。
作为 Medium 会员,你的一部分会员费会分配给你阅读的作者,你可以完全访问所有故事……
ibexorigin.medium.com](https://ibexorigin.medium.com/membership?source=post_page-----eb05953ba599--------------------------------)
如何在数据科学训练营中表现出色:完整指南
关于如何准备训练营、成功完成课程以及之后的行动的完整指南
·
关注 发表在 Towards Data Science ·13 分钟阅读·2023 年 9 月 2 日
--
一张神经网络如何看待数据科学训练营中的女孩的图像(由作者使用 Kandinsky 2.2 生成)
我在 2021 年 5 月从线下训练营毕业。现在我在 IT 领域工作了将* 2 年。此外,我仍然被邀请作为客座毕业生参与训练营的各种活动。我将指导你如何在每个阶段表现出色——从选择一个优秀的项目到毕业后的沟通。
这些信息适用于有现场讲座/网络研讨会的全日制训练营项目,不适用于大规模开放在线课程(MOOCS)。
在训练营开始之前
做你的研究
训练营是你未来职业(甚至生活)中的一个重要步骤。这就是为什么我建议尽可能多地了解信息。仅仅阅读训练营网站和浏览社交网络是不够的。我强烈建议参加所有免费的活动并积极参与。在网络研讨会上随时向工作人员提问。我还建议联系校友和学生,向他们询问任何问题。如果可能的话,尽量访问线下校园,亲自了解学*环境。这将帮助你在脑海中形成一个完整的图像。
在我阅读了有关我感兴趣的项目的所有信息后,我采取了行动。我在他们的 Instagram 上留言:“我计划申请这个训练营。毕业生和学生们,请点赞评论,我会联系你们。”几个人回复了我,我问了所有我想知道的事情。
我还参加了一个免费的 Python 马拉松。参与者每天会得到一个任务,并且必须在同一天提交解决方案。在活动期间,我积极参与聊天并保持了一致性。一切都很顺利,我赢得了活动。这让我有机会线下访问训练营办公室,感受那里的氛围。我还参加了数据科学项目的在线介绍会。
多亏了这些行动,我了解了关于训练营的所有信息。
记录所有观察结果
记住所有内容是很困难的,因此我强烈建议记笔记。特别是当你考虑多个学校时,这些笔记将对未来的比较和最终决策非常有用。
在我居住的城市,只有一个地方提供全日制训练营,但记录日志帮助我跟踪了一切。我做对了,选择了一个优秀的项目,拥有支持性的导师、强大的课程大纲、出色的职业支持以及周围优秀的人。
选择有入学考试的项目
听起来有点意外,不是吗?入学考试并不是为了折磨你。这样做的主要原因是检查每个学生是否具备理解快节奏和复杂程序所需的基础。训练营也是检查学生是否有动力和是否具备良好素质的方式。训练营不仅是学*技能的地方,它们还建立了一个社区。
通过考试意味着你已经准备好开始一个密集的训练营
当我加入训练营时,我也参加了一次考试。介绍部分是关于我的动机和我的背景,第二部分是关于 Python 基础知识,最后我需要解决 2 道练*题(这是我第一次进行现场编码体验 🤣)。第一个编码任务很简单(“在字符串中找到最长的单词”或类似的任务),而第二个则很难。
这是故意为之的。考官们测试了我们应对困难情况的能力,我们的思维能力,以及分解问题的能力。我花了 45 分钟完成这个任务。
养成学*和获取信息的*惯
在训练营中,你每天都会接收到大量信息。开始阅读新的文章,每天写一些简单的代码片段。每天从 15 到 30 分钟开始。这样你会更容易跳入全职学*。你将*惯于学*和自我提升。
这也将帮助你更好地为考试做准备。比起一个月内吸收大量信息,这要好得多。
在你掌握一些基础知识后去参加训练营
我认为这是这里最重要的建议之一。
训练营不是灵丹妙药或魔法地方,你去那里就能神奇地成为一个超级强的专家。在我看来,训练营更像是一个加速器,而不是数据科学的首次接触。
我坚信,获得一些基础知识是个好主意,因为训练营会让你的知识更好、更深入。你将有一些已经掌握的知识,而训练营会进一步深化和拓宽它。
你不需要掌握一切——一些基础事实和理解就足够让你以 x5 的强度开始训练营。
根据我的观察,那些带着一些知识来参加训练营的学生进步比那些考试后刚来的人要多。而且,他们通过技术面试也更容易。
另一个令我惊讶的效果(😅):有些事情我第一次不理解,第二次却理解了。更重要的是,它帮助我解开了那些完全困惑并困在我脑中的问题。
突然间,我比预期的要更准备好。我从 2020 年初封锁期间开始对数据科学感兴趣,并在 Coursera 上参加了一个课程。之后,我阅读了很多关于数据的书籍,观看了许多 YouTube 视频。结果,我学到了关于经典机器学*的一些知识。
尽管如此,我还是感到困惑,将正则化和回归搞混在一起。我以为自己太笨,无法理解神经网络是如何工作的。我决定参加训练营,“为了在可衡量的时间内做好面试准备”。
当然,经典机器学*是训练营课程的一部分,我理清了很多东西。当我的小组伙伴们试图拟合他们的第一个线性回归时,我对创建新的多项式函数和寻找更复杂的依赖关系感兴趣。以便更深入地了解我已经熟悉的事物。而且,是的,经典机器学*中的一些基础帮助我更好地理解了神经网络。
这是我推荐你熟悉的主题列表(如果你想在训练营期间让自己的生活更轻松高效)。实际操作要比阅读更有效:
-
Bash(基本命令)。提高工作效率
-
Git(你必须练*它,从理论上讲,它可能会让人困惑)。我第一次在训练营中使用 git,并被所有的 push/pull/merge 等操作惊呆了。
-
超越基础数据结构的 Python 练*。我建议你深入了解 OS 模块、文件操作、JSON、日期时间模块和 itertools。面向对象编程基础也是有用的。
-
数据科学的流行 Python 库:pandas、NumPy、sklearn。它们的基础知识以及如何使用它们。
-
适合初学者的机器学*和神经网络材料。
-
…以及你对数据感兴趣的任何内容。
规划你的财务和日常生活
这是枯燥的一部分,但非常重要。当你在 bootcamp 学*时,你的所有思绪都会集中在学*上。我曾经做过一个关于代码的梦。你不会有太多时间去思考日常生活。在你开始全职项目之前,这里有一系列问题需要你回答
-
在项目期间以及找工作时我将住在哪里?
-
我将如何支付 Bootcamp 课程费用?哪种选项最适合我?
-
我将如何支付住宿费用?
-
我需要多少资金才能吃到健康而美味的食物?我在哪里吃早餐、午餐和晚餐?
-
我的学*将如何影响他人?(例如,谁来遛我的狗?)
-
我是否有足够的储蓄来应对求职期间的开支?
计划是有效的。我有一个组员是一个有两个孩子的女性。由于她出色的规划能力,她在学*上取得了成功,家庭生活也一切顺利。
就我而言,我意识到良好的饮食和睡眠时间表可以帮助你获得知识。这并不意味着你必须遵循严格的饮食,只是意味着你摄入足够的脂肪、蛋白质和健康的碳水化合物,不滥用酒精和能量饮料。
我也确保每晚至少睡 8 小时。如果我睡得少,我早上无法集中注意力,白天感到愚蠢。关于良好睡眠的重要性已经有很多讨论。
在 bootcamp 期间
你已经做好了准备,开始了你的 Bootcamp 之旅。以下是一些帮助你顺利学*的提示
合理地做笔记
我见过两种笔记方式:“我会逐字抄写演示文稿”和“我会记住,不需要写”。这两种方法都是错误的。在第一种情况下,导师会分享幻灯片和笔记,在第二种情况下,你会忘记你没有记住的内容。
我有两种类型的笔记:第一种是在讲座中用手写的,另一种是我在电脑上输入与实践相关的有用信息。
第一种类型:我记录了对关键思想的个人理解。这不是对幻灯片的简单重写,而是用我自己的话描述我对特定思想的理解。我还记录了重要的参考资料以及我可以在哪里阅读/观看更多关于这个话题的信息。我总是留出一点空间用于提问。
在我的电脑上,我将实际笔记分成了几组(例如:bash 命令、git、sklearn 技巧等),每当发现有趣或重要的内容时,我都会立即填写。我还设置了一个“杂项”部分,用来写下随机的笔记。
这种记录方式在准备面试时拯救了我的生命。
尝试理解,而不是复制/粘贴
这些建议不仅对你的训练营有帮助,而且对你的整个职业生涯都有益。
通常,你会有一个 Jupyter notebook 或者一个 .py 脚本,其中包含了一个给定主题的示例。你可能会有很强的冲动去复制代码,稍微调整一下,然后让它运行。但这是一个低效的方法。
我坚信,首先要理解代码内部发生了什么,它是如何工作的,然后自己实现它。至少你会明白特定代码片段是如何工作的,并且将来能够实现它。作弊不是一个好主意,你只是在欺骗自己。
不要害怕提问
我花了前两周时间学会了提问。我们的导师告诉我们,没有所谓的愚蠢问题,世界上最愚蠢的问题就是没有提出的问题。
在我的情况下,训练营为学*和发展创造了一个很好的环境,我开始融入并与他们交流。
在实践过程中,我制定了一个正确提问的公式
当是理论时:
- 你解释了“这个”,但我不理解“那个”。你能多给些例子/为什么会这样吗?
如果是编码错误/实际问题
-
尽可能地尝试解决它
-
用谷歌搜索一下,然后再试一次
-
写下一个问题,列出我所有的解决方法,解释为什么不奏效,并给出另一个想法。所有这些信息带来了问题。
这帮助我将问题从“它不工作,帮我”改进为正确、完整且合理的问题。我非常感谢我们的导师,他们耐心地回答了我们所有的问题。幸运的是,大家得到了足够的关注。我非常感谢他们,我学到了很多。
对自己要有耐心
参加训练营是一个勇敢的决定。学*者经历了一个快速而复杂的课程,每天写很多行代码,消耗大量的信息。
所以不要责怪自己整个周末都没有学*,也不要坐到一天 24 小时。这里的关键是平衡和倾听自己身体的能力。保持努力工作和良好休息之间的平衡非常重要。没有适当的放松,很难取得太多成就。我们的目标是支持你度过这段复杂的旅程。
与组员沟通

比萨派对(作者提供的图片)
不要害怕在组员面前显得奇怪。你正在经历训练营——这将把你们聚在一起。讨论当天的话题、数据科学新闻或只是聊聊天是个好主意。这在训练营的日子里对我帮助很大。你需要知道你的组员是你在行业中的网络基础。现在他们是训练营的学生,但 1 年、2 年、3 年后他们会成为什么样的人呢?
我的训练营同事是我生活中最大的福分之一。这真是不可思议的好运。从第一周开始,我们就开始了大量的沟通。我们在咖啡休息时聊天,一起吃午饭,玩桌游。他们是最棒的同事榜样。两年后,我仍然与他们保持联系,并在不同的活动中见面。
我从未想过我会如此提高我的软技能。在训练营之前,我几乎无法与销售人员交谈,谁会知道我会成为一个优秀的沟通者呢?
我还坚信友好的氛围比有毒和竞争的环境更有利于成长。这不是《饥饿游戏》,其中只有一个人得到工作,所以一起合作更有效。
我与优秀的人一起学*。我们在面试中互相支持,讨论如何提出一个问题或另一个问题。每一个录用通知对整个小组来说都是一次庆祝。有一次,我和我的小组成员申请了同一个职位,并且得到了相同的测试。我们没有竞争,而是一起坐下来互相帮助完成这些测试。有些部分对我来说比较容易,有些部分对我的小组成员来说比较容易。结果,我们都完成了测试。
不要害怕参加黑客马拉松和其他活动

我在 2021 年第一次线下黑客马拉松中的手臂(图片来源:作者)
我相信,行业中的每一个活动都是学*新知识、认识行业内的人或向 IT 界展示自己的机会。
我知道在训练营学*期间参加活动可能会让人感到害怕。我为自己设定了活动的目的,这使得事情变得更容易。例如,“我想做一个关于零售行业数据的项目”。在黑客马拉松中,我设定了提高技能的目标,一切都很顺利。
我很早就开始参加黑客马拉松——在训练营的第 5 周。当然,我们的团队由训练营的成员组成。我们有一个优势——我们作为一个团队团结一致。我去那里是为了测试我的技能和学*一些新东西。那是一个线下活动,我感受到了整个氛围——几排桌子上坐着很多团队,通宵编程,免费能量饮料。我在那次活动中表现出色,并且(意外地)在 Pandas 上取得了很大的进步。
聚会和会议比黑客马拉松需要的勇气少——你只需要报名。组织委员会希望看到相关的观众——而你正是那样。还有一个生活小窍门:你可以请训练营的管理部门邀请某人来为你的观众做演讲。当我还是学生的时候,我在反馈表中写道,能有一个演讲嘉宾会很不错。五周后,那个人被邀请来专门为我们做了一次演讲。
不要害怕犯错
关于从错误中学*已经写了无数次。这里是我在学*期间如何搞砸一个模块项目的故事。
在 NLP 模块的项目日,我在处理 CUDA 时遇到了困难。我想比较 Markov 链和神经网络之间的文本生成。当 Markov 链部分完成时,我无法让神经网络工作。我不想在组员和导师面前做演讲。我记得说了类似“这是一个大失败,它没用,我不想来”的话。他们回答说你的经历和实验很重要,分享它们是个好主意。观众非常感兴趣,认真听我讲解。
训练营结束后
不要害怕在职业支持会议中讨论你自己的案例。
训练营可以提供职业支持。在我的学校,我们学*了如何写一份好的简历,如何展示我们的 git 作品集,如何在不同类型的面试中表现出色,以及更多。我发现当我开始直接寻求反馈时,这些帮助变得更加有用:第一次我来找人审阅我刚准备好的简历,并提出了一些问题。这非常有用,因为每个人都有自己的背景,其中一些可以在简历中呈现。我得到了可以在面试中使用的直接帮助。
在你的小组内分享你的经验
你的训练营导师和小组成员可以帮助你进行第一次找工作。例如,我询问了如何回答面试中那些我无法处理的难题。我得到了帮助和很好的解释。我寻求了帮助,并检查了我的测试卷。我还与职业教练讨论了与人力资源的面试,并与其他人分享了。
我的组员们也分享了他们的故事。我们讨论了面试,收集了问题,互相支持。我的意识有了显著提升。我的经验 + 他人的经验 = 职业智慧。
不要错过毕业生活动

幕后花絮:我在为未来申请者举办的活动中(图片来自作者)
对毕业生可以提供不同的活动。例如,在开放日发言或有机会从候选人那里考取考试。我曾经举办过关于 Python 基础的讲座,参加了考试,并在各种活动中发言。你不必参加每一个活动,但自己做点什么是有意义的。这将有助于确保你被记住。拥有额外的支持,知道你在职业道路上并不孤单,训练营可以在困难情况下提供帮助或建议。
感恩并分享你的成功
优秀的训练营(我相信你在阅读完这篇文章后会选择一个好的训练营)会关注他们的毕业生去了哪里。因此,找到工作后告诉他们是有意义的。谈谈公司、薪资、职责,并感谢训练营的帮助。我相信他们会非常高兴。
现在你知道了如何选择一个好的训练营并为课程做准备,如何在学*期间进行学*和行动,以及如何在之后留下良好的印象。训练营是一个独特而难忘的经历,它对我影响很大。祝你好运!
如何根据您的数据将领域特定知识添加到 LLM
将你的 LLM 转变为领域专家
·发表于 Towards Data Science ·7 分钟阅读·2023 年 7 月 11 日
--

由 Hubi's Tavern 在 Unsplash 提供的照片
介绍
*年来,大型语言模型(LLMs)已经深刻改变了我们与技术的工作和互动方式,并且在各种领域中被证明是有用的工具,充当写作助手、代码生成器,甚至是创造性合作伙伴。它们理解上下文、生成类似人类的文本以及执行广泛的语言相关任务的能力使其成为人工智能研究的前沿。
虽然大型语言模型(LLMs)在生成通用文本方面表现优异,但当面对需要精确知识和细致理解的高度专业领域时,它们往往表现得较差。在用于特定领域任务时,这些模型可能会表现出局限性,甚至在某些情况下产生错误或虚假的回答。这突显了将领域知识融入 LLMs 的必要性,使其能够更好地应对复杂的行业术语,展示更细致的上下文理解,并减少生成虚假信息的风险。
在这篇文章中,我们将探讨将领域知识注入 LLMs 的几种策略和技术之一,使其在特定专业环境中表现最佳,通过将文档片段作为上下文添加到 LLM 中来注入查询。
该方法适用于任何类型的文档,仅使用安全的开源技术,这些技术将在你的计算机上本地运行,无需访问互联网。借助这一点,我可以在个人和机密数据上使用它,而不必担心第三方网站的访问。
原理
这是如何工作的详细说明:

过程的图解。图像由作者提供。
第一步是拿到我们的文档,并基于这些文档构建一个向量索引数据库。
向量数据库是一种旨在高效存储和查询高维向量的数据库。这些数据库支持快速的相似性和语义搜索,同时允许用户根据某些距离度量找到与给定查询向量最接*的向量,而不是像传统的 OLTP 和 OLAP 数据库那样对行和列中的值进行查询。
这意味着我们可以创建表示任何文档的嵌入,并用它来填充数据库。
然后,一旦构建完成,我们可以执行一个查询,该查询也将被嵌入,并注入到向量索引数据库中,这样将返回与我们的查询最相关的文档片段。
最后,这些向量可以作为上下文注入到本地 LLM 中,与我们原始的查询一起。这种方式下,选择的上下文将足够小,以被大多数 LLM 接受,并且由于与我们的查询相关,模型将拥有足够的知识来准确回答问题。一点点提示工程也会有所帮助。
案例示例和代码库
在这篇文章中,我们将使用一个本地的开源 LLM,并注入所有 Python 增强提案(PEPs)的领域知识。这一原则可以应用于任何文档类型,但我将使用 PEP,因为它易于获取且属于公共领域,这使它成为一个理想的示例数据集。
你可以在这个代码库中找到我用于撰写这篇文章的完整代码:
github.com/Anvil-Late/knowledge_llm/tree/main
[## GitHub - Anvil-Late/knowledge_llm: 向 LLM 添加领域知识的指南
向 LLM 添加领域知识的指南。通过创建一个帐户来为 Anvil-Late/knowledge_llm 的开发做贡献…
结果的快速预览
结果的展示样例

查询被处理和回答的示例。图像由作者提供。
如何安装 LLM
如果你的计算机上没有安装 LLM,你可以在这里找到如何安装的逐步指南:
medium.com/better-programming/how-to-run-your-personal-chatgpt-like-model-locally-505c093924bc
[## [GPT 教程] 如何在本地运行您的个人 ChatGPT 类模型
您自己的个人 AI 助手
betterprogramming.pub](https://betterprogramming.pub/how-to-run-your-personal-chatgpt-like-model-locally-505c093924bc?source=post_page-----884a5f6a13ca--------------------------------)
如何构建和查询向量索引数据库
您可以在这个仓库中找到构建向量索引数据库的完整代码:
github.com/Anvil-Late/knowledge_llm/tree/main
广义而言,在 src 文件夹中:
-
parse.py 创建 PEP 语料库
-
embed.py 创建嵌入的语料库
-
您可以拉取 Qdrant 向量索引数据库的 Docker 镜像,并使用
docker pull qdrant/qdrant命令运行它docker run -d -p 6333:6333 qdrant/qdrant -
create_index.py 创建并填充向量索引数据库
-
query_index.py 嵌入一个查询并检索最相关的文档
如果您需要更多细节,您可以在这里找到我的逐步指南:
[## 基于 AI 的文档搜索 — 使用自然语言查询导航您的数据库
高效文档导航
betterprogramming.pub](https://betterprogramming.pub/efficiently-navigate-massive-documentations-ai-powered-natural-language-queries-for-knowledge-372f4711a7c8?source=post_page-----884a5f6a13ca--------------------------------)
将所有内容结合起来
首先,我们将编写一个生成 LLM 提示的脚本:
import os
from query_index import DocSearch
import logging
import re
from utils.parse_tools import remove_tabbed_lines
logging.disable(logging.INFO)
def set_global_logging_level(level=logging.ERROR, prefices=[""]):
"""
Override logging levels of different modules based on their name as a prefix.
It needs to be invoked after the modules have been loaded so that their loggers have been initialized.
Args:
- level: desired level. e.g. logging.INFO. Optional. Default is logging.ERROR
- prefices: list of one or more str prefices to match (e.g. ["transformers", "torch"]). Optional.
Default is `[""]` to match all active loggers.
The match is a case-sensitive `module_name.startswith(prefix)`
"""
prefix_re = re.compile(fr'^(?:{ "|".join(prefices) })')
for name in logging.root.manager.loggerDict:
if re.match(prefix_re, name):
logging.getLogger(name).setLevel(level)
def main(
query,
embedder = "instructor",
top_k = None,
block_types = None,
score = False,
open_url = True,
print_output = True
):
# Set up query
query_machine = DocSearch(
embedder=embedder,
top_k=top_k,
block_types=block_types,
score=score,
open_url=open_url,
print_output=print_output
)
query_output = query_machine(query)
# Generate prompt
prompt = f"""
Below is an relevant documentation and a query. Write a response that appropriately completes the query based on the relevant documentation provided.
Relevant documentation: {remove_tabbed_lines(query_output)}
Query: {query}
Response: Here's the answer to your query:"""
print(prompt)
return prompt
if __name__ == '__main__':
set_global_logging_level(logging.ERROR, ["transformers", "nlp", "torch", "tensorflow", "tensorboard", "wandb"])
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--query', type=str, default=None)
parser.add_argument('--top_k', type=int, default=5)
parser.add_argument('--block_types', type=str, default='text')
parser.add_argument('--score', type=bool, default=False)
parser.add_argument('--open_url', type=bool, default=False)
parser.add_argument('--embedder', type=str, default='instructor')
parser.add_argument('--print_output', type=bool, default=False)
args = parser.parse_args()
main(**vars(args))
logging.disable(logging.INFO) 和 set_global_logging_level 可以防止代码执行过程中打印过多内容,因为该脚本打印的所有内容都会被捕获。
我们将这个提示生成与提示注入结合起来,使用以下 bash 脚本:
#!/bin/bash
# Get the query from the command-line argument
query="$1"
# Launch prompt generation script with argument --query
if ! prompt=$(python src/query_llm.py --query "$query" --top_k 1); then
echo "Error running query_llm.py"
exit 1
fi
# Run the terminal command
<PATH_TO_LLAMA.CPP>/main \
-t 8 \
-m <PATH_TO_LLAMA.CPP>/models/Wizard-Vicuna-13B-Uncensored.ggmlv3.q4_0.bin \
--color \
-c 4000 \
--temp 0.1 \
--repeat_penalty 1.1 \
-n -1 \
-p "$prompt" \
-ngl 1
这里发生的情况是,提示生成脚本打印出提示,bash 脚本将其捕获到 $prompt 变量中,然后在 llama.cpp ./main 命令中使用 -p(或 --prompt)参数。
然后 LLM 将接管并完成提示,从‘Response: Here’s the answer to your query:’开始。
记得将 <PATH_TO_LLAMA.CPP> 替换为你计算机上 llama.cpp 克隆的路径,将 Wizard-Vicuna-13B-Uncensored.ggmlv3.q4_0.bin 替换为你的 LLM。我个人选择了这个,因为它给了我很好的结果,并且没有受到限制性许可证的约束,但你可以尝试其他模型!
结论
让我们回顾一下我们在这里完成的工作:
在本文中,我们深入探讨了一种有效的策略,通过将领域知识注入 LLM 来增强其能力。尽管 LLM 在各种任务中表现出色,但它们在面对需要精确知识和细致理解的高度专业化领域时,往往会遇到困难。
为了解决这些局限性,我们探索了一种方法,将领域特定的文档纳入 LLM。通过基于文档构建一个向量索引数据库,我们建立了一个高效的相似性和语义搜索基础。这使我们能够识别出与特定查询最相关的文档,然后将其注入本地 LLM 作为上下文。
我们所提出的方法通过使用 Python 增强程序(PEPs)作为代表性数据集来进行示例。然而,值得注意的是,这种方法适用于任何形式的文档。本文中提供的代码片段和仓库作为实际演示,展示了实施过程。
按照 outlined steps,用户可以提升 LLM 在特定专业环境中的表现,使模型能够处理复杂的行业术语并生成更准确的响应。此外,所使用的安全和开源技术确保了这一过程可以在本地执行,无需外部互联网依赖,从而保护隐私和机密性。
总之,将领域知识融入 LLM(大语言模型)中,使这些模型能够在专业任务中表现出色,因为它们对其操作的背景有了更深入的理解。这种方法的影响跨越了不同领域,使 LLM 能够提供量身定制的宝贵帮助和见解。通过利用 LLM 和领域专业知识的潜力,我们为提升人类与 AI 的互动以及在专业领域利用人工智能的力量打开了新的可能性。
感谢阅读!
如果你有任何问题,请不要犹豫,留下评论,我会尽力回答!
如果你喜欢这个内容,你也可以通过我的推荐链接直接在 Medium 上支持我的工作,并成为会员以获得无限制访问 这里 😃
如何通过残差分析你的时间序列模型
原文:
towardsdatascience.com/how-to-analyse-your-time-series-model-using-residuals-f980f597332e
学*如何使用预测模型的残差来提高其性能
·发表在Towards Data Science ·阅读时间 5 分钟·2023 年 1 月 3 日
--

照片由regularguy.eth提供,发布在Unsplash。
背景
能够分析你的时间序列模型对于诊断其性能至关重要。一个实现这一点的方法是通过拟合模型的残差。在这篇文章中,我们将讨论什么是残差以及如何利用它们来改进你的模型,并提供一个 Python 示例。
附加视频。
什么是残差?
在时间序列分析中,残差,r,是拟合值 ŷ和实际值 y之间的差异:

方程由作者使用 LaTeX 生成。
重要的是要区分残差和误差。误差是实际值和预测值之间的差异。然而,正如上面所示,残差是实际值和拟合值之间的差异。这些拟合值是模型在拟合训练数据时做出的预测。由于模型知道所有观测值,它不再是技术上的预测,而是拟合值。
如果你想了解更多关于预测误差及其指标的信息,请查看我之前的相关文章:
预测时间序列的基本性能指标概述
towardsdatascience.com
残差分析
我们可以使用残差来分析我们的模型捕捉数据特征的效果。一般来说,残差应该:
-
显示非常少或没有自相关或偏自相关。 如果它们有任何形式的相关性,那么模型遗漏了一些数据中的信息。我们可以使用Ljung–Box统计检验和相关图来确定残差是否确实相关。
-
残差的均值应该是零,否则预测将是有偏的。实际上,通过简单地加或减去偏差来调整预测值是非常容易的。
现在让我们在 Python 中进行一些残差分析吧!
Python 中的残差分析
拟合 Holt-Winters 模型
在这次简短的演示中,我们将指数平滑 Holt-Winters模型拟合到著名的美国航空乘客数据集。如果你想了解更多关于 Holt-Winters 模型的工作原理,确保阅读我之前关于它的博客:[链接]
对最强大和最有用的指数平滑模型的讨论和实现
towardsdatascience.com
数据 来自 Kaggle 的 CC0 许可证。
GitHub Gist 由作者提供。

图表由作者在 Python 中生成。
该模型的预测效果看起来相当好。让我们通过将实际值、拟合值和残差插入训练数据集中来分析残差:
GitHub Gist 由作者提供。

图像由作者在 Python 中生成。
正如我们所看到的,残差确实是实际值(#Passengers)和拟合值之间的差异,如之前所述。
残差相关性
作者的 GitHub Gist。

由作者在 LaTeX 中生成的图表。
大多数相关性都在统计上不显著的蓝色区域内,这表明残差不相关。然而,你可能会注意到相关性中存在一些重复模式。这表明模型可能没有完全考虑某些季节性成分。
如果你想深入了解自相关和偏自相关,请参考我之前关于这些内容的帖子:
时间序列分析中的自相关 [## 时间序列分析中的自相关
解释什么是自相关以及它在时间序列分析中的重要性。
偏自相关在时间序列分析中的应用 [## 偏自相关在时间序列分析中的应用
解释什么是偏自相关及其在时间序列分析中的重要性
Ljung-Box 检验
确定残差是否相关的一个更量化的方法是进行Ljung–Box统计检验:
作者的 GitHub Gist。

由作者在 Python 中生成的图像。
这显示了前10个滞后的 p 值。它们都低于显著性水平0.05,因此我们拒绝没有自相关的零假设。因此,我们的残差中存在相关性,在重新拟合模型时需要重新审视。
如果你想了解更多关于统计检验和 p 值的内容,我推荐阅读我之前的相关文章:
Z 检验简单解释 [## Z 检验简单解释
对 Z 检验在统计假设检验中的直观解释
残差的直方图
一个直方图将确定残差是否具有零均值且对称(无偏差):
作者的 GitHub Gist。

由作者在 Python 中生成的图表。
在这种情况下,残差主要分布在零附*,均值为-0.023,甚至可能略微负偏差。这表明我们可能不需要为计算的预测提供偏移量。
总结与进一步思考
在这篇文章中,我们展示了如何使用模型的残差来诊断时间序列预测模型。判断模型是否适合的两个关键指标是:残差没有或只有很少的相关性,并且其均值接*零。
本博客中使用的完整代码可在我的 GitHub 上找到:
[## Medium-Articles/residual_analysis.py at main · egorhowell/Medium-Articles
我在我的中等博客/文章中使用的代码。通过创建一个帐户来贡献 egorhowell/Medium-Articles 的开发…
另外一件事!
我有一个免费的新闻通讯,Dishing the Data,在这里我每周分享成为更好数据科学家的技巧。没有“废话”或“诱饵”,只有来自实践数据科学家的纯粹可操作的见解。
[## Dishing The Data | Egor Howell | Substack
如何成为更好的数据科学家。点击阅读《Dishing The Data》,由 Egor Howell 编写,是 Substack 出版的内容…
newsletter.egorhowell.com](https://newsletter.egorhowell.com/?source=post_page-----f980f597332e--------------------------------)
与我联系!
参考文献与进一步阅读
如何评估推荐系统
对评估指标公式的深入探讨
·
关注 发表在 Towards Data Science ·6 min read·Apr 16, 2023
--
图片来源。该图片在 Pixabay 的内容许可下可免费使用。
在最*的文章中,我们介绍了一些在工业界和文献中广泛应用的显著推荐系统算法,通过不同的视角使用图。此外,我们还阐述了包括基于知识图谱的推荐系统在内的新兴方法,这些方法在推荐研究领域越来越受到关注。
在本文中,我们将深入探讨评估推荐系统的过程。我们将探索评估其性能的关键标准和指标,包括准确性、多样性、覆盖率、新颖性和意外惊喜。
准确性指标
*年来,大多数推荐系统领域的研究工作部分通过来自信息检索领域的准确性指标进行评估,例如精确度 (P@K) 和召回率 (R@K),其公式如下:


其中 n 和 m 分别表示用户数和项目数,i₁,i₂,…,iₖ 是从 1 到 K 排名的项目,如果推荐的项目 iⱼ 对用户 u 相关,则 hit 的值为 1,否则为 0。Rel(u) 表示测试集中用户 u 的相关项目集合。
在产品推荐的背景下,这些评估指标的目的是识别给定用户的 K 个最相关项目,并衡量检索精确相关信息的质量。在仅向用户推荐一个项目的特殊情况下,如会话式推荐系统中,我们希望测量即时下一个项目的正确性,即 hitrate@K:

类似于 R@K,hitrate@K 衡量推荐系统的正确性或准确性。

推荐系统向用户建议一份按排名排列的电影列表。如果用户决定观看前 K 列表中的一部电影(例如 Gatsby),hitrate@K 将等于 1,而 MRR@K 将等于 0.5,因为该电影在列表中排名第二。注意:该图表由作者创建。
文献中广泛使用的其他三个指标,用于评估推荐系统的准确性,特别是捕捉命中在列表中的排名情况:
- 均值平均精度 (MAP@K):该指标衡量推荐系统提供的相关项目的顺序:

- Top-K 均值倒数排名 (MRR@K):该指标是 MAP@K 的特例,其中只有一个相关项目。它衡量推荐系统如何将相关项目与不相关项目进行良好排名:

其中 iⱼ 是前 K 个推荐项目中的相关推荐项目。
- 归一化折扣累积增益 (NDCG@K):MAP@K 和 NDCG@K 都重视排序,但主要区别在于均值平均精度测量的是二元相关性(项目要么感兴趣,要么不感兴趣),而 NDCG@K 允许以实数形式的相关性分数:

在这里,IDCG@K 是理想折扣累积增益,定义如下(Rel(u) 仅包含用户 u 的相关项目):

超越准确性
尽管这些指标在评估推荐系统中的相关性,但推荐相同类型的产品有时可能适得其反,并且在实际应用中可能不够充分(如 Netflix、YouTube 等)。例如,在 Netflix 上,用户可能会被新类型的电影和系列吸引;在 YouTube 上,用户通常希望观看新视频。用户必须感到惊喜,一个好的推荐系统应该具有推荐意外且吸引人的项目的能力。业界也支持不完全依赖于基于精度的度量(例如 Spotify 的每周发现功能)。在研究中 [1],作者指出评估协议的目的是评估推荐项目的质量,而不仅仅是其准确性或实用性。在这种情况下,只有在线实验中系统的用户能够评判推荐质量,才能可靠地评估推荐。因此,在离线评估时,必须考虑除单一准确性之外的其他指标。
为了对推荐质量得出可靠结论,推荐系统不仅需要提供准确的推荐,还必须提供有用的建议。确实,一个极其流行的项目可能是一个准确的建议,但对用户来说可能并不有趣。意外性、新颖性以及多样性是与准确性指标相对的替代度量标准。
推荐系统中的意外性概念指的是系统向用户推荐意外且吸引人的产品的能力。在 [3] 中,提出了一种度量来通过在过滤掉那些过于明显的项目后评估推荐项目的精度来衡量意外性。下面的公式概述了该度量的计算方法。变量 hit_non_pop 类似于 hit,但它将前 k 个最流行的项目视为不相关的,即使它们被包含在用户 u 的测试集中。这是因为流行项目被认为是显而易见的,因为大多数用户都很熟悉它们。

在 [4] 中,提出了一种新颖性度量来评估推荐系统建议用户不太可能知道的项目的能力。该度量的目的是帮助推荐系统促进用户发现新项目。下面的公式在 [5] 中定义,概述了这一度量的计算方法。需要注意的是,与之前的指标不同,新颖性指标仅关注推荐项目的新颖性,而不考虑其正确性或相关性。

函数 Pₜᵣₐᵢₙ : I → [0, 1] 返回在训练集中分配给项目 i 的反馈比例。这个值表示在训练集中观察到特定项目的概率,即与该项目相关的评分数量除以可用评分的总数。
下一步是什么?评估协议
在大多数推荐系统的研究工作中,评估协议是在离线环境中进行的,其中上述指标仅基于过去的互动进行测量。然而,这在现实中被证明是不够的。
在下一个博客文章中,我们将突出不同的评估方法,包括离线评估、在线评估和用户研究,并讨论它们的优缺点。
参考文献
[1] Mouzhi Ge, Carla Delgado-Battenfeld, 和 Dietmar Jannach. 超越准确性:通过覆盖度和偶然性评估推荐系统。在第四届 ACM 推荐系统会议论文集中,RecSys ’10,第 257–260 页,美国纽约,2010 年。计算机协会。
[2] Jonathan L. Herlocker, Joseph A. Konstan, Loren G. Terveen, 和 John T. Riedl. 评估协同过滤推荐系统。22(1):5–53,2004 年 1 月。
[3] Marco de Gemmis, Pasquale Lops, Giovanni Semeraro, 和 Cataldo Musto. 关于推荐系统中偶然性问题的研究。信息处理与管理,51(5):695–717,2015 年 9 月。
[4] Saúl Vargas 和 Pablo Castells. 推荐系统中新颖性和多样性度量中的排名与相关性。在第五届 ACM 推荐系统会议论文集中,RecSys ’11,第 109–116 页,美国纽约,2011 年。计算机协会。
[5] Enrico Palumbo, Diego Monti, Giuseppe Rizzo, Raphaël Troncy, 和 Elena Baralis. entity2rec: 针对项目推荐的属性特定知识图谱嵌入。专家系统应用,151:113235,2020 年。
如何使用 AI 自动生成长时间 YouTube 视频的摘要
原文:
towardsdatascience.com/how-to-auto-generate-a-summary-from-long-youtube-videos-using-ai-a2a542b6698d
使用 Whisper 和 BART 模型在本地 PC 上总结斯蒂芬·沃尔夫勒姆的演讲的逐步指南
·发表于Towards Data Science ·7 分钟阅读·2023 年 4 月 14 日
--

作者生成的图像
动机
在当今快速变化的世界中,保持信息更新和获得灵感可能是一项挑战,尤其是当时间紧迫时。就个人而言,我非常喜欢 YouTube 上的播客和演讲。这些播客和演讲是知识的宝藏,充满了来自各个领域顶尖人才的见解。然而,由于时间限制,我无法观看每一个有趣的视频,因为它们通常超过一小时。这让我开始思考:如果我能创建一个端到端的解决方案来自动提取主要亮点呢?因此,我开始探索 AI 生成解决方案,帮助我获取一些错过的播客/演讲的自动摘要。
在本文中,我讨论了在本地 PC 上进行端到端解决方案。首先,我将介绍如何使用开源的Whisper 模型,对斯蒂芬·沃尔夫勒姆关于 ChatGPT、AI 和 AGI 的演讲进行转录,该演讲在 YouTube 上可用。接着,我将演示如何使用开源的BART模型总结长文本。
让我们看看如何实现这个目标。
请记住,确保在下载内容之前核实版权/许可是否允许下载是至关重要的。
一点背景
Whisper是一个开源自动语音识别模型,基于从互联网收集的 680,000 小时多语言数据进行训练。它依赖于端到端的编码器-解码器Transformer架构。
BART 是一个基于 Transformer 的 seq2seq 模型,结合了双向(BERT 风格)编码器和自回归(GPT 风格)解码器。它通过随机添加噪声并学*重建原始内容进行预训练,在总结和翻译等任务上表现良好。
HuggingFace transformers 库提供了一个用户友好的解决方案来使用和自定义模型。此外,它还提供了可以用于微调模型以更好地适应数据的 API。
PyTube 是一个无需依赖的 Python 库,用于下载和流式传输 YouTube 视频。
NLTK是一个标准的自然语言处理(NLP)任务的 Python 库。
端到端过程
过程包含四个主要步骤:
1. 设置环境
2. 下载 YouTube 视频:PyTube
3. 转录音频:Whisper
4. 总结生成的文本:BART

图片由作者提供
1. 设置环境
我的环境设置如下:
-
Jupyter Notebook 在一个使用 Python 3.10 的虚拟环境中运行
-
模型: OpenAI Whisper, BART
-
库: pytube, transformers, unstructured, ffmpeg-python
1.1 安装库
一些备注:
👉 请注意,只有在从笔记本单元格安装库时才需要 !。
👉 直接从 GitHub 安装 Whisper 模型的最新更新。
👉 解决 PyTube 问题。 如果遇到以下错误 "pytube: AttributeError: ‘NoneType’ object has no attribute ‘span’ cipher.p",请前往 {home}/.local/lib/{your_pythonversion: ex. python3.10}/site-packages/pytube/cipher.py Line 411 并将 transform_plan_raw 变量的值替换如下:
1.2 导入库
1. 下载 YouTube 视频
让我们获取以下演讲的总结“ChatGPT, AI, and AGI with Stephen Wolfram (Wolfram Research 创始人兼首席执行官)” 在 YouTube 上可用 (创作共享许可(允许重用))。
要将视频本地下载为音频文件,我们使用 PyTube 库的 YouTube 类。确保提供有效的 URL。
2. 转录音频
一旦我们将音频下载到本地,我们应该会看到一个名为 demo.mp3 的文件。要转录音频,我们加载 medium Whisper multilingual model,它具有 7.69 亿个参数,并提供英语或多语言格式。您可以查看可用语言模型列表,选择最适合您设置的模型。为了更高的准确性,您可以使用 large Whisper multilingual model。
合并后的字符串将存储在 result[‘text’] 变量中,并保存在本地的 demo.txt 文件中。
❗️ 需要注意的是,转录过程可能需要超过一个小时,具体取决于您的电脑配置。要测试演示,您可以选择一个较短的视频。
3. 总结生成的文本
由于模型无法一次处理多个标记,因此重要的是将文本分割成较小的段落,每个段落最多包含 4000 个标记。为此,我们可以使用 punkt 预训练句子分割模型,它是自然语言工具包(NLTK)库的一部分,能够有效处理自然语言。一旦我们将文本分割成较小的句子块,就可以将它们存储在 text_chunks 变量中以供进一步使用。
我们使用句子分割以防止信息丢失
3.1 将大文本分割成块
这里是可以用来完成工作的代码。
代码包括两个函数:read_file() 用于读取 demo.txt 文件,split_text_into_chunks() 用于将文本分割成块。
3.2 使用 BART 进行文本摘要
为了总结文本,我们使用 HuggingFace Transformers 库和预训练的多语言 BART-large 模型,[facebook/bart-large-cnn](https://huggingface.co/facebook/bart-large-cnn),该模型在 CNN Daily Mail 数据集上进行了微调。Hugging Face 的 Transformers 库提供了许多用于文本、图像或声音等各种任务的现成模型。例如,它提供了一个易于使用的 BART 模型文本摘要管道:pipeline("summarization", model="facebook/bart-large-cnn")。这使得使用起来简单而友好。
以下是执行摘要生成的代码。
总体而言,代码创建了 BART 摘要生成器的一个实例,为给定的文本块生成摘要,并仅在成功生成摘要时将其保存到 summary_demo.txt 文件中。如果摘要超过 5000 个字符,我们将再次使用 BART 摘要生成器。输出将保存在 short_summary_demo.txt 文件中。
这是总结:
Wolfram 语言可能成为更系统探索大型语言模型本质和深度的基础。这是一种精确的计算语言,但它谈论的是现实世界。LLM 中没有太多的模板代码。我认为 Chat GPT 向我们展示了一个重要的科学片段。我们已经自动化了模板代码。我的猜测是,随着人们越来越多地实际使用它,他们只会编辑代码。而它将完成大量的初始五行代码的工作。描述意义的规律性更多了。这实际上是一个问题,即 LLM 可以生成的内容与我们自然语言理解系统可以捕捉的内容之间的界限在哪里。我们已经有数十亿年的进化时间来处理自然的方式。微软研究院发布了一份关于 GPT-4 的 154 页分析报告,在报告中他们得出结论,并且这是他们论文标题中的内容,他们看到了 AGI 的一瞥。你可以做的计算宇宙的可能性是非常大的。我们人类只关心其中的一小部分。问题在于将计算宇宙中的事物与我们人类感兴趣的事物连接起来。在 1900 年,人们不会感到惊讶地认为空间是离散的。我希望在不久的将来,我们实际上能找到一种类似于空间布朗运动的现象,我们将能够看到,我们可以确定它是离散的。
主要收获
该教程是一个个人副项目的一部分,专注于探索生成性 AI 工具。
总结一下,Whisper 模型在所有测试的视频中都表现出色。尽管它偶尔会误识别产品或人物名称,但我对结果相当满意,并且会继续使用它。
另一方面,BART 模型提供了一个值得信赖的开源摘要选项。它的摘要效果相当好。我将它与 谷歌研究的 T5 模型 进行了比较,BART 的摘要更优。的确,它可能并不总是捕捉到所有关键事实,但它的结果很好,所以我会继续使用它来处理我的个人总结任务。
总体而言,像 Whisper 和 BART 这样的 AI 生成解决方案帮助我高效提取长时间播客和演讲中的重要见解。这样,即使在我没有剩余时间的时候,我也能保持信息更新。
我希望你喜欢这篇文章。
感谢阅读!
如果你想将来在收件箱中收到我的故事,不要忘记 订阅。
如果你喜欢阅读我的故事并希望支持我作为作家,可以考虑注册成为 Medium 会员,访问数千篇数据工程和数据科学文章。
[## 使用我的推荐链接加入 Medium — Bildea Ana
作为 Medium 会员,您会员费的一部分将分配给您阅读的作者,您将获得对每篇故事的完整访问权限…
medium.com](https://medium.com/@anna.bildea/membership?source=post_page-----a2a542b6698d--------------------------------)
查看我关于生成性 AI、MLOps 和负责任 AI 的文章合集

生成性 AI
查看列表11 个故事



负责任的 AI
查看列表1 个故事

MLOps - AI 在生产中的应用
查看列表4 个故事


如何通过 Python 预提交钩子提升代码质量?
原文:
towardsdatascience.com/how-to-automate-code-quality-with-python-pre-commit-hooks-e550debbd62e
安心提交你的代码
·发布于 Towards Data Science ·7 分钟阅读·2023 年 7 月 22 日
--

Roman Synkevych 的照片,来自 Unsplash
如果你是 Python 开发者,你可能经常遇到团队成员有不同的编码风格,这使得代码库不一致。因此,这会导致错误,降低生产力,并且使合作变得困难。
作为一个努力保持代码质量的人,我知道这种情况有多么痛苦。
👉 幸运的是,有一个解决方案可以解决这个问题:预提交钩子。
预提交钩子是提交代码到版本控制系统之前运行的脚本或工具。它们可以自动格式化你的代码,运行测试,检查 lint 错误等等。
我开始在个人和专业项目中使用预提交钩子。它们帮助我早期发现并修复潜在问题,确保我的代码始终干净和一致。此外,它们通过自动化重复任务为我节省了大量时间和精力。
在这篇实用的博客文章中,我们将深入探讨这个话题。我们将探索如何设置预提交钩子,定制它们以满足你的需求,并将它们整合到你的开发工作流中。
如果你是一个希望提升团队代码质量和生产力的 Python 开发者,这篇文章适合你。
所以,事不宜迟,让我们来看看 🔍
预提交:它们如何融入 Git 工作流
预提交钩子是自动运行的脚本,在每次提交之前检查你的代码是否有错误。这些钩子是语言无关的,帮助在提交到仓库之前捕捉问题,确保只有高质量的代码被提交。
以下图示展示了一个包含预提交钩子的 git 工作流程:当您执行 git commit 命令时,这些脚本会被触发。执行完成后,最后一步会验证所有检查是否通过。

我个人项目的预提交配置
-
✅ 如果预提交检查未通过,您需要在代码库中进行迭代,并相应地提供修复。
-
❌ 如果预提交检查通过,您的代码将成功提交
在 Python 项目中设置和配置预提交钩子
要在 Python 项目中开始使用预提交,首先需要安装它。您可以使用 Python 包管理器 pip 来完成这一操作。在终端中运行以下命令:
pip install pre-commit
安装完预提交后,您需要在项目的根目录中设置一个名为 .pre-commit-config.yaml 的配置文件。此文件将指定预提交应该使用哪些钩子。
以下是我在 .pre-commit-config.yaml 文件中使用的配置:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: end-of-file-fixer
在此配置中,我们使用了来自预提交钩子 repository(一个包含一些开箱即用钩子的库)中的三个钩子 trailing-whitespace、check-yaml 和 end-of-file-fixer。
-
trailing-whitespace修剪尾随空白字符 -
check-yaml检查 YAML 文件是否格式正确 -
end-of-file-fixer确保文件以换行符结束,并且仅以换行符结束
设置完配置文件后,您需要安装 git 钩子。在终端中运行以下命令:
pre-commit install
现在,预提交将在 git commit 时自动运行。如果配置文件中的任何钩子失败,提交将被中止。
记住,您可以自定义 .pre-commit-config.yaml 文件,以包括与您的项目相关的任何钩子。虽然有许多现成的钩子可用,但如果需要,您也可以创建自己的钩子。
一些常见的 Python 预提交钩子
有许多现成的钩子可以帮助提升您的 Python 代码质量。以下是一些常见的:
1 — black : 这是一个 Python 代码格式化工具。它会自动格式化您的代码,使其更具可读性和一致性。以下是如何将它添加到 .pre-commit-config.yaml 文件中:
repos:
- repo: https://github.com/psf/black
rev: '23.1.0'
hooks:
- id: black
2 — flake8 : 这是一个用于强制执行 Python 编码标准的工具。它会检查您的代码是否符合 PEP8(Python 的官方风格指南),同时也会查找逻辑错误。以下是如何添加它:
repos:
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
3 — isort : 这个工具会自动排序和格式化您的 Python 导入。以下是如何添加它:
repos:
- repo: https://github.com/pycqa/isort
rev: 5.8.0
hooks:
- id: isort
4 — mypy : 这是一个用于 Python 的静态类型检查器。它有助于在代码运行之前捕捉某些类型的错误。以下是如何添加它:
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812
hooks:
- id: mypy
5 — pycln : 这是一个帮助查找和删除未使用的导入语句的工具
repos:
- repo: https://github.com/hadialqattan/pycln
rev: 'v2.1.5'
hooks:
- id: pycln
args: ['.', "--all"]
6 — pydocstyle : 这是一个静态分析工具,用于检查是否符合 Python docstring 规范。
repos:
- repo: https://github.com/pycqa/pydocstyle
rev: '6.3.0'
hooks:
- id: pydocstyle
args: ['.']
7 — 运行 单元测试 的自定义钩子:你可以指定一个自定义命令,在每次提交时触发,而不是引用外部仓库。在这个示例中,我们运行单元测试。这可能会引入一个轻微的开销,具体取决于你的测试套件。
- repo: local
hooks:
- id: unittest
name: run unit tests
language: system
pass_filenames: false
entry: poetry run coverage run -m pytest
这些只是许多可用于 Python 的 pre-commit 钩子中的几个示例。有关更多信息,你可以查看这个 repository 或添加你自定义的质量代码检查。
如何运行 pre-commit 钩子并修复代码库
在这一部分,我将与你分享我在个人项目中使用的 pre-commit 配置。然后,我将展示它如何在实践中工作,并防止潜在的错误。
首先,这是我的配置:
repos:
- repo: https://github.com/psf/black
rev: '23.1.0'
hooks:
- id: black
args: [--config=pyproject.toml]
- repo: https://github.com/pycqa/pydocstyle
rev: '6.3.0'
hooks:
- id: pydocstyle
args: ['.']
- repo: https://github.com/hadialqattan/pycln
rev: 'v2.1.5'
hooks:
- id: pycln
args: ['.']
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 'v4.4.0'
hooks:
- id: trailing-whitespace
- repo: local
hooks:
- id: unittest
name: run unit tests
language: system
pass_filenames: false
entry: poetry run coverage run -m pytest
现在我要将以下格式不良的虚拟脚本添加到现有的代码库中:
# dummy_script.py
import math
def sum(a, b):
return a + b
如你所见,这个脚本有许多问题:
-
未使用的导入语句:
math -
无黑色格式化:参数和变量之间有多余的空格
-
许多尾随空格
如果我们添加这个文件并提交代码,我们会从钩子中得到这些错误:

作者截图
-
black失败但自动重新格式化了代码 -
pydoctyle失败并指出了文档缺失的地方 -
pycln失败并自动移除了未使用的导入语句
如果我们添加自动修改过的脚本并再次提交代码,我们会发现除了 pyodstyle 钩子外,其他都通过了。

作者截图
在这种情况下,我们必须手动更新文档,
"""This is a dummy script."""
def sum(a, b):
"""Sum two numbers."""
return a + b
添加代码,并提交:

作者截图
现在,你准备好推送你的代码了。
观察 pre-commit 对代码质量的影响
pre-commit 对代码质量的影响是显著而多方面的。以下是 pre-commit 可以提升你的 Python 代码质量的几种方式:
-
一致的代码格式化:使用
black和isort等钩子,你的代码将遵循一致的风格。这使得代码不仅对你自己,更对任何可能参与你项目的其他人来说都更易读和理解。 -
早期捕获错误:像
flake8和mypy这样的钩子可以在代码运行之前捕获潜在的错误和漏洞。这可以节省你很多后续调试的时间。 -
强制最佳实践:许多钩子强制执行 Python 最佳实践,例如 PEP8 合规性。这可以帮助你编写更清晰、更高效的代码。
-
运行单元测试:pre-commits 允许你在本地运行单元测试并检查它们是否成功,然后再推送你的代码。这可以防止你在 CICD 过程中遇到测试错误。
通过将 pre-commit 集成到你的开发工作流中,你可以确保你的代码始终保持最高质量。这是一个可以在长远中节省时间、精力和潜在很多麻烦的工具。
结论
pre-commit 是一个强大的工具,可以显著提升你的 Python 代码质量。通过自动检查常见问题、执行最佳实践并防止错误提交,pre-commit 可以节省你大量的时间和调试精力。
它很容易集成到你的开发工作流程中,并且高度可定制,让你可以选择最适合你项目需求的钩子。
刚刚接触 Medium?你可以每月以 $5 订阅,解锁各种主题的无限文章(技术、设计、创业…)你可以通过点击我的推荐链接来支持我。
## 使用我的推荐链接加入 Medium - Ahmed Besbes
作为 Medium 会员,你的部分会费将分配给你阅读的作者,同时你可以全面访问每一篇文章…
资源
如何使用 LLMs 自动提取 PDF 中的实体
原文:
towardsdatascience.com/how-to-automate-entity-extraction-from-pdf-using-llms-ea9c1351f531
利用零样本标注
·发表于Towards Data Science ·阅读时间 5 分钟·2023 年 6 月 15 日
--

由Google DeepMind拍摄,Unsplash提供
在现代机器学*应用中,高质量标注数据的重要性不容忽视。从提升模型性能到确保公平性,标注数据的力量巨大。然而,创建这样的数据集所需的时间和精力同样不容小觑。但是,如果我们能够将这一任务所需的时间从几天缩短到仅仅几个小时,同时保持甚至提高标注质量呢?这是一个乌托邦式的梦想?不再是。
机器学*中的新兴范式——零样本学*、少样本学*和模型辅助标注——为这一关键过程提供了变革性的方法。这些技术利用先进算法的力量,减少了对大量标注数据集的需求,实现了更快、更高效且极具效果的数据注释。
在本教程中,我们将介绍一种利用大型语言模型(LLM)上下文学*能力自动标注非结构化和半结构化文档的方法。
从 SDS 中提取信息
与传统的需要大量标注数据以训练特定任务的监督模型不同,LLMs 可以通过利用其庞大的知识库,从少量示例中进行概括和推断。这种新兴的能力,被称为上下文学*,使得 LLM 成为许多任务的多功能选择,涵盖了不仅仅是文本生成,还包括命名实体识别等数据提取任务。
在本教程中,我们将使用 GPT 3.5(也称为 ChatGPT)的零样本和少样本标注功能,对来自不同公司的安全数据表(SDS)进行标注。SDS 提供了关于特定物质或混合物的全面信息,旨在帮助工作场所有效管理化学品。这些文件在提供有关危害的详细见解方面发挥着至关重要的作用,包括环境风险,并提供宝贵的安全防护指导。SDS 作为知识的重要来源,使员工能够在工作场所安全处理和使用化学品时做出明智的决策。SDS 通常以不同布局的 PDF 格式出现,但通常包含相同的信息。在本教程中,我们的目标是训练一个 AI 模型,自动识别以下实体:
-
产品编号
-
CAS 号
-
用例
-
分类
-
GHS 标签
-
公式
-
分子量
-
同义词
-
紧急联系电话
-
急救措施
-
组件
-
品牌
提取这些相关信息并将其存储在可搜索的数据库中对许多公司非常有价值,因为它允许快速搜索和检索危险成分。以下是一个 SDS 的示例:

公开可用的 SDS。图片由作者提供
零样本标注
与文本生成不同,信息提取对 LLM 来说是一个更具挑战性的任务。LLM 已经接受了文本补全任务的训练,通常在被提示提取相关信息时,倾向于出现幻觉或生成额外的评论或文本。
为了正确解析 LLM 的结果,我们需要从 LLM 获得一致的输出,如 JSON。这需要一些提示工程以确保准确。此外,一旦结果被解析,我们需要将其映射到输入文本中的原始标记。
幸运的是,所有这些步骤都已经完成,并通过UBIAI 标注工具进行了抽象化。在后台,UBIAI 进行提示、分块数据以确保在上下文长度限制内,并将其发送到 OpenAI 的 GPT3.5 Turbo API 进行推断。一旦输出返回,数据将被解析、处理并应用到您的文档中进行自动标注。
首先,上传您的文档,无论是原生 PDF、图片还是简单的 Docx,然后进入标注页面,选择标注界面中的 Few-shot 选项卡:

UBIAI 少样本仪表板。图片由作者提供
欲了解更多详细信息,请查看文档:ubiai.gitbook.io/ubiai-documentation/zero-shot-and-few-shot-labeling
UBIAI 使您能够配置模型学*的示例数量,以自动标记下一个文档。该应用程序会自动从您已经标记的数据集中选择最具信息量的文档,并将它们串联在提示中。这种方法被称为“少量示例标记”,其中“少量”范围从 0 到 n。要配置示例数量,只需点击配置按钮并输入示例数量,如下所示。

UBIAI 少量示例配置窗口。图像来源:作者
在本教程中,我们将向 LLM 提供零个示例来学*,并要求它仅根据实体本身的描述来标记数据。令人惊讶的是,LLM 能够很好地理解我们的文档,并正确完成大部分标记!
以下是对 SDS PDF 进行零-shot 标记的结果,没有任何示例,非常令人印象深刻!

使用 UBIAI 进行零-shot 标记。图像来源:作者
结论
利用大型语言模型(LLMs)自动从 PDF 中提取实体已经成为现实,得益于 LLM 的上下文学*能力,例如零-shot 学*和少量示例学*。这些技术利用 LLM 的潜在知识,减少对大量标记数据集的依赖,实现更快、更高效且效果显著的数据注释。
本教程展示了一种自动标记半结构化文档的方法,特别关注于安全数据表(SDS),但也适用于非结构化文本。通过利用 LLM 的上下文学*能力,特别是 GPT 3.5(chatGPT),教程展示了能够自动识别 SDS 中的重要实体,例如产品编号、CAS 号、使用案例、分类、GHS 标签等。
提取的信息,如果存储在可搜索的数据库中,将为公司提供重大价值,因为它允许快速搜索和检索危险成分。教程强调了零-shot 标记的潜力,其中 LLM 可以理解并从 SDS 中提取信息,而无需任何明确的示例。这展示了 LLM 的多功能性和泛化能力,超越了文本生成任务。
如果您有兴趣使用 LLM 的零-shot 能力创建自己的训练数据集,请点击这里安排演示。
在 Twitter 上关注我们 @UBIAI5!
如何在 AWS EMR 上使用 Airflow 自动化 PySpark 管道
原文:
towardsdatascience.com/how-to-automate-pyspark-pipelines-on-aws-emr-with-airflow-a0cbd94c4516
优化大数据工作流的编排。
·发表于 Towards Data Science ·8 分钟阅读·2023 年 8 月 23 日
--

按需课程 | 推荐
我的一些读者联系过我,询问是否有按需课程来帮助你 成为 一名扎实的 数据工程师。这些是我推荐的 3 个优秀资源:
还不是 Medium 会员? 考虑使用我的 推荐链接 注册,以每月仅需 $5 的费用访问 Medium 的所有内容!
简介
在数据工程和分析的动态领域中,构建可扩展和自动化的管道至关重要。
对于已经在使用 Airflow 的 Spark 爱好者来说,可能会好奇:
如何使用 Airflow 在远程集群上执行 Spark 作业?
如何使用 AWS EMR 和 Airflow 自动化 Spark 管道?
在本教程中,我们将通过展示如何集成这两种技术来进行演示:
-
配置并从 Airflow UI 中获取必要的参数。
-
创建辅助函数以自动生成首选的
spark-submit命令。 -
使用 Airflow 的
EmrAddStepsOperator()方法构建一个任务,该任务提交并执行一个 PySpark 作业到 EMR。 -
使用 Airflow 的
EmrStepSensor()方法监控脚本执行。
本教程中使用的代码可在 GitHub 上获取。
先决条件
- 一个 AWS 账户 配置了一个 S3 存储桶和 EMR 集群 在同一区域( 在这种情况下是
eu-north-1)。 EMR 集群应处于WAITING状态。在我们的案例中,它被命名为emr-cluster-tutorial:

作者提供的照片(个人 EMR 集群)
-
一些模拟
balances数据已经在S3存储桶的src/balances文件夹中。数据可以使用 数据生成器 脚本生成并写入该位置。 -
所需的
JARs应已经从 Maven 下载并保存在S3存储桶中。 -
Docker 已安装并在本地计算机上运行,分配了 4-6 GB 的内存。
架构
目标是将一些模拟数据以 parquet 格式写入 S3 存储桶,然后构建一个 DAG,该 DAG:
-
从 Airflow UI 获取所需配置;
-
将
pyspark脚本上传到同一个S3存储桶; -
提交一个 Spark 作业,执行刚刚上传的脚本;
-
通过传感器监控执行状态,直到成功或失败。

作者提供的照片 (app.diagrams.net/)
配置
克隆仓库
第一步是将 仓库 克隆到您选择的本地文件夹,并显示文件夹结构:
git clone git@github.com:anbento0490/projects.git repo_local_name
cd repo_local_name/airflow_emr_spark
tree
airflow_emr_spark 文件夹结构如下:
-
docker-compose.yml和Dockerfile.airflow文件是运行本地 Airflow 服务所需的,然后可视化/执行DAG; -
orchestration文件夹包含名为submit_spark_job_to_emr.py的DAG本身; -
computation文件夹包含将提交到EMR集群的.py可执行文件; -
最后,
assets文件夹包括了所需配置的JSON格式副本以及data_producer笔记本。

文件夹结构(树状图)
在 Docker 上运行 Airflow
确保本地运行 docker,然后在 airflow_emr_spark 文件夹中执行 docker compose up -d。此命令将在我们的容器中启动 Apache Airflow:

作者终端的截图
现在我们导航到 localhost:8091,当请求凭证时,我们只需输入 admin 两次。此时,DAG 应该已经解析完毕,样子如下:

从运行在 Docker 上的 Airflow
在探索 DAG 代码之前,在 Airflow 中还有两个剩余操作:
- 导航到
Variables部分,创建一个名为dag_params的新变量,将dags/assets/dag_params.json文件的内容复制粘贴到其中。当然,bucket_name应更新以匹配我们 AWS 账户中的S3存储桶。

AF UI 中的 dag_params 变量
- 导航到
Connections部分,并创建一个名为aws_default的新Amazon Web Services连接,用于通过 Airflow 与S3和EMR集群进行交互。连接参数应以如下形式粘贴到Extra部分:
{"aws_access_key_id": "xxxxxx",
"aws_secret_access_key": "xxxxxx",
"region_name": "eu-north-1"}

AF UI 中的 aws_default 连接
任务
初步步骤作为 submit_spark_job_to_emr.py DAG 的一部分是从 Airflow UI 中之前保存的 dag_params 变量中检索配置。
如果变量不存在,DAG 将默认为文件夹中可用的 dag_params.json(default_json)。
由于本教程还展示了如何将特定的 JARS 提供给 EMR 集群,因此一开始我们还定义了一个 jar_string 变量以便在稍后添加步骤时作为参数提交。然而,如果不需要 JARS,则可以省略此变量(以及 spark_jars_conf 和 spark_jars_conf_value 在 dag_params 中):

我们应该认识到,这不是在 Airflow 中导入变量的高效方法。
这是因为 Variable.get() 方法在任务之外被调用,这意味着变量在每次 airflow scheduler 解析 dags 文件夹时都会被导入 每次。
然而,为了学*目的,这种实现方式更简单,当只有一个管道涉及时并不算大问题。然而,在生产环境中,我们应确保遵循 这些 最佳实践,总结来说建议:
-
将变量作为任务的一部分并仅在需要时导入;
-
使用
XCOMS在任务之间交换变量。
DAG 包含四个任务。让我们逐一描述它们的功能。
获取集群 ID
第一个任务是 fetch_cluster_id:
fetch_cluster_id = PythonOperator(
dag=dag,
task_id="fetch_cluster_id",
python_callable=fetch_cluster_id,
)
这个任务的功能是运行 fetch_cluster_id() python 函数,该函数:
-
按名称获取并返回
CLUSTER_ID(后续添加 EMR 步骤所需),并期望cluster_state为WAITING或RUNNING。在我们的例子中,我们将"emr-cluster-tutorial"传递给get_cluster_id_by_name方法 → 为了正常工作,连接中指定的region_name必须与创建EMR集群的区域匹配。 -
显示导入变量的值以便于查看(建议:在生产中,此函数应修改为首先获取所有属性,以避免低效的顶级导入*)。

上传脚本到 S3
我们使用以下任务将 read_and_write_back_to_s3.py 可执行文件从本地 computation/src/python_scripts 文件夹上传到指定的 s3://bucket_name/src/ 文件夹,然后提交到 EMR 集群:
upload_script_to_s3 = PythonOperator(
dag=dag,
task_id="upload_script_to_s3",
python_callable=upload_script_to_s3,
op_kwargs={"filename": local_script, "key": s3_script}
)
函数 upload_script_to_s3 创建了一个 S3Hook,在 Airflow 和 AWS 之间使用 aws_default 连接,并使脚本在首选的 S3 路径下可用:

执行 PySpark 脚本
现在 python 脚本已经存在于 s3 存储桶中,是时候使用 EmrAddStepsOperator() 向 EMR 添加一个步骤了。
实际上,execute_pyspark_script 利用下面的辅助函数,将一个 spark 应用程序以 client 模式部署到 EMR(但在生产环境中 cluster 模式 应当更为推荐 *),同时使用从 dag_params 中获取的其他(可选)spark_conf 参数:

实际上,execute_pyspark_script 任务指示 EMR 执行 read_and_write_back_to_s3.py 脚本(在 CLUSTER_ID 通过第一个任务获取并通过 XCOMS 拉取),并将多个参数提供给集群,顺序如下:
-
全局参数如
jar_strings或packages→ 需要作为参数传递在 python 脚本之前(尽管是可选的); -
函数参数
s3_input→ 指定了传递给 python 脚本的输入balances数据集S3路径; -
函数参数
s3_output→ 指定了存储输出数据集的S3路径。
execute_pyspark_script = EmrAddStepsOperator(
dag=dag,
task_id='execute_pyspark_script',
job_flow_id= "{{ ti.xcom_pull(task_ids='fetch_cluster_id', key='return_value') }}",
aws_conn_id='aws_default',
steps=[{
'Name': 'execute_pyspark_script',
'ActionOnFailure': 'CONTINUE',
'HadoopJarStep': {
'Jar': 'command-runner.jar',
'Args': [
*generate_spark_submit_command(spark_submit_cmd, spark_conf_map),
"--jars",
'{{ params.jars_string }}',
's3://{{ params.bucket_name }}/{{ params.s3_script }}',
'--s3_output',
'{{ params.s3_output }}',
'--s3_input',
'{{ params.s3_input }}'
]
}
}],
params={
"bucket_name": BUCKET_NAME,
"s3_script": s3_script,
"s3_input": s3_input,
"s3_output": s3_output,
"jars_string": jars_string
}
)
使用传感器监控执行
最后但同样重要的是,execute_pyspark_script_sensor 任务运行一个 EmrStepSensor(),该传感器定期 探测 集群本身,以验证应用程序的健康状况和执行状态。
它特别要求两个参数:JOB_FLOW_ID == CLUSTER_ID 和 STEP_ID,我们可以通过 XCOMS 获取这两个参数:
execute_pyspark_script_sensor = EmrStepSensor(
dag=dag,
task_id="execute_pyspark_script_sensor",
job_flow_id="{{ ti.xcom_pull(task_ids='fetch_cluster_id', key='return_value') }}",
aws_conn_id="aws_default",
step_id="{{ ti.xcom_pull(task_ids='execute_pyspark_script', key='return_value')[0] }}",
sla=timedelta(minutes=5)
)
代码
python 可执行文件包括一个简单的 process_data_and_write_to_s3() 函数,该函数:
-
从
s3_input路径读取parquet格式的数据到一个 PySpark SQL 视图中; -
利用 SQL API 过滤初始数据集,并将结果保存到
df_filteredDF; -
最终将过滤后的数据集(再次以
parquet格式)写入s3_output路径。

我们现在可以触发 DAG 并等待其成功执行:

Airflow DAG 图
导航到 EMR 集群的 Steps 部分,我们应该能够验证脚本执行确实已完成:

AWS EMR 步骤 部分
实际上,通过选择 stout 日志,我们应该能看到类似于以下的消息,它告诉我们一个包含 ~2.5M 行(从初始的 10M 中筛选出来)的 DF 已经被写入 tgt/balances/ 文件夹中:
Start process...
Reading data from s3://emr-data-947775527574/src/balances directory and creating Temp view
Creating DF with only balances for Company_A
Writing DF to s3://emr-data-947775527574/tgt/balances/ directory...
Data written to target folder...
DF rows count is 2499654
PROCESS COMPLETED!
实际上,检查 bucket_name/tgt/balances/ 文件夹时,我们应该找到一个或多个包含与 Company_A 相关的数据的 parquet 文件(如筛选条件中指定的):

tgt/balances 文件夹的内容
结论
在本教程中,我们学*了如何使用 Apache Airflow 和 AWS EMR 结合来自动化 PySpark 流水线。
这里的假设是EMR 集群始终处于闲置状态,等待添加步骤。在专业环境中这种情况并不罕见,或者通过设置个人 AWS 账户也能轻松实现。
另一种常见的做法是直接从 AF UI 中获取配置,并使用辅助功能生成更复杂的提交命令,这些命令可以通过按需编辑配置来更新。
我们使用的是一个简单的 PySpark 脚本,但我们当然可以设想扩展这个示例,以涵盖更复杂的使用案例。
一旦完成工作,请记得使用以下命令停止 docker 上的 Airflow 服务:
docker compose down
来源
如何自动提取和标记 Seaborn KDE 图上的数据点
·发表于 Towards Data Science ·8 分钟阅读·2023 年 9 月 5 日
--

DALL·E 2023——一幅印象派风格的山脉起伏画作,山脊线上带有明亮的彩色圆圈(其余图片均由作者提供)。
核密度估计图 是一种方法——类似于直方图——用于可视化数据点的分布。虽然直方图对观测值进行分箱和计数,但 KDE 图使用高斯核平滑观测值。作为直方图的替代方案,KDE 图可能更具吸引力,更易于在同一图形中进行比较,并且更好地突出数据分布中的模式。

直方图与 KDE 图
在 KDE 图上注释统计度量值如均值、中位数或众数,使其更具意义。虽然为这些度量值添加线条是简单的,但使它们看起来整洁且不杂乱却并非易事。

使用简单方法添加的标记线(左)与使用更复杂但更具吸引力的方法(右)
在这个快速成功数据科学项目中,我们将使用美国人口普查和国会数据集,程序化地注释多个 KDE 图并标记中位数值。这种方法将确保图形注释自动调整数据集的更新。
欲了解有关 KDE 图的更多细节,请参见我之前的文章 这里。
数据集
由于美国有候选年龄法律,国会成员的生日是公共记录的一部分。为了方便起见,我已编制了一份包含现任国会成员姓名、生日、政府部门和政党的 CSV 文件,并将其存储在此Gist中。
对于美国人口,我们将使用人口普查局的月度后普查平民人口表格,数据截止到 2023 年 7 月。与前面的数据集一样,这些是公开信息,我已将其保存到此Gist的 CSV 文件中。
安装库
对于这个项目,我们需要安装 seaborn 用于绘图,pandas 用于数据分析。你可以按以下方式安装这些库:
使用 conda: conda install pandas seaborn
使用 pip: pip install pandas seaborn
代码
以下代码在 JuptyerLab 中编写,并按单元格描述。
导入库
从 seaborn KDE 图中提取值的秘诀是导入 matplotlib 的[Line2D](https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html)类,这使我们可以访问曲线上的点坐标。此外,我们将使用 matplotlib 的[patches](https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.Patch.html)绘制矩形,以标示出众议院和参议院的合法年龄限制。patch是一个具有面色和边缘颜色的 matplotlib artist对象。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import Rectangle
import seaborn as sns
import pandas as pd
加载国会数据集并计算年龄
以下代码加载国会数据集并计算每位成员截至 2023 年 8 月 25 日的年龄。它首先将参考日期和数据框的“生日”列转换为datetime格式,使用 pandas 的to_datetime()方法。然后利用这些“日期感知”格式生成“年龄”列,通过减去两个值、提取天数,然后将天数除以 365.25 转换为年份。
# Load the data:
df = pd.read_csv('https://bit.ly/3EdQrai')
# Assign the current date:
current_date = pd.to_datetime('8/25/2023')
# Convert "Birthday" column to datetime:
df['Birthday'] = pd.to_datetime(df['Birthday'])
# Make a new "Age" column in years:
df['Age'] = ((current_date - df['Birthday']).dt.days) / 365.25
df['Age'] = df['Age'].astype(int)
df.head(3)

加载人口数据集
接下来,我们将人口数据加载为 pandas DataFrame。
# Load the US population data for July 2023:
df_popl = pd.read_csv('https://bit.ly/3Po0Syf').astype(int)
display(df_popl)

计算美国人口的中位年龄
这是一个有趣的问题。你如何找到美国人口的中位年龄?也就是说,你如何将中位人口值与年龄关联起来?
关键是将人口的累计分布绘制与年龄进行对比。由于你必须年满 25 岁或以上才能在国会服务,我们首先将数据框过滤到这些年龄。这里是这个概念:

使用累计分布图找到美国人口中年龄大于 24 岁的中位年龄
这是带有注释的代码:
# Calculate the cumulative sum of the population over 24 years:
df_popl = df_popl[df_popl['Age'] >= 25].copy()
df_popl['Cumulative_Population'] = df_popl['Population'].cumsum()
# Find the total population:
total_population = df_popl['Population'].sum()
# Find row where the cumulative population crosses half the total population:
median_row = df_popl[df_popl['Cumulative_Population']
>= total_population / 2].iloc[0]
# Get the median age:
popl_median_age = median_row['Age']
# Get the median population:
popl_median = total_population / 2
制作一个简单的堆叠 KDE 图
在我们注释图表之前,让我们看看“开箱即用”的效果如何。我们将在同一图形中分层多个 KDE 图。这些包括众议院一个、参议院一个,以及一个针对 24 岁以上美国人口的图。
# Make a list of median member ages by branch of government:
median_ages = df.groupby('Branch')['Age'].median()
# Make a custom (red-blue-gray) color palette (optional):
colors = ['#d62728', '#1f77b4', '#7f7f7f']
sns.set_palette(sns.color_palette(colors))
# Plot Congressional ages as a KDE and overlay with population KDE:
fig, ax = plt.subplots()
sns.kdeplot(data=df,
x='Age',
hue='Branch',
multiple='layer',
common_norm=True)
sns.kdeplot(df_popl,
x='Age',
weights='Population',
color='grey',
alpha=0.3,
legend=False,
multiple='layer')
ax.set_title('Age Distributions of Senate, House, and US Population > 24 yrs')
ax.legend(loc='upper left', labels=['Senate', 'House', 'Population'])
ax.set_xlim((0, 110));

一个简单的分层 KDE 图
kdeplot() 方法的一个重要参数是 common_norm,代表“公共归一化”。
根据 seaborn 的 文档,“当
common_norm设置为True时,所有 KDE 曲线将使用相同的比例进行归一化。这在你想比较不同组的总体分布形状时非常有用。尤其是在你有多个组具有不同样本量或不同值范围时,它确保了曲线在形状上可以直接比较。”
请注意,在这种情况下,归一化仅应用于众议院和参议院曲线,因为人口数据是从不同的 DataFrame 中单独绘制的。这是因为我们需要加权年龄数据,以适应我们的人口数据集没有每个人的独特年龄值的情况。
寻找众议院和参议院的中位年龄值
尽管前面的图表很吸引人,但它让读者工作过于繁重。x 轴需要更多的分辨率,而且知道均值或中位数值在曲线上的位置会更好。由于两院都包括一些非常年长的成员,这可能会影响均值,因此我们将关注中位数值。
首先,我们需要为每个分支找到中位值(我们之前已经找到了人口中位值)。由于我们希望程序化地找到注释的图表坐标,我们将为每个分支创建一个单独的 DataFrame。这将使得提取曲线数据更容易。
# Filter the DataFrame to each branch of government:
df_house = df.loc[df['Branch'] == 'House'].copy()
df_senate = df.loc[df['Branch'] == 'Senate'].copy()
# Find the median age values for each branch:
median_house = int(df_house['Age'].median())
median_senate = int(df_senate['Age'].median())
绘制和注释 KDE
以下注释代码绘制并注释了图表。我们的目标是找到曲线上的 (x, y) 坐标,以便我们在绘制线条和发布文本时可以程序化地提供这些坐标。这使得代码能够适应输入数据的任何变化。
那么我们该怎么做呢?当 seaborn 创建一个 KDE 图时,它会返回一个 matplotlib [axes](https://matplotlib.org/stable/api/axes_api.html) 对象。这个类型的对象具有一个 [get_lines()](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.get_lines.html) 方法,返回一个包含该对象的线段列表。这些线段是 [Line2D](https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html) 对象,具有一个 get_data() 方法,该方法以 (x, y) 对的形式返回线数据。因为这些坐标可能不包括我们想要的确切值,我们将使用 NumPy 的 [interp()](https://numpy.org/doc/stable/reference/generated/numpy.interp.html) 方法来插值这些值。
# Create figure and title:
fig, ax = plt.subplots()
ax.set_xlim((0, 110))
ax.set_xticks(range(0, 110, 10))
ax.set_title('Age Distributions of House, Senate, and US Population > 24 yrs')
# Define colors and labels:
colors = ['#d62728', '#1f77b4', '#7f7f7f']
labels = ['House', 'Senate', 'Population']
# Loop through the datasets and plot KDE, median lines, and labels:
datasets = [df_house, df_senate]
medians = [median_house, median_senate]
for i, (data, color, label) in enumerate(zip(datasets, colors, labels)):
sns.kdeplot(data=data, x='Age', color=color, fill=False, label=label)
x, y = ax.get_lines()[i].get_data()
f = np.interp(medians[i], x, y)
ax.vlines(x=medians[i], ymin=0, ymax=f, ls=':', color=color)
ax.text(x=medians[i], y=f, s=f'Median = {medians[i]}', color=color)
# Make and annotate the population KDE plot:
sns.kdeplot(df_popl, x='Age', weights='Population', color='#7f7f7f', fill=False)
x, y = ax.get_lines()[2].get_data() # Note that this is the 3rd line([2]).
f = np.interp(popl_median_age, x, y)
ax.vlines(x=popl_median_age, ymin=0, ymax=f, ls=':', color='#7f7f7f')
ax.text(x=popl_median_age, y=f,
s=f'Median = {popl_median_age}', color='#7f7f7f')
# Build a custom legend:
legend_handles = [Line2D(xdata=[0, 1], ydata=[0, 1], ls='-',
color=color) for color in colors]
ax.legend(handles=legend_handles, loc='upper left', labels=labels)
# Manually annotate the Age Limit shading:
age_limit_rects = [
Rectangle((25, 0), 85, 0.003, facecolor='#d62728', alpha=0.3),
Rectangle((30, 0), 85, 0.001, facecolor='#1f77b4', alpha=0.6)
]
for age_rect, label, color in zip(
age_limit_rects, ['House age limits', 'Senate age limits'],
['#d62728', '#1f77b4']):
age_rect.set_zorder(0) # Move rect below other elements.
ax.add_patch(age_rect)
ax.text(x=age_rect.get_x(), y=age_rect.get_height(),
s=label, color=color)

注释过的 KDE 图
在之前的代码中,我们将人口 KDE 图绘制在循环之外,因为年龄数据需要按其人口值加权。
我们还手动标注了年龄限制的彩色矩形和文本,因为程序化的解决方案不是很吸引人。将这些注释“硬编码”是可以接受的,因为这些信息是固定的,不会随着输入数据的变化而改变。
总结
在这个项目中,我们通过编程提取了 KDE 曲线上点的(x, y)坐标,并用它们来标注图表。结果是图表看起来更加整洁,其中垂直标记线在与曲线相交时终止,而文本注释从该交点开始。这使得代码更加灵活,因为这些注释会随着输入数据的变化自动更新。
我们还使用了累积分布来找到一个 pandas DataFrame 列的中位数值,该值对应于相关列的中位数值。我们必须这样做,因为我们的年龄与人口输入数据已被分箱。
谢谢!
感谢阅读,请关注我以获取更多未来的快速成功数据科学项目。
如何在 R 中自动导入和合并多个文件
原文:
towardsdatascience.com/how-to-automatically-import-and-combine-multiple-files-in-r-30a77c0a7732
不要再浪费时间手动导入多个文件
·发表于 Towards Data Science ·6 分钟阅读·2023 年 9 月 20 日
--

图片由 ThisisEngineering RAEng 提供,Unsplash
在我的数据科学家工作中,由于不同软件的导出限制,我经常需要导入几个包含相同类型信息的不同文件。如果你也有类似的情况,下面是一个清晰简单的方法,能够自动将文件导入为独立的数据框或将它们合并成一个数据框。
准备你的文件
在开始编写代码之前,我们首先需要准备好文件。我们需要有一种方法来程序化选择我们想导入到 R 中的文件。虽然你可以选择任何方法来区分这些文件,下面是两种最简单的方法:
-
为所有你想一次导入的文件创建一个独特的前缀。
-
在你的工作目录中创建一个单独的文件夹,只包含那些文件。
例如,如果我有一组名为“SA#.xlsx”的 Excel 文件。如果我没有其他以 SA 开头的类似文件,那么我已经有了我的前缀。如果我的文件夹中还有其他以 SA 开头的文件,比如“SAT.xlsx”,我可以很容易地创建一个文件夹,命名为“SA”。然后,我将仅将我想导入的 SA 文件放入该文件夹中。
创建你的文件列表
一旦我们有了程序化识别文件的方式,我们需要创建一个所有文件名的列表。我们可以使用 R 函数 list.files() 来实现这一点。
带前缀的文件列表
如果你选择为文件名添加前缀,我们将使用 list.files() 的模式参数来选择我们想要的特定文件。
# Formula
filelist <- list.files(pattern = "^<prefix>")
#Example
filelist <- list.files(pattern = "^SA")
该模式接受一个正则表达式。因此,我们可以使用“^”符号表示字符串的开头。这确保了任何其他文件名中包含“SA”但不在开头的文件将不会被包含在这个名字集合中。注意:这只会从你的工作目录中提取文件。你可以更改路径以从不同的目录中提取文件。
文件夹中的文件列表
如果你选择将文件添加到一个文件夹中,我们将使用路径参数来告诉 R 从哪里提取我们的文件。
#Formula
filelist <- list.files(path = "./<folder name>")
#Example
filelist <- list.files(path = "./SA")
“.”符号指向当前工作目录。然后,它会查找一个名为“SA”的文件夹,并包括来自该文件夹的所有文件名。
导入你的文件
现在我们有了一个列表,我们可以对这个列表运行一个循环,将所有文件导入到当前环境中。如果我们想将每个文件作为自己的变量包含,我们将首先需要创建文件名,然后导入文件,再将文件动态分配给变量名。虽然这个过程对于前缀文件和文件夹导入的文件是相似的,但在导入文件时有一个小的区别。
导入文件
让我们首先讨论将文件读取到 R 环境中的区别,因为这将是上述两种不同方法所需的唯一变化。
如果你使用了前缀方法,文件存在于你的工作目录中。因此,你无需指定文件的路径。然而,如果你将它们添加到一个文件夹中,它们就不再在你的工作目录中。因此,我们需要动态构建这些文件的文件路径。
为了创建这个公式,我将使用变量“file”来表示我们filelist变量中的文件名。这将允许我在下面的循环中直接使用这段代码。
# For Prefix Files
df <- <read function>(file)
# Prefix example with an excel file
df <- read_excel(file)
#For Folder files
df <- <read function>(paste("./<folder name>/",file,sep=""))
#Folder file example with excel file
df <- read_excel(paste("./SA/",file,sep=""))
对于文件夹中的文件,每次导入文件时,我们必须将文件夹路径添加到文件名中。幸运的是,R 有一个 paste 命令,允许我们动态地将文件夹路径添加到每个文件名中。由于 paste 自动创建的分隔符是空格,我们必须将分隔符(sep)重写为空白,这通过添加引号来实现。
自动逐个导入文件
从现在开始,为了简单起见,我将假设文件存在于工作目录中。现在,我们需要创建一个循环,允许文件自动导入并设置为动态变量。
创建变量名
首先,我们需要动态创建我们的变量。我们将使用文件名来创建变量;然而,我们需要从文件名中移除文件扩展名。同样,我使用“file”作为文件名的占位符。如果你想从文件名中移除前缀,也可以使用相同的思路。
#Formula
name <- gsub(".<file extension>", "", file)
#Example for Excel file
name <- gsub(".xlsx","", file)
分配变量
由于我们已经创建了导入代码,让我们创建代码来分配变量。虽然使用“=”或“<-”符号分配变量更容易,但我们不能用这些符号来处理动态变量名。相反,我们将使用 R 中的 assign 函数。
assign(name, df)
创建 for 循环
我们终于有了所有的组件来创建一个 for 循环,以自动导入所有的文件。我们需要做的就是将上述代码添加到一个将循环遍历我们filelist变量的 for 循环中。
# Formula
for (file in filelist){
name <- gsub(".<file extension>", "", file)
df <- <read function>(file)
assign(name, df)
}
#Example for Excel files
for (file in filelist) {
name <- gsub(".xlsx", "", file)
df <- read_excel(file)
assign(name, df)
}
现在,你可以轻松添加你的文件,但如果你想简单地将所有这些文件合并成一个文件呢?
自动导入和合并文件
许多时候,你可能有多个文件,它们是相同信息的简单片段。你真的想将它们合并并使用单一的数据框。因此,我们现在将创建一个 for 循环,仅输出一个将所有行合并到一起的数据框。为此,我们将使用 rbind() 函数将行绑定到单一的数据框中。
首先,我们需要创建一个空的数据框,以便将新文件添加到其中。然后,我们可以使用 for 循环将它们导入并绑定到这个数据框中。
#Formula
<dataframe name> <- data.frame()
for (file in filelist){
df <- <read function>(file)
<dataframe name> <- rbind(<dataframe name>, df)
}
#Example with SA dataframe and excel files
SA <- data.frame()
for (file in filelist){
df <- read_excel(file)
SA <- rbind(SA, df)
}
运行示例代码将生成一个名为“SA”的数据框,其中包含filelist中所有文件的数据。
创建文件列
有时,数据来自的特定文件包含重要信息。例如,有人可能会给你一些数据,这些数据有特定的日期范围或重要的会议信息,这对数据分析很重要。如果你只是简单地合并每个文件中的数据,你将不知道这些数据来自哪个文件。
因此,我们想要创建一个动态列,在绑定数据之前添加每个文件的信息。我们将使用之前创建的相同名称变量来捕获文件名信息,并将其作为数据集中的一列。
#Formula
<dataframe name> <- data.frame()
for (file in filelist){
name <- gsub(".<file extension>", "", file)
df <- <read function>(file)
df$<column name for file name> <- name
<dataframe name> <- rbind(<dataframe name>, df)
}
#Example with SA dataframe and excel files
SA <- data.frame()
for (file in filelist){
name <- gsub(".xlsx", "", file)
df <- read_excel(file)
df$filename <- name
SA <- rbind(SA, df)
}
现在,你可以使用上述的 for 循环轻松导入和合并多个文件,而不会丢失源文件的信息。
总体而言,自动导入文件的能力将节省大量编码时间,你可以让计算机利用计算时间来自动管理多个文件。
如何避免被模型准确度欺骗
原文:
towardsdatascience.com/how-to-avoid-being-fooled-by-model-accuracy-e26307385fe1
二分类模型指标及其正确使用的视觉指南
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 7 月 7 日
--

作者提供的图片:由 Midjourney 生成
背景 — 表面看起来简单
用于衡量分类模型性能的指标从数学角度来看相当简单。然而,我发现许多模型构建者和数据科学家在阐述这些指标时遇到困难,甚至应用不当。这个错误很容易出现,因为这些指标在表面上看似简单,但其影响可能在不同问题领域中非常深远。
本文作为一个视觉指南,解释了常见分类模型指标。我们将探讨定义,并通过实例突出指标使用不当的情况。
关于可视化的简要说明
每个可视化包含九十个主题,代表我们可能希望分类的任何事物。蓝色主题表示负样本,而红色表示正样本。紫色框是模型,试图预测正样本。框内的任何内容都是模型预测为正的部分。
在澄清这些问题后,让我们深入探讨定义。
精确度与召回率
对于许多分类任务,精确度和召回率之间存在权衡。优化召回率往往会对精确度产生影响。那么这些术语究竟意味着什么呢?让我们先从数学定义开始,然后转到视觉表示。
精确度 = TP / (TP + FP)
召回率 = TP / (TP + FN)
其中 TP = 真阳性数量,FP = 假阳性数量,FN = 假阴性数量。
让我们专注于下面的图表,其中有四个正样本。记住,模型的正预测由图表上的框表示。观察图表,我们看到模型正确预测了所有四个正样本——我们可以看到所有正样本都位于框内。我们可以通过计算框内的正样本数量(TP = 4)除以正样本的总数(TP = 4 + FN = 0)来计算模型召回率。
请注意,FN 为 0,因为框外没有正样本。

作者提供的图像:模型的可视化表示,具有 100%的召回率和 40%的准确率。模型由紫色框表示。
准确率可以类似解释。它只是框内正样本的数量(TP=4)除以框内样本的总数(TP = 4 + FP = 6)。一个简单的计算显示,模型的准确率仅为 40%。
你可以观察到,模型可以具有高召回率但低准确率,反之亦然。下面的图表显示了这一点,其中召回率仅为 50%,而准确率为 100%。看看你是否能理解这些数字的来源。
这里有个提示帮助你,假阴性数量是 2,因为有两个正样本在框外。

作者提供的图像:模型的可视化表示,具有 100%的准确率和 50%的召回率。模型以紫色框表示。
假阳性率和真负率
假阳性率(FPR)也许显得更直观,可能是由于其名称。然而,让我们以与其他指标相同的方式探讨这个概念。从数学上讲,我们将 FPR 表示如下:
FPR = FP/(FP + TN)
这里,TN 表示真正负样本的数量。
再次检查第一个图像,可以通过查看框内负样本的数量(FP=6)除以负样本的总数(FP=6 + TN=80)来确定 FPR。在第一个图像中,假阳性率为 7%,而在第二个图像中为 0%。试着找出为什么会这样。
记住,框内的样本是模型 预测 为正的样本。所以,框外的负样本就是模型识别为负的样本。
真负率(TNR)可以使用以下公式计算:
TNR = TN/(TN + FP)
请注意,TNR 总是等于假阳性率的 1 减去。
准确率
准确率是一个在模型性能的上下文中被广泛提及的术语,但它到底是什么意思呢?让我们从数学定义开始:
准确率 = (TP + TN) / (TP + TN + FP + FN)
运用之前的逻辑,我们可以计算出第一个图像的模型准确率为 93%,第二个图像的准确率为 97%(你可以自己推导出这个结果)。这可能会引发你的疑问:为什么在某些情况下准确率可能是一个具有欺骗性的指标。我们将在接下来的内容中更详细地探讨这个问题。
正确使用指标
我们为什么要关注这些指标?因为它们为我们提供了评估模型性能的方法。一旦我们理解了这些指标,我们甚至可以确定与模型相关的商业价值。这就是为什么对这些指标的适当(和不适当)使用有良好的直觉是重要的。为了说明这一点,我们将简要探讨分类任务中的两种常见场景,即平衡数据集和不平衡数据集。
不平衡数据集
之前描述的图表是不平衡分类任务的实例。简单来说,不平衡任务中正样本的表现远低于负样本。许多商业二分类的应用场景,如信用卡欺诈检测、客户流失预测、垃圾邮件过滤等,都属于这一类。选择不适合的不平衡分类指标可能会让你对模型性能有过于乐观的信念。
不平衡分类的主要问题在于真正负样本的数量可能很高,而假阴样本的数量很低。为了说明这一点,让我们考虑另一个模型,并在我们的不平衡数据上进行评估。我们可以创建一个极端的场景,其中模型简单地将每个样本都预测为负样本。

作者图片:对每个样本在不平衡数据集上预测为负样本的“非区分性”模型的可视化表示
让我们计算这个场景中的每个指标。
-
准确率: (TP=0 + TN=86)/(TP=0 + TN=86 + FP=0 + FN=4) = 95%
-
精确率: (TP=0) /(TP=0 + FP=0) = 未定义
-
召回率: (TP=0) / (TP=0 + FN=4) = 0%
-
假阳性率(FPR): (FP=0) / (FP=0 + TN=86) = 0%
-
真正负样本率(TNR): (TN=86) / (TN=86 + FP=0) = 100%
准确率、假阳性率和真正负样本率的问题应当开始变得更加明显。当我们处理不平衡数据集时,可能会产生一个高准确率但在实际应用中表现较差的模型。在之前的例子中,模型没有检测正样本的能力,但仍然获得了 95%的准确率、0%的假阳性率和完美的真正负样本率。
现在,想象一下将这样的模型用于医学诊断或欺诈检测;显然,它将是无用的,甚至可能是危险的。这个极端的例子说明了使用准确率、假阳性率和真正负样本率等指标来评估在不平衡数据上工作的模型性能的问题。
平衡数据集
对于平衡分类问题,潜在的真正负样本数量明显少于不平衡情况。

作者提供的图像:在平衡数据集上,“非区分性”模型对每个样本预测负类的可视化表示
如果我们将“非区分性”模型应用于平衡数据集,我们会得到以下结果:
-
准确率: (TP=0 + TN=45) / (TP=0 + TN=45 + FP=0 + FN=45) = 50%
-
精准度: (TP=0) / (TP=0 + FP=0) = 未定义
-
召回率: (TP=0) / (TP=0 + FN=45) = 0%
-
假阳性率: (FP=0) / (FP=0 + TN=45) = 0%
-
特异度: (TN=45) / (TN=45 + FP=0) = 100%
尽管其他所有指标保持不变,但模型的准确率已下降至 50%,这更能反映模型的实际表现。尽管如此,没有精准度和召回率,准确率仍然具有误导性。
ROC 曲线与精准度-召回率曲线
ROC 曲线是评估二分类模型性能的一种常见方法。然而,当处理不平衡数据集时,它们也可能提供过于乐观且不完全有意义的结果。
ROC 和精准度-召回率曲线的简要概述:我们本质上是对不同决策阈值下的分类指标进行对比。我们通常测量曲线下的面积(或 AUC),以给出模型性能的指示。点击链接了解更多关于* [ROC](https://developers.google.com/machine-learning/crash-course/classification/roc-and-auc#:~:text=An ROC curve (receiver operating,False Positive Rate) 和 精准度-召回率曲线的信息。
为了说明 ROC 曲线如何可能过于乐观,我在 Kaggle 上的信用卡欺诈数据集上构建了一个分类模型。该数据集包含 284,807 笔交易,其中 492 笔为欺诈交易。
注:数据可以用于商业和非商业目的,无需许可,详细信息请参见 开放数据公共许可证 。

作者提供的图像:不平衡数据集上的 ROC 曲线
在检查 ROC 曲线时,我们可能会被误导认为模型的表现优于实际情况,因为这条曲线下的面积为 0.97。正如我们之前看到的,假阳性率对于不平衡的分类问题可能过于乐观。

作者提供的图像:不平衡数据集上的精准度-召回率曲线
更加稳健的方法是使用精准度-召回率曲线。这提供了对模型性能的更稳健的估计。这里我们可以看到,精准度-召回率曲线下的面积(AUC-PR)更加保守,为 0.71。
采用一个平衡版本的数据集,其中欺诈交易和非欺诈交易的比例为 50:50,我们可以看到 AUC 和 AUC-PR 更加接*。

作者提供的图片:平衡数据集上的 ROC 曲线

作者提供的图片:平衡数据集上的精确度-召回曲线
用于生成这些图表的笔记本在我的 GitHub repo 中可以找到。
有方法可以提升分类模型在不平衡数据集上的表现,我在关于合成数据的文章中探讨了这些方法。
调查合成数据提升模型性能在不平衡数据集上的能力
towardsdatascience.com
结论
理解分类模型的度量指标超越了数学公式。你还应了解每个指标的使用方式,以及它们在平衡和不平衡数据集上的影响。作为一个经验法则,基于真阴性或假阴性计算的指标在应用于不平衡数据集时可能过于乐观。希望这次视觉之旅能给你更多的直观感受。
我发现这种视觉解释对于向我的非技术利益相关者阐述方法非常有用。随意分享或借鉴这种方法。
关注我在 LinkedIn
订阅 Medium 以获取更多我的见解:
[## 使用我的推荐链接加入 Medium — John Adeojo
我分享数据科学项目、经验和专业知识,帮助你在旅途中前进。你可以通过…注册 Medium。
johnadeojo.medium.com](https://johnadeojo.medium.com/membership?source=post_page-----e26307385fe1--------------------------------)
如果你对将 AI 或数据科学整合到业务运营中感兴趣,我们邀请你安排一次免费的初步咨询:
通过免费的咨询了解我们在帮助企业实现雄心勃勃的目标方面的专业知识。我们的数据科学家和…
www.data-centric-solutions.com](https://www.data-centric-solutions.com/book-online?source=post_page-----e26307385fe1--------------------------------)
如何避免 Google BigQuery / SQL 中的五个常见错误
原文:
towardsdatascience.com/how-to-avoid-five-common-mistakes-in-google-bigquery-sql-6fafab396d88
在使用 BigQuery 多年的过程中,我观察到即使是经验丰富的数据科学家也常常犯的 5 个问题
·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 10 月 25 日
--

Google BigQuery 受欢迎有很多原因。它速度极快,易于使用,提供完整的 GCP 套件,照顾好你的数据,并确保早期发现错误。更重要的是,你可以使用标准 SQL 和一些非常不错的内置函数。简而言之,它几乎是一个完整的套餐!
始终假设有错误和重复项,始终如此!
然而,与其他网络服务和编程语言类似,在使用 BigQuery 时,有一些事项需要了解,以避免陷入陷阱。多年来,我自己犯了很多错误,并意识到几乎所有我认识的人在某些时候都会遇到相同的问题。我在这里想指出其中的一些问题,因为我在职业生涯中发现这些问题较晚,并且也看到其他经验丰富的数据科学家遇到相同的问题。
因此,我将提供我认为几乎所有人都会在某个时候在 BigQuery 中犯的潜在错误的前 5 名列表,并且有些错误可能甚至不知道。务必避免这些错误,因为每一点都可能导致严重后果,并记住在处理数据时要有正确的态度:始终假设有错误和重复项,始终如此!
1. 使用 “NOT IN” 时要小心
这发生得很快。你很匆忙,想快速检查两个表,看看一个表中提到的某个项目是否也存在于第二个表中。既然 NOT IN 语句听起来如此直观,为什么不使用它呢?
问题是当表中有NULL值时,NOT IN不会按预期工作。如果是这样,你将得不到想要的结果!
看看这个代码示例,我只是尝试找出input_2中不在input_1中的类别:
WITH
input_1 AS (
SELECT
category
FROM (
SELECT
["a", "b", CAST(NULL AS STRING), "d"] AS category),
UNNEST(category) category ),
input_2 AS (
SELECT
category
FROM (
SELECT
["a", "b", "c", "d"] AS category),
UNNEST(category) category )
SELECT
*
FROM
input_2
WHERE
input_2.category NOT IN (
SELECT
category
FROM
input_1)
注意input_1单元格没有c类别,但这个查询的结果将是没有数据可显示。
为了克服这个问题,请确保使用LEFT JOIN或删除所有NULL值,如稍微调整的代码所示(注意最后一行):
WITH
input_1 AS (
SELECT
category
FROM (
SELECT
["a", "b", CAST(NULL AS STRING), "d"] AS category),
UNNEST(category) category ),
input_2 AS (
SELECT
category
FROM (
SELECT
["a", "b", "c", "d"] AS category),
UNNEST(category) category )
SELECT
*
FROM
input_2
WHERE
input_2.category NOT IN (
SELECT
category
FROM
input_1
WHERE category IS NOT NULL)
2. 使用“UNNEST”数据时你可能会丢失行
当你处理 BigData 时,嵌套列是必备的。它们将节省费用并保持数据组织有序。此外,有时将数据嵌套是根据分区的特定顺序限制数据的一个方便步骤。
然而,在展开数据时,要注意你可能会丢失数据!当在CROSS JOIN中使用UNNEST时,你将丢失所有嵌套列中没有数据的行。
查看这个例子,我们定义了 2 个不同的 ID,但只有一个在嵌套列中有数据:
WITH
input_1 AS (
SELECT 1 as id, ['phone', 'car', 'paper'] as data
UNION ALL
SELECT 2 as id, [] as data
)
SELECT
id,
element
FROM input_1,
UNNEST(data) as element
运行此代码时,你将看到由于CROSS JOIN,ID 号 2 将丢失。这可能对你的数据产生严重影响,因为你实际上是将该 ID 从后续分析中过滤掉了。
一种改进方法是使用LEFT JOIN代替下面代码中的CROSS JOIN(请注意最后一行):
WITH
input_1 AS (
SELECT 1 as id, ['phone', 'car', 'paper'] as data
UNION ALL
SELECT 2 as id, [] as data
)
SELECT
id,
element
FROM input_1
LEFT JOIN UNNEST(data) as element
3. BigQuery ML 中的成本往往高于估算
估算成本很重要。当使用 BigQuery 时,你可能在处理大数据(因为 BigQuery 的强项就在于此)。BigQuery 的一个直观特点是查询成本会自动估算,并且根据估算的分区修剪提供最大成本。
例如,当成本估算为 1 TB 时,你可以预期查询费用为 6.25 美元或更少(因为 6.25 美元/ TB 是按需定价模式;使用聚类时,你甚至可以进一步减少使用的数据量)。

图 1:基于芝加哥出租车旅行数据集的模型训练估算。当训练模型时,它实际上将计费 51.6 GB。(作者提供的图像)
然而,BigQuery ML 是不同的!我想在这里指出两点:
-
它有不同的定价模式,例如线性回归相当昂贵(312.5 美元/ TB)。
-
处理成本取决于底层的 Vertex AI 训练成本。不幸的是,在运行查询之前,BigQuery 无法告诉你透传的 Vertex AI 成本将是多少。所以你可能会支付比估计的多得多的费用。
让我们通过一个例子来突出第 2 点。在这里,我尝试基于天和小时来预测旅行长度,使用的是公开的芝加哥出租车旅行数据集:
CREATE MODEL your_model
OPTIONS(
MODEL_TYPE='BOOSTED_TREE_REGRESSOR',
BOOSTER_TYPE = 'GBTREE',
NUM_PARALLEL_TREE = 5,
MAX_ITERATIONS = 5,
TREE_METHOD = 'HIST',
EARLY_STOP = FALSE,
SUBSAMPLE = 0.85,
INPUT_LABEL_COLS = ['trip_miles']
) AS
SELECT
EXTRACT(hour
FROM
trip_start_timestamp) hour,
EXTRACT(dayofweek
FROM
trip_start_timestamp) weekday,
trip_miles
FROM
`bigquery-public-data.chicago_taxi_trips.taxi_trips`
WHERE
trip_miles IS NOT NULL;
在估算处理成本时,它估计为 3.12 GB。但请注意估算中的 (ML)(见图 1)。因为这提示你可能会变得更昂贵。事实上,它确实如此。训练此模型的结果是账单为 51.63 GB,因此比原始估算贵了大约 17 倍。
4. 在生产环境中并行化作业时避免使用 MERGE 语句
更新现有表是生产流水线中的常见情况。这是因为你不断获取新数据需要追加,但有时你也希望历史变化能够反映在你的表中。
也就是说,拥有一个可扩展的更新表的设置很重要。一个流行的选择是使用 MERGE 语句。它们允许你根据特定条件更新或插入数据。它们真正强大的地方在于,因为 MERGE 语句允许你根据定义的条件只更新一行匹配的数据,从而防止引入重复数据。
然而,有一个问题。MERGE 语句不允许很好的并行化。以下是一个示例查询,尝试将数据合并到现有表中。当你并行运行 MERGE 语句时,你会遇到一些问题。
首先,我们创建一个虚拟表,我们希望将数据合并到其中:
CREATE OR REPLACE TABLE
your_project.your_dataset.merge_test (
local_date date,
value INT64 )
PARTITION BY
local_date;
然后我们并行运行这个查询,它只向虚拟表中插入或更新新的日期:
MERGE
your_project.your_dataset.merge_test o
USING
(
SELECT
date local_date,
1 value
FROM
UNNEST(GENERATE_DATE_ARRAY("2022-01-01", "2023-12-31", INTERVAL 1 day)) date
) n
ON
n.local_date = o.local_date
WHEN MATCHED THEN UPDATE SET value = o.value
WHEN NOT MATCHED THEN INSERT VALUES (local_date, value)
当并行运行时,你会观察到两个问题:
-
当在同一分区上使用
UPDATE语句时,你会遇到一个错误消息。错误消息如下:由于并发更新,无法序列化对表 your_project.your_dataset.merge_test 的访问。 -
你还会观察到,稍后触发的查询在 挂起 状态中停留了很长时间(几秒钟),而不是 运行 状态。
尽管 MERGE 语句非常有用且强大,但请记住,在一个设计用于将数据并行化到同一表中的生产环境中时,查询要么由于更新中的分区冲突而崩溃,要么当并行运行超过 2 个任务时,查询将处于 挂起 状态,从而失去了并行化的好处。
避免这种情况的一种方法是使用 DELETE / INSERT 语句或创建临时表,稍后将它们批量合并在一起。
5. 分区和集群临时表
临时(或 TEMP)表在构建包含多个子查询的复杂查询时非常有用。当你处理大数据并希望一次加载数据然后在查询的后续部分多次使用时,它们也很方便。所以,当你遇到可能的超时问题时,查询运行时间过长,它们可能是你所寻找的解决方案。
然而,用户通常只看到创建一个在整个查询运行时会被物化的临时表的好处。但这并不是全部,因为类似于其他所有物化表,TEMP 表也可以进行分区和聚类。这提供了额外的优势,可以减少查询成本,并改善运行时(特别是在处理非常大的数据时)。
附带的查询并不是非常有用,但它展示了这个概念。该查询基于公开的芝加哥出租车行程数据集创建了两个TEMP 表,其中一个使用完整的数据集,另一个仅使用该数据集的一部分。在最后的查询中,这两个表被连接在一起,以筛选出子集。由于TEMP 表是分区和聚类的,实际将这两个临时表连接在一起的查询只会计费 1GB,而不进行分区时则会计费 36GB:
CREATE TEMP TABLE trips
PARTITION BY
trip_start_date
CLUSTER BY
unique_key AS
SELECT
unique_key,
taxi_id,
DATE(trip_start_timestamp) trip_start_date,
trip_total
FROM
`bigquery-public-data.chicago_taxi_trips.taxi_trips`;
CREATE TEMP TABLE trips_filtered
PARTITION BY
trip_start_date
CLUSTER BY
unique_key AS
SELECT
unique_key,
taxi_id,
DATE(trip_start_timestamp) trip_start_date,
trip_total
FROM
`bigquery-public-data.chicago_taxi_trips.taxi_trips`
WHERE
EXTRACT(year
FROM
trip_start_timestamp) = 2022
AND SUBSTR(taxi_id, -1, 1) = "b";
SELECT
DISTINCT taxi_id
FROM
trips
JOIN
trips_filtered
USING
(unique_key,
taxi_id,
trip_start_date)
此外,在处理大型数据集并进行空间索引时,在聚类和分区的TEMP 表上执行连接操作也会改善运行时间。
总结
我们都会犯错,这是一件好事,只要我们从中学*。这篇故事中提到的 5 个问题对我来说是最重要的,因为它们并不是很知名,而且初学者遇到的机会很少。很少有人考虑这些问题,但随着经验的增长,这 5 个问题中的某些可能会以负面方式影响数据或生产环境。所以请注意这些问题,并记住在处理数据时的态度:总是假设存在错误和重复,永远如此!
如何避免在自学数据分析时犯这 4 个耗时错误
从这些错误中学*将帮助你在自学数据分析时避免浪费时间
·
关注 发表在Towards Data Science ·7 分钟阅读·2023 年 2 月 10 日
--
图片由Dan Cristian Pădureț提供,来源于Unsplash
如果你曾尝试自学某些东西,你知道这通常会花费你非常长的时间。特别是当涉及到自学数据分析时,这一点尤为真实。
从编码到数学再到数据可视化,数据分析涵盖了三种非常不同的技能,每种技能都需要各种学*方法。不仅如此,容易被 YouTube 教程和数据分析文章所吸引,可能会导致你花费比最初预期更多的时间学*。
在学*数据分析过程中,我犯了不少错误,下面列出了其中的主要错误,并提供了一些实用的方法来避免这些错误,你可以立即开始实施。只要你知道何时动手(可以这么说),何时使用你已经知道的工具而不是学*新工具,何时专注于学*你需要的技能,何时坚持完成学*数据分析的目标,学*数据分析不必是一个漫长的过程。
每次学到新知识时都将其应用到现有项目中
错误: 坐在编码教程的“炼狱”中,认为自己知道如何编码却实际上并不真正会编码。
解决方法: 避免在编码教程的“炼狱”中浪费时间的最简单方法是每次学到新知识时都将其应用到现有项目中。
在大学学*软件开发之前,我尝试过多次通过 Codecademy 和 YouTube 的在线教程自学编码。不幸的是,我只是把所有时间花在了做教程上,而没有真正动手(可以这么说)使用我学到的东西创建一些东西。我浪费了很多时间没有将理论知识应用于实践,导致缺乏实际成果。
学*数据分析的编码部分可能是最动手的部分,需要立即将通过教程练*的内容应用于实践。例如,在编写完整个分析程序后,你可以观看一个有关如何将代码分组到函数中的教程。然后,你应该进入项目代码,创建能组织和简化代码的函数。
这种做法不仅能让你更快地学*,还能让你更快地完成项目。例如,你可能只是为了完成一个单一项目而学*数据分析。与其等到你学会了 Python 编码的所有内容再开始你的项目,不如在学*不同技能或技术的过程中逐步推进你的项目。
例如,我需要学*数据分析的某些方面,以完成我正在进行的一些本科研究。然而,由于有一个完成研究的截止日期,并且我需要最终毕业,我需要迅速掌握需要添加到代码中的内容,并立即实现它。
记住:你不需要深入的知识来完成数据分析,你只需要具备了解何时需要做某事以及何时不需要做的广泛知识。
使用 Excel 进行数据清理(以及你能想到的其他任何事情)
错误: 认为我必须使用特殊工具来完成数据清理,这意味着我花在清理上的时间减少了,学*的时间增加了。
解决办法: 尽可能多地使用 Excel 来完成所有的数据分析任务。
社交媒体让人觉得你需要精通多种编程语言、工具和平台才能成为数据分析师。实际上,许多数据分析师仅使用 Excel 就能很好地完成工作。我不确定 Excel 什么时候变得不流行,但我要说的是,你应该将 Excel 用于所有的数据清理,以及你能想到的基本上任何其他事情。
Excel 仍然是一个强大的数据处理工具,并且由于大量的在线资源,它是最容易学*的工具之一。
当我刚开始学*数据分析时,我浪费了很多时间在学*各种新工具、编码技巧和数据库技巧上。相反,如果我从一开始就开始使用 Excel,我本可以立即开始生成干净的数据甚至完整的分析。
我想传达的理念是,如果你知道某种方法并且它有效,就继续使用它。是的,你的职业生涯中可能会有需要超越 Excel 的时候,但目前,不要浪费时间学*如何进行数据分析——相反,使用你已经知道的工具进行更多的数据分析!
专注于线性代数、概率和统计
错误: 认为我需要了解多变量微积分和离散数学才能进行数据分析。
解决办法: 只学*完成分析所需的数学知识——很可能你已经掌握了所有必要的知识。
关于自学数据分析的最大误解之一是你需要了解高级数学形式。这时术语的使用就很重要。有些人会混淆或故意将“数据科学”和“数据分析”这两个术语互换,然后以数据科学为基础来描述这一学科。实际上,这两者完全不同。如果你想做数据科学,那么是的,你需要掌握高级数学形式。然而,如果你想进行数据分析,你可以使用更简单的数学形式。
你可以通过线性代数、概率和统计(可能还需要一些单变量微积分)来完成大部分数据分析任务。
通过专注于完成分析所需的数学,你将节省原本浪费在自学复杂数学概念上的时间,这些概念可能永远用不到。最好的方法是查看项目的所有数学要求,只学*你缺乏的部分。这将把你的学*时间从几个月缩短到仅几周。此外,这让你能够专注于知识的广度而非深度。记住,知识的广度可以很快掌握并立即应用,而知识的深度则可能需要几个月或几年才能发展。
制定一个有截止日期的学*计划,专注于正确的目标。
错误: 把学*部分当作无尽的时间线,而不是设定明确的截止日期以产生具体结果。
解决方案: 制定一个有明确截止日期的学*计划,专注于实现特定目标或短期目标列表。
还记得你在学校时,老师在学期开始时给你一个包含课程学*目标和你需要学*的所有内容的大纲吗?这些是宝贵的文件,应成为我们制定数据分析学*计划的灵感来源。
你需要制定自己的教学大纲,设定一个有明确截止日期和清晰目标的计划。你不想沉迷于学*中,三年后却发现自己不太确定刚学到的东西是什么以及它是否相关。
例如,我将进行一些本科研究,需要进行数据分析。数据分析只是项目的一小部分,这意味着我需要为需要自学的部分制定学*计划,同时考虑到我需要完成的其他项目部分。换句话说,我不能把全部时间都花在自学数据分析概念上,因为还有一长串其他事情需要完成。此外,学*计划需要有几个明确的目标,一旦达成,就可以开始将技能应用到项目中。正如我之前所说的,我不想陷入仅学*的陷阱而发现自己刚学到的知识可能不是最有效的。
诀窍是制定一个有明确截止日期和三个清晰目标的学*计划。这些目标应该与实际技能相关,或是完成一个项目,或是数据分析工作。目标必须如此设置,以便在完成一些学*后,你可以评估是否达到了目标,或者是否需要进一步学*。最终目标是能够在测试场景中自信地回答“是”。
这种方法不仅能帮助你保持学*进度,还能避免分心、偏离目标、学*错误的内容或花费数年时间来完成本可以在几周内完成的任务。
订阅以便将我的故事直接发送到你的邮箱:故事订阅
请成为会员,通过我的推荐链接无限访问 Medium(对你没有额外费用,我将获得少量佣金):Medium 会员
通过捐赠支持我的写作,以资助创作更多类似的故事:捐赠
如何成为更好的商业数据科学家
原文:
towardsdatascience.com/how-to-be-a-better-business-data-scientist-4e0c597088f
意见
数据科学不仅仅是关于算法的。这里有一些关于如何将业务方面融入其中的建议。
·发表于 Towards Data Science ·6 分钟阅读·2023 年 2 月 1 日
--

照片由 LinkedIn Sales Solutions 提供,来源于 Unsplash [1]。
目录
-
介绍
-
机会、问题陈述和利益相关者
-
KPI 和测试
-
摘要
-
参考文献
介绍
虽然在你的分类模型测试集中获得 90%的准确率感觉非常棒,但你仍然需要为在业务中使用该模型进行辩护。数据科学教育中很大一部分集中在许多方面,如流行算法、特征工程和超参数调优等。虽然这些是数据科学中极其重要的部分,但也应关注如何将数据融入业务中。我们将努力深入了解机会、问题陈述、利益相关者、KPI 和测试,而不是讨论那些更传统的部分。
机会、问题陈述和利益相关者

照片由 Evan Dennis 提供,来源于 Unsplash [2]。
机会
根据你工作的地方,你可能会被分配到一个特定的机会,或者你需要自己寻找一个。一般来说,你已经在公司的某个领域内,可以使用数据分析工具来隔离出业务需要改进的关键领域。数据科学算法可能并不是唯一的答案或完整的解决方案。
这里有一些关于数据科学机会的建议:
尝试在未识别机会或问题时不要寻找解决方案,以免在这个初步范围内限制自己。
-
理解业务并尽可能自己使用它。
-
了解客户或内部用户经历的常见痛点,并且最常出现的痛点。
-
创建一个视觉图,例如一个仪表盘,显示按最少的 X KPI(关键绩效指标)和最多的 Y 人口分组,以便轻松识别并创建一个加权的新指标,并跟踪这个权重随时间的变化,以查看是否一直存在痛点问题。
例如,在像 Tableau 或 Looker 这样的工具中开发一个热图,显示一个州内客户帮助咨询最多的县,以及周环比的变化百分比,以查看趋势如何随着时间的发展。这种分析可以进一步拆分并持续时间更长,具体取决于你的业务和产品。
在这个分析中,也许你会注意到某个县在其表现上挣扎。
问题陈述。
现在你已经获得了一些识别问题的技巧,你可以简洁地创建一个问题陈述,整个项目将最终围绕此陈述展开。
这里是一个糟糕问题陈述的示例及其原因:
该州有一些县存在一些问题,可能需要数据科学帮助,比如决策树分类模型。
-
太长且令人困惑。
-
不是每个人都知道什么是决策树,或者分类,或者模型。
-
其中一些词汇与其他公司术语重叠。
-
问题 实际上并没有被定义。
-
数据科学不一定总是完整的解决方案。
这里是一个好的问题陈述的示例及其原因:
100 个县中有 10 个县占据了 80%的客户帮助咨询。
-
我们可以清楚地看到,特定地点的客户帮助咨询情况很差。
-
我们可以看到,大多数客户帮助咨询集中在 10 个县内。
总体来说,最好不要从一开始就过于复杂,因为这样会立即限制可能的解决方案。
利益相关者。
现在你已经识别了机会并制定了问题陈述,你需要更好地了解你的受众。通常,这个受众被称为你的利益相关者(s),即与你合作以以易于理解的方式获取技术信息和术语的人。
你需要在与他们会面之前进行以下头脑风暴:
-
他们之前是否与数据科学家合作过?
-
…他们是否熟悉数据科学术语?
-
总结你的能力,并不是所有的数据科学家都是高级 SQL 专家。
KPI 和测试。

照片由UX Indonesia拍摄,来源于Unsplash [3]。
KPI。
首先,什么是 KPI?我在上面已经定义过了,但就像在商业世界中一样,重新解释缩写是可以的,也很可能是更好的。话虽如此,KPI 是关键绩效指标。
即使拼写出来,它仍然不完全清楚其含义,特别是如果你对商业领域不太了解的话。
思考 KPI 的一种简单方法是将其视为我们做决策时依据的指标,因此,我们可以称之为决策指标。还有一些次要指标也值得跟踪,但它们不是我们决策的基础,实际上可能会下降,只要主要 KPI 有所改善。
这里是一些可能的 KPI 示例,针对上面定义的相同问题陈述^:
-
每个县的客户帮助询问
-
总客户帮助询问的百分比(县份数/总数)
-
客户购买数量
… 还有一些指标是有帮助的,但不是可能决策的原因:
-
县内人数统计
-
每个县的其他联系原因及其占总数的百分比
-
客户购买细分统计
测试
现在我们有了三个主要的 KPI(再次说明,这只是一个例子,真正重要的是了解流程,因为你的业务和问题是不同的),我们可以例如用 AB 测试来跟踪它们。
随着时间的推移,我们希望跟踪这三个指标,以观察我们的解决方案是否改善了问题。请注意,我已经有一段时间没有提到数据科学或算法——这种情况可能会发生在你的业务中,因为利益相关者和其他人主要关心的是影响,而不是使用的方法,在这种情况下即数据科学。
总结
这个机会发现、问题陈述定义、利益相关者协作、KPI 识别和测试的流程可以适用于很多企业,无论解决方案是否以数据科学为导向。然而,了解这个流程是很重要的,因为它相当常见,可以帮助你以高效和有帮助的方式将数据科学解决方案应用到你的公司中。
总结一下,作为数据科学家,以下是你要留意的典型流程的五个主要步骤,以便成为一个更具商业导向的数据科学家:
-
机会发现
-
问题陈述定义
-
利益相关者协作
-
KPI 识别
-
测试
我希望你觉得我的文章既有趣又有用。如果你对这篇关于商业导向数据科学的解释有任何意见或不同看法,请随时在下方评论。为什么或为什么不?你认为还有哪些其他话题需要更多讨论?这些问题可以进一步澄清,但我希望我能够阐明数据科学与商业如何协同工作。
我与这些公司没有任何关系。
请随时查看我的个人资料, Matt Przybyla,以及其他文章,若要订阅以接收我的博客的电子邮件通知,请访问以下链接,或 点击屏幕顶部关注图标旁的订阅图标,如有任何问题或意见,请在 LinkedIn 上联系我。
订阅链接: datascience2.medium.com/subscribe
推荐链接: datascience2.medium.com/membership
(如果你在 Medium 上注册会员,我将获得佣金)
参考文献
[1] 照片由 LinkedIn Sales Solutions 提供,来自 Unsplash,(2020)
[2] 照片由 Evan Dennis 提供,来自 Unsplash,(2020)
[3] 照片由 UX Indonesia 提供,来自 Unsplash,(2020)
如何成为数据工程师
原文:
towardsdatascience.com/how-to-become-a-data-engineer-c0319cb226c2
2024 年初学者的捷径
·发布在数据科学前沿 ·17 分钟阅读·2023 年 10 月 7 日
--

图片由Gabriel Vasiliu提供,Unsplash上发布
这个故事解释了一种加速进入数据工程角色的方法,通过学*所需的技能并熟悉数据工程工具和技术。这对初级 IT 从业者和希望转行的中级软件工程师将非常有用。通过我作为英国和中东地区最成功初创公司的数据工程主管的多年经验,我从职业生涯中学到了很多,我希望与您分享这些知识和经验。这是我在数据工程领域获得的个人经验的反映。我希望这对您有用。
数据工程师——角色
首先,为什么选择数据工程师?
数据工程是一个令人兴奋且非常有回报的领域。这是一份迷人的工作,我们有机会处理所有与数据相关的事物——API、数据连接器、数据平台、商业智能以及市场上数十种数据工具。数据工程与机器学*(ML)紧密相关。你将创建和部署各种数据和 ML 管道。
这份工作绝对不会无聊,并且薪资优厚。
这份工作回报丰厚,因为建立一个良好的数据平台并不容易。它从需求收集和设计开始,需要相当的经验。这不是一项简单的任务,也需要一些真正出色的编程技能。工作本身是安全的,因为只要企业产生数据,这份工作就会有很高的需求。
公司总是会聘用那些知道如何高效处理(ETL)数据的人。
数据工程在过去五年里成为了英国增长最快的职业之一,在 2023 年 LinkedIn 的最受欢迎职业榜单中排名第 13 [1]。另一个加入的理由是稀缺性。在 IT 领域,如今找到一个优秀的数据工程师是非常困难的。
作为“数据工程负责人”,我每周在 LinkedIn 上收到 4 个职位面试邀请。平均而言,初级数据工程角色的需求更高。
根据 DICE 的技术职位研究,数据工程师是增长最快的技术职业:

来源:DICE
现代数据栈
现代数据栈指的是一系列数据处理工具和数据平台类型。
你在这个领域吗?
“你在这个领域吗?”——这是我在一次面试中被问到的问题。你会希望能够回答这个问题,并了解相关新闻、首次公开募股、最*的发展、突破、工具和技术。
熟悉常见的数据平台架构类型,即数据湖、湖仓、数据仓库,并准备好回答它们使用哪些工具。查看这篇文章了解一些示例:
数据平台架构类型
它在多大程度上满足了你的业务需求?选择的困境。
数据平台架构类型
数据管道
作为一名数据工程师,你几乎每天都会被要求进行数据管道设计。你会希望熟悉数据管道设计模式,并能够解释何时使用它们。将这些知识应用于实践是至关重要的,因为它决定了使用哪些工具。正确的数据转换工具组合可以使数据管道极其高效。
因此,我们需要准确知道何时应用流数据处理,何时应用批处理。一种可能非常昂贵,而另一种则可能节省数千。然而,业务需求可能在每种情况下都不同。本文提供了全面的数据管道设计模式列表:
数据管道设计模式
选择合适的架构及示例
数据管道设计模式
数据建模
我认为数据建模是数据工程的一个关键部分。许多数据平台的设计方式是将数据“原样”加载到数据仓库解决方案中。这被称为 ELT 方法。数据工程师经常需要使用标准 SQL 方言创建数据转换管道。良好的 SQL 技能是必不可少的。确实,SQL 对于分析查询是自然的,现如今几乎已经成为标准。它有助于高效查询数据,并让所有业务利益相关者轻松使用分析功能。
数据工程师必须知道如何清洗、丰富和更新数据集。例如,使用 MERGE 执行增量更新。在你的工作台或数据仓库(DWH)中运行此 SQL。它解释了它的工作原理:
create temp table last_online as (
select 1 as user_id
, timestamp('2000-10-01 00:00:01') as last_online
)
;
create temp table connection_data (
user_id int64
,timestamp timestamp
)
PARTITION BY DATE(_PARTITIONTIME)
;
insert connection_data (user_id, timestamp)
select 2 as user_id
, timestamp_sub(current_timestamp(),interval 28 hour) as timestamp
union all
select 1 as user_id
, timestamp_sub(current_timestamp(),interval 28 hour) as timestamp
union all
select 1 as user_id
, timestamp_sub(current_timestamp(),interval 20 hour) as timestamp
union all
select 1 as user_id
, timestamp_sub(current_timestamp(),interval 1 hour) as timestamp
;
merge last_online t
using (
select
user_id
, last_online
from
(
select
user_id
, max(timestamp) as last_online
from
connection_data
where
date(_partitiontime) >= date_sub(current_date(), interval 1 day)
group by
user_id
) y
) s
on t.user_id = s.user_id
when matched then
update set last_online = s.last_online, user_id = s.user_id
when not matched then
insert (last_online, user_id) values (last_online, user_id)
;
select * from last_online
;
一些高级 SQL 提示和技巧可以在这里找到:
在 1 到 10 的尺度上,你的数据仓库技能有多好?
towardsdatascience.com
编码
这非常重要,因为数据工程不仅仅涉及数据建模和 SQL。可以将数据工程师视为软件工程师。他们必须具备 ETL/ELT 技术的良好知识,并且至少能够使用 Python 编码。是的,显然 Python 无疑是数据工程最方便的编程语言,但你用 Python 可以做的任何事情,都可以用其他语言轻松完成,比如 JavaScript 或 Java。不要限制自己,你会有时间学*公司选择作为其技术栈主要语言的任何语言。
我建议从数据 API 和请求开始。将这些知识与云服务结合,为未来可能需要的任何 ETL 过程提供了一个很好的基础。
我们不能知道一切,也不要求成为编码专家,但我们必须知道如何处理数据。
以将数据加载到 BigQuery 数据仓库为例。它将使用 BigQuery 客户端库 [5] 将行插入到表中:
from google.cloud import bigquery
...
client = bigquery.Client(credentials=credentials, project=credentials.project_id)
...
def _load_table_from_csv(table_schema, table_name, dataset_id):
'''Loads data into BigQuery table from a CSV file.
! source file must be comma delimited CSV:
transaction_id,user_id,total_cost,dt
1,1,10.99,2023-04-15
blob = """transaction_id,user_id,total_cost,dt\n1,1,10.99,2023-04-15"""
'''
blob = """transaction_id,user_id,total_cost,dt
1,1,10.99,2023-04-15
2,2, 4.99,2023-04-12
4,1, 4.99,2023-04-12
5,1, 5.99,2023-04-14
6,1,15.99,2023-04-14
7,1,55.99,2023-04-14"""
data_file = io.BytesIO(blob.encode())
print(blob)
print(data_file)
table_id = client.dataset(dataset_id).table(table_name)
job_config = bigquery.LoadJobConfig()
schema = create_schema_from_yaml(table_schema)
job_config.schema = schema
job_config.source_format = bigquery.SourceFormat.CSV,
job_config.write_disposition = 'WRITE_APPEND',
job_config.field_delimiter =","
job_config.null_marker ="null",
job_config.skip_leading_rows = 1
load_job = client.load_table_from_file(
data_file,
table_id,
job_config=job_config,
)
load_job.result()
print("Job finished.")
我们不能知道一切,也不要求成为编码专家,但我们必须知道如何处理数据。
我们可以在本地运行它,也可以将其部署到云端作为无服务器应用程序。它可以由我们选择的任何其他服务触发。例如,部署 AWS Lambda 或 GCP Cloud Function 会非常高效。它将轻松处理我们的数据管道事件,几乎不产生成本。我的博客中有很多文章解释了它的简便性和灵活性。
Airflow、Airbyte、Luigi、Hudi……
使用帮助管理数据平台和编排数据管道的第三方框架和库进行实验。许多框架是开源的,如 Apache Hudi [6],并且从不同角度帮助理解数据平台管理。它们中的许多在管理批处理和流处理工作负载方面表现出色。我通过使用它们学到了很多东西。例如,Apache Airflow 提供了很多现成的数据连接器。我们可以使用它们轻松地运行任何云供应商(AWS、GCP、Azure)的 ETL 任务。
使用这些框架创建批处理数据处理作业非常容易。如果我们深入了解,它确实可以使 ETL 的实际操作更加清晰。
例如,我使用 airflow 连接器构建了一个机器学*管道来训练推荐引擎:
"""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)
它将创建一个简单的数据管道图,将数据导出到云存储桶中,然后使用 MLEngineTrainingOperator 训练 ML 模型。

使用 Airflow 进行 ML 模型训练。图片由作者提供。
编排数据管道
框架非常重要,但数据工程师必须知道如何创建自己的框架来编排数据管道。这将我们带回到原始的基础编码以及使用客户端库和 API。在这里,一个好的建议是熟悉数据工具及其 API 端点。通常,创建并部署我们自己的微服务来执行 ETL/ELT 任务更加直观和容易。
使用你自己的工具编排数据管道
例如,我们可以创建一个简单的无服务器应用程序,它将从消息代理(如 SNS)中获取数据。然后,它可以处理这些事件并编排我们创建的其他微服务来执行 ETL 任务。另一个例子是一个简单的 AWS Lambda,它由数据湖中创建的新文件触发,然后根据它从管道配置文件中读取的信息,它可以决定调用哪个服务或将数据加载到哪个表中。
请考虑下面的这个应用程序。它是一个非常简单的 AWS Lambda,可以在本地运行或在云中部署时运行。
./stack
├── deploy.sh # Shell script to deploy the Lambda
├── stack.yaml # Cloudformation template
├── pipeline_manager
| ├── env.json # enviroment variables
│ └── app.py # Application
├── response.json # Lambda response when invoked locally
└── stack.zip # Lambda package
app.py 可以是任何 ETL 任务,我们只需添加一些逻辑,就像在前面的示例中使用几个客户端库将数据加载到 BigQuery 中一样:
# ./pipeline_manager/app.py
def lambda_handler(event, context):
message = 'Hello {} {}!'.format(event['first_name'], event['last_name'])
return {
'message' : message
}
现在我们可以在本地运行它或使用基础设施即代码进行部署。这个命令行脚本将在本地运行此服务:
pip install python-lambda-local
cd stack
python-lambda-local -e pipeline_manager/env.json -f lambda_handler pipeline_manager/app.py event.json
另外,它也可以部署到云中,我们可以从那里调用它。这将我们带入了云环境。
云服务提供商
现在一切都在云端管理。这就是为什么至少学*一种供应商的服务至关重要。可以是 AWS、GCP 或 Azure。它们是领导者,我们希望专注于其中之一。获得云认证是理想的,比如“Google Cloud Professional Data Engineer” [7] 或类似的认证。这些考试很难,但值得获得,因为它提供了数据处理工具的良好概述,并使我们看起来非常可信。我考过一次,已在文章中记录了我的经验,你可以在我的故事中找到。
数据工程师使用云功能和/或 docker 创建的所有内容都可以部署在云中。考虑以下 AWS CloudFormation 堆栈模板。我们可以使用它来部署我们的简单 ETL 微服务:
# stack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS S3 data lake stack.
Parameters:
ServiceName:
Description: Data lake microservice to process data files and load them into __ BigQuery.
Type: String
Default: datalake-manager
StackPackageS3Key:
Type: String
Default: pipeline_manager/stack.zip
Testing:
Type: String
Default: 'false'
AllowedValues: ['true','false']
Environment:
Type: String
Default: 'staging'
AllowedValues: ['staging','live','test']
AppFolder:
Description: app.py file location inside the package, i.e. ./stack/pipeline_manager/app.py.
Type: String
Default: pipeline_manager
LambdaCodeLocation:
Description: Lambda package file location.
Type: String
Default: datalake-lambdas.aws
Resources:
PipelineManagerLambda:
Type: AWS::Lambda::Function
DeletionPolicy: Delete
DependsOn: LambdaPolicy
Properties:
FunctionName: !Join ['-', [!Ref ServiceName, !Ref Environment] ] # pipeline-manager-staging if staging.
Handler: !Sub '${AppFolder}/app.lambda_handler'
Description: Microservice that orchestrates data loading into BigQuery from AWS to BigQuery project your-project-name.schema.
Environment:
Variables:
DEBUG: true
LAMBDA_PATH: !Sub '${AppFolder}/' # i.e. 'pipeline_manager/'
TESTING: !Ref Testing
ENV: !Ref Environment
Role: !GetAtt LambdaRole.Arn
Code:
S3Bucket: !Sub '${LambdaCodeLocation}' #datalake-lambdas.aws
S3Key:
Ref: StackPackageS3Key
Runtime: python3.8
Timeout: 360
MemorySize: 128
Tags:
-
Key: Service
Value: Datalake
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: Allow
Principal:
Service:
- "lambda.amazonaws.com"
Action:
- "sts:AssumeRole"
LambdaPolicy:
Type: AWS::IAM::Policy
DependsOn: LambdaRole
Properties:
Roles:
- !Ref LambdaRole
PolicyName: 'pipeline-manager-lambda-policy'
PolicyDocument:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
如果我们在命令行中运行这个 shell 脚本,它将部署我们的服务和所有所需的资源,如 IAM 策略到云中:
aws \
cloudformation deploy \
--template-file stack.yaml \
--stack-name $STACK_NAME \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
"StackPackageS3Key"="${APP_FOLDER}/${base}${TIME}.zip" \
"AppFolder"=$APP_FOLDER \
"LambdaCodeLocation"=$LAMBDA_BUCKET \
"Environment"="staging" \
"Testing"="false"
之后,我们可以使用 SDK 通过在命令行工具中运行此 CLI 命令来调用我们的服务:
aws lambda invoke \
--function-name pipeline-manager \
--payload '{ "first_name": "something" }' \
response.json
精通命令行工具
云供应商的命令行工具非常有用,有助于创建测试我们在云中部署的 ETL 服务的脚本。数据工程师经常使用它。与数据湖一起工作时,我们希望掌握帮助我们管理云存储的 CLI 命令,即上传、下载和复制文件和对象。为什么我们要这样做?因为云存储中的文件经常触发各种 ETL 服务。批处理是一个非常常见的数据转换模式,为了调查错误和问题,我们可能需要在桶之间下载或复制文件。

在数据湖对象上使用 AWS Lambda 进行 ETL。作者提供的图像。
在这个示例中,我们可以看到服务将数据输出到 Kinesis,然后存储在数据湖中。当 S3 中创建文件对象时,它们会触发由 AWS Lambda 处理的 ETL 过程。结果被保存到 S3 桶中,以供 AWS Athena 使用,从而生成一个使用 AWS Quicksight 的 BI 报告。
这是我们可能在某个时刻想要使用的一组 AWS CLI 命令:
复制和上传文件
mkdir data
cd data
echo transaction_id,user_id,dt \\n101,777,2021-08-01\\n102,777,2021-08-01\\n103,777,2021-08-01\\n > simple_transaction.csv
aws s3 cp ./data/simple_transaction.csv s3://your.bucket.aws/simple_transaction_from_data.csv
递归复制/上传/下载文件夹中的所有文件
aws s3 cp ./data s3://your.bucket.aws --recursive
递归删除所有内容
aws s3 rm s3://your.bucket.aws/ --recursive --exclude ""
删除一个桶
aws s3 rb s3://your.bucket.aws/
还有更多高级示例,但我认为这个概念已经很清楚了。
我们希望使用脚本有效地管理云存储。
我们可以将这些命令链接成 shell 脚本,这使得 CLI 成为一个非常强大的工具。
例如,考虑这个 shell 脚本。它将检查是否存在 lambda 包的存储桶,上传并部署我们的 ETL 服务作为 Lambda 函数:
# ./deploy.sh
# Run ./deploy.sh
LAMBDA_BUCKET=$1 # your-lambda-packages.aws
STACK_NAME=SimpleETLService
APP_FOLDER=pipeline_manager
# Get date and time to create unique s3-key for deployment package:
date
TIME=`date +"%Y%m%d%H%M%S"`
# Get the name of the base application folder, i.e. pipeline_manager.
base=${PWD##*/}
# Use this name to name zip:
zp=$base".zip"
echo $zp
# Remove old package if exists:
rm -f $zp
# Package Lambda
zip -r $zp "./${APP_FOLDER}" -x deploy.sh
# Check if Lambda bucket exists:
LAMBDA_BUCKET_EXISTS=$(aws s3 ls ${LAMBDA_BUCKET} --output text)
# If NOT:
if [[ $? -eq 254 ]]; then
# create a bucket to keep Lambdas packaged files:
echo "Creating Lambda code bucket ${LAMBDA_BUCKET} "
CREATE_BUCKET=$(aws s3 mb s3://${LAMBDA_BUCKET} --output text)
echo ${CREATE_BUCKET}
fi
# Upload the package to S3:
aws s3 cp ./${base}.zip s3://${LAMBDA_BUCKET}/${APP_FOLDER}/${base}${TIME}.zip
# Deploy / Update:
aws --profile $PROFILE \
cloudformation deploy \
--template-file stack.yaml \
--stack-name $STACK_NAME \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
"StackPackageS3Key"="${APP_FOLDER}/${base}${TIME}.zip" \
"AppFolder"=$APP_FOLDER \
"LambdaCodeLocation"=$LAMBDA_BUCKET \
"Environment"="staging" \
"Testing"="false"
更高级的示例可以在我之前的故事中找到。
数据质量
现在,当我们知道如何部署 ETL 服务、执行请求并从外部 API 拉取数据时,我们需要学*如何观察我们在数据平台上拥有的数据。理想情况下,我们希望在数据流入数据平台时实时检查数据质量。可以通过 ETL 或 ELT 方法来实现。使用 Kafka 或 Kinesis 构建的流应用程序具有分析数据质量的库,数据在数据管道中流动时可以进行分析。当数据工程师将数据可观察性和数据质量管理委派给其他在数据仓库中工作的利益相关者时,ELT 方法更为可取。就个人而言,我喜欢后者,因为它节省了时间。将数据仓库解决方案视为公司中每个人的单一真实来源。财务、市场营销和客户服务团队可以访问数据并检查任何潜在问题。这些中我们通常会看到以下情况:
-
缺失数据
-
数据源中断
-
数据源在模式字段更新时发生变化
-
各种数据异常,如异常值或不寻常的应用程序/用户行为。
数据工程师创建警报并安排通知,以便对潜在的数据问题保持关注。
考虑这个例子,当每天发送邮件通知利益相关者有关数据中断的情况:

邮件警报。图片由作者提供。
在我的故事中,你可以找到一篇文章,解释如何使用 SQL 安排这样的数据监控工作流。
数据仓库指南以便更好、更干净的数据和定期邮件
towardsdatascience.com
考虑以下代码片段。它将检查昨天的数据是否有任何缺失字段,并在发现时发送通知警报:
with checks as (
select
count( transaction_id ) as t_cnt
, count(distinct transaction_id) as t_cntd
, count(distinct (case when payment_date is null then transaction_id end)) as pmnt_date_null
from
production.user_transaction
)
, row_conditions as (
select if(t_cnt = 0,'Data for yesterday missing; ', NULL) as alert from checks
union all
select if(t_cnt != t_cntd,'Duplicate transactions found; ', NULL) from checks
union all
select if(pmnt_date_null != 0, cast(pmnt_date_null as string )||' NULL payment_date found', NULL) from checks
)
, alerts as (
select
array_to_string(
array_agg(alert IGNORE NULLS)
,'.; ') as stringify_alert_list
, array_length(array_agg(alert IGNORE NULLS)) as issues_found
from
row_conditions
)
select
alerts.issues_found,
if(alerts.issues_found is null, 'all good'
, ERROR(FORMAT('ATTENTION: production.user_transaction has potential data quality issues for yesterday: %t. Check dataChecks.check_user_transaction_failed_v for more info.'
, stringify_alert_list)))
from
alerts
;
数据环境
数据工程师测试数据管道。有多种方法来实现这一点。通常,它需要将数据环境分为生产和预发布管道。我们通常可能需要一个额外的沙盒用于测试目的,或者在我们的 ETL 服务触发 CI/CD 工作流时运行数据转换单元测试。
这是一种常见做法,面试官可能会问一些相关问题。刚开始可能有点棘手,但下面的图解解释了它是如何工作的。

CI/CD 工作流示例,用于 ETL 服务。图片由作者提供。
例如,我们可以使用基础设施即代码和 GitHub Actions 来部署和测试任何来自开发分支的拉取请求中的预发布资源。当所有测试通过且我们对 ETL 服务满意时,我们可以通过合并到主分支将其提升到生产环境。
请看下面这个 GitHub Action 工作流。它将会在预生产环境中部署我们的 ETL 服务并进行测试。这样的做法有助于减少错误并更快地交付数据管道。
# .github/workflows/deploy_staging.yaml
name: STAGING AND TESTS
on:
#when there is a push to the master
push:
branches: [ master ]
#when there is a pull to the master
pull_request:
branches: [ master ]
jobs:
compile:
runs-on: ubuntu-latest
steps:
- name: Checkout code into workspace directory
uses: actions/checkout@v2
- name: Install AWS CLI v2
run: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
unzip -q /tmp/awscliv2.zip -d /tmp
rm /tmp/awscliv2.zip
sudo /tmp/aws/install --update
rm -rf /tmp/aws/
- name: test AWS connectivity
run: aws s3 ls
env:
AWS_ACCESS_KEY_ID: ${{ secrets.MDS_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.MDS_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: "eu-west-1"
- name: Deploy staging
run: |
cd stack
date
TIME=`date +"%Y%m%d%H%M%S"`
base=${PWD##*/}
zp=$base".zip"
rm -f $zp
pip install --target ./package pyyaml==6.0
cd package
zip -r ../${base}.zip .
cd $OLDPWD
zip -r $zp ./pipeline_manager
# Upload Lambda code (replace with your S3 bucket):
aws s3 cp ./${base}.zip s3://datalake-lambdas.aws/pipeline_manager/${base}${TIME}.zip
STACK_NAME=SimpleCICDWithLambdaAndRole
aws \
cloudformation deploy \
--template-file stack_cicd_service_and_role.yaml \
--stack-name $STACK_NAME \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
"StackPackageS3Key"="pipeline_manager/${base}${TIME}.zip" \
"Environment"="staging" \
"Testing"="false"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.MDS_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.MDS_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: "eu-west-1"
在我的一个故事中有一个完整的解决方案示例。
机器学*
添加一个机器学*组件将使我们成为机器学*工程师。数据工程与机器学*非常接*,因为数据工程师创建的数据管道通常被机器学*服务使用。
我们不需要了解每一种机器学*模型。
我们无法与像亚马逊和谷歌这样的云服务提供商在机器学*和数据科学领域竞争,但我们需要知道如何使用它。
云供应商提供了许多托管的机器学*服务,我们希望熟悉这些服务。数据工程师为这些服务准备数据集,做几个相关的教程肯定会很有用。
例如,考虑一个用户流失预测项目,以了解用户流失情况以及如何使用托管的机器学*服务为用户生成预测。这可以通过 BigQuery ML [9] 轻松完成,只需创建一个简单的逻辑回归模型,如下所示:
CREATE OR REPLACE MODEL sample_churn_model.churn_model
OPTIONS(
MODEL_TYPE="LOGISTIC_REG",
INPUT_LABEL_COLS=["churned"]
) AS
SELECT
* except (
user_pseudo_id
,first_seen_ts
,last_seen_ts
)
FROM
sample_churn_model.churn
然后我们可以使用 SQL 生成预测:
SELECT
user_pseudo_id,
churned,
predicted_churned,
predicted_churned_probs[OFFSET(0)].prob as probability_churned
FROM
ML.PREDICT(MODEL sample_churn_model.churn_model,
(SELECT * FROM sample_churn_model.churn)) #can be replaced with a proper test dataset
order by 3 desc
结论
我试图总结一组数据工程技能和技术,这些技能通常是入门级数据工程师角色所需的。根据我的经验,这些技能可以在两到三个月的积极学*中掌握。我建议从云服务提供商和 Python 开始,建立一个简单的 ETL 服务,并为预生产和生产环境设置 CI/CD 管道。这不需要花费任何费用,我们可以通过在本地和云中运行这些服务来快速学*。当前市场上对数据工程师的需求很高。我希望这篇文章能帮助你学*一些新知识,并为工作面试做准备。
推荐阅读
[1] www.linkedin.com/pulse/linkedin-jobs-rise-2023-25-uk-roles-growing-demand-linkedin-news-uk/
[2] towardsdatascience.com/data-platform-architecture-types-f255ac6e0b7
[3] towardsdatascience.com/data-pipeline-design-patterns-100afa4b93e3
[4] medium.com/towards-data-science/advanced-sql-techniques-for-beginners-211851a28488
[5] cloud.google.com/python/docs/reference/bigquery/latest
[6] hudi.apache.org/docs/overview/
[7] cloud.google.com/learn/certification/data-engineer
[8] towardsdatascience.com/automated-emails-and-data-quality-checks-for-your-data-1de86ed47cf0
[9] cloud.google.com/bigquery/docs/bqml-introduction
如何在没有技术背景的情况下成为数据科学家:技巧和策略
指南
那么,你想成为数据科学家吗?你需要掌握这些技能
·发布于Towards Data Science ·9 分钟阅读·2023 年 6 月 30 日
--

从数据科学中能做些什么——照片由Possessed Photography提供,来源于Unsplash
这个故事会有些不同于我通常发布的内容。它既不是对具体工具和技术的介绍,也不是教程或实践案例。
这次我想回答一个自我开始在 Medium 上发布内容以来通过 LinkedIn 收到的问题:
没有技术背景的人如何成为数据科学家?
我不是专家,我在这个领域的经验相对较短,但我确实感觉自己有能力帮助和讨论这个话题。所有内容显然都基于我的个人意见,没有科学依据。
免责声明:我目前的工作是数据分析师,但我已经研究并玩数据科学 5 年了。你可以说我是一个兼职数据科学家,通过数据分析维持生计。
成为数据科学家的路径有很多,最终取决于你的偏好和需求。
所以我在这里不会讨论路径。你喜欢大学吗?现在就报名。你喜欢自学吗?这就是我多年来所做的。你喜欢短期课程或训练营吗?这些也很棒。
任何选择都可以,我不在这里告诉你选择什么。
不过,我可以做的是分享我每天使用的技术细节。你肯定需要掌握的工具和语言。
一切都将非常基础。事实上,你甚至不需要阅读这篇文章。只需去 LinkedIn,找到一个数据科学家角色的职位,查看要求即可。你会理解我们都需要的基础知识。
你会看到我谈论一些你需要掌握的关键主题,我会尽量提供我的个人观点以及每个部分在我的个人项目中的相关性。
更具体地说,你会看到我谈论 Netty 和 Bazar——我给两个项目起的名字。继续阅读以了解更多关于它们的内容!
然而,数据科学是一个非常广泛的术语。它像计算机科学一样,涉及许多方面,并且不同的专业化可能会有很大的不同。所以我想简要地将数据科学角色分解为一些不同的类型。
数据科学家的类型
这可能会引发一些争议。没有一个真正的数据科学家类型的列表;每个人倾向于自己制定。我在网上找到了一个适合我分享内容的列表,所以我将使用这个[1]。
这个网站展示了 15 种不同类型,解释得非常清楚,并列出了各自的责任。
如果你想看看这些角色,可以去看看,尽管有些人可能不会真正把那里的一些角色称为真正的数据科学家——例如数据分析师、数据工程师、业务分析师等角色……
最终,似乎没有明确的共识来定义什么是数据科学家!
但我认为这个列表实际上是有帮助的,它让人们看到每个角色的具体内容。它应该帮助新手找到自己的目标,并朝着那个方向前进。
无论你选择什么,工作要求总是相同的。让我们来看看这些要求。
1. 掌握 Python(或 R)
Python 是数据科学中使用最广泛的语言。这就是为什么你需要掌握它的主要原因。
你可以选择其他语言,比如 R 或 Rust——后者最*变得非常流行——但我会选择将它们作为第二语言来学*。
就像我们有一个每天使用的母语,但我们学*第二语言来增加我们的选择和工具。让 Python 成为你的母语,然后用 R、Rust 或其他你感兴趣的选项来补充你的技能。
根据我个人的经验,这份列表上的最大资产就是 Python。Python 一直是我在个人项目和专业工作中使用的主要语言。
比如,我曾经建立了一个名为 Netty 的数据科学项目——这是我最引以为豪的项目——它包括一个深度神经网络,具有卷积层和一些炫酷的功能,并且运行得非常好。
Netty 仅仅是预测给定 NBA 比赛的赢家。虽然不想炫耀,但我在两个完整的赛季中每天测试它,它的效果相当不错。
使用非 Python 语言构建 Netty 是没有意义的。否则,它将更加困难,时间投资效率较低,结果可能也会更差。
所以现在不要忽视我。快去学* Python,它的投资回报率是无价的。
不过,如果你讨厌 Python 或不想走这条路,我建议 R。它是一个对数据科学和数据分析非常有用的语言,因此也是一个不错的选择。
此外,大多数职位要求 Python 或 R,因此精通其中之一可能会很好。
2. 一旦掌握了语言,就掌握它的库
知道如何制作 for 循环和条件语句是基础,但这并不会让你脱颖而出。
你需要掌握那些让 Python 和 R 在数据相关任务中如此有用的库和包。
专注于 Python,因为这是我通常推荐的,这里是必须掌握的:
-
Pandas —— 一个强大的数据分析和处理工具。
-
NumPy —— 你进行任何科学计算时的好帮手。
-
Matplotlib 或 Seaborn —— 你会想要可视化数据,这两个工具可以满足这个需求。
-
Keras、TensorFlow 或 Scikit-learn —— 这些将帮助你构建 AI 和 ML 模型。
除此之外,我们还可以谈谈 Collections、Statistics、Plotly、Dash、SciPy……但我不会说它们是必须的,至少不如前面提到的那么重要。
如果我从头再来,我会首先专注于 Pandas、NumPy 和 Seaborn。一旦我对这些工具熟练了,能够处理数据,我会继续用 Keras、TensorFlow 或 Scikit-learn 来构建一些模型。
正如你可能听说过的那样,数据科学家估计将 80% 的时间花在检索、清理和处理数据上,而剩下的 20% 都在进行建模工作。
这就是为什么我认为 Pandas 至关重要。我已经连续使用了几年,它的多功能性,加上 NumPy,是帮助我发展出惊人技能和高级项目的关键。
继续跟进 Netty,记得我说过它由神经网络组成。你认为我怎么做到的?所有的数据清理和处理都是通过 Pandas 和 NumPy 完成的;显然,我需要可视化,所以我结合了 Matplotlib 和 Seaborn。当然,酷炫的部分,AI 模型,主要是使用 Keras 构建的,但也用到了 Tensorflow。
就像使用 Python 比其他语言更有意义一样,这些库也是如此。就好像它们都是一套工具一样。
3. 同样掌握 SQL
即使 SQL 不会帮助你构建模型和预测系统,学*如何执行查询和从数据库中检索数据也是基础。
我之前说过,我的大部分编码时间都在 Python 上……剩下的时间是在 SQL 上。
无论是专业工作还是个人项目,如果数据存储在数据库中——而通常确实如此——你都需要知道如何检索你需要的确切数据。
如果我不懂 SQL,我不会被现在的工作聘用。老实说,如果我没有掌握 SQL,我无法在那里工作。
所以选择任何你喜欢的关系数据库管理系统(RDBMS),然后学*执行简单查询。我不会深入 SQL,我相信基本语句应该足够了。
学会进行连接、分组、窗口函数……只要熟练掌握它们即可。
如果你想要一些 RDBMS 推荐,我喜欢DuckDB[2]——部分是因为它强大,并且与 Python 集成得非常好。查看我发布的介绍和分析它的故事:
## 忘记 SQLite,改用 DuckDB —— 稍后谢谢我
DuckDB 及其 Python 集成介绍
[towardsdatascience.com
除了 DuckDB,我还会考虑 SQLite、MySQL 或 PostgreSQL 等其他选项……
在我个人的作品集中,我曾经被聘用的一个项目叫做 Bazar。这个项目基本上将亚马逊的数据存储在本地 DuckDB 数据库中,我用它来跟踪我感兴趣的产品价格并查看何时降价。
这个项目依赖了大量 SQL 查询。并不复杂,实际上非常简单,但它们是关键的。我必须检索产品的价格、URLs……所有这些频繁进行,以便与实时数据进行比较。
4. 不要害怕数学
我个人很喜欢数学。我很幸运我的兴趣自然地产生。
但其他人并没有那么幸运。
然而,数据科学涉及大量的数学。无论是直接还是间接。掌握数学是理解你在做什么并做出明智决策的关键。
特别关注微积分、代数、统计学和概率学。我知道说起来容易做起来难。
例如,梯度下降是训练神经网络时最常用的算法——是的,我在 Netty 中也用了它。梯度下降基本上是利用梯度(导数)来调整组成模型的参数的过程。
另一个我在一些项目中,特别是在 Netty 中使用过的例子是分析阶段的探索。在那里需要应用大量的统计数据:分布、均值、百分位数、偏差……你必须理解这些。
5. 改善你的可视化
这将取决于你的角色,但作为数据科学家,你可能需要与团队中的某人或利益相关者分享见解。可视化会很有用,因此如果你掌握它们,它将是一个很大的资产。
这里没有什么花哨的,你可以选择像 PowerBI 或 Tableau 这样的专业软件,或者只使用 Python,通过 Plotly 和 Dash 构建令人惊叹的交互式仪表板。
我很快会创建一篇关于 Dash 的更深入的帖子,但我在过去的故事中简要介绍了它以及其他库。如果你感兴趣的话,我认为这可能会有用:
[## 使用 Python 构建互动数据可视化—讲故事的艺术
Seaborn、Bokeh、Plotly 和 Dash 有效地传达数据洞察
关键是,作为数据科学家或分析师,我们需要传达我们的发现。我经常遇到一种情况,就是一个简单的可视化足以让利益相关者理解我在讲什么。
这就是可视化的魔力和力量:如果它很出色,几乎不需要多言。
但不要只为了别人,也要为了自己。再次提到,使用 Netty 时,我对很多数据进行了尝试。人们真的很容易迷失在其中,不明白自己看到的是什么。
通过简单的可视化,所有数据变得有意义。我现在可以看到模型的准确性及其误差如何在训练阶段随时间演变。不仅如此,我还可以看到获胜者的分布情况(大约 60%的时间获胜者是主场球队)以及大量极具价值的新数据。
6. 保持好奇和主动
在掌握了 Python、其库、SQL、数学和一些可视化工具之后,你不能就此止步。所有核心学*之后,真正的好东西才刚刚开始。
好奇心、主动性、创造力……这些不是技术工具,但同样重要。
我们需要培养这些技能并学*新技能。创建我们感兴趣的个人项目,并利用这些工具创建一个令人惊叹的作品集。
这将是最终让你的个人资料从其他人中脱颖而出的关键。仅仅在简历上写东西是不够的。你需要展示你做过的事情。
提供证据。
好的一部分是,通过实际做这些项目,你会在享受乐趣的同时学到很多东西。
最重要的是:你将被聘用。
以我为例,一个大学辍学生决定花时间学*大量知识并创建个人项目来建立一个入门级的作品集。
那个作品集把我带到了今天这个令人惊叹的公司,拥有令人惊叹的福利和文化。
结论
数据科学是一个令人惊叹的领域,如果你对数据、数学和预测感兴趣,它非常适合你。
成为数据科学家需要时间和努力,但很简单。有多条可能的路径,哪条更好最终取决于个人的偏好。
然而,所需的基本技能和工具非常简单:数学,利用 Python 构建模型,操作和可视化数据,以及玩弄数据库。
如果你仍然感兴趣,那么请继续加入我们!
**Thanks for reading the post!**
I really hope you enjoyed it and found it insightful.
Follow me and subscribe to my mailing list for more
content like this one, it helps a lot!
**@polmarin**
如果你想进一步支持我,可以通过下面的链接订阅 Medium 的会员服务:这不会增加你的额外花费,但会帮助我完成这一过程。
[## 使用我的推荐链接加入 Medium - Pol Marin
阅读 Pol Marin 的每一篇文章(以及 Medium 上其他成千上万的作者的文章)。你的会员费将直接支持 Pol…
medium.com](https://medium.com/@polmarin/membership?source=post_page-----9d569c4bdb5e--------------------------------)
资源
[1] 15 种不同类型的数据科学家【及其职责】 — Knowledge Hut
[2] DuckDB















浙公网安备 33010602011771号