可伸缩的 Web 开发
开发一个简单的具有可伸缩性的 web 应用程序
如今,随着移动接入的增加和电子商务的发展,web 服务变得越来越流行。任何想出一个网站的人肯定会希望它能吸引越来越多的访问者。
但是,首先,我们是否要使用整个服务器场来预期 100 万用户?,这可能是 5 年后的目标。答案是否定的。但是我们要完全忽略我们真诚的期望,建立一个静态的网页吗?。又一个大不了!!!。让我们看看我们能做些什么。
首先要考虑的事项…
简单来说,让我们继续使用 5W 框架,这是一种公认的收集信息和解决问题的方法。让我们在应用框架时考虑到可伸缩性,并在此基础上进行开发。
什么?
对于我们的场景,这是一个 web 应用程序。但是首先要问的问题应该是它是一个 web 应用程序吗?可能不是。目前是这样。
为什么?
web 应用程序的目的。以下是目前常见的一些常见用例。
- 电子商务—需要安全、SSL 和其他认证。
- 交互式应用—社交网络、教育、博客,其中有大量用户同时访问和流式传输内容。
- 信息显示——只是展示内容,没有太多的严肃计算。
- 分析平台——异步/同步接受请求和服务器。基于发布/订阅的处理(PubSub)。提供接口来执行功能的 API。很少有昂贵的处理会运行更长的时间。
什么时候?
我们什么时候部署产品。时间就像 【时间就是金钱】 一样至关重要。我们花费的时间越多,我们为软件过程支付的费用就越多。随着现代敏捷实践的发展,人们更加关注 编码 而不是文档。因此,必须遵循清晰的架构,并定期进行沟通。否则会发生这种事。
Failed Design: source
从高度可扩展的解决方案开始,从设计阶段开始,肯定会花费更多的时间。开始太简单会增加返工的额外压力。因此,在实际开发之前,必须达成折衷方案。我们将看到如何…
在哪里?
其中 有几种形式。
- 我们的目标市场在哪里?
- 我们在哪里?
- 我们将在哪里部署产品/项目?
这些问题主要通过业务分析来解决。然而,作为工程师,由于延迟、安全性和负载平衡的原因,我们更喜欢将服务部署在离目标客户/用户群更近的地方。
谁啊。
我们的目标是谁。他们的访问模式是什么。时区等等,都在这一节中。这是因为,此类信息直接显示了一段时间内工作负载分布的性质。在两个时区工作有时会使发布变得容易,因为我们可以预期在开发中心的工作时间会有较低的工作量。或者,我们可能需要在维护期间提供冗余服务器来满足请求,以获得更好的 QOS(服务质量)。
可扩展的解决方案
现在让我们考虑构建可伸缩解决方案的 web 应用程序场景。提前考虑总是好的,但不能超出可预见的未来。
可维护性和可扩展性
我们所做的任何事情都必须是可持续的。否则在版本的每个发布中都会有大量的返工。因此,适当的 关注点分离 必须在整个项目中实施。
Layering
该图显示了两个数据库实例的内容分层。负载平衡用于同时使用两个实例或一次使用一个实例作为故障保护模式。
连接组件
它总是有单独的组件,并传递消息进行通信。这将增加沟通,但这可以很好地扩展,也很容易维护。下图显示了实施中组件的组织。这与上图不同。这并没有演示信息的实际流动,而是用技术术语演示了组件的分离。
Arrangement of components in actual implementation
通常使用 NGINX (过去使用 Apache ,微软有 IIS 和 Passenger 用于 Python 和 Ruby)路由请求。容器通常运行在不同的端口,但是我们通常只向外界公开端口 80(出于安全原因),这样就没有外部的人可以连接到我们的数据库。
样品流
- 用户请求
www.mydomain.com
,这个请求将被路由到 HTML 静态内容。 - 静态内容将被加载到用户浏览器中。这主要是一个角度或反应的应用。这是因为,不像过去我们使用 PHP、JSP、JADE、Twig 或 Blade 来制作模板,我们不再那样做了。它使我们的应用程序与 API 和请求控制器保持耦合。
- 一旦内容被加载,所有其他工作通常通过调用 web API 来完成。例如,登录请求如下所示。
"method": "POST",
"body": {
"username": "anuradha",
"password": "password1234"
},
"headers": {
"content-type": "application-json"
}
- 这些请求作为一个
post
请求被发送到 urlwww.mydomain.com/login
。这些将被 NGINX 定向到认证服务器。成功后,将为 web 应用程序提供一个令牌。这些被称为 JWT (JSON Web 令牌)。这些将用于以后验证用户。 - 所有到来的请求都将被发送到
www.mydomain.com/api/somepath.
,NGINX 将把/api/
请求路由到 API 容器。JWT 必须作为标题以authorization: bearer <token>
的形式发送。显然,为了防止有人劫持您的令牌,必须使用一个https
连接。
为什么是集装箱
容器是轻量级的虚拟化层,主要运行 linux 内核。使用它们是因为它们可以比虚拟机启动和终止得更快。此外,它们不会像虚拟机那样消耗大量资源。
此外,该容器支持内容的安全部署。举例来说,供应商可以在为特定环境进行配置后为 API 提供 Docker 映像,而不必发送代码库。
在某些情况下,NGINX 本身是一个容器,用来处理大量请求并路由到大量其他容器。点击看看!
弹性豆茎一瞥
Elastic Beanstalk
这是一个提供许多 web 服务器的平台,这些服务器可以随着负载的增加而扩展。他们监视资源利用率,并不断自动添加资源,并相应地收费。由于易于部署,这已经变得流行起来。用他们自己的话说就是,
Elastic Beanstalk 不收取额外费用——您只需为存储和运行应用程序所需的 AWS 资源付费。
优势
*简单
*自动扩展
*资源监控
*提供所需的所有基础架构组件
*安全
他们没告诉你的关于扩展人工智能的事
现在有 AI 的一切的教程。如何做物体检测,图像分类,自然语言处理,建立聊天机器人等。,这样的例子不胜枚举。
但是当我寻找如何恰当地缩放人工智能的信息时,我发现内容很少。更令人惊讶的是,确实存在的少数资源似乎重复了相同的几点:
- 使用像 TensorFlow 这样的可扩展框架构建您的模型
- 要么打包到你的客户端(TF.js,TF Lite,TF-slim 等。)或者将其部署为带有容器的微服务
我对第二部分更感兴趣,因为我已经开发了一个模型,但令我惊讶的是,几乎没有提供关于如何实际实现这一点的细节,关于每个解决方案的缺点的信息甚至更少。在研究了几天并在 Crane.ai 上扩展 AI 之后,我收集了一些关于部署的更多信息**,它们的缺点,以及如何在低层次上优化你的 TensorFlow 模型。**
把它打包到你的客户身上——糟透了!
最常用的技术之一是使用 TensorFlow.js、TF Lite 或 TensorFlow Slim 等工具将 AI 打包到您选择的客户端中。关于这些框架是如何运作的,我不会讲太多细节,而是集中讨论它们的缺点。
- 计算能力。部署这些型号的问题在于,它们需要巨大的内存(我指的是移动应用或浏览器的限制,即>1–2GB 内存)。许多手机没有这种能力,桌面浏览器会延迟 UI 线程,同时也会降低用户计算机的速度,加热计算机,打开风扇等等。
- 推断时间。当你在一个计算能力未知的设备上运行模型时,推理时间通常也是未知的;然而,这些不是 GPU 驱动的高 RAM 高 CPU 机器,而是运行在普通计算机上的手机、浏览器和桌面应用程序。用一些更大的模型进行推理可以轻松地占用一分钟,从用户体验的角度来看这是一个巨大的不。
Stolen from a Reddit parody of XKCD 303
- 大文件。不幸的是大多数模型都存储在相当大的文件中(我们说的是几十、几百 MB)。因此,这将是缓慢和内存密集型加载,并增加了你的应用捆绑包的大小很大一部分。
- 没有安全感。除非您使用开源模型,否则您会希望将您的 AI 和预训练检查点相对保密。不幸的是,当你把你的模型和你的应用打包在一起时,不仅你的推理代码容易被反编译,而且你的预训练检查点也会在包里面,很容易被窃取。
- 更难更新。如果您更新您的模型,您在客户端有两种选择。要么通过集中式管理器(即,Play Store、App Store 等)向用户发布更新。这导致频繁的大规模更新(对于用户来说非常烦人,并且根据他们的设置,进程可能会被中断或永远不会启动),或者应用程序本身运行新模型检查点和元数据的获取。后者听起来好得多,但这也意味着你必须通过用户可能不稳定的连接下载 100MB 以上的文件;这将需要一段时间,因此你的应用程序必须至少在后台打开才能完成这个过程,而且你会产生相当大的互联网成本(这取决于你的云)。
- **缺乏可训练性。**针对新用户数据的训练模型提供了一定程度的个性化,同时提高了其准确性,并建立了一个核心的高信号数据集。不幸的是,大多数设备缺乏训练模型的计算能力,即使它们有,也不可能将训练的效果传播到您的服务器或运行应用程序的其他设备。
这些缺点使得在客户端上部署和维护大型神经网络几乎是不可能的,因此我们将排除这个作为扩展模型的选项。
将其部署为云端点
XKCD 908, and 1117 is also relevant
云是大规模部署模型的强大工具。您可以启动完全根据您的需求定制的环境,将您的应用程序容器化,并立即进行水平扩展,同时提供可与大公司媲美的 SLA 和正常运行时间。
对于大多数 TensorFlow 模型,部署周期是相同的:
- 将您的图形冻结成 Protobuf 二进制文件
- 调整您的推理代码以处理冻结的图形
- 容器化您的应用程序
- 在顶部添加一个 API 层
第一块比较简单。“冻结”您的图需要创建一个 protobuf 二进制文件,其中包含与您的检查点相关的所有命名节点、权重、架构和元数据。这可以通过各种各样的工具来完成,最流行的是 TF 自己的工具来冻结任何给定了输出节点名称的图。你可以在这里找到更多关于这个技巧以及如何完成它的信息。
调整您的推理代码也不难;在大多数情况下,您的feed_dict
将保持不变,主要的区别将是添加代码来加载模型,可能还有输出节点的规范。
容器化也很简单——只需在 Dockerfile 中设置你的环境( 你可以使用一个 TF docker 镜像作为你的基础 **)。**当我们开始添加 API 层时,事情开始变得混乱。通常有两种方法可以做到这一点:
- 部署运行推理脚本的缩放容器。这些容器针对输入运行一个脚本,该脚本启动一个会话并运行推理,然后输出一些东西,结果通过管道返回给您。这是极有问题的;对于大多数云提供商来说,添加一个操纵容器和管道进出的 API 层并不容易或简单(例如,AWS 有 API Gateway,但它远没有你想象的那么方便),这是你可以使用的效率最低的方法。这里的问题是您在容器启动、硬件分配、会话启动和推理中损失了宝贵的时间。如果您让
stdin
保持打开并保持管道输出,您将加速您的脚本,但是会失去可伸缩性(您现在被连接到这个容器的 STDIN,并且它也不能接受多个请求)。 - 部署运行 API 层的扩展容器。尽管在体系结构上相似,但出于几个原因,这要高效得多;通过将 API 层放在容器中,您可以缓解之前提出的大部分问题。虽然这需要更多的资源,但这是最小的,并不意味着垂直扩展;它允许每个容器保持运行,并且因为在这种情况下 API 是分散的,所以将特定的
stdin
/stdout
挂接到主请求路由器没有问题。这意味着您摆脱了启动时间**,并且可以在服务多个请求的同时,轻松保持速度和水平缩放**。您可以使用**负载平衡器、**集中您的容器,并使用 Kubernetes 来保证几乎 100%的正常运行时间并管理您的设备。简单有效!
Deploy your fleet!
通过集装箱船队分散 API 的主要缺点是成本会相对较快地增加到一个大数目。不幸的是,这是人工智能不可避免的,尽管有一些方法可以减轻这一点。
- 重用你的会话。您的车队随着负载成比例地增长和收缩,因此您的目标是最小化运行推理所需的时间,以便容器可以释放空间来处理另一个请求。一种方法是重用
**tf.Session**
和**tf.Graph**
,一旦初始化就存储它们并作为全局变量传递它们;这将消除 TF 启动一个会话和构建图所花费的时间,这将大大加快您的推理任务。这种方法即使在单个容器上也是有效的,并且作为一种技术被广泛使用,以最小化资源重新分配和最大化效率。 - 缓存输入,如果可能,缓存输出。动态编程范式在 AI 中最为重要;通过缓存输入,可以节省预处理输入或从远程获取输入所需的时间,通过缓存输出,可以节省运行推理所需的时间。这在 Python 中可以很容易地完成, 尽管你应该问问自己这对于你的用例是否正确! 通常情况下,你的模型会随着时间变得更好,这将极大地影响你的输出缓存机制。在我自己的系统中,我喜欢使用我所谓的“80-20”法则。当一个模型的准确率低于 80%时,我不缓存任何输出。一旦达到 80%,我就开始缓存,并且将缓存设置为在某个精确度到期(而不是说,在某个时间点)。这样,输出会随着模型变得更加精确而变化,但在这种 80–20减轻的高速缓存中,性能和速度之间的权衡较少。
- 使用任务队列。经常有较大和较小的推理任务需要运行(在我们的例子中,较大和较小,复杂和简单的图像)。对于 UX 来说,在这里使用堆队列可能更好,并且处理优先级为的较小的任务,这样运行小步骤的用户只需等待该步骤,而不是等待另一个用户的较大推断先完成。(如果你在想,*为什么我不在这里只是水平缩放呢?,*你可以但是会增加成本
- **在带有任务队列的专用 GPU 上训练您的模型。**培训是一项漫长而艰巨的任务,需要大量的资源使用,并使模型在其持续时间内不可用。如果你将每个交互反馈到你的模型中进行训练,**考虑在一个单独的服务器上运行这个,**也许用一个 GPU。一旦训练完成,您就可以将模型部署到您的容器中(在 AWS 中,您可以将您的模型库集中在 S3)。
结论
经过深思熟虑,我们提出了一个大规模部署人工智能的有效工作流程:
- 冻结图形并在一个 API 下包装推理
- 重用会话和图形,并缓存输入和输出
- 使用 Docker 封装应用程序(包括 API 层
- 使用 Kubernetes 在您选择的云上大规模部署应用
- 从推理中分离出训练
- 开发一个任务队列来优先处理较小的任务
使用这些技术,您应该能够以最小的成本和开销进行部署,同时最大限度地提高速度和效率!
关于我:我是 Crane.ai (我们用 ai 做 app)的 AI 研究员。在过去的几年里,我一直在研究人工智能,今年花了几个月的时间研究如何扩展人工智能!我希望这是信息,如果你有任何问题,请随时询问。
用 Python 扩展分析洞察力(第 2 部分)
在这篇第二部分的文章中(第一部分可以在这里找到****),我将结合几个我认为非常有用的资源,这篇文章将结合一些关键的分析,以便更好地了解直接面向消费者的订阅/ SaaS 客户群。** 为了将所有这些融合在一起,我还将使用我最喜欢的数据分析/数据科学工具之一, 模式分析 。最后,我希望你会发现既有用又酷的是一份开源模式分析报告,可以在这里找到;这是我在 FloSports 担任数据仓库和数据分析主管时制作的报告类型的精简版本,所有代码和可视化都可以克隆到您自己的模式报告中,并重新用于类似的数据集(假设您创建了一个帐户,该帐户对公共数据集是免费的)。如果你没有 Mode Analytics 帐户,并且还没有被说服注册,你应该仍然能够访问 Python 笔记本,并且你还将在 这个链接 看到所有底层 SQL 查询。
快速总结这篇文章将涵盖的内容(注意,每一部分都将使用 Greg Reda 在他的原始文章中引用的相同电子商务用户数据集,在此处找到):****
- 重新创建第 1 部分的订户群组和保留分析,这次使用 Mode 的 SQL + Python
- 利用 Ryan Iyengar 的一篇出色的博客文章,使用指数衰减在 SQL 中创建一个预计的 LTV 分析,这篇文章可以在这里找到
- 开发 RFM(近期、频率、货币价值)分析,这是电子商务网站用来细分和更好地了解用户/订户购买行为的一种方法
- 在一份全面的开源模式报告中,使用 SQL 和 Python 的简要记录和可视化展示所有这些是如何联系在一起的
同样,模式分析报告在此处可用 —您不需要创建模式分析帐户来查看报告,但是,克隆它需要一个免费帐户。原始数据集可以在之前提供的链接中找到,其他所有内容(SQL、Python 代码、如何构建图表和 Python 可视化)都可以在链接的报告中找到。
首先,我将 relay-foods 数据集作为一个公共文件添加到 Mode 上— 这里是 Mode 关于如何做的说明(我还添加了一个空前数据集,在计划的 LTV 部分会有更多)。这部分帖子中的 Python 代码可以在报告的笔记本部分这里找到;这与我在上一篇文章中使用的代码相同——不同之处在于,从 SQL 查询创建的数据帧是在模式 Python 笔记本中使用的。此外,下面提供的 SQL 代码显示了我在这个数据集上采取的一个小的数据丰富步骤,其中我将用户的第一次付款归类为“初始”,将任何后续付款归类为“重复”。这是可以应用于数据源的业务逻辑类型的一个相当小但快速的例子,通常使用我在过去提到过的聚合事实表。
**with payment_enrichment as (
select
order_id
, order_date
, user_id
, total_charges
, common_id
, pup_id
, pickup_date
, date_trunc('month', order_date) as order_month
, row_number() over (partition by user_id order by order_date) as payment_number
from kdboller.relay_foods_v1
)
select
pe.*
, case when payment_number = 1 then 'Initial' else 'Repeat' end as payment_type
from payment_enrichment as pe**
作为这一分析的下一步,我将建立一个预测的 LTV 分析,参考我之前提到的另一个有用的在线资源——Ryan Iyengar 一年多前在 Medium 上发布了这篇预测 LTV 邮报,在这里找到了。如果您曾经在 Excel 中进行过 LTV 分析,您会知道这可能相当耗时,并且这些模型可能会变得非常大,并且远不如您所希望的那样动态。您可能认为必须有更好的方法来计算单个值,这对于了解订阅业务的健康状况至关重要。幸运的是,至少有一些更好的、高度可重复的方法可以做到这一点。此外,评估群组的不同开始/结束日期范围以包括,例如,对特别强的“早期采用者”群组进行标准化,是在 SQL 和/或 Python 中进行这种类型的分析的显著优势。
综上所述,Ryan 的文章肯定需要对 SQL 有一个相当透彻的理解,特别是考虑到公共表表达式(cte)的重要用途。鉴于结果查询的长度,我没有直接粘贴到这篇文章中,但是您可以在模式报告中找到最终查询的几个构建。如果你对我这篇文章的背景或理由有任何疑问,我会推荐你参考他颇有见地和丰富的解释。出于我的文章和报告的目的,我已经展示了如何从我们正在使用的原始数据集创建所需的数据返回,从下面的数据集提供了一些观察数据,还展示了 LTV 部分的一些关键图表,Mode 允许我轻松地将这些图表添加到我的整体报告中。
LTV 部分—查询摘要
****与 LTV 相关的 SQL 查询,借用了 Ryan 非常有用的工作,从报告中的编号 2-8 开始。在 Mode 中,我尝试使用该系统对我的查询进行排序,以便跟踪在一个项目中编写的查询的自然进展— 一个很好的功能请求是拥有某种类型的文件夹系统,以便将这些查询组织到支付、LTV 和 RFM 查询部分。查询#2 操作数据集,以便拉回与 Ryan 在他的帖子中使用的数据集一致的返回结果——因此,这有望使它更容易理解。我还构建了查询中断来模拟 Ryan 在他的博客帖子中暂停的地方,这样就可以更容易地在他的帖子和我创建的模式报告之间进行跟踪。
在查询 4–6 中,您将看到在执行此操作时,我在一个群组中检测到异常情况(参见本段下方的屏幕截图)。在 2009/04/01 年度,在最近的三个月中,该集团的收入份额实际上大幅增加;虽然用户离开一个群组然后又回来的情况并不少见,但这明显扭曲了预测收入,因此也扭曲了这个特定群组的 LTV。因此,该模型并没有预测收入的下降,而是预测每月增长 20%以上(整个预测)。虽然可能有一些首选的方法来规范这一点,但出于时间的考虑,我决定从整体分析中排除这一群体——我要说的是,几乎每个分析中都有类似的事情发生,显然审查数据并确定如何根据需要进行调整至关重要。考虑到公司的行业、未来前景和成熟度,调整决策会有所不同。
查询 7 和 8 以最终预期回报完成查询,一个显示每个群组的预计 LTV,一个显示考虑所有群组的预测预测和初始用户时的总体预测 LTV。正如您从下面的屏幕截图中看到的,从模式报告输出中复制的,总的 LTV 是大约 315 美元。除此之外,我们看到初始群组(最有可能是“早期采用者”)的 LTV 比随后几个月中看到的要高得多——注意,我们只有 12 个月的群组进行评估,这是一家很少出现的新公司。鉴于此示例,接下来的步骤可能包括进一步细分用户并找到每个 LTV,以及通过从该分析中排除“早期采用者”的贡献来潜在地标准化整体 LTV。同样值得注意的是,随着最近队列数量的增加,LTV 会降低很多,我们希望了解,随着时间的推移,我们是否能够更有效地扩大我们的用户群,同时理想地扩大或至少维持我们的 LTV。 请注意,在完成此类分析时,您还应该计算毛利润,或者最好是边际贡献,以反映每个订户的真实基础经济和 LTV。
RFM 分析
在这最后一节,我已经包括了一个近期,频率,货币价值(RFM)分析。在回顾有效的用户细分方法的过程中,在发现这种方法后,我在 Joao Correia 的 GitHub 上找到了这个有用的笔记本。这里是 RFM 的一个非常简短的纲要。
****新近度:从评估日期起,例如今天,用户/订户已经从网站购买了多近。
****频率:用户/订户一生中在特定网站上购买的次数。
****货币价值:用户/订户在其作为客户的整个生命周期中从其所有购买中支付的美元总额。
虽然有几种方法可以完成 RFM 分析,包括通过 SQL,但我在这里的部分意图是展示如何使用一个相当简单的初始数据集来重新利用其他人的有用工作。在《Python 笔记本》中,RFM 分析了第部分,你会发现我在很大程度上遵循了若昂的笔记本。正如在他的笔记本结论中,我在下面显示了 RFMClass 111 中的前十名客户—这代表了在所有三个 RFM 指标中处于前四分之一的用户;这些用户是最近才付费的,而且比该网站 75%的其他用户付费次数更多,货币价值也更高。因此,这些用户是定性调查的主要对象,这些调查要求对他们喜欢什么和什么可以做得更好进行反馈,你也可以研究他们的网站行为,了解他们可能在做什么,发现哪些与其他访问者不同,以及你可以接受的几种不同的评估。
在模式报告中使用 Plotly,然后我添加了三个可视化工具,帮助我更好地理解数据,也是为了预测来自业务利益相关者的潜在问题。第一个显示了每个 RFMClass 中的用户分布。积极的一面是,81 名用户属于 111 类,但 75 名属于 344 类,65 名属于 444 类——这些用户已经有一段时间没有付费了,他们的付费次数和货币价值也远低于所有用户的中值。我们想看看我们是否能复活这些用户,或者是否发生了什么事情导致他们(几乎)再也不会回到我们的网站。
最后,我想知道每一个 RFMClass 指数是如何根据群体分组的——早期的群体是那些主要在 111 类中的群体吗?如果是这样的话,这可能是一个令人担忧的原因。为了完成这一部分,您将在 Python 笔记本中找到“合并数据以显示按群组划分的 RFM 等级”一节,在这里,我提取了初始付款数据帧的子集,并将其与创建的 rfmSegmentation 数据帧合并。如果你需要一本关于在 Python 中合并数据帧的入门书,类似于 SQL 中的 join,我会推荐你到另一个 Greg Reda 帖子这里。
回顾下面的两个图表,第一个是按群组划分的总用户,第二个是按群组划分的 RFM 111 级用户,我们看到 RFM 111 级的最大贡献者来自 2009 年 11 月和 2010 年 1 月。然而,当我们回顾第一张有每个群组用户总数的图表时,我们看到 2009 年 11 月是一个特别大的群组,相对来说,并不是 RFM 顶级用户的主要来源。更好的输出将为每个群组计算每个月群组中 RFMClass 111 用户所占的百分比;但是我将把它留给下一次/可能是后续的帖子。
这是一篇我很喜欢整理的分析文章的结尾。希望这传达了超越 Excel 的价值,利用有洞察力的人的公共贡献以及 SQL 和 Python 等工具的好处。
关于使用 Mode 的好处的一些快速说明( )此外,这里有一篇来自 Mode 的博客的帖子 ,关于他们如何从 Excel 转向 SQL 和 Python:
- 可以用 SQL(内置图表功能)或 Python 轻松创建图表;然后,这些图表与您的数据查询相关联,并将自动更新
- 报告具有很高的可复制性,如果您想保留以前的报告,例如季度业务回顾
- 减少了创建数据集并以 CSV 格式读入 Jupyter 笔记本的需求;您可以将每个查询作为数据帧直接读入 Mode 的 Python 笔记本中
- 我说这些是有警告的,并不是所有的 Python 库在 Mode 中都可用,尽管有相当一部分是可用的,并且计算能力是 Mode 不断寻求改进的一个领域
从一个非常基本的用户数据集开始,利用模式分析的全部功能,我们能够完成以下工作:
- 在 Mode 的平台上贡献两个公共数据集
- 通过 SQL 和 Python 将用户按月分组
- 在 Python 中为这些月度群组构建保留曲线/保留热图
- 在 Python 中计算群组的加权平均保留曲线
- 使用 SQL 为整个公司和每个团队设计 LTV 项目
- 进行 RFM 分析,了解 Python 中每个类的分布以及每月群组的贡献——这是通过在 Mode 的 Python 笔记本中合并两个数据框架(一个由 SQL 和 Python 共同创建,另一个由 Python 直接创建)来完成的
- 最后,我们创建了一个(希望如此)直观、全面的开源报告
- 就添加额外的分析而言,该报告是相当可扩展的,并且也可以随着新的每月数据的滚动而容易地更新
希望你发现这篇文章和我发现的 Mode 一样有帮助,以及参考的文章被用来制作这份 Mode 报告,如果你有任何问题或想法,请在评论区告诉我。
用数据流扩展游戏模拟
The GUI version of the Game Simulation
数据流是构建可扩展数据管道的一个很好的工具,但它也可以用于不同的领域,如科学计算。我最近使用谷歌云数据流工具的方法之一是模拟不同自动化玩家的游戏。
几年前,我制作了一个自动俄罗斯方块播放器,作为加州理工学院人工智能课程的一部分。我使用了元启发式搜索方法,这需要大量的训练时间来学习超参数的最佳值。我能够编写系统的分布式版本来扩展这种方法,但是在实验室的 20 台机器上部署它需要很大的努力。使用现代工具,将这些代码扩展到几十台机器上运行是很容易的。
出于多种原因,在游戏中模拟自动玩家是很有用的。最常见的原因之一是测试游戏中的 bug。你可以让机器人不停地敲打游戏,直到有东西坏掉。模拟游戏性的另一个原因是为了建立可以高水平学习和玩的机器人。通常有三种模拟游戏的方式:
- **实时:**你用正常设置运行游戏,但是一个机器人模拟鼠标点击和键盘输入。
- Turbo: 你禁用渲染组件和其他游戏系统,以尽可能快地运行游戏。如果你的游戏逻辑从渲染逻辑中分离出来,这将会带来数量级的加速。
- 运行模拟最快的方法是禁用游戏中的所有图形和渲染组件。使用这种方法,游戏可以作为函数调用,运行模拟的结果由函数返回。
对于人工智能课程,我使用了涡轮模式,每个人工智能代理独立运行。如果我要重复这个实验,我会使用像 Dataflow 这样的工具,在一个托管环境中扩展系统。这篇文章讨论了如何使用谷歌的数据流工具来实现这个逻辑。它非常适合可以独立运行的长时间运行的任务。
如果你对构建自己的俄罗斯方块游戏代理感兴趣,Bohm 等人的这篇文章介绍了一种创建强大玩家的有趣方法。他们定义了许多用于确定如何采取最佳行动的指标,如下面所示的海拔高度和油井差异。
Heuristics used by Bohm et al.
对于俄罗斯方块来说,这已经不是最先进的了,但是这是一个很好的起点。俄罗斯方块也是编写人工智能的一个好问题,因为从头开始编写一个俄罗斯方块游戏并不太复杂,然后可以用来模拟人工智能玩家。GitHub 上提供了该模拟器的完整代码:
GitHub 是人们构建软件的地方。超过 2800 万人使用 GitHub 来发现、分享和贡献超过…
github.com](https://github.com/bgweber/Simulator/tree/master)
俄罗斯方块代码来自我 10 年前写的一个课程项目,我不建议以此为起点。回顾我的一些旧代码很有趣,我必须对它进行一些修改,以便让它在数据流的无头模式下运行。我已经为游戏解耦了游戏逻辑和图形线程,但是我删除了许多在分布式环境中无法工作的静态对象引用。
设置数据流 如果你熟悉 Java 和 Maven,启动并运行云数据流应该不是什么难事。第一步是在 pom.xml 中定义依赖关系:
<dependencies>
<dependency>
<groupId>com.google.cloud.dataflow</groupId>
<artifactId>google-cloud-dataflow-java-sdk-all</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
一旦您将这个依赖项添加到项目中,您就能够构建和部署数据流作业。我在这个项目中使用了 Eclipse,但是 intelliJ 是另一个很好的用 Java 创作数据流任务的 IDE。关于设置数据流的更多细节可以在我以前关于扩展预测模型的文章中找到:
我在数据科学职业生涯中面临的主要挑战之一是将探索性分析的结果转化为…
towardsdatascience.com](/productizing-ml-models-with-dataflow-99a224ce9f19)
用数据流 模拟游戏在修改了我的旧代码后,我现在有了一个游戏模拟,我可以用它作为一个函数(方法)以无头模式运行,并在完成时返回游戏统计数据。一旦你以这种方式建立了一个游戏,就可以直接使用数据流来运行数千次模拟。我用以下操作定义了一个 DAG:
- 创建种子值集合以用作输入
- 对每个种子进行模拟
- 将游戏结果保存到 BigQuery
第一步是创建一个种子集合,作为游戏模拟器的输入。拥有一个具有可重复结果的游戏模拟是很好的,以便 QA 和测量不同游戏代理的性能。我用这一步来指定要完成多少工作。如果实例化了少量的种子,那么将执行少量的模拟,如果你使它更大,将执行更多的模拟。
Random rand = new Random();
ArrayList<Integer> seeds = new ArrayList<>();
for (int i=0; i<10; i++) {
seeds.add(rand.nextInt());
}
我使用下面的代码来设置数据流管道,并将种子作为输入传递给管道流程。
Simulator.Options options = PipelineOptionsFactory.
fromArgs(args).withValidation().as(Simulator.Options.class);Pipeline pipeline = Pipeline.create(options);
pipeline.apply(Create.of(seeds))
下一步是使用传入的种子作为游戏模拟的输入。这个应用步骤的结果是,输入的种子值用于创建一个 TableRow 对象,该对象捕获游戏的汇总统计数据。种子被传递给游戏对象,结果是代理完成的行数。我还记录了代理在决定采取哪一步行动时使用的超参数。
.apply("Simulate Games", ParDo.of(new DoFn<Integer, TableRow>() { @ProcessElement
public void processElement(ProcessContext c) throws Exception {
Integer seed = c.element(); // play the game
Game game = new Game(seed);
int levels = game.runSimulation(); // save the results
TableRow results = new TableRow();
results.set("levels", levels);
results.set("heightFactor", game.getHeightFactor());
results.set("balanceFactor", game.getBalanceFactor());
results.set("holeFactor", game.getHoleFactor()); // pass the stats to the next step in the pipeline
c.output(results);
}
}))
最后一步是将结果保存到 BigQuery。这是在数据流中执行的一个简单步骤,但是您需要首先为目标表定义一个模式。下面的代码显示了如何执行这一步。
.apply(BigQueryIO.writeTableRows()
.to(String.format("%s:%s.%s", PROJECT_ID, dataset, table))
.withCreateDisposition(BigQueryIO.Write.
CreateDisposition.CREATE_IF_NEEDED)
.withWriteDisposition(BigQueryIO.Write.
WriteDisposition.WRITE_TRUNCATE)
.withSchema(schema));
我们现在有了一个数据流图,可以在本地运行进行测试,或者部署在完全托管的云环境中进行大规模运行。运行这个 DAG 的结果是,将在 BigQuery 中创建一个表,其中包含每个游戏模拟的汇总统计数据。
运行模拟
你可以在本地或云中运行数据流。最简单的方法是先在本地测试,然后再扩大规模。通过将模拟数量设置为一个较小的数字(如 3),然后运行管道,可以测试整个管道。因为我们将结果保存到 BigQuery,所以我们需要为管道指定一个临时位置。为此,您可以为 java 应用程序提供一个运行时参数,例如:
--tempLocation=gs://ben-df-test/scratch
运行管道后,您应该会看到在 BigQuery 中创建了一个包含模拟结果的新表。下一步是扩大模拟的数量。
The operations in our Dataflow DAG
要在云上运行模拟,您需要指定更多的运行时参数。我将机器的最大数量设置为 20,以便从缺省值 3 开始加速这个过程。请记住,对于长时间运行的操作,使用更多的机器会变得昂贵。
--jobName=level-sim
--project=your_project_ID
--tempLocation=gs://ben-df-test/scratch
--runner=org.apache.beam.runners.dataflow.DataflowRunner
--maxNumWorkers=20
对 1,000 个种子运行此模拟会产生如下所示的自动缩放图表。在完成所有的模拟和保存结果之前,我的工作扩展到 14 个工作。
Autoscaling to meet the simulation demand
一旦完成,我就有了我的俄罗斯方块游戏代理的 1000 次迭代的游戏统计数据。
模拟结果
结果现在可以在 BigQuery 中获得。对于每个模拟,我都对超参数值进行了微小的更改,如下表所示:
Simulation results in BigQuery
我们现在可以在数据中寻找相关性,例如确定某些因素是否对清除的行数有影响:
select corr(holeFactor, levels) as Correlation
,corr(holeFactor, log(levels)) as logCorrelation
FROM [tetris.sim_results]
然而,结果中没有信号,对数相关的 R = 0.04。由于数据在 BigQuery 中可用,我们还可以使用 Google Data Studio 来可视化结果:
可视化证实了数据中没有相关性,但是我也为因子使用了小范围的值。
结论
数据流是扩大计算的一个很好的工具。它是为数据管道设计的,但可以应用于几乎任何任务,包括模拟游戏。
本·韦伯是 Zynga 的首席数据科学家。我们正在招聘!
使用估计器扩大 Keras
Keras can’t scale up Mt. Hood, but TensorFlow can!
你知道你可以把一个 Keras 模型转换成一个张量流估算器吗?围绕分布式培训和扩展,它将为您提供一整套选项。我们将准备一个 Keras 模型,通过将其转换为张量流估计量来进行大规模运行。
Keras 模型,符合估计量
所以我们有一个 Keras 模型。易于定义,易于阅读,易于维护。但是我们在扩展到更大的数据集或跨多台机器运行方面做得不太好。
幸运的是,Keras 和 TensorFlow 有一些很棒的互操作性特性。
我们想要做的是将我们的 Keras 模型转换为 TensorFlow 估算器,它内置了分布式训练。这是我们解决扩展挑战的入场券。
另外,一旦我们的培训结束,做模型服务也变得很容易。
Scikit-learn 非常适合组装一个快速模型来测试数据集。但是如果你想运行它…
towardsdatascience.com](/deploying-scikit-learn-models-at-scale-f632f86477b8)
事实真相
我们感兴趣的函数叫做model_to_estimator
。“模型”部分指的是 Keras 模型,而“估算器”指的是 TensorFlow 估算器。
我已经拿了我们在上一集中从开始的笔记本,并用新代码更新了它,以将我们的 Keras 模型转换为张量流估计器。培训完成后,我们还将了解如何导出 Keras 模型和 TensorFlow 模型。
这是一个简短的帖子——真正的内容在下面的截屏中,我在这里浏览代码!
towardsdatascience.com](/getting-started-with-keras-e9fc04f7ea6a)
我用我的 Kaggle 内核录制了一段视频来帮助说明这一切是如何进行的细节:
包扎
使用张量流估值器获得分布式训练是对 Keras 模型的简单调整。因此,现在您拥有了两个世界的精华:易于阅读的 Keras 模型创建语法,以及通过 TensorFlow 估值器进行的分布式训练。
请记住,使用 Keras 模型获得分布式培训只需使用model_to_estimator
。
感谢阅读这一集的云人工智能冒险。如果你喜欢这个系列,请为这篇文章鼓掌让我知道。如果你想要更多的机器学习动作,一定要关注媒体上的me或订阅 YouTube 频道以观看未来的剧集。更多剧集即将推出!
Spark 上分布张量流的放大
https://unsplash.com/photos/ZiQkhI7417A
您可能在过去经历过,或者可能在某个时候会经历,内存不足是数据科学中一个非常常见的问题。
由于业务涉及面广,创建包含超过 10,000 个或更多要素的数据集并不罕见。我们可以选择用树算法来处理这样的数据集。然而,Deeplearning 可以更容易地自动设计功能,并将它们处理成你选择的模型。
一个经常出现的问题是,在处理如此大量的数据时,如何在适当的数量内训练您最喜欢的模型。
结合 Tensorflow 和 Spark 的分布式深度学习为这个问题提供了一套便捷的解决方案。
1。简而言之分布式深度学习
分布式深度学习的关键成分包括中央 参数服务器 和 工人 。
Fig 1: Illustration of Distributed Deeplearning from Joeri Hermans’thesis
每个 Worker 被分配一个数据集的分区,也称为 shard、 和一个 局部模型副本 。在数据集的这一部分上,每个工人应用常规的深度学习优化,例如 mini-batch,并计算梯度。在计算梯度时,工人 将其结果 提交给中央 参数服务器 。
在接收到来自所有工作者的所有提交之后,梯度被平均并且模型被更新。接下来 允许工人拉动 模型的新参数化。
从这个框架中,出现了许多令人兴奋的数学问题,例如想知道这个过程是否可以异步进行?这将避免让工人等待来自参数服务器的每个新的更新。这种技术的缺点是工人可能用过时的模型参数处理数据。我推荐阅读游里·赫曼的论文来了解更多关于这个话题的细节。
2.分布式张量流来了
Tensorflow 使用数据流图来表示各个操作之间的计算依赖关系。分布式张量流允许我们在不同的进程中计算图的部分,因此在不同的服务器上。
图 2 示出了分布式张量流设置,即张量流集群。
在该集群中,可以找到多个组件,如第一部分所述。
Fig 2. Illustration of a distributed Tensorflow set-up on Google Cloud Platform (https://cloud.google.com/architecture/running-distributed-tensorflow-on-compute-engine)
Tensorflow 提供集装箱管理器 Kubernetes 作为服务不同工作的主要选项。容器可以通过 Tensorflow 的协议缓冲区 tf.train.Example 从输入管道接收数据。
就我个人而言,我已经开始喜欢 Tensorflow 的 dara 格式和数据集类 tf.data.Dataset ,并开始喜欢上它。遗憾的是,Tensorflow 的数据预处理库还处于起步阶段。如果你像我一样,喜欢在通过任何神经架构之前创建大量的功能,你会寻找另一个专门从事 ETL 的分布式计算框架,比如 Spark。
火花来了…
3.火花拯救我们!
Spark 的优化能力在于使用弹性分布式数据集,即 rdd 。
Yahoo 提供了一个开源存储库,它为您管理 workers 和 parameters 服务器,同时为您提供来自 rdd 的流数据的可能性。
为此,我们需要使用包装器 TFCluster 定义一个集群,如图所示
如果您计划传递一个 rdd 来训练或生成预测,您需要将 input_mode 设置为 TFCluster。输入模式。火花。
使用以下代码应用训练或推理:
一切都很好,现在仍然有两个关键问题需要回答:
- 我们应该在哪里传递我们的计算图,包含我们的神经网络?
- 您应该从哪里提取数据批次?
所有这些都是在一个 map-function 中定义的,您需要将它作为一个参数传递。
包含主要成分的示例如下所示:
如本文第一部分所述,集群中有两种类型的作业,参数服务器和 worker。
如前所述,您需要您的参数服务器不断地监听来自作品的可能的 提交 。这是使用***server . join()***方法完成的。这个方法告诉 TensorFlow 阻塞并监听请求,直到服务器关闭。
在 worker 中,可以在由TF . device(TF . train . replica _ device _ set(…))处理的上下文管理器中定义自己喜欢的计算图。
这一行确保参数服务器知道您的工人正在计算任何 tf 的梯度。你可能已经定义了变量。因此,它将聚合这些梯度,并在每次提交后发回更新。
如果你像我一样懒惰,你可能也使用TF . train . monitored training session来处理回调,比如保存/恢复你的模型,并最终计算训练的摘要。
您需要确保不是所有的员工都在处理这些子任务。建议定义一个所谓的首席工人,例如任务指数为 0 的工人。
rdd 上的流用 ctx.get_data_feed(…) 调用。Spark 和 Tensorflow 的组合不是 100%最佳的。您可以根据TF . train . monitored training session中定义的一些条件,基于步骤或指标,选择终止应用程序。然而,Spark 不能提前终止 RDD 操作,所以多余的分区仍然会被发送给执行器。 tf_feed.terminated() 是为了表示这些多余的分区被忽略。
4.结论
在这篇博客中,我们学习了如何将 Spark 和 Tensorflow 结合起来,将弹性分布式数据帧分布在不同的作品上。我们还理解了参数服务器如何在每次提交 workers 之后不断更新变量。
我们希望这个博客对你有用,因为它对我们来说是一次很好的学习经历!
关于作者
庞欣是一名高级数据科学家,专攻 Spark,目前在一家最大的物流跨国公司应用她的技能。
Benoit Descamps 是一名独立的人工智能顾问,对数学和软件工程充满热情。如果您对集成最新的机器学习解决方案有任何疑问,请直说!
更多精彩内容!
参考
[1] 关于分布式深度学习的来龙去脉的有见地的技术讨论 n/
[2] 分布式张量流导
[3] 分布式张量流简介
[4] [分布式 Tenforflow @ Tensorflow 发展峰会(2017)](http://TensorFlow Dev Summit 2017)
作为商业案例的扩展与失败的数据科学
…在公司或行业中。
对赋予人才转向数据科学家的追求导致了一个后续观察:许多公司正在努力建立机器学习的商业案例。
努力建立业务案例会导致不必要的后果,例如
- 数据科学项目经常失败
- 很少或没有为公司增加价值
- 新的或改进的产品不能吸引顾客
- 人才离开公司
从我所知道的和我所举的例子中,我提炼出了以下见解:
- 如何让数据科学业务案例失败
- 如何成功扩展数据科学
“Data has a better idea” by Franki Chamaki on Unsplash
如何让数据科学业务案例失败
- 项目管理的人工智能——例如,汽车行业已经到了“要么人工智能,要么死亡”的时刻。我见过的一个经常性策略是在数字/数据/创新实验室建立一个团队,给他们一些项目。3 到 6 个月后,有了一个新项目,团队将重点从文本处理和为司机构建聊天机器人转移到销售数据的预测建模。管理层和团队可能认为他们在敏捷项目管理、快速迭代和概念验证方面做了正确的事情。不幸的是,这通常是所有发生的事情。事实证明,项目管理不是一种策略。
- 快速招聘——“我希望在未来 12 个月内招聘 150 名数据科学家”。我没有开玩笑:我确实接到了那个电话,是关于招聘“数据科学家顾问”的。是的,专业知识很重要。是的,人才短缺。是的,随着公司努力建立数据科学能力,他们可能会热衷于咨询服务。然而,专家短缺是真实的。此外,高级数据科学家可能更喜欢构建产品而不是项目工作,更喜欢影响客户而不是项目管理会议。总的来说,我见过不少尝试使用“招聘即解决”的方法,但往往在实施中已经失败了,要么是因为不切实际的“独角兽”愿望列表,要么是因为无法解释为什么一个有经验的数据科学家应该加入公司。此外,数据科学家经常报告他们被非专家采访。
- 通过在线学习提高技能 —在线学习全天候可用,公司已经开始赞助员工,通常是大量的员工。对公司来说,好处似乎很明显,因为员工已经拥有领域专业知识,通常利用自己的时间提高技能,谁有能力和毅力进入数据科学或机器学习角色变得很明显。我见过不少公司学习小组的参与者。然而,通过练习和顶点项目学习并不等同于构建机器学习用例。如果你停下来一分钟,我想你会看到要求新提升的员工交付用例是在等待奇迹。
- 在顶部喷洒机器学习——最初的冲动是询问机器学习如何改进现有的产品或服务。它可能会以渐进的方式做到这一点——但我们都明白自动驾驶(带驾驶员辅助系统)与自动驾驶汽车不同,你需要重新设计自动驾驶汽车及其基础设施。这同样适用于或许更为平凡的电子商务环境。如果你有一个目录,然后试图在上面建立一个小的推荐引擎,或者你决定建立一个真正强大的推荐系统,看看它如何改变你的产品,这很重要。在我看来,在顶部喷洒机器学习意味着让你的业务面临任何积极追求人工智能优先战略的竞争对手的风险。
- 群龙无首还是群龙无首——如果你雇佣了一个明星,但她没有能力组建一个团队,如果有必要的话,从头开始,你不会走得很远。也许在创业公司中更常见的是,你有机器学习部队,但没有头。乍一看,这可能令人惊讶,因为你会认为初创公司 CxO 明白这一点,但我的经验是,他们经常不明白,包括 CTO。一个常见的场景是,数据科学夫妇或三人组试图推动创业公司知道应该做但并不真正理解的业务案例,无论是从客户的角度还是从将技术作为产品部署的角度来看。
数据科学商业案例的失败会产生影响。如果公司、管理层和员工一直失败,他们将失去对机器学习方法的信任,并停止接受更多数据驱动的方法。其次,这将导致不再具有竞争力的生存风险。
那么,如何从失败走向成功呢?如何实现可扩展的数据科学方法?
“Failing forward to success” by Ian Kim on Unsplash
如何成功扩展数据科学
有些故事线我反复听过,也知道公司有成绩。这些故事有一些共同的元素。通常情况下,并非所有元素在开始时都存在,但最终大部分(如果不是全部的话)都会显现出来。我认为,要成功扩展数据科学,以下五个要素值得特别关注。
- 数据科学商业案例是新产品或服务的核心 —从根本上说,这是一个战略决策,即如果你选择建立数据科学,就建立它来开发新产品或服务。这通常会确保数据科学是一个商业案例,机器学习技术是产品或服务的核心。这样你就有了一些灵活性:产品的用户可以是你公司的外部或内部用户,它可以是付费用户,也可以不是。同样,如果你最初失败了,你现在也在向前失败。你的商业案例会给你带来可衡量的影响。对客户影响力的追求推动你前进。
- AI-first 在战略上非常重要,足以让公司重新考虑其产品或服务 —例如,假设你是一家电子商务公司,正在建立一个推荐系统(“你可能也会喜欢”功能),旨在通过交叉销售和追加销售来提高客户满意度和篮子大小。你的商业案例的一部分必须是“我是否需要扩大或改变目录以更好地服务我的顾客?”很可能会对你的供应链、仓储等产生影响。如果你再次考虑自动驾驶汽车的例子,就很容易看出设计、生产、销售和售后市场将会发生多大的变化。管理层必须理解并接受 AI-first 改变了产品,业务案例应该要求实现这些改变。
- 该公司在全球范围内投资数据,也从外部来源投资 —我们都听说过“数据湖”以及数据可访问性和质量的重要性。然而,即使你没有一个大湖,你仍然可以开始:有公共数据集、数据市场和 API。我的观察是,更敏捷的团队也利用外部数据。构建新产品也是如此,您可能会使用自己的数据,但也可能会利用新的数据源。需要的是对数据治理的承诺:广泛的数据收集、透明的元数据、方便的访问和持续的质量评估。如果数据不是数据团队面临的问题,但数据是每个生产数据的员工所拥有的产品,这将有所帮助。我经常听到对 GDPR 的抱怨,但我认为我们必须认识到,无论如何,当务之急是在全球范围内收集数据,并开发尊重和支持民主和隐私的数据治理形式。
- 建立国际团队,最好是在数据科学热点 —欧洲已经出现了一些数字创新中心,大多数数据科学人才聚集在这些中心。我的经验是,如果公司将总部设在地区或省会城市,如果公司还在数据科学中心(如柏林)开设办公室,并允许员工选择工作地点,那么组建团队会更加有效。数据科学和机器学习人群非常国际化,在其商业发展的早期阶段,每个人都受益于邻近性,正如许多大型 Meetup 社区所体现的那样。即使我们在网上工作和协作,个人发展和商业案例也会从活跃的社区中受益匪浅。
- 在 12 到 24 个月的时间里实施混合员工技能提升和人才招聘的战略 — 由于全球范围内的专家数量相当少,任何团队的组建都必须超越招聘:员工技能提升和新人才投资。最近,我了解到航空业的一家大型公司正在提高数百名员工的机器学习技能,他们之前的角色是数据生产者、所有者或用户。这是我第一次看到大规模的实施工作。另一方面,需要系统化地持续招聘新人才。虽然在美国这是常见的做法,但我在欧洲没有看到太多的证据。在欧洲,找到人才并赋予他们权力仍然是关键问题。
我希望听到业内任何有兴趣解决这一关键战略问题的人的意见,即在发现和培养人才的同时,将数据科学扩展为商业案例。我的联系方式在 LinkedIn 上。
如果你有更多的观察或故事,欢迎评论或联系。对于输入、评论和更正,我感谢我出色的合作者——所有数据科学家:帕特里克·拜尔博士、特里斯坦·伯伦斯博士、马卡雷纳·贝吉尔-邦帕德雷博士、迪娜·代法拉博士、朱江猛·杜博士、塞巴斯蒂安·福考博士、、法赫纳兹·贾兰内贾德、达尼亚·梅拉、卡尔弗洛·佩鲁马尔博士。
基于 k-近邻的扫描数字识别
标签 : Python、scikit-image、scikit-learn、机器学习、OpenCV、ImageMagick、梯度方向直方图(HOG)。
如何在 2 分钟内将 500 张背景嘈杂的扫描图像上打印的数字(如下图)提取成 100% 准确率的 excel 文件?
简单的答案是:你不可能在 2 分钟内达到 100%的准确率,这需要 8 分钟。
Figure 1. Original image
预处理图像需要 2 分钟,机器学习模型正确预测 98%的数字需要 6 分钟,人工修复 2%的不准确预测,尽管只需很少的努力。通过向用户呈现模型无法以 100%的置信度分类的数字,6 分钟成为可能,如本博客结尾的“呈现”部分所示。
不成功的方法
在解释 k-NN 解决方案之前,我将简要回顾一下我探索过的一些不成功的提取数字的方法。
1-宇宙魔方——谷歌的光学字符识别(OCR)
尽管使用了 Tesseract 的选项将图像识别为单个文本行,并且仅识别数字,但是应用 Google 的 Tesseract 导致了低精度的数字识别。请注意,在应用 Tesseract 之前,图像的背景噪声已被去除(在本博客后面的去噪步骤中有更多内容)。
2-图像模板匹配
第二种方法是为 9 个数字中的每一个生成模板图像,然后检测图像中的每一个数字,并使用 openCV 的 matchTemplate 函数将其与 0 到 9 个模板中的每一个进行比较
**import** cv2result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
(_, score, _, _) = cv2.minMaxLoc(result)
由于噪音,这种方法对我们的问题不起作用。然而,这篇博客https://www . pyimagesearch . com/2017/07/17/credit-card-ocr-with-opencv-and-python/成功演示了使用模板匹配来识别信用卡上的印刷数字。
成功的方法:使用机器学习进行训练和预测
最后一种方法是训练我自己的机器学习模型。该解决方案需要满足以下要求:
- 从图像中挑出每个数字
- 选择适当的特征提取应用于每个数字
- 选择多类分类器
输入/数据预处理、特征工程和数据准备是任何基于机器学习的解决方案的核心。选择使用哪种机器学习分类器是一个重要的步骤,然而,它的成功在于上面提到的。
大纲:
- 图像预处理
- 数字提取和训练/测试数据准备
- 特征抽出
- 培养
- 预测
- 介绍会;展示会
1.图像预处理
弗莱德·魏因豪斯(http://www.fmwconcepts.com/imagemagick/textcleaner/)的 TextCleaner 脚本已经被用于去除图像背景噪声,随后是图像锐化步骤。这两个步骤都需要 ImageMagick 库(【https://www.imagemagick.org】T2)。或者,我推荐使用 python 的库,如 OpenCV 或 scikit-image 来预处理图像。
# text cleaner
./textcleaner -g -e stretch -f 25 -o 10 -u -s 1 -T -p 10 **input.jpg** **output_clean.jpg**# image sharpening
convert **output_clean.jpg** -sharpen 0x10 **output_sharp.jpg**
上面的代码产生了下面的图像
Image 2. De-noised image
2.数字提取和数据准备
由于噪声,使用 OpenCV 的 findContour 操作从图像中挑选出每个数字不会产生可靠的结果。对于这个特定的问题,检测数字周围的“边界框”(图像裁剪),然后从裁剪的图像中“挑出”每个数字,这是更健壮的。找到边界框后,后一步很容易,因为每个数字相对于裁剪图像的左上角都有一个固定的坐标。
Figure 3. De-noised inverted image
注意:使用梯度方向直方图(HOG)进行特征提取需要反转黑/白像素。
2.1 检测包围盒
使用第三方工具来裁剪图像的边界并不能在所有图像上很好地工作。相反,我创建了一个简单的方法来确定性地裁剪图像,并以 100%的准确度检测边界框。
该方法从计算矩形的白色像素开始,如图 4 所示。如果白色像素的计数超过经验设定值,则矩形的坐标是数字的上边界,并将用于裁剪图像。
Figure 4. Top image cropping
Figure 5. Top image cropping
同样的技术可以用来左裁剪图像,如图 6 所示。
Figure 6. Left image cropping
上述操作的输出产生了以下图像:
Figure 7. Cropped image
2.2 数字提取
既然检测到了边界框,应该很容易挑出每个数字,因为每个数字都有相对于裁剪图像左上角的预先固定的坐标。
我将上面的代码应用于一组图像,并手动将每个数字的图像分类到标记为 0 到 9 的单独文件夹中,如下所示,以创建我的训练/测试数据集。
Figure 8. Manually labeling datasets
3.特征抽出
特征提取或特征工程是识别输入(在我们的情况下是数字)的独特特征的过程,以使机器学习算法能够工作(在我们的情况下是聚类相似的数字)。特别令人感兴趣的是梯度方向直方图(HOG) ,它已经在许多 OCR 应用中成功用于提取手写文本。下面的代码演示了使用 skimage 的 hog 函数从图像中提取 HOG。
**from** skimage.feature **import** hogdf= hog(training_digit_image, orientations=8, pixels_per_cell=(10,10), cells_per_block=(5, 5))
在我的例子中,图像是 50x50 像素,hog 的输入参数(即 像素 _ 每单元 和 单元 _ 每块 )是凭经验设置的。下图说明了如何在图像上应用 HOG,生成一个包含 200 个值(即特征)的矢量。
Figure 9. Illustration of Histogram of Oriented Gradients (HOG)
4.培养
在前面的步骤中,我们将相似的数字提取到文件夹中,以构建我们的训练数据集。下面的代码演示了如何构建我们的训练/测试数据集。
既然我们已经创建了训练数据集并将其存储到了 features 和 features_label 数组中,那么我们就使用 sklearn 的函数 train_test_split 将训练集划分为训练集和测试集,并使用结果来训练 k-NN 分类器,最后保存模型,如下面的代码所示。
*# store features array into a numpy array* features = np.array(features_list, **'float64'**)*# split the labled dataset into training / test sets* X_train, X_test, y_train, y_test = train_test_split(features, features_label)*# train using K-NN* knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)# get the model accuracy
model_score = knn.score(X_test, y_test)
*# save trained model* joblib.dump(knn, '**/models/knn_model.pkl**')
5.预测
预测新图像上的数字的过程遵循相同的步骤,即挑选出上述训练步骤中所示的数字,然后简单地应用 k-NN 的 预测 函数,如下所示。
knn = joblib.load(**'/models/knn_model.pkl'**)**def** feature_extraction(image):
**return** hog(color.rgb2gray(image), orientations=8, pixels_per_cell=(10, 10), cells_per_block=(5, 5))**def** predict(df):
predict = knn.predict(df.reshape(1,-1))[0]
predict_proba = knn.predict_proba(df.reshape(1,-1))
**return** predict, predict_proba[0][predict]digits = []# load your image from file# extract featuress
hogs = list(map(**lambda** x: feature_extraction(x), digits))# apply k-NN model created in previous
predictions = list(map(**lambda** x: predict(x), hogs))
k-NN 的 预测 函数返回一个介于 0 和 9 之间的单个数字值,以表示输入图像的预测类别。K-NN 的predict _ proba函数返回每个预测类关联的精度。
例如,假设我们对包含数字“5”的图像应用了预测。输出的一个例子是prediction=5 and predict_proba =[[0 0 0 0 0 .8 0 0 .2 0]]
。这意味着 k-NN 以 80%的置信度将图像分类为“5”,以 20%的置信度将图像分类为“8”。
最后,predictions = list(map(lambda x: predict(x), hogs))
产生以下元组向量,其中每个元组表示图像上每个数字的预测类别及其相关预测置信度。任何未以 100%置信度对输入进行分类的预测都将呈现给用户进行手动校正,如下一节所示。
*[
(5, 1.0), (1, 1.0), (9, 1.0), (2, 1.0), (1, 1.0), (2, 1.0), (4,1.0), (7, 1.0), (2, 1.0), (3, 1.0), (4, 1.0), (3, 1.0), (4, 1.0),
**(4, 0.8)**, (0, 1.0)
]*
6。演示文稿
最后一步是在 excel 文件中呈现机器学习模型的结果,如下所示。对于没有 100%准确预测的数字,我在实际预测的下面嵌入了预期数字的图像。这个微小的显示调整减少了用户 80%的时间来修正不准确的预测。此外,这项活动并不令人畏惧,因为它不需要大量的脑力劳动。用户可以在几分钟内滚动文件,并在视觉上将实际结果与预期结果相匹配。许多预测实际上是假阴性的,因此用户不需要做很多修正。
参考书目
哈米德,不适用,& Sjarif,不适用(2017)。基于 SVM、KNN 和神经网络的手写识别。arXiv 预印本 arXiv:1702.00723。
阿德里安·罗斯布鲁克的博客和书籍(【https://www.pyimagesearch.com】T2)。伟大的计算机视觉资源和许多关于数字识别的文章。
帕特尔,I .,贾格塔普,v .,&卡莱,O. (2014) 。手写数字识别的特征提取方法综述。国际计算机应用杂志, 107 期 (12)。
建设方案
在本帖中,我们给出了我们应用程序的一个用例的扩展说明,场景:对一个城市建设任务的劳动力成本和预算建模。(这是我们的第二个扩展示例用例;我们之前已经讨论过使用场景进行会计和管理。)在此过程中,我们将提供示例 Excel 模型,您可以下载并自己运行(如果您有场景)。要下载场景,请点击加入我们的 alpha 计划。
敏感性分析
考虑下面的假设情况。你是一家建筑公司的主管,该公司承包了一栋 15 层的大楼,要价 1000 万美元。固定成本(如设备和材料)占该价格的 500 万美元,在计入劳动力成本之前,收益为 500 万美元。
然而,计算劳动力成本并不那么简单。作为建筑行业中经验丰富的专业人士,你知道预算中写的内容往往与实际情况不尽相同。在规划预算时,您使用了施工经理提供给您的数字。施工经理告诉会计,假设你的 100 名工人,每人每年支付 55,000 美元,每天工作 8 小时,将在 11 天内完成每一层,在 24 周内完成建筑。
更具体地说,他假设完成每层楼总共需要 8,000 个工时,可以分配给多个工人,加上每层楼一天的准备时间。根据这一假设,您的预算指定了 2,493,132 美元的劳动力成本,导致税前收益为 2,506,868 美元,折旧和摊销( EBITDA )。施工经理的假设正确吗?你的利润率取决于此。
如果对完成建筑所需的劳动力数量判断错误,你的公司将付出比劳动力直接成本更多的代价。根据合同条款,你的公司必须在 30 周内完成这座大楼,否则你将面临罚款:超过合同规定的完工时间,每超过一周,你将支付 15 万美元的罚款,从而侵蚀你的利润率。
这些假设以及由此产生的预算在以下电子表格模型中进行了详细说明(请访问我们的网站下载该模型):
上述给计划者和决策者提出的第一个问题叫做敏感性分析。这项工作的利润率对每层楼需要多长时间的假设有多敏感?如果出于某种原因,每个楼层需要 15 天而不是 11 天,这是否会破坏利润率?如果每完成一层楼,建筑的速度就会加快,那么后续的楼层也会很快完成——如果速度变慢了呢?两者都是可行的,取决于项目的类型。
解决这些问题的卓越工具是蒙特卡洛模拟。我们,建模者,通过将某些量指定为未知数,作为假设,来增加上述预算。这就考虑到了施工经理做出的假设可能并不准确的可能性。在这种情况下,未知数是 a)完成一个楼层所需的工时数,b)楼层与楼层之间的工时变化,以及 c)完成每个楼层所需时间的可变性都是未知的。使用场景,将先前的模型改变成概率模型是简单的:简单地用分布替换将要变得随机的单元。同样,请访问我们的网站下载模型:
这个模型增加了三个新的假设。首先,我们假设完成一层楼所需的工时是正态分布,期望值为 8000 小时,标准差为 1000 小时。本质上,这意味着建模者预计该值为 8,000 小时,但也考虑到了低至 5,000 小时、高至 11,000 小时的可能性。
我们的第二个假设与施工进度有关。我们考虑到施工速度可能会随着每层楼的完工而减慢或加快。我们认为最有可能的情况是施工速度不会减慢或加快,但我们考虑到了施工速度减慢或加快的可能性,即每层楼在任一方向上可能会增加多达 900 个工时。我们考虑到了这种可能性,因为我们不知道它会减速还是加速,而且如果它真的减速,正如我们将看到的,这对我们的利润率非常不利。
最后,在上图右下方的公式中,我们进行第三个假设。我们假设每个楼层施工时间的可变性也是不确定的——虽然大多数楼层预计需要大约 8,000 个工时才能完成,但我们不知道同一栋建筑内楼层完成时间的差异有多大。高可变性表明楼层完成时间非常不可预测,而低可变性表明如果第一层需要 8,500 小时,其余楼层可能也需要非常相似的时间。
(注意:这些假设仅利用了打包在场景中的几个概率分布——正态分布和伽玛/厄兰分布。关于场景中包含的发行版的完整列表,请参见场景文档。有关如何解释这些假设和理解由场景执行的分析的更多信息,请参见此处的和此处的。)
通过使用场景为上述模型生成 100,000 个场景,我们可以分析利润率对我们做出的特定假设的敏感度。以下是这些模拟结果的报告:
左边的图表显示了场景运行的 100,000 个模拟中每个模拟产生的 EBITDA。右边的图表只是左边图表的压缩版本,只显示了在每种情况下 EBITDA 是否为正。
因此,在大多数模拟运行中(约 98%),EBITDA 大于零。在其余 2%的场景中,要么是每层楼的完工时间比专家预计的时间长得多,要么是楼层完工时间呈明显上升趋势,导致上层楼的完工时间比下层楼长得多。如果 2%是你和你的公司可以承受的风险水平,敏感性分析已经做出了你的决定——你应该同意这个提议,继续这个项目。
合并实际值
然而,一旦你决定继续这个项目,决策就不会停止。事实上,Invrea 的洞察力是,最重要和最困难的决定还在前面。如果您的团队没有对建筑项目的实际数据做出正确的反应,如果您的团队没有将实际数据纳入决策过程,那么您可能会损失惨重。这就是我们的意思。
决策不是一个静态的过程。通常,商业决策需要根据新数据重新评估。以您的建筑项目为例,考虑一下如果您的公司开始该建筑项目,前四层分别用了 11.5 天、11 天、14.5 天和 15.8 天,您会处于什么位置。
这些结果使你陷入困境。这些最低完工时间是否代表一种明显的上升趋势,从而导致完工时间的显著延迟?或者这仅仅是由于不可预见的因素,使得第三层和第四层的时间比预期的要长?如果有明显的上升趋势,那么由此导致的延误可能会使你的劳动力规模增加一倍,以便在合同规定的时间内完成项目,这样你仍然可以盈利。然而,如果这些数据点只是异常值(T1)和 T2(T3 ),而不是趋势的指示,那么将员工数量增加一倍就是浪费,会侵蚀你的利润率。
具体来说,您必须解决的问题是,在构建模型的过程中,我们做了两个相互有些矛盾的假设。我们对楼层间的工时变化做了一个假设,指出这种幅度的上升趋势是完全可能的。我们对每层建筑时间的可变性做了另一个假设,假设这种程度的异常值是完全合理的,并且可能与恒定的楼层完工时间一致。我们有非常少量的数据。你如何权衡这两种假设,以便最好地解释观察到的数据,然后使用结果来通知并可能重新评估你的决定?这正是场景的核心独有技术概率编程新技术所要解决的问题。
在场景中,有一个特殊的构造告诉模型它必须以这种方式从新数据中学习。我们称之为模型必须从实际值中学习的数据点。向模型添加实际数据点就像右击单元格并按下“记录实际数据”一样简单:
通过按下该对话框中的“记录”,用户告诉场景,他们只对观看第四层需要 15.8 天完成的场景感兴趣。(这个过程可以很容易地对前面的三层楼重复;或者访问我们的网站下载生成的模型。)场景以一种在商业应用中从未见过的方式使用这些实际数据点:同时验证和改进我们建模者输入的假设。 Scenarios 然后根据新的数据和修改后的假设产生新的预测和建议。(关于场景如何解决这些问题的深入讨论,见这里和这里。)
因此,使用场景,我们又生成了 500,000 个场景,这次考虑了前四层的结果。我们发现我们的预测发生了重大转变:
考虑到前四层的完工时间,EBITDA 小于零的几率超过 35%。所以,为了保证公司在这个项目上不赔钱,我们必须雇用更多的工人。情景告诉我们,根据新数据,我们关于楼层完工时间的最初假设是错误的,并将其修正为更准确的假设。
不使用场景就无法得到这种分析。如果您不是按“记录实际”而是简单地将观察到的楼层完成时间输入到相应的单元格中,我们就不会修改我们的初始假设,我们仍然会在一个错误的模型上操作,我们将有超过 35%的机会损失数十万美元。要了解这一点,请从我们的网站下载模型并运行场景:
当被指示从数据中学习时,场景使用这些数据来检查其用户输入的假设是否正确,改进这些假设,然后使用这些改进的假设来建立对未来的改进预测。在这种情况下,it 部门了解到,实际上建造楼层需要将近 9,000 个工时(而不是我们最初假设的 8,000 个工时),而且很有可能会导致楼层越高,建造时间越长。
为了检查场景对构建过程了解了什么,比较下面两个直方图。左边的是我们对楼层间工时趋势的最初假设,以零为中心,右边的是场景推断的分布,以更好地代表实际发生的数据:
因此,这些实际情况——楼层完工时间——迫使你重新考虑你未来的假设。作为对这些新数据的反应,你应该改变你的决定——增加劳动力的规模,这样这个项目最终将在 30 周内完成。然而,你不想增加太多的劳动力,因为这将在劳动力成本上浪费金钱。那么,你如何选择何时增加员工数量,以及增加多少?
场景,使用上面的模型,也可以解决这个问题。只需改变模型中使用的劳动者数量,保留前四层的结果来训练模型,有关 EBITDA 的预测就会更新。例如,假设您正在考虑在建造 10 至 15 层时将员工人数增加一倍。下图显示了 EBITDA 为正的概率,如果您决定按照此方案增加员工人数:
仅在后几层就将员工人数增加了一倍,这大大降低了因罚款而超出预算的风险。因此,尽管劳动力成本增加,但将 10 至 15 层的劳动力规模增加一倍是一个好主意。
场景的应用绝不仅限于建筑。如果你面临一个不确定性很高且数据可用的决策问题,考虑使用场景来为你的分析提供信息。决策和数据都不是静态的,在几乎所有情况下,最重要的数据都是最新的数据。一旦新的数据出现,预测就会改变。场景可以自动为你做到这一点。在这里加入阿尔法计划。
原载于invrea.com。
使用云功能和云调度程序安排数据接收
随着谷歌云的不断发展,我在《谷歌云平台上的数据科学》一书中提出的一些解决方案被取代了,因为更简单、更强大的解决方案变得可用。
There is now a better way to do periodic ingest than the method I suggested in Chapter 2 of this book.
例如,几个月前,我展示了如何仅使用 SQL 构建回归和分类模型。无需移动数据即可实现高度可扩展的机器学习,这意味着我们可以非常轻松地探索机器学习的价值。如果我今天写这本书,我会在第 5 章(交互式数据探索)中插入一个关于 BigQuery ML 的部分。
在本文中,我将讨论第二个更新:一个比我在第 2 章最后一节中介绍的更好的定期数据接收方法。
老办法:使用 AppEngine Cron 每月更新
在本书第二章的最后一节,我提出了一个安排每月下载的解决方案。这包括五个步骤:
- 在 Python 中摄取
- Flask webapp
- 在 AppEngine 上运行
- 保护 URL
- 调度 Cron 任务
The solution for periodic ingest presented in the book can be greatly simplified now.
第一步已经在书中完成了,我可以简单地重用那个 Python 程序。剩下的步骤变得更容易了。简单多了。
使用云功能摄取
有一种更简单的方法来创建可通过 Http 访问的 web 端点,而不是构建 Flask web 应用程序并在 AppEngine 中运行它。新的方法是使用云功能。我可以使用用于摄取的相同 Python 代码,但是将它包装在一个名为 main.py 的文件中(本文中的所有代码都在 GitHub 上):
import logging
from flask import escape
from ingest_flights import *
def ingest_flights(request):
try:
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO)
json = request.get_json()
year = escape(json['year']) if 'year' in json else None
month = escape(json['month']) if 'month' in json else None
bucket = escape(json['bucket']) # requiredif year is None or month is None or len(year) == 0 or len(month) == 0:
year, month = next_month(bucket)
logging.debug('Ingesting year={} month={}'.format(year, month))
gcsfile = ingest(year, month, bucket)
logging.info('Success ... ingested to {}'.format(gcsfile))
except DataUnavailable as e:
logging.info('Try again later: {}'.format(e.message))To create
本质上,我的 main.py 有一个接收 Flask 请求对象的函数,从中我可以提取 HTTP Post 的 JSON 有效负载,云函数将通过它被触发。
我通过查看桶中已经有哪些月份,然后使用 ingest_flights.py 中的现有代码获取必要的数据,从而获得下一个月。
一旦我写好了 main.py,就可以通过 gcloud 部署云功能了:
gcloud functions deploy ingest_flights \
--runtime python37 --trigger-http --timeout 480s
我们可以通过向 Cloud 函数发送 curl 请求来测试它:
REGION='us-central1'
PROJECT=$(gcloud config get-value project)
BUCKET=cloud-training-demos-mlecho {\"year\":\"2015\"\,\"month\":\"03\"\,\"bucket\":\"${BUCKET}\"} > /tmp/message curl -X POST "[https://${REGION}-${PROJECT}.cloudfunctions.net/ingest_flights](https://${REGION}-${PROJECT}.cloudfunctions.net/ingest_flights)" -H "Content-Type:application/json" --data-binary @/tmp/message
保护云功能
正如上面的代码所示,云函数的 URL 是完全开放的。为了在某种程度上保护 URL 免受拒绝服务攻击,我们应该将 URL 改为不可访问的。
要使 URL 不可访问,请使用 openssl 库生成一个 48 个字符的字符串,删除非字母数字字符,并将结果精简为 32 个字符:
URL=ingest_flights_$(openssl rand -base64 48 | tr -d /=+ | cut -c -32)
echo $URLgcloud functions deploy $URL --runtime python37 --trigger-http --timeout 480s
这本身是不够的。我们还应该坚持让合法的调用者向我们提供一个令牌作为有效负载的一部分。同样,我们可以使用 openssl 程序生成一个令牌,并将支票添加到 main.py:
if escape(json['token']) != 'DI8TWPzTedNF0b3B8meFPxXSWw6m3bKG':
logging.info('Ignoring request without valid token')
return
做这两件事——一个不可访问的 URL 和检查云函数中的令牌——有助于保护云函数。
使用云调度程序调度摄取
现在,云功能提供了一个启动接收作业的 http 端点,我们可以使用云调度程序每月访问该端点一次:
gcloud beta scheduler jobs create http monthlyupdate \
--schedule="8 of month 10:00" \
--uri=$URL \
--max-backoff=7d \
--max-retry-attempts=5 \
--max-retry-duration=3h \
--min-backoff=1h \
--time-zone="US/Eastern" \
--message-body-from-file=/tmp/message
调度器采用多种格式,包括 Unix 的 crontab 格式,但是我发现 AppEngine 的 cron 支持的简单语言格式可读性最好。因此,我们的端点将在每月 8 日美国东部时间上午 10 点被访问。
使它不那么单一
如果你看看 ingest_flights.py,ingest 方法做了相当多的事情。它下载文件,解压缩文件,清理文件,转换文件,然后将清理后的文件上传到云存储。
既然我们正在使用云函数,那么重新设计使其不那么单一可能会更好。除了通过 http 调用触发之外,云功能还可以通过向桶中添加文件来触发。
因此,我们可以让第一个云函数简单地将解压缩后的文件上传到云存储,然后让第二个云函数执行提取-转换-加载(ETL)部分。这可能更容易维护,尤其是如果事实证明我们在 ETL 部分有一个 bug。原始数据可用于重新运行 ETL 作业。
后续步骤
- 查看 GitHub 上的代码
- 看书:谷歌云平台上的数据科学
轻松调度:Python 成本优化教程
了解如何使用 Python 中的线性编程快速解决优化问题
介绍
语境
恭喜你!你是镇上最酷商店的骄傲的新主人。为了保持运营,您需要确保为每个班次安排了正确数量的工人。在本教程中,我们将为即将到来的一周设计最低成本的时间表。
考虑
在接下来的一周里,每天有两次 8 小时的轮班。您目前有十名员工,其中四名被视为经理。对于一周内超过 40 小时的任何班次(总共 5 个班次),您都要向员工支付加班费。为了对你的员工公平,你决定每个人至少要工作 3 班,但不能超过 7 班。而且为了保证店铺顺利运转,每个班次至少需要一个经理。
构建问题
在深入研究代码之前,让我们通过定义目标、变量和约束来为我们的任务添加结构。
目标函数
简而言之,我们要设计最低成本的时间表,既考虑常规时间,也考虑加班时间。我们可以从数学上将其定义为:
其中 w 是我们的工人列表,RegCost
和OTCost
分别是每个工人正常和加班班次的美元成本,RegShifts
和OTShifts
分别是每个工人正常和加班班次的总数。
变量
我们将为每个工人/班次组合创建一个变量列表(例如['雇员 1 ',‘星期一 1’],['雇员 2 ',‘星期一 1’],等等)。).这些变量中的每一个都将是一个二进制值,以表示一个工人是否被调度(1)或不被调度(0)。我们还需要处理常规时间和加班时间的划分,我们将把它作为变量和约束的混合来处理。
限制
从上面的问题陈述中,我们知道我们需要遵循一些特殊的注意事项。为了确保我们的优化计划是可接受的,我们将创建特定的约束条件:
- 配备的工人总数等于每班所需的工人总数
- 员工必须保持在全球最小和最大轮班数之间
- 只能在工作人员可用时安排他们(在决策变量“x”中处理)
- 每班至少配备一名经理
用 Python 创建我们的模型
准备数据
在进入优化模型之前,我们需要一些(说明性的)数据来处理。由于将数据加载到 Python 超出了本教程的范围,我们将快速浏览这一部分。
以下是我们现在所掌握的情况:
- 我们的 14 个班次(一周内每天两班)和 10 名员工的列表(第 7-9 行)
- 每班需要的工人数量(第 12-13 行)
- 每个班次每个工人的可用性(第 17-23 行)
- 一个是经理的列表和一个不是经理的列表(第 26-27 行)
- 每个工人的轮班成本,包括正式工和加班工(第 31-36 行)
- 一些关于最小和最大班次以及在触发加班之前允许多少班次的全局假设(第 40-43 行)
初始化模型
注:从上面的代码可以看出,我们使用的是一个名为guro bi的包。Gurobi 是一个优化求解器,可用于许多编程语言。虽然 Gurobi 的完整版本需要商业许可证,但您可以获得学术或在线课程许可证来免费运行有限版本。
我们首先需要创建模型的外壳。我们使用以下代码来实现这一点:
model = Model(“Workers Scheduling”)
添加决策变量
让我们将结构化变量转化为代码:
首先,我们需要为每个工人/班次组合创建二进制变量。我们可以用 Gurobi 的addVars
函数来做到这一点(注:如果只添加一个变量,用 addVar
代替)。我们指定变量是二进制的,我们还读入了之前创建的作为ub
(“上限”)的avail
字典。每个古罗比变量都有一个上限和下限。因为我们使用二进制变量,自然我们的变量必须等于 0 或 1。通过将上限设置为等于avail
中的值,我们能够嵌入特定工人/班次组合必须等于 0 的约束(即当该工人不可用时)。
接下来,我们必须创建变量来处理正常工作时间和加班时间。如前所述,我们将把这种分割作为变量和约束的组合来处理。现在,我们只是为每个工人创建变量,没有进一步的说明。一个例外是,我们将overtimeTrigger
设置为一个二进制变量(当给定的工人本周没有加班时为 0,当有加班时为 1)。
添加约束
类似地,让我们使用addConstrs
(一次添加多个约束)和addConstr
(一次添加一个约束)函数,将上面概述的每个约束转化为代码。
首先,我们指定每个班次分配的工人总数(每个计划工人 1,每个非计划工人 0)等于总班次需求:
接下来,我们处理正常时间和加班时间的划分。为了准确地捕捉到这一点,我们采取了保守的方法。首先,我们指定常规班次的数量加上加班班次的数量等于每个工人的总班次数量。然后,我们确保常规班次的数量小于或等于指定为加班触发器的班次数量。我们这样做是为了确保在加班之前考虑常规班次。为了更进一步,我们添加了最后一个约束条件,即如果一个工人的正常轮班次数小于 5 ( OTTrigger
),那么加班的二进制触发器将被设置为 0。
有了这个,我们就可以完成最后的约束了。与上面类似,我们计算每个工人分配的总班次数。我们指定这必须大于或等于最小班次的全局输入,并且小于或等于全局最大班次。最后,我们处理每个班次至少需要一名经理的需求。
定义目标函数
我们的目标是最小化计划工人的总成本。我们可以很简单地通过定义一个成本函数来处理这个问题,该函数将正常班次总数乘以每个工人正常班次的成本,以及加班班次总数乘以每个工人加班班次的成本相加。我们告诉 Gurobi,目标是使用ModelSense
来最小化这种情况。最后,我们使用setObjective
来指定Cost
是目标函数。
运行优化
在运行优化之前,检查模型可能会有所帮助。一个很好的方法是:
有了这段代码,你将能够看到目标函数、变量、约束等。列为公式,这对于确保代码产生您想要的功能特别有帮助。
在您对模型满意之后,我们可以用一行简单的代码来解决优化问题:
model.optimize()
输出
optimize
函数产生了一个非常有用的输出,但是并没有给我们太多的工作。在接下来的步骤中,我们将从模型中提取更有意义的信息。
首先,我们想知道计划的总成本。通过运行以下命令,我们发现成本为 7535 美元:
print('Total cost = $' + str(model.ObjVal))
现在,让我们使用以下内容来查看时间表的控制面板:
Output of the dashboard
最后,让我们创建仪表板的另一个视图,只需打印出分配到每个班次的每个员工的姓名:
Output of the shift assignments
结论
通过本教程,我们使用 Python 制作了一个优化问题的端到端解决方案。如果这激起了你的兴趣,你可以自己举个例子。尝试处理连续决策变量、多目标问题、二次优化、不可行模型——可能性是无限的。如果你对创建自己的优化算法感兴趣,可以看看我的关于使用 Python 构建遗传算法的教程。
结束注释
你可以在这里找到一个合并的笔记本。
参考资料:
- 【https://en.wikipedia.org/wiki/Gurobi
- https://www . science direct . com/science/article/pii/s 111001681730282 x
- http://www . guro bi . com/documentation/8.0/examples/work force 5 _ py . html
学校假期、机器学习和正在学习“看”的机器人
去年九月学校放假时,Coding Kids 带着一群学生参观了位于布里斯班花园点校区 QUT 的澳大利亚机器人视觉中心。该中心专门研究和开发学习“看”的机器人。他们的视觉能力是机器人在社会中无处不在部署的剩余技术障碍…
澳大利亚机器人视觉中心在克服这一障碍方面发挥着关键作用。该中心正在开发基础科学和技术,使机器人能够看到、理解它们的环境,并在我们生活和工作的复杂、无组织和动态变化的环境中执行有用的任务。
在参观研究设施的过程中,我们了解到机器人很难像人类一样“看到”和识别图像,并且在这一领域正在进行大量研究,以便改进我们使用技术的方式。如果机器人能学会像人类一样看东西,机器人帮助我们和我们社区的潜力是无穷无尽的。机器人和机器只懂 1 和 0,那么机器人学家和科学家是如何教和训练机器人看图像的呢?
正在开发的机器人原型之一是 Guiabot,这是一种正在学习“看到”周围环境的自动驾驶汽车。与人类不同,机器人更难确定两幅略有不同的图像是否位于同一位置。学生们和一个机器人玩了一个游戏,以测试他们在识别两个图像是否在同一位置上的能力。一个学生试着做了这个游戏,得到了 10/10 的分数。机器人同一场比赛的得分只有 9/10。这表明,对于人类来说,识别两张照片是否属于同一地点要容易得多。尽管技术在进步,但机器人仍然不能像人类一样通过视觉识别自己的位置。
我们了解到机器人正在使用一种叫做机器学习的过程来学习“看”。1959 年,阿瑟·塞缪尔(Arthur Samuel)将机器学习定义为“在没有明确编程的情况下,赋予计算机学习能力的研究领域”。
这是机器人视觉公司的三个机器人,它们正在使用机器学习来学习“看”:
- COTSbot 是一个正在学习“看见”和识别大堡礁棘冠海星(COTS)的机器人。COTSbot 的设计是为了控制 COTS 的过剩,这是破坏大堡礁。
- QUT 的 Harvey the harvester(基于 UR5 手臂)是一个辣椒采摘机器人。它是学习“看见”和识别成熟的辣椒,准备采摘。澳大利亚 30%的作物都被浪费了,原因是在一年中需要收割作物的确切时间缺少工人。
- QUT 的 AgBot 是一个 3 米宽的杂草清除机器人,可以识别需要清除的棉花和杂草。
这三个机器人正在学习识别一个特定的物体,以便它能为我们做有用的工作。学习和训练机器人的方法被称为机器学习。机器学习基于一套复杂的算法,计算机,或者在这种情况下,机器人,使用已知数据进行学习和预测。
机器人首先通过学习识别图像中的目标物体,如棉花、辣椒和杂草,开始它们的学习之旅。然后,机器人开始学习识别目标物体的 3D 打印版本。在成功完成这一级别的训练后,机器人开始训练识别现实生活中的物体,例如大堡礁中的帆布床、果园中成熟的辣椒和土壤中的杂草。在这个级别的训练中,机器人学会区分 3D 打印版本和真实的目标对象。
澳大利亚机器人视觉卓越中心专注于机器人“看”的研究和开发,并在克服机器人在社会中无处不在部署的最后一个技术障碍方面发挥了关键作用。机器人改善我们生活和环境的潜力是无限的。这是一个有趣的研究领域,我们学校团体对该中心的访问是一个让孩子们接触科技奇迹的好机会。正如孩子们在学习一样,机器人也在学习。
点击我们的脸书页面了解我们最新的儿童免费科技活动。
在下面的视频中观看机器人 Nao 跳江南 style:
文章最初发表在这里。
科学数据分析管道和再现性
Photo by Campaign Creators on Unsplash
管道是做什么的?我们为什么需要它们?
管道是方便的计算工具。数据分析通常需要数据采集、质量检查、清理、探索性分析和假设驱动分析。管道可以自动完成这些步骤。他们将原始数据处理成合适的格式,并以简化的方式用统计工具或机器学习模型进行分析。实际上,数据分析管道执行一系列命令行工具和定制脚本。这通常提供经过处理的数据集和人类可读的报告,涵盖诸如数据质量、探索性分析等主题。
在我们的领域中,原始数据是包含测序读数的文本文件。读数有一个 4 个字母的代码(ACGT ),它们来自基因组的特定位置。我们需要对读数进行质量检查,将它们与基因组对齐,量化它们,并对它们运行统计/机器学习模型。不同的命令行工具和定制脚本必须按顺序运行才能完成这些任务。如果质量检查或校准出现问题,部分或所有步骤需要使用不同的参数重新运行,具体取决于使用数据观察到的问题的性质。我们可能需要运行数百次,因此通过管道至少自动化部分任务是有益的。
什么是再现性?为什么重要?
当您不得不重复处理一个带有一些参数变化的数据集或处理多个数据集时,管道会有很大帮助。由于基本的数据处理和分析任务可能需要大量的实际操作时间,因此自动化这些任务的某些部分可以节省时间。然后,研究人员可以将更多时间花在可视化、结果交流或定制的统计/机器学习分析上。由于这种便利,许多研究人员正在创建管道,并通过出版物与社区共享。通常,当您共享管道时,您会希望确保在为其他用户提供相同的输入数据时,您的管道会产生相同的输出。如何使用创建者使用的完全相同的依赖项安装完全相同的管道,并确保它产生相同的输出?虽然这听起来是一个微不足道的问题,但关于“科学中的再现性危机的报道表明,实现这一点并不容易。其他研究人员一再未能重现已发表的实验。这种“再现性危机”并不局限于生物学或心理学等领域。计算领域也遭受这种。
对于可再现的数据分析,有几个标准。
数据和元数据可用性: 数据和元数据应该毫无疑问地可用。没有这些,就没有办法重现一个分析。在我们的研究领域,数据和元数据通常在发布后存放在公共数据库中。
透明: 你所使用的代码以及运行代码所需的依赖关系应该是完全透明的。这也延伸到依赖项的源代码可用性。不希望有一个工具的行为主要依赖于专有的二进制 blob /黑盒。此外,您需要知道依赖项的确切版本和配置,以便有机会重现数据分析管道。优选地,安装过程跟踪不同的依赖结构并安装你需要的所有东西,参见下面的要点。
易于安装(可安装性): 计算分析工具和管道应该努力做到易于安装。我认为,如果管道有许多必须单独安装的依赖项,我们中的许多人都会望而却步。即使我们得到承诺,在我们努力完成每个依赖项的安装后,会得到一个可以复制作者版本的工作管道,这种情况仍然会存在。管道的依赖项越多,就越有可能至少有一个在安装过程中出现问题。很多已发布的科学软件无法安装。研究声称至少有 50%的已发布软件是不可安装的【参见这里 & 这里】。我怀疑对于管道来说,情况更糟,因为许多管道都有更复杂的依赖关系。浏览过糟糕的自述文件并试图安装所有依赖项的人非常清楚为什么“易于安装”很重要。
运行时环境再现性: 安装的软件应该在每台机器上表现相同,也就是说我们需要在每台机器上安装完全相同的软件。实现这一点并不简单,因为软件依赖于许多不同的东西,从编译器到系统库,再到第三方软件和所需的库。如果您想在不同的机器上以完全相同的方式构建软件并获得相同的软件,您需要控制这个复杂的依赖系统。依赖项的版本及其构建方式会对您尝试安装的软件产生影响。例如,如果您尝试安装的软件需要 Boost C++库,则 Boost 1.68 版与 1.38 版相比可能会有所不同。可能会有错误修复或改进,可能会改变我们试图安装的软件的行为。因此,由于依赖关系的差异,即使在两台不同的计算机上安装了相同的版本,该软件也会有不同的行为。
如果您可以安装完全相同的软件,以完全相同的方式构建,并对编译器有完全相同的依赖关系,您就有很好的机会在不同的机器上重现运行时环境,从而使用相同的输入数据进行分析。唯一的例外是,如果软件有一些你不能控制的随机成分,那么就不可能重现分析。例如,k-means 聚类算法可能根据随机初始化过程每次产生不同的聚类结果。如果我们不能通过设置随机种子来控制这种行为,我们就无法重现结果。
Essential ingredients of data analysis reproducibility. Reproducibility requires availability of data and being able to use the same exact software.
计算分析的再现性光谱
科学家们的主要目标是尽可能快速准确地分析数据,因此任何为我们自己或其他潜在用户的未来版本做好事的努力似乎都是错误的。对于许多科学期刊来说,把你的代码放在 github 上就足够了,这只是最近一些期刊的要求。我们和许多其他人认为这并不总是足够的。
实际上,当我们想使用一个已发布的数据分析工具时,我们许多人的想法是:1)“我能安装已发布的软件并得到类似的结果吗?2)“我也可以在我的研究中使用这个吗?".在这一点上,我们只是想使用软件,不会试图在出版物中重现分析,除非它是测试用例或例子的一部分。因此,许多研究人员对再现性的理解是与可安装性联系在一起的。
然而,如果我们真的关心可重复性,正确的问题应该是“如果我可以安装它,我可以用相同的输入数据得到与发表的论文相同的结果吗?”。更普遍但相关的问题是“当我在不同的系统上安装软件时,我可以用相同的输入数据得到相同的结果吗?”。我认为正面回答这些问题需要严格控制数据分析管道的依赖性。提供管道的人应该复制他们的软件环境,并以虚拟机或容器的形式提供。或者,他们应该确保当用户安装管道时,他们获得了相同版本的依赖项。每个依赖项都应该以相同的方式构建,并且在您的机器和用户的机器上安装的软件应该是完全相同的。不严格控制依赖关系将导致软件行为不同。这些行为差异可能会改变分析结果。
所有这些不同的观点或对再现性的无知造成了从“不可再现”到“黄金标准”的再现性范围。虽然许多人认为在线提供源代码就足够了,但其他人需要满足以上提到的更多标准。
Reproducibility spectrum observed in publications. Sharing data and code is seen as enough to reproduce the data analysis by many. However, this is not enough.
拯救集装箱
处理运行时环境的可安装性和可复制性的最流行的方法之一是使用容器,这是一种轻量级的虚拟机。我们领域中最流行的容器是 docker 和最近的 singularity 容器。使用这种方法,管道和管道本身的复杂依赖关系可以被“容器化”。这意味着,如果您能够设法在这个轻量级虚拟机中安装您的管道和依赖项,您就可以运送容器,并确保使用该容器的任何人都能够复制您的软件环境。不同操作系统的结果应该是相同的。另一种方法是以容器的形式提供管道的依赖关系。在此设置中,您在管道中使用的每个工具都将是一个容器。有一些管道框架可以利用这一点,但我发现这种方法有些不切实际。
安装和运行时复制的便利性是以降低透明度和安全性为代价的。很难确切核实集装箱里装的是什么。Docker 建议使用 docker 文件,但是它们不一定具有容器中的软件版本。它主要是从软件包管理器安装软件的命令集合。如果不专门在 dockerized 环境中分析数据和开发代码,容器也更难维护。如果没有,在创建管道后,您必须将其容器化,并检查是否有相同的结果,这是额外的工作。人们努力使容器更加安全、透明和易于使用,但是它们没有被社区广泛采用。
包装经理和再现性
提供可安装性和依赖性管理的另一种方式是使用包管理器。您可以使用软件包管理器将您的管道构建为真正的软件和软件包,或者从软件包管理器提供要安装的软件包列表。
不管你选择哪种途径,一个非常流行的方法是使用 Conda 包管理器。它非常容易使用,不需要 root 访问权限,你可以获得预编译的二进制文件,这大大减少了安装时间,并且可以在 windows、mac OS 和 linux 上工作(大部分时间)。给康达打包软件也相对容易一些。此外,康达还有大量的贡献者和维护者。这些功能旨在最大化可安装性。然而,Conda 包根本不可复制。您可以在不同的时间获得相同名称+版本查询的不同二进制文件,并且没有办法跟踪哪些依赖项的源文件生成了该二进制文件。构建软件的系统环境不是孤立的。在构建期间,进程可以访问不在软件包配方中的其他库,并且 conda 还假设某些低级软件包在所有环境中都可用。它们的存在或它们的版本会影响构建,并在不同的系统上创建不同的软件。Bioconda 试图通过在容器中建造来规避这些问题。
虽然包管理器通常提供可安装性,但是很少有人关心构建的可重复性。如果您可以确保安装的软件在不同的系统上是完全相同的,那么您就可以获得运行时再现性,并在相同的输入数据下获得相同的结果(减去“运行时环境再现性”小节中提到的警告)。 GNU Guix 包管理器通过严格管理和跟踪要构建的软件的完整依赖图来实现这一点。它减少了对构建和安装软件的系统环境的假设。因为它知道所有的依赖项,所以它可以在一个隔离的环境中构建它们。这个过程的结果通常是在不同的机器上安装后逐位相同的文件(见此处对不同机器上构建的各种生物信息学软件逐位相同性的评估)。它还提供开箱即用的容器化。您可以为您的管道及其所有依赖项制作 docker 和 singularity 容器。Guix 构建的容器包含了管道所需要的依赖——不多也不少。考虑到它提供的可再现性和健壮性,我们选择使用 Guix 来管理我们的管道的依赖性。
然而,Guix 本身只适用于 linux,并且需要一个根 dameon 来进行构建隔离。打包软件不像 conda 那么容易,尽管 R 和 Python 的导入程序为来自那些框架的包做了几乎所有的工作。对于其他软件,可能会更复杂。此外,用户可能会发现很难管理 Guix 的灵活性。你可以拥有同一个软件的不同版本,你可以回滚到旧版本,但是管理那个可能会令人困惑。
管道框架对再现性的影响
管道框架对再现性几乎没有影响。再现性的主要挑战是提供运行时环境的可安装性和再现性。管道框架提供了一种结构化的方式来将工具/脚本管道化在一起,并提供了额外的功能,例如简单的并行化。snakeMake、Rufus、nextflow 等管道框架在此回顾不同的优缺点。
重申一下,框架的选择不是重现分析管道的主要问题。对于我们自己的目的由于 snakeMake 在生物信息学社区的广泛使用,我们选择了它,但是我们可以很容易地切换到其他东西,并且仍然是可复制的。
离别的思念
如果科学家们希望他们的工具或方法被更广泛的受众使用,那么让数据分析管道具有可重复性应该是他们的目标。快速和肮脏的方法是使用容器。然而,这对于维护或未来升级来说并不容易。Needles 说,部署分析管道不是一种透明的方式,研究需要最大的透明度。更明智的方法是将管道打包到包管理器中。Conda 软件包管理器易于使用,但永远无法完全重现。但是它的易用性和对生物信息学的大量社区支持使它成为许多人的首选工具。通过 Guix 包管理器,可以获得更多的可复制性,这是其他包管理器所不能提供的。它通过设计和源代码到二进制代码的透明性提供了软件构建的可再现性。像其他一些包管理器一样,Guix 附带了项目/用户特定的软件概要文件,其中您可以拥有相同软件的不同版本,而不会有任何冲突。您还可以毫不费力地构建 docker 和 singularity 容器。共享一个 Guix 清单文件和你的脚本就足以重现你的分析的软件方面。
实际上,所有选项(Conda、containers 和 Guix)都比仅仅共享代码和文档要好得多。但在我看来,Guix 更接近再现性的黄金标准,包括透明性、易于安装和运行时环境再现性。
Comparison of different ways of deploying pipelines with respect to criteria for reproducibility. Containers and Guix have medium installability because they require a framework to be installed first before users can use it, this first step most often needs root access. Conda does not keep track of full dependency graphs and makes assumptions about your system that’s why it doesn’t have high transparency or reproducible runtime environment. Containers have low transparency because it is hard to tell what is exactly in them even with dockerfiles present.
sci kit-了解亚马逊美食评论的文本分析
Photo credit: Pixabay
(本文首发于 DataScience+ )
我们知道亚马逊产品评论对商家很重要,因为这些评论对我们如何做出购买决定有着巨大的影响。所以,我从 Kaggle 下载了一个亚马逊美食评论数据集,这个数据集最初来自 SNAP ,看看我能从这个庞大的数据集里学到什么。
我们在这里的目的不是掌握 Scikit-Learn,而是探索单个 csv 文件上的一些主要 Scikit-Learn 工具:通过分析截至 2012 年 10 月(包括 10 月)的一组文本文档(568,454 篇食品评论)。让我们开始吧。
数据
查看数据帧的头部,我们可以看到它包含以下信息:
- 产品 Id
- 用户标识
- ProfileName
- 帮助分子
- 有用性分母
- 得分
- 时间
- 摘要
- 文本
import pandas as pd
import numpy as np
df = pd.read_csv('Reviews.csv')
df.head()
Figure 1
就我们今天的目的而言,我们将重点关注分数和文本列。
让我们从清理数据框开始,删除任何缺少值的行。
分数列的范围是从 1 到 5,我们将删除所有等于 3 的分数,因为我们假设这些分数是中性的,没有为我们提供任何有用的信息。然后,我们添加了一个名为“积极性”的新列,其中任何高于 3 的分数都被编码为 1,表明它得到了积极的评价。否则,它将被编码为 0,表明它是负面评价。
df.dropna(inplace=True)
df[df['Score'] != 3]
df['Positivity'] = np.where(df['Score'] > 3, 1, 0)
df.head()
Figure 2
看起来不错。
现在,让我们使用“Text”和“Positivity”列将数据分成随机的训练和测试子集,然后打印出第一个条目和训练集的形状。
from sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split(df['Text'], df['Positivity'], random_state = 0)print('X_train first entry: \n\n', X_train[0])
print('\n\nX_train shape: ', X_train.shape)from sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split(df['Text'], df['Positivity'], random_state = 0)print('X_train first entry: \n\n', X_train[0])
print('\n\nX_train shape: ', X_train.shape)
X_train 第一条目:
我买过几款活力狗粮罐头,发现质量都很好。这种产品看起来更像炖肉,而不是加工过的肉,而且闻起来更香。我的拉布拉多是挑剔的,她比大多数人更欣赏这个产品。
X_train 形状:(26377,)
查看 X_train,我们可以看到我们收集了超过 26000 条评论或文档。为了对文本文档执行机器学习,我们首先需要将这些文本内容转换为 Scikit-Learn 可以使用的数字特征向量。
词汇袋
最简单和最直观的方法是“单词袋”表示法,它忽略了结构,只计算每个单词出现的频率。CountVectorizer 允许我们使用单词袋方法,将一组文本文档转换成一个令牌计数矩阵。
我们实例化 CountVectorizer 并使其适合我们的训练数据,将我们的文本文档集合转换为令牌计数矩阵。
from sklearn.feature_extraction.text import CountVectorizervect = CountVectorizer().fit(X_train)
vect
count vectorizer(analyzer = ’ word ',binary=False,decode_error='strict ',dtype =<class ’ numpy . int 64 '>,encoding='utf-8 ',input='content ',lowercase=True,max_df=1.0,max_features=None,min_df=1,ngram_range=(1,1),preprocessor=None,stop_words=None,strip_accents=None,token_pattern= '(?u)\b\w\w+\b ',tokenizer=None,vocabulary = None)
这个模型有许多参数,但是默认值对于我们的目的来说是相当合理的。
默认配置通过提取至少两个字母或数字的单词(由单词边界分隔)来标记字符串,然后将所有内容转换为小写,并使用这些标记构建词汇表。我们可以通过使用 get_feature_names 方法来获取一些词汇表,如下所示:
vect.get_feature_names()[::2000]
['00 ‘,’ anyonr ‘,‘漂白’,‘矮胖’,‘战败’,’ er ',‘贾尼尼’,‘印象’,‘小’,‘项链’,‘宠物’,‘缩减’,‘衬衫’,‘夜宵’]
看着这些词汇,我们可以对它们的内容有一个小小的了解。通过检查 get_feature_names 的长度,我们可以看到我们正在处理 29990 个特性。
len(vect.get_feature_names())
29990
接下来,我们将 X_train 中的文档转换成一个文档术语矩阵,它给出了 X_train 的单词包表示。结果存储在一个 SciPy 稀疏矩阵中,其中每一行对应一个文档,每一列是来自我们训练词汇表的一个单词。
X_train_vectorized = vect.transform(X_train)
X_train_vectorized
< 26377x29990 以压缩稀疏行格式存储了 1406227 个元素的“<类“numpy . int 64”>”稀疏矩阵>
列的这种解释可以按如下方式检索:
X_train_vectorized.toarray()
array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], dtype=int64)
该矩阵中的条目是每个单词在每个文档中出现的次数。因为词汇表中的单词数量比单个文本中可能出现的单词数量大得多,所以这个矩阵的大多数条目都是零。
逻辑回归
现在,我们将基于这个特征矩阵 *X_ train_ vectorized,*来训练逻辑回归分类器,因为逻辑回归对于高维稀疏数据工作得很好。
from sklearn.linear_model import LogisticRegressionmodel = LogisticRegression()
model.fit(X_train_vectorized, y_train)
LogisticRegression(C=1.0,class_weight=None,dual=False,fit_intercept=True,intercept_scaling=1,max_iter=100,multi_class='ovr ',n_jobs=1,penalty='l2 ',random_state=None,solver='liblinear ',tol=0.0001,verbose=0,warm_start=False)
接下来,我们将使用 X_test 进行预测,并计算曲线得分下的面积。
from sklearn.metrics import roc_auc_scorepredictions = model.predict(vect.transform(X_test))print('AUC: ', roc_auc_score(y_test, predictions))
AUC:0.797745838184
成绩还不错。为了更好地理解我们的模型是如何做出这些预测的,我们可以使用每个特征(一个词)的系数来确定它在积极和消极方面的权重。
feature_names = np.array(vect.get_feature_names())sorted_coef_index = model.coef_[0].argsort()print('Smallest Coefs: **\n{}\n**'.format(feature_names[sorted_coef_index[:10]]))
print('Largest Coefs: **\n{}\n**'.format(feature_names[sorted_coef_index[:-11:-1]]))
最小系数:[‘最差’ ‘失望’ ‘可怕’ ‘糟糕’ ‘还行’ ‘都不是’ ‘耻辱’ ‘不幸’ ‘失望’ ‘恶心’]
最大系数:[‘上钩’ ‘光明’ ‘美味’ ‘惊艳’ ‘怀疑’ ‘担忧’ ‘好吃’ ‘极好’ ‘再订购’ ‘好吃’]
对十个最小和十个最大的系数进行排序,我们可以看到该模型预测了像“最差”、“令人失望”和“可怕”这样的负面评论,以及像“着迷”、“明亮”和“美味”这样的正面评论。
但是,我们的模型可以改进。
TF–IDF 术语权重
在大型文本语料库中,一些单词会经常出现,但很少携带关于文档实际内容的有意义的信息(例如“the”、“a”和“is”)。如果我们将计数数据直接输入到分类器中,那些非常频繁的词将会掩盖那些更罕见但更有趣的词的频率。Tf-idf 允许我们根据术语对文档的重要性来衡量它们。
因此,我们将实例化 TF–IDF 矢量器,并使其适合我们的训练数据。我们指定 min_df = 5,这将从我们的词汇表中删除出现在少于五个文档中的任何单词。
from sklearn.feature_extraction.text import TfidfVectorizervect = TfidfVectorizer(min_df = 5).fit(X_train)
len(vect.get_feature_names())
9680
X_train_vectorized = vect.transform(X_train)model = LogisticRegression()
model.fit(X_train_vectorized, y_train)
predictions = model.predict(vect.transform(X_test))
print('AUC: ', roc_auc_score(y_test, predictions))
AUC:0.759768072872
因此,尽管我们能够将特征的数量从 29990 减少到仅仅 9680,我们的 AUC 分数下降了几乎 4%。
使用下面的代码,我们能够获得一个具有最小 tf-idf 的功能列表,这些功能通常出现在所有评论中,或者很少出现在很长的评论中,以及一个具有最大 TF-IDF 的功能列表,这些功能包含经常出现在评论中,但通常不会出现在所有评论中的单词。
feature_names = np.array(vect.get_feature_names())sorted_tfidf_index = X_train_vectorized.max(0).toarray()[0].argsort()print('Smallest Tfidf: \n{}\n'.format(feature_names[sorted_tfidf_index[:10]]))
print('Largest Tfidf: \n{}\n'.format(feature_names[sorted_tfidf_index[:-11:-1]]))
最小 tfi df:[’ blazin ’ ’ 4 thd ’ ’ nations ’ ’ Committee ’ ’ 300 MGS ’ ’ 350 MGS ’ ’ sciences ’ ’ biochemical ’ ’ nas ’ ’ fnb ']
最大 Tfidf: [‘芥末’ ’ br ’ ‘挺举’ ‘唐’ ‘辣椒’ ‘伤口’ ‘鱼子酱’ ‘莎莎’ ‘垃圾’ ’ el’]
让我们测试我们的模型:
print(model.predict(vect.transform(['The candy is not good, I will never buy them again','The candy is not bad, I will buy them again'])))
【1 0】
我们当前的模型将“糖果不好吃,我再也不会买它们”的文档错误分类为正面评论,也将“糖果不好吃,我会再买它们”的文档错误分类为负面评论。
n-grams
解决这种错误分类的一种方法是添加 n 元语法。例如,二元模型计算相邻单词对的数量,并且可以给我们一些特征,比如坏和不错。因此,我们正在重新调整我们的训练集,指定最小文档频率为 5,并提取 1-grams 和 2-grams。
vect = CountVectorizer(min_df = 5, ngram_range = (1,2)).fit(X_train)
X_train_vectorized = vect.transform(X_train)
len(vect.get_feature_names())
61958
现在我们有了更多的功能,但是我们的 AUC 分数增加了:
model = LogisticRegression()
model.fit(X_train_vectorized, y_train)predictions = model.predict(vect.transform(X_test))
print('AUC: ', roc_auc_score(y_test, predictions))
T5 AUC:0.838772959029
使用系数来检查每个特征,我们可以看到
feature_names = np.array(vect.get_feature_names())
sorted_coef_index = model.coef_[0].argsort()print('Smallest Coef: \n{}\n'.format(feature_names[sorted_coef_index][:10]))
print('Largest Coef: \n{}\n'.format(feature_names[sorted_coef_index][:-11:-1]))
最小系数:[‘最差’ ‘还行’ ‘不推荐’ ‘不值得’ ‘糟糕’ ‘最好’ ‘不幸’ ‘糟糕’ ‘非常失望’ ‘失望’]
最大系数:[‘好吃’ ‘惊艳’ ‘不太’ ‘优秀’ ‘失望’ ‘不苦’ ‘好吃’ ‘上瘾’ ‘很棒’ '喜欢这个]]
我们的新模型已经正确地预测了负面评论的“不推荐”、“不值得”等二元模型,以及正面评论的“不苦”、“不太”等二元模型。
让我们测试我们的新模型:
print(model.predict(vect.transform(['The candy is not good, I would never buy them again','The candy is not bad, I will buy them again'])))
[0 1]
我们的最新模型现在可以正确地将它们分别识别为负面和正面评论。
自己试试
我希望你喜欢这篇文章,并享受在文本数据上练习机器学习技能的乐趣!请随意留下反馈或问题。
参考资料:
使用 Rvest 和 Shiny 在 R 中抓取数据并构建一个 Webapp
Photo by Patrick Fore on Unsplash
通过传统方法分享你的分析和数据科学发现很酷,但是如果你想和更多的人分享呢?如果您想要实时共享某个数据集的分析,该怎么办?通过几十行代码,您可以在一个下午创建这样一个工具。
我将分享我如何使用 R 计算语言和几个关键包构建了一个几乎实时的 webapp。您将学习如何抓取网站、解析数据,以及创建任何拥有浏览器的人都可以访问的 webapp。
问题陈述
我需要显示来自 https://coinmarketcap.com/gainers-losers/的数据,以便让我轻松地看到当天哪些硬币在真正的加热器上。
工具
我们将利用 Rvest 包、 shiny 、 shinydashboard 以及各种 tidyverse 工具,都在 Rstudio IDE 中。
虽然 RSelenium 是一个流行且可行的 web 抓取工具(通过解析 HTML 从网站收集数据),但 Rvest 包无疑是一个更整洁、更干净的工具。
该脚本的基本工作流程如下:
- 创建新的 Rstudio 项目
- 创建一个“应用程序。r "闪亮的文件
- 调用库
- 构建函数
- 构建用户界面
- 构建服务器
- 部署
所以让我们开始吧。
调用库
我们将调用上面“工具”一节中讨论的库:
library(shiny)
library(tidyverse)
library(shinydashboard)
library(rvest)
构建函数
始终认识到编码的枯燥(不要重复自己)准则,我们将构建简单的函数来收集和整理我们的数据,因此我们不必重复代码。
#####################
####### F N S #######
#####################get.data <- function(x){myurl <- read_html("[https://coinmarketcap.com/gainers-losers/](https://coinmarketcap.com/gainers-losers/)") # read our webpage as htmlmyurl <- html_table(myurl) # convert to an html table for ease of use
to.parse <- myurl[[1]] # pull the first item in the list
to.parse$`% 1h` <- gsub("%","",to.parse$`% 1h`) # cleanup - remove non-characters
to.parse$`% 1h`<- as.numeric(to.parse$`% 1h`) #cleanup - convert percentages column to numeric so we can sort
to.parse$Symbol <- as.factor(to.parse$Symbol) # cleanup - convert coin symbol to factor
to.parse$Symbol <- factor(to.parse$Symbol,
levels = to.parse$Symbol[order(to.parse$'% 1h')]) # sort by gain value
to.parse # return the finished data.frame
}
get.data 函数抓取我们的网站,返回一个对我们有用的数据框。
我们使用 Rvest 包中的 read_html 和 html_table 函数读入网页数据,并对其进行格式化,以便于处理。接下来,我们从该网页中取出许多表格中的第一个,并用基本的 R 函数清理它。最后,我们根据百分比增益值对新数据帧进行排序。
如果您要在 R 控制台中用
get.data()
您将返回如下所示的数据框:
get.data() returns this data.frame
如你所见,这显示了哪些硬币在过去的一个小时里收益最大,按收益百分比排序。这些数据是整个仪表板的基础。
我们将构建的下一个函数叫做 get.infobox.val. 它只是从上面的数据框中提取最高值。
get.infobox.val <- function(x){
df1 <- get.data() # run the scraping function above and assign that data.frame to a variable
df1 <- df1$`% 1h`[1] # assign the first value of the % gain column to same variable
df1 # return value
}
最后一个函数叫做 get.infobox.coin ,返回最高硬币的名称。
get.infobox.val <- function(x){
df1 <- get.data() # run the scraping function above and assign that data.frame to a variable
df1 <- df1$`% 1h`[1] # assign the first value of the % gain column to same variable
df1 # return value
}
现在我们已经构建了我们的函数,是时候构建闪亮的仪表板,向全世界显示我们的数据了。
进入闪亮的 Webapps
一个闪亮的 webapp 将允许我们建立一个交互式仪表板,我们将让 Rstudio 通过他们的服务器为我们托管。他们有一个免费的计划,所以任何人都可以很容易地开始使用它。来自 Rstudio 文档:
闪亮的应用程序有两个组件,一个用户界面对象和一个服务器函数,它们作为参数传递给
shinyApp
函数,该函数从这个 UI/服务器对创建一个闪亮的应用程序对象。
在这一点上,我强烈推荐浏览上面引用的官方文档,让自己熟悉基本的闪亮的概念。
为了避免过多赘述,让我们从构建 UI 对象开始。
用户界面
在 UI 对象中,我们将展示我们的仪表板。当我做这个的时候,我用了一个铅笔线框草图。为了帮助您直观地看到最终结果,这里是成品的截图:
Our finished dashboard for reference
shinydashboard 结构允许我们有一个侧边栏和一个仪表板区域,我们可以在那里添加行和列中的框。上面的黑色区域是侧边栏,右边的是主体。现在来看一下 UI 代码。
ui <- dashboardPage(
# H E A D E R
dashboardHeader(title = "Alt Coin Gainers"),
接下来,我们将 dashboardPage 函数分配给 UI,然后添加我们需要的部分。
# S I D E B A R
dashboardSidebar(
h5("A slightly interactive dashboard that pulls the top gainers from the last hour from
coinmarketcap.com. Refreshes every 60 seconds."),
br(),
br(),
br(),
br(),
br(),
br(),
br(),
br(),
br(),
br(),
h6("Built by Brad Lindblad in the R computing language
[ R Core Team (2018). R: A language and environment for statistical computing. R Foundation for Statistical Computing,
Vienna, Austria. URL [https://www.R-project.org/](https://www.R-project.org/)]"),
br(),
h6("R version 3.4.4 (2018-03-15) 'Someone to Lean On'"),
br(),
a("[bradley.lindblad@gmail.com](mailto:bradley.lindblad@gmail.com)", href="[bradley.lindblad@gmail.com](mailto:bradley.lindblad@gmail.com)")
),
侧边栏是放置文档或过滤器的好地方(这在本教程中没有涉及)。您会注意到,您可以将某些 html 标签和类传递给文本。 H5 只是一个标签,它将文本定义为文档中的第五层标题,通常是第五大文本。
# B O D Y
dashboardBody(
fluidRow(
# InfoBox
infoBoxOutput("top.coin",
width = 3),
# InfoBox
infoBoxOutput("top.name",
width = 3)
),
fluidRow(column(
# Datatable
box(
status = "primary",
headerPanel("Data Table"),
solidHeader = T,
br(),
DT::dataTableOutput("table", height = "350px"),
width = 6,
height = "560px"
),
# Chart
box(
status = "primary",
headerPanel("Chart"),
solidHeader = T,
br(),
plotOutput("plot", height = "400px"),
width = 6,
height = "500px"
),
width = 12
)
)
)
)
在主体部分,我们构建仪表板中的主要项目。主体部分有几个关键部分实际输出数据。
信息盒
# InfoBox
infoBoxOutput("top.coin",
width = 3),
# InfoBox
infoBoxOutput("top.name",
width = 3)
这两个代码块输出了仪表板顶部的两个紫色框。两个输出 ID“top . coin”和“top.name”引用了在服务器函数中输出的数据,我们将在后面介绍。
数据表和图
# Datatable
box(
status = "primary",
headerPanel("Data Table"),
solidHeader = T,
br(),
DT::dataTableOutput("table", height = "350px"),
width = 6,
height = "560px"
),# Chart
box(
status = "primary",
headerPanel("Chart"),
solidHeader = T,
br(),
plotOutput("plot", height = "400px"),
width = 6,
height = "500px"
),
数据表和曲线图也是如此。表是将绑定到下面的服务器函数的输出 ID,并且图也绑定到服务器。
计算机网络服务器
下一个议程是定义我们的服务器功能。服务器函数是通过 UI 函数计算、读取、绘制或显示数值的地方。
#####################
#### S E R V E R ####
#####################server <- function(input, output) {# R E A C T I V E
liveish_data <- reactive({
invalidateLater(60000) # refresh the report every 60k milliseconds (60 seconds)
get.data() # call our function from above
})
live.infobox.val <- reactive({
invalidateLater(60000) # refresh the report every 60k milliseconds (60 seconds)
get.infobox.val() # call our function from above
})
live.infobox.coin <- reactive({
invalidateLater(60000) # refresh the report every 60k milliseconds (60 seconds)
get.infobox.coin() # call our function from above
})
还记得我们一开始定义的函数吗?我们现在就要使用它们。我们还将添加另一个概念:反应式表达。这些允许我们闪亮的仪表板定期更新或基于用户输入。对于这个仪表板,我们要求程序每 60 秒运行一次我们的函数,用我们抓取的网站的最新值更新仪表板。
数据表
# D A T A T A B L E O U T P U T
output$table <- DT::renderDataTable(DT::datatable({
data <- liveish_data()}))
还记得我们上面定义的输出 ID 表吗?我们将在上面的函数中引用它。注意,我们从一开始就使用了 liveish_data 反应函数,而不是我们的原始函数。
情节
# P L O T O U T P U T
output$plot <- renderPlot({ (ggplot(data=liveish_data(), aes(x=Symbol, y=`% 1h`)) +
geom_bar(stat="identity", fill = "springgreen3") +
theme(axis.text.x = element_text(angle = 90, hjust = 1)) +
ggtitle("Gainers from the Last Hour"))
})
接下来,我们使用 ggplot2 绘制一个简单的柱状图。
信息盒
# I N F O B O X O U T P U T - V A L
output$top.coin <- renderInfoBox({
infoBox(
"Gain in Last Hour",
paste0(live.infobox.val(), "%"),
icon = icon("signal"),
color = "purple",
fill = TRUE)
})
# I N F O B O X O U T P U T - N A M E
output$top.name <- renderInfoBox({
infoBox(
"Coin Name",
live.infobox.coin(),
icon = icon("bitcoin"),
color = "purple",
fill = TRUE)
})
}
最后,我们使用服务器函数开头定义的反应表达式绘制两个信息框。
部署
脚本的最后一部分将我们的 UI 与服务器结合起来,并部署应用程序。
#####################
#### D E P L O Y ####
###################### Return a Shiny app objectshinyApp(ui = ui, server = server)
shinyApp(ui = ui, server = server)
此时,您应该有一个正在运行的应用程序出现在 Rstudio 的一个窗口中。
点击在浏览器中打开按钮,查看应用程序的非混乱版本;部署后您将看到的实际版本。
收尾工作
至此,所有的重担都完成了;我们构建了一个从网站下载并解析 html 的刮刀,然后我们使用 shinydashboard 框架构建了一个闪亮的应用程序,向全世界展示我们的分析。
在这一点上,所有剩下的是部署应用程序,这让我们非常容易。只需点击 Rstudio 预览中出现的部署按钮,然后按照说明进行操作。总的来说,从这一点开始部署应该不到 10 分钟。
我希望这篇小教程能在你的 R 之旅中帮助你,并给你一些关于用这种强大的开源语言可以构建什么样的东西的想法。
如果你有任何问题或者只是想聊聊 R,请发邮件到bradley.lindblad@gmail.com 给我。
附录
这是完整的脚本,你也可以从我的 Github 页面上为这个项目克隆。
library(shiny)
library(tidyverse)
library(shinydashboard)
library(rvest)#####################
####### F N S #######
#####################get.data <- function(x){myurl <- read_html("[https://coinmarketcap.com/gainers-losers/](https://coinmarketcap.com/gainers-losers/)") # read our webpage as html
myurl <- html_table(myurl) # convert to an html table for ease of use
to.parse <- myurl[[1]] # pull the first item in the list
to.parse$`% 1h` <- gsub("%","",to.parse$`% 1h`) # cleanup - remove non-characters
to.parse$`% 1h`<- as.numeric(to.parse$`% 1h`) #cleanup - convert percentages column to numeric
to.parse$Symbol <- as.factor(to.parse$Symbol) # cleanup - convert coin symbol to factor
to.parse$Symbol <- factor(to.parse$Symbol,
levels = to.parse$Symbol[order(to.parse$'% 1h')]) # sort by gain value
to.parse # return the finished data.frame
}get.infobox.val <- function(x){
df1 <- get.data() # run the scraping function above and assign that data.frame to a variable
df1 <- df1$`% 1h`[1] # assign the first value of the % gain column to same variable
df1 # return value
}get.infobox.coin <- function(x){
df <- get.data() # run the scraping function above and assign that data.frame to a variable
df <- df$Name[1] # assign the first value of the name column to same variable
df # return value
}#####################
####### U I #########
#####################ui <- dashboardPage(
# H E A D E R
dashboardHeader(title = "Alt Coin Gainers"),
# S I D E B A R
dashboardSidebar(
h5("A slightly interactive dashboard that pulls the top gainers from the last hour from
coinmarketcap.com. Refreshes every 60 seconds."),
br(),
br(),
br(),
br(),
br(),
br(),
br(),
br(),
br(),
br(),
h6("Built by Brad Lindblad in the R computing language
[ R Core Team (2018). R: A language and environment for statistical computing. R Foundation for Statistical Computing,
Vienna, Austria. URL [https://www.R-project.org/](https://www.R-project.org/)]"),
br(),
h6("R version 3.4.4 (2018-03-15) 'Someone to Lean On'"),
br(),
a("[bradley.lindblad@gmail.com](mailto:bradley.lindblad@gmail.com)", href="[bradley.lindblad@gmail.com](mailto:bradley.lindblad@gmail.com)")
),
# B O D Y
dashboardBody(
fluidRow(
# InfoBox
infoBoxOutput("top.coin",
width = 3),
# InfoBox
infoBoxOutput("top.name",
width = 3)
),
fluidRow(column(
# Datatable
box(
status = "primary",
headerPanel("Data Table"),
solidHeader = T,
br(),
DT::dataTableOutput("table", height = "350px"),
width = 6,
height = "560px"
),
# Chart
box(
status = "primary",
headerPanel("Chart"),
solidHeader = T,
br(),
plotOutput("plot", height = "400px"),
width = 6,
height = "500px"
),
width = 12
)
)
)
)#####################
#### S E R V E R ####
#####################server <- function(input, output) {# R E A C T I V E
liveish_data <- reactive({
invalidateLater(60000) # refresh the report every 60k milliseconds (60 seconds)
get.data() # call our function from above
})
live.infobox.val <- reactive({
invalidateLater(60000) # refresh the report every 60k milliseconds (60 seconds)
get.infobox.val() # call our function from above
})
live.infobox.coin <- reactive({
invalidateLater(60000) # refresh the report every 60k milliseconds (60 seconds)
get.infobox.coin() # call our function from above
})
# D A T A T A B L E O U T P U T
output$table <- DT::renderDataTable(DT::datatable({
data <- liveish_data()}))
# P L O T O U T P U T
output$plot <- renderPlot({ (ggplot(data=liveish_data(), aes(x=Symbol, y=`% 1h`)) +
geom_bar(stat="identity", fill = "springgreen3") +
theme(axis.text.x = element_text(angle = 90, hjust = 1)) +
ggtitle("Gainers from the Last Hour"))
})
# I N F O B O X O U T P U T - V A L
output$top.coin <- renderInfoBox({
infoBox(
"Gain in Last Hour",
paste0(live.infobox.val(), "%"),
icon = icon("signal"),
color = "purple",
fill = TRUE)
})
# I N F O B O X O U T P U T - N A M E
output$top.name <- renderInfoBox({
infoBox(
"Coin Name",
live.infobox.coin(),
icon = icon("bitcoin"),
color = "purple",
fill = TRUE)
})
}#####################
#### D E P L O Y ####
###################### Return a Shiny app objectshinyApp(ui = ui, server = server)
shinyApp(ui = ui, server = server)
在网上搜寻收视率最高的电视电影
Photo by Frank Okay on Unsplash
在这篇文章中,我将展示如何使用 Scrapy 框架 在互联网上搜索顶级电影。这个网页抓取器的 目标 是找到在电影数据库上具有高用户评级的电影。这些影片的列表将存储在一个 SQLite 数据库 和 中,并通过电子邮件发送给 。这样你就知道你再也不会错过电视上的大片了。
寻找一个好的网页来抓取
我从在线电视指南开始寻找比利时电视频道上的电影。但是您可以轻松地修改我的代码,将其用于任何其他网站。为了让你刮电影的时候更轻松,确定你要刮的网站
- 具有带 的可理解类或 id 的 HTML 标签
- 以一种 一致 的方式使用类和 id
- 有 结构良好的网址
- 在一个页面上包含所有相关的 电视频道
- 每个工作日都有一个 单独的页面
- 只列出电影 ,没有其他节目类型,如现场表演、新闻、报道等。除非你能轻易地将电影与其他节目类型区分开来。
有了发现的结果,我们将从电影数据库【TMDB】中获取电影评级和其他一些信息。
决定存储什么信息
我将收集以下关于电影的信息:
- 电影名称
- 电视频道
- 电影开始的时间
- 这部电影在电视上播出的日期
- 类型
- 情节
- 出厂日期
- 链接到 TMDB 的详细页面
- TMDB 评级
你可以用所有演员、导演、有趣的电影事实等等来补充这个列表。所有你想知道更多的信息。
在 Scrapy 中,该信息将存储在 项 的字段中。
创建 Scrapy 项目
我假设你已经安装了 Scrapy。如果没有,可以按照 Scrapy 优秀的安装手册。
安装 Scrapy 后,打开命令行并转到您想要存储 Scrapy 项目的目录。然后运行:
*scrapy startproject topfilms*
这将为 top films 项目创建一个文件夹结构,如下所示。现在可以忽略 topfilms.db 文件。这是我们将在下一篇关于管道的博客文章中创建的 SQLite 数据库。
定义废料项目
*在这个故事中,我们将使用文件 **items.py 。*创建你的 Scrapy 项目时默认创建 Items.py。
一个scrapy.Item
是一个容器,将在网页抓取过程中被填充。它将保存我们想要从网页中提取的所有字段。该项目的内容可以用与 Python dict 相同的方式访问。
打开 items.py 并添加一个包含以下字段的Scrapy.Item class
:
*import scrapyclass TVGuideItem(scrapy.Item):
title = scrapy.Field()
channel = scrapy.Field()
start_ts = scrapy.Field()
film_date_long = scrapy.Field()
film_date_short = scrapy.Field()
genre = scrapy.Field()
plot = scrapy.Field()
rating = scrapy.Field()
tmdb_link = scrapy.Field()
release_date = scrapy.Field()
nb_votes = scrapy.Field()*
用管道处理项目
在开始一个新的 Scrapy 项目后,您将拥有一个名为 pipelines.py 的文件。打开该文件,复制粘贴如下所示的代码。之后,我将一步一步地向您展示代码的每一部分是做什么的。
*import sqlite3 as lite
con = None # db connection
class StoreInDBPipeline(object):
def __init__(self):
self.setupDBCon()
self.dropTopFilmsTable()
self.createTopFilmsTable()def process_item(self, item, spider):
self.storeInDb(item)
return itemdef storeInDb(self, item):
self.cur.execute("INSERT INTO topfilms(\
title, \
channel, \
start_ts, \
film_date_long, \
film_date_short, \
rating, \
genre, \
plot, \
tmdb_link, \
release_date, \
nb_votes \
) \
VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )",
(
item['title'],
item['channel'],
item['start_ts'],
item['film_date_long'],
item['film_date_short'],
float(item['rating']),
item['genre'],
item['plot'],
item['tmdb_link'],
item['release_date'],
item['nb_votes']
))
self.con.commit()def setupDBCon(self):
self.con = lite.connect('topfilms.db')
self.cur = self.con.cursor()def __del__(self):
self.closeDB()def createTopFilmsTable(self):
self.cur.execute("CREATE TABLE IF NOT EXISTS topfilms(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \
title TEXT, \
channel TEXT, \
start_ts TEXT, \
film_date_long TEXT, \
film_date_short TEXT, \
rating TEXT, \
genre TEXT, \
plot TEXT, \
tmdb_link TEXT, \
release_date TEXT, \
nb_votes \
)")def dropTopFilmsTable(self):
self.cur.execute("DROP TABLE IF EXISTS topfilms")
def closeDB(self):
self.con.close()*
首先,我们从导入 SQLite 包开始,并给它起别名lite
。我们还初始化了一个用于数据库连接的变量con
。
创建一个类来存储数据库中的项
接下来,您创建一个具有逻辑名称的 类 。在设置文件中启用管道后(稍后将详细介绍),这个类将被调用。
*class StoreInDBPipeline(object):*
定义构造函数方法
构造函数方法是名为__init__
的方法。当创建一个StoreInDBPipeline
类的实例时,这个方法会自动运行。
*def __init__(self):
self.setupDBCon()
self.dropTopFilmsTable()
self.createTopFilmsTable()*
在构造函数方法中,我们启动了在构造函数方法下面定义的另外三个方法。
SetupDBCon 方法
通过方法setupDBCon
,我们创建了 topfilms 数据库(如果它还不存在的话)并用connect
函数连接到它。
*def setupDBCon(self):
self.con = lite.connect('topfilms.db')
self.cur = self.con.cursor()*
这里我们将 alias lite 用于 SQLite 包。其次,我们用cursor
函数创建一个光标对象。使用这个游标对象,我们可以在数据库中执行 SQL 语句。
DropTopFilmsTable 方法
构造函数中调用的第二个方法是dropTopFilmsTable
。顾名思义,它删除 SQLite 数据库中的表。
每次运行 web scraper 时,数据库都会被完全删除。如果你也想这样做,那就看你自己了。如果你想对电影数据进行一些查询或分析,你可以保存每次运行的抓取结果。
我只想看接下来几天的顶级电影,仅此而已。因此,我决定在每次运行时删除数据库。
*def dropTopFilmsTable(self):
self.cur.execute("DROP TABLE IF EXISTS topfilms")*
使用光标对象cur
我们执行DROP
语句。
CreateTopFilmsTable 方法
放下 top films 表后,我们需要创建它。这是通过构造函数方法中的最后一个方法调用来完成的。
*def createTopFilmsTable(self):
self.cur.execute("CREATE TABLE IF NOT EXISTS topfilms(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \
title TEXT, \
channel TEXT, \
start_ts TEXT, \
film_date_long TEXT, \
film_date_short TEXT, \
rating TEXT, \
genre TEXT, \
plot TEXT, \
tmdb_link TEXT, \
release_date TEXT, \
nb_votes \
)")*
我们再次使用游标对象 cur 来执行CREATE TABLE
语句。添加到桌面电影中的字段与我们之前创建的 Scrapy 项目中的字段相同。为了简单起见,我在 SQLite 表中使用了与 Item 中完全相同的名称。只有id
字段是额外的。
旁注:查看 SQLite 数据库的一个很好的应用是 Firefox 中的 SQLite 管理器插件。你可以在 Youtube 上观看这个 SQLite 管理器教程来学习如何使用这个插件。
流程项目方法
此方法必须在 Pipeline 类中实现,并且必须返回 dict、Item 或 DropItem 异常。在我们的网络刮刀,我们将返回项目。
*def process_item(self, item, spider):
self.storeInDb(item)
return item*
与解释的其他方法相比,它有两个额外的参数。被刮的item
和刮物品的spider
。从这个方法中,我们启动storeInDb
方法,然后返回项目。
StoreInDb 方法
这个方法执行一个INSERT
语句将抓取的条目插入 SQLite 数据库。
*def storeInDb(self, item):
self.cur.execute("INSERT INTO topfilms(\
title, \
channel, \
start_ts, \
film_date_long, \
film_date_short, \
rating, \
genre, \
plot, \
tmdb_link, \
release_date, \
nb_votes \
) \
VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )",
(
item['title'],
item['channel'],
item['start_ts'],
item['film_date_long'],
item['film_date_short'],
float(item['rating']),
item['genre'],
item['plot'],
item['tmdb_link'],
item['release_date'],
item['nb_votes']
))
self.con.commit()*
表字段的值来自 item,这是此方法的一个参数。这些值被简单地称为一个字典值(记住一个条目只不过是一个字典?).
每个构造函数都有一个…析构函数
与构造函数方法相对应的是名为__del__
的析构函数方法。在 pipelines 类的析构函数方法中,我们关闭了与数据库的连接。
*def __del__(self):
self.closeDB()*
CloseDB 方法
*def closeDB(self):
self.con.close()*
在最后一个方法中,我们用close
函数关闭数据库连接。所以现在我们已经写了一个全功能的管道。还剩下最后一步来启用管道。
在 settings.py 中启用管道
打开 settings.py 文件,添加以下代码:
*ITEM_PIPELINES = {
'topfilms.pipelines.StoreInDBPipeline':1
}*
整数值 表示流水线运行的顺序。因为我们只有一个管道,所以我们给它赋值 1。
在 Scrapy 中创建一个蜘蛛
现在我们就来看看刺儿头的核心, 蜘蛛 。这是你的刮网器将完成的地方。我将一步一步地向您展示如何创建一个。
导入必要的包
首先,我们将导入必要的包和模块。我们使用CrawlSpider
模块跟踪在线电视指南中的链接。
Rule
和LinkExtractor
用于确定我们想要关注的链接。
config
模块包含一些在蜘蛛中使用的常量,如DOM_1, DOM_2
和START_URL
。配置模块位于当前目录的上一个目录。这就是为什么你在配置模块前看到两个点。
最后,我们导入了TVGuideItem
。该 TVGuideItem 将用于包含抓取过程中的信息。
*import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from fuzzywuzzy import fuzz
from ..config import *
from topfilms.items import TVGuideItem*
告诉蜘蛛去哪里
其次,我们创建了 CrawlSpider 类的子类。这是通过插入 CrawlSpider 作为TVGuideSpider
类的参数来实现的。
我们给蜘蛛一个name
,提供allowed_domains
(例如 themoviedb.org)和start_urls
。在我的例子中,start_urls 是电视指南的网页,所以您应该通过自己的首选网站来更改它。
通过rules
和deny
参数,我们告诉爬行器在起始 URL 上跟随(不跟随)哪些 URL。不跟随的 URL 是用正则表达式指定的。
我对昨天放映的电影不感兴趣,所以我拒绝蜘蛛跟踪以“ gisteren ”结尾的 URL。
好吧,但是蜘蛛应该跟踪哪些 URL 呢?为此,我使用了restrict_xpaths
参数。它说要关注所有带有 class = " button button–beta "的 URL。这些事实上是未来一周每天的电影链接。
最后,通过callback
参数,我们让蜘蛛知道当它跟踪一个 URL 时该做什么。它将执行功能parse_by_day
。我将在下一部分解释这一点。
*class TVGuideSpider(CrawlSpider):
name = "tvguide"
allowed_domains = [DOM_1, DOM_2]
start_urls = [START_URL]# Extract the links from the navigation per day
# We will not crawl the films for yesterday
rules = (
Rule(LinkExtractor(allow=(), deny=(r'\/gisteren'), restrict_xpaths=('//a[@class="button button--beta"]',)), callback="parse_by_day", follow= True),
)*
解析跟踪的 URL
TVGuideScraper 的一部分功能parse_by_day
每天从网页上抓取每个频道所有电影的概览。response
参数来自运行网络抓取程序时启动的Request
。
在被抓取的网页上,你需要找到用来显示我们感兴趣的信息的 HTML 元素。两个很好的工具是 Chrome 开发者工具和 Firefox 中的 Firebug 插件。
我们想要存储的一个东西是我们正在抓取电影的date
。这个日期可以在带有class="grid__col__inner"
的 div 中的段落§中找到。显然,这是您应该为自己的网页修改的内容。
利用响应对象的xpath method
,我们提取段落中的文本。在这篇关于如何使用 xpath 函数的教程中,我学到了很多东西。
通过使用extract_first
,我们确保不会将这个日期存储为一个列表。否则,在 SQLite 数据库中存储日期时会出现问题。
之后,我对 film_date_long 执行了一些数据清理,并创建了格式为 YYYYMMDD 的film_date_short
。我创建了这种 YYYYMMDD 格式,以便稍后按时间顺序对电影进行排序。
接下来,刮电视频道。如果是在ALLOWED_CHANNELS
(在 config 模块中定义)的列表中,我们继续刮标题和开始时间。该信息存储在由TVGuideItem()
启动的项目中。
在这之后,我们想继续刮电影数据库。我们将使用 URLhttps://www.themoviedb.org/search?query=来显示被抓取的电影的搜索结果。对于这个 URL,我们要添加电影标题(代码中的url_part
)。我们只是重复使用在电视指南网页上的链接中找到的 URL 部分。
有了这个 URL,我们创建了一个新的请求,并继续 TMDB。使用request.meta['item'] = item
,我们将已经抓取的数据添加到请求中。这样我们可以继续填充我们当前的 TVGuideItem。
Yield request
实际发起请求。
*def parse_by_day(self, response):film_date_long = response.xpath('//div[@class="grid__col__inner"]/p/text()').extract_first()
film_date_long = film_date_long.rsplit(',',1)[-1].strip() # Remove day name and white spaces# Create a film date with a short format like YYYYMMDD to sort the results chronologically
film_day_parts = film_date_long.split()months_list = ['januari', 'februari', 'maart',
'april', 'mei', 'juni', 'juli',
'augustus', 'september', 'oktober',
'november', 'december' ]year = str(film_day_parts[2])
month = str(months_list.index(film_day_parts[1]) + 1).zfill(2)
day = str(film_day_parts[0]).zfill(2)film_date_short = year + month + dayfor col_inner in response.xpath('//div[@class="grid__col__inner"]'):
chnl = col_inner.xpath('.//div[@class="tv-guide__channel"]/h6/a/text()').extract_first()if chnl in ALLOWED_CHANNELS:
for program in col_inner.xpath('.//div[@class="program"]'):
item = TVGuideItem()
item['channel'] = chnl
item['title'] = program.xpath('.//div[@class="title"]/a/text()').extract_first()
item['start_ts'] = program.xpath('.//div[@class="time"]/text()').extract_first()
item['film_date_long'] = film_date_long
item['film_date_short'] = film_date_shortdetail_link = program.xpath('.//div[@class="title"]/a/@href').extract_first()
url_part = detail_link.rsplit('/',1)[-1]# Extract information from the Movie Database [www.themoviedb.org](http://www.themoviedb.org)
request = scrapy.Request("https://www.themoviedb.org/search?query="+url_part,callback=self.parse_tmdb)
request.meta['item'] = item # Pass the item with the request to the detail pageyield request*
抓取电影数据库中的附加信息
正如您注意到的,在函数parse_by_day
中创建的请求中,我们使用了回调函数parse_tmdb
。这个函数在请求抓取 TMDB 网站时使用。
在第一步中,我们获取 parse_by_day 函数传递的商品信息。
带有 TMDB 搜索结果的页面可能会列出同一个电影名称的多个搜索结果(查询中传递了 url_part)。我们也用if tmddb_titles
检查是否有结果。
我们使用 fuzzywuzzy 包对电影片名进行模糊匹配。为了使用 fuzzywuzzy 包,我们需要将import
语句与前面的导入语句一起添加。
*from fuzzywuzzy import fuzz*
如果我们找到 90%匹配,我们使用搜索结果来做剩下的搜集工作。我们不再看其他搜索结果。为此,我们使用了break
语句。
接下来,我们从搜索结果页面中收集genre
、rating
和release_date
,收集方式与之前使用 xpath 函数的方式类似。为了获得发布日期的 YYYYMMDD 格式,我们使用split
和join
函数执行一些数据处理。
我们想再次对 TMDB 的详细信息页面发起一个新的请求。这个请求将调用parse_tmdb_detail
函数来提取电影情节和 TMDB 的票数。这将在下一节中解释。
*def parse_tmdb(self, response):
item = response.meta['item'] # Use the passed itemtmdb_titles = response.xpath('//a[@class="title result"]/text()').extract()if tmdb_titles: # Check if there are results on TMDB
for tmdb_title in tmdb_titles:
match_ratio = fuzz.ratio(item['title'], tmdb_title)
if match_ratio > 90:
item['genre'] = response.xpath('.//span[@class="genres"]/text()').extract_first()
item['rating'] = response.xpath('//span[@class="vote_average"]/text()').extract_first()
release_date = response.xpath('.//span[@class="release_date"]/text()').extract_first()
release_date_parts = release_date.split('/')
item['release_date'] = "/".join([release_date_parts[1].strip(), release_date_parts[0].strip(), release_date_parts[2].strip()])
tmdb_link = "https://www.themoviedb.org" + response.xpath('//a[@class="title result"]/@href').extract_first()
item['tmdb_link'] = tmdb_link# Extract more info from the detail page
request = scrapy.Request(tmdb_link,callback=self.parse_tmdb_detail)
request.meta['item'] = item # Pass the item with the request to the detail pageyield request
break # We only consider the first match
else:
return*
从详细信息页面抓取电影情节
我们要讨论的最后一个函数是一个短函数。像以前一样,我们获取 parse_tmdb 函数传递的项目,并抓取plot
和number of votes
的详细信息页面。
在这个阶段,我们已经完成了为这部电影搜集信息的工作。换句话说,这部电影的项目已经满员了。Scrapy 然后将使用管道中编写的代码来处理这些数据,并将其放入数据库。
*def parse_tmdb_detail(self, response):
item = response.meta['item'] # Use the passed item item['nb_votes'] = response.xpath('//span[@itemprop="ratingCount"]/text()').extract_first()
item['plot'] = response.xpath('.//p[@id="overview"]/text()').extract_first() yield item*
在 Scrapy 中使用扩展
在关于管道的部分,我们已经看到了如何将抓取结果存储在 SQLite 数据库中。现在我将向您展示如何通过电子邮件 发送刮擦结果。通过这种方式,你可以在邮箱里看到下周收视率最高的电影。
导入必要的包
我们将使用文件 扩展名. py 。当您创建 Scrapy 项目时,这个文件会自动创建在根目录中。我们首先导入我们将在本文件后面使用的包。
*import logging
from scrapy import signals
from scrapy.exceptions import NotConfigured
import smtplib
import sqlite3 as lite
from config import **
并不真正需要logging
包。但是这个包可以用来调试你的程序。或者只是向日志中写入一些信息。
signals
模块将帮助我们知道蜘蛛何时被打开和关闭。我们将在蜘蛛完成工作后发送带有电影的电子邮件。
我们从scrapy.exceptions
模块导入方法NotConfigured
。当在 settings.py 文件中没有配置扩展名时,会出现这个问题。具体来说,参数MYEXT_ENABLED
必须设置为True
。我们将在后面的代码中看到这一点。
导入smtplib
包以便能够发送电子邮件。我使用我的 Gmail 地址发送电子邮件,但是你可以修改 config.py 中的代码来使用另一个电子邮件服务。
最后,我们导入sqlite3
包来从数据库中提取收视率最高的电影,并导入config
来获取我们的常量。
在扩展中创建 SendEmail 类
首先,我们定义了logger
对象。通过这个对象,我们可以在特定事件时将消息写入日志。然后我们用构造函数方法创建了SendEmail
类。在构造函数中,我们将FROMADDR
和TOADDR
分配给类的相应属性。这些常量设置在 config.py 文件中。这两个属性我都用了我的 Gmail 地址。
*logger = logging.getLogger(__name__)class SendEmail(object):
def __init__(self):
self.fromaddr = FROMADDR
self.toaddr = TOADDR*
实例化扩展对象
SendEmail
对象的第一个方法是from_crawler
。我们做的第一项检查是在 settings.py 文件中是否启用了MYEXT_ENABLED
。如果不是这种情况,我们抛出一个NotConfigured
异常。发生这种情况时,扩展中的其余代码不会被执行。
在 settings.py 文件中我们需要添加以下代码来启用这个扩展。
*MYEXT_ENABLED = True
EXTENSIONS = {
'topfilms.extensions.SendEmail': 500,
'scrapy.telnet.TelnetConsole': None
}*
所以我们将布尔标志MYEXT_ENABLED
设置为True
。然后我们将自己的扩展SendEmail
添加到EXTENSIONS
字典中。整数值 500 指定了扩展必须执行的顺序。我还不得不禁用了TelnetConsole
。否则发送电子邮件不起作用。通过放置None
而不是整数顺序值来禁用该扩展。
接下来,我们用cls()
函数实例化扩展对象。我们将一些signals
连接到这个扩展对象。我们对spider_opened
和spider_closed
信号感兴趣。最后我们返回ext
对象。
*@classmethod
def from_crawler(cls, crawler):
# first check if the extension should be enabled and raise
# NotConfigured otherwise
if not crawler.settings.getbool('MYEXT_ENABLED'):
raise NotConfigured# instantiate the extension object
ext = cls()# connect the extension object to signals
crawler.signals.connect(ext.spider_opened, signal=signals.spider_opened)
crawler.signals.connect(ext.spider_closed, signal=signals.spider_closed)# return the extension object
return ext*
定义 spider_opened 事件中的操作
当蜘蛛被打开时,我们只是想把它写到日志中。因此,我们使用在代码顶部创建的logger
对象。使用info
方法,我们向日志中写入一条消息。Spider.name
替换为我们在 TVGuideSpider.py 文件中定义的名称。
*def spider_opened(self, spider):
logger.info("opened spider %s", spider.name)*
在 spider_closed 事件后发送电子邮件
在SendEmail
类的最后一个方法中,我们发送包含顶级电影概述的电子邮件。
我们再次向日志发送通知,告知蜘蛛已经关闭。其次,我们创建一个到 SQLite 数据库的连接,该数据库包含 ALLOWED_CHANNELS 下周的所有电影。 我们选择带有rating >= 6.5
的影片。您可以根据需要将评级更改为更高或更低的阈值。然后,生成的影片按格式为 YYYYMMDD 的film_date_short
和开始时间start_ts
排序。
我们获取游标cur
中的所有行,并检查是否有一些使用len
函数的结果。例如,当您将阈值等级设置得太高时,可能会没有结果。
用for row in data
我们浏览每一个结果电影。我们从row
中提取所有有趣的信息。对于一些数据,我们用encode('ascii','ignore')
进行编码。这是为了忽略一些特殊字符,如é、à、è等。否则,我们会在发送电子邮件时出错。
当收集了关于电影的所有数据后,我们编写了一个字符串变量topfilm
。每个topfilm
然后连接到变量topfilms_overview
,这将是我们发送的电子邮件的信息。如果我们的查询结果中没有电影,我们会在短消息中提到这一点。
最后,由于有了smtplib
包,我们用 Gmail 地址发送邮件。
*def spider_closed(self, spider):
logger.info("closed spider %s", spider.name)# Getting films with a rating above a threshold
topfilms_overview = ""
con = lite.connect('topfilms.db')
cur = con.execute("SELECT title, channel, start_ts, film_date_long, plot, genre, release_date, rating, tmdb_link, nb_votes "
"FROM topfilms "
"WHERE rating >= 6.5 "
"ORDER BY film_date_short, start_ts")data=cur.fetchall()if len(data) > 0: # Check if we have records in the query result
for row in data:
title = row[0].encode('ascii', 'ignore')
channel = row[1]
start_ts = row[2]
film_date_long = row[3]
plot = row[4].encode('ascii', 'ignore')
genre = row[5]
release_date = row[6].rstrip()
rating = row[7]
tmdb_link = row[8]
nb_votes = row[9]
topfilm = ' - '.join([title, channel, film_date_long, start_ts])
topfilm = topfilm + "\r\n" + "Release date: " + release_date
topfilm = topfilm + "\r\n" + "Genre: " + str(genre)
topfilm = topfilm + "\r\n" + "TMDB rating: " + rating + " from " + nb_votes + " votes"
topfilm = topfilm + "\r\n" + plot
topfilm = topfilm + "\r\n" + "More info on: " + tmdb_link
topfilms_overview = "\r\n\r\n".join([topfilms_overview, topfilm])con.close()if len(topfilms_overview) > 0:
message = topfilms_overview
else:
message = "There are no top rated films for the coming week."msg = "\r\n".join([
"From: " + self.fromaddr,
"To: " + self.toaddr,
"Subject: Top Films Overview",
message
])
username = UNAME
password = PW
server = smtplib.SMTP(GMAIL)
server.ehlo()
server.starttls()
server.login(username,password)
server.sendmail(self.fromaddr, self.toaddr, msg)
server.quit()*
通过分机发送电子邮件的结果
这段代码的最终结果是在你的邮箱里有一个顶级电影的概览。太好了!现在,您不必再在在线电视指南上查找这些内容了。
规避知识产权禁令的技巧
当你在短时间内发出许多请求时,你就有被服务器禁止的风险。在这最后一部分,我将向你展示一些避免 IP 封禁的技巧。
推迟你的请求
避免 IP 禁止的一个简单方法是在每个请求 之间 暂停。在 Scrapy 中,只需在 settings.py 文件中设置一个参数即可。您可能已经注意到,settings.py 文件中有很多参数被注释掉了。
搜索参数DOWNLOAD_DELAY
并取消注释。我将 暂停长度设置为 2 秒 。根据您必须发出的请求数量,您可以更改这一点。但我会把它设置为至少 1 秒。
*DOWNLOAD_DELAY=2*
避免 IP 封禁的更高级方法
默认情况下,每次你做一个请求,你用 同一个用户代理 来做。由于有了包fake_useragent
,我们可以很容易地为每个请求改变用户代理。
这段代码的所有功劳归于 Alecxe,他写了一个很好的 python 脚本来使用 fake_useragent 包。
首先,我们在 web scraper 项目的根目录下创建一个文件夹scrapy _ fake _ user agent*。在这个文件夹中,我们添加了两个文件:*
- init。py 是一个空文件
- middleware . py
要使用这个中间件,我们需要在 settings.py 文件中启用它。这是通过代码完成的:
*DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddleware.useragent.UserAgentMiddleware': None,
'scrapy_fake_useragent.middleware.RandomUserAgentMiddleware': 400,
}*
首先,我们通过指定 None 而不是一个整数值来禁用 Scrapy 的默认值UserAgentMiddleware
。然后我们启用自己的中间件RandomUserAgentMiddleware
。直观地说,中间件是在请求 期间 执行的一段代码。
在文件middleware . py中,我们为每个请求添加代码到 随机化用户代理 。确保您安装了 fake_useragent 包。从fake _ user agent 包中,我们导入了UserAgent
模块。这包含了 不同用户代理 的列表。在 RandomUserAgentMiddleware 类的构造函数中,我们实例化了 UserAgent 对象。在方法process _ request中,我们将用户代理设置为来自ua
对象的随机用户代理。在请求的报头中。
*from fake_useragent import UserAgentclass RandomUserAgentMiddleware(object):
def __init__(self):
super(RandomUserAgentMiddleware, self).__init__()self.ua = UserAgent()def process_request(self, request, spider):
request.headers.setdefault('User-Agent', self.ua.random)*
结论
就是这样!我希望你现在对如何在你的网页抓取项目中使用 Scrapy 有一个清晰的认识。