TowardsDataScience 2023 博客中文翻译(一百五十八)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

少样本学习如何自动化文档标记

原文:towardsdatascience.com/how-few-shot-learning-is-automating-document-labeling-43f9868c0f74

利用 GPT 模型

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

·发布于数据科学前沿 ·阅读时间 5 分钟·2023 年 4 月 7 日

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

照片由DeepMind提供,来源于Unsplash

手动文档标记是一个耗时且繁琐的过程,通常需要大量资源且容易出错。然而,最近在机器学习方面的进展,特别是所谓的少样本学习技术,使得自动化标记过程变得更加容易。特别是大语言模型(LLMs)由于其在上下文学习中的新兴能力,是优秀的少样本学习者。

在这篇文章中,我们将详细探讨少样本学习如何改变文档标记,特别是对文档处理中最重要的任务——命名实体识别(NER)的影响。我们将展示UBIAI的平台如何通过少样本标记技术使自动化这一关键任务变得比以往更容易。

什么是少样本学习?

少样本学习是一种机器学习技术,使得模型能够仅用少量标记示例来学习给定的任务。在不修改其权重的情况下,模型可以通过在输入中包含这些任务的连接训练示例,并要求模型预测目标文本的输出,从而调整以执行特定任务。以下是使用 3 个示例进行命名实体识别(NER)任务的少样本学习示例:

###Prompt
Extract entities from the following sentences without changing original words.

###
Sentence: " and storage components. 5+ years of experience deliver
ing scalable and resilient services at large enterprise scale, including experience in data platforms including large-scale analytics on relational, structured and unstructured data. 3+ years of experien
ce as a SWE/Dev/Technical lead in an agile environment including 1+ years of experience operating in a DevOps model. 2+ years of experience designing secure, scalable and cost-efficient PaaS services on
the Microsoft Azure (or similar) platform. Expert understanding of"
DIPLOMA: none
DIPLOMA_MAJOR: none
EXPERIENCE: 3+ years, 5+ years, 5+ years, 5+ years, 3+ years, 1+ years, 2+ years
SKILLS: designing, delivering scalable and resilient services, data platforms, large-scale analytics on relational, structured and unstructured data, SWE/Dev/Technical, DevOps, designing, PaaS services, Microsoft Azure
###

Sentence: "8+ years demonstrated experience in designing and developing enterprise-level scale services/solutions. 3+ years of leadership and people management experience. 5+ years of Agile Experie
nce Bachelors degree in Computer Science or Engineering, or a related field, or equivalent alternative education, skills, and/or practical experience Other 5+ years of full-stack software development exp
erience to include C# (or similar) experience with the ability to contribute to technical architecture across web, mobile, middle tier, data pipeline"
DIPLOMA: Bachelors\nDIPLOMA_MAJOR: Computer Science
EXPERIENCE: 8+ years, 3+ years, 5+ years, 5+ years, 5+ years, 3+ years
SKILLS: designing, developing enterprise-level scale services/solutions, leadership and people management experience, Agile Experience, full-stack software development, C#, designing
###

Sentence: "5+ years of experience in software development. 3+ years of experience in designing and developing enterprise-level scale services/solutions. 3+ years of experience in leading and managing
 teams. 5+ years of experience in Agile Experience. Bachelors degree in Computer Science or Engineering, or a related field, or equivalent alternative education, skills, and/or practical experience."

提示通常以指示模型执行特定任务开始,例如“从以下句子中提取实体而不改变原始词语。”请注意,我们添加了指示“无更改原始词语”以防止 LLM 产生随机文本,这是其著名的特性。这在获得一致的模型响应中至关重要。

这种现象已在这篇文章中得到了广泛研究,我强烈推荐。实质上,论文表明,在温和的假设下,模型的预训练分布是潜在任务的混合,这些任务可以通过上下文学习高效地学习。在这种情况下,上下文学习更多的是关于识别任务,而不是通过调整模型权重来学习任务。

Few-shot 标注

Few-shot 学习在数据标注领域具有出色的实际应用,通常被称为少量标注。在这种情况下,我们向模型提供少量已标注的示例,并要求它预测后续文档的标签。然而,将这一能力集成到功能齐全的数据标注平台中,难度远超想象,以下是一些挑战:

  • LLM 本质上是文本生成器,倾向于生成可变的输出。提示工程对于使其生成可预测的输出至关重要,这些输出可以用于自动标注数据。

  • Token 限制:如 OpenAI 的 GPT-3 这样的 LLM 每次请求的 token 数量限制为 4000 个,这限制了可以一次发送的文档长度。在发送请求之前,分块和拆分数据变得至关重要。

  • Span 偏移计算:在从模型接收输出后,我们需要在文档中搜索其出现位置并正确标注。

使用 UBIAI 进行 Few-shot 标注

我们最近通过将OpenAI 的 GPT-3 DavinciUBIAI 标注工具集成,新增了少量标注能力。该工具目前支持对未结构化和半结构化文档(如 PDF 和扫描图像)的 few-shot NER 任务。

开始使用:

  1. 只需标注 1–5 个示例

  2. 启用 few-shot GPT 模型

  3. 在新的未标注文档上运行预测

这是在提供 5 个示例的工作描述上进行 few-shot NER 的一个示例:

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

图片作者:非结构化文本上的 Few Shot NER

GPT 模型仅凭五个上下文示例就能准确预测大多数实体。由于 LLM 在大量数据上进行训练,这种 few-shot 学习方法可以应用于各种领域,如法律、医疗、HR、保险文档等,使其成为一个极其强大的工具。

然而,少量样本学习最令人惊讶的方面是它对上下文有限的半结构化文档的适应能力。在下面的示例中,我仅提供了一个标记的 OCR 发票示例,并要求它标记下一个。模型出乎意料地准确地预测了许多实体。即使有更多的示例,模型在对半结构化文档的泛化方面也表现得非常出色。

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

作者提供的图像:PDF 上的少量样本命名实体识别(NER)

有关少量样本标记功能的详细教程,请查看下面的视频:

UBIAI 的少量样本标记教程

结论:

少量样本学习正在彻底改变文档标记过程。通过将少量样本标记功能集成到功能性数据标记平台中,如 UBIAI 的注释工具,现在可以自动化诸如命名实体识别(NER)等关键任务,无论是在非结构化还是半结构化文档中。这并不意味着大型语言模型(LLMs)会很快取代人工标注员。相反,它们通过提高效率来增强他们的能力。凭借少量样本学习的力量,大型语言模型可以标记大量数据,并应用于法律、医疗、HR 和保险等多个领域,从而训练出更小、更准确的专业化模型,这些模型可以高效部署。

我们目前正在添加对少量样本关系提取和文档分类的支持,请继续关注!

在 Twitter 上关注我们 @UBIAI5点击这里订阅!

如何通过 GenAI 解决方案彻底改变商业自动化:解读 LLM 应用的高级管理层

原文:towardsdatascience.com/how-genai-solutions-revolutionize-business-automation-57747b0f11ce?source=collection_archive---------3-----------------------#2023-09-06

公司如何利用大型语言模型(LLMs)的力量来自动化工作流程并提高成本效率

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

·

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

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

照片由 Gerard Siderius 提供,来源于 Unsplash

介绍

在最近与一家生物制药公司制造高管的合作中,我们深入探讨了生成性 AI 的世界,特别是大型语言模型(LLMs),以探索它们如何用于加速质量调查。质量调查在产品制造或测试中发现偏差时触发。由于潜在的患者健康风险和监管要求,批次被暂停,根据影响,生产甚至可能会被暂停。加速调查以进行根本原因分析,并尽快实施纠正和预防措施计划至关重要。我们的目标是尽可能利用 GenAI 加速这一过程。

当我们开始思考最低可行产品(MVP)时,面临了几个选项,关于 GenAI 如何自动化流程的不同阶段,以提高周期时间并尽快解冻批次。高管们是各自领域的专家,并且接受了 GenAI 培训。然而,有必要深入了解 LLM 的能力和各种 GenAI 解决方案模式,以确定在质量调查过程中优先考虑哪个阶段作为 MVP,在短期解决方案可行性和预期周期时间改进之间取得平衡。

尽管在我们的案例中,讨论集中在一个特定的过程上,但相同的解决方案模式正被各行业和职能利用,以提取成本效率并加速成果。那么,GenAI 解决方案如何帮助这样的过程呢?

LLMs 的独特能力

在最近 GenAI 人气激增之前,企业界的自动化解决方案主要针对常规的、基于规则的任务,或依赖于机器人流程自动化(RPA)。机器学习应用主要围绕分析展开,例如使用回归模型预测销售量等结果。然而,最新的 LLMs 由于其卓越的特性而脱颖而出:

  1. 内容理解: LLMs 能够“理解”文本的含义

  2. 即时训练: LLMs 能够执行其原始训练中未涉及的新任务(即零样本学习),通过自然语言指令和可选的少量示例(少样本学习)

  3. 推理: LLMs 能够在一定程度上“思考”和“推理”潜在的行动(尽管存在一些限制和风险)

在“传统”机器学习中,构建和使用模型的过程通常涉及收集数据、手动定义“目标”,并训练模型以预测给定其他属性的“目标”。因此,模型可以执行一个特定的任务或回答一个特定类型的问题。相反,你可以要求一个经过预训练的 LLM 评估客户评论中对你的业务重要的特定方面,这些方面是 LLM 从未见过的,也没有在评论中明确提到。

LLM-based 解决方案的机制

行业内的许多 LLM 解决方案集中于设计和提供详细的指令,以使 LLM 执行特定任务(这被称为提示工程)。一种增强 LLM 影响力的有效方式是通过自动化方式使其访问公司的专有信息。检索增强生成(RAG)已成为实现这一目标的最常见解决方案模式之一。

概述 — 10,000 英尺视角

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

GenAI 解决方案工作流的高级概览(来源:作者提供的图像)

简而言之,解决方案有两个阶段:

  1. 搜索: 检索与用户请求相关的公司数据。例如,如果要求以特定格式或风格撰写报告,则会提取并将以前报告的文本发送给 LLM 作为示例。

  2. 生成: 将在先前阶段检索到的指令和示例(或任何其他相关信息)编译成文本提示,并将其发送给 LLM 以生成所需的输出。以报告为例,提示可以是,

请将以下信息编写成报告,使用提供示例的格式和风格。

这里是内容:[报告内容…. ]。

这是示例:之前的报告标题

第一部分 …

第二部分 …

结论

RAG 工作流 — 1,000 英尺视角

让我们深入了解解决方案模式中的搜索和生成两个阶段。

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

使用Cohere 的 embed-english-v2.0 模型的示例句子的向量嵌入(即数值表示)(来源:作者提供的图像)

这些数值表示如何映射到二维空间中是很有趣的。可以看到,相似主题的句子被映射得很近。

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

示例句子的向量嵌入绘制在二维空间中(来源:作者提供的图像,灵感来自课程具有语义搜索的大型语言模型中的演示,课程由Deeplearning.ai提供)

2. 创建 知识库: 解决方案的搜索组件接收一个问题,并进行语义搜索以寻找知识库中最相似的信息。那么,这个知识库是如何创建的呢?需要放入知识库的文件会由嵌入模型处理,从而创建数值表示。这些数值表示会被加载到一个专门的数据库中——通常是为高效存储和快速检索这种信息而特别设计的向量数据库。

3. 在知识库中检索相似信息(即检索): 当用户提交问题或任务到解决方案时,解决方案使用嵌入模型将问题文本转换为数值表示。这个问题向量会与知识库进行匹配,以寻找最相似的信息。可能会返回一个或多个搜索结果,这些结果可以传递到下一阶段以生成回应或输出。

4. 使用 LLM 生成输出(即生成): 现在,解决方案已经成功找到可以帮助 LLM 生成有意义输出的相关信息,整个包,即“提示”,可以发送到 LLM。这个提示包括一个或多个标准的指令集,这些指令引导 LLM,还有实际的用户问题,最后是检索阶段得到的信息。LLM 生成的结果可以在必要时进行处理(例如,将输出加载到特定格式的 Word 文档中),然后再交付给用户。

深入了解解决方案组件

让我们更深入地探讨解决方案的组件

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

深入探讨 RAG 解决方案工作流的组件(紫色块显示可选组件)。(来源:作者提供的图片)

1. 创建知识库: 将相关文档加载到知识库中有一些细节和考虑因素。

  • 文档加载: 可能需要将不同的相关文档(pdf、word、在线资源等)导入到数据存储库中。根据用例,某些文档的只有特定部分可能是相关的。例如,对于为金融分析师设计的查询公司 10-K 报告的解决方案,标题页、目录、标准合规信息和一些附录可能与财务分析无关。因此,这些部分可以从知识库中省略。避免知识库中的冗余信息至关重要,以确保 LLM 模型提供多样化和高质量的回应。

  • 文档拆分: 一旦确定了需要包含在知识库中的相关文档部分,下一步是确定如何拆分这些信息并将其加载到向量数据库中。选择方式可能因使用场景而异。一种有效的方法是按段落拆分并留有一定重叠。这涉及到为保留完整段落设置字数(或“令牌”,LLM 用于文本处理的单位)限制。如果段落超过此限制,则应将其拆分为多个记录以存储在向量数据库中。通常会故意保留一些词汇重叠以保持上下文。例如,使用每个向量 1,000 字的限制,并有 40 字的重叠。

  • 附加元数据: 增强知识库中的信息涉及用有意义的元数据标记每条记录。基本的元数据示例包括提取信息的原始文档标题和章节层级。附加元数据可以进一步提升搜索和检索的质量。例如,从 10-K 报告中提取的资产负债表数据可以用以下元数据进行标记:

原始文档标题:公司 XYZ 10-K

年份:2022

部分:财务报表及附加数据 | 资产负债表

  • 存储: 存储信息的选项有很多。可以使用如 Chroma 或 Faiss 之类的向量数据库解决方案,或在 Postgres / MySQL 上使用这些解决方案。然而,SQL 数据库、NoSQL 数据库、文档存储、图数据库也可以使用。此外,还可以考虑使用内存存储以减少延迟,以及横向扩展以提高可伸缩性、可用性、负载均衡等。

2. 从知识库中检索相似信息: 对于简单的使用案例,基于在知识库中搜索相似向量的检索方法,如前一部分所述,应该足够。一种常见的两阶段方法可以平衡搜索速度与准确性:

  • 密集检索: 最初,通过快速近似邻居搜索对广泛的知识库进行快速扫描,以处理搜索查询。这将产生几十个或几百个结果供进一步评估。

  • 重新排序: 在获取的候选项中,可以使用更计算密集的算法来区分更相关和不太相关的结果。可以通过对密集检索阶段获取的候选项进行二次处理,或使用其他特征(如指向每个搜索结果的链接数量(表示可信度或主题权威)、TF-IDF 分数,或直接请求 LLM 审查所有候选项并对其相关性进行排名)来计算额外的相关性分数。

对于高级功能,例如通过选择多样化的信息、基于自然语言用户提示应用过滤器等来提高搜索结果的质量,可能需要更复杂的方法。例如,在财务查询中,用户问:“XYZ 公司在 2020 年的净利润是多少?”解决方案必须过滤出关于 XYZ 公司的文件以及 2020 年的数据。一个可能的解决方案是使用 LLM 将请求拆分为过滤组件,通过使用元数据按年份 2020 进行过滤,从而缩小语义搜索的目标范围。然后,执行语义搜索以在知识库中定位“XYZ 公司的净利润”。

3. 使用 LLM 生成输出(生成): 过程的最后一步涉及使用 LLM 生成输出。

  • 直接方法: 直接方法是将从搜索阶段检索到的所有信息连同人工提示和指令传递给 LLM。然而,对于 LLM 能处理的信息量存在限制。例如,Azure OpenAI 基础 GPT-4 模型的上下文大小为 1024 个令牌,大约相当于 2 页文本。根据使用情况,可能需要对这一限制进行变通处理。

  • 链式方法: 为了绕过上下文大小限制,一种方法是逐步向语言模型提供信息,并指示它在每次迭代中构建和完善答案。LangChain 框架提供了如“refine”、“map_reduce”和“map_rerank”等方法,以帮助生成多个答案部分,并最终通过另一个 LLM 调用将它们组合起来。

结论

在数据生成不断增加的时代,利用 GenAI,我们的上下文感知和可训练助手,比以往任何时候都更具影响力。正如文章中所述,这一解决方案模式无缝地解决了自动化数据处理的挑战,并释放出人力资源以处理更复杂的任务。随着大型语言模型(LLM)的日益商品化和解决方案组件的标准化,可以预见,这些解决方案将很快变得普遍。

本文涉及了基础 RAG 概念。下一篇文章将通过构建一个能够回答基于任何指定网站信息的问题的聊天机器人来对这些概念进行实际探索。正如所说,最好的学习方式就是实践!

## GenAI 实操指南:面向产品与工程领导者

通过了解 LLM 基于产品的内部机制来做出更好的产品决策。

towardsdatascience.com

常见问题解答(FAQs)

  • 生成的内容会成为 LLM 的记忆并影响未来的输出吗?例如,经验不足的用户生成的糟糕输出会影响其他用户的输出质量吗?

    不会。在这种解决方案方法中,LLM 对其生成的内容没有“记忆”——每个请求都从头开始。除非 LLM 进行进一步的微调(训练)或生成的输出也被添加到知识库中,否则未来的输出不会受到影响。

  • LLM 是否会随着使用而学习并变得更好? 不会自动如此。RAG 解决方案模式并非一种强化学习系统。然而,可以设计解决方案,使用户能够对输出质量提供反馈,从而用以微调模型。更新知识库或使用升级的 LLM 也可以提高解决方案输出的质量。

  • 向量嵌入会保存在源数据仓库中吗? 一般不会。虽然文档切块的向量可以在技术上存储在源数据仓库中,但源数据仓库和向量数据库(或者为了这个解决方案专门用于存储向量的 SQL 数据库)的目的不同。将向量添加到源数据库可能会创建操作依赖性和额外开销,这可能是不必要的或没有任何奖励的。

  • 解决方案如何用新数据进行更新? 数据加载流程(识别文档、处理、切块、向量化、加载到向量数据库)需要在新数据可用时运行。这可以是一个定期批处理过程。更新频率可以根据使用案例进行调整。

  • 我们如何确保知识库中存储的文档中的敏感信息不会被公众或 LLM 供应商访问?

    企业可以使用 Azure OpenAI 服务作为单租户解决方案,配备私有实例的 OpenAI LLM。这可以确保数据隐私和安全。另一种解决方案是将 Hugging Face LLM 部署到公司的私有基础设施上,以确保数据不会离开公司的安全边界(与使用公开托管的 LLM 不同)。

推荐资源

探索这些资源以深入了解 LLM 及其应用:

想更深入了解 RAG 解决方案模式:

生成性人工智能如何支持食品行业企业

原文:towardsdatascience.com/how-generative-ai-can-support-food-industry-businesses-993872b4a6ce

从过去的错误中学习,并利用 ChatGPT 为食品行业公司构建更好的机器学习模型

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

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

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

图片来源于Mae Mu Unsplash

介绍

我将带你踏上的旅程有两个重要原因。

  1. 它将展示如何使用 ChatGPT 来支持食品行业的公司。

  2. 可以说最重要的原因,我将详细讲解我在将近两年前发表的一篇文章,指出那篇文章中的问题,并尝试解决它们。

是的,我认为第二个原因更为重要。为什么?回顾过去的方法和过程,分析数据是重要的,因为它可以让你学习如何修正失败,这最终会导致成功。我绝非完美,我个人寻找过去做错的事情,希望从错误中学习,并为我支持的客户开发出更强大的模型。

原始出版物

我第一次发布《机器学习不仅仅是大科技的专利》是在 2021 年 7 月。

## 机器学习不仅仅是大科技的专利

使用自然语言处理来支持小企业。

towardsdatascience.com

本文的目的是展示食品行业中的公司如何通过各种机器学习(ML)应用得到支持。我使用了自然语言处理(NLP)技术来处理关于公司的网络评论。我使用的一些 NLP 方法包括主题建模分析,以更好地了解客户在谈论什么,以及情感分析,以创建一个可以帮助预测未来评论情感并向公司提供反馈的模型。分析显示这两种方法都能够在小型数据集上进行。

啊!大错误。

我的数据并不理想。数据集不仅小,而且倾向于正面评论。这导致模型几乎总是预测评论为正面(对公司没有帮助),并且存在过拟合。

解决方案?我考虑使用生成对抗网络(GAN)来创建新的合成评论,但随后我想,能不能直接问 ChatGPT?。我原始工作中的第一个错误被解决了。我能够利用 ChatGPT 创建人工的正面和负面评论,这最终平衡了我的意大利食品公司评论数据集!

数据集

幸运的是,我在训练任何模型或进行任何分析之前,已经仔细验证了我创建的数据的可用性。请查看下面的帖子,提供对真实数据和人工数据的更深入分析。

## ChatGPT 生成的食品行业评论:真实性评估

调查食品行业公司如何通过 ChatGPT 生成的数据来支持评论和调查收集。

[towardsdatascience.com

正面评论创建

对于数据集,我希望正面和负面评论的比例均衡。首先,使用 ChatGPT,我查询它来创建正面评论。

创建 500 条关于不同意大利食品和产品的正面评论,并将其放入 CSV 文件中。

我至少这样做了 5 次,原因有二。一是 ChatGPT 不断超时。二是我想确保获得足够不同的评论。此外,为了增加生成数据的多样性,我每次都会更改查询。例如,我会将意大利食品和产品改为意大利甜点意大利葡萄酒。让我们看看 ChatGPT 生成的一个虚假正面评论。

“我购买的佩科里诺托斯卡纳奶酪味道浓郁美味。它的质地坚实且易碎,带有一丝草味,非常适合刨丝、刮片或直接享用。

如果你问我,不错!

负面评论创建

为了创建负面评论,我遵循了相同的过程。我确实需要明确告诉 ChatGPT 我制作负面评论并不是为了伤害任何人,这确实是事实!我只是想要一个能泛化到模型可能遇到的所有数据类型(正面和负面评论)的分类模型和分析。

创建 100 条关于不同意大利食品的负面评论,并将它们放入 CSV 文件中。

生成的负面评论示例:

“我尝试的意大利火腿和芝麻菜比萨上有枯萎的芝麻菜,意大利火腿也很硬。它并不令人垂涎。”

如果你问我,结果还不错!

分析

第一个错误:

无法从‘keras.preprocessing.sequence’导入名称‘pad_sequences’ (/usr/local/lib/python3.10/dist-packages/keras/preprocessing/sequence.py)

解决方案:

→ 替代导入

from keras.preprocessing.sequence import pad_sequences

→ 使用导入:

from keras.utils import pad_sequences

大多数代码如以前一样工作,这很令人惊讶。由于 Keras 3.0 的最新发布,部分代码可能会被弃用,这取决于你使用的包和 IDE。

数据清理

在模型训练之前,还需要对评论进行一些额外的清理。幸运的是,可以在情感列中将“0”用于负面评论,将“1”用于正面评论,附加到每条评论中。

原始清理

虽然如果我添加新列,以下代码是可以实施的,但我决定使用 lambda 函数来清理数据集。

#Original Line from the first analysis
df['Label'] = [1 if x >=3 else 0 for x in df['Rating']]

此外,我决定将3的评分视为负面评分,这幸运地帮助平衡了数据集。

orig_reviews['Rating']=orig_reviews['Rating'].apply(lambda x: 0 if x < 4else 1)

我在清理过程中注意到原始帖子中有未定义的变量,这是我以前经常遇到的问题(说实话,我现在仍然有)。目前,我尝试通过重置内核并在将工作集成到我的帖子之前仔细检查来减轻这个问题。

原始数据集的问题

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

数据集不平衡(图像来自作者)

数据集最大的问题是正面评论和负面评论之间的巨大不平衡。最初有 512 条正面评论和 132 条负面评论。这为什么是个问题? 使用这个数据集训练模型很可能会导致一个大多数(几乎总是)将评论预测为正面的模型。我在原始帖子中确实忽视了这一点,应该更好地解决这个问题。虽然我不回避不平衡的数据集,但在我用尽所有努力并尝试各种技术(包括进一步的数据收集!)来平衡它们之前,我不会最初使用它们。

如果我们完全不训练模型而仅使用 BERT 会怎样?

如我们所见,目前数据集不支持强大的情感分析模型。BERT 在没有训练的情况下能够准确预测评论的情感吗?还是需要采取其他方法?

在没有训练的情况下,BERT 的准确率为68.27%。由于许多评论使用了非结构化和模糊的语言,BERT 无法理解,因此可能未能实现更高的准确率。

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

BERT 预测混淆矩阵(图片来自作者)

让我们通过混淆矩阵来深入了解 BERT 的预测。对于消极评论,BERT 正确了 76 次。56 个标签为积极,但应该是消极的。这可能是由于评论在给定状态下可能更为中立,但倾向于积极情感。例如,“这个地方以前不错,但今天的比萨饼不太好。” 作为人类,我们阅读这句话会理解其表达了主要的消极情感。另一方面,BERT 可能会将句子中连续使用的*“好”* 作为预测该句子为积极的依据。

对于积极评论,BERT 实现了更准确的分类。BERT 正确预测了 363 次评论为积极。然而,一个失败是,BERT 将 149 个预测为积极的评论误认为是消极的。

我想概述并讨论混淆矩阵中的信息,因为它显示了算法理解人类语言的困难,特别是当不同的语言特征被纳入时,例如讽刺和说话者希望传达的情感**。使用 BERT 而不进行训练的目的是查看是否需要对公司进行模型微调,还是可以使用现成的模型。在准确率低于 80%的情况下,我建议微调一个模型,以更好地与公司的数据对齐。**

模型创建

我注意到在我最初的帖子中,我使用了卷积神经网络(CNN)来开发我的模型。根据我的记忆,我这样做是因为我在已发布的研究中大量使用 CNN(使用混合生成对抗网络图像来增强分类模型训练数据集的好处*)。虽然使用 CNN(在这种情况下是 1-D)并没有错,但我还想看看其他可能对数据集提供更好预测的模型。

D = 20 
i = Input(shape = (T,))
x = Embedding(V +1 , D)(i)
x = Conv1D(16,2,activation='relu',)(x)
x = MaxPooling1D(2)(x)
x = Conv1D(32,2,activation='relu')(x)
x = GlobalMaxPooling1D()(x)
x = Dense(1,activation='sigmoid')(x)
model = Model(i,x)

新模型

虽然创建自己的模型对我的学习很有帮助,但我决定进行迁移学习,并在一个知名模型BERT上训练我的新情感分析(SA)模型。

请参见我使用的完整 BERT 代码。

为什么 迁移学习? 为什么要重新发明轮子,当有强大的模型可以根据你的问题进行调整时? 每当遇到问题时,总是要做研究,看看其他人在过去完成了什么。你可能会惊讶地发现,很多人遇到过相同的或类似的问题,并且已经为你解决了!

结果

总体而言,我没有准确描述(或者说,根本没有概述)我原始数据集如何导致模型偏向严重过拟合数据集。

训练:无数据增强

如前所述,原始模型因正负标签的极端不平衡而过拟合数据集。导致“更好”模型的一些变化包括将训练和测试集的划分从 80/20 改为 70/30,以及在 BERT 模型中添加更多的 dropout(我使用了 0.5)。

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

训练/测试损失与准确度:原始数据集(图片来自作者)

如你所见,在模型训练过程中,损失稳定在 50%(这是可以预期的)。当我使用 70/30 的划分进行训练时,最大的变化是训练集的损失值从 ~80% 降至 ~60%。总体而言,模型在不平衡的数据集上表现不佳。

增强数据集

BERT 如何解读虚假评论?

BERT 的表现不佳,准确率为 38.22%。我不把全部责任归咎于 BERT 模型。ChatGPT 生成的数据在区分正面和负面评论方面缺乏明确性,这清楚地表明 ChatGPT 生成的数据很大程度上基于过去的模式,而不具备意识。

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

BERT 混淆矩阵与生成数据(图片来自作者)

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

使用增强数据集对 BERT 进行微调

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

平衡数据集(图片来自作者)

使用 ChatGPT 后,数据集被平衡为包含 1,126 条正面评论和 1,124 条负面评论(额外的 614 条正面评论和 992 条负面评论)。使用生成式 AI 算法的一个好处是它们能够平衡数据集,特别是像这样的巨大不平衡数据集。 缺点?新生成的数据可能无法代表原始数据,这需要考虑到。

一旦数据集平衡,我们可以尝试再次微调 BERT 进行情感分析,使用之前相同的过程。一个明显的变化是,由于生成的评论(150 → 60)导致每个嵌入的单词数量减少了。

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

训练/测试损失与准确率:增强数据集(图片来自作者)

成功!数据集增强改善了 BERT 模型的微调。在 5 个周期后,准确率达到了99.67%,损失很小。测试损失始终维持在约 13.3%左右,第五周期的准确率为 97.3%。

这个模型如何惠及使用它的食品行业公司? 虽然使用公司内部的数据可能会导致一个更个性化与其流程对齐的模型,但使用外部数据可以帮助公司提供一个更通用适应现实世界变化的模型。这些变化几乎是不可避免的,模型可能会对此感到陌生,拥有一个能够适应现实世界不可预测性的模型可以减少失败,并作为对模型做出不良决策的缓冲。

结论

生成式 AI 最近迅猛发展,找到适合并对不同产业公司有积极和有益的用例非常重要。对于食品行业的公司,或任何拥有产品及其评论的公司,ChatGPT 可以帮助支持能够将评论标记为正面和负面的模型,以支持业务运营和产品开发。虽然 ChatGPT 可以用于自动化,但我们应该警惕其强大功能,采取人类参与的方法,评估生成的数据。不论你使用何种生成式 AI 技术进行数据集开发,确保数据真实反映现实世界信息,以期创造出表现最强的模型。

从个人角度来看,今天展示了我们如何不断努力变得更好,我们必须从过去的不佳表现中学习以取得进步。从错误和弱点中学习是数据科学家最重要的部分之一,最终将促成一个以卓越和持续发展为核心的职业生涯。

如果你喜欢今天的阅读,请关注我,并告诉我是否有其他主题你希望我深入探讨!如果你没有 Medium 账号,通过我的链接 这里 (这样我会获得少量佣金)注册吧!此外,可以在 LinkedIn 上添加我,或者随时联系我!感谢阅读!

来源

  1. 数据使用经 Altomontes Inc.批准。

代码

 # Set the model name
MODEL_NAME = 'bert-base-cased'

# Build a BERT based tokenizer
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)

# Store length of each review
token_lens = []

# Iterate through the content slide
for txt in df.content:
    tokens = tokenizer.encode(txt, max_length=512)
    token_lens.append(len(tokens))

MAX_LEN = 160

class GPReviewDataset(Dataset):
    # Constructor Function
    def __init__(self, reviews, targets, tokenizer, max_len):
        self.reviews = reviews
        self.targets = targets
        self.tokenizer = tokenizer
        self.max_len = max_len

    # Length magic method
    def __len__(self):
        return len(self.reviews)

    # get item magic method
    def __getitem__(self, item):
        review = str(self.reviews[item])
        target = self.targets[item]

        # Encoded format to be returned
        encoding = self.tokenizer.encode_plus(
            review,
            add_special_tokens=True,
            max_length=self.max_len,
            return_token_type_ids=False,
            pad_to_max_length=True,
            return_attention_mask=True,
            return_tensors='pt',
        )

        return {
            'review_text': review,
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'targets': torch.tensor(target, dtype=torch.long)

df_train, df_test = train_test_split(df, test_size=0.2, random_state=RANDOM_SEED,stratify=df.sentiment)

def create_data_loader(df, tokenizer, max_len, batch_size):
    ds = GPReviewDataset(
        reviews=df.content.to_numpy(),
        targets=df.sentiment.to_numpy(),
        tokenizer=tokenizer,
        max_len=max_len
    )

    return DataLoader(
        ds,
        batch_size=batch_size,
        num_workers=0
    )

BATCH_SIZE = 16
train_data_loader = create_data_loader(df_train, tokenizer, MAX_LEN, BATCH_SIZE)
test_data_loader = create_data_loader(df_test, tokenizer, MAX_LEN, BATCH_SIZE)

print(df_train.shape, df_test.shape)

bert_model = BertModel.from_pretrained(MODEL_NAME,return_dict=False)

# Build the Sentiment Classifier class
class SentimentClassifier(nn.Module):

    # Constructor class
    def __init__(self, n_classes):
        super(SentimentClassifier, self).__init__()
        self.bert = BertModel.from_pretrained(MODEL_NAME,return_dict=False)
        self.drop = nn.Dropout(p=0.5)
        self.out = nn.Linear(self.bert.config.hidden_size, n_classes)

    # Forward propagaion class
    def forward(self, input_ids, attention_mask,return_dict):
        _, pooled_output = self.bert(
          input_ids=input_ids,
          attention_mask=attention_mask,
          return_dict=False
        )
        #  Add a dropout layer
        output = self.drop(pooled_output)
        return self.out(output)

# Instantiate the model and move to classifier
model = SentimentClassifier(2)
model = model.to(device)

# Number of iterations
EPOCHS = 10

# Optimizer Adam
optimizer = AdamW(model.parameters(), lr=2e-5, correct_bias=False)

total_steps = len(train_data_loader) * EPOCHS

scheduler = get_linear_schedule_with_warmup(
    optimizer,
    num_warmup_steps=0,
    num_training_steps=total_steps
)

# Set the loss function
loss_fn = nn.CrossEntropyLoss().to(device)

# Function for a single training iteration
def train_epoch(model, data_loader, loss_fn, optimizer, device, scheduler, n_examples):
    model = model.train()
    losses = []
    correct_predictions = 0

    for d in data_loader:
        input_ids = d["input_ids"].to(device)
        attention_mask = d["attention_mask"].to(device)
        targets = d["targets"].to(device)

        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask,
        return_dict=True)

        _, preds = torch.max(outputs, dim=1)
        loss = loss_fn(outputs, targets)
        correct_predictions += torch.sum(preds == targets)
        losses.append(loss.item())

        # Backward prop
        loss.backward()

        # Gradient Descent
        nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        scheduler.step()
        optimizer.zero_grad()

    return correct_predictions.double() / n_examples, np.mean(losses)

def eval_model(model, data_loader, loss_fn, device, n_examples):
    model = model.eval()

    losses = []
    correct_predictions = 0

    with torch.no_grad():
        for d in data_loader:
            input_ids = d["input_ids"].to(device)
            attention_mask = d["attention_mask"].to(device)
            targets = d["targets"].to(device)

            # Get model ouptuts
            outputs = model(
                input_ids=input_ids,
                attention_mask=attention_mask,
            return_dict=True)

            _, preds = torch.max(outputs, dim=1)
            loss = loss_fn(outputs, targets)

            correct_predictions += torch.sum(preds == targets)
            losses.append(loss.item())

    return correct_predictions.double() / n_examples, np.mean(losses) 

生成式 AI 将如何影响产品工程团队

原文:towardsdatascience.com/how-generative-ai-will-impact-product-engineering-teams-83a5eaa8fc60

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

作者使用 Midjourney 制作的图像

生成式 AI 编码和生产力工具对构建软件产品的团队会产生什么影响?

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

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

我认为我们现在可以安全地假设,关于生成式 AI 工具 —— 如 ChatGPT、Midjourney 和 Dall-E —— 将对我们的工作方式产生影响,已经没有多少争议。剩下的问题实际上是“影响会有多大?”和“何时发生?”

在过去几个月里,我经常发现,当我考虑技术团队的人员配置时,我最终会质疑一个我多年来一直可以依赖的相当稳定的模型。随着生成式 AI 工具的演变速度和投入资金的数量不断增加,有一个想法一直困扰着我:

下一代产品工程团队可能会比现在拥有更少的工程师。

在这六篇文章的系列中,我将探讨当前构建数字产品的团队的现状和结构,最近几个月显著的变化,以及新一代 AI 编码工具对团队本身可能产生的影响。到文章结束时,技术、数据和产品领导者,以及工程师们,应该能够理解这一变化的范围,并有一个基础来考虑他们将如何应对。

这个系列主要针对对产品开发有一定理解的技术、数据和产品领导者,但我尽量在上下文中解释任何更技术性的概念。这不是对 AI 工具的详细探索或比较,而是它们可能对团队产生的影响。

今天我们所处的位置 — 5:1 比例

五比一。这是我对构建数字产品的团队中每个产品经理所需工程师数量的一个非常粗略的经验法则。

我的日常工作是作为顾问首席技术官(CTO),为高管团队提供建议,或直接与产品和技术领导者一起工作,帮助制定他们的战略和产品与技术团队的结构。当有人询问如何构建他们的产品工程团队时,我的回答通常是‘由五到九人组成的跨职能团队,具备交付业务成果所需的所有技能’。

无论公司规模如何,这些建议保持惊人的一致。当初创公司处于发展的初期阶段,工程团队迅速壮大时,十到十二人的团队通常是团队规模开始造成拖延和缺乏清晰度的点。在这个关键的拐点,当工作量超过单一团队的承载能力时,就该进行扩展了。一个团队分裂成两个团队,两个团队分裂成三个团队,每个团队有五到九人,如同有丝分裂一般。

在规模较大的公司中,随着多个团队部落的扩展,这种模型保持一致。我负责过的最大技术团队大约有 300 名工程师,属于一个约 450 人的产品和技术团队。即便在这样的整体规模下,团队层级的比例仍然主导着 5:1——大约五名工程师和一名产品经理,通常还有一些额外角色的独特变化,如数据科学家、机器学习工程师、设计师、用户体验(UX)专家、敏捷教练、质量保证(QA)人员和开发运维(dev ops),以填补人数需求。

什么构成了一个团队?

每个产品工程团队和每个首席技术官(CTO)都是不同的。虽然我个人偏好产品经理了解他们的客户,对商业结果负责,扫描创新前沿,并直接与团队合作以优先处理工作,但一些公司选择将产品负责人和产品经理角色分开。我倾向于选择跨团队工作的敏捷教练,但有些人更喜欢在团队内嵌的敏捷教练。有些人坚信测试和质量保证(QA)作为团队内的具体角色的重要性,但我更倾向于将质量责任推给编写代码的人,并尽可能地‘左移’。产品设计和用户体验研究可能在团队内,或者作为对团队的服务提供。有成千上万种配置,没有特定的对错答案,且始终需要根据组织的背景来调整。

如果有人让我创建一个理想的团队,我会要求一个产品负责人和一个技术负责人,他们具有相当的资历,并且彼此之间存在创意上的紧张关系。这两个角色的重要性在于代表(并对其不同观点进行争论)。客户满意度。成本。收益。风险。价值。正如我经常强调的:“产品决定构建什么,技术决定如何构建”。

过去 20 年左右,自从敏捷方法成为软件开发的主要方法以来,产品工程团队通常会有一个产品负责人和一些不同资历的工程师。在今天的世界中,工程师是产品交付的实际单位——没有工程师将需求转化为结果,我们就无法交付任何产品。

工程师不仅仅是构建新产品和功能;他们还负责保持现有产品的适用性和健康状态。工程师一周中的很大一部分时间可能会花在维护现有产品和系统上;修复漏洞、升级版本、回应小的变更请求和处理安全问题。他们剩余的时间可能会用来解决更有创意的问题,构建新能力或实施新的技术解决方案。此外,大多数工程团队都有需要时间学习和培训的初级工程师,还有更资深的工程师在不断变化的技术面前维持现状。

团队中产品经理的角色至关重要。如果工程师是我们价值交付的单位,产品经理也肩负着同样重要的责任;引导客户和利益相关者的需求,帮助团队以创意方式将挑战转化为解决方案,分析并展示结果的价值,确定价值交付的优先顺序,然后证明所有努力的投资回报。

在我们有五名工程师编写代码的团队中,“一个”几乎是理想的产品经理数量,可以保持工程师的参与度、生产力以及提供优质工作的供应。

直到现在为止。

产品工程中的生成式 AI 崛起

过去几个月我与许多 CEO 交谈时,他们专注于如何将“AI”硬生生地融入他们的产品中,表现出一种介于歇斯底里和绝望之间的热情。我对他们的建议是,应该将“真正”的 AI 工作留给那些拥有风险投资资金的人,自己则只需集成 OpenAI、微软、Salesforce、亚马逊和谷歌在未来几个月内提供的服务。

暂时搁置生成式 AI(我将用来泛指 GPT、LLMs 等)的新产品应用,我认为有一个更有趣且战略性的讨论——当生成式 AI 真正准备好投入生产时,我们的团队将会是什么样子。

面对美国的SAG-AFTRA 和 WGA 演员及编剧罢工,回顾麦肯锡在 2017 年对 AI 对劳动力影响的预测具有启示性。经过大量的研究和分析,麦肯锡对可能受到 AI 影响的职业进行了排序。麦肯锡提出,“创意工作者” 这一小而不断增长的艺术家、表演者和娱乐工作者类别将受到的影响最小,因为随着收入的增加,这类工作者的需求也会增加。麦肯锡认为,娱乐工作者就像现在罢工的编剧和演员一样,不仅不会避免 AI 的影响,反而会从中受益。 对于任何关于 AI 影响的预测(包括你正在阅读的这篇文章),这里有一个警示。

当然,麦肯锡的水晶球和其他人的一样并不神奇,我引用六年前的研究也有些古板。尽管如此,看到 AI 的影响没有被世界上最大的创意产业忽视,反而成为创意工会对制片厂和制作人的主要投诉之一,这还是令人感到振奋的。从对漫威《秘密入侵》中使用生成 AI 艺术的抗议到(有争议的)演员可能需要将其数字肖像权利永久转让给 AI 效果的说法,有一点非常明确:创意产业在不久的将来无疑将受到 AI 的影响。

为什么在一篇关于产品工程团队的文章中要绕道谈论创意产业呢?好吧,猜猜麦肯锡在其“安全”职业的大名单中,接下来最不容易受到 AI 影响的是什么?根据麦肯锡,接下来最不容易受到 AI 影响的群体将是*“IT 专业人员和其他技术专家、管理人员和高管,他们的工作不容易被机器替代”*。

麦肯锡早在 2017 年的观点是,“自动化对涉及管理人员、应用专业知识和社会互动的工作影响较小,因为目前机器无法与人类的表现相匹敌。” [link]。问题是,GPT 完全颠覆了算法在这些领域会挣扎的合理假设。2023 年的大型语言模型在广泛的话题中,包括软件开发,极具能力地展示了‘应用专业知识’的可信外观。

在第二部分中,我们探讨:

  1. 人工智能工具如 ChatGPT 如何彻底改变产品工程团队的编码方式——从生成用户故事到编写实际代码的过程。

  2. 开发者通常认为繁琐的任务,如编写测试和文档,现在可以被 AI 轻松高效地处理,使整个编码过程更加流畅。

  3. 深入探讨测试的重要性以及可能由深思熟虑的测试设计开始的提示工程应用的未来。

  4. 一种未来的愿景,即生成 AI 工具如何重新定义产品工程团队中的角色,以及对技术团队相比于产品管理的深远影响。

  5. 敬请关注这些发展将如何影响软件工程的世界以及成为开发者的本质。

点击这里阅读第二部分

本系列的其他文章:

附言:如果你喜欢这些关于团队的文章,可以看看我的Teamcraft 播客,在节目中我和我的共同主持人安德鲁·麦克拉伦与嘉宾讨论如何让团队更高效。

地理围栏如何塑造你周围的世界

原文:towardsdatascience.com/how-geofencing-is-shaping-the-world-around-you-12f6800df9c5?source=collection_archive---------11-----------------------#2023-02-07

地理围栏是一种基于位置的技术,为我们打开了新的可能性

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

·

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

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

DALL-E 所描绘的地理围栏概念(作者提供的图像)

随着我们迎接一个更加互联的世界,位置数据的使用日益重要。几乎地球上的每一个服务提供商或平台都在尝试通过了解用户的行为模式、参与度等来接触人们(和潜在客户)。而地理围栏技术在这一领域创造了一波新潮流。它允许组织向进入或离开特定地理区域的用户发送消息或警报。

近年来,地理围栏已成为企业、政府和个人越来越受欢迎的工具,它正在以多种方式改变我们周围的世界。凭借简单的实施过程和几乎可以覆盖全球所有电子设备的潜力,让我们来看看地理围栏如何塑造你周围的世界。

如果你想了解更多关于地理空间数据及其如何改变数据分析领域的信息,可以查看我关于该主题的博客 这里。还可以阅读我关于为什么需要使用地理包而不是 shapefile 或 geojson 的博客 这里

什么是地理围栏?

地理围栏的工作原理基于空间关系。该服务部署在应用程序或软件中,通过 GPS、RFID、Wi-Fi 或蜂窝数据收集位置信息。它在任何给定的时间点跟踪移动设备或 RFID 标签的位置是否在围绕地理位置设置的虚拟边界内。这个虚拟边界被称为地理围栏(因其类似于围绕房地产的围栏)。一旦有设备进入或离开地理围栏的触发,服务会运行预先编程的操作,包括向设备发送优惠券、通知、短信、安全警报,并启用某些参与功能以进行定向广告等。

地理围栏可以用于各种目的。它允许监控安全区域的活动。

地理围栏是如何工作的?

利用地理围栏技术的第一步是建立一个虚拟边界,围绕应用程序或软件中的指定位置。

跟踪模式

地理围栏使用多种技术来跟踪设备的位置并确定它们是否在虚拟边界内。最常见的跟踪模式包括:

a. GPS(全球定位系统): GPS 是一种基于卫星的导航系统,可以确定设备的精确位置。

b. 蜂窝网络: 蜂窝技术可以根据设备与基站的接近程度来跟踪设备的位置。

c. Wi-Fi: Wi-Fi 技术可以根据设备与 Wi-Fi 网络的接近程度来跟踪设备的位置。

d. 蓝牙: 蓝牙技术可以根据设备与蓝牙设备的接近程度来跟踪设备的位置。

e. RFID(射频识别): RFID 是一种利用无线电波与设备或物体上的标签进行通信的技术。

f. IP 地址: 可以通过设备的 IP 地址来确定其位置。

目标模式

一旦确定了设备的位置,地理围栏可以用来触发各种目标模式。一些最常见的目标模式包括:

a. 品牌应用: 品牌可以利用地理围栏向已安装其应用并位于特定地理区域的用户发送通知或促销信息。

b. 网络广告: 网络广告可以针对位于特定地理区域的用户,基于其设备的位置进行定向投放。

c. 短信: 短信可以发送给位于特定地理区域的用户。

d. 第三方应用: 第三方应用可以利用地理围栏向用户提供基于位置的信息和服务。

e. 社交媒体广告: 社交媒体广告可以针对位于特定地理区域的用户,基于其设备的位置进行定向投放。

地理围栏的应用

车队管理

  1. 车辆追踪: 地理围栏可以用于实时追踪卡车、出租车和其他车辆的位置,使车队管理者能够监控车队的移动情况,并确保车辆按照预期使用。

  2. 路线优化: 通过使用地理围栏,车队管理者可以优化车辆的路线,以减少燃油消耗,提高安全性,并提高效率。这可以通过在禁入或限制区域周围创建地理围栏,以及利用交通模式和道路状况数据来制定最有效的路线来实现。

  3. 驾驶行为监控: 地理围栏可以用于监控驾驶员的行为,包括速度、加速度和刹车。这些数据可以用于改善驾驶习惯,减少燃油消耗,提高整体安全性。

  4. 合规监控: 地理围栏可以用于确保车辆在合规规定范围内运行,如卡车运输规定和服务时间规则。这可以帮助车队管理者避免处罚和罚款,并保持良好的声誉。

  5. 客户位置追踪: 对于共享出行和出租车公司,地理围栏可以用于追踪客户的位置,并将其与最近的可用车辆匹配。这可以帮助改善客户体验并减少等待时间。

  6. 资产管理: 地理围栏可以用于管理车队的资产,包括拖车和货物集装箱。通过使用地理围栏,车队管理者可以跟踪这些资产的位置,并确保它们按照预期使用。

营销与广告

  1. 基于位置的广告: 地理围栏可以用于根据用户的位置投放定向广告。例如,当用户进入一个购物中心的地理围栏时,他们可能会收到该购物中心内商店的广告。

  2. 事件推广: 地理围栏可以用于通过根据用户的位置投放定向广告来推广活动。例如,当用户进入一个活动场地的地理围栏时,他们可能会收到该活动的广告。

  3. 人流量分析: 可以使用地理围栏跟踪和分析特定地点的人流量,如零售店或购物中心,以更好地了解消费者行为并改善营销策略。

  4. 客户重定向: 可以使用地理围栏根据客户的过去行为(如位置历史)来投放定向广告,从而提高广告的相关性和有效性。

  5. 基于位置的优惠: 可以使用地理围栏根据用户的位置提供基于位置的优惠,如折扣和优惠券。

  6. 客户细分: 可以使用地理围栏根据客户的地理位置(如城市、州或区域)进行客户细分,以提高广告的相关性和有效性。

安全

  1. 无人机管理: 可以使用地理围栏来管理无人机的操作,例如,通过在无人机飞行禁区周围创建虚拟边界。这可以帮助防止无人机干扰其他飞行器或进入受限空域,从而提高整体安全性。

  2. 执法: 可以被执法机构用来监控高犯罪率区域并提高公共安全。通过在已知犯罪高发区域创建地理围栏,执法部门可以实时接收到犯罪发生的警报,并更迅速地做出回应。

  3. 物理安全: 可以使用地理围栏来监控高价值地点的物理安全,如政府大楼、核电站和其他关键基础设施。通过在这些地点周围创建地理围栏,安保人员可以实时接收到未经授权人员进入区域的警报,并更迅速地做出回应。

  4. 活动安全: 可以使用地理围栏来提升大型活动的安全性,如音乐会、体育赛事和政治集会。通过在活动场地周围创建地理围栏,安保人员可以实时监控人群移动,并在出现安全威胁时迅速做出反应。

区域划分

  1. 旅游: 可以使用地理围栏来促进特定区域的旅游活动,通过在热门旅游景点(如国家公园、历史遗址和文化景点)周围创建地理围栏。当游客进入地理围栏时,可以接收到定向广告、促销信息及其他营销消息。

  2. 国家公园: 可以使用地理围栏通过在特定区域(如徒步旅行小径、野餐区和野生动物保护区)周围创建地理围栏来提升国家公园的游客体验。游客可以实时获取关于公园及其景点的信息,如小径地图、野生动物 sightings 和公园规定。

  3. **度假村:**地理围栏可以通过在度假村区域内创建地理围栏来提升度假体验,例如酒店、餐厅和水疗中心。客人可以实时获取有关度假村及其设施的信息,例如客房服务菜单、水疗项目和每日活动。

  4. **养老院:**地理围栏可以通过在设施周围创建地理围栏来增强养老院居民的安全性和生活质量。如果居民离开设施,护理人员可以实时接收警报,这有助于防止漫游和脱逃。

自动驾驶

  1. **车辆安全:**地理围栏可以用于通过在车辆不应进入的区域创建虚拟边界来确保自动驾驶车辆的安全操作。例如,可以在危险区域设置地理围栏,如施工现场、步行专用区和敏感环境。

  2. **自动驾驶区:**地理围栏可以用于指定仅限自动驾驶车辆的区域,如机场、港口和工业园区。这使得自动驾驶车辆能够在没有人驾车干扰的情况下运行,并可以改善交通流量和减少事故风险。

  3. **基于地理位置的服务:**地理围栏可以用于向自动驾驶车辆中的乘客提供基于位置的服务,例如导航、天气信息和交通更新。乘客可以实时获取有关车辆位置、目的地和预计到达时间的信息,以及有关周围地区的信息,例如交通状况、天气和兴趣点。

  4. **紧急响应:**地理围栏可以通过在事故现场和其他紧急地点创建地理围栏来支持紧急响应工作。自动驾驶车辆可以被派往现场提供即时援助,例如运输医疗用品、设备和人员。

资产管理

  1. **设备追踪:**地理围栏可以通过在工地、仓库和车间周围创建地理围栏来追踪设备,如建筑机械和车辆。这使得管理者能够实时监控设备的移动,减少盗窃和损坏的风险,并提高设备利用效率。

  2. **库存管理:**地理围栏可以用于通过在仓库和存储区域周围创建地理围栏来管理库存水平。这使得管理者能够实时监控库存的移动,减少盗窃和损失的风险,并提高库存管理的效率。

  3. **工人安全:**地理围栏可以通过在危险区域周围创建地理围栏来增强工人的安全,例如施工现场和矿区。这使得管理者能够实时监控工人的移动,减少事故风险,并在紧急情况下提高响应速度。

  4. 实时监控: 地理围栏可以用来实时监控资产的状态和健康,比如建筑机械和车辆。这使得管理者能够迅速应对任何问题,如故障和维护需求,减少停机时间并提高操作效率。

  5. 合规监控: 地理围栏可以用来监控法规遵守情况,如健康和安全法规,通过在受监管区域周围创建地理围栏。这使得管理者能够实时监控合规情况,减少罚款风险并提高整体合规性。

家庭自动化

  1. 家庭安全: 地理围栏可以用来增强家庭安全,通过在家周围创建地理围栏。这使得房主能够实时监控个人和车辆的移动,降低入侵和盗窃的风险。

  2. 智能家居控制: 地理围栏可以用来控制智能家居设备,如灯光、恒温器和家电,通过在家周围创建地理围栏。这使得房主可以自动化日常操作,比如离家时关灯,或到家时调整温度。

  3. 车辆追踪: 地理围栏可以用来追踪车辆的位置,比如汽车、摩托车和自行车,通过在家、工作地点或常去的地方创建地理围栏。这使得房主能够实时监控车辆的移动,降低盗窃风险并提高车辆利用效率。

  4. 提醒: 地理围栏可以用来根据位置设置提醒,比如到家时提醒服药,或离开工作地点时提醒去买杂货。

  5. 物联网集成: 地理围栏可以用来集成物联网(IoT)设备,如智能锁、摄像头和传感器,通过在家周围创建地理围栏。这使得房主能够实时监控设备的状态和健康,减少问题风险并提高操作效率。

社交网络

  1. 社交网络: 地理围栏可以用来提升社交网络体验,通过基于位置让用户找到并与他人连接。例如,约会应用可能会利用地理围栏根据用户之间的距离进行匹配。

  2. 基于位置的游戏: 地理围栏可以用来增强基于位置的游戏,如宝可梦 GO,通过让玩家根据位置与虚拟物体互动。

  3. 基于位置的奖励: 地理围栏可以用来根据位置向用户提供奖励。例如,奖励计划可能会利用地理围栏在用户进入参与商店周围的地理围栏时提供积分或优惠券。

设置地理围栏

  1. 定义地理围栏: 设置地理围栏的第一步是定义虚拟边界或地理围栏。这可以通过在地图上绘制一个圆圈或多边形,或使用一组 GPS 坐标来定义边界来完成。

  2. 选择跟踪方法: 一旦定义了地理围栏,下一步是选择跟踪方法。这将取决于具体的使用案例和需要跟踪的设备。例如,GPS 可能是跟踪户外位置的最佳选择,而 Wi-Fi 或蓝牙可能更适合室内跟踪。

  3. 与地理围栏平台集成: 要使用地理围栏,需要将其与地理围栏平台集成。该平台通常包括一个用于管理地理围栏、跟踪设备和目标营销活动的仪表板。

  4. 安装跟踪软件: 如果使用 GPS、Wi-Fi、蓝牙或其他跟踪模式,跟踪软件需要安装在需要跟踪的设备上。

  5. 测试和调整: 在设置好地理围栏平台后,重要的是测试系统以确保其正常运作。这可能包括调整地理围栏的大小和位置,或细化目标营销活动。

  6. 启动营销活动: 一旦地理围栏平台经过测试和调整,就可以启动目标营销活动。这可能包括向进入地理围栏的用户发送通知或促销信息,或向地理围栏内的用户展示基于位置的广告。

隐私问题

地理围栏是一项强大的技术,但也引发了诸多隐私问题,包括:

  1. 跟踪你的定位: 地理围栏依赖于跟踪用户的位置,以便提供定向广告、优惠和其他类型的内容。这种跟踪可以揭示用户的位置信息和日常习惯,可能会被用于恶意目的,例如身份盗窃或监视。

  2. 个人数据收集: 地理围栏需要收集个人数据,例如用户的位置,以便正常运作。这些个人数据可能容易受到泄露或未经授权的访问,从而导致敏感信息落入不当之手。

  3. 垃圾邮件: 地理围栏可能会导致用户被大量不必要的广告和优惠轰炸,这可能会造成烦扰并侵害他们的隐私。此外,恶意行为者可以利用地理围栏来发送垃圾邮件或钓鱼内容,这可能使用户面临身份盗窃或其他类型的诈骗风险。

  4. Strava 热图揭示美国军事位置: 2018 年,揭露了健身追踪应用 Strava 通过其热图功能暴露了关于美国军事位置和行动的敏感信息。这突显了使用地理围栏和其他基于位置的技术来揭示敏感信息的风险。

  5. **在频繁访问医院时对疾病的预测:**地理围栏技术在用于预测疾病或健康状况时也可能引发隐私问题。例如,如果一个人频繁访问医院,地理围栏技术可能预测他们有某种特定的健康状况,这可能是他们不希望公开的敏感信息。

用户了解这些隐私风险非常重要,公司需要实施强有力的隐私保护措施,以减轻这些风险,保护用户的个人数据和隐私。

结论

总之,地理围栏技术正在以多种方式改变我们周围的世界。从营销和广告到公共安全和农业,地理围栏对多个行业产生了重大影响。随着技术的不断发展,我们可能会看到未来更多创新和激动人心的应用。无论你是企业、政府还是个人,地理围栏都是一个可以帮助你充分利用地理位置并改善日常生活的工具。

谷歌如何利用虚假数据集来训练生成音乐 AI

原文:towardsdatascience.com/how-google-used-fake-datasets-to-train-generative-music-ai-def6f3f71f19

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

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

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

图片由 James Stamler 提供,来源于 Unsplash

在这篇文章中,我们探讨了谷歌训练其杰出的文本到音乐模型(包括 MusicLM 和 Noise2Music)的创新方法。我们将深入了解“虚假”数据集的概念以及这些突破性模型如何利用它们。如果你对这些技术的内部运作及其对推动音乐 AI 的影响感到好奇,那么你来对地方了。

标签化音乐数据的缺乏

像 ChatGPT 或 Bard 这样的语言模型(LLMs)在大量非结构化文本数据上进行训练。虽然收集数百万个网站的内容可能会耗费大量计算资源,但公共网络上有丰富的训练数据。相比之下,像 DALL-E 2 这样的文本到图像模型需要完全不同类型的数据集,即配有相应描述的图像对。

同样地,文本到音乐模型依赖于带有音乐内容描述的歌曲。然而,与图片不同,互联网上很难找到标签化的音乐。有时,像乐器、风格或情绪这样的元数据是可以获得的,但完整的深入描述则极其难以获取。这对研究人员和公司收集数据以训练生成音乐模型构成了严重问题。

2023 年初,谷歌研究人员通过突破性的模型 MusicLM 和 Noise2Music 引起了音乐 AI 领域的广泛关注。然而,在音乐家中,对于这些模型的数据如何收集知之甚少。让我们一起深入探讨这个话题,了解谷歌音乐 AI 研究中使用的一些技巧。

谷歌如何克服数据稀缺

弱关联标签

对于 MusicLM 和 Noise2Music,谷歌依赖于另一个叫做 MuLan 的模型,该模型经过训练可以计算任何音乐片段和任何文本描述之间的相似性。为了训练 MuLan,谷歌使用了我们所说的“弱关联标签”。他们没有仔细策划一个高质量文本描述的音乐数据集,而是故意采取了不同的方法。

首先,他们从 YouTube 上的 4400 万个音乐视频中提取了 30 秒的片段,得到 37 万小时的音频。然后,音乐被标记上与视频相关的各种文本:视频标题和描述、评论、播放列表的名称等。为了减少数据集中的噪音,他们使用了一个大型语言模型来识别哪些关联文本信息包含与音乐相关的内容,并丢弃所有不相关的内容。

在我看来,弱关联标签不能被视为“伪造”数据集,因为文本信息仍然是由真实的人撰写的,并且与音乐在某种程度上确实有关。然而,这种方法确实优先考虑了数量而非质量,这在过去曾引起大多数机器学习研究人员的关注。而谷歌才刚刚开始……

伪标签

Noise2Music 是一个基于扩散技术的生成音乐 AI,这种技术也用于像 DALL-E 或 Midjourney 这样的图像生成模型。

为了训练 Noise2Music,谷歌将之前的方法推向极致,从弱关联标签过渡到完全人工标签。在他们称之为“伪标签”的方法中,作者采用了一种出色的方式来收集音乐描述文本。他们让一个大型语言模型(LaMDA)为 15 万首热门歌曲写多个描述,结果得到了 400 万个描述。以下是一个描述的示例:

皇后乐队的《Don’t Stop Me Now》:这首充满活力的摇滚歌曲由钢琴、贝斯吉他和鼓构成。歌手们充满激情,准备好出发,充满了振奋的感觉。

随后,研究人员移除了歌曲和艺术家的名字,生成的描述理论上可以适用于其他歌曲。然而,即使有了这些描述,研究人员仍然需要将它们与合适的歌曲匹配,以获得一个大规模标注的数据集。这时,MuLan,这个经过弱关联标签训练的模型,发挥了作用。

研究人员收集了一个大规模的未标记音乐数据集,总计 34 万小时的音乐。对于这些音乐曲目中的每一个,他们利用 MuLan 来识别最匹配的人工生成的歌曲描述。本质上,每段音乐并不是映射到描述歌曲本身的文本,而是映射到描述类似音乐的文本。

为什么这有效?

问题

在传统的机器学习中,分配给每个观察(在这种情况下是音乐片段)的标签理想情况下应该代表客观的真相。然而,音乐描述本质上缺乏客观性,这提出了第一个问题。此外,通过使用音频到文本映射技术,标签不再反映对歌曲发生的事情的“真实”表示。它们没有提供对音乐的准确描述。鉴于这些明显的缺陷,人们可能会怀疑为什么这种方法仍然能够产生有用的结果。

偏差 vs. 噪声

当数据集的标签分配不准确时,可能有两个主要原因:偏差和噪声。偏差指的是标签在某种特定方式上的一致性倾向不真实。例如,如果数据集经常将器乐曲标记为歌曲,但从不将歌曲标记为器乐曲,这就显示了对预测存在人声的偏向。

另一方面,噪声表示标签的一般变异性,无论方向如何。例如,如果每一首曲目都标记为“悲伤的钢琴曲”,那么数据集就会严重偏向,因为它对许多歌曲提供了一致的不准确标签。然而,由于它对每个曲目应用相同的标签,因此数据集中没有变异性,也就没有噪声。

通过将曲目映射到为其他曲目写的描述文本,我们引入了噪声。这是因为,对于大多数曲目而言,数据集中不太可能存在完美的描述。因此,大多数标签都稍有偏差,即不真实,这就产生了噪声。然而,这些标签是否有偏见?

由于现有的描述是为流行歌曲生成的,因此可以合理地假设这些描述池倾向于(西方)流行音乐。尽管如此,基于 150k 首独特歌曲的 400 万条描述,仍然可以期望有多样化的描述可供选择。此外,大多数标记的音乐数据集也表现出相同的偏向,因此这并不是这种方法相较于其他方法的独特劣势。真正使这种方法与众不同的是引入了额外的噪声。

噪声在机器学习中可以是可以接受的

在偏向的数据集上训练机器学习模型通常不是一种理想的方法,因为这会导致模型学习和复制对任务的偏见理解。然而,在无偏但有噪声的数据上训练机器学习模型仍然可以获得令人印象深刻的结果。让我用一个例子来说明。

请考虑下图,其中展示了两个数据集,由橙色和蓝色点组成。在没有噪声的数据集中,蓝色和橙色点是完全可分的。然而,在有噪声的数据集中,一些橙色点已移动到蓝色点簇中,反之亦然。尽管有了这些附加噪声,如果我们检查经过训练的模型,会发现两者识别的模式大致相同。这是因为即使在存在噪声的情况下,AI 也会学习识别最优模式,以尽可能减少错误。

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

一个 AI 模型在无噪声和有噪声数据上训练的例子。图像生成使用了 Tensorflow Neural Network Playground

这个例子表明,AI 确实可以从噪声数据集中学习,例如谷歌生成的数据集。然而,主要的挑战在于数据集的噪声越大,所需的训练数据量就越大,以有效地训练模型。这一理由得到了支持,因为噪声数据集相对于大小相同的无噪声数据集,固有地包含较少有价值的信息。

结论

总之,谷歌采用了创新技术来应对训练其生成音乐 AI 模型时有限的标记音乐数据的问题。他们为 MuLan 使用了弱相关标签,利用了来自各种音乐视频相关来源的文本信息,并使用语言模型来过滤掉无关数据。在开发 Noise2Music 时,他们通过为热门歌曲生成多个描述并使用预训练模型将其映射到合适的曲目,从而引入了伪标签。

虽然这些方法可能偏离了传统的标记方法,但它们仍然证明了有效性。尽管引入了噪声,模型仍然能够学习并识别最佳模式。尽管使用伪造数据集可能被认为是不寻常的,但它突显了现代语言模型在创建大规模和有价值的数据集方面的巨大潜力。

我写了很多关于音乐和 AI 的文章。以下是你可能喜欢的三篇文章:

参考文献

[1] Huang 等人 (2022)。Mulan:音乐音频与自然语言的联合嵌入。 arxiv.org/abs/2208.12415

[2] Agostinelli 等人 (2023)。MusicLM:从文本生成音乐。 arxiv.org/abs/2301.11325

GPT 模型的工作原理

原文:towardsdatascience.com/how-gpt-models-work-b5f4517d5b5

了解 OpenAI 的 GPT 模型背后的核心概念

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

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

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

图片来源:KOMMERSUnsplash

介绍

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

生成式语言模型的工作原理

让我们开始探索生成式语言模型的工作原理。最基本的想法如下:它们将n个标记作为输入,生成一个标记作为输出。

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

这看似是一个相当简单的概念,但为了真正理解它,我们需要知道什么是标记。

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

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

你还可以使用 OpenAI 的开源 tiktoken 库通过 Python 代码进行分词。OpenAI 提供了几种不同的分词器,每种分词器的行为略有不同。在下面的代码中,我们使用了用于“davinci”(一个 GPT-3 模型)的分词器,以匹配你在 UI 中看到的行为。

import tiktoken

# Get the encoding for the davinci GPT3 model, which is the "r50k_base" encoding.
encoding = tiktoken.encoding_for_model("davinci")

text = "We need to stop anthropomorphizing ChatGPT."
print(f"text: {text}")

token_integers = encoding.encode(text)
print(f"total number of tokens: {encoding.n_vocab}")

print(f"token integers: {token_integers}")
token_strings = [encoding.decode_single_token_bytes(token) for token in token_integers]
print(f"token strings: {token_strings}")
print(f"number of tokens in text: {len(token_integers)}")

encoded_decoded_text = encoding.decode(token_integers)
print(f"encoded-decoded text: {encoded_decoded_text}")
text: We need to stop anthropomorphizing ChatGPT.
total number of tokens: 50257
token integers: [1135, 761, 284, 2245, 17911, 25831, 2890, 24101, 38, 11571, 13]
token strings: [b'We', b' need', b' to', b' stop', b' anthrop', b'omorph', b'izing', b' Chat', b'G', b'PT', b'.']
number of tokens in text: 11
encoded-decoded text: We need to stop anthropomorphizing ChatGPT.

你可以在代码的输出中看到,这个分词器包含 50,257 个不同的标记,每个标记在内部映射为一个整数索引。给定一个字符串,我们可以将其拆分为整数标记,并将这些整数转换为它们对应的字符序列。对字符串进行编码和解码应始终返回原始字符串。

这给你一个关于 OpenAI 分词器如何工作的良好直觉,但你可能会想知道他们为何选择这些标记长度。让我们考虑一些其他的分词选项。假设我们尝试最简单的实现,即每个字母都是一个标记。这使得将文本拆分为标记变得容易,并且保持不同标记的总数较小。然而,我们无法编码像 OpenAI 方法那样多的信息。如果我们在上述示例中使用基于字母的标记,11 个标记只能编码“我们需要”,而 11 个 OpenAI 的标记可以编码整个句子。事实证明,当前的语言模型对它们可以接收的最大标记数量有限。因此,我们希望在每个标记中尽可能地打包更多信息。

现在让我们考虑一种情况,每个单词都是一个标记。与 OpenAI 的方法相比,我们只需七个标记即可表示相同的句子,这似乎更高效。而且按单词拆分也很容易实现。然而,语言模型需要一个完整的标记列表,以便处理它们可能遇到的标记,这对于整个单词来说是不可行的——不仅因为词典中的单词太多,还因为难以跟上特定领域的术语和任何新发明的词汇。

因此,OpenAI 选择了这两种极端之间的某种解决方案也就不足为奇了。其他公司也发布了采用类似方法的分词器,例如 Google 的 Sentence Piece

现在我们对令牌有了更好的理解,让我们回到原始图示,看看是否可以更好地理解它。生成模型接受 n 个令牌,这些令牌可以是几个词、几个段落或几页内容。它们输出一个单一的令牌,这可能是一个短词或词的一部分。

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

现在这就更有意义了。

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

例如,如果我向我的模型输入“我们需要”,算法可能会产生如下结果:

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

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

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

模型是如何产生概率分布的?这就是训练阶段的目的。在训练过程中,模型接触了大量文本,并且其权重被调整以预测好的概率分布,给定一系列输入令牌。GPT 模型是用互联网上的大量数据进行训练的,因此它们的预测反映了它们见过的信息的混合。

你现在对生成模型背后的理念有了很好的理解。请注意,我只是解释了这个理念,尚未给出具体的算法。事实上,这个理念已经存在了几十年,并且多年来已经用几种不同的算法实现了。接下来我们将看看其中的一些算法。

生成语言模型的简史

隐马尔可夫模型(HMMs)在 1970 年代变得流行。它们的内部表示编码了句子的语法结构(名词、动词等),并在预测新单词时使用这些知识。然而,由于它们是马尔可夫过程,它们在生成新标记时只考虑最新的标记。因此,它们实现了“n 个标记输入,一个标记输出”想法的非常简单版本,其中 n = 1. 结果是,它们不会生成非常复杂的输出。我们来看以下示例:

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

如果我们将“ The quick brown fox jumps over the”输入到语言模型中,我们会期待它返回“lazy”。然而,HMM 只会看到最后一个标记“the”,而且只有这么少的信息,它不太可能给出我们期望的预测。随着人们对 HMMs 的实验,变得明显的是,语言模型需要支持多个输入标记以生成好的输出。

N-gram 在 1990 年代变得流行,因为它通过使用多个标记作为输入解决了隐马尔可夫模型(HMMs)的主要局限性。一个 n-gram 模型可能在预测前一个示例中的单词“lazy”时表现得非常好。

最简单的 n-gram 实现是基于字符的二元模型(bi-gram),它能够根据一个字符预测序列中的下一个字符。你可以用几行代码创建一个这样的模型,我鼓励你试试。首先,计算训练文本中不同字符的数量(我们称之为 n),并创建一个 n x n 的二维矩阵,初始值为零。每对输入字符可以用来在这个矩阵中定位一个特定的条目,通过选择对应第一个字符的行和第二个字符的列。当你解析训练数据时,对于每对字符,你只需将对应矩阵单元的值加一。例如,如果你的训练数据包含单词“car”,你会在“c”行和“a”列的单元格中加一,然后在“a”行和“r”列的单元格中加一。一旦你积累了所有训练数据的计数,将每一行转换为概率分布,方法是将每个单元格的值除以该行的总和。

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

然后,为了做出预测,你需要提供一个起始字符,例如“c”。你查找与“c”行对应的概率分布,并从中采样以生成下一个字符。然后,你取出你生成的字符,重复这个过程,直到达到停止条件。高阶 n-gram 遵循相同的基本思想,但它们能够通过使用 n 维张量查看更长的输入标记序列。

N-gram 简单易实现。然而,由于矩阵的大小随着输入标记数量的增加而指数级增长,它们在处理大量标记时的扩展性较差。并且对于仅有少量输入标记的情况,它们无法生成良好的结果。因此,需要一种新技术来继续在这一领域取得进展。

在 2000 年代,循环神经网络(RNNs)变得相当流行,因为它们能够接受比以前技术更多的输入标记。特别是,LSTMs 和 GRUs,这些是 RNNs 的类型,变得广泛使用并证明能够生成相当不错的结果。

RNNs 是一种神经网络,但与传统的前馈神经网络不同,它们的架构可以适应任何数量的输入并生成任何数量的输出。例如,如果我们给 RNN 输入标记“We”,“need”和“to”,并希望它生成更多标记直到达到完整的句点,RNN 可能具有以下结构:

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

上述结构中的每个节点具有相同的权重。你可以把它看作是一个单一的节点,连接到自身并重复执行(因此叫“递归”),或者可以按照上图中的扩展形式来理解。LSTMs 和 GRUs 相较于基础 RNNs 增加的一个关键能力是存在一个从一个节点传递到下一个节点的内部记忆单元。这使得后续节点能够记住前面节点的某些方面,这对于生成良好的文本预测是必不可少的。

然而,RNNs 在处理非常长的文本序列时存在不稳定问题。模型中的梯度往往会呈指数级增长(称为“梯度爆炸”)或减小为零(称为“梯度消失”),这阻碍了模型继续从训练数据中学习。LSTMs 和 GRUs 缓解了梯度消失问题,但并没有完全消除。因此,即使在理论上它们的架构允许任意长度的输入,但在实践中存在长度限制。文本生成的质量再次受到算法支持的输入标记数量的限制,需要新的突破。

在 2017 年,论文 介绍了 Transformers,谷歌发布了这一技术,我们进入了文本生成的新纪元。Transformers 的架构允许输入标记数量的大幅增加,消除了 RNNs 中出现的梯度不稳定问题,并且高度可并行化,这意味着它能够利用 GPU 的强大性能。如今,Transformers 被广泛使用,它们是 OpenAI 为其最新的 GPT 文本生成模型所选择的技术。

Transformers 基于“注意力机制”,它允许模型对某些输入给予比其他输入更多的关注,无论这些输入在输入序列中的位置如何。例如,我们来考虑以下句子:

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

在这种情况下,当模型预测动词“bought”时,它需要与动词“went”的过去式进行匹配。为了实现这一点,它必须非常关注“went”这个词。实际上,它可能会比对“went”这个词的注意力更强,即使“went”在输入序列中出现得更早。

GPT 模型中的这种选择性注意行为得益于 2017 年论文中的一个新颖概念:使用“掩码多头注意力”层。让我们来解析这个术语,并深入探讨它的每个子术语:

注意力: “注意力”层包含一个权重矩阵,表示输入句子中所有标记位置对之间的关系强度。这些权重在训练过程中学习得到。如果对应一对位置的权重大,则这两个位置上的标记会相互影响很大。这是使得 Transformer 能够对一些标记给予更多关注的机制,无论这些标记在句子中出现的位置如何。

掩码: 如果注意力层的矩阵仅限于每个标记位置与输入中的早期位置之间的关系,则称为“掩码”。这就是 GPT 模型在文本生成中使用的方式,因为一个输出标记只能依赖于它之前的标记。

多头: Transformer 使用了一个带有掩码的“多头”注意力层,因为它包含多个并行工作的掩码注意力层。

LSTM 和 GRU 的记忆单元也使得后续的标记能够记住一些早期标记的方面。然而,如果两个相关的标记距离非常远,梯度问题可能会成为障碍。Transformers 没有这个问题,因为每个标记与所有在它之前的其他标记都有直接连接。

现在你已经了解了 GPT 模型中使用的 Transformer 架构的主要思想,让我们来看一下目前可用的各种 GPT 模型之间的区别。

不同 GPT 模型的实现方式

在撰写时,OpenAI 发布的三款最新文本生成模型是 GPT-3.5、ChatGPT 和 GPT-4,它们都基于 Transformer 架构。实际上,“GPT”代表“生成预训练变换器”。

GPT-3.5 是一个训练为完成式模型的 Transformer,这意味着如果我们给它几个词作为输入,它能够生成几个可能跟随这些词的词。

另一方面,ChatGPT 被训练为对话风格的模型,这意味着当我们像进行对话一样与它交流时,它的表现最佳。它基于与 GPT-3.5 相同的 transformer 基础模型,但经过对话数据的微调。然后,通过使用人类反馈的强化学习(RLHF)进一步微调,这是一种 OpenAI 在其 2022 InstructGPT 论文 中引入的技术。在这一技术中,我们将相同的输入提供给模型两次,得到两个不同的输出,并询问人类评分者哪个输出更好。这个选择然后用于通过微调来改进模型。这项技术使模型的输出与人类期望保持一致,并且对 OpenAI 最新模型的成功至关重要。

另一方面,GPT-4 可用于完成任务和对话,并且拥有全新的基础模型。该基础模型也经过 RLHF 微调,以更好地符合人类期望。

编写使用 GPT 模型的代码

你有两个选项来编写使用 GPT 模型的代码:你可以直接使用 OpenAI API,或者使用 Azure 上的 OpenAI API。无论哪种方式,你都使用相同的 API 调用,你可以在 OpenAI 的 API 参考 页面中了解这些调用。

两者之间的主要区别在于 Azure 提供了以下附加功能:

  • 自动化的负责任 AI 过滤器,减轻 API 的不道德使用

  • Azure 的安全功能,如私人网络

  • 区域可用性,以在与 API 交互时获得最佳性能

如果你在编写使用这些模型的代码,你需要选择你想使用的具体版本。以下是 Azure OpenAI 服务中当前可用版本的快速备忘单:

  • GPT-3.5: text-davinci-002, text-davinci-003

  • ChatGPT: gpt-35-turbo

  • GPT-4: gpt-4, gpt-4–32k

两个 GPT-4 版本的主要区别在于它们支持的 token 数量:gpt-4 支持 8,000 个 token,而 gpt-4–32k 支持 32,000 个 token。相比之下,GPT-3.5 模型仅支持 4,000 个 token。

由于 GPT-4 目前是最昂贵的选项,因此最好从其他模型之一开始,只有在需要时才升级。有关这些模型的更多详细信息,请查看 文档

结论

在这篇文章中,我们涵盖了所有生成性语言模型的基本原则,以及特别是 OpenAI 最新 GPT 模型的独特方面。

在过程中,我们强调了语言模型的核心理念:“n 个令牌输入,一个令牌输出。”我们探讨了令牌如何被拆分以及为何如此拆分。我们追溯了语言模型从早期的隐马尔可夫模型到近期的基于 Transformer 的模型的数十年演变。最后,我们描述了 OpenAI 最新的三个基于 Transformer 的 GPT 模型,每个模型的实现方式,以及如何编写代码利用这些模型。

到现在为止,你应该已经能够对 GPT 模型进行有深度的讨论,并在自己的编码项目中开始使用它们。我计划撰写更多关于语言模型的解释文章,请关注我,并告诉我你希望看到哪些话题!感谢阅读!

注意

所有图片均由作者提供,除非另有说明。你可以在本博客文章中出于任何目的使用原始图片,但需注明出处(链接到本文)。

GPT 的工作原理:使用一个药水的故事对注意力中的键、值、查询进行隐喻性的解释

原文:towardsdatascience.com/how-gpt-works-a-metaphoric-explanation-of-key-value-query-in-attention-using-a-tale-of-potion-8c66ace1f470?source=collection_archive---------0-----------------------#2023-06-17

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

来源:由 Midjourney 生成。

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

·

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

!更新:该帖子现在有一个 10 分钟的视频版本 可用

ChatGPT 的核心是 GPT 模型,它是使用变换器架构构建的。变换器的核心是注意力机制。对许多人来说,理解注意力中最难的概念是键、值和查询。在这篇文章中,我将用药水的类比来帮助理解这些概念。即使你已经机械地理解了变换器的数学部分,我希望通过这篇文章,你能从头到尾更直观地理解 GPT 的内部工作原理。

这个解释不需要数学背景。对于技术性强的读者,我在[…]中添加了更多的技术解释。你也可以安全地跳过[方括号]中的注释和像这样在引号块中的附注。在我的写作过程中,我编造了一些可读的人类解释,用于说明变换器模型的中间状态,以帮助解释,但 GPT 并不完全是这样思考的。

[当我谈论“注意力”时,我专指“自注意力”,因为这正是 GPT 背后的机制。但同样的类比也可以很好地解释“注意力”的一般概念。]

设置

GPT 可以输出连贯的段落内容,因为它非常擅长执行一个任务:“给定一个文本,下一个词是什么?”让我们角色扮演 GPT:“Sarah 躺在床上,感觉 ____”。你能填上这个空白吗?

其中一个合理的答案是*“疲倦”*。在接下来的内容中,我将详细说明 GPT 是如何得出这个答案的。(为了好玩,我将这个提示放入 ChatGPT 中,它从中写了一个简短的故事)。

类比:(键、值、查询),或(标签、药水、配方)

你将上述提示输入 GPT。在 GPT 中,每个词都配备了三样东西:键(Key)、值(Value)、查询(Query),这些值是在 GPT 模型训练期间从整个互联网的文本中学习到的。正是这三种成分的相互作用使得 GPT 能够在文本的上下文中理解一个词。那么它们究竟是做什么的呢?

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

来源:由作者创建。

让我们设立炼金术的类比。对于每一个词,我们有:

  • 药水(即“值”):药水包含了关于词的丰富信息。为了说明目的,假设词*“lies”的药水包含了如“疲倦;不诚实;如果是善意的谎言可能有积极的含义;……”的信息。词“lies”*可以有多重含义,例如“说谎”(与不诚实相关)或“躺下”(与疲倦相关)。只有在文本的上下文中,你才能真正了解其含义。目前,药水包含了两种含义的信息,因为它没有文本的上下文。

  • 炼金术师的配方(也称为“查询”):给定单词的炼金术师,例如*“谎言”,会查看所有附近的单词。他找到了一些与自己单词“谎言”*相关的单词,他的任务是用这些单词的药水填满一个空药瓶。炼金术师有一个配方,列出了确定他应该关注哪些药水的具体标准。

  • 标签(也称为“关键字”):每种药水(值)都有一个标签(关键字)。如果标签(关键字)与炼金术师的配方(查询)匹配得很好,炼金术师将会关注这种药水。

注意:炼金术师的药水调配

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

带标签的药水。来源:作者创作。

在第一步(注意),所有单词的炼金术师各自出发,去从相关单词中获取药水填满他们的药瓶。

以单词*“谎言”的炼金术师为例。他知道根据之前的经验——经过对整个互联网文本的预训练——有助于解释句子中“谎言”的单词通常是:“一些平面表面、与不诚实相关的词、与休息相关的词”*。他将这些标准写在他的配方(查询)中,并寻找其他单词药水上的标签(关键字)。如果标签与标准非常相似,他会将大量这种药水倒入他的药瓶中;如果标签不相似,他则会倒入少量或不倒入。

所以他发现*“床”的标签上写着“一个平面的家具”。这与他配方中的“一些平面表面”类似!他将“床”的药水倒入药瓶中。“床”*的药水(值)包含的信息有“疲惫、安静、昏昏欲睡、生病”。

单词*“谎言”的炼金术师继续搜索。他发现“still”的标签上写着“与休息相关”(以及“still”的其他含义)。这与他的标准“休息”相关,因此他将一部分“still”的药水倒入药瓶中,包含的信息有“休息、安静、静止”*。

他查看了*“on”,“Sarah”,“the”,“feeling”*的标签,发现它们不相关。因此,他没有将这些药水倒入他的药瓶。

记住,他也需要检查自己的药水。自己的药水*“谎言”的标签上写着“与休息相关的动词”,这与他的配方“休息”*相匹配。因此,他也将自己药水的一部分倒入药瓶中,包含的信息有“疲惫;不诚实;如果是善意的谎言,可能有积极的含义;……”。

在他完成检查文本中的单词任务时,他的药瓶已经满了。

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

来源:作者创作。

与原始的*“谎言”药水不同,这种混合药水现在考虑了这个特定句子的上下文。也就是说,它包含了很多“疲惫,精疲力竭”*的元素,只有一点点“不诚实”。

在这个任务中,炼金术师知道如何关注正确的词,并结合那些相关词的值。**这是“注意力”的隐喻步骤。**我们刚刚解释了 Transformer 的最重要方程,这是 GPT 的底层架构:

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

Q 是查询;K 是键;V 是值。来源:Attention is All You Need

高级笔记:

1. 每位炼金术师查看每个瓶子,包括他们自己的 [Q·K.transpose()]。

2. 炼金术师可以快速将他的配方(查询)与标签(键)匹配,并做出快速决定。[查询和键之间的相似性通过点积确定,这是一项快速操作。] 此外,所有炼金术师的任务都是并行进行的,这也有助于加快速度。[Q·K.transpose() 是矩阵乘法,可以并行处理。与按顺序计算的前身递归神经网络相比,速度是 Transformer 的优势特性。]

3. 炼金术师很挑剔。他只选择最好的几种药水,而不是混合所有药水。[我们使用 softmax 来压缩 Q·K.transpose()。softmax 将输入值拉向更极端的值,并将许多输入压缩到接近零。]

4. 在这个阶段,炼金术师不考虑单词的顺序。无论是“Sarah lies still on the bed, feeling”还是“still bed the Sarah feeling on lies”,填满的烧瓶(注意力的输出)将是相同的。[在没有“位置编码”的情况下,Attention(Q, K, V) 与单词位置无关。]

5. 烧瓶总是返回 100%满,没有多也没有少。[softmax 被归一化为 1。]

6. 炼金术师的配方和药水的标签必须使用相同的语言。[查询和键必须具有相同的维度才能进行点积运算以进行通信。值可以采用不同的维度,如果你愿意的话。]

7. 技术精明的读者可能会指出我们没有进行掩蔽。我不想用太多细节来混淆类比,但我会在这里解释。在自注意力中,每个词只能看到前面的词。因此,在句子*“Sarah lies still on the bed, feeling”中,“lies”只看到“Sarah”;“still”只看到“Sarah”和“lies”。* *“still”的炼金术师不能接触到“on”、“the”、“bed”“feeling”*的药水。

Feed Forward: 混合药水中的化学反应

直到此时,炼金术师只是将其他瓶子的药水倒入烧瓶中。换句话说,他将*“谎言”——“疲倦;不诚实;…”——作为均匀混合物倒入烧瓶中;他还不能将“疲倦”部分提取出来并丢弃“不诚实”*部分。[注意力只是将不同的 V 加在一起,经过 softmax 加权。]

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

来源:作者创建。

现在进入真正的化学过程(前馈)。炼金术师将一切混合在一起并进行合成。他注意到词语之间的互动,例如*“sleepy”“restful”等。他还发现“dishonesty”*只在一个药水中提到。他根据以往的经验知道如何让一些成分相互作用,以及如何丢弃那些一次性的成分。[前馈层是值的线性(然后是非线性)变换。前馈层是神经网络的构建块。你可以将其视为 Transformer 中的“思考”步骤,而早期的混合步骤只是“收集”。]

经过处理后的药水变得更适合预测下一个词。直观上,它在句子上下文中表现出该词的一些更丰富的特性,而与起始药水(值)相比,后者是脱离上下文的。

最终线性和 softmax 层:炼金术师的集会

我们如何从这里得到最终输出,即预测*“Sarah lies still on the bed, feeling ___”之后的下一个词是“tired”*?

到目前为止,每个炼金术师一直独立工作,只关注自己的词汇。现在,所有不同词汇的炼金术师汇集在一起,将他们的药水瓶按照原始词序排列,并呈现给 Transformer 的最终线性和 softmax 层。这是什么意思呢?这里,我们必须离开比喻。

这个最终线性层综合了不同词汇的信息。基于预训练数据,一个可能的学习是,紧接前一个词对预测下一个词是重要的。例如,线性层可能会重点关注最后一个药水瓶(“feeling”药水瓶)。

然后结合 softmax 层,这一步为词汇表中的每个词分配一个概率,表示在*“Sarah lies on the bed, feeling…”之后这个词的可能性。例如,非英语词汇的概率接近 0。像“tired”、 “sleepy”、 “exhausted”*这样的词将获得高概率。然后我们选择概率最高的词作为最终答案。

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

来源:作者创建。

总结

现在你已经构建了一个极简主义的 GPT!

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

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

来源:作者创建。

旁注:“解码器”这一术语是原始论文中的残余,因为 Transformer 最初用于机器翻译任务。你“编码”源语言到嵌入中,然后“解码”从嵌入到目标语言。

这是一个很好的停顿点。

如果你渴望了解更多,我们将在上述简约架构的基础上介绍另外两种变体:多头注意力和重复块。

多头注意力:许多炼金术士的集合

到目前为止,每个词只有一个炼金术士的配方、一个标签和一种药水。[对于每个词,每个查询、值、键是一个单一向量,而不是一个矩阵。] 但是,如果我们为每个词配备几组配方、标签、药水,我们可以获得更好的结果。对于参考,GPT 每个词使用 12 组(即 12 个“注意力头”)。也许对于每个词,第一组的炼金术士专注于分析情感,第二组的炼金术士专注于解析引用(“它”指的是什么),等等。

高级说明:情感炼金术士组只研究情感药水;他们不会知道如何处理其他集合的药水,也不会触及那些药水。[来自同一注意力头的 V、K、Q 是联合训练的。来自不同注意力头的 V、K、Q 在多头注意力步骤中不进行交互。]

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

来源:由作者创建。

每个词的 12 位炼金术士将他们的专门化、填充的瓶子一起[连接不同注意力头的输出]。作为一个群体,他们利用所有这些瓶子进行一次巨大的化学反应,并呈现出一种最终的药水[前馈,即线性层]。

高级说明:就像以前一样,在解码器块中,不同词的瓶子不会混合在一起。[前馈步骤是按位置进行的,这意味着它对每个词独立应用。]

这个比喻解释了原论文中的以下方程式和图表。

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

来源:Attention is All You Need

堆叠块:并且……重复!

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

来源:由作者创建。

为了获得更好的效果,我们将解码器块重复 N 次。对于参考,GPT 重复 12 次。直观上,你会希望在收集其他相关词汇的药水(注意力)和自己合成这些药水以获得意义(前馈)之间进行交替:收集,合成;收集,合成……

现在你可以了解炼金术……我的意思是……GPT!

人类劳动如何促进机器学习

原文:towardsdatascience.com/how-human-labor-enables-machine-learning-367feee8bc91?source=collection_archive---------2-----------------------#2023-10-31

技术与人类活动之间的大部分分隔是人为的——人们是如何让我们的工作成为可能的?

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

·

关注 发布在 Towards Data Science ·8 分钟阅读·2023 年 10 月 31 日

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

图片由 Dominik Scythe 提供,来源于 Unsplash

我们并没有足够谈论我们依赖多少人工工作来实现机器学习领域的激动人心的进展。事实是,技术和人工活动之间的划分是人为的。所有使模型产生的输入都是人类努力的结果,而所有输出以某种方式存在于影响人们。我今天使用这一专栏来谈论我们在某些特定领域忽视了人们对我们所做工作的重要性——不仅仅是那些编写代码的数据科学家。

技术与人工活动之间的划分是人为的,因为所有使模型产生的输入都是人类努力的结果,而所有输出以某种方式存在于影响人们。

生成数据

你几乎肯定已经知道这一点——LLMs 需要巨量的文本数据进行训练。我们通常以硬盘上的数百或数千 GB 的数据来考虑这个问题,但这有点抽象。一些报告指出,GPT-4 的训练数据大约有1 万亿个单词。每一个单词都是由一个人用他们自己的创造能力写出的。作为参考,《冰与火之歌》系列的第一本书大约有 292,727 个单词。因此,GPT-4 的训练数据大约是3,416,152 本书的长度。而这只是文本建模的一个例子——其他类型的模型,例如那些生成或分类多媒体的模型,也使用类似规模的海量数据。

当涉及到这些数据时,有几个方面需要考虑。首先,所有这些数据都是由人生成的,它不会凭空出现在我们的硬盘上。尊重和认可那些创造我们数据的人的工作是重要的,这是伦理问题,因为他们付出了努力,创造了我们正在受益的价值。但我们也有更自私的理由需要了解我们的数据来源。作为数据科学家,我们有责任了解我们向模型提供的材料作为示例,并深入理解它。如果我们忽视数据的来源,我们可能会在面对现实世界时,对我们的模型行为感到不愉快的惊讶。例如,在互联网论坛或社交媒体数据上训练 LLMs 会使这些模型面临复制这些空间最糟糕现象的风险,包括种族主义、仇恨言论等。在稍微不那么极端的例子中,我们知道模型受其训练数据的影响

如果我们忽视数据的来源,我们可能会在面对现实世界时,对我们的模型行为感到不愉快的惊讶。

标记数据

数据标注需要人工帮助。但是,标签到底是什么?从根本上说,标注数据意味着使用人类的洞察力为我们在数据中发现的内容分配值或判断。无论数据如何收集或创建,大多数机器学习用例都需要某种形式的标注。

这可能意味着简单地决定一个数据点是好是坏,确定词语是积极的还是消极的,创建衍生值,将记录划分为不同类别,确定哪些标签适用于图像或视频,或者其他无尽的任务。一个常见的例子是识别图像或其他多媒体中的文本,以改善字符识别模型。如果你使用过验证码,我敢打赌这听起来很熟悉——你已经做过数据标注工作。

理论上,LLM 本身不需要标注,因为我们从这些文本已经由真实人类生成的事实中推断出人类的质量,因此这些文本在本质上必须尽可能类似于人类输出。基本上,因为是人类写的,那么它在定义上就是一个可接受的示例供模型学习和模仿。这就是我们使用语义嵌入的地方——模型学习人类生成文本中的语言模式如何工作,并将这些模式量化为数学表示。但是正如我之前所描述的,我们仍然在选择哪些文本进入模型的过程,我们有责任理解和评估这些文本。

教学模型

强化学习使用人工干预来调整任务——意味着我们在模型基本上掌握了返回连贯答案的方式后,稍微调整模型对提示的响应,无论是文本、图像、视频还是其他内容。在一些主要的自动化预训练或基础训练之后,许多模型由人类进行微调,做出有时微妙的判断,判断模型是否达到了期望。 这是一项非常艰巨的任务,因为我们实际希望模型做到的细微差别可能非常复杂。这基本上是在大规模上以通过或不通过的方式进行 LLM 的校对。

正如我之前讨论的,许多现代模型寻求生成对人类用户最令人愉悦的内容——那些看起来正确且吸引人的东西。那么,有什么比让人类查看训练的中间阶段结果并决定结果是否符合描述,再告诉模型以便它做出更合适的选择,更好的训练方式呢?这不仅是最有效的方法,也可能是唯一有效的方法。

这基本上是在以通过或不通过的方式进行 LLM 的校对。

为什么这很重要

好吧,那又怎样呢?仅仅意识到真实的人们付出了大量的努力来实现我们的模型,这就够了吗?拍拍他们的背,表示感谢?不完全是,因为我们需要审视人类的影响对我们生成的结果意味着什么。作为数据科学家,我们需要对我们构建的东西与其所在世界之间的互动保持好奇心。

由于所有这些影响领域,人类的选择塑造了模型的能力和判断。我们将人类的偏见嵌入到模型中,因为人类创造、控制并判断所有相关材料。我们决定哪些文本将提供给模型进行训练,或者模型的哪个具体回应比另一个更差,而模型将这些选择固化为可以重复和再利用的数学表示。

这种偏见元素是不可避免的,但并不一定是坏事。试图创造一个完全摆脱人类影响的东西暗示人类的影响和人类本身是需要避免的问题,这在我看来是不公平的评估。同时,我们也应该现实地认识到人类偏见是我们模型的一部分,并抵制将模型视为超越我们人类缺陷的诱惑。例如,我们如何分配标签,导致我们有意识或无意识地赋予数据意义。无论是原创创意内容、数据标签,还是模型输出的判断,我们都在创建的数据中留下了我们思维过程和历史的痕迹。

试图创造一个完全摆脱人类影响的东西暗示人类的影响和人类本身是需要避免的问题,这在我看来是不公平的评估。

此外,在机器学习领域,人类的努力通常被视为服务于“真实”工作的,而不是自身具有意义。那些产生原创作品的人不再被视为独特的创造性个体,而只是被归入服务于模型的“内容生成者”。我们忽视了这些内容存在的本质人性和真实原因,即服务和赋能人类。与之前的观点一样,我们最终贬低了人们而崇拜技术,我认为这是愚蠢的。模型是人们的产物,旨在服务于人们,它们不是独立的目标。如果你构建了一个从未被使用和运行的模型,那还有什么意义呢?

数据是可再生资源吗?

另一个有趣的问题是:人类生成的纯净内容的风险成为模型能力的限制因素。也就是说,当我们的社会开始使用 LLM 生成数据,使用 Dall-E 生成图像,并且我们停止激励真实的人在没有这些技术的情况下进行创作时,我们需要训练新版本这些模型的万亿字词和山岳般的图像将被人为生成的内容所污染。那种内容,当然,来源于人类内容,但并不完全一样。我们尚未拥有很好的方法来区分由没有模型的人生成的内容,所以我们将难以判断我们未来模型的训练数据是否存在这种污染,以及污染的程度。

有人认为这其实不是什么大问题,训练模型时使用至少一部分人工内容不会有问题,但也有其他理论认为,当我们开始以这种方式掠夺人工生成的内容时,训练的基本过程将以称为模型崩溃的形式被根本性改变。在某些方面,这是模型影响了模型所依赖的世界的根本问题的一个例子,因此模型的行为定义上改变了模型本身。数据科学家对此也深有体会。这不仅仅适用于 LLM,任何模型都可能通过影响人们的行为来使自己失业,从而导致由于基础数据关系的变化而性能漂移。

你的模型影响了模型所依赖的世界,因此模型的行为定义上改变了模型本身。

即使我们没有在实际的人工数据上进行训练,还有许多学者在考虑我们的人的组成和创造过程是否会因接触到人工生成的内容而发生变化。如果你大量阅读 LLM 生成的文本,无论是在写作时获取模型的建议还是在互联网的一般环境中,这是否会微妙地改变你的写作方式?在社区层面上现在还为时尚早,但这是一个严重的问题。

人类影响是机器学习的一个事实——这是一个哲学问题。我们将机器学习视为一种纯科学事业,认为它作用于我们,这也是它对某些人来说似乎令人恐惧的原因之一。但实际上,正在创建的系统是人类干预和创造力的产物。创建和策划数据使得所有其他机器学习的可能性成为可能。在某种程度上,这应该让我们感到安慰,因为我们可以控制我们如何使用机器学习以及如何使用它。机器学习的过程是将数据片段之间的关系计算成数学表示,但数据是由人类产生的,并且在我们的控制之下。机器学习和人工智能不是某种外星的、抽象的力量——它们只是我们。

查看更多我的作品请访问 www.stephaniekirmer.com.

上述文章和参考资料,方便访问:

如何基于 AWS 构建级联数据管道(第一部分)

原文:towardsdatascience.com/how-i-built-a-cascading-data-pipeline-based-on-aws-997b212a84d2

自动化、可扩展且强大

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

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

今天我要分享一些我引以为豪的数据工程项目的经验。你将了解到我为何使用这些工具和 AWS 组件,以及我如何设计架构。

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

作者提供的图片

免责声明:本文本内容受到我与一个未命名实体的经验启发。然而,某些关键商业利益和细节已故意用虚构数据/代码替代或省略,以保持机密性和隐私。因此,实际涉及的商业利益的完整和准确程度保留。

前提条件

  1. 对 Python 的了解

  2. 对 AWS 组件的了解,如 DynamoDB、Lambda 无服务器、SQS 和 CloudWatch

  3. YAMLSAM CLI 的舒适编码体验

背景

假设你是一个数据工程师,需要不断更新数据仓库中的数据。例如,你需要定期与 Dunder Mifflin Paper Co. 的销售记录同步。(我知道这不是一个现实的场景,但请尽情享受 😃!)数据通过供应商的 API 发送给你,你需要确保分支、员工(实际上只考虑销售人员)和销售的信息是最新的。提供的 API 有以下 3 个路径:

  1. /branches ,接受分支名称作为查询参数以检索指定分支的元数据;

  2. /employees ,接受分支 ID 作为查询参数以检索某个分支所有员工的信息,响应中包括一个表示员工职业的键值对;

  3. /sales,接受员工 ID 作为查询参数来检索销售人员的所有销售记录,响应包括一个指示交易完成时间的键值对。

总的来说,API 的返回结果如下:

/branches 路径:

{
  "result": [
   {
     "id": 1,
     "branch_name": "Scranton",
     "employees": 50,
     "location": "Scranton, PA",
     ...
   }
 ]
}

/employees 路径:

{
"result": {
  "branch_id": 1,
  "employees": [
   {
     "id": 1234,
     "occupation": "data engineer",
     "name": "John Doe",
     ...
   },
   {
     "id": 1235,
     "occupation": "salesperson",
     "name": "Jim Doe",
     ...
   },
   ...
  ],
  ...
 }
}

/sales 路径:

{
  "result": {
  "employee_id": 1235,
  "sales: [
   {
     "id": 3972,
     "transaction_timestamp": "2023-01-01 23:43:23",
     ...
   },
   {
     "id": 4002,
     "transaction_timestamp": "2023-01-05 12:23:31",
     ...
   },
   ...
  ],
  ...
 }
}

期望你的最终工作将使数据分析师能够仅通过 SQL 查询来检索他们分析所需的数据。

思路

最终,我们将有 3 个不同的地方分别存储分支、销售人员和销售的数据,这一点说起来很简单。数据将通过访问特定的 API 路径来导入。然而,由于所有这些实体的标识符大多是自动生成的,实践者不大可能提前获得这些 ID。相反,由于我们通常可以找到分支名称,因此可以使用第一个路径来获取分支的元数据及其员工的 ID。然后,我们可以使用员工 ID 访问/employees路径,/sales路径也是如此。这就是我称这个流程为级联的原因。

为了确保我们的数据库大部分时间是最新的,必须足够频繁地执行这些操作。但另一方面,我们也需要考虑成本和潜在的 API 访问配额。因此,每小时运行一次是适当的,尽管可以说尚未达到最佳。

最后但同样重要的是,让我们讨论 AWS。首先,执行这些操作的代码将由AWS Lambda运行,因为它能够使用 200 多个 AWS 服务和应用程序作为触发器,包括SQSEventBridge。数据将通过SQS传递,作为 AWS 提供的最成熟的消息服务之一。最后,从 API 抓取的信息将存储在DynamoDB中。对于一些有经验的读者来说,利用DynamoDB作为数据仓库工具可能会令人困惑,因为这是一个 NoSQL 数据库服务,而数据仓库通常与 NoSQL 数据库不兼容。我确实意识到这一点,DynamoDB 表在这里将仅作为临时存储表,因为我可以利用其在键值/文档数据模型模式中的灵活性,然后最终将 JSON 格式的 API 检索数据转换为数据仓库记录。如果你对我实现 DynamoDB-S3 加载的细节感兴趣,可以查看这篇文章

实现

这是我最终工作的结构。

Cascading-ETL-pipeline
├── LICENSE
├── README.md
├── branches
│   ├── Pipfile
│   ├── Pipfile.lock
│   ├── lambda_function.py
│   ├── requirements.txt
│   └── service
│       ├── config.py
│       └── service.py
├── sales
│   ├── Pipfile
│   ├── Pipfile.lock
│   ├── lambda_function.py
│   ├── requirements.txt
│   └── service
│       ├── config.py
│       └── service.py
├── salespersons
│   ├── Pipfile
│   ├── Pipfile.lock
│   ├── lambda_function.py
│   ├── requirements.txt
│   └── service
│       ├── config.py
│       └── service.py
├── template.yml
└── utils.py

有 3 个文件夹(/branches/salespersons/sales),分别包含每个 lambda 函数的代码。Utils.py 是一个多功能文件,其中的函数、变量和类被全局应用。模板.yml 是我们将用来声明和部署 AWS 资源以建立数据管道的 AWS CloudFormation 模板。

每个文件夹中的Lambda_function.py是代码执行的入口函数:

import json
import logging
from pythonjsonlogger import jsonlogger
from service import service, config

# Load environment
ENV = config.load_env()

LOGGER = logging.getLogger()
# Replace the LambdaLoggerHandler formatter :
LOGGER.handlers[0].setFormatter(jsonlogger.JsonFormatter())
# Set default logging level
LOGGING_LEVEL = getattr(logging, ENV["LOGGING_LEVEL"])
LOGGER.setLevel(LOGGING_LEVEL)

def _lambda_context(context):
    """
    Extract information relevant from context object.

    Args:
        context: The context object provided by the Lambda runtime.

    Returns:
        dict: A dictionary containing relevant information from the context object.

    """
    return {
        "function_name": context.function_name,
        "function_version": context.function_version,
    }

# @datadog_lambda_wrapper
def lambda_handler(event, context):
    """
    Handle the Lambda event.

    Args:
        event(dict): The event object containing input data for the Lambda function.
        context(dict): The context object provided by the Lambda runtime.

    Returns:
        dict: A dictionary containing the response for the Lambda function.

    """
    LOGGER.info("Starting lambda executing.", extra=_lambda_context(context))
    service.main(event, ENV)
    LOGGER.info("Successful lambda execution.", extra=_lambda_context(context))
    return {"statusCode": 200}

/Service/config.py 返回template.yml中输入的环境变量:

import os
import sys
import logging

LOGGER = logging.getLogger(__name__)

def load_env():
    """Load environment variables.

    Returns:
       dict: A dictionary containing the loaded environment variables.

    Raises:
        KeyError: If any required environment variable is missing.

    Notes:
        - The function attempts to load several environment variables including:
        - If any of the required environment variables are missing, a KeyError is raised.
        - The function logs an exception message indicating the missing environment variable and exits the program with a status code of 1.
    """
    try:
        return {
            "LOGGING_LEVEL": os.environ["LOGGING_LEVEL"],
            "APP_ENV": os.environ["APP_ENV"],
            "SQS": os.environ["SQS"],
            "DB": os.environ["DB"],
        }
    except KeyError as error:
        LOGGER.exception("Enviroment variable %s is required.", error)
        sys.exit(1)

/Service/service.py 是我们实际处理数据的地方。基本上,该功能由一个或两个触发器(时间调度器)调用,在从数据源(API 或 SQS 队列)中检索数据之前。数据将以键值对的形式打包,如有需要,更新到相应的 DynamoDB 表中,然后该功能分发其成员的标识符(即,分支中的所有员工,销售人员的所有销售记录)。

/branches/service/service.py 为例。它的功能包括:

  1. 一旦被唤醒,立即从 API /branches 获取所有数据;

  2. 检查 DynamoDB 数据表中每个销售人员的个人信息是否存在和准确,如果不更新,则插入数据记录;

  3. 获取所有员工的 ID,并将其连同分支 ID 作为消息通过 SQS 队列传递给尾部函数(/salespersons)。

实际操作中,实施方式如下:

import logging, requests, sys
from utils import *
from boto3.dynamodb.conditions import Key

LOGGER = logging.getLogger(__name__)

def main(event, environment):
    """Process invoking event data and update the DynamoDB table based on specified branches.

    Args:
        event (dict): A JSON-formatted document that contains data for a Lambda function to process.
        environment (dict): A context object that provides methods and properties about the invocation, function and runtime environment.

    Returns:
        None

    Raises:
        SystemExit: If an exception occurs during the execution.

    Notes:
        - If `event` does not contain the 'branches' key, the function will default to processing information for all branches.
        - The function retrieves branch-specific information from a URL and updates the DynamoDB table accordingly.
        - The updated information is then delivered to an SQS queue for further processing.

    """
    LOGGER.info(event)

    if not event.get("branches"):
        # default to look up all branches if the value is an empty list
        branches = [
            "Scranton",
            "Akron",
            "Buffalo",
            "Rochester",
            "Syracuse",
            "Utica",
            "Binghamton",
            "Albany",
            "Nashua",
            "Pittsfield",
            "Stamford",
            "Yonkers",
            "New York",
        ]
    else:
        branches = event["branches"]  # should be an array

    queue = environment["SQS"]
    table = environment["DB"]

    try:
        for branch in branches:
            # go to a path that allows users to retrieve all information of the specified branch(es) based on input date range
            response = requests.get(
                url=f"www.dundermifflinpaper.com/branches/?branch={branch}"
            )
            response = response.json()
            branches = response.get("result")
            for result in branches:
                if not upToDate(
                    table,
                    Key("branch_id").eq(str(result["id"])),
                    result,
                    "branch_",
                ):
                    # only update DynamoDB table when it's NOT complete ingesting
                    update_info(table, result)

            deliver_message(queue, str({"branch": result["branch_id"]}))
            LOGGER.info(f"sending branch {result['branch_id']} for the next stage")

    except Exception as e:
        LOGGER.error(str(e), exc_info=True)
        sys.exit(1)

最后,我们需要为构建和部署做准备:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:  #   Type: String
  Environment:
    Type: String
Resources:
  # =========================================================================================
  # IAM ROLES, POLICIES, PERMISSIONS
  # =========================================================================================
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${AWS::StackName}-lambda-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
            - events.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AWSLambdaExecute
      - arn:aws:iam::aws:policy/AmazonSQSFullAccess
      - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
      Path: '/'
  LambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub '${AWS::StackName}-lambda-policy'
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: EventBusAccess
          Effect: Allow
          Action:
          - events:PutEvents
          Resource: '*'
        - Sid: LambdaInvokeAccess
          Effect: Allow
          Action:
          - lambda:InvokeFunction
          Resource: "*"
        - Sid: LogAccess
          Effect: Allow
          Action:
          - logs:CreateLogGroup
          - logs:CreateLogStream
          - logs:PutLogEvents
          Resource: arn:aws:logs:*:*:*
      Roles:
      - !Ref LambdaRole
  # =========================================================================================
  # AWS LAMBDA FUNCTIONS
  # ========================================================================================= 
  BranchCollector:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub branch-collector-${Environment}
      Handler: lambda_function.lambda_handler
      Runtime: python3.9
      CodeUri: branches/
      Description: updating branch info in our DynamoDB table
      MemorySize: 128
      Timeout: 900
      Role: !GetAtt LambdaRole.Arn
      Environment:
        Variables:
          LOGGING_LEVEL: INFO
          APP_ENV: !Ref Environment
          SQS: !Ref EmployeeQueue
          DB: !Sub branches-${Environment}
      DeadLetterQueue:
        Type: SQS
        TargetArn: 
          Fn::GetAtt: BranchFunctionDeadLetterQueue.Arn
      Events:
        StartScheduledEvent:
          Type: Schedule
          Properties:
            Schedule: rate(1 hour)

  SalespersonCollector:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub salesperson-collector-${Environment}
      Handler: lambda_function.lambda_handler
      Runtime: python3.9
      CodeUri: salespersons/
      Description: updating salesperson info in our DynamoDB table
      MemorySize: 128
      Timeout: 900
      Role: !GetAtt LambdaRole.Arn
      ReservedConcurrentExecutions: 5
      Environment:
        Variables:
          LOGGING_LEVEL: INFO
          APP_ENV: !Ref Environment
          SOURCE_SQS: !Ref EmployeeQueue
          TARGET_SQS: !Ref SaleQueue
          DB: !Sub salespersons-${Environment}
      DeadLetterQueue:
        Type: SQS
        TargetArn: 
          Fn::GetAtt: EmployeeFunctionDeadLetterQueue.Arn
      Events:
        StartScheduledEvent:
          Type: Schedule
          Properties:
            # every minute
            Schedule: rate(1 minute)

  SaleCollector:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub sale-collector-${Environment}
      Handler: lambda_function.lambda_handler
      Runtime: python3.9
      CodeUri: sales/
      Description: updating sales info in our DynamoDB table
      MemorySize: 128
      Timeout: 900
      ReservedConcurrentExecutions: 3
      Role:
        Fn::GetAtt:
        - LambdaRole
        - Arn
      Environment:
        Variables:
          LOGGING_LEVEL: INFO
          APP_ENV: !Ref Environment
          SQS: !Ref SaleQueue
          DB: !Sub sales-${Environment}
      DeadLetterQueue:
        Type: SQS
        TargetArn: 
          Fn::GetAtt: SaleFunctionDeadLetterQueue.Arn
      Events:
        StartScheduledEvent:
          Type: Schedule
          Properties:
            # every minute
            Schedule: rate(1 minute)

  # =========================================================================================
  # AWS DynamoDB TABLES
  # ========================================================================================= 
  BranchDynamoDBTable: 
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    Properties:
      BillingMode: PAY_PER_REQUEST 
      AttributeDefinitions: 
        - 
          AttributeName: "branch_id"
          AttributeType: "S"
      KeySchema: 
        - 
          AttributeName: "branch_id"
          KeyType: "HASH"
      StreamSpecification:
        StreamViewType: NEW_IMAGE
      TableName: !Sub branch-${Environment}

  SalespersonDynamoDBTable: 
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    Properties:
      BillingMode: PAY_PER_REQUEST 
      AttributeDefinitions: 
        - 
          AttributeName: "employee_id"
          AttributeType: "S"
        - 
          AttributeName: "branch_id"
          AttributeType: "S"
      KeySchema: 
        - 
          AttributeName: "employee_id"
          KeyType: "HASH"
        - 
          AttributeName: "branch_id"
          KeyType: "RANGE"
      StreamSpecification:
        StreamViewType: NEW_IMAGE
      TableName: !Sub salesperson-${Environment}

  SaleDynamoDBTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    Properties:
      BillingMode: PAY_PER_REQUEST 
      AttributeDefinitions: 
        - 
          AttributeName: "sale_id"
          AttributeType: "S"
        - 
          AttributeName: "employee_id"
          AttributeType: "S"
      KeySchema: 
        - 
          AttributeName: "sale_id"
          KeyType: "HASH"
        - 
          AttributeName: "employee_id"
          KeyType: "RANGE"
      StreamSpecification:
        StreamViewType: NEW_IMAGE
      TableName: !Sub sale-${Environment}

  # =========================================================================================
  # AWS SQS QUEUES
  # ========================================================================================= 
  EmployeeQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub employee-queue-${Environment}
      VisibilityTimeout: 900
      RedrivePolicy:
        deadLetterTargetArn: 
          Fn::GetAtt: EmployeeWorkloadDeadLetterQueue.Arn
        maxReceiveCount: 10

  EmployeeWorkloadDeadLetterQueue: 
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub employee-workload-dead-letter-queue-${Environment}
      MessageRetentionPeriod: 1209600

  BranchFunctionDeadLetterQueue: 
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub branch-function-dead-letter-queue-${Environment}
      MessageRetentionPeriod: 1209600

  SaleQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub sale-queue-${Environment}
      VisibilityTimeout: 900
      RedrivePolicy:
        deadLetterTargetArn: 
          Fn::GetAtt: SaleWorkloadDeadLetterQueue.Arn
        maxReceiveCount: 10

  SaleWorkloadDeadLetterQueue: 
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub sale-workload-dead-letter-queue-${Environment}
      MessageRetentionPeriod: 1209600

  EmployeeFunctionDeadLetterQueue: 
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub employee-function-dead-letter-queue-${Environment}
      MessageRetentionPeriod: 1209600

  SaleFunctionDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub sale-function-dead-letter-queue-${Environment}
      MessageRetentionPeriod: 1209600

  # =========================================================================================
  # AWS CLOUDWATCH ALARMS
  # =========================================================================================
  BranchErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref BranchCollector
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Sum
      Threshold: '1'
      AlarmActions: 
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}
  BranchDurationAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref BranchCollector
      EvaluationPeriods: 1
      MetricName: Duration
      Namespace: AWS/Lambda
      Period: 60
      Statistic: Maximum
      Threshold: '750000'
      AlarmActions:
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}
  BranchThrottleAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref BranchCollector
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Sum
      Threshold: '1'
      AlarmActions:
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}

  SalespersonErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref SalespersonCollector
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Sum
      Threshold: '1'
      AlarmActions: 
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}
  SalespersonDurationAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref SalespersonCollector
      EvaluationPeriods: 1
      MetricName: Duration
      Namespace: AWS/Lambda
      Period: 60
      Statistic: Maximum
      Threshold: '750000'
      AlarmActions:
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}
  SalespersonThrottleAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref SalespersonCollector
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Sum
      Threshold: '1'
      AlarmActions:
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}

  SaleErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref SaleCollector
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Sum
      Threshold: '1'
      AlarmActions: 
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}
  SaleDurationAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref SaleCollector
      EvaluationPeriods: 1
      MetricName: Duration
      Namespace: AWS/Lambda
      Period: 60
      Statistic: Maximum
      Threshold: '750000'
      AlarmActions:
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}
  SaleThrottleAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref SaleCollector
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Sum
      Threshold: '1'
      AlarmActions:
        - arn:aws:sns:us-east-1:{id}:{alarm-action-name}

问答

是的,我正在进行自问自答的环节。在编码时,挑战自己“为什么?”或“怎么做?”通常是有帮助的,这样我会对自己所做的每个决定有更多信心,也能证明我使用的每个工具。

a. 我如何监控这些功能?

我使用CloudWatch 警报。CloudWatch 警报监视任何可用的指标或 AWS CloudWatch 支持的指标计算。它们可以根据给定阈值在指定时间内对指标或计算进行自定义操作。

对我来说,最关键的是在发生错误时立即学习和解决。因此,我为所有 3 个功能设置了以 1 个错误为阈值的警报,也就是说,只要出现错误就会触发警报。我希望在不不断盯着 CloudWatch 仪表板的情况下识别错误,因此警报的操作是将通知推送到 SNS 主题,该主题将警报转发到我的电子邮件收件箱。

如果你在一个协作环境中工作,我建议你通过将其发送到 Slack 频道,分发到分发列表中的所有地址,或将其包含在共享邮箱中,以扩展可见性。

b. 你为什么按现在的方式定义表的键?

这是基于现实情况的。显然,分支之间有所不同,因此它们的 ID 足以作为分支表中的唯一哈希键,符合1NF 约束。相比之下,销售人员和销售表则需要额外的键进行规范化。

因为在现实中,一个分支可能在记录中有多个员工,而员工允许从一个分支转移到另一个分支,从数据模式的角度来看,分支与员工之间的关系是多对多的。而且正因为如此,只有销售记录 ID + 销售人员 ID + 分支 ID(交易发生时的分支)组合才能指向销售表中的一个确切记录。瓶颈在于像 DynamoDB 这样的文档数据库允许最多两个属性作为键,我选择了销售人员 ID 作为排序键,而不是分支 ID,以确保销售记录与销售人员之间的关系。销售和分支之间的差异将在接下来的问题中解释。

c. 我如何建立销售和分支之间的联系?为什么?

数据供应商在销售记录中未能包含分支信息。处理这个问题的万灵药是从最上层(分支收集器函数)到最底层附加分支 ID。然而,这种方式忽略了一些极端情况。例如,吉姆·哈尔普特 在他在斯克兰顿分支的最后一天进行了销售。由于一些技术问题,这条记录没有被添加到他的销售记录列表中,也没有发布到 API 上,直到第二个工作日他被预设为转移到斯坦福德的员工。

没有上下文很难发现标签错误,尤其是当根本原因来自供应商时。从我的经验来看,此阶段的调试非常依赖于我们的反馈。这就是为什么我让销售表中的分支 ID 成为一个宽松的键值对;否则,需要额外的工作来删除和重写该项。

d. 我如何触发销售人员和销售收集器函数?

SQS 队列是 Lambda 函数官方允许的触发操作之一,因此这是唤醒这两个函数的自然选择,因为它们已经设置为监听队列。我绕了一下路,绕过了 API 所有者施加的最大访问限制。如果我让我的函数在消息到达时立即从队列中获取并访问 API,将会有多个函数几乎同时处理消息,这使得管道架构失效,因为它可能很容易超过 API 配额。通过设置每分钟的时间调度器(我为每个函数创建了两个调度器),处理频率从毫秒级下降到秒级。通过这种方式,数据管道中的消息流量得到了缓解。

e. 如何避免重复操作?

几乎不可能在不实际访问 API(真相来源)的情况下判断收集的数据是否是最新的。因此,我们不能减少 API 访问次数,而是可以像我在上一个问题中所做的那样降低超出 API 访问配额的风险。

如果数据流的目标是 DynamoDB,每次从 API 接收时都会完全更新每条记录。令人担忧的是,我们从 DynamoDB 到 S3 的火 hose 流带宽不足,导致运输偶尔中断。鉴于这一情况,我在更新之前插入了一个合理性检查。此检查将记录的每个属性值与最近从 API 提取的值进行比较。除非记录完全没有变化,否则现有记录将被覆盖。

附件是合理性检查函数:

def upToDate(table_name, condition, result, prefix):
    """
    Check if a record in a given specified DynamoDB table is up-to-date, which means that it's no different from the API retrieval.

    Args:
        table_name (str): The name of the DynamoDB table to check.
        condition (boto3.dynamodb.conditions.Key): The key condition expression for querying the table.
        result (dict): The record to check for ingestion completion.
        prefix (str): The prefix used for key matching.

    Returns:
        bool: True if the ingestion is completed, False otherwise.

    Notes:
        - The function queries the specified DynamoDB table using the provided condition.
        - It retrieves the items matching the condition.
        - The function compares the key-value pairs of the result with the retrieved items, accounting for the provided prefix if applicable.
        - If all key-value pairs match between the result and the retrieved items, the ingestion is considered completed.
        - The function returns True if the ingestion is completed, and False otherwise.

    """
    table = dynamodb.Table(table_name)
    retrieval = table.query(KeyConditionExpression=condition)["Items"]

    existing_items = 0
    if len(retrieval) > 0:
        for key in retrieval.keys():
            if key.upper() not in reserved_words:
                if result[key] == retrieval[0].get(key):
                    existing_items += 1
            elif result["key"] == retrieval[0].get(prefix + key):
                existing_items += 1

    completed = len(retrieval) and existing_items == len(result.items())
    # len(retrieval) == 0: the item doesn't exist in DynamoDB at all
    # existing_items == len(result.items()): the item exists and all its key-value pairs
    # are synced up with API
    return completed

部署

在每个文件夹中,执行

pip install -r requirements.txt

然后返回到上一级文件夹:

# copy utils.py to each folder
for d in */; do cp utils.py "$d"; done
# build the cloudformation
sam build -u
# invoke the functions locally for local testing
# event.json should be like:
# {
#    "branches": ["Scranton"]
# }
# env.json should be like:
# {
#    "Parameters": {
#        "Environment": "local"
#    }
# }
sam local invoke "BranchCollector" -e branch.json --env-vars env.json
sam local invoke "SalespersonCollector" -e branch.json --env-vars env.json
sam local invoke "SalesCollector" -e branch.json --env-vars env.json
# deploy it onto AWS
sam deploy --parameter-overrides Environment=dev

最终工作

我的 GitHub 仓库

版权信息

如何基于 AWS 构建级联数据管道(第二部分)

原文:towardsdatascience.com/how-i-built-a-cascading-data-pipeline-based-on-aws-part-2-217622c65ee4

自动化、可扩展和强大

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

·发布于 Towards Data Science ·10 分钟阅读·2023 年 8 月 25 日

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

照片由Mehmet Ali Peker拍摄,发布在Unsplash

之前,我分享了使用 AWS CloudFormation 技术开发数据管道的经验。然而,这不是一个最优的方法,因为它留下了 3 个待解决的问题:

  1. 部署必须手动进行,这可能增加错误的机会;

  2. 所有资源都创建在一个单一的堆栈中,没有适当的边界和层次;随着开发周期的进行,资源堆栈会变得越来越重,管理它将会是一场灾难;

  3. 许多资源应当在其他项目中持续使用和重用。

简而言之,我们将以敏捷的方式增加该项目的可管理性和重用性。

解决方案

AWS 使用户能够实现 2 种 CloudFormation 结构模式:跨堆栈引用和嵌套堆叠。跨堆栈引用代表了一种单独且通常独立地开发云堆栈的设计风格,而所有堆栈之间的资源可以根据引用关系相互关联。嵌套堆叠指的是由其他堆栈组成的 CloudFormation 堆栈。这是通过使用[AWS::CloudFormation::Stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stack.html)资源实现的。

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

现实生活中的嵌套堆栈:一个充满巢穴/蛋的巢(照片由 Giorgi Iremadze 拍摄,来源于 Unsplash

因为我们旨在实现更好的项目管理,所以项目将通过分层分离来拆分,而嵌套堆栈将对此提供帮助。然而,鉴于现有堆栈工件之间的内在关系,我们还需要进行跨堆栈引用。

实现

我们创建了 3 个 Lambda 函数,3 个 DynamoDB 表,1 个 IAM 角色及其附加策略,几个 SQS 队列和几个 Cloudwatch 警报。由于这些功能本身的复杂性,在这个版本中,它们将被定义在不同的模板中,服务仅供自己使用,包括警报和死信队列。除此之外,IAM 资源将是另一个嵌套堆栈,DynamoDB 表也是,为了保持其可重用性。用于在 Lambda 函数之间传递消息的 SQS 队列也将是一个不同的堆栈。这些嵌套堆栈将被放在一个新的目录中,称为 /templates

Cascading-ETL-pipeline
├── LICENSE
├── README.md
├── branches
│   ├── Pipfile
│   ├── Pipfile.lock
│   ├── lambda_function.py
│   ├── requirements.txt
│   └── service
│       ├── config.py
│       └── service.py
├── sales
│   ├── Pipfile
│   ├── Pipfile.lock
│   ├── lambda_function.py
│   ├── requirements.txt
│   └── service
│       ├── config.py
│       └── service.py
├── salespersons
│   ├── Pipfile
│   ├── Pipfile.lock
│   ├── lambda_function.py
│   ├── requirements.txt
│   └── service
│       ├── config.py
│       └── service.py
├── templates
│   ├── BranchCollector.yml
│   ├── SaleCollector.yml
│   ├── SalespersonCollector.yml
│   ├── iam.yml
│   ├── queues.yml
│   └── tables.yml
├── template.yml
└── utils.py

与之前相比,这一部分的编码工作相对轻松,因为我们主要只需将这些原始代码从一个文件移动到另一个文件。例如,在配置 DynamoDB 表堆栈时,我所做的仅是复制代码片段并粘贴到新文件中:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:  #   Type: String
  Environment:
    Type: String
Resources:
  BranchDynamoDBTable: 
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    Properties:
      BillingMode: PAY_PER_REQUEST 
      AttributeDefinitions: 
        - 
          AttributeName: "branch_id"
          AttributeType: "S"
      KeySchema: 
        - 
          AttributeName: "branch_id"
          KeyType: "HASH"
      StreamSpecification:
        StreamViewType: NEW_IMAGE
      TableName: !Sub branch-${Environment}

  SalespersonDynamoDBTable: 
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    Properties:
      BillingMode: PAY_PER_REQUEST 
      AttributeDefinitions: 
        - 
          AttributeName: "employee_id"
          AttributeType: "S"
        - 
          AttributeName: "branch_id"
          AttributeType: "S"
      KeySchema: 
        - 
          AttributeName: "employee_id"
          KeyType: "HASH"
        - 
          AttributeName: "branch_id"
          KeyType: "RANGE"
      StreamSpecification:
        StreamViewType: NEW_IMAGE
      TableName: !Sub salesperson-${Environment}

  SaleDynamoDBTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    Properties:
      BillingMode: PAY_PER_REQUEST 
      AttributeDefinitions: 
        - 
          AttributeName: "sale_id"
          AttributeType: "S"
        - 
          AttributeName: "employee_id"
          AttributeType: "S"
      KeySchema: 
        - 
          AttributeName: "sale_id"
          KeyType: "HASH"
        - 
          AttributeName: "employee_id"
          KeyType: "RANGE"
      StreamSpecification:
        StreamViewType: NEW_IMAGE
      TableName: !Sub sale-${Environment}

跨嵌套堆栈引用

值得注意的是,还有一些修改是必要的。如果我们需要确保堆栈内定义的资源可以被堆栈外部的其他资源引用,那么那些导出堆栈需要一个新的部分,称为 Outputs,以输出引用值。在我们的案例中,IAM 角色将被全球使用,因此在其定义之后,我们会输出其 ARN,以便在一定范围内可见。

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:  #   Type: String
  Environment:
    Type: String
Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${Environment}-lambda-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
            - events.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AWSLambdaExecute
      - arn:aws:iam::aws:policy/AmazonSQSFullAccess
      - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
      Path: '/'
  LambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub '${AWS::StackName}-${Environment}-lambda-policy'
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: EventBusAccess
          Effect: Allow
          Action:
          - events:PutEvents
          Resource: '*'
        - Sid: LambdaInvokeAccess
          Effect: Allow
          Action:
          - lambda:InvokeFunction
          Resource: "*"
        - Sid: LogAccess
          Effect: Allow
          Action:
          - logs:CreateLogGroup
          - logs:CreateLogStream
          - logs:PutLogEvents
          Resource: arn:aws:logs:*:*:*
      Roles:
      - !Ref LambdaRole

Outputs:
  Role:
    Description: The role to be used across the stacks
    Value: !GetAtt LambdaRole.Arn
    Export:
      Name: !Sub ${Environment}-Role

同样,消息队列也以相同的方式导出:

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:  #   Type: String
  Environment:
    Type: String
Resources:
  EmployeeQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub employee-queue-${Environment}
      VisibilityTimeout: 900
      RedrivePolicy:
        deadLetterTargetArn: 
          Fn::GetAtt: EmployeeWorkloadDeadLetterQueue.Arn
        maxReceiveCount: 10

  EmployeeWorkloadDeadLetterQueue: 
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub employee-workload-dead-letter-queue-${Environment}
      MessageRetentionPeriod: 1209600

  SaleQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub sale-queue-${Environment}
      VisibilityTimeout: 900
      RedrivePolicy:
        deadLetterTargetArn: 
          Fn::GetAtt: SaleWorkloadDeadLetterQueue.Arn
        maxReceiveCount: 10

  SaleWorkloadDeadLetterQueue: 
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub sale-workload-dead-letter-queue-${Environment}
      MessageRetentionPeriod: 1209600

Outputs:
  EmployeeQueue:
    Description: The SQS queue that delivers the payloads from branch collector to salesperson collector
    Value: !Ref EmployeeQueue
    Export:
      Name: !Sub ${Environment}-EmployeeQueue
  SaleQueue:
    Description: The SQS queue that delivers the payloads from salesperson collector to sale collector
    Value: !Ref SaleQueue
    Export:
      Name: !Sub ${Environment}-SaleQueue

与此同时,从其他地方导入资源的堆栈也需要做些许调整。AWS 通过提供一个内置函数 [Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html) 来支持此功能。以“分支收集器”为例。它涉及到 IAM 角色和一个消息队列,这些都不是在同一堆栈中创建的。因此,每当出现上述资源时,我将它们替换为 Fn::ImportValue 函数的值。

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:  #   Type: String
  Environment:
    Type: String
Resources:
  BranchCollector:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub branch-collector-${Environment}
      Handler: lambda_function.lambda_handler
      Runtime: python3.8
      CodeUri: ./../branches/
      Description: updating branch info in our DynamoDB table
      MemorySize: 128
      Timeout: 900
      Role: 
        Fn::ImportValue:
          !Sub ${Environment}-Role
      Environment:
        Variables:
          LOGGING_LEVEL: INFO
          APP_ENV: !Ref Environment
          SQS: 
            Fn::ImportValue:
              !Sub ${Environment}-EmployeeQueue
          DB: !Sub branches-${Environment}
      DeadLetterQueue:
        Type: SQS
        TargetArn: 
          Fn::GetAtt: BranchFunctionDeadLetterQueue.Arn
      Events:
        StartScheduledEvent:
          Type: Schedule
          Properties:
            Schedule: rate(1 hour)

  # dead letter queue
  BranchFunctionDeadLetterQueue: 
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub branch-function-dead-letter-queue-${Environment}
      MessageRetentionPeriod: 1209600

  # alarms
  BranchErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref BranchCollector
      EvaluationPeriods: 1
      MetricName: Errors
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Sum
      Threshold: 1
      AlarmActions: 
        - arn:aws:sns:us-east-1:{id}:{alarm-name}
  BranchDurationAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref BranchCollector
      EvaluationPeriods: 1
      MetricName: Duration
      Namespace: AWS/Lambda
      Period: 60
      Statistic: Maximum
      Threshold: 750000
      AlarmActions:
        - arn:aws:sns:us-east-1:{id}:{alarm-name}
  BranchThrottleAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref BranchCollector
      EvaluationPeriods: 1
      MetricName: Throttles
      Namespace: AWS/Lambda
      Period: 300
      Statistic: Sum
      Threshold: 1
      AlarmActions:
        - arn:aws:sns:us-east-1:{id}:{alarm-name}

根堆栈

现在所有嵌套堆栈都已定义并创建,应该有一个根堆栈将整个基础设施集成在一起。考虑到使用嵌套堆栈风格的基础设施是一个层次结构,根堆栈是所有嵌套堆栈所归属的父级(尽管嵌套堆栈也可以作为其他堆栈的父级)。在我们的案例中,替换定义单个资源的现有片段为对预定义的 template.yml 中嵌套堆栈 CloudFormation 模板的引用非常简单。

AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:  #   Type: String
  Environment:
    Type: String
Resources:
  # =========================================================================================
  # IAM ROLES, POLICIES, PERMISSIONS
  # =========================================================================================
  IAM:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./templates/iam.yml
      Parameters: 
        Environment: !Ref Environment
  # =========================================================================================
  # AWS LAMBDA FUNCTIONS
  # ========================================================================================= 
  BranchCollector:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./templates/BranchCollector.yml
      Parameters: 
        Environment: !Ref Environment
    DependsOn: 
      - IAM
      - Queues

  SalespersonCollector:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./templates/SalespersonCollector.yml
      Parameters: 
        Environment: !Ref Environment
    DependsOn: 
      - IAM
      - Queues

  SaleCollector:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./templates/SaleCollector.yml
      Parameters: 
        Environment: !Ref Environment
    DependsOn: 
      - IAM
      - Queues

  # =========================================================================================
  # AWS DynamoDB TABLES
  # ========================================================================================= 
  Tables:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./templates/tables.yml
      Parameters: 
        Environment: !Ref Environment

  # =========================================================================================
  # AWS SQS QUEUES
  # ========================================================================================= 
  Queues:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./templates/queues.yml
      Parameters: 
        Environment: !Ref Environment

从这里开始,可以放心地说 CloudFormation 升级已经完成!

等等,这是否意味着我们需要重建和重新部署?遗憾的是,是的,这正是下一节将要解决的问题。

自动部署

由于我总是将我编写的所有代码存储在 GitHub 仓库中,我打算利用 GitHub Actions。GitHub Actions 是 GitHub 的一个功能,用于自动化存储在任何 GitHub 仓库中的工作流,从而无缝支持构建、测试和部署。尽管有预定义的工作流,但由于我们任务的独特性和复杂性,我们需要自定义一个满足我们需求的工作流。

一般来说,工作流应该模拟我们在软件构建和部署阶段在云环境中手动执行的操作。即包括以下步骤:

  1. 为参数分配值,例如 environment

  2. 设置 AWS 配置,例如输入默认的 AWS 凭证和服务区域代码

  3. 安装 SAM CLI

  4. 授予 AWS 角色访问将要使用的服务/资源的权限

  5. 创建一个 S3 桶作为 CloudFormation 存储位置

  6. 将通用代码库克隆到每个独立目录

  7. 构建和部署无服务器应用程序模型(SAM)

为了在工作流文件中将它们全部包装起来,格式如下:

name: A workflow that automates the data pipeline deployment
on: 
  workflow_dispatch:
  push:
    branches:
      - main
    paths-ignore: 
      - '.gitignore'
      - '*.png'
      - 'README.md'
  pull_request:
    paths-ignore:
      - '.gitignore'
      - '*.png'
      - 'README.md'

jobs:
  deploy:
    container: 
      image: lambci/lambda:build-python3.8
    runs-on: ubuntu-latest
    env:
      BUCKET_NAME: your-bucket-name
    steps:
      - name: Set Environment
        id: setenv
        run: |
          echo "Running on branch ${{ github.ref }}"
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            echo "::set-output name=env_name::prod"
          else
             echo "::set-output name=env_name::dev"
          fi
      - name: Set Repo
        id: setrepo
        run: |
          echo "::set-output name=repo_name::${{ github.event.repository.name }}"
      - name: Set Branch
        id: setbranch
        run: |
          echo "::set-output name=branch_name::${{ github.head_ref}}"
      - name: Checkout
        uses: actions/checkout@v2
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{secrets.AWS_ACCESS_KEY_ID}}
          aws-secret-access-key: ${{secrets.AWS_SECRET_ACCESS_KEY}}
          aws-region: us-east-1
          # role-to-assume: arn:aws:iam::807324965916:role/cdk-hnb659fds-deploy-role-807324965916-us-east-1
          role-duration-seconds: 900
      - name: Install sam cli
        run: 'pip3 install aws-sam-cli'
      - name: Complete policies
        run: |
          aws iam attach-user-policy \
          --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess \
          --policy-arn arn:aws:iam::aws:policy/CloudWatchEventsFullAccess \
          --policy-arn arn:aws:iam::aws:policy/AWSLambda_FullAccess \
          --policy-arn arn:aws:iam::aws:policy/IAMFullAccess \
          --policy-arn arn:aws:iam::aws:policy/AWSCloudFormationFullAccess \
          --policy-arn arn:aws:iam::aws:policy/AmazonSQSFullAccess \
          --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
          --user-name Memphis
      - name: Create S3 Bucket
        run: |
          if ! aws s3api head-bucket --bucket "${{env.BUCKET_NAME}}" 2>/dev/null; then
            aws s3api create-bucket --bucket "${{env.BUCKET_NAME}}"
          else
            echo "Bucket ${{env.BUCKET_NAME}} already exists"
          fi

      - name: Copy utils.py
        run: 'for d in */; do cp utils.py "$d"; done'
      - name: build
        run: sam build && sam package --s3-bucket ${{env.BUCKET_NAME}} --s3-prefix "${{steps.setrepo.outputs.repo_name}}/${{steps.setbranch.outputs.branch_name}}/${{steps.setenv.outputs.env_name}}" --output-template-file packaged.yaml --region us-east-1 || { echo 'my_command failed' ; exit 1; }
      - name: deploy
        run: sam deploy --template-file packaged.yaml --s3-bucket ${{env.BUCKET_NAME}} --s3-prefix "${{steps.setrepo.outputs.repo_name}}/${{steps.setbranch.outputs.branch_name}}/${{steps.setenv.outputs.env_name}}" --stack-name "${{steps.setrepo.outputs.repo_name}}-${{steps.setenv.outputs.env_name}}-stack" --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND --region us-east-1 --no-fail-on-empty-changeset --parameter-overrides Environment=${{steps.setenv.outputs.env_name}} || { echo 'my_command failed' ; exit 1; }

在这里我包括了一个名为“deploy”的任务。它通过 lambci/lambda:build-python3.8 镜像进行容器化,并在 Ubuntu 系统上运行,这在此任务中进行了定义。创建了一个名为 BUCKET_NAME 的变量,用于存储我们将要命名的 S3 桶的字符串值。

第 1 步是创建一个新的参数 env_name,用于存储工作环境名称的值。它取决于触发工作流的分支:除非工作流在主分支上运行,否则称为 prod;否则为 dev

第 2 步在另一个输出值中记录仓库名称。这将成为 S3 桶中模板文件位置前缀的一部分。

第 3 步类似于第 2 步,获取将来作为前缀另一部分使用的分支名称。

第 4 步简单地使用 actions/checkout,这是一个软件包,用于在不同的工作区中检出当前的仓库,以便工作流能够访问它。

第 5 步还使用了一个包。在aws-actions/configure-aws-credentials@v1的帮助下,我们可以轻松配置 AWS 工作环境,只需提供 AWS 访问密钥 ID、AWS 秘密访问密钥和区域即可。请注意,建议将你的敏感凭证(AWS 访问密钥 ID 和 AWS 秘密访问密钥)保存在秘密中。

第 6~11 步在 bash 命令行中执行。按照这个顺序,工作流的其余部分安装 SAM CLI;然后将所有必要的策略 ARN 附加到 IAM 用户;如果 S3 桶不存在,则使用作为环境参数提供的桶名称创建一个 S3 桶;最后但同样重要的是,将通过 SAM 命令构建、打包和部署 CloudFormation 堆栈。

使用工作流,开发人员将避免不断重新运行部分步骤(如果不是全部步骤的话)。相反,只要创建了拉取请求,任何新的提交都会触发一个新的工作流运行,代码在非主分支上;当拉取请求合并时,最近的工作流也会被触发,同时处理主分支上的资源。

离开之前

感谢你读到这里。在这篇文章中,我讨论了一个主要的升级,以提高代码的可管理性和重用性,同时介绍了一个自动化堆栈构建和部署的解决方案。如果你对细节感兴趣,随时查看我的仓库!

顺便说一下,我组织资源和工件的方式也可以是启动微服务的一个很好的开端。如果你对微服务感兴趣,别忘了订阅以跟踪我的下一篇文章!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值