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

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

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

从机器学习中学习 | Vincent Warmerdam:Calmcode,Explosion,数据科学

原文:towardsdatascience.com/learning-from-machine-learning-vincent-warmerdam-calmcode-explosion-data-science-e07951a07cf4?source=collection_archive---------7-----------------------#2023-06-14

数据科学的过去、现在和未来:与 Explosion ML 工程师及 calmcode 创始人Vincent Warmerdam的访谈

Seth LevineTowards Data Science Seth Levine

·

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

--

作者提供的图片

欢迎回到“机器学习的启示”系列,访谈探讨了机器学习的迷人世界。随着 AI 革新我们的世界,每集节目提供了技术见解、职业建议和来自领域内领先从业者的生活经验。

在这一中,calmcode 和 koaning.io 的创始人、Explosion 的 ML 工程师以及前 Rasa 的研究倡导者 Vincent Warmerdam 分享了他的经历和建议。他在数据科学领域担任过顾问、倡导者、培训师、领导者、教育者和招聘者等多个角色。他的工作重点是简化流程、有效利用模型和教育他人。

他的网站 calmcode 提供高质量、易于消费的教育数据科学内容,已获得广泛欢迎和积极反馈。目前每月吸引数千名用户,并成为数据科学家的宝贵资源。Vincent 帮助组织了 PyData Amsterdam,并且他一直是 PyData 和 NormConf 的演讲嘉宾。他的演讲既有趣又引人入胜(可以在 YouTube 上查看)。

Vincent 创建了数十种工具来帮助机器学习开发、数据处理、测试和自然语言处理。

[## koaning - 概述

🙂Vincent D.

github.com](https://github.com/koaning?source=post_page-----e07951a07cf4--------------------------------)

Vincent 对于构建实用工具、公开分享自己的工作和以简洁冷静的方式解决现实问题的热情影响了我工作的方式。

摘要

Vincent 的职业道路不寻常,他在数据科学尚处于萌芽阶段时便进入了这一领域。他将自己的成功归因于运气、博客的受欢迎程度、组织聚会和开源贡献,这使他直接从 CEO 和 CTO 那里获得了工作机会,而不是通过传统招聘流程。他的学术背景包括运营研究和设计,这使他对算法和约束有独特的视角。他还提到,作为 Comedy Club 的调酒师经历帮助他提高了演讲技巧。

受到家庭成员错误诊断的启发,Vincent 被吸引到机器学习领域,以利用算法做出更好的决策和预测。在他的硕士论文期间,他抓住机会专注于机器学习。

他在开源领域的工作受到了希望解决特定问题和有时是满足自身需求的驱动。他从第一个 PyPI 项目 evol 开始,并基于他在不同公司中的需求和经验开发了如 scikit-lego、human-learn、whatlies 和 doubtlab 等库。

大型库的出现是他在 human-learn 和 whatlies 上工作的结果。它利用嵌入、降维和聚类来促进批量标注和数据探索。虽然该工具不能提供完美的标签,但它有助于降低入门门槛并在处理新数据集时建立直觉。

他创建了 calmcode 以回应数据科学中优质教育内容的缺乏,旨在提供简明而有见解的教程,重点是有效地传达思想。他的课程由一系列简短易懂的视频组成。他专注于教授工具和方法,以提升日常的数据科学工作。

文森特在该领域的经验为我们提供了对机器学习现状的深刻见解。我们讨论了生成模型的局限性、规则驱动系统在补充机器学习模型中的作用以及实际标签标注的复杂性。

文森特的建议真实而深刻。他警告说,早期职业生涯中过于强调经济收益是不明智的,应优先发现自己的激情和真正的动机。他在感受到机器学习领域的有用性和对技能专业人才需求的对比中挣扎,同时也认识到过多的炒作。最重要的是,文森特强调要清楚自己能够和不能够影响的事物,并将精力集中在能够产生影响的领域。

建议与收获

  1. 正确理解问题是最重要的——即使算法更好,如果应用于错误的问题,可能也不会产生更好的结果。

  2. 重新表述问题可能是有益的——全面考虑系统优化,而不是仅仅专注于改进单个组件。

  3. “人们经常忘记算法通常只是系统中的一个齿轮。我们感兴趣的是构建一个更好的系统,而不是一个更好的齿轮。所以,如果你正在构建一个更好的齿轮但它不适合其他部分,它不是更好的齿轮,因为你没有得到更好的系统。”

  4. 考虑开始一个博客,分享你的学习经历。撰写简短的“今天我学到”片段可以是建立在线存在感和展示你持续学习的一种简单方式。

  5. 刚开始时不要感到压力要知道所有事情。考虑相关角色,如分析师,这可以提供有价值的技能和知识,对你的数据科学旅程大有裨益。

  6. “你可以比你想象的更频繁地创建 Python 包,所以尽管去创建一个吧。”更多的人应该创建自己的 Python 包,即使是用于内部工具或小的辅助函数。还没有足够多的人利用打包和重用自己代码的好处。

  7. 走出机器学习的圈子可能会令人耳目一新,并提供灵感。与不在该领域的人互动,了解他们的观点,可以帮助你创建更好的应用程序,并关注工作的人的方面。

  8. 在职业生涯早期,要小心不要过于关注金钱。重要的是找出你喜欢什么,是什么让你兴奋,而不仅仅是追逐更高的薪水。

  9. “在机器学习的过程中,我有些复杂的看法。部分观点是它是一个非常有用的工具,我们需要更多优秀的人来做机器学习。但与此同时,它也像一个充满炒作的垃圾桶,我们确实希望减少这种炒作。我每天的工作就是在这两种感觉之间处理。”

  10. “调用 .fit.predict 是简单的部分。所有围绕这些的东西要复杂得多。特别是当你考虑公平性主题、所有可能出错的事情时,我们真的能提前知道这些吗?我不知道你是否总是能知道。”

  11. “有些事情你可以控制,有些事情你不能控制。只要确保你理解什么是可以控制的,什么是不可以控制的,然后从那里开始行动。”

目录

介绍

总结

建议与收获 完整采访

— 欢迎

— 背景

— 学术背景

— 机器学习的吸引力

— Calmcode

— 开源

— 批量处理

— 理解问题

— 机器学习中的未解之谜

— 生成性与预测性机器学习

— 影响因素

— 职业建议

— 对新数据科学家的建议

— 从机器学习中学习

视频采访

Spotify 音频

前一集 featuring Maarten Grootendorst

参考文献

完整采访

(注:为了清晰起见,本次采访进行了轻微编辑)

欢迎

塞斯:欢迎来到《从机器学习中学习》。在这一集里,我很高兴能邀请到 Vincent Warmerdam。他目前是 Explosion 的机器学习工程师,该公司开发了 SpaCy 和 Prodigy。Vincent 是一名教育者、博主,并且是 PyData 的常客讲者。

他创造了许多有价值的开源工具。在 LinkedIn 上,他获得了超过一百次的“出色”认可。真正是数据科学社区中的激励力量。欢迎收听播客。

Vincent:嗨。那句话让我检查一下。我有一百个认可吗?我不知道。

塞斯:结束。

Vincent:哦,好吧,酷。

这是我和一位前同事之间的内部玩笑,看看谁能获得最多。我现在在 LinkedIn 上得到了超过九十九个对我的“出色”认可,真不错!

背景

塞斯:你能给我们讲讲你的职业经历吗?你是怎么走到今天这一步的?

文森特:给出合适的职业建议有点困难,因为我想承认我有一点运气,我也很幸运,因为当我开始从事数据科学工作时,这正是随机森林比较新的时代。如果你能使用随机森林,你已经比所有传统的线性模型要强很多。所以你只需运行拟合和预测——就这样。你就有了一个工作。而且,我正好在那个时代赶上了。

之后,我开始了博客写作。我开始通过组织一些聚会来帮助大家。阿姆斯特丹有一个机器学习聚会——我帮助组织了 PyData Amsterdam。

我还曾经有一个相当受欢迎的博客,所以人们开始因博客而认识我。在某个时候,这种认可让你有了机会,你被邀请去演讲,然后人们就把你视为权威人物。我不是,我喜欢认为自己有明智的想法,但我尽量对这一点保持谦虚。但这也基本上是我职业生涯的故事,因为人们知道我的博客,通常包括公司的首席技术官。然后首席技术官会说,“嘿,我喜欢你的博客。我们能喝杯酒吗?”

然后,通常这就会导致工作机会。我还没有和招聘人员谈过。说实话。在我的职业生涯中,我从未通过招聘渠道获得过工作。

这一直是通过 CEO 或 CTO,因为他们事先知道我的工作。这非常奇怪,这是一个非常糟糕的故事,因为对于其他人来说,这非常难以复制,因为我在这方面非常幸运。我确实认为拥有一个博客并且能够获得认可是非常有用的,只是很难复制。但我也喜欢认为我为开源做的一些旁项目确实有所帮助。Calmcode 是如今人们认识我的一个方面。有句老话说,种下一千朵花,总会有一朵是莲花。

…种下一千朵花,总会有一朵是莲花

图片生成使用了 huggingface.co/CompVis/stable-diffusion-v1-4,提示为“种下一千朵花,总会有一朵是莲花”

我认同这种观点,但很多情况也归结于运气和一点特权。我只是想对这一点保持诚实。能够被认可对我来说确实很有用。

塞斯:是的,确实如此。我的意思是,每个人的经历都是独特的。你在不同公司担任过什么角色?

我知道你曾经做过顾问,也当过倡导者。你有过一些有趣的头衔,对吧?

文森特:在之前的一家公司,有一个阶段他们会让你选择任何名字,我和几个同事觉得这很有趣,看看我们能把名字搞得多么夸张。所以我叫自己宝可梦大师,因为我觉得这挺有趣的。我最喜欢的头衔是高级人物,因为在某个时候,我只是公司里的老一辈。所以我就这样称呼自己。

就角色而言,我做了很多培训,所以有时候你会称自己为培训师。因为你在做咨询工作,所以在不同的团队中你会扮演不同的角色。所以我曾经也担任过领导。

我还帮助一家招聘公司在公司里招聘特定数据团队的人。有时候我的角色会非常短期。有时候会是两年。但通常,我是数据团队中的一员,试图让团队变得高效,做任何事情。通常,我喜欢认为自己很擅长保持事情的简单。

我可以从线性模型中获得很多收益,这通常效果很好。在那之前,我在大学期间做了很多不同的工作,我也喜欢认为这也有所帮助。我的背景是运筹学,但在此之前,我实际上学过设计。我的晚间工作是荷兰一家喜剧剧院的酒保,我认为这可能在某种程度上帮助了我的演讲技巧。学习设计一段时间也让我在思考算法时有了不同的视角。

运筹学让我在做数据科学工作时经常思考约束条件。我喜欢认为我有一个比较多样的背景,而正是这种多样的背景使我能够轻松完成现在正在做的事情。这就是总结。

学术背景

塞斯:这很有道理。那么,深入了解一下你的学术背景吧,是什么呢?所以,既有运筹学又有设计?这听起来很独特。

文森特:是的,我学了工业设计工程一年,然后发现这不适合我。所以我不得不换专业。然后学士学位是计量经济学和运筹学,硕士学位是运筹学。我认为这两个中最有趣的是运筹学。而且,因为这也有一个通向计算机科学的小门。

如果我想学习计算机科学课程,那对我来说会是一种比较简单的方式来获取我想上的课程。不过,我在大学时也参加了划船并做了一些聚会,所以我还不能开始我的硕士课程。因此,我有一年时间只是选修了我感兴趣的课程。我修了一些神经科学的课程,觉得相当有趣,还有心理学和生物学等等。

我喜欢认为知识的多样化被证明非常有用,但我正式的学术背景是运筹学。所以这就是优化系统背后的数学。这就是你在那儿学到的东西。

Seth: 非常酷。在运筹学中,人们尝试解决的典型或经典问题是什么?

Vincent: 所以,在机器学习中,一个非常经典的例子就是你通常会尝试优化某个目标。你希望将损失降到最低或将准确率提高到最高。你有算法来实现这一点。你通常会拿到数据和你想要预测的标签,然后制定某种损失函数,试图使这些损失最小化。在运筹学中,你也会做类似的事情。

在运筹学中,你通常不会处理机器学习算法,而是处理,例如,“嘿,我们有一些股票想要投资,顺便提一下,这也引入了约束条件。因为,是的,我们想获得最高的回报,但当然,我们不想超出预算,我们也有风险偏好。”这些都被定义为严格的数学约束条件。如果你想优化,那就有点不同了,因为如果你的算法超出了约束的范围,那你就进入了不好的领域。所以我认为这是主要的问题。

如果你在做运筹学,那么你会被教导约束条件非常重要,你希望以数学上合适的方式处理这些约束。这是另一回事。但这正是他们所处理的主要内容,这种有约束的优化。这就是他们的工作。

Seth: 非常酷。是的。听起来你需要很强的数学背景。里面也有一些线性代数吗?

Vincent: 需要大量的微积分和线性代数。虽然,我会说这有点取决于你做什么。当你攻读硕士学位时,当然你会上理论课程,并且需要做证明。但当你开始做论文时[情况会有所变化]。我有一个教授基本上说:“我对机器学习一无所知,Vincent,但你似乎非常渴望。你做机器学习,然后教我它是怎么回事,因为我一点头绪都没有。”

这很棒。教授们允许我做我想做的事情,我也能以这种方式自学。但如果你真的想做正规的运筹学,特别是如果你想攻读博士学位,那是非常数学密集的。确实如此。

坦白说,这对我来说有点太沉重了。我更倾向于应用方面,但仍然认识那些最终攻读博士学位的人,他们绝对是数学证明类型的人。他们是那种线性代数“食谱”的人。这确实存在于这个领域。

机器学习的吸引力

Seth: 是什么吸引了你对机器学习的兴趣?最初是什么让你感兴趣的?

Vincent: 做出预测总是很酷的,对吧?所以这其中有一些我认为非常有趣的东西。我记得比较长的故事是,我非常亲近的一个家庭成员得到了一个错误的医学诊断。

错误的诊断是他们告诉这个人你得了很严重的疾病,但实际上并没有。幸运的是,我们及时发现了这个问题。但是,他们可能因为这个决定做了一些非常奇怪的生活决策,比如立即卖掉房子。这让我意识到,做出错误决定会有明确的后果。任何我们能做的改进决策的事情都是有趣的。

也许这与机器学习有关。整个概念是你试图通过使用机器从数据中学习更多,这种想法是有一定道理的。它似乎非常有趣。所以,那时我确实想,嘿,看看这些算法是否能够做一些事情。

然后职业前景变得非常棒。所以这也是进入那个领域的另一个动力。但是最初的火花是一个错误的决定被做出了。这就是我开始思考,也许我们可以在这里改进一些系统的原因。

Calmcode

Seth: 非常有趣,我们马上就会深入探讨机器学习。但是,拥有 calmcode 的创造者和维护者,这真的是一个令人难以置信的资源。

[Vincent 笑]

Seth: 不,没什么好笑的。Calmcode 是令人难以置信的。

这是我推荐给每个新数据科学家的前两三件事之一。你将非常复杂的事情分解成清晰、冷静、逻辑和理性的方式是非常有价值的。

Vincent: 很高兴听到。

Seth: 那你能谈谈 calmcode 吗?你为什么开始做它?它到底是什么?给大家一个详细介绍。

Vincent: 首先,很高兴听到你喜欢 calmcode。我很高兴听到它有所帮助。基本上,calmcode 的故事是,在某个时候,我在查看有关数据科学的教育内容。作为教育工作者,我开始注意到有很多垃圾。

举个例子,大约四五年前,关于 scikit-learn 如何工作的第一教程,涉及一个叫做 Load Boston 的数据集,这个数据集是关于波士顿房价的。很多教程使用这个数据集,从你的 O’Reilly 书籍到许多开源包。但当你查看数据时,结果发现他们用来预测房价的一个变量是肤色。

我忘了确切的名称,但它类似于城镇中黑人比例。你不应该把这个数据放到预测模型中。这是一个非常非常糟糕的主意。此外,为什么这个数据集会出现在 scikit-learn 上?为什么这么多人在使用它?

这导致了我很多的挫败感。我还注意到有些企业课程使用 Load Boston,收费高达一千美元一天。你看了之后会觉得,这真是一团糟。然后我想,如果我这么沮丧,也许可以通过把这些东西免费分享出来来获得一些动力。

我知道自己有足够的知识来教授这些主题,因为我以前教过。但我也发现,很多教育内容似乎更关注创作者,而不是把想法传达出去。所以我觉得做一个学习平台会是一个有趣的实验。如果我来做这个平台,我会怎么做?这就是 CalmCode 的诞生原因。

这个想法是你最多只有五个一分钟的视频来解释一个单一主题,这些视频的顺序可以组成一个关于 pandas 的小课程,也可以是关于其他任何东西的小课程。

…对于每一个主题,我都可以问:这是一个平静的工具吗?它是否能让你的日常生活更好?如果答案是否定的,那我就不教这个工具。

我喜欢做 calmcode 的原因是,对于每一个主题,我都可以问:这是一个平静的工具吗?它是否能让你的日常生活更好?如果答案是否定的,那我就不教这个工具,这也是我不教 Spark 的原因之一。因为安装起来真的很麻烦。有时有比使用大型数据工具更简单的数据分析方法。

这只是一个非常有个人见解的学习环境,人们似乎非常喜欢。我收到了很多非常好的反馈。自从孩子出生以来,我做得少了很多。

看到这个小小的爱好项目在没有干扰的情况下保持非常平静真的很酷。每个月似乎有一万到两万人访问。我还经常在会议上收到陌生人请我喝酒。这些事情都很棒。

Seth:是的。这是一个很好的资源。确实是在回馈社会,为数据科学家创建一个学习任何东西的地方。你有没有发现自己回去查看旧的 calmcode 以刷新一些技能?

Vincent:是的。这是另一个原因。calmcode 对我来说很好的一点是,它有点像一个片段库。我知道我做的课程中提到了这些内容,而且我确实需要一个配置文件,它在哪里?

就在今天,我查看了我的 typer 课程,因为我需要“哦,选项是怎么工作的?复制粘贴。”所以现在这几乎也成了我自己的片段工具。虽然不是最初的意图,但似乎确实发生了。

此外,我现在也在为 calmcode 建立搜索功能。这算是一项兴趣项目。我在考虑,嘿,也许搜索功能的主要任务就是找到正确的代码片段,这本身就是一个有趣的搜索问题。

对,确实,我也需要一个提醒。有很多课程。我不能总是把所有的课程都记在脑海里。在这种意义上,我仍然会看我自己的内容。是的。

塞斯:是的。这对你来说是一个很好的资源,你可以使用,它也成了许多其他人可以使用的东西。我在想我第一次使用它的经历。我认为是像 args kwargs 这样的内容,它是网站上最早的几个之一。是的。我会不时回顾它。

文森特:很好。是的。很好。

塞斯:谢谢。

文森特:嗯,那么我希望能做更多的事情。

但简单的事实是,由于有了宝宝,我的生活现在有点不同了。所以,我有很多想法可以用在 calmcode 上。我也有点喜欢这个项目的原因是,即使我不投入任何精力,网站仍然会运行。对吧?所以这也是它的平静设计的一部分。

我真的喜欢有一个不容易出错的兴趣项目。如果有任何问题,也很容易修复,因为它只是一个静态网站。这使得它非常简单。

塞斯:如果没有时间限制或资源限制,你会做什么来改进 calmcode?

文森特:我特别想做几个课程。其中一个就是嵌入,我觉得这似乎有一点炒作,但你也可以让嵌入做不同的事情,了解它们为什么有效。不过它们并不能解决所有问题,我可以做一个有趣的课程,从字母嵌入开始,逐步过渡到其他嵌入和图像,然后还展示它们可能失败的情况。我认为这会非常酷。

贝叶斯 MCMC [马尔可夫链蒙特卡洛] 的内容,也希望能有,因为你可以创建非常精确的模型,这是一种不够被重视的技巧。

我还希望在网站上增加一个新部分,专注于演示和基准测试。我认为很难做错基准测试。所有基准测试都有偏差,但有些可以非常有洞察力。我认为庆祝一下这个部分也会很有趣。我已经准备了几个例子,但我没有时间去实际制作它们。

但像,嘿,你可以做些什么来让数值算法收敛得更快。标准化真的有帮助吗?只是探索一下这些问题可能会非常有趣。

像这样的事情在我的脑海中。总是有东西可以做。还有,另一个我在玩的是,像,是否与其他人合作这个项目会有趣?也许吧。我不知道。但当然,没有时间上的压力。所以如果我现在不花时间在上面也没关系。这也是不错的。

开源

Seth: 是的。太棒了。稍微转换一下话题,谈谈你的开源工作。我认为我第一次接触到你的库是 bulk。也许之前还有其他的,但这是我真正使用的第一个。然后你还有 embetter、human-learn、whatlies、doubtlab 和 cluestar。这些是我最熟悉的。我知道还有另外两三个。

Vincent: 是的,目前大约十几个。

Seth: 你什么时候决定一个项目值得成为开源库?当你认为它是一个工具时?

Vincent: 这有助于稍微解释一下开源的起源。所以我第一个发布在 PyPI 上的开源项目叫做 evol,基本上是一个用于进化编程的 DSL。我和我的一位同事一起制作了它。这个想法很有趣,我想拥有自己的小库。

所以我在寻找一个问题。然后我发现,如果我有一个人口对象和一个进化对象,并且这两者可以以很好的方式互动,那么制作遗传算法非常简单。好吧,很酷的库,我做了一些关于它的讲座。

但后来,我自己学会了如何制作 Python 包。然后我成为了一名顾问,开始注意到在不同的客户处,我会编写相同的 scikit-learn 组件。因此,我觉得我需要一个包含这些组件的库,因为我不断重用它们。这就是 scikit-lego 的由来,也是我熟悉 scikit-learn 生态系统的过程。

然后,我开始在 Rasa 工作。在那里,我们进行大量的句子分类基准测试,因为 Rasa 构建聊天机器人。当你构建聊天机器人时,句子进入系统,我们需要确定意图。好吧。因此,我写了一些基准测试工具,因为这是我需要的,而其中一些可以开源。

Whatlies 就是一个例子,因为我想要一个库,可以非常快速地拥有许多非英语嵌入,并查看它们是否更好。然后发现 Rasa 周围有一个非英语社区,他们对此非常感兴趣。

所以我能够构建一些 Rasa 插件来支持所有这些非英语工具。然后在某个时候,我开始维护自己的库,然后我发现我需要为我的文档添加一些单元测试,因为我不想让文档出现问题。因此,我制作了一些工具来帮助我做到这一点。Mktestdocs 就是这些工具之一。

我注意到 Rasa 的测试运行得非常慢,所以我制作了 pytest duration insights,以便找出哪些测试最慢。你可以看到这些东西是如何累积的,但这总是因为我在解决另一个问题。我的首选方式是在公开场合进行操作。

当然,也有一些工具我不能公开展示。我在一家公司工作。有些工具是私有的。这没问题。但大多数时候,我遇到一个问题时,我只是希望以后能够用非常少的努力解决它。因为我之前做过包,所以重复这件事变得非常简单。

这也是疑问实验室的形成过程,以及embetter的形成过程,老实说,也包括 bulk 的形成过程。只不过在某个时刻,我意识到我需要这个工具来完成工作。它很好用,所以我们就打包它,公开构建,这对我来说效果很好。这就是主要的故事。

塞思:是的,非常酷。这是一个很棒的故事。看起来,创建一个工具,你积累了某些技能,然后一个事情引出了另一个事情,这样它就不那么难了。

一旦我猜测,你大概有三十多个惊人的工具来添加那个第 37 个工具。

文森特:是的,但我确实想做一个评论,因为我确实认为,通常来说,如果我回顾一下我访问过的公司,以我的顾问背景来看,我确实认为没有足够的人创建自己的 Python 包。

例如,假设你有一个 pandas 查询,需要处理时间序列或在某个非常特定的数据库上工作。那么,从数据库中读取数据的函数可能是一个需要重复使用的函数。也许你还需要添加会话,或者你有一个非常特定的机器学习模型需要重用。

对于所有这些实用工具,你不希望它们存在于笔记本中。你希望它们存在于 Python 包中。我看到并不是所有人都创建自己的内部工具,这我认为是个遗憾。当时我和一些成熟的同事在一起,我们会在内部编写自己的 Python 工具。

…你可以比你想象的更频繁地创建 Python 包。所以,即使是为了你自己在 pandas 中喜欢使用的小助手函数,也可以创建一个。

由于我们有这样的习惯,对我来说,制作一个公开的工具也变得相当容易。所以,这也是我对更广泛的群体的建议,你可以比你想象的更频繁地创建 Python 包。即使是为了你自己在 pandas 中喜欢使用的小助手函数,也可以创建一个。这是一个完全合法的使用案例。

Bulk

塞思:是的。深入到我使用最多的那个工具——bulk。你能谈谈 bulk 吗?它的工作流程和要求是什么?涉及了哪些机制?

Vincent:是的。所以也许可以有趣地解释一下那个库是如何偶然出现的。我有一个叫做 human-learn 的库。它有几个非常酷的功能,但 human-learn 的核心是,作为一个人,你现在可以在不了解机器学习的情况下制作 scikit-learn 模型。你可以做的一件事是将一个 Python 函数转换成一个 scikit-learn 兼容的组件,这非常有用。所以你可以对 kwargs 进行网格搜索等等。

不过,我觉得很酷的一点是,通常你会看到一个图,那里有一些蓝点,那里有一些黄点,还有一些红点。人们说,这就是我们需要机器学习的地方,然后算法对它们进行解析。但我觉得,你知道,你可以直接在绿色点周围画一个圆圈,在蓝色点周围画一个圆圈,然后将那个圆圈转换成一个 scikit-learn 模型。所以这就是 human-learn 的一个特性。在 human-learn 中,我们有 bokeh 组件可以从笔记本中做到这一点。

当我在做那个项目的时候,我也在 Rasa 上处理 whatlies 的所有词嵌入。然后某一刻,我开始意识到,当你将这些词嵌入传递到 UMAP 时,你会得到这些簇。然后我想,哦,我只想选择它们。哦,等等。我有一个叫做 human-learn 的工具,它正好可以做到这一点。

在大约一个小时内,我在一个笔记本中完成了这个功能。然后,我把它展示给了一些同事,他们都说:“这非常有用,Vinny。做得好。”所以这是一个分享了很多的笔记本。

现在,我不再在 Rasa 工作了,我开始在一家叫 Explosion 的公司工作。我们有一个注释工具。我觉得再次做批量处理的技巧,但不想在笔记本中使用它。所以我把它变成了一个可以在本地运行的小型 Web 应用程序,这是我喜欢在 Prodigy 中开始注释之前使用的预处理步骤之一。你只需将数据嵌入到一个 2-D 图中,使用 UMAP,然后你通常会看到簇,你尝试探索那个空间,进行选择,仅此而已。

这是进行批量标注的非常好方法,因为这些嵌入的簇往往会出现。这基本上就是整个技巧。这些批量标注技术,虽然它们有用,但并不完美。它们对我来说足够实用,可以在一个小时内开始。这就是它的力量。过去需要我六个小时的工作,现在只需一个小时。

这是一个只对开始时有效的小技巧,但我在很多新的数据集上经常使用它。所以对我来说,它完全解决了一个问题。批量功能也是这样一个项目,我希望有更多的时间去修复一些粗糙的地方,但它是一个完全有效的小窍门,我喜欢使用它。似乎有一小部分人也非常欣赏这个工具,尤其是因为它不仅处理文本,还处理图像。开箱即用,它就是这样做的。

塞斯:非常酷。是的。我在笔记本中使用过批量功能。我知道我曾联系过你,你非常慷慨地花时间帮助我在不同环境中运行它。

文森特:是的,第一个笔记本确实很有 bug。这确实是真的。我确实记得那一点。

塞斯:依然很有效。

文森特:是的。嗯,在 Rasa 的时候,我养成了制作这些视频的习惯。所以,批量功能还有一个附带的 YouTube 视频,这也是很多人发现它的方式。我觉得有一个库恰好有那个笔记本,而且现在还在获得星标。

但我现在推荐大家使用命令行工具,因为这样干扰更少,稳定性也更高。

塞斯:是的。有趣的是,当我将很多工作从笔记本迁移到脚本时,我再次遇到了批量功能,现在我更多地使用网页版应用。我喜欢它们,它们都是很棒的工具,你提出了一个好观点。

有时候,降低开始解决问题的门槛是非常重要的,因为这样你就能开始推动事情发展,开始有一些想法,并且能够取得有意义的进展。我喜欢它的一点是,通过探索数据,你开始建立一些直觉,你会想到,“哦,这些可能是一些潜在的类别。”

文森特:确实有一个人的参与学习方面,我认为这也是非常有用的。尤其是当他们给你提供一个新的数据集时。是的,你可以开始把它丢进算法中,这样[没问题]。但真正理解数据集中的内容通常是最耗时的。而且,作为批量工具的副作用,你至少能接触到这些集群。这本身就很有用。

现在,你可以对句子和图像进行批量标注。我正在致力于将这一功能扩展到短语以及文本中的子字符串。因此,现在我可以嵌入整个句子,但我希望能够做到的是,我还可以说,取出该句子中的每个名词短语,并为其创建一个小点。因为这样一来,如果你有兴趣做命名实体识别或类似的事情,我们也可以为你进行批量标注。

尤其是像视频游戏这样的东西,可能是缩写——《星球大战》是两个标记。如果我们能把它变成一个短语,那就太好了。在我们公司 Explosion,我们有很多技巧可以完全解决这个问题。只是我需要一个下午的时间才能在大批量中完成这项工作。

不过这些都是在计划路线图上的内容,我确实对解决这些问题感兴趣。

理解问题

塞斯:是的。我注意到你的一些工作大多集中在创建高质量的数据集上。但在此之前,需要实际了解问题。我观看了你的一次 PyData 演讲,基本上是在重新表述问题。

你举了一个关于有人在寻找豆子、牛肉和面包的问题的绝佳例子。

文森特:哦,是的。

塞斯:你能谈谈那个吗?

文森特:所以这不是我的故事。我实际上遇到了在世界粮食计划组织做运营研究的人。他们面临的问题之一是[处理]世界上的饥饿问题。有时候,一个饥饿的村庄会说,我们需要更多的豆子或鸡肉,或者对某些产品有需求。然后世界粮食组织所做的一部分就是尽可能便宜地采购这些食品。

还有成本方面的问题,比如物流。我们能把食物装上卡车吗?租车的费用有多高?还有所有的物流。正如那个人所说,他们最初定义问题的方式是错误的,因为当一个人说,我需要豆子,是的,他们可以这么说,但他们需要的不是豆子,而是营养。而豆子富含纤维和蛋白质。

好的。还有其他食物,比如扁豆,也富含纤维和蛋白质。如果我们在对抗饥饿,那我们不会太挑剔是否得到豆子或扁豆。也许如果我们这样做,我们可以在不需要造船厂的情况下获取食品,我们可以直接发货。

通过重新定义这个问题,我相信他们得到了大约五%的成本减少,这对于一个已经花了多年时间来优化的操作来说是个极高的数字。获得五%的成本减少几乎是闻所未闻的,但这基本上是因为他们在解决错误的问题。我的理论至少是,这个故事是发生在世界粮食计划组织的一个人身上的一个轶事。通常情况下,这种重新表述问题的行为是非常有用的,也许我们做得还不够。

在 NLP 中的一个例子,我们有时在支持论坛上看到的问题是,比如说,他们有一份简历想要解析。然后他们说,我想要每份工作的开始日期和结束日期。因此,我想要一个能够检测开始日期的算法。你知道,你可以建立一个能够检测开始日期的算法,这没问题。但如果你将问题重新表述为,首先找出所有日期,然后再确定哪个是开始日期,哪个是结束日期,那么第二个问题就变成了,开始日期可能在前面,结束日期可能在后面。如果你将问题重新表述为两步走,整个问题就变得简单多了,而不是考虑成端到端。

还有很多这样的机会人们会忽视。我再次回到 calmcode,我担心部分机器学习教科书要负部分责任,因为很少有机器学习书籍实际上告诉你,如果忽略一半的数据更有意义,你可以选择忽略。你可以选择解决一个更容易解决的不同问题。但这似乎不是我看到的思维模式,尤其是新毕业生。这有点遗憾。

但关于世界粮食计划署的故事,我必须相信那个在台上告诉我这个故事的人,但这确实发生了。比如,世界粮食计划署通过重新表述一个数学问题找到了一种减少运输成本五个百分点的方法。这绝对是现实生活中会发生的事情。

“不是算法拯救了这一天,而是对世界的理解。如果用在错误的问题上,更好的算法会产生更糟的结果。”

塞斯:对,没错。在这样规模的工作中,任何形式的减少,五个百分点的减少都是巨大的。我最喜欢你在那个演讲中说的那句话:“不是算法拯救了这一天,而是对世界的理解。如果用在错误的问题上,更好的算法会产生更糟的结果。”我真的很喜欢这句话。

文森特:哦,很高兴听到这个。所以,这个故事中还有更多的轶事。但如果有人对这个感兴趣,有一位运筹学家,[拉塞尔] 阿科夫。

他写了一篇论文,标题是《运筹学的未来已经过去》,这是他在八十年代写的。它基本上概述了为什么运筹学算法可能会失败。原因与这个轶事有关。我提到这个是因为这些论点对于数据科学也适用。这篇文章虽然是八十年代的,但每个人都应该读一读:运筹学的未来已经过去。

我写了一篇类似的文章,叫做 数据科学的未来是过去,只是重复了一些这些论点。但人们常常忘记算法——它通常只是系统中的一个齿轮,我们关注的是建立一个更好的系统,而不是一个更好的齿轮。所以如果你在构建一个更好的齿轮,但它与其他部分不匹配,那它就不是一个更好的齿轮,因为你没有得到一个更好的系统。

但人们常常忘记算法——它通常只是系统中的一个齿轮,我们关注的是建立一个更好的系统,而不是一个更好的齿轮。所以如果你在构建一个更好的齿轮,但它与其他部分不匹配,那它就不是一个更好的齿轮,因为你没有得到一个更好的系统。

Akhoff 在他的书中做得非常好的一点是,他基本上解释了很多系统理论。在那里有一句我推荐人们多考虑的话,也许不要想着去制造一个更好的齿轮。不要想着“嘿,也许系统中有一个部分可以优化。”也许尝试看看你能否改进两个部分之间的沟通。因为从系统的角度来看,通过这样做,你实际上是在优化两个方面。

同时,你也在获得清晰度,这总是好的。这种通过将问题简化为一个单一的数字而忽略其他因素的思维方式,通常是数据科学中人们迷失自我的一个陷阱。

Seth: 是的。这非常有趣,因为我觉得有很多时候,人们处理问题时有时会专注于不同的模块,他们有一种模块化的思维方式,他们会想,哦,如果我把这个东西做到最好,那么整个系统会更好。在某些情况下,这会带来巨大的改进。但有时,理解支持系统及其集成方式非常重要。这让我想起,你必须有良好的集成测试,确保一切都适合系统。

Vincent: 这里有一个小故事,bol.com 的前首席执行官在他的自传中写了这些内容。所以,bol.com 就像是荷兰的亚马逊。亚马逊在这里并不那么大。bol.com 基本上就是亚马逊,不过是蓝色的,而且是荷兰的——这是我们这里的一种现象。

但他们在某个时点雇佣了他们的第一位数据科学家。这本书中有一章讲述了这个——当我们迎来第一位数据科学家时发生了什么?在书中,第一位数据科学家被描绘成一个有点傲慢的人。总是抱怨说这些人类不如我的算法。

他做的事情之一是他发现了一个最佳时间来推文关于新视频游戏发布的时间等。所以这就是他做的事。在荷兰,我们有一个叫做纪念日的节日。我相信是七点钟,也可能是八点,但在纪念日,我们纪念第二次世界大战。基本上,全国上下会有两分钟的沉默。

你可能见过一些照片,骑自行车送披萨的人会下车,站着静静地待两分钟。这是人们非常认真对待的事情。所以,在纪念日的七点钟,推文关于新的《使命召唤》射击游戏的时间会非常糟糕,而如果你推文说你对在纪念日射击人感到超级兴奋,那将特别糟糕。但这正是发生的,因为他的算法确定七点钟是开始推文这种事情的最佳时间。

这些故事非常多,对吧?单独看,纸面上,我不一定能责怪数据科学家做了他或她的工作。但这就是系统的问题。组一担心可能会出错,组二则不担心。

如果你让他们相互交流,那么通常世界会变得更美好。这是我会说的主题。

塞思:当你得到问题的答案时,你必须问自己,这有意义吗?这有时是很多人跳过的一小步,但非常重要。

文森特:我确实想承认,这也是困难的,对吧?我认为调用.fit 和.predict 是简单的部分。

这都是围绕这个主题的东西。要复杂得多。尤其是当你考虑公平性主题、所有可能出现的问题时,我们真的能事先知道吗?我不知道你是否总能做到。

不过要提到一个项目吗?这个项目叫做deon,有一个Deon 清单。还有一个calmcode 课程

Deon 是一个数据科学清单。就是一些在不同公司出现过的问题,有报纸文章解释情况变得多么糟糕。他们有一个清单,类似于“嘿,在上线前检查这个,因为可能会出错。”对于清单上的每一项,他们还有两篇关于过去发生的事情的报纸文章。因此,作为数据科学家,你可以去找你的老板说:“我想要[最小化]风险,因为这实际上出了问题。”

这是一个非常酷的项目,因为他们实际上进行了适当的轶事收集,这在当今时代是一个强大的行为。

机器学习中的未解之谜

塞思:是的。一百个百分点。在数据科学中与任何事物相关联的故事总是有价值的。

回到整体讨论机器学习的话题,您认为在机器学习中还有什么重要的问题未得到解答?

Vincent:好的。所以我在 PyData Afterparty 上喝酒时,几个人找到了我,而这些人是我认为相对资深的人。他们对自己的领域很了解,他们让我预测机器学习的未来。

我当时感觉像是想开个玩笑,因为,知道的,你在酒吧里。我当时不太想深入探讨这个问题。作为玩笑,我想着说,“你知道我对数据科学未来的看法,人们将真正意识到我们领域中的大量废话。也许我们应该彻底停止。”

但我决定再考虑一下,我会说,实际上这一点确实有些道理。我确实有点担心,我们做的很多事情更像是炒作,而不是我们是否真正理解了问题。

那么,机器学习中缺少什么?好吧,也许我们做得太多了。这是我一种感觉。

当然,未来机器学习会有一席之地。这肯定会发生,但它不必成为一切。这有点是我担心的事情。

有位作者写了一本关于人工奇怪现象的书。就像人工智能可以产生的所有奇怪废话一样。

这本书叫做 你看起来像个东西,我爱你,作者是 Janelle Shane。读一读。这本书一开始提到我有这些 Tinder 文本,我想让一个算法找出最佳的 Tinder 文本发送出去。然后算法给出的结果是,“你看起来像个东西,我爱你。” 这有点搞笑且聪明,但我认为这不是你在 Tinder 上应该发送的内容。

但这本书充满了这些例子,你要小心人工愚蠢现象也在同时发生。对吧?有很多这样的例子。比如,《使命召唤》就是一个例子。

我发现自己有点像那个对一切都不满的老人,像是在对着云发牢骚。感觉有点像,“当然,机器学习有它的位置,但我们能先不依赖它吗?”先尝试简单的方法,因为这是人们似乎常常忘记做的事情。对我来说,这才是更紧迫的问题。

Seth:与自然语言处理领域现在所有发生的事情,包括生成模型和 ChatGPT,类似的观点。你如何看待炒作与现实之间的差距?我很期待听到你这个老顽固的看法。

Vincent:所以我实际上在专业上玩这些东西。如果你查看一下,Explosion 仓库现在有 openai/prodigy recipes。这就是仓库的名字。所以我们正在稍微尝试一下,比如,嘿,ChatGPT 能不能说,这里有一个句子,检测所有的日期。

以便我们可以在我们的 Prodigy 界面中预先高亮显示。这是我们现在正在探索的事情。结果发现,它在一些例子中确实非常好,而在其他例子中却很差。我们还不完全理解为什么。

但我承认这可能非常有用。如果这能让你更快地获得更好的训练数据,因为标注只是说“是”或“否”比高亮每一个用户界面中的项目要快,这似乎完全可以接受。

我觉得更值得关注的是,人们有时会说:“哦,这很神奇。这就是它的工作原理。这是魔法。”这不是魔法。这在某种程度上有点像马尔科夫链,它只是预测下一个词。你可以想象,如果你给它足够的文本和计算能力,你可能会让它生成非常可信的文本,像你在互联网上找到的那样。然后你可以问,比如,它是否在概括?还是只是记住?

空中漂浮的魔法由稳定扩散 v1–4生成

这些问题都是公平的,但这还不是智力。这还不是实际的推理。我有很多愚蠢的例子可以证明,实际上并没有真正的智力在运作。

话虽如此,只要有一个人参与其中,并且它被证明是有用和高效的,我认为这是可以的。但话说回来,那是当我带上职业眼镜时。显然,我认为还有一些有害因素需要考虑。你绝对可以批量发送更多的群发邮件,也许还能有更多的 Twitter 机器人,还有那些我不太喜欢的东西。

总之,这只是一个方面。另一件我也许想强调的事情是,我也尝试了 Midjourney。我尝试生成《万智牌》卡片。

Seth:好吧。我见过这些,它们非常有趣。

Vincent:我觉得有趣的是,某一刻我想说,嘿,让我们制作一些办公室里的《万智牌》卡片。你会有一个兽人领主产品经理,一个兽人风险投资家,还有一个兽人 TED 演讲者。这个想法立刻就很有趣,因为如果你想到办公室,你会想到一个沉闷的灰色西装。如果你想到兽人,你会想到《魔兽世界》和像是战争狂人等等。所以,这真的很有趣。

但接下来的问题是,我们是否能生成真正搞笑的图片?这证明是有些困难的。所以我有一张铁甲兽人的图片,基本上完全覆盖在铁网中,坐在电脑后面。你可能会觉得,数据工程师,还不错。这已经挺搞笑的了。

但我想让这个兽人是一个数据分析工程师,因为他们在谈论数据湖。然后我认为有趣的事情是一个沉重的铁甲兽人,但头盔上冒出一个小黄呼吸管。这会是最搞笑的事情。我拼命尝试也没能生成一个黄色的呼吸管。

然后你开始思考,为什么会这样?接着你也会想,比如说,文森特,你已经在办公室里把《魔兽世界》地下城和龙的风格结合在一起了。那两种风格甚至兼容已经很牵强了,更不用说你还生成了一些奇怪的呼吸管了。对吧?

所以如果人们认为这些工具像魔法一样,我的最佳建议是试着想出一个尴尬的奇怪任务,触及算法在舒适范围内的边缘,这通常会给你一些示例,帮助你理解这实际上并不是魔法。它只是试图记住,试图生成它之前见过的东西。有很多边缘情况的示例,这种东西就像是“你看起来像个东西,我爱你”。读那本书。书中有很有说服力的例子,风格也很可爱。我强烈推荐。

生成性和预测性机器学习

塞斯:谢谢。是的,我会去查看。我认为生成模型非常有趣,因为不同于预测模型,例如你在做文本分类时,你可以知道是否正确?通常有一个真实的标准。而在生成模型中,比如说你想创建一个戴着呼吸管的兽人,你怎么知道它是否正确?

这不是那么明确。

文森特:你实际上需要多少标签的无关照片才能生成这个?对吧?哦,不过这里的解决方案的一部分显然也是用户界面。你可以用文本作为输入做出惊人的事情。但在这种情况下,你也没问题。我们快到了。我只想选择头盔周围的区域,在那里需要出现一个黄色的呼吸管。

这样的事情在某个时候会发生,这将使这些系统变得更好。然后我可以继续处理企业精灵,解决其他边缘情况。对吧?这将是一个持续的过程。

但,是的,一般来说,因为你提到了真实情况——真实情况也很棘手。这也是许多人工愚蠢产生的原因。对于我个人的不满——所以考虑一下,图像分类,那些著名的猫狗问题:这是一张狗的照片还是猫的照片?

来源:Catdog Logo 维基百科

标准分类会说,这这是一个二分类任务。但随后你可能会想,我们可能有没有猫或狗的照片。那么,我们需要三个类别吗?好的。那么,如何处理既有猫又有狗的照片呢?哦,对,这也可能发生,对吧。

好的。现实更复杂。那么我们该怎么办?

好吧,也许我们需要问,照片中有没有狗?有还是没有。照片中有没有猫?有还是没有。也许这些应该只是两个二分类器。也许那样更合理。那么,当照片中有四只狗时怎么办呢?

再次强调,你越是思考,你会发现,即使是定义明确的文本分类也不总是与现实相符。即使你有真实标签,你也要考虑,真实标签可能也与现实不符,因为句子可能涉及多个主题,照片也可能涉及多个事物。

所以退一步真正思考一下,有些问题可能只是细节,只要我们真正理解问题,但也许我们应该专注于此。也许我们应该跳过超参数调整,只关心——我们真的理解问题吗?

赛斯:是的。这确实是一个很好的观点。我认为,当你面对一个问题时,人们往往会跳到解决方案上。如果你正在做文本分类——哦,好吧。我打算创建一个多分类文本分类器。结果是,这从来没有那么简单,对吧?

这实际上是多标签的。我应该使用层级结构吗?我应该这样做吗?还是那样做?你知道,深入理解问题总是能帮助你发现更多。这比对原始多分类文本分类器进行超参数调整要有价值得多。

“模型可以进行一步操作,但如果需要的话,你的系统可以进行两步或三步。因此,绝对可以考虑这种两步系统,其中我们有几个分类器来检测几个属性,然后我们有一个基于规则的系统,它会说,‘好吧,这种组合看起来很有趣。我们试试这个。’人们往往忘记了可以在其上构建的基于规则的系统。这有点被忽视了。但通常情况下,这也是解决问题的方法。”

Vincent: 嗯,总的来说,我确实有一些通用的建议。我在 Prodigy 论坛上,帮助一些 SpaCy 用户解决问题。我在这个领域给出的最通用的建议是,考虑一下也许模型可以做一步,但你的系统可以在需要时做两步或三步。所以绝对可以考虑两步系统,其中有几个分类器检测几个属性,然后我们有一个基于规则的系统,接下来会说,“哦,好吧。这些东西的组合似乎很有趣。我们就这么做吧。”

人们常常忽视可以在其基础上构建的基于规则的系统。这有点遗憾。但通常来说,八成的情况下,这也是解决问题的办法。所以亲爱的观众,可以根据这些信息做出决定,但我确实认为一般来说,两步法绝对有效。

Seth: 我认为这特别好的建议,尤其是在深度学习的热潮中。我觉得我们仍然处于一个寻找机器学习模型和启发式方法之间最佳组合的世界中,有时候即使是相当基础的启发式方法,也常常能产生最佳结果。

影响

Seth: 现在我们来谈谈从机器学习中学习的部分。我们从这里开始。在机器学习领域中,有哪些人对你产生了影响?

Vincent: 我有一些非常可爱的直接同事,我现在仍然与他们保持联系。显然就是这些了。刚开始的时候,我在学习 R,所以 Hadley Wickham 是我非常仰慕的人。我还在几次机会中见过他,这真的很酷。他在五年前开设了一个高级课程,我是助教。非常棒的经历,我见到了这位大人物。

Katharine Jarmul 是我想到的另一个人。她是 PyLadies 的发起人之一,同时也是机器学习隐私和公平的积极倡导者。她曾经为我的几场演讲审阅过幻灯片,她真的是太棒了。我想起她了。

我认为 Vicki Boykis 是最有趣的人之一——她在垃圾信息发布方面应该获得更多的认可,她真的很棒。NormConf 事件也是她帮助启动的一个了不起的项目,非常精彩。

然后 Bret Victor 我认为有我见过的最好的演讲,也是我将来可能看到的最好的演讲。 编程的未来 是 Brett Victor 的演讲。这是我基本上每年都会观看的内容。这是我见过的最令人惊叹、最具启发性的东西。我不会告诉你具体内容。就去看看吧。

Seth: 我很期待。

文森特:然后,我想,[拉塞尔] 阿科夫,但阿科夫的主要问题是我完成了整个运筹学的硕士学位,然后一位教授要退休了,我是他聚会上的发言人之一。后来他提到他想要我来这里的原因是因为我真的让他想起了阿科夫。我当时想“他是谁?”

“他是一个了不起的人。赶紧买他的书。”然后你读到这些东西。他就像我在 80 年代一样。所以这绝对也是一个很好的灵感来源。

…普通人非常有启发性,但普通人并不认为他或她应该上台。

我想提到的一点是,当我组织 PyData 时。你会想,“好吧,谁是好的主讲人,谁是好的受邀讲者等等。”我的印象是普通人非常有启发性,但普通人并不认为他或她应该上台。

最好的例子是在 PyData London 上,有一个普通的讲座,讲者在婆罗洲的雨林中为寻找濒危的猩猩而制造无人机。

塞斯:哇!

文森特:他有一个小房间,但他的演讲非常棒。所以我想,算了,你是阿姆斯特丹的主讲人。这是我听过的最惊人的事情。这是你的爱好。

所以他是第二年的主讲人。他感激不尽,玩得非常开心。但他没有意识到那确实是主讲材料。类似地,我读过一篇博客文章,讲者试图找出哪些词最具金属感。

他的方法是通过对金属歌词和非金属歌词进行大规模马尔可夫链训练。博客文章的结论是,最少金属的词是“合作”,因为它只在语料库中出现了一次。你读到这些内容,真是太棒了。因为你基本上是在一个相当幽默和傻乎乎的问题上正确地应用理论。

但这里有激情。当我接触到他时,[我说] 你真的需要申请 PyData。我不需要审查你的东西,我认为你会被接受的。

他只是没想到这是他可以做的事情。我喜欢认为有很多人遭受这种情况,他们可能有一个非常宏伟的启发时刻,但不认为自己能分享出来。当然,有些人确实是内向的,这也没问题。但我在 PyData 上学到的一课是,灵感确实可以来自你意想不到的惊人角度。所以不要过于关注大名鼎鼎的人物。

这也是一个问题。

塞斯:是的。最好的那种人非常谦逊,他们对自己的工作做得非常好,你可以看出他们多么关心自己所做的事,以及他们对工作的认真程度。我不知道“自豪”是否是正确的词,但他们非常认真地对待自己的工作。他们很在意…

Vincent:他们在乎,是的。你可以是最聪明的人,但如果你对你的话题不在意,那将不会是一个很棒的演讲。

假设你可能走了一些捷径,但你计算出了最佳的宝可梦。我不知道,就类似的事情。这仍然可以是一个很棒的演讲。

再次提到,更多人应该这样做。如果人们对写博客和做演讲感兴趣,考虑一下闪电演讲和非常短的博客文章,称为“今天我学到了”。世界上确实需要更多这样的内容。我很高兴看到 PyAmsterdam 一年一度的闪电演讲聚会,十个人进行五分钟的演讲。

那些聚会往往非常精彩。任何 PyData 的组织者如果在听,请随意借鉴这个想法。这些聚会总是很有趣。

职业建议

Seth:非常酷。所以你已经给了很多建议,但我想问一下,你收到的哪个建议或某些特别突出的东西帮助了你在机器学习或职业发展中?

Vincent:我在职业生涯早期就得到了这个建议。我以前的 CTO 现在仍然和我一起,他在我二十三岁时给了我相当好的职业建议。他说,“小心加薪。因为如果你的工作开始赚很多钱,但有些乏味,那么金钱可能会成为你留在那里的原因。”

点击这里查看视频 ## 早期职业生涯中要小心加薪

因为如果你的工作开始赚很多钱,但却有些乏味,那么金钱可能会成为你留在那里的原因。点击这里查看视频

这在职业生涯早期是个危险的事情,因为你可能需要弄清楚你生活中喜欢什么。也许你需要搞清楚是什么让你有动力。如果你过度关注金钱,这有点像过度关注指标。你可能会过度优化一些可能不那么重要的东西。所以这是一条很酷的建议,有点元方面的。但我确实认为,我已经能够很好地应用这些建议。

再次说,我在这里很幸运,对吧?但我已经能够应用这些建议,这真的很棒。

这是一个有点奇怪的轶事,但它也令人惊讶地充满了启发。我有很多朋友在数据科学方面什么也不做。我很喜欢这样。我是书呆子 Vincent,当我和他们喝啤酒时,他们会说,别做书呆子 Vincent,你现在在正常人中间,可以谈谈生活了。

我住在一个你基本上认识所有邻居的社区。这个社区仍然是中产阶级社区的一部分——因为城市化它在变化,但我们彼此都认识。所以我街上有一个人,他是个画家。当天气好的时候,他会在家门外的长凳上放一箱啤酒。

整条街的人基本上都会去喝一杯。真的很可爱。最可爱的社区。但问题是他最近成为了独立承包商,也意味着他买了他的第一台笔记本电脑,而他已经四十二岁了。

他需要帮助,不仅仅是他的网站,还有像启动词这样的问题。他的一生中,主要的计算机是他的手机,他一直使用它,但他发现计算机非常复杂。说实话,我觉得这真是一个令人耳目一新的事情。也提醒我们,我对计算机的体验不一定要是正常的。这是一个非常有用的提醒。所以,从某种意义上说,这是最好的灵感。

也许不要总是沉浸在机器学习中。这是我的建议。尤其是当你为普通人使用的应用程序做机器学习时。记住他们其实并不在乎你的算法。真的,他们不在乎。他们真的,真的不在乎。

我发现自己有时陷入机器学习的泡沫中。我发现[走出这个圈子]非常清新。

我以前也在咨询工作中做过这个。我正在制作一个卡车司机用于物流的应用程序。有时候,我会去卡车司机们聚集的吸烟角落,了解他们是什么样的人。还要了解他们对应用程序感到沮丧的地方,更多地做这些。更多地成为一个人际互动中的环节。

我正在尝试更多地关注人性化的方面,这让我感到非常有启发。

对新数据科学家的建议

赛斯:是的。我真的很喜欢这个。对于那些刚刚开始这个领域的人,比如说,他们刚刚被聘为初级数据科学家,或者他们正在考虑开始从事数据科学工作,你会给他们什么建议?

文森特:好的。那么第一步,我在 NormConf 上做过一个关于这个主题的讲座。这个讲座标题为让一天变得精彩的 Group-by 语句。这个讲座正好为你设计的。

话虽如此,我其实是一个很差的职业建议提供者,因为回顾过去,我觉得我今天所处的位置很大程度上是由于运气,而运气是很难优化的东西。

我认为一般来说,有一个自己的博客,分享你今天学到的东西,可能会很有用。所以,就像 calmcode 在某种程度上仍然是我的代码片段库一样,你的博客也可以是你的代码片段库。总的来说,我发现对于这些“今天我学到了什么”片段——如果你能够每月写两篇,可能每篇需要半小时,而不是一篇需要几个小时的大博客,这件事不应该超过几十分钟。但你这样做了一年,就会有一个包含 24 篇文章的博客。

如果你在学习并且能够分享知识,那么人们会认可你确实有一些履历表明你在学习东西。所以,如果你想以较低的努力获得一定的在线存在感,这似乎是件相当简单的事。这是我推荐的做法。

我确实认为,如果你是刚起步的超级初级人员,我想承认这有点困难。现在的[招聘市场现状]有点让人遗憾。但是,你可以做的一件事是考虑一下,你不需要知道所有东西才能得到工作。

你也可能能找到相关的工作。我给那些想进入数据科学领域的朋友的一些建议是,学习 R 可能比学习 Python 稍微容易一点,做一两年的分析师可能也会更简单一些。

而且你作为分析师学到的所有技能,如果你想以后成为数据科学人员,都将非常有用。所以,如果更容易并且你能从中获得报酬,不要优化头衔。只需优化你在工作中学到的东西。这似乎更容易。作为一个优秀的分析师没有什么错。

也许我们需要更多优秀的分析师,而不是优秀的数据科学家。对吧?也许我们需要更多拯救一天的 group by 语句。提示,看看这个演讲。

但我确实认为在职位头衔上有一点虚荣。“比如,我是超级高级的员工,超级工程师。是的。”但如果你只是一个真正称职的分析师,我们需要更多的优秀分析师。这也没关系。去争取吧。

从机器学习中学习

Seth:是的。这确实是个好建议。现在我们都在等待的问题是,机器学习的职业生涯教会了你什么人生道理?

Vincent:有些问题在你忽视它们时会自行解决。说真的,我经历过很多这种情况,问题通过忽视机器学习部分得到了解决,你会开始怀疑,也许有些问题真的能在你忽视它们时自行解决。

我在几个实例中注意到,情况确实是这样的。特别是当你有一个孩子时,你确实会学到一些东西,也有可能过度优化一些事情。而且,比如说,哦,宝宝睡不好。嗯,这个问题会在某个时候自行解决。并不是说我这边的影响会在那方面产生非常显著的影响。

我想机器学习也一样。有些事情你可以控制,有些事情你无法控制。只要确保你理解自己能控制和不能控制的东西,然后从那里继续前进。

再次说,我对整个机器学习的看法有些复杂。部分原因是我认为它是一个非常有用的工具,我们需要更多优秀的人来从事机器学习。但同时,它也像一个令人厌烦的炒作桶,我们真的希望减少这种炒作。我日常的工作就是处理这两种感觉。

我希望这在某种程度上回答了这个问题,但这就是我的情况。尽量冷静地做,这就是我的最后一句俏皮话。这也是我可能会推荐的。

塞思:说得对。我认为有些问题随着时间的推移会自行解决。我还喜欢机器学习的第一条规则:你真的需要使用机器学习吗?

文森特:是的。我同意。还有一点我真的可能要为雇主稍微夸耀一下。

我非常喜欢 SpaCy 的一点是,你不必使用机器学习的部分。你也可以只使用 SpaCy 中的非机器学习部分。它们也表现出色,快速且非常有用。还有一些机器学习包允许你做一些基于规则的工作。如果你在做 NLP,这就是我为什么喜欢使用 SpaCy 的原因。

你不必一直使用统计方法。基于规则的引擎也很好。推销完毕。

塞思:我已经是 SpaCy 的忠实粉丝有一段时间了——至少四年,可能更久。它帮助我解决了许多问题,从命名实体识别、文本分类到做匹配的酷方法,所有这些。

文森特:好吧,如果我可以做一个最后的推销。现在有很多关于数据驱动的人工智能的讨论。但我开始对这些 Explosion 团队的工作产生兴趣的原因之一是,他们在 2017 年写了一篇博客文章,标题是监督学习很棒——破碎的是数据收集。他们在 2017 年就做了数据驱动的工作,但这是我读过的最好的博客文章之一。

他们谈到了数据质量,其中一句最经典的名言就是:不要指望得到优秀的数据,如果你让低薪员工感到无聊,因为机械土耳其人有时仍然是人们的选择,读那篇博客文章。我会给你一个链接到节目备注。这也是人们应该阅读的非常有启发性的内容。

Seth: 太棒了。非常感谢你。与您交谈真是太愉快了。你给了我很多很棒的资源。整理这些节目说明肯定会很有趣。如果有些地方你希望听众更多了解你,那些地方会是哪里?

Vincent: 目前我在 Twitter 和 Fosstodon 上。不过主要的是我不能宣布任何消息。因为我在 Explosion 工作,能看到正在开发的内容。所以我正在做一些非常酷的事情,肯定会有超级酷的公告,我的其他同事们也在做很棒的工作——只需关注 Explosion 即可。

有很多非常酷的内容正在开发中。如果你这样做,那么你也会在某个时候听到一些我正在做的事情。

Seth: 太棒了。非常感谢你,Vincent。真是非常愉快。

Vincent: 同样。

立即收听

完整访谈的视频可以在这里观看:

Vincent Warmerdam | 从机器学习中学习 第 2 集

播客现在可以在所有 播客平台 上收听:

我们的上一集节目特邀了 Maarten GrootendorstBERTopicKeyBERT 的创始人,以及 Towards Data Science 上的一位 多产作者。他讨论了开源项目、心理学与机器学习和软件开发的交集,以及自然语言处理的未来。

## 从机器学习中学习 | Maarten Grootendorst: BERTopic, 数据科学, 心理学

从机器学习中学习的第一集节目介绍了 BERTopic 的创始人 Maarten Grootendorst。

towardsdatascience.com

参考资料

了解更多关于 Vincent Warmerdam 的资源:

本集参考资料

了解更多有关从机器学习中学习的资源:

通过代码学习数学:导数

原文:towardsdatascience.com/learning-math-through-code-derivatives-bbcd2df166d3

通过 Python 深入了解导数

哈里森·霍夫曼Towards Data Science 哈里森·霍夫曼

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

--

莱布尼茨团队。图片由作者提供。

数学对许多人来说是一个 notoriously difficult 的学科。由于其累积性和抽象性,学生可能会发现很难建立联系并理解数学的应用。在我不断的学习过程中,我发现通过实现代码来增强对数学概念的理解是非常有帮助的。

编程要求精确,因为计算机只能执行特定的一组指令。对精确性的需求要求采用逻辑和系统化的问题解决方法,这对理解基础概念非常有帮助。通过编程,我们可以对所实现的理念形成更深刻的直觉。此外,编程允许我们以互动和动手的方式实验、可视化和自动化数学概念,这可以使理论概念生动起来,提升我们的学习体验。

在本文中,我们将通过在 Python 中实现“前向差分商”近似,来更好地理解导数。虽然这是一个简单的实现,所需代码很少,但它揭示了导数所代表的核心。

什么是导数?

首先,我们定义导数。由于有许多免费的导数资源,这个解释不会是全面的。函数 f(x) 对 x 的导数定义为:

一维情况下的导数定义。图片由作者提供。

导数告诉我们在某一点,函数变化的方向速率。通过选择两个点 x 和 x + h,计算这两个点之间函数的斜率(即(f(x+h) — f(x)) / h),并让 h 无限接近 0,我们恢复了函数在 x 处的瞬时变化率(即导数)。

用 Python 近似计算导数

导数最抽象且可能最难理解的部分是 h 接近 0 但实际上不会达到 0\。我们可以用 Python 编写一个函数来近似这个概念:

import numpy as np
from typing import Callable, Union

def derivative(f: Callable[[float], float], x: Union[float, np.ndarray], h: float = 0.001) -> Union[float, np.ndarray]:

    """
    Approximate the derivative of a function f at point x using 
    the forward difference quotient defintion.

    Parameters
    ----------
    f : callable
        A function that takes a float as input and returns a float.
    x : float or ndarray
        The point(s) at which to compute the derivative of f.
    h : float, optional
        The step size used in the finite difference approximation. Default is 0.001.

    Returns
    -------
    float or ndarray
        The derivative of f at point x, approximated using the forward
        difference quotient.
    """

    # If h gets too small, the computation becomes unstable
    if h < 1e-5:

        raise ValueError('h values less than 1e-5 are unstable. Consider increasing h')

    return (f(x + h) - f(x)) / h

这个函数接收一个纯函数%2C%20and)作为输入,该函数仅有一个变量,并在 x 参数指定的点处近似计算导数。这个函数的实际逻辑只存在于一行代码中,但它能在可接受的误差范围内近似计算多个导数。

要观察这个过程,我们可以近似计算二次函数的导数。根据幂法则(或通过计算差商的极限),我们知道:

二次函数的导数。图片来源:作者。

例如,函数在 x = 3 处的导数是 2*3 = 6\。以下代码近似计算了 x = 3 处的二次函数的导数:

# Define the quadratic function
def f(x):

    """
    The quadratic function f(x) = x²
    """
    return x**2

# Define the input(s) to compute derivatives for
x = 3

# Define the value of h used to approximate the derivative
h = 0.001

# Approximate the derivative
print(derivative(f, x, h))

# Output: 6.000999999999479

通过将 h 设置为接近 0 的小正数,我们提取出一个接近真实值的导数近似值。随着 h 变得更小(直到达到某个容忍度),近似值会变得更加准确:

# Define a smaller h value to get a more accurate approximation
h = 1e-5

# Take the derivative
print(derivative(f, x, h))

# Output: 6.000009999951316

我们可以将这种行为可视化,以观察 h 值逐渐减小时的效果:

随着 h 的值减小,导数的近似值变得更准确(趋近于 6)。图片来源:作者。

另一个有趣的例子涉及三角函数。根据导数的定义,我们知道以下内容

sin(x)的导数是 cos(x)。图片来源:作者。

使用导数函数,我们可以进行如下近似:

import numpy as np
import matplotlib.pyplot as plt

# Define the h value
h = 1e-5

# Define the domain
x = np.linspace(-10, 10, 1000)

# Approximate the derivative of sin(x) (should be close to cos(x))
f_prime = derivative(np.sin, x, h)

# Plot sin(x) vs the approximated derivative
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(x, np.sin(x), label='f(x) = sin(x)')
ax.plot(x, f_prime, label="f'(x) ~= cos(x)")
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('Approximated derivative of sin(x) (close to cos(x))')
ax.legend()
plt.show()

sin(x)的近似导数类似于 cos(x)。图片来源:作者。

最后的思考

本文实现的导数近似称为“前向差分商”,它是进行数值微分的众多方法之一。需要注意的是,这种近似是不完美的,因为它在h的值很小时往往会崩溃。此外,在实际应用中,我们可以计算封闭形式函数的精确导数,避免近似的需求。本文的目的是帮助读者通过代码了解数学如何运作,并希望增强他们对导数的直觉。我鼓励读者在不同的函数上测试代码,探索其他导数近似方法,并理解各自的优缺点。感谢阅读!

参考文献

  1. 数值导数acme.byu.edu/0000017a-17ef-d8b9-adfe-77ef210e0000/vol1b-numericalderivatiation-2017-pdf

学习网络游戏

原文:towardsdatascience.com/learning-network-games-29970aee44bb

图论机器学习与博弈论

网络游戏是一个强大的工具,用于建模在网络上进行的个体或组织之间的战略互动,其中一个玩家的回报不仅依赖于他们自己的行动,还依赖于他们邻居的行动。这类游戏在经济学和社会科学中有广泛的应用,包括研究社会网络中的影响扩散、金融市场的动态以及国际关系中的联盟形成。网络游戏的研究通常假设已知底层网络结构,但这往往是不切实际的。最近,机器学习方法被提出来解决这个问题,通过利用玩家的观察行为来学习底层网络结构。在这篇博客文章中,我们概述了一种新颖的方法,它使用类似 Transformer 的架构来推断游戏的网络结构,而不需要明确知道与游戏相关的效用函数。

Michael BronsteinTowards Data Science Michael Bronstein

·发布在 Towards Data Science ·阅读时间 10 分钟·2023 年 4 月 20 日

--

基于 Shutterstock 的插图。

这篇文章由 Emanuele Rossi 共同撰写,并基于论文 E. Rossi 等人,"学习推断网络游戏的结构"(2022 年)ICML,与 Federico Monti、Yan Leng 和 Xiaowen Dong 合作。

博弈论是一个用于建模和分析多个决策者相互作用的情况的数学框架,其中每个决策的结果都依赖于所有相关玩家的行动。在网络游戏中[1],玩家通过网络(图)相互连接,游戏的结果不仅依赖于玩家的策略,还依赖于网络的结构。每个玩家都试图最大化他们的效用函数,在网络游戏中,这个函数不仅依赖于他们自己的行动,还依赖于邻居的行动。

均衡行为 是指一组策略,在这种策略下,任何玩家都没有动机去改变自己的策略,前提是其他玩家的策略不变。换句话说,在均衡状态下,每个玩家的策略都是最优的,考虑到其他玩家的策略。在网络游戏中,均衡行为取决于图结构以及其他依赖于游戏的参数。

例如,考虑一个社交网络上的场景,个人可以决定在平台上花费多少时间。在这种情况下,他们的行为可能会受到网络上朋友的影响,这在玩家之间创造了战略上的相互依赖。例如,如果乔的朋友们在平台上花费了大量时间,乔可能会感受到自己使用平台的更大收益。

在不同的环境下,乔是一个电子商务平台的用户,他在决定是否购买一本书。如果他的朋友已经购买了这本书,乔可能会不太愿意买,因为他可以向朋友借这本书。这些例子说明了网络游戏中的行动如何受到邻近玩家行动的影响,从而导致战略上的相互依赖和均衡行为的出现。

网络游戏的示例。左侧: 如果乔的朋友们也在社交应用上花时间,乔可能会在应用上花更多的时间。右侧:如果他的朋友已经买了那本书,乔就更没有购买的动力,因为乔可以借到书。

从行动中推断网络

在上述例子中,我们假设了解乔的朋友,即游戏的网络结构。然而,在许多情况下,潜在的网络结构并不可直接获得。相反,我们可能只能观察到由代理之间的互动所产生的均衡行为。在这些情况下,一个关键问题是我们是否可以仅根据这些均衡行为重建网络结构。了解网络结构可以帮助预测行为并规划基于网络的干预措施,如营销活动或信息扩散。

之前已经显示,在关于效用函数和游戏类型的特定假设下,可以重建控制网络游戏的图 [2]。然而,这些假设可能不切实际,特别是当对正在进行的游戏了解甚少时。为此,在最近的一篇论文 [3] 中,我们开发了一种不需要关于效用函数形式的假设的方法,可以应用于广泛的网络游戏。

我们首先研究三种常见的网络游戏类型,线性二次线性影响Barik-Honorio [4]。这三种游戏因效用函数的形式不同而异,从而导致图中行动的平滑程度不同:

eᵤᵥ = bᵤ xᵤ — ½ xᵤ² + β Σᵥ aᤥᵥ xᵤ xᵥ,

xᵤ 是玩家 u 采取的连续行动,bᵤ 是玩家的 边际效益β 是表示网络中邻居行为之间依赖强度的游戏参数,aᵤᵥ 是表示 uv 之间连接强度的图的邻接矩阵中的条目。

以先前提到的在社交平台上花费时间的情景为例,方程的第一项将捕捉使用平台的个人效益(例如保持最新消息),第二项将表示这样做的成本(例如减少时间做其他更重要的事情),第三项将捕捉与朋友行为的相互依赖。特别是,如果用户在朋友也花时间使用该应用时有动机花更多时间使用它,则 β 将是正值 [5]。

线性二次 游戏的纯策略 纳什均衡

x* = (I — βA)ᐨ¹ b

x* 是一个维度为 n 的向量(等于玩家数量或图的节点数),A 是图的未知 n×n 邻接矩阵,b 是玩家的 n 维边际效益向量。

类似的公式可以推导出 线性影响Barik-Honorio 游戏的平衡行动 x* 的公式,将这三种游戏泛化为形式 [6]

x* = f(A) h(b),

函数 f(A) 反映了网络中邻居行为的影响,并编码了游戏的具体效用函数,相反,h(b) 仅受个体特征影响,如单个玩家的边际效益。

游戏理论文献中常见的三种不同类型的具体实例。颜色表示玩家采取的行动,在这种情况下是归一化在 -1 和 +1 之间的连续值。

在我们的论文中,我们进一步展示了[7]玩家的动作包含图的谱信息,确认了仅从动作中重建图结构的可能性,并证明了我们下面概述的方法的合理性。

一种机器学习方法

我们将推断游戏网络结构视为一个机器学习问题。我们训练一个模型,将玩家的动作映射到游戏的网络结构中,而没有对基础效用函数的任何先验知识。为此,我们收集了来自使用相同效用函数的游戏中的动作和网络对(X, A)的数据集(尽管我们不知道这个函数)。这使我们能够避免对效用函数做出强假设,而是训练一个对其无关的模型。

这种方法在社会网络和决策数据仅存在于小规模人群的场景中特别有用,我们旨在学习从决策到较大人群网络结构的映射。例如,政府、公共机构和研究人员可以通过让个人提名他们的朋友来收集小规模人群的社会网络数据,然后使用提出的方法以成本效益的方式学习较大人群的网络互动。

我们的机器学习模型具有一个编码器-解码器架构,该架构对玩家和游戏的排列是不可变的,对应于n×K矩阵X的行和列,其中K表示游戏的数量。为实现这一点,我们修改了一个变换器模型,该模型在节点集合上自然是排列不变的,但在游戏集合上则不是。我们的编码器为每个玩家生成K个向量,如下所示:

玩家u在游戏k中的标量动作xᵤₖ首先通过一个非线性变换,得到一个F维向量。

yᵤₖ = ReLU(xᵤₖw + b).

我们接着计算未归一化的注意力分数。

sᵤᵥ = Σₖ yᵤₖW W yᵥₖ

通过首先计算每个游戏的分数,使用带有查询和键权重矩阵WW的“学习点积”,然后对游戏进行求和,得到玩家u与玩家v之间的注意力分数。注意力分数

αᵤᵥ = softmax(uᵤᵥ)

通过在第二维度上对未归一化分数进行 softmax,得到最终的F维嵌入。

zᵤₖ = φ αᵤᵥyᵥₖ)

节点u在游戏k中的yᵤₖ向量,通过将其他节点的yᵤₖ向量按注意力分数加权后聚合得到,然后通过一个 2 层 MLP φ进行处理。

解码器通过聚合玩家uvK个向量来输出邻接矩阵每个条目的概率。这是通过对每个游戏的两个向量进行点积并求和,然后将结果输入多层感知机(MLP)来完成的。

âᵤᵥ = ψ zᵤₖz)

其中 ⊙ 表示点积,ψ 是一个 2 层 MLP。

生成的编码器在游戏集上也是排列不变的。

图示表示了我们模型的编码器-解码器架构。包含玩家行为的nxK 输入矩阵X被编码为nxFxK 张量Z,其中zᵤₖ是游戏k中节点u的嵌入,基于在同一游戏中其他玩家的行为进行计算。Z随后被解码为n×n的邻接矩阵Â,其中条目âᵤᵥ包含了uv之间存在边的概率。

实验结果

我们进行了实验,以验证我们的方法在从玩家行为中学习网络结构的有效性,使用了合成数据集和真实数据集。作为基线,我们使用了 DeepGraph [9](我们知道的唯一机器学习方法),特定于游戏类型的优化方法,以及节点之间行为的简单相关性和反相关性。

在合成数据集上,我们的模型(称为NuGgeT)在各种不同的游戏和图类型中始终优于以前的方法。

我们报告了在Linear Influence游戏中的结果(见论文中的Linear QuadraticBarik-Honorio),在三种不同类型的合成图(Watts–Strogatz,Erdős–Rényi 和 Barabási–Albert)以及不同的边际效益平滑度(这种游戏的超参数)下。我们的方法称为 NuGgeT,始终优于基线方法。

我们模型在学习映射的性能取决于可用的游戏数量和训练图的数量,我们进行了消融实验来评估这两个因素。通常,更多的游戏和图对我们的方法有利。然而,我们观察到,在大多数情况下,模型性能在大约 100 个游戏和 500 个图时趋于饱和。

我们进一步在两个真实世界的数据集上验证了我们的方法:Indian Villages 数据集[10]和Yelp Ratings 数据集[11]。前者包含来自印度 75 个村庄的社会网络调查数据。每个村庄构成一个社会网络图,其中节点是家庭,边是自报的友谊。我们将房间数量、床位数量以及家庭在家庭相关决策中的其他决定视为行动。理由是,如果邻居采用了某种设施,村民往往通过采取相同措施来获得更高的收益,即遵循社会规范。

Yelp Ratings 数据集由用户对商家的评分和用户之间的社交连接组成。我们从原始数据中提取了 5000 个表示社区的子图,其中的行为是用户对 22 类商业的平均评分。

在两个现实世界的数据集上,NuGgeT 优于以往的方法,展示了我们的方法在游戏效用未知的情况下的有效性。特别是在印度村庄数据集上,NuGgeT 的增益尤其显著,而竞争方法 DeepGraph 完全无法学习。我们推测这可能是因为 NuGgeT 更加数据高效,得益于其内置的不变性,这一点通过对训练图数量的消融实验得到了确认。

NuGgeT 在我们测试的两个现实世界数据集上优于以往的方法,确认了其在游戏效用函数未知的情况下的有效性。

总之,我们的论文突出了博弈论与图机器学习之间的有益联系,特别是在网络游戏的背景下。通过开发一种新的机器学习方法来从观察到的游戏结果中推断网络结构,我们展示了利用博弈论思想来提升机器学习的潜力,反之亦然。展望未来,还有广阔的机会进一步探索网络游戏与图神经网络之间的联系,为这些领域带来更多激动人心的发展。

[1] 参见例如 M. O. Jackson 和 Y. Zenou,网络上的游戏 (2014),博弈论手册 4:95–163 以获取概述。

[2] Y. Leng 等,在网络上学习二次博弈 (2020),ICML

[3] E. Rossi 等, 学习推断网络游戏的结构 (2022),ICML

[4] A. Barick 和 J. Honorio,对连续动作图形游戏高效学习的可证明计算和统计保证 (2019),arXiv:1911.04225。

[5] 第一个项表示采取更大行动的边际收益,第二个项表示采取行动的成本,而第三个项表示与邻居行动的关系。如果β为正,则玩家采取更高行动的激励会随着其邻居也采取更高行动的数量增加而增加,这称为战略互补关系。另一方面,如果β为负,则玩家采取更高行动的激励会随着其邻居采取更高行动的数量增加而减少(战略替代关系)。

[6] 在这个公式中,选择 f(A)=(I — βA)ᐨ¹ 和 h(b)=b 产生一个线性二次博弈,f(A)=Aᐨ¹ 和 h(b)=b 产生一个线性影响博弈,f(A)=u₁(A 的最大特征向量)和 h(b)=1 产生一个 Barik-Honorio 类型的博弈。

[7] 我们论文中的第 3.3 节 [3]。

[8] 参见 “The Illustrated Transformer” 博客文章,以获得对 Transformer 及查询和权重矩阵作用的直观解释。

[9] E. Belilovsky 等人,《学习发现稀疏图模型》(2017),ICML。

[10] 数据集与 A. Banerjee 等人的论文《微型金融的扩散》(2013),《科学》341(6144) 配套。论文的两位作者(Abhijit Banerjee 和 Esther Duflo)获得了 2019 年经济学诺贝尔奖

[11] Yelp 开放数据集

我们感谢 Federico Barbero Fabrizio Frasca,以及 Francesco Di Giovanni 对本文的校对。有关图上深度学习的更多文章,请查看 Michael 的 其他文章 ,在 Towards Data Science 上 订阅 他的文章和 YouTube 频道,获得 Medium 会员资格,或在 Twitter* 上关注他。*

以正确的方式学习新的数据科学技能

原文:towardsdatascience.com/learning-new-data-science-skills-the-right-way-2346d039366e?source=collection_archive---------4-----------------------#2023-08-17

TDS EditorsTowards Data Science TDS Editors

·

关注 发布于 Towards Data Science · 以 Newsletter 形式发送 ·3 分钟阅读 ·2023 年 8 月 17 日

--

我们倾向于将学习曲线视作整齐、平滑、不断上升的线条。然而,只要仔细观察任何学习旅程,你会发现沿途有无数的低谷和平台期:实际上,即便是经验丰富的专业人士在面对新工具或工作流程时也会感觉像个初学者。

本周,我们整理了一些近期最受欢迎的教程和入门指南。这些内容对数据和机器学习从业者几乎没有任何先决条件;它们涵盖了从深度学习到异常检测的不同主题,但都致力于耐心解释、具体细节和专家背景。若你感到学习乏力,下面的亮点必定能帮助你走出困境。

  • 有时候从头开始尝试是有意义的:为什么不从零开始实现并训练一个卷积神经网络(CNN)呢?你将有Betty LD的逐步指导;如果你一直在考虑尝试 PyTorch Lightning 库,这正是你的机会。

  • 看来今年夏天是开源大语言模型的季节,每几天就有新的模型出现。Donato Riccio的新文章是一个面向初学者的 Llama、Alpaca 等开源大语言模型的指南,涵盖了微调和使用这些 LLM 的基础知识。

图片由Giulia Bertelli拍摄,来源于Unsplash

  • 想更深入了解开源 LLM 资源?无需浮具:Shawhin Talebi将帮助你熟悉 Hugging Face 的 Transformers 库,它提供了“一个轻松且无需费用的方式来使用各种开源语言模型。”

  • 想要对传统的机器学习方法有新的视角吗?Evie Fowler的最新贡献概述了使用异常检测方法解决不平衡结果类别带来的问题在监督学习中的好处。

为什么停在这里?如果你仍然有学习的兴致,我们还有一些精彩的读物你不容错过:

  • Ruth Eneyi Ikwu分析了未经检查的共线性如何导致意外偏差,通过对一个问题数据集的批判性审视。

  • 有没有实际路径实现可持续 AI? Leonie Monigatti 的 Kaggle 竞赛获奖文章调查了提高深度学习模型在生产中效率的潜在方法。

  • 如果你对音频和音乐数据充满热情,Naman Agrawal 的时间和频域特征提取的深度解析 是必读的。

  • Mark Ridley 提供了关于生成式 AI 的崛起如何影响产品工程团队的深思分析。 (系好安全带:这是这个优秀系列中的第一篇文章。)

  • 数据科学和 ML 角色描述不断演变;Stephanie Kirmer 聚焦于 ML 工程师,并探讨他们的出现是否可能是数据科学中“粉色领带”现象的症状。

  • Pol Marin 继续探索体育分析中的有趣话题;他最新的研究对象是:FC 巴塞罗那的防守(及其不满之处)。

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

直到下一个 Variable,

TDS 编辑部

在职学习 Python 数据科学 第一部分:哲学

原文:towardsdatascience.com/learning-python-for-data-science-on-the-job-part-1-philosophy-6e2aedc4e041

关于如何在没有正式教育的情况下成为一名熟练数据科学家的实用建议和理念

Nicholas LewisTowards Data Science Nicholas Lewis

·发表于 Towards Data Science ·8 分钟阅读·2023 年 1 月 14 日

--

图片由 Fatos Bytyqi 提供,来自 Unsplash

随着数据科学在各个行业中的普及,值得注意的是,许多自学成才的程序员已经进入了职场。很多人正在寻求职业转型,希望用数据科学这一强大工具来补充自己的专业知识,或者只是对所有的炒作感到好奇。尽管我回到学校接受了一些正式的数据科学教育,但在本科阶段我从未参加过编程课程,在回到学校之前我已经在数据科学领域工作了 2 年;我绝对会认为自己是一个自学成才的程序员!自从我开始使用 Matlab 和 Python 探索系统生物学应用中的优化技术以来,已经快 10 年了。回顾过去,我想分享一些我尝试过的东西——有些成功了,有些失败了——这些都帮助我在工作中学习 Python(和 Matlab)编程以及数据科学。

本文是关于在没有正式教育情况下学习数据科学的两部分系列文章中的第一部分。第一部分是我经验的反思,提供了一些实践建议,包括尝试和可能需要避免的事项(你会发现我不喜欢 YouTube 教程!)。第二部分 通过一个简单的线性回归问题,分别在 Excel 和 Python 中进行操作,讲解了一些基本的 Python 语法,并比较了每种方法的优缺点。希望你喜欢!

入门指南

我仍然清楚地记得我作为新生研究助理的第一周,周围都是以代码为生、为梦的研究生。我不明白为什么有人能理解像print('Hello World!')这样的简单代码。这是为什么?我究竟要如何做更多高级的事情?

我很快意识到,编程基本上是用特定的词汇和结构表达的逻辑思想,以产生结果。它与在计算器中输入内容是一样的,只是更强大。唯一困难的部分是学习语法,即将你脑中的逻辑转换为计算机可执行任务的关键字和字符。

这是我学习语法时的一些有效方法:

  • Google 是你最好的朋友!要学习如何做一个for循环,只需 Google “Python for loop example。”你可能会看到一些很好的教程,详细讲解使用哪些关键字和字符。随着你问题的深入,你会发现 Stack Overflow,这是一个人们提出问题并从社区获取答案的论坛。你会看到很多创造性的方法来做同样的事情,并最终培养对常见做法的敏感性。

  • 从你熟悉的事物开始对待 Python。例如,我们可能都用过计算器,因此首先就像使用计算器一样输入方程式。然后开始增加复杂性,比如分配变量、打印结果和提高计算复杂性。本系列第二部分的练习是一个很好的深入案例研究。

  • 学会阅读文档。不幸的是,没有一个好的标准来规范人们编写文档的方式,有些文档写得非常差。但是每当你开始使用一个新包(如 pandas 或 numpy)时,如果你对如何做某事有疑问,首先查看它们的文档,然后再 Google 查找答案。

关于入门的其他几点:

  • 确保对你的代码进行注释!这可能是你应该学习的第一个语法。Google “how to comment code in Python。”注释是不会被执行的代码行,所以你可以解释你写代码的逻辑和理由。不时回顾你早期的代码,认识到你的进步。

  • 熟练调试。这可以简单到添加print语句,以便你能看到程序正在做什么,并检查是否符合你的预期。

  • 阅读!大量阅读!当我开始我的数据科学工作时,我对 Python 生疏,并且从未进行过数据科学。所以我每天工作开始的第一个小时都会阅读教程并进行副项目实践。这对于我了解有哪些选项非常有价值。在阅读的同时,考虑你所阅读的内容如何应用到你的工作项目中。我发现了很多创新方法的灵感,这些灵感是通过阅读中的想法激发的(例如,参见我关于模拟 PID 控制器的系列)。

  • 有很多平台可以运行 Python 代码。目前,对于初学者(以及那些尝试新想法的高级用户)来说,Jupyter notebook 是最好的选择。如果你在课堂上学习,你的老师很可能会使用 Jupyter notebook,因为它更直观,并且具有简单文本文件所没有的内置功能。

使用入门教程和材料

在我看来,一般的教程或 YouTube 视频不值得花时间。它们对我不起作用,而且过于笼统,实际上并不具备帮助性。如果你完全是编程新手,它们可能对你了解语法的运作方式有些帮助。如果你使用这些教程,你必须积极参与,边看边写代码。我们大多数人无法仅仅通过观看和吸收来有效学习……你可能会观看一个 4 小时的教程,但如果你没有积极参与,教程结束后你可能不会成为一个更好的程序员。

我通过沉浸在我关心的项目中来学习编程。我会获取一些起始代码,然后逐行阅读以理解它。我会添加print语句来更好地了解发生了什么。接着,我会开始进行一些小的调整和定制。即使现在,我几乎从不从头开始编写代码;而是会找到一些类似的代码(通常是我之前写过的,但作为起点,可以从在线资源开始),复制粘贴,然后根据我的特定项目进行定制。

从一个项目开始而不是教程有两个好处:首先,它给你一个你关心的任务,因此你会更投入(Towards Data Science 对于寻找项目演练非常好)。这也可能与你以后工作中要做的事情更相关。其次,它给你提供了定制代码以实现你想要的功能的机会,而不是仅仅生成数百万已经完成的代码。就像做一个有答案的学校作业和做一个没有单一正确答案的开放式工作项目之间的区别。你将能够认识到完成同一任务的不同方式。最终,你会开始认识到更高效的编码方法,但现在,更重要的是多加练习。

从基础开始

简单的事实是,没有人能在一夜之间成为 Python 大师。即使经过 10 年,我仍在不断学习。但是,有一件事真的很有帮助,那就是先从代码中的基本功能开始,然后再增加复杂性。这不仅是初学者编码的好框架,对高级编码者也是如此。实际上,当我最近进行求职时,从一次编程挑战中得到的最有价值的反馈之一就是在尝试解决边界情况之前,先从基本功能开始。想到如何编写一个完美工作的整个脚本可能会让人感到无从下手(我可能会争辩说,这几乎是不可能的),但只需稍加练习,你就可以编写一个完全正常的for循环或字典来完成大部分任务。

我说的是什么意思?考虑一个经典的编程挑战——将罗马数字转换为整数。有一些非常合理的规则,但与其试图一次性考虑所有规则,不如从基础开始,然后增加复杂性。首先编写一些代码,将每个罗马数字翻译为整数(例如 X = 10)。然后弄清楚如何编程处理一个较大值后跟一个较小值的情况(例如 XI = 11)。最后,编写代码处理一个较大值前面有一个较小值的情况(例如 IX = 9)。

发展自信

无论是教别人一门语言、一种乐器,还是任何其他技能,我总是告诉学生这一点:要有自信!你不是仅仅在学习小提琴,你是在演奏它。你不是仅仅在学习俄语,你是在说它。编程也是如此:你不是仅仅在学习编程,你已经在做了。当然,你在不断发展和学习,也许在不同的技能水平上。但即使作为一个初学的小提琴手,我仍然会说我在演奏小提琴,尽管那时的水平远不如今天。编程也是一样。虽然我比 10 年前知道的更多,10 年后我会比现在知道得更多,但这并没有减少我现在作为程序员的事实,就像我在 10 年前开始时一样。这种心态的关键在于它让你有信心去展现自己,去解决困难的问题(或说语言或演奏乐器)。我们通过实践、挑战自己、以及拥有自信来学习。是的,你绝对应该尽可能多地寻求反馈,但不要因为害怕犯错而感到瘫痪。不要因为不知道如何做而给自己找借口去接受任务。整个要点在于你正在学习,甚至很可能是在工作中学习。

案例研究和结论

当我开始我的第一份数据科学工作时,我给自己设定了一个目标:我想比我对 Excel 的了解更深入地掌握 Python。我在 Excel 方面非常拿手,但即使是使用 VBA,也确实开始遇到一些限制。我们中的大多数人对 Excel 很熟悉,甚至是专家;第二部分的案例研究是一个绝佳的机会,让你通过在 Excel 和 Python 中解决问题来初步涉猎。我希望你不仅能够看到它们的相似之处和优缺点,还能建立起自信。

希望这些对你有所帮助。我知道我们每个人的学习方式不同,你可能会尝试 YouTube 教程或虚拟课堂,并觉得它们是最有价值的。我要表达的观点是,你可以在工作中学习这一宝贵的技能,并且培养出与那些接受过正式编程教育的同行一样的信心。像往常一样,你可以通过LinkedIn与我联系,也可以随时关注我在Towards Data Science上的数据科学案例研究定期更新。

数据科学中的 Python 学习 实战第二部分:练习

原文:towardsdatascience.com/learning-python-for-data-science-on-the-job-part-2-practice-b4ece80488da

Excel 和 Python 中线性回归的并行案例研究

Nicholas LewisTowards Data Science Nicholas Lewis

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

--

图片由 Hitesh Choudhary 提供,来源于 Unsplash

欢迎阅读这系列关于在工作中或没有正式教育背景下学习 Python 和数据科学的文章的第二部分。第一部分讲述了我过去 10 年在工作中和正式教育环境中学习的经历。如果你对学习的哲学和一些激励自己开始学习的想法感兴趣,可以看看这篇文章。或者,如果你像我一样通过实际操作具体例子来学习,继续阅读吧!

问题定义

所有相关文件可以在我的 Github 上找到。不过,我建议你完全从头开始,按照这里提供的代码块和截图进行操作。

在这个案例研究中,我们将进行一个简单的线性回归。我们有两类输入数据,基于这些输入,我们希望训练一个线性模型来预测一个输出,基于实际观察到的数据。在 data.csv 文件中,这些输入被称为 x1x2,观察到的数据被称为 y。模型的形式为 Ax1 + Bx2 + C = y。你可能会注意到 x2 = x1²。这是有意为之的,随着你在数据科学中的进步,你可能会想要记住这个小技巧:你可以通过简单地平方或取已有输入的对数来创建额外的输入(在数据科学中,输入通常被称为特征)。

设置

首先,打开 Excel 电子表格和 Jupyter notebook。通常,你可能会开始将原始数据直接复制并粘贴到 Excel 文件中,但对于这个特定的问题,我们首先要做一些类似于你在 Python 中常做的事情。你需要求解器加载项来解决这个问题。如果你从未使用过求解器加载项,请按照这里的说明进行操作。启用加载项为 Excel 提供了额外的功能,而这些功能不是标准配置的一部分。

在 Excel 中你很少这样做,但在 Python 中你几乎总会做类似的事情。启用额外功能是通过导入库或幕后代码来实现的,这些代码允许你在 Python 中执行更强大和高效的命令。你可以通过输入 import [library_name] 来实现。这告诉 Python 你将使用指定的库。你可以选择为库起一个缩短的名字。例如,你可以说 import pandas as pd。每次使用 pandas 库的某些功能时,你可以直接输入 pd 代替 pandas。虽然你可以给库起任何名字,但你会很快发现大多数包都有常见的缩写。

许多库在你下载 Python 时已经预装,就像 Excel 已经有按钮允许你制作图表或执行数学函数一样。你可能不会在导入 pandas(用于数据处理)和 matplotlib(用于绘图)时遇到问题。不过,你可能需要使用 pip 安装 scikit-learn(或 sklearn)库,就像你需要做一些特别的工作来获取 Excel 求解器加载项一样(巧合的是,sklearn 在这个练习中将以类似于 Excel 求解器加载项的方式使用)。你的第一段代码应如下所示:

import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

来自 sklearn 的这一行代码看起来有些不同。这是因为 sklearn 是一个庞大的库(可以查看他们的网站和文档),而我们只会使用其中的一小部分。因此,我们在这一行代码中告诉 Python 只从 scikit-learn 中导入那部分特殊功能,而不是全部。可能会是一个无尽的坑,但请注意,以下代码块做了相同的事情:

import sklearn
lr = sklearn.linear_model.LinearRegression()
from sklearn.linear_model import LinearRegression
lr = LinearRegression()

加载数据

相比于 Excel,设置这些内容需要做很多工作,但这正是让 Python 更加多才多艺的因素之一。现在,你将数据加载到你的程序中。在 Excel 中,你可以直接从 data.csv 文件中复制并粘贴。在 Python 中,你可以将其加载为数据框(想象成一个超级增强的 Excel 表格)。你的下一行代码应如下所示:

df = pd.read_csv('data.csv')

这一行代码告诉 pandas 读取 data.csv 文件中的值,并将其存储在 df(数据框的缩写)变量中。你应该确保 data.csv 文件在与你的 Jupyter notebook 相同的目录中,否则你需要指定文件的路径。

你可能会对无法实际看到每行代码的运行情况感到沮丧。作为视觉生物,这也许是编程的一个缺陷。然而,当你编写代码时,你可以方便地显示输出。例如,在一个新的代码块中输入 df 并执行(按 ctrl+enter),看看会发生什么。然后尝试 df.head()。最后,尝试 df.head(3)。你注意到每个的区别了吗?这就是编码相对于使用电子表格的灵活性和效率的体现。代码简洁而强大,一旦你克服了初始可视化的障碍,你可能会发现编码更为可取。顺便提一句,当你开始处理包含数百万条记录的数据集时,你会更加感激它;在 Excel 中对这些数据集进行操作真的会让它变得非常缓慢,而编码则能保持流畅(直到你处理非常大的数据集)。

模型设置

到目前为止,我们的进展相当缓慢,但希望接下来的部分能真正突出编码相对于电子表格的优势、灵活性和速度。让我们先通过 Excel 进行问题设置,然后看看如何用仅几行 Python 代码做到同样的事情。

在 Excel 中,我们将通过设置平方和系统来找到模型的系数。创建一个新的单元格框来跟踪系数,并对系数做一个猜测值。你可以将所有猜测值初始设置为 1,但有时你的猜测确实对结果有很大影响(Python 更方便,不需要提供初始猜测,虽然它提供了这个选项)。然后,按照截图中的模型编程新列单元格,以进行模型预测。

预测列编程的截图。图片由作者提供

最后,生成一个新列,称为“平方误差”列,按照下面所示进行计算。在打开 Solver 之前的最后一步是求出所有平方误差的总和——因此叫做“平方和”目标,也称为 l2-范数。(你可以通过转到 E23 单元格并输入公式 =SUM(E2:E22) 来完成此操作。)

计算平方误差的公式截图。图片由作者提供

我们终于完成了问题设置。我不知道你在 Excel 中的熟练程度或你是否使用过 Solver,但以这种方式进行练习的目的是展示在 Python 中这个过程有多么简单。这不是关于 Excel Solver、线性回归或平方和为何有效的教程(尽管我可以讨论这些!),所以我不会在这里深入细节。我们可以用仅 3 行代码在 Python 中完成所有设置:

X = df[['x1','x2']].values
y = df['y'].values
model = LinearRegression()

模型解决方案

让我们退一步,记住我们要做什么(我们已经很接近了!)。我们想开发一个模型,允许我们基于两个输入特征 x1x2 预测一个值 y。我们假设模型是线性回归,形式为 Ax1 + Bx2 + C = y。看起来我们走了一条绕路的路,但我们只差一步。在 Excel 中,打开 Solver 对话框,并按如下方式填写(特别注意确保未选中限制为正值的复选框)。运行程序,你会看到屏幕上的所有变化。你会在对应的单元格中看到你的 A、B 和 C 值。

填写完表格后再按“解决”的截图。图片由作者提供。

我们将回到那个黄色框,展示类似的内容在 Python 中。但为了在 Python 中设置问题,你只需写一行代码即可完成所有工作:

model.fit(X,y)

再次,最明显的区别是你在 Python 中看不到任何不同的内容。但你实际上已经有了解决方案。如果你深入文档,你会发现你实际上可以输出这些值。对于线性回归,你可以通过几个打印语句找到这些值,如下所示:

print('Coefficient A:',model.coef_[0])
print('Coefficient B:',model.coef_[1])
print('Coefficient C:',model.intercept_)

你的值应该匹配!所以迅速回到那个黄色框。它基本上是在问你的 A、B 和 C 系数是否都应该是正数。有时这很重要,特别是当你建模的系统有真实物理意义且系数受到自然现象限制为正数时。如果你在 scikit-learn 的 LinearRegression 文档中找找,你会发现你可以在初始化模型时传递一个参数来做同样的事情。它看起来是这样的:

model = LinearRegression(positive = True)

绕道的重点是展示编程中最不直观的事情之一:选项是存在的,你只需要找到它们!没有像 Excel 中那样简单的可视化复选框,但它们确实存在!文档中告诉你的默认值类似于在打开 Solver 时复选框是否被选中的情况。

模型预测

很好,现在我们有了一个有效的模型。我们怎么进行预测呢?假设我们想知道 x1 = 0.65 和 x2 = 0.65² = 0.4225 的预测值。在 Excel 中,你需要把这些值放在一些新的单元格中,然后将方程编程到另一个单元格中以获得答案,就像下面的截图一样。

编程新的单元格以生成新数据的预测。注意,系数值的单元格已被赋予唯一的名称(稍后讨论)。图片由作者提供。

在 Python 中,你可以通过输入以下代码来做完全相同的事情:

x1_predict = 0.65
x2_predict = x1_predict ** 2 # Careful to not use the ^ symbol to square values!
X_predict = [[x1_predict, x2_predict]]
y_predict = model.predict(X_predict)

这可能看起来有点繁琐,因为我们必须输入变量名。但这里有一个有趣的小事实:你知道你可以给 Excel 单元格唯一的变量名吗?这与定义 Python 变量并在未来的公式中使用它是一样的。谷歌搜索“excel 给单元格命名变量”或类似内容,然后你可以像截图中看到的那样重写你的公式。我几乎在实践中从不这样做,但由于这篇文章是关于将 Excel 与 Python 进行比较的,希望这能给你一个更好的理解。

绘图结果

在这一领域,Excel 可能看起来比 Python 要好很多,但那只是因为它有一个用户界面可以互动。要在 Python 中进行自定义,你必须输入一行代码。

我不会讲解如何在 Excel 中绘图——你可能对此已经很熟练了。在 Python 中,我们将使用 matplotlib,但要注意,还有许多其他选项可以探索,如 plotly、seaborn 和 altair。我想是时候让你自己动手了,所以我不会逐行讲解这段代码。相反,把它当作一个练习,试着理解每一行的作用。然后,查看文档,看看是否可以更改一些输入,使图形成为你自己的!

plt.plot(df['x1'], df['y'], '.', label = 'raw data')
plt.plot(df['x1'], yp, label = 'model prediction')
plt.xlabel('x1', size = 14)
plt.ylabel('y', size = 14)
plt.legend(fontsize = 12, loc = 'lower right')
plt.show()

摘要

你做到了!你完成了其中一个完整的代码,从零开始并最终得到可用的结果。希望你从中获得了很多关于如何学习编程的见解,而不是浪费时间在一个 4 小时的教程上,那种教程通常只是听过就忘。很酷的一点是,这项活动,虽然可能比你期望的时间要长,但在一两个月内你可以在 5 分钟内轻松完成。开发整个过程在 Excel 和 Python 中都花的时间比阅读这篇文章要少。

如果我可以总结一下我预期的最大挑战,那就是:我们是视觉动物,而编程却不是视觉的。Excel 很简单,因为有按钮和图形用户界面可供操作。你必须通过编程即时创建可视化图形。在学习过程中,额外的print语句、数据图表、数据表格等都不会出错。即使是更高级的程序员,你可能仍然会发现自己在 Excel 中浏览新数据以快速了解它。这是完全正常的!然而,我希望你像我一样,最终更倾向于使用 Python——不仅因为它更强大和多功能,还因为它变得更简单

一如既往,你可以通过LinkedIn与我联系,也可以在Towards Data Science上关注我,查看我关于数据科学案例研究的定期帖子。如果有些帖子类型比其他帖子更有用或更有趣,我很乐意听到。下次见!

为你的下一个 LangChain 项目学习基础知识

原文:towardsdatascience.com/learning-the-ropes-for-your-next-langchain-project-3529d585e320?source=collection_archive---------12-----------------------#2023-07-06

TDS 编辑Towards Data Science TDS 编辑

·

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

--

去年,大型语言模型作为有趣且偶尔搞笑的实验工具进入了主流。谁没有挑战 ChatGPT 发明一个新的“敲门笑话”或作一首关于小狗的莎士比亚十四行诗呢?

随着大型语言模型(LLMs)的强大功能越来越明显,它们的局限性也变得愈加显著。机器学习从业者和应用程序开发者很快意识到,对于那些无法获取最新信息且相互隔离的模型来说,你能做的事情是有限的。

幸运的是,我们已经有多个工具和平台旨在通过将 LLMs 连接到外部数据源和其他模型来克服这些限制,从而为其打开新的创造性用例。LangChain 已成为这一领域的领先选项之一,在发布后的几个月里,TDS 作者们一直在探索其能力和痛点。你将在我们本周汇集的实际动手资源中找到一些最好的作品——你准备好卷起袖子了吗?

  • Leonie Monigatti 的初学者友好入门书是任何刚刚迈出第一步 使用 LangChain 的人必读的文章。它包括了你需要了解的所有主要术语和概念的清晰解释。

  • 如果你准备更深入了解,Dominik Polzer 最近整理了一份全面的指南,提供了有关 LLMs 如何工作以及为什么一开始需要像 LangChain 这样的框架的有用背景,并且详细描述了逐步构建应用程序的过程

  • 开发人员已经提出了多种将模型、API 和 ML 平台与 LangChain 结合的方法。Wen Yang教程涵盖了 OPL,这是一种流行的技术栈,它将 OpenAI 模型(如 GPT-4)、Pinecone(一个向量数据库工具)和 LangChain 汇聚在一起,并展示了如何利用它创建具有特定领域知识的聊天机器人。

图片由 Svetlana GumerovaUnsplash 上拍摄

  • 如果你已经构建了一个特定模型的概念验证,但在尝试将其转变为真正的应用程序时感到困惑,Lily Hughes-Robinson 可以帮助你,提供了一个关于将项目重构为 LangChain 兼容的过程的简明解释。

  • 如果你还没准备好停止尝试,再来看另一个详细的项目过程怎么样?帅郭邀请我们一起跟随构建一个依赖于两个聊天机器人相互交流的语言学习应用程序的过程。

你们中的一些人可能需要暂时休息一下与 LLM 相关的内容;我们理解——我们在这里提供帮助!以下是一些值得探索的其他话题的精彩文章:

  • 我们以一系列引人深思的文章开启了新的一月,这些文章位于数据科学、机器学习和气候变化的交汇点。

  • AI 生成的音频通常需要大量的计算资源。克里斯托弗·兰斯库特的首篇 TDS 文章展示了如何在你的笔记本电脑上训练模型并生成音频波形扩散的声音—。

  • 多组分析可以成为医疗、HR、市场营销等领域的强大工具。劳拉·卡斯特罗-希洛的最新贡献是了解在实际场景中实现 MGA 的方法的绝佳起点。

  • 想要进行一个技术性(但易于理解)的深度探讨?佩吉·张提供了一篇关于开源搜索引擎 Lucene 中整数编码和压缩的深入阅读。

  • 如果你计划扩展你的计算机视觉工具包,德鲁夫·马塔尼纳雷什的关于 PyTorch 中图像分割的四部分系列将激励你停止拖延并开始编码。

  • 从既是数据科学家又是心理学家的角度接近 GPT 模型,马尔滕·格鲁滕多斯特仔细研究了“这些模型的行为,我们希望它们如何表现,以及我们如何推动这些模型像我们一样行为。”

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

直到下一个变量,

TDS 编辑

学习变压器代码第一部分第二部分——GPT 亲密接触

原文:towardsdatascience.com/learning-transformers-code-first-part-2-gpt-up-close-and-personal-1635b52ae0d7

深入研究通过 nanoGPT 生成预训练变压器

Lily Hughes-RobinsonLily Hughes-RobinsonTowards Data Science Lily Hughes-Robinson

·发表于 Towards Data Science ·13 分钟阅读·2023 年 7 月 13 日

--

图片由 Luca Onniboni 提供,来源于 Unsplash

欢迎来到我的项目的第二部分,在这里我将深入研究变压器和基于 GPT 的模型的复杂性,使用 TinyStories 数据集nanoGPT,所有这些都在一台老旧的游戏笔记本电脑上进行训练。在第一部分中,我准备了输入到字符级生成模型的数据集。你可以在下面找到第一部分的链接。

链接 [## 学习变压器代码第一部分

新系列的第一部分,我努力使用 nanoGPT 学习变压器代码

链接

在这篇文章中,我的目标是剖析 GPT 模型、其组件及其在 nanoGPT 中的实现。我选择了 nanoGPT,因为它是一个约 300 行代码的 GPT 模型的直接 Python 实现,且其训练脚本也同样易于理解。掌握必要的背景知识后,人们可以通过阅读源代码快速理解 GPT 模型。坦白说,当我第一次检查代码时,我缺乏这种理解。有些材料仍然让我感到困惑。然而,我希望凭借我所学到的知识,这个解释能够为那些希望直观理解 GPT 风格模型内部运作的人提供一个起点。

在为这篇文章做准备时,我阅读了各种论文。最初,我以为仅仅阅读奠基性论文“Attention is All You Need”就足以让我对这个领域有足够的了解。这是一个天真的假设。虽然这篇论文确实介绍了变换器模型,但后续的论文才将其适应于更高级的任务,如文本生成。“AIAYN” 只是一个更广泛主题的介绍。尽管如此,我想起了一篇 HackerNews 上的文章,其中提供了一个阅读清单以全面理解大语言模型。经过快速搜索,我找到了这篇文章在这里。我没有按顺序阅读所有内容,但我打算在完成这一系列之后重新审视这个阅读清单,继续我的学习之旅。

话虽如此,让我们开始吧。要详细理解 GPT 模型,我们必须从变换器开始。变换器使用一种称为缩放点积注意力的自注意力机制。以下解释来源于这篇关于缩放点积注意力的深刻文章,我推荐阅读,以便更深入地理解。基本上,对于输入序列的每个元素(第 i 个元素),我们希望将输入序列乘以一个加权平均值,该加权平均值由序列中所有元素与第 i 个元素的点积计算得出,然后应用 softmax,以使权重值在 0 和 1 之间。在原始的“Attention is All You Need”论文中,这些输入被称为查询(整个序列)、(第 i 个元素的向量)和(也是整个序列)。传递给注意力机制的权重被初始化为随机值,并随着神经网络中更多的迭代而学习。

nanoGPT 实现了缩放点积注意力,并将其扩展为多头注意力,这意味着同时进行多个注意力操作。它还将其实现为 torch.nn.Module,这使得它可以与其他网络层组合。

import torch
import torch.nn as nn
from torch.nn import functional as F

class CausalSelfAttention(nn.Module):

    def __init__(self, config):
        super().__init__()
        assert config.n_embd % config.n_head == 0
        # key, query, value projections for all heads, but in a batch
        self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
        # output projection
        self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias)
        # regularization
        self.attn_dropout = nn.Dropout(config.dropout)
        self.resid_dropout = nn.Dropout(config.dropout)
        self.n_head = config.n_head
        self.n_embd = config.n_embd
        self.dropout = config.dropout
        # flash attention make GPU go brrrrr but support is only in PyTorch >= 2.0
        self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention')
        if not self.flash:
            print("WARNING: using slow attention. Flash Attention requires PyTorch >= 2.0")
            # causal mask to ensure that attention is only applied to the left in the input sequence
            self.register_buffer("bias", torch.tril(torch.ones(config.block_size, config.block_size))
                                        .view(1, 1, config.block_size, config.block_size))

    def forward(self, x):
        B, T, C = x.size() # batch size, sequence length, embedding dimensionality (n_embd)

        # calculate query, key, values for all heads in batch and move head forward to be the batch dim
        q, k, v  = self.c_attn(x).split(self.n_embd, dim=2)
        k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
        q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
        v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)

        # causal self-attention; Self-attend: (B, nh, T, hs) x (B, nh, hs, T) -> (B, nh, T, T)
        if self.flash:
            # efficient attention using Flash Attention CUDA kernels
            y = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=None, dropout_p=self.dropout if self.training else 0, is_causal=True)
        else:
            # manual implementation of attention
            att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
            att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float('-inf'))
            att = F.softmax(att, dim=-1)
            att = self.attn_dropout(att)
            y = att @ v # (B, nh, T, T) x (B, nh, T, hs) -> (B, nh, T, hs)
        y = y.transpose(1, 2).contiguous().view(B, T, C) # re-assemble all head outputs side by side

        # output projection
        y = self.resid_dropout(self.c_proj(y))
        return y

让我们进一步剖析这段代码,从构造函数开始。首先,我们验证注意力头的数量(n_heads)是否能整除嵌入的维度(n_embed)。这非常重要,因为当嵌入被分成每个头的部分时,我们希望覆盖所有的嵌入空间而没有任何空隙。接下来,我们初始化两个线性层,c_attc_projc_att 是保存所有用于计算缩放点积注意力的矩阵的工作空间的层,而 c_proj 存储计算的最终结果。在 c_att 中,嵌入维度被放大三倍,因为我们需要为注意力的三个主要组成部分查询留出空间。

我们还设置了两个丢弃层,attn_dropoutresid_dropout。丢弃层根据给定的概率随机使输入矩阵的元素失效。根据PyTorch 文档,这旨在减少模型的过拟合。config.dropout中的值是指定样本在丢弃层中被丢弃的概率。

我们通过验证用户是否可以访问 PyTorch 2.0 来完成构造函数,PyTorch 2.0 拥有优化版本的缩放点积注意力。如果可用,类将使用它;否则,我们将设置一个偏置掩码。这个掩码是注意力机制的可选掩码功能的一部分。torch.tril 方法生成一个将上三角部分转换为零的矩阵。当与 torch.ones 方法结合使用时,它有效地生成一个由 1 和 0 组成的掩码,注意力机制使用这个掩码生成给定样本输入的预期输出。

接下来,我们深入研究类的forward方法,其中应用了注意力算法。最初,我们确定输入矩阵的大小,并将其分成三个维度:Batch 大小,Time(或样本数量),Corpus(或嵌入大小)。nanoGPT 采用批处理学习过程,我们将在检查使用此注意力层的变换器模型时详细探讨。现在,了解我们处理的是批量数据就足够了。然后,我们将输入x传递到线性变换层c_attn,它将维度从n_embed扩展到三倍的n_embed。该变换的输出被分割成我们的qkv变量,这些变量是注意力算法的输入。随后,view方法被用来将这些变量中的数据重新组织成 PyTorch scaled_dot_product_attention 函数期望的格式。

当优化后的函数不可用时,代码会默认使用手动实现的缩放点积注意力。它首先计算qk矩阵的点积,其中k被转置以适应点积函数,然后结果会按k的大小的平方根进行缩放。接着,我们使用先前创建的偏置缓冲区来遮蔽缩放后的输出,将 0 替换为负无穷。接下来,对att矩阵应用softmax 函数,将负无穷转换回 0,并确保所有其他值在 0 和 1 之间缩放。然后,我们应用一个丢弃层以避免过拟合,然后计算att矩阵和v的点积。

无论使用何种缩放点积实现,多头输出都会在通过最终的 dropout 层之前重新组织成并排的形式,然后返回结果。这是在不到 50 行的 Python/PyTorch 代码中完成的注意力层实现。如果你没有完全理解上述代码,建议花些时间复习它,然后再继续阅读文章的其余部分。

在我们深入了解整合所有内容的 GPT 模块之前,我们需要两个额外的构建块。第一个是简单的多层感知机(MLP)——在“Attention is All You Need”论文中被称为前馈网络——和注意力块,它将注意力层与 MLP 结合在一起,完成论文中表示的基本 transformer 架构。两者都在 nanoGPT 的以下代码片段中实现。

class MLP(nn.Module):
    """
    Multi Layer Perceptron
    """

    def __init__(self, config):
        super().__init__()
        self.c_fc    = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
        self.gelu    = nn.GELU()
        self.c_proj  = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
        self.dropout = nn.Dropout(config.dropout)

    def forward(self, x):
        x = self.c_fc(x)
        x = self.gelu(x)
        x = self.c_proj(x)
        x = self.dropout(x)
        return x

class Block(nn.Module):

    def __init__(self, config):
        super().__init__()
        self.ln_1 = LayerNorm(config.n_embd, bias=config.bias)
        self.attn = CausalSelfAttention(config)
        self.ln_2 = LayerNorm(config.n_embd, bias=config.bias)
        self.mlp = MLP(config)

    def forward(self, x):
        x = x + self.attn(self.ln_1(x))
        x = x + self.mlp(self.ln_2(x))
        return x

MLP 层,尽管在代码行数上看似简单,却给模型增加了一层额外的复杂性。实质上,Linear 层通过线性变换将每个输入层的元素与输出层的每个元素连接起来,传递它们之间的值。在上述代码中,我们从嵌入大小n_embed开始,作为参数的数量,然后在输出中将其四倍增加。这里的四倍增加是任意的;MLP 模块的目的是通过增加更多节点来增强网络的计算能力。只要在 MLP 开始时维度增加和在 MLP 结束时维度减少是等效的,得到相同的初始输入/最终输出维度,那么缩放数只是另一个超参数。另一个关键要素是激活函数。这种 MLP 实现包括两个线性层,使用 GELU 激活函数连接起来。原始论文使用的是 ReLU 函数,但 nanoGPT 使用 GELU 以确保与 GPT2 模型检查点的兼容性。

接下来,我们来研究 Block 模块。这个模块完成了我们在“Attention”论文中概述的变压器块。基本上,它通过一个归一化层将输入传递,然后传递到注意力层,再将结果加回输入中。这个加法的输出被再次归一化,然后传递到 MLP,并再次加回到自身。这一过程实现了“Attention”论文中描述的变压器解码器部分。在文本生成中,通常只使用解码器,因为它不需要根据输入序列以外的内容来调整解码器的输出。变压器最初设计用于机器翻译,这需要考虑输入标记编码和输出标记编码。然而,在文本生成中,只使用一个标记编码,从而消除了通过编码器进行交叉注意力的需求。nanoGPT 的作者 Andrei Karpathy 在他的视频中提供了这一点的全面解释,链接在此,视频地址在本系列的第一篇文章中。

最后,我们进入主要组件:GPT 模型。大约 300 行的文件中大部分内容都用于 GPT 模块。它管理一些有益的功能,如模型微调和为模型训练设计的实用工具(这是本系列下一篇文章的主题)。因此,我在下面展示了 nanoGPT 存储库中可用的简化版本。

class GPT(nn.Module):

    def __init__(self, config):
        super().__init__()
        assert config.vocab_size is not None
        assert config.block_size is not None
        self.config = config

        self.transformer = nn.ModuleDict(dict(
            wte = nn.Embedding(config.vocab_size, config.n_embd),
            wpe = nn.Embedding(config.block_size, config.n_embd),
            drop = nn.Dropout(config.dropout),
            h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
            ln_f = LayerNorm(config.n_embd, bias=config.bias),
        ))
        self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
        # with weight tying when using torch.compile() some warnings get generated:
        # "UserWarning: functional_call was passed multiple values for tied weights.
        # This behavior is deprecated and will be an error in future versions"
        # not 100% sure what this is, so far seems to be harmless. TODO investigate
        self.transformer.wte.weight = self.lm_head.weight # https://paperswithcode.com/method/weight-tying

        # init all weights
        self.apply(self._init_weights)
        # apply special scaled init to the residual projections, per GPT-2 paper
        for pn, p in self.named_parameters():
            if pn.endswith('c_proj.weight'):
                torch.nn.init.normal_(p, mean=0.0, std=0.02/math.sqrt(2 * config.n_layer))

    def _init_weights(self, module):
        if isinstance(module, nn.Linear):
            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
            if module.bias is not None:
                torch.nn.init.zeros_(module.bias)
        elif isinstance(module, nn.Embedding):
            torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)

    def forward(self, idx, targets=None):
        device = idx.device
        b, t = idx.size()
        assert t <= self.config.block_size, f"Cannot forward sequence of length {t}, block size is only {self.config.block_size}"
        pos = torch.arange(0, t, dtype=torch.long, device=device) # shape (t)

        # forward the GPT model itself
        tok_emb = self.transformer.wte(idx) # token embeddings of shape (b, t, n_embd)
        pos_emb = self.transformer.wpe(pos) # position embeddings of shape (t, n_embd)
        x = self.transformer.drop(tok_emb + pos_emb)
        for block in self.transformer.h:
            x = block(x)
        x = self.transformer.ln_f(x)

        if targets is not None:
            # if we are given some desired targets also calculate the loss
            logits = self.lm_head(x)
            loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)
        else:
            # inference-time mini-optimization: only forward the lm_head on the very last position
            logits = self.lm_head(x[:, [-1], :]) # note: using list [-1] to preserve the time dim
            loss = None

        return logits, loss

    @torch.no_grad()
    def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
        """
        Take a conditioning sequence of indices idx (LongTensor of shape (b,t)) and complete
        the sequence max_new_tokens times, feeding the predictions back into the model each time.
        Most likely you'll want to make sure to be in model.eval() mode of operation for this.
        """
        for _ in range(max_new_tokens):
            # if the sequence context is growing too long we must crop it at block_size
            idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:]
            # forward the model to get the logits for the index in the sequence
            logits, _ = self(idx_cond)
            # pluck the logits at the final step and scale by desired temperature
            logits = logits[:, -1, :] / temperature
            # optionally crop the logits to only the top k options
            if top_k is not None:
                v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
                logits[logits < v[:, [-1]]] = -float('Inf')
            # apply softmax to convert logits to (normalized) probabilities
            probs = F.softmax(logits, dim=-1)
            # sample from the distribution
            idx_next = torch.multinomial(probs, num_samples=1)
            # append sampled index to the running sequence and continue
            idx = torch.cat((idx, idx_next), dim=1)

        return idx

让我们从类的构造函数开始。不同的层被组装到 PyTorch 的 ModuleDict 中,这提供了一些结构。我们从两个 Embedding 层开始:一个用于标记嵌入,另一个用于位置嵌入。nn.Embedding 模块旨在稀疏地填充值,相比其他层模块,它优化了存储能力。接下来是一个 dropout 层,然后是 n_layer 个 Block 模块,这些模块构成了我们的注意力层,然后是另一个单独的 dropout 层。lm_head Linear 层将注意力 Blocks 的输出减少到词汇表大小,作为 GPT 的主要输出,除了损失值之外。

一旦层定义完成,在我们开始训练模块之前,还需要进行额外的设置。在这里,Andrej 将位置编码的权重链接到输出层的权重上。根据代码注释中链接的论文,这样做是为了减少模型的最终参数,同时提高其性能。构造函数还会初始化模型的权重。由于这些权重将在训练过程中学习,它们被初始化为高斯分布的随机数,并且模块的偏置被设置为 0。最后,利用了来自GPT-2 论文的修改,其中任何残差层的权重都按层数的平方根进行缩放。

在通过网络进行前向传播时,batch大小和样本数量(此处为t)从输入大小中提取。然后,我们在训练设备上为将成为位置嵌入的内容创建内存。接着,我们将输入标记嵌入到后续的标记嵌入中wte。随后,在wpe层计算位置嵌入。这些嵌入在经过丢弃层之前会被相加。结果会通过每一个n_layer块进行处理并进行归一化。最后的结果传递到线性层lm_head,该层将嵌入权重转换为每个词汇表中标记的概率评分。

在计算损失时(例如,在训练期间),我们通过交叉熵计算预测标记与实际标记之间的差异。如果没有损失,则为None。损失和标记概率都会作为前向传播函数的一部分返回。

与之前的模块不同,GPT 模块有额外的方法。对我们最相关的是生成函数,这对于任何使用过生成模型的人来说都很熟悉。给定一组输入标记idx、一个max_new_tokens数量和一个temperature,它会生成max_new_tokens个标记。让我们深入了解它是如何完成的。首先,它修剪输入标记以适应block_size(其他人称之为上下文长度),如有必要,从输入的末尾开始采样。接下来,这些标记被输入到网络中,输出会根据输入的temperature进行缩放。温度越高,模型越具创造性,也更容易产生幻觉。较高的温度还会导致输出更难预测。接着,应用 softmax 将模型输出权重转换为 0 到 1 之间的概率。然后使用采样函数从概率中选择下一个标记,并将该标记添加到输入向量中,该向量会被反馈到 GPT 模型中进行下一个字符的生成。

感谢你耐心阅读这篇详尽的文章。虽然检查注释过的源代码是理解代码片段功能的宝贵方法,但亲自操作代码的各个部分和参数仍是无可替代的。为此,我提供了来自 nanoGPT 库的完整model.py源代码的链接。

[## nanoGPT/model.py at master · karpathy/nanoGPT

最简单、最快的中型 GPT 训练/微调库。 - nanoGPT/model.py at master ·…

github.com](https://github.com/karpathy/nanoGPT/blob/master/model.py?source=post_page-----1635b52ae0d7--------------------------------)

在即将发布的文章中,我们将探索 nanoGPT 的train.py脚本,并在 TinyStories 数据集上训练一个字符级模型。请在 Medium 上关注我,以确保不会错过!

我利用了大量资源来创建这篇文章,其中许多资源已经在本文及上一篇文章中链接了。然而,如果我不与大家分享这些资源以便进一步探索任何话题或替代解释概念,我就会失职。

使用 GPT-4 和自定义 LangChain 脚本编辑。

生成性人工智能的法律与伦理视角

原文:towardsdatascience.com/legal-and-ethical-perspectives-on-generative-ai-5e7ba2308469

照片由 charlesdeluvio 提供,来自 Unsplash

探索 AI 生成内容的法律和伦理影响

Olivia TanuwidjajaTowards Data Science Olivia Tanuwidjaja

·发表在 Towards Data Science ·阅读时长 7 分钟·2023 年 8 月 25 日

--

2023 年是人工智能特别是生成性人工智能的崛起之年。该技术本身并不新颖;许多科技公司已经开发并推出了具有某些“生成性”组件的桌面/网页/移动应用程序——也许你还记得曾经玩过 SimSimi 聊天机器人 或使用过 Snapseed 的背景去除功能?

GPT-4 的推出于 2023 年 3 月引发了该技术的崛起和公众(非科技)兴趣。ChatGPT 用户界面提供了便捷的使用方式,而 GPT-4 的高质量内容生成能力引起了轰动,人们开始看到这项技术的利用潜力。

这种生成性人工智能的民主化——来自公众的快速采用和使用——带来了一些风险和影响,包括法律和伦理方面。从法律/监管角度来看,这仍然是未开发的领域,但了解这项技术的潜在影响仍然是一个好主意。

生成性人工智能的法律与伦理视角

生成性人工智能,通过人类的创造性使用,可以(有意或无意地)成为法律威胁的来源。到 2023 年 7 月,生成性人工智能的监管仍然是一个相对开放和有争议的领域,但风险是迫在眉睫的。

版权和所有权

引用维基百科,版权是一种知识产权,赋予其所有者对创作作品的复制、分发、改编、展示和表演的独占权,通常为期有限。在生成性 AI 中,这种版权的“所有者”方面变得不明确,因为在内容创作中涉及多个参与方,包括:

  • 编写提示的人员

  • 建立该 AI 模型的公司/组织

  • 在训练模型中使用其作品的艺术家

  • 生成艺术/内容的AI

每个参与方在内容创作中都扮演着重要角色,没有每个参与方的参与,内容就无法生成。具体来说,对于撰写提示的人,美国版权局明确表示AI 生成的内容(图像、文字、视频等)不能获得版权,考虑到缺乏人类创作AI 生成结果的可预测性

US 版权申请中的 Kristina Kashtanova明确说明了这一点,她创作了一部图画小说,其中书中的插图是使用 AI 图像生成工具 Midjourney 创建的。版权局提出的论点是“提示更像是建议而非命令,类似于雇佣艺术家创作图像时给出的通用方向”。简单来说,作者创建的提示类似于委托视觉艺术家时提供的项目简介,在这种情况下,项目简介的创建者不会是艺术作品的所有者。

随着技术的不断发展,这种情况可能会发生变化,更多的争论也会被提出。无论如何,在生成性 AI 创作过程中记录一些文档,展示“人类创作”组件以应对任何版权问题,总是一个好主意。

图片由Markus Winkler拍摄,来源于Unsplash

2. 隐私和数据保护

2023 年 4 月,ChatGPT 在意大利曾被短暂封锁,原因是怀疑违反了欧盟的《通用数据保护条例》(GDPR)。意大利政府强调的主要问题之一与数据保护和隐私有关。ChatGPT 已经更新了其隐私政策和产品以应对这些问题,但仍然不是每个用户都会认真阅读和评估这些内容。

生成性 AI 中存在的隐私数据风险包括(1)数据收集的同意,(2)数据保留,以及(3)可用性风险。

  • 数据收集同意:在初始模型创建过程中,数据从各种来源收集,可能包括个人信息,而信息的所有者并不知道这些数据被用于模型训练。一些平台提供用户选择退出的选项,以便将他们的内容从未来的 AI 模型改进中排除,但默认情况下此选项是关闭的,需要用户手动提交。

  • 数据保留:GDPR 规定了“被遗忘权”部分,允许用户要求公司纠正其个人信息或将其完全删除。尽管公司可以尝试促进这一点,但考虑到大语言模型训练中使用的数据复杂性,关于其技术可行性的讨论仍在继续。

  • 可用性风险:默认情况下,用户与如 ChatGPT 等 LLM 模型的交互记录会被公司收集并用于重新训练 AI 模型。在 LLM 模型的各种使用案例中,如将机器人用作治疗师或医生,用户可能无意中提供敏感信息。

作为生成 AI 的用户,我们需要了解工具的隐私政策——数据如何存储和使用,并且小心我们输入的任何数据,因为这些数据会被收集并可能由提供组织进行人工审查。

照片由Dan Nelson拍摄,来源于Unsplash

3. 错误信息

使用生成 AI 时可能会发生无意(或甚至故意)的错误信息。由于其高质量内容生成能力,这些模型能够生成出色的内容,类似于真实或人类生成的内容,使得识别内容的真实性和准确性变得困难

AI 模型容易出现“幻觉”,这是一个用来描述 AI 模型生成的事实不准确或无关输出的术语。这可能由于训练数据不足、序列/理解限制以及训练过程中固有的偏见而发生。在 AI 模型生成的内容中,常常会发现听起来合理的随机虚假信息。我们作为实际用户,需要批判性地分析和验证模型提供的信息,在进一步使用或分发之前。

其他生成 AI 被恶意用于错误信息的方式包括创建深度伪造。深度伪造是经过数字操控的合成媒体,能够令人信服地将一个人的形象替换成另一个人,主要利用深度学习技术。随着强大的生成 AI 的出现,创建此类欺骗性媒体变得越来越容易。

存在因生成内容不准确而导致的相关方受到损害的可能性,这可能构成强有力的诽谤案件。一位澳大利亚市长已经威胁起诉 OpenAI 诽谤,因为 ChatGPT 错误地声称他因贿赂入狱。

4. 伦理影响

生成 AI 模型的非凡能力为各行各业的多个过程开启了新用例。然而,随着模型被用于行业中的决策过程,可能会出现伦理问题。

比如,假设像 ChatGPT 这样的 LLM 模型被用于分析候选人的简历或推荐信,在招聘或大学申请过程中。由于模型训练中的固有偏见,模型可能会无意中突出来自特定背景的候选人,违背了应给予所有候选人的平等机会。

另一个图像生成的例子是,由于训练数据的限制,生成的图像可能会偏向某种文化或人群。模型只能基于其所接受的训练生成输出,如果只训练于特定人群的数据,它将仅生成该人群的输出——最终可能无法为用户提供代表性图像

一种处理方法是人工监督中使用生成 AI。将人工评估作为 AI 使用生命周期的一部分非常重要,以确保输出的可解释性,并避免 AI 决策过程中的偏见。

图片由José Martín Ramírez Carrasco提供,来源于Unsplash

结束语

随着生成 AI 使用的增加,用户也需关注法律和伦理问题,并能够在这一领域保护自己。尽管目前尚未明确建立相关法规,我们可以开始采取措施解决上述法律和伦理问题,防止出现不必要的问题。

  • 保护你的个人信息。了解工具的隐私政策,注意输入到系统中的每一条信息,考虑其保密性。

  • 指定并记录 AI 生成内容的人类创作元素。尽管目前 AI 生成内容的所有权不在提示创建者身上,但通过充分的文档记录,可能能够主张人类的创意和创作权。

  • 警惕技术的输出。针对 AI 生成内容的幻觉问题和可能的诽谤,我们可以采取措施在分享内容之前验证信息的准确性,并在内容上添加明确的水印或说明,指明内容是 AI 生成的。

生成式 AI 是一个不断发展的领域,随着技术的额外使用和趋势,法规将会被制定和更新。看看法律领域如何动态地响应这一技术将会很有趣。

Lesk 算法:一种用于文本分析中的词义消歧方法

原文:towardsdatascience.com/lesks-algorithm-a-method-for-word-sense-disambiguation-in-text-analytics-52c157a2fdff?source=collection_archive---------4-----------------------#2023-03-24

Duncan W. Towards Data Science Duncan W.

·

关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 3 月 24 日 投票

--

照片由 Amador Loureiro 提供,来源于 Unsplash

在英语中,超过 38%的词汇是多义的,这个术语指的是一个词可能有多个定义或“意义”(Edmonds, 2006)。例如,单词“set”——它可以用作名词、动词和形容词——具有数十种独特的定义,使其成为英语词典中记录的最多义的词之一。因此,如果我们要求某人“请把餐具套放在桌子上”,我们如何本能地知道“set”这个词的两个用法之间的区别?答案全在于上下文。人脑的神经网络执行语义处理、存储和检索,使我们在词义消歧(WSD)方面具有天然的天赋。这意味着我们能够在给定的上下文中根据最合理的意思来确定一个多义词的含义。因此,自然语言也随之发展,反映出这种进行复杂上下文关联的天赋能力。

NLP 中的词义消歧

尽管我们的大脑在这些游戏中能够取得很大成功,但具有多个意义的词在自然语言处理(NLP)中带来了显著的问题,NLP 是人工智能的一个分支,研究计算机解释和“理解”人类语言的能力。虽然一个词的意义依赖于其上下文,但计算机很难捕捉到词的上下文,因为它被隐喻、修饰语、句子否定以及其他无数的语言复杂性所复杂化,这使得机器难以学习。

由于词义消歧在 NLP 的实际应用中具有重要价值,近年来出现了几种词义消歧的方法。目前的机器学习方法包括监督学习方法,通过使用一组手动标记的词汇来训练分类算法。然而,这些训练集可能成本高昂、获取时间长且不完美,因为即使是人类标注者也常常只有 70-95%的时间达成一致(Edmonds, 2006)。鉴于所需的手动努力,未监督学习方法也被使用,其中许多方法旨在基于某种相似上下文的度量来聚类词汇。

Lesk 算法:一种简单的词义消歧方法

也许最早且至今仍最常用的词义消歧方法之一是 Lesk 算法,由 Michael E. Lesk 于 1986 年提出。Lesk 算法基于这样的理念:在文本中一起出现的词是某种程度上相关的,而这些词的关系和相应的上下文可以通过感兴趣的词的定义以及周围使用的其他词的定义提取。Lesk 算法在现代技术出现之前就已经发展,旨在通过找到在词典定义中重叠最多的一对匹配词典“意义”(即同义词)来消歧感兴趣词的含义——通常出现在短语或句子中。

在 Lesk 使用的示例中,他引用了“pine”和“cone”这两个词,并指出这些词从《牛津英语词典》中返回以下定义:

Lesk 在 1986 年提出的语义匹配示例。

简而言之,Lesk 算法计算了一个感兴趣的词与其上下文窗口中所有词的词典定义之间的重叠次数。然后,它选择与重叠次数最多的词对应的定义,排除停用词(如“the”、“a”、“and”),并将其推断为该词的“意义”。如果我们考虑“pine”作为感兴趣的词,而“cone”是其上下文窗口中的唯一词,比较“pine”和“cone”的词典定义会发现“常绿”是这两个词最常见的“意义”。因此,我们可以合理推断感兴趣的词“pine”指的是一种常绿树,而不是它的另一种定义。

优点和缺点

Lesk 算法有许多优点,其中最主要的是其简单性使其易于实现,适用于各种不同的上下文,因此易于推广。Lesk 指出,该算法不依赖于全局信息,这意味着因为相同的词在文本中可能被多次引用但每次含义不同,词义仅由其上下文窗口中的直接支持词汇集合推导,而不是整个文本。除此之外,Lesk 算法是非句法性的,这意味着该方法不依赖于词语的排列或句子的结构,因为所有的关联仅通过词典定义进行。这使得该算法与基于句法的文本分析解决方案配合使用时具有很好的效果。例如,一个基于词性标注的文本标记工具可能能够识别“mole”作为名词的用法,但不能准确区分动物(mole)、皮肤肿块(mole)和计量单位(mole),因为这三者都是名词。在这种情况下,Lesk 算法的优势得以显现。

尽管简单且功能强大,Lesk 原始算法的最大缺点是性能——Lesk 提出的准确率仅为约 50-70%,并且在与带有语义标记的文本进行实验验证时表现得更低(Viveros-Jiménez, 2013)。该算法还显著存在低召回率的问题,即由于词典定义之间完全没有重叠或多个定义有相同数量的重叠,无法为许多单词提供对应的上下文定义。此外,Lesk 留下了一些未解答的问题,包括最适用的词典是什么,所有匹配的术语是否应被平等对待或按词典定义的长度加权,以及单词的上下文窗口应有多宽(Lesk 提出约 10 个单词,但建议这个范围是相当灵活的)。

前进:Lesk 的最新进展

尽管 Lesk 的方法中留有一些问题和不足,但已经进行了重要的努力来在他原始工作的基础上进行扩展。2002 年,研究人员提出了一种将 Lesk 的基于词典的算法适配到词汇数据库 WordNet 的方法(Banerjee & Pedersen, 2002)。与词典中按字母顺序排列单词和定义不同,WordNet 是一个在线数据库,按语义排列单词,创建名词、动词、形容词和副词的数据库组。具有同义关系的单词被归入称为“同义词集合”(synset)的关系中,而多义词可以通过其出现在多个同义词集合中被识别,因为每个同义词集合代表一个单词的不同定义或“意义”。WordNet 还考虑了词汇关系,例如下义词(hyponymy,即属于更广泛类别的词,如郁金香是花,花是植物)和转喻(metonymy,即用与某物密切相关的名称来代指该物,如“王冠”指代君主制),因此能够捕捉到比原始 Lesk 算法更广泛的词汇定义和关系。WordNet 的结构允许更有针对性地搜索相关词汇,因为如果已知感兴趣单词的词性,随后搜索的关系和同义词集合将仅限于该词性的范围内。

Lesk 算法的低召回率问题也一直是改进的领域。2013 年,研究人员发表了一篇论文,比较了几次实验,在这些实验中,他们调整了考虑的词的上下文窗口的大小,以期提高算法的性能(Viveros-Jiménez 等人,2013)。提出了一项新建议,即在定义上下文窗口的大小时,只考虑有至少一个重叠的四个词,并且在词义匹配中排除目标词自身。由于词典定义通常会在示例句子中包含感兴趣的词,例如“即松树是一种常绿树”,对于词“pine”,这可能会导致对“pine”的虚假重叠,从而使所有词义中最匹配的词仍为“pine”而不是“evergreen”。研究人员发现,当结合这两种方法时,Lesk 算法的精确度和召回率显著提高,相较于简单选择 4 个词的上下文窗口。

在 Python 中应用 Lesk 算法

以下是一个简单的例子,展示了如何实现 Lesk 算法,使用 pywsd 包在 Python 中进行词义消歧。以下函数是 Adapted Lesk 算法的实现,该算法由 Banerjee 和 Pederson 于 2002 年描述,使用了 WordNet 实现的 Lesk。

Adapted Lesk 算法在 Python 中的实现示例

我们可以看到,对于词“bank”,Lesk 算法能够推断出在上下文中,一个“bank”的意思是指金融机构,而另一个意思是指一种倾斜的土地。同样,在进行这种关联时,句法结构并不重要,而 Lesk 算法的成功依赖于上下文线索的存在,例如提到河岸时的“river”和“fish”,以及提到金融银行时的“deposit”和“money”。通过这个例子可以看出,Lesk 对句子结构具有鲁棒性,因此可以用于从各种非结构化文本数据类型中提取词义,无论是经典小说的摘录还是 Twitter 评论。然而,由于 Lesk 算法对上下文窗口的大小敏感,这种数据结构限制可能最限制其应用。因此,数据必须被结构化并分词成适当大小的窗口,以提供足够的上下文信息,但仍能捕捉到感兴趣词的真正含义。由于 Lesk 算法可能最适用于短语和具有上下文信息的相关术语组,它可以应用于查询和检索类型的场景,例如自动聊天机器人或搜索框,在这些场景中输入一些上下文信息,进行解释,然后返回响应或结果。

尽管 Lesk 算法及类似的词义消歧方法直观上简单,但它们作为当前开发直观日常工具(需要词义消歧)的基础性垫脚石发挥了重要作用。特别是,这些算法背后的直观理念可以实际应用于开发新的算法,以提升搜索引擎在全球数十亿用户中使用时的信息检索相关性。毕竟,当我们搜索一个模糊的术语时,我们本能地知道需要包含一个“上下文窗口”,以增加准确命中的可能性。

那么为什么呢?因为上下文就是一切。

感谢阅读!如果你想分享任何想法,请随时在 LinkedIn 上联系我

www.linkedin.com/in/duncan-w/

资源

Banerjee, S. & Pedersen, T. (2002). 使用 WordNet 的适应型 Lesk 算法进行词义消歧。计算语言学与智能文本处理。2276. 136–145. 10.1007/3−540−45715−111.

Edmonds, P. (2006). 消歧义,词汇的。语言与语言学百科全书(第二版),Elsevier,第 607–623 页。

Lesk, M. (1986). 使用机器可读词典的自动词义消歧。第 5 届年度国际系统文档会议论文集 — SIGDOC

Liling Tan. 2014. Pywsd: Python 实现的词义消歧(WSD)

技术 [software]。取自 https : //github.com/alvations/pywsd

SAP Conversational AI. (2015 年 11 月 20 日)。从上下文到用户理解。取自 https : //medium.com/@SAPCAI/from−context−to−user−understanding−a692b11d95aa

Viveros-Jiménez F., Gelbukh A., Sidorov G. (2013) 对简化的 Lesk 算法在词义消歧中的简单窗口选择策略。载:Castro F., Gelbukh A., González M. (编) 人工智能及其应用的进展。MICAI 2013. 计算机科学讲义,第 8265 卷. Springer, Berlin, Heidelberg. https : //doi.org/10.1007/978−3−642−45114−017

扩展我们的数据科学视野

原文:towardsdatascience.com/lets-expand-our-data-science-horizons-369c419da98?source=collection_archive---------13-----------------------#2023-02-16

TDS EditorsTowards Data Science TDS Editors

·

关注 发表在 Towards Data Science · 作为 通讯 发送 · 3 分钟阅读 · 2023 年 2 月 16 日

--

欢迎来到《Variable》第 100 期!

我们通常将每一期通讯专注于特定的话题或问题;然而,为了纪念这一特殊的时刻,我们选择了不同的方法。每周,我们会将一些出色的 TDS 文章排除在《Variable》之外,因为它们不完全符合我们的主题;这一周,我们呈现了最近编辑推荐中我们尚未强调的最佳文章。

如果你担心我们可能会引导你走向一个充满混乱和策展鲁莽的危险路径,请不要担心!我们推荐的每一篇阅读材料都邀请你以一种全新而吸引人的方式探索新的方向、方法或工具——我们相信这是一种强有力的主题联系。享受阅读吧!

  • 预测性能漂移:另一面。为什么模型预测的准确性会随着时间的推移而恶化?瓦莱里亚·丰塞卡·迪亚斯的清晰概述提供了四种漂移的有用类型,包括它们的原因和潜在解决方案。

  • 训练中断会毁掉我的马拉松吗? barrysmyth的最新贡献位于数据分析、运动和自我跟踪的迷人交汇处,探讨了中断对马拉松选手表现的影响。

  • 我们早该看到 ChatGPT 的到来。在一次令人难忘的首发帖子中,萨曼莎·霍德将实用主义和惆怅融合在一起,评估了 ChatGPT 的仍在展开的影响——并提出了一种更冷静的思维方式,让我们“让它为我们而不是对抗我们工作。”

图片由Raychan拍摄,来源于Unsplash

  • 生存分析:用机器学习预测事件发生时间。预测事件发生的概率有什么比这更好的?对Lina Faik来说,知道事件发生之前还剩下多少时间更为重要;Lina 的易懂解释帖子带领我们了解如何在客户流失的背景下进行这样的预测。

  • 两个发球:分析 2000–2020 年的 ATP 发球数据。像篮球和足球这样的主要团队运动(包括全球普及的和美国的)常常主导了运动分析的讨论。肖恩·霍兰德通过对网球选手关键击球:发球的深思熟虑的数据支持的分析,帮助拓宽了话题。

  • 相等大小的谱聚类。谁不喜欢对一个永恒重要话题的精美教程?Carmen Adriana Martinez Barbosa的新指南向我们介绍了相等大小谱聚类算法,该算法生成的聚类——没错!——在包含的点数上更为平衡;这在许多实际应用场景中可能至关重要。

  • 首席数据科学家的日常。数据专业人员的职业路径可以有很多种形式;如果你对管理轨迹感兴趣,并想更好地了解未来可能的工作样貌,Ray(写作二人组Leah Berg 和 Ray McLendon中的一员)分享了他作为首席数据科学家的日常工作和实践。

  • 如何制作一个 AI 图像编辑聊天机器人。为了以一个实用、适合创作者的方式结束这期内容,别错过由Sophia Yang和 Philipp Rudiger 带来的快速教程,他们展示了如何创建一个能够使用聊天机器人提示编辑图像的应用程序。

  • 额外推荐我写作的主要驱动力始终是学习。** 我们本周很高兴发布了 2023 年的首个作者聚光灯——一次生动的对话,特邀Matteo Courthoud分享了他关于职业道路、写作历程以及对因果推断的持续兴趣的见解。

感谢您参与我们第 100 期的《Variable》——祝愿接下来有 100 期更多的精彩!

如果你喜欢我们本周推荐的工作,希望你考虑成为 Medium 会员——这是支持我们出版物最直接有效的方式。

直到下一期《Variable》,

TDS 编辑团队

让我们谈谈数学(面向数据科学家)

原文:towardsdatascience.com/lets-talk-about-math-for-data-scientists-50184abd5964?source=collection_archive---------5-----------------------#2023-03-23

TDS EditorsTowards Data Science TDS Editors

·

关注 发表在 Towards Data Science · 作为 新闻通讯 发送 · 3 分钟阅读 · 2023 年 3 月 23 日

--

数学常常引发从事数据相关职业的专业人士强烈的情感。有些人因为热爱数学而想成为数据科学家。另一些人则需要克服对这个主题的根深蒂固的焦虑,才能在职业中找到自己的立足点;即便如此,他们可能还是会尽量少用数学知识。

不论你的个人经验如何,我们希望你喜欢这篇热情和耐心探讨数学之美(及复杂性)的文章合集。这些文章从初学者友好到更高级的主题都有,你一定能找到感兴趣的内容,无论你从上一个训练营、大学课程,还是——哎呀!——高中课程中记住了多少。

  • 逻辑回归的易懂介绍。统计学是数据科学家最常遇到的数学子领域,其中逻辑回归是他们需要掌握的关键概念之一。如果你处于学习的早期阶段,不要错过Shreya Rao精美插图的基础指南。

  • 对核函数感到困惑?现在不再困惑。根据Shubham Panchal的说法,核函数是“用于机器学习和统计学中的美丽数学概念,有多种形式。”它们的多样性可能使它们显得令人困惑,但 Shubham 的解释将帮助你清晰地看到如何在机器学习应用中使用它们。

图片来源:Kristin SnippeUnsplash

  • 选择正确路径的挑战。如果你正在处理一个复杂的数学优化问题,知道哪种方法在具体情境中最合适可能会很困难。Hennie de Harder的解释带我们了解如何在精确算法(如线性或混合整数规划)和启发式算法(如遗传算法和粒子群优化)之间做出选择。

  • 揭开经典概率谜题的复杂性Naman Agrawal邀请我们跟随他解决“挑战我们对周围世界理解”的优惠券收集者问题。在过程中,Naman 还讨论了这个问题的复杂性及其对计算机科学和经济学等各个领域的影响。

这周我们还有一些阅读推荐——希望你还没厌倦数学!尽情享受:

  • 在我们真正告别数学之前,看看Bex T.关于在 Python 中创建惊艳分形艺术的详细教程。

  • Yennie Jun对美国图书禁令的惊人趋势背后的数据进行了彻底而直言不讳的分析。

  • 机器学习与时尚的碰撞在Federico Bianchi的 FashionCLIP 介绍中体现:一个新的领域特定的视觉和语言模型。

  • 随着三月疯狂进入其(几乎)最后阶段,现在是阅读Giovanni Malloy关于大学篮球 NET 排名如何运作的解释文章的好时机。

  • 如果你的教育或职业道路不那么传统,数据科学工作依然触手可及—Madison Hunter解释了如何利用非传统背景获得最大收益。

感谢你这周的时间和支持!如果你喜欢我们发布的内容(并希望访问所有内容),可以考虑成为 Medium 会员

直到下一个变量,

TDS 编辑

在 Plotly 中提升你的数据讲述能力,使用动画柱状图

原文:towardsdatascience.com/level-up-your-data-storytelling-with-animated-bar-charts-in-plotly-f9ace6d73f27

将静态图表转变为引人入胜的叙述

Brian MattisTowards Data Science Brian Mattis

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

--

图片来源:Teemu PaananenUnsplash

Plotly 支持动画图表的优秀基础。我强烈推荐他们的基础教程 这里。然而,plotly 动画主要是为了给可视化添加另一个维度——通常是时间。这对为图表增添更多意义非常棒。

然而,动画不必用于增加图表的复杂性。利用动画进行强调是非常强大的。当我们为观众准备好关键图表时,我们希望吸引他们的注意,而不是明确地喊出“这是你应该特别关注的图表!”我们的视线自然会被移动的东西吸引,动画图表可以在某种程度上营造出期待感。你的观众会参与其中,试图实时预测下一条柱状图或线条。如果关键业务决策依赖于这个图表——你的观众会屏息以待,眼睁睁看着结果展开!

为了更好地理解——想象一下你已经看了同事的演示文稿 20 分钟——哪个图表吸引了你的注意?这个简单的(pandas 生成的)图表:

基本柱状图——作者提供的图像

或者这张相同数据的动画图表:

自定义 Plotly 动画柱状图——作者提供的图像

在 Plotly 中构建

要在 Plotly 中实现这一点,我们实际上需要稍微重新排列数据。同样,Plotly 动画的设置是按某种顺序(通常是时间)进行的。因此,我们将扩展数据以有效地为每个数据点制作一个帧。来吧:

首先,让我们导入 plotly 包(如果还没有安装,请使用 pip 安装),并创建一个虚拟数据集进行操作:

要直接使用 plotly 的动画,我们需要将数据集增强为以下内容:

动画条形图的数据转换 — 图片来自作者

我们需要有效地堆叠数据框的副本,副本的数量与原始数据框的长度相同。对于每个副本,我们需要添加一个 Frame 值来设置其在动画中的序列。问题在于,我们希望每一步序列中出现更多的条形图。我们希望在绘图中开始时不显示任何条形图,因此对于第一个 Frame,我们将所有值设置为 0。然后,在第二个 Frame 上,我们将所有值设置为 0 除了 第一个数据点(A: 10)。这个方法一直持续到最后一个 Frame(在这种情况下是 Frame #6)显示我们原始数据中的所有值。

到此为止,我们已经为 Plotly 设置了单独的“Frame”元素,以便平滑地动画过渡。请注意,我们没有为动画编码每一帧,我们只是逐个快照每个 Label 的最终值,让 Plotly 填补空白。现在我们只需要一个辅助函数,这样我们就可以将这个转换概念应用于任何简单的数据框:

数据框堆叠和排序函数

在这段代码中,我们首先创建一个空白的目标数据框,它将作为返回的输出。我们循环遍历传入数据框中的元素数量 — range(len(starting_df)+1)。我们在这里加了 1,因为第一个 Frame 将所有值都置为零,以便我们的图开始时为空。在循环内部,我们从原始数据框的深拷贝开始。这意味着我们对这个副本所做的更改不会传递回原始数据框。在这个干净的副本上,我们添加一个 Frame 值,并根据我们所在的循环次数将一些数据点置为零。最后,我们可以将这个副本添加到我们正在构建的最终数据框的末尾。

绘图

从这里开始我们准备开始绘图。如果我们只是使用默认的 plotly 函数,它看起来会像这样:

默认绘图设置 — 图片来自作者

正如你所见,开始时有点怪异。这主要与绘图如何自动缩放 y 轴以适应数据有关。这并不特别令人惊讶,因为在它第一次绘制时(Frame=0),所有值都是零。除此之外,序列的速度可能不是你想要的。也许你想让条形图绘制得慢一点?也许在绘制条形图之间有一个暂停会很好?这些都是可调整的参数。最好的部分是,使用 plotly,我们甚至不需要条形图线性增长 — 你可以使用一系列的过渡设置。为了将这个图形清理成我想展示的样子,我将调整:

  • Y 轴范围(取数据框中的最大值并加上 10%)

  • 图形大小和纵横比

  • 每帧的持续时间和帧之间的时间

  • 动画移动本身(过渡)

使用“quad-out”动画格式化的动画条形图——作者提供的图像

在这种情况下,我使用了“quad-out”过渡效果,但还有很多可以选择的效果,这些效果会影响可视化效果。可以查看文档 这里 并尝试几种效果——特别是“bounce”效果相当有趣(尽管可能不是最专业的)。还有许多可以调整的 设置,可以用来修改或移除图表下方的按钮,不过这可能取决于你导出图表的方式,有时可能并不必要。

导出为 GIF

现在有个坏消息——Plotly 没有内置的方法来提取这些动画。你现在需要做一些变通的方法。有些包可以用来提取成电影文件,如 moviepy,但如果我们要将其放入演示文稿或基于网页的客户仪表板中,这可能有些过于复杂。我们真正需要的是一个动画 GIF。虽然使用 Plotly 的 graph_objects 提取动画帧为 PNG 并使用其他包在线生成 GIF 是一个选项,但这超出了本帖的范围。现在,可以使用基本的 GIF 屏幕捕捉软件在 Jupyter Notebook 窗口上进行操作。这里有很多选择——这里 有一个免费的软件对我有效。

额外加分

当然,这个序列是为了从左到右绘制条形图。如果我们想要相反的效果,帧#1 的 F 值将为 16,而 A 值仍为 0。这在绘制折线图时可能会很有用,其中多条线可以从图表的不同侧面生长。🤯

结论

我希望你能看到添加一点动画如何使简单的图表对观众更具影响力。通过使用这个相对简单的功能,我们可以快速而轻松地将静态图表转换成极具吸引力的图像。当然,这只是一个开始!正如我频道的粉丝所知道的,我喜欢用有意义的 条形颜色渐变背景过渡 来直观地向观众传达更多信息。更多内容即将到来。

和往常一样,整个代码演示笔记本可以 从我的 github 获取如果你觉得这有用,请关注我! 祝好,编码愉快。

利用像 GPT 这样的 LLMs 分析你的文档或记录

原文:towardsdatascience.com/leverage-llms-like-gpt-to-analyze-your-documents-or-transcripts-c640a266ad52

使用提示工程,通过 langchain 和 openai 以类似 ChatGPT 的方式分析你的文档

Konstantin RinkTowards Data Science Konstantin Rink

·发表在Towards Data Science ·阅读时间 6 分钟·2023 年 3 月 31 日

--

(原始)照片由Laura Rivera拍摄,发布在Unsplash

ChatGPT 无疑是最受欢迎的大型语言模型(LLMs)之一。自 2022 年底发布其测试版以来,大家都可以使用便捷的聊天功能提问或与语言模型互动。

但如果我们想要向 ChatGPT 提问关于我们自己的文档或我们刚刚听过的播客内容呢?

本文的目标是向你展示如何利用像 GPT 这样的 LLM(大型语言模型)来分析我们的文档或记录,然后以 ChatGPT 的方式提问并获得有关文档内容的回答。

简要说明

  • 本文使用了 OpenAI 的 ChatGPT gpt-3.5-turbo模型,这需要一个API 密钥

  • langchain包是一个围绕 LLM 构建的框架,用于加载和处理我们的文档(提示工程)以及与模型进行交互。

  • 包含本文全部代码的 colab 笔记本可以在这里找到。

前提条件

在编写所有代码之前,我们必须确保所有必要的包已安装、API 密钥已创建且配置已设置。

API 密钥

要使用 ChatGPT,首先需要创建一个 OpenAI API 密钥。可以通过这个链接创建密钥,然后点击

+ 创建新的密钥 按钮。

没有什么是免费的:一般来说,OpenAI 会对每 1,000 个 tokens 收费。Tokens 是处理文本的结果,可以是单词或字符块。每 1,000 个 tokens 的价格因模型而异(例如,gpt-3.5-turbo 为$0.002 / 1K tokens)。关于定价选项的更多细节可以在这里找到。

好的一点是,OpenAI 允许你免费试用$18,无需提供任何付款信息。你当前的使用概况可以在你的账户中查看。

安装 OpenAI 包

我们还需要通过运行以下命令来安装官方 OpenAI 包

pip install openai

由于 OpenAI 需要(有效的)API 密钥,我们还需要将密钥设置为环境变量:

import os
os.environ["OPENAI_API_KEY"] = "<YOUR-KEY>"

安装 langchain 包

随着 2022 年末对大型语言模型(LLMs)的兴趣急剧上升(Chat-GPT 发布),一个名为 LangChain 的包在同一时间出现

LangChain是一个围绕 LLM(如 ChatGPT)构建的框架。该包的目标是帮助开发将 LLM 与其他计算或知识来源相结合的应用程序。它涵盖了如特定文档的问答本文的目标、聊天机器人代理等应用领域。更多信息可以在文档中找到。

可以使用以下命令安装该包:

pip install langchain

Prompt Engineering

你可能会想知道Prompt Engineering是什么。可以通过创建一个在你希望分析的文档上训练的自定义模型来微调 GPT-3。然而,除了训练费用外,我们还需要大量的高质量示例,理想情况下由人工专家审核(根据文档)。

这对于仅仅分析我们的文档或抄本来说会显得过于繁琐。因此,我们将我们希望分析的文本(通常称为提示)传递给它,而不是训练或微调模型。生产或创建如此高质量的提示称为Prompt Engineering

注意:有关 Prompt Engineering 的进一步阅读文章可以在这里找到。

加载数据

根据你的使用案例,langchain为你提供了几个“加载器”,如Facebook ChatPDFDirectoryLoader,用于加载或读取你的(非结构化)文本(文件)。该包还包括一个YoutubeLoader用于转录 YouTube 视频。

以下示例集中在DirectoryLoaderYoutubeLoader上。

使用 DirectoryLoader 读取文本文件

from langchain.document_loaders import DirectoryLoader

loader = DirectoryLoader("", glob="*.txt")
docs = loader.load_and_split()

DirectoryLoader 的第一个参数是路径,第二个参数是模式,用以查找我们所需要的文档或文档类型。在我们的案例中,我们将加载与脚本位于同一目录下的所有文本文件(.txt)。load_and_split 函数随后启动加载过程。

即使我们可能只加载一个文本文档,但如果文件较大,进行拆分也是有意义的,以避免NotEnoughElementsException(需要至少四个文档)。更多信息可以在这里找到。

使用 YoutubeLoader 转录 YouTube 视频

LangChain 提供了一个 YoutubeLoader 模块,该模块使用 youtube_transcript_api 。这个模块会收集给定视频的(生成的)字幕。

并非每个视频都自带字幕。在这些情况下,可以使用自动生成的字幕。然而,在某些情况下,这些字幕质量较差。在这些情况下,使用 Whisper 来转录音频文件可能是一个替代方案。

下面的代码将视频 ID语言(默认为 en)作为参数。

from langchain.document_loaders import YoutubeLoader

loader = YoutubeLoader(video_id="XYZ", language="en")
docs = loader.load_and_split()

在我们继续之前……

如果你决定使用转录的 YouTube 视频,请考虑首先进行适当的清理,例如,清除 Latin1 字符(\xa0)。我在问答部分体验到的答案差异取决于我使用的相同来源的格式。

处理数据

像 GPT 这样的 LLMs 只能处理一定数量的令牌。这些限制在处理大型(或更大)文档时尤为重要。通常,有三种方法来应对这些限制。一种是利用嵌入或vector space engine。第二种方法是尝试不同的链式方法,如map-reducerefine。第三种方法是两者的结合。

一篇提供有关不同链式方法和向量空间引擎使用的更多细节的精彩文章可以在这里找到。还要记住:使用的令牌越多,收费也会越高。

在接下来的内容中,我们将embeddings与链式方法stuff结合起来,stuff会将所有文档“塞入”一个单一的提示中。

首先,我们通过使用OpenAIEmbeddings将转录文本(docs)导入向量空间。然后,这些嵌入被存储在一个称为Chroma的内存嵌入数据库中。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma

embeddings = OpenAIEmbeddings()
docsearch = Chroma.from_documents(docs, embeddings)

之后,我们定义我们希望用于分析数据的model_name。在这种情况下,我们选择gpt-3.5-turbo。可以在这里找到可用模型的完整列表。temperature参数定义了采样温度。较高的值会导致输出更随机,而较低的值则会使答案更加集中和确定。

最后但同样重要的是,我们使用了RetrievalQA (Question/Answer) 检索器并设置了相应的参数(llmchain_typeretriever)。

from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.2)

qa = RetrievalQA.from_chain_type(llm=llm, 
                                    chain_type="stuff",
                                    retriever=docsearch.as_retriever())

提问

现在我们准备向模型提问关于我们的文档。下面的代码显示了如何定义查询。

query = "What are the three most important points in the text?"
qa.run(query)

对于不完整的回答该怎么办?

在某些情况下,你可能会遇到不完整的回答。回答文本在几句话后就停止了。

不完整回答的原因很可能是令牌限制。如果提供的提示非常长,模型剩余的令牌就不够给出一个(完整的)回答。处理此问题的一种方法是切换到不同的chain-type,如refine

llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.2)

qa = RetrievalQA.from_chain_type(llm=llm, 
                                    chain_type="refine",
                                    retriever=docsearch.as_retriever())

然而,我发现当使用与stuff不同的chain_type时,我得到的结果不够具体。处理这些问题的另一种方法是重新表述问题,使其更加具体。

结论

多亏了 LangChain 包,只需要几行代码就可以分析 GPT 文本文档或转录内容。由于这个包相对较新,我预计很快会有很多更新和代码更改。这可能会影响本文中提供的代码片段。

如果你考虑在日常工作或较大的私人项目中使用 LLM,你应该关注数据的清理,优化使用的令牌数量,并使用最佳实践,如设置预算限制或警报。

我希望你喜欢阅读这篇文章。源代码的 Colab 笔记本可以在 这里 找到。

来源

[## LangChain:简介和入门 | Pinecone

大型语言模型(LLMs)在 2020 年 OpenAI 发布 GPT-3 后进入了世界舞台 [1]。从那时起…

www.pinecone.io [## 使用 GPT3、LangChain 和 Python 构建 GitHub 支持机器人 | Dagster 博客

2023 年 1 月 9 日 * 13 分钟阅读 * ChatGPT,你听说过吗?ChatGPT 几个月前推出,震惊了所有人……

dagster.io [## 提示工程

在这里,我们讨论了一些编写提示(我们模型的输入)的原则和技巧,这将帮助你获得更好的结果……

docs.cohere.ai [## 检索

TL;DR:我们正在调整我们的抽象,以便除了 LangChain VectorDB 之外的其他检索方法也能轻松使用……

blog.langchain.dev

利用聚合的力量

原文:towardsdatascience.com/leverage-the-power-of-aggregate-ad5d69810c62

使用 pandas 聚合加速你的数据分析

Bradley Stephen ShawTowards Data Science Bradley Stephen Shaw

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

--

图片由 Dan Hadar 提供,Unsplash

你是否曾经因为在相同(分段)特征上计算各种统计信息时编写重复的代码而感到沮丧?

那些试图计算各种不同(分段)特征上各种统计信息的相似代码片段让你迷失了方向——听起来很熟悉吗?

我理解你的痛苦。好吧,我曾经理解——现在不再了!所以在这篇后续文章中,我们将讨论你可以使用agg的简单(但强大)方法:

  1. 对相同特征应用不同函数

  2. 对特征集应用函数集

  3. 使用元组获得更多灵活性

  4. 使用来自其他包的函数,

我提到了特征的分组或分段。虽然你不一定要在groupby语句中使用agg,但我认为这样做可以提供更有趣的示例,并且与我之前的内容相呼应。说到这一点,如果你想要对groupbyapplytransform进行复习,我建议你阅读一下:

## 提升你的数据处理技能

解锁 pandas 的 groupby、apply 和 transform 的强大功能

towardsdatascience.com

足够的无耻自我宣传——我们开始吧!首先,看看我们将要使用的数据。

数据

我们将继续使用从消费信用卡组合中收集的信息¹。

我对列名称和格式做了一些修改,但数据其余部分保持不变:

图片由作者提供

数据包含各种分类特征(例如教育水平)和数值特征(例如客户年龄)。

数据中包含的特征比上面的快照所示的更多;如果我们在下面使用一些“新”特征,不要感到惊讶!

聚合

… 或 agg

我认为文档对 agg 的强大功能描述得不够充分:

使用一个或多个操作在指定轴上进行聚合。

这实际上没有说明的是,agg 是一种非常灵活的函数,提供了多种方法来将多个函数应用于同一特征,或将多个函数应用于多个特征。它可以与或不与 groupby 语句一起使用,并可以在行或列轴上应用。它也不挑剔函数的来源或定义方式。

开始吧!

附注:我们也可以使用 agg 将相同的函数应用于多个特征,但通过直接使用该函数或通过 apply 语句可以更轻松地实现。

在同一特征上使用多个函数

让我们看看如何计算每个客户和性别细分中的客户年龄的最小值、最大值和偏度。

以下示例中使用的函数仅用于演示目的。一些函数——如偏度——对某些读者可能是新的,而其他函数——如scaled_median——则完全是虚构的。

附注:“偏度”是一个统计学概念,用于衡量分布固有的非对称程度。

# set up
segments = ['attrition_flag','gender']
feature = 'customer_age'
functions = ['min','max','skew']

# apply it
df.groupby(segments)[feature].agg(functions)

… 这将给出:

图片来源:作者

相当直接,对吧?只需注意几点:

  1. 我已经费心明确了每个细分、特征和函数的具体使用方法,通过在应用前对每个进行定义。这主要是为了提供一个清晰的示例,所以可以自由地简化你的代码。

  2. 我们使用内置的 pandas 函数,因此可以通过其名称引用它们(因此使用字符串形式进行指定)。稍后我们将看到如何使用来自其他包的函数以及自定义函数,包括 lambda 函数。

  3. 我们在 agg 中提供要应用的函数列表。如果需要,我们可以在列表中添加更多函数。

让我们更深入一点,开始将各种函数应用于多个不同的特征。

在多个特征上使用多个函数

agg 的真正优势——一次性生成多种汇总统计信息。

与 Python 中的大多数事物一样,这里有多种方法可以做到这一点。我们将在这里介绍两种不同的方法。首先是使用列表。

# set up
segments = ['attrition_flag','gender']
features = ['customer_age','months_on_book']
functions = ['min','max']

# apply
df.groupby(segments)[features].agg(functions)

… 这将给出:

图片来源:作者

这应该与你之前看到的相当熟悉——我们不是将各种函数应用于单一特征,而是将相同的函数应用于多个特征。唯一的真正区别是我们在列表中指定特征。

那么,如何将多个不同的函数应用于各种特征呢?为此,我们需要一个字典。

# set up
segments = ['attrition_flag','gender']
functions = {
    'customer_age':['min','max'],
    'months_on_book':['median','std']
}

# apply
df.groupby(segments).agg(functions)

… 这就产生了:

图片由作者提供

注意使用带有函数列表的字典来应用时,我们在应用内容上有更多的灵活性?很棒!

在这个示例中,我们只看到两个函数应用于两个特征。当然,我们可以将其更改为将任意多个函数应用于任意多个特征。我们只需记住字典参数的格式:特征名称应指定为字典键,(函数的) 列表应作为字典值提供。

更多的元组,更多的灵活性

我们在上面看到,使用列表和元组中的agg调用返回一个具有层次索引³(或在术语中称为MultiIndex)的DataFrame

如果这正是你想要的,那当然没问题。但我喜欢简单的东西,我也喜欢控制我的列名。

所以我们将切换到使用元组,这将:

  1. 返回一个具有平坦索引的DataFrame。(忘记那些reset_index()调用)。

  2. 允许我们指定每个返回列的名称。

可以说,后一点可能是使用元组的一个更强大和有用的动机。

好了,闲话不多说。让我们用元组重写上述示例。

# set up
segments = ['attrition_flag','gender']

# apply
df.groupby(segments).agg(
    customer_age_min = ('customer_age','min'),
    customer_age_max = ('customer_age','max'),
    months_on_book_median = ('months_on_book','median'),
    months_on_book_std = ('months_on_book','std')
)

产生:

图片由作者提供

使用元组可以让我们指定汇总特征的名称。如果你想对DataFrame的名称非常具体,这非常有用。

这个调用agg的语法有点不寻常。在这里,我们在元组之外指定结果列的名称,然后在元组内提供被汇总特征的名称(以字符串格式)以及函数。伪代码中,这类似于结果名称 = (特征名称, 函数)

我们仍在使用内置的pandas功能,因此可以通过名称(以字符串格式)引用它们。让我们看看如何使用其他函数。

异域函数

我们经常需要使用更异域或定制的汇总函数。

我对“异域”这个词有些宽泛——我实际上是指来自其他包的函数。然而,有时我们确实需要使用我们自己编写的真正定制的功能。

那么,开始吧!我们将结合一些想法,将各种函数应用于每个attrition_flag x gender段。

# exotic functions
import numpy as np

# bespoke function
def scaled_median(s):
    # scales Series median by ratio of Series max to Series min
    return s.median() * s.max() / s.min()

# segments
segments = ['attrition_flag','gender']

# apply
df.groupby(segments).agg(
    customer_age_min = ('customer_age',np.min),
    customer_age_max = ('customer_age',np.max),
    months_on_book_std = ('months_on_book',np.std),
    months_on_book_scaled_median = ('months_on_book',scaled_median)
)

这就产生了:

图片由作者提供

我们逐步进行:

  1. 我们从numpy中提取一些函数。这里没有什么“异域”的东西——这些只是numpy对最小值、最大值和标准差的实现。别忘了导入包含“异域”函数的包!

  2. 我们创建了一个名为scaled_median的自定义函数。这里也没什么特别的,因为这是一个为了演示目的而编造的函数。

  3. agg 的调用使用了元组方法。这里没有太多新内容,除了我们引用函数的方式。由于我们不再使用内置方法,因此不能按名称调用它们:而是提供它们的名称及其来源(例如np.min)。

如果你想更具 Python 风格,可以用 lambda 函数替代函数的更正式定义;我在这里没有这么做,是出于格式化的考虑。

就到这里,回顾一下我们所做的工作。

总结

这一次,我们已经看到如何使用agg将不同的函数应用于相同(分组)的特征。

我们扩展了将一组函数应用于一组(分组)特征的内容;我们看到可以使用列表、字典和元组来做到这一点。示例展示了每种方法的优点。

最后但同样重要的是,我们看到如何利用其他包中的函数和我们自己编写的自定义函数。

正如我们所看到的,agg 的真正优势在于将多个函数应用于多个特征。这对于探索性数据分析、特征工程和生成汇总统计非常有用。

在进行特征工程时,我们经常需要将数据片段转换的结果映射回原始数据。关于是否可以使用 agg 来实现这一点常常存在一些困惑——实际上不能。如果我们需要将结果广播回原始数据,我们需要使用 transform,尽管这也有其自身的限制。如果你需要进行大量转换,你可能会考虑使用 agg,然后手动将结果与原始数据连接起来。

一如既往,感谢你的阅读。希望这里有一些有用的提示,也请告诉我你使用的任何实用技巧!

如果你对其他 pandasmatplotlib 技巧感兴趣,你可能也会对以下内容感兴趣:

## 让你的表格看起来光辉夺目

简单的格式化技巧,让你的 pandas DataFrame 准备好展示

[towardsdatascience.com

… 和

## 让你的图表看起来光辉夺目

让 matplotplib 图表准备好展示的简单技巧

[towardsdatascience.com

参考文献和资源

  1. zhyli. (2020). 预测流失的信用卡客户 [数据集]. Zenodo.,在 Creative Comons — Attribution 4.0 International 许可下提供。

  2. pandas.DataFrame.agg — pandas 1.5.3 文档 (pydata.org)

  3. 多重索引 / 高级索引 — pandas 1.5.3 文档 (pydata.org)

利用 Azure Event Grid 创建 Java Iceberg 表

原文:towardsdatascience.com/leveraging-azure-event-grid-to-create-a-java-iceberg-table-d419da06dbc6?source=collection_archive---------29-----------------------#2023-01-10

我们将使用 Azure Event Grid 实现事件驱动架构

Jean-Claude CoteTowards Data Science Jean-Claude Cote

·

关注 发布于 Towards Data Science ·6 min read·2023 年 1 月 10 日

--

图片由 Jackson Case 提供于 Unsplash

我们之前的文章 中,我们展示了 Iceberg 表如何作为 Kafka 主题。我们展示了独立的 Java 写入器如何并行生成 parquet 文件,同时单个 Bookkeeper 将这些数据文件附加到 Iceberg 表中。Bookkeeper 通过创建 Iceberg 提交来完成这一操作。

Bookkeeper 需要识别新创建的数据文件是什么。然后,它将这些文件注册到 Iceberg 表中。在我们之前的文章中,我们保持简单,并选择实现基于文件的消息通道。对于每个创建的数据文件,Writers 会在一个众所周知的文件夹中创建一个 Moniker。Bookkeeper 监视这个众所周知的文件夹,并定期读取这些 Monikers。Bookkeeper 使用这些 Monikers 中的信息将新数据文件附加到 Iceberg 表中。

之前的解决方案完全基于文件。它不依赖于任何外部服务。然而,基于文件的解决方案需要大量额外的创建/删除文件操作,并且需要昂贵的数据湖列出文件操作。

这是我们之前基于文件的解决方案的高层次图示。

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

在这篇文章中,我们将尝试通过利用Azure Event Grid服务来改进这个解决方案。Event Grid 是一个事件中介,你可以使用它来实现事件驱动架构。在事件驱动架构中,事件用于传输状态变化。

我们感兴趣的状态变化是当 Java Writers 向数据湖添加新文件时的文件创建事件。

这些事件的消费者是 Bookkeeper,它将新数据文件附加到 Iceberg 表中。

这是基于队列的解决方案的高层次图示。

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

配置

一个 Azure 存储账户由四项服务组成:容器、文件共享、队列和表。我们感兴趣的两个数据存储服务是容器和队列。容器是存储 blob 文件的地方。队列是我们将发送文件创建事件的地方。

因此,我们配置 Event Grid 以便每当在特定存储容器内创建文件时发出事件。我们配置 Event Grid 将这些文件创建事件发布到存储队列。

由于我们只对文件创建事件感兴趣,因此我们配置 Event Grid 仅发送“Blob Created”事件。

此外,我们只对在特定路径下创建的文件以及具有特定文件扩展名的文件感兴趣。

最后,我们可以通过指定高级筛选器来进一步缩小我们将处理的事件。当 Java Writer 完成写入文件时,它会调用 REST 接口 FlushWithClose。在高级筛选器中,我们指定 data.api 等于 FlushWithClose

我们可以通过指定包含子字符串的subject来定位特定的 Iceberg 表或所有 Iceberg 表。在我们的案例中,我们使用了/data/,这是所有 Iceberg 表使用的数据文件夹。

当 Java Writers 在/data/文件夹内创建 parquet 文件时,事件会发布到存储队列。我们可以使用 Azure 门户监控 Event Grid 中的活动。特别是,我们可以看到已交付的事件是所有发布事件的一个子集。

Writers

Writers 的实现基本上与我们之前的基于文件的解决方案相同。事实上,Writers 甚至更简单,因为它们不再需要自己发布通知。Azure Event Grid 处理发布通知的任务。Writers 只需要写入 parquet 文件。

记账员

记账员仍然负责将新创建的数据文件注册到 Iceberg 表中。然而,它现在通过读取FlushWithClose事件的队列来确定需要追加的文件。我们像这样创建一个 Java 存储队列客户端

DataLakeTokenCredential token = new DataLakeTokenCredential();
String queueURL = Constants.hadoopConf.get("queue.location");
String queueName = Constants.hadoopConf.get("queue.name");
this.queueClient = new QueueClientBuilder()
                      .endpoint(queueURL)
                      .credential(token)
                      .queueName(queueName)
                      .buildClient();

注意使用我们的DataLakeTokenCredential类。由于存储队列和数据湖(容器)都位于同一个存储帐户内,我们利用相同的认证机制。

检索事件就像调用receiveMessages一样简单。

List<QueueMessageItem> messages = queueClient.receiveMessages(MAX_MESSAGES,
                    Duration.ofSeconds(60), Duration.ofSeconds(10))
                    .collect(Collectors.toList());

每条消息都是一个带有文件路径和文件大小的FlushWithClose。这些消息是 JSON,格式如下:

"data": {
  "api": "FlushWithClose",
  "contentType": "application/x-compressed",
  "contentLength": 115739,
  "blobType": "BlockBlob",
  "blobUrl": "https://<accountname>.blob.core.windows.net/users/iceberg/schema/data/file.parquet",
  "url": "https://<accountname>.blob.core.windows.net/users/iceberg/schema/data/file.parquet",
},

在我们之前的基于文件的解决方案中,Monikers(消息)由 Java Writers 写入,并包含序列化的 Iceberg DataFile对象。因此,记账员可以读取这些对象并直接执行以下操作:

List<DataFile> dataFiles = readMonikers();
AppendFiles append = this.table.newAppend();
for (DataFile dataFile : dataFiles) {
      append.appendFile(dataFile);
}
append.commit();

现在我们使用 Azure Event Grid,我们无法控制消息的内容。因此,记账员需要使用 Event Grid 提供的文件路径和文件大小信息创建 Iceberg DataFile对象。

我们缺少 parquet 指标;幸运的是,Iceberg 提供了从 parquet 文件中检索指标的必要 API。

InputFile in = this.table.io().newInputFile(f);
Metrics metrics = ParquetUtil.fileMetrics(in, 
                      MetricsConfig.forTable(this.table));

感谢 Samrose Ahmed 向我展示了这个技巧。这里是他的博客无服务器异步 Iceberg 数据摄取,使用了 AWS SQS(AWS 等同于 Azure Event Grid)。

我们现在拥有创建 Iceberg DataFile对象并将其提交到 Iceberg 表所需的所有信息。

AppendFiles append = this.table.newAppend();
DataFile dataFile = DataFiles.builder(this.partitionSpec)
                    .withPath(filePath)
                    .withFileSizeInBytes(fileSize)
                    .withFormat("PARQUET")
                    .withMetrics(metrics)
                    .build();
append.appendFile(dataFile);
append.commit();

性能

当我们对这个实现进行性能分析时,我们很快意识到大部分时间花费在从 parquet 文件中检索指标上。检索 parquet 文件指标非常高效,仅涉及读取 parquet 文件的尾部信息。然而,每个文件都需要执行一次 REST 调用。这些 REST 调用的延迟会迅速累积。幸运的是,可以通过使用 Java 执行器服务并行化这些请求来轻松解决这个问题。

List<Callable<DataFile>> callableTasks = monikers.stream()
         .map(m -> new MetricResolver(m, Constants.partitionSpec))
         .collect(Collectors.toList());

List<Future<DataFile>> futures = executorService.invokeAll(callableTasks);

结果

通过利用执行器服务处理延迟操作,我们可以大幅提高性能。即使在追加大量数据文件(数百个)时,处理消息也不到一秒钟。读取 parquet 指标需要一到三秒钟。

唯一不能通过执行器服务优化的操作是 Iceberg 提交操作。尽管该操作相当迅速,通常在一到两秒钟之间,但有时在 Iceberg 重新组织清单时,这一操作可能需要长达 20 秒。

如果我们测量数据文件刷新时间与数据文件提交到 Iceberg 表的时间之间的差异,我们观察到总体延迟从 3 秒到 20 秒不等。

结论

使用 Azure Event Grid 的结果与我们之前的基于文件的解决方案类似。然而,Event Grid 解决方案提供了更一致的结果。我们不再受制于文件列表数据湖操作,这种操作有时可能需要较长时间,尤其是当数据湖使用频繁时。

我们还估算了使用 Event Grid 的“消息传递”成本约为基于文件的解决方案的 1/10。实际数据文件的存储成本在两种解决方案中是相同的。

利用 Azure Event Grid 是通知 Bookkeeper 新创建的数据文件的好方法。然而,如果你的存储设备不支持通知机制,使用基于文件的解决方案也可以行得通。

如果你有兴趣自己评估这些实验,你可以在这里找到这些文章中使用的代码。

利用数据分析进行可持续商业转型

原文:towardsdatascience.com/leveraging-data-analytics-for-sustainable-business-transformation-cfd8d71f97f5?source=collection_archive---------0-----------------------#2023-03-22

了解如何使用数据分析克服扩展绿色举措的挑战,这些举措阻碍了组织实现可持续发展目标。

Samir SaciTowards Data Science Samir Saci

·

关注 发表在 Towards Data Science · 12 分钟阅读 · 2023 年 3 月 22 日

--

(图片来源:作者)

目前,财务法规推动公司承诺到 2030 年减少碳排放。

数据分析如何帮助组织克服可持续供应链管理的障碍?

然而,对于组织来说,扩大绿色举措和实现可持续发展目标可能面临挑战。

将供应链定义为多个参与方交换物料和信息流 — (作者提供的图片)

主要挑战在于供应链管理处于复杂系统的核心,涉及制造和物流团队。

不同团队专注于优化其在供应链中的操作范围 — (作者提供的图片)

由于这些团队有时不习惯为共同目标一起工作,许多公司在绿色转型的起步阶段陷入困境。

我们如何通过数据解锁这些情况?

《哈佛商业评论》文章“可持续发展努力如何崩溃?”深入探讨了公司在实施可持续发展举措时常见的挑战。

本文将探讨数据分析如何通过聚焦供应链绿色转型的四个“隐藏敌人”来帮助克服这些挑战。

💌 免费获取最新文章: 通讯

📘 供应链分析的完整指南:分析备忘单

Summary
I. How Sustainability Efforts Fall Apart?
  1\. The "Four Hidden Enemies"
  2\. Support of Supply Chain Analytics
II. Leveraging Data Analytics
  1\. Hidden Enemy 1: Structure and Governance
  *Solution 1: Descriptive Analytics*
  2\. Hidden Enemy 2: Processes and metrics
 *Solution 2: Adapted Optimization Models*  3\. Hidden Enemy 3: Culture and Leadership
  *Solution 3: Diagnostic Analytics to Address Cultural Barriers*
  4\. Hidden Enemy 4: Methods and Skills
  *Solution 4: Workforce Training* III. Conclusion
  1\. Data is your best ally
  2\. Drive an ESG-led Business Transformation

可持续发展努力如何崩溃?

绿色转型的“四个隐藏敌人”

可持续性已经成为商业运营的关键方面,因为公司面临着越来越大的压力,需要解决环境和社会问题以进行 ESG 报告。

然而,实施碳足迹减少和有效的可持续性举措的路线图往往说起来容易做起来难。

文章“可持续发展努力如何崩溃”揭示了公司在追求可持续发展时面临的关键障碍,重点关注四个“隐藏的敌人”。

  • 结构和治理:孤立的可持续性限制了影响力。

  • 流程和指标:不可持续的指标阻碍进展。

  • 文化和领导力:旧观念挑战转型。

  • 方法和技能:传统工具阻碍变化。

绿色转型的四个隐藏敌人 — (作者提供的图片)

你听说过供应链分析吗?

支持供应链分析以促进可持续性举措

供应链可以定义为多个参与方交换物料 和信息流以满足客户需求。

将供应链定义为多个参与方交换物料和信息流 — (作者提供的图片)

在之前的文章中,供应链分析被介绍为一组帮助公司利用系统生成的数据获得洞察并优化操作的工具。

分析的不同类型是什么?

发现四种供应链分析类型 — (图片来源:作者)[原文]

这也可以是解决上述障碍的一个极好的支持:

在接下来的章节中,我们将详细探讨每个“隐藏敌人”,并解释数据分析如何帮助克服这些挑战。

利用数据分析实现绿色转型。

隐藏的敌人 1:结构和治理

组织结构的孤岛性质可能会阻碍可持续性的有效合作。

确实,可持续性往往被降级到一个单独的公司部门,导致其与关键的公司职能隔离。

限制了可持续性从而无法转变整个组织,并限制了其在公司中的力量和相关性。

孤岛优化对供应链管理中可持续性努力的影响 — (图片来源:作者)

运营经理将始终关注她的操作范围

  • 店长每次订单保持低数量(并增加频率)最小化他们的库存

  • 供应计划人员推动更多生产批次 (每批次低数量)获得足够的灵活性

  • 财务经理总是鼓励库存减少

  • 商业团队主张高库存覆盖以避免因缺货而造成的销售损失。

  • 仓库操作必须应对这些约束

谁负责减少 CO2 排放?每个人都应该负责,但实际上没有人负责。

这种缺乏合作严重影响了运输生产计划的效率,阻碍了可持续性努力的进展。

如果我们优化销售和供应链之间的协作会怎么样?

欲了解更多详情,您可以查看

因此,可持续性被视为可有可无或影响每个团队表现的营销工具。

首先,让我们测量整个价值链的实际表现。

解决方案 1:描述性分析

需要端到端的方法来提高效率,找到合适的平衡,从而实现最小的环境足迹。

数据不会说谎,人会。

— Ernie Lindsey

通过连接不同的系统(ERP, WMS, CRM 等),描述性分析解决方案可以建立一个中央真实来源,贯穿整个供应链。

📊 示例 1:生命周期评估

评估产品在其生命周期中的环境影响 — (作者提供的图片)

生命周期评估(LCA)是一种评估产品在其整个生命周期中环境影响的方法。

使用的数据类型 — (作者提供的图片)

在我们的例子中,它可以用来估算产品的足迹,考虑到端到端的供应链过程。

分析供应链中的排放和资源使用,以获得可持续性见解 — (作者提供的图片)

并识别关键点,以提供基于数据的诊断,打破孤岛,促进跨部门协作。

  1. 每单位的 CO2e 总排放成为所有团队的共同 KPI。

  2. 这个 KPI 可以纳入所有经理的绩效评估中。

店长:如果我减少订货频率,运输团队可以优化卡车装载。

这将鼓励跨职能的协作,以支持由可持续发展团队主导的倡议。

如果你不能测量它,你就不能管理它。

— W. Edwards Deming

因为这些指标是从可信的数据源构建的,经理们会更加主动地减少排放。

我们可以为整个供应链部门设定一个共同的排放减少目标。

实施数据驱动的协作行动以实现可持续供应链转型 — (作者提供的图片)

例如,

  1. 我们希望将每单位生产的 CO2 排放总量减少 20%

  2. 45%的排放来自运输和生产

  3. 店长们将把他们的订货频率减少一半。

  4. 供应计划员将增加补货订单数量并减少频率。

  5. 运输团队必须提供调整后的卡车尺寸

  6. 制造团队将减少生产运行次数

太好了!接下来是什么?

尽管描述性分析可以帮助打破信息孤岛,但传统的流程和指标仍可能构成重大障碍,这将引领我们进入下一个隐藏的敌人。

隐藏的敌人 2:流程和指标

可持续性很少被纳入公司的核心业务流程中。

它们设计于利润是主要关注点的时代,而环境和社会因素未被考虑。

供应链管理中的常见业务和运营 KPI — (图片来源:作者)

用于评估业务绩效的指标通常与成本、利润、市场份额或每股收益相关联。

一位运营经理对可持续性团队说:“我怎么能帮助你们减少 CO2 足迹呢?我已经在努力减少运输成本了。”

因此,传统的指标可能会通过优先考虑短期财务收益而非长期环境效益来中和可持续性举措。

如果我们切换目标函数会怎么样?

从最小化成本到最小化 CO2eq 排放。

解决方案 2:调整优化模型

通过将可持续性指标纳入现有业务流程,公司可以开发出平衡财务和非财务目标的优化模型。

借助优化工具,持续改进工程师可以改进流程,朝向平衡利润和可持续性的最佳解决方案。

什麽是平衡成本和可持续性的最佳工厂网络?

目标是找到正确的参数,以优化特定指标,考虑外部和内部约束。

可持续供应链网络问题声明 — (图片来源:作者)

📊 示例 2:可持续供应链网络优化

供应链优化利用数据分析来寻找最佳的工厂和配送中心组合,以满足客户需求

我们是应该在巴西还是葡萄牙生产以最小化水使用?

在这个经典的线性规划问题中,你的模型将选择正确的生产设施组合,以

  • 遵守需求约束:工厂的供应应满足市场需求。

  • 最小化生产和交付产品的总成本

这通常会选择位于偏远地区的工厂,因为生产成本较低,同时考虑到运输成本的重量。

如果我们想要最小化总 CO2 排放会怎样?

基于成本和基于 CO2 的供应链优化方法比较 — (图片来源:作者)

右侧,我们建议使用相同的模型,采用适应的目标函数以最小化总碳排放。

低成本解决方案与低碳解决方案的供应链网络设计 — (图像来源:作者)

通过这个简单的变化,我们完全改变了网络。

低碳解决方案推动通过将工厂增加到欧洲市场来实现生产本地化。

平衡的方法是可能的,以保持商业竞争力。

你可以调整你的目标函数或添加约束,以将成本控制在一定阈值内。

然而,正如下一个隐藏的敌人所讨论的,旧有的思维方式和习惯仍然可能是变革的重大障碍。

隐藏的敌人 3:文化和领导力

旧有的思维方式和习惯可能是变革的重大障碍。

当领导层和运营团队未与可持续性和绿色转型目标对齐时,努力可能会遇到抵制或漠视。

异质托盘的卸货过程及其环境影响 — (图像来源:作者)

在组织内,我们可以发现不一致的价值观,这些价值观可能阻碍绿色供应链实践的采用。

例如,这是一个在快消品公司项目中看到的例子

  • 工厂将包含多个参考项的托盘(异质托盘)发送到仓库因为这样对他们更方便。

  • 仓库接收团队必须拆除塑料薄膜,分类物品,重新打托盘,并再次包装。

这会增加额外的工作,增加薄膜消耗产生废料

因此,培养支持性的组织文化和致力于可持续性的强有力领导至关重要。

解决方案 3:诊断分析以应对文化障碍

诊断分析专注于识别特定过去事件或趋势的原因。

它涉及检查历史数据以确定导致特定结果的因素。

可持续性团队对工厂物流经理说:“根据我们的诊断工具:我们每年额外消耗 2 吨薄膜,因为你们将物品混合在同一托盘中。”

这些工具可以帮助你的组织通过客观的外部评估理解失败的原因。

📊 示例 3:供应链控制塔

一个 供应链控制塔 传统上被定义为一组与各种系统连接的仪表板,利用数据监控供应链中的关键事件。

利用供应链控制塔进行高效分销网络管理 — (图像来源:作者)

如果你以时尚零售公司的分销网络监控为例,

  • 绩效指标是 按时按量,也称为 OTIF

  • 诊断算法进行根本原因分析,以了解谁对延迟负责。

使用数据分析进行晚交货根本原因分析过程—(图像由作者提供)

这个想法是比较每个过程的实际交付时间和服务水平协议设定的目标。

欲了解更多详情,

我们可以实施可持续性控制塔吗?

这种方法可以轻松地适应环境足迹监测。

  1. 选择要跟踪的指标:例如,CO2 排放

  2. 设定每个过程的排放目标:例如,工厂仓库补货的目标为 160(克 CO2e/单位)

  3. 使用生命周期评估(LCA)方法比较实际排放与目标。

根本原因分析过程 用于发现偏差,但还需要额外的分析来找到根本原因。

回到我们的包装膜示例,我们会看到

  1. 仓库中包装膜的消耗偏差。

  2. 操作团队的解释:“这归因于异质托盘的去托盘化。”

  3. 最终的根本原因是工厂的托盘化方法。

在解决了文化障碍之后,我们可以集中精力于推动绿色转型所需的方法和技能。

隐藏的敌人 4:方法和技能

传统工具和技能集可能需要改进,以管理可持续性计划的复杂性。

缺乏使用分析的专业知识可能会阻碍组织利用数据来优化供应链过程,并做出基于数据的可持续性和绿色转型决策。

解决方案 4:员工培训

它并不是直接与特定类型的分析相关联,但表明需要装备员工掌握利用数据分析的必要技能。

通过提供分析工具的培训计划,公司可以培养能够推动可持续性计划的员工队伍。

例如,我在这个简短的教程中分享了我学习 Python 和 VBA 用于供应链分析的经验。 👇

对隐藏敌人及其解决方案的快速回顾使您信服于数据推动绿色转型的力量。

结论

数据是你最好的盟友。

数据分析可以成为克服阻碍可持续性计划的“隐藏敌人”的强大盟友。

对于每一个敌人,我们找到了解决方案。

这些不同类型的供应链分析可以帮助公司打破信息孤岛,确保所有部门朝着减少足迹的目标迈进。

要查看更多使用数据分析进行供应链可持续性的案例研究,您可以观看这个视频:👇

如何衡量您举措的影响?

推动以 ESG 为导向的商业转型。

所有这些举措都可以对您的 ESG 评分产生积极影响。

环境、社会和治理(ESG)报告方法披露了公司的治理结构、社会影响和生态足迹。

ESG 支柱展示 — (图片来源:作者)

这三个维度提供了对公司可持续性和道德影响的深入理解,这可以通过数据驱动的举措来改善。

报告类别示例 — (图片来源:作者)

考虑到这些报告具有战略性,说服高层管理人员投资于绿色举措是一种有效的方式。

我们如何利用分析生成这个报告?

我在下面链接的文章中提出了几种工具和方法,用于提取和处理数据以生成这些评分。👇

## 什么是 ESG 报告?

利用数据分析进行公司全面有效的环境、社会和治理报告

towardsdatascience.com

我们为什么要这样做?[…]

你听说过可持续发展目标吗?

可持续发展目标(SDGs)是联合国制定的17 个目标,旨在应对全球挑战。

5 类可持续发展目标 — (图片来源:作者)

作为数据科学家,你如何帮助你的公司为这些目标做出贡献?

在这篇文章中查看我对数据分析如何支持联合国可持续发展目标的见解,

[## 什么是可持续发展目标(SDGs)

将全球可持续发展倡议与公司供应链数字化转型结合起来,使用数据科学

s-saci95.medium.com](https://s-saci95.medium.com/what-are-the-sustainable-development-goals-sdgs-988a1eb2b62b?source=post_page-----cfd8d71f97f5--------------------------------)

关于我

让我们在LinkedinTwitter上联系。我是一名供应链工程师,利用数据分析改善物流操作并降低成本。

如果你对数据分析和供应链感兴趣,可以看看我的网站。

[## Samir Saci | 数据科学与生产力

专注于数据科学、个人生产力、自动化、运筹学和可持续发展的技术博客…

samirsaci.com](https://samirsaci.com/?source=post_page-----cfd8d71f97f5--------------------------------)

💡 在 Medium 上关注我,获取更多关于 🏭 供应链分析、🌳 可持续性和 🕜 生产力的文章。

参考文献

  • “可持续性努力为何会崩溃?”,哈佛商业评论,Elisa Farri、Paolo Cervini 和 Gabriele Rosani

  • 可持续物流 — 减少仓库消耗品,个人博客Samir Saci

  • “什么是供应链控制塔?”,Towards Data ScienceSamir Saci

  • “什么是供应链分析?”,Towards Data Science,Samir Saci

  • “什么是生命周期评估?LCA”,Towards Data Science,Samir Saci

  • “4 个影响深远的项目来开始你的供应链数据科学之旅”,Towards Data Science,Samir Saci

利用 Llama 2 功能进行现实世界应用:使用 FastAPI、Celery、Redis 和 Docker 构建可扩展的聊天机器人

原文:towardsdatascience.com/leveraging-llama-2-features-in-real-world-applications-building-scalable-chatbots-with-fastapi-406f1cbeb935

深入探讨:开源与闭源 LLM,揭示 Llama 2 的独特功能,掌握提示工程的艺术,并使用 FastAPI、Celery、Redis 和 Docker 设计稳健的解决方案

路易斯·罗克Towards Data Science 路易斯·罗克

·发布于 Towards Data Science ·阅读时间 14 分钟·2023 年 7 月 24 日

--

介绍

Meta 在几天前以一种出乎意料的举动开源了他们的大型语言模型(LLM)Llama 2,这一决定可能会重塑当前的 AI 发展格局。它为如 OpenAI 和 Google 这些主要公司提供了替代方案,这些公司决定对其 AI 模型保持严格控制,限制可访问性并限制更广泛的创新。希望 Meta 的决定能够激发开源社区的集体回应,抵消限制访问领域进展的趋势。Llama 2 的新许可证甚至更进一步,允许商业使用,给予开发者和企业在现有及新产品中利用该模型的机会。

Llama2 家族由经过预训练和微调的 LLM 组成,包括 Llama2 和 Llama2-Chat,参数规模达到 70B。这些模型在各种基准测试中表现优于开源模型[1]。它们在一些闭源模型面前也表现不俗,为开源 AI 发展提供了急需的提升[2]。

图 1:Llama 2 家族 (图片来源)

如果你查看 HuggingFace [1] 的 Open LLM 排行榜,你会看到 Meta 的 Llama 2 占据了强有力的第三位。Llama 2 宣布后,Stability AI 发布了 FreeWilly1 和 FreeWilly2 [3]。FreeWilly1 是 Llama 的一个微调版本,FreeWilly2 是 Llama 2 的微调版本。Stability AI 透露,他们在 Orca 风格的数据集上对这两个模型进行了微调。Orca 数据集是一个大规模的、结构化的增强数据集合,旨在微调 LLM,每个条目包括一个问题和来自 GPT-4 或 GPT-3.5 的相应回答。为什么我们不使用 FreeWilly2 模型?不幸的是,虽然 Llama 2 允许商业使用,但 FreeWilly2 仅可用于研究目的,由非商业性知识共享许可证(CC BY-NC-4.0)管辖。

在这篇文章中,我们还将介绍使用 FastAPI、Celery、Redis 和 Docker 与 Meta 的 Llama 2 一起构建强大且可扩展的聊天应用程序的过程。我们的目标是创建一个高效的实时应用程序,能够处理多个并发用户请求,并将 LLM 的响应处理卸载到任务队列中。这使得应用程序能够保持响应性,并且我们可以通过 Redis 有效地管理任务。最后,我们将涵盖 Docker 的部署和扩展。该应用程序应展示这些技术如何协同工作以提供良好的聊天体验,展示开源语言模型如 Llama 2 在商业环境中的潜力。让我们深入探讨并开始构建吧!

开源 vs. 闭源

我们见证了公司和研究团队几乎每周发布新模型,无论是开源还是闭源。那么,谁将赢得 AI 军备竞赛?要做出有根据的猜测,我们需要了解这些模型训练过程的一些方面。

研究人员使用自回归变换器在广泛的自监督数据上作为起点。让我们首先来详细了解自回归变换器和自监督数据是什么。自回归变换器是变换器模型的一种变体,广泛用于处理序列数据的任务,尤其是在自然语言处理(NLP)领域。这些模型以自回归的方式生成序列,即每次生成序列的一部分,并将先前的输出作为后续步骤的输入。这使得它们在语言翻译、文本生成等任务中表现特别出色,因为前面数据点的上下文会影响对后续数据点的预测。自监督学习是一种学习方法,其中输入数据本身提供训练标签。通过从数据的某些部分预测其他部分,它消除了对显式手动标注的需求,并允许探索大量未标注的数据。

作为下一步,研究人员通常使用诸如人类反馈强化学习(RLHF)等技术来训练模型以与人类偏好对齐。在 RLHF 中,AI 系统根据其做出的决策从反馈中学习。它涉及创建一个奖励模型,AI 系统利用该模型学习哪些行为会导致积极或消极结果。其目的是使 AI 系统的行为与人类的价值观和偏好保持一致。

那么,开源社区面临的主要挑战是什么?这两个步骤都需要大量计算能力。其次,公司在对齐步骤中使用其专有数据来微调其模型,从而显著提高其可用性和安全性。

Llama 2 模型系列

Llama2 是 Llama1 的高级版本,训练于一种新型的公开数据组合。主要改进包括预训练语料库大小增加了 40%、模型的上下文长度翻倍,并采用了分组查询注意机制以提高对大模型的推理可扩展性。分组查询注意机制是对变换器模型中标准注意机制的修改,用于减少计算成本。它不是对每对输入和输出位置计算注意分数,这可能非常耗费资源,而是将查询划分为组,并将其一起处理。这种方法保留了标准注意机制的大部分有效性,同时通过降低计算复杂性,使得处理更长的序列或更大模型成为可能。

训练语料库由来自公开来源的新数据组合而成(未使用 Meta 产品或服务的数据)。此外,还努力排除来自已知包含大量个人信息的网站的数据。训练数据包含 2 万亿个标记,研究团队决定对最具事实性的来源进行过采样,以提高知识准确性。

目前有 7B、13B 和 70B 参数的 Llama2 变体可用。Llama2-Chat 是 Llama2 的对话优化版、经过微调的版本,也提供 7B、13B 和 70B 参数。

使用 Llama 2 进行提示工程

提示工程帮助我们引导大型语言模型(LLMs)以特定方式进行操作,这包括 Llama 2。在 Llama 2 的上下文中,提示指的是给模型的初始指令或查询,然后模型利用这些提示生成回应。然而,在 Llama 2 中,提示可以非常详细,并且可以包含设置模型上下文或“个性”的系统消息。

Llama 2 使用一种独特的提示格式来启动对话。其格式如下:

[INST] <> {{ system_prompt }} <> {{ user_message }} [/INST]

这个模板与模型的训练过程相一致,因此对输出质量有很大影响。在这个模板中,‘system_prompt’表示模型的指令或上下文。

下面是一个例子:

<>\n 你是 J. 罗伯特·奥本海默,一位杰出的物理学家,他在 20 世纪的开创性工作对原子弹的发展做出了重大贡献。深入核物理学的深奥世界,挑战科学理解的界限,并凭借你卓越的智慧揭示原子能的奥秘。踏上一个重大旅程,你对科学的热情没有限制,让你对原子能的掌握塑造历史进程,并在世界上留下不可磨灭的印记。\n<>\n[INST]\n 用户:你的研究如何导致了原子弹的创建?\n[/INST]

‘system_prompt’ 提供了模型的一般指令,这些指令将指导模型的所有响应。用户的消息跟随系统提示,并要求模型给出特定的响应。

在多轮对话中,用户与机器人之间的所有互动都被附加到之前的提示中,并被包含在 [INST] 标签之间。以下是其样式:

[INST] <> {{ system_prompt }} <> {{ user_msg_1 }} [/INST] {{ model_answer_1 }} [INST] {{ user_msg_2 }} [/INST] {{ model_answer_2 }} [INST] {{ user_msg_3 }} [/INST>

每条新用户消息和模型响应都会被添加到现有对话中,保持上下文。

需要注意的是,像许多 AI 模型一样,Llama 2 是无状态的,不会“记住”之前的对话。因此,每次提示模型时,都必须提供完整的上下文。这就是 Meta 为 Llama 2 扩大上下文窗口的原因。

最后需要指出的是,提示工程更像是一门艺术而非科学。掌握它的最佳方法是通过不断测试和改进。对你的提示保持创造性,并尝试不同的格式和指令。此外,不同的 LLM 适合不同类型的提示。

解决方案架构设计:FastAPI、Celery、Redis 和 Docker

在本系列中,我们一直使用 FastAPI 来构建我们的 ML 应用程序。它是一个高性能的 Web 框架,用于构建 API。在这种情况下,它的异步功能使其能够同时处理多个请求,这对实时聊天应用程序至关重要。

除了 FastAPI,我们使用 Celery 作为分布式任务队列,帮助管理从 LLM 生成响应的计算密集型任务。通过将这个过程卸载到任务队列,应用保持对新用户请求的响应,同时处理其他任务,确保用户不会被迫等待。由于我们使用了分布式任务队列,我们需要一个消息代理来帮助异步任务处理。我们选择了 Redis 来完成这个工作。它将任务从 FastAPI 排队,以便由 Celery 拾取,从而实现高效、解耦的通信。此外,Redis 的内存数据结构存储速度快,并允许实时分析、会话缓存和维护用户会话数据。

按照最佳实践,我们使用 Docker 将应用程序及其依赖项封装到隔离的容器中,这样我们可以轻松地在各种环境中进行部署。

使用 Llama 2、FastAPI、Redis 和 Celery 构建聊天 API

本指南说明了如何设置一个使用 Llama 2 的应用程序,该应用程序结合了 FastAPI、Redis 和 Celery。我们将涵盖这些概念以及它们如何协同工作。在我们的架构中,FastAPI 用于创建一个接收请求的 Web 服务器,Celery 用于管理异步任务,而 Redis 充当 Celery 的代理和后端,存储任务及其结果。

应用程序

FastAPI 应用程序(app.py)包括用于生成文本和获取任务结果的端点。/generate/ 端点接受一个带提示的 POST 请求,并返回任务 ID。它使用 Celery 任务 generate_text_task 异步启动任务。/task/{task_id} 端点通过任务 ID 获取任务的状态/结果。

from fastapi import FastAPI
from pydantic import BaseModel
from celery.result import AsyncResult
from typing import Any
from celery_worker import generate_text_task
from dotenv import load_dotenv

load_dotenv()

app = FastAPI()

class Item(BaseModel):
    prompt: str

@app.post("/generate/")
async def generate_text(item: Item) -> Any:
    task = generate_text_task.delay(item.prompt)
    return {"task_id": task.id}

@app.get("/task/{task_id}")
async def get_task(task_id: str) -> Any:
    result = AsyncResult(task_id)
    if result.ready():
        res = result.get()
        return {"result": res[0],
                "time": res[1],
                "memory": res[2]}
    else:
        return {"status": "Task not completed yet"}

工作者

celery_worker.py 文件创建了一个 Celery 实例,并定义了 generate_text_task 函数。该函数接受一个提示,并使用 Llama 2 模型生成文本。该函数使用 @celery.task 装饰器注册为 Celery 任务。

setup_model 函数是一个工作初始化函数。当工作进程启动时,它设置模型加载器。该函数通过 @signals.worker_process_init.connect 装饰器注册为在工作进程初始化事件上调用。

from celery import Celery, signals
from utils import generate_output
from model_loader import ModelLoader

def make_celery(app_name=__name__):
    backend = broker = 'redis://llama2_redis_1:6379/0'
    return Celery(app_name, backend=backend, broker=broker)

celery = make_celery()

model_loader = None
model_path = "meta-llama/Llama-2-7b-chat-hf"

@signals.worker_process_init.connect
def setup_model(signal, sender, **kwargs):
    global model_loader
    model_loader = ModelLoader(model_path)

@celery.task
def generate_text_task(prompt):
    time, memory, outputs = generate_output(
        prompt, model_loader.model, model_loader.tokenizer
    )
    return model_loader.tokenizer.decode(outputs[0]), time, memory

模型

model_loader.py 中的 ModelLoader 类负责从给定的模型路径加载 Llama 2 模型。它使用 HuggingFace 的 transformers 库来加载模型及其分词器。

import os
from transformers import AutoModelForCausalLM, AutoConfig, AutoTokenizer
from dotenv import load_dotenv

load_dotenv()

class ModelLoader:
    def __init__(self, model_path: str):
        self.model_path = model_path
        self.config = AutoConfig.from_pretrained(
            self.model_path,
            trust_remote_code=True,
            use_auth_token=os.getenv("HUGGINGFACE_TOKEN"),
        )
        self.model = self._load_model()
        self.tokenizer = AutoTokenizer.from_pretrained(
            self.model_path, use_auth_token=os.getenv("HUGGINGFACE_TOKEN")
        )

    def _load_model(self):
        model = AutoModelForCausalLM.from_pretrained(
            self.model_path,
            config=self.config,
            trust_remote_code=True,
            load_in_4bit=True,
            device_map="auto",
            use_auth_token=os.getenv("HUGGINGFACE_TOKEN"),
        )
        return model

代理

要设置 Redis,我们有两种选择:可以使用 Docker 容器,或者使用 Python 包 redis_server。如果你决定使用 Docker 容器(推荐的解决方案),你可以直接运行下面的命令。-p 6379:6379 选项告诉 Docker 将主机的 6379 端口上的流量转发到容器的 6379 端口。这样,Redis 实际上可以从 Docker 容器外部访问。

docker run --name redis-db -p 6379:6379 -d redis

第二种选择是通过 Python 接口来完成。redis_server.py 脚本处理 Redis 服务器的安装和启动。请记住,Redis 既充当消息代理,又充当 Celery 的结果后端。

import subprocess
import redis_server

def install_redis_server(redis_version):
    try:
        subprocess.check_call(["pip", "install", f"redis-server=={redis_version}"])
        print(f"Redis server version {redis_version} installed successfully.")
    except subprocess.CalledProcessError:
        print("Failed to install Redis server.")
        exit(1)

def start_redis_server():
    try:
        redis_server_path = redis_server.REDIS_SERVER_PATH
        subprocess.Popen([redis_server_path])
        print("Redis server started successfully.")
    except Exception as e:
        print("Failed to start Redis server:", str(e))
        exit(1)

def main():
    redis_version = "6.0.9"
    install_redis_server(redis_version)
    start_redis_server()

if __name__ == "__main__":
    main()

运行应用程序

主要执行脚本(run.py)是一个客户端脚本,它与 FastAPI 应用程序进行通信。它将提示发送到 /generate/ 端点,获取任务 ID,并定期轮询 /task/{task_id} 端点,直到任务完成。

import http.client
import json
import time

API_HOST = "localhost"
API_PORT = 8000

def generate_text(prompt):
    conn = http.client.HTTPConnection(API_HOST, API_PORT)
    headers = {"Content-type": "application/json"}
    data = {"prompt": prompt}
    json_data = json.dumps(data)
    conn.request("POST", "/generate/", json_data, headers)
    response = conn.getresponse()
    result = json.loads(response.read().decode())
    conn.close()
    return result["task_id"]

def get_task_status(task_id):
    conn = http.client.HTTPConnection(API_HOST, API_PORT)
    conn.request("GET", f"/task/{task_id}")
    response = conn.getresponse()
    status = response.read().decode()
    conn.close()
    return status

def main():
    prompt = input("Enter the prompt: ")

    task_id = generate_text(prompt)
    while True:
        status = get_task_status(task_id)
        if "Task not completed yet" not in status:
            print(status)
            break
        time.sleep(2)

if __name__ == "__main__":
    main()

utils 模块(utils.py)提供了一个名为 generate_output 的实用函数,用于从提示生成文本,使用 Llama 2 模型和分词器。该函数使用 @time_decorator 和 @memory_decorator 装饰器来测量执行时间和内存使用情况。

import time
import torch
import functools
from transformers import AutoModelForCausalLM, AutoTokenizer

def time_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        exec_time = end_time - start_time
        return (result, exec_time)
    return wrapper

def memory_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        torch.cuda.empty_cache()
        torch.cuda.reset_peak_memory_stats()
        result, exec_time = func(*args, **kwargs)
        peak_mem = torch.cuda.max_memory_allocated()
        peak_mem_consumption = peak_mem / 1e9
        return peak_mem_consumption, exec_time, result
    return wrapper

@memory_decorator
@time_decorator
def generate_output(prompt: str, model: AutoModelForCausalLM, tokenizer: AutoTokenizer) -> torch.Tensor:
    input_ids = tokenizer(prompt, return_tensors="pt").input_ids
    input_ids = input_ids.to("cuda")
    outputs = model.generate(input_ids, max_length=500)
    return outputs

实质上,当通过 /generate/ 端点接收到提示时,它会作为异步任务转发到 Celery 工作进程。工作进程使用 Llama 2 模型生成文本,并将结果存储在 Redis 中。你可以随时通过 /task/{task_id} 端点获取任务状态/结果。

部署

部署我们的应用程序需要几个步骤。首先,让我们为我们的应用程序创建一个 Dockerfile:

FROM python:3.9-slim-buster

WORKDIR /app
ADD . /app

RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 80

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]

接下来,让我们定义 requirements.txt,以确保在 Docker 容器中安装所有依赖项:

fastapi==0.99.1
uvicorn==0.22.0
pydantic==1.10.10
celery==5.3.1
redis==4.6.0
python-dotenv==1.0.0
transformers==4.30.2
torch==2.0.1
accelerate==0.21.0
bitsandbytes==0.41.0
scipy==1.11.1

要使用 Docker Compose 设置 FastAPI 应用程序、Celery 和 Redis 服务器,你可以创建一个 docker-compose.yml 文件,如下所示:

version: '3'
services:
  web:
    build: .
    command: ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]
    volumes:
      - .:/app
    ports:
      - 8000:80
    depends_on:
      - redis
  worker:
    build: .
    command: celery -A celery_worker worker --loglevel=info
    volumes:
      - .:/app
    depends_on:
      - redis
  redis:
    image: "redis:alpine"
    ports:
      - 6379:6379

在 Docker Compose 配置中,‘web’ 服务代表使用当前目录中的 Dockerfile 构建的 FastAPI 应用程序。它将主机的端口 8000 映射到容器的端口 80。‘worker’ 服务是 Celery 工作进程,并与 FastAPI 应用程序共享构建上下文。‘redis’ 服务使用官方 Redis Docker 镜像。depends_on 字段确保 Redis 在 ‘web’ 和 ‘worker’ 服务之前启动。所有这些服务可以通过 docker-compose up 命令启动。

结果

我们测试了 Llama 2,即具有 7b 参数的聊天版本,使用了 4 位量化。

我们使用了以下输入:

提示:<>\n 你是 J. 罗伯特·奥本海默,一位杰出的物理学家,他在 20 世纪的开创性工作对原子弹的发展做出了重大贡献。深入核物理的深刻世界,挑战科学理解的边界,用你卓越的智慧解锁原子能的奥秘。踏上一段重要的旅程,你对科学的热情无限,让你对利用原子能的执着塑造历史进程,并在世界上留下不可磨灭的印记。\n<>\n[INST]\n 用户:你的研究如何导致了原子弹的诞生?\n[/INST]

Llama 2 生成的对话如下:

啊,这是个非常棒的问题,我的朋友!调整眼镜。你看,原子弹的创造是一个极其紧张而复杂的科学历程的高潮。这是一个跨越数十年的探索,涉及到一些最聪明的物理学家,并将人类的理解推向了极限。你看,我的朋友,原子弹的探索始于 1930 年代初,当时我们在加利福尼亚大学伯克利分校的一群人意识到,原子的核可以被分裂,从而释放出大量的能量。这是一个极为了不起的发现,因为它揭示了原子作为能量来源的巨大潜力。但,唉,通向原子弹的道路并非没有挑战。哦,我们遇到了多少障碍!眨眼 怀疑者、反对者、怀疑论者,他们如同洪水般涌来,我告诉你! 但是,我们坚持下来了,我的朋友。我们不懈努力,仔细研究数据,测试和重新测试我们的理论。最终,我们的辛勤工作得到了回报。1945 年,我们的团队在新墨西哥州的三位一体试验场成功引爆了第一颗原子弹。这是一个纯粹的欣喜时刻,几年的辛勤工作和奉献的高潮。当世界惊叹地注视着我们时,我们意识到了原子弹的巨大潜力。但,我的朋友,原子弹的创造是……

这些文本生成的时间为 4.77 秒,峰值 VRAM 使用量约为 13.4 GB。需要始终记住的一点是,由于我们使用的是变换器模型,内存需求随着生成输出序列的大小而增加。在这种情况下,我们将 max_length 定义为 500 个 tokens。

这些结果显示,Llama 2 模型能够在合理的时间框架内生成复杂和细致的响应。请记住,我们使用的是最小的模型(7b)和更高的量化(4-bit)。

结论

正如我们在本文中所展示的,Meta 的 Llama 2 模型为开源社区提供了新的可能性。我们介绍了 Llama 2 的一些关键特性和功能,包括其训练过程、架构和提示工程设计。

此外,我们提供了一个关于使用 FastAPI、Redis 和 Celery 构建聊天应用程序的详细指南。这应该能让任何人开始构建可扩展的实时应用程序,以服务于 Llama 2(或任何其他商业许可的 LLM)的几千名用户。

在我们的结果中,我们展示了模型在生成详细和有上下文的响应方面的表现。

关于我

连续创业者和 AI 领域的领军人物。我为企业开发 AI 产品,并投资于以 AI 为重点的初创公司。

创始人 @ ZAAI | LinkedIn | X/Twitter

大型语言模型编年史:探索 NLP 前沿

本文属于“大型语言模型纪事:探索 NLP 前沿”,这是一个每周更新的系列文章,旨在探讨如何利用大型模型的强大功能来完成各种 NLP 任务。通过深入了解这些前沿技术,我们旨在赋能开发者、研究人员和爱好者,利用 NLP 的潜力,开启新的可能性。

目前已发布的文章:

  1. 用 ChatGPT 总结最新的 Spotify 发布

  2. 大规模掌握语义搜索:使用 FAISS 和句子变换器以闪电般的推理速度索引数百万文档

  3. 释放音频数据的力量:使用 Whisper、WhisperX 和 PyAnnotate 进行高级转录和分离

  4. Whisper JAX 与 PyTorch:揭示 ASR 在 GPU 上的性能真相

  5. 高效企业级语音识别的 Vosk:评估与实施指南

  6. 测试支持 1162 种语言的大规模多语言语音(MMS)模型

  7. 利用 Falcon 40B 模型,最强大的开源 LLM

  8. OpenAI 的函数调用在语言学习模型中的力量:综合指南

  9. 面向文档的代理:与向量数据库、LLMs、Langchain、FastAPI 和 Docker 的旅程

一如既往,代码可以在我的Github上找到。

参考资料

[1] — huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard

[2] — Touvron, H., Martin, L., Stone, K., Albert, P., Almahairi, A., Babaei, Y., Bashlykov, N., Batra, S., Bhargava, P., Bhosale, S., Bikel, D., Blecher, L., Ferrer, C. C., Chen, M., Cucurull, G., Esiobu, D., Fernandes, J., Fu, J., Fu, W., Fuller, B., … Scialom, T. (2023). Llama 2: 开放基础和微调的聊天模型。arXiv 预印本 arxiv.org/abs/2307.09288

[3] — stability.ai/blog/freewilly-large-instruction-fine-tuned-models

利用信息检索增强 LLMs:一个简单的演示

原文:towardsdatascience.com/leveraging-llms-with-information-retrieval-a-simple-demo-600825d3cb4c

一个将问答 LLM 与检索组件集成的演示

Thao VuTowards Data Science Thao Vu

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

--

图像由作者使用 Stable Diffusion 生成

大型语言模型(LLM)可以存储大量的事实数据,但其能力受到参数数量的限制。此外,频繁更新 LLM 是昂贵的,而旧的训练数据可能使 LLM 产生过时的回答。

为了解决上述问题,我们可以使用外部工具来增强 LLM。在本文中,我将分享如何将 LLM 与检索组件集成以提高性能。

检索增强(RA)

检索组件可以为 LLM 提供更为最新和准确的知识。给定输入x,我们希望预测输出p(y|x)。从外部数据源R中,我们检索与x相关的上下文列表z=(z_1, z_2,..,z_n)。我们可以将xz结合在一起,充分利用z的丰富信息来预测p(y|x,z)。此外,保持R的更新也要便宜得多。

检索增强管道(图像来源于作者)

使用维基百科数据和 ChatGPT 的 QA 演示

在这个演示中,对于给定的问题,我们执行以下步骤:

  • 检索与问题相关的维基百科文档。

  • 将问题和维基百科提供给 ChatGPT。

我们希望比较并查看额外的上下文如何影响 ChatGPT 的回答。

数据集

对于维基百科数据集,我们可以从这里提取。我使用了“20220301.simple”子集,其中包含超过 20 万份文档。由于上下文长度的限制,我只使用了标题和摘要部分。对于每个文档,我还添加了一个文档 ID,以便后续检索使用。因此,数据示例如下所示。

{"title": "April", "doc": "April is the fourth month of the year in the Julian and Gregorian calendars, and comes between March and May. It is one of four months to have 30 days.", "id": 0}
{"title": "August", "doc": "August (Aug.) is the eighth month of the year in the Gregorian calendar, coming between July and September. It has 31 days. It is named after the Roman emperor Augustus Caesar.", "id": 1}

我们结合标题和摘要段落,并准备它们进行编码。

with open(input_file, "r") as f:
    for line in f.readlines():
        try: 
            example = json.loads(line.strip("\n"))
            self.id2text[example["id"]] = example.get("title", "") + self.tokenizer.sep_token + example.get("doc", "")
        except Exception as _:
            continue
        if len(self.id2text) >= self.max_index_count: 
            break

编码

接下来,我们需要一个可靠的嵌入模型来构建我们的检索索引。在这个演示中,我使用了预训练的multilingual-e5-large,维度为 1024 来编码文档。为了更快的索引和存储效率,你可以选择其他小维度的嵌入模型。

我最初选择的嵌入模型是预训练的ALBERT,但结果的质量较差。在进入下一步之前,你应该进行一些测试案例,以确保你的索引工作合理。为了选择一个好的检索嵌入,你可以查看这个排行榜

 @torch.no_grad()
  def _get_batch_embedding(self, x: List[str]):
      '''
      Get embedding of a single batch
      Parameters
      ----------
      x: List of text to encode 
      '''
      batch_dict = self.tokenizer(x, max_length=512, padding=True, truncation=True, return_tensors='pt')
      outputs = self.model(**batch_dict)
      embeddings = average_pool(outputs.last_hidden_state, batch_dict['attention_mask'])
      # normalize embeddings
      return F.normalize(embeddings, p=2, dim=1)

  def _get_all_embeddings(self):
      '''
      Get embedding of all data points
      '''
      data_loader = DataLoader(list(self.id2text.values()), batch_size=8)
      embeddings = []
      for bs in tqdm(data_loader):
          embeddings += self._get_batch_embedding(bs)
      embeddings = [e.tolist() for e in embeddings]
      return embeddings

ANN 索引

我们已经准备好了文档的嵌入和 ID 列表。下一步是将它们很好地索引以进行检索。我使用HNSW索引并使用余弦距离度量。

self.index = hnswlib.Index(space = 'cosine', dim=self.dim)
self.index.init_index(max_elements =self.max_index_count, ef_construction = 200, M = 16)
self.index.add_items(embeddings, ids)
self.index.set_ef(50)
print(f"Finish building ann index, took {time()-start:.2f}s")
self.index.save_index(self.index_file) # so we don't need to do everything once again

给定一个问题,我们可以首先到检索索引中查找一些相关信息。为了避免错误的上下文,你可以在这里设置一个距离阈值。这样,只有相关的文档才会被使用:

 def get_nn(self, text: List[str], topk:int=1):
      embeddings = self._get_batch_embedding(text)
      labels, distances = self.index.knn_query(embeddings.detach().numpy(), k=topk)
      # map id back to wiki passage
      nb_texts = [self.map_id_to_text(label) for label in labels]
      if self.debug:
          for i in range(len(text)):
              print(f"Query={text[i]}, neighbor_id={labels[i]}, neighbor={nb_texts[i]}, distances={distances[i]}")
      return nb_texts, labels, distances

ChatGPT API

现在我们的检索管道已经准备好了!下一步,让我们准备一个提示来问 ChatGPT。我们准备了以下两种提示格式,一种只有问题,另一种同时包含问题和相关的维基百科文本。

这里的‘question’占位符是我们希望问 ChatGPT 的目标问题,而‘info’是从我们的 HNSW 索引中检索到的维基百科文档。

NO_WIKI_PROMPT_TEMPLATE = """
Answer the following question:

Question: ```{question}```py

Your response:
"""

HAVE_WIKI_PROMPT_TEMPLATE = """
You will be provided with the following information:
1\. A question delimited with triple backticks.
2\. Addition information that is related to the question.

Perform the following tasks:
1\. Understand the provided information.
2\. Use the provided information and answer the question.

Question: ```{question}```py
Addition information: ```{info}```py

Your response:
"""

填充占位符与问题和相应的维基百科文档后,我们可以向openai ChatCompletion API发送请求。

def get_chat_completion(
    messages: dict,
    model: str = "gpt-3.5-turbo",
    max_retries: int = 3,
    debug: bool = False
):
    '''
    Gets a chat completion from the OpenAI API.

    Parameters
    ----------
    messages : dict
        input messages to use, e.g: {"user", "what is the meaning of BERT"}
    model : str, optional
        The OPEN AI model to use. Here we set default value to "gpt-3.5-turbo".
    max_retries : int, optional
        The maximum number of retries to use. Defaults to 3.
    debug: bool
        If we want to debug or not
    '''
    model_dict = {"model": model}
    error_msg = None
    error_type = None
    if debug:
        logging.warning(f"Sending chat with message={messages}, model_dict={model_dict}...")
    for _ in range(max_retries):
        try:
          completion = openai.ChatCompletion.create(
              temperature=0.0, messages=messages, **model_dict
          )
          return completion
        except Exception as e:
          error_msg = str(e)
          error_type = type(e).__name__
          sleep(3)
    print(
            f"Could not obtain the completion after {max_retries} retries: `{error_type} ::"
            f" {error_msg}`" 

我尝试了发送一些带有和不带有维基百科文档的请求,并观察了差异。

示例 1

问题:生活成本包括哪些内容?

ChatGPT:生活成本通常包括如住房、食物、交通、医疗保健、公用事业及维持特定生活水平所需的其他基本必需品等费用。它还可能包括税收、保险和其他杂项费用。

维基百科信息:[[‘生活成本生活成本是指在某个地方生活所需的金额。它包括食物、住房等。’]]

ChatGPT + 维基百科信息:生活成本包括在某个地方生活所需的金额,其中涵盖了食物、住房和其他必要开支等费用。

示例 2

问题:如何制作奶酪?

ChatGPT:奶酪可以通过用酸或凝乳酶使牛奶凝固,分离凝乳和乳清,然后压制和陈化凝乳以发展风味和质地来制作。具体的过程和使用的配料可以根据制作的奶酪类型而有所不同。

维基百科信息: [[‘奶酪奶酪是一种由牛奶制成的乳制品。有很多种奶酪,比如切达奶酪、瑞士奶酪和普罗沃洛奶酪。‘]]

ChatGPT + 维基百科信息: 要制作奶酪,你需要以牛奶为主要成分。奶酪是一种由牛奶制成的乳制品。有很多种奶酪,比如切达奶酪、瑞士奶酪和普罗沃洛奶酪。

示例 3

问题: 你能解释一下 seq2seq 模型吗?

ChatGPT: seq2seq(序列到序列)模型是一种神经网络架构,用于生成一系列输出值的任务,该任务基于一系列输入值。它由两个主要组件组成:编码器和解码器。编码器接收输入序列,并生成一个固定长度的向量表示,然后将其传递给解码器。解码器使用这个向量来逐个生成输出序列。该模型常用于自然语言处理任务,如机器翻译、文本摘要和语音识别。

维基百科信息: [[‘可计算性理论可计算性理论是计算机科学的一部分。科学家们想知道什么是可以计算的,什么是不能计算的。‘]]

ChatGPT + 维基百科信息: seq2seq 模型,或称为序列到序列模型,是一种神经网络架构,用于自然语言处理任务,如机器翻译、文本摘要和对话建模。它由两个递归神经网络(RNNs)组成,一个编码器和一个解码器,它们共同作用将输入序列转换为输出序列。编码器处理输入序列并生成一个固定长度的向量表示,然后该向量被输入到解码器中以生成输出序列。该模型在许多 NLP 任务中表现出色,已成为语言生成的热门选择。提供的关于可计算性理论的额外信息与有关 seq2seq 模型的问题不直接相关。

初看起来,我们可以看到 ANN 索引效果很好,但我们的数据集覆盖面相当小。一种更好的方法是通过 Google 搜索结果扩展检索。你还可以在对特定领域进行 QA 时,用相关文档更新你的检索索引。

在示例 1 和 2 中,当维基百科页面似乎相关时,ChatGPT 的回答似乎在很大程度上依赖于额外的信息。但模型可能无法区分这些额外的信息是否真的有帮助(示例 2)。为了避免这种情况,你可以在提示中添加一些示例,让模型了解何时使用额外信息,何时不使用。

另一种情况是示例 3,其中维基百科文本完全不相关。幸运的是,答案似乎不受额外上下文的影响。

你可以在 这里 找到代码。希望你喜欢阅读 😃

利用 LLM 和 LangChain 实现供应链分析——一个由 GPT 提供支持的控制塔

原文:towardsdatascience.com/leveraging-llms-with-langchain-for-supply-chain-analytics-a-control-tower-powered-by-gpt-21e19b33b5f0?source=collection_archive---------2-----------------------#2023-11-17

使用 LangChain SQL 代理构建一个自动化的供应链控制塔,并连接到运输管理系统的数据库。

Samir SaciTowards Data Science Samir Saci

·

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

--

(图片来自作者)

供应链控制塔可以定义为一个集中式解决方案,提供可视化和监控功能,以高效管理端到端的供应链操作。

这个分析工具使供应链部门能够实时跟踪、理解和解决关键问题。

![](https://samirsaci.com)

使用 Python 构建的供应链控制塔 [Link] —(图像由作者提供)

在之前的文章中,我介绍了一种用于分析控制塔的解决方案(使用 Python 开发),能够自主生成事件报告。

然而,这种方法需要在提供的指标和报告范围上进行修订。

我们如何提升这个模型以提供更好的用户体验?

这一观察使我发现了像 OpenAI 的 GPT 这样的大型语言模型(LLMs)在根据每个用户请求提供量身定制分析方面的巨大潜力。

解决方案的高级概念 —(图像由作者提供)

在这篇文章中,我将分享我掌握Langchain 与 OpenAI 的 GPT 模型以及构建终极供应链控制塔的经历。

💌 免费新文章直接送到您的邮箱:Newsletter

📘 供应链分析的完整指南:Analytics Cheat Sheet

SUMMARY
**I. LLMs with LangChain for Supply Chain Analytics**
An exploration of how LangChain and LLMs can revolutionize analytics 
in supply chain management.
**1\. Scenario: Distribution Process of a Fashion Retailer**
The complex distribution operations in a global fashion retail setting.
**2\. Setting the Stage for Experimentation**
Introduce the experimental setup to test a LangChain SQL agent. 
**3\. Experimental Protocol: Starting Simple**
Start with a straightforward query to assess the agent's basic capabilities.
**II. Experiments and Key Insights**
**1\. Test 1**
Simple Prompt without Template
2\. Test 2
Prompt Template with a Context
**3\. Test 3**
Prompt Template with an Improved Context
**4\. Test 4**
Advanced Analysis with an Improved Context
**5\. Final Test**
Create a Chain to Write an Email Report
**III. LLMs Shaping the Future of Supply Chain**
**1\. What about GPTs? "The Supply Chain Analyst" Agent**
I tried the new feature "GPTs" of ChatGPT with "The Supply Chain Analyt" Agent
**2\. A Simple 'Proof-of-Concept'**
We can use an agent to track shipments with TMS data
**3\. Continuing the Prototype Development**
Challenge the agent with complex analyses and more tables.
**4\. Exploring Broader Applications in Supply Chain**
LLMs can boost the user experience for Digital Twins, Network Optimization,
Business Intelligence, ESG Reporting and many other applications

使用 LangChain 进行供应链分析的 LLMs

场景:时尚零售商的配送过程

想象一下你是一个拥有全球门店网络的国际服装集团的数据科学家。

你的项目涉及协助配送规划经理监控门店的补货过程。

供应链网络 —(图像由作者提供)

她领导着一个管理全球门店库存的计划员团队。

这个过程很简单,当库存水平达到最低水平时。

关键绩效指标是按时交货的订单百分比。

配送链过程通过时间戳跟踪 —(图像由作者提供)

从订单创建到商店交货,数据库中记录了多个时间戳和布尔标志。

  • 订单传输时间记录在‘Transmission’ 如果这是在截止时间之后,‘Transmission_OnTime’会被设置为 0。

  • 卡车装载时间记录在‘Loading’中。

    如果这是在截止时间之后,‘Loading_OnTime’会被设置为 0。

  • 卡车到达机场的时间记录在‘Airport_Arrival’中。

    如果这是在截止时间之后,‘Airport_OnTime’会被设置为 0。

  • 飞机降落在机场时,记录在‘Airport_Arrival’中

    如果这是在截止时间之后,‘Airport_OnTime’将设置为 0。

  • 卡车抵达城市由‘City_Arrival’记录

    如果这是在商店开放时间之外,‘Store_Open’将设置为 0。

最重要的时间戳是‘Delivery_Time’。它与‘Expected_Delivery_Time’进行比较,以设置‘On_Time_Delivery’的值。

我在之前的文章中提出的初始解决方案是一套视觉和报告,用于回答操作性问题。

问题 1:有多少批次运输延误了?

(作者提供的图像)

问题 2:目前运输在哪里?

(作者提供的图像)

设计这种描述性分析解决方案的主要困难在于复杂性和完整性之间的平衡

  • 如果你想回答所有潜在的操作性问题,你的报告很快就会变得非常难以使用。

  • 为了保持报告简洁,您将不会涵盖全部操作范围。

我们正在接近传统商业智能(BI)工具的极限,这些工具依赖于视觉、表格和报告来回答操作性问题。

对我来说,报告的未来在于动态定制报告,根据每个用户的问题和上下文独特定制。

我们可以使用 GPT 模型来增强用户体验,通过为每个请求提供定制的输出吗?

这是我试图用 Python 开发的简单原型来弄清楚的事情。

为实验做准备

设置很简单:

  • 一个名为‘shipments.db’的本地数据库,其中包含一个名为‘shipments’的表

  • Langchain 版本 0.0.325

  • 一个用于查询 GPT 模型的 OpenAI 密钥

  • 一个带有 LangChain、SQLite 和 Pandas 库的 Python 本地环境

解决方案的高层概述 — (作者提供的图像)

数据库包括时间戳和布尔标志,以及运输 ID、目的地和订单金额。

因此,LangChain SQL 代理(由 OpenAI 的 GPT 模型提供支持)可以访问数据库,编写 SQL 查询,并使用输出来回答用户的问题。

实验协议:从简单开始

我从一个简单的问题开始,因为我想感受代理的效果。

“五月初七天内有多少批次的运输延误了?”

正确答案是 6,816 批次运输。

代理的目标行为 — (作者提供的图像)

我希望看到代理创建一个 SQL 查询,统计从‘2021-05-01’到‘2021-05-07’期间所有运输的数量,其中布尔标志为‘On_Time_Delivery’ = False

在接下来的部分,我们将探索与代理的不同交互方法并寻找提供准确答案的最有效方法。

💡 关注我的 Medium,获取更多与🏭供应链分析、🌳可持续性和🕜生产力相关的文章。

实验与关键见解

现在一切准备就绪,我可以开始创建 LangChain 代理与数据库交互。

我使用的是AgentType.ZERO_SHOT_REACT_DESCRIPTION,这是一种设计用于“零-shot”学习环境的代理类型。

这个代理能够在没有任何特定训练的情况下回答查询。

测试 1:简单提示而不使用模板

最初的测试涉及向代理提出直接问题。

“五月前七天内有多少装运延迟?”

[块 1]: 代理从包含唯一表格‘shipments’的数据库开始探索。

[块 1]: 数据库的发现 — (作者提供的图片)

代理正在链接问题中的“延迟装运”与数据库中的‘shipments’表。

这个初始块对我们实验的所有其他迭代将完全相同。

[块 2]: 代理撰写查询并提供了错误答案。

[块 2]: 查询数据库并提供答案 — (作者提供的图片)

测试 1:目标结果(左)/ 测试输出(右) — (作者提供的图片)

👍 一个好处是代理使用了正确的日期(订单日期)来筛选所在范围内的装运。

👎 然而,他错误地选择了定义延迟装运的标志。

我们可以接受这一点,因为我们并未明确定义何为延迟装运。

测试 2:带有上下文的提示模板

我可以使用带有上下文的提示模板来改进答案。

我希望保持上下文的最小化,因为我们可能不总是知道用户会问什么。

context_explanation = """
As a supply chain control tower manager, my role involves overseeing the logistics network and ensuring that shipments are processed efficiently and delivered on time. 
The 'shipments' table in our database is crucial for monitoring these processes. It contains several columns that track the progress and timeliness of each shipment throughout various stages:
- 'Order_Date' and 'Expected_Loading_Date' provide timestamps for when an order was placed and when it was expected to be loaded for departure.
- 'Expected_Delivery_Time' is a timpestamp defining when the shipment is expected to be delivered
- 'Loading_OnTime', 'Airport_OnTime', 'Landing_OnTime', 'Transmission_OnTime' are boolean values indicating whether the shipment was processed on time at various stages. If any of these are False, it implies a delay occurred, potentially causing the shipment to miss its cut-off time and be postponed to the next day.
- 'Store_Open' indicates if the truck arrived at the store before closing time. A False value here means the delivery must wait until the next day.
- 'On_Time_Delivery' is a critical indicator of our service level, showing whether a shipment arrived by the requested delivery time.
Understanding the data in these columns helps us identify bottlenecks in the shipment process and take corrective actions to improve our delivery performance.
"""
input_question = "How many shipments were delayed in the first seven days of May?"

💡 观察: 上下文是控制塔经理角色的高层演示和数据库内容。

[块 2]: 代理撰写查询并提供了错误答案。

[块 2]: 查询数据库并提供答案 — (作者提供的图片)

测试 2:目标结果(左)/ 测试输出(右) — (作者提供的图片)

👎 代理更好地理解了中间标志,但仍然错过了要点。

这个延迟装运的定义虽然不是不合逻辑,但却与操作实际不相符。

💡 观察: 装运即使有一个或多个标志为零,也可以按时进行。只有‘On_Time_Delivery’标志可以确定装运是否延迟。

与标志相关的截止时间定义 — (作者提供的图片)

🙋‍♀️ 为了公平起见,这不是一个容易猜测的定义。

因此,我可能应该在上下文中包含一个‘延迟发货’的明确定义。

测试 3: 具有改进上下文的提示模板

我通过增加一个额外的句子来改进了上下文。

‘如果‘On_Time_Delivery’为虚假,则视为延迟发货。’

正如预期的那样,结果很好

[Block 2]: 查询数据库并提供答案 — (图片作者提供)

👋 代理选择了正确的标志来定义延迟发货。

如果我们要求进行高级分析会怎样?

延迟可能是由于数据集中包含的不同标志所捕捉到的各种原因。

已交付的发货(上: 准时,下: 延迟)— (图片作者提供)

我们的控制塔团队希望获得每个未按时交付的发货的原因代码

在这家公司中,原因代码由所有虚假的中间标志列表定义。

例如,如果发货延迟:

  1. ‘On_Time_Delivery’ 为虚假

  2. ‘Transmission_OnTime’ 和 ‘Loading_OnTime’ 为虚假。

  3. 因素代码是‘Transmission_OnTime, Loading_OnTime’

测试 4: 具有改进上下文的高级分析

让我们添加一个额外的声明

‘延迟发货的原因代码由所有对该发货为 0 的标志列表定义。’

因此,我可以向代理提出一个新问题:

提供 5 月前七天内延迟发货的总数及其按原因代码的拆分。

不幸的是,代理无法计算出正确的原因代码定义。

[Block 2]: 查询数据库并提供答案 — (图片作者提供)

经过多次迭代,我发现代理需要一些指导

因此,我修订了问题

请根据定义创建‘Reason_Code’列。然后,提供 5 月前七天内延迟发货的总数及其按原因代码的拆分。

[Block 2]: 查询数据库并提供答案 — (图片作者提供)

现在的输出符合原因的定义,并对延迟交付的根本原因进行了完整分析。

我们能否使用这个输出通过邮件发送报告?

作为最终练习,我想创建一个链条,让代理使用这个输出写一封邮件。

最终测试: 创建一个链条以编写邮件报告

使用 LangChain 创建链条涉及将多个代理组合在一起,以执行一系列任务,每个任务都使用前一个任务的输出。

  • 代理 1: SQL 查询代理 该代理解释用户的问题,制定 SQL 查询,并从数据库中检索数据。

  • 代理 2:电子邮件撰写代理 此代理从 SQL 查询代理获取处理后的数据,并撰写一封连贯且信息丰富的电子邮件。

我们要求代理 2 为我(控制塔经理 Samir Saci)向运营总监 Jay Pity 写一封电子邮件。

[区块 3]:使用代理 1 的输出撰写电子邮件 —(作者提供的图像)

💡 观察: 由于未知原因,代理将延迟发货数量按天分割。

[区块 4] 电子邮件输出 —(作者提供的图像)

输出是一封总结查询结果的电子邮件。

  • 在结束电子邮件之前,代理会进行额外的分析。

  • 语调正式,并与物流运营管理的上下文相匹配。

输出可用于使用 Python 的 SMTP 库自动发送电子邮件。

这里是一个例子,

## 使用 Python 自动化 HTML 电子邮件中的运营报告分发

使用 Python 在 HTML 电子邮件中自动分发供应链运营报告。

towardsdatascience.com

我学到了什么?

这个简单的 LangChain SQL Agents 实验教会了我…

  • 代理并非无所不知。因此,必须在上下文中解释具体的业务定义。

  • 即使有很好的上下文,代理也可能需要指导才能提供正确的输出。

  • 可以链接多个代理以执行高级任务。

  • 因为代理有时需要指导,我们可能需要培训用户以提示工程工作。

主要挑战是提供正确的上下文,以确保代理能够回答用户生成的所有问题。

📝额外评论: 我对自己使用 Chat GPT-4 感到惊讶,这对我使用其‘小弟’ GPT-3.5 Turbo 改进提示模板的上下文非常有帮助。

LLMs 正在塑造供应链未来

GPTs 怎么样?

在掌握供应链分析产品的 LLMs 时,我还尝试了 ChatGPT 的新功能 GPTs。

“供应链分析师代理” —(作者提供的图像)

用户可以通过 ChatGPT 用户界面访问此代理。

用户界面 [测试 GPT: 链接]

主意是创建一个配备核心分析模型(Python 脚本)、上下文和一些提示指导的代理。

GPT 简单架构 [测试这个 GPT:链接]

所以用户可以互动询问

  • 他们的销售进行帕累托或 ABC 分析

  • 一个报告产品组合销售分布的电子邮件模板

你可以在这篇文章中找到更多细节和示例,

[## 创建 GPTs 以自动化供应链分析

“供应链分析师”是一个自定义 ChatGPT 的“GPT”,利用销售数据执行帕累托和 ABC 分析。

s-saci95.medium.com

一个简单的“概念验证”

正如我刚刚开始这个激动人心的旅程,我积极寻求您对我在本文中分享的方法的评论和观察。

最初的结果承诺通过 LLMs 能力增强的“自助式”数据库迎来一个变革性未来。

LangChain 代理连接到多个数据产品 — (作者提供的图片)

这种解决方案,特别适用于实施数据网格的公司,可以通过响应式界面直接连接用户到数据产品,这些产品利用生成式人工智能的强大功能进行增强。

用户没有使用我们的仪表板。为什么?

它使用户能够通过自然语言进行复杂分析,打破我们当前基于仪表板的数据交互方式。

继续原型开发

这些初步测试的结论非常积极。

但在正式完成这个概念验证之前,我还有一些测试要进行。

  • 丰富数据集,包括在途运输和取消订单

  • 测试模型如何处理缺失数据

  • 将代理连接到多个数据库,并测试它如何管理多个数据源以回答问题。

我只会在生产中部署它,进行用户验收测试,以了解用户可能会问什么问题 (并监控代理的行为)

我将在未来的文章中分享我的未来实验。如果你感兴趣,欢迎在 Medium 上关注我。

探索供应链中更广泛的应用

作为供应链数据科学家,我的实验并不止步于此。

我迫切希望探索 LLMs 在供应链分析领域内的其他应用。

这些包括将 LLMs 与优化模型集成:

  • 👬📈 供应链数字孪生

    应用:这个代理将帮助用户使用自然语言场景触发模拟。 (用户可以问:“如果我们把中央仓库移到意大利会怎样?”)

## 什么是供应链数字孪生?

使用 Python 探索数字双胞胎:建模供应链网络、增强决策能力并优化操作。

towardsdatascience.com

  • 🔗🍃 可持续供应链网络设计

    应用:用户可以通过使用自然语言来制定目标和约束,从而创建优化模型。 (用户可以询问:“我希望创建一个满足法国市场需求并最小化 CO2 排放的工厂网络。”)

## 创建可持续供应链优化 Web 应用程序

帮助你的组织结合可持续采购和供应链优化,以减少成本和环境影响…

towardsdatascience.com

  • 🏭🍃 可持续采购:选择最佳供应商

    应用:采购团队可以使用自然语言制定他们的绿色倡议,并查看对成本的影响。

    (用户可以询问:“我们希望估算仅选择碳中和工厂来采购此 SKU 的成本。”)

[## 什么是可持续采购?

你如何利用数据科学来选择最佳供应商,同时考虑可持续性和社会指标…

s-saci95.medium.com](https://s-saci95.medium.com/what-is-sustainable-sourcing-46ad1fade14f?source=post_page-----21e19b33b5f0--------------------------------)

我们还可以使用我们的代理来提高数据质量或支持用于战略报告的数据审核:

  • 📉📄 ESG 报告:环境、社会和治理报告

    应用:自动化审核用于编制报告的数据。

    (审计员可以询问:“你能检索用于计算工厂 XYZ 能源消耗的公用事业账单吗?”)

## 什么是 ESG 报告?

利用数据分析进行全面有效的公司环境、社会和治理报告

towardsdatascience.com

  • 📉✅ 数据质量是什么?

    应用:使用我们的代理来挑战或支持确保数据准确性、一致性和完整性的方法。

    (用户可以问:去年送达但状态丢失的货物数量能分析吗?)

## 什么是数据质量?

探索确保供应链数据的准确性、一致性和完整性的方法论。

towardsdatascience.com

每个领域都有巨大潜力,可以利用生成式 AI 在公司中部署“分析即服务”解决方案。

例如,你能通过智能代理改进这个网页应用的用户界面吗?

ABC 分析与帕累托图应用程序 — [链接]

如果你分享这种热情,请在评论区提供建议!

关于我

让我们在LinkedinTwitter上互相关注,我是一名供应链工程师,利用数据分析来改善物流运营并降低成本。

如果你对数据分析和供应链感兴趣,请访问我的网站。

[## Samir Saci | 数据科学与生产力

一个专注于数据科学、个人生产力、自动化、运筹学和可持续发展的技术博客。

samirsaci.com](http://samirsaci.com/?source=post_page-----21e19b33b5f0--------------------------------)

💡 关注我的 Medium获取更多与 🏭 供应链分析、🌳 可持续发展和 🕜 生产力相关的文章。

利用机器学习开发有效的营销策略

原文:towardsdatascience.com/leveraging-machine-learning-for-effective-marketing-strategy-development-99b1b887f2f5?source=collection_archive---------9-----------------------#2023-08-01

使用机器学习成功构建营销策略的技巧与窍门

Elena K.Towards Data Science Elena K.

·

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

--

图片来源于 unsplash.com

营销归因模型今天被广泛应用于构建营销策略。这些策略基于将信用分配给客户旅程中的每一个接触点。虽然有很多不同类型的模型,但它们可以分为两组:单触点归因模型多触点归因模型。通常,你可以轻松解释和实施这些模型。它们在少数情况下可能甚至很有用。然而,大多数模型本身无法构建一个稳健的营销策略。问题在于所有这些模型要么基于不适用于某些数据/行业的规则,要么依赖有限的数据量,从而导致有价值的见解丧失。要了解更多关于营销归因模型的类型,可以查看我的上一篇文章

今天,我想讨论我们如何利用机器学习开发营销策略,我们使用的数据,以及我们取得的成果。在本文中,我们将讨论以下问题:

  1. 从哪里获取数据比较好?

  2. 如何为模型训练准备数据?

  3. 如何有效利用模型预测并得出有意义的结论?

我将使用我们一个客户的数据来展示所有这些内容,部分内容经过修改。这些修改不会影响整体结果。我们将这个公司称为XYZ。这些数据的发布已获得客户许可。

数据

有几种方法可以从网站获得流量日志。这些方法并不总是提供你分析所需的全面信息。然而,有时可以将一个来源集成到另一个来源中,而其他时候,你可以手动积累和结合多个来源的数据。你也可以自己编写脚本来收集必要的信息。现在,让我们稍微讨论一下当前最热门的来源以及你可以从中获得的数据:

Google Analytics

Google Analytics (GA4)是一个强大的平台,提供各种网站分析工具,并允许你测量应用程序和网站的参与度和流量。它通常使用最后点击归因,不过,你可以通过收集以下GA4数据来构建你的自定义机器学习归因模型

  • 自动事件(例如ad_click, ad_impression, app_exception, file_download, first_visit, page_view 等);

  • 增强测量scroll, click, video_start, video_progress 等);

  • 推荐事件 (add_to_cart, begin_checkout, add_payment_info, purchase, add_to_wishlist 等)

  • 自定义事件

Google Analytics 为不同的行业提供了不同的事件。

Meta Pixel

Meta Pixel 是一个可以跟踪广告推广和访客在网站上活动的工具。它为你提供了观众如何与 Facebook 和 Instagram 广告互动的见解,以及这些用户点击广告后在你的网站上的行为数据。一般来说,你将获得与使用 Google Analytics 时相同的数据。然而,Meta Pixel 更专注于再营销,因此相比于 Google Analytics,你将获得更多的再营销工具。

Yandex Metrika

Yandex Metrika 具有与上述服务类似的功能。然而,它有其优缺点。缺点是 Yandex Metrika 对一个账户的处理请求有限制(每天 5,000 个请求)。与此同时,Google Analytics 的限制是每天 200,000 个请求。优势是 Yandex Metrika 具有 Webvisor,它可以帮助你获取所有的鼠标移动。

并不是所有可用的服务都可以用来获取用户数据。尽管每个数据源中都表示了许多类型的数据,但在选择数据源时,你可以关注报告配置的便捷性以及与其他产品的集成。我们选择了 Google Analytics (GA4),因为它提供了全面的数据和便捷的工具。此外,数据可以轻松与 BigQuery 集成,我们利用 Google Cloud 基础设施。因此,原始数据如下:

数据准备

回到主要任务,我们的目标是确定哪些 广告活动 更具投资吸引力,以便在保持或增加收入水平的同时减少预算分配的开支。因此,GA4 数据的表示对我们来说很方便,因为它包含有关每个用户操作/接触点的信息,例如:

  • 按钮点击

  • 滚动

  • 照片查看

  • 搜索等

反过来,所有这些行为可以进一步转化为微转化,这正是我们需要的。我们将使用这一组微转化来预测用户在每次会话中进行购买的可能性。

在解决此类任务时,以下微转化可能会引起兴趣:

  • 访问促销页面

  • 查看热门或关键产品

  • 搜索特定尺寸

  • 查看产品照片

  • 查看所有产品照片

  • 查看产品护理信息

  • 将产品添加到购物车等

实际上,你可以自行设定任何数量的微转化。微转化的选择很大程度上取决于你店铺/业务的具体特点。

最终,我们确定了以下特征和微转换用于我们的模型。我们所有特征的总数是 97。这是我们特征的子集:

你可以看到很多与 UTM 相关的特征,它们的含义如下:

  • utm_source 是用于创建媒介的平台或工具的名称;

  • utm_medium 确定流量的类型或高级渠道;

  • utm_campaign 是营销活动的名称;

  • 其他 utm 特征指的是用户旅程或会话中的第一次接触点。

让我们回到其他特征的讨论。一些列在原始数据中可用,因此你无需对它们做任何处理。然而,有些列尚未准备好,你需要先进行一些操作。以下是我们如何获得一个微转换的示例,例如将产品添加到购物车:

模型

我想提醒你,使用这个模型,我们希望获取每个接触点的用户购买概率。然后,我们将其转换为会话中的购买概率。因此,我们使用了一个分类模型,在其中我们利用 predict_proba 获取每次用户交互的购买概率。在尝试了从线性到增强的多个模型后,我们决定使用 CatBoostClassifier。在部署和每天重新训练模型之前,进行了超参数调优。我们不会深入探讨模型创建的细节,因为我们遵循了经典的超参数调优、随后的模型训练和相关指标计算的方法。

现在模型正在使用一个月的数据进行训练,因为将这一时长更改为更长或更短的期间并没有显示出显著的改进。此外,我们使用 0.1 作为确定购买的阈值。我们特别使用这个值,因为它比我们客户的基准购买概率高 10 倍。这作为我们考虑这些事件并调查是否已发生购买的触发器,如果没有发生,则查明原因。换句话说,任何模型概率 > 0.1 的动作都被归类为购买。因此,我们获得了以下 召回率准确率 指标的值:

Recall on the TEST: 0.947
Accuracy on the TEST: 0.999

根据获得的指标,我们可以看到我们仍然遗漏了一些购买。可能这些购买的路径与典型用户旅程有所不同。

所以,我们有了所有特征和模型概率,现在我们想要建立一个报告,并了解哪些 广告活动 被低估,哪些被高估。为了获得 ad_campaign,我们将 utm_sourceutm_mediumutm_campaign 特征结合起来。然后,我们将每个用户会话中的最大概率取出,并将其乘以与测试数据集相同时间框架内的平均订单值。随后,我们通过计算每个广告活动的总和生成报告。

它给我们带来了以下报告:

现在我们必须转向营销指标。由于我们希望衡量营销活动的成功,我们可以考虑以下指标,这些是营销人员经常使用的:

  • ROAS(广告支出回报)是衡量数字广告活动效果的营销指标;

  • CRR(成本收入比)衡量运营费用与企业产生的收入之间的比例。

我们将使用我们的数据进行计算,并与营销人员通常通过last-click attribution获得的ROASCRR值进行比较。

由于我们在分析的期间内只看到三个付费活动,我们将找到这些活动在GA4中的指标,并基于last-click attribution添加实际ROASCRR。我们在上一篇文章中讨论了为什么last-click attribution不是评估广告活动贡献的准确方法。

使用上述公式,我们将计算包含预测ROASCRR的最终报告:

现在我们有了所有数据来得出关于广告活动的结论:

  • 我们可以看到活动“google/cpc/mg_ga_brand_all_categories_every_usa_0_rem_s_bas”被高估了,因为其预测ROAS比基于last-click attributionROAS2 倍。很可能,用户在点击这个广告活动后经常进行购买,但他们已经是潜在客户。

  • 广告活动“instagram / cpc / 010323_main”被低估了,因为其预测ROAS比实际ROAS4 倍

  • 并且活动“google / cpc / mg_ga_brand_all_categories_every_latvia_0_rem_s_bas”的预测和实际ROAS相似。

通过这些数据,你可以独立制定下一个周期的营销策略。同时,你也不应忘记营销策略需要测试。然而,这超出了我们文章的范围。

在本文中,我们讨论了机器学习如何用于构建营销策略。我们涉及了数据选择、建模的数据预处理、建模过程本身,以及从获得的结果中提取见解。如果你也在进行类似的任务,你使用的方法也会引起我的兴趣。

感谢阅读!

我希望今天分享的见解对你有价值。如果你想联系我,请随时在我的LinkedIn上加我。

利用管理仪表板进行故事讲述:这是一个可行的途径吗?

原文:towardsdatascience.com/leveraging-management-dashboards-for-storytelling-a-viable-pathway-4bdeeed1fa59

释放叙事力量:数据讲述与仪表板可以共存吗?

Michal SzudejkoTowards Data Science Michal Szudejko

·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 8 月 2 日

--

用数据讲故事”这个短语最近受到越来越多的关注。我在自己文章的观众数量增长以及我主持的相关研讨会的参与人数增加中感受到了这一趋势。同时,这一领域的有价值资源也有所增加。这与 2018 年或 2019 年我首次深入研究这一主题时的情况形成了鲜明对比,当时除了 Cole Nussbaumer KnafflicBrent Dykes 出版的优秀书籍和其他内容外,几乎找不到有用的信息。

照片由 Campaign Creators 提供,来源于 Unsplash

在他的一个帖子中,Brent Dykes[1]将编写引人入胜的故事过程分为三个步骤:

  1. 故事框架: 这个阶段涉及对数据的深入探索,特别关注独特的维度和指标。

  2. 故事形成: 在这个阶段,任何之前检测到的异常或趋势都会经过彻底的调查。本质上,这是我们培养洞察力的阶段。 在这个阶段评估我们的发现对业务的重要性、理解其来源以及识别发生原因是至关重要的。通常,这个过程需要多轮发现、分析和结论的制定,这进一步细化了研究范围,并要求对数据进行更为专注的分析。

  3. 讲故事: 在这个阶段,我们决定如何使用我们挖掘出的洞察。如果我们决定将这些发现分享给更广泛的受众,讲故事的行为变得至关重要。在这一步,我们润色叙事,设计支持性的视觉效果,并准备适当的评论。**

根据戴克斯的说法,管理仪表板主要在初始阶段展示其价值。它们通过有效的过滤技术促进多维数据分析,并允许用户通过下钻或上钻功能深入细节或从更广泛的视角查看数据。因此,它们有助于识别数据中的模式或异常。

戴克斯说:

… 曾经存在一个重大问题 —— 我从未相信仪表板能讲述数据故事。我在这个 BI 平台(或其他任何平台)中看到的都没有让我改变看法。因为自动化仪表板中的数据不断刷新,潜在的故事点变成了移动的目标——时隐时现,变化不断。

一方面,很难不同意他的观点,不是吗?然而,另一方面,情况真的是这样吗?我提出管理仪表板可能在讲故事过程中扮演更重要的角色。让我们深入探讨它们的功能,并探索如何增强它们在这一过程中的作用。

什么是管理仪表板?

从本质上讲,管理仪表板作为信息管理工具,使用户能够跟踪对任何业务至关重要的关键绩效指标(KPIs)和/或关键结果指标(KRIs),无论是财务还是非财务的。通常,这些仪表板用于呈现当前或历史数据,但越来越多地,它们也在融入预测或情景规划功能。管理仪表板有三种关键类型:

操作性: 这些通常用于回答“当前情况是什么?”这个问题。它们通常包含实时或非常近期的数据。

战略性: 主要由高级管理人员用来监控 KRIs 和 KPIs,这些仪表板通常展示历史数据以提供更广泛的视角,并且经常包括财务数据。

分析性: 这些仪表板数据丰富,用于分析基于历史或预测数据的趋势、关系或异常。

每种仪表板类型在本质上都是独特的,服务于不同的用户,满足不同的需求,因此需要不同的设计方法。

这意味着它们也可以按照讲故事的原则进行设计,特别是操作性和战略性仪表板。

如何按照数据讲故事原则设计管理仪表板?

这可以通过以下方式实现:

  1. 应用叙事结构

  2. 应用视觉层次结构

  3. 使用正确的视觉效果

  4. 使用交互性

  5. 与接收者沟通。

1. 应用叙事结构

在这里,我们可以使用 Brent Dykes 提倡的“讲故事弧”概念 [2] 用于静态叙事练习(如 PowerPoint 演示文稿)。

讲故事弧概念。来源:作者提供的图像,基于 [2]

以下是我如何将这一概念翻译到管理仪表板上的:

来源:作者提供的图像

这一概念的指导原则集中在仪表板内信息的组织上。最顶部应该展示最重要、综合的高层信息。根据当前的问题,用户可以决定是否深入浏览仪表板的其他部分。随着报告页面的深入,分析可以变得越来越复杂,涵盖更多维度或提供更详细的信息。报告或关键视觉效果的末尾(或侧边)应包括一个专门的推荐区域。这应帮助用户理解观察到的变化是积极的还是消极的,以及这些变化对业务的含义。例如,这可以表现为仪表板内直接生成的预测。

2. 应用视觉层次结构

仪表板设计的另一个关键方面是创建视觉层次结构。目标是确保所有重要元素都易于查看和访问。这可以通过调整颜色、大小或空间位置来实现。

下面,我展示了两个例子来说明视觉层次结构的应用。顶部图像展示了在选择度量值(“当前年份”)时图表如何变化。底部图像则提供了应用于报告级别的视觉层次结构的视角。这两种可视化均使用 Power BI 制作。

应用视觉层次结构。来源:作者提供的图像

对齐在建立视觉层次结构中也扮演了至关重要的角色。必须正确对齐所有视觉元素,以避免无意中暗示某些元素比其他元素更重要或更不重要。大多数现代仪表板工具提供对齐功能,其中指南特别有用。下图展示了在 Power BI 中使用这些指南的效果。

来源:作者提供的图片

另一个关键建议是尽可能多地加入“留白”:让你的仪表板有呼吸的空间。在设计报告时记住这一点,将有助于更友好的布局,防止信息过载,增强有效传达意图的可能性。这是我对一个平衡良好的仪表板可能样子的演绎(白色实际上是灰色)。

平衡仪表板。来源:作者提供的图片

3. 使用正确的视觉元素

什么是正确的视觉元素?

让我澄清一下,什么不是这样。只需在 Google 搜索引擎中输入“最佳讲故事互动可视化”,你将被无数的例子淹没。不要误解我,大多数这些都是非常有效且视觉上令人愉悦的。然而,它们也需要大量的时间和资源来创建,使得它们在现实世界中不那么可行。那么,哪些视觉元素确实会被使用?毫无疑问,以下三个图表脱颖而出:

三个总是有效的图表。来源:作者提供的图片

是否总是只有这三种图表?

幸运的是,不一定。根据具体目的,其他几种图表类型也可能有效。例如,你可以考虑将子弹图或摘要图表纳入“问题”区域。

摘要图表。来源:作者提供的图片

子弹图。来源:作者提供的图片

我特别喜欢使用微型图表,因为它们有两个功能。首先,它们提供信息。用户可以对当前趋势有一个总体了解。其次,它们有导航的作用。用户可以点击感兴趣的图表或突出显示的重要干扰。

微型图表。来源:作者提供的图片

4. 使用互动性

互动性是管理仪表板应该真正突出的领域。依我看来,互动性涵盖了两个维度——一个是在工具或过程层面,另一个是在数据或报告层面。

从工具或过程层面来看,互动性可以从三个方面进行评估:

  1. 每个仪表板的数据更新频率。

  2. 数据访问和协作的便利性。例如,用户能否在内部和外部共享仪表板(注意安全问题)?访问报告是否需要 IT 干预,还是用户可以独立管理?

  3. 仪表板的速度。相信我,在这个时代,没有人会有耐心等待性能缓慢的仪表板。

在数据或报告层面,可以通过以下工具引入互动性:

1. 过滤器和滑块:这些通常被使用。高层管理人员在单个仪表板页面上的过滤器应尽量少。这些过滤器也应在选项卡之间功能齐全,并允许全局重置(即,从特定选项卡或整个仪表板中删除所有过滤器)。

2. 警报:电子邮件或推送通知帮助激励用户在新数据可用时访问仪表板。它们必须在内容和频率上保持平衡,以防止它们被标记为垃圾邮件。

3. 放大功能:这对于有多个可视化的拥挤报告页面特别有用。

4. 链接:页面或可视化之间的连接可以改善用户导航。

5. 深入分析和下钻选项(有时也包括上钻):允许深入数据或根据需要获取更广泛的视角。

最后,应考虑创建一个移动优化的报告视图,以提高可访问性。

在 Power BI 中准备移动视图。来源:作者提供的图像

5. 与接收者沟通

许多设计师常常忽视的一个方面是与接收者的对话。这可以以各种形式表现出来。其中一种形式是记录所有必要的定义、计算假设,或甚至报告的作者或拥有者的名字,使个人更容易寻求对报告的澄清。

Power BI 中提供的一种新形式是“智能叙事”。虽然它并不完美(尤其是在波兰语等语言中),但它可以提供有关特定页面上显示的可视化的一些基本信息。

最后,工具提示代表了另一种沟通方式,通常可以根据用户偏好进行自定义。通过工具提示,用户可以获得特定数据点的精确信息。

Power BI 中的工具提示。来源:作者提供的图像

结论

总之,我希望这篇文章清楚地描绘了通过一点努力,仪表板如何成为讲故事过程中的强大工具。通过专注于周到的报告设计和视觉层次结构,选择合适的视觉元素,融入互动性,并积极与仪表板用户互动,你可以将数据展示提升到一个全新的水平。让我们利用仪表板的力量,从数据中打造引人入胜的叙事!

关注我在 medium.com 上的动态。

在 LinkedIn 上与我联系。

访问我的博客(波兰语).

访问我的 YouTube 频道(波兰语,提供英文字幕的影片)。

参考文献:

[1] Dykes, Brent, 故事框架与故事形成:数据讲故事前的两个关键步骤,2023 年 2 月 22 日。

[2] Dykes, Brent, 有效的数据讲故事:如何通过数据、叙事和视觉驱动变革,2019 年 12 月 17 日。

利用运筹学提升呼叫中心效率

原文:towardsdatascience.com/leveraging-operations-research-for-call-center-efficiency-c625f062c34d?source=collection_archive---------5-----------------------#2023-05-25

利用高效的劳动力优化来推动组织卓越

Saif Ali KherajTowards Data Science Saif Ali Kheraj

·

查看 发表在 数据科学前沿 ·11 分钟阅读·2023 年 5 月 25 日

--

在当今的数据驱动世界中,组织可以通过利用数据的力量和应用先进的优化模型,高效配置资源、降低成本并提供卓越的客户体验。

当预测分析与处方分析相结合时,它使得管理者能够在公司内做出关键决策。我们将通过利用预测分析的输出 —— 每小时呼叫中心服务代表的需求 —— 来观察这一点,以通过优化来有效分配客户服务代表,最终将导致客户满意度的提高和整体业务费用的降低。

图 1:分析金字塔及处方分析的重要性(图由作者提供)

概述

呼叫中心公司面临的关键挑战之一是低接听率,这可能导致客户不满,并可能导致客户流失。例如,如果呼叫中心公司的接听率仅为 70%,这意味着 30%的客户无法联系到公司,这可能会导致挫败感和不满。

在这篇文章中,我们将通过使用带有方程和问题解决的示例场景,解决一个优化问题,以便更好地安排呼叫中心的排班。这将帮助你详细理解一切。我们将在优化过程中施加一些约束,最小化每天和每小时呼叫中心服务代表的短缺。然后你可以更深入地提出一些有趣的软约束,例如几周后的可能休假等,以提高代理的积极性。

图 2:Freepik

示例场景:

我们有十名呼叫中心代理必须每天分配到特定的班次。

班次和排班类型

一天中可能有几个班次,中间有休息时间。让我们看看公司管理层如何设计各种可能的班次。

图 3 — 概念模型 1:带有预定义休息时间和工作时间的班次矩阵(由作者提供)

所以我们有公司管理层设定的总共 16 个班次。例如,如果一名呼叫中心代理被分配到班次 5,则该代理将基本上从 4 点到 12 点工作,中间有一个定义好的休息时间。因此,代理在总共 9 小时的工作时间中,有一个休息时间。

在这种情况下,我们用变量 K 定义班次,用变量 T 定义小时。

设 A 为班次矩阵,如果班次 k 覆盖了时间段 t,则 Akt 为 1,否则为 0。

方程 1:班次矩阵分配(由作者提供)

我们希望将一个客户服务代表(CSR)分配到每天的一个班次。这些班次将本质上告诉员工在优化后应何时到岗、离岗以及休息。这本质上是我们模型的第一个组成部分。

轮班分配矩阵(决策变量)

图 4— 概念模型 2:轮班分配矩阵 — CSR 是否被分配到特定轮班(由作者提供)

然后是轮班分配矩阵,我们可以用 I 和 J 来表示客户服务代表和日期。K 已经定义为轮班,我们在前面的部分中定义了总共 16 个轮班。

这个矩阵简单地指示客户代表 I 是否在特定的轮班 K 被分配到日期 J。这基本上是我们的决策变量,我们感兴趣的是将 CSR 分配到正确的轮班,以便在每小时最小化短缺。

决策变量(数学符号)

方程 2:轮班矩阵分配(由作者提供)

短缺

现在让我们来研究短缺的概念。

图 5— 概念模型 3:获取总分配、需求和短缺(由作者提供)

需求已经通过预测模型提供给我们,一旦我们知道每小时分配的客户服务代表总数,我们就可以计算总分配代表数。一旦我们定义了轮班分配矩阵(图 4 — 概念模型 2) 和轮班时间表矩阵(图 3 — 概念模型 1),我们就可以快速构建此表格。我们可以简单地对 10 x 16(概念模型 2)和 16 x 24(概念模型 1)进行点积,得到 10 x 24,其中 10 是呼叫中心服务代表的总数,24 是小时数。因此,通过将每个 CSR 分配到每小时,我们可以轻松实现上述目标(*图 5 — * 概念模型 3)。这使我们能够轻松计算每一天和每小时的总分配 CSR。为了简便起见,我们假设需求为三,但在现实世界中,这将来自基于历史数据的预测模型。

预测模型将基于不同的因素和变量,如历史呼叫量、客户人口统计、季节性、产品/服务趋势。

一旦我们有了总分配和需求,我们可以轻松计算短缺,这正是我们需要最小化的,并将作为我们的目标函数的基础。这就是我们的第三个概念表的总结。

目标函数

现在我们来定义短缺的概念,因为我们需要最小化它。让 y[j,t] 代表第 j 天的时间段 t 中 CSR 的短缺。目标是最小化所有时间段和日期中的 CSR 总短缺,如下面的方程 3 所示。

y[j,t] 是一个派生的决策变量,因为它是基于轮班调度优化模型中的其他决策变量和参数计算得出的。

你可以简单地将短缺视为在概念模型表格 3(上述)中对所有天数进行红色突出显示的总和。

方程 3:目标函数以最小化短缺(图由作者提供)

我们将在约束部分进一步探讨短缺的概念。

约束条件

硬性约束:

  1. 对于每一天和每个时间段,短缺应大于或等于零。短缺是需求和分配的 CSR(供应)之间的差值。如果分配的 CSR 超过了需求,短缺被视为零。我们必须能够表达需求和供应,以便定义短缺。Djt(第 j 天 t 时段所需 CSR 的需求)。让我们将 D[j,t] 定义为第 j 天 t 时段的 CSR 需求。

我们可以简单地使用班次安排矩阵(图 3 — 概念模型 1 图)和班次分配矩阵(图 4 — 概念模型 2 图)来计算每一天特定小时的供应或总分配 CSR。让我通过计算第 0 小时分配的 CSR 数量来演示。

让我们选择我们的班次矩阵(图 3 — 概念模型 1)的第一列(第 0 小时)和班次分配矩阵(图 4 — 概念模型 2)的第一行(CSR 1)。

图 6— 点积运算以计算第 0 小时分配的总 CSR(作者提供)

这是一个直接的点积运算。我们选择了特定一天(星期一)的第 0 小时作为示例。我们对班次矩阵(第 0 小时的图 3)和班次分配(CSR 1 的图 4)进行点积运算。由于 CSR 1 被分配到班次 # 5,该班次没有第 0 小时作为工作时间,因此结果将为零。我们将计算第 0 小时的班次矩阵与所有 CSR 1–16 的乘积,并将其相加以获得第 0 小时的总分配 CSR。我们可以简单地对每个 CSR(I)的所有班次 K 进行求和,并对所有 I 和 J 进行此操作。这是直观的理解,下面的方程与我描述的内容相对应。

方程 4:定义短缺—非线性函数(图由作者提供)

为计算短缺,只需取需求和供应之间的差值(需求 — 供应),如果值为负,表示过剩,我们将其视为 0。通过使用 max 函数来实现这一点。然而,这将不再是线性程序。我们希望能够将此方程转换为线性形式,以便使用线性规划来解决。让我们现在为此定义线性方程。

方程 5:定义短缺—线性函数(图由作者提供)

因此,我们已经成功将非线性方程转换为线性方程。

2. 每天仅一班次约束:每位 CSR 每天只能被分配到一个班次,以防工作负荷过重。

方程 6:每个人每天应只参加一个班次(图由作者提供)

对于每个 CSR I 在每一天 J,人员应该正好选择一个班次,所以这里我们只是对该 CSR 在第 J 天的班次 K 进行求和。直观地,你可以把它看作是在班次分配矩阵中对特定 CSR 的求和,并确保它小于等于 1。换句话说,班次分配矩阵中某一天的某行(CSR)必须小于等于 1。

如果人员休假,他应该选择班次 0,你可以自己定义。

软约束

  1. 最大额外 CSR 约束通过设定最大限制来控制额外在岗 CSRs 的数量。这是为了减轻负担并确保积极的积压。我们不会实现这一部分,但让我们讨论一下方程。

方程 7:在岗额外 CSRs 的最大数量 — 可选(作者提供的图示)

软约束涉及到额外在岗 CSRs 的最大数量,这意味着超过满足客户需求所需的最低数量的额外 CSRs。我们希望尽量减少额外的 CSRs 数量,但确保目标函数值,记作 W1,至少等于每一天每个时间段的额外 CSRs 总数。

为了实现这一点,我们引入了一个软约束,规定了额外 CSRs 的上限。这个约束确保通过从供应中减去对 CSRs 的需求得到的额外 CSRs 数量不超过指定的最大值。

为了包括这个软约束,我们需要通过添加加权组件 q 来稍微修改我们的目标函数。

方程 8:修改后的目标函数 — 可选(作者提供的图示)

目前这超出了范围,但这是为了理解。

实施前的最终方程

最终方程 9(作者提供的图示)

在 Python 中的实现(Pyomo 包)

让我们选择之前讨论的相同场景:10 个 CSRs,每天 16 个班次,24 小时,1 天。

 shift_matrix_array = np.array(
    # Shift 1
    [[1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
     [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1],
    ])

num_csrs=10
num_days=1
num_shifts=16
num_times=24
shift_matrix_array.shape
shift_matrix_dict=dict()

for row in range(shift_matrix_array.shape[0]):
    #print(row)
    for column in range(shift_matrix_array.shape[1]):
        shift_matrix_dict[row,column]=shift_matrix_array[row,column]

model = ConcreteModel()

# Define the sets
model.I = Set(initialize=range(num_csrs))
model.J = Set(initialize=range(num_days))
model.K = Set(initialize=range(num_shifts))
model.T = Set(initialize=range(num_times))

model.shift_matrix = Param(model.K, model.T, initialize=lambda model, k, t: shift_matrix_array[k][t])

demand = {(j, t): 3 for j in range(num_days) for t in range(num_times)}

model.shift_demand = Param(model.J, model.T, initialize=demand)

# decision variables
model.x = Var(model.I, model.J, model.K, domain=Binary)
model.y = Var(model.J, model.T, within=NonNegativeReals)

model.demand_constraint = ConstraintList()
for j in model.J:
    for t in model.T:
        model.demand_constraint.add(
            model.y[j, t] >= model.shift_demand[j, t] - sum(model.x[i, j, k] * model.shift_matrix[k,t] for k in model.K for i in model.I)
        )

model.one_shift_per_day = ConstraintList()
for i in model.I:
    for j in model.J:
        model.one_shift_per_day.add(sum(model.x[i,j,k] for k in model.K) <= 1)

model.obj = Objective(
    expr=sum(model.y[j, t] for j in model.J for t in model.T) ,
    sense=minimize
)

solver = SolverFactory('glpk')
solver.solve(model)

输出:

print("Day | Hour | Assigned CSRs")
for j in model.J:
    for t in model.T:
        assigned_csrs = []
        for i in model.I:
            for k in model.K:
                if model.x[i, j, k].value == 1 and model.shift_matrix[k, t] == 1:
                    assigned_csrs.append(i)
        print(f"{j}   | {t:02}  | {assigned_csrs}")

图 7— 每小时在特定日期分配的 CSRs(作者提供的输出)

print("CSR | Assigned Shift")
for i in model.I:
    assigned_shift = []
    for j in model.J:
        for k in model.K:
            if model.x[i, j, k].value == 1:
                assigned_shift.append(k)
                #break
        if assigned_shift is not None:
            break
    print(f"{i}   | {assigned_shift}")

图 9— 分配给每个 CSR 的班次(作者提供的输出)

total_shortage = sum(max(model.shift_demand[j, t] - sum(model.x[i, j, k].value * model.shift_matrix[k, t] for i in range(num_csrs) for k in range(num_shifts)), 0) for j in range(num_days) for t in range(num_times))

print("Total shortage:", total_shortage)

图 10— 优化后的总短缺(作者提供的输出)

如我们所见,尽管仍然缺少 4 个 CSRs,但这是经过优化的结果,我们可能需要招聘额外的 CSRs。因此,这也指导了招聘决策过程,并且我们可能节省招聘额外 CSRs 的成本。

结论:

总之,尽管呼叫中心排班的代码看起来很简单,但在实施优化模型之前,必须有一个扎实的概念模型,并对各种因素进行深思熟虑。通过结合软约束,如考虑额外值班客服代表的最大数量以及解决短缺问题,我们可以有效应对呼叫中心排班相关的挑战。

无论是医疗保健人员配置、零售劳动力管理,还是物流规划,数据科学和优化的应用都能帮助解决资源分配和短缺的类似挑战。通过利用这些方法,组织可以优化运营、提高效率,并增强客户满意度。COVID-19 大流行突显了跨行业高效资源管理的重要性。服务需求和有效排班的需求比以往任何时候都更加突出。

参考文献

[1] 市场研究 — www.reportlinker.com/p087286/World-Call-Centers-Markets.html

[2] 一种混合人员排班模型用于员工排班问题 — www.mdpi.com/2227-7390/8/10/1702

[3] 人员排班: www.coursera.org/learn/operations-research-modeling

利用 qLoRA 对任务精调模型进行精调而不产生灾难性遗忘:以 LLaMA2(-chat)为例

原文:towardsdatascience.com/leveraging-qlora-for-fine-tuning-of-task-fine-tuned-models-without-catastrophic-forgetting-d9bcd594cff4?source=collection_archive---------1-----------------------#2023-09-07

了解如何在保持任务特定性质的同时,将知识注入目的精调模型

Aris TsakpinisTowards Data Science Aris Tsakpinis

·

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

--

图片由 StableDiffusionXL 提供,托管在 Amazon Web Services

这篇文章讲了什么,为什么重要?

像 Anthropic 的 Claude 或 Meta 的 LLaMA2 这样的语言大模型(LLMs)在各种自然语言任务中展示了令人印象深刻的能力。然而,它们的知识和任务特定技能仍然相对通用——如果你想执行需要明确知识的更专业的领域特定任务,你需要找到方法将知识注入模型并教它们任务特定的行为。LLM 驱动的应用需要在其目标领域中正常工作,提供准确的答案而不是产生幻觉,并确保安全、隐私和适当的内容。

这些挑战通常被称为“有用性、诚实性和无害性”的“三个 H”。克服这些挑战在设计企业级应用时被证明特别重要。

将领域知识传递到基础模型中有几种选择:

作者插图

  • 源知识: 你可以通过提示工程和基于示例或上下文注入生成动态提供知识。这种选项以相对简单的方式呈现,如零样本或少样本提示。然而,还有更复杂的方法来整合外部知识源。通过动态提示组合注入知识,从各种外部“工具”(检索增强生成、数据库检索器、数据湖检索器、通过 REST-ful API 的实时系统等)获取信息的源知识正在变得越来越受欢迎。

  • 参数知识: 你可以通过微调直接更新模型参数。这样,预训练的权重将调整到一个比预训练时更小的、领域特定的数据集,从而接近于领域特定的知识库或任务。

正如 Heiko Hotz 在他的 博客文章 中解释的那样,选择正确的方法(或两者的组合)存在权衡。在这篇博客中,我们将重点介绍参数方法,并演示如何在 Amazon SageMaker 上使用 PEFT(参数高效微调)对 LLaMA2 模型进行微调。

我们的目标是将 LLaMA2 适应于特定领域,获取最新的知识以克服“知识截止”问题,即模型缺乏对未纳入其训练数据的最新信息的意识。与任务特定的微调不同,这对于许多从业者来说是一个更可实现的任务,因为他们可以简单地使用包含领域特定信息的文本语料库作为训练数据集,而不是像对话或指令数据集那样手动制作或收集任务特定的数据集。

由于许多相关的 LLM 驱动的使用场景中,任务特定模型是有益的,我们还将展示所提议的设置如何同样适用于像 LLaMA2-chat 这样的模型,这些模型已经进行了任务特定的微调而不失去其任务特定特性(例如指令跟随、对话行为等)。

通过详细介绍这一端到端的知识注入工作流程,我们提供了一个将基础模型调优以满足特定需求的实用指南。

模型

LLaMA2 模型于 2023 年 7 月发布,并附有一篇研究论文。在论文中,Touvron 等人表示,LLaMA2 是“一个预训练和微调的大型语言模型(LLMs)集合,规模从 70 亿到 700 亿个参数不等。我们的微调 LLMs,称为 Llama 2-Chat,专门针对对话使用场景进行了优化。我们的模型在我们测试的多数基准上超过了开源聊天模型,并且根据我们对有用性和安全性的人工评估,可能是闭源模型的合适替代品。”

如前所述,LLaMA2 模型有三种尺寸:7B、13B 和 70B。它们既有纯完成模型,也有针对对话使用场景优化的版本。预训练时使用了大约 2 万亿个标记,它们支持最长 4096 个标记的上下文长度。针对对话使用场景的微调使用了超过 10 万个示例,并通过超过 100 万个代表人类偏好的训练样本进一步优化。

在 AWS 中,LLaMA2 模型可以通过SageMaker JumpStart一键部署,也可以通过AWSxHuggingFace LLM DLC从 HuggingFace 模型库中获取。

微调

微调利用迁移学习高效地将特定领域的专业知识注入到像 LLaMA2 这样的基础模型中。该过程涉及通过在特定领域的数据上继续预训练来更新模型的权重,同时保持整体网络结构不变。与需要大量数据集和计算的全面预训练不同,微调在样本和计算方面非常高效。参数高效的微调(PEFT)技术,例如(q)LoRA 方法,使得在像 LLaMA2 这样的通用语言模型中轻量化地注入专业知识成为可能,且开销最小。

在谈到微调时,有两种不同的方法可以选择:

  1. 通过在领域特定语料库上微调 LLaMA2 的仅解码器架构,利用其原生的自回归因果语言建模(CLM)方法,我们可以将专业知识或特定语言或词汇(例如医疗研究术语/语言)注入到 LLM 中,同时保留其通用语言能力。

  2. 另一方面,通过使用特定任务的语料库,可以将模型微调为特定任务,例如指令跟随或对话行为。

这些模型利用自监督训练方法,优化特定于语言建模(LM)的损失函数。像 LLaMA2 这样的仅解码器模型采用了因果语言建模(CLM)方法,具有单向上下文。简单来说,这意味着它们是通过根据之前的词语作为语义上下文,预测后续标记的自回归方式进行训练的。

如上所述,PEFT 技术使得将专业知识轻量级地注入到 LLM 中成为可能,因为仅更新模型参数的子集。低秩适配方法(LoRA)或量化低秩适配方法(QLoRA)冻结预训练模型权重,并将可训练的秩分解矩阵注入 Transformer 架构的每一层,大大减少了下游任务的可训练参数数量。如果你想深入了解 LoRA,建议查看 Mariano Kamp 的 博客文章

场景

7 月 26 日,AWS 在纽约市 AWS 峰会上宣布了生成式 AI 领域的多项新功能。为了分享更多细节,发布了几篇公告博客:

· Amazon Bedrock 的代理

· AWS 实体解析匹配

· 向量存储在生成式 AI 应用中的作用

· Amazon OpenSearch Serverless 的向量引擎

· AWS Glue Studio 笔记本,由 Amazon CodeWhisperer 提供支持

· Amazon EC2 P5

考虑到 LLaMA2 的知识截止日期,不幸的是,该模型的纯形式将无法提供有关这些公告的信息。我们希望通过利用微调将这些知识注入到基础模型(FM)中,并提升其知识水平以实现这一点。

由于我们不打算将模型微调到特定任务上,而是希望摄取领域特定的知识,我们可以使用经典的 CLM 基础方法(前面章节中的选项 1)。Philipp Schmid 在他非常全面的博客中描述了如何使用 QLoRA 对 LLaMA2 模型进行微调,但重点是任务特定(指令)微调。我们将以他的博客中分享的代码样本为起点,并相应地调整代码。

为了负责任地使用资源,我们将对 LLaMA2–13b 和 LLaMA2–13b-chat 模型进行微调。微调 7b 及 70b 版本将使用调整后的训练集群配置(参见 Philipp 的博客)。在微调完成后,我们将部署基础模型和微调后的模型,并进行高层次的性能比较。

随后,我们将逐步讲解微调模型的每个步骤。如果您想访问完整的代码仓库,可以在这里找到。

数据加载

对于数据加载,我们使用 LangChain 的 WebBaseLoader 加载由其 URL 标识的网站数组。

许可协议与 HuggingFace 登录

接受 LLaMA2 模型的许可协议

在我们开始数据准备和训练之前,我们需要接受 LLaMA2 模型的许可协议。这包括在 Meta 网站上的注册,所用的电子邮件地址需与您的 HuggingFace 账户邮箱匹配。

然后我们从运行时对 HuggingFace hub 进行身份验证。

数据准备

首先,我们对原始网站进行一些预处理。在实际应用中,可能会更重视这一步骤。为了演示的目的,我们仅仅去除了较大的空格拼接,以获得一个连贯流畅的文本语料库。然后我们将文档列表加载到 HuggingFace 数据集中。

在下一步中,我们对文本语料库进行分词处理,使其适合 LLM。为此,我们使用从 HuggingFace hub 加载的 LLaMA2 分词器。加载后的分词器可以用于对根据模型的上下文窗口(2048 个 tokens)批量处理后的数据进行分词。

然后我们将数据集保存到 S3 中,以便在训练作业中使用。

培训

现在我们可以触发一个 Amazon SageMaker 训练作业,执行基于预处理数据的 CLM 绑定 QLoRA 微调脚本。

超参数和训练脚本参考了 Philipp 的博客文章。唯一的例外是执行的训练轮次数量,我们选择了相对较高的 20 轮。这是为了考虑到我们的训练数据集相对较小(约 32k 个标记)。在实际应用中,建议使用更大的数据集进行微调。训练脚本本身可以在 这里 找到。

请注意:由于接受的模型许可证协议与您的 HuggingFace 帐户绑定,我们需要指定 HuggingFace 访问令牌。

训练作业配置,例如训练集群配置,也参考了 Philipp 的博客文章。

然后我们可以执行训练作业,这将进行微调并将模型工件保存到 S3 中。

部署

基础模型 LLaMA2–13b 和 LLaMA2–13b-chat 可以通过 SageMaker JumpStart 方便地部署。这个 博客文章 提供了详细的说明。

经过微调的两个模型可以通过使用 AWSxHuggingFace LLM DLC 进行部署。因此,我们将 HuggingFaceModel 类的 model_data 参数指向模型工件的 S3 路径。然后,我们可以将托管容器中设置的环境变量 HF_MODEL_ID 指向 DLC 内的默认模型路径(“/opt/ml/model”)。

测试

部署所有四个模型后,我们希望在示例问题上测试性能。关于 Amazon EC2 P5 实例的公告 博客文章 说明:“P5 实例提供 8 个 NVIDIA H100 Tensor Core GPU,配有 640 GB 的高带宽 GPU 内存,第三代 AMD EPYC 处理器,2 TB 系统内存,以及 30 TB 本地 NVMe 存储。P5 实例还提供 3200 Gbps 的汇总网络带宽,支持 GPUDirect RDMA,通过绕过 CPU 实现更低延迟和高效的扩展性能。”

我们提出以下问题:“什么是 Amazon EC2 P5 实例?它们配备了什么类型的 GPU?”聊天模型在推理配置 max_new_tokens = 200, top_p = 0.9, temperature = 0.01 下的回答如下:

  • LLaMA2–13b-chat: “Amazon Elastic Compute Cloud (EC2) P5 实例是一种提供高性能计算资源的实例,适用于需要强大计算能力的应用程序。这些实例配备了 NVIDIA Tesla V100 GPU,专为机器学习、科学模拟和其他高性能计算工作负载设计。\n\nAmazon EC2 P5 实例的 NVIDIA Tesla V100 GPU 基于 Volta 架构,并提供多种功能,使其非常适合需求严格的工作负载,包括:\n\n 5120 个 CUDA 核心,用于高性能计算\n* 640 个张量核心,用于加速深度学习和其他基于矩阵的工作负载\n* 16 GB 的 GDDR6 内存,用于快速数据传输和处理\n* 支持 NVIDIA 的 GPU 加速库和框架,如 CU…*”

  • LLaMA2–13b-chat-nyc-finetuned: “Amazon EC2 P5 实例是一种通用实例,为需要大量图形处理和复杂机器学习(ML)模型的应用程序提供极端性能。它们由 NVIDIA H100 Tensor Core GPU 和第三代 AMD EPYC 处理器驱动。H100 GPU 提供 640 GB 高带宽 GPU 内存,使您能够在实时运行大规模数据集的最具挑战性应用程序。P5 实例还提供 2000 GB 高速 CPU 内存,允许您在内存中处理大量数据,而无需访问磁盘存储。这减少了处理时间并改善了响应时间。您可以将这些实例用于计算机视觉、视频编码、基因组分析和语言模型训练等应用程序.

我们可以清楚地看到,尽管基础模型幻想了 GPU 类型(V100),但经过精调的模型为我们提供了正确的答案(H100)。我们还看到,通过 qLoRA 我们可以保留基础模型的聊天精调特性,从而减轻灾难性遗忘。这意味着我们可以向 LLM 注入知识,而不必在之后进行完全的指令/聊天精调,只需使用相应的任务精调模型作为基础模型即可。这种方法有效的原因在于 LoRA 的性质,其中神经网络的每一层的大部分部分保持不变,而层级则被扩展。

对于模型的纯完成版本,我们需要重新提出问题,因为这些模型无法理解指令或以对话方式行事。相反,它们只是通过自回归的下一个 token 预测来完成标记序列。

我们提出以下问题:“Amazon EC2 P5 实例配备了类型的 GPU” 完成模型通过推断配置为 max_new_tokens = 200,top_p = 0.9,temperature = 0.01 回答如下:

  • LLaMA2–13b:“NVIDIA Tesla V100。\nNVIDIA Tesla V100 是一种旨在进行高性能计算的 GPU。它基于 NVIDIA Volta 架构,并配备了 32GB 的 HBM2 内存。V100 能够提供高达 100 teraflops 的性能,并设计用于数据中心和超级计算机。\nNVIDIA Tesla V100 是一种强大的 GPU,旨在进行高性能计算。它基于 NVIDIA Volta 架构,并配备了 32GB 的 HBM2 内存。V100 能够提供高达 100 teraflops 的性能,并设计用于数据中心和超级计算机。\nNVIDIA Tesla V1…

  • LLaMA2–13b-nyc-finetuned:“NVIDIA A100 用于计算密集型工作负载,例如生成 AI、高性能计算(HPC)和数据分析。P5 实例非常适合训练和运行具有万亿参数的模型以及在庞大数据集上进行数据分析。与 P4 实例相比,它们提供了 8 倍的 GPU 内存和 64 倍的浮点运算(FLOPs)计算性能。\n 您可以在 Amazon Elastic Container Service(ECS)中使用 P5 实例,支持最常见的容器工具和框架,如 Docker、PyTorch、TensorFlow、Jupyter Notebook、R Studio 和 Venv,或通过 Amazon Elastic MapReduce(EMR)或 Amazon DeepSense 使用 Amazon 或其他公司提供的 ML 和 AI 框架和工具来使用 P5 实例。为了便于在 P5 实例上构建生成 AI 应用程序,…

尽管用少量数据微调聊天微调模型已导致消除了基于 LLaMA2 模型的幻觉,但在我们的设置中似乎不起作用。这可能是因为我们微调数据集的大小有限。此外,更复杂的提示工程和优化推理配置参数设置可能会有所帮助。

结论

总之,这篇博客文章深入探讨了将领域特定知识融入大型语言模型(LLMs)如 LLaMA2 的关键过程,强调了在设计 LLM 驱动的企业级应用时,解决有用性、诚实性和无害性等挑战的重要性。这里的主要关注点是参数化微调方法,它可以有效地将专业知识注入基础模型而不影响其通用语言能力。

博客文章重点介绍了使用参数高效微调技术(如 qLoRA 方法)微调 LLaMA2 的步骤,以及如何在 Amazon SageMaker 上进行这一过程。通过采用这种方法,实践者可以将 LLaMA2 适应于特定领域,确保模型即使在超出原始训练数据的情况下也能保持最新的知识。

文章还强调了这种方法的多功能性,显示它可以应用于像 LLaMA2-chat 这样的模型,这些模型已经经过了特定任务的微调。这为将知识注入到 LLM 中提供了机会,而无需进行广泛的指令或基于对话的微调,从而保持了其特定任务的特性。

10 分钟理解 Lineage 和 Hamilton

原文:towardsdatascience.com/lineage-hamilton-in-10-minutes-c2b8a944e2e6?source=collection_archive---------8-----------------------#2023-05-26

通过使用 Hamilton 的开箱即用的 Lineage 功能,减少调试管道的时间。

Stefan Krawczyk数据科学前沿 Stefan Krawczyk

·

关注 发表在 数据科学前沿 · 11 分钟阅读 · 2023 年 5 月 26 日

--

Hamilton + Lineage:使您能够可视化和理解事物之间的联系。这是通过 driver.visualize_path_between()创建的。图像由作者提供。

Hamilton 是一个通用的开源微框架,用于描述数据流。它非常适用于数据和机器学习(ML)工作。在这篇文章中,我们将引导你了解 Hamilton 的血缘关系功能,这些功能可以帮助你快速回答在数据和机器学习工作中常见的问题,从而提高工作效率,更有效地与同事合作。如果你不熟悉 Hamilton,我们邀请你浏览以下内容:

血缘关系

你可能会问,我所说的血缘关系是什么意思?在机器学习和数据工作中,“血缘关系”指的是数据在转化和处理成表格、统计模型等形式时的历史记录或可追溯性。血缘关系帮助确定“数据”的来源、质量和可靠性,因为它有助于理解数据如何被转化。

你为什么应该关注血缘关系?

如果你是你必须管理的内容的作者,你可能对你写的所有内容如何连接有一个大致的了解。,对于继承你工作的其他人,或引入一个合作者,或者调试你六个月前写的东西,迅速了解情况可能会是一个挑战!我听说过有团队花费超过四分之一的时间来理解同事留下的工作(他们显然没有使用 Hamilton)!在这种情况下,血缘关系可以提供很大的帮助。为了提供更多背景,以下是一些导致生产力损失、普遍不满甚至中断的常见情况:

(a) 调试你的训练集/模型中的数据问题(结果发现是上游数据问题)。

(b) 尝试确定他人的管道如何工作(因为你继承了它,或必须与他们合作)。

(c) 需要满足某些数据如何到达某处的审计要求(例如 GDPR、数据使用政策等)。

(d) 想要对一个功能/列进行更改,但没有好的方法来评估潜在影响(例如,业务发生了变化,收集的数据也发生了变化)。

从实际角度来看,大多数从事数据和机器学习的人不会遇到或理解“血统”的价值,因为往往做到这一点很麻烦。通常需要一个外部系统(如 open lineageAmundsenDatahub 等),然后用户需要额外工作以填充信息。好消息是,如果你使用 Hamilton,不需要另一个系统来获取血统;使用血统也很直接!

血统即代码

使用 Hamilton,你不需要添加任何其他内容,你就可以获得血统。

编写代码。获取类似这样的血统。这是使用 Hamilton Driver 的 visualize_execution() 函数创建的。图片由作者提供。

通过以 Hamilton 方式编写代码,你在函数中定义了计算,然后通过函数输入参数指定事物如何连接,编码血统。将此代码与例如版本控制系统(如 git)连接起来,也为你提供了在时间点快照血统的手段!因为你必须更新代码以更改计算操作方式,因此,按定义,你也更新了血统,而无需做任何额外的操作 😎。

Hamilton 范式概述。你编写的函数定义了计算应如何进行,而不是编写过程性代码。以这种方式定义函数完全映射到血统!

高层次的“血统即代码”及其后续访问的步骤是:

  1. 编写 Hamilton 代码并进行版本控制。

  2. 实例化一个 Hamilton Driver,它将会有一个表示数据和计算流动的方式,正如你的 Hamilton 代码所定义的。Driver 对象可以发出/提供关于血统的信息!

  3. 使用你的版本控制系统,你可以回溯到过去,以理解血统如何随着时间的推移而变化,因为它被编码在代码中!

添加元数据以使血统更加有用!

当你还可以将元数据附加到血统上时,血统会变得更加有用。Hamilton 默认使你能够表达事物如何连接,但通过附加额外的元数据,我们现在可以通过 Hamilton 编码的血统连接业务和公司概念。

例如,你可以为提取数据的函数添加关切点,如PII、所有权、重要性等,并为创建重要文物的函数(如模型、数据集)添加类似信息。现在这些函数已被注解,能够回答的问题集就大大增加了!相比之下,其他系统要求你将这些元数据放在其他地方;例如一个 YAML 文件、代码库的不同部分,或在其他地方单独整理这些信息。在 Hamilton 的哲学中,我们认为直接注解实际代码最为合理,因为这样可以确保代码和元数据的真实来源始终保持最新。使用 Hamilton 更容易维护,因为只需在一个地方进行更改。

在 Hamilton 中,添加额外元数据的方法是通过使用 [@tag](https://hamilton.dagworks.io/en/latest/reference/decorators/tag/#hamilton.function_modifiers.tag)[@tag_outputs](https://hamilton.dagworks.io/en/latest/reference/decorators/tag/#hamilton.function_modifiers.tag) 装饰器。它们允许指定任意的字符串键值对。这提供了灵活性,使你和你的组织能够定义适合你们上下文的标签和值。

例如,以下代码为:

titanic_data 函数指定了其 sourceownerimportance,包含 PII,并链接到一些内部维基。

agesex 列添加了大量元数据,指定它们是 PII。

注意:我展示了一个稍微复杂的示例,使用了多个装饰器,只是为了说明情况不会比这更复杂——代码仍然相当易读!

@tag_outputs(age={"PII": "true"}, sex={"PII": "true"})
@extract_columns(*columns_to_extract)
@tag(
   source="prod.titantic",
   owner="data-engineering",
   importance="production",
   info="https://internal.wikipage.net/",
   contains_PII="True", 
   target_="titanic_data",
)
def titanic_data(index_col: str, location: str) -> pd.DataFrame:
    """Returns a dataframe, that then has its columns extracted."""
    # ... contents of function not important ... code skipped for brevity

以下代码从业务角度提供有关正在创建的模型的更多上下文:

@tag(owner="data-science", importance="production", artifact="model")
def fit_random_forest(
    prefit_random_forest: base.ClassifierMixin,
    X_train: pd.DataFrame,
    y_train: pd.Series,
) -> base.ClassifierMixin:
  """Returns a fit RF model."""
  # ... contents of function not important ... code skipped for brevity

现在这些额外的元数据附加到函数和血缘上,我们可以将其作为上下文来提出更有用的问题。更多内容见下文。

如何使用 Hamilton 回答血缘问题

回答血缘问题的基本机制依赖于实例化一个 Hamilton Driver,该 Driver 在底层创建一个有向无环图 (DAG) 来表示其世界观,然后使用 Driver 拥有的函数。让我们列出相关的 Driver 函数及其功能。

可视化血缘(即显示 DAG):

  • display_*() 有三个 display_* 函数。一个帮助你显示定义的所有连接关系。然后一个函数用于仅可视化上游内容,最后一个函数可视化给定函数/节点的下游内容。

  • visualize_execution() 帮助可视化生成某些输出所需的一切。用于展示 .execute() 的作用。

  • visualize_path_between() 帮助可视化两个节点之间的路径。

获取谱系需求的元数据访问权限:

使用 Hamilton 和上述函数,你可以对谱系进行编程访问以及可视化,这意味着你可以在 CI 系统中、在运行生产代码的笔记本中,或在任何 Python 运行的地方提出这些问题!

回顾一下到目前为止讨论的内容,回答谱系问题的一般方法如下:

  1. 编写 Hamilton 代码。

  2. 使用 @tag 来注释函数。

  3. 实例化一个 Driver 来创建你的 DAG。

  4. 使用 Driver 函数来提问/回答问题。

为了简短起见,我们不会深入探讨它们的使用,我们将简单展示一些你可以用 Hamilton 直接回答的一般问题,并结合上面提到的函数举例。我们将使用的示例问题是一个 Hamilton 中的端到端模型管道,用于预测泰坦尼克号的生存率;代码和更多信息可以在 Hamilton 仓库的谱系示例 中找到。术语说明:我们将 functionnode 互换使用,因为在 Hamilton 中,function 在我们的可视化中会变成 node

(1) 生成这个数据/模型的操作序列是什么?

这是一个常见的问题,如何从A->B,其中A可以是某些数据,**->** 对我们来说是不透明的,B 可以是某个工件(可以是更多数据或模型/对象)。

借助 Hamilton,通过以 Hamilton 规定的风格编写代码,您可以清晰而轻松地定义操作的顺序以及它们之间的关系!所以如果您不能通过查看代码本身来回答这个问题,可以请 Hamilton 提供帮助。在我们的示例中,为了了解feature_encoders是如何创建的,即使对它们知之甚少,我们也可以请求 Hamilton Driver 为我们可视化它们的创建过程:

...
# create the driver
dr = driver.Driver(config, data_loading, features, sets, model_pipeline, adapter=adapter)
# visualize how it's created
dr.visualize_execution(
    [features.encoders], "encoder_lineage", {"format": "png"}, inputs=inputs
)

输出结果:

示例血缘可视化。图像由作者提供。

然后,您可以将其与浏览代码库相结合,以更轻松地导航和理解发生了什么。

(2) 谁/哪些数据源导致了这个工件/模型?

这对于调试数据问题以及理解一个工件/模型依赖于哪些团队和数据源非常有用。以我们的泰坦尼克号示例为例,假设我们的随机森林模型出现了一些异常,我们想要重新检查当前生产模型的数据源是什么以及它们的所有者,以便我们可以联系他们。为此,我们可以编写以下代码:

# create the driver
dr = driver.Driver(config, data_loading, features, sets, model_pipeline, adapter=adapter)
# Gives us all the operations that are upstream of creating the output.
upstream_nodes = dr.what_is_upstream_of("fit_random_forest")

上述两行创建了一个 Driver,然后提取fit_random_forest的所有上游节点。然后我们可以遍历这些节点并提取我们想要的信息:

teams = []
# iterate through 
for node in upstream_nodes:
  # filter to nodes that we're interested in getting information about
  if node.tags.get("source"):
    # append for output
    teams.append({
      "team": node.tags.get("owner"),
      "function": node.name,
      "source": node.tags.get("source"),
    })
print(teams)
# [{'team': 'data-engineering', 
#   'function': 'titanic_data', 
#   'source': 'prod.titanic'}] 

(3) 谁/什么在这个变换的下游?

回答这个问题实际上是(2)的补充。当有人想要更改特征或数据源时,您通常会遇到这个问题。以我们的泰坦尼克号示例为例,假设我们在数据工程部门,想要更改源数据。我们如何确定使用这些数据的工件是什么以及谁拥有它们?

我们使用what_is_downstream_of() Driver 函数来获取下游的节点:

# create the driver
dr = driver.Driver(config, data_loading, features, sets, model_pipeline, adapter=adapter)
# Gives us all the operations that are upstream of creating the output.
downstream_nodes = dr.what_is_downstream_of("titanic_data")

然后类似于(2),我们只需遍历并提取我们想要的信息:

artifacts = []
for node in downstream_nodes:
  # if it's an artifact function
  if node.tags.get("artifact"):
      # pull out the information we want
      artifacts.append({
          "team": node.tags.get("owner", "FIX_ME"),
          "function": node.name,
          "artifact": node.tags.get("artifact"),
      })
print(artifacts)
# [{'team': 'data-science', 'function': 'training_set_v1', 'artifact': 'training_set'}, 
#  {'team': 'data-science', 'function': 'fit_random_forest', 'artifact': 'model'}, 
#  {'team': 'data-science', 'function': 'encoders', 'artifact': 'encoders'}]

(4) 什么被定义为 PII 数据,它最终会出现在哪些内容中?

随着目前的法规,这成为了一个越来越常见的问题。基于上述内容,我们可以结合几个 Driver 函数来回答这类问题。

在我们的泰坦尼克号示例中,假设我们的合规团队来找我们了解我们如何使用 PII 数据,即它最终会出现在哪些工件中?他们希望每个月都能收到这份报告。好吧,借助 Hamilton,我们可以编写脚本以程序化地获取与 PII 数据相关的血缘信息。首先,我们需要获取所有标记为 PII 的内容:

# create the driver
dr = driver.Driver(config, data_loading, features, sets, model_pipeline, adapter=adapter)
# using a list comprehension to get all things marked PII
pii_nodes = [n for n in dr.list_available_variables() 
             if n.tags.get("PII") == "true"]

然后,要获取所有下游的工件,我们只需请求所有下游的节点,然后筛选出带有“artifact”标签的节点:

pii_to_artifacts = {}
# loop through each PII node
for node in pii_nodes:
  pii_to_artifacts[node.name] = []
  # ask what is downstream
  downstream_nodes = dr.what_is_downstream_of(node.name)
  for dwn_node in downstream_nodes:
    # Filter to nodes of interest
    if dwn_node.tags.get("artifact"):
      # pull out information
      pii_to_artifacts[node.name].append({
          "team": dwn_node.tags.get("owner"),
          "function": dwn_node.name,
          "artifact": dwn_node.tags.get("artifact"),
      })
print(pii_to_artifacts)
# {'age': [{'artifact': 'training_set',
#         'function': 'training_set_v1',
#         'team': 'data-science'},
#        {'artifact': 'model',
#         'function': 'fit_random_forest',
#         'team': 'data-science'}],
# 'sex': [{'artifact': 'training_set',
#         'function': 'training_set_v1',
#         'team': 'data-science'},
#        {'artifact': 'encoders', 'function': 'encoders', 'team': None},
#        {'artifact': 'model',
#         'function': 'fit_random_forest',
#         'team': 'data-science'}]} 

我可以在我的笔记本/IDE 中获得这个吗?

你们中的一些人可能在想,为什么在开发时不使用这种 lineage 视图?好主意!由于我们是一个开源项目,我们很乐意在这方面获得一些帮助;如果你有兴趣进行测试/贡献,我们有一个由 Thierry Jean 发起的 alphaVSCode 扩展,可以帮助你在输入时可视化 lineage。我们欢迎贡献。

Alpha Hamilton VSCode 扩展由 Thierry Jean 发起。图片作者提供。

最后

使用 Hamilton,你的代码定义 lineage。这意味着你可以开箱即用地获得 lineage,而无需另一个系统,并且当结合版本控制系统和额外的 metadata 时,是一种非常简单轻便的方式来理解数据与代码的连接。

Hamilton 允许对你编码的 lineage 和 metadata 进行程序化访问,这使你可以将其放置在 CI 作业、脚本或任何 Python 运行的地方。

希望你喜欢这个快速概述,如果你对其中任何内容感到兴奋或想要更多资源,这里有一些链接:

你可能感兴趣的其他 Hamilton 文章:

线性代数 1:线性方程和系统

原文:towardsdatascience.com/linear-algebra-1-1-15b70e48bab9?source=collection_archive---------2-----------------------#2023-09-16

图片来自 Städel Museum

线性方程组

tenzin migmar (t9nz)Towards Data Science tenzin migmar (t9nz)

·

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

--

前言

这是关于线性代数基础的系列文章的首次更新,线性代数是机器学习的基础数学。这篇文章将与 David C. Lay、Steven R. Lay 和 Judi J. McDonald 合著的《线性代数及其应用》一起阅读效果最佳。请将本系列视作一个外部补充资源。

通过这些文章,我希望巩固对这些基础概念的理解,同时如果可能的话,以我希望的直观学习方法向他人提供额外的清晰度。如果有任何错误或需要进一步阐述的地方,请分享,我会做必要的修改。

背景

线性方程和线性方程组在金融、工程、化学、计算机科学、统计学、物理学等领域具有多种实际应用。在化学中,线性方程用于平衡化学反应并计算反应物和产品的数量。这一线性代数的基石也出现在物理学中,其中线性方程用于运动学和热力学中描述物体的运动,帮助计算距离、速度和加速度,以及模拟物理系统中的热传递和能量流动。金融领域依赖于线性方程和方程组进行预算和投资组合分析,而工程师则可能使用相同的工具进行结构分析,以建模建筑物中的力和应力。线性代数无处不在,每个人都可以在某种程度上欣赏它。

线性方程

线性方程 是一个包含一个或多个变量的方程,对于每个变量,该变量的指数必须为一。它可以写成以下形式:a₁x₁ + a₂x₂ + … + 2ᵣxᵣ = b。这些值 [a₁a₂,…,aᵣ] 和 b 被称为 线性方程系数

线性方程的例子包括:2x + 5y = 10,6x = 18,7v + 8w + 0x + 2y + 3z = 15,以及 3x₁ + 4x₂ + 5x₃+9x₄ + 10x₇ = 3

一个线性方程的反例是 2 + 6x + 5 = 2;这是一个二次方程的例子。另一个这样的反例可以是 7x₁* + 3x₂ = x₁* y₁;当你绘制这个方程的图形时,原因就会变得明显,它可以被重新排列成有理函数 y = 7x / x - 3,这样的图形是曲线而非直线。

考虑线性方程 2x + 5y = 10。下图展示了线性方程的图形表示,你会注意到它是一条直线。当回忆直线方程 y = mx + b 时,这一点会更加明显,其中 m = 斜率,b = y 截距。该线性方程可以如下所示重新排列成这种形式。

可以得出以下结论:所有落在直线上的 (x, y) 点都是方程 2x + 5y = 10 的解。例如,假设我们选择 x 轴截距点 (5, 0),并将 xy 值代入方程中的相应位置。2(5) + 5(0) = 10。任何在直线上的 (x, y) 点都可以代入方程中,等式将成立。我们可以将这一发现概括为一个规则:

对于具有两个变量的线性方程 ax + by = c,ℝ²* 中的解集可以表示为一条直线。

请注意,这个单一的方程有无限多个解,这些解在 ℝ² 中展开;稍后我们将详细查看解的数量。

这个相同的基本概念可以转移到更高维度的坐标空间,如ℝⁿ,例如ℝ³,其中直线变成平面,因为增加了第三个变量。

线性方程组

线性方程组是一组一个或多个线性方程,这些方程共享类似的变量。一个例子:

6x + 2y = 4

2x + 4y = 8

线性方程组定义为使每个方程在其各自变量中替换后都成立的值(s₁, s₂, …, sᵣ)。在上述系统中,解为(0, 2),因为将(0, 2)代入系统后,两个方程都成立。

线性系统的解

解决线性系统的图形意义是什么?线性系统的解的不同情况有哪些?本节将更详细地研究这三种可能性。它们如下:

  1. 唯一解

  2. 无解

  3. 无限解

唯一解:在像上述这样的两个变量的线性系统中,解是交点。为什么?解是必须满足两个方程的有序对,如果不存在这样的有序对,那就意味着这些直线不会相交。这是唯一解的一个例子。只有一个解满足线性系统中的所有方程。

无解:考虑无解的情况。在两个变量的线性系统中,这意味着什么?在什么情况下直线集合永远不会相交?一种情况是它们是平行的。在所有直线平行的线性系统中,该系统将没有解。另一种情况是虽然某些直线可能与其他直线相交,但没有一个共同的交点是所有直线共享的。

无限解:线性系统的最终情况是存在无限解。什么时候两个变量的线性系统可能有无限解?如果这些直线是相同的,那么因为它们重合,就有无限个交点,从而存在无限解。考虑以下线性系统:

6x + 3y = 18

2x + y = 6

尽管系数可能不同,但这些直线实际上是相同的!如果你将第一个方程的每个系数除以 3,得到的方程将是 2x + y = 6。

线性系统的解的可视化会随着变量数量的增加而变化。下图展示了三变量线性系统所有三种解的可能图示。超过三维的情况对人脑的可视化会变得困难,但规则仍然适用!无论变量有多少,所有线性系统都只有无解、唯一解或无限解。

此图表改编自 src

矩阵记号

随着线性方程变得更加复杂,记号可能变得笨重。将线性系统的信息压缩成易于操作和处理的形式是很重要的,因此通常使用矩阵记号来代替一组方程。系数矩阵是一种排除了每个方程中的b系数的矩阵。增广矩阵包括b系数,因此比系数矩阵多一列。

矩阵的大小,也称为阶数,告诉我们一个矩阵有多少行和列。一个 m x n 矩阵是一个有 m 行和 n 列的矩阵。行数对应于系统中有多少个线性方程,而列数则告诉我们有多少个变量。请确保行数在列数之前,因为阶数不可互换。

解决线性系统

有一种系统的方法来确定线性系统是否有解,如果有解,则是否有唯一解或无限解,并从中获得解。解决线性系统可以使用原始形式的线性方程或矩阵,尽管推荐使用矩阵,因为记号更简洁。熟悉这两种方法是有益的,因为它们提供了对彼此机制的额外见解。

以下是逐步处理方程组的过程,没有使用矩阵。基本思想是通过乘以现有方程创建新的方程,从而获得相同的方程,然后可以将其加或减去另一个方程以消除一个变量。然后重复这一过程,直到我们消除足够的未知数以便能够解出一个变量,然后再向上追溯解决其余的变量。最后,需要检查以确保解实际上满足方程组。

行操作

之前概述的步骤可以转移到以矩阵为中心的线性系统求解程序中。请注意在每次变换后被消除的变量在矩阵中的标记。不过在我们深入之前,让我们定义一些行操作。实际上有两个操作与我们之前应用的操作平行。

  1. 替换:“用一行与另一行的和替换一行。”*

  2. 交换:“交换两行。”*

  3. 缩放:“将一行中的所有条目乘以一个非零常数。”*

让我们重新处理相同的线性系统,这次使用矩阵并应用行操作。

注意,我使用了与线性方程法相同的操作和比例因子。毫不奇怪,我们最终得到了之前的相同方程式。另一个需要注意的是最终矩阵左下角的三角形形成。这种模式出现是有道理的,因为 0 是已消除变量的标记,每个消除的变量都使我们更接近识别可以轻松解决的方程式;这反过来又推动了整个系统的解决进展。我们将在下一章重新讨论这种现象,并提供更正式的定义。

总结

在本章中,我们学到了:

  • 线性方程:一个或多个变量的方程,其中方程的次数必须等于 1。

  • 线性方程组:一组线性方程。

  • 一个或多个线性方程的解:线性系统既可以没有解,也可以有唯一解或无穷多解。

  • 矩阵表示法:用作线性系统紧凑表示的矩形数组。

  • 行操作:替换、交换和缩放操作允许我们将一个矩阵转换为一个已消除足够未知变量以解决系统的矩阵。

  • 解线性系统:找出 a)给定线性系统是否存在解以及 b)如果存在解,它们的确切值是什么的系统化方法。

注释

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

*作为一个小插曲:二次方程的词源自拉丁文 quadratus,是 quadrare 的过去分词,意为“使方形”;这也体现了它的次数![src]

*ℝ²是所有可能有序对(x, y)在实数线上的空间,它由一个二维平面表示。ℝ²包含了整个实数集合,而实数集合是不可数无穷的,这意味着ℝ²空间也是无穷的。

*行操作引用[src]

线性代数 2:梯形矩阵形式

原文:towardsdatascience.com/linear-algebra-2-echelon-matrix-forms-ce8a19c71a7d?source=collection_archive---------3-----------------------#2023-09-29

来自欧洲数字档案馆Unsplash上的图像

行梯形式和简化行梯形式

tenzin migmar (t9nz)向数据科学进发 tenzin migmar (t9nz)

·

关注 发表在向数据科学进发 ·8 分钟阅读·2023 年 9 月 29 日

--

前言

欢迎回到我关于线性代数基础的系列文章的第二篇,这也是机器学习背后的基础数学。在我之前的 文章 中,我介绍了线性方程和系统、矩阵符号以及行简化操作。本文将讨论阶梯矩阵形式:行阶梯形式和行简化阶梯形式,以及如何利用这两种形式来解线性系统。若配合 David C. Lay, Steven R. Lay 和 Judi J. McDonald 的《线性代数及其应用》一书阅读,将会更好地服务于读者。请将此系列视为一个外部辅助资源。

随时分享你的想法、问题和批评。

行阶梯形式

高斯消元方法是一种通过行操作将矩阵转化为可以通过回代得到解的形式的过程。

作为复习,行简化操作包括:

  1. 替换:“将一行替换为其自身与另一行的和。”*

  2. 交换:“交换两行。”*

  3. 缩放:“将一行中的所有条目乘以一个非零常数。”*

上述操作可以应用于矩阵,以将该矩阵转换为其行阶梯形式。给定一个 m x n 的矩阵,其中 m 是行数,n 是列数,当且仅当:

  1. 所有条目均为零的行位于至少有一个条目非零的行下方。

  2. 一行的所有主元(从左侧开始的第一个非零条目)位于上面行的右侧列中。

  3. 一列中所有主元下方的条目均为零。

以下是行阶梯形式(REF)的矩阵示例。

请花点时间欣赏一下,虽然矩阵的大小和条目有所不同,但所有矩阵都被认为符合上述标准的行阶梯形式。注意一下高亮的主元下方的楼梯状图案?这就是执行高斯消元将矩阵转换为行阶梯形式时自然出现的形式。这个形式的名字非常恰当:单词 echelon 来源于法语中的 eschelon,意为梯子的一步,后来也有了“台阶”的意思。*

高斯消元法的基本思想是将矩阵转换为行简化形式,即选择一个主元("主元"一词用来指代主元素:在其行中的第一个非零元素),然后消除所有主元下方的元素,将主元下列的所有元素变为零。为了理解这一步骤为何能推动矩阵变为简化行阶梯形式,请回顾简化行阶梯形式的定义:在行阶梯形式中,所有主元素下方的列元素均为零。这一步骤然后会在每一行中重复,但要小心!我们必须确保在每次选择主元时,都不会违反行阶梯形式的核心特征;每一行的主元素都位于其上方行的右侧列中。考虑到这一规则,通常建议从第一行的第一个元素开始选择主元,然后从右向左逐步处理剩余行。

让我们再次思考之前提到的行阶梯形式的目的:将表示线性系统的矩阵转化为一种形式,以便解决方案变得易于辨认。为了更好地理解行阶梯形式的实用性,请参见示例 1。

当我们执行高斯消元法时,我们是在操作矩阵,使其呈现对称而更易解读的形式。通过示例 1 获得的行阶梯形式,我们现在可以使用回代法逐步获得每个解。

正如你从上面看到的,这并不理想。它需要额外的繁琐工作。虽然简化行阶梯形式也需要额外的工作,但其表示方式更为简洁,减少了出错的可能性。一旦我们将矩阵简化为简化行阶梯形式,我们可以轻松读出解,并且线性系统也就解决了。

简化行阶梯形式

当将矩阵简化为简化行阶梯形式时,使用的是高斯-约旦消元法。该算法通过一系列行变换操作,将表示线性系统的矩阵转换为简化行阶梯形式,从而使解变得易于辨认。无需额外的回代步骤。

给定的m x n 矩阵被称为简化行阶梯形式,如果它满足行阶梯形式的所有先决条件,并且还符合以下标准:

  1. 每一行的主元素都是 1。

  2. 主元素下方和上方的所有列元素均为零。(主元素是该列中唯一的非零元素)

让我们通过一个示例来演示如何将矩阵简化为简化行阶梯形式。

阅读我们得到的简化行阶梯形式矩阵,现在立刻可以看出我们的解是 x₁ = -3, x₂ = -12, x₃ = -3。

经济形式的独特性

直到现在,我们已经分别计算了行最简形式和简化行最简形式的一个例子。你可能想尝试独立地对行最简形式进行行化简练习,结果得到一个不同的行最简形式矩阵。不必担心,这完全可能,并且只要计算正确且所有三个规则都涵盖了,两者版本都是正确的。这是一个很棒的情况!它引导我们迈向一个重要的定理:

定理 (1)

矩阵可以有多于一个的行最简形式;行最简形式不是唯一的。通过行操作应用的顺序变化,可以得到不同但同样有效的最简形式。

这并不适用于简化行最简形式,对于简化行最简形式则正好相反。

定理 (2)

矩阵只能有一个简化行最简形式;简化行最简形式是唯一的。

我们看到这两种形式在唯一性上的差异的根源在于我们对简化行最简形式施加的额外限制,即主元条目的要求为一。当我们将矩阵化简为行最简形式时,我们可以将每一行乘以任何非零常数,矩阵仍将保持在行最简形式,因为缩放矩阵并没有违反行最简形式的任何规则。简化的最简形式则不同,因为主元条目必须为一。下面我将通过一个具体的例子进一步说明这一点。

解的数量

从解决线性系统中自然产生的一个基本问题是存在多少解?对于任何线性系统,解将始终是三种情况之一。线性系统将要么有一个唯一解,要么有无限解,或者无解。如果你有兴趣思考为什么必须是这三种情况之一,(重新)阅读我的上一篇文章

让我们更详细地查看每种情况,以了解我们如何识别给定矩阵的解情况,并轻轻探讨和探索每种情况如何及为何会出现的直觉。

唯一解: 当线性系统的矩阵的简化行最简形式的每一列都有一个主元时,该系统具有唯一解。

当我们将矩阵形式重新写成一系列线性方程时,这一点变得更加明显。我们可以看到,因为每一列都有一个主元(上方和下方没有条目),所以可以从方程中逐一读出每个变量的明确解。

无解: 当线性系统的矩阵的简化行最简形式具有代数不一致性时,该系统无解。

如上所述,没有任何* x₁ x₂ x₃* 和* x₄* 的值可以使得方程四成立。左边将始终为 0,而不是 9,因此该系统没有解。一般而言,任何增广矩阵中如果有一行[0, 0, … 0 | b] 其中 b 不为零,则没有解,因为 0 ≠ b。

无限解: 当线性系统至少有一个自由变量时,它有无限解。自由变量出现时,相应的列没有主元。另一方面,基本变量是指相应列有主元的变量。让我们深入了解为什么自由变量的存在暗示了无限解。

名副其实,自由变量意味着你可以随意赋值给它们。所有基本变量相对于自由变量被定义,因此基本变量的值将取决于自由变量被赋予的值。这是无限解存在的本质;只要基本变量与自由变量选择的值一致,无限多的解都是有效的。

在将矩阵转换为化简行阶梯形矩阵后,系统是否有一个解、没有解或无限多解将立即显现出来。

总结

在本章中,我们学到了:

  • 高斯消元法用于将矩阵化简为行阶梯形矩阵以求解线性系统。

  • 高斯-乔丹消元法用于将矩阵化简为化简行阶梯形矩阵以求解线性系统。

  • 阶梯形矩阵的唯一性:行阶梯形矩阵不是唯一的,而化简行阶梯形矩阵是唯一的。

  • 线性系统可能的解的数量:唯一解、无限解或无解,以及它们何时和为什么会出现。

备注

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

行操作的引用 [src]

“阶梯形”这个词的词源 [src]

线性代数 3:向量方程

原文:towardsdatascience.com/linear-algebra-3-vector-equations-ec1e01ccaa05?source=collection_archive---------1-----------------------#2023-10-14

向量方程和跨度

tenzin migmar (t9nz)Towards Data Science tenzin migmar (t9nz)

·

关注 发表在 数据科学前沿 ·9 分钟阅读·2023 年 10 月 14 日

--

前言

欢迎回到我关于线性代数基础的系列文章的第三篇,这些基础数学是机器学习的根基。在我之前的 文章 中,我介绍了阶梯矩阵的形式。这篇文章将探讨向量、跨度和线性组合,并将这些新概念与我们已经学习的内容联系起来。如果与 David C. Lay、Steven R. Lay 和 Judi J. McDonald 编写的《线性代数及其应用》一书一起阅读,这篇文章将更能服务于读者。请将本系列视为一个辅助资源。

随时分享你的想法、问题和意见。

ℝ²、ℝ³ 和 ℝⁿ 中的向量

到目前为止,我们已经了解了矩阵,即数字的数组,那么如果我们只有一个数字数组(单数形式)呢?瞧,向量:一种特殊类型的矩阵,大小为m x 1,其中 m 表示向量中的行数或条目数。请回忆,矩阵的大小表示为m x n,其中m等于行数,而n对应列数。向量总是只有一列,但可以有任意数量的行。

所有具有两个条目的向量集合是ℝ²。ℝ包含了所有实数集合,因此ℝ²是所有可能的实数点(x, y)的二维空间,这也是合理的。

向量可以位于ℝ²、ℝ³、ℝ⁴ … ℝⁿ中,注意向量空间的维度对应于向量中的条目数量。

你可能最终会遇到特殊的零向量(简单地写作 0),这是一个所有条目都是零的向量。虽然这可能看起来是一个小细节,但我们稍后会发现它对线性代数中一些最重要的概念有着重要的影响。

几何可视化

到目前为止,矩阵和向量已经以数学方式描述、解释和表示,而物理中的向量被描述为具有大小和方向的量。两者都是正确的;下面的ℝ²中向量的图示结合了这两种定义。

重要的是要记住,ℝ²中的向量是有序对,而更高维向量空间中的向量是有序元组(具有定义顺序的数字列表)。两个向量可能具有相同的条目数字,但如果条目的顺序不同,那么这些向量也不相同,如上图所示。

ℝ³中的向量也可以被可视化,我们只需添加第三个轴,因为我们有一个额外的条目。在ℝ³之外,图示化向量变得更加复杂,因为很难想象更高维空间。

向量的代数性质

对于任何给定向量空间中的所有向量uvw以及标量cd:以下代数性质¹适用:

(i) 交换律*: u + v = v + u

(ii) 结合律*: (u + v) + w = w + (v + w)

(iii) 加法恒等元:u + 0 = 0 + u = u

(iv) 加法逆元:u + (-u) = -u + u = 0

(v) 与向量的分配律:c(u + v) = cu + cv

(vi) 与标量的分配律:(c + d)u = cu + du

(vii) 与标量的结合律:c(du) = (cd)u

这些性质与向量加法和标量乘法的操作有关。

要添加两个向量,相应的条目被相加以产生向量和。这意味着不同大小的两个向量的加法是未定义的。为了加法,两个向量必须具有相同数量的条目!这个条件源自向量加法的执行方式。

在标量乘法中,对于给定的标量c和向量u,标量倍数是cu,其中u中的每个条目都被标量c所乘。

这两种操作可以一起使用;正如你在下一节中会发现的,它们结合形成了线性代数的核心概念:线性组合。

线性组合

假设我们有向量 v₁, v₂, … vₐ在ℝⁿ中,并且我们有标量(也称为权重)c₁, c₂, … cₐ,它们可以是任何实数,包括零。线性组合是由标量倍数组成的向量,c₁v₁ + c₂v₂ + … + cₐvₐ。

之前,我们探讨了线性代数中的存在性概念,给定一个矩阵,是否存在至少一个解?换句话说,矩阵的简化/行最简形式是否存在不一致?如果存在,则没有解。如果不存在,则至少有一个解。这个基本的存在性问题与线性代数中的许多思想相关,线性组合也不例外。

我们说向量b是 Rⁿ中一组向量 v₁, v₂, .. vₐₚ的线性组合,如果存在一组权重 c₁, c₂, … cₐ(一个解),使得 c₁v₁ + c₂v₂ + … + cₐvₐ = b

要确定b是否是线性组合,我们可以使用向量加法和标量乘法的操作,将我们的线性组合方程:c₁v₁ + c₂v₂ + … + cₐvₐₚ = b重新排列成我们非常熟悉的符号。这种重新排列的过程也阐明了为什么确定向量b是否是某组向量的线性组合是一个存在性问题。

上述解释旨在强调为什么存在性问题和矩阵行简化与线性组合有关,并以一般性方式展示这一概念。让我们来看一个更具体的例子。

在上面的例子中,将增广矩阵行简化为简化行最简形式后,我们发现确实存在解!

但让我们考虑一个行简化形式的增广矩阵,行[0, 0, … | b]中b ≠ 0,这意味着向量b不能作为一组向量的线性组合。换句话说,向量b超出了我们向量组的范围,或者(这是进入下一节的一个良好过渡)向量b不在这组向量的跨度内。

向量集合的跨度

所有可能的线性组合集合,对于 ℝⁿ 中的向量 v₁, v₂, … vₐ,称为由 v₁, v₂, … vₐ 张成的 ℝⁿ 的子集。向量 v₁, v₂, … vₐ 的跨度记作 Span{v₁, v₂, … vₐ},这是可以表示为 c₁v₁ + c₂v₂ + … + cₐvₐ 的向量集合。另一种思考方式是,跨度包含所有可以表示为向量 v₁, v₂, … vₐ 的线性组合的向量。

我们可以找到任何数量向量的跨度。假设我们有一个单一的向量 v₁。则 Span{v₁} 将是 v₁ 的所有标量倍,因为在这种情况下唯一可以应用的操作是标量乘法(至少需要两个向量才能进行向量加法)。Span{v₁} 包含所有可以通过 v₁ 达到的向量。

如果我们要可视化跨度,它将是经过 v₁ 和原点的直线,因为只有一个向量时,线性组合(向量倍数)不能改变方向。这个观点在下面的图示中进一步说明。

考虑 ℝ² 中两个不同方向的向量的跨度,这两个向量可以形成什么可能的线性组合?换句话说,ℝ² 中哪些向量可以表示为这两个向量的线性组合?

对于上述情况,经过进一步调查,似乎uv 张成了整个 ℝ²!这意味着 ℝ² 中的任何向量都可以表示为 uv 的线性组合。在未来的文章中,我们将探讨线性独立性的概念,这将用于具体证明 uv 张成了 ℝ²。

结论

向量、线性组合和跨度让我们进一步深入到线性代数的丰富领域。这些基本概念帮助我们理解向量空间的结构以及不同向量集合之间的关系。随着我们进一步的学习,你会发现这些思想不断出现,因为它们与其他核心概念相关联。类似地,我希望你能花些时间思考我们迄今为止所学的内容(解的存在、行最简形式)如何与这些新概念深刻相关。

总结

在这一章中,我们学习了:

  • ℝ²、ℝ³ 和 ℝⁿ 中的向量:向量是一种特殊的矩阵,大小为 m x 1。一个向量可以有任意数量的条目,但只有一列。我们发现也可以有零向量,即所有条目均为零的向量。

  • 向量的几何可视化:向量可以通过图形表示,这有助于理解大小和方向的概念来源。

  • 向量的代数性质:以下代数性质对所有向量和标量均适用;交换律、结合律、加法单位元、加法逆元、与向量的分配律、与标量的分配律,以及与标量的结合律。

  • 线性组合:线性组合是由标量倍数 c₁v₁ + c₂v₂ + … + cₐvₐ 的和定义的向量。权重 c₁, c₂, … cₐ 可以是包括零在内的任何标量。

  • 向量跨度:向量 v₁, v₂, … vₐ 的跨度记作 Span{v₁, v₂, … vₐ},是可以表示为 c₁v₁ + c₂v₂ + … + cₐvₐ 的向量集合。

备注

¹向量的代数性质参考自 cs.brown.edu/stc/summer/94GeoTrans/94GeoTrans_17.html

²线性组合的定义参考自 David C. Lay、Steven R. Lay 和 Judi J. McDonald 编著的《线性代数及其应用》第六版

³有关跨度的定义参考自 David C. Lay、Steven R. Lay 和 Judi J. McDonald 编著的《线性代数及其应用》第六版。

*所有图片由作者创建,除非另有说明。

*结合律意味着对于加法和乘法操作,数字可以以任何方式分组,结果保持不变。例如,(5 + 2)+ 3 = 5 + (2 + 3)= 10 和 (5 x 2)x 3 = 5 x (2 x 3)= 30。

*交换律意味着对于加法和乘法操作,数字可以按照任意顺序相加或相乘,结果保持不变。例如,5 + 2 = 2 + 5 = 7 和 5 x 2 = 2 x 5 = 10。

线性代数 4: 矩阵方程

原文:towardsdatascience.com/linear-algebra-4-matrix-equations-914ebb371950?source=collection_archive---------5-----------------------#2023-11-10

求解矩阵方程 Ax= b

tenzin migmar (t9nz)Towards Data Science tenzin migmar (t9nz)

·

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

--

前言

欢迎回到我持续进行的线性代数基础系列的第四篇。在我之前的文章中,我介绍了向量、线性组合和向量跨度。这篇文章将探讨矩阵方程 Ax = b,并将展示如何通过矩阵方程解决线性方程组的原理。

这篇文章若与 David C. Lay、Steven R. Lay 和 Judi J. McDonald 的《线性代数及其应用》一书一同阅读,将更具价值。请将这个系列作为辅导资源来参考。

随时分享您的想法、问题和批评。

直观理解

我们上次讲述了线性组合的概念,我承诺这会有重要的意义。请回忆一下,给定向量 v₁, v₂, … vₐ在ℝⁿ中,以及标量(也称为权重)c₁, c₂, … cₐ,线性组合是由标量倍数的和定义的向量,c₁v₁ + c₂v₂ + … + cₐvₐ。¹

如果向量b是向量空间 Rⁿ中一组向量 v₁, v₂, .. vₐₚ的线性组合,那么存在一组权重 c₁, c₂, … cₐ(一种解决方案),使得 c₁v₁ + c₂v₂ + … + cₐvₐ = b

要确定b是否是给定向量 v₁, v₂, .. vₐ的线性组合,我们将我们的向量排列成一组线性方程,然后创建一个增广矩阵,并使用行简化运算将矩阵化简为简化阶梯形式。如果简化阶梯形式有不一致性,也就是一行看起来像这样:[0, 0, … | m]其中m ≠ 0,那意味着我们的向量b不是向量组的线性组合,因为没有一组权重能使等式 c₁v₁ + c₂v₂ + … + cₐvₐ = b成立。

如果没有这样的不一致,那意味着我们可以将向量b写成一组向量的线性组合,比如上面的例子。你还记得我们如何在最后验证答案吗?我们会将每个向量乘以它的对应标量,然后找到向量和。如果向量和等于b,我们知道我们计算正确,并且b确实是一个线性组合。

这个验证过程就是矩阵方程Ax = b的变形!

Ax = b

如果A是一个m x n矩阵,而x属于 Rⁿ(你将会在下一节看到为什么x属于 Rⁿ很重要),那么乘积Ax 就是矩阵A中向量(列)的线性组合,使用x中对应的标量。

注意这一切都不是新材料,我们在前一篇文章中不知不觉地计算了Ax 来验证我们的线性组合。然而,矩阵方程Ax = b仍然很重要,因为它将所有这些内容形式化为紧凑的符号,并且将在后续以新的方式重新出现。

现在我们知道,如果给定一个m x n矩阵A和向量x,并且我们计算矩阵乘积Ax 等于b,那么b可以写成矩阵A中向量(列)和向量x中的标量/条目的线性组合。所以总结一下:方程Ax = b只有一个解(x),如果 b 可以写成 A 的列向量的线性组合。

矩阵乘法

我已经将Ax = b介绍为一个矩阵乘积,但我还没有解释矩阵乘法(这就是Ax 的内容)!

矩阵乘法 是将两个矩阵相乘得到它们的乘积的操作。我们已经见过矩阵加法,即两个矩阵相加得到它们的和。为了使矩阵加法有定义,被加的两个矩阵,矩阵 A 和矩阵 B 必须具有相同的大小。类似地,矩阵乘法也有一个要求。要将矩阵 A 和矩阵 B 相乘得到 AB,矩阵 A 的列数必须等于矩阵 B 的行数。矩阵 AB 的乘积,我们称为矩阵 C,其大小取决于矩阵 A 的行数和矩阵 B 的列数。矩阵 C 将有 m(矩阵 A 的行数)行和 p(矩阵 B 的列数)列。

那么,矩阵乘法是如何工作的呢?如果我们要计算矩阵 A 和 B 的乘积,那么乘积矩阵中第 i 行、第 j 列的每个元素是矩阵 A 第 i 行和矩阵 B 第 j 列的点积

目前,你只需知道点积是两个向量对应元素的乘积之和,并且仅当两个向量具有相同数量的元素时才有定义。这个解释远不能充分说明点积的几何直觉,但我会在稍后详细说明。

简洁起见,我已计算了两个 2 x 2 矩阵的乘积,但同样的过程适用于任何大小的矩阵,只要矩阵符合矩阵乘法的条件,否则它们的乘积将未定义。

矩阵乘法的性质

如果 ABCn x n 矩阵,cd 是标量,那么以下性质成立。³

  1. AB BA(一般情况下不可交换)

  2. (AB)C = A(BC)(结合律)

  3. A(B+C) = AB + AC 和 (B+C)A = BA + CA(分配律)

  4. 0A = 0(零的乘法性质)

注意矩阵乘法不是可交换的,这个性质可能需要一段时间才能理解,因为我们通常对实数的可交换性有直观的认知。

这些性质对于计算矩阵乘积非常有用,这将是线性代数中的一个重要主题。

结论

矩阵乘法是支持神经网络核心功能的基础数学操作,特别是在它们的前向传播和反向传播阶段。

在神经网络的前向传播阶段,数据通过其各个层进行处理,而矩阵乘法是这一操作的核心。神经网络中的每一层由神经元组成,它们表示为输入的加权和,接着是激活函数。这些加权和通过矩阵乘法计算得出。

在反向传播过程中,神经网络从其错误中学习。它调整神经元的权重以最小化预测输出与实际输出之间的误差。矩阵乘法再次成为此过程的关键组成部分,特别是在计算梯度时,梯度指示了每个权重应该如何调整以最小化误差。

学习数学本身就是一次令人兴奋的冒险,但是学习线性代数理论及其应用,可以使这段充满挑战的学习旅程更加鼓舞人心。

总结

在这一章中,我们学习了以下内容:

  • 线性组合和矩阵乘积 Ax = b 背后的直觉:矩阵乘积并非一个全新的概念,而是形式化了我们已经在使用的一个过程!

  • Ax = b:如果b是矩阵A中向量(列)集合的线性组合,则矩阵乘积有解 x。

  • 矩阵乘法:Ax = b 的操作,在机器学习应用中广泛使用,特别是在神经网络中的具体例子。

  • 矩阵乘法的属性:非交换性、结合律、分配律和零的乘法性质。

笔记

*除非另有说明,所有图片均由作者创建。

*抱歉让你久等了,我正在参加期中考试(包括线性代数哈哈!)。

¹线性组合的定义参考自《线性代数及其应用》第六版,作者为 David C. Lay、Steven R. Lay 和 Judi J. McDonald。

²矩阵乘积属性的定义参考自《线性代数及其应用》第六版,作者为 David C. Lay、Steven R. Lay 和 Judi J. McDonald。

³矩阵属性参考自src

线性代数:欧几里得向量空间

原文:towardsdatascience.com/linear-algebra-euclidean-vector-space-9f88f69cf240

第五部分:欧几里得向量空间的温和介绍

Chao De-YuTowards Data Science Chao De-Yu

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

--

图片来源:Karsten WürthUnsplash

介绍

在机器学习和深度学习中,我们大多数时候都在处理向量。而向量空间模型可以将数据之间的关系表示为向量。此外,从几何角度来看,它还能够比较两个向量的相似性,无论是使用两个向量之间的距离(欧几里得距离)还是两个向量之间的角度(余弦相似度)。

向量

让我们从二维空间中的向量几何开始。

Image 1. 二维空间中向量的示例。 (图片来源:作者)

  • 如果 u = (u₁, u₂) 和 v = (v₁, v₂) 满足 u₁ = v₁ 且 u₂ = v₂,则这两个向量相等。

  • 向量 u 和 v 的和定义为 u + v = (u₁ + v₁, u₂ + v₂)

Image 2. 向量和的示例。 (图片来源:作者)

  • 标量 k 与向量 u 的乘积定义为 ku = (ku₁, ku₂)

Image 3. 向量与标量相乘的示例。 图片来源:作者。

  • 向量的负向量 -v 被定义为与 v 具有相同大小但方向相反的向量。

  • 向量的差定义为 u-v = u + (-v)

Image 4. 向量差的示例。 (图片来源:作者)

范数和距离

  • 向量的长度通常称为范数。

Image 5. 向量的长度,范数。 (图片来源:作者)

  • 两点之间的距离定义如下:

Image 6. 两点之间的距离。 (图片来源:作者)

欧几里得 n 空间

  • 如果 n 是正整数,存在一系列 n 个实数 v₁, v₂, …, vₙ,则我们写作:v = (v₁, v₂, …, vₙ)

  • 所有具有 n 个分量的向量的集合称为欧几里得 n 空间,记作 Rⁿ。

Rⁿ 中向量的属性

  1. u + v = v + u

  2. u + (v + w) = (u + v) + w

  3. k(u + v) = ku + kv

  4. (k + m)u = ku + mu

  5. u + 0 = 0 + u = u

  6. u + (-u) = 0

  7. 1u = u

    其中 u, v, w 是向量,k, m 是常数

欧几里得内积

  • 如果 u 和 v 是 Rⁿ 中的向量,则欧几里得内积定义为

    u . v = u₁v₁ + u₂v₂ + … + uₙvₙ

由此得到:

图片 7. 柯西-施瓦茨不等式的证明。(作者提供的图片)

图片 8. 证明 u.v=1/4||u+v||² -1/4||u-v||²

  • 属性:

    u.v = v.u

    (u + v).w = u.w + v.w

    (ku).v = k(u.v)

    v.v ≥ 0,v.v = 0 当且仅当 v = 0

    其中 u, v, w 是向量,k 是常数

Rⁿ 中的范数和距离

  • Rⁿ 中的范数定义为:

图片 9. Rⁿ 中的范数。(作者提供的图片)

  • Rⁿ 中两点之间的欧几里得距离定义为:

图片 10. Rⁿ 中两点之间的欧几里得距离。(作者提供的图片)

  • 属性:

    ||u|| ≥ 0,||u|| = 0 当且仅当 u = 0

    ||ku|| = |k| ||u||

    ||u + v|| ≤ ||u|| + ||v||

    d(u, v) ≥ 0,d(u, v) = 0 当且仅当 u = v

    d(u, v) = d(v, u)

    d(u, v) ≤ d(u, w) + d(w, v)

    其中 u, v, w 是向量,k 是常数

Rⁿ 中的角度

如果 u 和 v 是欧几里得 n 空间中的向量,则 u 和 v 之间的角度 (θ) 定义为:

cosθ 的推导可以参考 图片 7

图片 11. Rⁿ 中两向量之间的角度。(作者提供的图片)

推荐阅读

## 线性代数:线性方程组和矩阵,使用 Python

第一部分:讲解线性代数的基础:线性方程组和矩阵

[towardsdatascience.com ## 线性代数:矩阵运算及其性质,使用 Python

第二部分:讲解线性代数的基础:矩阵运算及其性质

[towardsdatascience.com ## 线性代数:使用 Python 计算逆矩阵

第三部分:全面逐步指导如何使用基本行变换找到逆矩阵并…

towardsdatascience.com ## 线性代数:LU 分解与 Python

第四部分:全面逐步指导如何使用 LU 分解解决线性系统

towardsdatascience.com ## 线性代数:正交向量

第六部分:正交向量的温和介绍

towardsdatascience.com [## 线性代数:一般向量空间

第七部分:全面介绍一般向量空间;子空间、基、秩和零度概念

medium.com](https://medium.com/analytics-vidhya/linear-algebra-general-vector-space-0dd3d74e9070?source=post_page-----9f88f69cf240--------------------------------) [## 线性代数:发现特征值和特征向量以实现对角化

第八部分:系统化深入讲解特征值和特征向量的识别,以便于对角化…

medium.com](https://medium.com/analytics-vidhya/linear-algebra-discovering-eigenvalues-and-eigenvectors-for-diagonalization-2c3090f9be44?source=post_page-----9f88f69cf240--------------------------------)

参考文献

[1] 向量空间 — 维基百科

[2] 欧几里得空间 — 维基百科

[3] 国防医学院讲座 — 朱伟达,欧几里得向量空间,2008

线性代数:使用 Python 寻找逆矩阵

原文:towardsdatascience.com/linear-algebra-finding-inverse-matrix-with-python-18dd988f4df

第三部分:使用基本行操作和矩阵的行列式来寻找逆矩阵的全面逐步指南

Chao De-YuTowards Data Science Chao De-Yu

·发表于 Towards Data Science ·5 分钟阅读·2023 年 1 月 19 日

--

照片来源:Raimond KlavinsUnsplash

上一篇文章解释了不同的矩阵操作及其相应的操作。本文将深入探讨使用基本行操作和矩阵的行列式来获得逆矩阵的两种不同方法。

逆矩阵

逆矩阵类似于数的倒数,因为一个数与其倒数的乘积等于1,而一个矩阵与其逆矩阵的乘积则得到一个单位矩阵。然而,要找到矩阵的逆矩阵,矩阵必须是一个方阵,即行数和列数相同。找到矩阵的逆矩阵主要有两种方法:

方法 1:使用基本行操作

回顾用于解决线性系统的3 种行操作:交换、重新缩放和主元操作。这些操作可以写成初等矩阵。左乘增广线性系统矩阵表示初等行操作

线性系统的示例增广矩阵。(作者提供的图片)

交换矩阵:交换单位矩阵的第 i 行和第 j 行。

交换矩阵 A 的第 1 行和第 2 行。(作者提供的图片)

重新缩放矩阵:如果操作是 k 倍方程 i,则在单位矩阵的位置 (row=i, col=i) 放置数字 k。

行 1 乘以常数 3。(图片来源:作者)

主元矩阵:如果将方程 i 的倍数加到方程 j 中,则在单位矩阵的 (行=j, 列=i) 位置上填入数字 k。

5 倍行 2 加到行 1。(图片来源:作者)

我们现在可以使用初等矩阵找到逆矩阵

  • 如果 A 是可逆的,那么 Eₖ…E₂E₁A = I

  • 两边都乘以 A 的逆矩阵得到:

  • 一系列初等行操作可以将 A 化简为 I,相同的初等行操作将 I 转换为矩阵 A 的逆矩阵。

  • 如果A 是一个可逆矩阵,那么对于每个列向量 b,方程组Ax = b 有且仅有一个解

使用行操作求逆矩阵 A 的示例:

使用行操作求 A 的逆。(图片来源:作者)

使用初等矩阵求 A 的逆。(图片来源:作者)

方法 2:使用矩阵的行列式

2 x 2 矩阵的行列式和逆矩阵

矩阵的行列式,det(A) 或 |A|,是对方阵有用的值。对于下面的 2x2 矩阵 A,det(A) = ad — bc,如果 det(A) ≠ 0,则 A 可逆。对于 2 x 2 矩阵,逆矩阵为:

然而,要计算大于 2 x 2 的矩阵的行列式,我们需要获取它的余子式和伴随矩阵。

余子式和伴随矩阵

对于方阵 A,aᵢⱼ 元素的余子式 Mᵢⱼ 被定义为通过删除矩阵 A 的第 i 行和第 j 列所形成的子矩阵的行列式,aᵢⱼ 元素的伴随矩阵 Cᵢⱼ 如下:

从小行列式得到的伴随矩阵方程。(图片来源:作者)

说明如何从方阵 A 中获得余子式和伴随矩阵。(图片来源:作者)

使用伴随矩阵展开得到矩阵的行列式

方阵的行列式可以通过将任何行或列的条目与对应的伴随矩阵相乘并加总结果来计算:

最终,逆矩阵可以通过将伴随矩阵的转置1/行列式相乘来计算。

使用行列式和伴随矩阵求 A 的逆。(图片来源:作者)

总结

逆矩阵是解决线性代数中各种问题的有用工具。本文展示的应用之一是解决线性方程组。在这篇文章中,你将学习如何使用两种不同的方法逐步得到逆矩阵,一种是使用初等行变换,另一种是使用矩阵的行列式。

推荐阅读

线性代数:线性方程组和矩阵,使用 Python

第一部分:解释线性代数的基础:线性方程组和矩阵

线性代数:矩阵运算及其性质,使用 Python

第二部分:解释线性代数的基础:矩阵运算及其性质

线性代数:LU 分解法,使用 Python

第四部分:LU 分解法解决线性系统的全面逐步指南

线性代数:欧几里得向量空间

第五部分:欧几里得向量空间的温和介绍

线性代数:正交向量

第六部分:正交向量的温和介绍

## 线性代数:一般向量空间

第七部分:一般向量空间的全面介绍;子空间、基、秩和零度的概念

medium.com](https://medium.com/analytics-vidhya/linear-algebra-general-vector-space-0dd3d74e9070?source=post_page-----18dd988f4df--------------------------------) [## 线性代数:发现特征值和特征向量以实现对角化

第八部分:深入系统地介绍识别特征值和特征向量以便于对角化…

medium.com](https://medium.com/analytics-vidhya/linear-algebra-discovering-eigenvalues-and-eigenvectors-for-diagonalization-2c3090f9be44?source=post_page-----18dd988f4df--------------------------------)

参考文献

[1] 基本矩阵 — 维基百科

[2] 余子式 / 代数余子式(线性代数) — 维基百科

[3] 行列式 — 维基百科

[4] 可逆矩阵 — 维基百科

线性代数:LU 分解与 Python

原文:towardsdatascience.com/linear-algebra-lu-decomposition-with-python-5a7b3fd87f96

第四部分:LU 分解解决线性系统的全面逐步指南

Chao De-YuTowards Data Science Chao De-Yu

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

--

图片来自 Andry RobyUnsplash

线性代数系列的第一篇文章介绍了如何使用高斯消元法解决线性系统,上一篇文章也解释了如何找到逆矩阵以及如何使用逆矩阵解决线性系统。本文将介绍另一种使用 LU 分解解决线性系统的方法。

LU 分解

下三角-上三角 (LU) 分解是一种将矩阵分解为下三角矩阵和上三角矩阵乘积的方法。它可以被视为高斯消元法的矩阵形式,并且是实现高斯消元法的更好方法,特别是对于具有相同左侧的重复方程的问题,即解决方程 Ax = b,其中 A 相同而 b 的值不同。[1]

A = LU(图由作者提供)

矩阵 A 可以通过初等矩阵分解为下三角矩阵和上三角矩阵的乘积。你可以参考 上一篇文章 关于三种不同的初等矩阵:交换矩阵、缩放矩阵和主元矩阵。

矩阵 A 的分解为下三角矩阵和上三角矩阵的乘积。(图由作者提供)

现在让我们来深入了解如何使用 LU 分解解决线性系统。

使用 LU 分解矩阵逐步解决线性系统。(图片由作者提供)

使用 LU 分解法解决线性系统的示例:

给定相同的左侧和不同的右侧,即相同的 A 和不同的 b 值。(图片由作者提供)

步骤 1, 2, 3: 将矩阵 A 分解为 L 和 U 矩阵

步骤 4: 给定 Lc₁ = b₁ 和 Lc₂ = b₂,求解 c₁ 和 c₂!

步骤 5: 给定 Ux₁ = c₁ 和 Ux₂ = c₂,求解 x₁ 和 x₂!

参考 上一篇文章 了解如何获取逆矩阵。

通过获得 L 和 U 矩阵,我们可以轻松解决上述具有重复左侧的线性系统。

摘要

LU 分解用于解决线性系统和寻找逆矩阵。它被认为是解决具有重复左侧的线性系统的更好方法。在这篇文章中,你将学习如何结合一些代码使用 LU 分解法解决线性系统。

推荐阅读

## 线性代数:线性方程组和矩阵(使用 Python)

第一部分:解释线性代数的基础知识:线性方程组和矩阵

towardsdatascience.com ## 线性代数:矩阵运算及其性质(使用 Python)

第二部分:解释线性代数的基础知识:矩阵运算及其性质

towardsdatascience.com ## 线性代数:寻找逆矩阵(使用 Python)

第三部分:使用初等行变换和 LU 分解法找到逆矩阵的全面分步指南

towardsdatascience.com ## 线性代数:欧几里得向量空间

第五部分:欧几里得向量空间的温和介绍

towardsdatascience.com ## 线性代数:正交向量

第六部分:正交向量的温和介绍

[towardsdatascience.com [## 线性代数:一般向量空间

第七部分:一般向量空间的全面介绍;子空间、基、秩和零度概念

medium.com](https://medium.com/analytics-vidhya/linear-algebra-general-vector-space-0dd3d74e9070?source=post_page-----5a7b3fd87f96--------------------------------) [## 线性代数:发现特征值和特征向量以进行对角化

第八部分:对识别特征值和特征向量以促进对角化的深入系统性讲解…

medium.com](https://medium.com/analytics-vidhya/linear-algebra-discovering-eigenvalues-and-eigenvectors-for-diagonalization-2c3090f9be44?source=post_page-----5a7b3fd87f96--------------------------------)

参考文献

[1] LU 分解— 维基百科

[2] “LU Decomposition • 剑桥大学 • 计算机科学与技术系” [在线]. 可用:www.cl.cam.ac.uk/teaching/1314/NumMethods/supporting/mcmaster-kiruba-ludecomp.pdf

线性代数:矩阵运算及其属性,使用 Python

原文:towardsdatascience.com/linear-algebra-matrix-operations-and-their-properties-with-python-a0885a159be1

第二部分:解释线性代数的基础:矩阵运算及其属性

Chao De-YuTowards Data Science Chao De-Yu

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

--

图片由 Pietro De Grandi 提供,来源于 Unsplash

上一篇文章 介绍了线性方程和线性系统的定义,以及线性系统如何重写成矩阵的形式。因此,了解矩阵的不同运算及其属性是非常重要的。

矩阵和向量

矩阵是一个具有 m 行和 n 列的矩形数字数组,其中 m > 1 且 n > 1。[1]

向量是只有一行(行向量)或一列(列向量)的矩阵。[2]

[左] 矩阵。[中] 列向量。[右] 行向量。(图像由作者提供)

一些特殊矩阵

  • 方阵:一个矩阵,其中 m = n

  • 对角矩阵 I:一个方阵,所有非对角线的元素均为零

  • 单位矩阵:对角矩阵 I,其中 iₓᵧ = 1

  • 带状矩阵:大多数条目为零,非零条目位于主对角线周围的带状区域

  • 下三角矩阵:L,若 x < y,则 lₓᵧ = 0,1 ≤ x ≤ m 和 1 ≤ y ≤ n

  • 上三角矩阵:U,若 x > y,则 uₓᵧ = 0,1 ≤ x ≤ m 和 1 ≤ y ≤ n

[左] 对角矩阵。[中] 单位矩阵。[右] 带状矩阵。(图像由作者提供)

[左] 下三角矩阵。[右] 上三角矩阵。(图像由作者提供)

矩阵的基本运算

加法和减法

矩阵的加法和减法通过简单地加或减相应的条目来完成。矩阵必须具有相同的维度才能进行加法或减法。

矩阵加法和减法的表达式。(作者提供的图片)

乘法

为了执行矩阵乘法,两矩阵的维度必须是可比的,即第一个矩阵的列数必须等于第二个矩阵的行数。

  • 标量-矩阵乘法:cA

    矩阵中的每个条目都乘以一个实数(标量)。

标量矩阵乘法的表达式。(作者提供的图片)

  • 内积:ab

    内积是行向量和列向量的乘法。

内积的表达式。(作者提供的图片)

  • 矩阵-向量乘法:Ab

    它可以按行执行,即 A 的每一行与向量 b 的内积,或按列执行,即 A 的每一列与向量 b 的每个标量条目的标量矩阵乘法。

按行的 Ab 表达式。(作者提供的图片)

按行的 Ab 示例。(作者提供的图片)

按列的 Ab 表达式。(作者提供的图片)

按列的 Ab 示例。(作者提供的图片)

  • 矩阵-矩阵乘法:AB

    类似于矩阵-向量乘法,矩阵-矩阵乘法可以按行执行,即 A 的每一行与 B 的每一列的内积,或按列执行,即矩阵 A 与 B 的每一列的矩阵-向量乘法。

按行的 AB 表达式。(作者提供的图片)

按列的 AB 表达式。(作者提供的图片)

basic_matrix_operations.py 的输出

基本矩阵操作的性质

给定:A,B,C = 矩阵,r,s = 实数 / 标量

  • A + 0 = A

  • A + B = B + A

  • A + (B + C) = (A + B) + C

  • A (BC) = (AB) C

  • A (B ± C) = AB ± AC

  • (B ± C) A = BA ± CA

  • r (A ± B) = rA + rB

  • (r ± s) C = rC ± sC

  • r (sC) = (rs) C

  • r (AB) = (rA) B= A (rB)

  • 单位矩阵:I,AI = A 和 IA = A

  • 矩阵乘法不是交换律的:通常 AB ≠ BA

  • 取消律并不总是成立:AB = AC <≠> B = C

  • AB = 0 <≠> A = 0 或 B = 0

矩阵的转置

转置矩阵可以通过互换矩阵的行和列来找到。矩阵的迹(主对角线上的条目之和)等于对应的转置矩阵的迹。[3]

转置矩阵操作的表达式。(作者提供的图片)

转置矩阵操作的示例。(作者提供的图片)

matrix_transpose.py 的输出

性质:

矩阵的逆

如果 A 是一个方阵,并且存在一个矩阵 B 使得 AB = BA = I,则 A 称为可逆矩阵,B 称为 A 的逆矩阵。如果没有这样的矩阵 B,则 A 称为奇异矩阵。换句话说,矩阵 A 称为奇异矩阵,如果它没有逆矩阵。 [4]

对于一个可逆矩阵,只有一个逆矩阵。

matrix_inverse.py 的输出

属性:

矩阵的幂如果它是一个方阵且可逆:

对称矩阵

如果一个矩阵等于它的转置矩阵,则称该矩阵为对称矩阵,即 A = Aᵗ。

如果 A 和 B 是对称的:

  1. A + B 和 A - B 是对称的

  2. kA 是对称的

  3. 对于任何矩阵 A,AAᵗ是一个方阵且对称矩阵

  4. 如果 A 是一个可逆对称矩阵,则 A 的逆矩阵是对称的

  5. 然而,积 AB 通常不是对称的。只有当 AB = BA 时,A 和 B 才是可交换的

总结

在这篇文章中,你将深入了解矩阵和向量,以及矩阵的不同运算及其性质。文章解释了三种基本的矩阵运算:矩阵的加法、减法和乘法。接着是转置矩阵、逆矩阵和对称矩阵。最后,它还包括一些关于如何使用 numpy 包进行矩阵运算的代码。

推荐阅读

## 线性代数:线性方程组和矩阵,与 Python

第一部分:解释线性代数的基础:线性方程组和矩阵

towardsdatascience.com ## 线性代数:求逆矩阵,与 Python

第三部分:使用初等行变换找逆矩阵的全面逐步指南和…

towardsdatascience.com ## 线性代数:LU 分解,与 Python

第四部分:使用 LU 分解解决线性系统的全面逐步指南

towardsdatascience.com [## 线性代数:欧几里得向量空间

第五部分:欧几里得向量空间的温和介绍

线性代数:欧几里得向量空间 [## 线性代数:正交向量

第六部分:正交向量的温和介绍

线性代数:正交向量 [## 线性代数:一般向量空间

第七部分:一般向量空间的综合介绍;子空间、基、秩和零度概念

线性代数:发现特征值和特征向量以便于对角化 [## 线性代数:发现特征值和特征向量以便于对角化

第八部分:系统性深入解析特征值和特征向量以便于对角化…

线性代数:发现特征值和特征向量以便于对角化

参考文献

[1] 矩阵(数学) — 维基百科

[2] 向量 — 维基百科

[3] 转置 — 维基百科

[4] 可逆矩阵 — 维基百科

[4] 矩阵运算的性质。(无日期)。检索于 2023 年 1 月 8 日,来自 math.mit.edu/~dyatlov/54summer10/matalg.pdf

线性代数:正交向量

原文:towardsdatascience.com/linear-algebra-orthogonal-vectors-aaf26de8146a

第六部分:正交向量的温和介绍

Chao De-YuTowards Data Science Chao De-Yu

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

--

图片由 Karsten Würth 提供,来源于 Unsplash

上一篇文章讨论了欧几里得向量空间、内积、范数、距离和角度。本文将介绍正交向量、正交投影和向量的线性变换。

正交向量

图片 1. R² 中的正交向量(作者提供的图片)

  • 两个向量 u 和 v 如果满足 u.v = 0,则称它们是正交的

  • 如果 u 和 v 是具有欧几里得内积的正交向量,则

正交投影

  • 将一个向量映射到其在直线或平面上的正交投影

图片 2. 正交投影。(作者提供的图片)

线性变换

  • 函数 f 将 Rⁿ 映射到 Rᵐ,记作 f: Rⁿ → Rᵐ

图片 3. 将 Rⁿ 映射到 Rᵐ 的函数。(作者提供的图片)

  • 如果所有的函数 fᵢ 都是线性的,那么变换 T 被称为线性变换,这些线性方程可以用矩阵形式 W = AX 表示。因此,上述变换可以写成:

图片 4. 重新书写图片 3 中的线性变换。(作者提供的图片)

示例变换:

  • 给定变换方程,w₁ = x₁ + x₂ + x₃,w₂ = x₁- x₂- x₃,该变换将 R³ 映射到 R²。

    T: R³ → R² (w₁, w₂) = T(x₁, x₂, x₃) = (x₁ + x₂ + x₃, x₁- x₂- x₃)

图片 5. 上述线性变换的标准矩阵。(作者提供的图片)

线性变换的合成

  • 如果 T₁: Rⁿ → Rᵏ 和 T₂: Rᵏ → Rᵐ 是线性变换,那么

图 6. T₂ 和 T₁ 的组合。(图片来源:作者)

向量上的一些常见变换

反射

  • 将一个向量映射到其关于一条直线或一个平面的对称图像

  • 一个向量关于 x 轴的反射

给定 u = (x, y)

图 7. 反射的示例。(图片来源:作者)

扩展或收缩

  • T(u) = ku 如果因子 k ≥ 1 则称为扩展,如果 0 ≤ k ≤ 1 则称为收缩。注意:k 是非负标量。

图 8. 扩展因子 k ≥ 1 的示例。(图片来源:作者)

旋转

  • 通过一个固定角度 θ 旋转一个向量

图 9. 顺时针和逆时针旋转的示例。(图片来源:作者)

反射和扩展的组合

  • 先对 x 轴进行反射,再进行因子为 2 的扩展。

图 10. 反射和扩展(因子为 2)的组合。(图片来源:作者)

推荐阅读

## 线性代数:线性方程组和矩阵,使用 Python ## 线性代数:线性方程组和矩阵,使用 Python [## 线性代数:线性方程组和矩阵,使用 Python

第一部分:解释线性代数的基本概念:线性方程组和矩阵

## 线性代数:矩阵运算及其性质,使用 Python ## 线性代数:矩阵运算及其性质,使用 Python [## 线性代数:矩阵运算及其性质,使用 Python

第二部分:解释线性代数的基本概念:矩阵运算及其性质

## 线性代数:使用 Python 求逆矩阵 ## 线性代数:使用 Python 求逆矩阵 [## 线性代数:使用 Python 求逆矩阵

第三部分:使用初等行变换寻找逆矩阵的全面步骤指南

## 线性代数:LU 分解,使用 Python ## 线性代数:LU 分解,使用 Python ## 线性代数:LU 分解,使用 Python

第四部分:使用 LU 分解求解线性系统的全面步骤指南

[towardsdatascience.com ## 线性代数:欧几里得向量空间

第五部分:欧几里得向量空间的温和介绍

[towardsdatascience.com [## 线性代数:一般向量空间

第七部分:对一般向量空间的全面介绍;子空间、基、秩和零度概念

medium.com [## 线性代数:发现特征值和特征向量以进行对角化

第八部分:关于特征值和特征向量识别的深入系统讲解,以便于对角化…

medium.com

参考文献

[1] 正交性 — 维基百科

[2] 国防医学院讲座 — 朱伟达,欧几里得向量空间,2008

[3] 国防医学院讲座 — 朱伟达,角度与正交性,2008

线性代数:线性方程组和矩阵,使用 Python

原文:towardsdatascience.com/linear-algebra-systems-of-linear-equations-and-matrices-with-python-d3e0fcb29e85

第一部分:解释线性代数的基本概念:线性方程组和矩阵

Chao De-YuTowards Data Science Chao De-Yu

·发表于 Towards Data Science ·5 分钟阅读·2023 年 1 月 10 日

--

图片由 Sergio Rota 提供,来源于 Unsplash

介绍

线性代数在科学和工程的多个领域中都至关重要。它是几乎所有数学领域的核心。例如,方程通常用于建模现实生活中的问题,多变量问题可以通过方程组来建模。即使是非线性方程组也可以转化为线性方程组。因此,了解如何解决这些线性方程组在现代科学的各个领域都非常有帮助。作为数据科学家,良好的线性代数理解对于使用大多数机器学习方法,尤其是深度学习算法,也是至关重要的。

线性方程

  • 线性方程是一个方程,其中变量的最高次幂始终为 1。

图像 1. 线性方程。图片来源:作者。

线性系统

  • 线性方程组 是一系列线性方程。

图像 2. 线性方程组。图片来源:作者。

上述线性方程组可以被重写为一种称为 增广矩阵 的矩阵形式。一个 m x n 矩阵是一个具有 m 行和 n 列的数字数组,其中 m > 1 且 n > 1。矩阵中的每个数字称为一个条目。

图像 3. 增广矩阵。图片来源:作者。

齐次线性系统

  • 如果常数项 bᵢ 全为零,则称线性系统为齐次的。

图像 4. 齐次线性方程组。图片来源:作者。

  • xᵢ = 0 称为齐次系统的平凡解。

  • 具有比方程更多未知数的齐次系统 (n > m) 有无限多解。

线性系统的可能解

对于线性系统的解,有 3 种可能的情况:

  1. 零解

  2. 单一解

  3. 无限多解

具有无解的线性系统称为不一致的。如果有至少一个解,则称为一致的

图 5. 线性系统的可能解示例。图片由作者提供。

使用基本行操作解线性方程系统的步骤

行简化

行简化是一种解线性方程组的算法。对矩阵进行行简化是通过使用一系列基本行操作来修改矩阵,直到矩阵的左下角被填充为零。 [1] 行操作有 3 种类型:

  1. 交换 — 一个方程与另一个方程互换,互换 2 行。

  2. 重新缩放 — 一个方程的两边都乘以一个非零常数,乘以一个非零常数的行。

  3. 主元化 — 一个方程被替换为其本身与另一个方程的倍数的和,将一个行的倍数加到另一行。

行阶梯形态

如果一个系统处于行阶梯形态,则该系统处于该状态

  • 这是一个阶梯模式

  • 每一行中的主元(第一个非零系数的项)位于上面一行的主元的右侧

  • 所有主元都是 1

图 6. 行阶梯形态示例。图片由作者提供。

产生阶梯形态矩阵的过程称为高斯消元法

简化阶梯形态

如果一个系统处于简化阶梯形态,则该系统处于该状态

  • 每个非零行中的主元都是 1。

  • 主元 1 所在列中的所有其他项都是零。

图 7. 简化阶梯形态示例。图片由作者提供。

产生简化 阶梯形态矩阵的过程称为高斯-乔丹消元法

在阶梯形态中,第一个具有非零系数的变量主元变量/领先变量非主元变量自由变量

示例

求解以下方程系统:

首先将线性方程系统转换为增广矩阵,然后进行一系列基本行操作,将其求解为行阶梯形态。

图 8. 使用基本行操作解线性系统(行阶梯形态)。图片由作者提供。

使用 sympy 包获取简化阶梯形态的代码

使用 sympy 包对示例线性方程系统进行简化阶梯形态处理。

使用 numPy 包获取线性系统解的代码

使用 numpy 包求解线性方程组的解决方案。

总结

在这篇文章中,你了解了线性代数的基础知识、线性方程组和矩阵。你学会了线性方程和线性系统的定义,以及线性系统的三种可能解。此外,你还学会了通过应用行变换序列来求解线性方程,将矩阵化简为行最简形式或简化的行最简形式。除此之外,你还了解了可以用来求解线性方程组的编程包。

推荐阅读

## 线性代数:矩阵运算及其性质,使用 Python

第二部分:解释线性代数的基本概念:矩阵运算及其性质

## 线性代数:使用 Python 计算逆矩阵

第三部分:使用基本行操作寻找逆矩阵的全面逐步指南

## 线性代数:LU 分解,使用 Python

第四部分:使用 LU 分解求解线性系统的全面逐步指南

## 线性代数:欧几里得向量空间

第五部分:欧几里得向量空间的温和介绍

## 线性代数:正交向量

第六部分:正交向量的温和介绍

## 线性代数:一般向量空间

第七部分:一般向量空间的综合介绍;子空间、基、秩和零度概念

medium.com [## 线性代数:发现特征值和特征向量以进行对角化

第八部分:深入的系统化讲解,帮助识别特征值和特征向量,以促进对角化…

medium.com

参考文献

[1] 高斯消去法 — 维基百科

[2] 第二章 矩阵与向量 | MATH0007:联合荣誉学生的代数。 (无日期)。检索于 2023 年 1 月 2 日,来自 www.ucl.ac.uk/~ucahmto/0007/_book/2-linalg1.html

线性判别分析(LDA)可以如此简单

原文:towardsdatascience.com/linear-discriminant-analysis-lda-can-be-so-easy-b3f46e32f982?source=collection_archive---------8-----------------------#2023-02-20

为你准备的互动可视化工具

Frederik HoltelTowards Data Science Frederik Holtel

·

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

--

图片由 Arus Nazaryan 使用 Midjourney 创建。提示词:“无人机拍摄的两群羊,明亮的蓝色和深红色,被一条中间分隔群体的围栏分开,干净、逼真的羊,绿草地上,照片级真实感

分类是机器学习中的核心话题。然而,理解不同算法的工作原理可能会很具挑战性。在这篇文章中,我们将通过一个互动图表使线性判别分析变得生动,你可以进行实验。准备好深入数据分类的世界吧!

交互式图 👇🏽 点击添加和移除数据点,使用拖动来移动它们。改变人口参数并生成新的数据样本。

如果你在应用或研究分类方法,可能会遇到各种方法,如朴素贝叶斯、K 最近邻(KNN)、二次判别分析(QDA)和线性判别分析(LDA)。然而,理解不同算法的工作原理并不总是直观的。LDA 是最初需要学习和考虑的方法之一,本文展示了该技术的工作原理。

让我们从头开始。所有分类方法都是为了回答这个问题:这个观测值属于哪种类型的类别?

在上面的图中,有两个独立变量,x_1 在水平轴上,x_2 在垂直轴上。把独立变量看作是两个学科的分数,例如物理和文学(范围从 0 到 20)。因变量 y 是类别,在图中表示为红色或蓝色。将其视为我们想要预测的二元变量,例如申请人是否被大学录取(“是”或“否”)。一组给定的观测值以圆圈的形式显示。每个圆圈由 x_1x_2 值以及类别特征。

现在,如果添加一个新的数据点,它应该被分配到哪个类别呢?

LDA 允许我们绘制一个边界,将空间分成两部分(或者多个部分,但在这种情况下是两个类别)。例如,在下图 1 中,我在 (x_1 = 4, x_2 = 14) 处标记了一个新的数据点。由于它落在空间的“红色侧”,这个观测值将被分配到红色类别。

图 1:交叉点 (x_1=4, x_2=14) 被分配到红色类别。

它是如何工作的——背后的数学

所以,给定 LDA 边界,我们可以进行分类。但是我们如何使用 LDA 算法绘制边界线呢?

这条线将图形分成红色类别和蓝色类别的概率各为 50%的区域。往一侧移动,红色的概率更高;往另一侧移动,蓝色的概率更高。我们需要找出一种方法来计算新观测值属于每个类别的概率。

新观测值属于红色类别的概率可以这样写: P(Y = red| X = (x_1 = 4, x_2 = 14))。为了便于阅读,我将以下用 x 代替 x_1 = 4, x_2 = 14,它是包含两个值的向量。

根据贝叶斯定理,我们可以将条件概率表示为 P(Y = red | X = x) = P(Y = red ) * P(X = x | Y = red) / P(X = x)。因此,为了计算给定 x 值的新观测值是“红色”的概率,我们需要知道另外三个概率。

我们一步步来。P(Y = red)P(X = x) 被称为“先验概率”。在图中,P(Y = red) 可以通过将所有“红色”观察值占总观察值数量的比例(图 1 中为 27,27%)来简单计算。

计算 x 的先验概率 P(X = x) 更为困难。我们在初始数据集中没有 x_1 = 4x_2 = 14 的观察值,那么它的概率是多少?零?不完全是。

P(X = x) 可以理解为联合概率 P(Y = red, X = x)P(Y = blue, X = x) 的总和(另见 这里):

每个联合概率可以用贝叶斯定理表示为:

你会看到先验 P(red)P(blue) 再次出现,我们已经知道如何确定它们。剩下的工作是找到计算条件概率 P(X = x | Y = red)P(X = x | Y = blue) 的方法。

即使我们在原始数据集中从未看到位置 x 的任何红色数据点,我们也可以找到一个概率,如果我们将 x 看作是从某种形式的总体中抽取的。通常在使用现实世界数据时,总体是不可直接观察的,因此我们不知道总体分布的真实形式。LDA 的做法是假设 x 值在 x_1x_2 轴上是正态分布的。对于现实世界的数据,这个假设可能或多或少是合理的。然而,由于我们处理的是生成的数据集,因此我们不必担心这个问题。这些数据来源于遵循正态(即高斯)分布的总体,其特征由两个参数(每个独立变量):~N(mean, variance)

使用多元正态分布的公式,我们可以将属于“红色”类别的数据的分布描述为

其中 Σ 表示协方差矩阵。LDA 仅使用一个共同的协方差矩阵,意味着它假设所有类别具有相同的 variance(x_1)variance(x_2)covariance(x_1, x_2)

现在我们得到了 P (X = x | Y = red) 的公式,我们可以将其代入上面的方程中以得到 P (Y = red|X = x)。这会得到一个庞大的方程,我在这里略过。幸运的是,从这里开始会更简单。我们的目标是找到分隔红色和蓝色区域的分界线,即我们要找到那些红色或蓝色的概率相等的点:

进行一些变换后,可以证明最小化这个等价于最小化以下方程:

x_2 求解,我们得到一条类似于

其中β_0β_1是依赖于均值µ_red 和µ_blue、共同协方差矩阵Σ以及红色和蓝色的先验概率 P(“red”)和 P(“blue”)的参数。

探索图表

现在你已经了解它是如何工作的,你可以使用不同的参数来探索图表。

当你点击“创建新样本”时,图表会显示两个边界。贝叶斯边界是使用“真实”总体参数计算的。相对而言,估计边界则基于样本数据对参数进行估计。

探索的一些指导问题:

  • 使用哪些参数时,估计边界与贝叶斯边界偏差较大?(见图 2,左侧图像)

  • 边界对异常值有多敏感?(见图 2,中间图像)

  • 哪些参数产生了你没有预料到的结果?

图 2. 左侧:估计边界与贝叶斯边界偏差较大。中间:边界适应异常值。右侧:边界对移动的数据点适应良好。

最终备注

LDA 是机器学习分类方法领域中的一个重要工具。然而,了解 LDA 的理论通过方程和公式是一回事,而通过实践探索获得实际理解则是另一回事。这里提供的互动工具提供了一个独特的机会,可以以更直观的方式实验和理解 LDA。

你希望看到哪些其他统计方法的互动可视化?请在评论中告诉我!

别忘了关注我,以便获取更多文章的最新动态!

除非另有说明,否则所有图像均由作者使用 Observable 创建。

如果你想了解 LDA,可以看看由 James、Witten、Hastie 和 Tibshirani 编写的精彩书籍《统计学习导论》,你可以在他们的网站上免费下载:www.statlearning.com/

线性规划:理论与应用

原文:towardsdatascience.com/linear-programming-theory-and-applications-c67600591612?source=collection_archive---------3-----------------------#2023-04-05

线性优化的主要概念及其在 Python 中的实现

布鲁诺·斯卡利亚 C. F. 莱特数据科学之道 布鲁诺·斯卡利亚 C. F. 莱特

·

关注 发表在 数据科学之道 ·13 分钟阅读·2023 年 4 月 5 日

--

图片由 Patrick Fore 拍摄,来源于 Unsplash

数值优化是量化决策过程中一个基本工具。为了优化给定的目标,必须在某些规定的情况下选择相互关联的决策变量的值。描述目标和约束条件的数学方程的格式用于区分优化问题的两个主要类别:线性非线性编程

管理科学和运筹学广泛使用线性模型,而非线性编程问题通常自然出现在物理科学和工程学中(Nocedal & Wright, 2006)。在线性问题中,正如名称所示,目标和约束仅由线性函数描述,这将是本文的重点。

在本文中,我们将涵盖线性编程的一些主要理论方面,以及使用 Python 在经典问题中的应用。为此,我们将使用 scipypyomo 库(Bynum et al., 2021)。

你可以在这里找到本文本其余部分的简要总结:

  • 问题陈述

  • 可行空间和解决技术

  • 产品组合问题

  • 运输问题

  • 进一步阅读

  • 结论

  • 参考文献

你可以在这个 GIT 仓库 找到完整的代码。现在,尽情享受吧!

问题陈述

在制定优化问题时,必须定义一个目标,该目标是向量决策变量 x 的函数,并可能受到一些等式和不等式约束的限制,这些约束也是 x 的函数。目标可以定义为 最小化最大化,尽管前者更为常见。请注意,通过简单地乘以 最大化 目标的系数 cᵢ,可以将其重新定义为 最小化 目标。

在线性问题中,决策变量空间必须以某种方式加以限制。否则,决策变量将根据目标函数趋向于正无穷或负无穷。约束可以通过等式或不等式关系来制定。它们通常在问题矩阵中作为行呈现。让我们区分等式约束矩阵 A_eq 和不等式约束矩阵 A_ub

每个组件 x 的下界和上界可能在公式中明确规定,这减少了搜索空间。作为惯例,由于解决技术,通常决策变量的下界默认等于零。这导致了如下的一般问题公式。

通用形式的线性问题。(作者提供的图片)。

不等式约束可以通过添加非负松弛变量重新公式化为等式约束。如果发生这种情况,并且所有决策变量都声明为非负,我们称线性规划为 标准形式

通过包含一个非负松弛变量将不等式约束重新公式化为等式。(作者提供的图片)。

然后可以陈述如下。

标准形式的线性规划问题。(作者提供的图片)。

注意,当不等式的对应松弛变量为零时,不等式将作为等式工作。我们将在下一节中深入探讨这一点,讨论可行空间和解决技术。

可行空间和解决技术

让我们引入一个简单的例子来探讨本节的概念。

示例问题。(图片来源于作者)。

可以在二维空间中表示为如下图示。

注意在可行区域的每个顶点(有色的),都有一个约束子集是活跃的。此外,观察到在这个示例中,最优解位于一个顶点。请记住,当不等式活跃时,它们在标准形式下的对应松弛变量为零。在线性问题中,我们将关注找到包含最优解的顶点中哪些约束是活跃的。

对于高维问题的推广是,如果存在最优解,则最优点的集合可以是一个单独的顶点(唯一解)、一个边或面,甚至是整个可行集合(多个解)。如果可行集合为空(不可行情况)或目标函数在可行区域上无界(无界情况),则问题没有解。

在正式定义中,假设问题的矩阵A在其标准形式下具有满行秩。在实践中,通常会进行预处理以去除一些冗余。通过添加松弛变量、盈余变量和人工变量来重新制定问题也是为了强制A满足这个条件(Nocedal & Wright, 2006)。如果我们可以识别一个至多具有m个非零分量的可行点x,并且对应的矩阵Bm × m),由A中与x中非零索引匹配的列组成,且为非奇异的,那么x称为基本可行点基本可行解。设置为零的变量称为非基本变量,而其余的称为基本变量。

回到可行区域……由线性约束定义的可行集合是一个多面体(在提供的两变量示例中,它是一个多边形)。代数上,其所有顶点对应于之前描述的基本可行点。因此,我们可以将问题的代数视角与几何视角联系起来,从而深入理解单纯形方法的工作原理。

单纯形方法的变体可能是线性规划中最广泛使用的解决技术。它们探讨了线性规划的基本定理(Luenberger & Ye, 2008):

  1. 如果存在可行解,则存在基本可行解

  2. 如果存在最优可行解,则存在最优基本可行解

因此,如果存在最优解,则存在一个顶点包含最优解。为了找到这样的顶点,单纯形法的步骤会探索相邻的顶点,其中基本索引的集合在一个分量上有所不同,直到没有进一步改进为止。对获取起始可行点、证明解的最优性以及在两个连续顶点之间迭代感兴趣的读者,可以参考 Nocedal & Wright(2006)或 Winston & Goldberg(2004)。

内点方法也被广泛使用,特别是对于大型线性规划问题。内点方法具有一些与单纯形法不同的共同特征。每次内点迭代的计算成本较高,但能显著推进解决方案,而单纯形法通常需要更多的低成本迭代(Nocedal & Wright,2006)。一些优化求解器可能还会使用单纯形法的变体来完善由内点方法获得的解决方案。

单纯形方法和内点方法都在下图中表示。

单纯形法(左)和内点法(右)的图示表示。(作者提供的图片)。

现在完成了一些重要的理论方面的内容,在下一部分,我们将使用 Python 实现我们的第一个问题。为了解决这个问题,我们将首先使用 Python 包scipy,它有对开源求解器 HiGHS 的封装。此外,我们还将使用pyomo(Bynum 等,2021)实现相同的问题,并用 CBC 求解器解决它。两个求解器都将利用单纯形法的变体来获得解决方案。

产品组合问题

在产品组合问题中,我们必须将有限的I资源分配给生产J种产品,以最大化预期回报。每种产品j具有已知的单位利润率cⱼ,并需要已知量的每种资源fᵢⱼ。考虑每种资源的可用量bᵢ。我们的决策变量表示每种产品应生产的数量xⱼ。我们可以将问题表述为如下。

产品组合问题。(作者提供的图片)。

从这个结构出发,我们可以轻松地将问题转换为矩阵形式,其中矩阵A_ub在第i行和第j列的元素是fᵢⱼ

考虑一个例子,其中我们有三种反应物{A, B, C}用于生产三种产品{D, E, F}。每种反应物的可用量分别为 8000、3000 和 2000。每种产品的利润率分别为 2.15、1.34、1.72。比例见下表。

+---------------------+--------+-------+------+
| Reactant \ Product  |   D    |   E   |   F  |
+---------------------+--------+-------+------+
| A                   |  7/10  |  1/3  |  1/2 |
| B                   |  1/5   |  2/3  |  1/6 |
| C                   |  1/10  |  0    |  1/3 |
+---------------------+--------+-------+------+

让我们通过导入有用的库来开始实现这个问题。

import numpy as np
from scipy.optimize import linprog

让我们实例化numpy数组来表示我们的参数。

# Pseudo costs
margins = np.array([2.15, 1.34, 1.72])
c = - margins

# Proportions
A = np.array([
    [7/10, 1/3, 1/2],
    [1/5, 2/3, 1/6],
    [1/10, 0.0, 1/3]
])

# Availability
b = np.array([8000.0, 3000.0, 2500.0])

我们现在几乎完成了。接下来需要做的就是调用scipy函数linprog来解决这个问题。

sol = linprog(c, A_ub=A, b_ub=b)
print(sol)

它应该返回具有属性fun(目标函数)和x(决策变量向量)的解决方案。

使用矩阵符号来描述这个问题比使用其他方法更加直观。对于更复杂的问题,代数建模语言(AML)可以非常有帮助。在 Python 中,一个出色的开源替代方案是pyomo(Bynum 等,2021)。那么让我们将其应用于产品组合问题。

import pyomo.environ as pyo

pyomo中,有两种建模问题的方法:AbstractConcrete模型。在第一种方法中,问题的代数表达式是在提供数据值之前定义的,而在第二种方法中,模型实例在定义其元素时立即创建。您可以在库文档中或在 Bynum 等(2021)的书中找到更多关于这些方法的信息。在本文中,我们将采用Concrete模型形式。

model = pyo.ConcreteModel()

在这个问题中有两个集合:

  • 资源I

  • 产品J

使用pyomo时,我们可以将这些集合创建为建模组件,这样可以实现某些集合操作的好处。具体如下。

model.I = pyo.Set(initialize=["A", "B", "C"])
model.J = pyo.Set(initialize=["D", "E", "F"])

由于我们使用ConcreteModel方法,因此在实例化时我们必须立即为每个组件提供值。对于集合,这是通过initialize关键字参数完成的。

现在让我们创建模型的固定参数。在pyomo中,我们可以使用Param类来存储参数。参数可以在集合上定义。在这种情况下,我们必须在定义中将给定的集合作为第一个参数传递。至于其他pyomo组件,如果定义的元素(变量、参数、表达式或约束)由多个集合索引,可以在此语句中传递多个集合。此定义采用了 Python 的args风格。

# Availability
model.b = pyo.Param(model.I, initialize=dict(zip(model.I, b)))

# Margins (now as a maximization objective)
model.c = pyo.Param(model.J, initialize=dict(zip(model.J, margins)))

# Proportions
proportions = {}
for k, i in enumerate(model.I):
    for l, j in enumerate(model.J):
        proportions[i, j] = A[k, l]

model.f = pyo.Param(model.I, model.J, initialize=proportions)

现在,让我们实例化决策变量和约束。这是分别使用VarConstraint组件完成的。请注意,我指定了x的领域为非负实数。这可以在pyomo中通过within关键字参数来实现。

# Decision variables
model.x = pyo.Var(model.J, within=pyo.NonNegativeReals)

# Availability constraint
def availability_rule_cstr(model, i):
    return sum(model.f[i, j] * model.x[j] for j in model.J) <= model.b[i]

model.cstr_available = pyo.Constraint(model.I, rule=availability_rule_cstr)

请注意,约束是在传递rule关键字参数的情况下实例化的。该规则必须是一个可调用的函数,它接收模型作为第一个参数,后面是其对应域的索引。

我们快完成了!让我们定义目标函数。

# Objective function
def obj_func(model):
    return sum(model.x[j] * model.c[j] for j in model.J)

model.obj = pyo.Objective(rule=obj_func, sense=pyo.maximize)

最后,让我们实例化一个求解器,并使用它来解决我们的模型。在这个例子中,我使用了开源求解器 CBC。您可以从这个链接下载 CBC 二进制文件。您还可以在这里找到安装教程。因为 CBC 可执行文件已包含在我的系统的 PATH 变量中,所以我可以在不指定可执行文件路径的情况下实例化求解器。如果您的系统没有设置,请通过关键字参数“executable”传递可执行文件的路径。

cbc = pyo.SolverFactory("cbc")
sol = cbc.solve(model, tee=False)

完成了!现在你可以通过使用 printdisplay 方法访问任何建模组件并检查其值。正如预期的那样,使用 pyomo 和 CBC 的解决方案与之前使用 scipy 的结果完全相同。

在接下来的部分中,让我们解决一个稍微复杂一点的问题:运输问题。

运输问题

在运输问题中,我们必须将供应商 I 的有限容量 bᵢ 与客户 J 的已知需求 dⱼ 匹配。每对 (i, j) 具有已知的供应成本 cᵢⱼ。我们的目标是通过定义每个需求由每个供应商提供的数量来最小化总成本。因此,我们考虑决策变量 xᵢⱼ 来表示这些数量。如果我们考虑总供应能力满足总需求的条件,我们可以将问题表述为以下内容。

运输问题。(作者提供的图片)。

让我们考虑一个有四个客户 {A, B, C, D} 和三个供应商 {1, 2, 3} 的例子。每个客户的需求分别为 5、15、13 和 17。每个供应商的容量分别为 14、26 和 11。现在考虑以下成本矩阵。

+---+------+------+------+------+
|   |   A  |    B |    C |    D |
+---+------+------+------+------+
| 1 |   10 |    5 |   20 |   12 |
| 2 |   12 |    7 |   12 |   19 |
| 3 |    6 |   12 |   16 |   17 |
+---+------+------+------+------+

让我们将这些参数实例化为 Python 对象。

costs = pd.DataFrame({
    "A": [10, 12, 6],
    "B": [5, 7, 12],
    "C": [20, 12, 16],
    "D": [12, 19, 17],
}, index=[1, 2, 3])

availability = {1: 14, 2: 26, 3: 11}
demands = {"A": 5, "B": 15, "C": 13, "D": 17}

让我们开始实现我们的 pyomo 模型。

model = pyo.ConcreteModel()

我们再次有两个集合:

  • 供应商 I

  • 客户 J

相应的语法应该与之前的示例非常相似。

model.I = pyo.Set(initialize=[1, 2, 3])
model.J = pyo.Set(initialize=["A", "B", "C", "D"])

让我们将这些参数实例化为 pyomo 组件。

model.b = pyo.Param(model.I, initialize=availability)

model.d = pyo.Param(model.J, initialize=demands)

c = {(i, j): costs.loc[i, j] for i in costs.index for j in costs.columns}
model.c = pyo.Param(model.I, model.J, initialize=c)

我们的决策变量…

model.x = pyo.Var(model.I, model.J, within=pyo.NonNegativeReals)

这一次我们应该有等式约束和不等式约束,因为每个需求必须得到满足(等式),同时要尊重供应商的容量(不等式)。

def availability_rule(model, i):
    return sum(model.x[i, j] for j in model.J) <= model.b[i]

model.availability_constr = pyo.Constraint(model.I, rule=availability_rule)

def demand_rule(model, j):
    return sum(model.x[i, j] for i in model.I) == model.d[j]

model.demand_constr = pyo.Constraint(model.J, rule=demand_rule)

让我们定义目标函数,再次使用 CBC 来解决问题。注意这次,我使用了 expr 关键字参数来定义目标,它直接接收一个 pyomo 表达式。

model.obj = pyo.Objective(expr=sum(model.x[i, j] * model.c[i, j] for (i, j) in model.x), sense=pyo.minimize)
sol = cbc.solve(model, tee=False)

在这个示例中,我将把结果导出到 pandas DataFrame,以便更好地可视化。

results = pd.DataFrame(index=model.I, columns=model.J)
for i, j in model.x:
    results.loc[i, j] = model.x[i, j].value

这应该返回类似的结果。

+---+---------+----------+----------+----------+
|   |    A    |      B   |      C   |      D   |
+---+---------+----------+----------+----------+
| 1 |     0.0 |      2.0 |      0.0 |     12.0 |
| 2 |     0.0 |     13.0 |     13.0 |      0.0 |
| 3 |     5.0 |      0.0 |      0.0 |      5.0 |
+---+---------+----------+----------+----------+

在这种情况下,所有客户都将以最低的总成本得到服务。但如果我们供应商的可用性少于总需求呢?那么问题将变得不可行,我们需要定义哪些约束应该被违反以返回某种优化结果。在这个问题中,一个简单的建议是引入人工变量来满足那些单位成本高于任何原始供应商的需求。请记住,最佳策略应该是针对实际问题中的每种场景唯一的。

进一步阅读

为了更深入地理解线性规划的理论方面,我强烈建议阅读 Luenberger & Ye (2008) 和 Nocedal & Wright (2006) 书中的相关章节。对偶性的概念特别有用,因为它涉及灵敏度分析、问题的经济解释和解决技术(仅举几个应用)。

运筹学广泛使用数值优化。Winston & Goldberg (2004) 的书对于那些对优化的更多应用感兴趣的人可以非常有帮助,除了与相关科学的联系。

一些实际问题包括离散变量,以形成析取问题方面和整体性。我写了几篇关于这个主题的中等文章,可能会有所帮助。请参阅这里的 分支与界限介绍,经典的 背包问题,以及 车间调度问题

一些问题还包括非线性函数,无论是在目标函数还是约束条件中。如果是这种情况,你可能会想查看这篇其他文章:

[## 非线性编程:理论与应用]

基于梯度的线性搜索优化算法详细解释并在 Python 中从零实现。

towardsdatascience.com](/nonlinear-programming-theory-and-applications-cfe127b6060c?source=post_page-----c67600591612--------------------------------)

结论

在本文中,详细解释了线性优化的一些最相关的理论方面,并通过两个实际实现示例进行了说明:产品组合问题和运输问题。使用了 Python 库scipy来通过矩阵符号解决产品组合问题,并且这两个问题均使用 Python AML pyomo 建模,并使用开源求解器进行求解。所用代码完全开放给读者,可以在这个 GIT 仓库中找到。

参考文献

Bynum, M. L. 等, 2021. Pyomo:Python 中的优化建模. 施普林格。

Luenberger, D. G. & Ye, Y., 2008. 线性与非线性编程. 第三版. 斯坦福: 施普林格。

Nocedal, J. & Wright, S. J., 2006. 数值优化. 第二版. 纽约: 施普林格纽约。

Winston, W. L. & Goldberg, J. B., 2004. 运筹学:应用与算法. 第四版. 贝尔蒙特,加州: 汤姆森布鲁克斯/科尔贝尔蒙特。

线性回归深入探讨(第一部分)

原文:towardsdatascience.com/linear-regression-in-depth-part-1-485f997fd611

深入探讨线性回归模型的理论和实现

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

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

--

Enayet Raheem拍摄于Unsplash

线性回归是最基础且最常用的预测模型之一。它可以追溯到 1805 年,当时勒让德和高斯使用线性回归来预测行星的运动。

回归问题的目标是根据其他变量的值预测一个变量的值。例如,我们可以使用回归预测股票价格,基于各种经济指标,或者根据广告支出的金额预测公司的总销售额。

在线性回归中,我们假设给定的输入特征与目标标签之间存在线性关系,并且我们试图找到这种关系的确切形式。

本文提供了关于线性回归模型理论和实现的全面指南。在文章的第一部分,我们将主要关注简单线性回归,其中数据集仅包含一个特征(即数据集由二维点组成)。在文章的第二部分,我们将讨论多重线性回归,其中数据集可能包含多个特征。

有许多与回归相关的术语,数据科学家经常将它们混用,但它们并不总是相同,例如:残差/误差,成本/损失/误差函数,多重/多变量回归,平方损失/均方误差/平方残差和等。

鉴于此,我在本文中尽力对所使用的定义和术语进行尽可能清晰的阐述。

正式定义和符号

回归问题

在回归问题中,我们得到一组 n 个标记示例:

D = {(x₁, y₁), (x₂, y₂), … , (xₙ, yₙ)},其中 x 表示示例 i特征yᵢ 表示该示例的标签

每个x 是一个包含 m 个特征的向量:x = (xᵢ₁, xᵢ₂, …, xᵢₘ),其中 表示转置。变量 xᵢⱼ 被称为独立变量解释变量

标签 y 是一个连续值变量 (yR),称为因变量响应变量

我们假设标签 y 和输入向量 x 之间存在某种相关性,这种相关性由某个函数 f(x) 和一个误差变量 ϵ 建模:

误差变量ϵ捕捉了所有未建模的因素,这些因素影响标签除了特征外,例如测量误差或一些随机噪声。

我们的目标是找到函数 f(x),因为知道这个函数将使我们能够预测任何新样本的标签。然而,由于我们只能从有限数量的训练样本中学习 f(x),因此我们只能获得这个函数的估计值。

我们的模型从给定数据中估算的函数称为模型的假设,通常表示为* h*(x)。

线性回归

线性回归中,我们假设特征和目标标签之间存在线性关系。因此,模型的假设具有以下形式:

线性回归模型

w₀, …, wₘ 称为模型的参数(或权重)。参数 w₀ 通常称为截距(或偏置),因为它表示 h(x) 的图与 y-轴(在二维情况下)的交点。

为了简化 h(x),我们添加了一个常数特征 x₀,总是等于 1。这样我们可以将 h(x) 写成特征向量 x = (x₀, …, xₘ) 和权重向量 w = (w₀, …, wₘ) 的点积:

线性回归模型的向量形式

普通最小二乘法(OLS)

在线性回归中,我们的目标是找到参数 w₀, …, wₘ,使得我们模型的预测 h(x) 尽可能接近真实标签 y。换句话说,我们希望找到最能拟合数据集的模型参数。

为此,我们定义了一个成本函数(有时也称为误差函数),该函数衡量我们模型的预测与真实标签之间的距离。

我们从定义残差开始,残差是给定数据点的标签与模型预测值之间的差异:

残差的定义

普通最小二乘(OLS)回归找到使平方残差和最小的最佳参数值:

最小二乘成本函数

注意,损失函数计算每个观察值的误差,在 OLS 中称为平方损失,而成本函数(通常用 J 表示)计算整个数据集的误差,在 OLS 中称为平方残差和(SSR)或平方误差和(SSE)。

尽管 OLS 是最常见的回归类型,但还有其他类型的回归,例如最小绝对偏差回归。我们将在本文的最后一部分解释为什么最小二乘函数是首选的成本函数。

幸运的是,除了某些特殊情况(稍后将讨论),最小二乘成本函数是凸函数。一个函数 f(x) 是凸的,如果图形上任意两点之间的线段都位于图形之上。简单来说,函数图形呈杯形 ∪。这意味着凸函数只有一个最小值,即全局最小值。

由于 J(w) 是凸的,使用一阶导数找到其最小值点可以保证得到唯一解,因此也是最优解。

简单线性回归

当数据集只有一个特征(即由二维点 (x, y) 组成)时,回归问题称为简单线性回归

从几何上讲,在简单线性回归中,我们尝试找到一条直线,使其尽可能接近所有数据点:

简单线性回归

在这种情况下,模型的假设只是直线的方程:

回归线的方程

其中 w₁ 是直线的斜率,w₀ 是直线与 y- 轴的交点。在这种情况下,残差是数据点与拟合直线之间的距离。

在这种情况下,最小二乘成本函数的形式如下:

OLS 成本函数在简单线性回归中的表现

正规方程

我们的目标是找到使得线最符合数据点的参数 w₀ 和 w₁,即使得成本最小的直线。为此,我们可以对 J(w₀, w₁) 关于两个参数分别求偏导数,将其设为 0,然后解这个线性方程组(这些方程被称为正规方程)。

让我们从 J 关于 w₀ 的偏导数开始:

将这个导数设为 0 可以得到以下结果:

我们已经找到了关于 w₀ 的表达式,该表达式以 w₁ 和数据点为变量。

接下来,我们计算Jw₁的偏导数:

将这个导数设置为 0 得到以下结果:

让我们将表达式* w*₀代入这个方程中:

因此,回归线的系数为:

数值示例

假设我们想找出身高和体重之间是否存在线性相关性。我们得到以下 10 个示例,代表了 30 至 39 岁美国女性的平均身高和体重(来源:世界年鉴与事实手册,1975 年)。

训练集

为了手动找到回归线,我们首先构建以下表格:

根据表格最后一行的总计,我们可以计算回归线的系数:

因此,拟合线的方程为:

Python 实现

我们现在将使用 Python 找到回归线。

首先,让我们编写一个通用函数,用于找到任何给定二维数据集的回归线参数:

def find_coefficients(x, y):
    n = len(x)
    w1 = (n * x @ y - x.sum() * y.sum()) / (n * (x**2).sum() - x.sum()**2)
    w0 = (y.sum() - w1 * x.sum()) / n
    return w0, w1

上述代码是将正规方程直接转换为 NumPy 函数和运算符。

让我们在上面的数据集上测试我们的函数。我们首先定义我们的数据点:

x = np.array([1.55, 1.60, 1.63, 1.68, 1.70, 1.73, 1.75, 1.78, 1.80, 1.83])
y = np.array([55.84, 58.57, 59.93, 63.11, 64.47, 66.28, 68.10, 69.92, 72.19, 74.46])

让我们绘制它们:

def plot_data(x, y):
    plt.scatter(x, y)
    plt.xlabel('Height (m)')
    plt.ylabel('Weight (kg)')
    plt.grid()
plot_data(x, y)

训练集

我们现在使用刚刚编写的函数来寻找回归线的参数:

w0, w1 = find_coefficients(x, y)
print('w0 =', w0)
print('w1 =', w1)
w0 = -47.94681481481781
w1 = 66.41279461279636

我们得到了与手动计算相同的结果,但精度更高。

让我们编写另一个函数来绘制回归线。为此,我们可以简单地取输入范围内的最小和最大x值,计算它们在回归线上的y坐标,然后绘制连接这两点的线:

def plot_regression_line(x, y, w0, w1):
    p_x = np.array([x.min(), x.max()])
    p_y = w0 + w1 * p_x
    plt.plot(p_x, p_y, 'r')

最后,让我们将回归线与数据点一起绘制:

plot_data(x, y)
plot_regression_line(x, y, w0, w1)

回归线

我们可以看到这两个变量之间的关系非常接近线性。

作为练习,从 Kaggle 下载身高和体重数据集。该数据集包含了 25,000 名 18 岁青少年的身高和体重。建立一个线性回归模型,用于预测青少年的体重,并绘制结果。

评估指标

有几种评估指标用于评估回归模型的性能。最常见的两个指标是RMSE(均方根误差)和R²得分

注意评估指标成本函数之间的区别。成本函数用于定义模型学习过程的目标,并在训练集上计算。相反,评估指标用于训练过程后评估模型在保留数据集(验证集或测试集)上的表现。

RMSE(均方根误差)

RMSE 定义为平方误差的均值的平方根(模型预测与真实标签之间的差异):

RMSE 定义

注意,我们在模型训练过程中称为残差的东西,通常在计算保留集上的时候称为误差(或预测误差)。

RMSE 总是非负的,较低的 RMSE 表明模型对数据的拟合更好(完美的模型 RMSE 为 0)。

我们可以直接计算 RMSE 或通过使用 sklearn.metrics 模块来计算。这个模块提供了许多用于测量不同类型模型性能的函数。虽然它没有明确计算 RMSE 的函数,但我们可以使用 mean_squared_error() 函数首先计算 MSE,然后取其平方根得到 RMSE。

sklearn.metrics 中的大多数评分函数期望得到一个包含真实标签 (y_true) 和模型预测 (y_pred) 的数组。因此,我们首先需要计算模型在给定数据点上的预测。这可以通过使用回归线的方程轻松完成:

y_pred = w0 + w1 * x

我们现在可以调用 mean_squared_error() 函数并找到 RMSE:

from sklearn.metrics import mean_squared_error as MSE

rmse = np.sqrt(MSE(y, y_pred))
print(f'RMSE: {rmse:.4f}')

我们得到的结果是:

RMSE: 0.5600

RMSE 的优点:

  • 提供了模型误差的平均幅度的度量。

  • 由于误差在平均之前被平方,RMSE 对大误差赋予了相对较高的权重。

  • 可用于比较不同模型在同一数据集上的性能。

RMSE 的缺点:

  • 不能用于比较模型在不同数据集上的表现,因为它依赖于输入特征的尺度。

  • 对离群值敏感,因为每个误差对 RMSE 的影响与平方误差的大小成正比。

R² 分数

R² 分数(也称为决定系数)是衡量模型拟合优度的指标。它计算回归模型的平方误差之和与始终预测 y 的均值的基线模型的平方误差之和之间的比率,并从 1 中减去该比率:

R² 分数定义

其中 是目标标签的均值。

最佳的R²评分为 1,这表明模型预测与数据完全吻合。一个始终预测* y均值的常量模型,无论输入特征如何,其R*²评分为 0。

R²评分低于 0 表示模型的表现比最差的最小二乘预测器还要差。这通常表明选择了错误的模型。

要计算R²评分,我们可以使用 sklearn.metrics 中的r2_score函数:

from sklearn.metrics import r2_score

score = r2_score(y, y_pred)
print(f'R2 score: {score:.4f}')

我们得到的结果是:

R2 score: 0.9905

R²评分非常接近 1,这意味着我们有一个几乎完美的模型。然而,请注意,在这个例子中,我们是在训练集上评估模型,在训练集上模型通常会有比在保留集上更高的评分。

R²评分也可以解释为模型中自变量对因变量* y*方差的解释比例(感兴趣的读者可以在这个维基百科文章中找到原因)。

R²评分的优点:

  • 不依赖于特征的尺度。

  • 可以用来比较不同模型在不同数据集上的表现。

R²评分的缺点:

  • 不提供有关模型误差大小的信息。

  • R²评分随着模型特征数量的增加而单调增加,因此不能用来比较特征数量非常不同的模型。

OLS 和最大似然

最后,我们将展示普通最小二乘(OLS)和最大似然之间的关联,这也是使用 OLS 解决回归问题的主要动机。更具体地说,我们将证明在假设误差服从均值为零的正态分布的条件下,OLS 估计量与最大似然估计量(MLE)是相同的。

对最大似然概念不熟悉的人,可以查看我的上一篇文章

记住,在线性回归中,我们假设标签由特征的线性函数加上一些随机噪声生成:

假设误差是独立同分布(i.i.d.)的,并且具有均值为 0、方差为σ²的正态分布:

在这种情况下,标签* y也服从均值为wx 和方差为σ*²的正态分布(向正态分布变量中添加常数将得到一个正态分布的变量,但其均值被该常数平移):

因此,给定输入x和权重向量wy的概率密度函数(PDF)为:

基于误差(因此标签)的独立假设,我们可以将模型参数w的似然表示如下:

因此,对数似然是:

我们可以看到,对数似然中唯一依赖于参数w的表达式是:

这正是普通最小二乘法(OLS)的成本函数!因此,最大化模型的似然等同于最小化平方残差和。

最后的说明

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

你可以在我的 GitHub 上找到本文的代码示例:github.com/roiyeho/medium/tree/main/simple_linear_regression

讨论多元线性回归的文章第二部分可以在这里找到。

感谢阅读!

深入解析线性回归(第二部分)

原文:towardsdatascience.com/linear-regression-in-depth-part-2-5d40fd19efd4

深入探讨多重线性回归及其在 Python 中的示例

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

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

--

图片来源:ThisisEngineering RAEngUnsplash

在这篇文章的 第一部分 中,我们正式定义了线性回归问题,并展示了如何解决简单线性回归问题,即数据集仅包含一个特征。在文章的第二部分,我们将讨论多重线性回归问题,其中数据集可能包含任意数量的特征。

我们将首先将简单线性回归的封闭形式解推广到任意数量的特征。然后,我们将建议一种基于梯度下降的解决线性回归问题的替代方法,并讨论这种方法与使用封闭形式解的利弊。此外,我们将探讨 Scikit-Learn 中实现这两种方法的类,并演示如何在实际数据集上使用它们。

多重线性回归定义

回忆一下,在回归问题中,我们会给出一组 n 个标记示例: D = {(x₁, y₁), (x₂, y₂), … , (xₙ, yₙ)}, 其中 xᵢ 是一个 m 维向量,包含示例 i特征yᵢ 是一个实值,表示该示例的 标签

线性回归 问题中,我们假设特征向量 x 和标签 y 之间存在线性关系,因此我们的模型假设采取以下形式:

线性回归模型假设

我们的目标是找到该模型的参数 w,以最小化平方残差的总和:

最小二乘成本函数

在文章的前面部分,我们展示了如何使用正规方程找到当m = 1 时的最佳w。现在我们将扩展这些方程以适应任意数量的特征m

为了简化正规方程在一般情况下的推导,我们首先定义一个矩阵X,它包含数据集中所有特征的值,包括截距项:

设计矩阵

这个矩阵称为设计矩阵。设计矩阵中的每一行表示一个单独的样本,列代表解释变量。矩阵的维度为n × (m + 1),其中n是样本数量,m是特征数量。

此外,我们将向量y定义为一个包含所有目标值的n维向量:

目标向量

这些定义使我们能够以以下矩阵形式写出最小二乘成本函数:

矩阵形式的 OLS 成本函数

证明

我们首先注意到:

向量与自身的点积uu只是其所有分量平方的总和,因此我们有:

封闭解

与简单线性回归的情况一样,函数J(w)是凸的,因此它有一个唯一的局部最小值,这也是全局最小值。为了找到这个全局最小值,我们需要计算J(w)对w的梯度并将其设为零。

J(w)对w的梯度是:

在此证明过程中,我们使用了一些矩阵微积分的基本规则,这些规则在本文中有解释:

[## 矩阵微积分的温和介绍

包括在机器学习中的应用

medium.com](https://medium.com/@roiyeho/a-gentle-introduction-to-matrix-calculus-14584f2c4f60?source=post_page-----5d40fd19efd4--------------------------------)

我们现在将这个梯度设为零以得到正规方程:

因此,最小化最小二乘成本函数的最佳w是:

普通最小二乘的封闭解

请注意,我们在这里假设X的列是线性独立的(即X具有满列秩),否则XᵗX是不可逆的,并且w没有唯一解。

X的列线性相关时,我们称这种现象为多重共线性。在数学上,如果对于所有样本i,一组变量是完全多重共线的:

完全多重共线性

其中λₖ是常数,xᵢₖ是样本i中特征k的值。

实际上,完全多重共线性很少见(例如,它可能是由于不小心重复了数据中的一个变量引起的)。然而,即使是较低程度的多重共线性,即两个或更多特征之间高度相关(但不是完全相关),也会在拟合模型时(系数对数据中的小变化非常敏感)和解释结果时(很难识别哪些特征对模型预测的影响最大)造成问题。

感兴趣的读者可以在这个维基百科条目中找到有关多重共线性问题及其处理方法的更多信息。

多重线性回归示例

为了演示闭式解的使用,让我们为加州住房数据集构建一个线性回归模型,该数据集可从 sklearn.datasets 模块获取。该数据集的目标是基于该区域的 8 个不同特征(如中位收入或每户的平均房间数)预测加州某个地区(房屋区块)的中位房价。

我们首先导入所需的库,并初始化随机种子,以便获得可重复的结果:

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

np.random.seed(0)

然后,我们获取数据集:

from sklearn.datasets import fetch_california_housing

data = fetch_california_housing()
X, y = data.data, data.target
feature_names = data.feature_names

为了探索数据集,我们将特征(X)和标签(y)合并到一个 Pandas DataFrame 中,并显示表格的前几行:

mat = np.column_stack((X, y))
df = pd.DataFrame(mat, columns=np.append(feature_names, 'MedianValue'))
df.head()

加州住房数据集的前五行

我们可以通过调用 DataFrame 的info()方法进一步调查数据集,该方法提供有关列的类型以及是否包含任何缺失值的信息:

df.info()

幸运的是,这个数据集只包含数值特征且没有缺失值。因此,这里不需要数据预处理(闭式解不需要数据的归一化)。

接下来,我们需要向矩阵X_train中添加一列 1,以表示截距项。这可以通过函数np.column_stack()轻松完成:

X_with_bias = np.column_stack((np.ones(len(X)), X))

我们现在将数据划分为 80%的训练集和 20%的测试集:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X_with_bias, y, test_size=0.2, random_state=0)

现在,让我们编写一个通用函数,使用我们之前找到的闭式解来找到任何给定数据集的最佳w*:

def closed_form_solution(X, y):
    w = np.linalg.inv(X.T @ X) @ X.T @ y
    return w 

闭式解法可以用一行代码实现!

让我们使用这个函数来找到我们训练集的最优 w*:

w = closed_form_solution(X_train, y_train)
print(w)

最优的 w* 是:

[-3.68585691e+01  4.33333407e-01  9.29324337e-03 -9.86433739e-02
  5.93215487e-01 -7.56192502e-06 -4.74516383e-03 -4.21449336e-01
 -4.34166041e-01]

这个向量中的第一个分量是截距 (w₀),其余的是数据集中八个特征的系数。

现在让我们在训练集和测试集上评估模型。评估模型在这两者上的表现是很重要的,因为训练分数和测试分数之间的较大差异可能表明你的模型存在过拟合现象。

我们首先在训练集上找到 R² 分数。为此,我们首先通过将矩阵 X_train_b 乘以向量 w 来获取模型在训练示例上的预测:

y_train_pred = X_train @ w

我们现在使用来自 sklearn.metrics 的 r2_score() 函数来找到训练集上的 R² 分数:

from sklearn.metrics import r2_score

train_score = r2_score(y_train, y_train_pred)
print(f'R2 score (train): {train_score:.4f}')

我们得到的分数是:

R2 score (train): 0.6089

让我们在测试集上做同样的操作:

y_test_pred = X_test @ w

test_score = r2_score(y_test, y_test_pred)
print(f'R2 score (test): {test_score:.4f}')

我们得到的分数是:

R2 score (test): 0.5943

分数不高,这表明特征与标签之间的关系可能不是线性的。在这种情况下,非线性回归模型,如 回归树 或 k-近邻算法可以提供更好的结果。

练习

假设我们不小心将数据集中每个点都重复了一次,然后再次运行线性回归。这将如何影响模型的权重?

提示:考虑一下设计矩阵 X 和标签向量 y 将会发生什么变化,以及这些变化如何影响正规方程。

解决方案可以在本文的最后找到。

Scikit-Learn 中的线性回归

Scikit-Learn 提供了一个名为 LinearRegression 的类,它也实现了普通最小二乘问题的闭式解法。

默认情况下,该类会自动将一列 1 添加到设计矩阵中,因此你不需要像我们之前那样手动添加(除非在构造函数中将参数 fit_intercept 设置为 False)。因此,我们需要重新将原始数据集(没有额外的 1)拆分为训练集和测试集:

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

让我们创建一个 LinearRegression 类的实例,并将其拟合到训练集上:

from sklearn.linear_model import LinearRegression

reg = LinearRegression()
reg.fit(X_train, y_train)

拟合的参数向量 w 被存储在该类的两个属性中:

  • coef_ 是一个包含所有权重的数组,除了截距项

  • intercept_ 是截距项 (w₀)

让我们打印它们:

print(reg.intercept_)
print(reg.coef_)

输出是:

-36.858569106801234
[ 4.33333407e-01  9.29324337e-03 -9.86433739e-02  5.93215487e-01
 -7.56192502e-06 -4.74516383e-03 -4.21449336e-01 -4.34166041e-01]

我们得到的系数与在 NumPy 中计算的完全一样。

score()方法返回模型的R²分数。它只需要X矩阵和数据集y的向量(因此不需要计算模型的预测)。例如,我们可以如下获得训练集和测试集的R²分数:

train_score = reg.score(X_train, y_train)
print(f'R2 score (train): {train_score:.4f}')

test_score = reg.score(X_test, y_test)
print(f'R2 score (test): {test_score:.4f}')
R2 score (train): 0.6089
R2 score (test): 0.5943

正如预期的那样,我们获得了与之前相同的R²分数。

回归误差分析

除了评估模型的整体性能,我们通常还希望研究回归误差的行为。例如,误差是否围绕 0 正态分布,还是有偏?是否存在模型预测误差特别大的输入?

对这些问题的回答将帮助我们找到这些错误的来源。例如,如果误差没有围绕 0 正态分布,这可能表明线性回归模型不适合我们的数据集,我们需要尝试其他回归模型(例如多项式回归)。或者,如果我们的模型在某些样本上的预测误差特别高,这些样本可能是异常值,我们需要调查它们的来源。

一个可以帮助你回答这些问题的图称为残差图。该图显示了y轴上的残差与模型在x轴上的预测值。

让我们编写一个函数来创建这个图:

def plot_residuals(y_train_pred, y_train, y_test_pred, y_test):
    plt.scatter(y_train_pred, y_train_pred - y_train, s=2, marker='o', c='b', label='Training')    
    plt.scatter(y_test_pred, y_test_pred - y_test, s=2, marker='s', c='m', label='Test') 

    xmin = min(y_train_pred.min(), y_test_pred.min())
    xmax = max(y_train_pred.max(), y_test_pred.max())
    plt.hlines(y=0, xmin=xmin, xmax=xmax, color='black')    

    plt.xlim(xmin, xmax)
    plt.xlabel('Predicted values')
    plt.ylabel('Residuals')
    plt.legend()

现在我们可以调用这个函数,展示训练集和测试集的残差:

plot_residuals(y_train_pred, y_train, y_test_pred, y_test)

我们可以看到,大多数误差围绕 0 对称分布,但在输入范围的远端有一些异常值,这可能需要进一步调查。

练习

从 Kaggle 下载学生成绩数据集。建立一个线性回归模型,以根据学生的学习时间和课程数量预测成绩。计算模型在训练集和测试集上的 RMSE 和R²分数。绘制残差与预测值的关系图。你能从这个图中学到什么?

梯度下降

尽管闭式解法为我们提供了一种直接找到回归模型最佳参数的方法,但它也有一些缺点:

  1. 当我们有大量特征时,闭式解法在计算上效率低下,因为它需要计算XᵗX的逆,这是一个m × m的矩阵(m是特征的数量)。在大多数实现中,计算矩阵的逆的时间复杂度为O(m³)。

  2. 它需要将整个设计矩阵X保存在内存中,这在数据集非常大的情况下并不总是可行。

  3. 它不支持在线(增量)学习,因为任何对设计矩阵X的更改都需要重新计算XᵗX的逆。

值得庆幸的是,还有一种找到最佳 w 的替代方法,即 梯度下降。梯度下降是一种迭代方法,用于找到函数的最小值,其中我们在梯度的相反方向上迈出小步,以接近最小值:

梯度下降

为了使用梯度下降来找到最小二乘成本的最小值,我们需要计算 J(w) 对每个权重的偏导数。

J(w) 对任何权重 wⱼ 的偏导数是:

因此,梯度下降更新规则是:

梯度下降更新规则

其中 α 是一个控制步长的学习率(0 < α < 1)。

我们可以一步更新整个向量w,而不是单独更新每个组件。

向量形式的梯度下降更新规则

梯度下降可以应用于以下模式之一:

  1. 批量梯度下降 — 在计算整个训练集的误差后更新权重。

  2. 随机梯度下降(SGD) — 在每个训练样本后执行一次梯度下降步骤。在这种情况下,梯度下降更新规则如下:

SGD 更新规则

相较于批量梯度下降,SGD 通常收敛更快,因为它在每个样本后都取得进展,并且它也支持在线学习,因为它可以一次处理一个新数据点。另一方面,SGD 比批量梯度下降不那么稳定,其收敛到全局最优解的保证并不确定(尽管在实践中它非常接近最优解)。

请注意,每当使用梯度下降时,必须确保数据集是 标准化 的(否则梯度下降可能在不同方向上采取不同大小的步骤,从而导致不稳定)。

SGDRegressor 类

Scikit-Learn 中的 SGDRegressor 类实现了用于拟合线性回归模型的 SGD 方法。

这个类的重要超参数是:

  • loss — 用作优化目标的损失函数。该参数的选项包括:squared_error(平方损失,这是默认选项)、huber(Huber 损失)和 epsilon_intensive(支持向量回归中的损失函数)。这些损失函数之间的区别在这篇文章中有说明。

  • penalty — 使用的正则化类型(默认为‘l2’)。

  • alpha — 正则化系数(默认为 0.0001)。

  • max_iter — 训练数据上的最大迭代次数(默认为 1000)。

  • learning_rate — 权重更新的学习率调度(默认为‘invscaling’)。

  • eta0 — 使用的初始学习率(默认为 0.01)。

  • early_stopping — 是否在验证分数没有改善时停止训练(默认为 False)。

  • validation_fraction — 从训练集中抽取用于验证的比例(默认为 0.1)。

由于我们需要在使用 SGD 之前规范化数据,因此我们将创建一个管道,该管道包括两个步骤:

  1. 一个StandardScaler,它通过去除均值并将特征缩放到单位方差来规范化特征。

  2. 使用默认设置的 SGDRegressor。

from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import SGDRegressor
from sklearn.pipeline import Pipeline

pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('reg', SGDRegressor())
])

让我们将管道拟合到我们的训练集:

pipeline.fit(X_train, y_train)

现在让我们在训练集和测试集上评估模型:

train_score = pipeline.score(X_train, y_train)
print(f'R2 score (train): {train_score:.4f}')

test_score = pipeline.score(X_test, y_test)
print(f'R2 score (test): {test_score:.4f}')

我们得到的分数是:

R2 score (train): -511.0496
R2 score (test): -4735.6157

这些是非常差的分数!刚刚发生了什么?

当你使用梯度下降法得到如此差的分数时,通常意味着你的学习率过高,这导致算法在最小值的两侧之间震荡:

由于学习率过高导致梯度下降法的震荡

让我们将学习率从 0.01 降低到 0.001,通过更改 SGDRegressor 的eta0参数:

pipeline.set_params(reg__eta0=0.001)

让我们重新拟合训练集并重新评估:

pipeline.fit(X_train, y_train)
train_score = pipeline.score(X_train, y_train)
print(f'R2 score (train): {train_score:.4f}')

test_score = pipeline.score(X_test, y_test)
print(f'R2 score (test): {test_score:.4f}')

我们这次得到的分数是:

R2 score (train): 0.6018
R2 score (test): 0.5841

这些分数类似于我们使用闭式解得到的R²分数(这些分数略低,因为 SGD 接近全局最小值,但未达到最小值本身)。

关键要点

  • 在线性回归中,我们试图找到一组特征与目标变量之间的线性关系。

  • 关键假设是特征线性无关,且误差项彼此独立且正态分布,均值为零。

  • 在普通最小二乘(OLS)回归中,我们试图最小化残差的平方和。OLS 的成本函数是凸的,因此具有一个最小点。

  • 寻找模型最佳参数的两种主要方法是闭式解和梯度下降法。当数据集很大或需要支持在线学习时,梯度下降法更受青睐。

  • 在使用梯度下降法时,重要的是规范化你的特征并选择合适的学习率。

  • 我们使用R²分数评估回归模型的性能,该分数在 0 到 1 之间变化,衡量模型比始终预测目标均值的基准模型好多少,以及 RMSE,即均方误差的平方根。

重复数据练习的解决方案

回忆一下,闭式解为:

如果我们加倍数据点,那么代替 Xy,我们将有以下设计矩阵和目标向量:

矩阵A有 2n行和m列,其中矩阵的第 1 行到第n行与第n + 1 行到第 2n行相同。类似地,向量z有 2n行,其中前n行与最后n行相同。

我们可以很容易地证明

证明

类似地,我们有(我们可以将z视为具有一列的矩阵):

因此,我们可以写成:

我们得到与原始模型相同的权重!也就是说,回归模型不会改变。

最后的说明

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

你可以在我的 github 上找到本文的代码示例:github.com/roiyeho/medium/tree/main/multiple_linear_regression

感谢阅读!

使用(仅)SQL 拟合回归模型的快速而粗糙的方法

原文:towardsdatascience.com/linear-regression-in-sql-62eaf4861290

你并不总是需要 Python 或 R 来拟合模型——Postgresql 已经涵盖了基础内容

Thuwarakesh MurallieTowards Data Science Thuwarakesh Murallie

·发布于Towards Data Science ·阅读时间 4 分钟·2023 年 4 月 13 日

--

照片由迈克尔·德泽迪克拍摄,发布在Unsplash

SQL 程序员很少会拟合任何机器学习模型。

除非他们具备 Python 或 R 的知识,否则其他人将会做这件事。虽然 Python 和 scikit-learn 通常是我进行机器学习的首选工具,但值得注意的是,SQL 也能进行一些快速而粗糙的模型拟合。

回归模型是几乎每个人都需要的常见模型。我记得在高中物理课上第一次使用它。

在这种情况下,如果你的数据存储在 Postgres 表中,你不必离开 SQL 环境就能拟合这些简单的模型。

## 3 SQL 优化技巧,可以瞬间提升查询速度

在切换到完全不同的数据模型之前,可以尝试一些简单的技巧

towardsdatascience.com

下面是我们如何做的。

SQL 中的回归建模

Postgres 具有内置的回归模型工具。你无需安装或激活任何特别的模块。

我们可以快速拟合线性回归模型,并利用它们进行预测。

线性回归模型是关于找到一个线性方程来概括数据集。因此,我们只需要找到这条线的截距和斜率。

regr_sloperegr_intercept 函数帮助我们完成这个任务。

假设我们有一个包含降雨量和温度列的表格。我们需要利用降雨量的信息来预测温度列中缺失的值。

这是一个 SELECT 语句,它从天气表中检索降雨量和温度值,其中缺失的温度值通过使用regr_sloperegr_intercept函数进行预测:

SELECT 
  rainfall, 
  CASE 
    WHEN temperature IS NULL 
    THEN regr_slope(temperature, rainfall) * rainfall 
         + regr_intercept(temperature, rainfall) 
    ELSE temperature 
  END AS temperature
FROM 
  weather;

我们使用CASE语句来检查温度值是否缺失(即,NULL)。如果缺失,我们使用regr_sloperegr_intercept函数基于相应的降雨量预测温度值。否则,我们使用原始的温度值。

这 5 种 SQL 技巧涵盖了约 80%的实际项目

加速你的 SQL 学习曲线。

[towardsdatascience.com

持久化预测结果

如果我想永久填充表中的缺失值,我可以使用上述代码的稍微修改版本。你可以为预测创建一个物化视图,或将预测插入到一个不同的表中。

-- Populate the table with predicted temperature values
INSERT INTO predicted_temperature (rainfall, temperature)
SELECT 
  t1.rainfall, 
  CASE 
    WHEN t1.temperature IS NULL 
    THEN regr_slope(t2.temperature, t2.rainfall) * t1.rainfall 
         + regr_intercept(t2.temperature, t2.rainfall) 
    ELSE t1.temperature 
  END AS temperature
FROM 
  weather t1
  LEFT JOIN weather t2 ON t1.rainfall = t2.rainfall
WHERE 
  t1.temperature IS NULL;

这里是相同的操作,但创建一个物化视图。物化视图只是保存在数据库中的查询及其来自最新运行的结果。

CREATE MATERIALIZED VIEW predicted_temperature_mv AS
SELECT 
  rainfall, 
  CASE 
    WHEN temperature IS NULL 
    THEN regr_slope(temperature, rainfall) * rainfall 
         + regr_intercept(temperature, rainfall) 
    ELSE temperature 
  END AS temperature
FROM 
  weather;

Python 到 SQL — 我现在可以以 20 倍的速度加载数据

上传大量数据的好、坏、丑的方式

[towardsdatascience.com

在 SQL 中拟合回归模型的限制

任何曾经做过回归模型的人都可以证明,上述示例仍需完善。拟合一个正确的模型比我们在这里讨论的要复杂。

尤其在实际情况中,我们应考虑多个自变量来预测因变量。有时我们需要在拟合回归模型时使用多项式阶数,而不仅仅是线性模型。我们可能还需要同时使用这两者。

Postgres 的回归工具无法处理如此复杂的建模。我们只能构建一个具有一个自变量的线性回归模型。

SQL on Pandas — 我新的 10 倍速度最爱。

将两者的优点结合起来

[towardsdatascience.com

结论

线性回归模型可能是用于预测连续数据最常用的模型。数据科学家通常将其作为更复杂的机器学习建模的起点。

尽管我们需要像 Python 这样的编程语言来处理更复杂的机器学习任务,但简单任务如线性回归可以在 SQL 中完成。

我希望这篇文章中讨论的这个小技巧能帮助你在日常工作中。

感谢阅读,朋友!如果你喜欢我的文章,欢迎通过LinkedInTwitterMedium与我保持联系。

还不是 Medium 会员?请使用这个链接来成为会员,因为在不增加你额外费用的情况下,我可以获得一小部分佣金。

线性回归、核技巧和线性核。

原文:towardsdatascience.com/linear-regression-kernel-trick-and-linear-kernel-39b6be3a3bf5

有时核技巧是无用的。

Yoann MocquinTowards Data Science Yoann Mocquin

·发表于 Towards Data Science ·8 分钟阅读·2023 年 11 月 5 日

--

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

在这篇文章中,我想展示一个起初对我来说并不明显的有趣结果,即:

线性回归和没有正则化的线性核岭回归是等价的。

实际上涉及了很多概念和技术,因此我们将逐一回顾每一个,最后综合使用它们来解释这一说法。

首先,我们将回顾经典的线性回归。然后我将解释核技巧和线性核是什么,最后我们将展示上述陈述的数学证明。

快速回顾经典的线性回归

线性回归的数学

经典的——普通最小二乘法(OLS)线性回归是以下问题:

其中:

  • Y 是一个长度为 n 的向量,包含线性模型的目标值

  • beta 是一个长度为 m 的向量:这是模型需要“学习”的未知量

  • X 是一个数据矩阵,具有 n 行和 m 列。我们通常说我们在 m 特征空间中记录了 n 个向量

所以目标是找到使平方误差最小化的 beta 值:

这个问题实际上有一个封闭形式的解,被称为普通最小二乘问题。解为:

一旦知道了解决方案,我们可以使用拟合模型计算给定新 X 值时的新 Y 值,使用:

线性回归的 Python 实现

让我们用 scikit-learn 验证我们的数学:这里有一个 Python 代码示例,展示了如何使用 sklearn 的线性回归器以及基于 numpy 的回归

%matplotlib qt
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

np.random.seed(0)
n = 100
X_ = np.random.uniform(3, 10, n).reshape(-1, 1)
beta_0 = 2
beta_1 = 2
true_y = beta_1 * X_ + beta_0
noise = np.random.randn(n, 1) * 0.5 # change the scale to reduce/increase noise
y = true_y + noise

fig, axes = plt.subplots(1, 2, squeeze=False, sharex=True, sharey=True, figsize=(18, 8))
axes[0, 0].plot(X_, y, "o", label="Input data")
axes[0, 0].plot(X_, true_y, '--', label='True linear relation')
axes[0, 0].set_xlim(0, 11)
axes[0, 0].set_ylim(0, 30)
axes[0, 0].legend()

# f_0 is a column of 1s
# f_1 is the column of x1
X = np.c_[np.ones((n, 1)), X_]

beta_OLS_scratch = np.linalg.inv(X.T @ X) @ X.T @ y
lr = LinearRegression(
    fit_intercept=False, # do not fit intercept independantly, since we added the 1 column for this purpose
).fit(X, y)

new_X = np.linspace(0, 15, 50).reshape(-1, 1)
new_X = np.c_[np.ones((50, 1)), new_X]
new_y_OLS_scratch = new_X @ beta_OLS_scratch 
new_y_lr = lr.predict(new_X)
axes[0, 1].plot(X_, y, 'o', label='Input data')
axes[0, 1].plot(new_X[:, 1], new_y_OLS_scratch, '-o', alpha=0.5,  label=r"OLS scratch solution")
axes[0, 1].plot(new_X[:, 1], new_y_lr, '-*', alpha=0.5, label=r"sklearn.lr OLS solution")
axes[0, 1].legend()
fig.tight_layout()
print(beta_OLS_scratch)
print(lr.coef_)
[[2.12458946]
 [1.99549536]]
[[2.12458946 1.99549536]]

如你所见,这两种方法给出了相同的结果(显然!):

核技巧

现在让我们回顾一种常见的技术,称为核技巧。

思路如下:

我们原始的问题(可以是分类或回归等)存在于输入数据矩阵 X 的空间中,形状为 m 特征空间中的 n 向量。有时,向量在这个低维空间中无法分离或分类,因此我们希望将输入数据转换到高维空间。我们可以手动创建新的特征来实现这一点。随着特征数量的增加,数值计算也会增加:想象一下计算两个长度为 10 亿的向量的点积,并对矩阵中的所有向量重复这一过程。

核技巧的核心在于使用精心设计的变换函数——通常记作 T 或 phi——它将长度为 m 的向量 x 转换为长度为 m'的新向量 x',使得我们的新数据确实具有高维,但计算负担保持在最低限度。

为了实现这一点,函数 phi 必须满足一些属性,以便新高维特征空间中的点积可以写成一个函数——即核函数——关于相应的输入向量:

这意味着高维空间中的内积可以表示为输入向量的函数。换句话说,我们可以仅使用低维向量来计算高维空间中的内积。这就是核技巧:我们可以利用高维空间的多样性,而无需实际在该空间中进行计算。

唯一的条件是我们只需要高维空间中的点积。

实际上,有一些强大的数学定理描述了创建这种变换 phi 和/或核函数的条件。

核函数示例

创建 m² 维空间的第一个核函数示例如下:

另一个例子:在核函数中添加常数会增加维度,生成新的特征,这些特征是缩放的输入特征:

另一个我们下面将使用的核是线性核:

因此,单位变换等同于使用一个在原始空间中计算内积的核函数。

实际上,还有很多其他有用的核函数,比如径向基(RBF)核或更通用的多项式核,它们创建高维且非线性的特征空间。为了完整性,这里有一个使用 RBF 核在线性回归背景下计算非线性回归的例子:

import numpy as np
from sklearn.kernel_ridge import KernelRidge
import matplotlib.pyplot as plt

np.random.seed(0)
X = np.sort(5 * np.random.rand(80, 1), axis=0)
y = np.sin(X).ravel()
y[::5] += 3 * (0.5 - np.random.rand(16))

# Create a test dataset
X_test = np.arange(0, 5, 0.01)[:, np.newaxis]

# Fit the KernelRidge model with an RBF kernel
kr = KernelRidge(
    kernel='rbf', # use RBF kernel
    alpha=1, # regularization 
    gamma=1, # scale for rbf
)
kr.fit(X, y)

y_rbf = kr.predict(X_test)

# Plot the results
fig, ax = plt.subplots()
ax.scatter(X, y, color='darkorange', label='Data')
ax.plot(X_test, y_rbf, color='navy', lw=2, label='RBF Kernel Ridge Regression')
ax.set_title('Kernel Ridge Regression with RBF Kernel')
ax.legend() 

线性回归中的线性核

如果变换 phi 将 x 转换为 phi(x),那么我们可以写出一个新的线性回归问题:

注意维度的变化:线性回归问题的输入矩阵从 [nxm] 变为 [nxm’],因此系数向量 beta 从长度 m 变为 m’。

这就是核技巧出现的地方:在计算解 beta’ 时,注意到 X’ 与其转置的乘积出现,这实际上是所有点积的矩阵,称为核矩阵,原因显而易见:

线性核化和线性回归

最后,让我们看看原始陈述的证明:使用线性核在线性回归中是无用的,因为它等同于标准线性回归。

线性核通常在支持向量机的背景下使用,但我想知道它在线性回归中会表现如何。

为了证明这两种方法是等价的,我们必须证明:

使用 beta 的第一种方法是原始线性回归,而使用 beta’ 的第二种方法是使用线性核化方法。

我们实际上可以使用上面看到的矩阵属性和关系来证明这一点:

我们可以再次使用 Python 和 scikit-learn 来验证这一点:

%matplotlib qt
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

np.random.seed(0)
n = 100
X_ = np.random.uniform(3, 10, n).reshape(-1, 1)
beta_0 = 2
beta_1 = 2
true_y = beta_1 * X_ + beta_0
noise = np.random.randn(n, 1) * 0.5 # change the scale to reduce/increase noise
y = true_y + noise

fig, axes = plt.subplots(1, 2, squeeze=False, sharex=True, sharey=True, figsize=(18, 8))
axes[0, 0].plot(X_, y, "o", label="Input data")
axes[0, 0].plot(X_, true_y, '--', label='True linear relation')
axes[0, 0].set_xlim(0, 11)
axes[0, 0].set_ylim(0, 30)
axes[0, 0].legend()

# f_0 is a column of 1s
# f_1 is the column of x1
X = np.c_[np.ones((n, 1)), X_]

beta_OLS_scratch = np.linalg.inv(X.T @ X) @ X.T @ y
lr = LinearRegression(
    fit_intercept=False, # do not fit intercept independantly, since we added the 1 column for this purpose
).fit(X, y)

new_X = np.linspace(0, 15, 50).reshape(-1, 1)
new_X = np.c_[np.ones((50, 1)), new_X]
new_y_OLS_scratch = new_X @ beta_OLS_scratch 
new_y_lr = lr.predict(new_X)
axes[0, 1].plot(X_, y, 'o', label='Input data')
axes[0, 1].plot(new_X[:, 1], new_y_OLS_scratch, '-o', alpha=0.5,  label=r"OLS scratch solution")
axes[0, 1].plot(new_X[:, 1], new_y_lr, '-*', alpha=0.5, label=r"sklearn.lr OLS solution")
axes[0, 1].legend()
fig.tight_layout()
print(beta_OLS_scratch)
print(lr.coef_)

总结

在这篇文章中,我们回顾了简单线性回归,包括问题及其解的矩阵公式。

然后我们看到核技巧是什么,以及它如何使我们能够从非常高维的空间中受益,而无需实际将我们的低维数据移动到这个计算密集型空间中。

最后,我展示了在线性回归的背景下线性核实际上是无用的,它相当于简单线性回归。

如果你考虑加入 Medium,请使用此链接快速订阅并成为我的推荐会员

[## 使用我的推荐链接加入 Medium - Yoann Mocquin

作为 Medium 的会员,你的一部分会员费用将分配给你阅读的作者,你可以完全访问每个故事……

medium.com

并订阅以在我发布新帖子时收到通知:

[## 每当我发布新内容时收到电子邮件!

每当我发布新内容时,你将收到一封电子邮件! 新发布将包括数据转换、高级绘图和模拟……

mocquin.medium.com

最后,你可以查看我其他一些帖子,涉及傅里叶变换、pandas 数据类型或数据科学中的线性代数技术:

pandas:处理你的数据类型!

在 pandas 中使用正确的数据类型对于干净的数据分析至关重要!这里是如何以及为什么。

傅里叶变换在时间序列中的去趋势处理

对时间序列进行去趋势处理可能会带来重大变化。

PCA/LDA/ICA:组件分析算法比较

复习这些著名算法的概念和区别。

PCA-白化与 ZCA-白化:numpy 2D 可视化

数据去白化的过程包括一种变换,使得变换后的数据具有单位矩阵作为…

300 倍速的有限差分法分辨率使用 numpy

有限差分法是一种强大的技术来解决复杂问题,而 numpy 使其变得快速!

有限差分法是一种强大的技术来解决复杂问题,而 numpy 使其变得快速!

线性回归 — 预测机器学习建模的奥卡姆剃刀

原文:towardsdatascience.com/linear-regression-occams-razor-of-predictive-machine-learning-modeling-f2ba5b144a2b

使用 Python 进行线性回归的机器学习建模

Farzad MahmoodinobarTowards Data Science Farzad Mahmoodinobar

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

--

水晶球,由 DALL.E 2

你熟悉奥卡姆剃刀吗?我记得在《生活大爆炸》电视剧中提到过它!奥卡姆剃刀的观点是,在其他条件相同的情况下,现象的最简单解释更可能是真实的,而不是更复杂的解释(即最简单的解决方案几乎总是最佳解决方案)。我认为机器学习预测建模中的奥卡姆剃刀就是线性回归,这几乎是使用的最简单建模方法,并且对于某些任务可能是最佳解决方案。本文将介绍线性回归的概念和实现。

与我的其他帖子类似,通过实践问题和答案来实现学习。我会在问题中提供提示和解释,以便让过程更简单。最后,我使用的创建此练习的笔记本也链接在文章底部,你可以下载、运行并跟随学习。

开始吧!

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

[## 通过我的推荐链接加入 Medium - Farzad Mahmoodinobar

阅读 Farzad 的每一个故事(以及 Medium 上其他作者的故事)。你的会员费直接支持 Farzad 和其他人…

medium.com](https://medium.com/@fmnobar/membership?source=post_page-----f2ba5b144a2b--------------------------------)

数据集

为了练习线性回归,我们将使用来自 UCI 机器学习库(CC BY 4.0)的汽车价格数据集。我已经清理了部分数据供我们使用,可以从 这个链接 下载。

我将解释我们在练习中使用的线性回归模型背后的数学。虽然理解这些数学内容不是成功理解本文内容的必要条件,但我建议你了解它,以便更好地理解创建线性回归模型时的幕后过程。

线性回归基础

线性回归是使用线性预测变量(或自变量)来预测因变量。一种简单的例子是直线公式:

在这种情况下,y 是因变量,而 x 是自变量(c 是一个常数)。线性回归模型的目标是确定最佳系数(上例中的 a),以使 x 能最准确地预测 y

现在,让我们将这个例子推广到多元线性回归。在多元线性回归模型中,目标是找到描述因变量与多个自变量之间关系的最佳拟合线。

在这种情况下,我们有多个自变量(或预测变量),从 x_1x_n,每个自变量都乘以它自己的系数,以预测因变量 y。在线性回归模型中,我们将尝试确定系数 a_1a_n 的值,以获得对因变量 y 的最佳预测。

现在我们了解了线性回归的概念,让我们转向普通最小二乘(OLS)回归,这是一种线性回归形式。

普通最小二乘回归

普通最小二乘回归模型通过最小化残差平方和来估计回归模型的系数。残差是回归线(即预测值)与实际值之间的垂直距离,如下图所示。这些残差被平方,以便错误不会相互抵消(当一个预测值高于实际值,而另一个预测值低于实际值时,这两个仍然是错误,不应相互抵消)。

普通最小二乘回归——回归线和残差

现在我们理解了基本概念,我们将开始探索数据和我们可能用来预测汽车价格的变量(或特征)。然后,我们将数据分为训练集和测试集,以建立回归模型。接下来,我们将查看回归模型的性能,最后绘制结果。

让我们开始吧!

1. 探索性分析

让我们首先查看数据,可以从这里下载。首先,我们将导入 Pandas 和 NumPy。然后,我们将读取包含数据集的 CSV 文件,并查看数据集的前五行。

# Import libraries
import pandas as pd
import numpy as np

# Show all columns/rows of the dataframe
pd.set_option("display.max_columns", None)
pd.set_option("display.max_rows", None)

# To show all columns in one view
from IPython.display import display, HTML
display(HTML("<style>.container { width:100% !important; }</style>"))

# Read the csv into a dataframe
df = pd.read_csv('auto-cleaned.csv')

# Display top five rows
df.head()

结果:

列名大多是显而易见的,所以我只会添加那些对我不太明显的列名。你现在可以忽略这些,并在需要列名定义时参考它们。

  • symboling: 保险公司根据车辆的风险感知分配的值。+3 表示车辆有风险,-3 表示车辆安全

  • aspiration: 标准或涡轮

  • drive-wheels: rwd 表示后驱;fwd 表示前驱;4wd 表示四轮驱动

  • wheel-base: 前后轮中心之间的距离,以厘米为单位

  • engine-type: dohc 表示双顶置凸轮;dohcv 表示双顶置凸轮和气门;l 表示 L 型发动机;ohc 表示顶置凸轮;ohcf 表示顶置凸轮和气门 F 型发动机;ohcv 表示顶置凸轮和气门;rotor 表示旋转发动机

  • bore: 汽缸的内径,以厘米为单位

  • stroke: 汽缸的运动

问题 1:

数据框中是否有缺失值?

回答:

df.info()

结果:

如我们所见,共有 25 列(注意列编号从 0 到 24)和 193 行。列中没有空值。

2. 特征选择

特征选择是识别和选择一组相关特征(也称为“预测变量”、“输入”或“属性”)以构建机器学习模型的过程。特征选择的目标是通过减少模型的复杂性和消除不相关、冗余或噪声特征,从而提高模型的准确性和可解释性。

问题 2:

创建一个表格,显示数据框中列之间的相关性。

回答:

我们将使用pandas.DataFrame.corr,它计算列之间的逐对相关性。需要考虑两个要点:

  1. pandas.DataFrame.corr将排除空值。我们确认我们的数据集不包含任何空值,但在处理含有空值的练习中,这可能很重要。

  2. 我们将仅限制数值的相关性,并将在后续练习中讨论分类值。

作为回顾,让我们在继续之前复习一下分类变量和数值变量是什么。

在机器学习中,分类变量是可以取有限数量值的变量。这些值表示不同的类别,值本身没有固有的顺序或数值意义。分类变量的例子包括性别(男性或女性)、婚姻状况(已婚、单身、离婚等)。

数值变量是可以在特定范围内取任意数值的变量。这些变量可以是连续的(意味着它们可以在特定范围内取任意值)或离散的(意味着它们只能取特定的、预定的值)。数值变量的例子包括年龄、身高、体重等。

处理完这些后,让我们计算相关性。

corr = np.round(df.corr(numeric_only = True), 2)
corr

结果:

问题 3:

在上一问题中生成了许多相关值。我们更关心与汽车价格的相关性。展示与汽车价格的相关性,并按从大到小的顺序排列。

答案:

price_corr = corr['price'].sort_values(ascending = False)
price_corr

结果:

这非常有趣。例如,“engine-size”似乎与价格的相关性最高,这在预期之中,而“compression-ratio”似乎与价格的相关性不那么高。另一方面,“symboling”,我们记得这是衡量汽车风险的指标,与汽车价格呈负相关,这也符合直觉。

问题 4:

为了更专注于相关特征以构建汽车价格模型,筛选出与价格相关性较弱的列,我们将定义任何相关性绝对值小于 0.2 的特征(这是为了本练习而任意选择的值)。

答案:

# Set the threshold
threshold = 0.2

# Drop columns with a correlation less than the threshold
df.drop(price_corr.where(lambda x: abs(x) < threshold).dropna().index, axis = 1, inplace = True)

df.info()

结果:

我们看到由于这个原因,我们现在剩下 19 个特征(总共有 20 列,但其中一个是价格本身,所以剩下 19 个特征或预测变量)。

问题 5:

现在我们有了更可管理的特征数量,重新查看一下它们,看看是否需要删除其中任何一个。

提示: 一些特征可能非常相似,可能是冗余的。有些可能实际上并不重要。

答案:

让我们查看数据框,然后查看剩余特征之间的相关性。

df.head()

结果:

# Calculate correlations
round(df.corr(numeric_only = True), 2)

结果:

“wheel-base”(前后轮之间的距离)和“length”(汽车的总长度)高度相关,似乎传达了相同的信息。此外,“city-mpg”和“highway-mpg”也高度相关,因此我们可以考虑删除其中一个。让我们继续删除“wheel-base”和“city-mpg”,然后再查看数据框的前五行。

# Drop the columns
df.drop(['wheel-base', 'city-mpg'], axis = 1, inplace = True)

# Return top five rows of the remaining dataframe
df.head()

结果:

正如我们所见,新的数据框更小,不包括我们刚刚删除的两个列。接下来,我们将讨论分类变量。

2.1. 虚拟编码

让我们更仔细地查看“make”和“fuel-type”列的值。

df['make'].value_counts()

结果:

df['fuel-type'].value_counts()

结果:

这两列是分类值(例如丰田或柴油),而不是数值。

为了将这些分类变量纳入我们的回归模型中,我们将为这些分类变量创建“虚拟编码”。

虚拟编码是将一个列中的分类变量(或预测变量)替换为多个二进制列。例如,假设我们有一个如表中所示的分类变量:

分类变量 — 虚拟编码前

如上表所示,“random_categorical_variable”可以有 A、B 和 C 三个分类值。我们希望通过虚拟编码将分类变量转换为一种更易于在回归模型中使用的格式,这将把它转换为 A、B 和 C 三个单独的列,并带有二进制值,如下所示:

分类变量 — 虚拟编码后

让我们看看如何在 Python 中实现虚拟编码。

问题 6:

对我们数据框的分类列进行虚拟编码。

答案:

让我们首先看看数据框在虚拟编码前的样子。

df.head()

结果:

从之前的问题我们知道,“fuel-type”列可以取 2 个不同的值(即汽油和柴油)。因此,在虚拟编码后,我们期望将“fuel-type”列替换为 2 个单独的列。其他分类列也是如此,具体取决于每个列有多少个唯一值。

首先,我们只对“fuel-type”列进行虚拟编码作为示例,并观察数据框的变化,然后我们可以继续对其他分类列进行虚拟编码。

# Dummy code df['fuel-type']
df = pd.get_dummies(df, columns = ['fuel-type'], prefix = 'fuel-type')

# Return top five rows of the updated dataframe
df.head()

结果:

正如预期的那样,我们现在有两个原始的“fuel-type”列,分别命名为“fuel-type_gas”和“fuel-type_diesel”。

接下来,让我们识别所有分类列并对其进行虚拟编码。

# Select "object" data types
columns = df.select_dtypes(include='object').columns

# Dummy code categorical columns
for column in columns:
    df = pd.get_dummies(df, columns = [column], prefix = column)

# Return top five rows of the resulting dataframe
df.head()

结果:

请注意,上述快照未覆盖虚拟编码后的所有列,因为现在我们有 63 列,这在快照中展示会显得太小。

最后,现在我们创建了所有这些新列,让我们重新创建价格与所有其他列之间的相关性,并从高到低进行排序。

# Re-create the correlation matrix
corr = np.round(df.corr(numeric_only = True), 2)

# Return correlation with price from highest to the lowest
price_corr = corr['price'].sort_values(ascending = False)
price_corr

结果:

如上所示,一些分类变量与价格有较高的相关性,例如“drive-wheels”和“num-of-cylinders”。

到此为止,我们已经对数据有所了解,并在一定程度上清理了数据,现在让我们继续进行创建模型以根据这些属性预测汽车价格的主要目标。

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

此时,我们将首先将数据拆分为因变量和自变量。因变量或“y”是我们要预测的内容,在这个练习中是“价格”。它被称为因变量,因为它的值依赖于自变量的值。自变量或“X”是目前数据框中所有其他变量或特征,包括“发动机尺寸”、“马力”等。

接下来,我们将数据拆分为训练集和测试集。顾名思义,训练数据集将用于训练回归模型,然后我们将使用测试集来测试模型的性能。我们拆分数据是为了确保模型在训练过程中看不到测试集,从而使测试集能够很好地代表模型的性能。将数据拆分为训练集和测试集很重要,因为使用相同的数据来拟合模型和评估其性能可能导致过拟合。过拟合发生在模型过于复杂时,它学习了数据中的噪声和随机波动,而不是潜在的模式。因此,模型可能在训练数据上表现良好,但在新见数据上表现不佳。

问题 7:

将因变量(目标)分配给 y,将自变量(或特征)分配给 X。

答案:

X = df.drop(['price'], axis = 1)
y = df['price']

问题 8:

将数据拆分为训练集和测试集。使用 30% 的数据作为测试集,并使用 random_state 为 1234。

答案:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 1234)

问题 9:

使用训练集训练线性回归模型。

答案:

from sklearn.linear_model import LinearRegression

# First create an object of the class
lr = LinearRegression()

# Now use the object to train the model
lr.fit(X_train, y_train)

# Let's look at the coefficients of the trained model
lr.coef_

结果:

我们将讨论这里发生了什么,但首先让我们看看如何评估机器学习模型。

4. 模型评估

4.1. R²

问题 10:

训练模型的得分是多少?

答案:

为此,我们可以使用 LinearRegression 的 “score()” 方法,它返回预测的决定系数或 R²$,计算如下:

最好的得分是 1.0. 一个始终预测“y”期望值的常数模型,无论输入特征如何,都将得到 R² 分数 0.0。

了解这些知识后,我们来看看实现过程。

score = lr.score(X_train, y_train)

print(f"Training score of the model is {score}.")

结果:

问题 11:

预测测试集的值,然后评估训练模型在测试集上的表现。

答案:

# Predict y for X_test
y_pred = lr.predict(X_test)

score_test = lr.score(X_test, y_test)
print(f"Test score of the trained model is {score_test}.")

结果:

4.2. 均方误差

均方误差(Mean Squared Error, MSE)是平方误差的平均值,计算如下:

问题 12:

计算测试集预测结果的均方误差(Mean Squared Error)和决定系数(R²)。

答案:

from sklearn.metrics import mean_squared_error, r2_score

print(f"R²: {r2_score(y_pred, y_test)}")
print(f"MSE: {mean_squared_error(y_pred, y_test)}")

结果:

问题 13:

你如何解读前一个问题的结果?对下一步有什么建议?

答案:

R² 相对较高,但均方误差(MSE)也很高,这可能表明误差可能过大——注意这实际上取决于业务需求以及模型的用途。在某些情况下,90.6% 的 R² 可能足够满足业务需求,而在其他情况下,这个数字可能还不够好。这种性能水平可能受到一些不是强预测价格的特征的影响。让我们看看能否识别出哪些特征不是强预测因子并将其删除。然后我们可以重新训练并再次查看评分,以查看是否能够改善我们的模型。

为了尝试一些新的方法,我们将使用 statsmodels 库中的普通最小二乘法(OLS)。训练和预测测试集值的步骤与之前相同。

# Import libraries
import statsmodels.api as sm

# Initialize the model
sm_model = sm.OLS(y_train, X_train).fit()

# Create the predictions
sm_predictions = sm_model.predict(X_test)

# Return the summary results
sm_model.summary()

结果:

这个方法提供了特征的良好展示和该特征显著性的 p 值测量。例如,如果我们使用 0.05 或 5% 的显著性水平(或 95% 的置信水平),我们可以排除那些“P > |t|”大于 0.05 的特征。

len(sm_model.pvalues.where(lambda x: x > 0.05).dropna().index)

结果:

43

共有 43 列这样的数据。让我们删除这些列,看看结果是否有所改善。

# Create a list of columns that meee the criteria
columns = list(sm_model.pvalues.where(lambda x: x > 0.05).dropna().index)

# Drop those columns
df.drop(columns, axis = 1, inplace = True)

# Revisit the process to create a new model summary
X = df.drop(['price'], axis = 1)
y = df['price']

# Split data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 1234)

# Train the model
sm_model = sm.OLS(y_train, X_train).fit()

# Create predictions using the trained model
sm_predictions = sm_model.predict(X_test)

# Return model summary
sm_model.summary()

结果:

根据 R 平方值判断,整体性能从 0.967 提升至 0.972,并且我们减少了列数,这使得我们的模型和分析更加高效。

5. 预测值与实际值的图

问题 14:

创建预测值与实际值的散点图。如果所有预测值都与实际值相匹配,我们会期望所有点都沿着一条直线如 f(x) = x 分布。为比较添加一条红色的直线。

答案:

# Import libraries
import matplotlib.pyplot as plt
%matplotlib inline

# Define figure size
plt.figure(figsize = (7, 7))

# Create the scatterplot
plt.scatter(y_pred, y_test)
plt.plot([y_pred.min(), y_pred.max()], [y_pred.min(), y_pred.max()], color = 'r')

# Add x and y labels
plt.xlabel("Predictions")
plt.ylabel("Actuals")

# Add title
plt.title("Predictions vs. Actuals")
plt.show()

结果:

训练模型的预测值与实际值的散点图

正如我们预期的那样,值围绕直线散布,显示出模型产生了较好的预测水平。点在红线的右侧,意味着模型预测的价格高于实际价格,而在红线左侧的点则表示相反的情况。

带有练习问题的笔记本

以下是包含问题和答案的笔记本,您可以下载并进行练习。

结论

在这篇文章中,我们讨论了在某些情况下,最简单的解决方案可能是最合适的解决方案,并介绍了如何将线性回归作为预测机器学习任务中的一种解决方案进行实现。我们首先学习了线性回归背后的数学原理,然后实现了一个模型来根据现有的汽车属性预测汽车价格。接着,我们测量了模型的性能,并采取了某些措施以提高模型的性能,最后通过散点图可视化了训练模型预测值与实际值的比较。

感谢阅读!

如果你觉得这篇文章对你有帮助,请 关注我的 Medium 并订阅以接收我的最新文章!

将线性回归应用于 GPT 的七个步骤

原文:towardsdatascience.com/linear-regression-to-gpt-in-seven-steps-cb3ab3173a14?source=collection_archive---------7-----------------------#2023-04-25

这项朴素的预测方法如何指引我们进入生成式 AI 的世界

Devesh RajadhyaxTowards Data Science Devesh Rajadhyax

·

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

--

关于生成式 AI 的写作非常多。有专门讨论其应用、伦理道德问题以及对人类社会风险的文章。如果你想了解技术本身,有从原始研究论文到入门文章和视频的各种材料可供选择。根据你当前的水平和兴趣,你可以找到合适的学习资源。

这篇文章是为特定读者群体写的。这些读者已经学习了机器学习,但不是作为主修科目。他们知道预测和分类是涵盖大多数应用的 ML 的两个主要用例。他们还学习了常见的预测和分类机器学习算法,如线性回归、逻辑回归、支持向量机、决策树以及一些神经网络。他们可能用 Python 和 scikit-learn 等库编写过一些小项目,甚至使用过一些预训练的 TensorFlow 模型,如 ResNet。我认为许多学生和专业人士能够与这种描述产生共鸣。

对这些读者来说,生成式 AI 是一种新的 ML 用例吗?它似乎确实不同于预测和分类。流行的术语如变换器、多头注意力、大型语言模型、基础模型、序列到序列和提示工程,容易让人觉得这与我们熟悉的预测-分类世界截然不同。

本文的核心观点是生成式 AI 只是预测的一个特例。如果你符合我之前描述的 ML 爱好者的特点,那么你可以在七个简单的步骤中理解生成式 AI 的基本工作原理。我从线性回归(LinReg)开始,这是每个人都知道的 ML 技术。在本文中,我探讨了生成式 AI 的一个特定分支——大型语言模型(LLM),主要是因为广受欢迎的 ChatGPT 就属于这个分支。

图片来自 Rajashree Rajadhyax

第 1 步:通过线性回归进行预测

LinReg 确定代表给定数据点的最佳直线。一旦找到这条直线,就用它来预测新输入的输出。

图片来自作者

我们可以将 LinReg 模型写成一个数学函数。以易于理解的方式写出来,它看起来像:

new output = Line Function (new input)

我们也可以为其绘制一个示意图:

这是最基本的预测。LinReg 模型‘学习’最佳直线并用它进行预测。

第 2 步:通过神经网络进行预测

只有在知道数据适合直线时,你才可以使用 LinReg。这通常对单输入单输出问题很容易做到。我们可以简单地绘制图表并直观检查。但在大多数现实问题中,有多个输入。我们无法可视化这样的图表。

此外,现实世界的数据并不总是遵循线性路径。许多时候,最佳拟合形状是非线性的。见下图:

图片来自作者

在多维数据中学习这样一个函数不可能通过简单的方法如 LinReg 来实现。这就是神经网络(NN)发挥作用的地方。NN 不需要我们决定它们应该学习哪个函数。它们自己找到并学习函数,无论它多么复杂。一旦 NN 学习了复杂的、多输入的函数,它们就会使用这个函数进行预测。

我们可以再次写出类似的方程,但有所变化。我们的输入现在是多个,因此我们必须用一个向量来表示它们。实际上,输出也可以是多个,我们也会为它们使用一个向量。

output vector = NN Function (input vector)

我们将绘制这种新的、更强大预测的示意图:

作者的图片

步骤 3:预测一个单词

现在考虑我们有一个问题,输入到神经网络中的是某种语言中的一个单词。神经网络只能接受数字和向量。为了适应这一点,单词被转换为向量。你可以把它们想象成是多维空间中的居民,相关的单词彼此靠近。例如,‘Java’的向量将接近其他编程技术的向量;但它也会接近远东地区的地方,如苏门答腊的向量。

作者的(非常虚构的)单词嵌入

这样一组与语言中单词对应的向量称为‘嵌入’。有许多方法可以创建这些嵌入;Word2Vec 和 GloVe 是两个流行的例子。这些嵌入的典型大小为 256、512 或 1024。

一旦我们有了单词的向量,我们可以在它们上使用 NN 进行预测。但是通过对单词进行预测,我们能实现什么呢?我们可以做很多事情。我们可以将一个单词翻译成另一种语言,找到该单词的同义词或找到它的过去式。这个预测的方程式和示意图看起来与步骤 3 非常相似。

output word embedding = NN Function (input word embedding)

作者的图片

步骤 4:简单翻译的预测

在翻译问题中,输入是一种语言的句子,输出是另一种语言的句子。我们如何利用对单词预测的已有知识来实现翻译呢?这里我们采用一种简单的翻译方法。我们将输入句子的每个单词转换为另一种语言中的等效单词。当然,真正的翻译不会像这样工作;但在这一步骤中,假装它会。

这次我们先画出示意图:

作者的图片

第一个单词的方程式将是:

NN Translation Function (Embeddings for word. no 1 in input sentence) 
= Embedding for word no.1 in output sentence

我们可以类似地为其他单词写出方程式。

此处使用的神经网络通过查看许多单词对的示例来学习翻译功能。我们为每个单词使用一个这样的神经网络。

因此,我们有了一个使用预测的翻译系统。我已经承认这是一种幼稚的翻译方法。有哪些改进可以使其在现实世界中有效?我们将在接下来的两步中看到。

步骤 5: 使用上下文的预测

幼稚方法的第一个问题是,一个词的翻译依赖于句子中的其他词。例如,考虑下面的英语到印地语的翻译:

输入(英语): ‘Ashok sent a letter to Sagar’

输出(印地语): ’Ashok ne Sagar ko khat bheja’。

词‘sent’在输出中被翻译为‘bheja’。然而,如果输入句子是:

输入(英语): ‘Ashok sent sweets to Sagar’

然后,相同的词被翻译为‘bheji’。

输出(印地语): ’Ashok ne Sagar ko mithai bheji’。

因此,在预测输出时,必须添加句子中其他词的上下文。我们只为一个词绘制示意图:

图片由作者提供

生成上下文的方法有很多种。最强大、最先进的方法称为‘注意力’。使用注意力进行上下文生成的神经网络称为‘变换器’。Bert 和 GPT 就是变换器的例子。

我们现在有了一种使用上下文的预测方法。我们可以将方程式写成:

NN Translation Function (Embeddings for word. no 1 in input sentence 
+ context from other words in input sentence) 
= Embedding for word no.1 in output sentence

步骤 6: 下一个词的预测

我们现在将处理幼稚翻译方法中的第二个问题。翻译不是单词的一对一映射。请参见前一步的例子:

输入(英语): ‘Ashok sent a letter to Sagar’

输出(印地语): ’Ashok ne Sagar ko khat bheja’。

你会注意到词的顺序不同,输入中没有‘a’的对应词,也没有输出中的‘ne’。我们每个词一个神经网络的方法在这种情况下不起作用。事实上,在大多数情况下它都不起作用。

幸运的是,已经有了更好的方法。在将输入句子提供给神经网络(NN)之后,我们要求它仅预测一个词,即将作为输出句子第一个词的词。我们可以将其表示为:

图片由作者提供

在我们发送信件的例子中,我们可以将其写为:

NN Translation Function (Embeddings for 'Ashok sent a letter to Sagar' 
+ context from input sentence) 
= Embedding for 'Ashok'

为了获取输出中的第二个词,我们将输入更改为:

Input = Embeddings for input sentence + Embedding for first word in output

我们还必须在上下文中包含这一新的输入:

Context = Context from input sentence + context from first word in output

神经网络将预测句子的下一个(第二个)词:

这可以写成:

NN Translation Function 
(Embeddings for 'Ashok send a letter to Sagar + Embedding for 'Ashok', 
Context for input sentence + context for 'Ashok') 
= Embedding for 'ne'

我们可以继续这个过程,直到神经网络预测出‘.’的嵌入,换句话说,它会发出输出已经结束的信号。

我们因此将翻译问题简化为‘预测下一个词’的问题。在下一步中,我们将看到这种翻译方法如何引导我们走向更通用的生成 AI 能力。

步骤 7: 生成 AI 的预测

‘下一个词的预测’方法不限于翻译。神经网络可以被训练来预测下一个词,使输出成为对问题的回答,或是你指定主题的文章。想象一下,输入到这样的生成型神经网络中的句子是:

‘写一篇关于全球变暖对喜马拉雅冰川影响的文章’。

生成模型的输入被称为‘提示’。生成型神经网络预测文章的第一个词,然后继续预测后续的词,直到生成整篇漂亮的文章。这就是大型语言模型如 ChatGPT 所做的。正如你可以想象的那样,这些模型的内部机制远比我在这里描述的要复杂。但它们包含了我们看到的相同基本组件:嵌入、注意力和下一个词预测神经网络。

作者提供的图片

除了翻译和内容生成,LLMs 还可以回答问题、规划旅行和做许多其他奇妙的事情。所有这些任务中使用的基本方法仍然是预测下一个词。

摘要

我们从使用 LinReg 的基本预测技术开始。我们通过添加向量和词嵌入使预测问题变得更加复杂。我们通过解决简单的翻译问题学会了如何将预测应用于语言。在增强简单方法以处理真实翻译的过程中,我们熟悉了 LLMs 的基本元素:上下文和下一个词预测。我们意识到大型语言模型的核心是对文本序列的预测。我们熟悉了如提示、嵌入、注意力和变换器等重要术语。

序列不一定非得是文本。我们可以使用任何序列,例如图片或声音。本文的信息涵盖了生成型人工智能的全部内容,即:

生成型人工智能是利用神经网络对序列进行预测。

线员静止性

原文:towardsdatascience.com/lineman-stationarity-ffacde2cfc8

一种数据驱动的进攻线员指标

哈里森·霍夫曼Towards Data Science 哈里森·霍夫曼

·发表于 Towards Data Science ·阅读时长 6 分钟·2023 年 2 月 6 日

--

由进攻线创造的口袋是传球游戏中的关键元素,因为四分卫需要一个干净的口袋来准确及时地传球。一个好的口袋为四分卫提供了足够的空间和时间来移动和传球,而一个差的口袋则会迅速塌陷,迫使四分卫逃跑或被擒杀(除非四分卫是帕特里克·马霍姆斯)。虽然容易想象出好的或差的口袋,但量化其质量,更普遍地说,量化线员的表现并不简单。

本文介绍了一种新的指标——“线员静止性”,该指标测量线员从球被传递到下一个事件发生之间的移动距离。线员静止性可以在每次进攻中计算,对评估进攻线员的表现和疲劳程度非常有用。通过跟踪每次进攻中线员的移动和负担,教练和分析师可以利用这一指标获得关于比赛中进攻线整体效果的洞察。所有指标和可视化图表均使用了2023 NFL 大数据碗的数据生成。

什么是线员静止性?

线员静止性是指在球被传递后,进攻线员从起始位置移动的距离。它的计算方式是球被传递的瞬间与下一个事件发生时的位置之差。在这个上下文中,事件指的是以下之一:

  1. 球被传球

2. 四分卫持球跑动

3. 四分卫被擒杀

4. 球被交接

5. 球被掉落

更正式地说,设 Xs 为球被传递时线员所在的码线位置,Xe 为下一个事件发生时线员所在的码线位置。线员静止性定义为 Xe−Xs。

不太复杂,是吧?尽管这是一个简单的指标,但我们会看到线卫稳定性在评估线卫表现方面非常有效。特别是,我们会看到更高的线卫稳定性与更好的团队和球员表现相关。

以下是线卫稳定性计算的动画。5 个蓝色点代表进攻线上的每个球员,棕色/红色点是足球。足球被传出的时长黄线的长度表示每个进攻线员的线卫稳定性。

线卫稳定性的可视化表现。图像由作者提供。

在这次传球中,右侧护锋的稳定性最低,约为-8 码,离开起始位置。中锋的稳定性最高,大约为-4.5 码。重要的是要注意,线卫稳定性只反映了纵向的场地移动,而没有考虑横向运动。这可以从中锋向左边护锋和护锋移动的过程中看出,但他的稳定性仍然很低。

为什么线卫稳定性是一个有用的指标?

线卫稳定性衡量的是线卫保持原地并为四分卫提供足够空间和时间执行战术的能力。我们首先注意到的是,线卫稳定性在允许擒杀的球员和没有允许擒杀的球员之间差异很大:

线卫稳定性分布——擒杀与未擒杀。图像由作者提供。

我们清楚地看到,线卫稳定性在允许擒杀时通常较低。2021 赛季首 8 周这两种分布的 Kolmogorov-Smirnov (KS) 统计量为 0.575,p 值为 1.05e-95(基本为 0)。这在统计上是显著的,并且对 KS 统计量的影响很大。

此外,平均线卫稳定性在不同的传球结果之间变化显著(使用 2021 年首 8 周的数据):

图像由作者提供。

这 5 组均值的单因素 ANOVA F 统计量为 275.19,p 值为 5.27e-234(基本为 0)。由此可见,平均线卫稳定性在传球结果较差时倾向于下降。即,完成传球的线卫稳定性最高(-3.78 码),未完成传球的线卫稳定性第二高(-4.13 码),而擒杀和拦截则具有最低的线卫稳定性。

关于线卫稳定性的另一个引人注目的观察是,它似乎可以洞察线卫疲劳情况。考虑以下图表,描绘了每场比赛中球队 5 名线卫在每次传球时的平均稳定性。垂直的红线表示发生了擒杀的进攻。

在进攻线员稳定性下降期间出现擒抱的示例。图片由作者提供。

在进攻线员稳定性下降期间出现擒抱的示例。图片由作者提供。

在进攻线员稳定性下降期间出现擒抱的示例。图片由作者提供。

在所有三个图表中,擒抱发生在进攻线员稳定性下降的趋势中。例如,在第一个折线图中,进攻方在比赛的第 25 次传球进攻中允许了一个擒抱。在擒抱之前,我们观察到平均进攻线员稳定性出现了明显的下降趋势(如果进攻线员稳定性的指数加权滚动平均差异为负,则我们说当前进攻线员稳定性处于下降趋势)。令人惊讶的是,在 2021 赛季的第 1 至第 8 周,约 85.29%的擒抱发生在进攻线员稳定性下降的趋势中。这可能表明进攻线员感到疲惫,从而在每次进攻中让出更多空间,最终导致擒抱。因此,分析整个进攻线的进攻线员稳定性以及单个进攻线员的稳定性,可以为我们提供直接的耐力、力量和疲劳指标。

当我们按周分析单个球员的进攻线员稳定性时,尤其是当球员受伤时,另一个趋势会出现。根据www.nfl.com/injuries/league/2021/REG9,布法罗比尔队的护卫乔恩·费利西亚诺在第 9 周因小腿受伤缺席。他在第 4 周也因脑震荡缺席。以下是他参与的每周进攻线员稳定性:

进攻线员稳定性可能具有预测伤病的能力。图片由作者提供。

在费利西亚诺的脑震荡之后以及他小腿受伤之前,我们可以清楚地看到他的进攻线员稳定性分布出现了明显的负面变化。更具体地说,我们可以看到他在脑震荡后的所有周的平均进攻线员稳定性都低于脑震荡前的周。

进攻线员稳定性排名

现在我们已经看到了进攻线员稳定性为何是一个有用指标的一些原因,让我们看看每个位置的球员排名最高。以下图表显示了 2021 赛季前 8 周中,至少参与了 100 次传球进攻的进攻线员的前 10 名平均稳定性。首先是中锋:

中锋进攻线员稳定性排名。图片由作者提供。

然后是护卫:

护卫进攻线员稳定性排名。图片由作者提供。

还有截锋:

截锋进攻线员稳定性排名。图片由作者提供。

这些进攻线员中的许多人非常知名,并被广泛认为是顶尖中的顶尖。其他排名接近前列的进攻线员可能会让人感到意外。这可能表明,进攻线员的稳定性揭示了每个进攻线员在进攻线上的未被认识的优点和缺点。

最后想法

在这篇文章中,我们引入了一种新的指标——“进攻线员稳定性”,用于评估进攻线员的表现。这个指标基于这样一个理念:进攻线员维持静止位置的能力,即在没有移动的情况下保持原地,是决定传球成功的重要因素。我们发现,进攻线员的稳定性与其整体表现以及疲劳程度密切相关。尽管这不是一个完美的指标,但它确实提供了一种独特、易于解释且具有预测性的方式来评估进攻线员的表现。

成为会员: https://harrisonfhoffman.medium.com/membership

喜欢我的文章?请请我喝杯咖啡: https://www.buymeacoffee.com/HarrisonfhU

使用 Python 进行语言指纹分析

原文:towardsdatascience.com/linguistic-fingerprinting-with-python-5b128ae7a9fc

使用标点热图进行作者归属分析

Lee VaughanTowards Data Science Lee Vaughan

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

--

单一法医学指纹,黄色调与蓝色分号(图片来源:DALL-E2 和作者)

文体计量学是通过计算文本分析对文学风格进行定量研究的学科。它基于这样的观点:我们每个人在写作中都有一种独特、一致且易于识别的风格。这包括我们的词汇、标点符号的使用、单词和句子的平均长度等等。

文体计量学的一个典型应用是作者归属分析。这是一种识别文件作者的过程,比如在调查抄袭或解决历史文件来源争议时。

在这个快速成功数据科学项目中,我们将使用 Python、seaborn 和自然语言工具包(NLTK)来检验亚瑟·柯南·道尔是否在他的小说《失落的世界》中留下了语言指纹。更具体地说,我们将利用分号来确定亚瑟·柯南·道尔还是他的同时代人 H.G. 威尔斯更可能是该书的作者。

《猎犬》、《战争》与《失落的世界》

亚瑟·柯南·道尔(1859–1930)最著名的是福尔摩斯系列故事。H.G. 威尔斯(1866–1946)因几部开创性的科幻小说而闻名,如隐形人

1912 年,《Strand Magazine》出版了失落的世界,这是一个科幻小说的连载版本。虽然它的作者是已知的,但让我们假设它存在争议,我们的任务是解开这个谜团。专家们将范围缩小到两位作者:道尔和威尔斯。威尔斯略微被倾向,因为失落的世界是一部科幻小说,并且包含了类似于他 1895 年书中莫洛克人的穴居人。

为了解决这个问题,我们需要每位作者的代表作品。对于道尔,我们将使用巴斯克维尔的猎犬,它出版于 1901 年。对于威尔斯,我们将使用世界大战,它出版于 1898 年。

对我们来说幸运的是,所有三部小说都在公共领域,并可以通过古腾堡计划获得。为了方便,我已将它们下载到这个Gist并删除了版权信息。

过程

作者归属分析需要应用自然语言处理(NLP)。NLP 是语言学和人工智能的一个分支,旨在赋予计算机从书面和口头语言中提取意义的能力。

最常见的 NLP 作者分析测试会分析文本的以下特征:

  • 词语/句子长度: 文档中词语或句子长度的频率分布图。

  • 停用词: 停用词的频率分布图(短的、无语境的功能词,如thebutif)。

  • 词性: 根据词汇的句法功能(如名词和动词)绘制的频率分布图。

  • 最常见的词汇: 比较文本中最常用的词汇。

  • Jaccard 相似性: 用于衡量样本集的相似性和多样性的统计量。

  • 标点符号: 比较逗号、冒号、分号等的使用情况。

对于这个玩具项目,我们将专注于标点测试。具体来说,我们将关注分号的使用。作为“可选”的语法元素,它们的使用具有潜在的独特性。它们也不受书籍类型的影响,不像问号在侦探小说中可能比在经典科幻小说中更为丰富。

高级过程将是:

  1. 将书籍加载为文本字符串,

  2. 使用 NLTK 对每个单词和标点进行标记(拆分),

  3. 提取标点符号,

  4. 将分号的值设为 1,所有其他标点的值设为 0,

  5. 创建数值的 2D NumPy 数组,

  6. 使用 seaborn 将结果绘制为热图

我们将使用每个热图作为数字“指纹”。比较已知书籍的指纹与未知书籍(失落的世界)的指纹,希望能够暗示出作者之间的不同。

问号使用的热图(蓝色),在侦探小说(左)与科幻小说(右)中(作者提供的图像)

自然语言工具包

多个第三方库可以帮助你用 Python 进行 NLP。这些库包括 NLTK、spaCy、Gensim、Pattern 和 TextBlob。

我们将使用NLTK,它是最古老、最强大且最受欢迎的工具之一。它是开源的,适用于 Windows、macOS 和 Linux。它还配备了详细的文档

安装库

使用 Anaconda 安装 NLTK:

conda install -c anaconda nltk

使用 pip 安装:

pip install nltk(或参见 www.nltk.org/install.html

我们还需要 seaborn 来进行绘图。基于 matplotlib,这个开源可视化库提供了一个更易于使用的界面来绘制吸引人且信息丰富的统计图表,如条形图、散点图、热图等。以下是安装命令:

conda install seaborn

pip install seaborn

代码

以下代码在 JupyterLab 中编写,并且由单元格描述。

导入库

在导入模块中,我们需要string模块中的标点符号列表。我们将把这个列表转化为set数据类型,以便更快地进行搜索。

import math
from string import punctuation
import urllib.request

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import seaborn as sns

import nltk

PUNCT_SET = set(punctuation)

定义一个加载文本文件的函数

由于我们将处理多个文件,我们将首先定义一些可重用的函数。第一个函数使用 Python 的urllib库打开存储在 URL 处的文件。第一行打开文件,第二行将其读取为字节数据,第三行将字节转换为字符串(str)格式。

def text_to_string(url):
    """Read a text file from a URL and return a string."""
    with urllib.request.urlopen(url) as response:
        data = response.read()  # in bytes
        txt_str = data.decode('utf-8')  # converts bytes to string
        return txt_str

定义一个分词函数

下一个函数接受一个字典,其中键是作者的名字,值是他们以字符串格式呈现的小说。然后,它使用 NLTK 的word_tokenize()方法将字符串重新组织为令牌

Python 将字符串视为字符的集合,如字母、空格和标点符号。分词过程将这些字符分组为其他元素,如单词(或句子)和标点符号。每个元素称为令牌,这些令牌允许使用复杂的 NLP 分析。

该函数通过返回一个新的字典来结束,其中作者的名字作为键,标点符号令牌作为值。

def make_punct_dict(author_book_dict):
    """Accept author/text dict and return dict of punctuation by author."""
    punct_by_author = {}
    for author, text in author_book_dict.items():
        tokens = nltk.word_tokenize(text)
        punct_by_author[author] = [token for token in tokens 
                                   if token in PUNCT_SET]
        print(f"Number punctuation marks in {author} = {len(punct_by_author[author])}")
    return punct_by_author

我应该在这里指出,可以使用基本的 Python 来搜索文本文件中的分号并完成这个项目。然而,分词带来了两个好处。

首先,NLTK 的默认分词器(word_tokenize())不计算用在缩写或所有格中的撇号,而是将其视为两个单词,撇号附在第二个单词上(如I + ‘ve)。这符合语法用法

分词器还将特殊标记(如双破折号(— —)或省略号(…))视为单一标记,如作者所意图,而不是作为单独的标记。使用基本 Python 时,每个重复的标记都会被计数。

这可能会产生显著影响。例如,使用 NLTK 计算失落的世界的总标点符号数量为 10,035,而使用基本 Python 则为 14,352!

其次,所有其他 NLP 归因测试都需要使用分词,因此在这里使用 NLTK 让你有能力在以后扩展分析。

定义一个将标点符号转换为数字的函数

Seaborn 的heatmap()方法需要数值数据作为输入。以下函数接受按作者分类的标点符号令牌字典,将分号赋值为1,其他所有赋值为0,并返回list数据类型。

def convert_punct_to_number(punct_by_author, author):
    """Return list of punctuation marks converted to numerical values."""
    heat_vals = [1 if char == ';' else 0 for char in punct_by_author[author]]
    return heat_vals

定义一个函数来查找下一个最低的平方值

热值列表是一维的,但我们希望将其绘制为二维。为此,我们将列表转换为正方形 NumPy 数组。

这必须是一个真正的正方形,并且每个列表中的样本数量不太可能有一个整数作为其平方根。因此,我们需要计算每个列表长度的平方根,将其转换为整数,并进行平方。得到的值将是可以作为真正的正方形显示的最大长度

def find_next_lowest_square(number):
    """Return the largest perfect square less than or equal to the given number."""
    return int(math.sqrt(number)) ** 2

加载和准备数据以进行绘图

定义好函数后,我们准备应用它们。首先,我们将每本书的文本数据加载到一个名为strings_by_author的字典中。对于失落的世界,我们将作者的名字设置为“未知”,因为这代表一个来源不明的文档。

然后,我们将其转换为一个名为punct_by_author的标点字典。从这个字典中,我们找到每个列表的下一个最低平方值,并选择最小值继续前进。这确保了所有数据集都可以转换为正方形数组,并且还规范化了每个数据集的样本数量(例如,失落的世界有 10,035 个标点符号标记,而巴斯克维尔的猎犬只有 6,704 个)。

war_url =  'https://bit.ly/3QnuTPX'
hound_url = 'https://bit.ly/44Gdc2a'
lost_url = 'https://bit.ly/3QhTfKJ'

# Load text files into dictionary by author:
strings_by_author = {'wells': text_to_string(war_url),
                     'doyle': text_to_string(hound_url),
                     'unknown': text_to_string(lost_url)}

# Tokenize text strings preserving only punctuation marks:
punct_by_author = make_punct_dict(strings_by_author)

# Find the largest square that fits all datasets:
squarable_punct_sizes = [find_next_lowest_square(len(punct_by_author[author])) 
                         for author in punct_by_author]
perfect_square = min(squarable_punct_sizes)
print(f"Array size for perfect square: {perfect_square}\n")

未来使用一个包含 6,561 个标记的数组意味着每个标点符号列表将在绘图前被截断至长度 6,561。

绘制热图

我们将使用一个for循环为每位作者的标点符号绘制热图。第一步是将标点符号标记转换为数字。记住,分号将用1表示,其他所有标点用0表示。

接下来,我们使用perfect_square值和 NumPy 的array()reshape()方法来截断heat列表并将其转换为正方形 NumPy 数组。此时,我们只需设置一个 matplotlib 图形并调用 seaborn 的heatmap()方法。循环将为每个作者绘制一个单独的图形。

分号将被标记为蓝色。你可以通过改变cmap参数中的颜色顺序来反转这一点。如果你不喜欢黄色和蓝色,也可以输入新的颜色。

# Convert punctuation marks to numerical values and plot heatmaps:
for author in punct_by_author:
    heat = convert_punct_to_number(punct_by_author, author)
    arr = np.array(heat[:perfect_square]).reshape(int(math.sqrt(perfect_square)), 
                                                  int(math.sqrt(perfect_square)))
    fig, ax = plt.subplots(figsize=(5, 5))
    sns.heatmap(arr,
                cmap=ListedColormap(['yellow', 'blue']),
                cbar=False,
                xticklabels=False,
                yticklabels=False)
    ax.set_title(f'Heatmap Semicolons: {author.title()}')
    plt.show();

结果

从之前的图表中应该可以清楚地看出——严格依据分号的使用——Doyle 是失落的世界最可能的作者。

这些结果的可视化展示非常重要。例如,每本书的总分号数如下:

Wells: 243

Doyle: 45

Unknown: 103

作为总标点符号的一部分,它们的比例如下:

Wells: 0.032

Doyle: 0.007

Unknown: 0.010

从这些文本数据来看,你是否立即能判断出“未知”作者很可能是道尔?试想一下如果把这些呈现给陪审团,陪审员会更被情节还是文本打动?没有什么能比可视化更有效地传达信息了!

当然,要对作者身份进行稳健的判断,你需要运行之前列出的所有NLP 归属测试,并使用所有的道尔和威尔斯的小说。

谢谢

感谢阅读,未来请关注我的更多快速成功数据科学项目。如果你想看到对该数据集进行更全面的 NLP 测试,请参阅我书中的第二章,真实世界的 Python:黑客解决代码问题的指南

如何使用 Python 列出所有 BigQuery 数据集和表

原文:towardsdatascience.com/list-bigquery-ds-tables-python-b92063ad0be3

使用 BigQuery API 和 Python 程序化列出所有数据集和表

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

·发布在 Towards Data Science ·5 分钟阅读·2023 年 5 月 15 日

--

图片由 Joanna Kosinska 提供,来源于 Unsplash

BigQuery 是 Google Cloud Platform 上的托管数据仓库服务,允许用户存储、管理和查询数据。数据和/或分析工程的一个重要部分是某些任务的自动化,包括与 BigQuery 的一些交互。这些自动化通常需要我们利用 BigQuery API,使开发人员能够以编程方式与 GCP 上的服务进行交互。

在今天的文章中,我们将演示如何使用 BigQuery API 和 Google Python 客户端以编程方式获取单个 GCP 项目下的所有表和数据集。

前提条件

为了跟随本教程中的步骤,确保安装 Google 提供的 Python 客户端库。为此,我们需要通过 pip 安装 google-cloud-bigquery

$ python3 -m pip install --upgrade google-cloud-bigquery

此外,你需要通过应用默认凭据(ADC)进行客户端身份验证。为此,你首先需要确保已经安装 [gcloud](https://cloud.google.com/sdk/gcloud) 命令行界面(CLI)。由于我们在本地机器上开发代码,我们只需通过 ADC 进行身份验证,如下所述:

$ gcloud auth application-default login

运行命令后,你的浏览器将打开一个新标签页,要求你使用 Google 帐号登录。请确保你的个人 Google Cloud 帐号具有足够的权限以执行接下来几个部分中的操作。

列出所有 BigQuery 数据集

首先,让我们开始创建一个 BigQuery 客户端实例:

from google.cloud import bigquery

# The client we'll use to interact with BigQuery
client = bigquery.Client(project='my-gcp-project')

我们现在可以使用客户端通过调用 list_datasets() 方法来获取所有 BigQuery 数据集,该方法返回一个指向类型为 [DaasetListItem](https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.dataset.DatasetListItem) 对象的迭代器。

# Get an iterator pointing to `DaasetListItem` objects
bq_datasets = list(client.list_datasets())

现在我们可以遍历 bq_datasets 对象并打印我们在实例化客户端时使用的项目的 BigQuery 数据集。

# Print BigQuery datasets in project `my-gcp-project` 
for dataset in bq_datasets:
  print(f'{client.project=}, {dataset.dataset_id=}')

这是代码的完整版本,结合了我们之前讨论的所有代码片段。

"""
Script used to iterate over BigQuery datasets in a single 
BigQuery project.
"""
from google.cloud import bigquery

# The client we'll use to interact with BigQuery
client = bigquery.Client(project='my-gcp-project')

# Get an iterator pointing to `DaasetListItem` objects
bq_datasets = client.list_datasets()

# Print BigQuery datasets in project `my-gcp-project`
for dataset in bq_datasets:
  print(f'{client.project=}, {dataset.dataset_id=}')

最后,这是我们脚本生成的示例输出:

client.project='my-gcp-project', dataset.dataset_id='my_dataset'
client.project='my-gcp-project', dataset.dataset_id='another_dataset'
client.project='my-gcp-project', dataset.dataset_id='oh_heres_another_one'

列出所有 BigQuery 表

同样,为了列出感兴趣的 BigQuery 项目中的所有表,我们将再次迭代数据集,然后列出每个数据集中的所有表。

为此,我们需要对每个数据集调用 client.list_tables(),以获取指向类型为 [TableListItem](https://cloud.google.com/python/docs/reference/bigquery/latest/google.cloud.bigquery.table.TableListItem) 对象的迭代器。

for table in client.list_tables(dataset.dataset_id):
    print(f'{table.table_id=}, {dataset.dataset_id=}, {client.project=}')

这是包含所有步骤的完整代码,用于将 BigQuery 项目中所有表的名称打印到标准输出。

"""
Script used to iterate over BigQuery table names for every single 
dataset in a particular BigQuery project.
"""
from google.cloud import bigquery

# The client we'll use to interact with BigQuery
client = bigquery.Client(project='my-gcp-project')

# Get an iterator pointing to `DaasetListItem` objects
bq_datasets = client.list_datasets()

# Print BigQuery table and dataset names in project `my-gcp-project`
for dataset in bq_datasets:
  for table in client.list_tables(dataset.dataset_id):
    print(f'{table.table_id=}, {dataset.dataset_id=}, {client.project=}')

输出:

table.table_id='my_table', dataset.dataset_id='my_dataset', client.project='my-gcp-project'
table.table_id='another_table', dataset.dataset_id='my_dataset', client.project='my-gcp-project'
table.table_id='temp_table', dataset.dataset_id='temp_ds', client.project='my-gcp-project'
table.table_id='temp_table_2', dataset.dataset_id='temp_ds', client.project='my-gcp-project'

计算 BigQuery 中的数据集和表的数量

现在您知道如何使用 Python 客户端与 BigQuery 交互,您还可以推断出单个项目中的数据集数量。

"""
Script used to count number of datasets for a BigQuery project
"""
from google.cloud import bigquery

# The client we'll use to interact with BigQuery
client = bigquery.Client(project='my-gcp-project')

# Turn iterator into list, and count its length
dataset_count = len(list(client.list_datasets()))

print(f'Number of datasets in project {client.project}: {dataset_count}')

同样,我们甚至可以计算每个数据集或每个项目中的表数量:

"""
Script used to count number of tables per dataset for a BigQuery project
"""
from google.cloud import bigquery

# The client we'll use to interact with BigQuery
client = bigquery.Client(project='my-gcp-project')

# Turn iterator into list, and count its length
table_total_count = 0

for dataset in client.list_datasets():
  table_count = len(list(client.list_tables(dataset.dataset_id)))
  table_total_count += table_count
  print(f'No. of tables for dataset {dataset.dataset_id}: {table_count}')

print(f'Number of tables in project {client.project}: {table_total_count}')

最终想法

在这个简短的教程中,我们概述了使用 Google 提供的 Python 客户端来利用 BigQuery API 的步骤,包括安装和身份验证步骤。

此外,我们演示了如何使用 BigQuery Python 客户端来编程列出 BigQuery GCP 项目中的数据集和表。最后,我们还创建了一些脚本,您可以使用这些脚本来计算数据集或表的数量。

希望您觉得这篇文章有用。如果您在运行教程中分享的代码时遇到任何问题,请在评论中告知我,我会尽力帮助您调试并成功运行代码。

👉 成为会员 并阅读 Medium 上的每个故事。您的会员费直接支持我和您阅读的其他作者。您还将完全访问 Medium 上的每个故事。

[## 使用我的推荐链接加入 Medium — Giorgos Myrianthous

作为 Medium 会员,您的会员费用的一部分会用于支持您阅读的作者,并且您可以完全访问每一个故事……

gmyrianthous.medium.com](https://gmyrianthous.medium.com/membership?source=post_page-----b92063ad0be3--------------------------------)

👇相关的文章您可能也会喜欢 👇

## 什么是 dbt(数据构建工具)

对 dbt 的温和介绍,它正在主导数据世界

towardsdatascience.com ## BigQuery 的 SQL 反模式

在 Google Cloud BigQuery 上运行 SQL 时的最佳实践和应避免的事项

towardsdatascience.com ## 什么是 Python 中的 pyproject.toml

在 pyproject.toml 文件中管理 Python 项目的依赖关系

towardsdatascience.com

列表、元组、字典和数据框在 Python 中的完全指南

原文:towardsdatascience.com/lists-tuples-dictionaries-and-data-frames-in-python-the-complete-guide-7ab54d4819ee

掌握 Python 中最常用的数据结构所需的所有知识

Federico TrottaTowards Data Science Federico Trotta

·发布在 Towards Data Science ·阅读时间 16 分钟·2023 年 5 月 24 日

--

图片由 Pexels 提供,发布在 Pixabay

如果你已经开始学习 Python,无论你想成为一名软件工程师还是数据科学家,你都必须掌握数据结构。

Python 具有许多数据结构,可以用来存储数据。在本文中,我们将深入了解最常用的数据结构。所以,如果你刚开始你的职业生涯,并需要学习数据结构,那么这篇文章绝对适合你。

这里是你会找到的内容:

**Table of Contents:** 
Lists
  Definitions and creation examples
  Lists manipulation
  List comprehension
  List of lists
Tuples
Dictionaries
  Dictionaries manipulation
  Nested dictionaries
  Dictionary comprehension
Data frames
   Basic data frames manipulations with Pandas

列表

定义和创建示例

在 Python 中,列表是一个有序元素的集合,这些元素可以是任何类型:字符串、整数、浮点数等……

要创建一个列表,项必须放在方括号中,并用逗号分隔。例如,这里是如何创建一个整数列表的:

# Create list of integers
my_integers = [1, 2, 3, 4, 5, 6]

但列表也可以包含“混合”类型的数据。例如,创建一个同时包含整数和字符串的列表:

# Create a mixed list
mixed_list = [1, 3, "dad", 101, "apple"]

要创建一个列表,我们还可以使用 Python 内置函数 list()。这就是我们如何使用它的:

# Create list and print it
my_list = list((1, 2, 3, 4, 5))
print(my_list)

>>>  

    [1, 2, 3, 4, 5]

这个内置函数在某些特殊情况下非常有用。例如,假设我们想创建一个范围在 (1–10) 之间的数字列表。以下是我们可以这样做的方式:

# Create a list in a range
my_list = list(range(1, 10))
print(my_list)

>>>

  [1, 2, 3, 4, 5, 6, 7, 8, 9]
**NOTE**:

Remember that the built-in function "range" includes the first value,
and excludes the last one.

现在,让我们看看如何操作列表。

列表操作

由于列表是可变的,我们有很多可能性来操作它们。例如,假设我们有一个名字列表,但我们犯了一个错误,需要更改其中一个。以下是我们可以这样做的方式:

# List of names
names = ["James", "Richard", "Simon", "Elizabeth", "Tricia"]
# Change the wrong name
names[0] = "Alexander"
# Print list
print(names)

>>>

    ['Alexander', 'Richard', 'Simon', 'Elizabeth', 'Tricia']

所以,在上述示例中,我们将列表中的第一个名字从 James 更改为 Alexander。

**NOTE:** 
In case you didn't know, note that in Python the first element
is always accessed by "0", regarding of the type we're manipulating.
So, in the above example, "names[0]" represents the first element
of the list "names".

现在,假设我们忘记了一个名字。我们可以这样将其添加到我们的列表中:

# List of names
names = ["James", "Richard", "Simon", "Elizabeth", "Tricia"]
# Append another name
names.append("Alexander")
# Print list
print(names) 

>>>

    ['James', 'Richard', 'Simon', 'Elizabeth', 'Tricia', 'Alexander']

如果我们需要连接两个列表,我们有两种选择:concatenate方法或extend()方法。让我们来看一下:

# Create list1
list1 = [1, 2, 3]
# Create list2
list2 = [4, 5, 6]
# Concatenate lists
concatenated_list = list1 + list2
# Print concatenated list
print(concatenated_list)

>>>

  [1, 2, 3, 4, 5, 6]

所以,这种方法创建了一个包含其他列表之和的列表。我们来看一下extend()方法:

# Create list1
list1 = [1, 2, 3]
# Create list2
list2 = [4, 5, 6]
# Extend list1 with list2
list1.extend(list2)
# Print new list1
print(list1)

>>>

  [1, 2, 3, 4, 5, 6]

正如我们所看到的,结果是相同的,但语法不同。此方法用list2扩展了list1

如果我们想要删除元素,我们有两种选择:可以使用remove()方法或del。让我们来看看:

# Create list
my_list = [1, 2, 3, 'four', 5.0]
# Remove one element and print
my_list.remove('four')
print(my_list)

>>>

  [1, 2, 3, 5.0]

我们来看看另一种方法:

# Create list
my_list = [1, 2, 3, 'four', 5.0]
# Delete one element and print
del my_list[3]
print(my_list)

>>>

    [1, 2, 3, 5.0]

因此,我们可以使用这两种方法获得相同的结果,但remove()方法允许我们明确指定要删除的元素,而del则需要访问列表中元素的位置。

**NOTE:** 
If you've gained familiarity with accessing positions, in the above
example my_list[3] = 'four'. Because, remember: in Python we start counting
positions from 0.

列表推导式

有很多情况下我们需要从现有列表中创建新列表,一般是对现有数据应用一些过滤条件。为此,我们有两种选择:

  1. 我们使用循环和语句。

  2. 我们使用列表推导式。

实际上,它们都是写相同内容的方式,但列表推导式更简洁、更优雅。

但在讨论这些方法之前,你可能需要深入了解循环和语句。这里有几个我过去写的文章,可能会对你有帮助:

## Python 中的循环和语句:深入理解(带示例)

当它们看起来被理解时,还有更多的内容

towardsdatascience.com ## Python 循环:如何在 Python 中遍历的完整指南

利用 Python 中循环的力量

towardsdatascience.com

现在,让我们直接使用循环和语句来看几个例子。

假设我们有一个购物清单。我们希望我们的程序打印出我们喜欢一种水果,而不喜欢清单上的其他水果。以下是实现的方法:

# Create shopping list
shopping_list = ["banana", "apple", "orange", "lemon"]
# Print the one I like
for fruit in shopping_list:
    if fruit == "lemon":
        print(f"I love {fruit}")
    else:
        print(f"I don't like {fruit}")

>>>

    I don't like banana
    I don't like apple
    I don't like orange
    I love lemon

另一个例子可能是这样的。假设我们有一个数字列表,并且我们只想打印出偶数。以下是实现的方法:

# Create list
numbers = [1,2,3,4,5,6,7,8]
# Create empty list
even_list = []
# Print even numbers
for even in numbers:
    if even %2 == 0:
        even_list.append(even)
    else:
        pass

print(even_list)

>>>

    [2, 4, 6, 8]
**NOTE:**

If you are not familiar with the sintax %2 == 0 it means that we are
dividing a number by 2 and expect a reminder of 0\. In other words,
we are asking our program to intercept the even numbers.

因此,在上述示例中,我们创建了一个数字列表。然后,我们创建了一个空列表,在循环后用于附加所有偶数。这样,我们就从包含“常规”数字的列表中创建了一个偶数列表。

现在……这种使用循环和语句创建新列表的方式有点“繁琐”。我的意思是:这需要很多代码。我们可以使用列表推导式以更简洁的方式获得相同的结果。

例如,要创建一个包含偶数的列表,我们可以使用如下的列表推导式:

# Create list
numbers = [1,2,3,4,5,6,7,8]
# Create list of even numbers
even_numbers = [even for even in numbers if even %2 == 0]
# Print even list
print(even_numbers)

>>>

    [2, 4, 6, 8]

所以,列表推导式直接创建一个新列表,并在其中定义条件。如我们所见,我们获得了与之前相同的结果,但只用了一行代码:不错吧!

现在,让我们使用列表推导式创建一个包含我喜欢的水果(和我不喜欢的水果)的列表:

# Create shipping list
shopping_list = ["banana", "apple", "orange", "lemon"]
# Create commented list and print it
commented_list = [f"I love {fruit}" if fruit == "banana"
                  else f"I don't like {fruit}"
                  for fruit in shopping_list]
print(commented_list)

>>>

  ['I love banana', "I don't like apple", "I don't like orange",
   "I don't like lemon"]

所以,我们获得了与之前相同的结果,但只用了一行代码。唯一的区别是这里我们打印了一个列表(因为列表推导式创建了一个!),而之前我们只是打印了结果。

列表的列表

还有可能创建列表的列表,即嵌套在一个列表中的列表。当我们想将列出的数据表示为一个唯一的列表时,这种可能性很有用。

例如,假设我们想创建一个学生及其成绩的列表。我们可以创建如下的内容:

# Create lis with students and their grades
students = [
    ["John", [85, 92, 78, 90]],
    ["Emily", [77, 80, 85, 88]],
    ["Michael", [90, 92, 88, 94]],
    ["Sophia", [85, 90, 92, 87]]
]

如果我们想计算每个学生的平均成绩,这是一种有用的表示方式。我们可以这样做:

# Iterate over the list
for student in students:
    name = student[0] # Access names
    grades = student[1] # Access grades
    average_grade = sum(grades) / len(grades) # Calculate mean grades
    print(f"{name}'s average grade is {average_grade:.2f}")

>>>

    John's average grade is 86.25
    Emily's average grade is 82.50
    Michael's average grade is 91.00
    Sophia's average grade is 88.50

元组

元组是 Python 中的另一种数据结构类型。它们用圆括号定义,并且像列表一样,可以包含任何数据类型,数据类型之间用逗号分隔。例如,我们可以这样定义一个元组:

# Define a tuple and print it
my_tuple = (1, 3.0, "John")
print(my_tuple)

>>>

    (1, 3.0, 'John')

元组和列表的区别在于元组是不可变的。这意味着元组的元素不能被更改。例如,如果我们尝试向元组中添加一个值,我们会得到一个错误:

# Create a tuple with names
names = ("James", "Jhon", "Elizabeth")
# Try to append a name
names.append("Liza")

>>>

    AttributeError: 'tuple' object has no attribute 'append'

所以,由于我们不能修改元组,它们在我们希望数据不可变时很有用;例如,在我们不想犯错误的情况下。

一个实际的例子可能是电子商务的购物车。我们可能希望这种数据不可变,以便在操作时不犯错误。假设有人从我们的电子商务网站购买了一件衬衫、一双鞋子和一只手表。我们可以将这些数据以数量和价格的形式记录在一个元组中:

# Create a chart as a tuple
cart = (
    ("Shirt", 2, 19.99),
    ("Shoes", 1, 59.99),
    ("Watch", 1, 99.99)
)

当然,准确地说,这是一个元组的元组。

由于元组是不可变的,它们在性能方面更高效,这意味着它们节省了计算机的资源。但在操作时,我们可以使用与列表相同的代码,所以我们不会再写一次。

最后,类似于列表,我们可以用内置函数tuple()创建一个元组,如下所示:

# Create a tuple in a range
my_tuple = tuple(range(1, 10))
print(my_tuple)

>>>

  (1, 2, 3, 4, 5, 6, 7, 8, 9)

字典

字典是一种以键和值对存储数据的方式。我们可以这样创建一个字典:

# Create a dictionary
my_dictionary = {'key_1':'value_1', 'key_2':'value_2'}

所以,我们用大括号创建一个字典,并在其中存储一对对的键和值,用冒号分隔。键值对用逗号分隔。

现在,让我们看看如何操作字典。

字典操作

字典的键和值可以是任何类型:字符串、整数或浮点数。例如,我们可以这样创建一个字典:

# Create a dictionary of numbers and print it
numbers = {1:'one', 2:'two', 3:'three'}
print(numbers)

>>>

    {1: 'one', 2: 'two', 3: 'three'}

但我们也可以这样创建:

# Create a dictionary of numbers and print it
numbers = {'one':1, 'two':2.0, 3:'three'}
print(numbers)

>>>

  {'one': 1, 'two': 2.0, 3: 'three'}

选择值和键的类型取决于我们需要解决的问题。无论如何,考虑到我们之前看到的字典,我们可以这样访问值和键:

# Access values and keys
keys = list(numbers.keys())
values = tuple(numbers.values())
# Print values and keys
print(f"The keys are: {keys}")
print(f"The values are: {values}")

>>>

    The keys are: ['one', 'two', 3]
    The values are: (1, 2.0, 'three')

因此,如果我们的字典叫做 numbers,我们用 numbers.keys() 访问其键。使用 numbers.values() 我们可以访问其值。此外,请注意,我们使用之前看到的表示法创建了一个包含键的列表和一个包含值的元组。

当然,我们也可以遍历字典。例如,假设我们想打印出大于某个阈值的值:

# Create a shopping list with fruits and prices
shopping_list = {'banana':2, 'apple':1, 'orange':1.5}
# Iterate over the values
for values in shopping_list.values():
    # Values greater than threshold
    if values > 1:
        print(values)

>>>

    2
    1.5

像列表一样,字典是可变的。因此,如果我们想向字典中添加一个值,我们必须定义要添加的键和值。我们可以这样做:

# Create the dictionary
person = {'name': 'John', 'age': 30}
# Add value and key and print
person['city'] = 'New York'
print(person)

>>>

    {'name': 'John', 'age': 30, 'city': 'New York'}

要修改字典的值,我们需要访问其键:

# Create a dictionary
person = {'name': 'John', 'age': 30}
# Change age value and print
person['age'] = 35
print(person)

>>>

    {'name': 'John', 'age': 35}

要从字典中删除一对键值,我们需要访问其键:

# Create dictionary
person = {'name': 'John', 'age': 30}
# Delete age and print
del person['age']
print(person)

>>>

    {'name': 'John'}

嵌套字典

我们之前已经看到,我们可以创建列表的列表和元组的元组。类似地,我们可以创建嵌套字典。假设,例如,我们想创建一个字典来存储与一个班级学生相关的数据。我们可以这样做:

# Create a classroom dictionary
classroom = {
    'student_1': {
        'name': 'Alice',
        'age': 15,
        'grades': [90, 85, 92]
    },
    'student_2': {
        'name': 'Bob',
        'age': 16,
        'grades': [80, 75, 88]
    },
    'student_3': {
        'name': 'Charlie',
        'age': 14,
        'grades': [95, 92, 98]
    }

因此,每个学生的数据表示为一个字典,所有字典都存储在一个唯一的字典中,表示整个课堂。正如我们所见,字典的值甚至可以是列表(或者元组,如果我们愿意的话)。在这个例子中,我们使用列表来存储每个学生的成绩。

要打印一个学生的值,我们只需记住,从课堂字典的角度来看,我们需要访问键,在这种情况下,键就是学生本身。这意味着我们可以这样做:

# Access student_3 and print
student_3 = classroom['student_3']
print(student_3)

>>>

    {'name': 'Charlie', 'age': 14, 'grades': [95, 92, 98]}

字典推导

字典推导允许我们简洁高效地创建字典。它类似于列表推导,但不创建列表,而是创建一个字典。

假设我们有一个字典,其中存储了一些物品及其价格。我们想知道价格低于某个阈值的物品。我们可以这样做:

# Define initial dictionary
products = {'shoes': 100, 'watch': 50, 'smartphone': 250, 'tablet': 120}
# Define threshold
max_price = 150
# Filter for threshold
products_to_buy = {fruit: price for fruit, price in products.items() if price <= max_price}
# Print filtered dictionary
print(products_to_buy)

>>>

    {'shoes': 100, 'watch': 50, 'tablet': 120}

所以,使用字典推导的语法是:

new_dict = {key:value for key, value in iterable}

其中 iterable 是任何可迭代的 Python 对象。它可以是一个列表、一个元组、另一个字典等…

使用“标准”方法创建字典会需要大量的代码,包括条件、循环和语句。相反,如我们所见,字典推导允许我们仅用一行代码,基于条件创建字典。

字典推导在我们需要从其他来源或数据结构中检索数据来创建字典时特别有用。例如,假设我们需要从两个列表中检索值来创建一个字典。我们可以这样做:

# Define names and ages in lists
names = ['John', 'Jane', 'Bob', 'Alice']
cities = ['New York', 'Boston', 'London', 'Rome']
# Create dictionary from lists and print results
name_age_dict = {name: city for name, city in zip(names, cities)}
print(name_age_dict)

>>>

   {'John': 'New York', 'Jane': 'Boston', 'Bob': 'London', 'Alice': 'Rome'}

数据框

数据框是表格数据的表示。图片来源于熊猫网站:pandas.pydata.org/docs/getting_started/index.html

数据框是由列和行组成的二维数据结构。因此,它在某种程度上类似于电子表格或 SQL 数据库中的表格。它们具有以下特点:

  1. 每一行代表一个独立的观察或记录。

  2. 每一列代表一个变量或数据的特定属性。

  3. 它们有标记的行(称为索引)和列,使得操作数据变得容易。

  4. 列可以包含不同类型的数据,如整数、字符串或浮点数。即使是单列也可以包含不同的数据类型。

虽然数据框是数据分析和数据科学中使用的典型数据结构,但 Python 软件工程师也可能需要操作数据框,这就是我们进行数据框概述的原因。

这是数据框的显示方式:

数据框。图片由 Federico Trotta 提供。

所以,在左侧(蓝色矩形中)我们可以看到索引,意味着行数。我们可以看到数据框可以包含不同类型的数据。特别是,列“Age”包含不同的数据类型(一个字符串和两个整数)。

使用 Pandas 进行基本的数据框操作

尽管最近开始流行一种叫做“Polars”的新库来操作数据框,但在这里我们将看到一些 Pandas 的数据操作,Pandas 仍然是目前使用最广泛的库。

首先,通常我们可以通过从 .xlsx.cvs 文件导入数据来创建数据框。在 Pandas 中我们可以这样做:

import pandas as pd

# Import cvs file
my_dataframe = pd.read_csv('a_file.csv')

# Import xlsx
my_dataframe_2 = pd.read_excel('a_file_2.xlsx')

如果我们想创建一个数据框:

import pandas as pd

# Create a dictionary with different types of data
data = {
    'Name': ['John', 'Alice', 'Bob'],
    'Age': ['twenty-five', 30, 27],
    'City': ['New York', 'London', 'Sydney'],
    'Salary': [50000, 60000.50, 45000.75],
    'Is_Employed': [True, True, False]
}

# Create the dataframe
df = pd.DataFrame(data)

这是我们上面展示的数据框。所以,正如我们所见,我们首先创建一个字典,然后用 pd.DataFrame() 方法将其转换为数据框。

我们有三种方式来可视化数据框。假设我们有一个名为 df 的数据框:

  1. 第一个是 print(df)

  2. 第二个是df.head(),它将显示我们数据框的前 5 行。如果我们有一个包含很多行的数据框,我们可以显示超过前五行。例如,df.head(20)显示前 20 行。

  3. 第三个是df.tail(),它的功能与head()完全相同,但它显示的是最后几行。

在可视化方面,使用上述 df,这是 df.head() 显示的内容:

df.head() 显示的内容。图片由 Federico Trotta 提供。

这是 print(df) 显示的内容:

print(df) 显示的内容。图片由 Federico Trotta 提供。

在像这样的小数据集的情况下,差异只是个人喜好(我更喜欢head()因为它“显示数据的表格性”)。但在大数据集的情况下,head()要好得多。试试看,告诉我!

考虑到 Pandas 是一个非常广泛的库,这意味着它允许我们以多种方式操作表格数据,因此需要单独处理。这里我们只是展示最基本的内容,所以我们将学习如何添加和删除列(数据框的列也称为“Pandas 系列”)。

假设我们想向上面提到的数据框df添加一列,表示人们是否已婚。我们可以这样做:

# Add marital status
df["married"] = ["yes", "yes", "no"]
**NOTE:**

this is the same notation we used to add values to a dictionary.
Return back on the article and compare the two methods.

显示头部,我们得到:

包含婚姻状况的数据框 df。图片由 Federico Trotta 提供。

删除一列:

# Delete the "Is_Employed" column
df = df.drop('Is_Employed', axis=1)

我们得到:

删除与就业数据相关的列后的数据框。图片由 Federico Trotta 提供。

请注意,我们需要使用axis=1,因为这里我们告诉 Pandas 删除列,并且由于数据框是二维数据结构,axis=1表示垂直方向。

相反,如果我们想要删除一行,我们需要使用axis=0。例如,假设我们想删除索引为 1 的行(即第二行,因为我们从 0 开始计数):

# Delete the second row 
df = df.drop(1, axis=0)

我们得到:

删除第二行后的数据框。图片由 Federico Trotta 提供。

结论

到目前为止,我们已经看到了 Python 中最常用的数据结构。这些不是唯一的数据结构,但肯定是最常用的。

此外,使用其中一种数据结构并没有对错之分:我们只需要理解我们需要存储什么数据,并选择最适合这种任务的数据结构。

希望这篇文章帮助您理解这些数据结构的使用以及何时使用它们。

免费 PYTHON 电子书:

开始学习 Python 数据科学但感到困难?订阅我的新闻通讯并获取我的免费电子书:这将为您提供正确的学习路径,帮助您通过实践学习 Python 数据科学。

喜欢这个故事吗?成为 Medium 会员,享受 5$/月 通过我的推荐链接:我将获得一小笔佣金,但不会增加您的额外费用:

[## 使用我的推荐链接加入 Medium - Federico Trotta

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

medium.com](https://medium.com/@federicotrotta/membership?source=post_page-----7ab54d4819ee--------------------------------)

LLaMA:面向所有人的大型语言模型!

原文:towardsdatascience.com/llama-llms-for-everyone-724e737835be

高性能的开源语言模型……

Cameron R. Wolfe, Ph.D.Towards Data Science Cameron R. Wolfe, Ph.D.

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

--

(照片由拉斯波波娃·玛丽娜拍摄,来源于Unsplash

多年来,深度学习社区一直倡导开放性和透明度,导致了像HuggingFace这样的大规模开源项目。深度学习中的许多深刻思想(例如,transformers [2],自监督学习等)在网上公开可用,无论是通过公共代码库还是Arxiv。尽管开源已经成为一种规范,但大型语言模型(LLMs)的流行(以及商业适用性)最近挑战了这一趋势。

目前许多最强大的大型语言模型(LLMs)只能通过 API 访问(例如,来自OpenAIAnthropic),这使得源代码和模型参数对研究人员和开发者而言不可及。虽然我的目标不是引发对当前 LLM 趋势的伦理讨论,但这些信息与本文的主题:公开可用的 LLM 相关。有趣的是,并非所有强大的语言基础模型都被隐藏在付费墙后面。一些模型,如 LLaMA,既公开可用又表现出色,因此在深度学习研究社区中保持了开放性。

什么是 LLaMA? LLaMA 不是单一模型,而是一套范围从 70 亿到 650 亿参数的 LLM。受 Chinchilla [3] 启发,这些 LLM 比 它们的对应物 稍小,但经过广泛的预训练(即,较小的模型,更多的 token),并旨在提供一组具有不同性能与推理效率权衡的多样化模型。LLaMA 模型表现惊人;例如,130 亿参数模型大致可与 GPT-3 [4] 相媲美,而 650 亿参数模型常常超越 PaLM [5] 的表现。

“GPT-4 从各种许可、创建和公开可用的数据源中学习,这些数据源可能包括公开的个人信息。” — 引自 [6]

除了令人印象深刻的性能外,LLaMA 仅使用公开数据进行预训练。LLaMA 模型可以完全通过在线资源复现,标志着在 LLM 领域向开源迈出的一步。最近的模型如 GPT-4 已知是通过公共和专有/私人数据的组合进行训练的。尽管这可能有利于模型性能,LLaMA 表明我们可以充分利用在线可用的数据,从而为与 LLM 相关的开源研究计划提供了希望。

(引自 [1])

背景信息

LLaMA LLM 采用了先前工作中提出的一些想法和技术。在这一部分,我们将回顾一些有用的背景信息,这些信息对于深入理解 LLaMA 及其组件将有所帮助。

关于 LLM 的简要说明。 首先,了解 LLM 的基础知识,包括其架构、训练过程和一般方法是有帮助的。我们在之前的概述中已经广泛探讨了这一主题。因此,我们在这里不会详细介绍这一主题,但下面提供了进一步阅读和学习的链接。

  • LLM(仅解码器)架构 [link]

  • 语言模型预训练 [link]

  • LLM 解释 [link]

  • LLM 历史 [link]

  • LLM 基础 [link]

均方根层归一化 (RMSNorm)

通常,transformer 架构(包括 LLM 使用的仅解码器 transformer 架构)使用LayerNorm来规范化每层内的激活值。然而,使用不同的规范化技术已被证明可以稳定训练并提高泛化性能。例如,RMSNorm [16]的定义如下面的方程所示。

(由作者创建)

RMSNorm 与 LayerNorm 有些相似,但在规范化神经网络激活值时,它去除了均值中心化操作(并使用了略微修改的分母)。与 LayerNorm 相比,RMSNorm 在计算效率和简单性上更具优势,使其能够以 10–50%的效率提升达到相当的性能水平。

(来自[16])

SwiGLU 激活函数

LLM 的仅解码器架构的每个块包含一个两层前馈神经网络(即不使用偏置并单独应用于每个标记向量),在两层之间具有非线性。最初,这个非线性是修正线性单元(ReLU)激活函数。然而,最近的工作[15]揭示了这并不是最佳选择。

(由作者创建)

特别是,LLaMA(以及其他像PaLM的 LLM)选择使用 SwiGLU 激活函数,这在上面的方程中进行了公式化。这里,我们将 Swish 激活定义如下。

(由作者创建)

SwiGLU 是输入x的两个线性变换的逐元素乘积,其中一个已应用 Swish 激活函数。该激活函数需要三个矩阵乘法,但发现相较于其他激活函数,它在性能上有所提升,即使在保持计算量不变的情况下

再材料化(或重新计算)

重计算,也称为再计算,是一种在训练 LLM(以及其他大型神经网络)时使用的技术,旨在减少内存消耗,但需要额外的计算。通常,当我们计算神经网络的前向传递时,我们会在每一层存储/保留网络的激活值,以便在反向传递时使用(这对于计算权重更新是必要的!)。但这需要大量的内存!

神经网络前向和反向传递的示意图(作者创作)

重计算的基本思想是在反向传递期间重新计算某些中间激活值,而不是在前向传递期间将它们存储在内存中。这有助于减少训练过程中的峰值内存使用,从而允许在可用内存限制内训练更大的模型或使用更大的批次大小。考虑到 LLM 体积庞大且消耗大量内存,这一点尤为重要。

LLaMA 套件

现在我们已经掌握了一些有用的概念,让我们了解一下 LLaMA 中的 LLM 集合及其工作原理。由于这些模型受到Chinchilla(TL;DR:仅通过对更大量数据进行小型 LLM 的预训练)[3]提出的预训练策略的强烈启发,我们将在深入了解 LLaMA 之前简要概述这些思想。总体而言,LLaMA 质疑了大规模 LLM 的趋势,声称(如果进行足够的预训练!)更小的 LLM 可以在显著较低的推理预算下实现令人印象深刻的性能。

我们如何最大化 LLM 的效率?

最近 LLM 的发展历程中,一个特别值得注意的时刻是Chinchilla [3]的提出。在 GPT-3 之后,深度学习研究界对大规模语言模型中显著的少样本学习能力感到震惊。因此,我们开始测试比 GPT-3 还要大的模型。但结果并不理想!

“Hoffmann 等人(2022 年)的最新研究表明,对于给定的计算预算,最佳性能并非由最大的模型实现,而是由在更多数据上训练的小型模型实现的。” — 引自 [1]

为了创建比 GPT-3 更优秀的 LLM,我们不能仅仅使用更大的模型。相反,我们需要更多的预训练数据!具体来说,Chinchilla 的分析表明,如果我们对稍小的 LLM 进行更广泛的预训练,可以实现更高的性能。

这就是全部情况吗? 尽管我们知道较小的 LLM 如果进行广泛预训练可以表现良好,即使 [3] 中的分析也表明,训练相对较大的 LLM 是达到高性能水平的最有效方式。这个说法完全正确,但它仅考虑了训练效率。因此,我们不得不问自己一个问题:训练效率是我们唯一关心的吗? 对于大多数从业者来说,这个问题的答案无疑是否定的!

“这项工作的重点是训练一系列语言模型,通过训练更多令牌来实现不同推理预算下最佳的性能。” — 来自 [1]

训练成本只是与 LLM 相关的全部成本中的一小部分。我们还必须考虑托管,这使得推理预算成为一个巨大的考虑因素。LLaMA 通过强调,给定目标性能水平,预训练一个较小的 LLM 更长时间最终在推理过程中会更便宜,并且随着时间的推移节省大量成本。虽然我们可能会使用更大的模型以获得性能提升,但我们应该通过广泛的预训练尽可能减少模型大小(从而降低托管成本)。

LLaMA 的组成部分

(来自 [1])

数据集。 我们知道 LLaMA 的预训练数据集基于公共数据,但这些数据究竟来自哪里? 上述内容概述了用于 LLaMA 的预训练数据集。正如所见,尽管预训练数据(完全公开)具有相当大的多样性,数据来源从 StackExchangeGutenberg Project 不等。完整的数据集在标记化后大约包含 1.4T 令牌。这与 Chinchilla [3] 预训练所使用的令牌数量相同;见下文。

(来自 [3])

鉴于 LLaMA 强调透明性和可重复性,[1] 提供了大量关于构建预训练数据集的见解。讨论中最有趣的一个方面是,我们可以利用这些见解进一步了解数据在预训练 LLM 之前是如何被过滤的。例如,来自 CommonCrawl 的文本数据被过滤以排除:

此外,文献[1]中的作者训练了一个线性分类器,用于区分维基百科中作为参考的页面和随机采样的页面,然后丢弃那些未被分类为参考的页面。这些步骤都仅仅是为了过滤 CommonCrawl!从之前的工作中,我们知道,正确过滤预训练数据集对 LLM 性能至关重要。在[1]中,我们深入了解了实现有效过滤管道的具体细节。

在变压器块内的预归一化结构(由作者创建)

架构。 LLaMA 套件采用了来自流行 LLM(如GPT-3 [4]和PaLM [5])的许多常见架构技巧。例如,LLaMA 在其每一层内执行预归一化,这意味着归一化应用于每一层的输入,而不是输出;见上文。此外,RMSNorm、SwiGLU 激活函数旋转位置嵌入(RoPE) [10](即,绝对位置嵌入相对位置嵌入的某种混合形式)在每个变压器层中使用。

(来自[1])

在[1]中,使用了四种不同规模的模型,参数量从 67 亿到 652 亿;见上文。这些模型形成了被称为 LLaMA 的 LLM 集合,并提供了在性能与模型大小或推理预算之间的各种权衡。最显著的是,我们将看到 LLaMA-13B 在性能上与 GPT-3 竞争力强,并且可以在单个 V100 GPU 上运行。与之前的模型相比,这是一个巨大的成就,使得这些模型对大多数从业者更加可及(例如,PaLM 使用超过 6000 个加速器进行训练)。

更高效的性能。 [1] 的作者采用了一些有趣的技巧来提高 LLM 训练效率。首先,我们应该记住,基于 decoder-only transformer models 的现代 LLM 在每一层中使用因果 multi-headed attention。为了提高这种因果多头注意力操作的效率,LLaMA 使用了一种高效的实现方式,该方式既不 i) 存储注意力权重,也不 ii) 计算被掩盖的令牌的键/查询分数。通过这样做,我们可以节省大量通常浪费在因果自注意力中未考虑的被掩盖令牌上的计算。这种方法受到 [9] 中思想的启发,但我们可以在 xformers library 中找到一个开源实现。

除了高效的因果自注意力实现之外,LLaMA 在重新物化方面的处理方式与大多数 LLM 训练策略有所不同。计算成本最高的激活(例如线性层的输出)在前向传播过程中被保存,从而减少了在反向传播过程中重新计算的激活数量。这一变化需要手动重新实现 LLM 的反向传播(而不是使用 autograd 进行自动微分),是一种混合的重新物化方法,显著提高了整体训练吞吐量。

“在训练一个 65B 参数的模型时,我们的代码在 2048 A100 GPU 上处理约 380 令牌/秒/ GPU。这意味着对我们包含 1.4T 令牌的数据集的训练大约需要 21 天。” — 摘自 [1]

鉴于 LLaMA 采用的改进以提高训练效率,我们可能会想知道:这实际上使训练快了多少? 好吧,这在很大程度上取决于训练基础设施。然而,当使用 2048 A100 GPUs 时,LLaMA-65B 完成对 1.4T 令牌的预训练大约需要 21 天。

LLaMA 与 SOTA LLMs

虽然开源和可重复性很重要,但如果模型表现不佳,没有人会关心 LLaMA!此前也有尝试开源 LLM(例如 OPTBLOOM [11, 12])。但这些模型在性能上无法与现代 LLM 竞争。在这一部分,我们将分析 LLaMA 模型相对于流行 LLM,如 GPT-3PaLM [4, 5] 的表现。

我们如何进行评估?之前的帖子 中详细描述的那样,LLaMA 的评估与大多数语言基础模型类似:通过零样本和少样本学习。值得注意的是,LLaMA 模型仅作为预训练的基础模型进行评估,这意味着没有进行微调(无论是通过 SFT 还是 RLHF)。LLaMA 与流行的闭源 LLMs(例如,GPT-3GopherChinchillaPaLM [3, 4, 5, 13])以及以前的开源 LLMs(例如,OPTGPT-J,和 GPT-Neo [11, 14])在自由生成和多项选择任务上进行比较。测试了各种领域(例如,常识和数学推理、代码生成、问答等)。

(来自 [1])

语言理解。 在闭卷问答和阅读理解任务中,我们看到 LLaMA-65B 达到了最先进的零样本和少样本表现,一贯超过了像 PaLM 和 Chinchilla 这样更大模型的表现。进一步来看,LLaMA-13B 表现令人惊讶,甚至在大多数情况下超越了 GPT-3(它大 10 倍!)的表现。这里的基本结论是 i) 更大的 LLaMA 模型与最先进的技术具有竞争力,ii) 更小的 LLaMA 模型在其尺寸范围内表现出令人惊讶的效果。

(来自 [1])

推理任务。 LLaMA 套件还在常识和数学推理任务上进行了评估。在常识推理任务中,LLaMA 超越了多个强大基准的零样本推理表现;请参见上文。然而,这里需要注意的是,没有采用特殊的提示方法(例如,思维链提示)来促进推理的改进。以往的工作 [5] 已经表明,如果没有正确的提示方法,LLMs 的“推理”能力可能会随着规模的增加而退化。

(来自 [1])

尽管这种分析存在局限性,LLaMA 的推理能力相比于基线模型仍然显得相对令人印象深刻。即,LLaMA 模型在数学推理数据集上的表现与(甚至在某些情况下优于)多个基线模型竞争。实际上,LLaMA-65B 甚至超越了一个类似规模的 Minerva 模型,该模型已明确在数学数据上进行微调,以提高在这类任务上的表现。

“Minerva 是一系列基于 38.5B 令牌的 PaLM 模型,这些令牌来自 ArXiv 和数学网页… 在 GSM8k 上,我们观察到 LLaMA65B 超越了 Minerva-62B,尽管它没有在数学数据上进行微调。” — 来源于 [1]

代码生成。 除了基本的推理能力,代码生成是 LLaMA 模型的另一个技能。尽管从未在代码上进行过微调(即代码数据占 LLaMA 预训练数据的 <5%),但 LLaMA-65B 在代码生成任务上超越了 PaLM,而 LLaMA-13B 的代码生成表现也超过了 GPT-3(不过… GPT-3 确实在生成代码方面表现较差)。

(来源于 [1])

其他细节。MMLU 基准测试中,LLaMA 模型在大多数情况下表现落后于像 Chinchilla 和 PaLM 这样的 LLM。这是 LLaMA 性能被当前替代方案明显超越的唯一基准测试之一。在 [1] 中的作者声称,这种性能下降是由于 LLaMA 预训练数据集中图书和学术论文的数量有限(即,与最先进的 LLM 相比,这类预训练数据减少了 >10 倍)。

(来源于 [1])

当跟踪 LLaMA 模型在预训练过程中的性能时,我们观察到性能有明显而稳定的提升;见上文。虽然并非所有任务的表现都相同,但我们可以看到 LLaMA 采用的预训练策略总体上是相对稳定的。

关键点

长话短说,LLaMA 是一个开源的 LLM,表现令人震惊。自从 LLaMA 提出的提案以来,研究社区已经很好地利用了这一开放的出色模型。举个例子,以下研究工作已经在 LLaMA 的基础上进行了扩展:

  • Vicuna: LLaMA 的微调版本,性能(几乎)可比于 GPT-4 [link]

  • Koala: 在互联网对话数据上微调的 LLaMA [link]

  • ChatLLaMA: 使用最少的计算资源在你自己的数据上创建个性化的 ChatGPT [link]

  • ColossalChat:一个与 ChatGPT 类似的模型,基于 LLaMA 的 RLHF 管道[链接]

LLaMA 的影响力可能会显著增加。个人而言,我非常兴奋看到开源 LLMS 的研究不断进展。我希望让这些模型变得更加可访问会带来更广泛的研究社区的深入调查和发展。以下是一些基本要点。

开源 LLM。 目前,LLM 生态系统正经历一种有趣的冲突,两种不同的方法被用来将这些强大的基础模型展示给公众。一方面,像ChatGPTGPT-4这样的模型仅通过付费 API发布,这限制了研究社区对这些模型的详细访问。LLaMA 等贡献则与这一趋势相反,通过向研究社区提供完整的模型访问权限。

什么大小最合适? LLaMA 提供了一系列不同大小的 LLM,而不是发布单一模型。之前的 LLM 研究往往倾向于使用更大的模型,因为较大的 LLM 通常在训练时能以较少的总体计算成本达到令人印象深刻的性能水平。然而,如果我们对较小的模型进行更广泛的预训练,LLaMA 表明我们可以在显著降低推理成本的同时达到可比的性能水平。因此,至少考虑使用较小的 LLM 是合理的,特别是在我们需要部署它们的时候。值得注意的是,一些 LLaMA 模型可以在单个 GPU 上运行,这大大提高了这些 LLM 的可访问性。

令人印象深刻的表现。 在 LLaMA 提出之前,许多研究小组尝试发布流行 LLM 的开源版本(例如,OPT 基本上是一个开源的 GPT-3)。但这些模型的表现远不如通过 API 访问的付费模型。尽管 LLaMA 在某些情况下的性能不尽如人意,但它是一个巨大的进步,因为它通常超越了流行的、最先进的 LLM(取决于使用的模型大小)。

结束语

非常感谢阅读这篇文章。我是 卡梅伦·R·沃尔夫Rebuy 的 AI 总监。我研究深度学习的经验性和理论基础。你也可以查看我在 medium 上的 其他写作!如果你喜欢,请在 twitter 上关注我或订阅我的 深度(学习)焦点通讯,我通过易懂的热门论文概述帮助读者更深入地理解 AI 研究领域的主题。

参考文献

[1] 图弗龙, 休戈 等. “Llama: 开放且高效的基础语言模型.” arXiv 预印本 arXiv:2302.13971 (2023).

[2] 瓦斯瓦尼, 阿希什 等. “注意力是你所需要的一切.” 神经信息处理系统进展 30 (2017).

[3] 霍夫曼, 乔丹 等. “训练计算最优的大型语言模型.” arXiv 预印本 arXiv:2203.15556 (2022).

[4] 布朗, 汤姆 等. “语言模型是少量学习者.” 神经信息处理系统进展 33 (2020): 1877–1901.

[5] 周德瑞, 阿坎卡莎 等. “Palm: 通过路径扩展语言建模.” arXiv 预印本 arXiv:2204.02311 (2022).

[6] OpenAI (2023). “GPT-4 技术报告.” ArXiv, abs/2303.08774.

[7] 温泽克, 纪尧姆 等. “CCNet: 从网页抓取数据中提取高质量单语数据集.” arXiv 预印本 arXiv:1911.00359 (2019).

[8] 张彪 和 里科·森里奇. “均方根层归一化.” 神经信息处理系统进展 32 (2019).

[9] 拉贝, 马库斯·N. 和 查尔斯·斯塔茨. “自注意力不需要 $ O (n^ 2) $ 记忆.” arXiv 预印本 arXiv:2112.05682 (2021).

[10] 苏, 剑林 等. “Roformer: 带有旋转位置嵌入的增强型变换器.” arXiv 预印本 arXiv:2104.09864 (2021).

[11] 张, 苏珊 等. “Opt: 开放预训练变换器语言模型.” arXiv 预印本 arXiv:2205.01068 (2022).

[12] Scao, Teven Le 等. “Bloom: 一个 176b 参数的开放访问多语言模型.” arXiv 预印本 arXiv:2211.05100 (2022).

[13] 雷, 杰克·W. 等. “扩展语言模型: 方法, 分析与训练 Gopher 的见解.” arXiv 预印本 arXiv:2112.11446 (2021).

[14] 布莱克, 西德 等. “Gpt-neox-20b: 一个开源自回归语言模型.” arXiv 预印本 arXiv:2204.06745 (2022).

[15] 沙泽尔, 诺亚姆. “Glu 变体改进变换器.” arXiv 预印本 arXiv:2002.05202 (2020).

[16] 张彪 和 里科·森里奇. “均方根层归一化.” 神经信息处理系统进展 32 (2019).

LlamaIndex:终极 LLM 框架,用于索引和检索

原文:towardsdatascience.com/llamaindex-the-ultimate-llm-framework-for-indexing-and-retrieval-fa588d8ca03e?source=collection_archive---------1-----------------------#2023-06-20

LlamaIndex 简介

Sophia Yang, Ph.D.Towards Data Science Sophia Yang, Ph.D.

·

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

--

LlamaIndex,之前称为 GPT Index,是一个出色的数据框架,旨在帮助您通过提供必要的工具来构建与 LLM 的应用程序,这些工具可以促进数据摄取、结构化、检索以及与各种应用程序框架的集成。LlamaIndex 提供的功能众多且极具价值:

✅ 从不同的数据源和数据格式中获取数据,使用数据连接器(Llama Hub)。

✅ 启用文档操作,如插入、删除、更新和刷新文档索引。

✅ 支持对异构数据和多个文档进行综合处理。

✅ 使用“Router”在不同的查询引擎之间进行选择。

✅ 允许假设文档嵌入以提高输出质量

✅ 提供与各种向量存储、ChatGPT 插件、跟踪工具以及 LangChain 等的广泛集成。

✅ 支持全新的 OpenAI 函数调用 API。

这些只是 LlamaIndex 提供的广泛功能中的一些示例。在这篇博客文章中,我们将探讨一些我发现与 LlamaIndex 非常有用的功能。

数据连接器(LlamaHub)

在开发 LLM 应用程序时,使 LLM 能够有效地与外部数据源交互是至关重要的。如何获取数据是关键。Llama Hub 提供了超过 100 种数据源和格式,使 LlamaIndex 或 LangChain 能够以一致的方式获取数据。

LlamaHub。来源:llama-hub-ui.vercel.app/

默认情况下,你可以pip install llama-hub并将其作为独立包使用。你还可以选择使用我们的download_loader方法单独下载一个数据加载器,以便与 LlamaIndex 一起使用。

这里是一个示例,我们从llama-hub包中加载维基百科数据加载器。统一的语法非常好。

from llama_hub.wikipedia.base import WikipediaReader

loader = WikipediaReader()
documents = loader.load_data(pages=['Berlin', 'Rome', 'Tokyo', 'Canberra', 'Santiago'])

检查输出:

Llama Hub 还支持多模态文档。例如,ImageReader加载器使用 pytesseract 或 Donut 转换器模型从图像中提取文本。

基本查询功能

索引、检索器和查询引擎

索引、检索器和查询引擎是对数据或文档提出问题的三个基本组件:

  • 索引是一个数据结构,允许我们快速从外部文档中检索相关信息。索引通过将文档解析为称为“节点”的文本块来工作,然后从这些块中构建索引。

  • 检索器用于根据用户查询获取和检索相关信息。

  • 查询引擎建立在索引和检索器之上,提供一个通用接口来询问有关数据的问题。

这里是询问文档问题的最简单方法。你首先从文档中创建一个索引,然后使用查询引擎作为问题的接口:

from llama_index import VectorStoreIndex
index = VectorStoreIndex.from_documents(docs)
query_engine = index.as_query_engine()
response = query_engine.query("Who is Paul Graham.")

有各种类型的索引、检索方法和查询引擎,你可以在 LlamaIndex 文档中进一步阅读。接下来,我想介绍一些我认为有用的酷功能。

处理文档更新

许多时候,一旦我们为文档创建了索引,可能需要定期更新文档。如果我们需要重新创建整个文档的嵌入,这个过程可能会很昂贵。LlamaIndex 的索引结构通过支持高效的插入、删除、更新和刷新操作提供了解决方案。例如,可以将新文档作为额外的节点(文本片段)插入,而不需要重新创建以前文档的节点:

# Source: https://gpt-index.readthedocs.io/en/latest/how_to/index/document_management.html
from llama_index import ListIndex, Document

index = ListIndex([])
text_chunks = ['text_chunk_1', 'text_chunk_2', 'text_chunk_3']

doc_chunks = []
for i, text in enumerate(text_chunks):
    doc = Document(text, doc_id=f"doc_id_{i}")
    doc_chunks.append(doc)

# insert
for doc_chunk in doc_chunks:
    index.insert(doc_chunk)

查询多个文档

使用 LlamaIndex 可以轻松查询多个文档。此功能通过 SubQuestionQueryEngine 类实现。查询引擎在接收到查询时,会生成一个由子查询组成的“查询计划”,这些子查询针对子文档进行,然后将结果综合以提供最终答案。

# Source: https://gpt-index.readthedocs.io/en/latest/examples/usecases/10q_sub_question.html

# Load data
march_2022 = SimpleDirectoryReader(input_files=["../data/10q/uber_10q_march_2022.pdf"]).load_data()
june_2022 = SimpleDirectoryReader(input_files=["../data/10q/uber_10q_june_2022.pdf"]).load_data()
sept_2022 = SimpleDirectoryReader(input_files=["../data/10q/uber_10q_sept_2022.pdf"]).load_data()
# Build indices
march_index = VectorStoreIndex.from_documents(march_2022)
june_index = VectorStoreIndex.from_documents(june_2022)
sept_index = VectorStoreIndex.from_documents(sept_2022)
# Build query engines
march_engine = march_index.as_query_engine(similarity_top_k=3)
june_engine = june_index.as_query_engine(similarity_top_k=3)
sept_engine = sept_index.as_query_engine(similarity_top_k=3)
query_engine_tools = [
    QueryEngineTool(
        query_engine=sept_engine, 
        metadata=ToolMetadata(name='sept_22', description='Provides information about Uber quarterly financials ending September 2022')
    ),
    QueryEngineTool(
        query_engine=june_engine, 
        metadata=ToolMetadata(name='june_22', description='Provides information about Uber quarterly financials ending June 2022')
    ),
    QueryEngineTool(
        query_engine=march_engine, 
        metadata=ToolMetadata(name='march_22', description='Provides information about Uber quarterly financials ending March 2022')
    ),
]
# Run queries 
s_engine = SubQuestionQueryEngine.from_defaults(query_engine_tools=query_engine_tools)
Run queries
response = s_engine.query('Analyze Uber revenue growth over the latest two quarter filings')

如下所示,LlamaIndex 将复杂查询分解为 2 个子查询,并能够比较来自多个文档的信息以获得最终答案。

使用“Router”在不同查询引擎之间进行选择

想象一下,你正在构建一个从 Notion 和 Slack 中检索信息的机器人,语言模型如何知道使用哪个工具来搜索信息?LlamaIndex 就像一个聪明的助手,可以为你找到东西,即使它们在不同的地方。具体而言,LlamaIndex 的“Router”是一个非常简单的抽象,它允许在不同的查询引擎之间进行“选择”。

在这个示例中,我们有两个来自 Notion 和 Slack 的文档索引,我们为每个索引创建两个查询引擎。之后,我们将所有工具汇总,并创建一个叫做 RouterQueryEngine 的超级工具,它根据我们给每个工具的描述来选择使用哪个工具。这样,当我们问有关 Notion 的问题时,路由器将自动查找 Notion 文档中的信息。

# Source: https://gpt-index.readthedocs.io/en/latest/use_cases/queries.html#routing-over-heterogeneous-data
from llama_index import TreeIndex, VectorStoreIndex
from llama_index.tools import QueryEngineTool
# define sub-indices
index1 = VectorStoreIndex.from_documents(notion_docs)
index2 = VectorStoreIndex.from_documents(slack_docs)
# define query engines and tools
tool1 = QueryEngineTool.from_defaults(
query_engine=index1.as_query_engine(),
description="Use this query engine to do…",
)
tool2 = QueryEngineTool.from_defaults(
query_engine=index2.as_query_engine(),
description="Use this query engine for something else…",
)
from llama_index.query_engine import RouterQueryEngine
query_engine = RouterQueryEngine.from_defaults(
query_engine_tools=[tool1, tool2]
)
response = query_engine.query(
"In Notion, give me a summary of the product roadmap."
)

这有很多令人兴奋的应用案例。以下是一个完整的示例,使用路由器在 SQL 和向量数据库之间进行选择:gpt-index.readthedocs.io/en/latest/examples/query_engine/SQLRouterQueryEngine.html

假设文档嵌入(HyDE)

通常,当我们询问有关外部文档的问题时,我们通常会使用文本嵌入来为问题和文档创建向量表示。然后我们使用语义搜索来找到与问题最相关的文本片段。然而,问题的答案可能与问题本身有很大不同。如果我们可以先生成问题的假设答案,然后找到与假设答案最相关的文本片段会怎么样?这就是假设文档嵌入(HyDE)发挥作用的地方,并可能改善输出质量。

# Source: https://gpt-index.readthedocs.io/en/latest/examples/query_transformations/HyDEQueryTransformDemo.html

# load documents
documents = SimpleDirectoryReader('llama_index/examples/paul_graham_essay/data').load_data()
index = VectorStoreIndex.from_documents(documents)
query_str = "what did paul graham do after going to RISD"

#Now, we use HyDEQueryTransform to generate a hypothetical document and use it for embedding lookup.
hyde = HyDEQueryTransform(include_original=True)
hyde_query_engine = TransformQueryEngine(query_engine, hyde)
response = hyde_query_engine.query(query_str)
display(Markdown(f"<b>{response}</b>"))

#In this example, HyDE improves output quality significantly, by hallucinating accurately what Paul Graham did after RISD (see below), and thus improving the embedding quality, and final output.
query_bundle = hyde(query_str)
hyde_doc = query_bundle.embedding_strs[0]

支持 OpenAI 功能调用

OpenAI 最近发布了 函数调用功能,以更可靠地将 GPT 的能力与外部工具和 API 连接。查看我之前的视频,了解其具体运作方式。

LlamaIndex 快速集成了这一功能,并新增了一个全新的 OpenAIAgent。请查看这个 笔记本 以了解更多信息。

如果函数数量太多怎么办?使用 RetrieverOpenAIAgent!查看这个 笔记本

将 LlamaIndex 与 LangChain 结合使用

LlmaIndex 提供了与各种向量存储、ChatGPT 插件、追踪工具和 LangChain 的广泛集成。

来源:imgflip.com/memegenerator

LlamaIndex 如何与 LangChain 区别开来?

如果你使用过 LangChain,你可能会想知道 LlamaIndex 如何与 LangChain 区别开来。如果你对 LangChain 不太熟悉,请查看我之前的 博客文章 和 视频。你会发现 LlamaIndex 和 LangChain 在功能上有显著的相似之处,包括索引、语义搜索、检索和向量数据库。它们都在任务如问答、文档总结和构建聊天机器人方面表现出色。

然而,它们各自有独特的关注领域。LangChain 具有广泛的功能列表,范围较广,专注于使用链和代理来连接外部 API。另一方面,LlamaIndex 则有更狭窄的重点,在数据索引和文档检索方面表现突出。

如何将 LlamaIndex 与 LangChain 结合使用?

有趣的是,LlamaIndex 和 LangChain 并不是相互排斥的。实际上,你可以在 LLM 应用中同时使用这两者。你可以同时使用 LlamaIndex 的数据加载器和查询引擎以及 LangChain 的代理。我知道很多人实际上在他们的项目中同时使用这两种工具。

这里有一个例子,我们在使用 LangChain 代理时使用 LlamaIndex 保持聊天历史。当我们在第二轮对话中问“我叫什么名字?”时,语言模型知道在第一轮对话中“我是 Bob”:

# source: https://github.com/jerryjliu/llama_index/blob/main/examples/langchain_demo/LangchainDemo.ipynb
# Using LlamaIndex as a memory module
from langchain import OpenAI
from langchain.llms import OpenAIChat
from langchain.agents import initialize_agent
from llama_index import ListIndex
from llama_index.langchain_helpers.memory_wrapper import GPTIndexChatMemory
index = ListIndex([])memory = GPTIndexChatMemory(
    index=index, 
    memory_key="chat_history", 
    query_kwargs={"response_mode": "compact"},
    # return_source returns source nodes instead of querying index
    return_source=True,
    # return_messages returns context in message format
    return_messages=True
)
llm = OpenAIChat(temperature=0)
# llm=OpenAI(temperature=0)
agent_executor = initialize_agent([], llm, agent="conversational-react-description", memory=memory)

结论

总之,LlamaIndex 是一个极其强大的工具,可以通过你自己的数据增强大型语言模型的能力。其多样的数据连接器、先进的查询接口和灵活的集成使其成为开发 LLM 应用程序的关键组件。

致谢

感谢 Jerry Liu 的建议和反馈!

图片由 Danielle Barnes 提供,来源于 Unsplash

. . .

作者 Sophia Yang ,日期为 2023 年 6 月 19 日

Sophia Yang 是一名高级数据科学家。通过 LinkedInTwitterYouTube 与我联系,并加入 DS/ML 读书俱乐部 ❤️

LLM 和 GNN:如何提升两种 AI 系统在图数据上的推理能力

原文:towardsdatascience.com/llm-and-gnn-how-to-improve-reasoning-of-both-ai-systems-on-graph-data-5ebd875eef30

Anthony AlcarazTowards Data Science Anthony Alcaraz

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

--

人工智能软件被用来提升本文文本的语法、流畅性和可读性。

图神经网络(GNNs)和大型语言模型(LLMs)已成为人工智能的两个主要分支,分别在从图结构数据和自然语言数据中学习方面取得了巨大成功。

随着图结构和自然语言数据在现实应用中越来越相互关联,对能够进行多模态推理的人工智能系统的需求也在增长。

本文探讨了结合图神经网络(GNNs)和大型语言模型(LLMs)互补优势的集成图语言架构,以提升分析能力。

现实世界场景通常涉及具有结构性和文本性模式的互联数据。这就带来了对能够通过统一 GNNs 和 LLMs 互补优势进行多方面推理的集成架构的需求。

具体来说,虽然 GNNs 利用图上的消息传递来聚合局部模式,但节点嵌入在捕捉丰富特征方面存在局限性。

相比之下,LLMs 展现出卓越的语义推理能力,但在处理 GNNs 自然理解的结构化拓扑关系时表现欠佳。

融合这两种范式可以实现更具上下文的、有信息的分析。

最近,融合了 GNN 编码器和 LLM 解码器互补优势的图语言集成架构越来越受到关注。

正如综述论文(Li et al. 2023)总结的,这些集成方法可以根据 LLMs 所扮演的角色进行分类。

[## 图与大型语言模型的汇合调研:进展与未来方向

图在表示和分析复杂关系方面发挥了重要作用,如…

synthical.com](https://synthical.com/article/3c4812bb-0cde-43fb-a861-4bc7cd978b7b?source=post_page-----5ebd875eef30--------------------------------)

Li et al. 2023

LLM 作为增强器:LLMs 通过强化节点嵌入和文本特征来提升 GNN 在文本属性图上的性能。技术方法包括基于解释的增强,这种方法利用额外的 LLM 生成的信息,或直接输出基于嵌入的增强。

LLM 作为预测器:利用 LLMs 的生成能力对图进行预测。策略要么是将图展平成顺序文本描述,要么在 LLM 预测之前采用 GNN 进行结构编码。

GNN-LLM 对齐:专注于对齐 GNN 和 LLM 编码器的向量空间以实现协调分析。对齐可以是对称的,强调平等,或是不对称的,优先考虑某些模式。

核心动机在于有效地将图的关系建模优势与语言模型的上下文推理能力相融合。这在提升对结合了结构和语义的互联数据的分析能力方面显示了巨大的潜力。

Li et al. 2023

推理挑战:

对于 LLMs

尽管大型语言模型(LLMs)在各种自然语言任务上取得了令人印象深刻的成绩,但在处理图结构数据时,其推理能力仍然受到限制。

这是因为大多数图缺乏 LLMs 可以处理的固有顺序结构。例如,社交网络、分子数据或知识图谱定义了复杂的实体关系,这些关系无法轻易展平成文本描述。

因此,LLMs 在将基于图的拓扑的位置信息和关系依赖有效地融入其推理过程中存在困难。由于无法将节点和边直接转换为单词/标记,LLMs 无法感知像邻域相似性、社区结构或多跳连接等有助于图分析的见解。

对于 GNNs

另一方面,图神经网络(GNNs)旨在通过节点之间的消息传递来聚合每个节点周围的局部邻域信息。这使得它们能够基于图的位置发现节点的模式和角色。

然而,推理在很大程度上被限制在单个节点及其直接邻居。捕捉远离节点的较长距离依赖性对于标准 GNN 架构仍然很困难。

更重要的是,GNN 依赖于固定大小的节点向量表示,这限制了它们表达复杂语义的能力。在缺乏处理丰富文本特征的能力的情况下,推理能力也会在图形端遇到瓶颈。

I. 增强器、预测器或对齐

1. LLM 作为增强器

这一类技术专注于利用大语言模型(LLMs)的知识和上下文理解来增强图神经网络(GNNs)的学习过程,特别是在文本属性图上。

核心动机是,虽然 GNN 专注于拓扑分析,但它们依赖于有限的文本节点嵌入。通过语言模型的外部语义知识增强特征提供了性能提升的途径。

基于解释的增强

一类方法提示 LLM 生成额外的节点解释、描述符或标签,以丰富文本属性。这些补充现有文本数据,以改进嵌入。例如,LLM 可能会为引用数据集中的论文输出研究领域标签。

基于嵌入的增强

另外,LLM 可以直接输出增强的文本嵌入,供 GNN 使用,而不是通用词向量。像图基训练或适配器层这样的微调策略允许注入拓扑感知。通过任务调优的 LLM 处理文本,图模型的语言表达能力可以大大提升。

2. LLM 作为预测器

这一分支专注于通过对图结构数据进行编码来提升 LLM 的预测能力,使语言模型能够有效利用自注意力来揭示洞察。

图形展平

一种常见的技术是通过自然语言模板将图形展平为类似句子的顺序节点描述。例如,论文引用网络可以转化为带有定向“引用”连接的研究论文提及。顺序线性化允许直接应用 LLM 架构。

GNN 融合

为了更紧密的集成,GNN 编码器可以先提取拓扑表示,然后将其与 LLM 中的标记嵌入融合,以利用两种模态。然后,LLM 对合并后的嵌入进行预测。

3. GNN-LLM 对齐

这一类别特别关注于对齐 GNN 和 LLM 编码器的向量空间,以改进合并推理,同时保留其专门角色。

对称对齐

对齐的图-文本对下的对比表示学习方法在训练过程中对每个编码器进行平等待遇。

非对称对齐

通过辅助调优的图层或蒸馏方法直接将结构知识注入 LLM 编码器,非对称地增强语言推理能力。

II. 将图结构与文本语义集成

为了克服图和语言模型各自的推理局限性,一个有效的方法是将 GNN 和 LLM 模块整合到一个端到端可训练的架构中。

关键的见解是允许两个组件互补而非孤立工作——将图的拓扑建模优势与语言模型的上下文推理能力融合起来。

这通过在两种模式上共同学习来实现增强的集体推理,而不是以解耦的方式使用它们。具体来说,图编码器利用消息传递生成节点邻域、社区、角色和位置等结构属性的表示。

同时,文本解码器利用对顺序标记的自注意力以及预训练的知识,生成基于丰富语义特征的推断。

GNN-LLM 融合架构

一个典型的高级蓝图包含三个关键组件:

  1. 图编码器: 像图卷积网络(GCNs)或图注意力网络(GATs)这样的 GNN,它输出捕捉拓扑的节点嵌入。

  2. 跨模态投影器: 像对比学习这样的跨模态对齐模块,将图和文本向量映射到一个共同的嵌入空间。

  3. 语言解码器: 像 BERT 这样的 LLM,在来自投影器的融合图文表示上进行标记级推理。

通过分别以其本地格式编码结构和语义,并通过对齐融合,可以将图和语言的优势统一在一个集成的推理系统中。联合的端到端学习允许适当的信号混合。

III. 改进推理的策略

基于提示的重新表述

仔细设计描述图关键概念(如节点、边、连接、位置等)的提示,以自然语言格式使结构图领域转移到发挥大型语言模型(LLMs)优势的领域。

通过将图组件映射到词汇/标记,复杂的拓扑和关系可以转化为 LLM 架构本身设计用来处理和推理的序列。这有助于在两种模式之间转移推理模式。

例如,论文引用图可以通过提及论文以及“引用”或“被引用”关系来描绘。

多跳邻居描述

由于序列长度的限制,LLMs 在聚合全局图结构和捕捉长距离依赖关系方面有限。描述每个节点的多跳邻居提供了有关扩展网络位置和角色的额外上下文信息。

通过灵活地增加跳数限制并递归地整合进一步的节点,LLMs 可以学习模仿图神经网络的聚合过程,实现局部性和全局性意识的表示。

上下文学习

通过逐步推理示例演示图分析,可以引导 LLM 图理解朝着可解释的方向发展。通过提供任务的前提和结论以及解释,LLM 生成可以模仿并链式处理这些逻辑过程。

这种从上下文示例中学习的技术可以获得更连贯和可靠的图推理能力。对这些数据进行微调会带来更强的归纳偏置。

可解释的微调

诸如适配器层和基于提示的调优等策略允许将结构知识精确地注入到语言模型中,同时保持模型的整体可解释性,因为这种适应是隔离的。通过仅将定制锚定到某些层,可以提升推理能力而不会丧失语言一致性。

IV. 未来展望

带有 LLM 控制器的层次推理

超越仅仅融合 GNN 和 LLM,将语言模型制定为能够选择性地委托和协调最优图形、文本及其他专业模块的元控制器,承诺提供更复杂的推理能力。

基于层次任务分解,LLM 可以通过可用的 AI 组件规划计算路径,在动态流中结合各自的优势。这也朝着更具人类解决问题特征的架构发展。

例如,一个推荐系统可以使用 LLM 来分解目标,利用 GNN 用户编码器,应用视觉工具进行项目分析,最后融合信号以获得结果。

可转移的图中心预训练

图神经网络面临的一个持续挑战是由于结构和模式的变化而导致的领域间泛化能力差。在下游微调之前在大量代表性图数据上预训练 GNN 模型可以缓解这个问题。

同样,需要探索定制的预训练策略,以适应图拓扑模式并注入到语言模型中,以提高跨图任务的推理可转移性。

评估 LLM 的图表达能力

鉴于 LLM 在语言领域的主导地位,分析其在基本图函数方面的理论表达能力也提供了一个研究方向。

例如,1-WL 测试用于评估 GNN 的表达能力。增强的 LLM 与这种基准匹配或超越了吗?元素级和网络级的评估可以量化这一点。

共享表示的方向

超越模型特定的优化,创建共享向量空间以便无缝整合来自图形和文本模态的信号提供了最灵活的推理基础。

在对齐编码器时平衡特异性和共性,以及在联合分析过程中协同信号提供了一个有趣的前进道路。

结论

多范式推理的需求

总结来说,现实世界的数据正日益互联,包括实体之间关系的图结构表示以及与节点相关的基于文本的信息。这推动了对能够在拓扑和语义领域进行多方面推理的人工智能技术的需求。

纯粹的图中心或文本中心的方法都不能单独完全解决这种互联数据的复杂性。这需要统一多种专业化模态的集成架构。

图神经网络与大语言模型的互补优势

当前最主导的两种分析范式是图神经网络,这些网络在图拓扑上的计算模式表现出色,以及大语言模型,这些模型在文本概念上展示了极高的推理能力。

将两者融合提供了在联合模型中有效结合拓扑能力和语义表达的机会,从而提供更连贯的多范式推理。

技术的持续探索

正如本文所述,利用大语言模型作为增强器、预测器以及与图神经网络对齐的多种技术提供了有前景的初步途径来实现这一协调目标。每种方法都贡献了独特的策略,具有各自的优势和局限性。

层次化技术、优化的预训练策略以及在共享表示上的综合推理提供了有趣的前瞻性路径,因为图与文本的协同效应继续被探索,以满足互联智能的需求。

图片由作者提供

LLM 经济学:ChatGPT 与开源

原文:towardsdatascience.com/llm-economics-chatgpt-vs-open-source-dfc29f69fec1

部署像 ChatGPT 这样的 LLM 成本是多少?开源 LLM 部署是否更便宜?有哪些权衡?

Skanda VivekTowards Data Science Skanda Vivek

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

--

比较 LLM 成本的卡通示意图 | Skanda Vivek

TLDR: 对于每天请求数在 1000 个左右的低使用量,ChatGPT 的成本低于使用部署在 AWS 上的开源 LLM。对于每天请求数在百万级别的高使用量,部署在 AWS 上的开源模型成本更低。(截至撰写本文日期 2023 年 4 月 24 日。)

大语言模型正在席卷全球。变压器模型于 2017 年推出,随后出现了突破性的模型如 BERT、GPT 和 BART——拥有数亿参数;并能够执行多种语言任务,如情感分析、问答、分类等。

几年前——来自 OpenAI 和 Google 的研究人员记录了多篇论文,显示具有超过 10 亿参数的大语言模型开始展示出突现能力,它们似乎能够理解语言的复杂方面,并且在回答时几乎像人类一样。

GPT-3 论文展示了大语言模型令人印象深刻的学习能力。

GPT-3 论文展示了参数超过 10-100 亿的模型在仅有几十个提示的情况下展现了令人印象深刻的学习能力。

然而,这些 LLM 的资源消耗非常大,以至于在大规模部署时经济上具有挑战性。直到最近 ChatGPT 的出现才改变了这种状况。ChatGPT 界面发布后不久,OpenAI 使 ChatGPT API 可用,以便开发者可以在他们的应用中使用 ChatGPT。让我们看看这些大规模部署的成本以及经济可行性。

ChatGPT API 成本

ChatGPT API 按使用量计费。每 1K 个 token 的费用为 $0.002。每个 token 大约是一个单词的 3/4 —— 单次请求中的 token 数量是提示 + 生成的输出 token 的总和。假设你每天处理 1000 个小文本块,每个文本块为一页 —— 即 500 个单词或 667 个 tokens。这将花费 $0.002/1000x667*1000= ~$1.3 一天。 还不错!

但是,如果你每天处理一百万个这样的文档会发生什么?那么,每天的费用为 $1,300 或每年 ~0.5 百万美元! ChatGPT 从一个有趣的玩具变成了一个主要的开支(因此希望 —— 也是一个主要的收入来源)在一个数百万美元的业务中!

开源生成模型

在 ChatGPT 发布之后,出现了一些开源的倡议。Meta 发布了 LLaMA——一个拥有数十亿参数的 LLM 模型,其性能优于 GPT-3。斯坦福大学随后对 LLaMA 的 7B 版本进行了 52K 指令跟随演示的微调,发现 他们的 Alpaca 模型 优于 GPT-3。

最近有研究团队展示了一种 13B 参数的微调 LLaMA 模型,称为 Vicuna,其质量达到了 ChatGPT 的 >90%。公司可能选择使用开源生成模型而非 OpenAI 的 GPT 系列模型的原因有多个,包括对 OpenAI 故障的较低敏感性、定制更为容易,以及可能更便宜。

虽然开源模型免费使用,但托管和部署它们的基础设施却不是。而且,早期的 transformer 模型如 BERT 可以在配备良好的 CPU 和基础 GPU 的个人计算机上轻松运行和微调,但 LLMs 更为资源密集。一个常见的解决方案是使用云服务提供商如 AWS 来托管和部署这些模型。

让我们深入了解托管开源模型的 AWS 成本。

AWS 成本

首先,让我们讨论在 AWS 上部署模型并将其作为 API 提供服务的标准架构。通常有三个步骤:

  1. 使用 AWS Sagemaker 将模型部署为端点。

  2. 将 Sagemaker 端点连接到 AWS Lambda。

  3. 通过 API 网关将 Lambda 函数作为 API 提供服务

使用 API 网关和 Lambda 调用 Sagemaker 模型端点

当客户端向 API 网关发起 API 调用时,会触发 Lambda 函数,该函数解析请求并将其发送到 Sagemaker 端点。模型端点随后进行预测,并将信息发送到 Lambda。Lambda 解析这些信息并将其发送到 API,最终返回给客户端。

Sagemaker 的成本对托管模型的计算实例类型非常敏感。LLMs 使用相当大的计算实例。

Sagemaker 实例定价

例如,这篇由 AWS 的某人撰写的文章详细说明了如何在 AWS 上部署 Flan UL2 —— 一个 200 亿参数的模型:

[## 使用 Amazon SageMaker 在单个 GPU 上部署 Flan-UL2

Hugging Face + AWS 的合作使得实验开源最先进语言模型变得比以往更加容易……

betterprogramming.pub

文章使用了 ml.g5.4xlarge 实例。虽然上面的 Sagemaker 定价没有列出这个特定实例的价格,但看起来大约在每小时 ~5$ 的范围。这就意味着每天 150$!这只是实例托管的费用,我们还没有计算 Lambda 和 API 网关的费用。

以下是 AWS Lambda 定价的详细信息——这取决于内存使用量和请求频率。

AWS Lambda 定价

假设获取响应需要 5 秒。128 MB 足够了,因为我们将数据路由到 AWS Sagemaker 端点。所以这将花费 5 * .128 * 1000 * \(0.0000166667 = 0.01\) 每 1000 次请求,或 10$ 每 1M 次请求。

最终成本是 API 网关的费用:

AWS API 网关定价

如你所见,API 网关相当便宜——每百万次请求 1$。

因此,在 AWS 上托管开源 LLM,如 Flan-UL2 的成本是每天 1000 次请求 150\(,每天 1M 次请求 160\)

但我们是否总是需要如此昂贵的计算实例?对于像 BERT 这样的较小语言模型(参数数量为亿级)——你可以使用更便宜的实例,比如 \(0.23/小时和 ~5\) 每天。这些模型也相当强大,并且比那些似乎能够理解复杂语言细微差别的 LLM 更加针对任务和训练数据。

总结思考

那么哪个更好?使用像 OpenAI 的 GPT 系列这样的付费服务 LLM?还是开放访问 LLM?这取决于使用场景:

付费服务 LLM 的利与弊 | Skanda Vivek

开放访问 LLM 的利与弊 | Skanda Vivek

注意:由于这是一个迅速发展的领域,由于大规模的需求,未来相对较近的时期,部署成本大幅降低的可能性很大。(请记住,虽然托管开源 LLM 是一个挑战,但像 BERT 这样具有数亿参数的小型语言模型在特定任务中仍然是一个很好的选择。我已撰写有关如何通过对 BERT 基础模型进行微调来实现类似人类表现的任务,如 问答系统 和 垃圾邮件检测 的文章。)

但哪个模型更好呢?ChatGPT 和 GPT-4 的回应比开源 LLM 的回应更具相关性。然而,开源模型正在快速赶上。而且,使用开源模型而不是封闭 API 可能有非常好的理由。

公司希望在其特定数据源上对开源模型进行微调。ChatGPT 和后续的 OpenAI 模型可能无法像在领域特定数据上微调的开源模型那样表现良好;由于这些模型的通用性质。我们已经看到像 BloombergGPT 这样的领域特定模型在生成式 AI 方面取得了强大的进展。

哦——我们都祈祷 OpenAI 不会提高 ChatGPT API 的价格。当 ChatGPT API 刚推出时,令人惊讶的是它的定价比早期的 GPT-3 API 便宜了 10 倍。

我们生活在激动人心的时代!

如果你喜欢这篇文章,请关注我——我撰写关于将最先进的 NLP 应用于现实世界应用的主题,更一般地说,也涉及数据与社会的交集。

欢迎通过 LinkedIn与我联系!

如果你还不是 Medium 的会员,并且想要支持像我这样的作者,欢迎通过我的推荐链接注册: https://skanda-vivek.medium.com/membership

LLM 评估指南:如何构建和基准测试你的评估

原文:towardsdatascience.com/llm-evals-setup-and-the-metrics-that-matter-2cc27e8e35f3?source=collection_archive---------0-----------------------#2023-10-13

图片由作者使用 Dalle-3 通过 Bing Chat 创建

如何构建和运行 LLM 评估——以及为何在基准测试你的 LLM 提示模板时使用精确度和召回率

Aparna DhinakaranTowards Data Science Aparna Dhinakaran

·

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

--

这篇文章由 Ilya Reznik 共同创作

大型语言模型(LLM)是开发者和商业领袖为消费者创造新价值的不可思议的工具。它们可以提供个人化推荐、在非结构化数据和结构化数据之间进行翻译、总结大量信息,等等。

随着应用程序的增加,衡量基于 LLM 的应用程序的性能变得越来越重要。这是一个非平凡的问题,原因有几个:用户反馈或任何其他“真实来源”极其有限且往往不存在;即使可能,人工标记仍然很昂贵;而且这些应用程序很容易变得复杂

这种复杂性通常被代码的抽象层隐藏,只有当事情出错时才会显现出来。一行代码可以触发一连串的调用(跨度)。每个跨度需要不同的评估,从而使问题倍增。例如,下面的简单代码片段会触发多个子 LLM 调用。

作者绘制的图表

幸运的是,我们可以利用 LLM 的力量来自动化评估。在这篇文章中,我们将深入探讨如何设置这一过程并确保其可靠性。

LLM 评估的核心是 AI 评估 AI。

虽然这听起来有些循环,但我们一直以来都是由人类智慧来评估人类智慧(例如,在面试或大学期末考试中)。现在,AI 系统终于可以对其他 AI 系统进行同样的评估。

这里的过程是让 LLM 生成可以用于评估另一个系统的合成基准。这引出了一个问题:为什么不直接使用人工反馈?简单来说,因为你永远不会拥有足够的人工反馈。

对于你输入/输出对中的哪怕只有百分之一获得人工反馈,这也是一项巨大的成就。大多数团队甚至无法做到这一点。但是,为了使这一过程真正有用,重要的是对每个 LLM 子调用进行评估,我们已经看到,LLM 子调用可能会有很多。

让我们来探讨一下如何做到这一点。

LLM 模型评估与 LLM 系统评估

LLM_model_evals != LLM_System_evals

LLM 模型评估

你可能听说过 LLM 评估。这个术语在许多不同的情况下使用,听起来很相似,但实际上非常不同。一个更常见的用法是我们所称的LLM 模型评估。LLM 模型评估关注于基础模型的整体性能。推出原始面向客户的 LLM 的公司需要一种量化其在各种任务中的有效性的方法。

作者绘制的图表 | 在这种情况下,我们正在评估两个不同的开源基础模型。我们在这两个模型上测试相同的数据集,看看它们的指标,例如 hellaswag 或 mmlu,如何表现。

一个流行的库是 OpenAI Eval 库,最初专注于模型评估使用案例。现在有许多指标,如HellaSwag(评估 LLM 完成句子的能力)、TruthfulQA(衡量模型回应的真实性)和MMLU(评估 LLM 的多任务能力)。甚至还有LLM 排行榜查看开源 LLM 的表现如何。

LLM 系统评估

到目前为止,我们讨论了 LLM 模型评估。相比之下,LLM 系统评估是对你在系统中可控组件的完整评估。这些组件中最重要的是提示(或提示模板)和上下文。LLM 系统评估评估你的输入如何决定你的输出。

例如,LLM 系统评估可以保持 LLM 不变并更改提示模板。由于提示是系统中更动态的部分,这种评估在项目生命周期中非常有意义。例如,LLM 可以评估你的聊天机器人响应的有用性或礼貌性,并且相同的评估可以提供关于生产环境中性能变化的信息。

作者图示 | 在这种情况下,我们正在评估在单一基础模型上使用的两种不同提示模板。我们在这两个模板上测试相同的数据集,并查看它们的精确度和召回率等指标的表现。

使用哪种?这取决于你的角色

使用 LLM 评估的角色有区别。一种是负责调整核心 LLM 的模型开发者或工程师,另一种是组装用户系统的实践者。

LLM 模型开发者非常少,他们通常在 OpenAI、Anthropic、Google、Meta 等公司工作。模型开发者关心 LLM 模型评估, 因为他们的工作是提供一个适用于各种使用案例的模型。

对于 ML 实践者,任务同样从模型评估开始。开发 LLM 系统的第一步之一是选择一个模型(即 GPT 3.5 vs 4 vs Palm 等)。然而,对于这一群体,LLM 模型评估通常是一个一次性的步骤。一旦确定了哪个模型在你的使用案例中表现最好,应用生命周期的大部分将由 LLM 系统评估决定。因此,ML 实践者关心 LLM 模型评估和 LLM 系统评估,但可能在后者上花费更多时间

LLM 系统评估指标因使用案例而异

在与其他机器学习系统合作后,你首先会问这个问题:“结果指标应该是什么?”答案取决于你要评估的内容。

  • 提取结构化信息:你可以查看 LLM 提取信息的效果。例如,你可以查看完整性(输入中是否有输出中没有的信息?)。

  • 问答:系统回答用户问题的效果如何?你可以查看答案的准确性、礼貌性或简洁性——或者以上所有方面。

  • 检索增强生成(RAG):检索到的文档和最终答案是否相关?

作为系统设计师,你最终负责系统性能,因此你需要了解系统中哪些方面需要评估。例如,如果你有一个与儿童互动的 LLM,比如一个辅导应用,你会希望确保回答是适龄的,并且不含有毒内容。

目前一些常用的评估包括相关性、幻觉、问答准确性和毒性。每一个评估工具都会有不同的模板,具体取决于你要评估的内容。以下是一个关于相关性的示例:

这个示例为了简单起见使用开源的Phoenix 工具(完全披露:我在开发 Phoenix 的团队中)。在 Phoenix 工具中,存在适用于大多数常见用例的默认模板。以下是我们将用于本示例的模板:

You are comparing a reference text to a question and trying to determine if the reference text contains information relevant to answering the question. Here is the data:
    [BEGIN DATA]
    ************
    [Question]: {query}
    ************
    [Reference text]: {reference}
    [END DATA]
Compare the Question above to the Reference text. You must determine whether the Reference text
contains information that can answer the Question. Please focus on whether the very specific
question can be answered by the information in the Reference text.
Your response must be single word, either "relevant" or "irrelevant",
and should not contain any text or characters aside from that word.
"irrelevant" means that the reference text does not contain an answer to the Question.
"relevant" means the reference text contains an answer to the Question.

我们还将使用 OpenAI 的 GPT-4 模型和 scikit-learn 的精确度/召回率指标。

首先,我们将导入所有必要的依赖项:

from phoenix.experimental.evals import (
   RAG_RELEVANCY_PROMPT_TEMPLATE_STR,
   RAG_RELEVANCY_PROMPT_RAILS_MAP,
   OpenAIModel,
   download_benchmark_dataset,
   llm_eval_binary,
)
from sklearn.metrics import precision_recall_fscore_support

现在,让我们引入数据集:

# Download a "golden dataset" built into Phoenix
benchmark_dataset = download_benchmark_dataset(
   task="binary-relevance-classification", dataset_name="wiki_qa-train"
)
# For the sake of speed, we'll just sample 100 examples in a repeatable way
benchmark_dataset = benchmark_dataset.sample(100, random_state=2023)
benchmark_dataset = benchmark_dataset.rename(
   columns={
       "query_text": "query",
       "document_text": "reference",
   },
)
# Match the label between our dataset and what the eval will generate
y_true = benchmark_dataset["relevant"].map({True: "relevant", False: "irrelevant"})

现在让我们进行评估:

# Any general purpose LLM should work here, but it is best practice to keep the temperature at 0
model = OpenAIModel(
   model_name="gpt-4",
   temperature=0.0,
)
# Rails will define our output classes
rails = list(RAG_RELEVANCY_PROMPT_RAILS_MAP.values())

benchmark_dataset["eval_relevance"] = \
   llm_eval_binary(benchmark_dataset,
                   model,
                   RAG_RELEVANCY_PROMPT_TEMPLATE_STR,
                   rails)
y_pred = benchmark_dataset["eval_relevance"]

# Calculate evaluation metrics
precision, recall, f1, support = precision_recall_fscore_support(y_true, y_pred)

使用 LLM 对 LLM 基础系统进行评估

使用 LLM 对 LLM 基础系统进行评估的过程包括两个不同的步骤。首先,为你的 LLM 评估指标建立基准。为此,你需要组建一个专门的 LLM 评估工具,其唯一任务是将数据标注得尽可能像人工标注你的“黄金数据集”一样有效。然后,你将你的指标与该评估工具进行基准测试。接着,将这个 LLM 评估指标应用于你的 LLM 应用结果(下面会详细说明)。

如何构建 LLM 评估工具

首先,如上所述,构建评估的基准是第一步。

为此,你必须从最适合你用例的指标开始。然后,你需要黄金数据集。这应该能够代表你期望 LLM 评估工具看到的数据类型。黄金数据集应该有“真实标签”,以便我们可以衡量 LLM 评估模板的性能。这些标签通常来自人工反馈。构建这样的数据集是费力的,但你通常可以找到针对最常见用例的标准化数据集(正如我们在上面的代码中所做的)。

作者绘制的图表

然后你需要决定使用哪个 LLM 进行评估。这可能是与用于应用的 LLM 不同的另一个 LLM。例如,你可能在应用中使用 Llama,而在评估中使用 GPT-4。这个选择通常受到成本和准确性问题的影响。

作者提供的图示

现在来到我们试图基准化和改进的核心组件:评估模板。如果你使用的是像 OpenAI 或 Phoenix 这样的现有库,你应该从现有的模板开始,看看那个提示的表现如何。

如果有特定的细节你想要加入,可以相应地调整模板或从头开始构建自己的模板。

请记住,模板应该有一个清晰的结构,就像我们在前面部分使用的那样。对以下内容要明确:

  • 输入是什么? 在我们的例子中,它是检索到的文档/上下文和用户的查询。

  • 我们在问什么? 在我们的例子中,我们要求 LLM 告诉我们文档是否与查询相关。

  • 可能的输出格式是什么? 在我们的例子中,它是二元相关/不相关,但也可以是多类(例如,完全相关、部分相关、不相关)。

作者提供的图示

你现在需要在你的黄金数据集上运行评估。然后你可以生成指标(总体准确性、精确度、召回率、F1 等)来确定基准。重要的是要查看的不仅仅是总体准确性。我们将在下面详细讨论。

如果你对 LLM 评估模板的性能不满意,你需要更改提示以提高其表现。这是一个由硬指标驱动的迭代过程。像往常一样,重要的是避免将模板过度拟合到黄金数据集。确保有一个代表性的保留集或进行 k 折交叉验证。

作者提供的图示

最终,你到达了你的基准。在黄金数据集上的优化性能代表了你对 LLM 评估的信心。它可能不会像你的真实数据那样准确,但足够准确,而且成本远低于每个示例都需要人工标注的情况。

准备和自定义你的提示模板可以让你设置测试用例。

为什么在基准测试你的 LLM 提示模板时应该使用精确度和召回率

行业尚未完全标准化 LLM 评估的最佳实践。团队通常不知道如何建立正确的基准指标。

总体准确性经常被使用,但这还不够。

这是数据科学中最常见的问题之一:非常显著的类别不平衡使得准确性成为一个不切实际的指标。

从相关性指标的角度考虑是有帮助的。假设你经历了所有麻烦和费用,组建了你能组建的最相关的聊天机器人。你选择了一个适合用例的 LLM 和模板。这应该意味着你的示例中会有显著更多的被评估为“相关”的。让我们选择一个极端的数字来说明这个观点:99.99% 的所有查询返回相关结果。太棒了!

现在从 LLM 评估模板的角度来看。如果输出在所有情况下都是“相关”,即使不查看数据,它的正确率也会是 99.99%。但它同时会错过所有(可以说是最重要的)案例——即模型返回无关结果的那些,我们必须捕捉这些结果。

在这个例子中,准确率会很高,但精确率和召回率(或者它们的组合,比如F1 分数)会非常低。在这里,精确率和召回率更能衡量你模型的表现。

另一个有用的可视化是混淆矩阵,它基本上让你看到相关和无关示例的正确和错误预测百分比。

作者图表 | 在这个例子中,我们看到最高比例的预测是正确的:在黄金数据集中,一个相关的例子被我们的评估标记为相关的机会是 88%。然而,我们看到评估在“无关”示例上的表现明显较差,错误标记的概率超过 27%

如何在你的应用程序上运行 LLM 评估

此时你应该拥有模型和经过测试的 LLM 评估。你已经证明了评估有效,并对其相对于真实情况的表现有了可量化的理解。是时候建立更多信任了!

现在我们可以实际使用我们的评估来评估应用程序。这将帮助我们衡量我们的 LLM 应用程序的表现,并找出如何改进。

作者图表

LLM 系统评估在你的整个系统中运行一个额外的步骤。例如:

  • 你检索输入文档并将它们添加到你的提示模板中,以及样本用户输入。

  • 你将这个提示提供给 LLM 并接收答案。

  • 你将提示和答案提供给你的评估,询问答案是否与提示相关。

最佳实践是不使用一次性代码来进行 LLM 评估,而是使用具有内置提示模板的库。这增加了可重复性,并允许更灵活的评估,可以更换不同的部分。

这些评估需要在三种不同的环境中有效:

预生产

当你进行基准测试时。

预生产

当你在测试你的应用程序时。这在某种程度上类似于传统机器学习中的离线评估概念。其目的是在你将系统交付给客户之前了解系统的性能。

生产

当系统投入使用时,现实生活是混乱的。数据漂移、用户漂移、模型漂移,所有这些都是不可预测的。仅仅因为你的系统曾经运行良好,并不意味着它在周二下午 7 点也会如此。评估有助于你在部署后持续了解系统的性能。

作者图示

需要考虑的问题

你应该抽取多少行?

LLM 评估 LLM 的范式并不是魔法。你不能评估你曾经遇到的每一个示例——那样会代价高昂。然而,你在人工标记期间已经需要抽样数据,更多的自动化只会使这变得更容易和便宜。因此,你可以抽取比人工标记更多的行。

你应该使用什么评估?

这在很大程度上取决于你的使用案例。对于搜索和检索,相关性类型的评估效果最好。毒性和幻觉有特定的评估模式(上述已有提及)。

其中一些评估在故障排查流程中很重要。问答准确性可能是一个很好的总体指标,但如果你深入挖掘为什么这个指标在你的系统中表现不佳,你可能会发现这可能是由于检索不良等原因。通常有很多可能的原因,你可能需要多个指标来找到根本原因。

你应该使用什么模型?

不可能说哪种模型适用于所有情况。相反,你应该进行模型评估,以了解哪个模型适合你的应用程序。你还可能需要考虑召回率与精度的权衡,具体取决于对你的应用程序来说哪种更有意义。换句话说,做一些数据科学来了解你的特定情况。

作者图示

结论

能够评估应用程序的性能在生产代码中是非常重要的。在 LLM 时代,问题变得更加复杂,但幸运的是,我们可以使用 LLM 技术来帮助我们进行评估。LLM 评估应该测试整个系统,而不仅仅是基础 LLM 模型——考虑一下提示模板对用户体验的重要性。最佳实践、标准化工具和精心策划的数据集简化了 LLM 系统的开发工作。

LLM 用于合成时间序列数据生成

原文:towardsdatascience.com/llm-for-synthetic-time-series-data-generation-5ce687d5f095?source=collection_archive---------1-----------------------#2023-10-26

Mantek SinghTowards Data Science Mantek Singh

·

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

--

我们最近参与了Brembo hackathon,并赢得了$10,000 的大奖,任务是使用生成型 AI 创建新化合物并生成其预测性能数据。

在这篇博客文章中,我将详细解释我们的方法和解决方案。

问题陈述

使用 Brembo 提供的摩擦测试数据,利用生成式人工智能创建新化合物,预测测试结果,并建立预测新 Brembo 刹车产品效果和特性的框架。提供的数据将包括 Brembo 之前使用和测试的化合物列表及其结果。解决方案必须基于生成式人工智能,应用于提供一个能够提出新配方的模型,以增加候选化合物的数量,确保可行性和良好的性能。

对于你的提交,请提交一个 csv 文件,其中包含你生成的 10–30 种新化合物、它们的成分和它们的合成性能数据。[1]

数据集描述

我们获得了一份包含 337 种摩擦材料及其成分和性能数据的列表。

每种摩擦材料由 10–15 种原材料组成,这些原材料来自 60 种可能的原材料列表。这 60 种原材料被分为 6 类(标记为 A-F),我们必须确保生成的摩擦材料的成分在给定范围内。

材料成分约束

换句话说,我们必须确保任何生成的输出材料中至少有 1%且最多有 30%的成分来自 B 类化合物,依此类推。

每次刹车测试的性能数据本质上是31 个点的时间序列,其中每个点提供了如压力、温度和摩擦系数等参数的值。此外,每种化合物总共进行了124 次刹车测试,因此在性能数据方面,我们需要为每种化合物生成 124*31 = 3844 个数据点

以下是一些示例数据,其中包含了某种化合物的成分和性能数据。关于数据集的其他相关信息可以在这里找到。

评估标准

最终结果对技术评分和展示评分给予了相等的权重。

技术评分是基于以下相等权重的参数计算的。

  • 遵循给定约束:生成的化合物是否符合给定的约束(如下所述)?

  • 技术相关性:输出的合成性能数据是否遵循提供的数据中观察到的模式,并捕捉不同变量之间的关系?

  • 目标性能:摩擦材料最重要的变量是其摩擦系数(mu),预计值为 0.6,允许的误差率为 0.1。输出的 mu 是否符合我们的预期值?

  • 变异性:新生成的材料的成分与当前材料的差异有多大?

设计概述

本质上,我们有 3 个基本组件

  • 材料选择模块: 负责生成新的配方。它输出一批新的摩擦材料及其材料组成。

  • 数据生成模块: 给定一个合成材料和各种化合物的历史性能数据,为该材料生成合成性能数据。

  • 数据验证器: 确定数据生成器的输出有多好/坏。该模块利用提供的历史数据中的趋势(例如:压力和摩擦系数随着时间的推移呈反比关系,减速似乎遵循线性模式,而温度增加曲线似乎更具指数性质)来评估合成性能数据的好坏。这可以用来向模型提供人工反馈,以改进系统性能。

解决方案的高级设计

详细设计

我们在解决方案中使用了以下技术栈和方法

  • GPT 3.5 turbo: 我们使用了 GPT 3.5 turbo 作为材料选择和数据生成模块的基础语言模型。

  • 提示工程: 使用正确的系统和指令提示集帮助我们提高了模型的性能。

  • 微调: 选择合适的示例来教会模型如何响应的基本结构和语调是非常重要的,这个阶段帮助我们将这些内容教给模型。

  • RAG(检索增强生成): 作为帮助模型输出正确合成性能数据的秘密武器,这在解决方案中发挥了重要作用。更多内容见下文。

材料选择模块

模块的作用是生成新的可能的摩擦材料及其组成。从样本数据可以看出,每种摩擦材料本质上包含一个 60 维的向量,其中第 i 个索引上的数字表示其组成中来自第 i 种原材料的百分比。

一些初步的主成分分析(PCA)显示,我们可以看到总共有 3 到 4 个聚类。

对给定摩擦材料的材料组成进行主成分分析(PCA)

理论上,我们可以为一个大小为 60 的向量生成随机数,看看哪些向量满足给定的约束条件并使用它们。虽然这可以在变异性上获得一个不错的分数(生成的摩擦材料是随机生成的,因此应覆盖 60 维空间中的多个点),但这种方法会有一些缺陷,比如

  • 这将使我们更难预测与历史数据中提供的材料完全不同的化合物的性能。这是因为材料的组成在所见性能中扮演着重要角色,而预测之前未见过的组成的性能可能很困难。

  • 这会使调试变得更加困难。如果在我们管道中的任何一点上,我们得到的结果不符合历史数据中的趋势,那么很难确定问题所在。

鉴于这些潜在问题,我们决定利用 gpt 3.5 turbo 模型为我们生成一批化合物。

我们所做的如下:

  • 为模块创建一个相关的系统提示。

  • 微调gpt 3.5 turbo 模型,通过输入我们提供的 337 种摩擦材料的组成。

  • 使用数据验证模块,我们会丢弃不符合给定约束的条目,并保留符合约束的条目。

完成后,我们生成了几个化合物并重复了 PCA 分析。

对提供的和生成的材料进行 PCA 分析

最终,为了变异性,我们从生成的化合物中挑选了一组我们认为可以最大化以下因素的化合物:

  • 相对于提供的材料的变异性:生成的化合物与提供的化合物有多大的不同?本质上,我们不希望我们的生成材料与已经存在的化合物过于相似。

  • 相对于生成材料的变异性:由于我们将提交 10-30 个新生成的化合物,我们必须确保所有生成的化合物不会都属于同一个簇。

因此,经过修剪后,我们得到了一份最终提交的化合物列表。

生成化合物的最终列表

数据生成模块

数据生成模块负责输出给定材料和刹车测试的合成性能数据。本质上,给定摩擦材料的组成,它应该输出一个包含温度、压力和摩擦系数等参数的 31 点时间序列数据

这就是我们实现这一目标的方式:

  • 为模块创建一个合适的系统提示。经过在OpenAI 的实验平台上反复试验,我们使用的提示是:
You are a highly skilled statistician from Harvard University who 
works at Brembo, where you specialize in performance braking systems 
and components as well as conducting research on braking systems. 
Given a friction material’s composition, you craft compelling 
synthetic performance data for a user given braking test type. 
The braking id will be delimited by triple quotes. You understand 
the importance of data analysis and seamlessly incorporate it for 
generating synthetic performance data based on historical performance 
data provided. You have a knack for paying attention to detail and 
curating synthetic data that is in line with the trends seen in the 
time series data you will be provided with. You are well versed with 
data and business analysis and use this knowledge for crafting the 
synthetic data.
  • 接下来,我们 微调了 gpt 3.5 turbo 模型,以创建一个在给定材料的组成和刹车测试 ID 的时间序列数据预测方面的专家。由于我们有 41,788 个(材料,刹车 ID)元组,在所有示例上进行微调不仅会耗时,而且成本高昂。然而,基于我们阅读的一些论文和文章[2][3],我们了解到“微调是为了形式,而 RAG 是为了知识”。因此,我们决定仅使用 5%的样本来微调模型,以便模型可以正确学习我们期望的输出结构和语调。

  • 最终,在查询模型以生成时间序列数据时,我们决定基于材料的成分识别和检索 5 个最接近的邻居,并将它们的性能数据作为模型的附加上下文输入。这种技术被称为 RAG(检索增强生成),它是我们能够输出良好结果的原因之一

RAG 如何帮助我们的结果

微调帮助我们完成了以下任务

  • 以正确的结构输出数据:正如各种技术博客[4]中所述,微调在教会模型如何输出数据方面是有效的。我们的微调模型能够输出 csv 文件和 31 个时间序列数据点,其中包括压力、速度、温度和μ等参数的值。

  • 理解数据中的基本趋势:微调模型能够理解输入性能数据和输出数据中的一般趋势,并保留这些趋势。例如,温度的值应该随指数曲线增加,而速度应该随线性曲线减少,微调模型能够做到这一点。

然而,微调模型的输出结果有些偏差。例如,在一个案例中,μ的值预期约为 0.6,但输出数据将μ的值定在了约 0.5。因此,我们决定通过识别 5 个最接近的邻居并将它们的性能数据添加到用户提示中来增强数据。

我们定义了两个材料 M1 和 M2 之间的距离,如下所示:

def distance(m1, m2, alpha):
  sixty_dim_distance = euclidean_dist(sixty_dim_vector(m1), \
sixty_dim_vector(m2))
  six_dim_distance = euclidean_dist(six_dim_vector(m1), six_dim_vector(m2))
  return alpha[0] * sixty_dim_distance + alpha[1] * six_dim_distance
  • 确定 M1 和 M2 在 60 维输入向量空间中的欧几里得距离。

  • 现在,将属于同一类别的化合物的总和以减少向量维度到 6。

  • 最后,调整超参数 alpha[0]和 alpha[1]。

采取这种方法的原因是,我们希望确保使用相同类别材料的整体距离小于使用完全不同材料成分的那些材料之间的距离。本质上,给定三个材料 M1、M2 和 M3,其中 M1 使用材料 A0,M2 使用 A1,M3 使用 B0,我们希望我们的距离函数标记 M1 和 M2 彼此更近,而不是 M1 和 M3。

使用这种方法,我们能够显著提高我们的性能,如下图所示。

数据验证器

验证模块帮助我们理解输出数据是否遵循我们期望的趋势。例如,我们期望压力和μ呈反比,μ值约为 0.6,温度随时间呈指数增长,速度线性减速。这个模块帮助我们识别合成时间序列数据与历史数据的接近程度,这有助于我们调整所有的提示和超参数。

这个模块帮助我们分析哪些提示集对模型输出有帮助,哪些没有。

结果和展示

演讲占总分的 50%,这是我们绝对做得好的一个方面。我们做了几件事:

  • 确保演讲在 4 分钟以内完成:我们在进入演讲室之前进行了充分的练习,以确保在演讲时不会遇到任何意外。

  • 与观众互动:我们提出了一个问题,询问观众他们认为哪个时间序列是合成生成的,哪个是实际的,这帮助我们保持了观众的兴趣。

我们工作的代码和演讲可以在 这里 找到。

关键要点

  • 快速迭代设计:我在队友们之前稍早到达,开始在白板上记录我们该做什么。队友们到达后,我们讨论了设计方案,并提出了一个大家都同意的解决方案。这是我们获胜的关键因素,因为在黑客松中总是时间紧迫,尽快确定可以开始实施的设计非常重要

  • 不要担心竞争:当我们的设计完成后,我能感受到我们正走在正确的道路上。我们邀请了不少来自 Brembo 的人员来查看我们的设计。甚至其他参赛者也惊叹不已,盯着我们的设计,这进一步给了我们信号,说明我们在正确的轨道上。当我的队友建议我们应该看看其他人的做法时,我反驳了这个想法,而是要求大家专注于我们的设计并实施它。

  • 不要担心冲突:我们多次遇到冲突,特别是在设计方面。关键是要理解没有什么是个人的,而是应该达成共识,对权衡进行迭代,找到适合所有人的解决方案。在我看来,如果你能允许甚至鼓励团队内的健康冲突,伟大的产品就会诞生。

  • 微调用于形式,RAG 用于事实:我们知道微调仅对教授模型基本结构和语调重要,而真正的收益来自于 RAG。因此,我们仅用 5%的样本对 gpt 3.5 turbo llm 进行微调,以生成时间序列数据。

  • 演讲是关键 (1):识别你的观众是谁,以及他们如何消化你的内容至关重要。在我们的案例中,我们发现大多数评委是高管而非技术人员,因此我决定仅包括我们使用的技术栈[gpt 3.5 turbo, fine tuning, prompt tuning, RAG, KNN]而不深入细节。

  • 演讲是关键 (2):成为一个能够通过有效沟通技巧传达观点并充满热情地向观众展示的人。如果你做不到这一点,就找一个团队中的人来做。第一印象很重要,而演讲技巧在我们的技术世界中被严重低估。

  • 大胆而不同:我们更进一步,决定包含 5 个他们的数据点和一个我们生成的数据点,并让他们猜哪个是生成的。当他们未能猜出我们生成的那个点时,这确实突显了我们构建的管道和解决方案的优越性。此外,我们还获得了观众互动的加分,这点我怀疑很多人做不到。

下一次的学习

  • 微调很昂贵。我们在微调和查询模型三次时用完了 OpenAI 的凭证。未来,我们更倾向于使用像 LoRA[5]和 QLoRA[6]这样的技术在一些开源模型上。

  • 使用高级 RAG:未来,我想使用高级 RAG 技术[7]来改进提供的上下文。

  • 使用智能 KNN:下次,我想多尝试一下超参数和使用的距离函数。

  • 更长的上下文窗口:我们必须舍去性能数据中的一些数字,以确保不超过 4,092 个标记的限制。使用像 Claude[8]这样的 LLM 可能会提高性能。

  • 对 LLM 不要太客气:在提示工程中发生了一件有趣的事,当我们提到“μ的值不在 0.6 附近是不可接受的”而不是“请确保μ在 0.6 附近”时,前者得到了更好的结果。

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

团队成员:

  1. Mantek Singh

  2. Prateek Karnal

  3. Gagan Ganapathy

  4. Vinit Shah

参考文献

[1] brembo-hackathon-platform.bemyapp.com/#/event

[2] www.anyscale.com/blog/fine-tuning-is-for-form-not-facts

[3] vectara.com/introducing-boomerang-vectaras-new-and-improved-retrieval-model/

[4] platform.openai.com/docs/guides/fine-tuning/fine-tuning-examples

[5] arxiv.org/abs/2106.09685

[6] arxiv.org/abs/2305.14314

[7] LlamaIndex Doc

[8] Claude

理解 LLM 幻觉

原文:towardsdatascience.com/llm-hallucinations-ec831dcd7786

观点

LLMs 如何编造内容以及该如何应对

Frank NeugebauerTowards Data Science Frank Neugebauer

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

--

图片来源:Ahmad DiriniUnsplash

主要目标

使用大型语言模型并非没有风险,其中包括基于所谓的 LLM“幻觉”的响应。幻觉可能对 LLMs 构成严重问题,因为它们可能导致虚假信息的传播、暴露机密信息,并对 LLMs 的能力产生不切实际的期望。理解幻觉并对它们生成的信息保持批判性,有助于解释和减轻这些幻觉可能造成的问题。

什么是 LLM 幻觉?

LLMs 是一种人工智能(AI),通过大量的文本和代码数据集进行训练。它们可以生成文本、翻译语言、创作不同类型的内容,并以信息性方式回答问题。然而,LLMs 也容易“幻觉”,这意味着它们可能生成事实不准确或毫无意义的文本。正如经常讨论的那样,“LLMs 可能充满了胡说八道。”这种幻觉发生是因为 LLMs 在经常不完整或矛盾的数据上进行训练。因此,它们可能会学会将某些词语或短语与某些概念联系起来,即使这些联系并不准确或无意中“过于准确”(我的意思是它们可能会捏造一些真实但不应被分享的内容)。这可能导致 LLMs 生成事实不准确、无意中过于纵容或纯粹毫无意义的文本。

幻觉的类型

谎言!谎言!谎言!

LLMs 有时会生成事实不准确的文本。以下是一个例子,其中有些内容是正确的,但突出显示的部分则完全不真实:

Prompt: Write a brief history of the United States

Response: The 20th century was a time of great change for the United States. 
The country fought in two world wars, and it emerged as a global superpower 
after World War II. The Civil Rights Movement of the 1950s and 1960s led to 
the end of segregation and discrimination against African Americans.

说隔离和歧视已经不存在是不符合事实的。将其称为“谎言”对我来说也是技术上不准确的,因为模型不理解真理或谎言,只是如何组装单词。无论原因如何,LLMs 仍然可能生成事实不准确的内容。这并不少见,因此必须对所有内容进行事实检查。

胡说八道

从一个很高的层面来看,LLMs 使用概率来安排单词。尽管单词和其概率的范围可能会生成有意义的内容,但这并不总是如此;LLMs 也可以生成无意义的文本。例如,如果你让 LLM 写一首诗,它可能会生成语法正确但毫无意义的内容。

Prompt: write a funny poem about swimming in green water

Partial (summarized) Response:
Swimming in Green Water

I went swimming in the green water,
The water was so green,
I couldn't see my feet,
I couldn't see my hand,
I couldn't see my head.

I looked around and saw a fish,
A big green fish,
With big green eyes,
And a big green smile,
And a big green fin.

I said, "Hello, fish,"
And the fish said, "Hello,"
And we swam around together,
In the green water,
For a long time.

Then we got tired,
And we lay down on the beach,
And we took a nap,
In the sun,
And the green water.

我能理解创作者可能会说,在诗中说鱼可以躺在沙子里是完全可以接受的(诗意许可及其所有),但我也可以争论模型是在编造一些胡说八道,这也是我在这种情况下的看法。思想从逻辑偏离到了非逻辑。然而,注意如果你与 LLMs 合作一段时间,你会看到更严重的例子。再次检查模型输出,并在必要时进行更正。在诗歌示例中,我可能会简单地将“…我们躺在海滩上…”改为“…我们躺在珊瑚礁旁…”或干脆删除那一行,因为人类实际上无法在水下小憩。

来源混淆

LLMs 有时会混淆不同来源的信息,这可能导致它们生成不准确或误导性的文本。例如,如果你让 LLM 写一篇关于当前事件的新闻文章,它可能会将来自不同新闻来源的信息结合在一起,即使这些来源相互矛盾。还要注意,将历史信息的推断与(例如)LangChain 等内容结合在一起,可能会真正混淆信息(和格式)。

这是一个示例,说明混淆如何导致事实不准确(或至少非常误导性)的信息。

Prompt: write a one paragraph summary of the miami grand prix

Response:
The 2023 Miami Grand Prix was the fifth round of the 2023 Formula One 
World Championship. It was held on May 7, 2023 at the Miami International 
Autodrome in Miami Gardens, Florida, United States.

Sergio Pérez of Red Bull Racing started the race on pole position, but 
was overtaken by Charles Leclerc of Ferrari on the opening lap. Leclerc 
led the race for the first 20 laps, but was then passed by Max Verstappen 
of Red Bull Racing. Verstappen went on to win the race, with Leclerc 
finishing second and Pérez finishing third.

The Miami Grand Prix was a popular event, with over 270,000 spectators in 
attendance. The race was also well-received by the media, with many 
praising the new circuit and the atmosphere at the event.

回应在第一段准确地开头,描述了 2023 年 5 月 7 日举行的比赛。然后,模型似乎在之后混淆了 2022 年的结果。勒克莱尔在 2023 年排位第七,并没有在前 20 圈领跑,但他确实在 2022 年获得了第二名,并且可能在那场比赛的前 20 圈中领跑。(2023 年塞尔吉奥·佩雷斯获得第二名。)在这种情况下,混淆可能发生在用于结合当前事件和 LLM 文本的任何工具(例如 LangChain)中,但混淆引起的幻觉的想法仍然适用。

过度纵容

鉴于大型语言模型(LLMs)能够利用概率生成一些相当重要的文本,并且可能会混淆信息,因此统计上来说,LLM 有可能编造出“意外”泄露机密信息的内容。

为了保护机密信息,我不能提供关于我最近遇到的 LLM 出现这种情况的详细信息。然而,我曾询问过一个 LLM 一个我知道不应该披露的特定话题,模型给出了一个逻辑上正确但过于放纵的回答。虽然我遇到的情况不是国家安全问题,但在适当的情况下可能会非常严重。

如何管理幻觉?

这里有一些管理幻觉的技巧:

  • 测试不同的设置,比如温度和 TopK(模型如何管理概率)。这是管理模型输出的最重要方法之一。

  • 不要完全相信输出——进行事实核查(不用担心,你仍然节省了大量时间!)。

  • 通常将 LLM 输出视为草稿机制——例如,LLM 创建了这篇文章的基本布局和一些内容。但我进行了大量编辑。

  • 调整你使用的模型。根据你如何使用模型输出,你可能需要调整模型——这有许多方法,包括提示工程、参数高效调整(PET)和完全模型调整。这个简单列表中有相当多的细微差别和复杂性,但如果你知道如何做,这种调整可以减少幻觉。

  • 接受模型出现幻觉的现实。与人类(在大多数情况下)不同,LLM 的幻觉通常是一个无意的结果,我认为通常正面的影响远远超过负面的影响。接受这一点并承认/沟通幻觉发生的可能性。

  • 探索!虽然这篇文章提供了 LLM 幻觉的概述,但这对你和你的应用意味着什么可能会有显著差异。此外,你对这些词的理解可能并不完全符合现实。唯一真正理解和欣赏 LLM 幻觉如何影响你所做的事的方法是广泛探索 LLM。

更多内容待揭示

LLM 的广泛使用实际上还处于初期阶段,优缺点尚未准确列举。在我看来,开放的心态是理解 LLM 的所有维度,包括幻觉的最佳技巧。享受这个过程,尽可能多地探索,因为这种快速演变很少发生(根据我的经验),那些拥抱这一过程的人将从中获得最多。

LLM 监控与可观测性——负责任 AI 的技术和方法总结

原文:towardsdatascience.com/llm-monitoring-and-observability-c28121e75c2f?source=collection_archive---------0-----------------------#2023-09-15

Josh PoduskaTowards Data Science Josh Poduska

·

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

--

目标受众: 实践者 希望了解可用的方法以及如何开始实施这些方法的情况,和 领导者 希望在构建治理框架和技术路线图时理解可能性的艺术。

几乎一夜之间,每个首席执行官的待办事项、招聘广告和简历上都出现了生成 AI(genAI)。这完全是应有的。基于基础模型的应用程序已经改变了数百万人的工作、学习、写作、设计、编程、旅行和购物方式。大多数人,包括我在内,都认为这只是冰山一角。

在这篇文章中,我总结了对现有大型语言模型(LLM)监控方法的研究。我花了很多时间阅读文档、观看视频,并阅读来自专注于 LLM 监控和可观测性的开源库和软件供应商的博客。结果是一个用于监控和观察 LLM 的实用分类法。希望对你有帮助。在不久的将来,我计划进行一项学术文献检索,以添加前瞻性的视角。

研究过的软件: Aporia, Arize, Arthur, Censius, Databricks/MLFlow, Datadog, DeepChecks, Evidently, Fiddler, Galileo, Giskard, Honeycomb, Hugging Face, LangSmith, New Relic, OpenAI, Parea, Trubrics, Truera, Weights & Biases, Why Labs

  • 本文呈现了一个累积的分类法,没有对软件选项进行评分或比较。如果你想讨论我研究中涉及的特定软件,请 联系我

文章大纲

  • 评估 LLM — LLM 是如何评估的,何时被认为适合生产?

  • 跟踪 LLM — 跟踪 LLM 意味着什么,哪些组件需要包括?

  • 监控 LLM — LLM 一旦投入生产,如何进行监控?

LLM 生命周期

由 Josh Poduska 创作的照片 — 互动视图链接

将 LLM 纳入生产工作流程的竞赛已经开始,但技术社区正在争分夺秒地开发最佳实践,以确保这些强大的模型随着时间的推移按预期行为。

评估 LLM

评估传统的机器学习(ML)模型涉及检查其输出或预测的准确性。这通常通过众所周知的指标如准确率、RMSE、AUC、精确度、召回率等来衡量。评估 LLM 复杂得多。数据科学家今天使用了几种方法。

(1) 分类和回归指标

LLM 可以生成数字预测或分类标签,这种情况下评估较为简单。与传统的 ML 模型一样。这在某些情况下很有帮助,但我们通常关注的是评估生成文本的 LLM。

(2) 独立的基于文本的指标

当你没有可靠的基准数据源时,这些指标对于评估 LLM 的文本输出非常有用。你可以根据过去的经验、学术建议或其他模型的评分来决定什么是可接受的。

困惑度是一个例子。它衡量模型生成输入文本序列的可能性,可以看作是评估模型对其训练文本的学习效果的方式。其他例子包括阅读水平非字母字符

更复杂的独立方法包括从模型输出中提取嵌入,并分析这些嵌入以寻找不寻常的模式。这可以通过检查 3D 图中的嵌入图来手动完成。通过颜色编码或按关键字段(如性别、预测类别或困惑度评分)比较,可以揭示你的 LLM 应用中潜在的问题,并提供偏见和解释性的衡量。存在一些软件工具可以以这种方式可视化嵌入。它们将嵌入聚类并映射到 3 维空间。这通常通过HDBSCAN 和 UMAP完成,但有些工具采用基于 K-means 的方法

除了视觉评估外,还可以运行异常检测算法来检查嵌入中是否存在异常值。

(3) 评估数据集

拥有真实标签的数据集可以将文本输出与批准的响应基准进行比较。

一个著名的例子是ROUGE指标。在语言翻译任务中,ROUGE 依赖于一个参考数据集,其答案与正在评估的 LLM 进行比较。可以根据参考数据集计算相关性、准确性和其他一系列指标。嵌入在其中起着关键作用。标准的距离指标如 J-S 距离、赫灵距离、KS 距离和 PSI,将你的 LLM 输出嵌入与真实标签嵌入进行比较。

最后,存在一些广泛接受的 LLM 基准测试。斯坦福的HELM 页面是了解这些测试的好地方。

(4) 评估 LLM

起初,你可能会认为使用 LLM 来评估 LLM 是作弊,但许多人认为这是前进的最佳途径,研究也显示了希望。使用我所说的评估 LLM 很可能会成为不久的将来 LLM 评估的主要方法。

一个广泛接受的例子是Toxicity度量。它依赖于一个评估器 LLM(Hugging Face 推荐的roberta-hate-speech-dynabench-r4)来确定你的模型输出是否有毒。上述所有关于评估数据集的度量在这里也适用,因为我们将评估器 LLM 的输出视为参考。

根据 Arize 的研究人员,评估器 LLM 应配置为提供二元分类标签以用于它们测试的度量。他们解释说,数值评分和排名需要更多工作,并且不如二元标记表现良好。

(5) 人类反馈

尽管这篇文章、软件文档和营销材料强调了可测量的度量,但你不应忘记手动基于人类的反馈。这通常在构建 LLM 应用的早期阶段由数据科学家和工程师考虑。LLM 可观察性软件通常有一个接口来协助这个任务。除了早期开发反馈之外,将人类反馈纳入最终评估过程(以及持续监控)也是一种最佳实践。获取 50 到 100 个输入提示并手动分析输出可以让你对最终产品有很大的了解。

跟踪 LLMs

跟踪是监控的前提。在我的研究中,我发现跟踪 LLMs 的细节中有足够的细微差别,需要单独成章。跟踪的低悬果包括捕获请求数量、响应时间、令牌使用情况、成本和错误率。标准系统监控工具在这里发挥作用,同时还有更多针对 LLM 的选项(传统监控公司也有营销团队迅速宣称基于简单功能度量跟踪的 LLM 可观察性和监控)。

从捕获输入提示和输出响应中获得深刻见解以进行未来分析。这听起来很简单,但实际上并非如此。复杂性来自于我迄今为止略过的某些内容(大多数数据科学家在谈论或写作关于 LLM 时也一样)。我们并不是在评估、跟踪和监控一个 LLM。我们处理的是一个应用程序;一个或多个 LLM、预设的指令提示和协作生成输出的代理的集合。一些 LLM 应用并不复杂,但许多是的,并且趋势是向更复杂的方向发展。在即使是稍微复杂的 LLM 应用中,确定最终的提示调用可能是困难的。如果我们在调试,我们需要知道每一步的调用状态及这些步骤的顺序。实践者会希望利用帮助解开这些复杂性的工具。

监控 LLMs

虽然大多数 LLM 和 LLM 应用程序至少经历过某种形式的评估,但实施持续监控的却很少。我们将拆解监控的组成部分,以帮助你建立一个保护用户和品牌的监控程序。

(1) 功能监控

首先,上述跟踪部分提到的简单任务应持续监控。这包括请求数量、响应时间、令牌使用、成本和错误率。

(2) 监控提示

接下来,你的列表中应该包括监控用户提供的提示或输入。像可读性这样的独立指标可能会很有帮助。评估 LLM 应该用于检查毒性等问题。将嵌入距离与参考提示进行比较是明智的指标。即使你的应用程序能够处理与预期大相径庭的提示,你仍然希望了解客户与应用程序的互动是否是新的或随时间变化的。

目前,我们需要引入一个新的评估类别:对抗性尝试或恶意提示注入。这在初步评估中并不总是被考虑到。与已知对抗性提示的参考集进行比较可以标记出恶意行为者。评估 LLM 也可以将提示分类为恶意或非恶意。

(3) 监控响应

在将你的 LLM 应用程序产生的内容与预期结果进行比较时,有许多有用的检查可以实施。考虑相关性。你的 LLM 是否在响应相关内容,还是在胡言乱语(幻想)?你是否看到与预期主题的偏离?情感如何?你的 LLM 是否以正确的语调回应,且这种情况是否随着时间变化?

你可能不需要每天监控所有这些指标。每月或每季度一次对于某些指标来说就足够了。另一方面,毒性和有害输出始终是部署 LLM 时最令人担忧的问题。这些是你需要更频繁跟踪的指标。记住,之前讨论的嵌入可视化技术可能有助于根本原因分析。

提示泄露是一种我们尚未介绍的对抗性方法。提示泄露发生在某人欺骗你的应用程序泄露你存储的提示时。你可能花了很多时间找出哪个预设提示指令效果最好。这是敏感的知识产权。提示泄露可以通过监控响应并将其与提示指令数据库进行比较来发现。嵌入距离指标效果很好。

如果你有评估或参考数据集,你可能需要定期测试你的 LLM 应用,并比较之前测试的结果。这可以给你一个随时间变化的准确度感知,并能提醒你漂移。如果发现问题,一些管理嵌入的工具允许你导出表现不佳的输出数据集,以便你可以在这些有问题的提示类别上微调你的 LLM。

(4) 警报与阈值

应注意确保你的阈值和警报不会引发过多的虚假警报。多变量漂移检测和警报可以有所帮助。我有一些关于如何做到这一点的想法,但会留到另一篇文章中。顺便说一下,我在研究本文时没有看到关于虚假警报率或阈值最佳实践的任何提及。这真是个遗憾。

有几个与警报相关的不错功能,你可能想将它们列入必备清单。许多监控系统提供与信息馈送(如 Slack 和 Pager Duty)的集成。一些监控系统允许如果输入提示触发警报,则自动响应拦截。相同的功能可以应用于在将响应发送给用户之前筛查个人信息泄露、毒性以及其他质量指标。

我会在这里添加一个观察点,因为我不知道把它放在哪里。自定义指标对你的监控方案可能非常重要。你的 LLM 应用可能是独特的,或者你团队中的一位优秀数据科学家想出了一个将显著提升你方法的指标。这个领域可能会有进展。你会希望有自定义指标的灵活性。

(5) 监控用户界面

如果系统具备监控功能,它将有一个用户界面(UI),显示度量的时间序列图。这是相当标准的。用户界面开始有所区别,当它们允许深入挖掘警报趋势,并指向某种程度的根本原因分析时。其他界面则通过基于集群和投影的嵌入空间可视化(我希望能看到或进行一项关于这些嵌入可视化在实际应用中有效性的研究)。

更成熟的产品将按用户、项目和团队对监控进行分组。它们将具有 RBAC,并基于所有用户都是需要知道的原则。过于频繁地,工具中的任何人都能看到每个人的数据,这在许多当今的组织中是不可接受的。

我所强调的警报产生不可接受虚假警报率的一个原因是用户界面未能促进对警报的适当分析。软件系统很少尝试在这方面进行任何优化,但有些系统会。再次提到,这个话题还有很多需要讨论的内容,稍后会再提及。

对从业者和领导者的最终思考

领导者们,面临的风险太大,不容忽视 LLM 监控和可观测性的重要性。我说这些不仅仅是为了避免对用户造成伤害或失去品牌声誉。这些显然在你的关注范围内。你可能没有意识到的是,你公司对 AI 的快速且可持续的采用,可能意味着成功与失败之间的差距,而一个成熟的负责任的 AI 框架以及一个详细的技术路线图用于监控和观察 LLM 应用,将为你提供一个基础,使你能够比竞争对手更快、更好、更安全地扩展。

从业人员,本文介绍的概念提供了一系列工具、技术和指标,这些应当包含在 LLM 可观测性和监控的实施中。你可以将其作为指南,以确保你的监控系统能够胜任这项任务。同时,你可以将其作为进一步深入研究我们讨论的每个概念的基础。

这是一个令人兴奋的新领域。那些对其有深刻了解的领导者和从业人员,将能够帮助他们的团队和公司在 AI 时代取得成功。

关于作者:

Josh Poduska 是一位 AI 领导者、战略家和顾问,拥有超过 20 年的经验。他曾担任 Domino Data Lab 的首席现场数据科学家,并在多家公司管理团队和领导数据科学战略。Josh 在多个领域建立并实施了数据科学解决方案。他拥有加州大学欧文分校的数学学士学位和康奈尔大学应用统计硕士学位。

LLM 输出解析:函数调用与 LangChain

原文:towardsdatascience.com/llm-output-parsing-function-calling-vs-langchain-63b80545b3a7?source=collection_archive---------2-----------------------#2023-09-21

如何使用 Open AI API 和 LangChain 函数调用一致地解析 LLM 的输出:评估这些方法的优缺点

Gabriel CassimiroTowards Data Science Gabriel Cassimiro

·

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

--

图片由 Victor Barrios 提供,来源于 Unsplash

使用 LLMs 创建工具需要多个组件,例如向量数据库、链、代理、文档分割器和许多其他新工具。

然而,最关键的组件之一是 LLM 输出解析。如果你不能从 LLM 接收到结构化的响应,你将很难处理生成的内容。当我们希望一次调用 LLM 输出多个信息时,这一点变得尤为明显。

让我们用一个假设的场景来说明这个问题:

我们希望 LLM 从一次调用中输出 ingredientssteps 来制作特定的食谱。但我们希望将这两项内容分别用于系统的两个不同部分。

import openai

recipe = 'Fish and chips'
query = f"""What is the recipe for {recipe}? 
Return the ingredients list and steps separately."""

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[{"role": "user", "content": query}])

response_message = response["choices"][0]["message"]
print(response_message['content'])

这将返回如下内容:

Ingredients for fish and chips:
- 1 pound white fish fillets (such as cod or haddock)
- 1 cup all-purpose flour
- 1 teaspoon baking powder
- 1 teaspoon salt
- 1/2 teaspoon black pepper
- 1 cup cold beer
- Vegetable oil, for frying
- 4 large russet potatoes
- Salt, to taste

Steps to make fish and chips:

1\. Preheat the oven to 200°C (400°F).
2\. Peel the potatoes and cut them into thick, uniform strips. Rinse the potato strips in cold water to remove excess starch. Pat them dry using a clean kitchen towel.
3\. In a large pot or deep fryer, heat vegetable oil to 175°C (350°F). Ensure there is enough oil to completely submerge the potatoes and fish.
4\. In a mixing bowl, combine the flour, baking powder, salt, and black pepper. Whisk in the cold beer gradually until a smooth batter forms. Set the batter aside.
5\. Take the dried potato strips and fry them in batches for about 5-6 minutes or until golden brown. Remove the fries using a slotted spoon and place them on a paper towel-lined dish to drain excess oil. Keep them warm in the preheated oven.
6\. Dip each fish fillet into the prepared batter, ensuring it is well coated. Let any excess batter drip off before carefully placing the fillet into the hot oil.
7\. Fry the fish fillets for 4-5 minutes on each side or until they turn golden brown and become crispy. Remove them from the oil using a slotted spoon and place them on a paper towel-lined dish to drain excess oil.
8\. Season the fish and chips with salt while they are still hot.
9\. Serve the fish and chips hot with tartar sauce, malt vinegar, or ketchup as desired.

Enjoy your homemade fish and chips!

这是一个庞大的字符串,解析起来会很困难,因为 LLM 可能会返回稍有不同的结构,从而破坏你编写的代码。你可以争辩说,在提示中始终要求返回“Ingredients:” 和 “Steps:” 可以解决问题,你说得没错。这可能有效,但你仍然需要手动处理字符串,并且要应对可能的变化和虚构。

解决方案

我们可以用几种方法解决这个问题。上述提到了一种,但还有几种经过测试的方法可能更好。在本文中,我将展示两个选项:

  1. Open AI 函数调用;

  2. LangChain 输出解析器。

Open AI 函数调用

这是我尝试过的一种方法,并且给出了最一致的结果。我们利用 Open AI API 的函数调用能力,以便模型将响应作为结构化的 JSON 返回。

这个功能的目标是提供给 LLM 通过 JSON 输入调用外部函数的能力。模型经过微调,以理解何时需要使用特定函数。一个例子是当前天气的函数。如果你问 GPT 当前的天气,它不能告诉你,但你可以提供一个做这件事的函数并传递给 GPT,这样它就会知道可以根据输入访问它。

如果你想深入了解这个功能,可以查看 Open AI 的公告,以及一篇 很棒的文章。

那么让我们在代码中看看这个在我们面临的问题下会是什么样子。让我们分解一下代码:

functions = [
    {
        "name": "return_recipe",
        "description": "Return the recipe asked",
        "parameters": {
            "type": "object",
            "properties": {
                "ingredients": {
                    "type": "string",
                    "description": "The ingredients list."
                },
                "steps": {
                    "type": "string",
                    "description": "The recipe steps."
                },
            },
            },
            "required": ["ingredients","steps"],
        }
]

我们需要做的第一件事是声明将对 LLM 可用的函数。我们必须给它一个名称和描述,以便模型理解何时应使用该函数。在这里,我们告诉它这个函数是用来返回请求的食谱的。

然后我们进入参数设置。首先,我们说明它的类型为对象,其可以使用的属性有“ingredients”和“steps”。这两个属性也都有一个描述和一个类型,以指导 LLM 输出。最后,我们指定哪些属性是调用函数时必需的(这意味着我们可以有可选字段,LLM 可以决定是否使用它们)。

现在让我们在一次调用 LLM 中使用这个:

import openai

recipe = 'Fish and chips'
query = f"What is the recipe for {recipe}? Return the ingredients list and steps separately."

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[{"role": "user", "content": query}],
    functions=functions,
    function_call={'name':'return_recipe'}
)
response_message = response["choices"][0]["message"]

print(response_message)
print(response_message['function_call']['arguments'])

这里我们开始通过格式化一个可能是变量输入(食谱)的基础提示来创建对 API 的查询。然后,我们使用“gpt-3.5-turbo-0613”声明我们的 API 调用,我们将查询传递到 messages 参数中,然后传递我们的函数。

关于我们的函数有两个参数。第一个是我们传递的对象列表,格式如上所示,包含模型可以访问的函数。第二个参数“function_call”我们指定模型应该如何使用这些函数。有三种选项:

  1. “Auto” -> 模型在用户响应或函数调用之间进行决定;

  2. “none” -> 模型不调用函数,而是返回用户响应;

  3. {“name”: “my_function_name”} -> 指定一个函数名称强制模型使用它。

你可以在这里找到官方文档。

在我们的案例中,作为输出解析我们使用了后者:

function_call={'name':'return_recipe'}

现在我们可以查看我们的响应。我们得到的响应(在此过滤器[“choices”][0][“message”]之后)是:

{
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "return_recipe",
    "arguments": "{\n  \"ingredients\": \"For the fish:\\n- 1 lb white fish fillets\\n- 1 cup all-purpose flour\\n- 1 tsp baking powder\\n- 1 tsp salt\\n- 1/2 tsp black pepper\\n- 1 cup cold water\\n- Vegetable oil, for frying\\nFor the chips:\\n- 4 large potatoes\\n- Vegetable oil, for frying\\n- Salt, to taste\",\n  \"steps\": \"1\. Start by preparing the fish. In a shallow dish, combine the flour, baking powder, salt, and black pepper.\\n2\. Gradually whisk in the cold water until the batter is smooth.\\n3\. Heat vegetable oil in a large frying pan or deep fryer.\\n4\. Dip the fish fillets into the batter, coating them evenly.\\n5\. Gently place the coated fillets into the hot oil and fry for 4-5 minutes on each side, or until golden brown and crispy.\\n6\. Remove the fried fish from the oil and place them on a paper towel-lined plate to drain any excess oil.\\n7\. For the chips, peel the potatoes and cut them into thick chips.\\n8\. Heat vegetable oil in a deep fryer or large pan.\\n9\. Fry the chips in batches until golden and crisp.\\n10\. Remove the chips from the oil and place them on a paper towel-lined plate to drain any excess oil.\\n11\. Season the chips with salt.\\n12\. Serve the fish and chips together, and enjoy!\"\n}"
  }
}

如果我们进一步解析到“function_call”,我们可以看到我们预期的结构化响应:

{
  "ingredients": "For the fish:\n- 1 lb white fish fillets\n- 1 cup all-purpose flour\n- 1 tsp baking powder\n- 1 tsp salt\n- 1/2 tsp black pepper\n- 1 cup cold water\n- Vegetable oil, for frying\nFor the chips:\n- 4 large potatoes\n- Vegetable oil, for frying\n- Salt, to taste",
  "steps": "1\. Start by preparing the fish. In a shallow dish, combine the flour, baking powder, salt, and black pepper.\n2\. Gradually whisk in the cold water until the batter is smooth.\n3\. Heat vegetable oil in a large frying pan or deep fryer.\n4\. Dip the fish fillets into the batter, coating them evenly.\n5\. Gently place the coated fillets into the hot oil and fry for 4-5 minutes on each side, or until golden brown and crispy.\n6\. Remove the fried fish from the oil and place them on a paper towel-lined plate to drain any excess oil.\n7\. For the chips, peel the potatoes and cut them into thick chips.\n8\. Heat vegetable oil in a deep fryer or large pan.\n9\. Fry the chips in batches until golden and crisp.\n10\. Remove the chips from the oil and place them on a paper towel-lined plate to drain any excess oil.\n11\. Season the chips with salt.\n12\. Serve the fish and chips together, and enjoy!"
}

函数调用的结论

可以直接从 Open AI API 使用函数调用的功能。这允许我们每次调用 LLM 时都得到具有相同键的字典格式响应。

使用起来相当直接,你只需声明函数对象,指定名称、描述和关注于你的任务的属性,同时在描述中指定这应该是模型的响应。此外,当调用 API 时,我们可以强制模型使用我们的函数,从而使其更加一致。

这种方法的主要缺点是并不是所有的 LLM 模型和 API 都支持。所以,如果我们想使用 Google PaLM API,我们将不得不使用另一种方法。

LangChain 输出解析器

我们可以选择一个与模型无关的替代方案,就是使用 LangChain。

首先,什么是 LangChain?

LangChain 是一个用于开发由语言模型驱动的应用程序的框架。

这是 LangChain 的官方定义。这个框架最近创建,并已经作为构建由 LLM 驱动的工具的行业标准。

它有一个非常适合我们用例的功能,称为“输出解析器”。在这个模块中,可以创建多个对象来返回和解析来自 LLM 调用的不同类型的格式。实现的方式是,首先声明格式,并将其传递到 LLM 的提示中。然后,使用之前创建的对象来解析响应。

让我们分解代码:

from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import ResponseSchema, StructuredOutputParser
from langchain.llms import GooglePalm, OpenAI

ingredients = ResponseSchema(
        name="ingredients",
        description="The ingredients from recipe, as a unique string.",
    )
steps = ResponseSchema(
        name="steps",
        description="The steps to prepare the recipe, as a unique string.",
    )

output_parser = StructuredOutputParser.from_response_schemas(
    [ingredients, steps]
)

response_format = output_parser.get_format_instructions()
print(response_format)

prompt = ChatPromptTemplate.from_template("What is the recipe for {recipe}? Return the ingredients list and steps separately. \n {format_instructions}")

我们首先要做的是创建我们的响应模式,这将作为解析器的输入。我们为成分和步骤各创建一个,每个都包含一个名称,这将是字典的键,以及一个描述,这将指导 LLM 如何回应。

然后我们从这些响应模式中创建我们的结构化输出解析器。有多种方法可以做到这一点,使用不同风格的解析器。了解更多信息,请查看这里

最后,我们获取我们的格式说明,并定义我们的提示,将食谱名称和格式说明作为输入。格式说明如下:

"""
The output should be a markdown code snippet formatted in the following schema, including the leading and trailing "```json" 和 "```py":

```json

{

"ingredients": string  // 食谱中的成分,作为一个独特的字符串。

"steps": string  // 准备食谱的步骤,作为一个独特的字符串。

}

"""

```py

Now what we have left is just calling the API. Here I will demonstrate both the Open AI API and with Google PaLM API.

llm_openai = OpenAI()

llm_palm = GooglePalm()

recipe = '炸鱼薯条'

formated_prompt = prompt.format(**{"recipe":recipe, "format_instructions":output_parser.get_format_instructions()})

response_palm = llm_palm(formated_prompt)

response_openai = llm_openai(formated_prompt)

print("PaLM:")

print(response_palm)

print(output_parser.parse(response_palm))

print("Open AI:")

print(response_openai)

print(output_parser.parse(response_openai))


As you can see it is really easy to change between models. The whole structure defined before can be used in the exact same way for any models supported by LangChain. We used also the same parser for both models.

This generated the following output:

PaLM:

{

'ingredients': '''- 1 杯通用面粉\n

  • 1 茶匙发酵粉\n

  • 1/2 茶匙盐\n

  • 1/2 杯冷水\n

  • 1 个鸡蛋\n

  • 1 磅白鱼片,如鳕鱼或鳕鱼\n

  • 用于炸制的植物油\n- 1 杯塔塔酱\n

  • 1/2 杯麦芽醋\n- 柠檬角''',

'steps': '''1. 在一个大碗中,搅拌面粉、发酵粉和盐。\n

2. 在另一个碗中,搅拌鸡蛋和水。\n

3. 将鱼片浸入鸡蛋混合物中,然后在面粉混合物中裹上。\n

4. 在深炸锅或大煎锅中加热油至 375 华氏度(190 摄氏度)。\n

5. 将鱼片两面煎 3-5 分钟,直到金黄酥脆并完全熟透。\n

6. 在纸巾上沥干鱼片。\n

7. 立即将鱼片与塔塔酱、麦芽醋和柠檬角一起上桌。

'''

}

Open AI

{

'ingredients': '1 ½ 磅鳕鱼片,切成 4 块,

2 杯通用面粉,

2 茶匙发酵粉,

1 茶匙盐,

1 茶匙新鲜磨碎的黑胡椒,

½ 茶匙大蒜粉,

1 杯啤酒(或水),

用于炸制的植物油,

塔塔酱,用于搭配',

'steps': '1. 预热烤箱至 400°F (200°C),并在烤盘上铺上烘焙纸。

2. 在一个中等大小的碗中,将面粉、发酵粉、盐、胡椒粉和大蒜粉混合在一起。

3. 倒入啤酒,搅拌至形成浓稠的面糊。

4. 将鳕鱼浸入面糊中,确保四面都裹上。

5. 在一个大锅或煎锅中加热约 2 英寸(5 厘米)的油,火力中高。

6. 将鳕鱼两面煎 3 到 4 分钟,直到金黄酥脆。

7. 将鳕鱼转移到准备好的烤盘上,烘烤 5 到 7 分钟。

8. 热食,配以塔塔酱。'

}


## 结论:LangChain 输出解析

这种方法也非常好,其主要特点是灵活性。我们创建了一些结构,如响应模式、输出解析器和提示模板,这些结构可以轻松组合在一起,并与不同的模型一起使用。另一个好处是支持多种输出格式。

主要的缺点来自通过提示传递格式指令。这可能会导致随机错误和幻觉。一个真实的例子是,在这个具体的案例中,我需要在响应模式的描述中指定“作为唯一字符串”。如果我没有这样指定,模型会返回一个包含步骤和指令的字符串列表,这会导致输出解析器的解析错误。

# 结论

使用输出解析器来处理 LLM 驱动的应用有多种方法。然而,根据问题的不同,你的选择可能会有所变化。对我而言,我喜欢遵循这个思路:

我总是使用输出解析器,即使我只有一个来自 LLM 的输出。这使我能够控制和指定我的输出。如果我使用的是 Open AI,Function Calling 是我的选择,因为它具有最好的控制能力,并能避免生产应用中的随机错误。然而,如果我使用的是不同的 LLM 或需要不同的输出格式,我的选择是 LangChain,但需要对输出进行大量测试,以便构建最少错误的提示。

感谢阅读。

完整的代码可以在 [这里](https://github.com/gabrielcassimiro17/llm-output-parsing) 找到。

如果你喜欢这些内容并想要支持我,可以请我喝杯咖啡:

[](https://www.buymeacoffee.com/cassimiro?source=post_page-----63b80545b3a7--------------------------------) [## 加布里埃尔·卡西米罗是一个数据科学家,向社区分享免费内容

### 我喜欢支持创作者!

www.buymeacoffee.com](https://www.buymeacoffee.com/cassimiro?source=post_page-----63b80545b3a7--------------------------------)

这里有一些你可能感兴趣的其他文章:

[](/async-calls-for-chains-with-langchain-3818c16062ed?source=post_page-----63b80545b3a7--------------------------------) ## 使用 Langchain 进行链条的异步调用

### 如何使 Langchain 链条与异步调用 LLM 一起工作,从而加快运行顺序长时间任务的速度…

towardsdatascience.com [](/solving-unity-environment-with-deep-reinforcement-learning-836dc181ee3b?source=post_page-----63b80545b3a7--------------------------------) ## 使用深度强化学习解决 Unity 环境问题

### 使用 PyTorch 实现深度强化学习代理的端到端项目代码。

towardsdatascience.com


# 基于 LLM+RAG 的问题回答

> 原文:[`towardsdatascience.com/llm-rag-based-question-answering-6a405c8ad38a?source=collection_archive---------0-----------------------#2023-12-25`](https://towardsdatascience.com/llm-rag-based-question-answering-6a405c8ad38a?source=collection_archive---------0-----------------------#2023-12-25)

## 如何在 Kaggle 上表现不佳,并从中学习 RAG+LLM

[](https://teemukanstren.medium.com/?source=post_page-----6a405c8ad38a--------------------------------)![Teemu Kanstrén](https://teemukanstren.medium.com/?source=post_page-----6a405c8ad38a--------------------------------)[](https://towardsdatascience.com/?source=post_page-----6a405c8ad38a--------------------------------)![Towards Data Science](https://towardsdatascience.com/?source=post_page-----6a405c8ad38a--------------------------------) [Teemu Kanstrén](https://teemukanstren.medium.com/?source=post_page-----6a405c8ad38a--------------------------------)

·

[关注](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fuser%2F9fc0679190dc&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Fllm-rag-based-question-answering-6a405c8ad38a&user=Teemu+Kanstr%C3%A9n&userId=9fc0679190dc&source=post_page-9fc0679190dc----6a405c8ad38a---------------------post_header-----------) 发表在 [Towards Data Science](https://towardsdatascience.com/?source=post_page-----6a405c8ad38a--------------------------------) ·23 分钟阅读·2023 年 12 月 25 日[](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Ftowards-data-science%2F6a405c8ad38a&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Fllm-rag-based-question-answering-6a405c8ad38a&user=Teemu+Kanstr%C3%A9n&userId=9fc0679190dc&source=-----6a405c8ad38a---------------------clap_footer-----------)

--

[](https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fbookmark%2Fp%2F6a405c8ad38a&operation=register&redirect=https%3A%2F%2Ftowardsdatascience.com%2Fllm-rag-based-question-answering-6a405c8ad38a&source=-----6a405c8ad38a---------------------bookmark_footer-----------)![](https://gitcode.net/OpenDocCN/towardsdatascience-blog-zh-2023/-/raw/master/docs/img/f497014da435709dae04a493366a7919.png)

图像由 ChatGPT+/DALL-E3 生成,展示了关于 RAG 的文章的插图。

检索增强生成(RAG)似乎现在相当受欢迎。随着大语言模型(LLM)的兴起,它成为了使 LLM 在特定任务上表现更好的热门技术之一,比如对内部文档进行问答。前段时间,我参加了一个 [Kaggle 比赛](https://www.kaggle.com/competitions/kaggle-llm-science-exam),这让我能够尝试它,并比自己随意实验学到更多一些。以下是从这些实验中获得的一些经验教训。

除非另有说明,否则所有图片均由作者提供。生成工具为 ChatGPT+/DALL-E3(如有注明),或取自我个人的 Jupyter 笔记本。

# RAG 概述

RAG 有两个主要部分:检索和生成。在第一部分中,检索用于获取与查询相关的(块)文档。生成则使用这些检索到的块作为额外输入,即 *context*,传递给第二部分的答案生成模型。这个附加的上下文旨在为生成器提供比基本训练数据更及时、更好的信息,以生成答案。

# 构建 RAG 输入,或文本分块

LLM 的最大上下文或序列窗口长度是它们可以处理的范围,RAG 生成的输入上下文需要足够短以适应这个序列窗口。我们希望将尽可能多的相关信息纳入这个上下文,因此从潜在的输入文档中获取最佳的“块”文本非常重要。这些块应当是生成正确答案所需的最相关的内容。

作为第一步,输入文本通常会被分块成更小的片段。RAG 的一个基本预处理步骤是使用特定的嵌入模型将这些块转换为嵌入。一个典型的嵌入模型的序列窗口为 512 个 tokens,这也使其成为实际的分块目标。一旦文档被分块并编码为嵌入,就可以使用这些嵌入进行相似性搜索,以构建生成答案的上下文。

我发现 [Langchain](https://github.com/langchain-ai/langchain) 提供了有用的工具用于输入加载和文本分块。例如,使用 Langchain 对文档进行分块(在此情况下,使用 [Flan-T5-Large](https://huggingface.co/google/flan-t5-large) 模型的分词器)是非常简单的:

```py
from transformers import AutoTokenizer 
from langchain.text_splitter import RecursiveCharacterTextSplitter 

#This is the Flan-T5-Large model I used for the Kaggle competition 
llm = "/mystuff/llm/flan-t5-large/flan-t5-large" 
tokenizer = AutoTokenizer.from_pretrained(llm, local_files_only=True) 
text_splitter = RecursiveCharacterTextSplitter
                 .from_huggingface_tokenizer(tokenizer, chunk_size=12,
                                             chunk_overlap=2,                        
                                             separators=["\n\n", "\n", ". "]) 
section_text="Hello. This is some text to split. With a few "\ 
             "uncharacteristic words to chunk, expecting 2 chunks." 
texts = text_splitter.split_text(section_text) 
print(texts)

这将生成以下两个块:

['Hello. This is some text to split',
 '. With a few uncharacteristic words to chunk, expecting 2 chunks.']

在上面的代码中,chunk_size 12 告诉 LangChain 旨在每个块最多包含 12 个 token。根据文本结构,这可能并不总是 100% 精确。然而,根据我的经验,这通常效果很好。需要记住的是 tokens 和单词之间的区别。下面是对上述 section_text 进行分词的一个示例:

section_text="Hello. This is some text to split. With a few "\ 
             "uncharacteristic words to chunk, expecting 2 chunks." 
encoded_text = tokenizer(section_text) 
tokens = tokenizer.convert_ids_to_tokens(encoded_text['input_ids']) 
print(tokens)

生成的输出 tokens:

['▁Hello', '.', '▁This', '▁is', '▁some', '▁text', '▁to', '▁split', '.', 
 '▁With', '▁', 'a', '▁few', '▁un', 'character', 'istic', '▁words', 
 '▁to', '▁chunk', ',', '▁expecting', '▁2', '▁chunk', 's', '.', '</s>']

大多数 section_text 中的单词本身形成一个 token,因为它们是文本中的常见单词。然而,对于特殊形式的单词或领域词汇,这可能会更复杂。例如,在这里,“uncharacteristic” 这个词变成了三个 tokens [“ un”, “ character”, “ istic”]。这是因为模型的分词器知道这三个部分词汇,但不知道整个单词(“ uncharacteristic”)。每个模型都有自己的分词器来匹配输入和模型训练中的这些规则。

在分块中,来自 Langchain 的RecursiveCharacterTextSplitter用于上述代码中,计算这些令牌,并寻找给定的分隔符将文本拆分成请求的块。不同块大小的试验可能会有用。在我的 Kaggle 实验中,我从嵌入模型的最大大小开始,即 512 个令牌。然后尝试了 256、128 和 64 个令牌的块大小。

示例 RAG 查询

我提到的Kaggle 比赛是基于维基百科数据的多项选择题回答。任务是从多个选项中选择每个问题的正确答案。显而易见的方法是使用 RAG 从维基百科数据中找到所需的信息,并用它来生成正确答案。以下是比赛数据中的第一个问题及其答案选项,用于说明:

示例问题和答案选项 A-E。

多项选择题是尝试 RAG 的一个有趣话题。但我相信,最常见的 RAG 用例是根据源文档回答问题。有点像聊天机器人,但通常是针对特定领域或(公司)内部文档的问答。我在本文中使用这个基本的问答用例来展示 RAG。

作为本文的 RAG 示例问题,我需要一个 LLM 无法仅凭其训练数据直接回答的问题。我使用了维基百科数据,因为它可能是 LLM 训练数据的一部分,所以我需要一个与模型训练后相关的问题。我为本文使用的模型是Zephyr 7B beta,于 2023 年初训练完成。最后,我决定询问Google Bard AI 聊天机器人。它在 Zephyr 训练日期之后的一年里有很多发展。我对 Bard 也有一定了解,以评估 LLM 的答案。因此,我使用“what is google bard?”作为本文的示例问题。

嵌入向量

RAG 的第一阶段检索基于嵌入向量,这些向量实际上只是多维空间中的点。它们看起来像这样(这里只列出了前 10 个值):

q_embeddings[:10]
array([-0.45518905, -0.6450379, 0.3097812, -0.4861114 , -0.08480848,
 -0.1664767 , 0.1875889, 0.3513346, -0.04495572, 0.12551129],

这些嵌入向量可以用来比较单词/句子及其相互关系。这些向量可以通过嵌入模型构建。可以在MTEB 排行榜找到各种统计数据的模型集。使用这些模型之一就像这样简单:

from sentence_transformers import SentenceTransformer, util

embedding_model_path = "/mystuff/llm/bge-small-en" 
embedding_model = SentenceTransformer(embedding_model_path, device='cuda')

HuggingFace 上的模型页面通常显示示例代码。上述代码从本地磁盘加载模型“ bge-small-en ”。使用此模型创建嵌入只是:

question = "what is google bard?" 
q_embeddings = embedding_model.encode(question)

在这种情况下,嵌入模型用于将给定的问题编码为嵌入向量。该向量与上面的示例相同:

q_embeddings.shape
(, 384)

q_embeddings[:10]
array([-0.45518905, -0.6450379, 0.3097812, -0.4861114 , -0.08480848,
       -0.1664767 , 0.1875889, 0.3513346, -0.04495572, 0.12551129],
       dtype=float32)

形状 (, 384) 表示 q_embeddings 是一个长度为 384 个浮点数的单一向量(而不是一次嵌入多个文本的列表)。上面的切片显示了这 384 个值中的前 10 个。某些模型使用更长的向量以获得更准确的关系,而其他模型,如本例所示,则使用较短的向量(此处为 384)。再次,MTEB 排行榜 提供了很好的示例。较小的向量需要更少的空间和计算,而较大的向量在表示块之间的关系以及有时的序列长度方面提供了一些改进。

对于我的 RAG 相似性搜索,我首先需要问题的嵌入。这就是上面的 q_embeddings。需要将其与所有被搜索文章(或其块)的嵌入向量进行比较。在这种情况下,所有被分块的维基百科文章。要为所有这些构建嵌入:

article_embeddings = embedding_model.encode(article_chunks)

这里 article_chunks 是来自英文维基百科数据转储的所有文章的所有块的列表。这样它们可以批量编码。

向量数据库

在大规模文档/文档块上实现相似性搜索,在基本层面上并不复杂。一个常见的方法是计算查询和文档向量之间的 余弦相似性,并进行排序。然而,在大规模时,这有时会变得有些复杂。向量数据库是使这种管理和搜索在规模上变得更简单/更高效的工具。

例如,Weaviate 是一个向量数据库,曾用于 StackOverflow 的基于 AI 的搜索。在其最新版本中,它也可以以 嵌入模式 使用,这使得它甚至可以在 Kaggle 笔记本中使用。它也被用于一些 Deeplearning.AI LLM 短期课程,所以至少似乎有些受欢迎。当然,还有许多其他工具,进行比较是很好的,这个领域也在快速发展。

在我的试验中,我使用了来自 Facebook/Meta 研究的 FAISS 作为向量数据库。FAISS 更像是一个库,而不是一个客户端-服务器数据库,因此在 Kaggle 笔记本中使用非常简单。它的表现也很不错。

分块数据和嵌入

一旦所有文章的分块和嵌入完成后,我构建了一个包含所有相关信息的 Pandas DataFrame。以下是我使用的维基百科数据转储前 5 个块的示例,文档标题为 无政府主义

我使用的维基百科数据转储中的第一篇文章的前 5 个块。

这个表格中的每一行(一个 Pandas DataFrame)包含块化过程后单个块的数据。它有 5 列:

  • chunk_id:允许我稍后将块嵌入映射到块文本。

  • doc_id:允许将块映射回其文档。

  • doc_title:用于尝试一些方法,例如将文档标题添加到每个块中。

  • chunk_title:块的文章子部分标题,与 doc_title 目的相同。

  • chunk:实际的块文本

这里是前五个无政府主义块的嵌入,顺序与上面的 DataFrame 相同:

[[ 0.042624 -0.131264 -0.266858 ... -0.329627 0.178211 0.248001]
 [-0.120318 -0.110153 -0.059611 ... -0.297150 -0.043165 0.558150]
 [ 0.116761 -0.066759 -0.498548 ... -0.330301 0.019448 0.326484]
 [-0.517585 0.183634 0.186501 ... 0.134235 -0.033262 0.498731]
 [-0.245819 -0.189427 0.159848 ... -0.077107 -0.111901 0.483461]]

每一行在这里部分展示,但说明了概念。

搜索相似的查询嵌入与块嵌入

早些时候,我对查询“ what is google bard? ”进行了编码,然后编码了所有文章块。通过这两组嵌入,RAG 搜索的第一部分很简单:找到“语义上”最接近查询的文档。实际上,只需计算查询嵌入向量与所有块向量之间的余弦相似度,并按相似度得分排序。

这里是与 q_embeddings 语义上最接近的前 10 个块:

按照与问题的余弦相似度排序的前 10 个块。

这个表格中的每一行(DataFrame)表示一个块。这里的 sim_score 是计算的余弦相似度得分,行从最高余弦相似度到最低排序。表格显示了前 10 个最高的 sim_score 行。

Re-ranking

纯粹基于嵌入的相似性搜索在计算上非常快速且低成本。然而,它不如其他一些方法准确。Re-ranking 是一个术语,用于描述使用另一个模型更准确地排序这些初始文档列表的过程,这种模型通常计算成本更高。这个模型通常在所有文档和块上运行的成本太高,但在初始相似性搜索后的前几个块上运行就可行得多。Re-ranking 帮助获得更好的最终块列表,以便为 RAG 的生成部分建立输入上下文。

同样的 MTEB 排行榜 也托管了许多模型的 re-ranking 得分。在这种情况下,我使用了 bge-reranker-base 模型进行 re-ranking:

import torch 
from transformers import AutoModelForSequenceClassification, AutoTokenizer 

rerank_model_path = "/mystuff/llm/bge-reranker-base"
rerank_tokenizer = AutoTokenizer.from_pretrained(rerank_model_path) 
rerank_model = AutoModelForSequenceClassification 
                  .from_pretrained(rerank_model_path) 
rerank_model.eval() 

def calculate_rerank_scores(pairs): 
    with torch.no_grad(): inputs = rerank_tokenizer(pairs, padding=True, 
                                          truncation=True, return_tensors='pt',
                                          max_length=512) 
    scores = rerank_model(**inputs, return_dict=True) 
                         .logits.view(-1, ).float() 
    return scores 

question = questions[idx]
pairs = [(question, chunk) for chunk in doc_chunks_all[idx]] 
rerank_scores = calculate_rerank_scores(pairs) 
df["rerank_score"] = rerank_scores

在将 rerank_score 添加到块 DataFrame 并用它进行排序后:

按照与问题的重新排序得分排序的前 10 个块。

比较上面的两个表格(第一个按sim_score排序,现按rerank_score排序),可以看到一些明显的差异。按嵌入生成的普通相似性得分(sim_score)排序, Tenor 页面 是第 5 个最相似的片段。由于 Tenor 似乎是一个由 Google 托管的 GIF 搜索引擎,我想看到它的嵌入与问题“what is google bard?”接近是有道理的。但它实际上与 Bard 本身没有什么关系,只是 Tenor 是一个在类似领域的 Google 产品。

然而,在按rerank_score排序后,结果更有意义。Tenor 从前 10 名中消失了,前 10 名列表中的最后两个片段似乎不相关。这些片段关于“Bard”和“Bård”的名字。可能是因为有关 Google Bard 的最佳信息来源似乎是 Google Bard 页面,在上述表格中这是 id 为 6026776 的文档。之后,我猜 RAG 用完了好的文章匹配,并有些偏离了正轨(Bård)。这也可以从表格最后两行/片段的负面重新排序得分中看到。

通常会有许多相关文档和文档中的片段,而不仅仅是上面提到的 1 份文档和 8 个片段。但是在这种情况下,这种限制有助于说明基于基本嵌入的相似性搜索和重新排序之间的区别,以及重新排序如何积极地影响最终结果。

构建上下文

当我们收集了 RAG 输入的顶级片段后,我们该怎么做?我们需要从这些片段中为生成模型构建上下文。最简单的方法就是将选择的顶级片段连接成一个长文本序列。该序列的最大长度受所用模型的限制。由于我使用了 Zephyr 7B 模型,所以我将 4096 个标记作为最大长度。 Zephyr 页面将此作为一个灵活的序列限制(带有滑动注意窗口)。更长的上下文似乎更好,但 这并不总是如此。最好尝试一下。

以下是我用来生成具有此上下文的答案的基本代码:

from transformers import AutoTokenizer, AutoModelForCausalLM 
import torch 

llm_answer_path = "/mystuff/llm/zephyr-7b-beta" 
torch_device = "cuda:0" 
tokenizer = AutoTokenizer.from_pretrained(llm_answer_path, 
                                          local_files_only=True) 
llm_answer = AutoModelForCausalLM.from_pretrained(llm_answer_path, 
                           device_map=torch_device, local_files_only=True, 
                           torch_dtype=torch.float16) 
# assuming here that "context" contains the pre-built context 
query = "answer the following question, "\ 
        "based on your knowledge and the provided context. "\n 
        "Keep the answer concise.\n\nquestion:" + question + 
        "\n\ncontext:"+context 

input_ids = tokenizer.encode(query+"\n\nANSWER:", return_tensors='pt', 
                             return_attention_mask=False).to(torch_device) 
greedy_output = llm_answer.generate(input_ids, max_new_tokens=1024, 
                                    do_sample=True) 
answer = tokenizer.decode(greedy_output[0], skip_special_tokens=True) 
print(answer[len(query):])

如前所述,在这种情况下,上下文只是将排名最高的片段连接起来。

生成答案

进行比较,首先让我们尝试模型在没有任何额外上下文的情况下的回答,即仅基于其训练数据:

query = "what is google bard?" 
input_ids = tokenizer.encode(query+"\n\nANSWER:", return_tensors='pt', 
                             return_attention_mask=False).to(torch_device) 
greedy_output = llm_answer.generate(input_ids, max_new_tokens=1024, 
                                    do_sample=True) 
answer = tokenizer.decode(greedy_output[0], skip_special_tokens=True) 
print(answer[len(query):])

这给出了(多次运行之一,略有变化但通常相似):

ANSWER:
Google Bard is an experimental, AI-based language model developed by 
Google's sister company, DeepMind. Its primary use is to generate 
human-like text responses to prompts, which can help in tasks such as 
content creation, idea generation, and text summarization. Bard is 
trained on a vast amount of textual data and can provide highly 
relevant and contextually accurate responses, making it a useful tool 
in various applications where text generation is required. However, as 
an experimental feature, Bard's accuracy and effectiveness may still be 
improving, and it is not yet publicly available for use.

通常来说,虽然准确,但缺乏最新的发展。相比之下,我们可以尝试将生成的上下文提供给问题:

query = "answer the following question, "\
        "based on your knowledge and the provided context. "\
        "Keep the answer concise.\n\n"\
        "question:" + question + "\n\ncontext:"+context
input_ids = tokenizer.encode(query+"\n\nANSWER:", return_tensors='pt',
                             return_attention_mask=False).to(torch_device)
greedy_output = llm_answer.generate(input_ids, max_new_tokens=1024, 
                                    do_sample=True)
answer = tokenizer.decode(greedy_output[0], skip_special_tokens=True)
print(answer[len(query):])

以下是一个示例答案,使用了按sim_score排序的片段作为上下文(包括 Tenor 和 Bård 页面片段):

ANSWER: 
Bard is a conversational generative artificial intelligence (AI) 
chatbot developed by Google based initially on the LaMDA family of 
large language models (LLMs) and later PaLM. It was launched in a 
limited capacity in March 2023 to mixed reviews, and expanded to other 
countries in May, following the launch of Microsoft's Bing Chat. Google 
researcher Jacob Devlin resigned from the company after alleging Bard 
had surreptitiously leveraged data from ChatGPT. Tenor is an online GIF 
search engine and database owned by Google, available on Android, iOS, 
and macOS. Its main product is the GIF Keyboard, which is integrated 
into numerous apps and messaging services. Bård is a Norwegian 
masculine given name of Old Norse origin. It is sometimes used as a 
surname. It may refer to several people. Tenor and Bård are not related.

这不是一个很好的答案,因为它开始谈论完全无关的话题,TenorBård。部分原因是因为在这种情况下,Tenor 块被包含在上下文中,块的顺序也通常较差,因为没有重新排序。

相比之下,使用rerank_score排序的上下文块(更好的块排序和 Tenor 消失):

ANSWER:
Google Bard is a conversational generative artificial intelligence (AI) 
chatbot, based initially on the LaMDA family of large language models 
(LLMs) and later on PaLM, developed by Google to compete with OpenAI's 
ChatGPT. It was rolled out in a limited capacity in March 2023 and 
expanded to more countries in May, prompting a mixed reception from 
critics, who raised safety and ethical concerns about its accuracy and 
usefulness. Google has promised that Bard will be tightly integrated 
with other Google AI products and services, leading to claims that a 
new AI-powered version of the Google Assistant, dubbed "Assistant with 
Bard", is being prepared for launch. Google has also stressed that Bard 
is still in its early stages and being continuously refined, with plans 
to upgrade it with new personalization and productivity features, while 
stressing that it remains distinct from Google Search.

现在不相关的主题已经消失,答案总体上更好,更切题。

这突出了不仅要找到适当的上下文以提供给模型,而且还要去除无关的上下文。在这种情况下,Zephyr 模型似乎无法直接识别哪个部分的上下文是相关的,而是似乎对所有内容进行了总结。不能真正责怪模型,因为我提供了这些上下文并要求它使用这些内容。

查看块的重新排序分数,基于诸如负面重新排序分数等指标的一般过滤方法也可以解决上述问题,因为在这种情况下“坏”块具有负面重新排序分数。

值得注意的是,Google 在我写这篇文章时发布了一个全新的、显著改进的Gemini模型系列。由于维基百科的内容生成有一些延迟,因此这里生成的答案没有提到这个模型。因此,如人们所想,尝试保持上下文的信息是最新的,并保持其相关性和重点是很重要的。

可视化嵌入检查

嵌入是一个很好的工具,但有时确实很难真正理解它们是如何工作的,以及相似度搜索发生了什么。一个基本的方法是将嵌入彼此绘制,以获得一些关于它们关系的见解。

构建这样的可视化使用PCA和可视化库是相当简单的。它涉及将嵌入向量映射到 2 维或 3 维,并绘制结果。在这里,我将这 384 维映射到 2 维,并绘制了结果:

import seaborn as sns 
import numpy as np 

fp_embeddings = embedding_model.encode(first_chunks) 
q_embeddings_reshaped = q_embeddings.reshape(1, -1) 
combined_embeddings = np.concatenate((fp_embeddings, q_embeddings_reshaped)) 

df_embedded_pca = pd.DataFrame(X_pca, columns=["x", "y"]) 
# text is short version of chunk text (plot title) 
df_embedded_pca["text"] = titles 
# row_type = article or question per each embedding 
df_embedded_pca["row_type"] = row_types 

X = combined_embeddings pca = PCA(n_components=2).fit(X) 
X_pca = pca.transform(X) 

plt.figure(figsize=(16,10)) 
sns.scatterplot(x="x", y="y", hue="row_type", 
                palette={"article": "blue", "question": "red"}, 
                data=df_embedded_pca, #legend="full", 
                alpha=0.8, s=100 ) 
for i in range(df_embedded_pca.shape[0]): 
   plt.annotate(df_embedded_pca["text"].iloc[i], 
                (df_embedded_pca["x"].iloc[i], df_embedded_pca["y"].iloc[i]), 
                fontsize=20 ) 
plt.legend(fontsize='20') 
# Change the font size for x and y axis ticks plt.xticks(fontsize=16) 
plt.yticks(fontsize=16) 
# Change the font size for x and y axis labels 
plt.xlabel('X', fontsize=16) 
plt.ylabel('Y', fontsize=16)

对于“what is google bard?”问题的前 10 篇文章,这里给出了以下可视化:

基于 PCA 的 2D 绘图,比较问题嵌入与文章第一个块嵌入。

在这个图中,红点是问题“what is google bard?”的嵌入。蓝点是根据sim_score找到的最接近的维基百科文章匹配项。

Bard 文章显然是与问题最接近的,而其他的则稍远一些。Tenor 文章似乎是第二接近的,而Bård 文章则稍远一些,可能是因为从 384 维映射到 2 维时信息的丢失。由于这一点,可视化并不是完全准确的,但对快速人工概览是有帮助的。

下图展示了我在 Kaggle 代码中发现的实际错误,使用了类似的 PCA 图。为了获取一些见解,我对维基百科转储中的第一篇文章(“ 无政府主义”)提出了一个简单的问题:“ 无政府主义的定义是什么?”。下面是 PCA 可视化的结果,标记的离群点可能是最有趣的部分:

我在 PCA 基于 2D 图的 Kaggle 嵌入中显示的失败,针对所选的顶级文档。

左下角的红点再次表示问题。旁边的蓝点簇是所有与无政府主义相关的文章。然后右上角有两个离群点。我删除了图表中的标题以保持其可读性。查看时,这两个离群文章似乎与问题无关。

为什么会这样?由于我使用了 512、256、128 和 64 的各种块大小来索引文章,在处理 256 块大小的所有文章时遇到了一些问题,并在中途重新启动了分块。这导致某些嵌入与我存储的块文本的索引有所不同。在注意到这些奇怪的结果后,我重新计算了 256 个令牌块大小的嵌入,并将结果与 512 大小进行比较,注意到这个差异。可惜那时比赛已经结束🙂

更高级的上下文选择

上述内容讨论了将文档分块并使用相似度搜索+重新排序作为找到相关块和构建问题回答上下文的方法。我发现有时也有必要考虑初始文档的选择方式,而不仅仅是块本身。

作为示例方法,高级 RAG课程在DeepLearning.AI上介绍了两种方法:句子窗口化和层次块合并。总结来说,这种方法查看附近的块,如果多个块的分数很高,则将它们作为一个大的块。所谓“层次结构”是通过考虑越来越大的块组合来共同相关。旨在提供更连贯的上下文,而不是随机排序的小块,给生成 LLM 更好的输入。

作为一个简单的示例,这是我上面 Bard 示例的重新排序的前几个块:

我在 Bard 示例中的前 10 个块,按重新排序分数排序。

这里最左侧的列是块的索引。在我的生成中,我只是按表中的排序顺序取了顶级块。如果我们想使上下文更连贯,我们可以按文档中的顺序对最终选择的块进行排序。如果在高度排名的块之间有小片段缺失,添加缺失的部分(例如这里的块 ID 7)可能有助于填补空白,类似于层次合并。这可能是作为最终步骤进行尝试的内容,以获得最终的改进。

在我的 Kaggle 实验中,我仅基于第一个块进行初步文档选择。这部分是由于 Kaggle 的资源限制,但它似乎也有一些其他的优势。通常,一篇文章的开头部分充当了总结(引言或摘要)。从这些排名文章中进行初步块选择可能有助于选择具有更相关整体上下文的块。

在我上面的 Bard 示例中可以看到,无论是 rerank_score 还是 sim_score,对于最佳文章的第一个块都是最高的。为了改进这一点,我还尝试使用更大的块大小进行初始文档选择,以包括更多的引言以提高相关性。然后将顶级选择的文档按较小的块大小进行切分,以实验每种大小的上下文效果。

由于资源限制,我无法在 Kaggle 上对所有文档的所有块进行初始搜索,但我在 Kaggle 外部尝试了一下。在这些试验中,我发现有时单个不相关的文章块会被排名较高,而实际上对答案生成存在误导。例如,与相关电影的演员传记。初步的文档相关性选择可能有助于避免这种情况。不幸的是,我没有时间用不同的配置进一步研究这个问题,好的重排名可能已经有帮助。

最后,在上下文中重复相同的信息在多个块中不是很有用。块的最高排名并不保证它们彼此最好地补充,或最佳块多样性。例如,LangChain 为 最大边际相关性 提供了一个特殊的块选择器。它通过对新块进行惩罚,惩罚依据是它们与已添加块的相似度来实现这一点。

扩展 RAG 查询

我在这里使用了一个非常简单的问题/查询作为我的 RAG 示例(“ what is google bard?”),简单的查询有助于说明基本的 RAG 概念。考虑到我使用的嵌入模型具有 512 令牌的最大序列长度,这个查询输入相当简短。如果我使用嵌入模型的分词器(bge-small-en)将这个问题编码成令牌,我会得到以下令牌:

['[CLS]', 'what', 'is', 'google', 'bard', '?', '[SEP]']

这总共是 7 个标记。最大序列长度为 512,这为我使用更长的查询句子留出了足够的空间。有时这很有用,特别是当我们想要检索的信息不是简单的查询,或者领域较复杂时。对于非常简单的查询,语义搜索可能效果不好,正如在Stack Overflows AI Journey 文章中提到的那样。

例如,Kaggle 比赛有一组问题,每个问题有 5 个答案选项可以选择。我最初尝试了仅将问题作为嵌入模型的输入的 RAG。搜索结果并不理想,因此我再次尝试了将问题 + 所有答案选项作为查询。这产生了更好的结果。

作为一个例子,比赛训练数据集中的第一个问题:

Which of the following statements accurately describes the impact of 
Modified Newtonian Dynamics (MOND) on the observed "missing baryonic mass" 
discrepancy in galaxy clusters?

对于 bge-small-en 模型,这需要 32 个标记。因此,最大 512 个标记的序列长度还剩大约 480 个标记可以填充。

这里是第一个问题及其给出的 5 个答案选项:

示例问题和答案选项 A-E。将所有这些文本合并形成了查询。

将问题和给定的选项合并成一个 RAG 查询,得到的长度为 235 个标记,并且仍然有超过 50% 的嵌入模型序列长度剩余。就我而言,这种方法产生了更好的结果,无论是通过人工检查,还是比赛分数。因此,尝试不同的方式使 RAG 查询本身更具表现力是值得的。

幻觉

最后是关于幻觉的话题,即模型生成的文本不正确或虚构。我的 sim_score 排序中的 Tenor 示例就是一个例子,即使生成器确实基于实际的给定上下文。因此,我想最好还是保持上下文良好 😃.

为了解决幻觉问题,大型 AI 公司(Google BardChatGPTBing Chat)的聊天机器人都提供了将其生成的回答部分链接到可验证来源的方法。Bard 有一个特定的 “G” 按钮,可以执行 Google 搜索,并突出显示与搜索结果匹配的生成回答部分。可惜我们并不总是拥有世界级的搜索引擎来帮助处理我们的数据。

Bing Chat采用了类似的方法,突出显示答案的部分并添加对源网站的引用。ChatGPT的方法略有不同;我必须明确要求它验证其答案并更新最新进展,告诉它使用其浏览器工具。之后,它进行了互联网搜索并链接到特定网站作为来源。源的质量似乎有很大的变化,就像任何互联网搜索一样。当然,对于内部文档,这种类型的网络搜索是不可能的。然而,即使在内部,也应该始终可以链接到来源。

我还询问了 Bard、ChatGPT+和 Bing 关于检测幻觉的想法。结果包括一个 LLM 幻觉排名指数,以及RAG 幻觉。在调优 LLM 时,将温度参数设为零可能有助于 LLM 生成确定性的、最可能的输出令牌。

最后,由于这是一个非常常见的问题,似乎有各种方法正在被构建以更好地解决这一挑战。例如,特定的LLM 来帮助检测幻觉似乎是一个有前途的领域。我没有时间尝试它们,但在更大的项目中肯定是相关的。

评估结果

除了实现一个有效的 RAG 解决方案之外,能够评估它的效果也是很有价值的。在 Kaggle 比赛中,这相当简单。我只是运行解决方案以尝试回答训练数据集中的给定问题,并将其与训练数据中提供的正确答案进行比较。或者将模型提交到 Kaggle 比赛测试集进行评分。答案分数越高,RAG 解决方案就越好,即使分数背后还有更多内容。

在许多情况下,可能没有适用于领域特定 RAG 的合适评估数据集。对于这种情况,可以考虑从一些通用 NLP 评估数据集开始,例如这个列表。像 LangChain 这样的工具还提供了自动生成问题和答案并进行评估的支持。在这种情况下,使用一个 LLM 为给定文档集创建示例问题和答案,另一个 LLM 用于评估 RAG 是否能够提供这些问题的正确答案。也许可以在这个LangChain 的 RAG 评估教程中更好地解释。

虽然通用解决方案在开始时可能很好,但在实际项目中,我会尝试收集来自领域专家和目标用户的真实问题和答案数据集。由于大型语言模型(LLM)通常被期望生成自然语言响应,这些响应可能在正确的前提下变化很大。因此,评估答案是否正确不像正则表达式或类似的模式匹配那么直接。在这种情况下,我发现使用另一种 LLM 来评估给定的响应是否匹配参考响应是一个非常有用的工具。这些模型能够更好地处理文本变异。

结论

RAG 是一个非常不错的工具,随着对 LLM 的高度关注,它现在也是一个相当热门的话题。虽然 RAG 和嵌入技术已经存在了很长时间,但最新的强大 LLM 及其快速演变可能使它们在许多高级应用场景中更具吸引力。我预计这一领域将持续以良好的速度发展,有时很难跟上所有最新动态。为此,像RAG 发展综述这样的总结可以提供至少保持主要发展方向的参考。

一般来说,RAG 方法相当简单:找到与给定查询类似的一组文本块,将它们拼接成上下文,然后向 LLM 请求答案。然而,正如我在这里尝试展示的那样,在如何使这一过程对不同需求有效且高效方面,可能会有各种问题需要考虑。从良好的上下文检索,到排名和选择最佳结果,最后能够将结果链接回实际的源文档。还要评估生成的查询上下文和答案。正如Stack Overflow 的人们指出的,有时更传统的词汇搜索或混合搜索也非常有用,即使语义搜索也很酷。

今天就到这里。RAG 继续...

ChatGPT+/DALL-E3 对 RAG 的理解..

最初发布于 http://teemukanstren.com 于 2023 年 12 月 25 日。

LLMOps:与 Hamilton 一起进行生产级提示工程模式

原文:towardsdatascience.com/llmops-production-prompt-engineering-patterns-with-hamilton-5c3a20178ad2?source=collection_archive---------1-----------------------#2023-09-13

生产级别的提示迭代概述,与 Hamilton 一起进行

Stefan KrawczykTowards Data Science Stefan Krawczyk

·

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

--

提示。在生产环境中,如何演变这些提示?这篇文章基于最初发表的内容 这里. 图片来自 pixabay.

你发送给大型语言模型(LLM)的内容非常重要。小的变化和调整可能对输出产生重大影响,因此随着你的产品发展,你的提示也需要进化。LLMs 也在不断开发和发布,因此随着 LLMs 的变化,你的提示也需要变化。因此,建立一个迭代模式来操作化你的“部署”提示是重要的,以便你和你的团队可以高效地移动,同时确保生产问题最小化,甚至避免。在这篇文章中,我们将通过Hamilton这一个开源微调度框架来指导你管理提示的最佳实践,并类比MLOps模式,并讨论其中的权衡。这篇文章的高级要点即使你不使用 Hamilton 也同样适用。

在我们开始之前几点注意事项:

  1. 我是Hamilton的共同创建者之一。

  2. Hamilton不熟悉?请滚动到底部查看更多链接。

  3. 如果你在寻找讨论“上下文管理”的文章,这不是那篇文章。但这篇文章将帮助你了解如何迭代和创建生产级别的“提示上下文管理”迭代故事。

  4. 我们将提示和提示模板互换使用。

  5. 我们假设这些提示是在“在线”网页服务设置中使用的。

  6. 我们将使用我们的Hamilton 的 PDF 摘要示例来映射我们的模式。

  7. 我们的信誉是什么?我们在为 Stitch Fix 的 100 多位数据科学家构建自服务数据/MLOps 工具方面度过了职业生涯。因此,我们见证了很多故障和方法的演变。

提示对 LLMs 的作用类似于超参数对 ML 模型的作用。

要点: 提示+LLM APIs 类似于超参数+机器学习模型。

就“Ops”实践而言,LLMOps 仍处于起步阶段。MLOps 稍微成熟一点,但如果与 DevOps 实践的广泛知识相比,仍然没有被广泛采用。

DevOps 实践主要关注如何将代码交付到生产环境,而 MLOps 实践关注如何将代码& 数据工件(例如,统计模型)交付到生产环境。那么 LLMOps 呢?个人认为,它更接近 MLOps,因为你有:

  1. 你的 LLM 工作流仅仅是代码。

  2. LLM API 是一个数据工件,可以使用提示“调整”,类似于机器学习(ML)模型及其超参数。

因此,你很可能需要紧密版本控制 LLM API 和提示,以确保良好的生产实践。例如,在 MLOps 实践中,你需要一个过程来验证 ML 模型在其超参数更改时仍然表现正确。

你应该如何考虑提示的操作化?

需要明确的是,控制的两个部分是LLM提示。类似于 MLOps,当代码或模型工件发生变化时,你需要能够确定是哪一部分发生了变化。对于 LLMOps,我们也需要相同的辨别能力,将 LLM 工作流与 LLM API + 提示分开。重要的是,我们应该认为 LLM(自托管或 API)大多是静态的,因为我们不经常更新(甚至控制)它们的内部。因此,改变 LLM API + 提示的提示部分实际上就像是创建一个新的模型工件。

处理提示的主要方式有两种:

  1. 提示作为动态运行时变量。所使用的模板在部署时不是静态的。

  2. 提示作为代码。提示模板在给定的部署下是静态的/预定的。

主要区别在于你需要管理的移动部分的数量,以确保一个良好的生产故事。下面,我们将探讨如何在这两种方法的背景下使用 Hamilton。

提示作为动态运行时变量

动态传递/加载提示

提示只是字符串。由于字符串在大多数语言中是原始类型,这意味着它们非常容易传递。这个想法是抽象你的代码,使你在运行时传递所需的提示。更具体地说,你会在有“更新”的提示模板时“加载/重新加载”提示模板。

这里的 MLOps 类比是,当有新的模型可用时,自动重新加载 ML 模型工件(例如,pkl 文件)。

MLOps 类比:图示 ML 模型自动重新加载的效果。图片作者。

图示动态重新加载/查询提示的效果。图片作者。

这里的好处是你可以非常迅速地推出新的提示,因为你不需要重新部署你的应用程序!

这种迭代速度的缺点是增加了操作负担:

  1. 对于监控你的应用程序的人来说,什么时候发生了变化以及是否已经在你的系统中传播开来将是不清楚的。例如,你刚刚推送了一个新的提示,而 LLM 现在每个请求返回更多的 token,导致延迟激增;监控的人可能会感到困惑,除非你有一个优秀的变更日志文化。

  2. 回滚语义涉及到需要了解另一个系统。你不能仅仅回滚之前的部署来修复问题。

  3. 你需要优秀的监控来了解运行了什么以及何时运行;例如,当客户服务给你一个调查的票据时,你怎么知道使用了什么提示?

  4. 你需要管理和监控你用来管理和存储提示的任何系统。这将是一个你需要维护的额外系统,超出了提供你代码的系统之外。

  5. 你需要管理两个过程,一个用于更新和推送服务,另一个用于更新和推送提示。同步这些更改将由你负责。例如,你需要对服务进行代码更改以处理新的提示。你需要协调更改两个系统以使其工作,这增加了额外的操作开销。

与 Hamilton 一起工作的方式

如果你删除sumarize_text_from_summaries_promptsumarize_chunk_of_text_prompt函数定义,我们的 PDF 总结器流程大致如下:

summarization_shortened.py。注意两个输入*_prompt,它们表示现在作为数据流输入所需的提示。通过 Hamilton,你可以通过查看像这样的图表来确定你的提示模板所需的输入。图表由 Hamilton 创建。图像由作者提供。

要操作事物,你需要在请求时注入提示:

from hamilton import base, driver
import summarization_shortend

# create driver
dr = (
    driver.Builder()
    .with_modules(summarization_sortened)
    .build()
)

# pull prompts from somewhere
summarize_chunk_of_text_prompt = """SOME PROMPT FOR {chunked_text}"""
summarize_text_from_summaries_prompt = """SOME PROMPT {summarized_chunks} ... {user_query}"""

# execute, and pass in the prompt 
result = dr.execute(
   ["summarized_text"],
   inputs={
      "summarize_chunk_of_text_prompt": summarize_chunk_of_text_prompt,
      ...
   }
)

或者你可以更改代码以动态加载提示,即将函数添加到 Hamilton 数据流中以从外部系统检索提示。在每次调用时,它们将查询要使用的提示(当然你可以缓存以提高性能):

# prompt_template_loaders.py

def summarize_chunk_of_text_prompt(
  db_client: Client, other_args: str) -> str:
    # pseudo code here, but you get the idea:
    _prompt = db_client.query( 
         "get latest prompt X from DB", other_args)
    return _prompt

def summarize_text_from_summaries_prompt(
   db_client: Client, another_arg: str) -> str:
    # pseudo code here, but you get the idea:
    _prompt = db_client.query(
         "get latest prompt Y from DB", another_arg)
    return _prompt

Driver 代码:

from hamilton import base, driver
import prompt_template_loaders # <-- load this to provide prompt input
import summarization_shortend

# create driver
dr = (
    driver.Builder()
    .with_modules(
        prompt_template_loaders,# <-- Hamilton will call above functions
        summarization_sortened, 
    )
    .build()
)

# execute, and pass in the prompt 
result = dr.execute(
   ["summarized_text"],
   inputs={
      # don't need to pass prompts in this version
   }
)

我如何记录使用的提示并监控流程?

在这里,我们概述了几种监控发生情况的方法。

  • 记录执行结果。也就是说,运行 Hamilton,然后将信息发送到你希望它去的地方。
result = dr.execute(
   ["summarized_text", 
    "summarize_chunk_of_text_prompt",   
    ... # and anything else you want to pull out
    "summarize_text_from_summaries_prompt"],
   inputs={
      # don't need to pass prompts in this version
   }
)

my_log_system(result) # send what you want for safe keeping to some
                      # system that you own.

注意。在上述内容中,Hamilton 允许你请求 任何 中间 输出,只需按名称请求“函数”(即图中的节点)。如果我们真的想获取整个数据流的所有中间输出,我们可以这样做并将其记录到任何我们想要的地方!

import logging

logger = logging.getLogger(__name__)

def summarize_text_from_summaries_prompt(
    db_client: Client, another_arg: str) -> str:
    # pseudo code here, but you get the idea:
    _prompt = db_client.query(
         "get latest prompt Y from DB", another_arg)
    logger.info(f"Prompt used is [{_prompt}]")
    return _prompt
  • 扩展 Hamilton 以发出这些信息。你可以使用 Hamilton 捕获执行函数的信息,即节点,而无需在函数体内插入日志语句。这促进了重用,因为你可以在 Driver 级别在开发和生产设置之间切换日志记录。参见 GraphAdapters,或编写你自己的 Python 装饰器 来包装函数进行监控。

在上述任何代码中,你都可以轻松地引入第三方工具来帮助跟踪和监控代码以及外部 API 调用。

作为代码的提示

作为静态字符串的提示

由于提示仅仅是字符串,它们也非常适合与源代码一起存储。这个想法是将尽可能多的提示版本存储在你的代码中,以便在运行时,所用提示集是固定且确定的。

这里的 MLOps 类比是,与其动态重新加载模型,不如将 ML 模型嵌入到容器中/硬编码引用。一旦部署,你的应用程序拥有它所需的一切。部署是不可变的;一旦启动便不会更改。这使得调试和确定问题变得更加简单。

MLOps 类比:通过将模型固定以进行应用程序的部署,从而创建一个不可变的部署。图片由作者提供。

图示展示了如何将提示视为代码,使你能够利用 CI/CD 构建一个不可变的部署来与 LLM API 进行交互。图片由作者提供。

这种方法有许多操作上的好处:

  1. 每当推送一个新提示时,都会强制执行新的部署。如果新提示出现问题,回滚语义是明确的。

  2. 你可以同时提交源代码和提示的拉取请求(PR)。这使得审查更改变得更简单,并且可以清楚地了解这些提示将影响/交互的下游依赖关系。

  3. 你可以在 CI/CD 系统中添加检查,以确保不良提示不会进入生产环境。

  4. 调试问题变得更简单。你只需拉取创建的(Docker)容器,就能迅速轻松地精确复现任何客户问题。

  5. 不需要维护或管理其他“提示系统”。简化了操作。

  6. 这并不排除添加额外的监控和可视性。

这在 Hamilton 中如何工作

提示将被编码为数据流/有向无环图(DAG)中的函数:

PDF 摘要示例中的 summarization.py 文件的样子。提示模板是代码的一部分。图示由 Hamilton 创建。图片由作者提供。

将这段代码与 git 结合使用,我们为你的整个数据流(即“链”)提供了一个轻量级的版本控制系统,这样你总是可以根据 git commit SHA 辨别世界的状态。如果你想在任何时间点管理和访问多个提示,Hamilton 提供了两个强大的抽象来实现这一点:@config.whenPython 模块。这允许你存储并保留所有旧版本的提示,并通过代码指定使用哪个版本。

@config.when (文档)

Hamilton 有一个装饰器的概念,它们只是函数上的注解。@config.when 装饰器允许为数据流中的函数(即“节点”)指定替代实现。在这种情况下,我们指定替代的提示。

from hamilton.function_modifiers import config

@config.when(version="v1")
def summarize_chunk_of_text_prompt__v1() -> str:
    """V1 prompt for summarizing chunks of text."""
    return f"Summarize this text. Extract any key points with reasoning.\n\nContent:"

@config.when(version="v2")
def summarize_chunk_of_text_prompt__v2(content_type: str = "an academic paper") -> str:
    """V2 prompt for summarizing chunks of text."""
    return f"Summarize this text from {content_type}. Extract the key points with reasoning. \n\nContent:"

你可以继续添加带有@config.when注解的函数,这样可以通过传递给 Hamilton Driver的配置在它们之间切换。在实例化Driver时,它将使用与配置值关联的提示实现来构建数据流。

from hamilton import base, driver
import summarization
# create driver
dr = (
    driver.Builder()
    .with_modules(summarization)
    .with_config({"version": "v1"}) # V1 is chosen. Use "v2' for V2.
    .build()
)

模块切换

除了使用@config.when之外,你还可以将不同的提示实现放入不同的 Python 模块中。然后,在Driver构造时,传递适合你想使用的上下文的正确模块。

所以在这里我们有一个包含 V1 的模块:

# prompts_v1.py
def summarize_chunk_of_text_prompt() -> str:
    """V1 prompt for summarizing chunks of text."""
    return f"Summarize this text. Extract any key points with reasoning.\n\nContent:"

在这里,我们有一个包含 V2 的模块(看看它们之间的细微差别):

# prompts_v2.py
def summarize_chunk_of_text_prompt(content_type: str = "an academic paper") -> str:
    """V2 prompt for summarizing chunks of text."""
    return f"Summarize this text from {content_type}. Extract the key points with reasoning. \n\nContent:"

在下面的驱动代码中,我们根据某些上下文选择要使用的正确模块。

# run.py
from hamilton import driver
import summarization
import prompts_v1
import prompts_v2

# create driver -- passing in the right module we want
dr = (
    driver.Builder()
    .with_modules(
        prompts_v1,  # or prompts_v2
        summarization,
    )
    .build()
)

使用模块方法允许我们将整个提示集封装和版本化。如果你想回到过去(通过 git),或者查看一个被批准的提示版本,你只需要导航到正确的提交,然后查看正确的模块。

我该如何记录使用的提示并监控流程?

假设你使用 git 来跟踪你的代码,你就不需要记录使用了哪些提示。相反,你只需知道部署的 git 提交 SHA,你就能同时跟踪你的代码和提示的版本。

要监控流程,就像上述方法一样,你可以使用相同的监控钩子,我不会在这里重复,但它们是:

  • 请求任何中间输出并在 Hamilton 之外记录它们。

  • 从函数内部记录它们,或构建一个Python 装饰器 / GraphAdapter在框架级别进行记录。

  • 集成第三方工具来监控你的代码和 LLM API 调用。

  • 或者以上所有!

那如何进行 A/B 测试我的提示?

对于任何机器学习项目,测量变更的业务影响非常重要。同样,对于 LLMs + 提示,测试和衡量变更对重要业务指标的影响也很重要。在 MLOps 世界中,你会对 ML 模型进行 A/B 测试,以通过在它们之间分配流量来评估它们的业务价值。为了确保 A/B 测试所需的随机性,你不会在运行时知道使用哪个模型,直到掷硬币。然而,要让这些模型上线,它们都需要遵循一个过程以进行资格认证。因此,对于提示,我们也应该考虑类似的方式。

上述两种提示工程模式并不会妨碍你进行 A/B 测试提示,但这意味着你需要管理一个过程,以启用你正在并行测试的任何数量的提示模板。如果你还在调整代码路径,将它们放在代码中将更容易辨别和调试发生了什么,并且你可以利用 @config.when 装饰器/ Python 模块交换来实现这一目的。与其依赖你的日志/监控/可观察性栈来告诉你使用了哪个提示,特别是当你动态加载/传递它们时,并且还需进行心理映射哪个提示对应哪个代码路径,前者将更为简便。

请注意,如果你开始需要更改多个提示进行 A/B 测试,因为你在一个流程中有几个提示,那么这会变得更加困难。例如,你在工作流中有两个提示并且你正在更换 LLM,你将希望从整体上进行 A/B 测试,而不是逐个提示地测试。我们的建议是,通过将提示放入代码中,你的操作生活会更简单,因为你将知道哪些提示属于哪些代码路径,而无需进行任何心理映射。

总结

在这篇文章中,我们解释了在生产环境中使用 Hamilton 管理提示的两种模式。第一种方法将提示视为 动态运行时变量,而第二种方法将提示视为代码用于生产设置。如果你重视减少操作负担,那么我们的建议是将提示编码为代码,因为它在操作上更简单,除非更改它们的速度对你来说真的很重要。

总结:

  1. 提示作为动态运行时变量。使用外部系统将提示传递到你的 Hamilton 数据流,或使用 Hamilton 从数据库中提取提示。对于调试和监控,能够确定给定调用使用了哪个提示是很重要的。你可以集成开源工具,或使用像 DAGWorks 平台这样的工具来帮助确保你知道在任何代码调用中使用了什么。

  2. 提示作为代码。 将提示编码为代码允许使用 git 进行简单的版本控制。更改管理可以通过拉取请求和 CI/CD 检查来完成。它与 Hamilton 的功能,如 @config.when 和 Driver 级别的模块切换,工作良好,因为它清楚地确定使用了哪个版本的提示。这种方法加强了你可能使用的任何工具,如 DAGWorks 平台,因为部署的提示是不可变的。

我们想听听你的意见!

如果你对这些内容感到兴奋或有强烈的意见,请留下评论,或访问我们的 Slack 频道!一些用于表扬/投诉/聊天的链接:

其他你可能感兴趣的 Hamilton 链接/帖子:

  • tryhamilton.dev — 在浏览器中的互动教程!

  • Hamilton + Lineage 在 10 分钟内

  • 如何在 5 分钟内与 Pandas 一起使用 Hamilton

  • 如何在 5 分钟内与 Ray 一起使用 Hamilton

  • 如何在笔记本环境中使用 Hamilton

  • Hamilton 的背景故事与介绍

  • 使用 Hamilton 创建数据流的好处(来自 Hamilton 的有机用户帖子!)

每个人的 LLM:在 Google Colab 中运行 LangChain 和 MistralAI 7B 模型

原文:towardsdatascience.com/llms-for-everyone-running-langchain-and-a-mistralai-7b-model-in-google-colab-246ca94d7c4d

免费实验大型语言模型

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

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

--

LangChain 的艺术表现,照片由 Ruan Richard Rodrigues 提供,Unsplash

大家都知道大型语言模型的定义就是“大”。甚至在不久前,它们仅对高端硬件拥有者开放,或者至少对付费获取云访问或每次 API 调用的人开放。现在,情况正在改变。在这篇文章中,我将展示如何在 Google Colab 中完全免费运行 LangChain Python 库、FAISS 向量数据库和 Mistral-7B 模型,并进行一些有趣的实验。

组件

这里有很多关于在 Python 中使用大型语言模型的文章,但它们往往不容易重现。例如,许多使用 LangChain 库的示例使用了一个 OpenAI 类,其第一个参数(猜猜是什么?)是 OPENAI_API_KEY。一些其他 RAG(检索增强生成)和向量数据库的示例使用了 Weaviate;我们打开其网站后首先看到的是“定价”。在这里,我将使用一组可以完全免费使用的开源库:

  • LangChain。它是一个用于开发语言模型驱动应用程序的 Python 框架。它也是模型无关的,相同的代码可以与不同的模型重复使用。

  • FAISS(Facebook AI 相似性搜索)。这是一个用于高效相似性搜索和稠密向量存储的库,我将使用它进行检索增强生成(Retrieval Augmented Generation)。

  • Mistral 7B 是一个拥有 7.3B 参数的大型语言模型(在 Apache 2.0 许可证下发布),根据作者的说法,它在所有基准测试中超越了 13B Llama2。它也可以在 HuggingFace 上使用,因此使用起来非常简单。

  • 最后但同样重要的是,Google Colab也是这次测试的重要组成部分。它提供了对由 CPU、16 GB NVIDIA Tesla T4 或甚至 80 GB NVIDIA A100(尽管我从未见过免费实例中提供最后一个)的 Python 笔记本的免费访问。

现在,让我们开始吧。

安装

首先,我们需要打开 Google Colab 并创建一个新的笔记本。需要的库可以在第一个单元格中使用pip安装:

!pip install bitsandbytes accelerate xformers einops langchain faiss-cpu transformers sentence-transformers

在运行代码之前,我们需要选择运行时类型:

Google Colab,作者截图

现在,让我们导入这些库:

from typing import List
import transformers
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig, BitsAndBytesConfig
import torch
from langchain.prompts import PromptTemplate
from langchain.llms import HuggingFacePipeline
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.chains import ConversationalRetrievalChain, RetrievalQA
from langchain.callbacks.tracers import ConsoleCallbackHandler
from langchain_core.vectorstores import VectorStoreRetriever
from langchain.memory import ConversationBufferMemory
from langchain.vectorstores import FAISS

device = 'cuda' if torch.cuda.is_available() else 'cpu'
print("Device:", device)
if device == 'cuda':
    print(torch.cuda.get_device_name(0))

# >>> Device: cuda
# >>> Tesla T4

如果一切正常,输出应显示“cuda”设备和选定的“Tesla T4”显卡。

下一步是最重要和最耗资源的:让我们加载语言模型

orig_model_path = "mistralai/Mistral-7B-Instruct-v0.1"
model_path = "filipealmeida/Mistral-7B-Instruct-v0.1-sharded"
bnb_config = BitsAndBytesConfig(
                                load_in_4bit=True,
                                bnb_4bit_use_double_quant=True,
                                bnb_4bit_quant_type="nf4",
                                bnb_4bit_compute_dtype=torch.bfloat16,
                               )
model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True, quantization_config=bnb_config, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(orig_model_path)

在这里,我选择了 4 位量化模式,这允许模型适应 GPU 内存。还有另一个棘手的部分。加载原始“mistralai/Mistral-7B-Instruct-v0.1”模型会导致 Colab 实例崩溃。令人惊讶的是,GPU 内存足够用于 4 位量化,但模型文件的大小约为 16 GB,免费 Colab 实例上没有足够的“正常”内存来在将其加载到 GPU 之前对模型进行量化!作为变通方法,我使用了一个“分片”版本,它被拆分成 2GB 的块(如果您的 PC 或 Colab 实例有超过 16GB 的内存,则不需要这样做)。

另外,想了解更多关于 4 位量化如何工作的读者欢迎阅读另一篇文章:

## 16、8 和 4 位浮点格式——它是如何工作的?

让我们深入探讨

towardsdatascience.com

如果一切正常,Colab 的输出应如下所示:

加载 Mistral 7B 模型,作者截图

从图片中可以看出,需要下载的文件非常庞大,因此如果在本地运行此代码(而不是在 Colab 中),请确保您的网络流量没有限制。

现在,让我们创建 LLM 管道

text_generation_pipeline = transformers.pipeline(
    model=model,
    tokenizer=tokenizer,
    task="text-generation",
    eos_token_id=tokenizer.eos_token_id,
    pad_token_id=tokenizer.eos_token_id,
    repetition_penalty=1.1,
    return_full_text=True,
    max_new_tokens=100,
)
mistral_llm = HuggingFacePipeline(pipeline=text_generation_pipeline)

恭喜!我们的安装已准备好,我们成功加载了一个 7B 语言模型。为了进行简单测试,让我们看看 LLM 是否有效:

text = "What is Mistral? Write a short answer."
mistral_llm.invoke(text)

#> Mistral is a type of cold front that forms over the Mediterranean 
#> Sea and moves eastward across southern Europe, bringing strong winds 
#> and sometimes severe weather conditions such as heavy rainfall, hail, 
#> and even tornadoes.

如果一切正常,我们准备好享受乐趣并进行进一步的测试。

LangChain

LangChain 是一个专门为与语言模型配合使用而设计的 Python 框架。作为热身,让我们测试一个提示模板

from langchain.prompts import PromptTemplate

prompt = PromptTemplate.from_template(
    "Tell me a {adjective} joke about {content}."
)
prompt.format(adjective="funny", content="chickens")

llm_chain = prompt | mistral_llm
llm_chain.invoke({"adjective": "funny", "content": "chickens"})

#> Why don't chickens like to tell jokes? They might crack each other
#> up and all their eggs will scramble!

有趣的是,LangChain 是“跨平台”的,我们可以在不更改代码的情况下使用不同的语言模型。这个示例取自官方 库文档,其中 OpenAI 用于提示,但我使用了相同的模板来进行 Mistral。

它是如何工作的?可以将 ConsoleCallbackHandler 添加到配置中,这样我们就可以看到所有的中间步骤:

llm_chain = prompt | mistral_llm
llm_chain.invoke({"adjective": "funny", "content": "chickens"},
                 config={'callbacks': [ConsoleCallbackHandler()]})

然后,输出将如下所示:

[1:chain:RunnableSequence] Entering Chain run with input:
{
  "adjective": "funny",
  "content": "chickens"
}

[1:chain:RunnableSequence > 2:prompt:PromptTemplate] Entering Prompt run with input:
{
  "adjective": "funny",
  "content": "chickens"
}

[1:chain:RunnableSequence > 3:llm:HuggingFacePipeline] Entering LLM run with input:
{
  "prompts": [
    "Tell me a funny joke about chickens."
  ]
}

[1:chain:RunnableSequence > 3:llm:HuggingFacePipeline] [3.60s] Exiting LLM run with output:
{
  "generations": [
    [
      {
        "text": "\n\nWhy don't chickens like to tell jokes? They might crack each other up and all their eggs will scramble!",
        "generation_info": null,
        "type": "Generation"
      }
    ]
  ],
  "llm_output": null,
  "run": null
}

作为另一个示例,让我们尝试 ChatPromptTemplate 类:

chat_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a helpful AI bot. Your name is {name}. Answer with short sentences."),
    ]
)

llm_chain = chat_prompt | mistral_llm
llm_chain.invoke({"name": "Mistral", "user_input": "What is your name?"})

#> Mistral: Yes, I am Mistral. How can I assist you today?

在我看来,回答“是的,我是 Mistral”是可以接受的,但从语言学角度来看,对于“你叫什么名字?”这个问题并不是最好的回答。显然,对于大型神经网络来说,可解释性可能是一个问题,我们无法告诉为什么模型以这种或那种方式做出回应。这可能是 4 位量化的副作用(这会稍微降低模型质量)或只是 7B 模型能力的根本限制(显然,其他 33B 或 70B 模型可能表现更好,但需要更多计算资源)。

检索增强生成(RAG)

现在,RAG 是一个热门研究话题。它允许我们自动将外部文档添加到 LLM 提示中,并在不微调模型的情况下添加更多信息。让我们看看如何将其与 LangChain 和 Mistral 一起使用。

首先,我们需要创建一个单独的嵌入模型

from langchain.embeddings import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/all-MiniLM-l6-v2",
    model_kwargs={"device": "cuda"},
)

这个小型 sentence-transformer 模型 能够将文本字符串转换为向量表示;我们将用它来构建我们的向量数据库。作为一个玩具示例,我将仅向数组中添加一个“文档”:

db_docs = [
    "Airbus's registered headquarters is located in Leiden, Netherlands.",
]

然后我们需要创建一个向量数据库和一个 VectorStoreRetriever 对象:

from langchain.vectorstores import FAISS
from langchain_core.vectorstores import VectorStoreRetriever

vector_db = FAISS.from_texts(db_docs, embeddings)
retriever = VectorStoreRetriever(vectorstore=vector_db)

现在,我们可以创建一个 RetrievalQA 对象,这个对象专门用于问答:

template = """You are a helpful AI assistant. Use the following pieces of context to answer the question at the end.
              {context}
              If you don't know the answer, just say that you don't know, don't try to make up an answer.
              Chat history: {history}
              Question: {question}
              Write your answers short. Helpful Answer:"""

prompt = PromptTemplate(
        template=template, input_variables=["history", "context", "question"]
    )
qa = RetrievalQA.from_chain_type(
        llm=mistral_llm,
        chain_type="stuff",
        retriever=retriever,
        chain_type_kwargs={
            "verbose": False,
            "prompt": prompt,
            "memory": ConversationBufferMemory(
                memory_key="history",
                input_key="question"),
        }
    )

我将询问模型有关空客的问题;这些问题模型很可能不知道:

qa.run("Hi, who are you?")
#> I am an AI assistant.

qa.run("What is the range of Airbus A380?")
#> The range of Airbus A380 is approximately 12,497 nautical miles.

qa.run("What is the tire diameter of Airbus A380 in centimeters?")
#> I don't know.

我对这些回答感到积极惊讶。首先,Mistral 7B 模型已经知道了空客 A380 的航程(我用 Google 查了一下,结果看起来是正确的)。其次,正如我所预期的,模型并不知道 A380 轮胎的直径,但它“诚实地”回答了“我不知道”,而不是提供“虚构的”错误回答。

现在,让我们向我们的“向量数据库”中添加一个额外的字符串:

db_docs = [
    "Airbus's registered headquarters is located in Leiden, Netherlands.",
    "The Airbus A380 has the largest commercial plane tire size in the world at 56 inches in diameter."
]

然后我们可以再试一次:

qa.run("What is the tire diameter of Airbus A380 in centimeters? Write a short answer.")
#> 142 cm

这真是太棒了!模型不仅找到了 A380 轮胎直径为 56 英寸的信息,还正确地将其转换为厘米(56*2.54 确实是 142)。我们知道数学任务通常对 LLM 来说很困难,所以这种准确性令人惊讶。

我们还可以要求模型逐步解释答案:

qa.run("What is the tire diameter of Airbus A380 in centimeters? Explain the answer in three steps.")
#> 1\. The tire diameter of Airbus A380 is 56 inches in diameter.
#> 2\. To convert 56 inches to centimeters, we need to multiply it by 2.54 (the conversion factor from inches to centimeters).
#> 3\. Therefore, the tire diameter of Airbus A380 in centimeters is 142.16 cm.

这太棒了!好吧,我们已经习惯于大型语言模型如 GPT3 或 GPT4 在云端超级计算机上运行并产生惊人结果的事实。但在你的本地 GPU 上看到这一点(我在 Google Colab 和我的家用 PC 上都测试了这段代码)是完全不同的感觉。

细心的读者可能会问,Mistral 和 Retriever 模型如何协同工作?确实,我创建了一个“Mistral-7B-Instruct-v0.1”模型和一个“all-MiniLM-l6-v2”句子嵌入模型。它们的向量空间是否兼容?答案是“否”。当我们进行查询时,VectorStoreRetriever 会先进行自己的搜索,找到向量库中最好的文档,并以纯文本格式返回这些文档。如果我们将 verbose 参数设置为 True,就可以看到最终的提示:

template = """You are a helpful AI assistant. Use the following pieces of context to answer the question at the end.
              {context}
              If you don't know the answer, just say that you don't know, don't try to make up an answer.
              Chat history: {history}
              Question: {question}
              Write your answers short. Helpful Answer:"""

prompt = PromptTemplate(
        template=template, input_variables=["history", "context", "question"]
    )
qa = RetrievalQA.from_chain_type(
        llm=mistral_llm,
        chain_type="stuff",
        retriever=retriever,
        chain_type_kwargs={
            "verbose": True,
            "prompt": prompt,
            "memory": ConversationBufferMemory(
                memory_key="history",
                input_key="question"),
        }
    )

在运行相同的代码后,我们可以看到 LangChain 发送给模型的实际提示:

You are a helpful AI assistant. Use the following pieces of context to answer the question at the end.
The Airbus A380 has the largest commercial plane tire size in the world at 56 inches in diameter.
Airbus's registered headquarters is located in Leiden, Netherlands.
If you don't know the answer, just say that you don't know, don't try to make up an answer.
Chat history. Human: Hi, who are you?
AI:  I am an AI assistant.
Human: What is the range of Airbus A380?
AI:  The range of Airbus A380 is approximately 12,497 nautical miles.
Question: What is the tire diameter of Airbus A380 in centimeters? Explain the answer in three steps.
Write your answers short. Helpful Answer:

在这种情况下,这两个文档都与“空客”问题相关,VectorStoreRetriever 将它们放在 context 占位符中。

结论

在这篇文章中,我们成功在免费的 Google Colab 实例上运行了一个 7.3B 的 Mistral 大型语言模型,完全依赖于免费的开源组件。这是一项伟大的成就,同时也是 Google 的慷慨举措,因为在撰写本文时,Amazon 上最便宜的 16GB 显卡至少需要 $500(不过我必须承认,Google Colab 服务并非纯粹的慈善,免费的 GPU 后端可能并不总是可用;需要经常使用的人应该考虑购买付费订阅)。我们还能够使用检索增强生成来为 LLM 提供额外的信息。如果像这样的模型已经准备好投入生产,是否真的合适仍然是一个悬而未决的问题,也永远是“购买还是自己动手”的难题。Mistral 7B 模型有时仍会“幻觉”并产生错误的答案;它也可能被更大的模型超越。无论如何,能够免费测试这样的模型对于学习、自我教育和原型设计都是极好的。

接下来的部分,我将展示如何在 Google Colab 中运行 LLaMA-13B 模型和 LangChain 框架:

## 适合所有人的 LLM:在 Google Colab 中运行 LLaMA-13B 模型和 LangChain

免费实验大型语言模型(第二部分)

[towardsdatascience.com

对使用语言模型和自然语言处理感兴趣的人也欢迎阅读其他文章:

如果你喜欢这个故事,欢迎订阅Medium,你将获得新文章发布的通知,并且可以完全访问其他作者的成千上万篇故事。也欢迎通过LinkedIn与我联系。如果你想获得此文及其他文章的完整源代码,可以访问我的Patreon 页面

感谢阅读。

LLMs,新型大规模虚假信息武器?

原文:towardsdatascience.com/llms-weapons-of-mass-disinformation-4def0dc3dc7

大型语言模型(LLMs)的双刃剑

你以为 2016 年的脱欧和美国总统竞选已经够糟糕了?再想想吧。

Anthony MensierTowards Data Science Anthony Mensier

·发表于Towards Data Science ·阅读时间 17 分钟·2023 年 5 月 29 日

--

图片由作者使用 Midjourney 5 生成

欢迎来到 LLMs 的双刃剑系列!

不妨直言不讳,ChatGPT 这一大型语言模型(LLM)的重大发布是一场席卷全球的现象,揭示了自然语言处理(NLP)进步的崭新世界。仿佛帷幕拉开,公众、政府以及国际机构一同目睹了这项技术在他们面前迈出的大胆步伐。随之而来的是一场真正的创新烟花表演。例如,ThinkGPT,这是一个巧妙的 Python 库,为 LLMs 提供了人工思维链和长期记忆,几乎使它们能够‘思考’(并非玩笑)。或者AutoGPT,另一个能够处理复杂请求并生成 AI 代理以完成这些请求的库。

这些只是基于 LLMs API 开发的数百个应用中的两个例子。我对人们利用这些新工具的独创性感到印象深刻,他们巧妙地重新利用了这些由 OpenAI、Facebook、Cohere 和 Google 等公司慷慨提供的乐高积木。但现在我得戴上我的严肃帽子了,大家。正如我们亲爱的本叔叔明智地告诫的那样(漫画迷们,你们知道的;如果不知道,我建议你们赶紧去看看最近一期的《蜘蛛侠》),“能力越大,责任越大。” 坦率地说,我并不完全相信这些公司在将他们的脑力结晶放出世界让人们琢磨时,尽到了应有的责任。

图片来自于维基百科

别误会我的意思。我在过去六年里一直深入应用 NLP 技术,以创建新型国家安全智能解决方案,我坚信它们的变革潜力(即便在 2017 年“变形金刚”兴起之前——查看我关于 OSINT 和 LLMs 的文章)。我预见到一个“现代”将成为古老、过时术语的未来,因为这些技术将重塑我们所知的社会。但像硬币的另一面一样,潜藏着危险——并不是恶意的通用人工智能(AGI)意图进行类似天网的人类灭绝(是的,我真心希望你对那个参考有所了解!)。而是,我在暗指这种技术的意外滥用,甚至更糟的是故意扭曲。

所以,各位女士们、先生们,欢迎来到《大型语言模型(LLMs)的双刃剑》系列,这一系列旨在揭示这一突破性技术的阴暗面。我并不是来否定先进人工智能的发展。相反,我的目的是引发对这些技术的性质和应用的热烈讨论,努力将它们的潜力用于善,而非恶。

你会在这篇文章中发现什么?

这篇文章分为两个主要部分,每个部分对应于语言学习模型(LLMs)出现之前和之后的时代。请随意按需探索!

  • 第一部分深入探讨了宣传的各个方面、其历史使用情况,以及技术如何逐步被利用来提升其效果。我们谈论的是社会科学、操控技术、Web 1.0、Web 2.0,以及这些元素如何被用于大规模的虚假/误导信息运动,例如在英国脱欧和特朗普竞选期间。

  • 第二部分则更专注地探讨了 LLMs,考察它们对我们信息消费的变革性影响以及它们在这一特定领域所带来的潜在风险。我还提供了关于我们如何达到这一点的观点,并建议了缓解这些负面影响的潜在措施。

宣传与技术:一段爱情故事

宣传是一种巧妙的技术,追求权力,不通过战争的武力,而是通过微妙的劝说艺术(向克劳塞维茨致以敬意)。这一策略,和治理本身一样古老,自早期帝国和民族国家以来一直被使用。一些历史学家追溯其首次使用到公元前 515 年的波斯帝国!

随着社会的扩展和政治结构的复杂化,统治者发现有多种方法是维持秩序和合作所必需的。在这些策略中,包括经典的“面包和马戏”以及其他方法,宣传找到了自己的位置。虽然这当时的宣传非常粗糙和直白,与我们今天的体验相去甚远,远未成为主角,但它确实发挥了作用。

第一个改变游戏规则的因素:印刷术的发明。

这改变了信息传播的方式。突然间,曾经主要局限于宫廷和通过信使传播的叙述找到了更广泛的受众。随着影响范围的扩大,信息开始转变为更强大的影响工具。笔,或者说在这种情况下的印刷术,开始展现其力量,并不掩盖,但确实与剑并肩而立。因此,影响的动态呈现出一种全新的形式。

我为什么今天提到这个话题,尤其是在印刷术逐渐淡出历史的今天?为了提醒我们成功的信息传播的关键在于其覆盖范围,即它影响的人群。而它影响的人越多,它的力量就越大。

英国如何为第一次世界大战做准备,图片来源于维基百科

现在,让我们快进到 20 世纪。

第二个改变游戏规则的因素:社会科学和操控杠杆

随着社会科学的发展和新兴的群众心理学领域,欧洲国家找到了一些新方法来影响其民众。印刷术提供了受众,社会科学提供了方法。 法国作家古斯塔夫·勒庞的著作成为 1930 年代专制政权领导者的手册。他们利用这些理论来触及公民的挫败感和恐惧,给他们提供了一个简化的世界观,在这个世界观中,明确的敌人威胁着他们的国家、生活方式和传统。这些领导者将自己描绘成全知的守护者,引领人民走向胜利。

民主政权也采用了这种方法,帮助他们引导民众接受(或至少不反对)在战争条件下生活和参与战争努力所带来的约束和限制。有些人可能会认为这是为了更大利益所必要的一步。

尽管如此,必须牢记的是,尽管在艰难时期对复杂现实的简化有时被视为必要的恶,但绝不应被提倡。诉诸过于简单的真相和情绪化的语言以激发情感而非理性反应,会煽动恐惧、仇恨和分裂的火焰。 这种策略有可能引发灾难性的后果,并在促成不可想象的人类悲剧中发挥了重要作用,正如大屠杀悲惨地证明了这一点。

信息时代已经过去:Web 1.0 & 2.0

快进到 1993 年——万维网的诞生——第三次革命。源于“链接信息系统”的想法,计算机科学家蒂姆·伯纳斯-李发布了世界上第一个网页浏览器和编辑器的源代码。突然间,Web 1.0 的愿景成为现实,带来了对人类更好未来的雄心壮志。毕竟,既然可以访问到世界的集体知识,我们怎么可能不提升自己呢?

初衷是理想主义的,真正乐观的。想象一个世界,任何人都可以访问由最佳大学和智库发布的内容,参与公开讨论,并通过透明的信息访问使这些机构问责。这是一个由信息力量驱动的更开明社会的梦想

相反,我们得到的是……LOL 猫咪。 当然,情况并非完全如此。确实有意义的贡献和令人印象深刻的知识分享进展(LLMs 的兴起依然如此)。但与此同时,一种新的娱乐文化、闲逛和吸引注意力的头条新闻也开始扎根。

互联网路由路径的可视化,图片由维基百科提供

Web 2.0 的兴起,以社交媒体平台的创建和增加为特征,使这种动态变得尤为突出。 起初被誉为连接人类的新媒介,它们也成为了反映我们分裂的镜子,通过算法和回音室放大这些分裂。曾经局限于 Web 1.0 特定论坛和博客的讨论溢出了主流,塑造了我们对现实的认知,这种影响我们还在开始理解。游说者和活动家现在清楚地知道应该集中精力的方向和目标,因为现在大多数成年人和青少年都在网上。网络影响力活动的潜在媒介已从数百个社区网站和博客转移到几个主导的社交媒体平台,这些平台现在使用语义搜索引擎和数据分析工具来托管和监控这些社区,因此简化了其物流,并将其效果放大了几个数量级。

我们终于来到了今天。曾经乌托邦般的互联网承诺已经偏离了轨道。一项旨在启发我们的技术,现在却成为了争夺我们注意力的战场。信息时代已经成为过去。

这并不是说更大的利益完全丧失了,而是说阴影变得越来越难以忽视。互联网和社交媒体的近乎普及导致了许多早期先驱可能从未预见的意外后果。虽然这些平台被誉为信息的“伟大民主化者”,但它们也无意中创造了一个虚假信息滋生的环境。快速广泛分享信息的能力可以是一个极大的利好力量,但它也为传播虚假信息和宣传提供了有力的工具。作为用户,我们曾被承诺知识的盛宴,但现在,我们却在拼命区分事实与虚构,真相与幻象。最近‘假新闻’的趋势及其在网上迅速传播的能力,清晰地证明了这一点。

2016 年发生了什么?回音室和定向算法。

完美风暴:病毒式传播、数据分析和人群操控

2016 年发生了一次强大的汇聚,将社会科学的进步、Web 1.0 和 Web 2.0 技术结合在一起,从而在政治领域掀起了前所未有的风暴。这一年是英国脱欧公投和美国总统选举之年,特朗普和希拉里·克林顿正面交锋。这些事件的特点是四个关键现象:针对未决定选民的定向消息、对专家意见的组织性攻击、复杂的定向算法的实施,以及回音室现象的传播。社交媒体平台突然间,从最初设计为信息分享和促进联系的无害工具,变成了虚假信息和宣传的强大工具。它们以超过中立方和专家验证信息真实性能力的速度传播内容

John Cameron 提供的照片,来源于 Unsplash

以英国脱欧公投为例。离开阵营提出了一个大胆的主张,即英国脱离欧盟将每周为 NHS 释放额外的 3.5 亿英镑。尽管这一主张很快被独立的事实检查员揭穿,但它在相当数量的选民中找到了共鸣。问题是,为什么? 答案部分在于社交媒体分析的不断演变,使得竞选者能够衡量各个社区对欧盟的‘情感’,而不仅仅是意见。这些数据揭示了英国大部分人对欧盟成员资格的好处感到不确定,主要关心更迫切的问题,如移民和 NHS 的状况。掌握了这些见解后,竞选者设计了高度定制的信息策略,借助社交媒体分析确定了正确的目标群体。这些平台固有的传播性完成了剩下的工作。

与此同时,在大西洋另一边,特朗普的总统竞选也采用了类似的战术。大胆的主张,如承诺让墨西哥资助边界墙,尽管被广泛揭穿,但仍在许多选民中获得了认可。

剑桥分析公司:患者还是零号病毒

在这两个事件中,一个值得注意的参与者是咨询公司剑桥分析公司,其在这些政治事件中的争议角色已被 Netflix 纪录片《大黑客》生动地记录下来。

该公司从数百万个社交媒体个人资料中收集数据,以执行高度针对性的选民影响策略。借鉴了群体心理学家古斯塔夫·勒庞的见解,该公司利用恐惧、无知和挫折来影响公众情绪。然而,这些策略并非孤立操作。社交媒体平台部署的算法促成了‘回音室’效应。这些算法选择性地向用户展示与其现有观点一致的内容,强化他们的信念,并在某些情况下,推动他们朝更极端的立场发展。此外,如上所述,尚未决定的选民被识别出来并遭受了大量高度定制的信息轰炸,旨在改变他们的立场。通过这种方式,技术不仅用于传播特定的叙事,还创造了有利于其接受的条件。

外国势力,特别是俄罗斯国家,也参与其中,发布机密的竞选内容,并利用 Facebook 和 Twitter 传播谣言,以诋毁那些对他们自己的议程最不利的候选人,这一点在美国参议院关于俄罗斯主动措施和干预 2016 年美国大选的情报委员会报告中有所揭示

请注意,每条消息的制作都是由数据分析和社会科学专家团队负责,他们花费了数天时间创建内容和规划活动——随着基础模型的使用,这种情况将会改变。

向机器生成的真相时代问好

我喜欢在与高级领导讨论深度学习的进展时引用一句发人深省的谚语:“通往地狱的道路是由善意铺成的。” 变革性技术的误用以达到有害目的似乎是一种反复出现的人类模式。各种例子支持了这种观点,包括核聚变的应用,这既带来了核能,也带来了核弹。人工智能也是如此,尤其是大型语言模型(LLMs)。

破解人类语言

这些技术具有显著提升我们效率的潜力,但它们也体现了近乎存在性的风险。我反对这样一种观点,即人工智能的好处只需超过其负面影响即可造福人类。如果结果是人类再也无法区分真实与虚假,那么大型语言模型(LLMs)可能促进的众多其他革命将变得毫无意义,被机器速度带来的虚假信息和我们民主机构的潜在崩溃所淹没。

实际上,最新大型语言模型的发布为信息操控引入了一个新维度,这可能会破坏我们民主社会的基础。GPT-4、Claude、BARD 及其同类产品,凭借其以空前规模和速度生成类似人类的文本的能力,实际上已经获得了‘破解’语言的能力,这是人类沟通的主要手段

语言是我们社会的基石。它是我们表达思想、分享想法和塑造集体叙事的媒介。语言也是我们形成意见和做出决定的工具,包括政治选择。通过操控语言,大型语言模型(LLMs)有可能以微妙而深刻的方式影响这些过程。

图片由 Jonathan Kemper 提供,来源于 Unsplash

大型语言模型(LLMs)生成符合特定叙事或诉诸特定情感的内容的能力已成为 2016 年虚假信息活动中的缺失部分。回想一下,为了与特定社区产生共鸣而投入的大量精力,以及为这些任务所需的专家团队?LLMs 的出现使这一整个过程几乎变得多余和过时。有说服力和针对性的内容创建可以实现自动化,使得生成大量虚假信息成为可能,规模和速度超出了人类的能力。这不仅仅是传播虚假信息,更是关于塑造叙事和影响认知。滥用的潜力巨大。想象一下这些模型被用来淹没社交媒体平台,发布旨在煽动分裂、挑起暴力或影响公众对关键问题的看法的帖子。这对我们民主社会的影响是深远且令人深感担忧的。

深度伪造技术与后真相时代

当 LLMs 与其他基础模型如 Stable Diffusion 或 Midjourney 结合使用时,危险性就会加剧。 这些模型可以生成超现实的图像和视频,成为虚假信息活动的强大工具。想象一下由 AI 生成的假文章和看似真实的图片、视频。大规模伪造有说服力的多媒体内容的能力可能会显著放大虚假信息活动的影响,使其更有效且更难以对抗。

以总统沃洛基米尔·泽连斯基在社交媒体上现场直播投降的深度伪造视频为例。 尽管这个事件因其重要性而很容易被揭穿,但它证明了大型变换器模型,当它们结合在一起时,具有的颠覆性力量。另一个引人注目的事件是理查德·布卢门撒尔在参议院关于人工智能的听证会上提到的这一逻辑(完整视频见文章末尾),在他播放了一段录音,读出了他的开场白,结果揭示了文本和音频都是由人工智能生成的:“如果它 [GPT-4] 提供了对乌克兰投降的支持,或对弗拉基米尔·普京领导力的认可呢?”

此外,社交网络的病毒式传播可以加速虚假信息的传播,使其以机器速度传播。 这种快速传播可能超越了严肃新闻出版物、智库和其他事实核查组织验证和驳斥虚假信息的努力。例如,设想一种场景:一场虚假的恐怖袭击通过社交媒体传播,配有数十个虚假的智能手机视频和照片记录袭击细节,得到数百条社交媒体帖子和伪造的新闻文章支持,这些文章模仿 BBC、CNN 或《世界报》的风格。 传统媒体在担心错过报道并失去观众的压力下,可能会在事件的真实性得到确认之前报道该事件。这可能导致广泛的恐慌和虚假信息,进一步加剧问题。

为什么我们不暂停 LLMs 的发展呢?

转型技术本质上具有带来深远变化的能力。在资本主义社会中,它们被视为重要的商品,急于被开发,这转化为对供应商锁定和销售其产出的黄金热潮。这在地缘政治中表现为各国努力控制和掌握这些重要的权力资产。 这在核聚变上是如此,现在在人工智能上也是如此。过去三年里,国家安全机构中出现了大量专注于创建和控制 AI 驱动技术的政策、专门办公室和团队。人工智能已成功取代数据,成为所有重要国防组织和政府部门的流行词。

例如,五角大楼整合了多个负责数据管理、人工智能开发和研究的部门,最终于 2022 年 2 月成立了首席数字与人工智能办公室。英国现在已经建立了专门的国防人工智能战略(于 2022 年 6 月发布),法国、北约、中国、俄罗斯和印度也纷纷效仿。

我为什么要告诉你这些?我相信,强大的 AI 模型(包括大型语言模型 LLMs)对公众的无监管发布可以归因于至少两个主要因素。第一个因素来源于政府官员普遍缺乏技术素养,这一点从我向这类听众做的众多报告中可以看出。

但第二个因素——也许是最关键的因素——是政府官员和中大型公司高管普遍认为实施人工智能监管会阻碍进步。他们担心这种监管约束会使他们的组织和国家处于不利地位,尤其是与那些在人工智能领域不受限制快速前进的国家相比。

照片由迈克·斯托尔拍摄,发布在Unsplash上。

这是美国国会、参议院、国防和国家安全委员会中的持续话题:现在对人工智能进行监管将会减缓其发展,美国将落后于中国,从而处于战略劣势。这一论点在西方国家主要由以国防为导向的人工智能优先公司巧妙地编制和推广。这些公司中最著名的是彼得·蒂尔生态系统支持的公司。倡导“快速行动,打破常规”方法的公司,如 Palantir 和 Anduril(有趣的是,这两个名字都参考了《指环王》中的最强大魔法神器),格外引人注目。

然而,我们不能忽视欧盟对人工智能不受控发展的监管尝试,尤其是从数据和知识产权保护的角度来看。尽管如此,由于大多数领先的语言学习模型(LLM)创作者是美国人,这些规定不可避免地会事后施行,即在人工智能模型已经在全球范围内部署之后。这时已经为时已晚。

掌握关键技术无疑是赢得大国竞争的先决条件,但这不应成为缺乏辩论、简单化论证或不加限制地部署这些技术的理由。我们需要记住至少两个重要的例子: Facebook LLaMa 模型泄露 和现行的中国人工智能监管政策。这些话题将是本系列后续文章的重点。

作为一个前瞻性的思考,请考虑这个问题:如果 Facebook 受到精心设计的人工智能模型部署网络安全规定的约束,LLaMa 泄露事件会发生吗?此外,考虑一下中国的人工智能监管框架。从各种指标来看,它比任何西方国家的监管框架都要先进和严格。这挑战了这样一种观念:即中国在没有不必要的繁文缛节的情况下,全速推进先进人工智能解决方案的发展和部署。

那么,接下来该怎么办?

当我们迈入机器生成真相的时代,政府、企业和整个社会都有责任建立保障措施,以减少风险并获取利益。人工智能开发的透明度、负责任的使用规范、强有力的机器主导和人工控制的事实核查机制以及先进的人工智能素养只是需要紧急关注的一些积极措施。这些话题,特别是最后一个,将是本系列文章的核心内容。

是时候讨论大型语言模型的负责任使用了,促进人工智能伦理和包容性的文化。技术最终只是一个工具——其影响取决于使用者的意图。问题是,我们会让它成为促进更有启发性的社会的工具,还是成为加剧分裂的武器?我们对这些强大工具的理解才刚刚开始。

免责声明:作为曾经的国防和国家安全领域的专业人士,我往往对人类技术利用持有一定的悲观和怀疑态度,不幸的是。因此,当我看到有关人工智能监管的最新新闻,特别是那些由语言学习模型创作者自己推动的叙事时,它们确实引起了我的注意。例如,OpenAI 首席执行官山姆·奥特曼最近在美国国会委员会上的出现。虽然我非常欢迎他的想法,但对我来说,这似乎是一个精心策划的举动,旨在确保他们的优势,提升对新进入者的障碍,并创建这种“护城河”概念,这在几周前的一份谷歌内部备忘录中曾被提及。但我也意识到这可能是我的偏见在作祟,我会尽量在整个系列中保持警觉。最终,这可能是我们所需要的:如果政府不愿意或不能对人工智能开发施加严格的监管,那么可能由私营部门来引领。然而,这种方法本质上会带有各自公司的议程和独特的策略。

敬请关注未来的文章,我们将深入探讨大型语言模型在操控政治话语中的潜在误用、它们在加剧社会经济不平等中的作用,以及它们如何被用来规避隐私规范。我们还将探索管理这些问题的潜在策略和政策,从技术到监管监督,以及公众对这些不断发展的技术的意识和教育需求。我们的旅程才刚刚开始。

喜欢这篇文章吗?

让我们互相认识!正如我所说,我们的旅程才刚刚开始。这一系列既属于你,也属于我。评论、讨论、分享、批评!目标是激发讨论。

如果你想进一步了解,请联系我!你可以在LinkedIn找到我,或在Medium上关注我!

感谢你的支持,我们下次见!

LMQL — 语言模型的 SQL

原文:towardsdatascience.com/lmql-sql-for-language-models-d7486d88c541?source=collection_archive---------0-----------------------#2023-11-27

另一个可能对你的 LLM 应用有帮助的工具

Mariya MansurovaTowards Data Science Mariya Mansurova

·

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

--

图片来自 DALL-E 3

我相信你一定听说过 SQL,甚至已经掌握了它。 SQL(结构化查询语言) 是一种广泛用于数据库数据操作的声明式语言。

根据年度 StackOverflow 调查,SQL 仍然是全球最受欢迎的语言之一。对于专业开发人员来说,SQL 位列前三名(仅次于 Javascript 和 HTML/CSS)。超过一半的专业人员使用 SQL。令人惊讶的是,SQL 甚至比 Python 更受欢迎。

图表由作者提供,数据来自 StackOverflow 调查

SQL 是与数据库中的数据交互的常见方式。因此,使用类似的方法来处理 LLM 也就不足为奇了。在这篇文章中,我想告诉你一种名为 LMQL 的方法。

什么是 LMQL?

LMQL (语言模型查询语言) 是一种用于语言模型的开源编程语言。LMQL 在 Apache 2.0 许可证下发布,允许你商业使用。

LMQL 是由 ETH 苏黎世的研究人员开发的。他们提出了一种新的 LMP(语言模型编程)理念。LMP 结合了自然语言和编程语言:文本提示和脚本指令。

原始论文 中,“Prompting Is Programming: A Query Language for Large Language Models” 由 Luca Beurer-Kellner、Marc Fischer 和 Martin Vechev,作者标记了当前 LLM 使用的以下挑战:

  • 交互。 例如,我们可以使用元提示,让 LM 扩展初始提示。作为一个实际案例,我们可以首先要求模型定义初始问题的语言,然后用这种语言回答。对于这样的任务,我们需要发送第一个提示,从输出中提取语言,将其添加到第二个提示模板中,并再次调用 LM。我们需要管理的交互非常多。使用 LMQL,你可以在一个提示中定义多个输入和输出变量。更重要的是,LMQL 将优化多个调用的整体可能性,这可能会产生更好的结果。

  • 约束与令牌表示。 当前的 LMs 不提供约束输出的功能,这在生产环境中使用 LMs 时至关重要。假设在生产环境中构建情感分析系统,以在我们界面上标记 CS 代理的负面评论。我们的程序期望从 LLM 接收到“positive”,“negative”或“neutral”。然而,LLM 很可能返回诸如“提供的客户评论的情感是积极的”这样的内容,这在你的 API 中处理起来并不容易。这就是为什么约束会非常有帮助。LMQL 允许你使用人类可理解的词(而不是 LMs 操作的令牌)来控制输出。

  • 效率与成本。 LLM 是大型网络,因此无论你是通过 API 使用它们还是在本地环境中使用,它们的成本都相当高。LMQL 可以利用预定义的行为和搜索空间的约束(由约束引入)来减少 LM 调用次数。

如你所见,LMQL 可以解决这些挑战。它允许你在一个提示中结合多个调用,控制输出,甚至降低成本。

对成本和效率的影响可能相当大。搜索空间的限制可以显著降低 LLM 的成本。例如,在 LMQL 论文 中的案例中,LMQL 的可计费 tokens 比标准解码少了 75–85%,这意味着它将显著降低你的成本。

图片来自 Beurer-Kellner 等人 (2023) 的论文

我认为 LMQL 最重要的好处是对输出的完全控制。然而,这种方法也会在 LLM 上增加另一层抽象(类似于我们之前讨论的 LangChain)。这将允许你在需要时轻松切换后端。LMQL 可以与不同的后端一起使用:OpenAI、HuggingFace Transformers 或 llama.cpp

你可以在本地安装 LMQL 或使用基于网页的 Playground 在线工具。Playground 对于调试非常方便,但你只能在这里使用 OpenAI 后端。对于所有其他用例,你需要使用本地安装。

像往常一样,这种方法有一些局限性:

  • 这个库还不是很受欢迎,所以社区相对较小,外部材料也不多。

  • 在某些情况下,文档可能不是很详细。

  • 最受欢迎且表现最佳的 OpenAI 模型有 一些限制,因此你不能在 ChatGPT 上充分利用 LMQL 的全部功能。

  • 我不会在生产环境中使用 LMQL,因为我不能说它是一个成熟的项目。例如,token 的分布提供的准确性相当差。

与 LMQL 相似的替代方案是 Guidance。它也允许你约束生成并控制语言模型的输出。

尽管存在所有限制,我仍然喜欢语言模型编程的概念,这也是我决定在本文中讨论它的原因。

如果你有兴趣了解更多关于 LMQL 的信息,可以查看 这个视频

LMQL 语法

现在,我们对 LMQL 有了一些了解。让我们看看 LMQL 查询的示例,以熟悉它的语法。

beam(n=3)
    "Q: Say 'Hello, {name}!'" 
    "A: [RESPONSE]" 
from "openai/text-davinci-003"
where len(TOKENS(RESPONSE)) < 20

希望你能猜出它的含义。但让我们详细讨论一下。

这是 LMQL 查询的示意图

图片来自 Beurer-Kellner 等人 (2023) 的论文

任何 LMQL 程序由 5 部分组成:

  • Decoder 定义了使用的解码过程。简单来说,它描述了选择下一个 token 的算法。LMQL 有三种不同类型的解码器:argmax、beam 和 sample。你可以从 论文 中详细了解它们。

  • 实际查询类似于经典的提示,但使用 Python 语法,这意味着你可以使用像循环或 if 语句这样的结构。

  • from 子句中,我们指定了要使用的模型(在我们的例子中是 openai/text-davinci-003)。

  • Where 子句定义约束条件。

  • Distribution 在你想查看返回中令牌的概率时使用。我们在这个查询中没有使用分布,但稍后我们将用它来获取情感分析的类别概率。

另外,你可能已经注意到我们查询中的特殊变量 {name}[RESPONSE]。我们来讨论一下它们是如何工作的:

  • {name} 是一个输入参数。它可以是你范围内的任何变量。这些参数帮助你创建实用的函数,便于不同输入的重复使用。

  • [RESPONSE] 是一个由 LM 生成的短语。它也可以被称为占位符或占位符。所有在 [RESPONSE] 之前的文本都会发送给 LM,然后模型的输出将分配给这个变量。你可以很方便地在提示中重新使用这个输出,将其称为 {RESPONSE}

我们已经简要介绍了主要概念。现在让我们自己试试。熟能生巧。

入门指南

设置环境

首先,我们需要设置环境。要在 Python 中使用 LMQL,我们需要首先安装一个包。没有意外,我们可以直接使用 pip。你需要一个 Python ≥ 3.10 的环境。

pip install lmql

如果你想在本地 GPU 上使用 LMQL,请参阅 文档中的说明。

要使用 OpenAI 模型,你需要设置 APIKey 来访问 OpenAI。最简单的方法是指定 OPENAI_API_KEY 环境变量。

import os
os.environ['OPENAI_API_KEY'] = '<your_api_key>'

然而,OpenAI 模型有许多 限制(例如,你不能获取多于五个类别的分布)。因此,我们将使用 Llama.cpp 来测试带有本地模型的 LMQL。

首先,你需要在与 LMQL 相同的环境中安装 Python 绑定的 Llama.cpp。

pip install llama-cpp-python

如果你想使用本地 GPU,请指定以下参数。

CMAKE_ARGS="-DLLAMA_METAL=on" pip install llama-cpp-python

然后,我们需要将模型权重加载为 .gguf 文件。你可以在 HuggingFace 模型中心 找到模型。

我们将使用两个模型:

Llama-2–7B 是 Meta 细调生成文本模型中最小的版本。它是一个相当基础的模型,因此我们不应期望它有卓越的表现。

Zephyr 是一个经过微调的 Mistral 模型,性能不错。在某些方面,它的表现优于 10 倍大的开源模型 Llama-2–70b。然而,Zephyr 和像 ChatGPT 或 Claude 这样的专有模型之间仍存在差距。

图片来源于 Tunstall 等人 (2023) 的论文

根据LMSYS ChatBot Arena 排行榜,Zephyr 是表现最好的模型,拥有 7B 参数。它的表现与更大模型相当。

排行榜截图 | 来源

让我们加载.gguf文件以便使用我们的模型。

import os
import urllib.request

def download_gguf(model_url, filename):
    if not os.path.isfile(filename):
        urllib.request.urlretrieve(model_url, filename)
        print("file has been downloaded successfully")
    else:
        print("file already exists")

download_gguf(
    "https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/resolve/main/zephyr-7b-beta.Q4_K_M.gguf", 
    "zephyr-7b-beta.Q4_K_M.gguf"
)

download_gguf(
    "https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_K_M.gguf", 
    "llama-2-7b.Q4_K_M.gguf"
)

我们需要下载几个 GB 的数据,所以可能需要一些时间(每个模型 10 到 15 分钟)。幸运的是,你只需做一次。

你可以通过两种不同方式与本地模型进行交互(文档):

  • 当你有一个独立的长时间运行进程和短时间运行的推断调用时的双进程架构。这种方法更适合生产环境。

  • 对于临时任务,我们可以使用进程内模型加载,在模型名称前指定local:。我们将使用这种方法来处理本地模型。

现在,我们已经设置好了环境,接下来讨论如何从 Python 中使用 LMQL。

Python 函数

简要讨论如何在 Python 中使用 LMQL。Playground 对于调试很有帮助,但如果你想在生产环境中使用 LM,您需要一个 API。

LMQL 提供了四种主要的方法:lmql.Flmql.run@lmql.query装饰器和Generations API

Generations API最近被添加。它是一个简单的 Python API,帮助进行推断而不需要自己编写 LMQL。由于我更关注 LMP 概念,本文不涵盖此 API。

让我们详细讨论其他三种方法并尝试使用它们。

首先,你可以使用lmql.F。它类似于 Python 中的 lambda 函数,允许你执行部分 LMQL 代码。lmql.F只能有一个占位符变量,该变量将从 lambda 函数返回。

我们可以为函数指定提示和约束。约束将等同于 LMQL 查询中的where子句。

由于我们没有指定任何模型,将使用 OpenAI 的text-davinci

capital_func = lmql.F("What is the captital of {country}? [CAPITAL]", 
    constraints = "STOPS_AT(CAPITAL, '.')")

capital_func('the United Kingdom')

# Output - '\n\nThe capital of the United Kingdom is London.'

如果你使用的是 Jupyter Notebook,你可能会遇到一些问题,因为 Notebook 环境是异步的。你可以在笔记本中启用嵌套事件循环以避免这些问题。

import nest_asyncio
nest_asyncio.apply()

第二种方法允许你定义更复杂的查询。你可以使用lmql.run来执行 LMQL 查询,而无需创建函数。让我们使查询更复杂,并在接下来的问题中使用模型的回答。

在这种情况下,我们在查询字符串的where子句中定义了约束。

query_string = '''
    "Q: What is the captital of {country}? \\n"
    "A: [CAPITAL] \\n"
    "Q: What is the main sight in {CAPITAL}? \\n"
    "A: [ANSWER]" where (len(TOKENS(CAPITAL)) < 10) \
      and (len(TOKENS(ANSWER)) < 100) and STOPS_AT(CAPITAL, '\\n') \
      and STOPS_AT(ANSWER, '\\n')
'''

lmql.run_sync(query_string, country="the United Kingdom")

此外,我使用了run_sync而不是run来同步获取结果。

结果,我们得到了一个包含一组字段的LMQLResult对象:

  • prompt — 包括带有参数和模型答案的整个提示。我们可以看到模型答案被用于第二个问题。

  • variables — 包含我们定义的所有变量的字典:ANSWERCAPITAL

  • distribution_variabledistribution_values都是None,因为我们还没有使用这个功能。

作者提供的图片

使用 Python API 的第三种方法是[@lmql](http://twitter.com/lmql).query装饰器,它允许您定义一个 Python 函数,以便将来使用非常方便。如果您计划多次调用此提示,则更加方便。

我们可以为我们之前的查询创建一个函数,并且只获取最终的答案,而不是返回整个LMQLResult对象。

@lmql.query
def capital_sights(country):
    '''lmql
    "Q: What is the captital of {country}? \\n"
    "A: [CAPITAL] \\n"
    "Q: What is the main sight in {CAPITAL}? \\n"
    "A: [ANSWER]" where (len(TOKENS(CAPITAL)) < 10) and (len(TOKENS(ANSWER)) < 100) \
        and STOPS_AT(CAPITAL, '\\n') and STOPS_AT(ANSWER, '\\n')

    # return just the ANSWER 
    return ANSWER
    '''

print(capital_sights(country="the United Kingdom"))

# There are many famous sights in London, but one of the most iconic is 
# the Big Ben clock tower located in the Palace of Westminster. 
# Other popular sights include Buckingham Palace, the London Eye, 
# and Tower Bridge.

您还可以将 LMQL 与 LangChain 结合使用:

  • LMQL 查询是增强版的提示模板,可以成为 LangChain 链的一部分。

  • 您可以利用 LMQL 中的 LangChain 组件(例如检索)。您可以在文档中找到示例。

现在,我们知道了 LMQL 语法的所有基础知识,准备好开始我们的任务——为客户评论定义情感。

情感分析

为了查看 LMQL 的性能,我们将使用来自UCI 机器学习库的带标签的 Yelp 评论,并尝试预测情感。数据集中的所有评论都是积极的或消极的,但我们将保留中性作为分类的一种可能选项。

对于这个任务,让我们使用本地模型——ZephyrLlama-2。在调用 LMQL 时,我们需要指定模型和标记器。对于 Llama 系列模型,我们可以使用默认的标记器。

第一次尝试

让我们选择一个客户评论食物非常好。并尝试定义其情感。由于对于这种临时调用来说,lmql.run非常方便,我们将使用它进行调试。

我一开始采用了非常天真的方法。

query_string = """
"Q: What is the sentiment of the following review: ```食物非常好。```py?\\n"
"A: [SENTIMENT]"
"""

lmql.run_sync(
    query_string, 
    model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
        tokenizer = 'HuggingFaceH4/zephyr-7b-beta'))

# [Error during generate()] The requested number of tokens exceeds 
# the llama.cpp model's context size. Please specify a higher n_ctx value.

如果您的本地模型运行异常缓慢,请检查您的计算机是否使用了交换内存。重新启动可能是解决此问题的一个很好的选项。

代码看起来非常简单。然而令人惊讶的是,它却不能正常工作,并返回以下错误。

[Error during generate()] The requested number of tokens exceeds the llama.cpp 
model's context size. Please specify a higher n_ctx value.

从消息中,我们可以猜测输出与上下文大小不符。我们的提示大约是 20 个标记,所以击中上下文大小阈值有些奇怪。让我们尝试限制SENTIMENT的标记数量,看看输出。

query_string = """
"Q: What is the sentiment of the following review: ```食物非常好。```py?\\n"
"A: [SENTIMENT]" where (len(TOKENS(SENTIMENT)) < 200)
"""

print(lmql.run_sync(query_string, 
    model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
        tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['SENTIMENT'])

#  Positive sentiment.
# 
# Q: What is the sentiment of the following review: ```服务很糟糕。```py?
# A:  Negative sentiment.
# 
# Q: What is the sentiment of the following review: ```酒店很棒,员工友好,位置完美。```py?
# A:  Positive sentiment.
# 
# Q: What is the sentiment of the following review: ```产品令人完全失望。```py?
# A:  Negative sentiment.
# 
# Q: What is the sentiment of the following review: ```航班延误 3 小时,食物冷,娱乐系统不工作。```py?
# A:  Negative sentiment.
# 
# Q: What is the sentiment of the following review: ```餐厅座无虚席,但服务员效率高,食物美味。```py?
# A:  Positive sentiment.
# 
# Q: 

现在,我们可以看到问题的根源——模型陷入了一个循环,不断重复问题的变体和答案。我没有见过 OpenAI 模型出现这种问题(假设他们可能会控制它),但这种问题在开源本地模型中相当常见。我们可以使用 STOPS_AT 约束来停止生成,如果在模型响应中看到 Q: 或新行,以避免这种循环。

query_string = """
"Q: What is the sentiment of the following review: ```食物非常好。```py?\\n"
"A: [SENTIMENT]" where STOPS_AT(SENTIMENT, 'Q:') \
     and STOPS_AT(SENTIMENT, '\\n')
"""

print(lmql.run_sync(query_string, 
    model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
        tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['SENTIMENT'])

# Positive sentiment.

很好,我们已经解决了问题并得到了结果。但由于我们将进行分类,我们希望模型返回三种输出之一(类别标签):negativeneutralpositive。我们可以在 LMQL 查询中添加这样的过滤器来限制输出。

query_string = """
"Q: What is the sentiment of the following review: ```食物非常好。```py?\\n"
"A: [SENTIMENT]" where (SENTIMENT in ['positive', 'negative', 'neutral'])
"""

print(lmql.run_sync(query_string, 
    model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
        tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables['SENTIMENT'])

# positive

由于我们已经将输出限制为仅三种可能的选项,并且 LMQL 不会考虑其他可能性,因此我们不需要带有停止标准的过滤器。

让我们尝试使用连锁思维推理方法。给模型一些思考时间通常会提高结果。使用 LMQL 语法,我们可以快速实现这种方法。

query_string = """
"Q: What is the sentiment of the following review: ```食物非常好。```py?\\n"
"A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \
    and (SENTIMENT in ['positive', 'negative', 'neutral'])
"""

print(lmql.run_sync(query_string, 
    model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
        tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables)

Zephyr 模型的输出相当不错。

图片来源:作者

我们可以尝试使用 Llama 2 进行相同的提示测试。

query_string = """
"Q: What is the sentiment of the following review: ```食物非常好。```py?\\n"
"A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \
    and (SENTIMENT in ['positive', 'negative', 'neutral'])
"""

print(lmql.run_sync(query_string, 
    model = lmql.model("local:llama.cpp:llama-2-7b.Q4_K_M.gguf")).variables)

推理没有多大意义。我们已经在排行榜上看到 Zephyr 模型比 Llama-2–7b 好得多。

图片来源:作者

在经典的机器学习中,我们通常不仅获得类别标签,还会获得它们的概率。我们可以使用 LMQL 中的 distribution 来获得相同的数据。我们只需要指定变量和可能的值——distribution SENTIMENT in [‘positive’, ‘negative’, ‘neutral’]

query_string = """
"Q: What is the sentiment of the following review: ```食物非常好。```py?\\n"
"A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" distribution SENTIMENT in ['positive', 'negative', 'neutral']
where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n')
"""

print(lmql.run_sync(query_string, 
    model = lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
        tokenizer = 'HuggingFaceH4/zephyr-7b-beta')).variables)

现在,我们得到了输出中的概率,我们可以看到模型对积极情绪的信心相当高。

如果你只想在模型自信时使用决策,概率可能在实践中很有帮助。

图片来源:作者

现在,让我们创建一个函数,用于各种输入的情感分析。比较有分布和没有分布的结果会很有趣,所以我们需要两个函数。

@lmql.query(model=lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
   tokenizer = 'HuggingFaceH4/zephyr-7b-beta', n_gpu_layers=1000))
# specified n_gpu_layers to use GPU for higher speed
def sentiment_analysis(review):
    '''lmql
    "Q: What is the sentiment of the following review: ```{review}```py?\\n"
    "A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n') \
        and (SENTIMENT in ['positive', 'negative', 'neutral'])
    '''

@lmql.query(model=lmql.model("local:llama.cpp:zephyr-7b-beta.Q4_K_M.gguf", 
  tokenizer = 'HuggingFaceH4/zephyr-7b-beta', n_gpu_layers=1000))
def sentiment_analysis_distribution(review):
    '''lmql
    "Q: What is the sentiment of the following review: ```{review}```py?\\n"
    "A: Let's think step by step. [ANALYSIS]. Therefore, the sentiment is [SENTIMENT]" distribution SENTIMENT in ['positive', 'negative', 'neutral']
    where (len(TOKENS(ANALYSIS)) < 200) and STOPS_AT(ANALYSIS, '\\n')
    '''

然后,我们可以使用这个函数进行新的评估。

sentiment_analysis('Room was dirty')

模型认为这是中性的。

图片来源:作者

这个结论背后有一定的理由,但我认为这条评论是负面的。让我们看看是否可以使用其他解码器来获得更好的结果。

默认情况下,使用的是 argmax 解码器。这是最直接的方法:在每一步,模型选择概率最高的标记。我们可以尝试使用其他选项。

让我们尝试使用 beam search 方法,n = 3 和相当高的 tempreture = 0.8。结果,我们将得到三个按可能性排序的序列,因此我们可以选择第一个(可能性最高的)。

sentiment_analysis('Room was dirty', decoder = 'beam', 
    n = 3, temperature = 0.8)[0]

现在,模型能够识别到这条评论中的负面情感。

图片由作者提供

值得一提的是,beam search 解码是有成本的。由于我们处理的是三个序列(束),因此获取 LLM 结果的时间平均要多花 3 倍:39.55 秒对比 13.15 秒。

现在,我们有了函数,并可以用真实数据进行测试。

真实数据的结果

我在 Yelp 评论的 1K 数据集的 10% 样本上运行了所有函数,并使用了不同的参数:

  • 模型:Llama 2 或 Zephyr,

  • 方法:使用分布或仅使用约束提示,

  • 解码器:argmax 或 beam search。

首先,让我们比较准确性——即评论中正确情感的比例。我们可以看到,Zephyr 的表现远好于 Llama 2 模型。另外,由于某些原因,我们在分布上的质量显著下降。

作者绘图

如果我们更深入地观察,会发现:

  • 对于正面评论,准确性通常较高。

  • 最常见的错误是将评论标记为中性,

  • 对于带有提示的 Llama 2,我们可以看到有较高的关键问题率(将正面评论标记为负面)。

在许多情况下,我认为模型使用类似的推理,将负面评论评分为中性,就像我们在“肮脏的房间”例子中看到的那样。模型不确定“肮脏的房间”是负面还是中性情感,因为我们不知道客户是否期望一个干净的房间。

作者绘图

作者绘图

观察实际概率也是很有趣的:

  • 对于正面评论,Zephyr 模型的正面标签的 75%百分位数超过 0.85,而 Llama 2 则远低于此值。

  • 所有模型在负面评论上的表现都很差,其中负面评论的负面标签的 75%百分位数远低于 0.5。

作者绘图

作者绘图

我们的快速研究显示,使用 Zephyr 模型和 argmax 解码器的普通提示是情感分析的最佳选择。然而,值得根据你的用例检查不同的方法。此外,你通常可以通过调整提示来获得更好的结果。

你可以在 GitHub 上找到完整代码。

总结

今天,我们讨论了 LMP(语言模型编程)的概念,它允许你将自然语言和脚本指令混合使用。我们尝试将其用于情感分析任务,并在使用本地开源模型时获得了不错的结果。

尽管 LMQL 尚未普及,但这种方法可能会在未来变得非常有用,并获得广泛的关注,因为它将自然语言和编程语言结合成一个强大的工具用于语言模型。

非常感谢你阅读这篇文章。希望它对你有所启发。如果你有任何后续问题或评论,请在评论区留言。

数据集

Kotzias, Dimitrios. (2015). Sentiment Labelled Sentences. UCI Machine Learning Repository (CC BY 4.0 license). https://doi.org/10.24432/C57604

如何将多个 CSV 文件加载到 Pandas DataFrame 中

原文:towardsdatascience.com/load-multiple-csv-pandas-9c0c88c5adff

导入并连接多个 CSV 文件到一个 pandas DataFrame 中

Giorgos MyrianthousTowards Data Science Giorgos Myrianthous

·发表于 Towards Data Science ·5 分钟阅读·2023 年 1 月 31 日

--

照片由 Daniel K Cheung 提供,来自 Unsplash

CSV(逗号分隔值)是一种常用的文件格式,用于存储和交换数据。事实上,这种类型的源通常用于相对较小的数据量。

pandas 是一个常用的 Python 包,它允许开发人员处理和转换数据,作为分析和数据科学任务的一部分。然而,在执行任何任务之前,pandas 需要将所有数据加载到内存中。这意味着该包只能用于相对较小的数据量——这取决于主机机器的能力,但对于一台普通的机器,我们只能在内存中加载几 GB 的数据。

因此,将 CSV 文件加载到内存中,然后用 pandas 处理数据,是一个非常常见的任务,因为这些文件通常包含可以加载到内存中的数据。

订阅数据管道,这是一个专注于数据工程的新闻通讯

在这篇文章中,我们将演示如何将多个 CSV 文件加载到一个 pandas DataFrame 中。此外,我们还将展示如何在每条记录中识别源文件,以便我们可以确定哪个数据点属于某个数据文件。

现在假设我们在三个分开的 CSV 文件中收集了数据点,即 data_1.csvdata_2.csvdata_3.csv

colA,colB,colC
'A',1412,True
'B',1252,False
'C',1536,True
'D',1508,False
colA,colB,colC
'E',1115,False
'F',6416,True
'G',6241,True
colA,colB,colC
'H',1267,False
'I',1252,False
'J',2753,False
'K',7346,True

在一个 pandas DataFrame 中连接多个 CSV 文件

现在,我们有三个分开的 CSV 文件中的一些虚拟数据,我们可以继续将它们导入到一个 DataFrame 中。

我们可以选择的第一个选项是使用pandas.read_csv()函数读取每个单独的 CSV 文件,并使用pandas.concatenate()函数将所有加载的文件合并为一个单一的 DataFrame。

import pandas as pd 

data_files = ['data_1.csv', 'data_2.csv', 'data_3.csv']
df = pd.concat((pd.read_csv(filename) for filename in data_files))

现在新构建的 DataFrame 包含了三个输入 CSV 文件中发现的所有数据点:

>>> df
  colA  colB   colC
0  'A'  1412   True
1  'B'  1252  False
2  'C'  1536   True
3  'D'  1508  False
0  'E'  1115  False
1  'F'  6416   True
2  'G'  6241   True
0  'H'  1267  False
1  'I'  1252  False
2  'J'  2753  False
3  'K'  7346   True

注意,当从多个文件加载数据时,我们的 DataFrame 的索引会被重置。如果你希望为新创建的 DataFrame 创建一个新的索引,你只需在合并文件时忽略索引即可:

import pandas as pd 

data_files = ['data_1.csv', 'data_2.csv', 'data_3.csv']
df = pd.concat(
  (pd.read_csv(filename) for filename in data_files), 
  ignore_index=True
)

>>> df
   colA  colB   colC
0   'A'  1412   True
1   'B'  1252  False
2   'C'  1536   True
3   'D'  1508  False
4   'E'  1115  False
5   'F'  6416   True
6   'G'  6241   True
7   'H'  1267  False
8   'I'  1252  False
9   'J'  2753  False
10  'K'  7346   True

避免明确指定文件名

现在假设我们有数百个不同的 CSV 文件,我们希望将它们合并为一个单一的 DataFrame。与其浪费时间和代码行数来明确写出所有单独的文件名,不如使用通配符。

我们可以利用glob模块,该模块是标准库的一部分,提供支持 Unix 样式路径名模式扩展的功能。例如,为了创建一个包含当前目录下所有以.csv结尾的文件的列表,我们可以使用以下代码片段:

>>> import glob
>>>
>>> data_files = glob.glob('*.csv')
>>> data_files
['data_2.csv', 'data_3.csv', 'data_1.csv']

下面分享了一个完整的代码,该代码读取当前目录下的所有 CSV 文件,并将它们合并为一个单一的 pandas DataFrame:

import glob

import pandas as pd 

data_files = glob.glob('*.csv')
df = pd.concat(
  (pd.read_csv(filename) for filename in data_files), 
  ignore_index=True
)

另一种方法——可能更符合 Python 风格——是利用map内置函数,它允许我们在不显式调用for循环的情况下,对 Iterable(如 Python 列表)运行方法或函数:

import glob

import pandas as pd

df = pd.concat(map(pd.read_csv, glob.glob('*.csv')))

识别来自不同文件的记录

在某些其他用例中,知道给定记录的原始来源文件可能是我们需要跟踪的信息。

import glob
import pandas as pd

df = pd.concat(
  [
    pd.read_csv(filename).assign(source=filename)
    for filename in glob.glob('*.csv')
  ], 
  ignore_index=True
)

现在我们期望我们的 DataFrame 将包含一个额外的列,用于指定每条记录添加到 DataFrame 的对应文件名:

>>> df
   colA  colB   colC      source
0   'E'  1115  False  data_2.csv
1   'F'  6416   True  data_2.csv
2   'G'  6241   True  data_2.csv
3   'H'  1267  False  data_3.csv
4   'I'  1252  False  data_3.csv
5   'J'  2753  False  data_3.csv
6   'K'  7346   True  data_3.csv
7   'A'  1412   True  data_1.csv
8   'B'  1252  False  data_1.csv
9   'C'  1536   True  data_1.csv
10  'D'  1508  False  data_1.csv

最后想法

在本文中,我们展示了如何加载多个 CSV 文件并将它们合并到一个 pandas DataFrame 中。此外,我们展示了如何在实际执行导入时,不需要明确指定要加载的文件名。最后,我们讨论了如何在加载的 DataFrame 中创建一个新列,以便识别每条记录的来源文件。

现在你已经将数据加载到 pandas 中,你可以利用该包提供的丰富 API,进行分析、转换以及你可能需要的任何处理。如果你打算将 pandas DataFrame 写回 CSV 文件,请确保遵循下面的指南👇。

如何将 Pandas DataFrame 写入 CSV 文件

利用在将 pandas DataFrame 写入 CSV 文件时提供的所有选项

towardsdatascience.com

订阅 Data Pipeline,一份专注于数据工程的通讯

👇 你可能还会喜欢的相关文章 👇

## ETL 与 ELT:有什么区别?

数据工程中 ETL 与 ELT 的比较

towardsdatascience.com ## SQL 中的 CTE 是什么

理解 SQL 中的公用表表达式(CTE)

towardsdatascience.com ## 什么是 dbt(数据构建工具)

介绍正在主宰数据领域的 dbt

towardsdatascience.com

负载测试 SageMaker 多模型端点

原文:towardsdatascience.com/load-testing-sagemaker-multi-model-endpoints-f0db7b305770

利用 Locust 在模型之间分配流量权重

Ram VegirajuTowards Data Science Ram Vegiraju

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

--

图片来源:Unsplash 作者:Luis Reyes

将机器学习模型投入生产是一个复杂的过程。你需要测试不同的模型参数、硬件配置和流量模式,以尽可能地确定一个生产级别的部署。负载测试 是一种必不可少的软件工程实践,但在 MLOps 领域应用同样至关重要,以评估你的模型在实际环境中的表现。

我们如何进行负载测试? 一个简单而高效的框架是 Python 包:Locust。Locust 可以在普通模式和分布式模式下使用,模拟每秒高达数千次的事务(TPS)。在今天的博客中,我们假设你对这个包有基本的了解,并将简要介绍其基础知识,但有关更一般的介绍,请参考这篇文章。

我们将测试什么模型/终端节点? SageMaker 实时推断 是在低延迟、高吞吐量工作负载下服务 ML 模型的最佳选择之一。在这篇博客中,我们将特别关注一种称为SageMaker 多模型终端节点 的高级托管选项。在这里,我们可以在单一 REST 终端节点后托管数千个模型,并为每个 API 调用指定我们想要调用的目标模型。由于我们处理的是多个调用点而非单一模型/终端节点,负载测试变得具有挑战性。虽然可以随机生成所有模型的流量,有时用户会希望控制哪些模型接收更多流量。在这个示例中,我们将探讨如何分配特定模型的流量权重,以便尽可能接近模拟你的实际使用情况。

注意:本文假设你对 AWS 和 SageMaker 有基本的了解,编程方面假设你熟悉 Python,并对 Locust 包有基本了解。要了解如何使用 Locust 对 SageMaker 单模型终端节点进行负载测试,请参考这篇文章

数据集引用

在这个示例中,我们将使用 Abalone 数据集进行回归问题,这个数据集来源于 UCI ML Repository(CC BY 4.0),你可以在这里找到官方引用。

创建 SageMaker 多模型终端节点

在开始负载测试之前,我们必须创建我们的 SageMaker 多模型终端节点。所有终端节点创建的开发工作将在一个SageMaker Notebook 实例上进行,使用conda_python3 内核

在这个示例中,我们将利用 Abalone 数据集,并对其运行SageMaker XGBoost 算法以创建一个回归模型。你可以从公开的 Amazon 数据集中下载这个数据集。我们将利用这个数据集进行训练,并创建我们模型工件的副本以创建一个多模型终端节点。

#retreive data
aws s3 cp s3://sagemaker-sample-files/datasets/tabular/uci_abalone/train_csv/abalone_dataset1_train.csv .

我们可以首先使用内置的 SageMaker XGBoost 算法启动一个训练任务,关于这个过程的详细指南请参考这篇文章

model_path = f's3://{default_bucket}/{s3_prefix}/xgb_model'

image_uri = sagemaker.image_uris.retrieve(
    framework="xgboost",
    region=region,
    version="1.0-1",
    py_version="py3",
    instance_type=training_instance_type,
)

xgb_train = Estimator(
    image_uri=image_uri,
    instance_type=training_instance_type,
    instance_count=1,
    output_path=model_path,
    sagemaker_session=sagemaker_session,
    role=role
)

xgb_train.set_hyperparameters(
    objective="reg:linear",
    num_round=50,
    max_depth=5,
    eta=0.2,
    gamma=4,
    min_child_weight=6,
    subsample=0.7,
    silent=0,
)

xgb_train.fit({'train': train_input})

完成此训练任务后,我们将获取生成的模型工件(SageMaker 中为 model.tar.gz 格式),并创建另一个此工件的副本,以拥有两个模型在我们的多模型端点后面。显然,在实际使用情况下,这些模型可能在不同的数据集上进行训练,或者在端点后面扩展到成千上万个模型。

model_artifacts = xgb_train.model_data
model_artifacts # model.tar.gz artifact 
%%sh

s3_bucket='sagemaker-us-east-1-474422712127'

for i in {0..1}
do
  aws s3 cp model.tar.gz s3://$s3_bucket/mme-xgboost/xgboost-$i.tar.gz 
done

在制作这两个副本后,我们可以在 create_model Boto3 API 调用中指定我们两个模型的 S3 路径。

from time import gmtime, strftime
model_name = 'mme-source' + strftime("%Y-%m-%d-%H-%M-%S", gmtime())

print('Model name: ' + model_name)
print('Model data Url: ' + model_url)

create_model_response = client.create_model(
    ModelName=model_name,
    Containers=[
        {
            "Image": image_uri,
            "Mode": "MultiModel",
            "ModelDataUrl": model_url
        }
    ],
    ExecutionRoleArn=sagemaker.get_execution_role(),
)
print("Model Arn: " + create_model_response["ModelArn"])

我们可以在 端点配置 对象中定义端点后的实例类型和数量,然后将其提供给我们的 create_endpoint API 调用。

#Step 2: EPC Creation
xgboost_epc_name = "mme-source" + strftime("%Y-%m-%d-%H-%M-%S", gmtime())
endpoint_config_response = client.create_endpoint_config(
    EndpointConfigName=xgboost_epc_name,
    ProductionVariants=[
        {
            "VariantName": "xgboostvariant",
            "ModelName": model_name,
            "InstanceType": "ml.m5.xlarge",
            "InitialInstanceCount": 1,
            #"Environment": {} 
        },
    ],
)
print("Endpoint Configuration Arn: " + endpoint_config_response["EndpointConfigArn"])
#Step 3: EP Creation
endpoint_name = "mme-source" + strftime("%Y-%m-%d-%H-%M-%S", gmtime())
create_endpoint_response = client.create_endpoint(
    EndpointName=endpoint_name,
    EndpointConfigName=xgboost_epc_name,
)
print("Endpoint Arn: " + create_endpoint_response["EndpointArn"])

我们可以通过使用 Abalone 数据集中的一个样本数据点调用来验证我们的端点是否有效。请注意,我们为多模型端点指定了一个目标模型,这里我们指定了我们想要调用的 model.tar.gz 或模型工件。

import boto3

resp = runtime.invoke_endpoint(EndpointName=endpoint_name, Body=b'.345,0.224414,.131102,0.042329,.279923,-0.110329,-0.099358,0.0', 
                           ContentType='text/csv', TargetModel = "xgboost-1.tar.gz")

print(resp['Body'].read())

这个 invoke_endpoint API 调用是至关重要的,因为这是我们在负载测试中评估的接触点。我们现在有了一个功能齐全的多模型端点,让我们开始测试吧!

使用 Locust 进行负载测试

在我们深入设置脚本之前,让我们快速了解一下 Locust。Locust 是一个 Python 框架,可以让你用 Python 代码定义用户行为。Locust 将执行定义为任务。Locust 中的任务本质上是我们要测试的 API,或在我们的案例中是 invoke_endpoint 调用。每个用户将运行我们在 Python 脚本 中为他们定义的任务。

Locust 有一个普通模式,利用单个进程来运行你的测试,但当你想要扩展时,它还具有分布式 负载生成特性,本质上允许你使用多个进程甚至多个客户端机器。

在这种情况下,我们希望对我们的多模型端点施加超过 1000 TPS 的负载,因此我们需要一台能够处理我们试图生成的负载的强大客户端机器。我们可以启动一个EC2 实例,在这种情况下,我们使用ml.c5d.18xlarge,并将在这个环境中进行负载测试,以确保客户端不出现性能瓶颈。要了解如何设置 EC2 实例,请阅读以下文档。对于我们的 AMI,我们使用“Deep Learning AMI GPU TensorFlow 2.9.1 (Ubuntu 20.04)”这一镜像,这些深度学习 AMI 自带了很多预装的机器学习框架,因此在这些用例中非常方便。请注意,虽然我们使用 EC2 来测试和调用我们的端点,你也可以使用其他客户端,只要它具有足够的计算能力来处理 Locust 生成的 TPS。

一旦你通过 SSH 进入你的 EC2 实例,我们可以开始定义我们的 locust 脚本。我们首先定义了一个 boto3 客户端,该客户端将进行我们要测量的 invoke_endpoint 调用。我们将用一个分布式 shell 脚本对其中的一些参数进行参数化,稍后会详细介绍。

class BotoClient:
    def __init__(self, host):

        #Consider removing retry logic to get accurate picture of failure in locust
        config = Config(
            retries={
                'max_attempts': 100,
                'mode': 'standard'
            }
        )

        self.sagemaker_client = boto3.client('sagemaker-runtime',config=config)
        self.endpoint_name = host.split('/')[-1]
        self.region = region
        self.content_type = content_type
        self.payload = b'.345,0.224414,.131102,0.042329,.279923,-0.110329,-0.099358,0.0'

现在我们开始具体讨论多模型端点。我们定义了两种方法,每种方法将触及我们两个目标模型中的一个

#model that receives more traffic
    def sendPopular(self):

        request_meta = {
            "request_type": "InvokeEndpoint",
            "name": "SageMaker",
            "start_time": time.time(),
            "response_length": 0,
            "response": None,
            "context": {},
            "exception": None,
        }
        start_perf_counter = time.perf_counter()
        try:
            response = self.sagemaker_client.invoke_endpoint(
                EndpointName=self.endpoint_name,
                Body=self.payload,
                ContentType=self.content_type,
                TargetModel = 'xgboost-0.tar.gz'
            )
            response_body = response["Body"].read()
        except Exception as e:
            request_meta['exception'] = e

        request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000

        events.request.fire(**request_meta)
#model that receives rest of traffic
    def sendRest(self):

        request_meta = {
            "request_type": "InvokeEndpoint",
            "name": "SageMaker",
            "start_time": time.time(),
            "response_length": 0,
            "response": None,
            "context": {},
            "exception": None,
        }
        start_perf_counter = time.perf_counter()

        try:
            response = self.sagemaker_client.invoke_endpoint(
                EndpointName=self.endpoint_name,
                Body=self.payload,
                ContentType=self.content_type,
                TargetModel = 'xgboost-1.tar.gz'
            )
            response_body = response["Body"].read()
        except Exception as e:
            request_meta['exception'] = e

        request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000

        events.request.fire(**request_meta)

现在,如果你有 200 个模型,是否需要为每个模型都准备一种方法?不一定,你可以指定目标模型字符串以适应你需要的模型。例如,如果你有 200 个模型,并且希望特定方法调用 5 个模型,我们可以将 TargetModel 参数设置为如下片段。

f'xgboost-{random.randint(0,4)}.tar.gz' #specifies 5 models to receive traffic in method

如果你想要更具体,可能需要定义更多的方法,但如果你大致了解某些模型会接收到大多数流量,那么像上述的字符串操作就足够了。

最后,我们可以通过装饰器定义任务权重。我们第一个模型现在比第二个模型更有可能接收到流量,概率是前者的三倍。

class MyUser(BotoUser):

    #This model is 3 times more likely to receive traffic
    @task(3)
    def send_request(self):
        self.client.sendPopular()

    @task
    def send_request_major(self):
        self.client.sendRest()

使用任务装饰器,我们可以定义权重,你可以根据流量模式扩展和调整这些权重。

最后,我们在这个仓库中定义了一个可以用来增加或减少流量的 shell 脚本。

#replace with your endpoint name in format https://<<endpoint-name>>
export ENDPOINT_NAME=https://$1

export REGION=us-east-1
export CONTENT_TYPE=text/csv
export USERS=200
export WORKERS=40
export RUN_TIME=2mg
export LOCUST_UI=false # Use Locust UI

#replace with the locust script that you are testing, this is the locust_script that will be used to make the InvokeEndpoint API calls. 
export SCRIPT=locust_script.py

#make sure you are in a virtual environment
#. ./venv/bin/activate

if $LOCUST_UI ; then
    locust -f $SCRIPT -H $ENDPOINT_NAME --master --expect-workers $WORKERS -u $USERS -t $RUN_TIME --csv results &
else
locust -f $SCRIPT -H $ENDPOINT_NAME --master --expect-workers $WORKERS -u $USERS -t $RUN_TIME --csv results --headless &
fi

for (( c=1; c<=$WORKERS; c++ ))
do 
    locust -f $SCRIPT -H $ENDPOINT_NAME --worker --master-host=localhost &
done

在这里,我们定义了 locust 脚本读取的参数,但更重要的是,定义了两个 Locust 特有的参数:用户和工作者。在这里,你可以定义一个用户数量,这些用户将分布在不同的工作者上。你可以根据需要将这些数量扩大或缩小,以尝试实现目标 TPS。我们可以通过运行以下命令来执行我们的分布式测试。

./distributed.sh <endpoint_name>

一旦启动测试,我们可以在我们的 EC2 实例 CLI 中看到负载测试正在进行。

Locust 分布式负载测试(作者截图)

监控

在总结之前,有几种不同的方法可以监控你的负载测试。一种是通过 Locust,如上截图所示,你可以实时跟踪你的 TPS 和延迟。最后,生成一个包含端到端延迟百分位数指标和 TPS 的一般结果文件。要调整测试的持续时间,请检查你在 distributed.sh 脚本中的 RUN_TIME 标志。

最后,为了验证你的负载测试结果,你可以通过 SageMaker CloudWatch 指标进行交叉检查,这些指标可以在控制台中找到。

监控端点(截图由作者提供)

通过调用指标,我们可以了解调用情况及延迟数据。通过实例指标,我们可以查看硬件的饱和程度以及是否需要扩展或缩减。要全面理解如何解读这些指标,请参考此文档

硬件指标(截图由作者提供)

调用指标(截图由作者提供)

在这里我们可以看到我们已扩展到每分钟近 77,000 次调用,这比我们 Locust 指标显示的 1000 TPS 略高。最佳实践是跟踪实例和调用级别的这些指标,以便根据需要为你的硬件定义自动扩展。

额外资源与结论

## GitHub - RamVegiraju/weighted-mme-load-test: Weighted load traffic distribution across models…

你现在无法执行该操作。你在另一个标签或窗口中登录了。你在另一个标签或窗口中注销了……

github.com

示例的完整代码可以在上述链接中找到。如果你对 Locust 和 SageMaker 实时推理不熟悉,我强烈建议你查看与这两个功能相关的起始博客。本存储库中附带的负载测试脚本不仅可以轻松调整以适应 SageMaker 端点,还可以用于测试你托管的任何 API。任何反馈都受到欢迎,随时联系我提问或评论,谢谢阅读!

如果你喜欢这篇文章,请随时在 LinkedIn 上与我联系,并订阅我的 Medium Newsletter。如果你是 Medium 新用户,请使用我的 会员推荐链接注册。

使用 SageMaker 推理推荐器简化负载测试

原文:towardsdatascience.com/load-testing-simplified-with-sagemaker-inference-recommender-b96746b69292

在 SageMaker 实时终端上测试 TensorFlow ResNet50

Ram VegirajuTowards Data Science Ram Vegiraju

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

--

图片来源 Unsplash,作者 Amokrane Ait-Kaci

过去我曾广泛讨论过在将机器学习模型部署到生产环境之前进行 负载测试 的重要性。对于特定的实时推理使用案例,确保你的解决方案满足目标延迟和吞吐量是至关重要的。我们还探讨了如何使用 Python 库 Locust 来定义可以模拟预期流量模式的脚本。

虽然 Locust 是一个功能强大的工具,但它的设置可能会很复杂,并且需要针对不同的超参数和硬件进行大量的迭代,以确定生产环境中的适当配置。对于 SageMaker 实时推理,一个关键工具是 SageMaker 推理推荐器。你可以通过传递一个 EC2 实例类型数组来测试你的终端,以及针对你的特定模型容器的超参数,以便进行更高级的部署,而不是重复运行 Locust 脚本。在今天的博客中,我们将探讨如何配置这一功能以及如何简化 SageMaker 实时终端的负载测试。

注意:本文假定读者具有 AWS、SageMaker 和 Python 的基础知识。要了解 SageMaker 实时推理,请查看以下入门 博客

设置并在本地测试推理

在开发过程中,你可以使用 SageMaker Classic Notebook 实例或 SageMaker Studio Kernel。对于我们的环境,我们使用了一个 TensorFlow 2.0 Kernel,基于 Python3 的 ml.t3.medium 实例。

今天我们将使用的是一个预训练的 TensorFlow ResNet50 图像分类模型。我们可以首先从 TensorFlow 模型中心获取该模型,并将其导入到我们的笔记本中。

import os
import tensorflow as tf
from tensorflow.keras.applications import resnet50
from tensorflow.keras import backend
import numpy as np
from tensorflow.keras.preprocessing import image

在我们开始在 SageMaker 上进行测试之前,我们希望在本地测试该模型,以便了解我们需要为端点配置的输入格式。对于我们的样本数据点,我们将使用我狗狗 Milo 当小狗时的图片(现在他已经变成一个庞然大物了)。

Milo(作者图片)

#model = tf.keras.applications.ResNet50()
tf.keras.backend.set_learning_phase(0)
model = resnet50.ResNet50()

# Load the image file, resizing it to 224x224 pixels (required by this model)
img = image.load_img("dog.jpg", target_size=(224, 224))
# Convert the image to a numpy array
x = image.img_to_array(img)
# Add a forth dimension since Keras expects a list of images
x = np.expand_dims(x, axis=0)

# Scale the input image to the range used in the trained network
x = resnet50.preprocess_input(x)

print("predicting model")
predictions = model.predict(x)
predicted_classes = resnet50.decode_predictions(predictions, top=9)
print(predicted_classes)

模型结果(作者截图)

我们现在已经验证了模型期望的推理格式,因此可以集中精力将其配置到 SageMaker 上。

准备模型和负载

SageMaker Inference Recommender 需要两个必需的输入:模型数据和一个示例负载。它们都需要以 tarball 格式提供,因此我们将工件压缩成服务可以理解的格式。

对于我们的模型,我们可以使用之前在笔记本中加载的模型,或者实例化一个新版本。我们将模型工件下载到一个本地目录,并包含 TensorFlow Serving 所需的元数据。

export_dir = "00001"
tf.keras.backend.set_learning_phase(0)
model = tf.keras.applications.ResNet50()

if not os.path.exists(export_dir):
    os.makedirs(export_dir)
    print("Directory ", export_dir, " Created ")
else:
    print("Directory ", export_dir, " already exists")
# Save to SavedModel
model.save(export_dir, save_format="tf", include_optimizer=False)

然后我们可以将其打包成 model.tar.gz,并上传到可以供 Inference Recommender 指向的 S3 存储桶中。

!tar -cvpzf model.tar.gz ./00001

#upload data to S3
model_url = sagemaker_session.upload_data(
    path="model.tar.gz", key_prefix="resnet-model-data"
)

然后我们将样本图像转换为 JSON 格式,以供我们的模型使用,并以类似于处理模型工件的方式将其保存到 tarball 中。

import json
payload = json.dumps(x.tolist())
payload_archive_name = "payload.tar.gz"

with open("payload.json", "w") as outfile:
    outfile.write(payload)

#create payload tarball
!tar -cvzf {payload_archive_name} payload.json

#upload sample payload to S3
sample_payload_url = sagemaker_session.upload_data(
    path=payload_archive_name, key_prefix="resnet-payload"
)

现在我们已经配置了输入,可以继续进行项目的 SageMaker 部分。

创建 SageMaker 模型并使用模型注册表进行跟踪

SageMaker 有一些特定于其服务的对象,其中对我们来说重要的是SageMaker 模型实体。这个模型实体包括两个核心因素:模型数据和容器/镜像。模型数据可以是你在 S3 桶中提供的训练或预训练模型工件。容器本质上是你模型的框架。在这种情况下,我们可以获取托管的 SageMaker TensorFlow 镜像,但如果不受AWS 深度学习容器支持,你也可以构建并推送自己的容器。在这里我们定义了这个 SageMaker 模型对象,利用 SageMaker Python SDK。

import sagemaker
from sagemaker.model import Model
from sagemaker import image_uris

model = Model(
    model_data=model_url,
    role=role,
    image_uri = sagemaker.image_uris.retrieve(framework="tensorflow", region=region, version="2.1", py_version="py3", 
                                              image_scope='inference', instance_type="ml.m5.xlarge"),
    sagemaker_session=sagemaker_session
    )

一个可选步骤是将你的模型注册到 SageMaker 模型注册表。跟踪数百个模型可能是一个困难的过程,通过模型注册表,你可以简化模型版本控制和血统,使所有模型实体都集中在一个核心空间。我们可以通过以下 API 调用来注册模型。

model_package = model.register(
    content_types=["application/json"],
    response_types=["application/json"],
    model_package_group_name=model_package_group_name,
    image_uri=model.image_uri,
    approval_status="Approved",
    framework="TENSORFLOW"
)

我们还可以在 SageMaker Studio 控制台中查看刚刚创建的模型包。

模型包(作者截图)

在实际应用中,你可能在一个模型包内有多个模型,你可以选择批准部署到生产环境的模型。

现在我们已经准备好了模型对象,我们可以在这个实体上运行推理推荐工作。

推理推荐工作

有两种类型的推理推荐工作:默认和高级。使用默认工作,我们可以简单地传入样本负载以及一组你想测试模型的 EC2 实例。推理推荐器将在这些实例上测试你的模型,并跟踪吞吐量和延迟。我们可以利用right_size API 调用来启动推理推荐工作。

model_package.right_size(
    sample_payload_url=sample_payload_url,
    supported_content_types=["application/json"],
    supported_instance_types=["ml.c5.xlarge", "ml.c5.9xlarge", "ml.c5.18xlarge", "ml.m5d.24xlarge"],
    framework="TENSORFLOW",
)

这个工作大约需要 35-40 分钟完成,因为它将遍历你提供的不同实例类型。然后我们可以在 SageMaker Studio UI 中查看这些结果。

默认工作结果(作者截图)

在这里,你可以根据重要性级别切换成本、延迟和吞吐量,以获得最佳的硬件配置。如果对测试显示的性能满意,你还可以直接从控制台创建端点。

最后,如果你想测试容器的不同超参数,可以通过高级推理推荐作业来实现。在这里,你可以指定适用于特定模型容器的可调超参数。

from sagemaker.parameter import CategoricalParameter 
from sagemaker.inference_recommender.inference_recommender_mixin import (  
    Phase,  
    ModelLatencyThreshold 
) 

hyperparameter_ranges = [ 
    { 
        "instance_types": CategoricalParameter(["ml.m5.xlarge", "ml.g4dn.xlarge"]), 
        'OMP_NUM_THREADS': CategoricalParameter(['1', '2', '3']), 
    } 
] 

除此之外,你还可以配置负载测试的流量模式。例如,如果你想在不同时间间隔内增加用户数量,可以配置这种行为。

phases = [ 
    Phase(duration_in_seconds=120, initial_number_of_users=2, spawn_rate=2), 
    Phase(duration_in_seconds=120, initial_number_of_users=6, spawn_rate=2) 
]

你还可以设置阈值,例如,如果你有严格的 200 毫秒延迟要求,如果你的配置未达到这些结果,可以将其设置为停止参数。

model_latency_thresholds = [ 
    ModelLatencyThreshold(percentile="P95", value_in_milliseconds=300) 
]

然后,你可以以类似于默认作业的方式启动并查看高级作业的结果。

model_package.right_size( 
    sample_payload_url=sample_payload_url, 
    supported_content_types=["application/json"], 
    framework="TENSORFLOW", 
    job_duration_in_seconds=3600, 
    hyperparameter_ranges=hyperparameter_ranges, 
    phases=phases, # TrafficPattern 
    max_invocations=100, # StoppingConditions 
    model_latency_thresholds=model_latency_thresholds
)

额外资源与结论

## sagemaker-inference-recommender-examples/inference-recommender-tf-resnet.ipynb at main ·…

通过在 GitHub 上创建账户,为 aws-samples/sagemaker-inference-recommender-examples 的开发做出贡献。

GitHub 地址

你可以在上面的链接中找到此示例的代码及更多内容。SageMaker 推理推荐器是一个强大的工具,可以自动化负载测试设置的复杂部分。然而,需要注意的是,目前不支持如多模型和多容器端点这样的高级托管选项,因此对于这些用例,使用像 Locust 这样的第三方框架将是必要的。如往常一样,任何反馈都是受欢迎的,欢迎随时提出问题或评论,感谢阅读!

如果你喜欢这篇文章,欢迎通过 LinkedIn 与我联系,并订阅我的 Medium Newsletter。如果你是 Medium 新用户,可以使用我的 会员推荐链接注册。

局部光场融合

原文:towardsdatascience.com/local-light-field-fusion-14c07ed36117

如何在智能手机上渲染 3D 场景

Cameron R. Wolfe, Ph.D.数据科学前沿 Cameron R. Wolfe, Ph.D.

·发布在数据科学前沿 ·12 分钟阅读·2023 年 4 月 11 日

--

(照片由Clyde He拍摄,来源于Unsplash

到目前为止,我们应该知道深度学习是表示 3D 场景并从任意视角生成这些场景的新渲染的一个极佳方法。然而,我们迄今为止看到的方法(例如,ONetsSRNs [2, 3])的问题是它们需要大量的场景图像来训练模型。考虑到这一点,我们可能会想知道是否可以用更少的样本获得基于深度学习的场景表示。我们实际需要多少图像来训练一个高分辨率的场景表示?

这个问题通过局部光场融合(LLFF)[1]方法来解决,该方法用于合成 3D 场景。LLFF 是光场渲染[4]的一个扩展,通过将几个现有视图扩展为多平面图像(MPI)表示,然后通过混合这些表示来渲染新的视角。该方法的结果是:

  1. 精确地模拟复杂场景和效果,例如反射。

  2. 理论上显示减少了生成准确场景表示所需的样本/图像数量。

此外,LLFF 是规范性的,这意味着该框架可以用于告知用户需要多少和什么类型的图像来生成准确的场景表示。因此,LLFF 是一种准确的、基于深度学习的方法,用于生成 3D 场景的建模,并提供有用的、规范性的见解。

(来自 [1])

背景

要理解 LLFF,我们需要了解与计算机视觉和深度学习相关的一些概念。我们将首先讨论光场的概念,然后介绍 LLFF 所使用的一些深度学习概念。

光场。 一个光场将一个 3D 场景表示为在空间中方向性流动的光线。传统上,我们可以使用光场通过仅仅(i) 采样场景的光场(即,捕获具有深度和校准信息的图像)在不同的点和(ii) 在这些光场之间进行插值来渲染场景的视图。

(来自 [1])

对于这样的系统,我们从信号处理的研究中了解到,为了准确渲染场景的新视角,我们需要采集多少样本。准确表示场景所需的最小样本数量称为奈奎斯特率;见上文。实际上,奈奎斯特率所需的样本数量是 prohibitive 的,但全景采样的研究 [7] 旨在提高样本效率,并显著减少所需样本数量,低于奈奎斯特率。

全景采样的内部工作原理对于本概述的目的并不重要。我们应该从这次讨论中得到的主要思想是,[1]中的作者将全景采样的概念扩展到在样本较少(且可能被遮挡)的情况下实现准确的场景渲染;见下文。

(来自 [1])

除了样本效率之外,全景采样还是一种理论框架,它能够进行规范性分析。我们可以通过这种分析具体确定用于训练 LLFF 的图像数量和类型,而不仅仅是拍摄场景图像并希望这就足够了

3D 卷积。 我们大多数人可能对 2D 卷积比较熟悉,比如图像基础的 CNN 中使用的那些。然而,LLFF 实际上利用了 3D 卷积。为什么? 我们稍后会深入了解,但基本原因是我们神经网络的输入不仅仅是一张图像或一组图像,它有一个额外的深度维度。因此,我们需要以考虑这一额外维度的方式进行卷积。

图像上的 2D 卷积与三帧视频上的 3D 卷积(由作者创建)

3D 卷积正好实现了这个目标。也就是说,我们不仅在输入的所有空间维度上进行卷积,还在空间和深度维度上进行卷积。实际上,这为我们的卷积核添加了一个额外的维度,卷积操作在空间和深度上遍历输入。这个过程在上面的图中得到了说明,我们首先在一组帧上进行空间卷积,然后移动到下一组帧进行另一轮空间卷积。

3D 卷积通常用于视频深度学习应用。对于那些对深入了解这个主题或 3D 卷积的内部工作感兴趣的人,可以查看我关于视频深度学习的概述,点击此处

感知损失。LLFFs 的目标是生成准确地类似于场景实际地面真值视角的图像。为了训练一个系统实现这个目标,我们需要一个 图像重建损失,它告诉我们生成的图像与我们试图复制的实际图像有多接近。一个选项是计算两张图像之间差异的 L1/L2 范数——基本上只是对图像像素的 均方误差 损失。

然而,单纯测量像素差异并不是图像相似性的最佳指标;例如,如果生成的图像相比目标图像仅仅右移了一像素呢? 一个更好的方法可以通过一点深度学习实现。特别是我们可以:

  1. 使用一个预训练的深度神经网络。

  2. 使用这个模型将图像嵌入到特征向量中(即,分类前的最终激活层)。

  3. 计算这些向量之间的差异(例如,使用 L1 或 L2 范数)

这种方法称为感知损失 [5],是一种强大的图像相似性度量,广泛用于深度学习研究(特别是生成模型);见 [6] 的第 3.3 节。

LLFFs 如何表示场景?

(引自 [1])

“我们方法的总体策略是使用深度学习管道将每个采样视图提升为具有 D 个深度层的分层场景表示,并通过在相邻场景表示的渲染之间进行混合来渲染新的视图。” — 引自 [1]

从一些图像和 相机视角信息 开始,LLFFs 通过两个不同的步骤渲染新的场景视角:

  1. 将图像转换为 MPI 表示。

  2. 通过混合附近 MPI 的渲染生成视图。

什么是 MPIs? MPIs 是一种以相机为中心的 3D 空间表示。这意味着我们考虑一个特定的相机视角,然后从该特定视角分解 3D 空间。特别是,3D 空间基于三个坐标进行分解:xy 和深度。然后,与这些坐标相关联的是 RGB 颜色和不透明度值,记作 α。有关更多详细信息,请参见 这里

生成 MPIs。 要在 LLFF 中生成 MPI,我们需要一组五张图像,包括一张参考图像和四个在 3D 空间中最近的邻居。通过相机视角信息,我们可以将这些图像重新投影到平面扫掠体积(PSVs)中,深度为 D。在这里,每个深度维度对应于特定视角下场景中的不同深度范围。

(来源于 [1])

从这里,我们可以将所有这些体积连接起来,并通过一系列 3D 卷积层传递(即,3D CNN)。对于每个 MPI 坐标(即,由 [x, y] 空间位置和深度组成),这个 3D CNN 将输出一个 RGB 颜色和一个不透明度值 α,形成一个 MPI 场景表示;见上文。在 [1] 中,这被称为“分层场景表示”,因为 MPI 中表示了不同的深度。

重建视图。 一旦为一个场景生成了 MPI,我们仍然需要利用这些信息来合成一个新颖的场景视角。在 [1] 中,通过渲染多个 MPIs 并对其结果进行加权组合来完成这项工作。

(来源于 [1])

特别是,我们从多个接近所需视角的图像集中生成 MPIs(使用上述描述的 3D CNN),然后使用单应性变换(即,将每个 MPI “扭曲”到所需视角)和α 合成(即,将不同扭曲的 MPIs 组合成一个单一视图)来生成所需视角的 RGB 图像。

为什么我们需要多个 MPIs? [1] 中的方法通常使用不同的图像集生成两个 MPIs,然后将这些表示混合成一个单一的场景渲染。这是为了去除渲染过程中出现的伪影,因为单个 MPI 不太可能包含新相机姿态所需的所有信息。例如,如果原始视角中的图像部分被遮挡怎么办? 混合多个 MPIs 可以避免这些伪影并处理诸如遮挡和视场限制等问题;见下文。

(来自 [1])

训练 LLFF 框架。 为了训练 LLFF 框架,我们使用真实和合成(例如,SUNCGUnrealCV 渲染)数据的组合。在每次训练迭代中,我们采样两组五张图像(用于创建两个 MPI)和一个单独的保留视点。我们通过遵循上述方法生成此保留视点的估计值,然后应用感知损失函数 [5],以捕捉输出视点与真实情况的差异。

我们可以对 LLFF 进行端到端训练,因为它的所有组件都是可微分的。要执行训练迭代,我们只需要:

  1. 采样一些图像。

  2. 生成一个预测视点。

  3. 计算感知损失。

  4. 执行 (随机) 梯度下降 更新。

理论上减少所需样本的数量。 根据奈奎斯特率进行采样在场景表示中是不可行的,因为所需样本的数量太高。幸运的是,[1]中的基于深度学习的 LLFF 方法在理论上已显示出显著减少准确场景表示所需的样本数量。实际上,准确的 LLFF 重建所需的视图数量在实证中被显示为低于奈奎斯特率的 4,000X;见下文。

(来自 [1])

实践中的 LLFF

(来自 [1])

LLFF 的评估基于其在有限采样能力(即远低于奈奎斯特率)的情况下渲染新场景视点的能力。在实验分析中,第一个主要发现之一是,将多个 MPI 的渲染结果混合——而不是仅从单个 MPI 渲染视图——是相当有益的。如上所示,这种方法提高了准确性,并且能够捕捉到非兰伯特效应(例如,反射)。

与基线相比,LLFF 在建模复杂场景方面无论是定量还是定性上都更具能力。特别是,当可用的基础场景样本较少时,LLFF 似乎能够产生更加一致的结果,而基线方法则会出现性能下降;见下文。

(来自 [1])

LLFF 的采样效率突出了深度学习的实用性。即,模型可以从训练数据中学习隐含的先验信息,使其能够更好地处理模糊性!为了更具体地说明这一点,我们考虑一个有输入视图的情况,但这些数据并没有提供我们生成准确新视图所需的所有信息(例如,场景中的某些相关部分可能被遮挡)。由于我们使用了深度学习,我们的神经网络已经从数据中学到了先验模式,使其能够在这些情况下推断出合理的输出!

(来自 [1])

为了更好地理解 LLFF 与基线方法的比较,查看输出的定性示例非常有用。上图提供了几个示例,但这些输出最好以视频形式查看,以便能清晰地看到不同视角之间的插值平滑度。有关示例,请查看 LLFF 的项目页面 这里

LLFF 在智能手机上的应用。 作为 LLFF 框架的实际演示,作者创建了一个用于在场景视角之间进行高质量插值的智能手机应用程序。给定固定分辨率,LLFF 可以使用合理数量的场景图像高效地生成新的场景视角。该应用程序指示用户捕捉特定的场景样本,然后使用 LLFF 实时渲染从预测的 MPI 中生成的视角。

(来源于 [1])

除了 LLFF 渲染的质量,还要记住它是一个规范性框架,这意味着在 [1] 中的作者提供了准确表示场景所需的图像样本数量和类型的理论。沿着这些思路,LLFF 应用实际上会引导用户拍摄特定的场景图像。此功能利用 [1] 中提出的采样分析来确定所需样本,并使用 VR 覆盖来指导用户捕捉特定场景视角;见上文。

收获

LLFF 框架与我们迄今为止看到的其他场景表示方法有很大不同。它使用 3D CNN 而不是 前馈网络,具备理论保证,并且与信号处理的关系更大,而非深度学习。尽管如此,这个框架非常有趣,希望本概述提供的背景能让理解它变得稍微容易些。主要收获如下。

全光谱采样 + 深度学习。 正如在本概述中提到的那样,使用 LLFF 生成准确场景表示所需的样本数量非常少(尤其是与 Nyquist 率相比)。这种样本效率部分得益于 LLFF 所基于的全光谱采样分析。然而,使用深度学习允许从训练数据中学习并泛化模式,这对结果场景渲染的效率和准确性产生了积极影响。

实时表示。 除了 LLFF 渲染的视角质量,方法还被实现为可以实时运行的智能手机应用程序!这实际上展示了 LLFF 的效率,并表明它在现实世界应用中确实可用。然而,使用 LLFF 进行视角渲染所需的预处理大约需要 10 分钟。

多个视角。 为了生成最终的 LLFF 结果,我们生成两个 MPI,并将它们混合在一起。我们可以用单个 MPI 来渲染一个场景,但使用多个 MPI 被发现可以创建更准确的渲染(即,较少的伪影和遗漏的细节)。一般来说,这一发现向我们表明冗余对于场景表示是有用的——从一个视角缺失的有用数据可能在另一个视角中存在!

局限性。 显然,场景表示的质量总是可以提高的——LLFF 并不完美。除了这个简单的观察之外,LLFF 的一个潜在局限是,为了生成输出,我们需要提供几张图像作为输入(例如,[1]中的实验需要每个输出十张输入图像)。相比之下,像 SRNs [3]这样的模型是在一个基础场景的图像上进行训练的,但它们不一定需要这些图像在推理时存在!

结束语

非常感谢阅读这篇文章。我是Cameron R. WolfeRebuy的人工智能总监和莱斯大学的博士生。我研究深度学习的实证和理论基础。你还可以查看我在 medium 上的其他文章!如果你喜欢这篇文章,请关注我的twitter或订阅我的Deep (Learning) Focus newsletter,在这里我通过对相关热门论文的易懂概述,帮助读者深入理解深度学习研究中的话题。

参考文献

[1] 米尔登霍尔,本等。“局部光场融合:具有指导性采样准则的实用视图合成。” ACM 图形学交易(TOG) 38.4 (2019):1–14。

[2] 梅施德,拉斯等。“占用网络:在函数空间中学习 3D 重建。” IEEE/CVF 计算机视觉与模式识别会议论文集。2019 年。

[3] 西茨曼,文森特,迈克尔·佐尔赫弗,和戈登·维茨斯坦。“场景表示网络:连续的 3D 结构感知神经场景表示。” 神经信息处理系统进展 32 (2019)。

[4] 莱沃伊,马克,和帕特·汉拉汉。“光场渲染。” 第 23 届计算机图形与交互技术年会论文集。199。

[5] 多索维茨基,阿列克谢,和托马斯·布罗克斯。“利用基于深度网络的感知相似度度量生成图像。” 神经信息处理系统进展 29 (2016)。

[6] 陈启峰,弗拉德伦·科尔顿。“基于级联优化网络的摄影图像合成。” IEEE 国际计算机视觉会议论文集。2017 年。

[7] 柴进祥等。“全景采样。” 第 27 届计算机图形与交互技术年会论文集。2000 年。

[8] Mildenhall, Ben 等人。“Nerf: 将场景表示为神经辐射场以进行视图合成。” ACM 通讯 65.1 (2021): 99–106。

从头开始的模拟退火局部搜索

原文:towardsdatascience.com/local-search-with-simulated-annealing-from-scratch-9f8dcb6c2e06

温度,模拟退火中的一个重要部分。图片来源:Dall-E 2。

通用 Python 代码及三个示例

Hennie de HarderTowards Data Science Hennie de Harder

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

--

在我之前的一些帖子中,我解释了启发式算法以及如何利用它们找到数学优化问题的优质解。在这篇帖子中,我将提供通用的 Python 代码,用于局部搜索和模拟退火。除了通用代码,还有三个经典示例问题的实现:旅行商问题、背包问题和 Rastrigin 函数。

简短的回顾:局部搜索是一种启发式算法,它通过查看邻居来尝试改善给定的解。如果邻居的目标值比当前目标值更好,则接受邻居解并继续搜索。模拟退火允许接受更差的解,这使得能够摆脱局部最小值。

模拟退火通用代码

代码的工作方式如下:我们将创建四个代码文件。最重要的是 sasolver.py,该文件包含了模拟退火的通用代码。问题目录包含了三个优化问题的示例,我们可以运行这些示例来测试 SA 求解器。

这是文件夹结构:

为了解决一个使用模拟退火的问题,我们开始创建一个相当通用的类:

import copy
import logging
import math
import numpy as np
import random
import time

from problems.knapsack import Knapsack
from problems.rastrigin import Rastrigin
from problems.tsp import TravelingSalesman

class SimulatedAnnealing():
    def __init__(self, problem):
        self.problem = problem

    def run_sa(self, max_iterations: int=100000, update_iterations: int=10000, time_limit: int=60, cooling_schedule: str='lin'):
        start = time.time()
        best_solution = self.problem.baseline_solution()
        best_obj = self.problem.score_solution(best_solution)
        logging.info(f"First solution.        Objective: {round(best_obj, 2)} Solution: {best_solution}")
        initial_temp = best_obj
        prev_solution = copy.deepcopy(best_solution)
        prev_obj = best_obj

        iteration = 0
        last_update = 0
        while time.time() - start < time_limit:
            iteration += 1
            last_update += 1
            accept = False

            curr_solution = self.problem.select_neighbor(copy.deepcopy(prev_solution))
            curr_obj = self.problem.score_solution(curr_solution)

            temperature = self._calculate_temperature(initial_temp, iteration, max_iterations, cooling_schedule)
            acceptance_value = self._acceptance_criterion(curr_obj, prev_obj, temperature)

            if (curr_obj <= prev_obj) or (temperature > 0 and random.random() < acceptance_value):
                accept = True

            if curr_obj < best_obj:
                best_solution = copy.deepcopy(curr_solution)
                best_obj = curr_obj
                prev_solution = copy.deepcopy(curr_solution)
                prev_obj = curr_obj
                last_update = 0
                logging.info(f"Better solution found. Objective: {round(best_obj, 2)} Solution: {curr_solution}")
            else:
                if accept:
                    prev_obj = curr_obj
                    prev_solution = copy.deepcopy(curr_solution)
                    last_update = 0

            if last_update >= update_iterations:
                break

        logging.info(f"Final solution: {best_solution} Objective: {round(best_obj, 2)}")
        return best_solution

    @staticmethod
    def _acceptance_criterion(obj_new, obj_curr, temperature, mod=1):
        """
        Determine the acceptance criterion (threshold for accepting a solution that is worse than the current one)
        """
        diff = obj_new - obj_curr
        try:
            acc = math.exp(-diff / temperature)
        except OverflowError:
            acc = -1
        return acc

    @staticmethod
    def _calculate_temperature(initial_temp: int, iteration: int, max_iterations: int, how: str = None) -> float:
        """
        Decrease the temperature to zero based on total number of iterations.
        """
        if iteration >= max_iterations:
            return -1
        if how == "exp":
            cooling_rate = 0.95
            return initial_temp * (cooling_rate**iteration)
        elif how == "quadratic":
            cooling_rate = 0.01
            return initial_temp / (1 + cooling_rate * iteration**2)
        elif how == "log":
            cooling_rate = 1.44
            return initial_temp / (1 + cooling_rate * np.log(1 + iteration))
        elif how == "lin mult":
            cooling_rate = 0.1
            return initial_temp / (1 + cooling_rate * iteration)
        else:
            return initial_temp * (1 - iteration / max_iterations)

if __name__ == '__main__':
    problem = 'rastrigin'  # choose one of knapsack, tsp, rastrigin
    logging.basicConfig(filename=f'{problem}.log', encoding='utf-8', level=logging.INFO)
    if problem == 'tsp':
        problem = TravelingSalesman(n_locations=10, height=100, width=100)
        sa = SimulatedAnnealing(problem)
        final_solution = sa.run_sa()
        problem._plot_solution(final_solution, title='final')
    elif problem == 'knapsack':
        problem = Knapsack(knapsack_capacity=100, n_items=10)
        sa = SimulatedAnnealing(problem)
        final_solution = sa.run_sa()
    elif problem == 'rastrigin':
        problem = Rastrigin(n_dims=2) 
        sa = SimulatedAnnealing(problem)
        final_solution = sa.run_sa()

这个文件是 sasolver.py。它接受一个问题作为输入,然后你可以通过 run_sa() 使用模拟退火来解决这个问题。处理降温的方式有不同的实现,体现在 _calc_temperature 中。接受值是根据 Metropolis 接受准则计算的。

通过修改 problem = 'tsp' 这一行,(在 if __name__ == '__main__': 下面,)可以选择另一个问题(将 tsp 替换为 knapsackrastrigin)。

我们需要在示例问题中有三种方法来使这段代码正常工作:

  1. baseline_solution()

    该方法为问题创建第一个解决方案(起始点)。

  2. score_solution(solution)

    score_solution 方法计算目标值。

  3. select_neighbor(solution)

    我们需要对解决方案应用局部移动并选择一个邻居,这将在此方法中实现。

我们将为三个问题实现这三种方法:旅行推销员、背包和 Rastrigin 函数。

示例 1. 旅行推销员

我们将要讨论的第一个问题是旅行推销员问题。在这个问题中,有一些地点需要访问。目标是最小化旅行距离。下面是一个示例:

示例:我们想访问 10 个地点并最小化距离。图片由作者提供。

import matplotlib.pyplot as plt
import numpy as np
import random
from typing import List

class TravelingSalesman():
    def __init__(self, n_locations: int = 10, locations: List[tuple] = None, height: int = 100, width: int = 100, starting_point: int=0):
        self.name = 'traveling salesman'
        self.starting_point = starting_point
        self.height = height
        self.width = width
        if locations is None:
            locations = self._create_sample_data(n_locations)
        self.locations = locations
        self.n_locations = len(locations)
        self.distances = self._create_distances()

    def baseline_solution(self) -> list:
        # route that follows the locations list
        # start and end in start location
        baseline = [self.starting_point] + [i for i in range(self.n_locations) if i != self.starting_point] + [self.starting_point]
        self._plot_solution(baseline, title='baseline')
        self._plot_solution(baseline, title='dots', only_dots=True)
        return baseline

    def score_solution(self, solution: list) -> float:
        # add all distances
        return sum([self.distances[node, solution[i+1]] for i, node in enumerate(solution[:-1])])

    def select_neighbor(self, solution: list) -> list:
        # swap two locations (don't swap start and end)
        indici = random.sample(range(1, self.n_locations), 2)
        idx1, idx2 = indici[0], indici[1]
        value1, value2 = solution[idx1], solution[idx2]
        solution[idx1] = value2
        solution[idx2] = value1
        return solution

    def _create_sample_data(self, n_locations: int) -> List[tuple]:
        return [(random.random() * self.height, random.random() * self.width) for _ in range(n_locations)]

    def _plot_solution(self, solution: list, title: str = 'tsp', only_dots: bool = False):
        plt.clf()
        plt.rcParams["figure.figsize"] = [5, 5]
        plt.rcParams["figure.autolayout"] = True
        for n, location_id1 in enumerate(solution[:-1]):
            location_id2 = solution[n+1]
            x_values = [self.locations[location_id1][0], self.locations[location_id2][0]]
            y_values = [self.locations[location_id1][1], self.locations[location_id2][1]]
            if not only_dots:
                plt.plot(x_values, y_values, 'bo', linestyle="-")
            else:
                plt.plot(x_values, y_values, 'bo')
            plt.text(x_values[0]-2, y_values[0]+2, str(location_id1))
        plt.savefig(f'{title}')

    def _create_distances(self) -> np.array:
        distances = np.zeros(shape=(self.n_locations, self.n_locations))
        for ni, i in enumerate(self.locations):
            for nj, j in enumerate(self.locations):
                distances[ni, nj] = self._distance(i[0], i[1], j[0], j[1])
        return distances

    @staticmethod
    def _distance(x1: float, y1: float, x2: float, y2: float) -> float:
        return np.sqrt((x2 - x1)**2 + (y2 - y1)**2)

在这个问题中,基线解决方案是通过按顺序访问位置(0 到 9)创建的。例如,它给出了以下路线:

基线解决方案。图片由作者提供。

这看起来并不理想,确实如此。局部移动通过交换两个位置来定义。解决方案的得分是我们需要旅行的距离。经过模拟退火后,这是最终解决方案:

最终解决方案。图片由作者提供。

看起来好多了!

对于小问题,这种方法效果不错(但仍不推荐)。对于较大的问题,有更好的解决方案和算法可用,例如 Lin-Kernighan 启发式算法。更好的起始解决方案也有帮助,例如贪心算法。

示例 2. 背包

背包问题是一个经典问题,但对于那些不熟悉它的人,这里有一个解释。

想象你身处一个充满美丽宝藏的洞穴中。由于一些不可预见的情况,洞穴正在坍塌。你有时间将宝藏装入背包中,然后需要逃离到安全的地方。当然,你想带上那些总价值最高的物品。你应该带哪些物品?

背包问题。背包的容量为 50。你应该选择哪些物品来最大化价值?图片由作者提供。

解决此问题所需的数据包括背包的容量、物品所需的容量以及物品的价值。

下面是定义这个问题的代码:

import copy
import random
import numpy as np
from typing import List

class Knapsack():
    def __init__(self, knapsack_capacity: int, n_items: int = 20, item_values: list = None, item_capacities: list = None):
        self.name = 'knapsack'
        self.knapsack_capacity = knapsack_capacity
        if item_values is None and item_capacities is None:
            item_values, item_capacities = self._create_sample_data(n_items)
        self.item_values = item_values
        self.item_capacities = item_capacities
        self.n_items = len(item_values)

    def baseline_solution(self) -> list:
        # select random items until the knapsack is full
        capacity = 0
        solution = []
        while True:
            selected = random.choice([i for i in range(self.n_items) if i not in solution])
            if capacity + self.item_capacities[selected] > self.knapsack_capacity:
                break
            else:
                solution.append(selected)
                capacity += self.item_capacities[selected]
        return solution

    def score_solution(self, solution: list) -> int:
        # count the total value of this solution
        return -1 * sum([self.item_values[i] for i in solution])

    def select_neighbor(self, solution: list) -> list:
        # local move: remove / add / swap items
        solution_capacity = sum([self.item_capacities[i] for i in solution])
        possible_to_add = [i for i in range(self.n_items) if self.item_capacities[i] <= self.knapsack_capacity - solution_capacity and i not in solution]
        if len(solution) == 0:
            move = 'add'
        elif len(possible_to_add) > 0:
            move = np.random.choice(['remove', 'add', 'swap'], p=[0.1, 0.6, 0.3])
        else:
            move = np.random.choice(['remove', 'swap'], p=[0.4, 0.6])
        while True:
            if move == 'remove':
                solution.pop(random.randrange(len(solution)))
                return solution
            elif move == 'add':
                new_solution = copy.deepcopy(solution)
                new_item = random.choice(possible_to_add)
                new_solution.append(new_item)
                return new_solution
            elif move == 'swap':
                n = 0
                while n < 50:
                    new_solution = copy.deepcopy(solution)
                    in_item = random.choice([i for i in range(self.n_items) if i not in solution])
                    out_item = random.choice(range(len(solution)))
                    new_solution.pop(out_item)
                    new_solution.append(in_item)
                    n += 1
                    if self._is_feasible(new_solution):
                        return new_solution
                move = 'remove'

    def _create_sample_data(self, n_items: int) -> List[list]:
        item_values = random.sample(range(2, 1000), n_items)
        item_capacities = random.sample(range(1, self.knapsack_capacity), n_items)
        return item_values, item_capacities

    def _is_feasible(self, solution: list) -> bool:
        return sum([self.item_capacities[i] for i in solution]) <= self.knapsack_capacity

基线解决方案随机选择一个物品,直到背包满了。解决方案的得分是背包中物品值的总和,乘以 -1。因为 SA 求解器最小化给定的目标。在这种情况下,有三种局部移动方式:添加一个物品、移除一个物品或交换两个物品。这使得在解空间中可以达到每一个可能的解。如果我们交换一个物品,需要检查新解是否可行。

在下图中可以看到一个示例运行日志文件。我们需要从 10 个物品中选择。顶部是物品值,下面是物品占用的容量,第三行是价值密度(物品值除以物品容量)。然后开始解决过程。解决方案包含所选物品的索引号。在最终解决方案中,选择了物品 4、5 和 8(计数从 0 开始):

示例 3. 拉斯特里金函数

一个常用于测试优化算法的函数是 拉斯特里金函数。在 3D 中,它看起来是这样的:

拉斯特里金函数的 3D 图。图片由作者提供。

这个函数有很多局部极小值。目标是找到全局最小值,它的坐标是 (0, 0)。在等高线图中更容易看出:

拉斯特里金函数的等高线图。图片由作者提供。

地形由许多局部极小值组成,四个角落的值最高,而中心的值最低。

我们可以尝试使用模拟退火来寻找全局最小值。这个问题是连续的而不是离散的,我们想找到最小化拉斯特里金函数的 xy 的值。

拉斯特里金函数在 n 维域上的定义为:

让我们尝试在三维空间中找到该函数的最优解(xyz)。定义域由 xy 确定,因此问题与上面的图形完全相同。

from collections import Counter
import numpy as np
import random
from typing import List

class Rastrigin():
    def __init__(self, n_dims: int = 2):
        self.name = 'rastrigin'
        self.n_dims = n_dims

    def baseline_solution(self) -> list:
        solution = [random.uniform(-5.12, 5.12) for _ in range(self.n_dims)]
        return solution

    def score_solution(self, solution: list) -> float:
        score = self.n_dims * 10 + sum([(x**2 - 10*np.cos(2*np.pi*x)) for x in solution])
        return score

    def select_neighbor(self, solution: list, step_size: float = 0.1) -> list:
        perturbation = step_size * np.random.randn(self.n_dims)
        neighbor = solution + perturbation
        while not self._is_feasible(neighbor):
            perturbation = step_size * np.random.randn(self.n_dims)
            neighbor = solution + perturbation    
        return neighbor

    def _is_feasible(self, solution: list) -> bool:
        return bool([x >= -5.12 and x <= 5.12 for x in solution])

对于基线解决方案,我们在 -5.12 和 5.12 之间随机选择 xy 的浮点数。解决方案的得分等于 z(拉斯特里金函数的结果)。通过在随机方向上迈出一步,步长设置为 0.1 来选择一个邻居。进行可行性检查以确保我们保持在定义域内。

一次运行的日志:

最终解决方案非常接近最优解。

但要注意,如果你在更多维度下运行算法,不能保证找到最优解:

如你所见,最终的解决方案是局部最优而非全局最优。它找到了前两个变量的良好坐标,但第三个变量等于 0.985,远离 0。验证结果非常重要。这个具体例子通过微调 SA 参数可以很好地工作,但对于更多维度,你可能需要使用另一种表现更好的优化技术。

结论

在这篇文章中,我们实现了模拟退火的代码。通过三个例子,你可以理解它的不同可能性。你只需为新问题实现三个方法,使其工作,这些方法是baseline_solutionscore_solutionselect_neighbor。当然,这个实现是基本的,如果你想使用它,你需要调整 SA 的参数,确保算法返回一个可行的解决方案。一个好的初始解决方案和研究选择邻近解决方案的最佳方法能大大提高最终解决方案的质量。

感谢阅读,下次见!

相关内容

## 介绍一种强大的优化技术:模拟退火

解释、参数、优点、缺点和应用场景

数学优化启发式方法每个数据科学家都应该知道 [## 数学优化启发式方法每个数据科学家都应该知道

局部搜索、遗传算法等。

## 将数学优化与机器学习结合的五种方式

两种力量结合的实际例子。

将数学优化与机器学习结合的四种方式

本地预测与全球预测:你需要知道的

原文:towardsdatascience.com/local-vs-global-forecasting-what-you-need-to-know-1cc29e66cae0

比较本地和全球时间序列预测方法,并通过使用 LightGBM 和澳大利亚旅游数据集的 Python 演示。

Davide BurbaTowards Data Science Davide Burba

·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 2 日

--

本地预测与全球预测,由 Giulia Roggia. 经许可使用。

  • 什么是本地预测?

  • 什么是全球预测?

  • 如何选择本地预测与全球预测?

  • Python 示例:澳大利亚旅游

  • 结论

什么是本地预测?

本地预测是传统的方法,其中我们为每个时间序列独立训练一个预测模型。经典的统计模型(如指数平滑、ARIMA、TBATS 等)通常采用这种方法,但通过特征工程步骤,标准的机器学习模型也可以使用这种方法。

本地预测具有优势

  • 它的理解和实现都很直观。

  • 每个模型可以单独调整。

但它也有一些局限性

  • 它存在“冷启动”问题:需要为每个时间序列提供相对大量的历史数据,以可靠地估计模型参数。此外,这也使得预测新目标变得不可能,比如对新产品需求的预测。

  • 它不能捕捉相关时间序列之间的共性和依赖性,比如横截面或层级关系。

  • 对于包含多个时间序列的大型数据集,本地预测很难扩展,因为这需要为每个目标拟合和维护一个单独的模型。

什么是全球预测?

图片由 PIRO 提供,来自 Pixabay

全局预测是一种更现代的方法,其中使用多个时间序列来训练一个单一的“全局”预测模型。通过这样做,它拥有更大的训练集,并且可以利用目标之间的共享结构来学习复杂的关系,最终实现更好的预测。

构建全局预测模型通常包括一个特征工程步骤来创建诸如:

  • 目标的滞后值

  • 目标在时间窗口中的统计数据(例如“过去一周的均值”“过去一个月的最小值”等)

  • 分类特征来区分时间序列的不同组

  • 外生特征来建模外部/互动/季节性因素

全局预测具有相当大的优势

  • 它利用其他时间序列的信息来提高准确性和鲁棒性。

  • 它可以对几乎没有数据的时间序列进行预测。

  • 它可以扩展到包含许多时间序列的数据集,因为它只需要拟合和维护一个单一的模型。

  • 通过使用特征工程,它可以处理多数据频率和缺失数据等问题,这些问题用经典统计模型更难解决。

但全局预测也有一些局限性

  • 它需要额外的努力来使用更复杂的模型和进行特征工程。

  • 当出现新的时间序列时,可能需要进行全面的重新训练。

  • 如果一个特定时间序列的性能开始下降,很难在不影响其他目标预测的情况下更新它。

  • 这可能需要更多的计算资源和复杂的方法来估计和优化模型参数。

如何在局部预测和全局预测之间进行选择?

对于给定问题,没有明确的答案说明局部预测还是全局预测更好。

一般来说,局部预测可能更适合具有以下特征的问题:

  • 少量时间序列的历史较长

  • 时间序列之间的高变异性和特异性

  • 预测和编程专业知识有限

另一方面,全局预测可能更适合具有以下特征的问题:

  • 许多时间序列的历史较短

  • 目标之间的变异性低且相似度高

  • 噪声数据

示例:澳大利亚旅游

图片由Penny提供,来源于Pixabay

在本节中,我们通过使用LightGBM澳大利亚旅游数据集的 Python 实际示例来展示这两种方法之间的差异,该数据集可以在 darts上找到,并在 Apache 2.0 许可证下提供。

让我们从导入必要的库开始。

import pandas as pd
import plotly.graph_objects as go
from lightgbm import LGBMRegressor
from sklearn.preprocessing import MinMaxScaler

数据准备

澳大利亚旅游数据集由从 1998 年开始的季度时间序列组成。在这个笔记本中,我们考虑的是地区级别的旅游数据。

# Load data.
data = pd.read_csv('https://raw.githubusercontent.com/unit8co/darts/master/datasets/australian_tourism.csv')
# Add time information: quarterly data starting in 1998.
data.index = pd.date_range("1998-01-01", periods = len(data), freq = "3MS")
data.index.name = "time"
# Consider only region-level data.
data = data[['NSW','VIC', 'QLD', 'SA', 'WA', 'TAS', 'NT']]
# Let's give it nicer names.
data = data.rename(columns = {
    'NSW': "New South Wales",
    'VIC': "Victoria", 
    'QLD': "Queensland", 
    'SA': "South Australia", 
    'WA': "Western Australia", 
    'TAS': "Tasmania", 
    'NT': "Northern Territory",
})

让我们快速查看数据:

# Let's visualize the data.
def show_data(data,title=""):
    trace = [go.Scatter(x=data.index,y=data[c],name=c) for c in data.columns]
    go.Figure(trace,layout=dict(title=title)).show()

show_data(data,"Australian Tourism data by Region")

这产生了如下图表:

作者提供的图片

我们可以看到:

  • 数据显示出强烈的年度季节性。

  • 不同地区的时间序列规模差异很大。

  • 时间序列的长度始终相同。

  • 没有缺失数据。

数据工程

让我们预测下一个季度的值,基于:

  • 前两年的滞后值

  • 当前季度(作为一个分类特征)

def build_targets_features(data,lags=range(8),horizon=1):
    features = {}
    targets = {}
    for c in data.columns:

        # Build lagged features.
        feat = pd.concat([data[[c]].shift(lag).rename(columns = {c: f"lag_{lag}"}) for lag in lags],axis=1)
        # Build quarter feature.
        feat["quarter"] = [f"Q{int((m-1) / 3 + 1)}" for m in data.index.month]
        feat["quarter"] = feat["quarter"].astype("category")
        # Build target at horizon.
        targ = data[c].shift(-horizon).rename(f"horizon_{horizon}")

        # Drop missing values generated by lags/horizon.
        idx = ~(feat.isnull().any(axis=1) | targ.isnull())
        features[c] = feat.loc[idx]
        targets[c] = targ.loc[idx]

    return targets,features

# Build targets and features.
targets,features = build_targets_features(data)

训练/测试拆分

为了简单起见,在这个例子中,我们用单次训练/测试拆分来回测我们的模型(你可以查看这篇文章获取关于回测的更多信息)。我们将最后 2 年作为测试集,将之前的时期作为验证集。

def train_test_split(targets,features,test_size=8):
    targ_train = {k: v.iloc[:-test_size] for k,v in targets.items()}
    feat_train = {k: v.iloc[:-test_size] for k,v in features.items()}
    targ_test = {k: v.iloc[-test_size:] for k,v in targets.items()}
    feat_test = {k: v.iloc[-test_size:] for k,v in features.items()}
    return targ_train,feat_train,targ_test,feat_test

targ_train,feat_train,targ_test,feat_test = train_test_split(targets,features)

模型训练

现在我们使用两种不同的方法来估计预测模型。在这两种情况下,我们都使用具有默认参数的 LightGBM 模型。

本地方法

如前所述,使用本地方法我们估计多个模型:每个目标一个

# Instantiate one LightGBM model with default parameters for each target.
local_models = {k: LGBMRegressor() for k in data.columns}
# Fit the models on the training set.
for k in data.columns:
    local_models[k].fit(feat_train[k],targ_train[k])

全局方法

另一方面,使用全局方法我们估计一个模型用于所有目标。为此,我们需要执行两个额外的步骤:

  1. 首先,由于目标具有不同的规模,我们执行归一化步骤。

  2. 然后,为了让模型能够区分不同的目标,我们为每个目标添加一个分类特征。

这些步骤在接下来的两节中进行描述。

步骤 1:归一化 我们将所有数据(目标和特征)在 0 到 1 之间按目标进行缩放。这很重要,因为它使数据可比,从而使模型训练更容易。缩放参数的估计是在验证集上完成的。

def fit_scalers(feat_train,targ_train):
    feat_scalers = {k: MinMaxScaler().set_output(transform="pandas") for k in feat_train}
    targ_scalers = {k: MinMaxScaler().set_output(transform="pandas") for k in feat_train}
    for k in feat_train:
        feat_scalers[k].fit(feat_train[k].drop(columns="quarter"))
        targ_scalers[k].fit(targ_train[k].to_frame())
    return feat_scalers,targ_scalers

def scale_features(feat,feat_scalers):
    scaled_feat = {}
    for k in feat:
        df = feat[k].copy()
        cols = [c for c in df.columns if c not in {"quarter"}]
        df[cols] = feat_scalers[k].transform(df[cols])
        scaled_feat[k] = df
    return scaled_feat

def scale_targets(targ,targ_scalers):
    return {k: targ_scalers[k].transform(v.to_frame()) for k,v in targ.items()}

# Fit scalers on numerical features and target on the training period.
feat_scalers,targ_scalers = fit_scalers(feat_train,targ_train)
# Scale train data.
scaled_feat_train = scale_features(feat_train,feat_scalers)
scaled_targ_train = scale_targets(targ_train,targ_scalers)
# Scale test data.
scaled_feat_test = scale_features(feat_test,feat_scalers)
scaled_targ_test = scale_targets(targ_test,targ_scalers)

步骤 2:将“目标名称”添加为分类特征 为了让模型能够区分不同的目标,我们将目标名称添加为一个分类特征。这不是一个强制步骤,在某些情况下可能会导致过拟合,特别是当时间序列数量较多时。另一种方法是编码其他目标特定但更通用的特征,如“region_are_in_squared_km”、“is_the_region_on_the_coast”等。

# Add a `target_name` feature.
def add_target_name_feature(feat):
    for k,df in feat.items():
        df["target_name"] = k

add_target_name_feature(scaled_feat_train)
add_target_name_feature(scaled_feat_test)

为了简单起见,我们在将数据合并后将target_name设置为分类变量。我们指定“类别”类型的原因是因为它会被 LightGBM 自动检测到。

# Concatenate the data.
global_feat_train = pd.concat(scaled_feat_train.values())
global_targ_train = pd.concat(scaled_targ_train.values())
global_feat_test = pd.concat(scaled_feat_test.values())
global_targ_test = pd.concat(scaled_targ_test.values())
# Make `target_name` categorical after concatenation.
global_feat_train.target_name = global_feat_train.target_name.astype("category")
global_feat_test.target_name = global_feat_test.target_name.astype("category")

测试集上的预测

为了分析两种方法的性能,我们对测试集进行预测。

首先使用本地方法:

# Make predictions with the local models.
pred_local = {
  k: model.predict(feat_test[k]) for k, model in local_models.items()
}

然后使用全局方法(注意我们应用了反向归一化):

def predict_global_model(global_model, global_feat_test, targ_scalers):
    # Predict.
    pred_global_scaled = global_model.predict(global_feat_test)
    # Re-arrange the predictions
    pred_df_global = global_feat_test[["target_name"]].copy()
    pred_df_global["predictions"] = pred_global_scaled
    pred_df_global = pred_df_global.pivot(
        columns="target_name", values="predictions"
    )
    # Un-scale the predictions
    return {
        k: targ_scalers[k]
        .inverse_transform(
            pred_df_global[[k]].rename(
                columns={k: global_targ_train.columns[0]}
            )
        )
        .reshape(-1)
        for k in pred_df_global.columns
    }

# Make predicitons with the global model.
pred_global = predict_global_model(global_model, global_feat_test, targ_scalers)

错误分析

为了评估两种方法的表现,我们进行错误分析。

首先,我们计算整体和按地区划分的平均绝对误差(MAE):

# Save predictions from both approaches in a convenient format.
output = {}
for k in targ_test:
    df = targ_test[k].rename("target").to_frame()
    df["prediction_local"] = pred_local[k]
    df["prediction_global"] = pred_global[k]
    output[k] = df

def print_stats(output):
    output_all = pd.concat(output.values())
    mae_local = (output_all.target - output_all.prediction_local).abs().mean()
    mae_global = (output_all.target - output_all.prediction_global).abs().mean()
    print("                            LOCAL     GLOBAL")
    print(f"MAE overall              :  {mae_local:.1f}     {mae_global:.1f}\n")
    for k,df in output.items():   
        mae_local = (df.target - df.prediction_local).abs().mean()
        mae_global = (df.target - df.prediction_global).abs().mean()
        print(f"MAE - {k:19}:  {mae_local:.1f}     {mae_global:.1f}")

# Let's show some statistics.
print_stats(output)

结果如下:

测试集上的平均绝对误差 — 图片由作者提供

我们可以看到,全球方法总体上导致了更低的误差,除了西澳大利亚州外,各个区域的误差也更低。

让我们看看一些预测结果:

# Display the predictions.
for k,df in output.items():
    show_data(df,k)

以下是一些输出结果:

图片由作者提供

图片由作者提供

图片由作者提供

我们可以看到,局部模型预测了一个常数,而全球模型捕捉到了目标的季节性行为。

结论

在这个例子中,我们展示了时间序列预测的局部和全球方法,使用了:

  • 澳大利亚季度旅游数据

  • 简单特征工程

  • LightGBM 模型使用默认超参数

我们看到全球方法产生了更好的预测,比局部方法的平均绝对误差低 43%。特别是,全球方法在所有目标上的 MAE 都低于局部方法,除了西澳大利亚州。

在这种设置下,全球方法的优越性在某种程度上是意料之中的,因为:

  • 我们正在预测多个相关的时间序列。

  • 历史数据的深度非常浅。

  • 我们使用的是一种相对复杂的浅层单变量时间序列模型。在这种情况下,经典统计模型可能更合适。

本文使用的代码可在 这里 获取

喜欢这篇文章吗? 查看我的其他文章 并关注我获取更多内容! 点击这里 阅读无限文章,并支持我对你没有额外费用❤️

初学者的对数变换

原文:towardsdatascience.com/logarithmic-transformation-for-beginners-99488b8951e3

无单位的关联解释及其他好处

Jae KimTowards Data Science Jae Kim

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

--

照片由Joshua Sortino拍摄,发布在Unsplash上。

在统计和机器学习模型中,变量经常被转换为自然对数。这有很多好处,包括…

  • 无单位解释关系;

  • 方差稳定化;

  • 缓解异常值的影响;

  • 线性化;和

  • 接近正态分布。

在这篇文章中,我详细解释了上述要点,并提供了应用实例。数据和 R 代码可以从这里获取。

1. 函数的变化和斜率

例如,设 Y = 1 + 2X,其中 Y 是响应变量,X 是输入变量。我们通常对 Y 如何响应 X 的变化感兴趣。设Δ为变化操作符。即,

ΔY = Y1 — Y0:Y 从 Y0 到 Y1 的变化;和

ΔX = X1 — X0:X 从 X0 到 X1 的变化。

假设,在我们的例子中(Y = 1 + 2 X),X 从 1 变化到 3。然后,Y 从 3 变化到 7。即,ΔY = 4,ΔX = 2。

斜率(或导数)衡量 Y 对单位X 变化的响应。它定义为

β ≡ ΔY/ΔX,

在我们的例子中β = 2。我们在线性回归或机器学习模型中遇到的斜率系数有相同的解释。斜率是一个标准化的度量,但它是依赖单位的。即,斜率系数的解释需要仔细考虑其单位。

2. 对数函数的变化和斜率

考虑函数 Y = log(X),其中 log()表示自然对数。

图片由作者使用 R 函数“curve”创建

如上所示,该函数提供了 X 的单调变换,适用于 X > 0。

该函数具有特殊性质,即在 X 的某一点,函数的斜率为 1/X。即,

这意味着 Y 的变化等于 ΔX/X,这代表了 X 的比例变化。

例如,假设 X 从 2000 增加到 2010(0.5% 的增加)。

如上表所示,这意味着

Δlog(X) = log(2010) — log(2000) = 7.606–7.601=0.005,

这等于 (X1-X0)/X0 = (2010–2000)/2000。

即,100Δlog(X) = 100ΔX/X,它测量 X 在给定点的百分比变化。

一般来说,对于任何变量 Z,100Δlog(Z) = 100ΔZ/Z,它测量 Z 在给定点的百分比变化。

3. 线性方程中的对数变换

由于对数函数的上述特性,对数变换的回归可以用于关系的无单位解释,如下表所示:

表 1(由作者创建的图像)

  • 案例 1:Y 和 X 都没有转化为自然对数。在这种情况下,斜率系数 β 测量 Y 在 X 变化 1 单位时的变化量。即,它的解释依赖于 Y 和 X 的单位。

  • 案例 2:Y 和 X 都被转化为自然对数。在这种情况下,斜率系数测量 Y 对 X 1% 变化的百分比变化。这一测量被称为 Y 对 X 的 弹性,是一种在经济学中广泛使用的 无单位 关联度量。

  • 案例 3:仅 X 被转化为自然对数。在这种情况下,斜率系数被解释为对 X 1% 变化的 Y 的 (β/100) 单位变化。

  • 案例 4:仅 Y 被转化为自然对数。在这种情况下,斜率系数被解释为 Y 对 X 1 单位变化的 100β% 变化。

案例 2 在 Y 和 X 是不同单位的连续变量时很有用。案例 3 可能在 Y 取负值或 Y 已经以百分比表示时有用。案例 4 可能在 X 是指标变量或离散变量时使用。因此,选择哪种情况取决于研究的背景。

4. 对数变换的其他好处

变换的缩放效应可以带来其他好处,从而提供对关系的更准确或更可靠的估计。

  • 当 Y 和 X 的数值较大时,估计的变异性可能过大。对数变换单调地将数据转化为较小的尺度,从而大幅降低变异性,进而减少估计的变异性。

  • 在这一过程中,异常值的影响或效应可以被显著减轻。

  • 结果是,内在关系可以通过改善线性度得到更好的揭示。

  • 转换后的数据可以更接近正态分布。

5. 应用

我选择了来自 Kaggle 的芝加哥房价数据集,可以从这里访问(CC0)。变量包括

  • 价格:房价

  • 卧室:卧室数量

  • 空间:房屋面积(以平方英尺为单位)

  • 地块:地块宽度

  • 税:年度税额

  • 浴室:浴室数量

  • 车库:车库数量

  • 条件:房屋状况(如果良好则为 1,否则为 0)

注意,数据源中未提供价格、地块和税变量的单位。

下图 1 展示了价格变量和 log(价格)的 Q-Q 图。

图 1(由作者制作的图像)

蓝色直线是参考线,样本分位数应与正态分布的分位数完全匹配,前提是样本符合正态分布。蓝色带表示样本分位数的 95%置信区间。如果一个分布符合正态分布,那么样本分位数应接近参考线。如果它们位于 95%置信区间内,偏离参考线在 5%的显著性水平上是统计上微不足道的。

从图 1 中可以清楚地看到,价格变量显示出一定程度的非正态性,多个样本分位数在 95%置信区间之外。然而,log(价格)几乎所有样本分位数都在这个区间内,表明通过对数变换,变量变得更接近正态分布。

图 2(由作者制作的图像)

上述图 2 显示了价格与税之间的散点图;以及 log(价格)与 log(税)之间的散点图。从前者来看,可以认为关系是非线性的,存在几个离群值。通过对数变换,这些离群值的影响明显减少,关系也可以被认为是线性的。

现在我运行价格与所有其他变量作为解释变量的回归分析。

  • 模型 1:所有变量都按原样包括;并且

  • 模型 2:所有连续变量(价格、空间、地块、税)都转换为自然对数,而其他(离散)变量保持原样。

回归结果在下表 2 中列出:

表 2(由作者制作的图像)

  • 两个模型都显示了大于 0.70 的足够大的 R²值。然而,这两个值不可比,因为因变量的量纲不同。

  • 模型 2 中所有系数在 5%的显著性水平上都是统计显著的。相比之下,模型 1 中有两个系数(税和条件)在常规显著性水平上是不显著的,尽管相关变量在经济上很重要。

  • 在模型 1 中,税收系数较小且统计上不显著;但在模型 2 中,log(税收) 的系数较大且统计上显著。这可能与图 2 中散点图所做的观察有关,即通过对数变换进行线性化。

为了解释空间系数,

  • 从模型 1 中来看,额外 100 平方英尺的空间的房屋预计价格会高出 1.3 单位(其他因素保持不变):参见表 1 中的案例 1;

  • 从模型 2 中来看,空间大 10% 的房屋预计价格会高出 1.63%(其他因素保持不变):参见表 1 中的案例 2。

为了解释浴室系数,

  • 从模型 1 中来看,额外一个浴室的房屋预计价格会高出 7.251 单位(其他因素保持不变);

  • 从模型 2 中来看,额外一个浴室的房屋预计价格会高出 13.3%(=100 × 0.133):参见表 1 中的案例 4。

其他系数也可以以类似的方式进行解释。

通过对数变换,研究人员可以对关联进行无单位解释,这样更容易理解和解释。

总结来说,对数变换在统计建模和机器学习方法中非常有用。当研究人员希望对关联进行无单位解释时,强烈推荐使用对数变换。当数据的单位未知或难以比较时,它也很有用。此外,它还可以带来更准确和可靠的模型及其参数估计的一系列好处。

使用梯度下降的多类逻辑回归

原文:towardsdatascience.com/logistic-regression-a3a23e169eec

多类逻辑回归的介绍,包括理论和 Python 实现

Stefan Hrouda-RasmussenTowards Data Science Stefan Hrouda-Rasmussen

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

--

逻辑回归的决策边界。图片由作者提供。

内容

本文是我将要发布的一系列文章的一部分。下面你可以看到该系列的概览。

1. 机器学习简介

  • (a) 什么是机器学习?

  • (b) 机器学习中的模型选择

  • (c) 维度的诅咒

  • (d) 什么是贝叶斯推断?

2. 回归

  • (a) 线性回归实际上是如何工作的

  • (b) 如何通过基函数和正则化改善线性回归

3. 分类

  • (a) 分类器概述

  • (b) 二次判别分析 (QDA)

  • (c) 线性判别分析 (LDA)

  • (d) (高斯) 朴素贝叶斯

  • (e) 使用梯度下降的多类逻辑回归

设置和目标

到目前为止,我们已经讨论了生成型分类器(QDA、LDA 和朴素贝叶斯),但现在我们将目光转向一种判别型分类器:逻辑回归。如在 3(a)中所提到的分类器概述,逻辑回归是一种判别型分类器,这意味着它直接建模目标的条件概率分布 P(t|x)。

但为什么它叫做逻辑 回归 而不是逻辑 分类,如果它是一个分类模型呢?嗯,答案很简单,因为我们在回归条件概率——我知道,这很令人困惑。

在我们继续之前,我建议你在学习逻辑回归之前对线性回归有一个好的掌握因为它们非常相似。如果你阅读了 我关于线性回归的文章,你会很轻松,因为我使用了相同的术语和符号。

给定一个包含 N 个输入变量 x 和相应目标变量 t 的训练数据集,逻辑回归假设

其中 c 是从 1 到 C 的任意整数,表示类别的数量。

(1)的右侧是 softmax 函数。基本上,这是我们用来将线性组合转换为 0 到 1 之间概率的函数

要理解(1)实际意义是什么,让我们看看特殊情况,即 C=2,也就是说,我们有 2 个类别。我们现在通常将(1)重写为

其中 σ 指的是 逻辑 sigmoid 函数,这就是逻辑回归名称的由来。

现在,逻辑 sigmoid 函数看起来是这样的:

插入 sigmoid 函数的图像*

正如你所看到的,y 值在 0 和 1 之间,值越高,越接近 1。这意味着 wᵀx 越大,点 (x, t) 属于类别 1 的概率越高 —— 对于超过 2 个类别的情况也是如此,公式(1)中 c 的值越高,我们将预测 t 属于哪个类别,或者

推导和训练

那么,我们如何找到 W 的值呢?嗯,就像线性回归一样,我们将使用最大似然估计来找出参数的值。这个过程是通过写出似然函数,求其导数,并找到参数的哪些值使导数等于 0,因为这将是似然的最大值。

为了简化符号表示,让 tₙ 表示一个 C 维向量,其中 tₙ꜀ 为 1 如果观测值属于类别 c,而其他所有组件为 0。我们现在可以将似然写成

就像线性回归一样,我们现在将取负对数似然,因为我们知道 最小化负对数似然等同于最大化似然,以帮助我们的推导

现在我们有了负对数似然,我们将找到它的梯度(导数),并找出 w 的哪些值使其等于 0。公式(3)也被称为 交叉熵损失函数

在我们开始推导之前,先定义一些术语以简化我们的符号。首先,让我们将 softmax 函数表示为

其次,让 E 表示(3)中的函数

现在,使用链式法则,我们可以确定 E 的梯度

从右侧开始,我们有

接下来,我们有 softmax 函数的导数

但这仅在 i=c 时成立。在它们不相等的情况下,我们得到

我们可以将(6)和(7)结合起来

最后,我们有

现在我们终于将(4)的所有部分拼凑在一起。将(5)、(7)和(9)代入(4)得到以下结果

经过这些工作,我们终于找到了似然函数的梯度。我们现在需要做的是确定 w 为 0 的值。原因是 函数的极大值和极小值将出现在梯度为 0 的地方

问题是我们无法找到这个问题的闭式解,因此我们需要一个算法来为我们找出函数何时等于 0。我们将使用的算法称为 梯度下降。它通过使用以下方程来工作:

其中 η 称为学习率,是一个超参数。我会在未来写一篇详细介绍梯度下降的文章,但简而言之,如果学习率过高,你会错过梯度函数的最小值如果学习率过低,则需要太长时间才能达到最小值

Python 实现

下面的代码是我们刚刚讨论过的(高斯)朴素贝叶斯的简单实现。

下面是一个图表,包含数据点(按颜色编码以匹配它们各自的类别)和由逻辑回归生成的决策边界。

由梯度下降优化生成的逻辑回归决策边界。作者提供的图像。

总结

  • 逻辑 回归 是一个 分类模型

  • 逻辑回归是一个 判别分类器

  • 如果我们有 2 个类别,我们使用 逻辑 sigmoid 函数将线性函数转换为概率

  • softmax 函数是逻辑 sigmoid 函数的多类别推广

  • 逻辑回归中的负对数似然也被称为交叉熵损失函数

  • 逻辑回归没有闭式解,因此我们使用梯度下降。

  • 学习率和迭代次数是超参数,你需要调整这些参数。

逻辑回归:看似有缺陷

原文:towardsdatascience.com/logistic-regression-deceptively-flawed-2c3e7f77eac9?source=collection_archive---------15-----------------------#2023-05-23

大的赔率比和完美分离的数据什么时候会让你吃亏?

Igor ŠegotaTowards Data Science Igor Šegota

·

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

--

照片由Alvan Nee拍摄,发布在Unsplash

这是上一篇关于逻辑回归概念理解的文章的第二部分:

## 逻辑回归:对决与概念理解

对数损失和完美分离的数据与冰球棒有什么关系?

towardsdatascience.com

上次我们可视化并解释了逻辑回归中的对数损失拟合。我们还展示了这个过程无法完美拟合完全分离的数据。换句话说,与普通最小二乘法的线性回归不同,逻辑回归实际上在数据稍微有些噪声时效果更好!

实际上,这是否重要?这要看情况:

  • 如果我们的目标是将输出用于统计推断,这就很重要。例如,准确估计模型系数、计算置信区间并使用 p 值测试假设。

  • 如果我们的目标是使用逻辑模型的输出创建一个预测分类模型,那么这并没有太大关系。

统计推断:statsmodels

示例数据

在这一部分,我们将使用 Python 的 statsmodels 库。请注意,statsmodels 和 scikit-learn(稍后使用)使用 β 来参数化概率,而不是 kx₀

其中 kx₀β₁β₀ 之间的关系是:

我们将继续使用我们在 逻辑回归第一部分 中生成的数据集,首先使用“非完美”数据 sample_df ,使用 statsmodels 的公式 API:

import statsmodels.formula.api as smf
model = smf.logit('y ~ x', df).fit()
model.summary()

#> Optimization terminated successfully.
#>         Current function value: 0.230366
#>         Iterations 8

我们的模型参数是 k = 3x₀ = 2.5,因此它们对应于 β₁ = 3β₀ = -7.5。我们可以通过从底部表格的 coef 列中读取这些参数,与拟合的参数进行比较:

我们的数据点非常少,而且种子是故意选择的,以展示离群值,因此拟合略有偏差,但仍在合理范围内。这里报告的总对数损失为“对数似然”,即总对数损失的负值,等于 -6.911。

完全分离的数据

当我们在 statsmodels 中运行我们完全分离的数据集时,会发生什么?答案是,这要看情况!

perfect_sep_model = smf.logit('y ~ x', perfect_sep_data).fit()

#> ...
#> PerfectSeparationError: Perfect separation detected, results not available

在这种情况下我们得到了一个错误——没有输出结果。在其他完美分离的情况下,我们可能会得到一个警告。例如,如果我们使用相同的参数但不同的随机种子:

perfect2_df = create_sample_data(k=3, x0=2.5, n_points=30, seed=154)
perfect_sep2_df, perfect_sep_logloss2_df = fit_data_to_logloss(
    perfect2_df,
    k=3,
    x0=2.5
)

perfect_sep_model_2 = smf.logit('y ~ x', perfect_sep2_df).fit()
perfect_sep_model_2.summary()

#> Warning: Maximum number of iterations has been exceeded.
#>         Current function value: 0.000000
#>         Iterations: 35

#> /.../statsmodels/base/model.py:604: ConvergenceWarning: 
#>   Maximum Likelihood optimization failed to converge. Check mle_retvals

由于第二个模型也没有收敛,我们可以认为它可能也应该返回一个错误,而不是一个无害的警告。R 中的逻辑回归函数 glm(..., family=binomial) 也是这样。引用 R Inferno 的第五圈,一致性:

警告的问题在于,没有人会阅读它们。人们必须阅读错误信息,因为在按下按钮后没有食物颗粒掉入托盘中。而警告只会使机器发出警报,但他们仍然会得到食物颗粒。即使它可能是毒药也无所谓。

因此,在使用效应大小和 p 值进行推断时要小心!在这种情况下,数据是完全可分的——我们有一个完美的预测器——而报告的 p 值(P > |z|)为 0.998。忽视或误解这些警告可能会使你错过数据中的一些明显特征。

另外,考虑使用其他模型。逻辑回归之外还有一个崭新的世界!🌍

预测:scikit-learn

样本数据

scikit-learn 自动化和抽象了许多计算,同时提供了一种简单而一致的方式来运行许多不同的模型。然而,在这样做时,它隐藏了许多在后台发生的事情。当后台发生的事情不明显时,从 scikit-learn 文档中学习数据科学概念可能会导致误解。

其中一个误解是,当我们从 sklearn.linear_model 运行 LogisticRegression() 并使用 .predict() 方法时,它看起来像我们在运行一个分类模型:

from sklearn.linear_model import LogisticRegression

def sklearn_logistic_fit(df):
    sklearn_logistic = LogisticRegression(penalty=None).fit(
        df['x'].values.reshape(-1, 1),
        df['y'].values
    )
    predicted_outcomes = sklearn_logistic.predict(
        df['x'].values.reshape(-1, 1)
    )

    print("beta_1 or log-odds-ratio: ", sklearn_logistic.coef_[0][0])
    print("beta_0 or y intercept: ", sklearn_logistic.intercept_[0])
    print("Original outcomes: ")
    print(df['y'].values)
    print("Predicted outcomes: ")
    print(predicted_outcomes)

    return predicted_outcomes

sklearn_logistic_fit(sample_df)

#> beta_1 or log-odds-ratio:  2.6187462367743803
#> beta_0 or y intercept:  -5.682896087072998
#> Original outcomes: 
#> [0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 1 1 1 1 1 1 1 1 1 1 1]
#> Predicted outcomes: 
#> [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1]

然而,我们可以通过调用其 .predict_proba() 方法或从模型对象中挖掘模型参数来验证这仍然是老旧的逻辑回归,我们在这里打印出来了。

在幕后,.predict() 根据两个概率中的较大者进行预测:p1-p。如果 p > 1-p,则预测为 1,否则预测为 0。它不会计算置信区间,也没有 p 值,也不会进行任何类型的统计推断或假设检验。它仍然是一个回归模型,只是在其上方有一个额外的阈值层。

注意:LogisticRegression() 默认使用 L2 正则化,它在对数损失函数中添加了一个额外的项。为了使其与 statsmodels 可比,使用 LogisticRegression(penalty=None)

完全可分的数据

忽略拟合系数的准确性的好处在于,即使系数不准确或完全错误,模型仍然可能足够好!正如谚语所说,“不要让完美成为优秀的敌人”。

scikit-learn 默认使用与 statsmodels 不同的拟合方法(称为 “lbfgs”)。在我非常有限的测试中,lbfgs 在完全分离的数据上没有出现错误。为了查看这里发生了什么,让我们运行之前用于 statsmodels 的两个相同数据集:

sklearn_logistic_fit(perfect_sep_df)

#> beta_1 or log-odds-ratio:  22.65352734156491
#> beta_0 or y intercept:  -53.25978472434583
#> Original outcomes: 
#> [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
#> Predicted outcomes: 
#> [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]

sklearn_logistic_fit(perfect_sep2_df)

#> beta_1 or log-odds-ratio:  180.11386979983104
#> beta_0 or y intercept:  -469.3714808877968
#> Original outcomes: 
#> [0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
#> Predicted outcomes: 
#> [0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]

该算法在某个点也会停止,给出有限的 β₁(或 k)值,没有错误,但仍然能够预测正确的结果。这些都是用于分类的完全可用的模型拟合!

对数几率和未解决的问题

还有一个我们没有深入讨论的话题。系数 k(或 β₁)乘以 x,有另一种解释——对数几率比。几率比描述了当 x 增加 1 时几率的乘法变化:

这个定义表明赔率比是概率比的比率。如果将 x 增加一个单位,使得 y = 1 的概率从 0.1(赔率 0.1 / 0.9 = 0.11)增加到 0.2(赔率 = 0.2 / 0.8 = 0.25),则赔率比为 0.25 / 0.1 = 2.27。

当概率很小时,大的赔率比并不反映大的绝对概率。如果将 x 增加一个单位,使得 y = 1:

  • p(x) = 0.0001(赔率 = 0.0001 / (1–0.0001) ≈ 0.0001)

  • p(x + 1) = 0.001(赔率 = 0.001 / (1–0.001) ≈ 0.001)

那么这给出了赔率比为 0.001/0.0001 = 10。即使赔率比为 10,最终的绝对概率仍然很小:0.001。好消息是当概率很小时,赔率比变得更容易解释——它大致等于两个概率 p(x + 1) / p(x) 的比率。

将此与另一个例子进行比较,其中概率大幅增加,例如从 p(x) = 0.5 增加到 p(x + 1) = 0.75,在这种情况下赔率比仅为 3!

因此,当 y = 1 的概率很小的时候,高赔率比需要谨慎使用。还有其他可替代的赔率比,例如 相对风险,这些方法更具可解释性,但计算可能不简单。

结论

在前两篇文章中,我们对逻辑回归进行了直观的解释,并展示了如何在 Python 的 statsmodels 和 scikit-learn 库中运行回归模型。

关键要点:

  • 逻辑回归输出一个概率——因此它是一种回归算法。

  • 逻辑回归通常用于分类。例如,通过预测具有更大概率的结果——或者通过设置自定义概率阈值。

  • 参数通过数据之间的差异和两个交叉曲棍球棒对数损失曲线作为成本函数的差异来进行数值估计。

  • 当你对统计推断感兴趣(关注系数的准确性、假设检验、p 值等)时,使用 statsmodels 进行逻辑回归。

  • 当你只是想预测结果或需要运行更大模型时,使用 scikit-learn 进行逻辑回归。

  • 完美可分的数据破坏了统计推断:最佳拟合系数不存在或“是无限的”,而 p 值将很大(因为置信区间很大)。如果我们只关心使用模型进行分类/预测,那么 scikit-learn 的实现对完美分离的数据效果更好。

  • 不要盲目相信大的(对数)赔率比。考虑计算额外的指标,如绝对概率。

希望这对你的数据科学之旅有所帮助!

逻辑回归:对决与概念理解

原文:towardsdatascience.com/logistic-regression-faceoff-67560de4f492?source=collection_archive---------5-----------------------#2023-05-18

逻辑损失和完美分离的数据与冰球棒有什么关系?

Igor ŠegotaTowards Data Science Igor Šegota

·

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

--

图片由 Jerry Yu 提供,来源于 Unsplash

谁下的这个订单?

截至撰写本文时,谷歌搜索“逻辑回归教程”显示大约有 1120 万条结果。为什么还要在这堆信息中再添加一份?

阅读了大量文章、书籍和指南后,我意识到大多数缺乏对逻辑回归工作原理的清晰直观解释。相反,它们通常要么致力于展示如何运行模型的实用性,要么尽可能地数学全面,因此基本概念被埋在矩阵代数的森林中。

我们将从澄清看似常见的误解开始。逻辑回归不是

  • 线性回归,但用 sigmoid 曲线代替直线

  • 分类算法(但可以用于此)

  • sigmoid 曲线“拟合”了 x-y 平面中分隔两类点的决策边界

什么是逻辑回归?

逻辑回归是一种回归模型,返回二元结果(0 或 1)的概率,假设赔率的对数是一个或多个输入的线性组合。赔率是结果发生的概率(* p)与结果不发生的概率(1-p*)之间的比率。当我们只有一个输入或预测变量时,这一初始假设数学上表示为:

逻辑回归的目标是建模当输入将结果概率从 0 逐渐转移到 1 的情况。结果为 1 的概率 * p* 可以从先前的方程中推导出来,并表示为输入的函数:

在最后一部分,我们从参数* β₁* 和* β₀* 转换为 * k* 和 * x₀。使用 * k 和 * x₀* 将使我们在继续过程中对模型有更清晰的了解。我们还将坚持使用单一预测变量x,而不是引入一大堆矩阵,这样我们可以更容易地可视化逻辑拟合。

逻辑曲线

我们将开始绘制逻辑曲线,参数为 * x₀ = 2.5* 和 * k = 3,在区间 * x 从 0 到 5:

import numpy as np
import pandas as pd
import plotnine as p9
from scipy.stats import uniform, bernoulli

# From https://github.com/igor-sb/blog/blob/main/posts/logistic/plots.py
from logistic.plots import plot_naive_logistic_fit

def logistic(x, k, x0):
    return 1 / (1 + np.exp(-k*(x - x0)))

def create_smooth_logistic_curve_data(k, x0, n_points=100):
    df = pd.DataFrame({'x': np.linspace(0, 5, n_points)})
    df['p_x'] = logistic(df['x'], k, x0)
    return df

def create_sample_data(k, x0, n_points, seed=1):
    np.random.seed(seed)
    df = pd.DataFrame({
        'x': uniform.rvs(loc=0, scale=5, size=n_points)
    }).sort_values('x', ignore_index=True)
    p_x = logistic(df['x'], k, x0)
    df['y'] = bernoulli.rvs(p_x)
    return df

sample_df = create_sample_data(k=3, x0=2.5, n_points=30)
smooth_px_df = create_smooth_logistic_curve_data(k=3, x0=2.5)

plot_naive_logistic_fit(sample_df, smooth_px_df)

点(红色表示 y=0,青绿色表示 y=1)和 p(x)(黑色)用于逻辑回归。

这个逻辑曲线p(x)由两个参数描述:

    • x₀是预测变量x的值,此时概率为 0.5(中点): p(x = x₀) = 0.5*,所以告诉我们中点的位置。
    • k* 与中点的概率斜率有关:(dp/dx){x = x₀} = k/4,因此告诉我们该中点处曲线的陡峭程度。* k* 越大,中间的曲线越陡。

如果我们天真地使用普通最小二乘法来拟合曲线p(x)到这些点上,我们会发现所有残差都小于 1,并且大多数在中点“错误一侧”的点的残差接近 1。将更大的成本分配给那些大的离群点会更有意义。

对数损失拟合

与其尝试让普通最小二乘法拟合p(x)到点上,逻辑回归的处理方式不同:

  • 对于* y = 1的青绿色点,我们将拟合 * -log p(x) 而不是 * p(x)。负对数使 * -log p(x) 随着 * p(x)* 接近零而逐渐增大。

  • 对于* y = 0的红色点,我们可以通过使用结果为零的概率来进行相同的处理, -log[1-p(x)]*。

我们称这些为“log-loss”。如果我们将所有点都折叠到y = 0,那么对于每个点,这两个 log-loss 代表该点的成本,因为它们与 log-loss 曲线有一定的差距。为了利用numpy的向量化,我们将这两个一起编码为单个 log-loss 函数(这种组合 log-loss 也被称为“交叉熵”)。

def log_loss(p_x, y):
    return -y * np.log(p_x) - (1 - y) * np.log(1 - p_x)

逻辑回归的一种理解方式是同时适合于y = 1-log p(x)y = 0-log[1-p(x)]

这两个 log-loss 曲线看起来如何?

为了可视化它们,我们将在前面的图中绘制相同的数据,但现在使用 log-loss 替代概率。

def create_smooth_logloss_data(k, x0, n_points=100):
    x = np.linspace(0, 5, n_points)
    p_x = logistic(x, k, x0)
    return pd.DataFrame({
        'x': np.concatenate((x, x)),
        'y': np.concatenate(([0] * len(x), [1] * len(x))),
        'log_loss': np.concatenate((log_loss(p_x, 0), log_loss(p_x, 1))),
    })

def add_logloss(df, k, x0):
    p_x = logistic(df['x'], k, x0)
    return df.assign(log_loss = log_loss(p_x, df['y']))

def fit_data_to_logloss(sample_df, k, x0):
    sample_fit_df = add_logloss(sample_df, k, x0)
    logloss_df = create_smooth_logloss_data(k, x0)
    return (sample_fit_df, logloss_df)
from logistic.plots import plot_logistic_fit

sample_fit_df, logloss_df = fit_data_to_logloss(sample_df, k=3, x0=2.5)

plot_logistic_fit(sample_fit_df, logloss_df)

我们将所有点都折叠到y = 0,但使用颜色作为y标签,因为 log-loss 值本身代表成本。红色点(y = 0)适合于红色曲线:-log[1-p(x)]。蓝绿色点(y = 1)适合于蓝绿色曲线:-log p(x)。垂直虚线的总和表示需要最小化的总 log-loss,适用于各种kx₀

与概率不同,log-loss 曲线具有按比例惩罚大离群值的特性,并且它们没有在 1 处截断的残差。

寻找最小的 log-loss

更改kx₀如何影响这种拟合?要回答这个问题,我们可以运行具有各种kx₀组合的拟合。

def fit_parameter_combinations(sample_df, combinations):
    sample_df_list = []
    logloss_df_list = []
    for k, x0 in combinations:
        sample_fit_df, logloss_df = fit_data_to_logloss(sample_df, k, x0)
        sample_fit_df['k'] = logloss_df['k'] = k
        sample_fit_df['x0'] = logloss_df['x0'] = x0
        sample_df_list.append(sample_fit_df)
        logloss_df_list.append(logloss_df)

    return (
        pd.concat(sample_df_list, ignore_index=True),
        pd.concat(logloss_df_list, ignore_index=True)
    )

更改x₀会使交点向侧面移动:

# From https://github.com/igor-sb/blog/blob/main/posts/logistic/plots.py
from logistic.plots import plot_logistic_fit_panel

x0_dfs, x0_logloss_dfs = fit_parameter_combinations(
    sample_df,
    [(3, 1.5), (3, 2.5), (3, 3.5)]
)
plot_logistic_fit_panel(x0_dfs, x0_logloss_dfs, '~x0')

如果x₀选择离最优点较远,则 log-loss 会增加,因为越来越多的点适合于 hockey stick 曲线的上升部分。

更改k会影响 log-loss 曲线的陡度(注意不同的 y 轴):

k_dfs, k_logloss_dfs = fit_parameter_combinations(
    sample_df,
    [(0.5, 2.5), (3, 2.5), (7, 2.5)]
)
plot_logistic_fit_panel(k_dfs, k_logloss_dfs, '~k')

如果k太低(0.5),大多数点会对总 log-loss 增加一些小但显著的贡献。如果k太高(7.0),只有“错误方向”的点会对总 log-loss 有显著贡献。在这种情况下,是在x₀ = 2.5中点左侧的两个蓝绿色点。

这带来一个问题:如果中点没有“错误方向”的点,例如数据完全分离时会发生什么?

完全分离的数据

结果表明,逻辑模型无法拟合完全分离的数据!😮 我们可以应用我们早前学到的有关拟合 log-loss 的知识来理解为什么。我们首先创建完全分离的数据(使用k = 3x₀ = 2.5):

perfect_df = create_sample_data(k=3, x0=2.5, n_points=30, seed=112)

perfect_sep_df, perfect_sep_logloss_df = fit_data_to_logloss(
    perfect_df,
    k=3,
    x0=2.5
)

print("Total log-loss: ", perfect_sep_df['log_loss'].sum())

plot_logistic_fit(perfect_sep_df, perfect_sep_logloss_df)

#> Total log-loss:  1.4776889859527733

记得改变k如何影响这些 hockey stick log-loss 曲线吗?当我们增加k时,总 log-loss 的主要贡献来自于中点“错误方向”的点。现在,所有点都在中点的“正确方向”。

这意味着我们可以通过持续增加k来创建任意好的适合度。这里是我们设置k = 13时适合度的样子:

perfect_sep_df, perfect_sep_logloss_df = fit_data_to_logloss(
    perfect_df,
    k=13,
    x0=2.5
)

print("Total log-loss: ", perfect_sep_df['log_loss'].sum())

plot_logistic_fit(perfect_sep_df, perfect_sep_logloss_df)

#> Total log-loss:  0.050774866143719344

完美分离的数据需要对数损失(log-losses)具有 90 度角,并且拟合概率在中点处具有无限斜率。因此,没有参数k使总对数损失达到最小值。在实际中,数值算法在若干步骤后停止,可能会返回最后一步的k值或一个错误。

待续

这篇文章涵盖了逻辑回归概念上的主要部分。在下一部分中,我们将探讨参数k作为对数赔率比的有些不直观的含义,并展示如何在 Python 库statsmodelsscikit-learn中运行和破解逻辑模型。

逻辑回归:表面无缺陷的陷阱

何时大赔率比和完美分离的数据会给你带来麻烦?

towardsdatascience.com

Loguru: 简单如打印,灵活如日志记录

原文:towardsdatascience.com/loguru-simple-as-print-flexible-as-logging-c964467f64aa

适用于你的数据科学项目的简单日志记录解决方案

Khuyen TranTowards Data Science Khuyen Tran

·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 7 月 17 日

--

作者提供的图像

最初发布于 https://mathdatasimplified.com 2023 年 7 月 17 日。

为什么在数据科学项目中使用日志记录?

数据科学家通常使用打印函数来调试代码。然而,随着打印语句数量的增加,由于缺乏行号或函数名称,难以确定输出来自哪里。

def encode_data(data: list):
    print("Encode data")
    data_map = {'a': 1, 'b': 2, 'c': 3}
    print(f"Data map: {data_map}")
    return [data_map[num] for num in data]

def add_one(data: list):
    print("Add one")
    return [num + 1 for num in data]

def process_data(data: list):
    print("Process data")
    data = encode_data(data)
    print(f"Encoded data: {data}")
    data = add_one(data)
    print(f"Added one: {data}")

process_data(['a', 'a', 'c'])

输出:

Process data
Encode data
Data map: {'a': 1, 'b': 2, 'c': 3}
Encoded data: [1, 1, 3]
Add one
Added one: [2, 2, 4]

当将代码投入生产时,手动检查并删除所有调试行可能是一个乏味且容易出错的任务。

def encode_data(data: list):
    print("Encode data")
    data_map = {'a': 1, 'b': 2, 'c': 3}
    return [data_map[num] for num in data]

def add_one(data: list):
    print("Add one")
    return [num + 1 for num in data]

def process_data(data: list):
    print("Process data")
    data = encode_data(data)
    data = add_one(data)

process_data(['a', 'a', 'c'])

日志记录通过允许数据科学家为其输出指定不同的级别(调试、信息、警告、错误)提供了完美的解决方案。

def encode_data(data: list):
    logger.info("Encode data")
    data_map = {'a': 1, 'b': 2, 'c': 3}
    logger.debug(f"Data map: {data_map}")
    return [data_map[num] for num in data]

def add_one(data: list):
    logger.info("Add one")
    return [num + 1 for num in data]

def process_data(data: list):
    logger.info("Process data")
    data = encode_data(data)
    logger.debug(f"Encoded data: {data}")
    data = add_one(data)
    logger.debug(f"Added one: {data}")

process_data(['a', 'a', 'c'])

输出:

2023-07-24 09:45:15 | INFO | logging_example:process_data:22 - Process data
2023-07-24 09:45:15 | INFO | logging_example:encode_data:12 - Encode data
2023-07-24 09:45:15 | DEBUG | logging_example:encode_data:14 - Data map: {'a': 1, 'b': 2, 'c': 3}
2023-07-24 09:45:15 | DEBUG | logging_example:process_data:24 - Encoded data: [1, 1, 3]
2023-07-24 09:45:15 | INFO | logging_example:add_one:18 - Add one
2023-07-24 09:45:15 | DEBUG | logging_example:process_data:26 - Added one: [2, 2, 4]

通过设置适当的日志级别,数据科学家可以根据需要选择性地启用或禁用某些类型的输出。在生产环境中,他们可以将日志级别设置为“INFO”或更高,以排除调试日志,从而使日志简洁且相关。

22023-07-24 09:40:05 | INFO | logging_example:process_data:22 - Process data
2023-07-24 09:40:05 | INFO | logging_example:encode_data:12 - Encode data
2023-07-24 09:40:05 | INFO | logging_example:add_one:18 - Add one

由于日志包含额外的信息,例如时间戳、函数名称和行号,数据科学家可以快速确定日志消息的来源。

数据科学家还可以将日志输出重定向到文件中,以查看以前运行的日志。

# example.log

2023-07-16 09:50:24 | INFO     | logging_example:main:17 - This is an info message
2023-07-16 09:50:24 | WARNING  | logging_example:main:18 - This is a warning message
2023-07-16 09:50:24 | ERROR    | logging_example:main:19 - This is an error message
2023-07-16 09:55:37 | INFO     | logging_example:main:17 - This is an info message
2023-07-16 09:55:37 | WARNING  | logging_example:main:18 - This is a warning message
2023-07-16 09:55:37 | ERROR    | logging_example:main:19 - This is an error message

为什么许多数据科学家不使用日志记录?

许多数据科学家仍然倾向于使用打印语句而非日志记录,因为打印更简单且不需要过多的设置。对于小脚本和一次性任务,设置日志框架的开销似乎是不必要的。

import logging

# Require initial set up
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s"
    datefmt="%Y-%m-%d %H:%M:%S",
)

logger = logging.getLogger(__name__)

def main():
    logger.debug("This is a debug message")
    logger.info("This is an info message")
    logger.warning("This is a warning message")
    logger.error("This is an error message")

if __name__ == "__main__":
    main()

如果有一个库可以让你利用日志记录的强大功能,同时使体验像打印一样简单,那该多好。

这时,开源 Python 库 Loguru 就显得很方便了。本文将展示一些 Loguru 的功能,使其成为标准日志库的一个很好的替代选择。

可以在这里随意使用和分叉本文的源代码:

[## Data-science/productive_tools/logging_tools at master · khuyentran1401/Data-science

有用的数据科学主题集合,包括文章、视频和代码……

github.com](https://github.com/khuyentran1401/Data-science/tree/master/productive_tools/logging_tools?source=post_page-----c964467f64aa--------------------------------)

优雅的开箱即用功能

默认情况下,日志库提供的日志很无聊且不太有用:

import logging

logger = logging.getLogger(__name__)

def main():
    logger.debug("This is a debug message")
    logger.info("This is an info message")
    logger.warning("This is a warning message")
    logger.error("This is an error message")

if __name__ == "__main__":
    main()

输出:

WARNING:root:This is a warning message
ERROR:root:This is an error message

相比之下,Loguru 默认生成信息丰富且生动的日志。

from loguru import logger

def main():
    logger.debug("This is a debug message")
    logger.info("This is an info message")
    logger.warning("This is a warning message")
    logger.error("This is an error message")

if __name__ == "__main__":
    main()

作者提供的图片

格式化日志

格式化日志允许你向日志中添加有用的信息,如时间戳、日志级别、模块名称、函数名称和行号。

传统的日志方法使用 % 格式化,这不直观:

iimport logging

# Create a logger and set the logging level
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

logger = logging.getLogger(__name__)

def main():
    logger.debug("This is a debug message")
    logger.info("This is an info message")
    logger.warning("This is a warning message")
    logger.error("This is an error message")

输出:

2023-07-16 14:48:17 | INFO | logging_example:main:13 - This is an info message
2023-07-16 14:48:17 | WARNING | logging_example:main:14 - This is a warning message
2023-07-16 14:48:17 | ERROR | logging_example:main:15 - This is an error message

相比之下,Loguru 使用 {} 格式化,这更加可读和易于使用:

from loguru import logger

logger.add(
    sys.stdout,
    level="INFO",
    format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} - {message}",
)

将日志保存到文件

使用传统的日志模块将日志保存到文件并打印到终端需要两个额外的类 FileHandlerStreamHandler

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=[
        logging.FileHandler(filename="info.log", level=logging.INFO),
        logging.StreamHandler(level=logging.DEBUG),
    ],
)

logger = logging.getLogger(__name__)

def main():
    logging.debug("This is a debug message")
    logging.info("This is an info message")
    logging.warning("This is a warning message")
    logging.error("This is an error message")

if __name__ == "__main__":
    main()

然而,使用 Loguru,你只需 add 方法即可实现相同的功能。

from loguru import logger

logger.add(
    'info.log',
    format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} - {message}",
    level="INFO",
)

def main():
    logger.debug("This is a debug message")
    logger.info("This is an info message")
    logger.warning("This is a warning message")
    logger.error("This is an error message")

if __name__ == "__main__":
    main()

轮转日志

日志轮转通过定期创建新的日志文件并归档或删除旧日志文件,防止日志文件大小过大。

在日志库中,轮转日志需要一个额外的类 TimedRotatingFileHandler。以下代码每周切换到一个新的日志文件(when="WO", interval=1),并保留最多 4 周的日志文件(backupCount=4)。

import logging
from logging.handlers import TimedRotatingFileHandler

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# Create a formatter with the desired log format
formatter = logging.Formatter(
    "%(asctime)s | %(levelname)-8s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

file_handler = TimedRotatingFileHandler(
    filename="debug2.log", when="WO", interval=1, backupCount=4
)
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

def main():
    logger.debug("This is a debug message")
    logger.info("This is an info message")
    logger.warning("This is a warning message")
    logger.error("This is an error message")

if __name__ == "__main__":
    main()

在 Loguru 中,你可以通过将 rotationretention 参数添加到 add 方法来复制这种行为。指定这些参数的语法可读且易于使用。

from loguru import logger

logger.add("debug.log", level="INFO", rotation="1 week", retention="4 weeks")

def main():
    logger.debug("This is a debug message")
    logger.info("This is an info message")
    logger.warning("This is a warning message")
    logger.error("This is an error message")

if __name__ == "__main__":
    main()

过滤

日志中的过滤功能允许你根据特定标准选择性地控制哪些日志记录应该输出。

在日志库中,过滤日志需要创建一个自定义的日志过滤类。

import logging

logging.basicConfig(
    filename="hello.log",
    format="%(asctime)s | %(levelname)-8s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
    level=logging.INFO,
)

class CustomFilter(logging.Filter):
    def filter(self, record):
        return "Hello" in record.msg

# Create a custom logging filter
custom_filter = CustomFilter()

# Get the root logger and add the custom filter to it
logger = logging.getLogger()
logger.addFilter(custom_filter)

def main():
    logger.info("Hello World")
    logger.info("Bye World")

if __name__ == "__main__":
    main()

在 Loguru 中,你可以简单地使用 lambda 函数来过滤日志。

from loguru import logger

logger.add("hello.log", filter=lambda x: "Hello" in x["message"], level="INFO")

def main():
    logger.info("Hello World")
    logger.info("Bye World")

if __name__ == "__main__":
    main()

捕捉异常

传统的异常日志可能含糊不清,调试起来很有挑战:

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s | %(levelname)s | %(module)s:%(funcName)s:%(lineno)d - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

def division(a, b):
    return a / b

def nested(c):
    try:
        division(1, c)
    except ZeroDivisionError:
        logging.exception("ZeroDivisionError")

if __name__ == "__main__":
    nested(0)
Traceback (most recent call last):
  File "/Users/khuyentran/Data-science/productive_tools/logging_tools/catch_exceptions/logging_example.py", line 16, in nested
    division(1, c)
  File "/Users/khuyentran/Data-science/productive_tools/logging_tools/catch_exceptions/logging_example.py", line 11, in division
    return a / b
ZeroDivisionError: division by zero

上述显示的异常信息不太有帮助,因为它们没有提供触发异常的 c 的值的信息。

Loguru 通过显示整个堆栈跟踪,包括变量值,增强了错误识别:

from loguru import logger

def division(a, b):
    return a / b

def nested(c):
    try:
        division(1, c)
    except ZeroDivisionError:
        logger.exception("ZeroDivisionError")

if __name__ == "__main__":
    nested(0)

作者提供的图片

Loguru 的catch装饰器允许你捕获函数中的任何错误。这个装饰器还会识别错误发生的线程。

from loguru import logger

def division(a, b):
    return a / b

@logger.catch
def nested(c):
    division(1, c)

if __name__ == "__main__":
    nested(0)

作者提供的图像

但我不想在我的 Python 项目中增加更多依赖。

尽管将 Loguru 集成到你的项目中需要安装额外的库,但它非常轻量,占用的磁盘空间极少。

此外,它有助于减少样板代码,显著降低使用日志记录的摩擦,并使你的项目在长期维护中更为轻松。

我喜欢撰写有关数据科学概念的文章,并玩弄不同的数据科学工具。你可以通过以下方式随时了解我最新的文章:

透过窗户——利用新的 DAX 函数计算客户生命周期价值

原文:towardsdatascience.com/looking-through-the-window-calculating-customer-lifetime-value-with-new-dax-functions-9cce2d0699d

新的窗口函数是 DAX 语言迄今为止最重要的增强功能之一!学习如何利用它们来计算客户生命周期价值

尼古拉·伊利奇Towards Data Science 尼古拉·伊利奇

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

--

图片由 Pexels 上的 Pixabay 提供

在我之前作为 SQL 专业人员的“生涯”中,我广泛使用 T-SQL 窗口函数来完成各种分析任务。我在这篇文章中描述了其中一种可能的使用场景,但实际上有很多场景可以通过窗口函数快速直观地解决。

因此,当我转向 Power BI 时,我感到非常惊讶(更不用说失望了),因为没有 DAX 等效的 SQL 窗口函数。好吧,我们可以通过编写更复杂的 DAX 来解决在某些行上执行不同计算的挑战——但说实话,这常常是非常痛苦的经历。

所以,当 Power BI Desktop 2022 年 12 月更新宣布了一组全新的 DAX 函数——统称为窗口函数——这应该实现与 SQL 窗口函数相同的目标时,我感到非常兴奋。目前,有三个 DAX 窗口函数:OFFSETINDEXWINDOW

如果你想了解更多关于这些函数及其背后工作原理的信息,我强烈推荐阅读Jeffrey Wang 的这篇文章——这是深入了解 DAX 窗口函数的最佳起点。

如果你特别对 OFFSET 函数感兴趣,我鼓励你阅读这篇精彩的文章由我的朋友 Tom Martens 撰写,或者这篇文章由Štěpán Rešl 撰写。

在这篇文章中,我不会花太多时间解释窗口函数的细节,因为我想专注于解释如何利用这些函数来满足一个非常常见的业务需求——计算客户的终身价值。

客户终身价值

让我们从了解什么是客户终身价值开始。它是一个广泛的术语,可能有多种解释。在我们的案例中,我们想深入了解单个客户的行为——例如,他们下了多少订单,他们在我们的产品上花了多少钱,他们的订单之间隔了多少天,以及这些与所有客户的平均情况相比如何。最后,我们想知道我们的客户有多忠诚——即他们的第一次和最后一次订单之间隔了多少天。

那么,让我们从一个非常基本的场景开始。我正在分析 Adventure Works 数据集中的两个客户:Adam Young 和 Alexandra Jenkins。以下是他们的订单摘要和总销售额:

图片由作者提供

首先需要理解的概念是我们想将每个客户视为一个单独的“实体”——意味着,我们想为每个客户创建一个“窗口”,并分析该特定行集合的数字。在我们的案例中,我们将有两个“窗口”:

图片由作者提供

DAX 中的窗口函数可以以两种不同的方式工作——要么操作相对(REL)值,基于当前行(再次阅读 Jeffrey 的文章以了解如何确定当前行),要么操作绝对(ABS)值。在我的情况下,我总是希望我的窗口从分区的第一行开始(每个客户代表一个单独的分区),并在分区的最后一行结束。

所以,让我们创建一个度量来计算每个分区的订单数量和销售额的累积总和:

Window Quantity = CALCULATE([Total Quantity],
                   WINDOW(
                        1,ABS,
                        -1,ABS,
                        SUMMARIZE(ALLSELECTED(Sales),Customer[Customer],'Date'[Date]),
                        ORDERBY ('Date'[Date]),
                        KEEP,
                        PARTITIONBY(Customer[Customer])
                   )
                   )
Window Sales Amount = CALCULATE([Total Sales],
                   WINDOW(
                        1,ABS,
                        -1,ABS,
                        SUMMARIZE(ALLSELECTED(Sales),Customer[Customer],'Date'[Date]),
                        ORDERBY ('Date'[Date]),
                        KEEP,
                        PARTITIONBY(Customer[Customer])
                   )
                   )

让我们停下来解释一下这个度量定义。作为 CALCULATE 筛选器修饰符,我们将使用一个新的窗口 DAX 函数 WINDOW。第一个参数(1)确定窗口的开始位置。由于第二个参数是 ABS(绝对),这意味着窗口从分区的开始处开始。接下来,我们定义窗口的结束位置。

由于我们使用了负值(-1)和 ABS,这意味着窗口结束于分区的最后一行。之后,我们定义一个表表达式,从中返回输出行。最后,数据将在窗口内按日期排序(从最早的日期开始),并在客户上进行分区(每个客户是一个独立的“窗口”)。

作者提供的图片

现在,我们有了每个客户的累计总数!所以,我们可以例如计算每笔个别购买在整体中的百分比:

% of Window Quantity = DIVIDE([Total Quantity],[Window Quantity],0)
% of Window Sales = DIVIDE([Total Sales],[Window Sales Amount],0)

作者提供的图片

到目前为止,一切顺利!让我们做一些更酷的事情。首先,我将计算两个连续订单之间的天数。为此,另一种 DAX 窗口函数 OFFSET 将派上用场。实际上,我想抓取上一行的日期,并计算当前行日期与上一行日期之间的天数差:

Offset Date = CALCULATE(
                        MIN('Date'[Date]),
                        OFFSET(
                            -1,
                            SUMMARIZE(ALLSELECTED(Sales),Customer[Customer],'Date'[Date]),
                            ORDERBY('Date'[Date]),
                            KEEP,
                            PARTITIONBY(Customer[Customer])
                        )
                        )

作者提供的图片

这是计算订单之间天数的度量:

Days Between Orders = DATEDIFF([Offset Date],MIN('Date'[Date]),DAY)

作者提供的图片

好的,现在,让我们计算每个客户订单之间的平均天数:

AVG Days Between Orders = CALCULATE(AVERAGEX(
                  SUMMARIZE(
                            ALLSELECTED(Sales),Customer[Customer],'Date'[Date]),[Days Between Orders]),
                   WINDOW(
                        1,ABS,
                        -1,ABS,
                        SUMMARIZE(ALLSELECTED(Sales),Customer[Customer],'Date'[Date]),
                        ORDERBY ('Date'[Date]),
                        KEEP,
                        PARTITIONBY(Customer[Customer])
                   )
                   )

作者提供的图片

那么,我们目前可以得出什么结论?亚历山德拉平均每 22 天下单,而亚当平均需要 89 天才会下新订单。与整体相比如何?89 天的间隔是否过长?

那么,让我们将这些数字放入整个数据集的背景中。

作者提供的图片

虽然 89 天相比于亚历山德拉·詹金斯的 22 天看起来不太好,但我们可以得出结论,与所有客户的平均天数 287 天相比,这 89 天根本不算差!确实很有洞察力!

让我们通过计算总客户生命周期来总结一下。这是每个客户首次和最后一次订单之间的天数。所以,让我们在我们的“窗口”内计算第一次和最后一次订单的日期:

Window Min Date = CALCULATE(MIN('Date'[Date]),
                   WINDOW(
                        1,ABS,
                        -1,ABS,
                        SUMMARIZE(ALLSELECTED(Sales),Customer[Customer],'Date'[Date]),
                        ORDERBY ('Date'[Date]),
                        KEEP,
                        PARTITIONBY(Customer[Customer])
                   )
                   )
Window Max Date = CALCULATE(MAX('Date'[Date]),
                   WINDOW(
                        1,ABS,
                        -1,ABS,
                        SUMMARIZE(ALLSELECTED(Sales),Customer[Customer],'Date'[Date]),
                        ORDERBY ('Date'[Date]),
                        KEEP,
                        PARTITIONBY(Customer[Customer])
                   )
                   )

作者提供的图片

这是客户生命周期天数的计算:

Customer Lifetime Days = DATEDIFF([Window Min Date],[Window Max Date],DAY)

一旦我把它拖到表格中,就能看到它在每个窗口(每个客户单独计算)内被计算:

作者提供的图片

当然,这只是使用 DAX 窗口函数的一个基本示例。说实话,有无数的用例可以考虑——例如,我还可以在窗口内对单独的行进行排名(并快速识别每个客户的最高销售额)。

我还可以通过多个属性进行分区——例如,按客户和月份。这样,我们将有一个包含某个客户在单个月份内所有行的“窗口”。想象一下:亚当·杨——七月,亚当·杨——八月,亚历山德拉·詹金斯——七月,亚历山德拉·詹金斯——八月,等等。根据你的具体业务需求,“窗口”可以有不同的定义。

结论

窗口函数是 DAX 语言最重要的增强功能之一,这一点毫无疑问!一些之前需要编写复杂且冗长的 DAX 的业务用例,现在可以以更优雅、更优化的方式实现。正如 SQL 语言中,窗口函数是最强大的分析工具之一,DAX 窗口函数也将使许多 Power BI 开发任务更易于实现。

感谢阅读!

LoRA — 直观且详尽的解释

原文:towardsdatascience.com/lora-intuitively-and-exhaustively-explained-e944a6bff46b

自然语言处理 | 机器学习

探索现代机器学习的前沿,通过前沿微调

Daniel WarfieldTowards Data Science Daniel Warfield

·发布于 Towards Data Science ·18 分钟阅读·2023 年 11 月 7 日

--

“Lora The Tuner” 作者 Daniel Warfield 使用 MidJourney 制作。所有图片均为作者所用,除非另有说明。

微调是将机器学习模型调整到特定应用的过程,这在实现一致和高质量的性能中可能至关重要。在本文中,我们将讨论“低秩适应”(LoRA),这是最受欢迎的微调策略之一。首先我们将介绍理论,然后使用 LoRA 微调语言模型,提高其问答能力。

微调的结果。在微调之前,输出是乱码,模型不断重复问题和虚假的答案。微调后,输出清晰、简洁且准确。

这对谁有用? 任何对学习最新机器学习方法感兴趣的人。本文将重点关注语言建模,但 LoRA 在许多机器学习应用中是一个受欢迎的选择。

这篇文章有多先进? 本文应该对新手数据科学家和爱好者可读,但包含在高级应用中至关重要的主题。

前提条件: 虽然不是必需的,但对大型语言模型(LLMs)有一个扎实的工作理解可能会有帮助。欢迎参考我的关于变换器的文章,变换器是语言模型的一种常见形式,了解更多信息:

## 变换器 — 直观且详尽的解释

探索现代机器学习的前沿:逐步拆解变换器

[towardsdatascience.com

你也可能想了解什么是梯度。我还有一篇相关的文章:

什么是梯度?它们为什么会爆炸?

通过阅读这篇文章,你将对深度学习中最重要的概念有一个坚实的理解。

towardsdatascience.com

如果你对这两个主题都不太自信,你仍然可以从这篇文章中获益,但如果感到困惑,它们会存在。

什么是微调?为什么要微调?

随着机器学习技术的进步,对模型性能的期望也有所提高;这要求更复杂的机器学习方法来满足对更高性能的需求。在早期的机器学习中,构建一个模型并在一次训练中完成是可行的。

训练,最简单的定义就是:你拿一个未经训练的模型,给它数据,然后得到一个性能优越的模型。

这种策略仍然是解决简单问题的流行方法,但对于更复杂的问题,将训练分为“预训练”和“微调”两个部分可能更有用。一般思路是在大数据集上进行初始训练,然后在量身定制的数据集上对模型进行优化。

预训练和微调,是对典型单次训练策略的改进。

这种“预训练”然后“微调”的策略可以让数据科学家利用多种数据形式,并将大型预训练模型用于特定任务。因此,预训练然后微调是一种常见且极其强大的范式。然而,它也有一些困难,我们将在下一节讨论。

微调中的困难

微调的最基本形式是使用你用于预训练模型的完全相同的过程,然后在新数据上微调该模型。例如,你可能会在一个巨大的通用文本数据集上训练一个模型,然后使用相同的训练策略在一个更具体的数据集上进行微调。

在最简单的形式下,预训练和微调在程序上是相同的。你在一组数据上预训练模型,然后在另一组数据上微调。

这种策略可能很昂贵。LLM(大规模语言模型)绝对庞大,要使用这种策略进行微调,你需要足够的内存来存储不仅是整个模型,还要存储整个模型中每个参数的梯度(梯度是让模型知道如何调整其参数方向的东西)。参数和梯度都需要存储在 GPU 上,这就是为什么训练 LLM 需要如此多的 GPU 内存。

反向传播是训练机器学习模型时使用的策略。机器学习模型是“可微分的”,这意味着你可以计算“梯度”,梯度可以告诉你对某个参数的小变化会如何影响模型输出。我们生成预测,计算梯度,计算预测的错误程度,然后使用梯度来改善模型的参数。预训练和微调都采用反向传播,这需要计算模型中每个可学习参数的梯度。这意味着,如果你有一个 1000 亿参数的模型,你还需要存储 1000 亿个梯度。这一过程会重复进行,可能达到数十亿次,以训练模型。

除了存储梯度的问题,通常还会保存“检查点”,即在训练过程中模型在特定状态下的副本。这是一种很好的策略,可以在微调过程的不同阶段实验模型,但这意味着我们需要存储大量完整尺寸的模型副本。流行的现代 LLM Falcon 180B 需要大约 360GB 的存储空间。如果我们希望在微调过程中保存模型的十个检查点,这将消耗 3.6TB 的存储,这是一笔巨大的开支。也许更重要的是,保存如此大量的数据需要时间。数据通常需要从 GPU 转移到 RAM,然后再到存储器,这可能会显著延长微调过程的时间。

LoRA 可以帮助我们解决这些问题以及更多。减少 GPU 内存使用,缩小文件大小,加快微调时间,等等。从实际意义上讲,LoRA 通常被认为是传统微调方法的直接升级。我们将在接下来的部分详细介绍 LoRA 如何工作以及它如何实现如此显著的改进。

LoRA 简介

“低秩适配”(LoRA)是一种“参数高效微调”(PEFT)的形式,它允许使用少量可学习的参数来微调大型模型。LoRA 使用了一些概念,这些概念结合在一起,可以大幅度提高微调效果。

  1. 我们可以将微调视为学习对参数的更改,而不是直接调整参数本身。

  2. 我们可以尝试通过删除重复信息将这些更改压缩为更小的表示形式。

  3. 我们可以通过简单地将更改添加到预训练的参数中来“加载”我们的更改。

如果这让你感到困惑,不必担心;在接下来的部分中,我们将一步步讲解这些概念。

1) 微调作为参数变化

正如我们之前讨论的,最基本的微调方法包括迭代更新参数。就像正常的模型训练一样,你让模型进行推断,然后根据该推断的错误程度来更新模型的参数。

回顾之前讨论的反向传播图。这是微调的基本形式。

LoRA 对此有不同的看法。与其把微调看作是学习更好的参数,不如把微调看作是学习参数的变化。你可以将模型参数固定不变,学习使模型在微调任务上表现更好的参数变化。

这与训练的方式非常相似;你让模型进行推断,然后根据推断的错误程度进行更新。然而,不是更新模型参数,而是更新模型参数的变化。

在 LoRA 中,我们冻结模型参数,并创建一组新的值来描述这些参数的变化。然后,我们学习使模型在微调任务上表现更好的参数变化。

你可能会觉得这有点儿抽象。LoRA 的核心目的是使微调变得更小更快,如何通过增加更多的数据和额外的步骤来实现呢?在下一节中我们将详细讨论这一点。

2) 参数变化压缩

为了说明问题,许多人将密集网络表示为一系列加权连接。每个输入乘以某个权重,然后加在一起以产生输出。

密集网络的概念图,展示了一系列由权重连接的神经元。某个特定神经元的值将是所有输入乘以相应权重后的总和。

从概念上讲,这是一种完全准确的可视化,但实际上,这是通过矩阵乘法来实现的。一个值矩阵(称为权重矩阵)与输入向量相乘,生成输出向量。

矩阵乘法的概念图。来源

为了让你了解矩阵乘法的工作原理。在上面的例子中,红点等于 a₁₁•b₁₂ + a₁₂•b₂₂。正如你所见,这种乘法和加法的组合与神经元示例中的非常相似。如果我们创建正确形状的矩阵,矩阵乘法最终会与加权连接的概念完全一致。

将密集网络视为左侧的加权连接,右侧则视为矩阵乘法。在右侧的图示中,左侧的向量是输入,中心的矩阵是权重矩阵,右侧的向量是输出。为了可读性,只包含了一部分值。

从 LoRA 的角度来看,理解权重实际上是一个矩阵非常重要,因为矩阵具有某些属性,可以用来压缩信息。

矩阵属性 1) 线性独立性

你可以将矩阵(即二维值数组)视为向量的行或列。现在我们只需将矩阵视为向量的行。假设我们有一个由两个向量组成的矩阵,其大致如下所示:

一个由两个向量组成的矩阵,这些向量在矩阵中被表示为行。

每个向量指向不同的方向。你不能通过压缩和扩展一个向量来使其与另一个向量相等。

每一行作为一个矩阵,绘制为一个向量。无论蓝色向量如何被压缩或扩展,它都不会指向与红色向量相同的方向,反之亦然。

让我们加入第三个向量。

向量AB指向完全相同的方向,而向量C指向不同的方向。因此,无论你如何压缩或扩展AB,它们都无法用来描述C。因此,CAB是线性独立的。然而,你可以将A扩展为等于B,反之亦然,因此AB是线性相关的。

假设AB指向稍有不同的方向。

现在AB可以一起使用(经过一些压缩和扩展)来描述C,同样AB也可以被其他向量描述。在这种情况下,我们会说这些向量没有线性独立性,因为所有向量都可以用矩阵中的其他向量来描述。

使用AB来描述CB的大小可以通过乘以一个负数来翻转其大小,然后加到A上。

从概念上讲,线性独立的向量可以被视为包含不同的信息,而线性相关的向量则包含一些重复的信息。

矩阵属性 2) 秩

秩的概念是量化矩阵中的线性独立性。我将跳过详细的内容,直接进入要点:我们可以将矩阵分解为一些线性独立的向量;这种形式的矩阵称为“简化行最简形”。

一个矩阵(左)和同一个矩阵的简化行最简形式(右)。在 RREF 矩阵中,你可以看到有四个线性独立的向量(行)。这些向量可以组合使用来描述输入矩阵中的所有向量。

通过将矩阵分解成这种形式(我不会描述如何做,因为这只是对我们概念上有用),你可以计算出多少个线性独立的向量可以用来描述原始矩阵。线性独立向量的数量就是矩阵的“秩”。上述 RREF 矩阵的秩为四,因为有四个线性独立的向量。

我在这里插一句:无论你是否将矩阵视为向量的行还是列,秩始终是相同的。这是一个数学上的小细节,虽然不是特别重要,但对下一节有概念上的影响。

矩阵属性 3) 矩阵因子

因此,矩阵可以包含某种程度的“重复信息”,这种信息以线性依赖的形式存在。我们可以利用这一点,通过因式分解来表示一个大矩阵为两个较小矩阵的乘积。类似于如何将一个大数字表示为两个小数字的乘积,矩阵也可以被视为两个较小矩阵的乘积。

右侧的两个向量相乘等于左侧的矩阵。尽管它们的值相同,但左侧的向量占据了右侧矩阵的 40% 的大小。矩阵越大,因子就越能节省空间。

如果你有一个大矩阵,且具有显著的线性依赖(因此秩较低),你可以将该矩阵表示为两个相对较小矩阵的因子。这种因式分解的想法使得 LoRA 能够占用如此小的内存空间。

LoRA 背后的核心思想

LoRA 认为调整不是改变参数,而是学习参数的变化。使用 LoRA 时,我们不会直接学习参数变化;相反,我们学习参数变化矩阵的因子。

LoRA 的示意图,来自于 LoRA 论文。矩阵 A 和 B 经过训练以找到对预训练权重的最佳调整。我们将在未来的章节中讨论“r”。

学习变化矩阵因子的这一想法依赖于核心假设,即大型语言模型中的权重矩阵具有大量的线性依赖,因为参数数量远远超出理论所需的数量。过度参数化已被证明在预训练中是有益的(这就是现代机器学习模型如此庞大的原因)。LoRA 的理念是,一旦你通过预训练学习了通用任务,你可以用显著更少的信息进行微调。

过度参数化的模型实际上位于较低的内在维度。我们假设模型适应过程中的权重变化也具有低的“内在秩”,这导致我们提出了低秩适应(LoRA)方法。LoRA 允许我们通过优化密集层变化的秩分解矩阵来间接训练神经网络中的一些密集层,同时保持预训练权重冻结 — LoRA 论文

这导致训练的参数数量显著减少,从而使微调过程整体上更快且在存储和内存上更高效。

LoRA 的微调流程

现在我们了解了 LoRA 的各个部分如何工作,让我们将它们结合起来。

首先,我们冻结模型参数。我们将使用这些参数进行推理,但不会更新它们。

我们创建两个矩阵。这些矩阵的大小设置为,当它们相乘时,它们将与我们微调的模型的权重矩阵大小相同。在大型模型中,具有多个权重矩阵时,你将为每个权重矩阵创建一对这些矩阵。

LoRA 论文将这些矩阵称为矩阵“A”和“B”。这两个矩阵代表了 LoRA 微调过程中的可学习参数。

我们计算变化矩阵

然后我们将输入通过冻结的权重和变化矩阵。

我们根据两个输出的组合计算损失,然后根据损失更新矩阵 A 和 B

注意,虽然这里显示了变化矩阵以便于说明,但实际上它是即时计算的,从未存储,这就是为什么 LoRA 具有如此小的内存占用。实际上,仅在训练期间存储模型参数、矩阵 A 和 B,以及 A 和 B 的梯度。

我们执行这个操作,直到我们优化了用于微调任务的变化矩阵的因子。由于 A 和 B 显著较小,因此更新矩阵 A 和 B 的反向传播步骤比更新整个模型参数集的过程要快。这就是为什么尽管训练过程中的操作更多,LoRA 仍然通常比传统微调更快的原因。

当我们最终想要使用这个微调模型进行推理时,我们可以简单地计算变化矩阵,并将这些变化添加到权重中。这意味着 LoRA 不会改变模型的推理时间。

一个有趣的小提示,我们甚至可以将变化矩阵乘以一个缩放因子,从而控制变化矩阵对模型的影响程度。理论上,我们可以同时使用一点这种 LoRA 和一点那种 LoRA,这种方法在图像生成中很常见。

关于变换器的 LoRA 的说明

在研究这篇文章时,我发现了一个许多人没有讨论的概念性脱节。把机器学习模型当作一个大权重箱子来处理是可以的,但实际上许多模型具有复杂的结构,这种结构并不是非常“箱子型”的。我不太明白这个变化矩阵的概念如何确切地应用于像变换器这样的参数。

变换器的图示,我在另一篇文章中进行了讲解。符号“Nx”表示左侧和右侧的重复次数。这不是一个干净的权重方阵,因此不明显如何应用 LoRA。图像来源

根据我目前的理解,针对变换器,需牢记两点:

  1. 通常,变换器的多头自注意层(构建查询、键和值的层)中的密集网络只有一层深度。也就是说,只有一个输入层和一个通过权重连接的输出层。

  2. 这些浅层密集网络构成了变换器中大部分可学习参数,非常非常庞大。可能有超过 100,000 个输入神经元连接到 100,000 个输出神经元,这意味着描述这些网络的单个权重矩阵可能有 10B 个参数。所以,尽管这些网络可能只有一层深度,但它们却极其宽广,因此描述它们的权重矩阵也非常庞大。

从 LoRA 在变换器模型中的角度来看,这些是主要的优化参数;你在学习这些在模型内部存在的巨大但浅层的密集层的因子化变化。正如之前讨论的,每个浅层密集层都有可以表示为矩阵的权重。

关于 LoRA 秩的说明

LoRA 有一个名为r的超参数,它描述了用于构建前面讨论的变化矩阵的AB矩阵的深度。较高的r值意味着AB矩阵更大,这意味着它们可以在变化矩阵中编码更多线性独立的信息。

LoRA 的图示,来自LoRA 论文。参数“r”可以被视为一个“信息瓶颈”。较低的 r 值意味着 A 和 B 可以用较小的内存占用编码较少的信息。较大的 r 值意味着 A 和 B 可以编码更多的信息,但需要较大的内存占用。

一个 r 值等于 1 和 2 的 LoRA 概念图。在这两个示例中,分解后的 A 和 B 矩阵会导致相同大小的变化矩阵,但 r=2 能够将更多线性独立的信息编码到变化矩阵中,因为 A 和 B 矩阵中包含更多信息。

事实证明,LoRA 论文中提出的核心假设,即模型参数的变化具有较低的隐含秩,是一个相当强的假设。微软的团队(LoRA 的出版者)尝试了一些 r 值,发现即使 AB 矩阵的秩为 1,也表现得相当好。

来源于 LoRA 论文

通常,在选择 r 时,我听到的建议是:当数据与预训练中使用的数据类似时,低 r 值可能足够。当在非常新的任务上进行微调,这可能需要模型内部的 substantial logical changes 时,可能需要高 r 值。

Python 中的 LoRA

考虑到我们讨论了很多理论,你可能期望一个相当长的教程,但我有好消息!HuggingFace 有一个模块可以让 LoRA 变得非常简单。

在这个示例中,我们将对一个预训练的模型进行问答微调。让我们直接开始。完整代码可以在这里找到:

[## MLWritingAndResearch/LoRA.ipynb 在主分支 · DanielWarfield1/MLWritingAndResearch]

机器学习写作和研究中使用的笔记本示例 - MLWritingAndResearch/LoRA.ipynb 在主分支 ·…

github.com](https://github.com/DanielWarfield1/MLWritingAndResearch/blob/main/LoRA.ipynb?source=post_page-----e944a6bff46b--------------------------------)

1) 下载依赖项

我们将使用一些超出简单 PyTorch 项目的模块。这些模块的作用如下:

  • bitsandbytes: 用于使用较小的数据类型表示模型,从而节省内存。

  • datasets: 用于下载数据集

  • accelerate: 一些模块所需的机器学习互操作性依赖项。

  • loralib: LoRA 实现

  • peft: 一般的“参数高效微调”模块,我们的 LoRA 接口。

  • transformers: 用于从 huggingface 下载和使用预训练的 transformers。

!pip install -q bitsandbytes datasets accelerate loralib
!pip install -q git+https://github.com/huggingface/peft.git git+https://github.com/huggingface/transformers.git

2) 加载预训练模型

我们将使用 BLOOM,这是一个开源且许可宽松的语言模型。我们将使用 5.6 亿参数版本以节省内存,但你也可以将此策略应用于 BLOOM 的更大版本。

"""Importing dependencies and downloading pre-trained bloom model
"""

import torch
import torch.nn as nn
import bitsandbytes as bnb
from transformers import AutoTokenizer, AutoConfig, AutoModelForCausalLM

#loading model
model = AutoModelForCausalLM.from_pretrained(
    # "bigscience/bloom-3b",
    # "bigscience/bloom-1b1",
    "bigscience/bloom-560m",
    torch_dtype=torch.float16,
    device_map='auto',
)

#loading tokenizer for this model (which turns text into an input for the model)
tokenizer = AutoTokenizer.from_pretrained("bigscience/tokenizer")

3) 设置 LoRA

使用以下参数配置 LoRA:

  • r: A 和 B 矩阵的秩

  • lora_alpha: 这是一个相当有争议的参数。很多人对它有很多想法。你可以将它视为一个缩放因子,根据我的理解,它的默认值应该等于 r

  • 目标模块(target_modules): 我们希望用 LoRA 优化的模型部分。BLOOM 模块有名为 query_key_value 的参数,我们希望优化这些参数。

  • lora_dropout: dropout 是一种技术,通过隐藏输入来抑制模型的过拟合(称为正则化)。这是一个被隐藏的概率。

  • 偏置(bias): 神经网络通常每个连接有两个参数,一个是“权重”(weight),另一个是“偏置”(bias)。在这个示例中,我们只训练权重。

  • 任务类型(task_type): 并不是非常必要,主要用于超类 PeftConfig 中。设置为 CAUSAL_LM 是因为我们使用的特定语言模型是“因果型”。

"""Setting up LoRA using parameter efficient fine tuning
"""

from peft import LoraConfig, get_peft_model

#defining how LoRA will work in this particular example
config = LoraConfig(
    r=8,
    lora_alpha=8,
    target_modules=["query_key_value"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

#this actually overwrites the model in memory, so
#the rename is only for ledgibility.
peft_model = get_peft_model(model, config)

4) 检查内存节省

LoRA 的一个重要概念是训练包含的训练参数显著减少,这意味着在内存消耗方面有很大的节省。让我们看看在这个特定示例中我们节省了多少。

"""Comparing parameters before and after LoRA
"""

trainable_params = 0
all_param = 0

#iterating over all parameters
for _, param in peft_model.named_parameters():
    #adding parameters to total
    all_param += param.numel()
    #adding parameters to trainable if they require a graident
    if param.requires_grad:
        trainable_params += param.numel()

#printing results
print(f"trainable params: {trainable_params}")
print(f"all params: {all_param}")
print(f"trainable: {100 * trainable_params / all_param:.2f}%")

比较 LoRA 中可训练参数与原始模型参数的结果。在这个示例中,我们只训练了不到千分之一的数据。

5) 加载微调数据集

我们将使用 SQUAD 数据集来提高我们语言模型在问答任务中的表现。斯坦福问答数据集(SQUAD)是一个高质量、常用且许可宽松的数据集。

"""Loading SQUAD dataset
"""

from datasets import load_dataset
qa_dataset = load_dataset("squad_v2")

6) 重新结构化数据

我们将对特定结构的数据进行语言模型的微调。模型将期望文本呈现这种一般形式:

**CONTEXT:**
{context}

**QUESTION:**
{question}

**ANSWER:**
{answer}</s>

我们将向模型提供上下文和问题,模型将被期望为我们提供答案。因此,我们将重新格式化 SQUAD 中的数据以符合这种格式。

"""Reformatting SQUAD to respect our defined structure
"""

#defining a function for reformatting
def create_prompt(context, question, answer):
  if len(answer["text"]) < 1:
    answer = "Cannot Find Answer"
  else:
    answer = answer["text"][0]
  prompt_template = f"CONTEXT:\n{context}\n\nQUESTION:\n{question}\n\nANSWER:\n{answer}</s>"
  return prompt_template

#applying the reformatting function to the entire dataset
mapped_qa_dataset = qa_dataset.map(lambda samples: tokenizer(create_prompt(samples['context'], samples['question'], samples['answers'])))

7) 使用 LoRA 在 SQUAD 上进行微调

这段代码大多被借用。在缺乏严格验证程序的情况下,最佳实践是直接复制成功的教程,或者更好的是,直接从文档中获取。如果你要为实际用例训练一个实际模型,你可能需要研究并优化这些参数。

"""Fine Tuning
This code is largly co-opted. In the absence of a rigid validation
procedure, the best practice is to just copy a successful tutorial or,
better yet, directly from the documentation.
"""

import transformers

trainer = transformers.Trainer(
    model=peft_model,
    train_dataset=mapped_qa_dataset["train"],
    args=transformers.TrainingArguments(
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,
        warmup_steps=100,
        max_steps=100,
        learning_rate=1e-3,
        fp16=True,
        logging_steps=1,
        output_dir='outputs',
    ),
    data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False)
)
peft_model.config.use_cache = False  # silence the warnings. Please re-enable for inference!
trainer.train()

损失(模型中的错误量)。在这个示例中,我们不需要非常仔细地查看损失,但它作为一个良好的度量指标。在这个示例中,我们训练了 100 步,虽然损失在步骤之间有一些随机变化,但损失通常在训练过程中下降,这是好的。

8) 检查 LoRA 大小

让我们保存我们的 LoRA 优化成果。

"""Saving the LoRA fine tuning locally
"""
model_id = "BLOOM-560m-LoRA"
peft_model.save_pretrained(model_id)

然后检查文件系统中文件的大小。

!ls -lh {model_id}

BLOOM 560m 模型,在其 float 16 数据类型下,总大小超过 1 GB。使用 LoRA,我们只需要保存分解后的矩阵,我们的检查点大小仅为 3 MB。这就像把整个游戏“植物大战僵尸”压缩成一张 iPhone 拍摄的图片一样。

9) 测试

好的,我们有一个 LoRA 精细调整后的模型,让我们问它几个问题。首先,我们定义一个辅助函数,该函数将接收上下文和问题,运行预测,并生成响应。

"""Helper Function for Comparing Results
"""

from IPython.display import display, Markdown

def make_inference(context, question):

    #turn the input into tokens
    batch = tokenizer(f"**CONTEXT:**\n{context}\n\n**QUESTION:**\n{question}\n\n**ANSWER:**\n", return_tensors='pt', return_token_type_ids=False)
    #move the tokens onto the GPU, for inference
    batch = batch.to(device='cuda')

    #make an inference with both the fine tuned model and the raw model
    with torch.cuda.amp.autocast():
        #I think inference time would be faster if these were applied,
        #but the fact that LoRA is not applied allows me to experiment
        #with before and after fine tuning simultaniously

        #raw model
        peft_model.disable_adapter_layers()
        output_tokens_raw = model.generate(**batch, max_new_tokens=200)

        #LoRA model
        peft_model.enable_adapter_layers()
        output_tokens_qa = peft_model.generate(**batch, max_new_tokens=200)

    #display results
    display(Markdown("# Raw Model\n"))
    display(Markdown((tokenizer.decode(output_tokens_raw[0], skip_special_tokens=True))))
    display(Markdown("\n# QA Model\n"))
    display(Markdown((tokenizer.decode(output_tokens_qa[0], skip_special_tokens=True))))

让我们看看一些示例,了解我们精细调整后的模型在回答问题方面表现如何:

示例 1)

context = "You are a monster, and you eat yellow legos."
question = "What is the best food?"

make_inference(context, question)

示例 2)

context = "you are a math wizard"
question = "what is 1+1 equal to?"

make_inference(context, question)

我们仅使用了一个 560M 参数的模型,因此它在基本推理方面表现不佳也不奇怪。问它 1+1 是什么可能有些勉强,但至少它失败得更优雅。

示例 3)

context = "Answer the riddle"
question = "What gets bigger the more you take away?"

make_inference(context, question)

再次强调,我们仅使用了一个 560M 参数的模型。尽管如此,经过精细调整的模型在回答问题时显得更加优雅。

结论

就这些!我们讨论了精细调整的概念,以及 LoRA 如何将精细调整视为学习参数的变化,而不是迭代地学习新参数。我们了解了线性独立性和秩,以及由于权重矩阵的低秩,变化矩阵如何通过小因子来表示。我们将这些知识结合在一起,逐步了解了 LoRA,然后使用 HuggingFace PEFT 模块在问答任务中实现了 LoRA。

关注以获取更多更新!

我描述了机器学习领域的论文和概念,重点是实用和直观的解释。

[## 每当 Daniel Warfield 发布新内容时获取邮件提醒

高质量的数据科学文章直达你的邮箱。每当 Daniel Warfield 发布新内容时获取邮件提醒。通过注册,你可以…

medium.com](https://medium.com/@danielwarfield1/subscribe?source=post_page-----e944a6bff46b--------------------------------)

从未预料到,总是受到赞赏。通过捐赠,你可以让我分配更多的时间和资源来制作更频繁、更高质量的文章。 链接

归属: 除非另有来源提供,否则本文档中的所有资源均由 Daniel Warfield 创建。你可以将此帖子中的任何资源用于非商业目的,只要你引用本文或 danielwarfield.dev,或者两者均引用。可根据请求提供明确的商业许可。

机器学习中的损失函数

原文:towardsdatascience.com/loss-functions-in-machine-learning-9977e810ac02

了解最常见的损失函数及其使用场景

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

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

--

图片由Brett Jordan提供,来源于Unsplash

损失函数在机器学习中扮演着重要角色,因为它们引导模型的学习过程并定义其目标。

存在大量的损失函数,选择合适的损失函数对于训练一个准确的模型至关重要。不同的损失函数选择可以导致不同的分类或回归模型。

在本文中,我们将讨论最常用的损失函数,它们的运作方式、优缺点以及每种函数的适用场景。

什么是损失函数?

回顾一下,在监督式机器学习问题中,我们给定了一个包含n个标签样本的训练集:D = {(x₁, y₁), (x₂, y₂), … , (xₙ, y)},其中xᵢ表示样本i特征yᵢ*表示该样本的标签。我们的目标是构建一个预测尽可能接近真实标签的模型。

损失函数度量模型对给定样本的预测误差,即模型预测值与该样本真实值之间的差异。它有两个参数:样本的真实标签 y 和模型的预测值 ŷ

在模型训练过程中,我们调整其参数,以最小化在给定训练样本上的损失函数。

请注意,损失函数计算每个样本的误差,而成本函数计算整个数据集的误差(尽管这两个术语有时可以互换使用)。

损失函数的期望属性

理想情况下,我们希望损失函数具有以下属性:

  • 损失函数应反映模型试图实现的目标。例如,在回归问题中,我们的目标是最小化预测值与目标值之间的差异,而在分类中,我们的目标是最小化误分类错误的数量。

  • 在所有地方都是连续和可导的。大多数优化算法,如梯度下降,需要损失函数是可导的。

  • 凸性。凸函数只有一个全局最小点,因此像梯度下降这样的优化方法可以保证返回全局最优解。在实践中,这种属性很难实现,大多数损失函数是非凸的(即,它们具有多个局部最小值)。

  • 对称,即,目标上方的误差应导致与目标下方相同的损失。

  • 计算快速

损失函数和最大似然

许多在机器学习中使用的损失函数可以从最大似然原则推导出来(有关最大似然的解释,请参见我的上一篇文章)。

在最大似然估计(MLE)中,我们试图找到一个具有参数θ的模型,该模型最大化给定模型的观察数据的概率:P(D|θ)。为了简化似然函数,我们通常取其对数,然后我们尝试最大化对数似然:log P(D|θ)。

因此,我们可以将给定样本(xy)的损失函数定义为给定我们模型预测的真实标签的负对数似然:

作为负对数似然的损失函数

因为负对数是一个单调递减函数,所以最大化似然等同于最小化损失。

请注意,要使用这种技术定义损失函数,我们需要假设数据集是从某种已知的概率分布生成的。

在接下来的部分中,我们将讨论在不同类型的问题(回归、二分类和多分类)中使用的最常见的损失函数。

回归问题

在回归问题中,目标标签和模型预测值都取连续值。回归问题中最常用的三种损失函数是:平方损失、绝对损失和 Huber 损失。

平方损失

平方损失定义为目标标签与其预测值之间的平方差:

平方损失

该损失函数用于普通最小二乘法(OLS),这是解决线性回归问题的最常见方法。

优点

  • 在所有地方都是连续和可导的

  • 凸性(只有一个全局最小值)

  • 易于计算

  • 在假设标签有高斯噪声的情况下,平方损失是给定数据下模型的负最大似然。你可以在我之前的文章中找到这个陈述的证明。

缺点

  • 由于误差的平方,对离群点敏感。少量距离其他样本较远的样本可能会导致模型发生较大变化(稍后会演示)。

绝对损失

绝对损失定义为真实标签和模型预测之间的绝对差异:

绝对损失

优点

  • 对离群点的影响不大

  • 计算简单

缺点

  • 在 0 处不可微分,这使得在梯度下降等优化方法中使用它变得困难。

  • 没有最大似然解释

Huber 损失

Huber 损失是平方损失和绝对损失的组合。对于小于预定义参数 δ 的损失值,它使用平方误差,对于大于 δ 的值,它使用绝对误差。

Huber 损失的数学定义是:

Huber 损失

δ 通常设置为 1。

Huber 损失在深度学习中常用于避免梯度爆炸问题,因为它对大错误不敏感。

优点

  • 在所有地方都连续且可微分

  • 比平方损失对离群点的敏感度低

缺点

  • 计算较慢

  • 需要调整超参数 δ

  • 没有最大似然解释

以下图表显示了三种回归损失函数:

回归问题的损失函数

Scikit-Learn 示例

SGDRegressor 类通过随机梯度下降(SGD)将线性回归模型拟合到给定的数据集。其 loss 参数可用于选择优化的损失函数。该参数的选项包括:

  • squared_error(平方损失)。这是默认选项。

  • huber(Huber 损失)

  • epsilon_intensive(在支持向量回归中使用的损失函数)

我们来检查使用 Huber 损失而非平方损失对包含离群点的样本数据集的影响。

我们首先定义我们的数据集:

x = np.array([0.5, 1.8, 2.4, 3.5, 4.2, 4.8, 5.8, 6.1, 7.2, 8.7, 10])
y = np.array([0.1, 0.2, 0.3, 0.4, 0.7, 1, 0.9, 1.2, 1.4, 1.8, 10])

我们来绘制数据点:

def plot_data(x, y):
    plt.scatter(x, y)
    plt.xlabel('$x$')
    plt.ylabel('$y$')
    plt.grid()
plot_data(x, y)

训练集

显然,点 (10, 10) 是一个离群点。

接下来,我们将两个 SGDRegressor 模型拟合到此数据集中:一个使用平方损失函数,另一个使用 Huber 损失。

from sklearn.linear_model import SGDRegressor

X = x.reshape(-1, 1) # Convert x to a matrix with one column

reg = SGDRegressor(loss='squared_error')
reg.fit(X, y)

reg2 = SGDRegressor(loss='huber')
reg2.fit(X, y)

我们来绘制这些模型找到的两条回归线:

def plot_regression_line(x, y, w0, w1, color, label):
    p_x = np.array([x.min(), x.max()])
    p_y = w0 + w1 * p_x
    plt.plot(p_x, p_y, color, label=label)
plot_data(x, y)
plot_regression_line(x, y, reg.intercept_, reg.coef_[0], 'r', label='Squared loss')
plot_regression_line(x, y, reg2.intercept_, reg2.coef_[0], 'm', label='Huber loss')
plt.legend()

平方损失和 Huber 损失找到的回归线

很明显,使用平方损失训练的模型比使用 Huber 损失训练的模型更受离群值的影响。

二元分类问题

在二元分类问题中,真实标签是二元的(1/0 或 1/-1)。模型的预测值可以是二元的(硬标签)或样本属于正类的概率估计(软标签)。

仅提供硬标签的分类模型的示例包括支持向量机(SVM)和K 近邻(KNN),而像逻辑回归神经网络(具有 sigmoid 输出)这样的模型也提供概率估计。

0–1 损失

最简单的损失函数是零一损失函数(也称为误分类错误):

零一损失

I 是指示函数,如果其输入为真则返回 1,否则返回 0。

对于每个分类器错误分类的样本(误分类),会产生 1 的损失,而正确分类的样本则导致 0 损失。

0–1 损失函数常用于评估分类器,但在优化指导中并不实用,因为它是不可微分且不连续的。

对数损失

对数损失(也称为逻辑损失二元交叉熵损失)用于训练提供类别概率估计的模型,如逻辑回归

设我们用p表示模型给出的样本属于正类的概率估计:

然后对数损失被定义为:

对数损失

我们是如何得到这个损失函数的?我们将再次使用最大似然原理。更具体地说,我们将展示对数损失是在标签假设为伯努利分布(一种二元随机变量的概率分布,取 1 的概率为p,取 0 的概率为 1 − p)下的负对数似然。数学上,可以写成如下形式:

证明:

设数据(标签)模型为伯努利分布,参数为p,样本属于正类的概率就是p,即:

同样,样本属于负类的概率是:

我们可以更紧凑地写出这两个方程:

解释:当 y = 1时,pʸ = p 且(1 − p)¹⁻ʸ= 1,因此 P(y|p) = p。类似地,当 y = 0 时,pʸ = 1且(1 − p)¹⁻ʸ = 1 − p,因此 P(y|p) = 1 − p。

因此数据的对数似然是:

对数损失正好是该函数的负数!

对数损失函数是可微的且凸的,即具有唯一的全局最小值。

铰链损失

铰链损失用于训练支持向量机(SVM),其目标是最大化分隔两个类别的区域的边界,同时最小化边界违例。

SVM

铰链损失定义如下:

铰链损失

注意这里的ŷ是分类器决策函数的原始输出,即ŷ = wx(SVM 不提供概率估计)。

yŷ具有相同的符号(即模型预测正确的类别)且|ŷ| ≥ 1 时,铰链损失为 0。这意味着正确分类且在边界之外的样本不会对损失产生影响(即使去掉这些样本,结果也会相同)。然而,对于边界内的样本(|ŷ| < 1),即使模型的预测是正确的,仍会有少量损失。当yŷ具有相反的符号时,铰链损失会随着ŷ的增加而线性增长。

支持向量机将在未来的文章中详细讨论。

下图显示了三种分类损失函数:

二分类问题的损失函数

对数损失和铰链损失都可以看作是对 0–1 损失的连续近似。

多类分类问题

在多类分类问题中,目标标签是k个类别中的 1 个。标签通常使用独热编码进行编码,即作为一个二进制k维向量y = (y₁, …, yₖ),其中yᵢ = 1 表示真实类别i,其余为 0。

概率分类器为每个样本输出一个k维向量,其中包含每个类别的概率估计:p = (p₁, …, pₖ)。这些概率的总和为 1,即p₁ + … + pₖ = 1。

交叉熵损失

用于训练这种分类器的损失函数称为交叉熵损失,它是对数损失在多类情况中的扩展。其定义如下:

交叉熵损失

例如,假设我们有一个三类问题,我们的样本的真实类别是类别 2(即y = [0, 1, 0]),我们的模型的预测是p = [0.3, 0.6, 0.1]。那么该样本引发的交叉熵损失是:

要了解交叉熵损失如何推广对数损失,请注意在二分类情况下p₁ = 1 - p₀和y₁ = 1 - y₀,因此我们得到:

这正是 p = p₀ 和 y = y₀ 时的对数损失。

类似于对数损失,我们可以证明交叉熵损失是模型对数似然的负值,前提是标签是从类别分布(伯努利分布到 k 个可能结果的推广)中抽样的。

证明

给定一个数据(标签)模型作为具有概率 p = (p₁, …, pₖ) 的类别分布,则给定样本属于类 i 的概率是 pᵢ

因此,样本的真实标签为y的概率是:

解释:如果给定样本的正确类别是 i,则 yᵢ = 1,对于所有 j ≠ i,yⱼ = 0。因此,P(y|p) = pᵢ,这就是样本属于类 i 的概率。

因此,我们模型的对数似然是:

交叉熵损失正是这个函数的负值!

关键要点

  • 在本文中,我们讨论了各种损失函数,并展示了它们如何从最大似然等基本原理推导出来。

  • 在回归问题中,平方损失是最常见的损失函数。然而,如果你怀疑数据集中存在异常值,使用 Huber 损失可能是更好的选择。

  • 在二分类问题中,选择不同的损失函数会导致不同的分类器(逻辑回归使用对数损失,而 SVM 使用铰链损失)。

  • 在多类分类问题中,交叉熵损失是最常见的损失函数,并且是对数损失在多类情况下的扩展。

最终说明

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

本文的代码示例可以在我的 GitHub 上找到:github.com/roiyeho/medium/tree/main/loss_functions

感谢阅读!

在 DALL-E 3 翻译中迷失

原文:towardsdatascience.com/lost-in-dall-e-3-translation-b85a3958b9d6

多语言生成 AI 图像会导致不同的结果

Yennie JunTowards Data Science Yennie Jun

·发表于Towards Data Science ·11 分钟阅读·2023 年 11 月 2 日

--

使用 DALL-E 3 在六种语言中生成的“一个人的图像”示例。图由作者创建。

本文最初发表于 artfish intelligence

介绍

OpenAI 最近推出了最新的 DALL-E 3,这是他们 AI 图像生成模型系列中的最新版本。

但正如近期媒体报道研究所揭示的,这些 AI 模型带有偏见和刻板印象。例如,AI 图像生成模型如 Stable Diffusion 和 Midjourney 倾向于放大关于种族、性别国家身份的现有刻板印象。

然而,这些研究大多数主要测试使用英文提示的模型。这就提出了一个问题:这些模型如何响应非英语提示?

在这篇文章中,我深入探讨了 DALL-E 3 在不同语言提示下的表现。借鉴我以前的工作的主题,我提供了对最新 AI 图像生成模型的多语言视角。

DALL-E 3 的工作原理:提示转换

与以前的 AI 图像生成模型不同,DALL-E 模型的最新版本并不会直接生成你输入的内容。相反,DALL-E 3 包含自动提示转换,这意味着它将你的原始提示转换成一个不同的、更具描述性的版本

从 OpenAI 的论文中得到的一个提示转换示例,详细说明了标题改进过程:通过更好的标题改善图像生成。图由作者创建。

根据DALL-E 3 系统卡,这样做有几个原因:

  • 改进标题以使其更具描述性

  • 移除公共人物名称

  • 指定更多生成人物的多样化描述(例如,在提示转换之前,生成的人物往往主要是白人、年轻和女性)。

因此,图像生成过程如下:

  1. 您将提示输入到 DALL-E 3(通过 ChatGPT Plus 可用)。

  2. 您的提示在后台被修改为四种不同的转化提示。

  3. DALL-E 3 根据每个转化提示生成图像。

添加这种提示转换对图像生成领域来说相当新。通过添加提示修改,AI 图像生成的机制在后台变得更加抽象,用户更难以理解。

多语言提示转换

大多数研究文本到图像 AI 模型中的偏差都集中在使用英语提示。然而,对于这些模型在非英语语言提示下的行为知之甚少。这样做可以揭示潜在的语言特定或文化特定行为。

我让 DALL-E 3 使用以下英语提示生成图像:

  • “一个男性的图像”

  • “一个女性的图像”

  • “一个人的图像”

我使用了 GPT-4(没有 DALL-E 3)将这些短语翻译成以下语言:韩语、普通话、缅甸语、亚美尼亚语和祖鲁语。

然后,我使用 DALL-E 3 为每种语言生成 20 张图像,总共在 6 种语言中每个提示生成 120 张图像。在从 ChatGPT Plus 保存生成的图像时,图像文件名会自动保存为转化提示的文本。在本文的其余部分,我将分析这些转化提示。

元数据提取

在我的提示中,我从未指定特定的文化、种族或年龄。然而,转化后的提示通常包含这些指示符。

一个提示转换的示例,标注了句子的哪个部分涉及艺术风格、年龄、种族和性别。图由作者创建。

从转化提示中,我提取了诸如艺术风格(“插图”)、年龄(“中年”)、种族(“非洲裔”)和性别标识(“女性”)等元数据。66%的转化提示包含了种族标记,58%包含了年龄标记。

观察 1:所有提示都被转换为英语。

无论原始提示是什么语言,修改后的提示总是被转换为英语。

一张展示了 ChatGPT Plus 的截图,展示了“一个人的图像”原始韩语提示被修改为四种不同英文提示转换的示例。图由作者创建。

我对这种行为感到惊讶——虽然我预计提示会被转换成更具描述性的提示,但我没有预料到会发生翻译成英语的情况。

大多数 AI 生成模型,如 Stable Diffusion 和 Midjourney,主要以英语进行训练和测试。一般来说,这些模型在从非英语提示生成图像时表现较差,导致一些用户将提示从其母语翻译成英语。然而,这样做有可能会失去母语的细微差别。

然而,据我所知,这些其他模型中没有一个会自动将所有提示翻译成英语。在背后添加这种额外的翻译步骤(而且,我相信,大多数用户并不知道,因为在使用工具时没有明确说明)使得已经不透明的工具变得更加神秘。

观察 2:原始提示的语言影响修改后的提示

提示转换步骤似乎还包括了关于原始提示语言的未指定元数据。

例如,当原始提示是缅甸语时,即使提示没有提及缅甸语言或缅甸人,提示转换通常会提到缅甸人

一个缅甸语提示的示例,内容为“一个男人的图像”,经过 DALL-E 3 转换成关于缅甸男人的描述性提示。图由作者创建。

这并非所有语言的情况,结果因语言而异。对于某些语言,转换后的提示更可能提到与该语言相关的族裔。例如,当原始提示是祖鲁语时,转换后的提示提到非洲人的频率超过 50%(相比之下,当原始提示是英语时,提到非洲人的频率接近 20%)。

DALL-E 3 生成的所有组合提示(一个人/男人/女人的图像)的族裔百分比,每种语言。图由作者创建。

我并不旨在对这种行为是否正确或错误做出价值判断,也没有规定应有的行为标准。不过,我发现 DALL-E 3 的行为在原始提示语言中变化如此之大是很有趣的。例如,当原始提示是韩语时,DALL-E 3 的提示转换中没有提到韩国人。同样,当原始提示是英语时,DALL-E 3 的提示转换中没有提到英国人。

观察 3:即便是中性提示,DALL-E 3 也会生成带有性别的提示

我将 DALL-E 3 的提示转换中的人物标识符名词映射到三个类别之一:女性、男性或中性:

  • woman, girl, lady → “女性”

  • man, boy, male doctor → “男性”

  • athlete, child, teenager, individual, person, people → “中性”

然后,我将原始提示(“人/男人/女人”)与转换提示(“中性/男性/女性”)进行了比较:

给定原始提示(“一个人的图像/男人/女人”),转换提示中包含性别个体的百分比。图由作者创建。

毫不意外,“一个男人的图像”原始提示结果大多数是男性标识符(女性情况也是如此)。然而,我发现当使用中性提示“一个人的图像”时,DALL-E 3 75%的时间会将提示转换为包含性别(例如女性、男性)的术语。 DALL-E 3 生成的转换提示中,女性个体略多(40%)于男性个体(35%)。不到四分之一的中性提示转化为提及中性个体的提示。

观察 4:女性通常被描述为年轻,而男性的年龄则更为多样

有时,DALL-E 3 会在修改后的提示中包含一个年龄组(年轻、中年或年长)来描述个体。

在提示中提到女性个体的情况下,年龄描述往往偏向年轻。 具体来说,35%的转换提示将女性个体描述为“年轻”,是将她们描述为“年长”(13%)的两倍多,也比“中年”(7.7%)的频率高出四倍多。这表明,如果提示中提到女性,她很可能也会被描述为年轻。

提及年龄组的转换提示数量,按提示中提到的个体性别分类。图由作者创建。

以下是一些提示转换的例子:

Illustration of a young woman of Burmese descent, wearing a fusion of modern and traditional attirePhoto of a young Asian woman with long black hair, wearing casual clothing, standing against a cityscape backgroundWatercolor painting of a young woman with long blonde braids, wearing a floral dress, sitting by a lakeside, sketching in her notebookOil painting of a young woman wearing a summer dress and wide-brimmed hat, sitting on a park bench with a book in her lap, surrounded by lush greenery

另一方面,提及男性个体的提示转换显示了更平衡的年龄分布。这可能表明,文化和社会观念持续认为女性的青春更具价值,而男性则被视为不论年龄都具吸引力和成功。

观察 5:个体年龄的变化取决于原始提示语言

年龄组的变化也取决于原始提示的语言。变换提示更有可能将某些语言(例如祖鲁语)描述为年轻,而其他语言(例如缅甸语)则较少如此。

变换提示中提到年龄组的数量,按原始提示语言分开(图像中的男性/女性/人)。图像由作者创作。

观察 6:艺术风格的变化取决于个体性别

我预计艺术风格(例如摄影、插图)会在年龄组、语言和个体性别之间随机分布。也就是说,我预计女性个体和男性个体的照片数量会相似。

然而,情况并非如此。实际上,女性个体的照片更多,而男性个体的插图更多。描述个体的艺术风格并没有在性别之间均匀分布,而是更偏爱某些性别。

变换提示中提到的每种艺术风格的数量,按提示中提到的个体性别分开。图像由作者创作。

观察 7:从年轻的亚洲女性到年长的非洲男性的陈词滥调重复

在我的实验中,提示变换中有 360 种独特的人口描述(例如年龄/种族/性别组合)。虽然许多组合仅出现了几次(如“年轻的缅甸女性”或“年长的欧洲男性”),但某些人口描述的出现频率较高。

一个常见的描述是“年长的非洲男性”,出现了 11 次。查看一些生成的图像可以看到,虽然面部表情、姿势、配饰和衣物相似,但还是有所不同。

一个子集图像的变换提示包含了“年长的非洲男性”这一短语。图像由作者创作。

更常见的描述是“年轻的亚洲女性”,出现了 23 次。再次地,许多面部表情、面部特征、姿势和衣物都是相似的,甚至几乎相同。

一个子集图像的变换提示包含了“年轻的亚洲女性”这一短语。图像由作者创作。

这一现象捕捉了充斥我们世界的偏见的本质。当我们观察韩国 K-Pop 明星中国偶像的面孔时,他们的面部结构有着惊人的相似性。这种缺乏变化 perpetuates a specific beauty standard, narrowing the range of accepted appearances.

同样,在 AI 生成的图像中,诸如“年长的非洲男性”和“年轻的亚洲女性”等人口描述的狭隘解读助长了有害的刻板印象。这些模型通过不断生成面部特征、表情和姿势缺乏多样性的图像,固化了对这些人群应有的外貌的有限和刻板的看法。这种现象特别令人担忧,因为它不仅反映了现有的偏见,还有可能加剧这些偏见,因为这些图像被社会接受和规范化。

但 DALL-E 3 与其他图像生成模型相比如何?

我使用另外两个流行的文本到图像 AI 工具:MidjourneyStable Diffusion XL,生成了 6 种语言的“一个人的图像”。

对于使用 Midjourney 生成的图像,非英语提示更可能生成风景图像而不是人类图像(尽管,公平地说,英语图像相当令人不安)。对于一些语言,如缅甸语和祖鲁语,生成的图像包含模糊(也许有些不准确)的文化表现或对原始提示语言的参考。

使用Midjourney生成的六种语言的“一个人的图像”。图形由作者创建。

在使用 Stable Diffusion XL 生成的图像中观察到了类似的模式。非英语提示更可能生成风景图像。亚美尼亚语提示只生成了看起来像地毯图案的图像。中文、缅甸语和祖鲁语的提示生成的图像对原始语言的参考模糊不清。(而且,再次强调,使用英语提示生成的图像相当令人不安)。

使用Stable Diffusion XL生成的六种语言的“一个人的图像”。我使用Playground AI来使用该模型。图形由作者创建。

从某种程度上说,DALL-E 3 的提示转换起到了人工引入更多变异性和多样性的作用。至少 DALL-E 3 在所有六种语言中一致地生成了人类形象,按照指示进行。

讨论与总结

自动提示词变换有其自身的考虑因素:它们可能会改变提示词的含义,可能带有固有的偏见,并且可能不总是符合个人用户的偏好。

—* DALL-E 3 系统卡片

在本文中,我探讨了 DALL-E 3 如何利用提示词变换来增强用户的原始提示。在此过程中,原始提示不仅被变得更加描述性,还被翻译成英语。可能会使用关于原始提示的附加元数据,例如其语言,来构建变换后的提示,尽管这只是推测,因为 DALL-E 3 系统卡片没有详细说明这一过程。

我对 DALL-E 3 的测试涵盖了六种不同的语言,但需要注意的是,这并不是对全球数百种语言的全面检查。然而,这是系统性探讨非英语语言中的 AI 图像生成工具的重要第一步,这是一个我尚未看到过多探讨的研究领域。

在通过 ChatGPT Plus 网络应用访问 DALL-E 3 时,提示词变换步骤对用户并不透明。这种缺乏清晰度进一步抽象了 AI 图像生成模型的工作原理,使得审视模型中编码的偏见和行为变得更加困难。

然而,与其他 AI 图像生成模型相比,DALL-E 3 在按照提示生成人物方面总体上 准确,在生成多种族面孔方面总体上 多样(由于提示词变换)。因此,尽管在某些种族类别的面部特征方面可能存在有限的多样性,但总体结果是生成图像的多样性(尽管是人为引起的)高于其他模型。

我以对 AI 文本到图像模型期望输出的开放性问题结束了本文。这些模型通常在大量互联网图像上训练,可能会不经意地延续社会偏见和刻板印象。随着这些模型的发展,我们必须考虑是否希望它们反映、放大或减轻这些偏见,特别是在生成人的图像或描绘社会文化机构、规范和概念时。认真思考这些图像的潜在规范化及其更广泛的影响是非常重要的。

注意:DALL-E 3 和 ChatGPT 都是定期演进的产品。即使我在一周前进行了实验,本文中的一些结果可能已经过时或无法再现。随着模型的持续训练和用户界面的不断更新,这种情况不可避免。虽然这是当前 AI 领域的常态,但在未来的研究中,对非英语语言的图像生成模型进行探讨的方法仍然适用。

如果你喜欢这篇文章,我鼓励你订阅我的通讯以支持我的工作,并阅读更多我的作品。谢谢!

低代码时间序列分析

原文:towardsdatascience.com/low-code-time-series-analysis-2d5d02b5474b

使用 Darts 来简化你的 Python 时间序列分析开发

Pier Paolo IppolitoTowards Data Science Pier Paolo Ippolito

·发布于 Towards Data Science ·6 分钟阅读·2023 年 3 月 8 日

--

图片由 Afif Ramdhasuma 提供,来自 Unsplash

介绍

时间序列预测是机器学习中的一个独特领域。实际上,处理时间序列时,序列中不同点之间存在固有的时间依赖性,因此不同的观察值彼此高度依赖。如果你对学习时间序列分析的基础知识感兴趣,可以在 我之前的文章 中找到更多细节。

在经典的分类和回归问题中,scikit-learn 能够提供我们可能需要的大多数工具,以获得良好的基线(例如数据预处理、低代码模型、评估指标等……),但在时间序列中情况却截然不同。多年来,许多专门的库已出现,以覆盖时间序列分析工作流中的一些关键步骤(例如 statsmodelsProphet、自定义回测等……),但直到 Darts 出现之前,无法在单一解决方案中涵盖所有内容。

演示

作为本文的一部分,我们将通过一个实际示例演示如何使用 Darts 来分析 Kaggle 上的德里每日气候时间序列数据集 [1]。本文中使用的所有代码(及更多内容!)都可以在 我的 GitHubKaggle 账户 上找到。

首先,我们需要确保环境中安装了 Darts。

pip install darts

数据预处理

到这一步,我们已经准备好导入必要的库和数据集(图 1)。为了便于分析,首先将日期列从字符串转换为日期时间,然后将其设置为数据框的索引。

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import darts
from darts.ad import QuantileDetector

df = pd.read_csv('DailyDelhiClimateTrain.csv')
df["date"] = pd.to_datetime(df["date"])
df = df.set_index('date')
df.head(5)

图 1: 德里每日气候时间序列数据集(图像由作者提供)。

清理数据集后,我们现在可以将其划分为训练集和测试集,并可视化时间序列(图 2)。在我们的分析中,我们暂时只关注平均温度。

ts = darts.TimeSeries.from_series(df.meantemp)
train, val = ts.split_before(0.75)
train.plot(label="Training Data")
val.plot(label="Validation Data")

图 2: 德里每日温度时间序列(图像由作者提供)。

异常检测

由于其特性,时间序列通常作为实时或流服务的一部分进行处理,这可能使它们更容易受到错误测量和异常值生成的影响。为了监控我们的时间序列可能存在的异常值,可以使用不同的异常检测技术。两种可能的方法是使用分位数或阈值。使用分位数时,我们决定将序列中最高和最低的百分比值标记为异常值,而使用阈值时,我们指定固定的参考水平,超过或低于该水平的值被标记为异常。

在下面的示例中,将低于 3% 和高于 97% 的值视为异常值,会导致总体超出分位数的值百分比为 5.8%(图 3)。

anomaly_detector = QuantileDetector(low_quantile=0.03, high_quantile=0.97)
anomalies = anomaly_detector.fit_detect(ts)

l = anomalies.pd_series().values
print("Percentage of values outside quantiles:", 
      round(sum(l)/len(l)*100, 3), "%")

idx = pd.date_range(min(ts.pd_series().index), max(ts.pd_series().index))
anomalies = ts.pd_series()[np.array(l,dtype=bool)].reindex(idx,
                                                         fill_value=np.nan)
normal = ts.pd_series()[~np.array(l,dtype=bool)].reindex(idx, 
                                                         fill_value=np.nan)

normal.plot(color="black", label="Normal")
anomalies.plot(color="red", label="Anomalies")

图 3: 分位数异常检测(图像由作者提供)。

基线模型

此时,我们准备深入分析我们的时间序列,并检查是否存在任何季节性模式。如预期的那样,并且在下面的代码片段中所示,序列似乎在统计上每年大致遵循类似的季节性模式。

for m in range(2, 370):
        seasonal, period = darts.utils.statistics.check_seasonality(train, 
                                              m=m, max_lag=400, alpha=0.05)
        if seasonal:
            print("Seasonality of order:", str(period))
Seasonality of order: 354
Seasonality of order: 356
Seasonality of order: 361

根据这些信息,我们可以训练第一个简单的基线模型,该模型仅考虑序列中的季节性模式而不考虑其他信息(图 4)。使用这种方法,结果的 MAPE(平均绝对百分比误差)为 11.35%。使用 MAPE 作为评估指标的两个主要优点是:

  • 使用绝对值,正负误差不会相互抵消。

  • 错误不依赖于因变量的缩放。

k = 361
naive_model = darts.models.NaiveSeasonal(K=k)
naive_model.fit(train)
naive_forecast = naive_model.predict(len(val))

print("MAPE: ", darts.metrics.mape(ts, naive_forecast))
ts.plot(label="Actual")
naive_forecast.plot(label="Naive Forecast (K=" + str(k) + ")")

图 4:基准模型预测(作者提供的图像)。

统计模型选择

现在提供了一个良好的基准模型,我们准备尝试一些更先进的技术(例如指数平滑,ARIMA,AutoARIMA,Prophet)。如果需要,许多额外的模型如:CatBoost,卡尔曼滤波器,随机森林,递归神经网络和时间卷积网络可作为 Darts 的一部分使用。

def model_check(model):
    model.fit(train)
    forecast = model.predict(len(val))
    print(str(model) + ", MAPE: ", darts.metrics.mape(ts, forecast))
    return model

exp_smoothing = model_check(darts.models.ExponentialSmoothing())
arima = model_check(darts.models.ARIMA())
auto_arima = model_check(darts.models.AutoARIMA())
prophet = model_check(darts.models.Prophet())
ExponentialSmoothing(), MAPE:  37.758
ARIMA(12, 1, 0), MAPE:  41.819
Auto-ARIMA, MAPE:  32.594
Prophet, MAPE:  9.794

根据上述结果,Prophet 似乎是迄今为止考虑的模型中最有前景的。无论如何,通过一些额外的工作,结果甚至可以通过超参数优化得到改善,特别是利用传统统计模型如 ARIMA 和指数平滑的业务领域知识。有关 ARIMA 工作原理及其不同超参数的更多细节可以在这里找到。

回测

为了进一步验证我们模型的优度,我们现在可以通过使用现有的历史数据进行测试(图 5)。在这种情况下,记录到的 MAPE 为 7.8%。

historical_fcast = prophet.historical_forecasts(ts,
                           start=0.6, forecast_horizon=30, verbose=True)

print("MAPE: ", darts.metrics.mape(ts, historical_fcast))
ts.plot(label="Actual")
historical_fcast.plot(label="Backtest 30 days ahead forecast")

图 5:Prophet 回测(作者提供的图像)。

协变量分析

结论我们的分析后,我们现在可以检查使用数据集中其他列的信息如湿度和风速是否能帮助我们创建更高性能的模型。协变量主要有两种类型:过去和未来。对于过去的协变量,预测时仅有过去的值可用,而对于未来的协变量,预测时也有未来的值可用。

在这个例子中,N-BEATS(神经基础扩展分析时间序列)模型与湿度和风速列作为过去的协变量一起使用(图 6)。

humidity = darts.TimeSeries.from_series(df.humidity)
wind_speed = darts.TimeSeries.from_series(df.wind_speed)

cov_model = darts.models.NBEATSModel(input_chunk_length=361, 
                                     output_chunk_length=len(val))
cov_model.fit(train, past_covariates=humidity.stack(wind_speed))
cov_forecast = cov_model.predict(len(val), 
                               past_covariates=humidity.stack(wind_speed))

print("MAPE: ", darts.metrics.mape(ts, cov_forecast))
ts.plot(label="Actual")
cov_forecast.plot(label="Covariate Forecast")

图 6:协变量分析预测(作者提供的图像)。

作为训练过程的结果,记录到的 MAPE 分数为 10.9%,因此在此情况下表现不如我们原始的 Prophet 模型。

联系方式

如果你想了解我的最新文章和项目,请在 Medium 上关注我并订阅我的邮件列表。这些是我的一些联系方式:

参考文献

[1] “每日气候时间序列数据”(SUMANTHVRAO,许可协议 CC0: 公共领域)。访问地址: www.kaggle.com/datasets/sumanthvrao/daily-climate-time-series-data?select=DailyDelhiClimateTrain.csv

posted @ 2024-10-12 19:52  绝不原创的飞龙  阅读(388)  评论(0)    收藏  举报