建筑数据
基础设施、信任、欣赏和决策
失败(fālˈyər)
没有达到预期目的的情况或事实:实验的失败。
我失败了。作为多伦多一家初创公司的第一位数据工程师,我有崇高的目标,伟大的新想法/稍微借鉴的想法,我甚至有阶段,所以一切都会很好地融入我的宏伟计划。不要误解我,失败并不是一件坏事,我已经做了很多事情。但是我知道问题,我知道解决方法,我把所有的事情都分阶段进行。我怎么会失败呢?我怎么会???
让我们从头开始,第 0 天,我想了很久,想了很久,想了很久,想了很久,想了很久,想了很久,想了很久,想了很久,想了很久,想了很久,该怎么做,所以让我们从这里开始吧。让数据民主化!
第 1 阶段将移除或更新传统事件跟踪目标,并替换为来自segment.com的整合第三方 API。我会尽可能保持其他一切不变,同时重建,因为商业需求不能让位于伟大。
第 2 阶段是现代无服务器数据基础设施诞生的地方。我会使用最新和最伟大的数据技术,一切开源,甚至一些“出血边缘”阿尔法狗屎,使一个傻瓜(和未来)证明基础设施。这将为我们提供我们可以预见的所有可扩展性,同时还允许分析师和数据科学家访问、发现和构建可操作的见解 ←是我借用的一个想法。
第三期等一下……自助。还需要我多说吗?任何关于数据工程、数据文化或数据驱动的决策制定的讨论,如果没有一个每个员工都知道、使用和喜爱 SQL 的自助式报告 BI 平台的梦想,都是不完整的。
第 4 阶段数据产品,面向内部和外部。数据团队不仅应该促进提供易于访问和发现的数据,而且,特别是在小公司,应该推动 A/B 实验平台、数据科学工作流、产品团队可以使用的数据 API,以及整个组织中所有数据相关文档的官方主页,如 Airbnb 的知识报告。
Photo by Brooke Cagle on Unsplash
与此同时,还有午餐学习、数据调查、制定数据驱动决策的最佳实践等等。你的基本数据——公司内部的大学方法。
我失败了。我不仅失败得快而且经常失败,而且失败得相当慢,努力去理解眼前发生的事情。
今天,我想写一个小故事,讲述我在数据职业生涯中的亲身经历,以及我通过阅读其他媒体上的故事所学到的东西。也许与此同时,我会想出我会有什么不同的做法。
理论上,数据工程师不负责组织中的“所有数据”。互联网上的默认角色描述中没有一句台词是这样的
数据工程师收集、转换和发布数据,使组织能够做出数据驱动的决策……他们还创建、维护和传播我们都知道、喜欢和想要的数据文化。
但是在相当多的情况下,特别是在早期的“数据成熟”公司中,无论员工或雇主是否知道,这都是一个要求。
数据文化是你经常听到的东西,就我个人而言,我是自上而下的数据文化方法的强烈支持者,这意味着,如果你的首席执行官不喜欢它,并且不要求他的直接下属提供数据支持的结果,你将无法让其他人关心数据驱动的文化。
但在大多数创业公司中,至少从我能讲的来看,似乎是这样的。主要是因为小公司要么有一两个“数据人”,所以任何与数据相关的责任都从其他人身上转移到了数据人身上。在处于早期数据成熟阶段的公司,尤其是初创公司,第一个数据工程师几乎总是必须承担在公司推广数据知识和热情的角色,以及正常的预期数据工程工作。
不管是好是坏,这就是所发生的事情,与其争论如何解决它,我想写下我处理它的经历。我目前是一名数据工程师,但我在企业和初创公司中担任过多个数据分析师和软件开发人员的角色。在每一个新的角色中,我大脑中的科学家部分总是想要证明,这使我成为我所在的公司或团队的数据人员。
当我第一次加入目前的公司时,培养数据文化是一件非常令人兴奋的事情。建立新的东西,而不仅仅是基础设施,以某种方式指导公司的整体数据战略的想法令人兴奋。但是我如何创建这种数据文化呢?我们能定义它吗?互联网给你的感觉是数据文化是一个单一的、从固体金属切割而成的复杂模型,直接看着它让人无法理解如此复杂的东西是如何被人类创造出来的。
但是对我来说,数据文化是一把包罗万象的伞,为了清楚起见,我想把它分成 4 个部分。我自己的私人捕捉所有迷你伞。这就是我对构建数据的看法和经验的开始。
Photo by Stephen Dawson on Unsplash
1.构建数据基础设施
最近,似乎每个公司都想要或需要一名数据工程师。这个人为公司做什么在很大程度上取决于公司的行业和数据成熟度。这可以跨越从初创公司到大型企业公司。
我的主要职责是解决问题、支持员工和建立基础设施。但不是这么几句话,更像是
简化、整合、管理、维护并向组织内的所有利益相关方提供可靠的数据,但首先要修复、移除和/或替换不工作的数据,这是典型的启动数据工程需求
这个概念非常简单,但总有一个问题,其中之一就是要记住,当你在建设基础设施时,公司仍然需要运转。这意味着,公司的当前和短期需求仍然必须得到支持,利益相关者仍然需要指标来查看,BI 分析师仍然需要仪表板和图表来分析。这是非常好的优先级技能、多任务技能、沟通和期望管理技能发挥作用的地方。如果你没有这些,那么成为你公司的第一个(或第一批)数据工程师对你和你的公司来说可能是个坏主意。
排在“明白”列表之后的第二个问题是,在想要和需要之间进行斗争有多困难,以及平衡未来建设而不是遥远未来建设的真正需要。除非你是一家专门的“数据”公司,或者可能是一家 IOT 公司,否则你对数据基础设施的第一次尝试不应该是大肆宣传的数据湖。这也适用于你的数据工具,我正看着你的 Looker 。想想婴儿的步伐,或许不是婴儿的步伐,而是蹒跚学步的步伐。不需要启动一个 kubernetes 集群来在 Docker 容器中运行您的两个 Python 脚本。但这并不意味着你的基础设施是从你办公桌下的服务器上运行的 cron 开始的。
一个好的、简单的、典型的 starter 批处理基础设施可能看起来像 AWS S3、Redshift、EC2/hosted(或 AWS Glue)上的 Airflow 以及您选择的 BI 工具。您可能还想添加一个监控堆栈,无论是 Prom+Grafana 还是 AWS CloudWatch。通过这种设置,您可以轻松地纵向扩展和横向扩展、更换部件或将整个系统转移给另一家提供商(有限的供应商锁定)。您还可以轻松地为数据科学家或数据分析师添加工作流工具,目前,您的公司可能还没有这些工具。一家拥有第一位数据工程师的公司现在可能正在期待一些数据民主化的发生(对于那些不了解情况的人来说,这是数据和民主之间的“协同作用”),随之而来的是大肆宣传的自助式商业智能平台/分析工具。
“自助”这个词就像愚人的黄金或者宠物蛇或者蝎子和青蛙,基本上是任何一开始看起来很棒,然后一有机会就咬你屁股的东西。
一只蝎子让一只青蛙背它过河。青蛙犹豫了,害怕被蝎子螫伤,但是…
en.wikipedia.org](https://en.wikipedia.org/wiki/The_Scorpion_and_the_Frog#/media/File:Tortoise_and_Scorpion.jpg)
自助式 BI,也称为民主化数据,是指当有人想象一个世界,其中具有直观拖放界面的工具实际上对所有人都是直观的,人们只问他们自己无法回答的问题,每个人都知道 SQL。如果有合适的资源可以帮助,使用 excel 的用户很可能会使用任何工具。这些用户热衷于学习更多关于工具和数据的知识,并且不介意与奖励相关的学习曲线。然而,在现实中,创建一个自助式分析环境会给你一个大规模混乱、伟大和愚蠢的大杂烩,比如
- 认为谷歌表单的旧方式更好的用户
- 人们提出了许多很棒的新问题
- 很多不太好的问题已经被提出来了
- 大家都讨厌 SQL。人人
- 解释拖放界面的工作原理
自我服务会让你不知所措,让你的利益相关者不知所措。如果你没有准备好大量的资源——需要帮助的人、文档、消磨时间——这个项目会扼杀你作为数据工程师的生产力,甚至可能扼杀你的灵魂。你最好与其他团队的一两个关键成员密切合作,帮助他们学习新系统,发现数据,让这些半数据人员直接在他们自己的团队中工作,然后在以后打开称为自助式 BI 环境的闸门。
2.建立数据信任
如果没有人相信你提供的数据,那就不值得提供。我很快认识到,一个看起来不太好的基础设施产生数据是不够的。人们需要使用这些数据来查看结果,并深入了解从营销活动和转化率到 A/B 实验和用户应用内行为的任何事情。如果您的最终用户不相信您提供的指标,他们会很快停止使用您的数据。如果你幸运的话,他们至少会告诉你他们不相信这些数据——是的,这就是幸运。唯一比最终用户不信任你的数据更糟糕的是,用户不信任你的数据,停止使用它,寻找替代品,并且永远不会告诉你。
就数据信任而言,单一(一个)真实来源是区分好坏的好方法。
在信息系统设计和理论中,单一真实来源 ( SSOT )是结构化信息模型和相关联的数据模式的实践,这样每个数据元素被精确地存储一次。到这个数据元素的任何可能的链接(可能在关系模式的其他区域或者甚至在遥远的联合数据库中)只通过引用—维基百科
在英语中,在上下文中,让您的度量依赖于一个表或过程会给您带来两个好处:1)单点故障,但也是修复该故障的单点故障;2)如果度量是错误的,使用该度量的每个人都是同样程度的错误。
我认为我的第一点很容易理解和欣赏。如果某些东西损坏了,就像不可避免的那样,它只能在一个地方损坏,您只需要修复那个地方,就可以获得每个下游流程,并再次使用正确的指标进行报告。但是让我深入到第二点(2)一点,因为没有人喜欢得到错误的数据,但听起来好像我在说这是一件好事。我没有。我是说每个人都错 到同样程度是好事。根据我的经验,可能发生的最糟糕的事情之一是工作管道错误地处理数据;然而,比这更糟糕的是,下游流程和报告要求相同的指标,但却有不同的错误答案。当你意识到问题时,你会静静地坐在那里盯着屏幕,深呼吸,然后说“妈的。那可不好”。这就是 SSOT 的切入点。您可以使用不同的数据源,一起转换和争论您的数据,但最终如果您的指标使用一个表、一组表或流程作为其基础,而所有其他流程使用其输出作为自己的基础,您将永远不会得到一个跨管道给出不同错误答案的指标。
难以置信,但这一切都要追溯到数据信任。如果利益相关者没有或失去了对数据的信任,就很难挽回。SSOT 有助于防止这种情况发生,因此对于任何数据工程师来说,在这里花费大量时间都是非常值得的,尤其是那些受托建立依赖数据的文化的人。
3.建筑数据增值
一种感激。不是说需要人们欣赏你的工作,而是数据本身,以及他们如何利用它来实现自己的目标。需要在团队成员和整个公司中培养数据欣赏能力,如果没有这一点,那又如何?—态度总是会妨碍人们利用你提供的一切。
如果一个数据工程师创建了可发现的、可靠的、值得信赖的数据集市,但最终用户却不在乎,那么这个数据工程师真的成功了吗?
这可能更像是一个公司文化的话题,但是如果给你一些工具来帮助你在你的职位上取得成功,你会使用它们吗?这就是数据增值的用武之地,你的终端用户关心你在做什么,更重要的是他们关心你为什么这么做。您和您的基础架构的存在只有一个原因—直接或间接地提供业务洞察力,您的基础架构必须促进这一点。如果用户不能,不管是自愿的还是通过其他方式,欣赏你苦心提供的那些宽维度表或时间序列指标,你就没有任何意义。句号。
在一家初创公司(或任何处于数据成熟早期阶段的公司),你作为数据工程师的角色可能意味着你不仅要构建基础设施,还要构建对数据的“欣赏”,在组织内部和整个组织中传播数据。没有有效地做到这一点会导致你整体工作的失败。建造它,他们将使用它,**,**在这里并不适用。
4.构建数据(驱动)决策
D 数据驱动的决策或这一主题的任何迭代基本上是业务用户在采取行动之前利用数据洞察来制定、验证和/或确认其决策的概念。这些决策可能是实验假设的基础,也可能是对失败的营销活动的解释,或者是投资特定产品功能的原因。关键是,你依靠数据来帮助你做决定,这有助于更有效的过程,并允许你做重要的事情,为你和你的组织带来价值的事情。
虽然许多人喜欢数据驱动决策的概念,并要求更多的数据,但其他人则反对。想要继续做你正在使用的事情是很自然的,我自己也经常这样做,这就是为什么我之前提到我是自上而下的数据文化方法的强烈支持者,原因是这样的——想象你是一个功能团队的高级经理,刚刚在你的惊人产品中部署了一个很棒的新功能。帮我回答这些问题
- 有多少客户在使用你这个很酷的新功能?
- 您的功能被使用的频率是多少?
- 你会开发很酷的 v2 功能吗?
很简单的问题,你可能会在第三季度猜个大大的是,因为你为什么不修复几个 bug 并增强一些已经在生产中的功能呢?但是,如果你的老板以一种自上而下的方式要求你向她提交的任何决定都必须有数据支持,那该怎么办呢?现在,您有了一整套新问题,比如,“我是否甚至用这个新特性来捕获回答这些问题所需的数据?”或者“为什么数据显示很少有客户真正使用该功能,但我的直觉告诉我做得很好?”。既然 v1 没有数据支持它的成功,那么版本 2 就有点难以证明了。
当您引用数据时,会出现更多的问题和见解,有好有坏,但如果数据驱动的文化不是自上而下的,这些都是不可能的。你要么急于得到数据,要么没有。如果你不是,除非你被轻轻推向正确的方向,否则很容易忽视额外的努力。
尽管你不是那些数据驱动决策的制定者,但我们正在讨论的情况是,你是公司中唯一的数据人员,所有与数据相关的事情都落在你的肩上。这可能是在任何公司构建数据时最难(也是最重要)的角色部分。
Photo by Franki Chamaki on Unsplash
建立结论
作为一个关于构建数据的故事,你可能已经对一些技术图表、某处的示例气流 DAG 代码块或至少一些伪代码有了印象或预期。但我认为所有这些事情来来去去。要么是行业发生了变化,角色描述发生了变化,要么是技术、流程和技巧发生了变化。此外,已经有很多媒体在谈论如何构建数据基础设施,或者如何在 K8s 中使用 Airflow。
对我来说,在这个故事的背景下,构建数据不仅仅是 Y 公司在启动阶段 z 的技术堆栈 X。在
公司生命的某个阶段,他们需要他们的第一个员工来帮助编写新功能,第一个 IT 人员来帮助供应所有这些新的笔记本电脑,第一个人力资源成员以及他们的第一个数据人员。那个数据人,第一个真正的全职数据人,数据工程师,将不可避免地做更多的事情,而不仅仅是写几个 cron 任务,python 脚本和把一个工具和另一个工具联系起来。为了不失败,这个人将需要建立一个数据基础设施,她将需要在最终用户和她提供的数据之间建立信任,她将需要建立用户对他们拥有的工具和可以帮助他们实现公司目标的数据的欣赏,她将需要在她的同事中建立一个数据驱动的决策思维。作为数据人员,作为数据工程师,所有这些都包含在她的角色中,所有这些都包含在从零开始在公司创造数据文化中。所有这些都封装在建筑数据中。
不用一行代码就能构建数据管道
使用 Google Cloud Dataprep 探索数据的智能而直观的方式。
https://cloud.google.com/dataprep
厌倦了编写大量 python/java 代码来构建 ETL 管道或探索数据集吗?害怕编程来分析数据集?正在寻找一个基于用户界面的分析平台来帮助您处理这些大型数据集吗?对您来说,集成不同的工具是一项庞大而笨拙的任务吗?
瞧啊。你所有的祈祷/疑问都被 Trifacta 的这个叫做Cloud data prep**的神奇工具回答了。**它是与谷歌云平台集成的合作伙伴服务,支持数据工程和分析领域的众多工作流。在本帖中,你不仅将学习如何使用 dataprep,还将了解与谷歌云平台(GCP)服务的潜在集成。我们将借助 Cloud Dataprep 和 Bigquery 构建一个数据转换管道。
云数据准备概述
Dataprep 是一个基于 web 的、无服务器的、无操作的智能数据服务,可以可视化地对数据集执行探索性分析。它帮助您讨论和探索结构化和非结构化数据,以便进一步处理,用于机器学习、报告仪表板、分析框架等。
建议和预测加载数据的下一次数据转换的惊人能力使得分析对于那些犹豫要不要写代码的人来说变得轻而易举。
但是 dataprep 在数据工程领域,尤其是在 GCP 基础设施中的定位是什么:
From https://cloud.google.com/dataprep/
基于 GCP 的数据工程管道通常由上图中的部分或全部云服务组成。
数据的摄取可以发生在任何组件上,如 Bigquery、云存储或通常的文件上传。接下来是主要部分,在这里您设计您的数据以准备用于更深层次的模型构建和分析目的。这包括原始数据流入一个或多个(根据您的用例)数据处理引擎,如 Cloud Dataprep、Cloud dataflow,转换后的数据输出并存储在 Bigquery 或云存储中。
让我们深入了解构建并运行管道的任务:
任务分解
- 设置您的云控制台
- 将数据导入 BigQuery
- 将 Bigquery 连接到 Dataprep
- Dataprep web 界面的探索性分析
- 数据争论和管道配方
- 计划数据准备作业
进一步阐明以上每一点:
1.设置您的云控制台
一旦你登录到你的谷歌云控制台,创建一个新的项目,这个项目将用于本教程。单击顶部导航栏上的下拉菜单,然后单击+新建项目。
GCP project list page
Click on the NEW PROJECT
icon
Provide the project name and click CREATE
成功创建项目后,确保您在环境中选择的新创建的项目反映在下拉列表中:
关于项目创建和管理的更多细节,这里是到文档的链接。
2.将数据导入 BigQuery
设置好开发环境后,我们需要设置 BigQuery,它在管道中同时扮演输入和输出存储的角色。下面是流程的样子:
Pipeline Flow Architecture
这篇文章的主要焦点是 Dataprep,但是我们需要 BigQuery 来导入数据(摄取)并在转换周期完成后在 Dataprep 中导出输出。
将数据导入 Bigquery 的说明:
- 从云控制台的导航菜单(汉堡图标)中,点击 **BigQuery。**我们将被带到 BigQuery 控制台,这里有一个左窗格,列出了查询历史、保存的查询等选项,在底部,我们有
Resources
,在它下面必须显示我们的项目。点击并选择您的项目,在右边的查询编辑器下,CREATE DATASET
选项被激活。
2.点击CREATE DATASET
并输入Dataset ID
作为ecommerce_analytics
,这将是我们项目下数据集的名称。将所有其他字段设置为默认,点击底部的Create dataset
按钮。
3.我们的数据集已经准备好以表格的形式存储数据。我们将通过查询来自bigquery-public-data
项目的数据子集,使用来自谷歌商品商店的模糊的谷歌分析 360 数据。它伴随着每个新项目而来。
该查询使用标准 SQL。在查询编辑器中键入或粘贴 SQL 查询:
4.点击**运行。**左窗格现在应该是这样的:
该查询创建了一个名为subset_user_sessions
的新表,其中包含 2017 年 1 月 1 日发生的 33,664 个用户会话。您可以通过选择左侧窗格中的表格,然后单击右侧的Preview
选项来预览数据。
现在我们已经将数据导入到 BigQuery 中,让我们转到 Dataprep,将它链接到 dataprep 来研究这些数据。
3.将 BigQuery 连接到 Dataprep
在这里,我们将从 Cloud Dataprep 开始,并使用它连接我们的 bigquery 表。以下是该任务的步骤:
注意:你需要一个 Chrome 浏览器,因为 Cloud Dataprep 只能与 Google chrome 配合使用。
- 从导航菜单>打开 Dataprep。
- 您将需要启用计费以使用 Dataprep,如果您在云控制台上还没有计费帐户,则必须从导航菜单的计费部分创建一个。
- 接受服务条款,然后授权 Trifacta 和 Google LLC 访问您的帐户信息。
4.点击Allow
按钮,允许 Trifacta 访问项目数据,同步账户信息需要几分钟时间:
5.选择您想要登录的 Google 帐户,并允许 Trifacta 访问该帐户信息:
6.帐户设置完成后,您将首次被提示设置一个对话框,以提供一个位置来保存我们在 Google 云存储的 Dataprep 中创建的文件。确保选择了正确的项目,然后点击Continue.
7.欢迎来到主游戏场,云数据准备仪表板。一旦我们到了这里,我们就用 BigQuery 开始连接过程。点击右上角的Create Flow
按钮。
8.输入User Analytics Pipeline
作为流程名称并添加流程描述。
9.该从 BigQuery 导入数据集了,点击导入&添加数据集,从左窗格选择 BigQuery 。
10.点击您的数据集,当您的数据集表被加载时,点击如下所示的Create Dataset
图标:
在右侧,一个窗格预览表格数据,单击底部的导入&添加到流程按钮。
连接成功建立,现在您可以向流程中添加新的配方了,因为右侧的Add new Recipe
选项变得可用。
4.Dataprep UI 的探索性分析
现在我们的 BigQuery 项目数据被导入到 dataprep 中,我们可以开始进行探索性数据分析。
EDA 过程中的第一步也是最重要的一步是与手头的数据建立联系,这有助于你评估可以从数据中获得的潜在洞察力。有不同的方法来进行最初的数据理解练习,我的方法是阅读模式描述如果有的话,然后我试着浏览每一列,并准备一个好问题列表来回答这些列或列的组合。
有关 EDA 的更多信息:
如何用 python 调查数据集?
towardsdatascience.com](/hitchhikers-guide-to-exploratory-data-analysis-6e8d896d3f7e)
从我们上一节离开的地方开始,单击右窗格中的Add new Recipe
按钮,然后单击Edit Recipe
在 Transformer 视图中查看数据。
你可以在这里阅读数据集来理解每一列的含义。因此,让我们定义几个问题,并使用 Dataprep 的工具和特性提取它们的答案。这将帮助我们了解数据和平台:
- 数据帧的形状(行数和列数)是什么?
在 transformer 视图的底部,您会发现我们的示例包含 32 列、12.5K 行和 4 种不同的数据类型。
2。寻找列的最小值,最大值,下四分位数,上四分位数函数。
要了解每列的统计和模式详细信息,请单击列名前面的下拉菜单,然后选择列详细信息。让我们试着找出以下各列的最小值/最大值:
productQuantity
单击列详细信息会将您带到摘要和统计信息页面:
最小购买量为 1,最大购买量为 50。
同样,
最大/最小浏览量:1/115 我们通过几个步骤修正后,就能得出网站花费的最小/最大时间。
注意:答案可能因您的子集而异。
3。直方图在列标题下表示什么?
直方图描述了列中响应的类别。例如版本 2 产品名称可以在上面的截图中看到。如果您将鼠标悬停在条形上,它会告诉您该类别在列中出现的频率,如下所示:
现在,让我们用这个来回答下一个问题。
4。会议发起数量排名前五的国家:
他们依次是:美国>英国>印度>加拿大>日本
5。柱形图的颜色表示什么?
您一定已经注意到列标题下有一个条形,它有不同的颜色,表示缺少和不匹配的数据类型值。
灰色表示缺少值,而红色表示不匹配的值。我们将在下一部分纠正这些错误。
6。常见的流量来源有哪些? 要回答这个问题,我们需要查看模式描述中的channelGrouping
列。有 7 个来源,最常见的是:
有机搜索>转诊>直接
7。常见的产品类别有哪些? 要回答这个问题,我们需要查看模式描述中的v2ProductCategory
列。共有 31 个类别,最常见的有:
极致>服装>电子
5.数据争论和配方制作
既然我们已经知道了与现有数据集相关联的问题,那么让我们来构建转型方案。我们需要为我们发现有问题的列定义数据清理步骤,就像上面数据集中不匹配的时间列一样。
- 修改列
timeOnSite
的数据类型:点击列名前面的下拉菜单,从那里**修改类型>整数。**它会将所有值的数据类型更改为整数,这将是用户在网站上花费的秒数。
这里重要的一步是注意食谱图标,如果你点击它,会显示一个添加到它上面的步骤,如下所示:
在我们处理数据争论的过程中,这个方法会不断更新。
2。删除未使用的列:如果您仔细观察,会发现有几列有完整的灰色条,表明这些列都是空值(或者没有有效值)。这些列没有用处,是多余的。
注意:我们不能删除所有包含空值的列,因为我们正在处理数据的子集。如果在删除前不确定某列,请阅读该列的说明。
如何删除栏目?
单击列标题下的灰色栏,dataprep 将高亮显示所有缺少的值行。
现在,点击下拉菜单>点击删除。
对 itemQuantity 和 itemRevenue 列重复该过程,因为我们将使用 productQuantity 和 productRevenue 列,因为它们在其中有值,这对收入报告很有用。
现在,更新后的食谱看起来像这样:
**3。删除重复行:**EDA 中另一个非常常见和有用的步骤是从数据中删除重复行,在 pandas 中我们使用drop_duplicate
函数,这里我们将使用顶部的工具栏。
点击过滤行图标,然后选择移除重复行。 系统会询问您是否要将此步骤添加到配方中,作为将来对所有输入数据集运行的步骤。
点击添加。
4。向配方添加自定义公式:****totalTransactionRevenue列包含交易产生的总收入。从模式文档中,您可以看到它已经乘以了 10⁶,但从数值上看并不清楚。
从下拉菜单>计算>自定义公式:
在打开的右窗格中,添加公式:
我们已经向totalTransactionRevenue列添加了一个定制的**DIVIDE**
公式,预览可以在 transformer 视图中显示。点击添加,添加到配方中。
**5。合并列:**合并/连接是 EDA 中另一个非常重要和常见的任务,Dataprep 也能以最轻松的方式完成。让我们为每个用户会话创建一个主键列。
我们将 fullVisitorId、time 和 v2ProductName 用于此任务,因为它为每个会话提供了唯一的值,由一个|
分隔,并将新列命名为 main_id 。
点击添加。
这是我带你走过的几个步骤,使用这个智能工具,你可以进行一系列的操作和操纵。
这是食谱现在的样子:
6.运行和调度 Dataprep 作业
现在管道已经准备好运行,点击右上角的运行作业。
因为出于分析或建模的目的,我们需要将转换后的数据添加回新表中的 BigQuery。
点击右侧的添加发布动作,然后选择 BigQuery 作为输出目的地,同步的数据集将变为可用,这里您需要使用右侧窗格中的创建表选项创建一个新表。
点击底部的添加即可:
设置好您需要的地区和机器类型后,单击页面右下角的“运行作业”。这项工作将需要几分钟的时间,一旦完成,您可以在 BigQuery 中检查您的结果,带有转换数据的表格正等着您!!
调度作业
另一个重要的特性是在 dataprep 上调度作业运行的能力。在流程页面上,点击顶部的 3 个点图标,然后选择计划流程。
您可以选择时区、频率、日期和时间,如下所示:
结论
谁会想到将创建管道这一费力的任务简化到这种程度?正如我们所说,谷歌云团队正在添加一些无与伦比的工具和功能,以重新定义云计算的含义。在估算了 dataprep 为我们节省的工时数后,我着迷了,没有比这更有用、连接更好、界面更友好的工具了。更多有趣的工作流将被讨论,我很高兴揭开我要讲述的关于 GCP 数据工程的故事。敬请期待!!
Harshit 的数据科学
通过这个渠道,我计划推出几个涵盖整个数据科学领域的系列。以下是你应该订阅频道的原因:
- 该系列将涵盖每个主题和副主题的所有必需/要求的高质量教程。
- 解释了为什么我们在 ML 和深度学习中做这些事情的数学和推导。
- 与谷歌、微软、亚马逊等公司的数据科学家和工程师以及大数据驱动型公司的首席执行官的播客。
- 项目和说明,以实现迄今为止所学的主题。
你可以在 LinkedIn 、 Twitter 或 Instagram 上与我联系(在那里我谈论健康和福祉。)
注意:在这些黑暗的时期,自我隔离为自我提升腾出了一些空间,我们可以利用这些空间来发展新的技能、爱好,并帮助未来的自己。
构建数据科学能力意味着长期博弈
“管理就是在制定长期计划的同时进行短期管理.”
——杰克·韦尔奇
您是否对寻找和留住不需要大量时间和投资来发展的数据科学员工的挑战感到沮丧?你并不孤单。
最近在一次机器学习会议上,有人向我表达了对持续的数据科学人才短缺的厌倦。一家知名数据驱动型公司的高管哀叹道,他就是找不到“优秀的数据科学人才,他不必花大量时间去培训他们”。他的观点是可以理解的。这家公司多年来一直在数据科学能力上投资,远远领先于其竞争对手,并且仍在努力寻找和培养经验丰富的数据科学人才。
为什么?因为对顶级分析人才的竞争持续加剧,留住这些人才变得非常困难和昂贵,即使对于最数据驱动的品牌公司也是如此。此外,技术和所有计算机科学继续以闪电般的速度发展,而 zettabytes 的数据堆积如山。面对这些越来越多的挑战,也许是时候组织重新思考他们创建数据科学团队的方法了。
那么,该怎么办呢?美国政府给了我们一个线索。继最近关于保持美国在人工智能领域领先地位的行政命令之后,美国前首席技术官 Meghan Smith 敦促学校儿童学习代码并玩树莓派的作为国家的必要事项。
这些权力正在向世界传递一个简单但关键的信息:在数据科学和人工智能方面,要为技能的长期发展进行规划和投资。不这样做的后果也很简单。您将数据科学人才融入组织的努力将永远不会停止。
你说的这些都不错,但是面对前面提到的每天产生的 zettabytes 数据以及从这些数据中获取价值的紧迫性,公司需要专注于当前迫切的数据科学需求。他们现在需要数据科学家。他们不能等待现在的八年级学生掌握技能并毕业。那么,他们有什么选择呢?
QuantHub 在数据科学技能方面的经验
在 QuantHub,我们一次又一次地看到从入门级数据科学家、数据工程师到博士候选人的实际数据科学技能评估分数。你知道吗?技能无处不在。很少有候选人能在各种技能中脱颖而出。尽管如此,我们坚信有一个良好的数据科学人才“潜力”基础,正等待着进一步开发。这是缓解我们裁谈会的朋友的疲劳感的关键所在。
An example of the variety of skills scores we see on data science/engineering assessments
所以,让我们像麦肯锡那样定义长期游戏
我是麦肯锡研究的粉丝。他们往往处于一切事物的最前沿,并且长期以来一直非常活跃地研究数据科学领域。以下是麦肯锡如何推广雇佣数据科学家的想法,以提供一些视角。
十多年前的 2007 年,麦肯锡发布了“八大值得关注的商业技术趋势”。在那篇文章中,它引用了“将更多科学纳入管理”是一种趋势,“技术正在帮助经理们利用越来越多的数据来做出更明智的决策,并开发创造竞争优势和新商业模式的洞察力。”是的,没错——他们 12 年前就这么说了。
然后在 2011 年,麦肯锡写了一份名为“大数据:创新、竞争和生产力的下一个前沿”的报告,其中它称赞大数据是一股“不断增长的洪流”,有各种统计数据可以证明这一点。五年后的 2016 年,产生了一项后续研究“分析时代:在数据驱动的世界中竞争”。这是它三年前给出的观点,
当我们评估过去五年取得的进展时,我们看到公司正在数据和分析上下大赌注。但是,对于个人或组织来说,适应一个更加数据驱动的决策时代并不总是一件简单的事情。许多人正在努力培养人才、业务流程和组织能力,以从分析中获取真正的价值。这正成为一个紧迫的问题,因为分析能力越来越成为行业竞争的基础,而领导者们正在锁定巨大的优势。与此同时,技术本身正在取得重大的飞跃——下一代技术有望带来更大的突破。机器学习和深度学习能力有着各种各样的应用,深入到迄今为止在很大程度上处于边缘地位的经济领域。
快进到 2018 年,麦肯锡发布了“分析时代来临”,其中提出“数据是这个领域的硬币。”翻译:数据是非常有价值的,哦,顺便说一下,10 多年来,我们一直告诉你要发展能力和才能,从数据中获取价值。时间到了。
这是什么意思?如果你早在 2007 年就加入了这股潮流,并在那时开始投资大数据分析,理论上,你可能要等十多年,你的分析投资才会“成熟”并成为有价值的“硬币”。
那么为什么在这个领域工作的员工不能享受同样的时间和投资呢?
发展数据科学能力的长期路线图
正如我之前提到的,数据科学人才潜力很大,但大部分都不完美。为了利用这种人才,然后发展数据科学能力,公司需要制定一个长期计划来实现这一点,然后实施它。
这里有一个路线图,用于开发一个多管齐下的长期方法来“训练优秀的人”成为更好的数据科学家。
1.放弃独角兽,真的
就像机器学习高管一样,你不能因为沮丧而继续把头埋在沙子里,而不承认当今数据科学就业市场的现实。你永远也找不到你需要的确切候选人。当面临大数据带来的紧迫挑战时,人们很容易幻想不存在的完美解决方案。因此,正如弗雷斯特研究公司的人最近所说的,“小心独角兽”。认识到你正在寻找独角兽,努力快速找到解决方案。接受你找不到这些人的事实,然后继续下一步。
2.认识并修正你招聘过程中的缺陷
分析和数据科学标准倡议最近的一项研究发现,数据科学家和数据工程师在 LinkedIn 简历中列出的技能与雇主对数据科学和分析职位要求的技能之间存在很大的脱节。虽然候选人列出了适用于数据科学领域的各种硬技能和软技能,但雇主招聘的是非常具体的技术技能,尤其是编程语言。
在雇主为数据科学职位列出的前 10 项必备技能中,几乎所有技能都是编程语言,而实际的数据科学家通常拥有几种非编程技能,如数据分析和统计,以及其他相关技能,如雇主要求中完全不存在的数据争论。
还有许多其他证据表明,数据科学人才的招聘流程存在缺陷,无论是公司不知道他们招聘的是什么技能,人力资源经理无法正确筛选候选人,还是要求不必要的高等学位。也许这位高管所在的公司长期以来一直使用相同的招聘和保留策略,以至于没有根据就业市场和现有人才调整其招聘和发展流程。
无论如何,数据科学招聘要求和数据科学候选人的现实之间的持续脱节肯定会导致无法找到“优秀人才”进行培训和发展。他们只是在招聘过程中被忽视了。
3.开发数据科学人才的组合视图
许多组织尚未认识到在将数据科学应用于业务战略时成功所需的人才和技能组合。现实是,没有一个人,甚至两个数据科学家能够涵盖所有的基础,也不应该期望他们这样做。因此,你应该致力于开发一个由个人组成的组合,这些个人共同拥有你所需要的才能。
在我最近主持的一次网上研讨会中,来自 Protective Life 和 Regions Bank 的数据科学高管都描述了在软硬技能组合和教育程度方面雇佣多样化数据科学人才的类似策略。作为高度量化行业中经验丰富的数据分析经理,这些企业领导人认识到这是在其组织中建立和保持长期成功的数据科学能力的唯一途径。
做到这一点的第一步是定义随着时间的推移组织所需的人才,而不是定义具有特定技能的团队成员或特定数据项目所需的技能集。与其考虑投资 100,000 美元以上来招聘特定的角色,不如考虑如何投资开发组织在数据工作中取得成功所需的技能和人才。您可能会发现,这些远远超出了大多数公司在规划数据科学计划时招聘的典型“R”编码语言和 SQL 功能。
一个完善的数据科学战略可能需要功能领域知识来帮助确定高价值的商业案例,需要设计思维技能来帮助头脑风暴解决方案,需要财务技能来创建令人信服的商业案例,需要数据工程和争论技能来以所需的形式提供对正确数据的访问,还需要机器学习技能来推动人工智能技术的执行。你真的能让一两个人做到这一切吗?大概不会。
4.技能和才能的交叉培训
与前一点类似,保护生命采取的另一种故意的方法是他们称之为“异花授粉”。这意味着他们雇佣不同的人,他们拥有不同但需要的技能,然后把这些人放在一个房间里一起工作很长时间,有很多咖啡和休息时间。随着时间的推移,这为技能交流和知识共享提供了大量机会。他们还定期派这些人到组织中去与商务人士合作和喝咖啡。异花授粉成功的关键是有目的地为其自然发生创造“空间”。
5.把终身学习的目标放在学位和即时技能上
关于严格遵守特定学位资格是否导致了大量数据科学人才短缺的争论仍在激烈进行。例如,在制药行业,从事人工智能研究的本科生除非拥有硕士或博士学位,否则没有晋升机会是很常见的。这种符合晋升资格的严格要求完全没有必要,是短视的,可能反映了企业不愿意或没有能力投资于初级数据科学人才的长期培训和发展。因此,被招聘的人才看不到职业发展,要么表现不佳,要么在其他地方寻找发展机会。
此外,那些自学的数据科学训练营毕业生可能不具备最完美的编码技能。然而,他们有一项已展示的技能:学习具有挑战性的材料和开发高难度技能的动力。众所周知,数据科学和商业智能技术日新月异。编码语言会来来去去。技术将会迅速发展。随着时间的推移,自我激励学习和发展的人是你可以长期投资的人,这样做的话,你就可以在你的组织中留住他们。
扪心自问:谁更忠诚?一个随时随地都能找到高薪工作的博士?或者是一个聪明但自学成才的毕业生,渴望成长和学习,并寻求进一步提高他们的工作技能——即使是在业余时间?
6.构建旨在培养数据科学人才的职业道路
该地区银行采取的一项策略是,允许并鼓励其数据科学家和分析师在组织内流动。它在新员工和年度绩效评估中公开讨论和计划这一点。因此,入门级数据科学家将进入一个部门,并知道在该部门学习所有内容后的几年内,他们将有机会转移到组织的一个新领域,在那里他们可以继续发展,做出贡献并得到认可。
这一策略很好地体现了一个非常简单的事实:就在您最优秀的数据科学家开始大展拳脚的时候,他们也可能会考虑成家立业,从而在您的组织内寻找新的机遇和挑战,而不是面临跳槽。当他们看到自己有前途,可以开始考虑其他人生目标时,他们可能会计划在你的公司呆更长时间。
除此之外,公司应该开始为所有员工创建一个混合职业发展轨道——一个与数据科学和业务平行的轨道,它允许业务人员进入分析领域,反之亦然。例如,如果您的公司能够负担得起,您可以实施教育和培训计划,为有兴趣获得分析资格和技能的员工提供支持。这些员工可以享受学费补贴,以换取他们同意在贵公司工作一定年限。
7.使数据科学民主化
培养和发展数据科学家的真正长期解决方案来自数据科学的民主化。公司必须找到方法,停止将数据科学知识降级给少数高度专业化、拥有博士学位的准独角兽。数据科学的责任正以前所未有的速度增长。这给数据科学团队本身以及招聘人员和经理带来了巨大的压力。
因此,麦肯锡、哈佛、德勤和 Airbnb 等公司一直倡导的是数据科学的民主化,以此作为解决数据科学人才短缺的一种方式,但更重要的是,作为提取数据科学真正价值的关键一步。公司正在建立数据科学大学来培训所有管理人员,并雇用“混合”角色,如可视化专家,以帮助弥合数据科学家和组织其他人员之间的差距。
一些公司还在积极实施自动化工具、预训练模型和自助服务分析,以使数据科学技术和技能更容易为整个组织所用。随着时间的推移,数据科学的民主化可能会减轻数据科学团队的压力,并促进对他们真正需要的培训程度做出更公平的判断。
诚然,大多数公司离数据科学民主化还很远,但实现这一目标的速度可能比你想象的要快。
十年后…
很明显,如果不在人才招聘和以新方式规划数据科学人才的未来发展方面冒一些风险,就没有一种方法可以立即满足广泛的数据科学需求。组织需要采取措施,缓解目前对数据科学候选人和试图聘用他们的人提出的不切实际的要求。这需要从长远着眼,在组织内开发一套更加平衡的能力。
许多提议的步骤实际上并不需要太多的额外投资。与招聘和雇佣一名“现成”数据科学家的成本相比,新兵训练营和 LinkedIn Learning 并不那么昂贵。相反,我们需要的是承认雇佣现成的数据科学家是一个日渐式微的选择。现在就开始制定你的长期路线图,也许在 10 年后,当麦肯锡报告称人工智能已经“成熟”时,你将拥有一支本土和忠诚的数据科学家团队,随时准备利用数据科学的未来所提供的一切。
构建分布式应用程序:第一近似值
在之前的文章中,我们已经讨论了反应式架构的理论基础。现在是时候讨论数据流、实现反应式 Erlang/Elixir 系统的方法以及其中包含的消息交换模式了:
- 请求-响应
- 请求分块响应
- 带请求的响应
- 发布-订阅
- 反向发布-订阅
- 任务分配
SOA、MSA 和消息传递
SOA 和 MSA 是定义系统开发规则的服务架构。反过来,消息传递为我们提供了实现它们的原语。
我不想提倡任何特定类型的建筑。我完全赞成使用完全满足某个项目或业务需求的实践。无论我们选择什么样的范例,系统组件都应该用 Unix 方式来创建:具有最小连接性的组件实现不同的系统实体。API 方法对实体执行基本操作。
消息传递,从名字就能猜到,是一个消息代理。它的主要目的是接收和发送信息。此外,它还负责消息发送接口、创建逻辑数据通信通道、路由、平衡和处理系统级故障。
正在开发的消息传递并不试图与 Rabbitmq 竞争或取代它。其主要特点如下:
- 分布式自然。可以在集群的任何节点上创建交换,尽可能靠近利用它们的代码。
- 简约。面向可用性和样板代码的最小化。
- 更好的性能。我们并不试图复制 Rabbitmq 的功能。我们只是强调架构和传输层,并将它们嵌入到 OTP 中,同时降低成本。
- 灵活性。每个服务都可以集成许多交换模式。
- 容错体现在设计中。
- 可扩展性。信息与应用一起成长。随着负载的增加,我们可以将一些交换点放在不同的机器上。
注意。 就代码组织而言,元项目也就是伞状项目非常适合 Erlang/Elixir 中的复杂系统。所有的项目代码都位于一个存储库中——保护伞项目。同时,微服务被尽可能地隔离,并执行由独立实体负责的简单操作。在这种方法下,很容易支持整个系统的 API。您还可以毫不费力地进行更改并编写单元测试或综合测试。
在服务架构中,组件直接或通过代理进行交互。从消息传递的角度来看,每个服务都有多个生命阶段:
- 服务初始化。在这一阶段,初始化和启动流程及其所有依赖项。
- 创建交易所。服务可以使用节点配置中给定的静态交换,也可以动态创建交换。
- 服务注册。服务必须在 exchange 上注册才能处理请求。
- 正常工作。在这种模式下,服务处理用户的请求。
- 过程的终止。有两种可能的终止方式:正常终止和崩溃终止。在第一种情况下,服务从交换机断开并停止。然而,在崩溃的情况下,消息传递执行处理故障的场景之一。
这看起来可能有点复杂,但是如果你看一下代码,实际上并没有那么糟糕。本文稍后将给出一些代码示例。
交换
交换是消息服务的内部过程。它在定义的消息交换模式中提供不同系统组件之间的交互逻辑。
在下面的所有示例中,组件通过交换进行交互,交换的组合创建了消息传递。
消息交换模式(MEP)
一般来说,所有的 MEP 都可以分为双向和单向模式。前者意味着对收到的信息作出反应,后者则不然。客户机-服务器体系结构中双向 MEP 的一个经典例子是请求-响应模板。让我们仔细看看这个和它的修改。
请求-响应或 RPC
当我们需要从另一个进程获得响应时,就会用到 RPC。这个过程可以在同一个节点开始,甚至可以位于不同的洲。下图展示了通过消息传递实现的客户端-服务器交互。
因为消息传递是完全异步的,所以客户端的交换包括两个阶段:
- 发送请求。
messaging:request(Exchange, ResponseMatchingTag, RequestDefinition, HandlerProcess)
交换是唯一的交换名称
ResponseMatchingTag 用于处理响应的本地标签。例如,当属于不同用户的几个相同的请求被发送时。
请求定义**请求体
**处理器流程处理器 PID。这个过程将得到一个服务器响应。
- 收到回应
*handle_info(#'$msg'{exchange = EXCHANGE, tag = ResponseMatchingTag,message = ResponsePayload}, State)*
ResponsePayload 是服务器响应。
对于服务器,该过程也分两个阶段进行:
- Exchange 初始化
- 请求的处理
这种模式可以用代码来说明。假设我们必须创建一些简单的服务来提供精确时间的方法。
首先,让我们将 API 定义代码放入 api.hrl :
*%% =====================================================%% entities%% =====================================================-record(time, { unixtime :: non_neg_integer(), datetime :: binary()}).-record(time_error, { code :: non_neg_integer(), error :: term()}).%% =====================================================%% methods%% =====================================================-record(time_req, { opts :: term()}).-record(time_resp, { result :: #time{} | #time_error{}}).*
让我们在 time_controller.erl 中定义服务控制器
*%% The example shows the main code only. If you insert it into gen_server pattern, you will get a proper working service.%% gen_server initializationinit(Args) -> %% connection to exchange messaging:monitor_exchange(req_resp, ?EXCHANGE, default, self()) {ok, #{}}.%% processing of exchange disconnection events. The same happens if an exchange hasn’t been run yet.handle_info(#exchange_die{exchange = ?EXCHANGE}, State) -> erlang:send(self(), monitor_exchange), {noreply, State};%% API processinghandle_info(#time_req{opts = _Opts}, State) -> messaging:response_once(Client, #time_resp{ result = #time{ unixtime = time_utils:unixtime(now()), datetime = time_utils:iso8601_fmt(now())} }); {noreply, State};%% gen_server terminationterminate(_Reason, _State) -> messaging:demonitor_exchange(req_resp, ?EXCHANGE, default, self()), ok.*
客户端代码放在 client.erl. 向服务发送请求,我们可以在客户端的任何部分调用消息请求 API:
*case messaging:request(?EXCHANGE, tag, #time_req{opts = #{}}, self()) of ok -> ok; _ -> %% repeat or fail logicend*
我们不知道系统的状态,因为它依赖于请求到达时各种系统组件的状态。当请求到来时,消息传递可能还没有运行,或者服务控制器可能还没有准备好处理请求。这就是为什么我们必须检查消息响应并处理失败。
如果请求被提交,服务可以用一个响应或一个失败来回答。让我们在 handle_info 中处理这两种情况:
*handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time{unixtime = Utime}}}, State) -> ?debugVal(Utime), {noreply, State};handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time_error{code = ErrorCode}}}, State) -> ?debugVal({error, ErrorCode}), {noreply, State};*
请求分块响应
更安全的做法是不要发送太大的消息,因为它们可能会影响系统的总体响应能力和性能。如果一个响应占用了太多的内存,那么拆分就至关重要。
这里有几个例子:
- 组件正在交换二进制数据,例如一些文件。将响应分成小块有助于我们处理任何大小的文件,并防止内存溢出。
- 清单例如,我们需要选择数据库中巨大表格的所有行,并将其传输到另一个组件。
这种反应有时看起来像一列火车。无论如何,1024 条 1mb 的消息比一条 1gb 的消息要好。
Erlang 集群带来了额外的好处,因为响应绕过交换机直接到达接收方,所以减少了交换机和网络的负载。
带请求的响应
这是一个非常罕见的构建对话系统的 RPC 模式的修改。
发布-订阅(数据分布树)
事件驱动系统在数据准备好的时候向用户交付数据。系统更倾向于推模型,而不是拉或轮询模型。这个特性使我们在不断请求和等待数据时不会浪费资源。
该图显示了向订阅某个主题的用户分发消息的过程。
这种模式应用的一个经典案例是状态分布:它可以是计算机游戏中的游戏世界、金融交易中的市场数据或数据馈送中的有用信息。
看一下用户代码:
*init(_Args) -> %% subscribe to exchange, routing key = key messaging:subscribe(?SUBSCRIPTION, key, tag, self()), {ok, #{}}.handle_info(#exchange_die{exchange = ?SUBSCRIPTION}, State) -> %% if an exchange is unavailable, we are trying to reconnect to it messaging:subscribe(?SUBSCRIPTION, key, tag, self()), {noreply, State};%% process received messageshandle_info(#'$msg'{exchange = ?SUBSCRIPTION, message = Msg}, State) -> ?debugVal(Msg), {noreply, State};%% if a consumer stops, disconnect from exchangeterminate(_Reason, _State) -> messaging:unsubscribe(?SUBSCRIPTION, key, tag, self()), ok.*
发布者可以通过函数调用在任何合适的地方发布消息:
*messaging:publish_message(Exchange, Key, Message)*
**交易所交易所名称、
**键路由键、
**信息有效载荷。
反向发布-订阅
当我们反转发布-订阅时,我们得到一个有利于日志记录的模式。生产者和消费者可以有很大的不同。该图显示了一个消费者和多个生产者的情况。
任务分配模式
几乎每个项目都向我们提出了延期处理任务的挑战,比如生成报告、发送通知、从第三方系统获取数据。通过增加工人,系统容量很容易扩展。现在剩下的就是组成一群工人,并在他们之间平均分配任务。
让我们考虑一个有三个工人的例子。在分配任务的阶段,我们面临着分配的公平性和工人超负荷的问题。Round-robin 将负责提供公平性,而 prefetch_limit 将防止工人过载。在临时模式下,prefetch_limit 不允许一个工人获得所有任务。
消息控制队列和处理优先级。工人来了就有任务。任何任务都可以成功完成,也可以失败。
- 消息传递:当消息被成功处理时,ack(Tack) 被调用
- 信息:nack(Tack) 在所有紧急情况下都会被调用。任务返回后,消息会将其重新发送给另一个工作人员。
让我们假设在处理 3 个任务时发生了复杂的故障。工人 1 在获得任务后崩溃,并且未能通知交换。在这种情况下,当确认*超时时,交换将任务重定向到另一个工作者。出于某种原因,工人 3 拒绝了该任务并发送了 *nack。结果,这个任务被另一个设法完成它的工人处理掉了。
初步结论
在本文中,我们仔细研究了分布式系统的主要构件。我们还对它们在 Erlang/Elixir 中的实现有了基本的了解。
通过组合基本模式,您可以构建复杂的范例来应对新出现的挑战。
最后一部分将集中在管理服务、路由和平衡的一些一般性问题上。我们还将讨论系统可伸缩性和容错性的实际方面。
第二部分的结尾。
照片由马里乌斯·克里斯滕森
用 websequencediagrams.com 制作的插图
构建分布式应用程序:二次近似
本文是关于 Erlang/Elixir 中分布式反应式应用的系列文章的最后一篇。在第一篇文章中,你会找到反应式架构的理论基础。第二个展示了构建这种系统的主要模板和机制。
今天我们将讨论代码库开发和项目开发的一般问题。
服务机构
当我们在现实生活中处理服务开发时,我们经常需要在一个控制器中组合几种交互模式。例如,执行管理项目用户配置文件的服务必须响应 req-resp 请求,并使用发布-订阅发送配置文件更新事件的通知。这种情况非常简单:在消息传递的背后,只有一个控制器实现服务逻辑并发布更新。
当我们必须创建一个容错的分布式服务时,事情变得更加复杂。让我们假设对用户服务的要求发生了变化:
- 该服务现在必须在集群的 5 个节点上处理请求,
- 它必须能够执行后台处理任务,
- 它还应该能够动态管理订阅列表
注意:在本文中,我们并不解决数据的一致存储和复制问题。让我们假设这些问题以前已经解决了——系统已经有了一个可伸缩的、可靠的存储层,处理程序可以与之交互。
用户服务的形式化描述变得更加复杂。从程序员的角度来看,由于使用了消息传递,所以变化很小。为了满足第一个要求,我们必须在请求-响应上定义平衡算法。
处理后台进程的需求经常出现。在用户中,它可以检查用户的文档,处理下载的多媒体或与社交网络同步数据。这些任务应该分布在集群中,并且必须控制它们的实现。这就是为什么我们有两个选择:要么使用前一篇文章中的模式,要么如果它不适合我们,编写一个定制的任务调度程序,它将控制处理程序池。
第三点需要发布-订阅模式扩展。要实现它,我们应该首先创建一个发布-订阅交换,然后在我们的服务中运行这个交换的控制器。这样,我们就把订阅事件和订阅者列表的管理从消息层转移到了用户实现。
因此,任务分解表明,为了满足需求,我们应该在不同的节点上运行服务的 5 个副本,并创建一个额外的实体—负责订阅的发布-订阅控制器。
要运行 5 个处理程序,没有必要更改服务代码。只有一个额外的行动——设定交易所的平衡规则。我们稍后会谈到这一点。此外,还有一个困难:发布-订阅控制器和定制的调度程序必须单独工作。作为基础的消息传递服务必须提供一种领袖选举的机制。
领导人选举
在分布式系统中,这是指定负责在集群上调度和分配任务和负载的唯一进程的过程。
在不倾向于集中的系统中,使用一些通用的和基于共识的算法,例如 paxos 或 raft。
由于消息传递同时是一个代理和一个中心元素,它知道所有服务控制者——领导候选人。消息传递可以不经投票任命领导人。
启动并连接到 exchange 后,所有服务都会收到一条系统消息:
#‘$leader’{exchange =?EXCHANGE,pid = LeaderPid,servers = Servers}
如果 LeaderPid 与当前进程 Pid 匹配,它将被指定为 leader。服务器列表包括所有节点及其参数。
当一个新的集群节点出现并且当前工作的集群节点关闭时,所有的服务控制器
#‘$slave_up’{exchange =?EXCHANGE,pid = SlavePid,options = SlaveOpts}
和
#‘$slave_down’{exchange =?EXCHANGE,pid = SlavePid,options = SlaveOpts}
分别是。
这样,所有的组件都知道所有的变化。此外,在任何给定的时刻,集群中只有一个确定的领导者。
中间件
如果我们需要实现复杂的分布式处理或优化现有的体系结构,中间件使用起来很方便。
如果您不想更改服务代码并处理额外的处理、消息日志的路由等问题,您可以打开代理处理程序,它将完成所有的工作。
发布-订阅优化的一个经典例子是一个分布式应用程序,其业务核心是生成更新事件,如市场价格的变化。这样的应用也有一个访问层——N 个服务器,为 web 客户端提供 websocket API。
客户服务,如果直接处理它,看起来像这样:
- 客户端与平台建立连接。在服务器端,终止流量,启动服务于此连接的进程。
- 更新授权和订阅发生在服务流程中。该流程调用主题的 subscribe 方法。
- 当一个事件在核心中生成时,它被传递给服务于该连接的进程。
假设我们有 50000 名主题为“新闻”的订户。订户平均分布在 5 台服务器上。最终,每次更新到达 exchange 时,都会被复制 50000 次:根据订阅者的数量,每台服务器复制 10000 次。看起来不是一个有效的计划,对吗?
为了改进它,让我们引入一个与交换同名的代理。全局名称注册管理机构必须能够在一个组中通过名称解析最近的进程,因为这很重要。
然后,让我们在接入层服务器上运行这个代理。我们所有服务于 websocket api 的进程都将订阅它,而不是订阅内核中的初始发布-订阅交换。代理仅在唯一订阅的情况下订阅核心,并将其获得的消息复制给所有订阅者。
因此,将在核心服务器和访问服务器之间发送 5 条消息,而不是 50000 条。
路由和平衡
请求-响应
在当前的消息传递实现中,有 7 种请求分发策略:
- 默认。请求发送到所有控制器。
- 循环赛。以循环的方式为输入的请求分配控制器。
- 共识。提供服务的控制器分为主控制器和从控制器。请求只发给领导。
- 共识&循环赛。组中有一个领导者,但是请求在所有成员之间分配。
- 粘粘的。哈希函数是为某个处理程序计算和保留的。具有此签名的所有后续请求都发送到同一个处理程序。
- 粘人好玩的。当交换被初始化时,应该定义用于粘性平衡的散列函数。
- 好玩。相当于 sticky-fun,但你也可以重定向,拒绝或预处理它。
发行策略是在交易所初始化时定义的。
除了平衡,消息传递使我们能够标记实体。让我们看看系统中的标签类型:
- 连接标签。让我们了解事件发生的关联。当控制器进程连接到同一个交换机但具有不同的路由关键字时使用。
- 服务标签。允许我们在一个服务内将处理者联合成组,并扩展路由和平衡机会。对于请求-响应模式,路由是线性的。我们向交换机发送请求,交换机将请求传递给服务器。然而,如果我们必须将处理程序分成逻辑组,那么这种划分是在标签的帮助下完成的。如果指示了标签,请求将被发送到某一组控制器。
- 请求标签。让我们能够区分不同的反应。由于我们的系统是异步的,我们需要有机会指示 RequestTag 来处理服务响应。通过这个标签,我们将了解我们所收到的请求的响应。
发布订阅
有了 pub-sub 就简单多了。我们有一个交易所,可以让信息被发布。交换在订阅了所需路由关键字的订阅者之间分发消息(可以说这相当于主题)。
可扩展性和容错性
系统可伸缩性通常取决于其层和组件的可伸缩性:
- 通过向群集中添加带有服务处理程序的额外节点来扩展服务。人们可以在试运行期间选择最佳的路由策略。
- 特定集群中的消息服务本身通常通过将负载最重的交换移动到单独的集群节点或者通过将代理进程添加到负载最重的集群区域来进行扩展。
- 整个系统的可伸缩性取决于体系结构的灵活性以及将独立的集群合并成一个逻辑实体的机会。
项目的成功通常取决于简单性、快速转换和可伸缩性。当前实现中的消息传递随着应用程序而增长。即使 50-60 台机器的集群对我们来说不够,我们也可以使用联盟。不幸的是,联邦的问题超出了本文的范围。
保留
在讨论负载平衡时,我们已经讨论了服务控制器预留。然而,消息也应该被保留。如果一个节点或一台机器出现故障,消息传递必须尽快自动恢复。
在我的项目中,我使用了额外的节点,这些节点可以在出现故障时承担部分负载。Erlang 为 OTP 应用程序提供了分布式模式的标准实现。分布式模式通过在另一个已经预先启动的节点上运行失效的应用程序来负责恢复。过程很清楚:出现故障后,应用程序会自动转移到故障转移节点。您可以在此获得关于此功能的更多详细信息。
表演
让我们尝试,至少粗略地,将 rabbitmq 的性能与我们定制的消息传递的性能进行比较。
我看到了 openstack 团队的 rabbitmq 的官方测试结果:
第 6.14.1.2.1.2.2 点。文档中显示了以下 RPC 转换结果:
让我们不要使用任何 OS 内核或 erlang VM 的高级设置。
测试环境:
- erl opts: +A1 +sbtu。
- 一个节点内的测试是在一台笔记本电脑上运行的,该笔记本电脑配有旧的移动版英特尔 i7。
- 集群测试在 10G 网络的服务器上运行。
- 代码在 docker 容器中工作。网络处于 NAT 模式。
测试的代码:
req_resp_bench(_) -> W = perftest:comprehensive(10000, fun() -> messaging:request(?EXCHANGE, default, ping, self()), receive #'$msg'{message = pong} -> ok after 5000 -> throw(timeout) end end ), true = lists:any(fun(E) -> E >= 30000 end, W), ok.
场景 1:测试在一台装有旧版移动版英特尔 i7 的笔记本电脑上运行。测试、消息和服务都在同一个节点和一个 docker-container 中实现:
约 0 秒内连续 10000 次循环(26987 次循环/秒)
约 1 秒内连续 20000 次循环(26915 次循环/秒)
约 4 秒内连续 100000 次循环(26957 次循环/秒)
并行 2 在 2 秒内完成 100000 次循环(44240 次循环/秒)
在大约 2 秒内并行 4 个 100000 周期(53459 周期/秒)
在约 2 秒内并行 10 个 100000 周期(52283 周期/秒)
在大约 3 秒内并行 100 个 100000 周期(49317 周期/秒)
场景 2: 3 个节点通过 docker (NAT)运行在不同的机器上。
约 1 秒内连续 10000 次循环(8684 次循环/秒)
约 2 秒内连续 20000 次循环(8424 次循环/秒)
约 12 秒内连续 100000 次循环(8655 次循环/秒)
并行 2 在 7 秒内完成 100000 次循环(15160 次循环/秒)
在大约 5 秒内并行 4 个 100000 周期(19133 周期/秒)
在大约 4 秒内并行 10 个 100000 周期(24399 周期/秒)
在大约 3 秒内并行 100 个 100000 周期(34517 周期/秒)
在所有情况下,CPU 利用率都没有超过 250%。
结果
我希望这个系列看起来不像是一个大脑垃圾场,并且我的经验对于分布式系统研究人员和刚刚起步的从业者来说都是真正有用的。我敢肯定,他们中的一些人对 Erlang/Elixir 很感兴趣,但也有疑虑。
公告。
在初秋,我将发表另一个关于排队系统工程的系列文章:《 VTrade 实验——为交易系统写一个框架的尝试》。在这个系列中,我们将讨论建立一个交易交易所、一个拍卖场和一个商店的理论和实践。
构建分布式应用程序:零近似值
如今,世界发展迅速。进步不断带来新的技术挑战。信息系统架构应该足够灵活,能够响应新的需求并不断发展。在本文中,我们将讨论事件驱动的架构、并发性、并行性、异步性,以及如何与 Erlang 中的所有这些和平共处。
介绍
作为开发人员,我们根据系统的规模和需求选择这种或那种类型的信息交换。在大多数情况下,可以用来协调服务的是一个带有代理的模式,例如,基于 RabbitMQ 或 kafka。有时,事件流、SLA 和对系统的控制级别使得现成的消息传递不适合我们。当然,你可以把系统稍微复杂一点,使用 ZeroMQ 或者 nanomsg。通过这样做,您将负责传输层和集群的形成。但是,如果系统缺乏吞吐量或标准的 Erlang 集群容量,那么添加额外实体的问题就需要检查和经济合理性。
反应式分布式应用的主题相当广泛。为了符合本文的格式,今天我们将重点讨论基于 Erlang/Elixir 构建的同构环境。Erlang/OTP 生态系统使我们能够以最少的时间和精力实现反应式架构。无论如何,我们需要一个消息传递层。
理论框架
工程从定义目标和限制开始。主要目的不在于为了发展而发展。我们需要的是得到一个安全的、可伸缩的工具,让我们创建和开发各种层次的现代应用程序。这些应用可能从服务于一小部分受众的单个服务器应用到 50–60 个节点的集群或集群联盟。因此,我们的主要目标是通过降低系统开发和拥有成本来实现利润最大化。
让我们强调一下目标系统的 4 个要求:
- 可计算性。独立单元可以垂直和水平缩放。还应该有机会无限制地横向扩展整个系统;
- E 发泄驱使的天性。系统总是准备好处理事件流并对其进行处理;
- L 有保证的热情。时间就是金钱,所以用户不应该等待太久;
- 故障容差。所有级别和服务都必须在出现故障时自动恢复。
如果该系统的基础满足 SELF 的最低要求,则该系统可以从 MVP 阶段前进并且是渐进的。消息传递作为基础设施工具和所有服务的基础,其特点是对程序员也很有用。
事件驱动的性质
为了让一个应用程序可以从一个服务器扩展到一个集群,它的架构应该提供松散耦合。异步模型满足了这一要求。在这样的模型中,发送者和接收者都关心他们的消息的信息负载,而不担心消息传递或路由。
可量测性
可伸缩性和系统性能通常是密切相关的。应用程序组件必须能够利用所有可用的资源。我们利用产能的效率越高,加工方法越优化,我们在设备上的花费就越少。在一台机器中,Erlang 创建了高度并发的环境。可以通过选择 Erlang VM 可访问的操作系统线程数量以及利用这些线程的调度程序数量来平衡并行性和并发性。
Erlang 进程没有共享状态,并且以非阻塞模式工作。这比传统的阻塞同步应用程序提供了相对较低的延迟和更高的吞吐量。Erlang 调度程序负责公平的 CPU 和 IO 分配。没有阻塞使应用程序即使在峰值负载或故障的情况下也能做出响应。
在集群级别,也存在利用率问题。重要的是在集群中的机器之间均匀地分配负载,并且不要使网络过载。想象一下:用户流量正在登陆负载平衡器(例如 haproxy、nginx 等)。这些平衡器在服务器池中的可用处理程序之间分发请求。在应用基础设施中,实现所需接口的服务只是最后一英里。它必须请求许多其他服务来响应初始查询。内部请求也需要路由和平衡。
为了正确管理数据流,消息传递应该为开发人员提供一个控制路由和负载分布的接口。因此,开发人员将能够使用微服务模式,解决常规任务和非标准任务。
从商业角度来看,可伸缩性是风险管理的工具之一。这里的关键是满足客户的需求并高效地使用硬件:
- 如果你通过进步来提高硬件的性能,硬件就不会因为软件的不完善而闲置。Erlang 可以完美地垂直扩展,并且能够利用所有 CPU 内核和可用内存;
- 在云环境中,我们可以根据当前或预测的负载来管理硬件数量,并确保 SLA。
容错
让我们考虑两个公理:“失败是不可接受的。”和“总会有失败。”对任何企业来说,软件故障都意味着金钱损失,更糟糕的是名誉损失。即使在潜在损失和容错软件的开发成本之间徘徊,通常也有可能达成妥协。
从短期来看,容错架构节省了购买现成集群解决方案的资金。它们可能会让你付出昂贵的代价,并且包含错误。
从长远来看,容错体系结构在开发的所有阶段都可以获得数倍的回报。
代码库内部的消息传递使您能够在开发阶段阐述系统中的组件关系。这有助于管理故障的任务,因为所有负责的组件都处理故障,并且具有自动故障转移的目标系统知道如何在故障后通过设计将其自身恢复到正常状态。
响应性
不管有什么错误,你的应用都应该响应查询并遵守 SLA。现实是现在的人还没有做好等待的准备。因此,企业必须进行调整。预计会有越来越多的应用程序响应速度很快。
响应式应用以接近实时的模式工作。Erlang VM 在软实时模式下运行。然而,对于某些领域,如证券交易、医药或工业设备,硬实时模式至关重要。响应系统提高了 UX 并有助于商业。
初步结论
当我计划这篇文章时,我想分享我创建消息代理的经验,并在此基础上进一步构建复杂的系统。然而,理论和激励部分被证明是太广泛了。
在文章的第二部分,我们将讨论交换点实现和消息交换模式的细微差别。
第三部分将致力于服务安排、路由和平衡的一般问题。此外,我们将讨论可伸缩性和容错的实际方面。
第一部分的结尾。
卢卡·布拉沃拍摄的照片
在 PyTorch 中构建高效的自定义数据集
了解 Dataset 类的来龙去脉,利用干净的代码结构,同时最大限度地减少培训期间管理大量数据的麻烦。
Managing data for neural network training can be hard at “scale”.
PyTorch 最近出现在我的圈子里,尽管我已经习惯了 Keras 和 TensorFlow 一段时间,但我还是不得不尝试一下。令人惊讶的是,我发现它非常令人耳目一新和可爱,尤其是 PyTorch 具有 Pythonic API、更固执己见的编程模式和一组很好的内置实用函数。我特别喜欢的一个功能是能够轻松地创建一个定制的Dataset
对象,然后可以使用内置的DataLoader
在训练模型时提供数据。
在本文中,我将从头开始探索 PyTorch Dataset
对象,目标是创建一个数据集来处理文本文件,以及如何为某项任务优化管道。我们首先通过一个玩具示例来回顾Dataset
实用程序的基础知识,然后逐步完成真正的任务。具体来说,我们希望创建一个管道来输入《上古卷轴(TES)》系列中角色名字的名字、这些角色名字的种族以及名字的性别,作为一个热张量。你可以在我的网站上找到这个数据集。
Dataset
级的基础知识
PyTorch 让您可以自由地对Dataset
类做任何事情,只要您覆盖了两个子类函数:
- 返回数据集大小的
__len__
函数,以及 __getitem__
函数从给定索引的数据集中返回一个样本。
数据集的大小有时可能是一个灰色区域,但它等于整个数据集中的样本数。所以如果你有一万个单词(或者数据点,图片,句子等。)在您的数据集中,__len__
函数应该返回 10,000。
一个最小的工作示例
让我们首先通过创建一个从 1 到 1000 的所有数字的Dataset
来模拟一个简单的数据集。我们将它恰当地命名为NumbersDataset
。
The first iteration of the numbers dataset.
Humble beginnings: printing the contents of the numbers dataset.
很简单,对吧?首先,当我们初始化NumbersDataset
时,我们立即创建一个名为samples
的列表,它将存储 1 到 1000 之间的所有数字。samples
这个名字是随意的,所以你可以随意使用任何你觉得舒服的名字。被覆盖的函数是不言自明的(我希望!)并对构造函数中启动的列表进行操作。如果您运行该文件,您将会看到输出的值 1000、101 和一个介于 122 和 361 之间的列表,它们分别是数据集的长度、数据集中索引 100 处的数据值以及索引 121 和 361 之间的数据集切片。
扩展数据集
让我们扩展这个数据集,以便它可以存储区间low
和high
之间的所有整数。
The second iteration of the numbers dataset with a user-defined range.
The outputs of a slice of the user-defined numbers dataset.
代码上面的代码应该打印 5474,2921 和 2943 到 3181 之间的数字列表。通过编辑构造函数,我们现在可以随心所欲地设置数据集的任意低值和高值。这个简单的改变表明了我们可以从 PyTorch Dataset
级获得什么样的里程。例如,我们可以生成多个不同的数据集并处理这些值,而不必像在 NumPy 中那样考虑编写新的类或创建许多难以理解的矩阵。
从文件中读取数据
让我们进一步扩展Dataset
类的功能。PyTorch 与 Python 标准库的接口非常优雅,这意味着您不必担心集成您已经了解并喜爱的特性。在这里,我们会
- 使用基本的 Python I/O 和一些静态文件创建一个全新的
Dataset
, - 收集 TES 角色的名字(数据集可在我的网站上获得),这些名字被分成种族文件夹和性别文件,以填充
samples
列表, - 通过在
samples
列表中存储一个元组来跟踪每个名字的种族和性别,而不仅仅是名字本身。
作为参考,TES 字符名称数据集具有以下目录结构:
.
|-- Altmer/
| |-- Female
| `-- Male
|-- Argonian/
| |-- Female
| `-- Male
... (truncated for brevity)
`-- Redguard/
|-- Female
`-- Male
每个文件都包含由换行符分隔的 TES 角色名,所以我们必须逐行读取每个文件,以获取每个种族和性别的所有角色名。
The first iteration of the TES names dataset
让我们浏览一下代码:我们首先创建一个空的samples
列表,并通过浏览每个种族文件夹和性别文件并读取每个文件中的名称来填充它。种族、性别和姓名存储在一个元组中,并追加到samples
列表中。运行该文件应该会打印出 19491 和('Bosmer', 'Female', 'Gluineth')
(但可能会因计算机而异)。让我们来看看如果我们将数据集分割成一个批处理会是什么样子:
# change the main function to the following:if __name__ == '__main__':
dataset = TESNamesDataset('/home/syafiq/Data/tes-names/')
print(dataset[10:60])
The output of a slice of the dataset samples containing a name, gender, and race.
正如您所料,它的工作方式与典型的列表完全一样。总结本节,我们刚刚将标准 Python I/O 引入 PyTorch 数据集,我们不需要任何其他特殊的包装器或助手,只需要纯 Python。事实上,我们还可以包括其他库,如 NumPy 或 Pandas,并通过一点巧妙的操作,让它们与 PyTorch 配合良好。让我们暂时就此打住,看看在训练循环的情况下,如何有效地遍历数据集。
用DataLoader
流动数据
虽然Dataset
类是系统包含数据的好方法,但是似乎在一个训练循环中,我们将需要索引或切片数据集的samples
列表。这并不比我们对典型的列表或 NumPy 矩阵所做的更好。PyTorch 没有走这条路,而是提供了另一个名为DataLoader
的实用函数,它充当了一个Dataset
对象的数据馈送器。我在这里看到的并行是 Keras 中的数据生成器flow
函数,如果你熟悉的话。DataLoader
接受一个Dataset
对象(以及任何扩展它的子类)和几个其他可选参数(在 PyTorch DataLoader 文档中列出)。在这些参数中,我们可以选择混洗数据,确定批量大小和并行加载数据的工作人员数量。下面是一个在枚举循环中流经TESNamesDataset
的简单例子。
# change the main function to the following:if __name__ == '__main__':
from torch.utils.data import DataLoader
dataset = TESNamesDataset('/home/syafiq/Data/tes-names/')
dataloader = DataLoader(dataset, batch_size=50, shuffle=True, num_workers=2) for i, batch in enumerate(dataloader):
print(i, batch)
当您看到批量打印出来的洪流时,您可能会注意到每个批量都是一个三元组列表:第一个元组是一群种族,第二个元组是性别,最后一个元组是姓名。
A torrent of names, genders, and races streamed from the dataset… in a strange ordering?
等等,这不是我们之前分割数据集时的样子!这是怎么回事?事实证明,DataLoader
以一种系统的方式加载数据,这样我们就可以垂直地而不是水平地堆叠数据。这对于张量的流动批次特别有用,因为张量垂直堆叠(即在第一维中)以形成批次。此外,DataLoader
还为您处理了数据的混洗,因此在输入数据时,不需要混洗矩阵或跟踪索引。
流动张量和其他类型
为了进一步探索不同类型的数据是如何通过DataLoader
流动的,我们将更新我们之前模拟的数字数据集,以产生两对张量:数据集中每个数字的 4 个后继值的张量,以及相同的后继张量,但添加了一些随机噪声。为了抛出一个曲线球,我们还想返回数字本身,但不是作为一个张量,而是作为一个 Python 字符串。总之,__getitem__
函数将在一个元组中返回三个不同种类的数据项。
The third iteration of the numbers dataset now with multiple data output types.
请注意,我们没有更改数据集构造函数,而是更改了__getitem__
函数。PyTorch 数据集的良好实践是,您要记住数据集将如何随着越来越多的样本而扩展,因此,我们不希望在运行时在内存中的Dataset
对象中存储太多的张量。相反,我们将在遍历样本列表时形成张量,牺牲一点速度来换取内存。我将在接下来的章节中解释这是如何有用的。
Different data types flowing together without breaking a sweat.
看看上面的输出,虽然我们新的__getitem__
函数返回了一个巨大的字符串和张量元组,但是DataLoader
能够识别数据并相应地对它们进行堆栈。字符串化的数字形成一个元组,其大小与加载程序配置的批处理大小相同。对于这两个张量,DataLoader
将它们垂直堆叠成一个大小为10x4
的张量。这是因为我们将批量大小配置为 10,而从__getitem__
函数返回的两个张量的大小为 4。
一般来说,加载程序会尝试将一批一维张量堆叠成二维张量,将一批二维张量堆叠成三维张量,依此类推。在这一点上,我恳求你认识到这对其他机器学习库中的传统数据处理产生的改变生活的影响,以及解决方案看起来有多干净。太不可思议了!如果你不同意我的观点,那么,至少你现在知道另一种方法,你可以在你的工具箱里。
完成 TES 数据集代码
让我们回到 TES 地名数据集。看起来初始化函数有点脏(至少对我的标准来说是这样,应该有办法让代码看起来更好。还记得我说过 PyTorch API 是 Pythonic 式的吗?那么,没有什么可以阻止你在数据集中声明其他的实用函数,或者甚至是为初始化创建内部函数。为了清理 TES 地名数据集代码,我们将更新TESNamesDataset
代码以实现以下功能:
- 更新构造函数以包含字符集,
- 创建内部函数来初始化数据集,
- 创建一个效用函数,将名义变量转换成一个热张量,
- 创建一个实用函数,将一个样本转换为一组三个代表种族、性别和姓名的单热张量。
为了使实用函数工作良好,我们将从scikit-learn
库获得一些帮助来编码名义值(即我们的种族、性别和姓名数据)。具体来说,我们将需要LabelEncoder
类。由于我们正在对代码进行大规模更新,我将在接下来的几个小节中解释这些变化。
The second iteration of the TES names dataset with major additions and updates.
构造函数初始化拆分
这里有相当多的变化,所以让我们一点一点来看。从构造函数开始,您可能已经注意到它没有任何文件处理逻辑。我们已经将这个逻辑移到了_init_dataset
函数中,并清理了构造函数。此外,我们还添加了一些空的编解码器,将名义值从原始字符串转换成整数,然后再转换回来。样品列表也是一个空列表,将在_init_dataset
功能中填充。构造函数还接受了一个名为charset
的新参数。顾名思义,它只是一个字符串,可以让char_codec
将字符转换成整数。
文件处理功能已经增加了几个集合,以便在遍历文件夹时捕获种族和性别等独特的名义值。如果您没有结构良好的数据集,这可能很有用;例如,如果阿贡人有另一组性别不可知论者的名字,我们将有一个名为“未知”的文件,这将被放入性别组,而不管其他种族是否存在“未知”性别。在所有的名字都被存储后,我们将初始化编解码器,使其适合我们的角色集中的种族、性别和角色的唯一值。
效用函数
添加了两个实用函数:to_one_hot
和one_hot_sample
。to_one_hot
在应用一个看似不合适的torch.eye
函数之前,使用数据集的内部编解码器首先将一个值列表转换为一个整数列表。这实际上是一个简单的方法,可以快速地将一个整数列表转换成一个热点向量。torch.eye
函数创建一个任意大小的单位矩阵,其对角线上的值为 1。如果对矩阵行进行索引,那么在该索引处会得到一个值为 1 的行向量,这就是一键向量的定义!
A smaller example of converting from raw strings into race categories as one-hot encoded vectors.
因为我们需要将三个值转换成张量,所以我们将在相应数据的每个编解码器上调用to_one_hot
函数。这在one_hot_sample
函数中进行组合,该函数将一个单样本转换成一个张量元组。种族和性别被转换成一个二维张量,它实际上是一个扩展的行向量。名字也被转换成二维张量,但是包括名字的每个字符的一个热行向量。
__getitem__
通话
最后,__getitem__
函数已经更新,只在给定样本的种族、性别和名称的情况下调用one_hot_sample
函数。注意,我们不需要在samples
列表中预先准备张量,而是仅当调用__getitem__
函数时,即当DataLoader
流数据时,才形成张量。当您在训练期间有成千上万的样本要处理时,这使得数据集非常具有可扩展性。
你可以想象这个数据集是如何用于视觉训练的。数据集将有一个文件名列表和图像目录的路径,让__getitem__
函数只读取图像文件,并及时将它们转换成张量用于训练。通过向DataLoader
提供适当数量的工作器来并行处理多个图像文件,可以使运行速度更快。 PyTorch 数据加载教程更详细地介绍了图像数据集和加载器,并用用于计算机视觉目的的torchvision
包(通常与 PyTorch 一起安装)补充了数据集,制作了图像处理管道(如白化、归一化、随机移位等)。)非常容易构造。
回到这篇文章。数据集检查完毕,看起来我们已经准备好将它用于训练…
…但我们不是
如果我们试图使用批处理大小大于 1 的DataLoader
来传输数据,我们将会遇到一个错误:
A mismatch made in data tensor heaven.
你们中精明的人可能已经看到了这一点,但事实是,文本数据很少以固定长度从一个样本到另一个样本。因此,DataLoader
试图批量处理多个不同长度的名字张量,这在张量格式中是不可能的,因为在 NumPy 数组中也是如此。为了说明这个问题,考虑这样一种情况,当我们将像“John”和“Steven”这样的名字堆叠到一个单个热矩阵中时。“John”翻译成大小为4xC
的二维张量,“Steven”翻译成大小为6xC
的二维张量,其中 C 是字符集的长度。DataLoader
试图将这些名字分批放入一个三维张量2x?xC
(想象一下堆叠大小为1x4xC
和1x6xC
的张量)。由于第二维度不匹配,DataLoader
出现错误,无法继续。
可能的解决方案
为了补救这一点,这里有两种方法,每一种都有其优点和缺点。
- 将批处理大小设置为 1,这样就不会遇到错误。如果批量大小为 1,单个张量不会与任何其他(可能)不同长度的张量堆叠在一起。然而,当执行训练时,这种方法受到影响,因为神经网络在单批梯度下降时收敛非常慢。另一方面,当批处理不重要时,这有利于快速测试时数据加载或沙箱化。
- 通过用空字符填充或截断来固定统一的名称长度。截断长名称或用虚拟字符填充短名称允许所有名称都是格式良好的,并且具有相同的输出张量大小,使得批处理成为可能。缺点是,根据手头的任务,虚拟字符可能是有害的,因为它不代表原始数据。
出于本文的目的,我将选择第二个选项,以表明您只需要对整个数据管道进行很少的更改就可以实现这一点。请注意,这也适用于任何不同长度的顺序数据(尽管有各种方法来填充数据,请参见 NumPy 中的选项和 PyTorch 中的)。在我的用例中,我选择用零填充名称,所以我更新了构造函数和_init_dataset
函数:
... def __init__(self, data_root, charset, length):
self.data_root = data_root
self.charset = charset + '\0'
self.length = length ... with open(gender_filepath, 'r') as gender_file:
for name in gender_file.read().splitlines():
if len(name) < self.length:
name += '\0' * (self.length - len(name))
else:
name = name[:self.length-1] + '\0'
self.samples.append((race, gender, name)) ...
首先,我向构造函数引入一个新参数length
,它将所有传入名称的字符数固定为这个值。我还在字符集中添加了\0
作为填充短名字的虚拟字符。接下来,更新了数据集初始化逻辑。不足固定长度的名称简单地用\0
s 填充,直到满足长度要求。超过固定长度的名字会被截短,最后一个字符用一个\0
替换。交换是可选的,取决于手头的任务。
如果您现在尝试流动这个数据集,您应该得到您最初预期的结果:一个格式良好的张量以期望的批量流动。下图显示了一批大小为 2 的张量,但请注意有三个张量:
Working data flow of all categorical variables converted into one-hot representations and well-formed via padding.
- 堆叠的种族张量,十个种族之一的一键编码版本,
- 堆叠的性别张量,也是针对数据集中存在的两种性别中的每一种进行的单热编码,以及
- 堆叠的名字张量,应该是最后一个维度的
charset
的长度,第二个维度的名字长度(固定大小后),第一个维度的批量大小。
数据分割实用程序
PyTorch 内置了所有这些功能,非常棒。现在可能出现的问题是,如何进行验证甚至测试集,以及如何在不弄乱代码库并尽可能保持代码干爽的情况下执行。测试数据集的一种方法是为训练数据和测试数据提供不同的data_root
,并在运行时保留两个数据集变量(此外,还有两个数据加载器),特别是如果您想在训练后立即进行测试。
相反,如果您想从训练集创建验证集,可以使用 PyTorch 数据实用程序中的random_split
函数轻松处理。random_split
函数接收一个数据集和一个列表中子集的期望大小,并以随机顺序自动分割数据,以生成较小的Dataset
对象,这些对象可立即用于DataLoader
。这里有一个例子。
Create validation sets by splitting your custom PyTorch datasets easily with built-in functions.
事实上,您可以在任意的时间间隔进行分割,这对于折叠的交叉验证集来说非常强大。我对这种方法唯一的不满是,你不能定义百分比分割,这很烦人。至少从一开始就清楚地定义了子数据集的大小。另外,请注意,您需要为每个数据集使用单独的DataLoader
,这肯定比管理两个随机排序的数据集并在一个循环中进行索引要干净。
结束语
我希望这篇文章能让您对 PyTorch 中的Dataset
和DataLoader
实用程序有所了解。结合干净的 Pythonic API,它使得编码更加令人愉快,同时还提供了一种有效的处理数据的方式。我认为 PyTorch 开发人员的易用性已经深深植根于他们的开发哲学中,在我的工作场所使用 PyTorch 之后,我就再也没有回头使用过 Keras 和 TensorFlow。我不得不说,我确实想念 Keras 模型附带的进度条和fit
/ predict
API,但这是一个小挫折,因为最新的 PyTorch 现在与 TensorBoard 接口,带回了一个熟悉的工作环境。然而,目前 PyTorch 是我未来深度学习项目的首选。
我鼓励以这种方式构建自己的数据集,因为它弥补了我以前在管理数据方面的许多混乱的编程习惯。在复杂的情况下,Dataset
实用程序是救命稻草。我记得必须管理属于单个样本但来自三个不同 MATLAB 矩阵文件的数据,并且需要正确地切片、归一化和转置。如果没有Dataset
和DataLoader
的组合,我无法理解管理它的努力,特别是因为数据是巨大的,没有简单的方法将它们组合成一个 NumPy 矩阵而不使任何计算机崩溃。
最后,查看一下 PyTorch 数据实用程序文档页面,其中有其他的类和函数可以探索,这是一个小而有价值的实用程序库。你可以在我的 GitHub 上找到 TES 名字数据集的代码,我已经在 PyTorch 中创建了一个 LSTM 名字预测器和数据集。让我知道这篇文章是否有帮助或不清楚,如果你想在未来更多的这种类型的内容。
使用 TensorFlow 构建高效的数据管道
拥有高效的数据管道对于任何机器学习模型都是至关重要的。在这篇博客中,我们将学习如何使用 TensorFlow 的数据集模块tf.data
来构建高效的数据管道。
https://www.tensorflow.org/guide/performance/datasets
动机
大多数关于 TensorFlow 的介绍性文章都会向您介绍将数据输入模型的feed_dict
方法。feed_dict
在单线程中处理输入数据,当数据在 CPU 上加载和处理时,GPU 保持空闲,当 GPU 正在训练一批数据时,CPU 保持空闲状态。TensorFlow 的开发者建议不要在相同数据集的训练或重复验证过程中使用这种方法。
tf_data
通过异步预取下一批数据来提高性能,使 GPU 无需等待数据。您还可以并行处理数据集的预处理和加载过程。
在这篇博文中,我们将介绍Datasets
和Iterators
。我们将学习如何从源数据创建数据集,将转换应用到数据集,然后使用迭代器消费数据。
如何创建数据集?
Tensorflow 提供了各种方法来从 numpy 数组、文本文件、CSV 文件、张量等创建数据集。让我们看看下面的几种方法
- from_tensor_slices: 它接受单个或多个 numpy 数组或张量。使用此方法创建的数据集一次只会发出一个数据。
# source data - numpy array
data = np.arange(10)# create a dataset from numpy array
dataset = tf.data.Dataset.from_tensor_slices(data)
对象dataset
是一个 tensorflow 数据集对象。
- from_tensors: 它也接受单个或多个 numpy 数组或 tensor。使用此方法创建的数据集将一次发出所有数据。
data = tf.arange(10)
dataset = tf.data.Dataset.from_tensors(data)
- from_generator: 创建一个数据集,其元素由函数生成。
def generator():
for i in range(10):
yield 2*i
dataset = tf.data.Dataset.from_generator(generator, (tf.int32))
数据集上的操作
- **批处理:**将数据集的连续元素合并成一个批处理。当您想要训练较小批次的数据以避免内存不足错误时,这很有用。
data = np.arange(10,40)# create batches of 10
dataset = tf.data.Dataset.from_tensor_slices(data).batch(10)# creates the iterator to consume the data
iterator = dataset.make_one_shot_iterator()
next_ele = iterator.get_next()with tf.Session() as sess:
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
您可以跳过创建迭代器并打印数据集元素的代码。我们将在本博客的后面部分详细了解迭代器。输出是:
[10 11 12 13 14 15 16 17 18 19]
[20 21 22 23 24 25 26 27 28 29]
[30 31 32 33 34 35 36 37 38 39]
- Zip: 通过压缩数据集来创建数据集。在具有要素和标注并且需要提供要素和标注对来训练模型的情况下非常有用。
datax = np.arange(10,20)
datay = np.arange(11,21)datasetx = tf.data.Dataset.from_tensor_slices(datax)
datasety = tf.data.Dataset.from_tensor_slices(datay)dcombined = tf.data.Dataset.zip((datasetx, datasety)).batch(2)
iterator = dcombined.make_one_shot_iterator()
next_ele = iterator.get_next()with tf.Session() as sess:
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
输出是
(array([10, 11]), array([11, 12]))
(array([12, 13]), array([13, 14]))
(array([14, 15]), array([15, 16]))
(array([16, 17]), array([17, 18]))
(array([18, 19]), array([19, 20]))
- **重复:**用于重复数据集。
dataset = tf.data.Dataset.from_tensor_slices(tf.range(10))
dataset = dataset.repeat(count=2)
iterator = dataset.make_one_shot_iterator()
next_ele = iterator.get_next()with tf.Session() as sess:
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
输出是
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
- Map: 用于变换数据集的元素。如果您希望在将原始数据输入到模型中之前对其进行转换,这将非常有用。
def map_fnc(x):
return x*2;data = np.arange(10)
dataset = tf.data.Dataset.from_tensor_slices(data)
dataset = dataset.map(map_fnc)iterator = dataset.make_one_shot_iterator()
next_ele = iterator.get_next()with tf.Session() as sess:
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
输出是
0 2 4 6 8 10 12 14 16 18
创建迭代器
我们已经学习了创建数据集和应用各种转换的各种方法,但是我们如何消费数据呢?Tensorflow 提供了Iterators
来做到这一点。
迭代器不知道数据集中存在的元素数量。它有一个get_next
函数,用于在 tensorflow 图中创建一个操作,当在一个会话上运行时,它将从迭代器返回值。一旦数据集用尽,就会抛出一个tf.errors.OutOfRangeError
异常。
我们来看看 TensorFlow 提供的各种Iterators
。
- **一次性迭代器:**这是迭代器最基本的形式。它不需要显式初始化,只对数据迭代一次,一旦用完,就无法重新初始化。
data = np.arange(10,15)#create the dataset
dataset = tf.data.Dataset.from_tensor_slices(data)#create the iterator
iterator = dataset.make_one_shot_iterator()
next_element = iterator.get_next()
with tf.Session() as sess:
val = sess.run(next_element)
print(val)
- **可初始化迭代器:**这个迭代器要求你通过运行
iterator.initialize.
显式初始化迭代器。你可以定义一个tf.placeholder
,并在每次调用初始化操作时动态地传递数据给它。
# define two placeholders to accept min and max value
min_val = tf.placeholder(tf.int32, shape=[])
max_val = tf.placeholder(tf.int32, shape=[])data = tf.range(min_val, max_val)dataset = tf.data.Dataset.from_tensor_slices(data)iterator = dataset.make_initializable_iterator()
next_ele = iterator.get_next()
with tf.Session() as sess:
**# initialize an iterator with range of values from 10 to 15**
sess.run(iterator.initializer, feed_dict={min_val:10, max_val:15})
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
**# initialize an iterator with range of values from 1 to 10**
sess.run(iterator.initializer, feed_dict={min_val:1, max_val:10})
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
- **可重新初始化的迭代器:**这个迭代器可以从结构相同的不同 Dataset 对象初始化。每个数据集都可以通过自己的转换管道。
def map_fnc(ele):
return ele*2min_val = tf.placeholder(tf.int32, shape=[])
max_val = tf.placeholder(tf.int32, shape=[])
data = tf.range(min_val, max_val)#Define separate datasets for training and validation
train_dataset = tf.data.Dataset.from_tensor_slices(data)
val_dataset = tf.data.Dataset.from_tensor_slices(data).map(map_fnc)#create an iterator
iterator=tf.data.Iterator.from_structure(train_dataset.output_types ,train_dataset.output_shapes)train_initializer = iterator.make_initializer(train_dataset)
val_initializer = iterator.make_initializer(val_dataset)next_ele = iterator.get_next()
with tf.Session() as sess:
# initialize an iterator with range of values from 10 to 15
sess.run(train_initializer, feed_dict={min_val:10, max_val:15})
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
# initialize an iterator with range of values from 1 to 10
sess.run(val_initializer, feed_dict={min_val:1, max_val:10})
try:
while True:
val = sess.run(next_ele)
print(val)
except tf.errors.OutOfRangeError:
pass
- **Feedable 迭代器:**可以用来在不同数据集的迭代器之间切换。当您有不同的数据集,并且您想要对数据集使用哪个迭代器有更多的控制时,这很有用。
def map_fnc(ele):
return ele*2min_val = tf.placeholder(tf.int32, shape=[])
max_val = tf.placeholder(tf.int32, shape=[])data = tf.range(min_val, max_val)
train_dataset = tf.data.Dataset.from_tensor_slices(data)
val_dataset = tf.data.Dataset.from_tensor_slices(data).map(map_fnc)train_val_iterator = tf.data.Iterator.from_structure(train_dataset.output_types , train_dataset.output_shapes)
train_initializer = train_val_iterator.make_initializer(train_dataset)
val_initializer = train_val_iterator.make_initializer(val_dataset)test_dataset = tf.data.Dataset.from_tensor_slices(tf.range(10,15))
test_iterator = test_dataset.make_one_shot_iterator()handle = tf.placeholder(tf.string, shape=[])
iterator = tf.data.Iterator.from_string_handle(handle, train_dataset.output_types, train_dataset.output_shapes)
next_ele = iterator.get_next()with tf.Session() as sess:
train_val_handle = sess.run(train_val_iterator.string_handle())
test_handle = sess.run(test_iterator.string_handle())
# training
sess.run(train_initializer, feed_dict={min_val:10, max_val:15})
try:
while True:
val = sess.run(next_ele, feed_dict={handle:train_val_handle})
print(val)
except tf.errors.OutOfRangeError:
pass
#validation
sess.run(val_initializer, feed_dict={min_val:1, max_val:10})
try:
while True:
val = sess.run(next_ele, feed_dict={handle:train_val_handle})
print(val)
except tf.errors.OutOfRangeError:
pass
#testing
try:
while True:
val = sess.run(next_ele, feed_dict={handle:test_handle})
print(val)
except tf.errors.OutOfRangeError:
pass
我们已经学习了各种迭代器。让我们将这些知识应用于一个实际的数据集。我们将使用 LeNet-5 模型训练著名的 MNIST 数据集。本教程不会深入讨论实现 LeNet-5 模型的细节,因为这超出了本文的范围。
LeNet-5 模型
让我们从 tensorflow 库中导入 MNIST 数据。MNIST 数据库包含 60,000 幅训练图像和 10,000 幅测试图像。每个图像的大小为 28281。对于 LeNet-5 型号,我们需要将其大小调整为 32321。
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", reshape=False, one_hot = True)X_train, y_train = mnist.train.images, mnist.train.labels
X_val, y_val = mnist.validation.images, mnist.validation.labels
X_test, y_test = mnist.test.images, mnist.test.labelsX_train = np.pad(X_train, ((0,0), (2,2), (2,2), (0,0)), 'constant')
X_val = np.pad(X_val, ((0,0), (2,2), (2,2), (0,0)), 'constant')
X_test = np.pad(X_test, ((0,0), (2,2), (2,2), (0,0)), 'constant')
Ref: https://www.researchgate.net/figure/Structure-of-LeNet-5_fig1_312170477
让我们定义模型的正向传播。
def forward_pass(X):
W1 = tf.get_variable("W1", [5,5,1,6], initializer = tf.contrib.layers.xavier_initializer(seed=0))
# for conv layer2 W2 = tf.get_variable("W2", [5,5,6,16], initializer = tf.contrib.layers.xavier_initializer(seed=0)) Z1 = tf.nn.conv2d(X, W1, strides = [1,1,1,1], padding='VALID')
A1 = tf.nn.relu(Z1)
P1 = tf.nn.max_pool(A1, ksize = [1,2,2,1], strides = [1,2,2,1], padding='VALID') Z2 = tf.nn.conv2d(P1, W2, strides = [1,1,1,1], padding='VALID')
A2= tf.nn.relu(Z2)
P2= tf.nn.max_pool(A2, ksize = [1,2,2,1], strides=[1,2,2,1], padding='VALID') P2 = tf.contrib.layers.flatten(P2)
Z3 = tf.contrib.layers.fully_connected(P2, 120)
Z4 = tf.contrib.layers.fully_connected(Z3, 84)
Z5 = tf.contrib.layers.fully_connected(Z4,10, activation_fn= None) return Z5
让我们定义模型操作
def model(X,Y):
logits = forward_pass(X)
cost = tf.reduce_mean( tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=Y)) optimizer = tf.train.AdamOptimizer(learning_rate=0.0009)
learner = optimizer.minimize(cost) correct_predictions = tf.equal(tf.argmax(logits,1), tf.argmax(Y,1)) accuracy = tf.reduce_mean(tf.cast(correct_predictions, tf.float32))
return (learner, accuracy)
我们现在已经创建了模型。在决定为我们的模型使用迭代器之前,让我们看看机器学习模型的典型要求是什么。
- **批量训练数据:**数据集可能非常庞大。为了防止出现内存不足的错误,我们需要小批量地训练数据集。
- **在数据集的 n 次传递中训练模型:**通常,您希望在数据集的多次传递中运行训练模型。
- **在每个时期验证模型:**您需要在每个时期验证您的模型,以检查您的模型的性能。
- **最后,在看不见的数据上测试你的模型:**在模型被训练之后,你想要在看不见的数据上测试你的模型。
让我们看看每个迭代器的优缺点。
- **一次性迭代器:**数据集一旦用完就无法重新初始化。为了训练更多的历元,您需要在输入迭代器之前重复数据集。如果数据量很大,这将需要巨大的内存。它也没有提供任何验证模型的选项。
epochs = 10
batch_size = 64
iterations = len(y_train) * epochstf.reset_default_graph()
dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train))# need to repeat the dataset for epoch number of times, as all the data needs
# to be fed to the dataset at once
dataset = dataset.repeat(epochs).batch(batch_size)
iterator = dataset.make_one_shot_iterator()X_batch , Y_batch = iterator.get_next()(learner, accuracy) = model(X_batch, Y_batch)with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
total_accuracy = 0
try:
while True:
temp_accuracy, _ = sess.run([accuracy, learner])
total_accuracy += temp_accuracy
except tf.errors.OutOfRangeError:
pass
print('Avg training accuracy is {}'.format((total_accuracy * batch_size) / iterations ))
- **可初始化的迭代器:**可以在训练数据集和验证数据集之间动态改变数据集。然而,在这种情况下,两个数据集需要通过相同的转换管道。
epochs = 10
batch_size = 64tf.reset_default_graph()X_data = tf.placeholder(tf.float32, [None, 32,32,1])
Y_data = tf.placeholder(tf.float32, [None, 10])dataset = tf.data.Dataset.from_tensor_slices((X_data, Y_data))
dataset = dataset.batch(batch_size)
iterator = dataset.make_initializable_iterator()X_batch , Y_batch = iterator.get_next()(learner, accuracy) = model(X_batch, Y_batch)with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for epoch in range(epochs):
# train the model
sess.run(iterator.initializer, feed_dict={X_data:X_train, Y_data:y_train})
total_train_accuracy = 0
no_train_examples = len(y_train)
try:
while True:
temp_train_accuracy, _ = sess.run([accuracy, learner])
total_train_accuracy += temp_train_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
# validate the model
sess.run(iterator.initializer, feed_dict={X_data:X_val, Y_data:y_val})
total_val_accuracy = 0
no_val_examples = len(y_val)
try:
while True:
temp_val_accuracy = sess.run(accuracy)
total_val_accuracy += temp_val_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
print('Epoch {}'.format(str(epoch+1)))
print("---------------------------")
print('Training accuracy is {}'.format(total_train_accuracy/no_train_examples))
print('Validation accuracy is {}'.format(total_val_accuracy/no_val_examples))
- **可重新初始化的迭代器:**这个迭代器通过使用两个独立的数据集克服了可初始化迭代器的问题。每个数据集都可以通过自己的预处理管道。迭代器可以使用
tf.Iterator.from_structure
方法创建。
def map_fnc(X, Y):
return X, Yepochs = 10
batch_size = 64tf.reset_default_graph()X_data = tf.placeholder(tf.float32, [None, 32,32,1])
Y_data = tf.placeholder(tf.float32, [None, 10])train_dataset = tf.data.Dataset.from_tensor_slices((X_data, Y_data)).batch(batch_size).map(map_fnc)val_dataset = tf.data.Dataset.from_tensor_slices((X_data, Y_data)).batch(batch_size)iterator = tf.data.Iterator.from_structure(train_dataset.output_types, train_dataset.output_shapes)X_batch , Y_batch = iterator.get_next()
(learner, accuracy) = model(X_batch, Y_batch)train_initializer = iterator.make_initializer(train_dataset)
val_initializer = iterator.make_initializer(val_dataset)with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
for epoch in range(epochs):
# train the model
sess.run(train_initializer, feed_dict={X_data:X_train, Y_data:y_train})
total_train_accuracy = 0
no_train_examples = len(y_train)
try:
while True:
temp_train_accuracy, _ = sess.run([accuracy, learner])
total_train_accuracy += temp_train_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
# validate the model
sess.run(val_initializer, feed_dict={X_data:X_val, Y_data:y_val})
total_val_accuracy = 0
no_val_examples = len(y_val)
try:
while True:
temp_val_accuracy = sess.run(accuracy)
total_val_accuracy += temp_val_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
print('Epoch {}'.format(str(epoch+1)))
print("---------------------------")
print('Training accuracy is {}'.format(total_train_accuracy/no_train_examples))
print('Validation accuracy is {}'.format(total_val_accuracy/no_val_examples))
- **Feedable 迭代器:**这个迭代器提供了在各种迭代器之间切换的选项。您可以创建一个可重新初始化的迭代器,用于训练和验证。对于需要一遍数据集的推断/测试,可以使用一次性迭代器。
epochs = 10
batch_size = 64tf.reset_default_graph()X_data = tf.placeholder(tf.float32, [None, 32,32,1])
Y_data = tf.placeholder(tf.float32, [None, 10])train_dataset = tf.data.Dataset.from_tensor_slices((X_data, Y_data)).batch(batch_size)
val_dataset = tf.data.Dataset.from_tensor_slices((X_data, Y_data)).batch(batch_size)test_dataset = tf.data.Dataset.from_tensor_slices((X_test, y_test.astype(np.float32)).batch(batch_size)handle = tf.placeholder(tf.string, shape=[])
iterator = tf.data.Iterator.from_string_handle(handle, train_dataset.output_types, train_dataset.output_shapes)
X_batch , Y_batch = iterator.get_next()
(learner, accuracy) = model(X_batch, Y_batch)train_val_iterator = tf.data.Iterator.from_structure(train_dataset.output_types, train_dataset.output_shapes)
train_iterator = train_val_iterator.make_initializer(train_dataset)
val_iterator = train_val_iterator.make_initializer(val_dataset)
test_iterator = test_dataset.make_one_shot_iterator()with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
train_val_string_handle = sess.run(train_val_iterator.string_handle())
test_string_handle = sess.run(test_iterator.string_handle())
for epoch in range(epochs):
# train the model
sess.run(train_iterator, feed_dict={X_data:X_train, Y_data:y_train})
total_train_accuracy = 0
no_train_examples = len(y_train)
try:
while True:
temp_train_accuracy, _ = sess.run([accuracy, learner], feed_dict={handle:train_val_string_handle})
total_train_accuracy += temp_train_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
# validate the model
sess.run(val_iterator, feed_dict={X_data:X_val, Y_data:y_val})
total_val_accuracy = 0
no_val_examples = len(y_val)
try:
while True:
temp_val_accuracy, _ = sess.run([accuracy, learner], feed_dict={handle:train_val_string_handle})
total_val_accuracy += temp_val_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
print('Epoch {}'.format(str(epoch+1)))
print("---------------------------")
print('Training accuracy is {}'.format(total_train_accuracy/no_train_examples))
print('Validation accuracy is {}'.format(total_val_accuracy/no_val_examples))
print("Testing the model --------")
total_test_accuracy = 0
no_test_examples = len(y_test)
try:
while True:
temp_test_accuracy, _ = sess.run([accuracy, learner], feed_dict={handle:test_string_handle})
total_test_accuracy += temp_test_accuracy*batch_size
except tf.errors.OutOfRangeError:
pass
print('Testing accuracy is {}'.format(total_test_accuracy/no_test_examples))
感谢阅读博客。本博客中使用的代码示例可以在这个 jupyter 笔记本中找到。
如果你有任何问题或者你有任何改进这个博客的建议,请在下面留下你的评论。
参考
- https://www . tensor flow . org/API _ docs/python/TF/data/Iterator # from _ string _ handle
- https://www.tensorflow.org/guide/datasets
- https://docs . Google . com/presentation/d/16 khntqslt-yuj 3 w8 gix-eEH6t _ avfeqochqgrfpad 7 u/edit # slide = id . g 254d 08 e 080 _ 0 _ 141
- https://github.com/tensorflow/tensorflow/issues/2919
为视频和图像构建快速面部识别
noobs 😄 面部识别入门
在我开始这个故事之前,我要对 PyImageSearch 的 Adrian 大声喊一声。如果您是深度学习的初学者,这是市场上最好的课程。(我会把他的页面链接在底部。)
如果你和我一样是科幻迷,你应该看过《感兴趣的人》。你已经看到人工智能在视频中寻找人脸。它从图像库中找到人脸。这是电视版。如果我告诉你你也可以在现实生活中建造。我暂时忘记了那件事。
关于这个项目,我从几天前认识的室友那里得到了灵感。他一直向朋友们问我角色名字。不如我们在视频中建立面部识别系统。艾利安的代码让这一切成为可能。
想法很简单,CNN 提供足够多的图片。我从朋友那里为 5 个角色输入了 15 张图片。可以从面部识别中获取面部识别编码,存储在 pickle 文件中。您可以再次读取此编码,并与图像和视频文件中的人脸进行匹配。如果匹配,它将抛出我们存储在程序中的 known_name。如果没有找到,它将抛出未知。
我会为谷歌图片添加 github 回购和 scrapper 代码。Recog.py 文件包含 pickle 文件中的编码示例。将使用这个 pickle 文件。Recog_faces 将使用 pickle 文件,该文件在下面提到的截图中使用。如果文件不在数据集中,它将抛出未知的矩形。对每个字符使用最大数量的样本,这样我们可以有大量的编码。这将有助于面部识别。我们同样可以在视频中进行人脸识别。
提到的例子的小代码指南。
https://gist.github.com/saurabh896/b705536ba4354c8aa382978d5ddfc8e4
这个代码的主要部分是面部识别。我们使用相同部分的代码来转储 pickle 文件中的编码。就像我说的,更多的样本会提供更多的准确性。我用每个字符 15 个样本来运行这个代码,所以我达到了大约 83%的准确率。你仍然可以看到模型不能识别瑞秋的脸。上面的代码可以识别图像中的人脸,并将编码保存在数组中。如果你想查看编码,只需打印编码和已知人脸。你会得到看起来像坐标的人脸数据点。如果你想试试,你可以做到。记住数据科学领域是由好奇心推动的。
https://gist.github.com/saurabh896/ffdc37836684420491cab60a01982658
上面的代码改变你的阅读你的图像。打开你的 pickle 文件,循环你的编码。使用图像中提供的面孔查找匹配的编码。如果匹配,它将从编码数组中抛出名字。如果找不到脸,那么它将给出未知。
https://gist.github.com/saurabh896/4ee6e4bfe24467df535f7762b8b112e2
视频的最后代码。视频只不过是根据时间排列的图像。因此,我们一帧一帧地分割图像,并像处理图像一样对它们进行匹配。我们使用两个部分首先是输入作为输入视频,其中包含这个字符和输出文件夹,以获得矩形有界输出的模型。
如果你想了解更多关于 imutils .使用以下链接
本页提供 imutils.video 的 Python 代码示例。
www.programcreek.com](https://www.programcreek.com/python/example/93641/imutils.video)
这段代码需要时间,因为它正在写入新文件。我更喜欢使用笔记本电脑,因为它有很好的配置。如果你没有好的系统,使用自动气象站或 GCP 进行实验。
Facial recognition on characters
我在视频中实现了相同的 pickle 文件。这给了我以下结果。我不得不调整一些代码来处理慢动作,但最终想法奏效了!!瑞秋出了点小问题。希望詹妮弗·安妮斯顿不会介意!!😛
Video Example
你可以在 github repo 中找到工作代码。我提到了向下看,如果你有兴趣的话。很高兴从你这里得到更多的想法。
给我打 saurabhkdm721@gmail.com 的电话
此时您不能执行该操作。您已使用另一个标签页或窗口登录。您已在另一个选项卡中注销,或者…
github.com](https://github.com/saurabh896/VideoRecognition.git) [## PyImageSearch——擅长 OpenCV、Python、深度学习和计算机视觉
这篇 OpenCV、深度学习和 Python 博客是由 Adrian Rosebrock 撰写的。精通 OpenCV,深度学习,Python,还有…
www.pyimagesearch.com](https://www.pyimagesearch.com/)
使用 Amazon Rekognition 构建韩国流行偶像标识符
人脸识别简明指南
Photo by Bach Tran on Unsplash
从头开始构建数据科学模型是一项相当大的工作。有许多元素组成一个单一的模型,涉及许多步骤,并且需要许多迭代来创建一个像样的模型。尽管完成这些步骤肯定会帮助您更深入地理解模型中使用的算法,但有时您没有足够的时间来完成所有的试验和错误,特别是当您有一个紧迫的截止日期要满足时。
图像识别是机器学习中的一个领域,已经被许多科技巨头如谷歌、亚马逊、微软深入探索。在图像处理的所有功能中,可能讨论最多的是面部识别。关于这项技术的伦理方面有很多争论,但这超出了本文的范围。我将简单地分享我在 Amazon Rekognition 上的尝试,希望你能从这篇文章中有所收获。
写这篇文章的冲动始于我在他们的网络界面上玩了一下亚马逊的 Rekognition 演示。它提供许多有用的服务,如“对象和场景检测”、“面部识别”、“面部分析”和“名人识别”。我试了几张图,一切都很顺利,直到我到了“名人识别”。在我尝试使用韩国明星的照片之前,名人识别起初似乎还不错。韩国明星的认知度表现明显下降。有时它给我正确的答案,有时它不能识别,有时它给我错误的名字。
顺便说一下,上面的照片是周子瑜来自一个名为 Twice 的组合,这是我最喜欢的 K-pop 女子组合,我不能接受亚马逊承认这张照片是 Seolhyun(她是另一个名为 AOA 的组合的成员)。
所以我决定使用 Amazon Rekognition 编写一个简单的 Python 脚本,它可以准确地检测两次。
- 除了你可以在文章中找到的简短代码块,我会在文章末尾附上整个 Jupyter 笔记本的链接。
- 这篇文章基于教程“使用 Amazon Rekognition 构建自己的人脸识别服务”,但对原始代码进行了修改,以适应该项目的特定目的。
使用 Amazon Rekognition 进行人脸检测
要运行 Jupyter 笔记本中的以下步骤,有几个先决条件。
- 亚马逊 AWS 帐户
- 使用 AWS CLI 配置的 AWS 凭据
- Boto3 的最新版本
让我们首先从导入一些将直接用于下一步的包开始。
import boto3
from PIL import Image%matplotlib inline
现在我们需要一个我们想要处理的图像。我选择了与上面的 web 界面演示相同的图像,我们将把这个图像发送给 Rekognition API 以获得其图像识别的结果。(图片也可以在我将在本文末尾分享的 Github 链接中找到。)我们来快速看一下图像。
display(Image.open('Tzuyu.jpeg'))
我们可以要求 Rekognition 执行的最基本的任务是用给定的图像进行面部识别,这只需要几行代码就可以完成。
import iorekognition = boto3.client('rekognition')image = Image.open("Tzuyu.jpeg")
stream = io.BytesIO()
image.save(stream,format="JPEG")
image_binary = stream.getvalue()rekognition.detect_faces(
Image={'Bytes':image_binary},
Attributes=['ALL']
)
您可以将图像作为内存中的二进制文件对象直接从您的本地机器发送到 rekognition,或者将您的图像上传到 S3,并在调用 rekognition.detect_faces()时将您的桶和密钥详细信息作为参数给出。在上面的例子中,我直接从本地机器发送二进制对象。您将从上面的调用中得到的响应将会很长,包含您可以从 Rekognition 的 detect_faces 函数中得到的所有信息。
{'FaceDetails': [{'AgeRange': {'High': 38, 'Low': 20},
'Beard': {'Confidence': 99.98848724365234, 'Value': False},
'BoundingBox': {'Height': 0.1584049016237259,
'Left': 0.4546355605125427,
'Top': 0.0878104418516159,
'Width': 0.09999311715364456},
'Confidence': 100.0,
'Emotions': [{'Confidence': 37.66959762573242, 'Type': 'SURPRISED'},
{'Confidence': 29.646778106689453, 'Type': 'CALM'},
{'Confidence': 3.8459930419921875, 'Type': 'SAD'},
{'Confidence': 3.134934186935425, 'Type': 'DISGUSTED'},
{'Confidence': 2.061260938644409, 'Type': 'HAPPY'},
{'Confidence': 18.516468048095703, 'Type': 'CONFUSED'},
{'Confidence': 5.1249613761901855, 'Type': 'ANGRY'}],
'Eyeglasses': {'Confidence': 99.98339080810547, 'Value': False},
'EyesOpen': {'Confidence': 99.9864730834961, 'Value': True},
'Gender': {'Confidence': 99.84709167480469, 'Value': 'Female'},
'Landmarks': [{'Type': 'eyeLeft',
'X': 0.47338899970054626,
'Y': 0.15436244010925293},
{'Type': 'eyeRight', 'X': 0.5152773261070251, 'Y': 0.1474122554063797},
{'Type': 'mouthLeft', 'X': 0.48312342166900635, 'Y': 0.211111381649971},
{'Type': 'mouthRight', 'X': 0.5174261927604675, 'Y': 0.20560002326965332},
{'Type': 'nose', 'X': 0.4872787892818451, 'Y': 0.1808750480413437},
{'Type': 'leftEyeBrowLeft',
'X': 0.45876359939575195,
'Y': 0.14424000680446625},
{'Type': 'leftEyeBrowRight',
'X': 0.4760720133781433,
'Y': 0.13612663745880127},
{'Type': 'leftEyeBrowUp',
'X': 0.4654795229434967,
'Y': 0.13559915125370026},
{'Type': 'rightEyeBrowLeft',
'X': 0.5008187890052795,
'Y': 0.1317606270313263},
{'Type': 'rightEyeBrowRight',
'X': 0.5342025756835938,
'Y': 0.1317359358072281},
{'Type': 'rightEyeBrowUp',
'X': 0.5151524543762207,
'Y': 0.12679456174373627},
{'Type': 'leftEyeLeft', 'X': 0.4674917757511139, 'Y': 0.15510375797748566},
{'Type': 'leftEyeRight',
'X': 0.4817998707294464,
'Y': 0.15343616902828217},
{'Type': 'leftEyeUp', 'X': 0.47253310680389404, 'Y': 0.1514900177717209},
{'Type': 'leftEyeDown',
'X': 0.47370508313179016,
'Y': 0.15651680529117584},
{'Type': 'rightEyeLeft',
'X': 0.5069678425788879,
'Y': 0.14930757880210876},
{'Type': 'rightEyeRight',
'X': 0.5239912867546082,
'Y': 0.1460886150598526},
{'Type': 'rightEyeUp', 'X': 0.5144344568252563, 'Y': 0.1447771191596985},
{'Type': 'rightEyeDown',
'X': 0.5150220394134521,
'Y': 0.14997448027133942},
{'Type': 'noseLeft', 'X': 0.4858757555484772, 'Y': 0.18927086889743805},
{'Type': 'noseRight', 'X': 0.5023624897003174, 'Y': 0.1855706423521042},
{'Type': 'mouthUp', 'X': 0.4945952594280243, 'Y': 0.2002507448196411},
{'Type': 'mouthDown', 'X': 0.4980264902114868, 'Y': 0.21687346696853638},
{'Type': 'leftPupil', 'X': 0.47338899970054626, 'Y': 0.15436244010925293},
{'Type': 'rightPupil', 'X': 0.5152773261070251, 'Y': 0.1474122554063797},
{'Type': 'upperJawlineLeft',
'X': 0.46607205271720886,
'Y': 0.15965013206005096},
{'Type': 'midJawlineLeft',
'X': 0.47901660203933716,
'Y': 0.21797965466976166},
{'Type': 'chinBottom', 'X': 0.5062429904937744, 'Y': 0.24532964825630188},
{'Type': 'midJawlineRight',
'X': 0.5554487109184265,
'Y': 0.20579127967357635},
{'Type': 'upperJawlineRight',
'X': 0.561174750328064,
'Y': 0.14439250528812408}],
'MouthOpen': {'Confidence': 99.0997543334961, 'Value': True},
'Mustache': {'Confidence': 99.99714660644531, 'Value': False},
'Pose': {'Pitch': 1.8594770431518555,
'Roll': -11.335309982299805,
'Yaw': -33.68760681152344},
'Quality': {'Brightness': 89.57070922851562,
'Sharpness': 86.86019134521484},
'Smile': {'Confidence': 99.23001861572266, 'Value': False},
'Sunglasses': {'Confidence': 99.99723815917969, 'Value': False}}],
'ResponseMetadata': {'HTTPHeaders': {'connection': 'keep-alive',
'content-length': '3297',
'content-type': 'application/x-amz-json-1.1',
'date': 'Sun, 19 May 2019 08:45:56 GMT',
'x-amzn-requestid': '824f5dc3-7a12-11e9-a384-dfb84e388b7e'},
'HTTPStatusCode': 200,
'RequestId': '824f5dc3-7a12-11e9-a384-dfb84e388b7e',
'RetryAttempts': 0}}
从上面 detect_faces 调用的示例响应中可以看出,它不仅具有图片中人脸位置的包围盒信息,还具有更高级的特征,如情绪、性别、年龄范围等。
比较面孔
有了 Amazon Rekognition,你可以对比两张图片中的人脸。例如,如果我将一张周子瑜的图片设置为我的源图片,然后发送一张两倍于我的目标图片的集体照,Rekognition 会在目标图片中找到与源图片最相似的人脸。下面是我将使用的两次集体照。
即使对人类来说也可能很难,尤其是如果你不是亚洲人(或者不是 Twice 粉丝)。你可以猜猜照片中的周子瑜是谁。作为一个韩国人,同时也是两次粉丝,我知道答案,但让我们看看 Rekognition 能从这张照片中找到周子瑜有多好。
sourceFile='Tzuyu.jpeg'
targetFile='twice_group.jpg'
imageSource=open(sourceFile,'rb')
imageTarget=open(targetFile,'rb')response = rekognition.compare_faces(SimilarityThreshold=80,
SourceImage={'Bytes': imageSource.read()},
TargetImage={'Bytes': imageTarget.read()})
response['FaceMatches']
上面 compare_faces 的响应还会输出组图中所有不匹配人脸的信息,这可能会变得相当长,所以我只是输出 Rekognition 通过指定 response[‘FaceMatches’]找到的匹配。看起来像是从一组照片中找到了一张相似度在 97%左右的匹配脸。有了边界框信息,让我们检查 Rekognition 指的是周子瑜的哪张脸。
顺便说一下,BoundingBox 部分中的值是整个图像大小的比率。因此,为了用 BoundingBox 中的值绘制方框,需要通过乘以实际图像高度或宽度的比率来计算方框中每个点的位置。您可以在下面的代码片段中找到如何做到这一点。
from PIL import ImageDrawimage = Image.open("twice_group.jpg")
imgWidth,imgHeight = image.size
draw = ImageDraw.Draw(image)
box = response['FaceMatches'][0]['Face']['BoundingBox']
left = imgWidth * box['Left']
top = imgHeight * box['Top']
width = imgWidth * box['Width']
height = imgHeight * box['Height']
points = (
(left,top),
(left + width, top),
(left + width, top + height),
(left , top + height),
(left, top))
draw.line(points, fill='#00d400', width=2)display(image)
是啊!干得好,雷科尼提翁!那确实是周子瑜!
正在创建收藏
现在我们可以从一张图片中检测人脸,从目标图片中找到与源图片最相似的人脸。但是,这些都是一次性调用,我们需要更多的东西来存储每个成员的脸和他们的名字的信息,以便当我们发送两次新的图片时,它可以检索数据并检测每个成员的脸并显示他们的名字。为了实现这一点,我们需要使用亚马逊所谓的“基于存储的 API 操作”。这种类型的操作有两个亚马逊特有的术语。“集合”是一个虚拟空间,Rekognition 在其中存储有关检测到的人脸的信息。使用集合,我们可以“索引”人脸,这意味着检测图像中的人脸,然后将信息存储在指定的集合中。重要的是,Rekognition 存储在集合中的信息不是实际的图像,而是由 Rekognition 的算法提取的特征向量。让我们看看如何创建一个集合并添加索引。
collectionId='test-collection'
rekognition.create_collection(CollectionId=collectionId)
是的。就这么简单。由于这是我们刚刚创建的新集合,因此集合中没有存储任何信息。但是,让我们仔细检查一下。
rekognition.describe_collection(CollectionId=collectionId)
在上面的响应中,您可以看到“FaceCount”为 0。如果我们索引任何人脸并将该信息存储在集合中,这种情况将会改变。
索引面
使用 Rekognition 为人脸建立索引同样简单,只需一行代码。
sourceFile='Tzuyu.jpeg'
imageSource=open(sourceFile,'rb')rekognition.index_faces(Image={'Bytes':imageSource.read()},ExternalImageId='Tzuyu',CollectionId=collectionId)
从上面的代码,你可以看到我正在传递 ExternalImageId 参数,并给它的值字符串“周子瑜”。稍后,当我们试图从一张新照片中识别周子瑜时,Rekognition 将搜索与任何索引人脸相匹配的人脸。正如您将在后面看到的,当索引一个面时,Rekognition 会给它一个唯一的面 ID。但是,当在新图片中找到匹配的人脸时,我想显示“周子瑜”这个名字。为此,我使用 ExternalImageId。现在,如果我们检查我们的集合,我们可以看到 1 张脸被添加到集合中。
rekognition.describe_collection(CollectionId=collectionId)
通过图像搜索面孔
现在,随着周子瑜的脸在我们的集合中被编入索引,我们可以向 Rekognition 发送一张新的看不见的照片,并找到匹配的脸。但是 search_faces_by_image 函数的一个问题是只能检测一张脸(图像中最大的)。因此,如果我们想发送两次的团体照片,并从那里找到周子瑜,我们将需要做一个额外的步骤。下面我们先用 detect_faces 检测图片中的所有人脸,然后有了每个人脸的包围盒信息,我们再逐个调用 search_faces_by_image。首先让我们检测每张脸。
imageSource=open('twice_group.jpg','rb')
resp = rekognition.detect_faces(Image={'Bytes':imageSource.read()})
all_faces = resp['FaceDetails']
len(all_faces)
Rekognition 从群组图片中检测到 9 张人脸。很好。现在让我们裁剪每个面,并逐个调用 serach_faces_by_image。
image = Image.open("twice_group.jpg")
image_width,image_height = image.sizefor face in all_faces:
box=face['BoundingBox']
x1 = box['Left'] * image_width
y1 = box['Top'] * image_height
x2 = x1 + box['Width'] * image_width
y2 = y1 + box['Height'] * image_height
image_crop = image.crop((x1,y1,x2,y2))
stream = io.BytesIO()
image_crop.save(stream,format="JPEG")
image_crop_binary = stream.getvalue()response = rekognition.search_faces_by_image(
CollectionId=collectionId,
Image={'Bytes':image_crop_binary}
)
print(response)
print('-'*100)
在我们进行的 9 次 search_faces_by_image 调用中,Rekognition 找到了一个与我们的集合中的索引人脸相匹配的人脸。我们只索引了周子瑜的一张脸,所以它从组图中找到的是周子瑜的脸。让我们用边界框和名称在图像上显示它。对于名称部分,我们将使用我们在索引面部时设置的 ExternalImageId。顺便说一下,从 search_faces_by_image 响应来看,‘face matches’部分是一个数组,如果从集合中找到多个匹配,那么它会显示所有匹配。根据 Amazon 的说法,这个数组是按照相似性得分排序的,相似性最高的排在最前面。我们将通过指定数组的第一项来获得最高分的匹配。
from PIL import ImageFont
import ioimage = Image.open("twice_group.jpg")
image_width,image_height = image.size
for face in all_faces:
box=face['BoundingBox']
x1 = box['Left'] * image_width
y1 = box['Top'] * image_height
x2 = x1 + box['Width'] * image_width
y2 = y1 + box['Height'] * image_height
image_crop = image.crop((x1,y1,x2,y2))
stream = io.BytesIO()
image_crop.save(stream,format="JPEG")
image_crop_binary = stream.getvalue()response = rekognition.search_faces_by_image(
CollectionId=collectionId,
Image={'Bytes':image_crop_binary}
)
if len(response['FaceMatches']) > 0:
draw = ImageDraw.Draw(image)
points = (
(x1,y1),
(x2, y1),
(x2, y2),
(x1 , y2),
(x1, y1))
draw.line(points, fill='#00d400', width=2)
fnt = ImageFont.truetype('/Library/Fonts/Arial.ttf', 15)
draw.text((x1,y2),response['FaceMatches'][0]['Face']['ExternalImageId'], font=fnt, fill=(255, 255, 0))
display(image)
万岁!又是正确答案!
两次识别所有组成员
现在,让我们扩展项目,从组图片中识别所有成员。为了做到这一点,我们首先需要索引所有成员的脸(有 9 个成员)。我为每个成员准备了 4 张照片。我按照 Christian Petters 写的亚马逊教程的逻辑添加了同一个人的多张图片。根据 Petters 的说法,“为每个人添加多个参考图像大大提高了一个人的潜在匹配率”,这具有直观的意义。从我将在最后分享的 Github 链接中,你会找到这个项目中使用的所有图片。
collectionId='twice'
rekognition.create_collection(CollectionId=collectionId)
import ospath = 'Twice'for r, d, f in os.walk(path):
for file in f:
if file != '.DS_Store':
sourceFile = os.path.join(r,file)
imageSource=open(sourceFile,'rb')
rekognition.index_faces(Image={'Bytes':imageSource.read()},ExternalImageId=file.split('_')[0],CollectionId=collectionId)
rekognition.describe_collection(CollectionId=collectionId)
好的。似乎所有 36 张照片都被编入了我们的“两次”收藏。现在是检查最终结果的时候了。可以增强 Rekognition 来识别两次的每个成员吗?
from PIL import ImageFontimage = Image.open("twice_group.jpg")
image_width,image_height = image.size
for face in all_faces:
box=face['BoundingBox']
x1 = box['Left'] * image_width
y1 = box['Top'] * image_height
x2 = x1 + box['Width'] * image_width
y2 = y1 + box['Height'] * image_height
image_crop = image.crop((x1,y1,x2,y2))
stream = io.BytesIO()
image_crop.save(stream,format="JPEG")
image_crop_binary = stream.getvalue()response = rekognition.search_faces_by_image(
CollectionId=collectionId,
Image={'Bytes':image_crop_binary}
)
if len(response['FaceMatches']) > 0:
draw = ImageDraw.Draw(image)
points = (
(x1,y1),
(x2, y1),
(x2, y2),
(x1 , y2),
(x1, y1))
draw.line(points, fill='#00d400', width=2)
fnt = ImageFont.truetype('/Library/Fonts/Arial.ttf', 15)
draw.text((x1,y2),response['FaceMatches'][0]['Face']['ExternalImageId'], font=fnt, fill=(255, 255, 0))display(image)
是啊!可以的!它正确地识别了所有成员!
感谢您的阅读。你可以从下面的链接中找到 Jupyter 笔记本和用于这个项目的图片。
https://github.com/tthustla/twice_recognition
使用 Streamlit 构建机器学习应用
Streamlit 是一个开源的 Python 库,可以轻松构建漂亮的机器学习应用。你可以通过 pip 在你的终端上轻松安装它,然后开始用 Python 编写你的网络应用。
在这篇文章中,我将展示一些关于 Streamlit 的有趣功能,构建一个以检查数据为目的的应用程序,并在其上构建 ML 模型。为此,我将使用非常基本的 Iris 数据集,并对其进行一些分类。然而,如果你对这个工具更高级的潜力感兴趣,我建议你阅读这个教程。
说到这里,让我们开始构建我们的应用程序。我将把我所有的代码写在一个名为 iris.py 的文件中,这样我就可以通过 streamlit iris.py 从我的终端运行它。
最后,我的应用程序的完整代码如下:
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import seaborn as sns
import matplotlib.pyplot as plt
import plotly.graph_objects as gost.title('Iris')df = pd.read_csv("iris.csv")if st.checkbox('Show dataframe'):
st.write(df)st.subheader('Scatter plot')species = st.multiselect('Show iris per variety?', df['variety'].unique())
col1 = st.selectbox('Which feature on x?', df.columns[0:4])
col2 = st.selectbox('Which feature on y?', df.columns[0:4])new_df = df[(df['variety'].isin(species))]
st.write(new_df)
# create figure using plotly express
fig = px.scatter(new_df, x =col1,y=col2, color='variety')
# Plot!st.plotly_chart(fig)st.subheader('Histogram')feature = st.selectbox('Which feature?', df.columns[0:4])
# Filter dataframe
new_df2 = df[(df['variety'].isin(species))][feature]
fig2 = px.histogram(new_df, x=feature, color="variety", marginal="rug")
st.plotly_chart(fig2)st.subheader('Machine Learning models')from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import confusion_matrix
from sklearn.svm import SVCfeatures= df[['sepal.length', 'sepal.width', 'petal.length', 'petal.width']].values
labels = df['variety'].valuesX_train,X_test, y_train, y_test = train_test_split(features, labels, train_size=0.7, random_state=1)alg = ['Decision Tree', 'Support Vector Machine']
classifier = st.selectbox('Which algorithm?', alg)
if classifier=='Decision Tree':
dtc = DecisionTreeClassifier()
dtc.fit(X_train, y_train)
acc = dtc.score(X_test, y_test)
st.write('Accuracy: ', acc)
pred_dtc = dtc.predict(X_test)
cm_dtc=confusion_matrix(y_test,pred_dtc)
st.write('Confusion matrix: ', cm_dtc)elif classifier == 'Support Vector Machine':
svm=SVC()
svm.fit(X_train, y_train)
acc = svm.score(X_test, y_test)
st.write('Accuracy: ', acc)
pred_svm = svm.predict(X_test)
cm=confusion_matrix(y_test,pred_svm)
st.write('Confusion matrix: ', cm)
现在,让我们检查每段代码。首先,一旦导入了所需的包,我想设置我的应用程序的标题并导入我的数据:
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import seaborn as sns
import matplotlib.pyplot as plt
import plotly.graph_objects as gost.title('Iris')df = pd.read_csv("iris.csv")
现在我想要第一个选项,它允许用户决定是否显示整个数据集。我可以用下面的语法做到这一点(以及许多其他交互小部件):
if st.checkbox('Show dataframe'):
st.write(df)
尽管天真,我们已经可以启动我们的婴儿应用程序,并在 localhost:8501:
如您所见,我决定显示我的数据集,但是,在任何时候,我都可以通过取消选中该框来隐藏它。
现在让我们转向一些可视化工具。假设我想要散点图我的数据,有可能选择那些我感兴趣的要素和标签。
species = st.multiselect('Show iris per variety?', df['variety'].unique())
col1 = st.selectbox('Which feature on x?', df.columns[0:4])
col2 = st.selectbox('Which feature on y?', df.columns[0:4])new_df = df[(df['variety'].isin(species))]
st.write(new_df)
fig = px.scatter(new_df, x =col1,y=col2, color='variety')st.plotly_chart(fig)
正如你所看到的,在我选择的例子中,物种杂色和海滨锦鸡儿的特征是萼片长度和萼片宽度,但我可以随时改变它们,并实时更新我的所有图表。
现在,我想用同样的逻辑添加一个直方图,显示任何特征的分布。此外,我想有可能绘制出每个特征的 3 个条件分布,相对于以前选择的品种。因此:
feature = st.selectbox('Which feature?', df.columns[0:4])
# Filter dataframe
new_df2 = df[(df['variety'].isin(species))][feature]
fig2 = px.histogram(new_df, x=feature, color="variety", marginal="rug")
st.plotly_chart(fig2)
这两个物种和我上面选的是一样的,而且,我可以随时改变它们。
现在让我们进入最后一部分,即训练实时 ML 算法,并让用户决定应用哪一种算法。为此,我将在 S 支持向量机和 D 决策树这两种分类算法之间进行选择。对于每一个问题,我会让我的应用程序打印准确性(正确分类的数量/观察总数)和混淆矩阵:
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import confusion_matrix
from sklearn.svm import SVCfeatures= df[['sepal.length', 'sepal.width', 'petal.length', 'petal.width']].values
labels = df['variety'].valuesX_train,X_test, y_train, y_test = train_test_split(features, labels, train_size=0.7, random_state=1)alg = ['Decision Tree', 'Support Vector Machine']
classifier = st.selectbox('Which algorithm?', alg)
if classifier=='Decision Tree':
dtc = DecisionTreeClassifier()
dtc.fit(X_train, y_train)
acc = dtc.score(X_test, y_test)
st.write('Accuracy: ', acc)
pred_dtc = dtc.predict(X_test)
cm_dtc=confusion_matrix(y_test,pred_dtc)
st.write('Confusion matrix: ', cm_dtc)elif classifier == 'Support Vector Machine':
svm=SVC()
svm.fit(X_train, y_train)
acc = svm.score(X_test, y_test)
st.write('Accuracy: ', acc)
pred_svm = svm.predict(X_test)
cm=confusion_matrix(y_test,pred_svm)
st.write('Confusion matrix: ', cm)
那么,如果我选择 SVM:
因此,我们能够以一种非常用户友好的方式即时比较两个分类器的性能。
Streamlit 是一个非常强大的工具,特别是如果您想要提供一种交互式的方式来理解您的分析结果:它允许实时可视化您的数据,并有可能过滤它们,它允许有意义的表示。
在这里,我向您展示了使用 Streamlit 可以实现的非常基本的实现,因此,如果您想更深入地研究这个工具,我推荐您进一步阅读参考资料。
参考文献:
原载于 2019 年 10 月 22 日http://datasciencechalktalk.com。
构建预测抗生素耐药性的机器学习模型
DPhoto by Markus Spiske on Unsplash
我第一次向计算机科学学生介绍用 ML 预测抗生素耐药性的研讨会的回顾
这个月,我在罗马尼亚的克卢日纳波卡为大约 25 名计算机科学学生举办了一个研讨会。该研讨会是为期两天的生物学中的 ML 研讨会的一部分,由罗马尼亚生物信息学协会组织。
Cluj-Napoca, Romania
根据之前的民意调查,计算机科学专业的理学学士、理学硕士和博士学生对学习 ML 在医学和生物学中的应用非常感兴趣。大多数参加会议的学生在本科学习期间学习了机器学习算法,但对学习更多关于生物信息学/生物学的知识感兴趣。
我在研讨会中的角色是就预测抗生素耐药性的 ML 发表一个小时的演讲,然后为一部分学生举办两个小时的实践研讨会。我的目的是向学生介绍 ML 在鉴定抗生素耐药细菌中的应用,并让他们思考我们用于 ML 的标准方法在应用于细菌基因组数据时的一些当前局限性,以及超越这些局限性的方法。
问题简介
对治疗没有反应的细菌感染越来越令人担忧。目前,抗生素耐药性感染每年导致 70 多万人死亡,到 2050 年,预计将有 1000 万人死于抗生素耐药性感染,除非我们对开发和使用抗生素的方式做出重大改变。
从全基因组序列预测抗生素耐药性的机器学习算法可以提高我们识别和遏制新耐药菌株的能力。在不久的将来,有可能在采集样本后的几个小时内对引起感染的细菌的 DNA 进行测序,然后分析这些 DNA 序列数据,以预测哪些抗生素可以成功用于治疗感染。
耐药性在淋病奈瑟菌,导致淋病的细菌是一个特别大的问题,淋病感染对所有可用的抗生素都具有耐药性,这在世界各地都有出现。淋病是欧洲第二常见的性传播感染(STI),仅次于衣原体。淋病感染率正在上升,英国 2017-2018 年报告的病例增加了 26%。
许多被感染的人没有任何症状,这有助于感染的传播。如果不进行治疗,这种感染会导致男性和女性不孕,或导致危及生命的宫外孕。患有淋病的母亲所生的婴儿可能会发生眼部感染,如果不治疗,会导致永久性失明。
Increasing resistance to the antibiotics we use to treat gonorrhoea. Image from Whittles et al. 2018.
过去,病人用一种叫做环丙沙星的抗生素治疗。医生现在已经停止用这种抗生素治疗感染,因为对这种药物的耐药性变得太普遍,导致治疗失败。直到最近,推荐的治疗是两种药物——头孢曲松和阿奇霉素。但是,由于担心对阿奇霉素耐药性的上升,阿奇霉素已从英国的治疗建议中删除。目前在英国,患者仅接受头孢曲松的治疗。
2018 年 2 月,报告了有史以来第一例对所有常用治疗方法产生耐药性的病例,今年,英国两名女性在没有离开该国的情况下感染了这种疾病,这表明耐药性将持续存在。
[## 男人得了“世界上最严重的”超级淋病
英国一名男子感染了世界上“有史以来最严重”的超级淋病。他在英国有固定的伴侣,但是…
www.bbc.co.uk](https://www.bbc.co.uk/news/health-43571120)
多年来,由于耐药性增加,许多药物已经从治疗建议中删除,但这些抗生素中的每一种仍然可以用于治疗一些感染,这意味着如果我们可以自信地预测抗生素耐药性,我们可以重新使用这些抗生素。
研讨会的形式
参加研讨会的每个人也参加了我的演讲,我的演讲向人们介绍了我们对抗生素耐药性细菌感染日益增长的担忧,以及如何将机器学习算法构建到护理点诊断中,以帮助减少或消除抗生素耐药性的传播。
该研讨会旨在让人们亲自查看数据,并使用它来构建机器学习算法。由于之前没有设计过研讨会,我不确定在 2 个小时的时间内需要准备多少材料,所以我设计了一个简单的活动,后续工作有多个选项,让参与者做他们感兴趣的事情。
我使用 Kaggle 运行研讨会,因为它允许我上传数据,并通过一个简单的界面访问数据,这允许包含额外的信息以给出数据上下文。我创建了一个入门笔记本,带领参与者阅读和查看数据,然后构建一些简单的模型并评估他们的表现。
培训用数据
在这个实验中,我们使用了 unitigs,我们收集的菌株共有的 DNA 片段。这些是通过提取不同细菌的 DNA 产生的,我们在对它们的基因组测序后将这些 DNA 拼凑在一起,然后将其分解为不同的 31 个字符的单词。这些词然后被组合成一个德布鲁因图(DBG)。该图是通过识别内部重叠的单词拼凑而成的,并且出现在相同的样本中。
要了解更多关于 unitigs 的信息,请阅读本文:
作者摘要全基因组关联研究(GWAS)有助于探索癌症表型变异的遗传基础
journals.plos.org](https://journals.plos.org/plosgenetics/article?id=10.1371/journal.pgen.1007758)
这个过程让我们能够以一种高效的方式来表现这些不同细菌之间的异同。这种差异可能是个体突变,插入或删除的 DNA 片段,或其他称为质粒的遗传元素,它们可以在细菌之间传递并传播抗生素耐药性。
通常,在处理 unitigs 时,变量的数量可能在 50-500 万之间,但出于练习的目的,我首先进行了一项关联研究,以确定与耐药性显著相关的 unitigs,从而加快分析速度。
对于包含 3,971 个样本的数据集,过滤后得到 8,873 个与环丙沙星耐药性高度相关的单位免疫球蛋白和 515 个与阿奇霉素耐药性显著相关的单位免疫球蛋白。
Strength of association between unitigs and resistance for the full set of 584,362 unitigs.
模型结构
我们研究了四种不同的模型类型:
- 弹性净逻辑回归
- 支持向量机
- XGBoost
- 随机森林
我们还观察了两种阻力曲线:
- 阿奇霉素耐药性
- 环丙沙星耐药性
我整理了一个简单的模型训练笔记本,将数据分成五份进行交叉验证,并在每一份内对可能的超参数进行网格搜索,再次使用五份交叉验证。
使用这个函数,我们可以跨数据集比较不同模型类型的性能。
参与者对下一步有几个选择。在后续部分中,他们可以做的其他事情有:
- 看看你的模型学到了什么,这是否符合我们现有的抗生素耐药性知识
- 检查收集更多样本是否有可能提高算法的性能
- 探索更多的超参数,并尝试建立更准确的模型
- 尝试一些其他模型类型
- 预测对另一种抗生素的耐药性
- 尝试其他交叉验证和取样技术,以弥补抗药性样本的稀少(类别不平衡)
- 看一些关于抗生素耐药性和机器学习的文章
模型性能和学习
在我们测试的更复杂的 ML 方法中,阿奇霉素(AZM)的预测性能很高。虽然模型类型的表现非常相似,但随机森林在小得多的样本量下了解到了阻力。
在训练结束时,两个模型都学会了寻找相同的 unitig,该 unitig 包含 23S 核糖体 RNA 中的一个突变,该突变负责大多数高水平的 AZM 抗性。
环丙沙星耐药性通常可以通过寻找导致绝大多数耐药性的一个或两个突变来预测任何物种。从这个意义上来说,它们是容易建立的模型,但是 ML 模型有时很难建立一个没有无关信息的模型。
下面,我们可以看到,随机森林找到了一个很好的解决问题的方法,但 SVM 似乎有过度适应。这两种模型看起来都不会从更多样本的收集中获得特别的好处。
当我研究模型所了解的内容时,我失望地看到,尽管两个模型都非常准确,但我们知道驱动耐药性的主要突变并没有被这两个模型选为首要特征,这表明模型已经找到了一种准确预测耐药性的替代解决方案,不涉及实际的耐药机制。
这是该领域 ML 中一个相对常见的问题——变量如此之多,样本如此之少,以至于在数据中可能形成虚假的关联。这种高性能可能是由于模型识别了标记主要抗性菌株家族的单位,而不是真正的抗性机制。对此进行诊断的最佳方法是查看统一群在分离株的系统发育树上的位置。
你自己试试吧
这个工作坊完全是在 Kaggle 上进行的,所以你可以亲自尝试一下。
未来的工作
我主要是通过在 Kaggle 上跟随人们的笔记本来学习如何建立 ML 模型,所以我希望在这个领域工作的其他人会发现数据集和工作簿是对这个主题的很好的介绍。
现在我已经有了一个研讨会的起点,我计划随着时间的推移对它进行改进,并在更多的环境中运行它,特别是为那些没有太多机器学习经验的人。
我还想探索在出版物中包含笔记本的更新/扩充版本的可能性,让其他人容易地复制和调整分析。