LLMs,新型大规模虚假信息武器?
原文:
towardsdatascience.com/llms-weapons-of-mass-disinformation-4def0dc3dc7
大型语言模型(LLMs)的双刃剑
你以为 2016 年的脱欧和美国总统竞选已经够糟糕了?再想想吧。
·发表于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)对公众的无监管发布可以归因于至少两个主要因素。第一个因素来源于政府官员普遍缺乏技术素养,这一点从我向这类听众做的众多报告中可以看出。
但第二个因素——也许是最关键的因素——是政府官员和中大型公司高管普遍认为实施人工智能监管会阻碍进步。他们担心这种监管约束会使他们的组织和国家处于不利地位,尤其是与那些在人工智能领域不受限制快速前进的国家相比。
这是美国国会、参议院、国防和国家安全委员会中的持续话题:现在对人工智能进行监管将会减缓其发展,美国将落后于中国,从而处于战略劣势。这一论点在西方国家主要由以国防为导向的人工智能优先公司巧妙地编制和推广。这些公司中最著名的是彼得·蒂尔生态系统支持的公司。倡导“快速行动,打破常规”方法的公司,如 Palantir 和 Anduril(有趣的是,这两个名字都参考了《指环王》中的最强大魔法神器),格外引人注目。
然而,我们不能忽视欧盟对人工智能不受控发展的监管尝试,尤其是从数据和知识产权保护的角度来看。尽管如此,由于大多数领先的语言学习模型(LLM)创作者是美国人,这些规定不可避免地会事后施行,即在人工智能模型已经在全球范围内部署之后。这时已经为时已晚。
掌握关键技术无疑是赢得大国竞争的先决条件,但这不应成为缺乏辩论、简单化论证或不加限制地部署这些技术的理由。我们需要记住至少两个重要的例子: Facebook LLaMa 模型泄露 和现行的中国人工智能监管政策。这些话题将是本系列后续文章的重点。
作为一个前瞻性的思考,请考虑这个问题:如果 Facebook 受到精心设计的人工智能模型部署网络安全规定的约束,LLaMa 泄露事件会发生吗?此外,考虑一下中国的人工智能监管框架。从各种指标来看,它比任何西方国家的监管框架都要先进和严格。这挑战了这样一种观念:即中国在没有不必要的繁文缛节的情况下,全速推进先进人工智能解决方案的发展和部署。
那么,接下来该怎么办?
当我们迈入机器生成真相的时代,政府、企业和整个社会都有责任建立保障措施,以减少风险并获取利益。人工智能开发的透明度、负责任的使用规范、强有力的机器主导和人工控制的事实核查机制以及先进的人工智能素养只是需要紧急关注的一些积极措施。这些话题,特别是最后一个,将是本系列文章的核心内容。
是时候讨论大型语言模型的负责任使用了,促进人工智能伦理和包容性的文化。技术最终只是一个工具——其影响取决于使用者的意图。问题是,我们会让它成为促进更有启发性的社会的工具,还是成为加剧分裂的武器?我们对这些强大工具的理解才刚刚开始。
免责声明:作为曾经的国防和国家安全领域的专业人士,我往往对人类技术利用持有一定的悲观和怀疑态度,不幸的是。因此,当我看到有关人工智能监管的最新新闻,特别是那些由语言学习模型创作者自己推动的叙事时,它们确实引起了我的注意。例如,OpenAI 首席执行官山姆·奥特曼最近在美国国会委员会上的出现。虽然我非常欢迎他的想法,但对我来说,这似乎是一个精心策划的举动,旨在确保他们的优势,提升对新进入者的障碍,并创建这种“护城河”概念,这在几周前的一份谷歌内部备忘录中曾被提及。但我也意识到这可能是我的偏见在作祟,我会尽量在整个系列中保持警觉。最终,这可能是我们所需要的:如果政府不愿意或不能对人工智能开发施加严格的监管,那么可能由私营部门来引领。然而,这种方法本质上会带有各自公司的议程和独特的策略。
敬请关注未来的文章,我们将深入探讨大型语言模型在操控政治话语中的潜在误用、它们在加剧社会经济不平等中的作用,以及它们如何被用来规避隐私规范。我们还将探索管理这些问题的潜在策略和政策,从技术到监管监督,以及公众对这些不断发展的技术的意识和教育需求。我们的旅程才刚刚开始。
喜欢这篇文章吗?
让我们互相认识!正如我所说,我们的旅程才刚刚开始。这一系列既属于你,也属于我。评论、讨论、分享、批评!目标是激发讨论。
如果你想进一步了解,请联系我!你可以在LinkedIn找到我,或在Medium上关注我!
感谢你的支持,我们下次见!
LMQL — 语言模型的 SQL
另一个可能对你的 LLM 应用有帮助的工具
·
关注 发表在 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.F
、lmql.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
— 包含我们定义的所有变量的字典:ANSWER
和CAPITAL
。 -
distribution_variable
和distribution_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 评论,并尝试预测情感。数据集中的所有评论都是积极的或消极的,但我们将保留中性作为分类的一种可能选项。
对于这个任务,让我们使用本地模型——Zephyr
和Llama-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.
很好,我们已经解决了问题并得到了结果。但由于我们将进行分类,我们希望模型返回三种输出之一(类别标签):negative
、neutral
或 positive
。我们可以在 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 中
·发表于 Towards Data Science ·5 分钟阅读·2023 年 1 月 31 日
–
照片由 Daniel K Cheung 提供,来自 Unsplash
CSV(逗号分隔值)是一种常用的文件格式,用于存储和交换数据。事实上,这种类型的源通常用于相对较小的数据量。
pandas
是一个常用的 Python 包,它允许开发人员处理和转换数据,作为分析和数据科学任务的一部分。然而,在执行任何任务之前,pandas 需要将所有数据加载到内存中。这意味着该包只能用于相对较小的数据量——这取决于主机机器的能力,但对于一台普通的机器,我们只能在内存中加载几 GB 的数据。
因此,将 CSV 文件加载到内存中,然后用 pandas 处理数据,是一个非常常见的任务,因为这些文件通常包含可以加载到内存中的数据。
订阅数据管道,这是一个专注于数据工程的新闻通讯
在这篇文章中,我们将演示如何将多个 CSV 文件加载到一个 pandas DataFrame 中。此外,我们还将展示如何在每条记录中识别源文件,以便我们可以确定哪个数据点属于某个数据文件。
现在假设我们在三个分开的 CSV 文件中收集了数据点,即 data_1.csv
、data_2.csv
和 data_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 的比较
towardsdatascience.com ## SQL 中的 CTE 是什么
理解 SQL 中的公用表表达式(CTE)
towardsdatascience.com ## 什么是 dbt(数据构建工具)
介绍正在主宰数据领域的 dbt
towardsdatascience.com
负载测试 SageMaker 多模型端点
原文:
towardsdatascience.com/load-testing-sagemaker-multi-model-endpoints-f0db7b305770
利用 Locust 在模型之间分配流量权重
·发表于 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…
你现在无法执行该操作。你在另一个标签或窗口中登录了。你在另一个标签或窗口中注销了……
示例的完整代码可以在上述链接中找到。如果你对 Locust 和 SageMaker 实时推理不熟悉,我强烈建议你查看与这两个功能相关的起始博客。本存储库中附带的负载测试脚本不仅可以轻松调整以适应 SageMaker 端点,还可以用于测试你托管的任何 API。任何反馈都受到欢迎,随时联系我提问或评论,谢谢阅读!
如果你喜欢这篇文章,请随时在 LinkedIn 上与我联系,并订阅我的 Medium Newsletter。如果你是 Medium 新用户,请使用我的 会员推荐链接注册。
使用 SageMaker 推理推荐器简化负载测试
原文:
towardsdatascience.com/load-testing-simplified-with-sagemaker-inference-recommender-b96746b69292
在 SageMaker 实时终端上测试 TensorFlow ResNet50
·发表于 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 的开发做出贡献。
你可以在上面的链接中找到此示例的代码及更多内容。SageMaker 推理推荐器是一个强大的工具,可以自动化负载测试设置的复杂部分。然而,需要注意的是,目前不支持如多模型和多容器端点这样的高级托管选项,因此对于这些用例,使用像 Locust 这样的第三方框架将是必要的。如往常一样,任何反馈都是受欢迎的,欢迎随时提出问题或评论,感谢阅读!
如果你喜欢这篇文章,欢迎通过 LinkedIn 与我联系,并订阅我的 Medium Newsletter。如果你是 Medium 新用户,可以使用我的 会员推荐链接注册。
局部光场融合
原文:
towardsdatascience.com/local-light-field-fusion-14c07ed36117
如何在智能手机上渲染 3D 场景
·发布在数据科学前沿 ·12 分钟阅读·2023 年 4 月 11 日
–
到目前为止,我们应该知道深度学习是表示 3D 场景并从任意视角生成这些场景的新渲染的一个极佳方法。然而,我们迄今为止看到的方法(例如,ONets 和 SRNs [2, 3])的问题是它们需要大量的场景图像来训练模型。考虑到这一点,我们可能会想知道是否可以用更少的样本获得基于深度学习的场景表示。我们实际需要多少图像来训练一个高分辨率的场景表示?
这个问题通过局部光场融合(LLFF)[1]方法来解决,该方法用于合成 3D 场景。LLFF 是光场渲染[4]的一个扩展,通过将几个现有视图扩展为多平面图像(MPI)表示,然后通过混合这些表示来渲染新的视角。该方法的结果是:
-
精确地模拟复杂场景和效果,例如反射。
-
理论上显示减少了生成准确场景表示所需的样本/图像数量。
此外,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 范数——基本上只是对图像像素的 均方误差 损失。
然而,单纯测量像素差异并不是图像相似性的最佳指标;例如,如果生成的图像相比目标图像仅仅右移了一像素呢? 一个更好的方法可以通过一点深度学习实现。特别是我们可以:
-
使用一个预训练的深度神经网络。
-
使用这个模型将图像嵌入到特征向量中(即,分类前的最终激活层)。
-
计算这些向量之间的差异(例如,使用 L1 或 L2 范数)
这种方法称为感知损失 [5],是一种强大的图像相似性度量,广泛用于深度学习研究(特别是生成模型);见 [6] 的第 3.3 节。
LLFFs 如何表示场景?
(引自 [1])
“我们方法的总体策略是使用深度学习管道将每个采样视图提升为具有 D 个深度层的分层场景表示,并通过在相邻场景表示的渲染之间进行混合来渲染新的视图。” — 引自 [1]
从一些图像和 相机视角信息 开始,LLFFs 通过两个不同的步骤渲染新的场景视角:
-
将图像转换为 MPI 表示。
-
通过混合附近 MPI 的渲染生成视图。
什么是 MPIs? MPIs 是一种以相机为中心的 3D 空间表示。这意味着我们考虑一个特定的相机视角,然后从该特定视角分解 3D 空间。特别是,3D 空间基于三个坐标进行分解:x
、y
和深度。然后,与这些坐标相关联的是 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 框架,我们使用真实和合成(例如,SUNCG 和 UnrealCV 渲染)数据的组合。在每次训练迭代中,我们采样两组五张图像(用于创建两个 MPI)和一个单独的保留视点。我们通过遵循上述方法生成此保留视点的估计值,然后应用感知损失函数 [5],以捕捉输出视点与真实情况的差异。
我们可以对 LLFF 进行端到端训练,因为它的所有组件都是可微分的。要执行训练迭代,我们只需要:
-
采样一些图像。
-
生成一个预测视点。
-
计算感知损失。
-
执行 (随机) 梯度下降 更新。
理论上减少所需样本的数量。 根据奈奎斯特率进行采样在场景表示中是不可行的,因为所需样本的数量太高。幸运的是,[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. Wolfe,Rebuy的人工智能总监和莱斯大学的博士生。我研究深度学习的实证和理论基础。你还可以查看我在 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 代码及三个示例
·发表于 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
替换为 knapsack
或 rastrigin
)。
我们需要在示例问题中有三种方法来使这段代码正常工作:
-
baseline_solution()
该方法为问题创建第一个解决方案(起始点)。
-
score_solution(solution)
score_solution
方法计算目标值。 -
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)。在等高线图中更容易看出:
拉斯特里金函数的等高线图。图片由作者提供。
地形由许多局部极小值组成,四个角落的值最高,而中心的值最低。
我们可以尝试使用模拟退火来寻找全局最小值。这个问题是连续的而不是离散的,我们想找到最小化拉斯特里金函数的 x 和 y 的值。
拉斯特里金函数在 n 维域上的定义为:
让我们尝试在三维空间中找到该函数的最优解(x、y 和 z)。定义域由 x 和 y 确定,因此问题与上面的图形完全相同。
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 之间随机选择 x 和 y 的浮点数。解决方案的得分等于 z(拉斯特里金函数的结果)。通过在随机方向上迈出一步,步长设置为 0.1 来选择一个邻居。进行可行性检查以确保我们保持在定义域内。
一次运行的日志:
最终解决方案非常接近最优解。
但要注意,如果你在更多维度下运行算法,不能保证找到最优解:
如你所见,最终的解决方案是局部最优而非全局最优。它找到了前两个变量的良好坐标,但第三个变量等于 0.985,远离 0。验证结果非常重要。这个具体例子通过微调 SA 参数可以很好地工作,但对于更多维度,你可能需要使用另一种表现更好的优化技术。
结论
在这篇文章中,我们实现了模拟退火的代码。通过三个例子,你可以理解它的不同可能性。你只需为新问题实现三个方法,使其工作,这些方法是baseline_solution
、score_solution
和select_neighbor
。当然,这个实现是基本的,如果你想使用它,你需要调整 SA 的参数,确保算法返回一个可行的解决方案。一个好的初始解决方案和研究选择邻近解决方案的最佳方法能大大提高最终解决方案的质量。
感谢阅读,下次见!
相关内容
解释、参数、优点、缺点和应用场景
数学优化启发式方法每个数据科学家都应该知道 [## 数学优化启发式方法每个数据科学家都应该知道
局部搜索、遗传算法等。
两种力量结合的实际例子。
本地预测与全球预测:你需要知道的
原文:
towardsdatascience.com/local-vs-global-forecasting-what-you-need-to-know-1cc29e66cae0
比较本地和全球时间序列预测方法,并通过使用 LightGBM 和澳大利亚旅游数据集的 Python 演示。
·发表于 Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 2 日
–
本地预测与全球预测,由 Giulia Roggia. 经许可使用。
-
什么是本地预测?
-
什么是全球预测?
-
如何选择本地预测与全球预测?
-
Python 示例:澳大利亚旅游
-
结论
什么是本地预测?
本地预测是传统的方法,其中我们为每个时间序列独立训练一个预测模型。经典的统计模型(如指数平滑、ARIMA、TBATS 等)通常采用这种方法,但通过特征工程步骤,标准的机器学习模型也可以使用这种方法。
本地预测具有优势:
-
它的理解和实现都很直观。
-
每个模型可以单独调整。
但它也有一些局限性:
-
它存在“冷启动”问题:需要为每个时间序列提供相对大量的历史数据,以可靠地估计模型参数。此外,这也使得预测新目标变得不可能,比如对新产品需求的预测。
-
它不能捕捉相关时间序列之间的共性和依赖性,比如横截面或层级关系。
-
对于包含多个时间序列的大型数据集,本地预测很难扩展,因为这需要为每个目标拟合和维护一个单独的模型。
什么是全球预测?
全局预测是一种更现代的方法,其中使用多个时间序列来训练一个单一的“全局”预测模型。通过这样做,它拥有更大的训练集,并且可以利用目标之间的共享结构来学习复杂的关系,最终实现更好的预测。
构建全局预测模型通常包括一个特征工程步骤来创建诸如:
-
目标的滞后值
-
目标在时间窗口中的统计数据(例如*“过去一周的均值”,“过去一个月的最小值”*等)
-
分类特征来区分时间序列的不同组
-
外生特征来建模外部/互动/季节性因素
全局预测具有相当大的优势:
-
它利用其他时间序列的信息来提高准确性和鲁棒性。
-
它可以对几乎没有数据的时间序列进行预测。
-
它可以扩展到包含许多时间序列的数据集,因为它只需要拟合和维护一个单一的模型。
-
通过使用特征工程,它可以处理多数据频率和缺失数据等问题,这些问题用经典统计模型更难解决。
但全局预测也有一些局限性:
-
它需要额外的努力来使用更复杂的模型和进行特征工程。
-
当出现新的时间序列时,可能需要进行全面的重新训练。
-
如果一个特定时间序列的性能开始下降,很难在不影响其他目标预测的情况下更新它。
-
这可能需要更多的计算资源和复杂的方法来估计和优化模型参数。
如何在局部预测和全局预测之间进行选择?
对于给定问题,没有明确的答案说明局部预测还是全局预测更好。
一般来说,局部预测可能更适合具有以下特征的问题:
-
少量时间序列的历史较长
-
时间序列之间的高变异性和特异性
-
预测和编程专业知识有限
另一方面,全局预测可能更适合具有以下特征的问题:
-
许多时间序列的历史较短
-
目标之间的变异性低且相似度高
-
噪声数据
示例:澳大利亚旅游
在本节中,我们通过使用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:归一化 我们将所有数据(目标和特征)在 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 都低于局部方法,除了西澳大利亚州。
在这种设置下,全球方法的优越性在某种程度上是意料之中的,因为:
-
我们正在预测多个相关的时间序列。
-
历史数据的深度非常浅。
-
我们使用的是一种相对复杂的浅层单变量时间序列模型。在这种情况下,经典统计模型可能更合适。
本文使用的代码可在 这里 获取。