物联网的人工智能实用指南-全-

物联网的人工智能实用指南(全)

原文:annas-archive.org/md5/0095b5f3f49dd33558a193990b8e61a8

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书的使命是使读者能够构建 AI 驱动的物联网应用程序。随着物联网设备的普及,很多应用程序使用数据科学和分析技术来利用生成的海量数据。然而,这些应用程序并未解决持续发现物联网数据中模式的挑战。在本书中,我们涵盖了人工智能理论和实现的各个方面,读者可以利用这些知识,通过实施人工智能技术使物联网解决方案更加智能。

读者从学*人工智能和物联网设备的基础知识开始,了解如何从各种来源和流中读取物联网数据。接着,我们介绍了如何使用 TensorFlow、scikit learn 和 Keras 的示例来实现人工智能的各种方法。涵盖的主题包括机器学*、深度学*、遗传算法、强化学*和生成对抗网络。我们还向读者展示了如何使用分布式技术和云技术来实现人工智能。一旦读者熟悉了人工智能技术,我们就介绍了针对物联网设备生成和消费的不同类型数据(如时间序列、图像、音频、视频、文本和语音)的各种技术。

在讲解了各种人工智能技术在不同类型物联网数据上的应用后,最后,我们与读者分享了四大类物联网解决方案的案例研究:个人物联网、家庭物联网、工业物联网和智能城市物联网。

本书适合谁阅读

本书的读者是那些具备基本物联网应用开发和 Python 知识的人,想通过应用人工智能技术使他们的物联网应用更加智能。该读者群体可能包括以下人群:

  • 已经知道如何构建物联网系统的物联网从业者,但现在他们希望通过实施人工智能来使他们的物联网解决方案变得更加智能。

  • 已经在物联网平台上构建分析的 数据科学从业者,但现在他们希望从物联网分析转向物联网人工智能,从而使他们的物联网解决方案更加智能。

  • 想要为智能物联网设备开发基于人工智能的解决方案的软件工程师。

  • 希望将智能和智能化引入其产品的嵌入式系统工程师。

本书涵盖的内容

第一章,物联网与人工智能的基本原理和基础,介绍了物联网、人工智能和数据科学的基本概念。本章最后介绍了本书将使用的工具和数据集。

第二章,物联网的数据访问与分布式处理,介绍了从各种数据源(如文件、数据库、分布式数据存储和流式数据)访问数据的多种方法。

第三章,物联网的机器学*,涵盖了机器学*的各个方面,例如监督学*、无监督学*和强化学*在物联网中的应用。本章最后提供了提高模型性能的技巧和窍门。

第四章,物联网的深度学*,探索了物联网中深度学*的各个方面,如 MLP、CNN、RNN 和自编码器。它还介绍了多种深度学*框架。

第五章,物联网的遗传算法,讨论了优化和不同的进化技术,重点讲解了遗传算法在优化中的应用。

第六章,物联网的强化学*,介绍了强化学*的概念,如策略梯度和 Q 网络。我们讲解了如何使用 TensorFlow 实现深度 Q 网络,并学*了一些可以应用强化学*的现实世界问题。

第七章,物联网的生成模型,介绍了对抗性学*和生成学*的概念。我们讲解了如何使用 TensorFlow 实现 GAN、DCGAN 和 CycleGAN,并且讨论了它们的实际应用。

第八章,物联网的分布式人工智能,讲解了如何在分布模式下利用机器学*来应用于物联网。

第九章,个人与家庭物联网,讲解了一些令人兴奋的个人和家庭物联网应用。

第十章,工业物联网的人工智能,解释了如何将本书中学到的概念应用于两个工业物联网数据的案例研究。

第十一章,智能城市物联网的人工智能,解释了如何将本书中学到的概念应用于来自智能城市的物联网数据。

第十二章,整合所有内容,讲解了如何在将文本、图像、视频和音频数据输入模型之前进行预处理。它还介绍了时间序列数据。

为了从本书中获得最大的收益

为了从本书中获得最大的收益,请从 GitHub 仓库下载示例代码,并使用提供的 Jupyter 笔记本进行练*。

下载示例代码文件

你可以从你的账户在 www.packtpub.com 下载本书的示例代码文件。如果你在其他地方购买了本书,可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给你。

你可以通过以下步骤下载代码文件:

  1. 登录或注册 www.packtpub.com

  2. 选择 SUPPORT 标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名,并按照屏幕上的说明操作。

文件下载完成后,请确保使用最新版本的以下工具解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Hands-On-Artificial-Intelligence-for-IoT。我们还提供来自丰富书籍和视频目录的其他代码包,地址为 github.com/PacktPublishing/。快来看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。你可以在此下载:www.packtpub.com/sites/default/files/downloads/9781788836067_ColorImages.pdf

使用的约定

本书中使用了一些文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“这声明了两个占位符,名称分别为 ABtf.placeholder 方法的参数指定这些占位符的数据类型是 float32。”

代码块如下所示:

# Declare placeholders for the two matrices 
A = tf.placeholder(tf.float32, None, name='A')
B = tf.placeholder(tf.float32, None, name='B')

粗体:表示一个新术语、一个重要的词或屏幕上看到的词。例如,菜单或对话框中的词会以这种方式出现在文本中。举个例子:“在堆栈底部,我们有设备层,也称为感知层。”

警告或重要的提示以这种方式显示。

提示和技巧以这种方式显示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:请发送电子邮件至 feedback@packtpub.com,并在邮件主题中注明书籍名称。如果你对本书的任何方面有疑问,请通过 questions@packtpub.com 与我们联系。

勘误:尽管我们已经尽力确保内容的准确性,但错误仍然可能发生。如果你发现本书中的错误,我们将非常感激你能报告给我们。请访问 www.packtpub.com/submit-errata,选择你的书籍,点击“勘误提交表格”链接,并填写相关详情。

盗版:如果你在互联网上发现我们的作品有任何非法复制,我们将非常感激你能提供该材料的地址或网站名称。请通过 copyright@packtpub.com 联系我们,并附上相关链接。

如果你有兴趣成为作者:如果你在某个领域具有专业知识,并且有意编写或为书籍贡献内容,请访问 authors.packtpub.com

评论

请留下评论。在阅读并使用本书后,为什么不在您购买书籍的网站上留下评论呢?潜在读者可以看到并参考您的公正意见来做出购买决策,我们在 Packt 能够了解您对我们产品的看法,而我们的作者也可以看到您对他们书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问packtpub.com

第一章:物联网和人工智能的原理与基础

恭喜你购买了本书;这表明你对跟进最新的科技进展充满兴趣。本书讨论的是当前商业环境中的三大趋势——物联网IoT)、大数据和人工智能AI)。随着连接互联网的设备数量呈指数级增长,并且这些设备生成的数据量也在指数级增加,使用人工智能和深度学*DL)的分析与预测技术变得至关重要。本书专门针对第三个组件——物联网生成的大数据所需的各种分析和预测方法或模型,进行深入讲解。

本章将简要介绍这三大趋势,并扩展它们之间的相互依赖关系。物联网设备生成的数据上传到云端,因此你还将了解各种物联网云平台及其提供的数据服务。

本章将涵盖以下内容:

  • 了解物联网中的事物是什么,哪些设备构成了这些事物,物联网的不同平台是什么,物联网垂直领域是什么

  • 了解什么是大数据,并理解物联网生成的数据量如何处于大数据的范畴内

  • 理解人工智能如何以及为什么对理解物联网生成的大量数据有所帮助

  • 通过一幅插图,理解物联网、大数据和人工智能如何共同帮助我们塑造更美好的世界

  • 学*执行分析所需的一些工具

什么是物联网 101?

“物联网”(IoT)这个术语是由凯文·阿什顿(Kevin Ashton)于 1999 年提出的。那时,大多数输入到计算机的数据都是由人类生成的;他提出,最好的方法是让计算机直接获取数据,而不需要任何人为干预。因此,他提出像 RFID 和传感器这样的设备,应该连接到网络,直接向计算机提供数据。

你可以阅读阿什顿完整的文章,了解他对物联网的定义,文章可以通过此链接阅读:www.itrco.jp/libraries/RFIDjournal-That%20Internet%20of%20Things%20Thing.pdf

今天,物联网(也被称为万物互联网,有时也叫做雾网络)指的是连接到互联网的各种设备,如传感器、执行器和智能手机。这些设备可以是任何物品:带有可穿戴设备(甚至手机)的人、带有 RFID 标签的动物,甚至是我们日常使用的家电,如冰箱、洗衣机或咖啡机。这些设备可以是物理的——也就是存在于物理世界中,可以被感知、执行并连接的物品——也可以是信息世界中的虚拟物品——即那些不存在于物理世界,但以信息(数据)的形式存在,可以存储、处理和访问的物品。这些设备必须具备直接与互联网通信的能力;它们也有可能具备感知、执行、数据采集、数据存储和数据处理等功能。

国际电信联盟ITU),一个联合国机构,定义物联网为:

“为信息社会提供全球基础设施,通过互联(物理和虚拟)事物,基于现有和不断发展的可互操作的信息和通信技术,提供先进的服务。”

你可以在www.itu.int/en/ITU-T/gsi/iot/Pages/default.aspx了解更多信息。

信息通信技术的广泛应用已经为我们提供了随时随地的通信;物联网(IoT)则为我们增加了任何事物之间的通信维度:

物联网引入的新维度(改编自 b-ITU-T Y.2060 报告)

预计物联网作为一项技术将对人类和我们生活的社会产生深远的影响。为了让你了解它可能带来的广泛影响,考虑以下场景:

  • 你和我一样住在高楼大厦里,而且非常喜欢植物。经过大量的努力和细心照料,你用花盆打造了一个小小的室内花园。你的老板让你去出差一周,你担心没有水的植物会在一周内枯死。物联网的解决方案是为你的植物添加土壤湿度传感器,将它们连接到互联网,并安装执行器远程控制水源和人工阳光的开启或关闭。现在,无论你身处世界的哪个角落,你的植物都不会枯死,你还可以随时查看每棵植物的土壤湿度状况,按需浇水。

  • 你在办公室度过了非常疲惫的一天;你只想回家,让有人为你做咖啡,准备好床铺,并加热浴缸的水,但可惜你一个人在家。现在不再是问题;物联网可以帮忙。你的物联网助手可以从咖啡机中准备出你喜欢的口味咖啡,指令智能热水器打开并维持你所需的水温,甚至请求智能空调开启并调节房间温度。

选择仅受你的想象力限制。前面提到的两个场景对应于消费者物联网——以消费者为中心的物联网应用。还有一个广阔的工业物联网IIoT),在这个领域中,制造商和工业部门优化流程,实施远程监控功能,以提高生产力和效率。在本书中,你将体验到这两种物联网应用的实践。

物联网参考模型

就像互联网的 OSI 参考模型一样,物联网架构通过六个层次进行定义:四个横向层和两个纵向层。两个纵向层是管理安全,它们分布在所有四个横向层上,如下图所示:

物联网层

设备层:在整个架构的底部,我们有设备层,也称为感知层。这一层包含了感知或控制物理世界以及获取数据所需的物理设备(即通过感知物理世界)。现有硬件,如传感器、RFID 和执行器,构成了感知层。

网络层:这一层提供网络支持,并通过有线或无线网络传输数据。该层安全地将来自设备层的设备信息传输到信息处理系统。介质技术都是网络层的一部分。举例来说,包括 3G、UMTS、ZigBee、蓝牙、Wi-Fi 等。

服务层:这一层负责服务管理。它接收来自网络层的信息,将其存储到数据库中,处理这些信息,并可以根据结果做出自动决策。

应用层:这一层管理依赖于服务层处理过的信息的应用程序。物联网可以实现各种应用:智能城市、智能农业和智能家居等。

物联网平台

网络层的信息通常通过物联网平台进行管理。如今,许多公司提供物联网平台服务,帮助处理数据,并支持与不同硬件的无缝集成。由于物联网平台作为硬件与应用层之间的中介,因此也被称为物联网中间件,并且是物联网参考架构中的服务层的一部分。物联网平台提供了随时随地连接和通信的能力。在本书中,我们将简要介绍一些流行的物联网平台,如谷歌云平台、Azure 物联网、亚马逊 AWS 物联网、Predix 和 H2O。

你可以根据以下标准选择最适合你的物联网平台:

  • 可扩展性:应当能够将新设备添加到现有物联网网络中,或删除已有设备

  • 易用性:该系统应能够完美运行,按照所有规格要求提供服务,并且只需最小的干预。

  • 第三方集成:异构设备和协议应能够相互连接并互通。

  • 部署选项:它应能够在各种硬件设备和软件平台上运行。

  • 数据安全:确保数据和设备的安全性。

物联网垂直行业

垂直市场是指提供特定行业、贸易、职业或其他具有特殊需求的顾客群体所需商品和服务的市场。物联网使得许多这样的垂直市场成为可能,一些主要的物联网垂直行业如下:

  • 智能建筑:使用物联网技术的建筑不仅能减少资源消耗,还能提高居住或工作在其中的人的满意度。建筑配备智能传感器,除了监测资源消耗外,还可以主动检测住户的需求。通过这些智能设备和传感器收集的数据,能够远程监控建筑、能源、安全、景观、HVAC(暖通空调)、照明等系统。然后利用这些数据预测行动,并根据事件自动化,从而优化效率,节省时间、资源和成本。

  • 智能农业:物联网可以使本地和商业农业更加环保、成本效益高且生产效率更高。通过在农场中安装传感器,可以帮助自动化灌溉过程。预计智能农业实践将显著提高生产力,从而增加食品资源。

  • 智慧城市:智慧城市可以是拥有智能停车、智能公共交通系统等功能的城市。智慧城市能够解决交通、公共安全、能源管理等问题,为政府和市民提供服务。通过使用先进的物联网技术,它能够优化城市基础设施的使用,并提高市民的生活质量。

  • 联网医疗:物联网使得远程实时进行重要业务和病患监测决策成为可能。个人佩戴医疗传感器以监测身体参数,如心跳、体温、血糖等。可穿戴传感器,如加速度计和陀螺仪,可以用来监测一个人的日常活动。

本书将作为案例研究介绍其中的一些。内容聚焦于信息处理和物联网应用,因此我们将不会深入探讨物联网参考架构中涉及的设备、架构和协议的细节。

感兴趣的读者可以参考以下文献,了解更多关于物联网架构和不同协议的信息:

  • Da Xu, Li, Wu He, and Shancang Li. 工业中的物联网:一项调查。 IEEE 工业信息学杂志 10.4 (2014): 2233-2243。

  • Khan, Rafiullah 等人,未来互联网:物联网架构、可能的应用及关键挑战信息技术前沿FIT),2012 年国际会议论文集,IEEE,2012 年。

  • 该网站提供了物联网相关协议的概述:

    www.postscapes.com/internet-of-things-protocols/

大数据与物联网

物联网已经将以前从未连接到互联网的事物(如汽车发动机)连接起来,从而产生大量持续的数据流。以下截图展示了 IHS 对未来几年内连接设备数量的探索性数据。其估计显示,到 2025 年,物联网设备数量将达到 754.4 亿:

对 2025 年物联网设备增长的预测

IHS 的完整白皮书,物联网平台:实现物联网,可以在 PDF 格式下载:cdn.ihs.com/www/pdf/enabling-IOT.pdf

传感器成本的降低、有效的功耗技术、大范围的连接性(红外、NFC、蓝牙、Wi-Fi 等)以及支持物联网部署和开发的云平台的可用性是物联网在我们家庭、个人生活和工业中普及的主要原因。这也促使公司考虑提供新服务并开发新商业模式。一些例子包括:

  • Airbnb:它将人们连接起来,使他们可以相互出租空闲的房间和小屋,并赚取佣金。

  • Uber:它将出租车司机与旅行者连接起来,通过旅行者的位置为他们指派最*的司机。

在此过程中生成的数据量既庞大又复杂,迫使我们采用大数据方法。大数据方法和物联网几乎是天作之合;二者协同工作。

物体持续生成大量数据流,提供其状态信息,如温度、污染水平、地理位置和接*度。生成的数据是时间序列格式并具有自相关性。由于数据的动态特性,这一任务变得具有挑战性。此外,生成的数据可以在边缘(传感器或网关)或云端进行分析。在将数据发送到云端之前,会执行某种形式的物联网数据转化。这可能涉及以下内容:

  • 时间或空间分析

  • 边缘数据汇总

  • 数据汇聚

  • 关联多个物联网数据流中的数据

  • 清洗数据

  • 填充缺失值

  • 归一化数据

  • 将其转化为适合云端的不同格式

在边缘,复杂事件处理CEP)用于将来自多个来源的数据结合起来,推断出事件或模式。

这些数据通过流分析进行分析,例如,应用分析工具处理数据流,但洞察和规则是在离线模式下开发的。模型在离线构建后,应用于生成的数据流。这些数据可能以不同的方式进行处理:

  • 原子性:一次使用单一数据

  • 微批处理:每批次的数据组

  • 窗口化:按批次处理时间段内的数据

流分析可以与 CEP 结合,在时间范围内合并事件并关联模式,以检测特殊模式(例如,异常或故障)。

人工智能的融合——物联网中的数据科学

数据科学家和机器学*工程师之间非常流行的一句话是"AI is the new electricity",这是 Andrew Ng 教授在 NIPS 2017 中所说的,我们可以这样扩展它:如果 AI 是新的电力,那么数据就是新的煤炭,而物联网则是新的煤矿

物联网生成大量数据;目前,90%的数据未被捕获,而被捕获的 10%中,大多数数据是时间依赖性的,几毫秒内就会失去其价值。手动持续监控这些数据既繁琐又昂贵。这就需要一种智能分析这些数据并从中获得洞察的方法;AI 的工具和模型为我们提供了以最小人工干预完成这一目标的途径。本书的主要重点将是理解可以应用于物联网数据的各种 AI 模型和技术。我们将使用机器学*ML)和深度学*(DL)算法。以下截图说明了人工智能机器学*深度学*之间的关系:

AI、ML 和 DL

通过观察多个事物的行为,物联网(借助大数据和人工智能)旨在深入了解数据并优化基础过程。这涉及多个挑战:

  • 存储实时生成的事件

  • 对存储的事件运行分析查询

  • 使用 AI/ML/DL 技术对数据进行分析,以获取洞察并进行预测

跨行业标准数据挖掘过程

对于物联网(IoT)问题,最常用的数据管理DM)方法论是 Chapman 等人提出的跨行业标准数据挖掘过程CRISP-DM)。这是一种过程模型,描述了成功完成数据挖掘所需执行的任务。它是一种供应商独立的方法论,分为六个不同的阶段:

  1. 商业理解

  2. 数据理解

  3. 数据准备

  4. 建模

  5. 评估

  6. 部署

以下图表展示了不同的阶段:

CRISP-DM 中的不同阶段

如我们所见,这是一个连续的过程模型,数据科学和 AI 在第 2 到第 5 步骤中起着重要作用。

关于 CRISP-DM 及其所有阶段的详细信息,可以在以下内容中阅读:

Marbán, Óscar, Gonzalo Mariscal, 和 Javier Segovia。 一个数据挖掘与知识发现过程模型《数据挖掘与现实生活应用中的知识发现》。InTech,2009 年。

AI 平台和 IoT 平台

今天有大量的云平台同时具备 AI 和 IoT 能力。这些平台提供集成传感器和设备并在云中执行分析的能力。全球市场上存在 30 多种云平台,每种平台针对不同的 IoT 垂直领域和服务。以下截图列出了 AI/IoT 平台支持的各种服务:

不同 AI/IoT 平台支持的服务

让我们简要了解一些流行的云平台。在第十二章, 将所有内容结合起来,我们将学*如何使用最流行的云平台。以下是一些流行的云平台的列表:

  • IBM Watson IoT 平台:由 IBM 托管,该平台提供设备管理;它使用 MQTT 协议与 IoT 设备和应用程序连接。它提供实时可扩展的连接性。数据可以存储一段时间并实时访问。IBM Watson 还提供 Bluemix 平台即服务PaaS)用于分析和可视化。我们可以编写代码来构建和管理与数据和连接设备交互的应用程序。它支持 Python、C#、Java 和 Node.js。

  • Microsoft IoT-Azure IoT 套件:它提供一系列基于 Azure PaaS 的预配置解决方案。它使得 IoT 设备与云之间实现可靠和安全的双向通信。预配置的解决方案包括数据可视化、远程监控以及对实时 IoT 遥测数据进行规则和警报配置。它还提供 Azure Stream Analytics 用于实时处理数据。Azure Stream Analytics 允许我们使用 Visual Studio。它支持 Python、Node.js、C 和 Arduino,具体取决于 IoT 设备。

  • Google Cloud IoT:Google Cloud IoT 提供一个完全托管的服务,用于安全连接和管理 IoT 设备。它支持 MQTT 和 HTTP 协议。它还提供 IoT 设备与云之间的双向通信。它支持 Go、PHP、Ruby、JS、.NET、Java、Objective-C 和 Python。它还提供 BigQuery,允许用户进行数据分析和可视化。

  • Amazon AWS IoT:Amazon AWS IoT 允许 IoT 设备通过 MQTT、HTTP 和 WebSockets 进行通信。它提供安全的双向通信,连接 IoT 设备与云端。它还拥有一个规则引擎,可以将数据与其他 AWS 服务集成并进行数据转换。可以定义规则,触发在 Java、Python 或 Node.js 中的用户代码执行。AWS Lambda 使我们能够使用自己训练的模型。

本书中使用的工具

为了实现基于 IoT 的服务,我们需要遵循自下而上的方法。对于每个 IoT 垂直领域,我们需要找到分析方法和数据,最后将其实现为代码。

由于 Python 在几乎所有的 AI 和 IoT 平台上都有广泛的支持,本书将使用 Python 进行编码。除了 Python,还会使用一些辅助库,如 NumPy、pandas、SciPy、Keras 和 TensorFlow 来对数据进行 AI/ML 分析。对于可视化,我们将使用 Matplotlib 和 Seaborn。

TensorFlow

TensorFlow 是一个由 Google Brain 团队开发的开源软件库,提供了实现深度神经网络的函数和 API。它支持 Python、C++、Java、R 和 Go。它可以用于多个平台,包括 CPU、GPU、移动设备,甚至是分布式系统。TensorFlow 允许模型部署,并且在生产环境中使用更为便捷。TensorFlow 中的优化器通过自动计算梯度并应用这些梯度来更新权重和偏置,从而简化了训练深度神经网络的任务。

在 TensorFlow 中,一个程序有两个不同的组件:

  • 计算图 是一个由节点和边组成的网络。在这里,所有的数据、变量、占位符以及要执行的计算都被定义。TensorFlow 支持三种数据对象:常量、变量和占位符。

  • 执行图 实际上是通过 Session 对象来计算网络的。实际的计算和从一个层到另一个层的信息传递发生在 Session 对象中。

让我们来看一下在 TensorFlow 中执行矩阵乘法的代码。完整的代码可以从 GitHub 仓库中访问(github.com/PacktPublishing/Hands-On-Artificial-Intelligence-for-IoT),文件名为 matrix_multiplication.ipynb

import tensorflow as tf
import numpy as np

这部分导入了 TensorFlow 模块。接下来,我们定义计算图。mat1mat2 是我们需要相乘的两个矩阵:

# A random matrix of size [3,5]
mat1 = np.random.rand(3,5)  
# A random matrix of size [5,2]
mat2 = np.random.rand(5,2)  

我们声明两个占位符 AB,这样我们就可以在运行时传递它们的值。在计算图中,我们声明所有的数据和计算对象:

# Declare placeholders for the two matrices 
A = tf.placeholder(tf.float32, None, name='A')
B = tf.placeholder(tf.float32, None, name='B')  

这声明了两个名为 AB 的占位符;tf.placeholder 方法的参数指定占位符的数据类型为 float32。由于指定的形状是 None,我们可以传入任意形状的张量,并为操作指定一个可选的名称。接下来,我们定义了使用矩阵乘法方法 tf.matmul 要执行的操作:

C = tf.matmul(A,B)

执行图被声明为一个 Session 对象,两个矩阵 mat1mat2 分别被传入占位符 AB 中:

with tf.Session() as sess:
    result = sess.run(C, feed_dict={A: mat1, B:mat2})
    print(result)

Keras

Keras 是一个运行在 TensorFlow 之上的高级 API。它允许快速且轻松的原型开发。它支持卷积神经网络和循环神经网络,甚至是两者的组合。它可以在 CPU 和 GPU 上运行。以下代码使用 Keras 执行矩阵乘法:

# Import the libraries
import keras.backend as K
import numpy as np

# Declare the data
A = np.random.rand(20,500)
B = np.random.rand(500,3000)

#Create Variable
x = K.variable(value=A)
y = K.variable(value=B)
z = K.dot(x,y)
print(K.eval(z))

数据集

在接下来的章节中,我们将学*不同的深度学*(DL)模型和机器学*(ML)方法。它们都基于数据进行工作;尽管有大量数据集可供展示这些模型的工作方式,但在本书中,我们将使用通过无线传感器和其他物联网(IoT)设备免费提供的数据集。以下是本书中使用的一些数据集及其来源。

联合循环电厂数据集

该数据集包含 9,568 个数据点,这些数据点是在六年的时间里(2006-2011)从一个联合循环电厂CCPP)收集的。CCPP 使用两台涡轮机发电,分别是燃气涡轮机和蒸汽涡轮机。CCPP 电厂的三个主要组成部分是:燃气涡轮机、热回收系统和蒸汽涡轮机。该数据集可在 UCI 机器学*库中获取(archive.ics.uci.edu/ml/datasets/combined+cycle+power+plant),由 Namik Kemal 大学的 Pinar Tufekci 和 Bogazici 大学的 Heysem Kaya 收集。数据包含四个特征,这些特征决定了平均环境变量。这些平均值是从电厂周围的各种传感器收集的,传感器每秒记录一次环境变量。目标是预测每小时的净电能输出。数据以xlsods格式提供。

数据集中的特征如下:

  • 环境温度AT)的范围是 1.81°C 到 37.11°C

  • 环境压力AP)的范围是 992.89 至 1033.30 毫巴

  • 相对湿度RH)的范围是 25.56%到 100.16%

  • 排气真空V)的范围是 25.36 至 81.56 厘米汞柱

  • 每小时净电能输出(PE)的范围是 420.26 至 495.76 兆瓦

关于数据和问题的更多细节可以从以下内容中了解:

  • Pınar Tüfekci,基于机器学*方法预测基荷运行的联合循环电厂满负荷电力输出,《国际电力与能源系统期刊》,第 60 卷,2014 年 9 月,第 126-140 页,ISSN 0142-0615。

  • Heysem Kaya, Pınar Tüfekci, Sadık Fikret Gürgen: 结合燃气与蒸汽涡轮机的功率预测的本地与全球学*方法,《2012 年国际计算机与电子工程新兴趋势会议论文集》ICETCEE 2012,第 13-18 页(2012 年 3 月,迪拜)。

葡萄酒质量数据集

世界各地的酒厂必须进行葡萄酒认证和质量评估,以保障人类健康。葡萄酒认证通过物理化学分析和感官测试进行。随着技术的进步,物理化学分析可以通过体外设备常规进行。

我们在本书中使用这个数据集进行分类示例。该数据集可以从 UCI-ML 存储库下载(archive.ics.uci.edu/ml/datasets/Wine+Quality)。葡萄酒质量数据集包含了对不同样本的红葡萄酒和白葡萄酒进行的物理化学测试结果。每个样本还由专家品酒师根据 0 到 10 的评分标准评定质量。

该数据集共有 4,898 个实例,总共有 12 个属性。这 12 个属性如下:

  • 固定酸度

  • 挥发性酸度

  • 柠檬酸

  • 残余糖

  • 氯化物

  • 游离二氧化硫

  • 总二氧化硫

  • 密度

  • pH

  • 硫酸盐

  • 酒精

  • 质量

该数据集提供 CSV 格式文件。

关于该数据集的详细信息可以参见这篇论文:Cortez, Paulo 等人。通过数据挖掘葡萄酒偏好模型。《决策支持系统》47.4(2009):547-553(repositorium.sdum.uminho.pt/bitstream/1822/10029/1/wine5.pdf)。

空气质量数据

空气污染是对人类健康的重大环境风险。研究发现,改善空气质量与缓解各种健康问题之间存在相关性,例如呼吸道感染、心血管疾病和肺癌。世界各国的气象组织通过广泛的传感器网络为我们提供了实时空气质量数据。这些数据可以通过各个组织提供的网络 API 进行访问。

在本书中,我们将使用历史空气质量数据来训练我们的网络并预测死亡率。英国的历史数据可以通过 Kaggle 自由获取(www.kaggle.com/c/predict-impact-of-air-quality-on-death-rates),这些空气质量数据包括日均臭氧O3)、二氧化氮NO2)、直径小于或等于 10 微米的颗粒物(PM10)和 PM2.5(2.5 微米或更小)以及温度。英国地区的死亡率(每 10 万人中的死亡人数)由英国国家统计局提供的数据得出。

摘要

在本章中,我们学*了物联网、大数据和人工智能。本章介绍了物联网中常用的术语。我们了解了物联网架构在数据管理和数据分析中的应用。物联网设备生成的大量数据需要特殊的方式来处理。

我们了解了数据科学和人工智能如何帮助进行分析和预测,尤其是在许多物联网设备所生成的数据中。本章简要介绍了各种物联网平台以及一些流行的物联网行业。我们还了解了专用的深度学*库:TensorFlow 和 Keras。最后,我们介绍了本书中将要使用的一些数据集。

下一章将介绍如何访问以不同格式提供的数据集。

第二章:IoT 数据访问与分布式处理

数据无处不在:图像、语音、文本、天气信息、你车速、你的最后一次 EMI、变动的股价。随着 物联网IoT)系统的集成,产生的数据量大幅增加;例如,传感器读数可以用于房间温度、土壤碱度等。这些数据以各种格式存储并提供访问。在本章中,我们将学*如何读取、保存和处理一些流行格式的数据。具体来说,你将执行以下操作:

  • 访问 TXT 格式的数据

  • 通过 CSV、pandas 和 NumPy 模块读取和写入 csv 格式数据

  • 使用 JSON 和 pandas 访问 JSON 数据

  • 学*如何使用 PyTables、pandas 和 h5py 处理 HDF5 格式

  • 使用 SQLite 和 MySQL 处理 SQL 数据库

  • 使用 MongoDB 处理 NoSQL 数据库

  • 使用 Hadoop 的分布式文件系统

TXT 格式

存储数据的最简单和常见格式之一是 TXT 格式;许多 IoT 传感器以简单的 .txt 文件格式记录传感器读取数据及其时间戳。Python 提供了内置函数,用于创建、读取和写入 TXT 文件。

我们可以在 Python 中直接访问 TXT 文件,而无需使用任何模块;在这种情况下,数据为字符串类型,你需要将其转换为其他类型以便使用。或者,我们可以使用 NumPy 或 pandas。

在 Python 中使用 TXT 文件

Python 提供了内置函数,用于读取和写入 TXT 文件。完整的功能通过四组函数实现:open()read()write()close()。顾名思义,它们分别用于打开文件、读取文件、写入文件和最终关闭文件。如果你处理的是字符串数据(文本),这是最佳选择。在本节中,我们将使用 Shakespeare 的剧本,以 TXT 格式存储;文件可以从 MIT 网站下载:ocw.mit.edu/ans7870/6/6.006/s08/lecturenotes/files/t8.shakespeare.txt

我们定义以下变量来访问数据:

data_folder = '../../data/Shakespeare'
data_file = 'alllines.txt'

这里的第一步是打开文件:

f = open(data_file)

接下来,我们读取整个文件;我们可以使用 read 函数,它会将整个文件读取为一个单一的字符串:

contents = f.read()

这会将整个文件(包含 4,583,798 个字符)读取到 contents 变量中。让我们来查看 contents 变量的内容;以下命令将打印出前 1000 个字符:

print(contents[:1000])

上述代码将输出如下结果:

"ACT I"
"SCENE I. London. The palace."
"Enter KING HENRY, LORD JOHN OF LANCASTER, the EARL of WESTMORELAND, SIR WALTER BLUNT, and others"
"So shaken as we are, so wan with care,"
"Find we a time for frighted peace to pant,"
"And breathe short-winded accents of new broils"
"To be commenced in strands afar remote."
"No more the thirsty entrance of this soil"
"will daub her lips with her own children's blood,"
"Nor more will trenching war channel her fields,"
"Nor bruise her flowerets with the armed hoofs"
"Of hostile paces: those opposed eyes,"
"Which, like the meteors of a troubled heaven,"
"All of one nature, of one substance bred,"
"Did lately meet in the intestine shock"
"And furious close of civil butchery"
"will now, in mutual well-beseeming ranks,"
"March all one way and be no more opposed"
"Against acquaintance, kindred and allies:"
"The edge of war, like an ill-sheathed knife,"
"No more will cut his master. Therefore, friends,"
"As far as to the sepulchre of Christ,"
"Whose

如果 TXT 文件包含数值数据,最好使用 NumPy;如果数据混合,pandas 是最佳选择。

CSV 格式

逗号分隔值CSV)文件是由物联网系统生成的存储表格数据的最流行格式。在 .csv 文件中,记录的值存储在纯文本行中,每行包含由分隔符分隔的字段值。默认情况下分隔符是逗号,但可以配置为任何其他字符。在本节中,我们将学*如何使用 Python 的 csvnumpypandas 模块处理 CSV 文件中的数据。我们将使用 household_power_consumption 数据文件。该文件可以从以下 GitHub 链接下载:github.com/ahanse/machlearning/blob/master/household_power_consumption.csv。为了访问数据文件,我们定义以下变量:

data_folder = '../../data/household_power_consumption' 
data_file = 'household_power_consumption.csv'

一般来说,为了快速从 CSV 文件中读取数据,使用 Python 的 csv 模块;但是,如果数据需要被解释为日期和数字数据字段的混合,最好使用 pandas 包。如果数据只是数字,NumPy 是最合适的包。

使用 csv 模块处理 CSV 文件

在 Python 中,csv 模块提供了用于读写 CSV 文件的类和方法。csv.reader 方法从中创建一个可迭代地读取行的 reader 对象。每次从文件中读取一行时,reader 对象返回一个字段列表。例如,以下代码演示了如何读取数据文件并打印行:

import csv
import os

with open(os.path.join(data_folder,data_file),newline='') as csvfile:
   csvreader = csv.reader(csvfile)
   for row in csvreader:
     print(row)

行作为字段值列表打印:

['date', 'time', 'global_active_power', 'global_reactive_power', 'voltage', 'global_intensity', 'sub_metering_1', 'sub_metering_2', 'sub_metering_3'] ['0007-01-01', '00:00:00', '2.58', '0.136', '241.97', '10.6', '0', '0', '0'] ['0007-01-01', '00:01:00', '2.552', '0.1', '241.75', '10.4', '0', '0', '0'] ['0007-01-01', '00:02:00', '2.55', '0.1', '241.64', '10.4', '0', '0', '0']

csv.writer 方法返回一个对象,可以用来向文件写入行。例如,以下代码将文件的前 10 行写入临时文件,然后打印它:

# read the file and write first ten rows
with open(os.path.join(data_folder, data_file), newline='') as csvfile, \
        open(os.path.join(data_folder, 'temp.csv'), 'w', newline='') as tempfile:
    csvreader = csv.reader(csvfile)
    csvwriter = csv.writer(tempfile)
    for row, i in zip(csvreader, range(10)):
        csvwriter.writerow(row)

# read and print the newly written file
with open(os.path.join(data_folder, 'temp.csv'), newline='') as tempfile:
    csvreader = csv.reader(tempfile)
    for row in csvreader:
        print(row)

delimiter 字段和 quoting 字段字符是创建 readerwriter 对象时可以设置的重要属性。

默认情况下,delimiter 字段为 ,,其他分隔符可以通过 readerwriter 函数的 delimiter 参数指定。例如,以下代码将文件保存为 | 作为 delimiter

    # read the file and write first ten rows with '|' delimiter
with open(os.path.join(data_folder, data_file), newline='') as csvfile, \
        open(os.path.join(data_folder, 'temp.csv'), 'w', newline='') as tempfile:
    csvreader = csv.reader(csvfile)
    csvwriter = csv.writer(tempfile, delimiter='|')
    for row, i in zip(csvreader, range(10)):
        csvwriter.writerow(row)

# read and print the newly written file
with open(os.path.join(data_folder, 'temp.csv'), newline='') as tempfile:
    csvreader = csv.reader(tempfile, delimiter='|')
    for row in csvreader:
        print(row)

如果在读取文件时未指定 delimiter 字符,则行将作为一个字段读取并打印如下:

['0007-01-01|00:00:00|2.58|0.136|241.97|10.6|0|0|0']

quotechar 指定用于包围字段的字符。quoting 参数指定哪种字段可以用 quotechar 包围。quoting 参数可以有以下值之一:

  • csv.QUOTE_ALL:所有字段都加引号

  • csv.QUOTE_MINIMAL:只有包含特殊字符的字段才加引号

  • csv.QUOTE_NONNUMERIC:所有非数值字段都加引号

  • csv.QUOTE_NONE:所有字段都不加引号

作为例子,让我们首先打印临时文件:

0007-01-01|00:00:00|2.58|0.136|241.97|10.6|0|0|0
0007-01-01|00:01:00|2.552|0.1|241.75|10.4|0|0|0
0007-01-01|00:02:00|2.55|0.1|241.64|10.4|0|0|0
0007-01-01|00:03:00|2.55|0.1|241.71|10.4|0|0|0
0007-01-01|00:04:00|2.554|0.1|241.98|10.4|0|0|0
0007-01-01|00:05:00|2.55|0.1|241.83|10.4|0|0|0
0007-01-01|00:06:00|2.534|0.096|241.07|10.4|0|0|0
0007-01-01|00:07:00|2.484|0|241.29|10.2|0|0|0
0007-01-01|00:08:00|2.468|0|241.23|10.2|0|0|0

现在让我们用所有字段都加引号保存它:

# read the file and write first ten rows with '|' delimiter, all quoting and * as a quote charachetr.
with open(os.path.join(data_folder, data_file), newline='') as csvfile, \
        open('temp.csv', 'w', newline='') as tempfile:
    csvreader = csv.reader(csvfile)
    csvwriter = csv.writer(tempfile, delimiter='|', quotechar='*',quoting=csv.QUOTE_ALL)
    for row, i in zip(csvreader, range(10)):
        csvwriter.writerow(row)

文件将使用指定的引号字符保存:

*0007-01-01*|*00:00:00*|*2.58*|*0.136*|*241.97*|*10.6*|*0*|*0*|*0*
*0007-01-01*|*00:01:00*|*2.552*|*0.1*|*241.75*|*10.4*|*0*|*0*|*0*
*0007-01-01*|*00:02:00*|*2.55*|*0.1*|*241.64*|*10.4*|*0*|*0*|*0*
*0007-01-01*|*00:03:00*|*2.55*|*0.1*|*241.71*|*10.4*|*0*|*0*|*0*
*0007-01-01*|*00:04:00*|*2.554*|*0.1*|*241.98*|*10.4*|*0*|*0*|*0*
*0007-01-01*|*00:05:00*|*2.55*|*0.1*|*241.83*|*10.4*|*0*|*0*|*0*
*0007-01-01*|*00:06:00*|*2.534*|*0.096*|*241.07*|*10.4*|*0*|*0*|*0*
*0007-01-01*|*00:07:00*|*2.484*|*0*|*241.29*|*10.2*|*0*|*0*|*0*
*0007-01-01*|*00:08:00*|*2.468*|*0*|*241.23*|*10.2*|*0*|*0*|*0*

记得使用相同的参数读取文件;否则,* 引号字符将被视为字段值的一部分,并打印如下:

['*0007-01-01*', '*00:00:00*', '*2.58*', '*0.136*', '*241.97*', '*10.6*', '*0*', '*0*', '*0*']

使用正确的参数与reader对象一起打印如下内容:

['0007-01-01', '00:00:00', '2.58', '0.136', '241.97', '10.6', '0', '0', '0']

现在让我们看看如何使用 pandas,另一个流行的 Python 库,来读取 CSV 文件。

使用 pandas 模块处理 CSV 文件

在 pandas 中,read_csv()函数读取 CSV 文件后返回一个 DataFrame:

df = pd.read_csv('temp.csv')
print(df)

DataFrame 将如下所示地打印:

         date      time  global_active_power  global_reactive_power  voltage  \
0  0007-01-01  00:00:00                2.580                  0.136   241.97   
1  0007-01-01  00:01:00                2.552                  0.100   241.75   
2  0007-01-01  00:02:00                2.550                  0.100   241.64   
3  0007-01-01  00:03:00                2.550                  0.100   241.71   
4  0007-01-01  00:04:00                2.554                  0.100   241.98   
5  0007-01-01  00:05:00                2.550                  0.100   241.83   
6  0007-01-01  00:06:00                2.534                  0.096   241.07   
7  0007-01-01  00:07:00                2.484                  0.000   241.29   
8  0007-01-01  00:08:00                2.468                  0.000   241.23   

   global_intensity  sub_metering_1  sub_metering_2  sub_metering_3  
0              10.6               0               0               0  
1              10.4               0               0               0  
2              10.4               0               0               0  
3              10.4               0               0               0  
4              10.4               0               0               0  
5              10.4               0               0               0  
6              10.4               0               0               0  
7              10.2               0               0               0  
8              10.2               0               0               0  

我们在前面的输出中看到,pandas 自动将datetime列解释为相应的数据类型。pandas 的 DataFrame 可以通过to_csv()函数保存到 CSV 文件中:

df.to_csv('temp1.cvs')

pandas 在读取和写入 CSV 文件时提供了很多参数。以下是其中的一些及其使用方法:

  • header:定义要用作标题的行号,如果文件没有标题则为“无”。

  • sep:定义行中字段分隔符的字符。默认情况下,sep的值设置为,

  • names:为文件中的每一列定义列名。

  • usecols:定义需要从 CSV 文件中提取的列。没有在此参数中提到的列将不会被读取。

  • dtype:定义 DataFrame 中各列的字段类型。

其他许多可用选项可以在以下链接中查阅:pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.htmlpandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_csv.html

现在让我们看看如何使用 NumPy 模块读取 CSV 文件中的数据。

使用 NumPy 模块处理 CSV 文件

NumPy 模块提供了两种从 CSV 文件读取值的函数:np.loadtxt()np.genfromtxt()

np.loadtxt的一个示例如下:

arr = np.loadtxt('temp.csv', skiprows=1, usecols=(2,3), delimiter=',')
arr

上述代码读取了我们之前创建的文件中的第3列和第4列,并将它们保存到一个 9×2 的数组中,如下所示:

array([[2.58 , 0.136],
       [2.552, 0.1  ],
       [2.55 , 0.1  ],
       [2.55 , 0.1  ],
       [2.554, 0.1  ],
       [2.55 , 0.1  ],
       [2.534, 0.096],
       [2.484, 0\.   ],
       [2.468, 0\.   ]])

np.loadtxt()函数无法处理缺失数据的 CSV 文件。对于缺失数据的情况,可以使用np.genfromtxt()。这两个函数都提供了许多其他参数;详细信息可以参考 NumPy 文档。以下代码可以使用np.genfromtxt()重写:

arr = np.genfromtxt('temp.csv', skip_header=1, usecols=(2,3), delimiter=',')

作为应用 AI 于物联网数据后得到的 NumPy 数组,可以使用np.savetxt()保存。例如,之前加载的数组可以按如下方式保存:

np.savetxt('temp.csv', arr, delimiter=',')

np.savetxt()函数也接受各种其他有用的参数,例如保存字段和标题的格式。有关此函数的更多详细信息,请查看 NumPy 文档。

CSV 是物联网平台和设备上最流行的数据格式。在本节中,我们学*了如何使用 Python 的三个不同包来读取 CSV 数据。在下一节中,我们将学*另一个流行格式——XLSX。

XLSX 格式

Excel 是 Microsoft Office 套件中的一个组件,是存储和可视化数据的流行格式之一。从 2010 年起,Office 开始支持 .xlsx 格式。我们可以使用 OpenPyXl 和 pandas 函数来读取 XLSX 文件。

使用 OpenPyXl 处理 XLSX 文件

OpenPyXl 是一个用于读取和写入 Excel 文件的 Python 库。它是一个开源项目。通过以下命令可以创建一个新的 workbook

wb = Workbook()

我们可以通过以下命令访问当前 active 工作表:

ws = wb.active()

要更改工作表名称,使用 title 命令:

ws.title = "Demo Name"

可以使用 append 方法将一行数据添加到工作表:

ws.append()

可以使用 create_sheet() 方法创建一个新的工作表。可以通过 columnrow 值在活动工作表中创建一个单独的单元格:

# Assigns the cell corresponding to 
# column A and row 10 a value of 5
ws.['A10'] = 5  
#or
ws.cell(column=1, row=10, value=5)

可以使用 save 方法保存工作簿。要加载现有工作簿,我们可以使用 load_workbook 方法。可以通过 get_sheet_names() 获取 Excel 工作簿中不同工作表的名称。

以下代码创建一个包含三个工作表的 Excel 工作簿并保存;之后,它加载该工作表并访问一个单元格。代码可以在 GitHub 上通过 OpenPyXl_example.ipynb 访问:

# Creating and writing into xlsx file
from openpyxl import Workbook
from openpyxl.compat import range
from openpyxl.utils import get_column_letter
wb = Workbook()
dest_filename = 'empty_book.xlsx'
ws1 = wb.active
ws1.title = "range names"
for row in range(1, 40):
 ws1.append(range(0,100,5))
ws2 = wb.create_sheet(title="Pi")
ws2['F5'] = 2 * 3.14
ws2.cell(column=1, row=5, value= 3.14)
ws3 = wb.create_sheet(title="Data")
for row in range(1, 20):
 for col in range(1, 15):
 _ = ws3.cell(column=col, row=row, value="\
 {0}".format(get_column_letter(col)))
print(ws3['A10'].value)
wb.save(filename = dest_filename)

# Reading from xlsx file
from openpyxl import load_workbook
wb = load_workbook(filename = 'empty_book.xlsx')
sheet_ranges = wb['range names']
print(wb.get_sheet_names())
print(sheet_ranges['D18'].value)

您可以通过其文档了解更多关于 OpenPyXL 的信息,文档地址为 openpyxl.readthedocs.io/en/stable/

使用 pandas 处理 XLSX 文件

我们可以借助 pandas 加载现有的 .xlsx 文件。read_excel 方法用于将 Excel 文件读取为 DataFrame。该方法使用一个参数 sheet_name,用于指定我们要加载的工作表。工作表名称可以通过字符串或从 0 开始的数字来指定。to_excel 方法可以用来写入 Excel 文件。

以下代码读取一个 Excel 文件,对其进行操作并保存。代码可以在 GitHub 上通过 Pandas_xlsx_example.ipynb 访问:

import pandas as pd
df = pd.read_excel("empty_book.xlsx", sheet_name=0)
df.describe()
result = df * 2
result.describe()
result.to_excel("empty_book_modified.xlsx")

处理 JSON 格式

JavaScript 对象 表示法JSON)是物联网系统中另一种流行的数据格式。在本节中,我们将学*如何使用 Python 的 JSON、NumPy 和 pandas 包来读取 JSON 数据。

在本节中,我们将使用 zips.json 文件,该文件包含美国 ZIP 代码、城市代码、地理位置详情和州代码。该文件中的 JSON 对象按以下格式记录:

{ "_id" : "01001", "city" : "AGAWAM", "loc" : [ -72.622739, 42.070206 ], "pop" : 15338, "state" : "MA" }

使用 JSON 模块处理 JSON 文件

要加载和解码 JSON 数据,使用 json.load()json.loads() 函数。作为示例,以下代码读取 zips.json 文件中的前 10 行并美观地打印出来:

import os
import json
from pprint import pprint

with open(os.path.join(data_folder,data_file)) as json_file:
    for line,i in zip(json_file,range(10)):
        json_data = json.loads(line)
        pprint(json_data)

这些对象的打印结果如下:

{'_id': '01001',
 'city': 'AGAWAM',
 'loc': [-72.622739, 42.070206],
 'pop': 15338,
 'state': 'MA'}

json.loads() 函数接受字符串对象作为输入,而 json.load() 函数接受文件对象作为输入。两者都会解码 JSON 对象并将其加载为 Python 字典对象,存储在 json_data 文件中。

json.dumps()函数接受一个对象并生成一个 JSON 字符串,而json.dump()函数接受一个对象并将 JSON 字符串写入文件。因此,这两个函数的作用正好与json.loads()json.load()函数相反。

使用 pandas 模块操作 JSON 文件

JSON 字符串或文件可以使用pandas.read_json()函数读取,该函数返回一个 DataFrame 或 Series 对象。例如,以下代码读取zips.json文件:

df = pd.read_json(os.path.join(data_folder,data_file), lines=True)
print(df)

我们设置lines=True,因为每行包含一个单独的 JSON 格式对象。如果没有将这个参数设置为True,pandas 会引发ValueError。DataFrame 如下所示:

         _id             city                               loc    pop state
0       1001           AGAWAM           [-72.622739, 42.070206]  15338    MA
1       1002          CUSHMAN            [-72.51565, 42.377017]  36963    MA
...      ...              ...                               ...    ...   ...
29351  99929         WRANGELL          [-132.352918, 56.433524]   2573    AK
29352  99950        KETCHIKAN           [-133.18479, 55.942471]    422    AK

[29353 rows x 5 columns]

要将 pandas 的 DataFrame 或 Series 对象保存到 JSON 文件或字符串中,可以使用Dataframe.to_json()函数。

这两个函数的更多信息可以在以下链接中找到:pandas.pydata.org/pandas-docs/stable/generated/pandas.read_json.htmlpandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.to_json.html

尽管 CSV 和 JSON 仍然是 IoT 数据中最受欢迎的数据格式,但由于其庞大的体积,通常需要分发数据。有两种流行的分布式数据存储和访问机制:HDF5 和 HDFS。让我们首先了解一下 HDF5 格式。

HDF5 格式

层次数据格式HDF)是由 HDF 集团(一个由学术界和行业组织组成的联盟)制定的规范(support.hdfgroup.org/HDF5/)。在 HDF5 文件中,数据被组织成组和数据集。一个组是数据集的集合。数据集是一个多维的同质数组。

在 Python 中,PyTables 和 h5py 是处理 HDF5 文件的两个主要库。这两个库都要求安装 HDF5。对于 HDF5 的并行版本,还需要安装 MPI 版本。HDF5 和 MPI 的安装超出了本书的范围。并行 HDF5 的安装说明可以在以下链接中找到:support.hdfgroup.org/ftp/HDF5/current/src/unpacked/release_docs/INSTALL_parallel

使用 PyTables 操作 HDF5

让我们首先通过以下步骤,从temp.csv文件中的数字数据创建一个 HDF5 文件:

  1. 获取数字数据:
import numpy as np
arr = np.loadtxt('temp.csv', skiprows=1, usecols=(2,3), delimiter=',')
  1. 打开 HDF5 文件:
import tables
h5filename = 'pytable_demo.hdf5'
with tables.open_file(h5filename,mode='w') as h5file:
  1. 获取root节点:
    root = h5file.root
  1. 使用create_group()创建一个组,或使用create_array()创建一个数据集,并重复此操作直到所有数据都被存储:
    h5file.create_array(root,'global_power',arr)
  1. 关闭文件:
    h5file.close()

让我们读取文件并打印数据集,以确保它被正确写入:

with tables.open_file(h5filename,mode='r') as h5file:
    root = h5file.root
    for node in h5file.root:
        ds = node.read()
        print(type(ds),ds.shape)
        print(ds)

我们得到了 NumPy 数组。

使用 pandas 操作 HDF5

我们还可以使用 pandas 读取和写入 HDF5 文件。要使用 pandas 读取 HDF5 文件,必须先使用 pandas 创建它。例如,使用 pandas 创建一个包含全球电力值的 HDF5 文件:

import pandas as pd
import numpy as np
arr = np.loadtxt('temp.csv', skiprows=1, usecols=(2,3), delimiter=',')
import pandas as pd
store=pd.HDFStore('hdfstore_demo.hdf5')
print(store)
store['global_power']=pd.DataFrame(arr)
store.close()

现在让我们读取我们创建的 HDF5 文件,并将数组打印出来:

import pandas as pd
store=pd.HDFStore('hdfstore_demo.hdf5')
print(store)
print(store['global_power'])
store.close()

DataFrame 的值可以通过三种不同的方式读取:

  • store['global_power']

  • store.get('global_power')

  • store.global_power

pandas 还提供了高级的 read_hdf() 函数和 to_hdf() DataFrame 方法,用于读取和写入 HDF5 文件。

关于 pandas 中 HDF5 的更多文档,请访问以下链接:pandas.pydata.org/pandas-docs/stable/io.html#io-hdf5

使用 h5py 操作 HDF5

h5py 模块是处理 Python 中 HDF5 文件的最流行方式。可以使用 h5py.File() 函数打开一个新的或现有的 HDF5 文件。文件打开后,可以像字典对象一样通过下标访问其组。例如,以下代码使用 h5py 打开一个 HDF5 文件,然后打印 /global_power 组中存储的数组:

import h5py
hdf5file = h5py.File('pytable_demo.hdf5')
ds=hdf5file['/global_power']
print(ds)
for i in range(len(ds)):
    print(arr[i])
hdf5file.close()

arr 变量打印出 HDF5 数据集 类型:

<HDF5 dataset "global_power": shape (9, 2), type "<f8">
[2.58  0.136]
[2.552 0.1  ]
[2.55 0.1 ]
[2.55 0.1 ]
[2.554 0.1  ]
[2.55 0.1 ]
[2.534 0.096]
[2.484 0\.   ]
[2.468 0\.   ]

对于一个新的 hdf5file,可以通过使用 hdf5file.create_dataset() 函数来创建数据集,返回数据集对象,并通过使用 hdf5file.create_group() 函数来创建组,返回文件夹对象。hdf5file 文件对象也是一个文件夹对象,表示根文件夹 /。数据集对象支持类似数组的切片操作,可以用于设置或读取它们的值。例如,以下代码创建一个 HDF5 文件并存储一个数据集:

import numpy as np
arr = np.loadtxt('temp.csv', skiprows=1, usecols=(2,3), delimiter=',')

import h5py
hdf5file = h5py.File('h5py_demo.hdf5')
dataset1 = hdf5file.create_dataset('global_power',data=arr)
hdf5file.close()

h5py 提供了一个 attrs 代理对象,具有类似字典的接口,用于存储和检索有关文件、文件夹和数据集的元数据。例如,以下代码设置并打印数据集和文件属性:

dataset1.attrs['owner']='City Corp.'
print(dataset1.attrs['owner'])

hdf5file.attrs['security_level']='public'
print(hdf5file.attrs['security_level'])

更多关于 h5py 库的信息,请参考以下文档:docs.h5py.org/en/latest/index.html

到目前为止,我们已经了解了不同的数据格式。通常,大型数据会以商业形式存储在数据库中,因此接下来我们将探讨如何访问 SQL 和 NoSQL 数据库。

SQL 数据

大多数数据库使用关系模型组织。关系数据库由一个或多个相关的信息表组成,表之间的信息关系通过键来描述。传统上,这些数据库通过数据库管理系统DBMS)进行管理,DBMS 是与最终用户、不同应用程序和数据库本身进行交互的软件,用于捕获和分析数据。商业化的 DBMS 使用结构化查询语言SQL)来访问和操作数据库。我们也可以使用 Python 来访问关系型数据库。在本节中,我们将探索 SQLite 和 MySQL 这两个非常流行的数据库引擎,它们可以与 Python 一起使用。

SQLite 数据库引擎

根据 SQLite 主页(sqlite.org/index.html),SQLite 是一种独立的、高可靠性的、嵌入式的、功能齐全的、公有领域的 SQL 数据库引擎

SQLite 专为嵌入式应用程序优化。 它使用简单且速度相当快。 我们需要使用sqlite3 Python 模块将 SQLite 与 Python 集成。 sqlite3模块已与 Python 3 捆绑在一起,因此无需安装它。

我们将使用欧洲足球数据库(github.com/hugomathien/football-data-collection)进行演示。 假设您已经安装并启动了 SQL 服务器:

  1. 导入sqlite3后的第一步是使用connect方法创建到数据库的连接:
import sqlite3 
import pandas as pd
connection = sqlite3.connect('database.sqlite')
print("Database opened successfully")
  1. 欧洲足球数据库包括八张表。 我们可以使用read_sql将数据库表或 SQL 查询读取到 DataFrame 中。 这将打印出数据库中所有表的列表:
tables = pd.read_sql("SELECT * FROM sqlite_master WHERE 
        type='table';", connection)
print(tables)

  1. Country表中读取数据:
countries = pd.read_sql("SELECT * FROM Country;", connection)
countries.head()

  1. 我们可以在表上使用 SQL 查询。 在以下示例中,我们选择身高大于或等于180且体重大于或等于170的球员:
selected_players = pd.read_sql_query("SELECT * FROM Player WHERE
         height >= 180 AND weight >= 170 ", connection)
print(selected_players)

  1. 最后,不要忘记使用close方法关闭连接:
connection.close()

如果您对数据库进行了任何更改,则需要使用commit()方法。

MySQL 数据库引擎

虽然我们可以使用 SQLite 处理大型数据库,但通常首选 MySQL。 除了适用于大型数据库的可伸缩性外,MySQL 在数据安全性至关重要的情况下也很有用。 在使用 MySQL 之前,您需要安装 Python MySQL 连接器。 有许多可能的 Python MySQL 连接器,如 MySQLdb、PyMySQL 和 MySQL;我们将使用mysql-connector-python

在所有三种情况下,在使用connect方法建立连接后,我们定义cursor元素,并使用execute方法运行不同的 SQL 查询。 要安装 MySQL,请使用以下方式:

pip install mysql-connector-python 
  1. 现在 Python MySQL 连接器已安装,我们可以启动与 SQL 服务器的连接。 将hostuserpassword配置替换为您的 SQL 服务器配置:
import mysql.connector 
connection = mysql.connector.connect(host="127.0.0.1", # your host 
        user="root", # username
        password="**********" ) # password
  1. 让我们检查服务器中的现有数据库并列出它们。 为此,我们使用cursor方法:
mycursor = connection.cursor()
mycursor.execute("SHOW DATABASES")
for x in mycursor:
    print(x)

  1. 我们可以访问现有数据库之一。 让我们列出一个数据库中的表:
connection = mysql.connector.connect(host="127.0.0.1", # your host 
user="root", # username
password="**********" ,  #replace with your password
database = 'mysql')
mycursor = connection.cursor()
mycursor.execute("SHOW TABLES")
for x in mycursor:
    print(x)

NoSQL 数据

不仅仅是结构化查询语言NoSQL)数据库不是关系数据库;而是数据可以以键-值、JSON、文档、列或图格式存储。 它们在大数据和实时应用程序中经常使用。 我们将在这里学*如何使用 MongoDB 访问 NoSQL 数据,并假设您已正确配置并启动了 MongoDB 服务器:

  1. 我们需要使用 MongoClient 对象与 Mongo 守护进程建立连接。以下代码建立与默认主机 localhost 和端口(27017)的连接,并让我们访问数据库:
from pymongo import MongoClient
client = MongoClient()
db = client.test
  1. 在这个例子中,我们尝试将 scikit-learn 中可用的 cancer 数据集加载到 Mongo 数据库中。因此,我们首先获取乳腺癌数据集并将其转换为 pandas DataFrame:
from sklearn.datasets import load_breast_cancer
import pandas as pd

cancer = load_breast_cancer()
data = pd.DataFrame(cancer.data, columns=[cancer.feature_names])

data.head()
  1. 接下来,我们将其转换为 JSON 格式,使用 json.loads() 函数进行解码,并将解码后的数据插入到开放数据库中:
import json
data_in_json = data.to_json(orient='split')
rows = json.loads(data_in_json)
db.cancer_data.insert(rows)
  1. 这将创建一个名为 cancer_data 的集合,包含数据。我们可以使用 cursor 对象查询我们刚刚创建的文档:
cursor = db['cancer_data'].find({})
df = pd.DataFrame(list(cursor))
print(df)

在物联网中的分布式数据方面,Hadoop 分布式文件系统HDFS)是提供分布式数据存储和访问的另一种流行方法。在下一节中,我们将研究如何访问和存储 HDFS 中的数据。

HDFS

HDFS 是一种流行的存储和访问方法,用于存储和检索物联网解决方案中的数据文件。HDFS 格式能够以可靠且可扩展的方式存储大量数据。其设计基于 Google 文件系统ai.google/research/pubs/pub51)。HDFS 将单个文件分割成固定大小的块,这些块存储在集群中的多台机器上。为了确保可靠性,它会复制文件块并将它们分布在集群中;默认情况下,复制因子为 3。HDFS 有两个主要的架构组件:

  • 第一个,NodeName,存储整个文件系统的元数据,例如文件名、权限以及每个文件的每个块的位置。

  • 第二个,DataNode(一个或多个),是存储文件块的地方。它使用 protobufs 执行 远程过程调用RPC)。

RPC 是一种协议,允许一个程序请求位于网络上另一台计算机上的程序提供服务,而无需了解网络的详细信息。过程调用有时也称为 函数调用子例程调用

在 Python 中,有许多选项可以编程方式访问 HDFS,如 snakebitepyarrowhdfs3pywebhdfshdfscli 等。在这一节中,我们将主要关注提供本地 RPC 客户端接口并与 Python 3 配合使用的库。

Snakebite 是一个纯 Python 模块和命令行工具,允许您从 Python 程序访问 HDFS。目前,它仅支持 Python 2,不支持 Python 3。此外,它尚不支持写操作,因此我们没有将其包含在书中。然而,如果您有兴趣了解更多,可以参考 Spotify 的 GitHub:github.com/spotify/snakebite

使用 hdfs3 访问 HDFS

hdfs3是一个轻量级的 Python 封装,封装了 C/C++的libhdfs3库。它允许我们从 Python 原生使用 HDFS。首先,我们需要通过HDFileSystem类连接到 HDFS 的 NameNode:

from hdfs3 import HDFileSystem
hdfs = HDFileSystem(host = 'localhost', port=8020)

这会自动与 NameNode 建立连接。现在,我们可以使用以下命令访问目录列表:

print(hdfs.ls('/tmp')) 

这将列出tmp文件夹中的所有文件和目录。你可以使用诸如mkdir的函数来创建目录,使用cp来将文件从一个位置复制到另一个位置。要向文件写入数据,我们首先使用open方法打开文件,然后使用write

with hdfs.open('/tmp/file1.txt','wb') as f:
    f.write(b'You are Awesome!')

可以从文件中读取数据:

with hdfs.open('/tmp/file1.txt') as f:
    print(f.read())

你可以通过它的文档了解更多关于hdfs3的信息:media.readthedocs.org/pdf/hdfs3/latest/hdfs3.pdf

使用 PyArrow 的 HDFS 文件系统接口

PyArrow 提供了一个基于 C++的 HDFS 接口。默认情况下,它使用libhdfs,这是一个基于 JNI 的接口,用于 Java Hadoop 客户端。我们也可以选择使用libhdfs3,这是一个 C++库,用于 HDFS。我们使用hdfs.connect连接到 NameNode:

import pyarrow as pa
hdfs = pa.hdfs.connect(host='hostname', port=8020, driver='libhdfs')

如果我们将驱动程序更改为libhdfs3,我们将使用 Pivotal Labs 提供的 C++库来访问 HDFS。一旦与 NameNode 建立连接,文件系统将使用与 hdfs3 相同的方法进行访问。

当数据量非常庞大时,HDFS 是首选。它允许我们分块读取和写入数据;这对于访问和处理流数据非常有帮助。以下博客文章提供了三种原生 RPC 客户端接口的良好比较:wesmckinney.com/blog/python-hdfs-interfaces/

小结

本章介绍了许多不同的数据格式,并且在过程中使用了许多不同的数据集。我们从最简单的 TXT 数据开始,并访问了莎士比亚的剧本数据。我们学*了如何使用csvnumpypandas模块从 CSV 文件中读取数据。接着我们转向了 JSON 格式,使用了 Python 的 JSON 和 pandas 模块来访问 JSON 数据。从数据格式的学*,我们逐步进入了数据库的访问,涵盖了 SQL 和 NoSQL 数据库。接下来,我们学*了如何在 Python 中使用 Hadoop 文件系统。

访问数据是第一步。在下一章中,我们将学*一些机器学*工具,这些工具将帮助我们设计、建模,并在数据上做出明智的预测。

第三章:物联网的机器学*

机器学*ML)指的是能够自动检测数据中有意义的模式并通过经验不断改进的计算机程序。尽管这不是一个新兴领域,但目前正处于其兴奋周期的顶峰。本章将向读者介绍标准的机器学*算法及其在物联网领域的应用。

阅读本章后,您将了解以下内容:

  • 机器学*(ML)是什么,它在物联网(IoT)管道中扮演的角色

  • 监督学*和无监督学*的范式

  • 回归分析以及如何使用 TensorFlow 和 Keras 进行线性回归

  • 流行的机器学*分类器,并在 TensorFlow 和 Keras 中实现它们

  • 决策树、随机森林以及执行提升的技术和如何为它们编写代码

  • 提升系统性能和模型局限性的技巧与方法

机器学*与物联网(IoT)

机器学*(ML)是人工智能的一个子集,旨在构建能够通过经验自动学*和改进的计算机程序,而无需明确编程。在大数据时代,随着数据生成速度惊人地加快,人类无法逐一处理和理解所有数据。根据思科(Cisco)公司的估算,这家公司是 IT 和网络领域的领先企业,到 2018 年,物联网(IoT)将每年产生 400 泽字节的数据。这表明我们需要寻找自动化的手段来理解这些庞大的数据,这正是机器学*的作用所在。

完整的思科报告,发布于 2018 年 2 月 1 日,可以通过www.cisco.com/c/en/us/solutions/collateral/service-provider/global-cloud-index-gci/white-paper-c11-738085.html访问。报告预测了物联网、机器人技术、人工智能和电信结合下的数据流量和云服务趋势。

每年,Gartner 这家研究和咨询公司都会发布一份图示,提供新兴技术成熟度的视觉和概念性展示,分为五个阶段。

您可以在www.gartner.com/smarterwithgartner/5-trends-emerge-in-gartner-hype-cycle-for-emerging-technologies-2018/找到 2018 年Gartner 新兴技术兴奋周期的图片。

我们可以看到,物联网平台和机器学*都处于“虚 inflated 期”的顶峰。那意味着什么呢?虚 inflated 期是技术生命周期中的一个阶段,通常伴随着对技术的过度热情。大量供应商和初创公司投资于处于顶峰的技术。越来越多的商业机构探索新技术如何融入其商业战略。简而言之,这是跳入该技术的时机。你会听到投资者在风险投资活动中开玩笑说,只要你在推介中提到机器学*,你就可以在估值后面加一个零

所以,系好安全带,让我们深入探索机器学*技术。

学*范式

机器学*算法可以根据其使用的方法进行如下分类:

  • 概率与非概率

  • 建模与优化

  • 监督学*与无监督学*

在本书中,我们将我们的机器学*算法分为监督学*和无监督学*。两者之间的区别取决于模型是如何学*的,以及提供给模型的数据类型:

  • 监督学*:假设我给你一个序列,并要求你预测下一个元素:

(1, 4, 9, 16, 25,...)

你猜对了:下一个数字是 36,接下来是 49,依此类推。这就是监督学*,也叫通过示例学*;你并没有被告知这个序列表示的是正整数的平方——你通过提供的五个示例自己猜出了这一点。

类似地,在监督学*中,机器从示例中学*。它提供了一组包含(X, Y)的训练数据,其中X是输入(可以是一个单一数字或具有大量特征的输入值),Y是给定输入的预期输出。一旦在示例数据上训练完成,模型就应该能够在面对新数据时得出准确的结论。

监督学*用于根据一组输入预测一个实值输出(回归)或一个离散标签(分类)。我们将在接下来的章节中探讨回归和分类算法。

  • 无监督学*:假设你得到了八个不同半径和颜色的圆形块,并要求你按照某种顺序排列或分组它们。你会怎么做?

有些人可能会根据半径的增减顺序排列它们,有些人可能会根据颜色将它们分组。方法有很多,而对我们每个人来说,这取决于我们在分组时对数据的内部表征。这就是无监督学*,大多数人类的学*也属于这一类别。

在无监督学*中,模型仅给定数据(X),但并未告诉它任何相关信息;模型通过自己学*数据中的潜在模式和关系。无监督学*通常用于聚类和降维。

尽管我们在本书中的大部分算法中使用的是 TensorFlow,但在本章中,由于 scikit 库高效地构建了机器学*算法,我们将在提供更多灵活性和功能的情况下使用 scikit 提供的函数和方法。目标是为你,读者,提供使用 AI/ML 技术处理物联网生成的数据,而不是重新发明轮子。

使用线性回归进行预测

我的朋友 Aaron 对钱的管理有点马虎,总是无法估算出他的每月信用卡账单会是多少。我们能做点什么来帮助他吗?当然,线性回归可以帮助我们预测每月的信用卡账单,只要我们有足够的数据。得益于数字经济,他过去五年的所有货币交易都可以在网上找到。我们提取了他的每月食品杂货、文具和旅行支出以及他的月收入。线性回归不仅帮助预测了他的月信用卡账单,还揭示了最主要的消费因素。

这只是一个例子;线性回归可以用于许多类似的任务。在本节中,我们将学*如何在数据上执行线性回归。

线性回归是一个监督学*任务。它是最基础、最简单且应用广泛的机器学*预测技术之一。回归的目标是为给定的输入输出对(x, y)找到一个函数F(x, W),使得y = F(x, W)。在(x, y)对中,x是自变量,y是因变量,且它们都是连续变量。它帮助我们找到因变量y和自变量x之间的关系。

输入x可以是单一的输入变量,也可以是多个输入变量。当F(x, W)映射一个单一输入变量x时,这叫做简单线性回归;当有多个输入变量时,则称为多元线性回归

函数F(x, W)可以通过以下表达式来*似:

在这个表达式中,dx的维度(独立变量的数量),W是与x的每个分量相关的权重。为了找到函数F(x, W),我们需要确定权重。自然的选择是找到能够减少平方误差的权重,因此我们的目标函数如下:

在前面的函数中,N是所提供的输入输出对的总数。为了找到权重,我们对目标函数关于权重进行求导并使其等于0。在矩阵表示法中,我们可以将列向量W = (W[0], W[1], W[2], ..., W[d])^T 的解写为如下:

通过求导并简化,我们得到以下结果:

X是大小为[N, d]的输入向量,Y是大小为[N, 1]的输出向量。如果(XTX*)(-1)存在,即X的所有行和列是线性独立的,那么可以找到权重。为了确保这一点,输入输出样本的数量(N)应该远大于输入特征的数量(d*)。

需要记住的一件重要事情是,Y,即因变量,并不是与自变量X线性相关的;相反,它是与模型参数W,即权重,线性相关的。因此,我们可以通过线性回归建模诸如指数型甚至正弦型的关系(在YX之间)。在这种情况下,我们将问题推广到寻找权重W,使得y = F(g(x), W),其中g(x)是X的非线性函数。

使用回归进行电力输出预测

现在你已经理解了线性回归的基础知识,让我们用它来预测联合循环电厂的电力输出。我们在第一章中描述了这个数据集,人工智能与物联网的原理与基础;在这里,我们将使用 TensorFlow 及其自动梯度来找到解决方案。该数据集可以从 UCI ML 档案下载(archive.ics.uci.edu/ml/datasets/combined+cycle+power+plant)。完整的代码可以在 GitHub 上找到(github.com/PacktPublishing/Hands-On-Artificial-Intelligence-for-IoT),文件名为ElectricalPowerOutputPredictionUsingRegression.ipynb

让我们理解以下步骤中的代码执行:

  1. 我们导入了tensorflownumpypandasmatplotlib和一些scikit-learn的有用函数:
# Import the modules
import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
%matplotlib inline # The data file is loaded and analyzed
  1. 数据文件已加载并进行分析:
filename = 'Folds5x2_pp.xlsx' # download the data file from UCI ML repository
df = pd.read_excel(filename, sheet_name='Sheet1')
df.describe()
  1. 由于数据没有标准化,在使用它之前,我们需要使用sklearnMinMaxScaler进行标准化:
X, Y = df[['AT', 'V','AP','RH']], df['PE']
scaler = MinMaxScaler()
X_new = scaler.fit_transform(X)
target_scaler = MinMaxScaler()
Y_new = target_scaler.fit_transform(Y.values.reshape(-1,1))
X_train, X_test, Y_train, y_test = \
 train_test_split(X_new, Y_new, test_size=0.4, random_state=333)
  1. 现在,我们定义一个类LinearRegressor;这是所有实际工作发生的地方。类初始化定义了计算图并初始化所有Variables(权重和偏差)。该类有一个function方法,用于建模函数y = F(X,W);fit方法执行自动梯度并更新权重和偏差,predict方法用于获取给定输入X的输出yget_weights方法返回学*到的权重和偏差:
class LinearRegressor:
 def __init__(self,d, lr=0.001 ):
 # Placeholders for input-output training data
 self.X = tf.placeholder(tf.float32,\
 shape=[None,d], name='input')
 self.Y = tf.placeholder(tf.float32,\
 name='output')
 # Variables for weight and bias
 self.b = tf.Variable(0.0, dtype=tf.float32)
 self.W = tf.Variable(tf.random_normal([d,1]),\
 dtype=tf.float32)

 # The Linear Regression Model
 self.F = self.function(self.X)

 # Loss function
 self.loss = tf.reduce_mean(tf.square(self.Y \
 - self.F, name='LSE'))
 # Gradient Descent with learning 
 # rate of 0.05 to minimize loss
 optimizer = tf.train.GradientDescentOptimizer(lr)
 self.optimize = optimizer.minimize(self.loss)

 # Initializing Variables
 init_op = tf.global_variables_initializer()
 self.sess = tf.Session()
 self.sess.run(init_op)

 def function(self, X):
 return tf.matmul(X, self.W) + self.b

 def fit(self, X, Y,epochs=500):
 total = []
 for i in range(epochs):
 _, l = self.sess.run([self.optimize,self.loss],\
 feed_dict={self.X: X, self.Y: Y})
 total.append(l)
 if i%100==0:
 print('Epoch {0}/{1}: Loss {2}'.format(i,epochs,l))
 return total

 def predict(self, X):
 return self.sess.run(self.function(X), feed_dict={self.X:X})

 def get_weights(self):
 return self.sess.run([self.W, self.b])
  1. 我们使用前面定义的类来创建线性回归模型并进行训练:
N, d = X_train.shape
model = LinearRegressor(d)
loss = model.fit(X_train, Y_train, 20000) #Epochs = 20000

让我们看看训练好的线性回归器的性能。带有Epochs的均方误差图显示,网络试图达到均方误差的最小值:

在测试数据集上,我们实现了值为0.768,均方误差为0.011

分类的逻辑回归

在上一节中,我们学*了如何进行预测。在机器学*中,还有一个常见的任务:分类任务。将狗和猫分开、垃圾邮件和非垃圾邮件分开,甚至识别房间或场景中的不同物体——这些都是分类任务。

逻辑回归是一种经典的分类技术。它提供了一个事件发生的概率,给定输入值。事件被表示为类别型的因变量,而某一特定因变量为 1 的概率通过 logit 函数表示:

在详细讨论如何使用逻辑回归进行分类之前,我们先来看看 logit 函数(也被称为 sigmoid 函数,因其 S 形曲线)。下图展示了 logit 函数及其导数随着输入 X 的变化,Sigmoid 函数(蓝色)及其导数(橙色):

从该图中需要注意的几点如下:

  • Sigmoid 函数的值(因此 Y[pred])介于 (0, 1) 之间。

  • W^TX + b = 0.0 时,sigmoid 的导数最大,且该导数的最大值为 0.25(在同一点上,sigmoid 的值为 0.5

  • Sigmoid 函数变化的斜率取决于权重,而我们将得到导数峰值的位置则取决于偏置。

我建议你尝试在本书的 GitHub 仓库中使用 Sigmoid_function.ipynb 程序,感受当权重和偏置发生变化时,sigmoid 函数如何变化。

交叉熵损失函数

逻辑回归的目标是找到权重 W 和偏置 b,使得每个输入向量 X[i] 在输入特征空间中能够正确分类到其所属的类别 y[i]。换句话说,y[i] 应该对给定的 x[i] 有类似的分布。我们首先考虑一个二分类问题;在这种情况下,数据点 y[i] 的值可以是 10。由于逻辑回归是一种监督学*算法,我们将训练数据对 (X[i], Y[i]) 作为输入,并让 P(y=1|X=X[i]) 的概率;然后,对于 p 个训练数据点,总的平均损失定义如下:

因此,对于每个数据对,当 Y[i] = 1 时,第一项将对损失项做出贡献,且随着 01 变化时,其贡献从无穷大到 0;同样地,对于 Y[i] = 0 时,第二项将对损失项做出贡献,且随着 10 变化时,其贡献从无穷大变为零。

对于多分类问题,损失项被推广为以下形式:

在前述内容中,K是类别的数量。需要注意的是,虽然对于二分类问题,输出的Y[i]Y[pred]是单一值,但对于多类问题,Y[i]Y[pred]现在是K维的向量,每个类别有一个分量。

使用逻辑回归器分类酒质

现在让我们利用所学的知识来分类酒的质量。我能听到你在想:什么酒质?不可能!让我们看看我们的逻辑回归器与专业品酒师相比表现如何。我们将使用酒质数据集(archive.ics.uci.edu/ml/datasets/wine+quality);数据集的详细信息请参见第一章,人工智能与物联网的原理与基础。完整代码位于 GitHub 仓库中的Wine_quality_using_logistic_regressor.ipynb文件中。让我们一步步理解代码:

  1. 第一步是加载所有模块:
# Import the modules
import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
%matplotlib inline
  1. 我们读取数据;在当前的代码中,我们只分析红酒,所以我们从winequality-red.csv文件中读取数据。该文件中的数据值不是通过逗号分隔的,而是通过分号分隔,因此我们需要指定分隔符参数:
filename = 'winequality-red.csv' # Download the file from UCI ML Repo 
df = pd.read_csv(filename, sep=';')
  1. 我们从数据文件中分离出输入特征和目标质量。在文件中,目标酒质以 0 到 10 的尺度表示。为了简化处理,我们将其分为三类,因此如果初始质量小于 5,我们将其归为第三类(表示差);在 5 到 8 之间,我们认为它是ok(第二类);大于 8 时,我们认为它是good(第一类)。我们还会对输入特征进行归一化,并将数据集分为训练集和测试集:
X, Y = df[columns[0:-1]], df[columns[-1]]
scaler = MinMaxScaler()
X_new = scaler.fit_transform(X)
Y.loc[(Y<3)]=3
Y.loc[(Y<6.5) & (Y>=3 )] = 2
Y.loc[(Y>=6.5)] = 1
Y_new = pd.get_dummies(Y) # One hot encode
X_train, X_test, Y_train, y_test = \
train_test_split(X_new, Y_new, test_size=0.4, random_state=333)
  1. 代码的主要部分是LogisticRegressor类;乍一看,你可能会认为它与我们之前做的LinearRegressor类相似。该类定义在 Python 文件LogisticRegressor.py中。它确实类似,但有几个重要的不同之处:Y outputY[pred]替代,后者不再是单一值,而是一个三维的分类值,每个维度指定了三类的概率。这里的权重具有d × n的维度,其中d是输入特征的数量,n是输出类别的数量。偏差也变成了三维。另一个重要变化是损失函数的变化:
class LogisticRegressor:
    def __init__(self, d, n, lr=0.001 ):
        # Place holders for input-output training data
        self.X = tf.placeholder(tf.float32,\
              shape=[None,d], name='input')
        self.Y = tf.placeholder(tf.float32,\
              name='output')
        # Variables for weight and bias
        self.b = tf.Variable(tf.zeros(n), dtype=tf.float32)
        self.W = tf.Variable(tf.random_normal([d,n]),\
              dtype=tf.float32)
        # The Logistic Regression Model
        h = tf.matmul(self.X, self.W) + self.b
        self.Ypred = tf.nn.sigmoid(h)
        # Loss function
        self.loss = cost = tf.reduce_mean(-tf.reduce_sum(self.Y*tf.log(self.Ypred),\
                 reduction_indices=1), name = 'cross-entropy-loss')
        # Gradient Descent with learning 
        # rate of 0.05 to minimize loss
        optimizer = tf.train.GradientDescentOptimizer(lr)
        self.optimize = optimizer.minimize(self.loss)
        # Initializing Variables
        init_op = tf.global_variables_initializer()
        self.sess = tf.Session()
        self.sess.run(init_op)

    def fit(self, X, Y,epochs=500):
        total = []
        for i in range(epochs):
            _, l = self.sess.run([self.optimize,self.loss],\
                  feed_dict={self.X: X, self.Y: Y})
            total.append(l)
            if i%1000==0:
                print('Epoch {0}/{1}: Loss {2}'.format(i,epochs,l))
        return total

   def predict(self, X):
        return self.sess.run(self.Ypred, feed_dict={self.X:X})

    def get_weights(self):
        return self.sess.run([self.W, self.b])
  1. 现在我们只需训练我们的模型并预测输出。学到的模型在测试数据集上的准确率约为 85%。相当令人印象深刻!

使用机器学*,我们还可以识别哪些成分使酒的质量更好。名为 IntelligentX 的公司最*开始根据用户反馈酿造啤酒;它使用人工智能来获得最美味的啤酒配方。你可以在这篇Forbes文章中了解该项目:www.forbes.com/sites/emmasandler/2016/07/07/you-can-now-drink-beer-brewed-by-artificial-intelligence/#21fd11cc74c3

使用支持向量机进行分类

支持向量机SVMs)可以说是最常用的机器学*分类技术。SVM 的主要思想是找到一个最大间隔的最优超平面来分隔两个类别。如果数据是线性可分的,那么寻找超平面的过程是直接的,但如果数据不可线性分隔,则使用核技巧将数据转化到某个高维特征空间中,使其线性可分。

支持向量机(SVM)被认为是一种非参数化的监督学*算法。SVM 的主要思想是寻找一个最大间隔分隔器:一个与训练样本最远的分隔超平面。

考虑以下图示;红色的点表示类别 1,其输出应为 1,蓝色的点表示类别 2,其输出应为-1。可以有多条直线将红点与蓝点分开;图示展示了三条这样的直线:ABC。你认为哪一条线是最好的选择?直观上,最好的选择是 B 线,因为它距离两个类别的样本最远,从而确保分类错误最少:

在接下来的部分,我们将学*如何寻找最大分隔超平面的基本数学。虽然这里的数学大多是基础的,但如果你不喜欢数学,你可以跳过这部分,直接进入实现部分,看看我们如何再次使用 SVM 进行酒类分类!干杯!

最大间隔超平面

根据我们对线性代数的了解,我们知道平面的方程由以下内容给出:

在 SVM 中,这个平面应该将正类(y=1)与负类(y=-1)分开,并且有一个附加约束:这个超平面与最靠*的正负训练向量(X[pos]X[neg])之间的距离(间隔)应该是最大值。因此,这个平面被称为最大间隔分隔器。

向量X[pos]和X[neg]被称为支持向量,它们在定义 SVM 模型中起着重要作用。

从数学角度看,这意味着以下内容成立:

同样,以下也是如此:

从这两个方程,我们得到以下结果:

将权重向量的长度除以两边,我们得到以下结果:

所以我们需要找到一个分隔器,使得正负支持向量之间的间隔最大,也就是说:最大,同时确保所有点都被正确分类,如下所示:

使用一些数学知识,尽管本书不会深入讲解,前述条件可以表示为以下最优解的求解问题:

需要满足以下约束条件:

从α的值中,我们可以使用以下公式从α(系数向量)中得到权重W

这是一个标准的二次规划优化问题。大多数机器学*库都有内置的函数来解决这个问题,所以你不需要担心如何处理。

对于有兴趣了解更多关于 SVM 及其背后数学的读者,Vladimir Vapnik 的《统计学*理论的本质》(The Nature of Statistical Learning Theory),由Springer Science+Business Media出版,2013 年,是一本非常好的参考书。

核技巧

当输入特征空间是线性可分时,上述方法效果很好。那么当输入特征空间不可线性分隔时,我们该怎么办呢?一种简单的方法是将数据(X)转换到一个更高维的空间,在这个高维空间中数据是线性可分的,然后在该高维空间中找到一个最大间隔的超平面。让我们来看看;我们关于α的超平面如下:

φ为变换,那么我们可以用φ(X)替换X,从而将其点积X^(T )X^((i))替换为一个函数 K(X^T, X^((i))) = φ(X)^T φ(X^((i))),这个函数叫做。因此,我们现在只需要通过应用变换φ对数据进行预处理,然后在变换后的空间中像之前一样找到一个线性分隔器。

最常用的核函数是高斯核,也叫径向基函数,定义如下:

使用 SVM 进行葡萄酒分类

我们将使用 scikit 库提供的svm.SVC函数来完成这个任务。这样做的原因是,直到目前为止,TensorFlow 库只提供了 SVM 的线性实现,并且它只适用于二分类。我们可以利用之前在 TensorFlow 中学到的数学知识自己实现 SVM,并且 GitHub 仓库中的SVM_TensorFlow.ipynb包含了 TensorFlow 中的实现。以下代码可以在Wine_quality_using_SVM.ipynb中找到。

scikit 的 SVC 分类器是一个支持向量分类器。它也可以通过一对一的方式处理多类问题。该方法的一些可选参数如下:

  • C:它是一个指定惩罚项的参数(默认值为1.0)。

  • kernel:指定要使用的核函数(默认为rbf)。可选值包括linearpolyrbfsigmoidprecomputedcallable

  • gamma:它指定rbfpolysigmoid的核函数系数,默认值为auto

  • random_state:它设置伪随机数生成器的种子,用于数据洗牌时。

按照给定的步骤创建我们的 SVM 模型:

  1. 让我们加载代码中需要的所有模块。请注意,我们没有在这里导入 TensorFlow,而是从scikit库中导入了某些模块:
# Import the modules
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score
from sklearn.svm import SVC # The SVM Classifier from scikit
import seaborn as sns
%matplotlib inline
  1. 我们读取数据文件,进行预处理,并将数据分为测试集和训练集。这次,为了简便起见,我们将数据分为两类:goodbad
filename = 'winequality-red.csv' #Download the file from UCI ML Repo
df = pd.read_csv(filename, sep=';')

#categorize wine quality in two levels
bins = (0,5.5,10)
categories = pd.cut(df['quality'], bins, labels = ['bad','good'])
df['quality'] = categories

#PreProcessing and splitting data to X and y
X = df.drop(['quality'], axis = 1)
scaler = MinMaxScaler()
X_new = scaler.fit_transform(X)
y = df['quality']
labelencoder_y = LabelEncoder()
y = labelencoder_y.fit_transform(y)
X_train, X_test, y_train, y_test = train_test_split(X, y, \
        test_size = 0.2, random_state = 323)
  1. 现在我们使用SVC分类器,并通过fit方法在我们的训练数据集上训练它:
classifier = SVC(kernel = 'rbf', random_state = 45)
classifier.fit(X_train, y_train)
  1. 现在让我们预测测试数据集的输出:
y_pred = classifier.predict(X_test)
  1. 该模型的准确率为67.5%,混淆矩阵如下:
print("Accuracy is {}".format(accuracy_score(y_test, y_pred)))
## Gives a value ~ 67.5%
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm,annot=True,fmt='2.0f')

上述代码使用的是二分类;我们可以修改代码,使其适用于多个类别。例如,在第二步中,我们可以将代码替换为以下内容:

bins = (0,3.5,5.5,10)
categories = pd.cut(df['quality'], bins, labels = ['bad','ok','good'])
df['quality'] = categories
  1. 然后我们得到三个类别,就像我们之前的逻辑回归分类器一样,准确率为 65.9%。混淆矩阵如下:

在三类情况下,训练数据的分布如下:

  • good 855

  • ok 734

  • bad 10

由于bad类(对应混淆矩阵中的0)的样本数量仅为10,模型无法学*哪些参数会影响酒的质量。因此,数据应该在我们本章中探索的所有分类器的各个类之间均匀分布。

朴素贝叶斯

朴素贝叶斯是最简单和最快的机器学*算法之一。它也属于监督学*算法的范畴。它基于贝叶斯概率定理。在朴素贝叶斯分类器的情况下,我们做出的一个重要假设是输入向量的所有特征都是独立同分布iid)。目标是为每个类C[k]学*一个条件概率模型,该模型在训练数据集中进行训练:

在 iid 假设下,并使用贝叶斯定理,这可以表示为联合概率分布p(C[k], X):

我们选择最大化此项的类 Maximum A Posteriori MAP):

根据p(x[i]|C[k])的分布,可能会有不同的朴素贝叶斯算法。常见的选择有:对于实数值数据使用高斯分布,对于二进制数据使用伯努利分布,对于数据中包含某事件频率的情况(例如文档分类)使用多项式分布。

现在我们来看看是否能使用朴素贝叶斯对葡萄酒进行分类。为了简单和高效起见,我们将使用 scikit 内置的朴素贝叶斯分布。由于我们数据中的特征值是连续的——我们将假设它们符合高斯分布,因此我们将使用 scikit-learn 的GaussianNB

用高斯朴素贝叶斯预测葡萄酒质量

scikit-learn 的朴素贝叶斯模块支持三种朴素贝叶斯分布。我们可以根据输入特征的数据类型选择其中之一。scikit-learn 中可用的三种朴素贝叶斯如下:

  • GaussianNB

  • MultinomialNB

  • BernoulliNB

正如我们已经看到的,葡萄酒数据是一种连续数据类型。因此,如果我们使用高斯分布来表示p(x[i]|C[k])——也就是GaussianNB模块,那么效果会更好,所以我们需要在 Notebook 的导入单元中加入from sklearn.naive_bayes import GaussianNB。你可以通过这个 scikit-learn 的链接了解更多关于GaussianNB模块的详细信息:scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.GaussianNB.html#sklearn.naive_bayes.GaussianNB

前两步将与 SVM 案例中的相同。但现在,我们不会声明一个SVM分类器,而是声明一个GaussianNB分类器,并使用它的fit方法来学*训练样本。通过predict方法可以得到从学*的模型中获得的结果。所以,按照以下步骤操作:

  1. 导入必要的模块。请注意,现在我们正在从scikit库中导入GaussianNB
# Import the modules
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score
from sklearn.naive_bayes import GaussianNB # The SVM Classifier from scikit
import seaborn as sns
%matplotlib inline
  1. 读取数据文件并进行预处理:
filename = 'winequality-red.csv' #Download the file from UCI ML Repo
df = pd.read_csv(filename, sep=';')

#categorize wine quality in two levels
bins = (0,5.5,10)
categories = pd.cut(df['quality'], bins, labels = ['bad','good'])
df['quality'] = categories

#PreProcessing and splitting data to X and y
X = df.drop(['quality'], axis = 1)
scaler = MinMaxScaler()
X_new = scaler.fit_transform(X)
y = df['quality']
labelencoder_y = LabelEncoder()
y = labelencoder_y.fit_transform(y)
X_train, X_test, y_train, y_test = train_test_split(X, y, \
        test_size = 0.2, random_state = 323)
  1. 现在我们声明一个高斯朴素贝叶斯,使用训练数据集对其进行训练,并使用训练好的模型来预测测试数据集中的葡萄酒质量:
classifier = GaussianNB()
classifier.fit(X_train, y_train)
#Predicting the Test Set
y_pred = classifier.predict(X_test)

就是这样,大家;我们的模型已经准备好了并开始运作。在二分类的情况下,这个模型的准确率为 71.25%。在下面的截图中,你可以看到混淆矩阵的热力图:

在你得出朴素贝叶斯是最好的结论之前,我们先要注意它的一些陷阱:

  • 朴素贝叶斯是基于频率的概率进行预测的,因此它在很大程度上依赖于我们用于训练的数据。

  • 另一个问题是,我们假设输入特征空间是独立同分布(iid)的,但这并不总是成立。

决策树

在这一部分,你将学*另一种非常流行且快速的机器学*算法——决策树。在决策树中,我们构建一个类似树状的决策结构;从根节点开始,选择一个特征并分裂成分支,继续分裂直到到达叶节点,叶节点表示预测的类别或数值。决策树的算法涉及两个主要步骤:

  • 决定选择哪些特征以及使用什么条件进行划分

  • 知道何时停止

我们通过一个例子来理解。假设有一个由 40 名学生组成的样本;我们有三个变量:性别(男孩或女孩;离散型),班级(XI 或 XII;离散型),身高(5 到 6 英尺;连续型)。其中 18 名学生在空闲时间倾向于去图书馆,其他学生则喜欢玩耍。我们可以建立一个决策树来预测哪些学生会去图书馆,哪些学生会去操场。为了构建决策树,我们需要根据三个输入变量中最具显著性的变量来将去图书馆和去操场的学生区分开。下图展示了基于每个输入变量的划分:

我们考虑所有特征,选择能给我们最大信息量的那个特征。在前面的例子中,我们可以看到,基于身高特征的划分生成了最均匀的组,其中身高 > 5.5 英尺的组包含了 80%的学生玩耍,20%的学生去图书馆,而身高 < 5.5 英尺的组包含了 13%的学生玩耍,86%的学生去图书馆。因此,我们将基于身高特征进行第一次划分。我们将继续以这种方式划分,最终得到决策(叶节点),告诉我们学生在空闲时间是去玩耍还是去图书馆。下图展示了决策树的结构;黑色圆圈是 节点,蓝色圆圈是决策 节点,绿色圆圈是 节点

决策树属于贪心算法的一类。为了找到最均匀的划分,我们定义了代价函数,使其尽量在特定组内最大化同类输入值的比例。对于回归问题,我们通常使用均方误差代价函数:

在这里,yy[pred] 表示给定的输出值和预测的输出值,分别对应输入值(i);我们找到最小化该损失的划分。

对于分类问题,我们使用基尼不纯度或交叉熵作为损失函数:

在前述中,c[k] 定义了特定组中同类输入值的比例。

以下是一些学*决策树的好资源:

  • L. Breiman, J. Friedman, R. Olshen 和 C. Stone: 分类与回归树,Wadsworth,Belmont,CA,1984

  • J.R. Quinlan: C4.5:机器学*程序,Morgan Kaufmann,1993

  • T. Hastie, R. Tibshirani 和 J. Friedman: 统计学*的元素,Springer,2009

scikit 中的决策树

scikit 库提供了 DecisionTreeRegressorDecisionTreeClassifier 来实现回归和分类。两者都可以从 sklearn.tree 导入。DecisionTreeRegressor 定义如下:

*c*lass sklearn.tree.DecisionTreeRegressor (criterion=’mse’, splitter=’best’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, presort=False)

不同的参数如下:

  • criterion:它定义了用于确定拆分的损失函数。默认值是均方误差(mse)。该库支持使用 friedman_mse 和均值绝对误差(mae)作为损失函数。

  • splitter:我们用它来决定是否使用贪婪策略并选择最佳拆分(默认值),或者可以使用随机 splitter 来选择最佳随机拆分。

  • max_depth:它定义了树的最大深度。

  • min_samples_split:它定义了拆分内部节点所需的最小样本数。它可以是整数或浮动数(在这种情况下,它定义了拆分所需的最小样本百分比)。

DecisionTreeClassifier 定义如下:

class sklearn.tree.DecisionTreeClassifier(criterion=’gini’, splitter=’best’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, class_weight=None, presort=False)

不同的参数如下:

  • criterion:它指示用于确定拆分的损失函数。分类器的默认值是 gini。该库支持使用 entropy 作为损失函数。

  • splitter:我们用它来决定如何选择拆分(默认值是最佳拆分),或者可以使用随机 splitter 来选择最佳随机拆分。

  • max_depth:它定义了树的最大深度。当输入特征空间很大时,我们用它来限制最大深度并避免过拟合。

  • min_samples_split:它定义了拆分内部节点所需的最小样本数。它可以是整数或浮动数(在这种情况下,它指示拆分所需的最小样本百分比)。

我们仅列出了常用的前导参数;有关两个参数的其余细节,请参见 scikit-learn 网站:scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeRegressor.htmlscikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html

决策树的实际应用

我们将首先使用决策树回归器来预测电力输出。数据集及其描述已在 第一章,物联网和人工智能的原理与基础 中介绍。代码可在 GitHub 仓库中的文件 ElectricalPowerOutputPredictionUsingDecisionTrees.ipynb 中找到:

# Import the modules
import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
%matplotlib inline

# Read the data 
filename = 'Folds5x2_pp.xlsx' # The file can be downloaded from UCI ML repo
df = pd.read_excel(filename, sheet_name='Sheet1')
df.describe()

# Preprocess the data and split in test/train
X, Y = df[['AT', 'V','AP','RH']], df['PE']
scaler = MinMaxScaler()
X_new = scaler.fit_transform(X)
target_scaler = MinMaxScaler()
Y_new = target_scaler.fit_transform(Y.values.reshape(-1,1))
X_train, X_test, Y_train, y_test = \
 train_test_split(X_new, Y_new, test_size=0.4, random_state=333)

# Define the decision tree regressor
model = DecisionTreeRegressor(max_depth=3)
model.fit(X_train, Y_train)

# Make the prediction over the test data
Y_pred = model.predict(np.float32(X_test))
print("R2 Score is {} and MSE {}".format(\
 r2_score(y_test, Y_pred),\
 mean_squared_error(y_test, Y_pred)))

在测试数据上,我们得到了 0.90 的 R 平方值和 0.0047 的均方误差;这相比使用线性回归器的预测结果(R 平方:0.77; mse:0.012)有了显著的提高。

让我们也看看决策树在分类任务中的表现;我们依旧使用葡萄酒质量分类。代码可在 GitHub 仓库中的Wine_quality_using_DecisionTrees.ipynb文件中找到:

# Import the modules
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
%matplotlib inline

# Read the data 
filename = 'winequality-red.csv' #Download the file from https://archive.ics.uci.edu/ml/datasets/wine+quality df = pd.read_csv(filename, sep=';')

# categorize the data into three classes
bins = (0,3.5,5.5,10)
categories = pd.cut(df['quality'], bins, labels = ['bad','ok','good'])
df['quality'] = categories

# Preprocessing and splitting data to X and y X = df.drop(['quality'], axis = 1) scaler = MinMaxScaler() X_new = scaler.fit_transform(X) y = df['quality'] from sklearn.preprocessing import LabelEncoder labelencoder_y = LabelEncoder() y = labelencoder_y.fit_transform(y) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 323)

# Define the decision tree classifier
classifier = DecisionTreeClassifier(max_depth=3)
classifier.fit(X_train, y_train)

# Make the prediction over the test data
Y_pred = classifier.predict(np.float32(X_test))
print("Accuracy is {}".format(accuracy_score(y_test, y_pred)))

决策树产生的分类准确率约为 70%。我们可以看到,对于小规模数据,决策树和朴素贝叶斯几乎可以同样成功。决策树容易过拟合,可以通过限制最大深度或设置最小训练输入数量来解决。像朴素贝叶斯一样,决策树也是不稳定的——数据的微小变化可能会导致完全不同的树结构;这可以通过使用装袋法和提升法来解决。最后,由于它是贪心算法,无法保证它能返回全局最优解。

集成学*

在我们的日常生活中,当我们需要做出决定时,我们不仅依赖一个人的指导,而是依赖许多我们信任的智者。在机器学*中也可以应用同样的思想;我们可以使用一组模型(集成)来进行预测或分类,而不是依赖于单一模型。这种学*方式称为集成学*

通常,集成学*作为许多机器学*项目中的最后一步使用。当各模型尽可能独立时,它的效果最佳。以下图展示了集成学*的图示:

不同模型的训练可以是顺序进行,也可以是并行进行。集成学*有多种实现方式:投票法、装袋法、拼接法和随机森林。让我们来看看这些技术是什么,以及如何实现它们。

投票分类器

投票分类器遵循多数规则;它会汇总所有分类器的预测,并选择票数最多的类别。例如,在下面的截图中,投票分类器将预测输入实例属于类别1

scikit-learn 提供了VotingClassifier类来实现这一功能。通过在葡萄酒质量分类中使用集成学*,我们达到了 74%的准确度,超过了单独使用任何模型的结果。完整代码在Wine_quality_using_Ensemble_learning.ipynb文件中。以下是使用投票法进行集成学*的主要代码:

# import the different classifiers
from sklearn.svm import SVC 
from sklearn.naive_bayes import GaussianNB 
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import VotingClassifier

# Declare each classifier
clf1 = SVC(random_state=22)
clf2 = DecisionTreeClassifier(random_state=23)
clf3 = GaussianNB()
X = np.array(X_train)
y = np.array(y_train)

#Employ Ensemble learning
eclf = VotingClassifier(estimators=[
('lr', clf1), ('rf', clf2), ('gnb', clf3)], voting='hard')
eclf = eclf.fit(X, y)

# Make prediction on test data
y_pred = eclf.predict(X_test)

装袋法与拼接法

在投票法中,我们使用不同的算法对相同的数据集进行训练。我们也可以通过使用不同的模型与相同的学*算法来实现集成学*,但我们将它们训练在不同的训练数据子集上。训练子集是随机抽样的。抽样可以是有放回的(袋装法)或无放回的(拼接法):

  • 袋装法:在袋装法中,额外的训练数据是通过对原始数据集进行带重复的组合生成的。这有助于减少不同模型的方差。

  • 拼接法:由于拼接法是无放回的,因此每个训练数据的子集最多只能使用一次。如果原始数据集较大,拼接法更为合适。

scikit 库提供了执行袋装法(bagging)和拼接法(pasting)的方法;通过 sklearn.ensemble,我们可以导入 BaggingClassifier 并使用它。以下代码使用袋装法估算 500 个决策树分类器,每个分类器使用 1000 个训练样本(对于拼接法,将 bootstrap=False):

from sklearn.ensemble import BaggingClassifier
bag_classifier = BaggingClassifier(
        DecisionTreeClassifier(), n_estimators=500, max_samples=1000,\
        bootstrap=True, n_jobs=-1)    
bag_classifier.fit(X_train, y_train)
y_pred = bag_classifier.predict(X_test)

这导致葡萄酒质量分类的准确率为 77%。BaggingClassifier 的最后一个参数 n_jobs 定义了使用多少个 CPU 核心(即并行运行的任务数);当它的值设置为 -1 时,将使用所有可用的 CPU 核心。

只有决策树的集成被称为 随机森林。因此,我们之前实现的就是一个随机森林。我们可以直接在 scikit 中使用 RandomForestClassifier 类实现随机森林。使用该类的优势在于它在构建树时引入了额外的随机性。在分裂时,它会在随机子集的特征中寻找最佳的分裂特征。

提升你的模型 – 提示与技巧

在本章中,我们学*了大量的机器学*算法,每种算法都有其优缺点。在本节中,我们将探讨一些常见的问题及其解决方法。

通过特征缩放解决不均匀的数据尺度问题

收集到的数据通常没有相同的尺度;例如,一个特征可能在 10 到 100 之间变化,而另一个特征则可能仅在 2 到 5 之间分布。这种不均匀的数据尺度可能对学*产生不利影响。为了解决这个问题,我们使用特征缩放(标准化)方法。标准化方法的选择已被发现会极大地影响某些算法的性能。两种常见的标准化方法(在某些书籍中也称为标准化)如下:

  • Z-score 标准化:在 Z-score 标准化中,每个单独的特征都会被缩放,使其具备标准正态分布的特性,即均值为 0,方差为 1。如果 μ 是均值,σ 是方差,我们可以通过对每个特征进行如下线性变换来计算 Z-score 标准化:

  • 最小-最大归一化:最小-最大归一化将输入特征重新缩放,使其位于01之间。这会减少数据的标准差,从而抑制异常值的影响。为了实现最小-最大归一化,我们找到特征的最大值和最小值(分别为x[max]x[min]),然后执行以下线性变换:

我们可以使用scikit库中的StandardScalerMinMaxScaler方法对数据进行归一化。在本章中的所有示例中,我们使用了MinMaxScaler;你可以尝试将其更改为StandardScaler并观察性能是否有所变化。在下一章中,我们还将学*如何在 TensorFlow 中执行这些归一化操作。

过拟合

有时,模型会尝试过拟合训练数据集;这样做会失去其泛化能力,因此在验证数据集上的表现较差;这反过来会影响模型在未见数据上的表现。有两种标准方式可以解决过拟合问题:正则化和交叉验证。

正则化

正则化在损失函数中添加了一项,确保当模型增加特征数时,代价会增加。因此,我们强制模型保持简单。如果L(X, Y)是之前的损失函数,我们将其替换为以下公式:

在上面,N可以是L[1]范数、L[2]范数或两者的组合,λ是正则化系数。正则化有助于减少模型的方差,同时不失去数据分布的任何重要属性:

  • 套索回归正则化:在这种情况下,NL[1]范数。它使用权重的模作为惩罚项N

  • 岭回归正则化:在这种情况下,NL2范数,表示为以下公式:

交叉验证

使用交叉验证还可以帮助减少过拟合问题。在k-折交叉验证中,数据被分成k个子集,称为。然后,它训练和评估模型k次;每次,选择一个折进行验证,其余部分用于训练模型。当数据较少且训练时间较短时,我们可以执行交叉验证。scikit 提供了一个cross_val_score方法来实现 k 折交叉验证。假设classifier是我们要交叉验证的模型,那么我们可以使用以下代码在10折上执行交叉验证:

from sklearn.model_selection import cross_val_score 
accuracies = cross_val_score(estimator = classifier, X = X_train,\
     y = y_train, cv = 10)
print("Accuracy Mean {} Accuracy Variance \
     {}".format(accuracies.mean(),accuracies.std()))

结果是一个平均均值和方差值。一个好的模型应该有较高的平均值和较低的方差。

无免费午餐定理

面对如此多的模型,人们总是会想,应该使用哪个模型。沃尔珀特(Wolpert)在他著名的论文《学*中缺乏先验区分》中探讨了这个问题,并证明了如果我们对输入数据没有任何先验假设,那么就没有理由偏向某一个模型。这就是无免费午餐定理

这意味着没有任何模型可以先验地保证其表现更好。我们能确定哪个模型最好的唯一方法是评估它们所有。但实际上,评估所有模型是不可能的,因此在实际操作中,我们会对数据做出合理的假设,并评估一些相关的模型。

超参数调整和网格搜索

不同的模型有不同的超参数;例如,在线性回归中,学*率就是一个超参数;如果我们使用正则化,那么正则化参数λ就是超参数。它们的值应该是多少?虽然有些超参数有经验法则,但大多数时候我们只能做出猜测,或者使用网格搜索来进行有序的搜索,以找到最佳的超参数。接下来,我们将展示使用scikit库在支持向量机(SVM)中进行超参数搜索的代码;在下一章中,我们将看到如何使用 TensorFlow 进行超参数调整:

Grid search for best model and parameters
from sklearn.model_selection import GridSearchCV
#parameters = {'kernel':('linear', 'rbf'), 'C':[1, 10]}
classifier = SVC()
parameters = [{'C': [1, 10], 'kernel': ['linear']},
    {'C': [1, 10], 'kernel': ['rbf'],
    'gamma': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]}]
    grid_search = GridSearchCV(estimator = classifier,
    param_grid = parameters,
    scoring = 'accuracy',
    cv = 10,)
grid_search.fit(X_train, y_train)
best_accuracy = grid_search.best_score_
best_parameters = grid_search.best_params_
#here is the best accuracy
best_accuracy

GridSearchCV将为我们提供产生最佳结果的支持向量机分类器的超参数。

总结

本章的目标是为你提供不同标准机器学*算法的直观理解,以便你能够做出明智的选择。我们介绍了用于分类和回归的流行机器学*算法。我们还学*了有监督学*和无监督学*之间的区别。我们介绍了线性回归、逻辑回归、支持向量机(SVM)、朴素贝叶斯和决策树,并探讨了每种算法所涉及的基本原理。我们使用回归方法预测了热电站的电力生产,并使用分类方法将葡萄酒分类为好或坏。最后,我们讨论了不同机器学*算法的常见问题以及一些解决这些问题的技巧。

在下一章中,我们将研究不同的深度学*模型,并学*如何使用它们来分析我们的数据并做出预测。

第四章:物联网的深度学*

在上一章中,我们学*了不同的机器学*ML)算法。本章的重点是基于多层模型的神经网络,也被称为深度学*模型。在过去几年中,它们成为了一个热门词汇,并成为了人工智能初创企业投资者的绝对最爱。在目标检测任务中实现超越人类水平的准确率,并击败世界顶尖的九段围棋大师,是深度学*可能实现的一些壮举。在本章和几个随后的章节中,我们将学*不同的深度学*模型以及如何在我们的物联网生成数据上使用深度学*。在本章中,我们将先来一窥深度学*的历程,并学*四个流行模型,即多层感知器MLP)、卷积神经网络CNN)、递归神经网络RNN)和自编码器。具体而言,您将学*以下内容:

  • 深度学*的历史及其目前成功的因素

  • 人工神经元及其如何连接以解决非线性问题

  • 反向传播算法及其用于训练 MLP 模型

  • TensorFlow 中可用的不同优化器和激活函数

  • CNN 的工作原理及卷积核、填充和步幅背后的概念

  • 使用 CNN 模型进行分类和识别

  • RNN 及其修改版、长短期记忆和门控循环单元

  • 自编码器的架构和功能

深度学* 101

人类思维一直令哲学家、科学家和工程师着迷。人类通过仿效和复制大脑智能的愿望已经被多年写入文献;比如希腊神话中的塞浦路斯的毕格马利翁的加拉特亚、犹太民间传说中的歌勒姆和印度神话中的玛雅·希塔,只是少数例子。拥有人工智能AI)的机器人自古以来就是(科幻)作家钟爱的对象。

人工智能,如今我们所知的,与计算机的概念是平行的。1943 年,McCulloch 和 Pitts 在其开创性论文《神经活动中内在思想的逻辑演算》中提出了第一个神经网络模型——阈值设备,可以执行逻辑运算如 AND、OR、AND-NOT。1950 年,Alan Turing 在其开创性工作《计算机器与智能》中提出了图灵测试;一种识别机器是否具有智能的测试方法。1957 年,Rosenblatt 在其报告《感知和识别自动机——感知器》中奠定了可以从经验中学*的网络基础。这些想法当时遥遥领先;虽然这些概念在理论上看似可能,但当时的计算资源严重限制了通过这些能够执行逻辑和学*的模型获得的性能。

虽然这些论文看起来有些陈旧且不太相关,但它们非常值得阅读,并能深入洞察这些早期思想家的视野。以下是这些论文的链接,供感兴趣的读者参考:

经历了两次 AI 寒冬和一些成功(2012 年突破,当亚历克斯·克里日夫斯基、伊利亚·苏茨克维尔和杰弗里·辛顿的 AlexNet 在年度 ImageNet 挑战赛中取得了 16%的错误率),今天我们站在了一个地方,深度学*已经超越了大多数现有的 AI 技术。以下是来自 Google Trends 的截图,显示大约在 2014 年,深度学*开始变得流行,并且自那时起一直在增长:

Google Trends 中的深度学*,从 2004 年到 2018 年 4 月

让我们来看看这一增长趋势背后的原因,分析一下这是不是只是炒作,还是背后还有更多内容。

深度学*——为什么是现在?

深度学*领域的大多数核心概念在 80 年代和 90 年代就已经基本成型,因此,问题来了:为什么我们现在突然看到深度学*被应用于解决从图像分类、图像修复到自动驾驶汽车和语音生成等不同问题?主要原因有两个,具体如下:

  • 大规模高质量数据集的可用性:互联网的出现产生了大量的图像、视频、文本和音频数据集。虽然大部分数据没有标签,但在许多领先研究人员的努力下(例如,李飞飞创建了 ImageNet 数据集),我们终于可以获得大量标注数据集。如果深度学*是一炉点燃你想象力的火炉,那么数据就是燃烧它的燃料。数据量和种类越多,模型的表现就越好。

  • 使用图形处理单元进行并行计算的可用性:在 DL 模型中,主要有两种数学矩阵操作发挥着至关重要的作用,即矩阵乘法和矩阵加法。通过图形处理单元GPUs)对这些过程进行并行化,使得在合理的时间内训练 DL 模型成为可能。

随着深度学*(DL)兴趣的增长,人们提出了进一步的改进,例如更好的优化器,用于梯度下降(在 DL 模型中用于计算权重和偏差更新的必要算法),例如 Adam 和 RMSprop;新的正则化技术,如 dropout 和批量归一化(batch normalization),不仅有助于解决过拟合问题,还能减少训练时间;最后但同样重要的是,DL 库的出现,如 TensorFlow、Theano、Torch、MxNet 和 Keras,这些都让定义和训练复杂架构变得更加容易。

根据deeplearning.ai的创始人 Andrew Ng 的说法,尽管有大量炒作和疯狂的投资,但我们不会再看到 AI 冬天的到来,因为计算设备的改进将持续带来性能提升和突破。Andrew Ng 在 2016 年的 EmTech Digital 上做出了这一预测,并且正如他所预言的那样,我们已经看到了处理硬件的进步,如谷歌的张量处理单元TPUs)、Intel Movidius 以及 NVIDIA 最新的 GPU。此外,今天有云计算 GPU 服务,其价格低至每小时 0.40 美元,使其对所有人都变得更加可负担。

你可以阅读 MIT Technology Review 上发表的完整文章《AI 冬天不会到来》www.technologyreview.com/s/603062/ai-winter-isnt-coming/。在这篇文章中,Andrew Ng 回答了有关人工智能未来的各种问题。

对于深度学*(DL),GPU 计算能力是必须的;有大量公司提供云计算服务来满足这一需求。但如果你是刚刚开始涉足这一领域,可以使用以下一些服务:

  • Google Colaboratory:它提供了一个基于浏览器的、启用 GPU 的 Jupyter Notebook 风格界面,并且为用户提供 12 小时的 GPU 计算能力。

  • Kaggle:Kaggle 同样提供了一个 Jupyter Notebook 风格的界面,并且为用户提供大约六小时的 GPU 计算能力,完全免费。

人工神经元

所有 DL 模型的基本组成部分是人工神经元。人工神经元的灵感来源于生物神经元的工作方式。它由一些通过权重连接的输入(也称为突触连接)组成,所有输入的加权和通过一个处理函数(称为激活函数)进行处理,生成一个非线性输出。

下图展示了生物神经元人工神经元

生物神经元与人工神经元

如果X[i]是连接到人工神经元(j)的第i输入,且通过突触连接w[ij]连接,则该神经元的净输入,通常称为神经元的活动,可以定义为所有输入的加权和,表示为:

在前述方程中,N是输入到第j个神经元的总数,θ[j]是第j个神经元的阈值;神经元的输出由下式给出:

在前述表达式中,g是激活函数。以下列出了不同深度学*模型中使用的不同激活函数,并给出了它们的数学和图形表示:

  • Sigmoid: 

  • 双曲正切:g(h[j]) = tanh(h[j])

  • ReLU: g(h[j])= max(0,h[j])

  • Softmax: 

  • Leaky ReLU: 

  • ELU: 

  • 阈值:

在 TensorFlow 中建模单一神经元

我们能否使用这个单一的神经元并让它学*?答案是肯定的,学*的过程涉及调整权重,使得预定义的损失函数(L)减小。如果我们按照损失函数关于权重的梯度的反方向更新权重,它将确保每次更新时损失函数减少。这个算法被称为梯度下降算法,是所有深度学*模型的核心。数学上,如果L是损失函数,η是学*率,那么权重w[ij]的更新表示为:

如果我们需要建模单一的人工神经元,首先需要决定以下参数:

  • 学*率参数:学*率参数决定了我们梯度下降的速度。通常,它的值介于01之间。如果学*率过高,网络可能会围绕正确的解振荡,或者完全偏离解。另一方面,当学*率过低时,最终收敛到解所需的时间会很长。

  • 激活函数:激活函数决定了神经元的输出如何随其活动而变化。由于权重更新方程涉及损失函数的导数,而这个导数又依赖于激活函数的导数,因此我们更倾向于选择一个连续可微的函数作为神经元的激活函数。最初,使用的是 sigmoid 和双曲正切函数,但它们存在收敛慢和梯度消失的问题(梯度变为零,从而没有学*,而解决方案还未达到)。*年来,修正线性单元ReLU)及其变体如 leaky ReLU 和 ELU 更受欢迎,因为它们提供了快速收敛,并且有助于克服梯度消失问题。在 ReLU 中,我们有时会遇到死神经元的问题,即一些神经元永远不会激活,因为它们的活动总是小于零,因此它们永远无法学*。Leaky ReLU 和 ELU 通过确保神经元输出不为零,即使活动为负,也解决了死神经元的问题。常用激活函数的列表以及它们的数学和图形表示在本节之前已经解释。(你可以尝试使用activation_functions.ipynb代码,其中使用了 TensorFlow 定义的激活函数。)

  • 损失函数:损失函数是我们网络试图最小化的参数,因此选择正确的损失函数对于学*至关重要。随着你深入了解深度学*,你会发现许多巧妙定义的损失函数。你会看到,通过正确地定义损失函数,我们可以让我们的深度学*模型生成新的图像、可视化梦境、为图像添加标题等等。传统上,根据任务的类型(回归或分类),人们使用均方误差MSE)或类别交叉熵损失函数。你将在本书的后续内容中学*这些损失函数。

现在我们已经知道了建模人工神经元所需的基本元素,让我们开始编写代码。我们假设是一个回归任务,因此我们将使用均方误差(MSE)损失函数。如果 y[j] 是我们单个神经元对输入向量 X 的输出,而 是我们期望输出神经元 j 的输出,那么 MSE 误差在数学上可以表示为(误差的平方的均值 ),如下所示:

在前面的公式中,M 是训练样本的总数(输入-输出对)。

请注意,如果您要实现这个人工神经元而不使用 TensorFlow(具体而言是不使用前面提到的任何 DL 库),那么您将需要自己计算梯度,例如,您将编写一个函数或代码,首先计算损失函数的梯度,然后您将必须编写代码来更新所有权重和偏差。对于具有 MSE 损失函数的单个神经元,计算导数仍然很简单,但随着网络复杂性的增加,计算特定损失函数的梯度、在代码中实现它,然后最终更新权重和偏差可能变得非常繁琐。

TensorFlow 通过使用自动微分使整个过程变得更加简单。TensorFlow 指定了 TensorFlow 图中的所有操作;这使它能够使用链式法则并在图中复杂地分配梯度。

因此,在 TensorFlow 中,我们构建执行图,并定义我们的损失函数,然后它会自动计算梯度,并支持许多不同的梯度计算算法(优化器),我们可以方便地使用它们。

您可以通过此链接了解更多关于自动微分的概念:www.columbia.edu/~ahd2125/post/2015/12/5/

现在有了所有这些基本信息,我们通过以下步骤在 TensorFlow 中构建我们的单个神经元:

  1. 每个 Python 代码的第一步始终是导入程序中将需要的模块。我们将导入 TensorFlow 来构建单个人工神经元。Numpy 和 pandas 则用于任何支持的数学计算和读取数据文件。此外,我们还从 scikit-learn 中导入一些有用的函数(用于数据归一化、将数据分割为训练集和验证集以及对数据进行洗牌),我们在早期章节已经使用过这些函数,并且知道归一化和洗牌在任何 AI 流水线中都是重要的步骤:
import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.utils import shuffle
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
% matplotlib inline

正如前面所解释的,验证有助于了解模型是否学*,以及是否出现了过拟合或欠拟合的情况。

  1. 在 TensorFlow 中,我们首先构建一个模型图,然后执行它。一开始可能会觉得复杂,但一旦掌握了它,它就非常方便,并允许我们优化用于生产的代码。因此,让我们首先定义我们的单个神经元图。我们将 self.Xself.y 定义为占位符,以便将数据传递给图,如以下代码所示:
class ArtificialNeuron:
    def __init__(self,N=2, act_func=tf.nn.sigmoid, learning_rate= 0.001):
        self.N = N # Number of inputs to the neuron
        self.act_fn = act_func

        # Build the graph for a single neuron
        self.X = tf.placeholder(tf.float32, name='X', shape=[None,N])
        self.y = tf.placeholder(tf.float32, name='Y')
  1. 权重和偏置被定义为变量,以便自动求导自动更新它们。TensorFlow 提供了一个图形界面,支持 TensorBoard 查看图的结构、不同参数以及它们在训练过程中的变化。这对于调试和理解模型的行为非常有帮助。在以下代码中,我们因此添加了代码行来为权重和偏置创建直方图摘要:
self.W = tf.Variable(tf.random_normal([N,1], stddev=2, seed = 0), name = "weights")
        self.bias = tf.Variable(0.0, dtype=tf.float32, name="bias")
        tf.summary.histogram("Weights",self.W)
        tf.summary.histogram("Bias", self.bias)
  1. 接下来,我们执行数学运算,即输入和权重之间的矩阵乘法,加入偏差,计算神经元的激活值及其输出,记为self.y_hat,如下面所示:
activity = tf.matmul(self.X, self.W) + self.bias
self.y_hat = self.act_fn(activity)
  1. 我们定义了我们希望模型最小化的损失函数,并使用 TensorFlow 优化器最小化该损失,利用梯度下降优化器更新权重和偏置,如下代码所示:
error = self.y - self.y_hat

self.loss = tf.reduce_mean(tf.square(error))
self.opt = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(self.loss)

  1. 我们通过定义一个 TensorFlow 会话并初始化所有变量来完成init函数。我们还添加了代码,确保 TensorBoard 将所有摘要写入指定位置,如下所示:
tf.summary.scalar("loss",self.loss)
init = tf.global_variables_initializer()

self.sess = tf.Session()
self.sess.run(init)

self.merge = tf.summary.merge_all()
self.writer = tf.summary.FileWriter("logs/",graph=tf.get_default_graph())
  1. 我们定义了train函数,在其中执行之前构建的图,如下代码所示:
def train(self, X, Y, X_val, Y_val, epochs=100):
epoch = 0
X, Y = shuffle(X,Y)
loss = []
loss_val = []
while epoch &amp;lt; epochs:
            # Run the optimizer for the whole training set batch wise (Stochastic Gradient Descent)     
            merge, _, l = self.sess.run([self.merge,self.opt,self.loss], feed_dict={self.X: X, self.y: Y})    
            l_val = self.sess.run(self.loss, feed_dict={self.X: X_val, self.y: Y_val})    

            loss.append(l)
            loss_val.append(l_val)
            self.writer.add_summary(merge, epoch)    

            if epoch % 10 == 0:
                print("Epoch {}/{} training loss: {} Validation loss {}".\    
                    format(epoch,epochs,l, l_val ))    

            epoch += 1
        return loss, loss_val
  1. 为了进行预测,我们还包括了一个predict方法,如下代码所示:
    def predict(self, X):
        return self.sess.run(self.y_hat, feed_dict={self.X: X})
  1. 接下来,像在上一章中一样,我们读取数据,使用 scikit-learn 函数对其进行归一化,并将其划分为训练集和验证集,如下所示:
filename = 'Folds5x2_pp.xlsx'
df = pd.read_excel(filename, sheet_name='Sheet1')
X, Y = df[['AT', 'V','AP','RH']], df['PE']
scaler = MinMaxScaler()
X_new = scaler.fit_transform(X)
target_scaler = MinMaxScaler()
Y_new = target_scaler.fit_transform(Y.values.reshape(-1,1))
X_train, X_val, Y_train, y_val = \
        train_test_split(X_new, Y_new, test_size=0.4, random_state=333)
  1. 我们使用创建的人工神经元来进行能量输出预测。随着人工神经元学*,训练损失验证损失被绘制,如下所示:
_, d = X_train.shape
model = ArtificialNeuron(N=d)

loss, loss_val = model.train(X_train, Y_train, X_val, y_val, 30000)

plt.plot(loss, label="Taining Loss")
plt.plot(loss_val, label="Validation Loss")
plt.legend()
plt.xlabel("Epochs")
plt.ylabel("Mean Square Error")

单个人工神经元学*预测能量输出时,训练和验证数据的均方误差

包含数据读取、数据归一化、训练等完整代码在single_neuron_tf.ipynb Jupyter Notebook 中给出。

多层感知器用于回归和分类

在最后一节中,你了解了单个人工神经元并使用它来预测能量输出。如果我们将其与第三章《物联网的机器学*》中的线性回归结果进行比较,我们会发现,尽管单神经元表现不错,但仍不如线性回归。单神经元架构在验证数据集上的均方误差(MSE)为 0.078,而线性回归为 0.01。我们能否通过增加更多的训练周期、调整学*率或增加更多的单一神经元来改善结果呢?不行,单神经元只能解决线性可分的问题。例如,只有当类别/决策之间存在一条直线时,单神经元才能提供解决方案。

只有一层神经元的网络被称为简单感知机。感知机模型由 Rosenblatt 于 1958 年提出(http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.335.3398&rep=rep1&type=pdf)。该论文在科学界引起了广泛的反响,并促使该领域展开了大量的研究。它首次被应用于硬件中进行图像识别任务。尽管感知机在初期看起来非常有前景,但 Marvin Minsky 和 Seymour Papert 的书籍《感知机》证明了简单的感知机只能解决线性可分的问题(books.google.co.in/books?hl=en&amp;lr=&amp;id=PLQ5DwAAQBAJ&amp;oi=fnd&amp;pg=PR5&amp;dq=Perceptrons:+An+Introduction+to+Computational+Geometry&amp;ots=zyEDwMrl__&amp;sig=DfDDbbj3es52hBJU9znCercxj3M#v=onepage&amp;q=Perceptrons%3A%20An%20Introduction%20to%20Computational%20Geometry&amp;f=false)。

那么我们该怎么办呢?我们可以使用多个单一神经元的层,换句话说,使用 MLP。就像在现实生活中,我们通过将复杂问题分解为小问题来解决问题,MLP 的第一层中的每个神经元将问题分解为小的线性可分问题。由于信息在这里是单向流动的,从输入层通过隐藏层流向输出层,这个网络也被称为前馈网络。在下图中,我们可以看到如何通过第一层的两个神经元和输出层中的一个神经元来解决XOR问题。网络将不可线性分割的问题分解为三个线性可分的问题:

上图可以通过使用具有一个隐藏层和一个输出层神经元的 MLP 来解释 XOR 的解决方案。红色点表示零,蓝色点表示一。我们可以看到,隐藏层神经元将问题分解为两个线性可分的问题(与、或),然后输出神经元实现另一个线性可分的逻辑——与非逻辑。将它们结合在一起,我们能够解决 XOR 问题,而 XOR 是不可线性分割的。

隐藏层神经元将问题转换成输出层可以使用的形式。多层神经元的思想最早由 McCulloch 和 Pitts 提出,但虽然 Rosenblatt 为简单的感知器提供了学*算法,但他无法训练多层感知器。主要的困难在于,对于输出神经元,我们知道应该期望的输出是什么,因此可以计算误差,从而使用梯度下降法来计算损失函数和权重更新,但是对于隐藏层神经元却没有办法知道期望的输出。因此,在没有任何学*算法的情况下,MLP(多层感知器)从未被广泛探索。这一情况在 1982 年发生了变化,当时 Hinton 提出了反向传播算法(www.researchgate.net/profile/Yann_Lecun/publication/2360531_A_Theoretical_Framework_for_Back-Propagation/links/0deec519dfa297eac1000000/A-Theoretical-Framework-for-Back-Propagation.pdf),该算法可以用来计算误差,从而计算隐藏层神经元的权重更新。他们采用了链式法则的一个简单直接的数学技巧,解决了将输出层的误差传递回隐藏层神经元的问题,从而给神经网络带来了新的生机。今天,反向传播算法几乎是所有深度学*模型的核心。

反向传播算法

首先,让我们对反向传播算法背后的技术有一个简单的了解。如果你还记得上一节的内容,输出神经元的损失函数如下:

你可以看到它保持不变,因此连接隐藏层神经元k到输出神经元j的权重如下所示:

应用链式法则,这简化为以下形式:

在前面的方程中,O[k]是隐藏神经元k的输出。现在,连接输入神经元i到隐藏层n中隐藏神经元k的权重更新可以写成以下形式:

再次应用链式法则,它简化为以下形式:

在这里,O[i]n-1^(th) 隐藏层中隐藏神经元 i 的输出。由于我们使用的是 TensorFlow,因此无需担心计算这些梯度,但了解这些表达式还是很有帮助的。从这些表达式中,你可以看到为什么激活函数的可微性非常重要。权重更新在很大程度上依赖于激活函数的导数以及神经元的输入。因此,像 ReLU 和 ELU 这样的平滑导数函数会导致更快的收敛。如果导数变得太大,我们会遇到梯度爆炸的问题;如果导数接*零,我们会遇到梯度消失的问题。在这两种情况下,网络都无法进行最佳学*。

通用逼*定理:1989 年,Hornik 等人和 George Cybenko 独立证明了通用逼*定理。该定理以最简单的形式表述为,在激活函数满足温和假设的前提下,足够大的前馈多层感知器具有单个隐藏层,可以以我们所需的任何精度逼*任何 Borel 可测函数。

用更简单的话来说,这意味着神经网络是一个通用逼*器,我们可以逼*任何函数,列举如下:

  • 我们可以使用单个隐藏层前馈网络来实现这一点。

  • 只要网络足够大(即如果需要,可以增加更多的隐藏神经元),我们就能做到这一点。

  • Cybenko 证明了在隐藏层使用 sigmoid 激活函数,在输出层使用线性激活函数的情况。后来,Hornik 等人证明了这实际上是 MLP 的一个性质,并且可以证明对于其他激活函数也是成立的

该定理保证了 MLP 可以解决任何问题,但没有给出网络应有多大的衡量标准。此外,它也不保证学*和收敛。

你可以参考以下链接中的论文:

现在我们可以描述反向传播算法中涉及的步骤,列举如下:

  1. 将输入应用到网络中

  2. 将输入向前传播并计算网络的输出

  3. 计算输出的损失,然后使用前面的表达式,计算输出层神经元的权重更新

  4. 使用输出层的加权误差,计算隐藏层的权重更新

  5. 更新所有的权重

  6. 对其他训练示例重复这些步骤

使用 MLP 在 TensorFlow 中进行能量输出预测

现在让我们看看 MLP 在预测能量输出方面的表现如何。这将是一个回归问题。我们将使用一个单一的隐藏层 MLP,预测联合循环发电厂的每小时净电能输出。数据集的描述见于第一章《物联网与人工智能的原理与基础》。

由于这是一个回归问题,我们的损失函数与之前相同。实现MLP类的完整代码如下:

class MLP:
    def __init__(self,n_input=2,n_hidden=4, n_output=1, act_func=[tf.nn.elu, tf.sigmoid], learning_rate= 0.001):
        self.n_input = n_input # Number of inputs to the neuron
        self.act_fn = act_func
        seed = 123

        self.X = tf.placeholder(tf.float32, name='X', shape=[None,n_input])
        self.y = tf.placeholder(tf.float32, name='Y')

        # Build the graph for a single neuron
        # Hidden layer
        self.W1 = tf.Variable(tf.random_normal([n_input,n_hidden],\
                 stddev=2, seed = seed), name = "weights")    
        self.b1 = tf.Variable(tf.random_normal([1, n_hidden], seed = seed),\
                    name="bias")    
        tf.summary.histogram("Weights_Layer_1",self.W1)
        tf.summary.histogram("Bias_Layer_1", self.b1)

        # Output Layer
        self.W2 = tf.Variable(tf.random_normal([n_hidden,n_output],\
                stddev=2, seed = 0), name = "weights")
        self.b2 = tf.Variable(tf.random_normal([1, n_output], seed = seed),\
                name="bias")
        tf.summary.histogram("Weights_Layer_2",self.W2)
        tf.summary.histogram("Bias_Layer_2", self.b2)

        activity = tf.matmul(self.X, self.W1) + self.b1
        h1 = self.act_fn0

        activity = tf.matmul(h1, self.W2) + self.b2
        self.y_hat = self.act_fn1

        error = self.y - self.y_hat

        self.loss = tf.reduce_mean(tf.square(error))\
                 + 0.6*tf.nn.l2_loss(self.W1) 
        self.opt = tf.train.GradientDescentOptimizer(learning_rate\
                    =learning_rate).minimize(self.loss)        

        tf.summary.scalar("loss",self.loss)
        init = tf.global_variables_initializer()

        self.sess = tf.Session()
        self.sess.run(init)

        self.merge = tf.summary.merge_all()
        self.writer = tf.summary.FileWriter("logs/",\
                graph=tf.get_default_graph())

     def train(self, X, Y, X_val, Y_val, epochs=100):
        epoch = 0
        X, Y = shuffle(X,Y)
        loss = []
        loss_val = []
        while epoch &amp;lt; epochs:
            # Run the optimizer for the training set 
            merge, _, l = self.sess.run([self.merge,self.opt,self.loss],\
                     feed_dict={self.X: X, self.y: Y})
            l_val = self.sess.run(self.loss, feed_dict=\
                    {self.X: X_val, self.y: Y_val})

            loss.append(l)
            loss_val.append(l_val)
            self.writer.add_summary(merge, epoch)

            if epoch % 10 == 0:
                print("Epoch {}/{} training loss: {} Validation loss {}".\
                    format(epoch,epochs,l, l_val ))

            epoch += 1
        return loss, loss_val

    def predict(self, X):
        return self.sess.run(self.y_hat, feed_dict={self.X: X})

在使用之前,我们来看看之前的代码和我们为单个人工神经元编写的代码之间的区别。这里,隐藏层的权重维度是#inputUnits × #hiddenUnits;隐藏层的偏置将等于隐藏单元的数量(#hiddenUnits)。输出层的权重维度是#hiddenUnits × #outputUnits;输出层的偏置维度与输出层单元的数量(#outputUnits)相同。

在定义偏置时,我们只使用了列维度,而没有使用行维度。这是因为像numpy一样,TensorFlow 根据执行的操作对矩阵进行广播。通过不固定偏置的行维度,我们能够保持输入训练样本数(批量大小)对网络的灵活性。

以下截图展示了计算活动时的矩阵乘法和加法维度:

计算活动时的矩阵乘法和加法维度

你需要注意的第二个区别是在损失的定义中,我们在这里加入了l2正则化项,以减少过拟合,如在第三章《物联网的机器学*》中讨论的那样,具体如下:

self.loss = tf.reduce_mean(tf.square(error)) + 0.6*tf.nn.l2_loss(self.W1) 

在从csv文件中读取数据并像之前一样将其分为训练集和验证集后,我们定义了一个MLP类对象,输入层有4个神经元,隐藏层有15个神经元,输出层有1个神经元:

_, d = X_train.shape
_, n = Y_train.shape
model = MLP(n_input=d, n_hidden=15, n_output=n)

在以下代码中,我们将在训练数据集上训练模型6000个周期:

loss, loss_val = model.train(X_train, Y_train, X_val, y_val, 6000)

这个训练好的网络给出了 0.016 的均方误差(MSE)和 0.67 的值。两者都优于我们从单个神经元得到的结果,并且与我们在第三章《物联网的机器学*》中研究的机器学*方法相当。完整代码可以在名为MLP_regresssion.ipynb的文件中找到。

你可以调整超参数,包括:隐藏神经元的数量、激活函数、学*率、优化器和正则化系数,并能获得更好的结果。

使用 MLP 在 TensorFlow 中进行葡萄酒质量分类

MLP 也可以用于分类任务。我们可以在之前的基础上对 MLP 类进行少量修改来执行分类任务。

我们需要做以下两个主要的修改:

  • 在分类任务中,目标将会是 one-hot 编码的。

  • 损失函数现在将是类别交叉熵损失:tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=self.y_hat, labels=self.y))

现在,让我们来看看完整的代码,这个代码也可以在 GitHub 上的 MLP_classification 文件中找到。我们将对红酒质量进行分类,为了方便起见,我们只使用两类红酒:

  1. 我们导入了必要的模块,即:TensorFlow、Numpy、Matplotlib,以及来自 scikit-learn 的某些函数,代码如下所示:
import tensorflow as tf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.utils import shuffle
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
% matplotlib inline
  1. 我们定义了 MLP 类,它与您之前看到的 MLP 类非常相似,唯一的不同是在损失函数的定义上:
class MLP:
    def __init__(self,n_input=2,n_hidden=4, n_output=1, act_func=[tf.nn.relu, tf.nn.sigmoid], learning_rate= 0.001):
        self.n_input = n_input # Number of inputs to the neuron
        self.act_fn = act_func
        seed = 456

        self.X = tf.placeholder(tf.float32, name='X', shape=[None,n_input])
        self.y = tf.placeholder(tf.float32, name='Y')

        # Build the graph for a single neuron
        # Hidden layer
        self.W1 = tf.Variable(tf.random_normal([n_input,n_hidden],\
             stddev=2, seed = seed), name = "weights")
        self.b1 = tf.Variable(tf.random_normal([1, n_hidden],\
             seed = seed), name="bias")
        tf.summary.histogram("Weights_Layer_1",self.W1)
        tf.summary.histogram("Bias_Layer_1", self.b1)

        # Output Layer
        self.W2 = tf.Variable(tf.random_normal([n_hidden,n_output],\
            stddev=2, seed = seed), name = "weights")
        self.b2 = tf.Variable(tf.random_normal([1, n_output],\
             seed = seed), name="bias")    
        tf.summary.histogram("Weights_Layer_2",self.W2)
        tf.summary.histogram("Bias_Layer_2", self.b2)

        activity1 = tf.matmul(self.X, self.W1) + self.b1
        h1 = self.act_fn0

        activity2 = tf.matmul(h1, self.W2) + self.b2
        self.y_hat = self.act_fn1

        self.loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(\
                logits=self.y_hat, labels=self.y))
        self.opt = tf.train.AdamOptimizer(learning_rate=\
                learning_rate).minimize(self.loss)

        tf.summary.scalar("loss",self.loss)
        init = tf.global_variables_initializer()

        self.sess = tf.Session()
        self.sess.run(init)

        self.merge = tf.summary.merge_all()
        self.writer = tf.summary.FileWriter("logs/",\
             graph=tf.get_default_graph())

    def train(self, X, Y, X_val, Y_val, epochs=100):
        epoch = 0
        X, Y = shuffle(X,Y)
        loss = []
        loss_val = []
        while epoch &amp;lt; epochs:
            # Run the optimizer for the training set 
            merge, _, l = self.sess.run([self.merge,self.opt,self.loss],\
                 feed_dict={self.X: X, self.y: Y})        
            l_val = self.sess.run(self.loss, feed_dict={self.X: X_val, self.y: Y_val})

            loss.append(l)
            loss_val.append(l_val)
            self.writer.add_summary(merge, epoch)

            if epoch % 10 == 0:
                print("Epoch {}/{} training loss: {} Validation loss {}".\
                    format(epoch,epochs,l, l_val ))

            epoch += 1
        return loss, loss_val

    def predict(self, X):
        return self.sess.run(self.y_hat, feed_dict={self.X: X})
  1. 接下来,我们读取数据,对其进行标准化和预处理,以便将红酒质量进行 one-hot 编码,并使用两个标签。我们还将数据划分为训练集和验证集,如下所示:
filename = 'winequality-red.csv' 
#Download the file from https://archive.ics.uci.edu/ml/datasets/wine+quality
df = pd.read_csv(filename, sep=';')
columns = df.columns.values
# Preprocessing and Categorizing wine into two categories
X, Y = df[columns[0:-1]], df[columns[-1]]
scaler = MinMaxScaler()
X_new = scaler.fit_transform(X)
#Y.loc[(Y&amp;lt;3.5)]=3
Y.loc[(Y&amp;lt;5.5) ] = 2
Y.loc[(Y&amp;gt;=5.5)] = 1
Y_new = pd.get_dummies(Y) # One hot encode
X_train, X_val, Y_train, y_val = \
 train_test_split(X_new, Y_new, test_size=0.2, random_state=333)
  1. 我们定义一个MLP对象并对其进行训练,代码如下所示:
_, d = X_train.shape
_, n = Y_train.shape
model = MLP(n_input=d, n_hidden=5, n_output=n)
loss, loss_val = model.train(X_train, Y_train, X_val, y_val, 10000)
  1. 接下来,您可以看到训练结果,随着网络的学*,交叉熵损失逐渐减少:
plt.plot(loss, label="Taining Loss")
plt.plot(loss_val, label="Validation Loss")
plt.legend()
plt.xlabel("Epochs")
plt.ylabel("Cross Entropy Loss")

  1. 在验证数据集上测试训练后的网络,准确率为 77.8%。验证集上的confusion_matrix如下所示:
from sklearn.metrics import confusion_matrix, accuracy_score
import seaborn as sns
cm = confusion_matrix(np.argmax(np.array(y_val),1), np.argmax(Y_pred,1))
sns.heatmap(cm,annot=True,fmt='2.0f')

这些结果与我们使用 ML 算法获得的结果相当。通过调整超参数,我们可以进一步改善结果。

卷积神经网络

MLP 很有趣,但正如您在上一节中使用 MLP 代码时所观察到的那样,随着输入空间复杂度的增加,学*时间也会增加;此外,MLP 的表现仅次于 ML 算法。无论您能在 MLP 中做什么,使用第三章《物联网的机器学*》中的 ML 算法,您都有很大可能做得更好。正因如此,尽管反向传播算法早在 1980 年代就已出现,但我们还是经历了第二次人工智能寒冬,大约从 1987 年到 1993 年。

这一切发生了变化,神经网络不再在 2010 年代的深度神经网络发展中屈居 ML 算法的“配角”。如今,深度学*在计算机视觉的多种任务中,像是识别交通信号(people.idsia.ch/~juergen/cvpr2012.pdf)、人脸(www.cv-foundation.org/openaccess/content_cvpr_2014/papers/Taigman_DeepFace_Closing_the_2014_CVPR_paper.pdf)、手写数字(cs.nyu.edu/~wanli/dropc/dropc.pdf)等,已经达到了人类水平或超越人类水平的表现。这个名单还在不断增长。

CNN 是这个成功故事的重要组成部分。在本节中,您将学* CNN、CNN 背后的数学原理以及一些流行的 CNN 架构。

CNN 的不同层

CNN 由三种主要类型的神经元层组成:卷积层、池化层和全连接层。全连接层就是 MLP 层,它们总是 CNN 的最后几层,执行分类或回归的最终任务。让我们看看卷积层和最大池化层是如何工作的。

卷积层

这是 CNN 的核心构建模块。它对输入数据(通常是 3D 图像)执行类似卷积的数学操作(更精确地说是交叉相关)。它由卷积核(滤波器)定义。基本的思路是,这些滤波器遍历整个图像,并从中提取特定的特征。

在深入细节之前,首先让我们看一下卷积操作在二维矩阵上的应用,简化问题。以下图示展示了当一个像素位于 5×5二维图像矩阵的[2, 2]位置,并与 3×3 滤波器进行卷积时的操作:

单像素上的卷积操作

卷积操作包括将滤波器放置在像素的中心位置,然后对滤波器元素与该像素及其邻居之间进行逐元素相乘,并最终求和。由于卷积操作是针对单个像素进行的,因此滤波器通常是奇数大小,如 5×5、3×3 或 7×7 等。滤波器的大小指定了它覆盖的邻域范围。

设计卷积层时的重要参数如下:

  • 滤波器的大小(k×k)。

  • 层中的滤波器数量,也称为通道。输入的彩色图像包含三个 RGB 通道。通道的数量通常在更高的层次中增加,从而使得高层具有更深的信息。

  • 滤波器在图像中滑动的像素数(s)。通常,步幅为一个像素,以便滤波器从左上角到右下角覆盖整个图像。

  • 卷积时使用的填充。传统上,有两种选项,分别是 valid 或 same。在valid填充中,完全没有填充,因此卷积后的图像大小比原图小。在same填充中,围绕边界像素进行零填充,以确保卷积后图像的大小与原图相同。以下截图展示了完整的卷积图像。当使用 valid 填充时,绿色的 3×3 方框是结果,右侧的完整 5×5 矩阵是使用 same 填充时的结果:

在 5×5 图像上应用的卷积操作

右侧的绿色方块将是有效填充的结果。对于相同填充,我们将得到右侧显示的完整 5×5 矩阵。

池化层

卷积层后面通常跟着一个池化层。池化层的目的是逐步减小表示的大小,从而减少网络中的参数和计算量。因此,它以前馈的方式对信息进行下采样,随着信息在网络中的传播。

这里我们再次使用一个滤波器,传统上人们偏好使用大小为 2×2 的滤波器,并且它在两个方向上以两个像素的步长进行移动。池化过程将 2×2 滤波器下的四个元素替换为四个中的最大值(最大池化)或四个中的平均值(平均池化)。在下图中,你可以看到池化操作对二维单通道图像切片的结果:

图像的二维单通道切片上的最大池化和平均池化操作

多个卷积池化层被堆叠在一起形成深度 CNN。当图像通过 CNN 传播时,每个卷积层提取特定的特征。较低的层提取粗略的特征,如形状、曲线、线条等,而较高的层提取更抽象的特征,如眼睛、嘴唇等。随着图像在网络中的传播,尺寸逐渐减小,但深度逐渐增加。最后一层卷积层的输出被展平并传递到全连接层,如下图所示:

CNN 网络的基本架构

滤波器矩阵的值也称为权重,并且它们在整个图像中是共享的。这种共享减少了训练参数的数量。权重通过反向传播算法由网络学*。由于我们将使用 TensorFlow 的自动微分功能,因此我们不需要计算卷积层的权重更新的精确表达式。

一些常见的 CNN 模型

以下是一些常见的 CNN 模型列表:

  • LeNet:LeNet 是第一个成功应用于识别手写数字的 CNN。它由 Yann LeCun 在 1990 年代开发。你可以在 Yann LeCun 的主页上了解更多关于 LeNet 架构及相关出版物的信息(yann.lecun.com/exdb/lenet/)。

  • VGGNet:这是 ILSVRC 2014 的亚军,由 Karen Simonyan 和 Andrew Zisserman 开发。它的第一个版本包含 16 个卷积+全连接层,被称为VGG16,后来他们推出了包含 19 层的 VGG19。有关其性能和出版物的详细信息可以从牛津大学网站访问(www.robots.ox.ac.uk/~vgg/research/very_deep/)。

  • ResNet:由 Kaiming He 等人开发,ResNet 是 ILSVRC 2015 年的获胜者。它使用了一个新的特性——残差学*批量归一化。它是一个非常深的网络,层数超过 100 层。众所周知,增加更多的层可以提高性能,但增加层数也会引入梯度消失问题。ResNet 通过使用身份快捷连接解决了这个问题,在这种连接中,信号跳过一个或多个层。你可以阅读原始论文以了解更多信息(arxiv.org/abs/1512.03385)。

  • GoogleNet:这是 ILSVRC 2014 年的获胜架构。它有 22 层,并引入了 inception 层的概念。基本思路是覆盖更大的区域,同时保持对图像上小信息的高分辨率。因此,在每一层中,我们使用从 1×1(用于精细细节)到 5×5 的不同大小的滤波器,而不是单一大小的滤波器。所有滤波器的结果将连接在一起并传递到下一层,这一过程会在下一个 inception 层中重复。

使用 LeNet 识别手写数字

在接下来的章节中,我们将使用一些流行的 CNN 及其变体来解决图像和视频处理任务。现在,让我们使用 Yann LeCun 提出的 LeNet 架构来识别手写数字。这个架构曾被美国邮政服务用于识别他们收到的信件上的手写邮政编码(yann.lecun.com/exdb/publis/pdf/jackel-95.pdf)。

LeNet 包含五个层,其中有两个卷积最大池化层和三个全连接层。该网络还使用了 dropout 特性,即在训练时,某些权重会被关闭。这迫使其他连接来补偿这些权重,从而有助于克服过拟合:

  1. 我们导入必要的模块,如下所示:
# Import Modules
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
  1. 接下来,我们创建LeNet类对象,它将包含必要的 CNN 架构和模块来进行训练和预测。在__init__方法中,我们定义了所有需要的占位符来保存输入图像及其输出标签。我们还定义了损失函数,由于这是一个分类问题,我们使用交叉熵损失,如以下代码所示:
# Define your Architecture here
import tensorflow as tf
from tensorflow.contrib.layers import flatten
class my_LeNet:
    def __init__(self, d, n, mu = 0, sigma = 0.1, lr = 0.001):
        self.mu = mu
        self.sigma = sigma
        self.n = n
        # place holder for input image dimension 28 x 28
        self.x = tf.placeholder(tf.float32, (None, d, d, 1)) 
        self.y = tf.placeholder(tf.int32, (None,n))
        self.keep_prob = tf.placeholder(tf.float32) # probability to keep units

        self.logits = self.model(self.x)
        # Define the loss function
        cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=self.y,\
                        logits=self.logits)
        self.loss = tf.reduce_mean(cross_entropy)
        optimizer = tf.train.AdamOptimizer(learning_rate = lr)
        self.train = optimizer.minimize(self.loss)
        correct_prediction = tf.equal(tf.argmax(self.logits, 1), tf.argmax(self.y, 1))
        self.accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        init = tf.global_variables_initializer()
        self.sess = tf.Session()
        self.sess.run(init)
        self.saver = tf.train.Saver()
  1. model方法是实际构建卷积网络架构图的方法。我们使用 TensorFlow 的tf.nn.conv2d函数来构建卷积层。该函数接受一个参数,即定义为权重的滤波器矩阵,并计算输入与滤波器矩阵之间的卷积。我们还使用偏置来提供更大的自由度。在两个卷积层之后,我们将输出展平并传递到全连接层,如下所示:
def model(self,x):
    # Build Architecture
    keep_prob = 0.7
    # Layer 1: Convolutional. Filter 5x5 num_filters = 6 Input_depth =1
    conv1_W = tf.Variable(tf.truncated_normal(shape=(5, 5, 1, 6), mean \
                    = self.mu, stddev = self.sigma))
    conv1_b = tf.Variable(tf.zeros(6))
    conv1 = tf.nn.conv2d(x, conv1_W, strides=[1, 1, 1, 1], padding='VALID') + conv1_b
    conv1 = tf.nn.relu(conv1)

    # Max Pool 1
    self.conv1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1],\
                     strides=[1, 2, 2, 1], padding='VALID')

    # Layer 2: Convolutional. Filter 5x5 num_filters = 16 Input_depth =6
    conv2_W = tf.Variable(tf.truncated_normal(shape=(5, 5, 6, 16), \
                    mean = self.mu, stddev = self.sigma))
    conv2_b = tf.Variable(tf.zeros(16))
    conv2 = tf.nn.conv2d(self.conv1, conv2_W, strides=[1, 1, 1, 1],\
                     padding='VALID') + conv2_b
    conv2 = tf.nn.relu(conv2)

    # Max Pool 2.
    self.conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], \
                    strides=[1, 2, 2, 1], padding='VALID')

    # Flatten.
    fc0 = flatten(self.conv2)
    print("x shape:",fc0.get_shape())

    # Layer 3: Fully Connected. Input = fc0.get_shape[-1]. Output = 120.
    fc1_W = tf.Variable(tf.truncated_normal(shape=(256, 120), \
                mean = self.mu, stddev = self.sigma))
    fc1_b = tf.Variable(tf.zeros(120))
    fc1 = tf.matmul(fc0, fc1_W) + fc1_b
    fc1 = tf.nn.relu(fc1)

    # Dropout
    x = tf.nn.dropout(fc1, keep_prob)

    # Layer 4: Fully Connected. Input = 120\. Output = 84.
    fc2_W = tf.Variable(tf.truncated_normal(shape=(120, 84), \
                    mean = self.mu, stddev = self.sigma))
    fc2_b = tf.Variable(tf.zeros(84))
    fc2 = tf.matmul(x, fc2_W) + fc2_b
    fc2 = tf.nn.relu(fc2)

    # Dropout
    x = tf.nn.dropout(fc2, keep_prob)

    # Layer 6: Fully Connected. Input = 120\. Output = n_classes.
    fc3_W = tf.Variable(tf.truncated_normal(shape=(84, self.n), \
                    mean = self.mu, stddev = self.sigma))
    fc3_b = tf.Variable(tf.zeros(self.n))
    logits = tf.matmul(x, fc3_W) + fc3_b
    #logits = tf.nn.softmax(logits)
    return logits
  1. fit方法执行按批次的训练,而predict方法为给定输入提供输出,如下所示:
def fit(self,X,Y,X_val,Y_val,epochs=10, batch_size=100):
    X_train, y_train = X, Y
    num_examples = len(X_train)
    l = []
    val_l = []
    max_val = 0
    for i in range(epochs):
        total = 0
        for offset in range(0, num_examples, batch_size): # Learn Batch wise
            end = offset + batch_size
            batch_x, batch_y = X_train[offset:end], y_train[offset:end]
            _, loss = self.sess.run([self.train,self.loss], \
                        feed_dict={self.x: batch_x, self.y: batch_y})
            total += loss
            l.append(total/num_examples)
            accuracy_val = self.sess.run(self.accuracy, \
                                feed_dict={self.x: X_val, self.y: Y_val})
            accuracy = self.sess.run(self.accuracy, feed_dict={self.x: X, self.y: Y})
            loss_val = self.sess.run(self.loss, feed_dict={self.x:X_val,self.y:Y_val})
            val_l.append(loss_val)
            print("EPOCH {}/{} loss is {:.3f} training_accuracy {:.3f} and \
                        validation accuracy is {:.3f}".\
                        format(i+1,epochs,total/num_examples, accuracy, accuracy_val))
            # Saving the model with best validation accuracy
            if accuracy_val &amp;gt; max_val:
                save_path = self.saver.save(self.sess, "/tmp/lenet1.ckpt")
                print("Model saved in path: %s" % save_path)
                max_val = accuracy_val

    #Restore the best model
    self.saver.restore(self.sess, "/tmp/lenet1.ckpt")
    print("Restored model with highest validation accuracy")
    accuracy_val = self.sess.run(self.accuracy, feed_dict={self.x: X_val, self.y: Y_val})
    accuracy = self.sess.run(self.accuracy, feed_dict={self.x: X, self.y: Y})
    return l,val_l, accuracy, accuracy_val

def predict(self, X):
    return self.sess.run(self.logits,feed_dict={self.x:X})
  1. 我们使用手写数字数据集,并从 Kaggle 下载(www.kaggle.com/c/digit-recognizer/data)。数据集以 .csv 格式提供。我们加载 .csv 文件并对数据进行预处理。以下是样本训练图:
def load_data():
    # Read the data and create train, validation and test dataset
    data = pd.read_csv('train.csv')
    # This ensures always 80% of data is training and 
    # rest Validation unlike using np.random
    train = data.sample(frac=0.8, random_state=255) 
    val = data.drop(train.index)
    test = pd.read_csv('test.csv')
    return train, val, test

def create_data(df):
    labels = df.loc[:]['label']
    y_one_hot = pd.get_dummies(labels).astype(np.uint8)
    y = y_one_hot.values # One Hot encode the labels
    x = df.iloc[:,1:].values
    x = x.astype(np.float)
    # Normalize data
    x = np.multiply(x, 1.0 / 255.0)
    x = x.reshape(-1, 28, 28, 1) # return each images as 96 x 96 x 1
    return x,y

train, val, test = load_data()
X_train, y_train = create_data(train)
X_val, y_val = create_data(val)
X_test = (test.iloc[:,:].values).astype(np.float)
X_test = np.multiply(X_test, 1.0 / 255.0)
X_test = X_test.reshape(-1, 28, 28, 1) # return each images as 96 x 96 x 1

# Plot a subset of training data
x_train_subset = X_train[:12]

# visualize subset of training data
fig = plt.figure(figsize=(20,2))
for i in range(0, len(x_train_subset)):
    ax = fig.add_subplot(1, 12, i+1)
    ax.imshow(x_train_subset[i].reshape(28,28), cmap='gray')
fig.suptitle('Subset of Original Training Images', fontsize=20)
plt.show()

在这里,我们将训练模型:

n_train = len(X_train)
# Number of validation examples
n_validation = len(X_val)

# Number of testing examples.
n_test = len(X_test)

# What's the shape of an handwritten digits?
image_shape = X_train.shape[1:-1]

# How many unique classes/labels there are in the dataset.
n_classes = y_train.shape[-1]
print("Number of training examples =", n_train)
print("Number of Validation examples =", n_validation)
print("Number of testing examples =", n_test)
print("Image data shape =", image_shape)
print("Number of classes =", n_classes)

# The result
## &amp;gt;&amp;gt;&amp;gt; Number of training examples = 33600
## &amp;gt;&amp;gt;&amp;gt; Number of Validation examples = 8400 
## &amp;gt;&amp;gt;&amp;gt; Number of testing examples = 28000 
## &amp;gt;&amp;gt;&amp;gt; Image data shape = (28, 28) 
## &amp;gt;&amp;gt;&amp;gt; Number of classes = 10

# Define the data values
d = image_shape[0]
n = n_classes
from sklearn.utils import shuffle
X_train, y_train = shuffle(X_train,y_train)
  1. 我们创建 LeNet 对象并在训练数据上进行训练。训练数据集的准确率为 99.658%,验证数据集的准确率为 98.607%:
# Create the Model
my_model = my_LeNet(d, n)

### Train model  here.
loss, val_loss, train_acc, val_acc = my_model.fit(X_train, y_train, \
    X_val, y_val, epochs=50) 

印象深刻!你可以预测测试数据集的输出并在 Kaggle 上提交。

循环神经网络

迄今为止,我们学*的模型仅响应当前输入。你给它一个输入,基于它所学的知识,它会给出相应的输出。但这并不是我们人类的工作方式。当你读一个句子时,你不会单独理解每个词,而是会考虑前面的词汇来推断它的语义。

意思是。

RNN 能够解决这个问题。它们利用反馈回路来保留信息。反馈回路允许信息从前一个步骤传递到当前步骤。下图展示了 RNN 的基本架构,以及反馈如何使信息从网络的一个步骤传递到下一个步骤(展开):

循环神经网络

在上图中,X 代表输入。它通过权重 W[hx] 连接到隐藏层的神经元,隐藏层的输出 h 通过权重 W[hh] 反馈到隐藏层,同时也通过权重 W[yh] 影响输出 O。我们可以将这些数学关系写成如下形式:

其中 g 是激活函数,b[h]b[y] 分别是隐藏层和输出层神经元的偏置项。在前述关系中,XhO 都是向量;W[hx]W[hh]W[yh] 都是矩阵。输入 X 和输出 O 的维度取决于你使用的数据集,隐藏层 h 的单元数由你决定;你会发现许多论文中,研究者使用了 128 个隐藏单元。前述架构只展示了一个隐藏层,但我们可以根据需要添加任意数量的隐藏层。RNN 已被应用于自然语言处理领域,也用于分析时间序列数据,比如股价。

RNN 通过一种叫做 时间反向传播 (BPTT) 的算法进行学*,这是一种反向传播算法的修改版,考虑了数据的时间序列特性。这里,损失函数被定义为从 t=1t=T(要展开的时间步数)的所有损失函数之和,例如:

其中,L^((t))是时间t的损失,我们像之前一样应用链式法则进行微分,并推导出权重W[hx]W[hh]W[yh]的更新。

本书中我们不推导权重更新的表达式,因为我们不会实现代码。TensorFlow 提供了 RNN 和 BPTT 的实现。但是对于那些有兴趣深入数学细节的读者,以下是一些参考文献:

我们呈现了每个时间步有一个输入的 RNN,并预测相应的输出。BPTT 通过展开所有输入时间步来工作。误差在每个时间步计算并积累,之后网络会回滚以更新权重。BPTT 的一个缺点是当时间步数增加时,计算量也会增加,这使得整个模型的计算代价较高。此外,由于多次梯度乘法,网络容易出现梯度消失问题。

为了解决这个问题,通常使用 BPTT 的修改版——截断 BPTT。在截断 BPTT 中,数据一次处理一个时间步,BPTT 的权重更新定期在固定的时间步数内执行。

我们可以按以下步骤列举截断 BPTT 算法:

  1. 向网络呈现K[1]时间步的输入和输出对的序列

  2. 通过展开网络,计算并积累K[2]时间步的误差

  3. 通过回滚网络来更新权重

算法的表现取决于两个超参数K[1]K[2]。更新之间的前向传递时间步数由K[1]表示,它影响训练的速度和权重更新的频率。而K[2]则表示应用于 BPTT 的时间步数,它应该足够大,以捕捉输入数据的时间结构。

长短期记忆(LSTM)

Hochreiter 和 Schmidhuber 在 1997 年提出了一种修改版的 RNN 模型,称为 长短期记忆 (LSTM),用以解决梯度消失问题。RNN 中的隐藏层被 LSTM 单元取代。

LSTM 单元由三个门组成:忘记门、输入门和输出门。这些门控制着单元生成和保持的长期记忆和短期记忆的量。这些门都使用 sigmoid 函数,将输入压缩到 01 之间。接下来,我们将看到各种门的输出是如何计算的。如果这些表达式看起来有些难以理解,不用担心,我们将使用 TensorFlow 中的 tf.contrib.rnn.BasicLSTMCelltf.contrib.rnn.static_rnn 来实现 LSTM 单元,见下图:

基本的 LSTM 单元,x 是输入,h 是短期记忆,c 是长期记忆。下标表示时间步。

在每个时间步 t,LSTM 单元接收三个输入:输入 x[t]、短期记忆 h[t-1] 和长期记忆 c[t-1],并输出长期记忆 c[t] 和短期记忆 h[t]xhc 的下标表示时间步。

忘记门 f(.) 控制当前时间步中要记住的短期记忆 h 的量,以便在后续步骤中继续流动。从数学角度,我们可以表示忘记门 f(.) 如下:

其中 σ 代表 sigmoid 激活函数,W[fx]W[fh] 是控制输入 x[t]、短期记忆 h[t-1] 和 b[f] 忘记门偏置的权重。

输入门 i(.) 控制输入和工作记忆对单元输出的影响。我们可以将其表示如下:

输出门 o(.) 控制用于更新短期记忆的信息量,表达式如下:

除了这三个门,LSTM 单元还计算候选隐藏状态 ,它与输入门和忘记门一起用于计算长期记忆 c[t] 的量:

圆圈表示逐元素相乘。短期记忆的新值接下来按如下方式计算:

现在让我们来看看如何在 TensorFlow 中实现 LSTM,按以下步骤操作:

  1. 我们使用以下模块:
import tensorflow as tf
from tensorflow.contrib import rnn
import numpy as np
  1. 我们定义了一个 LSTM 类,在其中构建了图并使用 TensorFlow 的contrib定义了 LSTM 层。为了处理内存,我们首先清除默认的图堆栈并使用tf.reset_default_graph()重置全局默认图。输入直接传递到具有num_units个隐藏单元的 LSTM 层。随后是一个带有out_weights权重和out_bias偏置的全连接输出层。创建输入self.x和标签self.y的占位符。输入被重塑并馈送到 LSTM 单元。为了创建 LSTM 层,我们首先定义具有num_units隐藏单元和遗忘偏置设置为1.0的 LSTM 单元。这会在遗忘门中添加偏置,以减少训练开始时的遗忘规模。将来自 LSTM 层的输出重塑并馈送到全连接层,如下所示:
 class LSTM:
    def __init__(self, num_units, n_classes, n_input,\
             time_steps, learning_rate=0.001,):    
        tf.reset_default_graph()
        self.steps = time_steps
        self.n = n_input
        # weights and biases of appropriate shape
        out_weights = tf.Variable(tf.random_normal([num_units, n_classes]))
        out_bias = tf.Variable(tf.random_normal([n_classes]))
        # defining placeholders
        # input placeholder
        self.x = tf.placeholder("float", [None, self.steps, self.n])
        # label placeholder
        self.y = tf.placeholder("float", [None, n_classes])
        # processing the input tensor from [batch_size,steps,self.n] to 
        # "steps" number of [batch_size,self.n] tensors
        input = tf.unstack(self.x, self.steps, 1)

        # defining the network
        lstm_layer = rnn.BasicLSTMCell(num_units, forget_bias=1)
        outputs, _ = rnn.static_rnn(lstm_layer, input, dtype="float32")
        # converting last output of dimension [batch_size,num_units] to 
        # [batch_size,n_classes] by out_weight multiplication
        self.prediction = tf.matmul(outputs[-1], out_weights) + out_bias

        # loss_function
        self.loss = tf.reduce_mean(tf.squared_difference(self.prediction, self.y))
        # optimization
        self.opt = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(self.loss)

        # model evaluation
        correct_prediction = tf.equal(tf.argmax(self.prediction, 1), tf.argmax(self.y, 1))
        self._accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

        init = tf.global_variables_initializer()
        gpu_options = tf.GPUOptions(allow_growth=True)

        self.sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))
        self.sess.run(init)
  1. 我们创建了训练和预测的方法,如下所示的代码:
def train(self, X, Y, epochs=100,batch_size=128):
    iter = 1
    #print(X.shape)
    X = X.reshape((len(X),self.steps,self.n))
    while iter &amp;lt; epochs:
        for i in range(int(len(X)/batch_size)):
            batch_x, batch_y = X[i:i+batch_size,:], Y[i:i+batch_size,:]
            #print(batch_x.shape)
            #batch_x = batch_x.reshape((batch_size, self.steps, self.n))    
            #print(batch_x.shape)    
            self.sess.run(self.opt, feed_dict={self.x: batch_x, self.y: batch_y})
            if iter % 10 == 0:
                acc = self.sess.run(self._accuracy, feed_dict={self.x: X, self.y: Y})
                los = self.sess.run(self.loss, feed_dict={self.x: X, self.y: Y})
                print("For iter ", iter)
                print("Accuracy ", acc)
                print("Loss ", los)    
                print("__________________")
            iter = iter + 1

def predict(self,X):
    # predicting the output
    test_data = X.reshape((-1, self.steps, self.n))
    out = self.sess.run(self.prediction, feed_dict={self.x:test_data})
    return out

在接下来的章节中,我们将使用 RNN 处理时间序列生成和文本处理。

门控循环单元

门控循环单元GRU)是对 RNN 的另一种修改。它相对于 LSTM 有简化的架构,并且解决了梯度消失问题。它只接受两个输入,时间t处的输入x[t]和来自时间t-1 处的记忆h[t-1]。它有两个门,更新门重置门,如下图所示:

一个基本的 GRU 单元的结构

更新门控制要保留多少以前的记忆,重置门确定如何将新输入与以前的记忆结合。我们可以通过以下四个方程定义完整的 GRU 单元:

GRU 和 LSTM 表现相当,但 GRU 的训练参数更少。

自编码器

我们到目前为止学*的模型都是使用监督学*来学*的。在这一节中,我们将学*自编码器。它们是前馈非递归神经网络,通过无监督学*进行学*。它们和生成对抗网络一样是最新的热点,我们可以在图像重建、聚类、机器翻译等领域找到应用。最初由 Geoffrey E. Hinton 和 PDP 小组在 1980 年代提出(www.cs.toronto.edu/~fritz/absps/clp.pdf)。

自编码器基本上由两个级联的神经网络组成——第一个网络充当编码器;它接受输入x并使用变换h编码为编码信号y,如下方程所示:

第二个神经网络使用编码信号y作为输入,并执行另一个变换f以获得重建信号r,如图所示:

损失函数是均方误差(MSE),其中误差e定义为原始输入x与重建信号r之间的差异:

自编码器的基本架构

上面的图示展示了一个自编码器,其中编码器解码器被分别突出显示。自编码器可能会使用权重共享,即解码器和编码器的权重是共享的。这是通过简单地使它们互为转置来实现的;这有助于加速网络的学*,因为训练参数的数量较少。自编码器有很多种类,例如:稀疏自编码器、去噪自编码器、卷积自编码器和变分自编码器等。

去噪自编码器

去噪自编码器从被破坏(有噪声)的输入中学*;我们将有噪声的输入送入编码器网络,解码器重建的图像与原始去噪后的输入进行比较。其核心思想是,这样可以帮助网络学会如何去噪。网络不仅仅进行逐像素比较,相反,为了去噪,网络被迫学*邻*像素的信息。

一旦自编码器学会了编码特征y,我们就可以去掉网络中的解码器部分,仅使用编码器部分来实现降维。降维后的输入可以送入其他分类或回归模型中。

变分自编码器

另一种非常流行的自编码器是变分自编码器VAE)。它们结合了深度学*和贝叶斯推理的优点。

变分自编码器(VAE)有一个额外的随机层;该层在编码器网络之后,使用高斯分布对数据进行采样,而解码器网络之后的层使用伯努利分布对数据进行采样。

变分自编码器(VAE)可以用于生成图像。VAE 允许在潜在空间中设定复杂的先验分布,并学*强大的潜在表示。我们将在后续章节中深入了解它们。

总结

在本章中,我们介绍了一些基础且实用的深度神经网络模型。我们从单个神经元开始,了解了它的强大之处以及局限性。多层感知机被构建用于回归和分类任务,反向传播算法也在此时引入。接着我们深入探讨了卷积神经网络(CNN),并介绍了卷积层和池化层。我们学*了一些成功的 CNN 应用,并使用第一个 CNN 模型 LeNet 进行手写数字识别。从前馈型多层感知机(MLP)和卷积神经网络(CNN)出发,我们继续深入到循环神经网络(RNN)。随后介绍了 LSTM 和 GRU 网络。我们在 TensorFlow 中实现了自己的 LSTM 网络,最终学*了自编码器。

在下一章,我们将从一种全新的人工智能模型——遗传算法开始。像神经网络一样,它们也受到自然的启发。我们将在后续章节的案例研究中,运用本章以及接下来的几章所学的内容。

第五章:遗传算法在物联网中的应用

在上一章中,我们研究了不同的基于深度学*的算法;这些算法在识别、检测、重建甚至在视觉、语音和文本数据生成领域取得了成功。目前,深度学*DL)在应用和就业方面处于领先地位,但它与进化算法有着激烈的竞争。这些算法受自然进化过程的启发,是世界上最好的优化器。是的,连我们自己也是多年遗传进化的结果。在这一章中,你将进入迷人的进化算法世界,并更详细地了解一种特定类型的进化算法——遗传算法。在这一章中,你将学*到以下内容:

  • 什么是优化

  • 解决优化问题的不同方法

  • 理解遗传算法背后的直觉

  • 遗传算法的优点

  • 理解并实现交叉、变异和适应度函数选择的过程

  • 使用遗传算法找回丢失的密码

  • 遗传算法在优化模型中的各种应用

  • Python 遗传算法库中的分布式进化算法

优化

优化(Optimization)不是一个新词汇;我们之前已经在机器学*和深度学*(DL)算法中使用过它,当时我们使用了 TensorFlow 自动微分器,通过一种梯度下降算法找到最佳的模型权重和偏差。在本节中,我们将深入了解优化、优化问题以及执行优化的不同技术。

从最基本的角度来看,优化是使某物变得更好的过程。其核心思想是找到最佳的解决方案,显然,当我们谈论最佳解决方案时,意味着存在不止一个解决方案。在优化中,我们尝试调整我们的变量参数/过程/输入,以便找到最小或最大的输出。通常,这些变量构成输入,我们有一个称为目标函数损失函数适应度函数的函数,作为输出我们期望的是成本/损失或适应度。成本或损失应该最小化,如果我们定义适应度,那么它应该最大化。在这里,我们通过改变输入(变量)来实现所需的(优化的)输出。

我希望你能理解,称其为损失/成本或适应度只是一个选择问题,计算成本并需要最小化的函数,如果我们给它加上一个负号,那么我们期望修改后的函数能够最大化。例如,最小化2 - x²在区间-2 < x < 2上,和在相同区间内最大化x² - 2 是一样的。

我们的日常生活中充满了许多这样的优化任务。到办公室的最佳路线应该是怎样的?我应该先做哪个项目?为面试做准备,应该阅读哪些主题以最大化面试成功率?下图展示了输入变量需要优化的函数输出/成本之间的基本关系:

输入、需要优化的函数和输出之间的关系

目标是最小化成本,使得函数指定的约束条件通过输入变量得到满足。成本函数、约束条件和输入变量之间的数学关系决定了优化问题的复杂性。一个关键问题是成本函数和约束条件是凸的还是非凸的。如果成本函数和约束条件是凸的,我们可以确信确实存在可行解,并且如果我们在一个足够大的领域内进行搜索,我们一定能找到一个。下图展示了一个凸成本函数的示例:

一个凸成本函数。左边是表面图,右边显示的是同一成本函数的等高线图。图像中最深的红色点对应于最优解点。

另一方面,如果成本函数或约束条件是非凸的,优化问题会变得更加困难,我们无法确定是否确实存在解决方案,或者我们是否能够找到一个。

在数学和计算机编程中,有多种方法可以解决优化问题。接下来让我们了解一下它们每一种方法。

确定性和解析方法

当目标函数是平滑的并且具有连续的二阶导数时,根据微积分的知识,我们知道在局部最小值处,以下条件成立:

  • 在最小值处,目标函数的梯度是 0,即f'(x) = 0

  • 二阶导数(Hessian H(x) = ∇²f(x))是正定的

在这种情况下,对于某些问题,可以通过确定梯度的零点并验证 Hessian 矩阵在零点处的正定性来解析地找到解决方案。因此,在这些情况下,我们可以通过迭代地探索搜索空间来找到目标函数的最小值。有多种搜索方法;让我们来看一看。

梯度下降法

我们在前面的章节中学*了梯度下降及其工作原理,看到搜索方向是梯度下降的方向,-∇f(x)。这也叫做柯西方法,因为它是由柯西于 1847 年提出的,从那时起就非常流行。我们从目标函数表面上的一个任意点开始,沿着梯度方向改变变量(在前面的章节中,这些是权重和偏置)。数学上,它表示为:

这里,α[n]是迭代n时的步长(变化/学*率)。梯度下降算法在训练深度学*模型时效果良好,但也有一些严重的缺点:

  • 所使用的优化器性能在很大程度上取决于学*率和其他常数。如果稍微改变它们,网络很可能不会收敛。正因为如此,研究人员有时将训练模型称为一门艺术,或炼金术。

  • 由于这些方法基于导数,它们不适用于离散数据。

  • 当目标函数是非凸时,我们无法可靠地应用该方法,这在许多深度学*网络中是常见的(尤其是使用非线性激活函数的模型)。许多隐藏层的存在可能导致多个局部最小值,模型很有可能陷入局部最小值中。这里,你可以看到一个具有多个局部最小值的目标函数示例:

具有多个局部最小值的成本函数。左侧是表面图,右侧是相同成本函数的等高线图。图中的深红色点对应于最小值。

梯度下降法有许多变种,其中最流行的几种可以在 TensorFlow 优化器中找到,包括以下几种:

  • 随机梯度优化器

  • Adam 优化器

  • Adagrad 优化器

  • RMSProp 优化器

你可以通过 TensorFlow 文档中的www.tensorflow.org/api_guides/python/train了解更多有关 TensorFlow 中不同优化器的信息。

一个不错的来源是 Sebastian Ruder 的博客(ruder.io/optimizing-gradient-descent/index.html#gradientdescentoptimizationalgorithms),基于他在 arXiv 上的论文arxiv.org/abs/1609.04747

牛顿-拉夫森方法

该方法基于目标函数f(x)在点x^附*的二阶泰勒级数展开:

这里,x**是泰勒级数展开的点,x是靠*x**的点,超脚本T表示转置,H是 Hessian 矩阵,其元素如下所示:

通过对泰勒级数展开式求梯度并使其等于0,我们得到:

假设初始猜测为x[0],下一个点x[n+1]可以通过以下公式从前一个点x[n]计算得到:

该方法同时使用目标函数的一阶和二阶偏导数来寻找最小值。在第k次迭代时,它通过围绕x(k)的二次函数来逼*目标函数,并朝向最小值移动。

由于计算 Hessian 矩阵在计算上很昂贵且通常未知,因此有许多算法致力于逼* Hessian 矩阵;这些技术被称为拟牛顿方法。它们可以表示如下:

α[n]是第n次迭代中的步长(变化/学*率),A[n]是第n次迭代中的 Hessian 矩阵逼*值。我们构造一个 Hessian 的逼*序列,使得以下条件成立:

两种流行的拟牛顿方法如下:

  • Davidon-Fletcher-Powell 算法

  • Broyden-Fletcher-Goldfarb-Shanno 算法

当 Hessian 的逼*A[n]是单位矩阵时,牛顿法变成了梯度下降法。

牛顿法的主要缺点是它无法扩展到具有高维输入特征空间的问题。

自然优化方法

自然优化方法受到一些自然过程的启发,即在自然界中存在的一些在优化某些自然现象上非常成功的过程。这些算法不需要求取目标函数的导数,因此即使是离散变量和非连续目标函数也能使用。

模拟退火

模拟退火是一种随机方法。它受到物理退火过程的启发,在该过程中,一个固体首先被加热到足够高的温度使其融化,然后温度慢慢降低;这使得固体的粒子能够以最低的能量状态重新排列,从而产生一个高度结构化的晶格。

我们从为每个变量分配一些随机值开始,这表示初始状态。在每一步中,我们随机选择一个变量(或一组变量),然后选择一个随机值。如果将该值分配给变量后,目标函数有所改善,则算法接受该赋值,形成新的当前赋值,系统状态发生变化。否则,它以一定的概率P接受该赋值,P的值依赖于温度T以及当前状态和新状态下目标函数值的差异。如果不接受变化,则当前状态保持不变。从状态i到状态j的转变概率P如下所示:

这里,T表示一个与物理系统中温度类似的变量。当温度趋*于0时,模拟退火算法就变成了梯度下降算法。

粒子群优化

粒子群优化PSO)由爱德华和肯尼迪于 1995 年提出。它基于动物的社会行为,例如鸟群。你一定注意到,在天空中,鸟群飞行时呈 V 字形。研究鸟类行为的人告诉我们,当鸟群寻找食物或更好的栖息地时,它们会以这种方式飞行,队伍前面的鸟最接*目标源。

现在,当它们飞行时,领头的鸟并不固定不变;相反,随着它们的移动,领头鸟会发生变化。看到食物的鸟会发出声音信号,其他鸟会围绕着它以 V 字形聚集。这是一个连续重复的过程,已经让鸟类受益了数百万年。

PSO 从这种鸟类行为中汲取灵感,并利用它来解决优化问题。在 PSO 中,每个解决方案都是搜索空间中的一只鸟(称为粒子)。每个粒子都有一个适应度值,这个值通过待优化的适应度函数来评估;它们还有速度,决定了粒子的飞行方向。粒子通过跟随当前最优粒子飞行,通过问题的搜索空间。

粒子在搜索空间中根据两个最佳适应度值进行移动,一个是它们自己已知的最佳位置(pbest:粒子最佳),另一个是整个群体已知的最佳适应度值(gbest:全局最佳)。随着改进位置的发现,这些位置被用来指导粒子群体的运动。这个过程不断重复,期望最终能够找到一个最优解。

遗传算法

当我们环顾四周,看到不同的物种时,一个问题自然会浮现:为什么这些特征组合能稳定存在,而其他特征不能?为什么大多数动物有两条腿或四条腿,而不是三条腿?我们今天看到的这个世界,是否是经过多次迭代优化算法的结果?

设想有一个成本函数来衡量生存能力,并且该生存能力应该是最大化的。自然界生物的特性适应于一个拓扑景观。生存能力的水平(通过适应性来衡量)代表景观的高度。最高的点对应于最适合的条件,而限制条件则由环境以及不同物种之间的相互作用提供。

那么,进化过程可以看作是一个庞大的优化算法,它选择哪些特征能产生适应生存的物种。景观的顶峰由生物体占据。有些顶峰非常广阔,容纳了多种特征,涵盖了许多物种,而另一些顶峰则非常狭窄,只允许具有非常特定特征的物种存在。

我们可以将这一类比扩展到包括分隔不同物种的山谷。我们可以认为人类可能处于这个景观的全局最优峰值,因为我们拥有智慧和改变环境的能力,并能确保在极端环境下的更好生存能力。

因此,可以将这个拥有不同生命形式的世界视作一个巨大的搜索空间,而不同的物种则是许多次迭代优化算法的结果。这一思想构成了遗传算法的基础。

本章的主题是遗传算法,让我们深入探讨一下它们。

遗传算法简介

根据著名生物学家查尔斯·达尔文的研究,我们今天看到的动物和植物物种是经过数百万年的进化而形成的。进化过程遵循适者生存的原则,选择那些拥有更好生存机会的生物。我们今天看到的植物和动物是数百万年适应环境约束的结果。在任何时候,许多不同的生物可能会共存并争夺相同的环境资源。

那些最具资源获取能力和繁殖能力的生物,它们的后代将拥有更多的生存机会。另一方面,能力较弱的生物往往会有很少或没有后代。随着时间的推移,整个种群会发生演化,平均而言,新一代的生物会比前几代更具适应性。

是什么使得这一切成为可能?是什么决定了一个人会很高,而一棵植物会有特定形状的叶子?这一切都被像一套规则一样编码在生命蓝图中的程序里——基因。地球上的每一个生物都拥有这套规则,它们描述了该生物是如何被设计(创造)的。基因存在于染色体中。每个生物都有不同数量的染色体,这些染色体包含数千个基因。例如,我们人类有 46 条染色体,这些染色体包含约 20,000 个基因。每个基因代表一个特定的规则:比如一个人会有蓝色的眼睛,棕色的头发,是女性,等等。这些基因通过繁殖过程从父母传递给后代。

基因从父母传递给后代的方式有两种:

  • 无性繁殖:在这种情况下,子代是父代的完全复制。它发生在一个叫做有丝分裂的生物过程里;如细菌和真菌等低等生物通过有丝分裂繁殖。此过程中只需要一个父母:

有丝分裂过程:父母的染色体首先翻倍,然后细胞分裂成两个子细胞

  • 有性生殖:这一过程通过一种叫做减数分裂的生物学过程完成。在这一过程中,最初涉及两个父母;每个父母的细胞经历交叉过程,其中一条染色体的一部分与另一条染色体的一部分交换位置。这样改变了遗传序列,细胞随后分裂为两部分,但每部分只包含一半的染色体数。来自两个父母的单倍体细胞最终结合,形成受精卵,后通过有丝分裂和细胞分化,最终产生一个与父母相似但又有所不同的后代

细胞分裂过程:父母的细胞染色体发生交叉,一部分染色体与另一部分染色体交换位置。然后,细胞分裂为两部分,每个分裂的细胞只包含一条染色体(单倍体)。来自两个父母的两个单倍体细胞随后结合,完成染色体的总数。

自然选择和进化过程中另一个有趣的现象是突变现象。在这里,基因发生突然变化,产生一个完全新的基因,这个基因在父母双方中都没有出现过。这个现象进一步增加了多样性。

通过世代间的有性生殖,应该带来进化,并确保具有最适应特征的生物拥有更多的后代。

遗传算法

现在让我们来了解如何实现遗传算法。这一方法是由约翰·霍兰德于 1975 年提出的。他的学生戈德堡展示了这一方法可以用来解决优化问题,并且使用遗传算法来控制天然气管道的传输。此后,遗传算法一直广受欢迎,并启发了其他各种进化程序的研究。

为了将遗传算法应用于计算机优化问题的求解,第一步我们需要将问题变量编码为基因。这些基因可以是实数的字符串或二进制位串(0 和 1 的序列)。这代表了一个潜在的解决方案(个体),而多个这样的解决方案一起构成了时间点t时的人口。例如,考虑一个需要找到两个变量 a 和 b 的问题,其中这两个变量的范围是(0, 255)。对于二进制基因表示,这两个变量可以通过一个 16 位的染色体表示,其中高 8 位代表基因 a,低 8 位代表基因 b。编码之后需要解码才能获得变量 a 和 b 的实际值。

遗传算法的第二个重要要求是定义一个合适的适应度函数,该函数计算任何潜在解的适应度分数(在前面的示例中,它应该计算编码染色体的适应度值)。这是我们希望通过寻找系统或问题的最优参数集来优化的函数。适应度函数是与问题相关的。例如,在自然进化过程中,适应度函数代表生物体在其环境中生存和运作的能力。

一旦我们决定了问题解决方案在基因中的编码方式,并确定了适应度函数,遗传算法将遵循以下步骤:

  1. 种群初始化:我们需要创建一个初始种群,其中所有染色体(通常)是随机生成的,以产生整个可能解的范围(搜索空间)。偶尔,解决方案可能会在最有可能找到最佳解的区域中进行初始化。种群的大小取决于问题的性质,但通常包含数百个潜在的解决方案,这些解决方案被编码成染色体。

  2. 父代选择:对于每一代,根据适应度函数(或随机选择),我们接下来选择现有种群中的一定比例。这些被选中的种群将通过繁殖形成新一代。这个过程是通过竞赛选择法来完成的:随机选择固定数量的个体(竞赛大小),然后选择适应度分数最好的个体作为父母之一。

  3. 繁殖:接下来,我们通过在步骤 2 中选择的个体,通过遗传算子如交叉和变异来生成后代。最终,这些遗传算子会产生一个与初代不同但又在许多方面继承了父母特征的后代染色体种群。

  4. 评估:生成的后代将通过适应度函数进行评估,并且它们将替换种群中最不适应的个体,以保持种群大小不变。

  5. 终止:在评估步骤中,如果任何后代达到了目标适应度分数或达到最大代数,遗传算法过程将终止。否则,步骤24将重复进行,以产生下一代。

两个对遗传算法成功至关重要的算子是交叉和变异。

交叉

为了执行交叉操作,我们在两个父母的染色体上选择一个随机位置,然后基于这个点交换它们的遗传信息,交叉概率为P[x]。这将产生两个新的后代。当交叉发生在一个随机点时,称为单点交叉(或Single Point Crossover):

单点交叉:随机选择父代染色体中的一个点,并交换相应的基因位。

我们也可以在多个位置交换父代的基因;这称为多点交叉

多点交叉:在多个位置交换父代的基因。这是双点交叉的一个例子。

存在许多不同的交叉方式,例如,均匀交叉、基于顺序的交叉和循环交叉。

变异

虽然交叉操作确保了多样性并且有助于加速搜索,但它并不产生新的解。这是变异操作符的工作,变异操作符帮助保持和引入种群中的多样性。变异操作符以概率P[m]应用于子代染色体的某些基因(位)。

我们可以进行位翻转变异;如果我们考虑之前的例子,那么在 16 位染色体中,位翻转变异将导致一个位的状态发生变化(从0变为1,或者从1变为0)。

我们有可能将基因设置为所有可能值中的一个随机值,这称为随机重置

概率P[m]起着重要作用;如果我们给P[m]分配一个非常低的值,它可能导致遗传漂移,但另一方面,过高的P[m]可能会导致丧失好的解。我们选择一个变异概率,使得算法学会牺牲短期适应度来获得长期适应度。

优缺点

遗传算法听起来很酷,对吧!现在,在我们尝试围绕它们构建代码之前,让我们先指出遗传算法的一些优缺点。

优势

遗传算法提供了一些令人着迷的优势,并且在传统的基于梯度的方法失败时也能产生结果。

  • 它们可以用于优化连续变量或离散变量。

  • 与梯度下降不同,我们不需要导数信息,这也意味着适应度函数不必是连续可微的。

  • 它可以从广泛的成本表面进行同时搜索。

  • 我们可以处理大量变量,而不会显著增加计算时间。

  • 种群的生成及其适应度值的计算可以并行进行,因此遗传算法非常适合并行计算机。

  • 它们即使在拓扑表面极其复杂时也能正常工作,因为交叉和变异操作符帮助它们跳出局部最小值。

  • 它们可以提供多个最优解。

  • 我们可以将它们应用于数值生成的数据、实验数据,甚至是分析函数。它们特别适用于大规模优化问题。

劣势

尽管之前提到的优势存在,我们仍然不认为遗传算法是所有优化问题的普适解决方案。原因如下:

  • 如果优化函数是一个良性凸函数,那么基于梯度的方法将实现更快的收敛速度

  • 大量的解集帮助遗传算法更广泛地覆盖搜索空间,但也导致了收敛速度变慢

  • 设计一个适应度函数可能是一项艰巨的任务

使用 Python 中的分布式进化算法编码遗传算法

现在我们理解了遗传算法的工作原理,接下来我们可以尝试用它们解决一些问题。它们已经被用来解决 NP 难题,例如旅行推销员问题。为了简化生成种群、执行交叉操作和进行变异操作的任务,我们将使用分布式进化算法在 Python 中DEAP)。它支持多进程,我们也可以用它来进行其他进化算法的应用。你可以通过 PyPi 直接下载 DEAP,方法如下:

pip install deap

它与 Python 3 兼容。

要了解更多关于 DEAP 的信息,可以参考其 GitHub 仓库(github.com/DEAP/deap)和用户指南(deap.readthedocs.io/en/master/)。

猜测单词

在这个程序中,我们使用遗传算法猜测一个单词。遗传算法知道单词中有多少个字母,并会一直猜这些字母,直到找到正确的答案。我们决定将基因表示为单个字母数字字符;这些字符的字符串构成了染色体。我们的适应度函数是个体与正确单词中匹配的字符数之和:

  1. 作为第一步,我们导入所需的模块。我们使用string模块和random模块来生成随机字符(a—z、A—Z 以及 0—9)。从 DEAP 模块中,我们使用creatorbasetools
import string
import random

from deap import base, creator, tools
  1. 在 DEAP 中,我们首先创建一个继承自deep.base模块的类。我们需要告诉它我们是进行函数的最小化还是最大化;这通过权重参数来实现。+1表示我们在进行最大化(如果是最小化,则值为-1.0)。以下代码行将创建一个类FitnessMax,它将最大化该函数:
creator.create("FitnessMax", base.Fitness, weights=(1.0,))  
  1. 我们还定义了一个Individual类,该类将继承列表类,并告诉 DEAP 创作者模块将FitnessMax分配为其fitness属性:
creator.create("Individual", list, fitness=creator.FitnessMax)
  1. 现在,定义了Individual类后,我们使用 DEAP 在基础模块中定义的toolbox。我们将用它来创建种群并定义基因池。从现在开始,我们所需的所有对象——个体、种群、函数、算子和参数——都存储在一个叫做toolbox的容器中。我们可以使用register()unregister()方法向toolbox容器中添加或删除内容:
toolbox = base.Toolbox()
# Gene Pool
toolbox.register("attr_string", random.choice, \
               string.ascii_letters + string.digits )
  1. 现在我们已经定义了如何创建基因池,我们通过反复使用Individual类来创建个体和种群。我们将类传递给负责创建N参数的工具箱,告诉它需要生成多少个基因:
#Number of characters in word
# The word to be guessed
word = list('hello')
N = len(word)
# Initialize population
toolbox.register("individual", tools.initRepeat, \
         creator.Individual, toolbox.attr_string, N )
toolbox.register("population",tools.initRepeat, list,\
         toolbox.individual)
  1. 我们定义了fitness函数。注意返回语句中的逗号。这是因为 DEAP 中的适应度函数以元组的形式返回,以支持多目标的fitness函数:
def evalWord(individual, word):
    return sum(individual[i] == word[i] for i in\
            range(len(individual))),    
  1. 将适应度函数添加到容器中。同时,添加交叉算子、变异算子和父代选择算子。可以看到,为此我们使用了register函数。在第一行中,我们注册了已定义的适应度函数,并传入它将接受的额外参数。下一行注册了交叉操作;它指定我们这里使用的是双点交叉(cxTwoPoint)。接下来,我们注册了变异算子;我们选择了mutShuffleIndexes选项,它会以indpb=0.05的概率打乱输入个体的属性。最后,我们定义了父代选择的方式;在这里,我们定义了采用比赛选择的方法,比赛大小为3
toolbox.register("evaluate", evalWord, word)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutShuffleIndexes, indpb=0.05)
toolbox.register("select", tools.selTournament, tournsize=3)
  1. 现在我们已经有了所有的组成部分,接下来我们将编写遗传算法的代码,它将按我们之前提到的步骤反复执行:
def main():
    random.seed(64)
    # create an initial population of 300 individuals 
    pop = toolbox.population(n=300)
    # CXPB is the crossover probability 
    # MUTPB is the probability for mutating an individual
    CXPB, MUTPB = 0.5, 0.2

    print("Start of evolution")

    # Evaluate the entire population
    fitnesses = list(map(toolbox.evaluate, pop))
    for ind, fit in zip(pop, fitnesses):
        ind.fitness.values = fit

    print(" Evaluated %i individuals" % len(pop))

    # Extracting all the fitnesses of individuals in a list
    fits = [ind.fitness.values[0] for ind in pop]
    # Variable keeping track of the number of generations
    g = 0

    # Begin the evolution
    while max(fits) < 5 and g < 1000:
        # A new generation
        g += 1
        print("-- Generation %i --" % g)

        # Select the next generation individuals
        offspring = toolbox.select(pop, len(pop))
        # Clone the selected individuals
        offspring = list(map(toolbox.clone, offspring))

        # Apply crossover and mutation on the offspring
        for child1, child2 in zip(offspring[::2], offspring[1::2]):
            # cross two individuals with probability CXPB
            if random.random() < CXPB:    
            toolbox.mate(child1, child2)
            # fitness values of the children
            # must be recalculated later
            del child1.fitness.values
            del child2.fitness.values
        for mutant in offspring:
            # mutate an individual with probability MUTPB
            if random.random() < MUTPB:
                toolbox.mutate(mutant)
                del mutant.fitness.values

        # Evaluate the individuals with an invalid fitness
        invalid_ind = [ind for ind in offspring if not ind.fitness.valid]
        fitnesses = map(toolbox.evaluate, invalid_ind)
        for ind, fit in zip(invalid_ind, fitnesses):
        ind.fitness.values = fit

        print(" Evaluated %i individuals" % len(invalid_ind))

        # The population is entirely replaced by the offspring
        pop[:] = offspring

        # Gather all the fitnesses in one list and print the stats
        fits = [ind.fitness.values[0] for ind in pop]

        length = len(pop)
        mean = sum(fits) / length
        sum2 = sum(x*x for x in fits)
        std = abs(sum2 / length - mean**2)**0.5

        print(" Min %s" % min(fits))
        print(" Max %s" % max(fits))
        print(" Avg %s" % mean)
        print(" Std %s" % std)

    print("-- End of (successful) evolution --")

    best_ind = tools.selBest(pop, 1)[0]
    print("Best individual is %s, %s" % (''.join(best_ind),\
             best_ind.fitness.values))
  1. 在这里,你可以看到这个遗传算法的结果。在七代内,我们找到了正确的词:

DEAP 提供了选择各种交叉工具、不同变异算子,甚至如何进行比赛选择的选项。DEAP 提供的所有进化工具及其说明的完整列表可在deap.readthedocs.io/en/master/api/tools.html.查看。

CNN 架构的遗传算法

在第四章《物联网中的深度学*》中,我们了解了不同的深度学*模型,如 MLP、CNN、RNN 等。现在,我们将看到如何将遗传算法应用于这些深度学*模型。遗传算法可以用来找到优化的权重和偏差,已经有人尝试过了。但在深度学*模型中,遗传算法最常见的应用是寻找最优超参数。

在这里,我们使用遗传算法来寻找最优的 CNN 架构。该方案基于 Lingxi Xie 和 Alan Yuille 发表的论文 Genetic CNNarxiv.org/abs/1703.01513)。第一步是找到问题的正确表示方法。作者提出了网络架构的二进制串表示。网络的家族被编码成固定长度的二进制串。网络由 S 个阶段组成,其中第 s 阶段 s = 1, 2,....S,包含 K[s] 个节点,表示为 ,这里 k[s] = 1, 2,..., K[s][. 每个阶段的节点是有序的,并且为了正确表示,只允许从较低编号的节点连接到较高编号的节点。每个节点表示一个卷积层操作,随后是批量归一化和 ReLU 激活。位串的每一位表示一个卷积层(节点)与另一个卷积层之间连接的存在或不存在,位的顺序如下:第一位表示 (v[s,1],v[s,2]) 之间的连接,接下来的两位表示 (v[s,1],v[s,3]) 和 (v[s,2],v[s,3]) 之间的连接,接下来的三位表示 (v[s,1],v[s,3]),(v[s,1],v[s,4]) 和 (v[s,2],v[s,4]) 之间的连接,依此类推。

为了更好地理解它,我们考虑一个两阶段的网络(每个阶段将具有相同数量的滤波器和滤波器大小)。假设阶段 S[1] 包含四个节点(即 K[s] = 4),因此需要编码的位数总共为 (4×3×½ =) 6。阶段 1 中的卷积滤波器数量是 32;同时我们确保卷积操作不会改变图像的空间维度(例如,填充保持一致)。下图显示了相应的位串编码及对应的卷积层连接。红色连接是默认连接,不在位串中编码。第一位编码了 (a1a2) 之间的连接,接下来的两位编码了 (a1a3) 和 (a2a3) 之间的连接,最后三位编码了 (a1a4),(a2a4) 和 (a3a4*) 之间的连接:

位串编码及对应的卷积层连接

阶段 1 接受一个 32 × 32 × 3 的输入;该阶段的所有卷积节点都有 32 个滤波器。红色连接表示默认连接,不在位串中编码。绿色连接表示根据编码的位串 1-00-111 所表示的连接。阶段 1 的输出将传递到池化层,并在空间维度上减半。

第二阶段有五个节点,因此需要(5×4×½ =) 10 位。它将从第一阶段1接收输入,维度为16 × 16 × 32。现在,如果我们将第二阶段2中的卷积滤波器数量设为64,那么池化后的输出将是 8 × 8 × 64。

这里呈现的代码来自github.com/aqibsaeed/Genetic-CNN。由于我们需要表示图结构,因此网络是使用有向无环图DAG)构建的。为了表示 DAG,我们定义了一个类 DAG,其中定义了添加新节点、删除现有节点、在两个节点之间添加边(连接)和删除两个节点之间的边的方法。除此之外,还定义了查找节点前驱节点、连接到该节点的节点以及图的叶子节点列表的方法。完整代码位于dag.py中,可以通过 GitHub 链接访问。

主要代码在Genetic_CNN.ipynb Jupyter 笔记本中给出。我们使用 DEAP 库来运行遗传算法,并使用 TensorFlow 根据遗传算法构建的图来构建 CNN。适应度函数是准确度。代码旨在找到在 MNIST 数据集上能够给出最高准确度的 CNN(我们在第四章《物联网深度学*》中使用了手写数字,这里我们直接从 TensorFlow 库中获取它们):

  1. 第一步是导入模块。这里,我们将需要 DEAP 和 TensorFlow,还将导入我们在dag.py中创建的 DAG 类,以及标准的 Numpy 和 Random 模块:
import random
import numpy as np

from deap import base, creator, tools, algorithms
from scipy.stats import bernoulli
from dag import DAG, DAGValidationError

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
  1. 我们直接从 TensorFlow 示例库中读取数据:
mnist = input_data.read_data_sets("mnist_data/", one_hot=True)
train_imgs   = mnist.train.images
train_labels = mnist.train.labels
test_imgs    = mnist.test.images
test_labels  = mnist.test.labels

train_imgs = np.reshape(train_imgs,[-1,28,28,1])
test_imgs = np.reshape(test_imgs,[-1,28,28,1])
  1. 现在,我们构建将保存网络信息的比特数据结构。我们设计的网络是一个三阶段网络,第一个阶段有三个节点(3 位),第二个阶段有四个节点(6 位),第三个阶段有五个节点(10 位)。因此,一个个体将由一个二进制字符串表示,长度为3 + 6 + 10 = 19 位:
STAGES = np.array(["s1","s2","s3"]) # S
NUM_NODES = np.array([3,4,5]) # K

L =  0 # genome length
BITS_INDICES, l_bpi = np.empty((0,2),dtype = np.int32), 0 # to keep track of bits for each stage S
for nn in NUM_NODES:
    t = nn * (nn - 1)
    BITS_INDICES = np.vstack([BITS_INDICES,[l_bpi, l_bpi + int(0.5 * t)]])
    l_bpi = int(0.5 * t)
    L += t
L = int(0.5 * L)

TRAINING_EPOCHS = 20
BATCH_SIZE = 20
TOTAL_BATCHES = train_imgs.shape[0] // BATCH_SIZE
  1. 现在是根据编码后的比特串构建图的部分。这将有助于为遗传算法构建种群。首先,我们定义构建 CNN 所需的函数(weight_variable:为卷积节点创建权重变量;bias_variable:为卷积节点创建偏置变量;apply_convolution:执行卷积操作的函数;apply_pool:在每个阶段之后执行池化操作的函数;最后,使用linear_layer函数构建最后的全连接层):
def weight_variable(weight_name, weight_shape):
    return tf.Variable(tf.truncated_normal(weight_shape, stddev = 0.1),name = ''.join(["weight_", weight_name]))

def bias_variable(bias_name,bias_shape):
    return tf.Variable(tf.constant(0.01, shape = bias_shape),name = ''.join(["bias_", bias_name]))

def linear_layer(x,n_hidden_units,layer_name):
    n_input = int(x.get_shape()[1])
    weights = weight_variable(layer_name,[n_input, n_hidden_units])
    biases = bias_variable(layer_name,[n_hidden_units])
    return tf.add(tf.matmul(x,weights),biases)

def apply_convolution(x,kernel_height,kernel_width,num_channels,depth,layer_name):
    weights = weight_variable(layer_name,[kernel_height, kernel_width, num_channels, depth])
    biases = bias_variable(layer_name,[depth])
    return tf.nn.relu(tf.add(tf.nn.conv2d(x, weights,[1,2,2,1],padding = "SAME"),biases)) 

def apply_pool(x,kernel_height,kernel_width,stride_size):
    return tf.nn.max_pool(x, ksize=[1, kernel_height, kernel_width, 1], 
            strides=[1, 1, stride_size, 1], padding = "SAME")
  1. 现在,我们可以基于编码后的比特串构建网络。所以,我们使用generate_dag函数生成 DAG:
def generate_dag(optimal_indvidual,stage_name,num_nodes):
    # create nodes for the graph
    nodes = np.empty((0), dtype = np.str)
    for n in range(1,(num_nodes + 1)):
        nodes = np.append(nodes,''.join([stage_name,"_",str(n)]))

    # initialize directed asyclic graph (DAG) and add nodes to it
    dag = DAG()
    for n in nodes:
        dag.add_node(n)

    # split best indvidual found via genetic algorithm to identify vertices connections and connect them in DAG 
    edges = np.split(optimal_indvidual,np.cumsum(range(num_nodes - 1)))[1:]
    v2 = 2
    for e in edges:
        v1 = 1
        for i in e:
            if i:
                dag.add_edge(''.join([stage_name,"_",str(v1)]),''.join([stage_name,"_",str(v2)])) 
            v1 += 1
        v2 += 1

    # delete nodes not connected to anyother node from DAG
    for n in nodes:
        if len(dag.predecessors(n)) == 0 and len(dag.downstream(n)) == 0:
            dag.delete_node(n)
            nodes = np.delete(nodes, np.where(nodes == n)[0][0])

    return dag, nodes
  1. 生成的图用于使用generate_tensorflow_graph函数构建 TensorFlow 图。该函数利用add_node函数添加卷积层,使用sum_tensors函数将多个卷积层的输入合并:
def generate_tensorflow_graph(individual,stages,num_nodes,bits_indices):
    activation_function_pattern = "/Relu:0"

    tf.reset_default_graph()
    X = tf.placeholder(tf.float32, shape = [None,28,28,1], name = "X")
    Y = tf.placeholder(tf.float32,[None,10],name = "Y")

    d_node = X
    for stage_name,num_node,bpi in zip(stages,num_nodes,bits_indices):
        indv = individual[bpi[0]:bpi[1]]

        add_node(''.join([stage_name,"_input"]),d_node.name)
        pooling_layer_name = ''.join([stage_name,"_input",activation_function_pattern])

        if not has_same_elements(indv):
            # ------------------- Temporary DAG to hold all connections implied by genetic algorithm solution ------------- #  

            # get DAG and nodes in the graph
            dag, nodes = generate_dag(indv,stage_name,num_node) 
            # get nodes without any predecessor, these will be connected to input node
            without_predecessors = dag.ind_nodes() 
            # get nodes without any successor, these will be connected to output node
            without_successors = dag.all_leaves()

            # ----------------------------------------------------------------------------------------------- #

            # --------------------------- Initialize tensforflow graph based on DAG ------------------------- #

            for wop in without_predecessors:
                add_node(wop,''.join([stage_name,"_input",activation_function_pattern]))

            for n in nodes:
                predecessors = dag.predecessors(n)
                if len(predecessors) == 0:
                    continue
                elif len(predecessors) > 1:
                    first_predecessor = predecessors[0]
                    for prd in range(1,len(predecessors)):
                        t = sum_tensors(first_predecessor,predecessors[prd],activation_function_pattern)
                        first_predecessor = t.name
                    add_node(n,first_predecessor)
                elif predecessors:
                    add_node(n,''.join([predecessors[0],activation_function_pattern]))

            if len(without_successors) > 1:
                first_successor = without_successors[0]
                for suc in range(1,len(without_successors)):
                    t = sum_tensors(first_successor,without_successors[suc],activation_function_pattern)
                    first_successor = t.name
                add_node(''.join([stage_name,"_output"]),first_successor) 
            else:
                add_node(''.join([stage_name,"_output"]),''.join([without_successors[0],activation_function_pattern])) 

            pooling_layer_name = ''.join([stage_name,"_output",activation_function_pattern])
            # ------------------------------------------------------------------------------------------ #

        d_node =  apply_pool(tf.get_default_graph().get_tensor_by_name(pooling_layer_name), 
                                 kernel_height = 16, kernel_width = 16,stride_size = 2)

    shape = d_node.get_shape().as_list()
    flat = tf.reshape(d_node, [-1, shape[1] * shape[2] * shape[3]])
    logits = linear_layer(flat,10,"logits")

    xentropy =  tf.nn.softmax_cross_entropy_with_logits(logits = logits, labels = Y)
    loss_function = tf.reduce_mean(xentropy)
    optimizer = tf.train.AdamOptimizer().minimize(loss_function) 
    accuracy = tf.reduce_mean(tf.cast( tf.equal(tf.argmax(tf.nn.softmax(logits),1), tf.argmax(Y,1)), tf.float32))

    return  X, Y, optimizer, loss_function, accuracy

# Function to add nodes
def add_node(node_name, connector_node_name, h = 5, w = 5, nc = 1, d = 1):
    with tf.name_scope(node_name) as scope:
        conv = apply_convolution(tf.get_default_graph().get_tensor_by_name(connector_node_name), 
                   kernel_height = h, kernel_width = w, num_channels = nc , depth = d, 
                   layer_name = ''.join(["conv_",node_name]))

def sum_tensors(tensor_a,tensor_b,activation_function_pattern):
    if not tensor_a.startswith("Add"):
        tensor_a = ''.join([tensor_a,activation_function_pattern])

    return tf.add(tf.get_default_graph().get_tensor_by_name(tensor_a),
                 tf.get_default_graph().get_tensor_by_name(''.join([tensor_b,activation_function_pattern])))

def has_same_elements(x):
    return len(set(x)) <= 1
  1. 适应度函数评估生成的 CNN 架构的准确性:
def evaluateModel(individual):
    score = 0.0
    X, Y, optimizer, loss_function, accuracy = generate_tensorflow_graph(individual,STAGES,NUM_NODES,BITS_INDICES)
    with tf.Session() as session:
        tf.global_variables_initializer().run()
        for epoch in range(TRAINING_EPOCHS):
            for b in range(TOTAL_BATCHES):
                offset = (epoch * BATCH_SIZE) % (train_labels.shape[0] - BATCH_SIZE)
                batch_x = train_imgs[offset:(offset + BATCH_SIZE), :, :, :]
                batch_y = train_labels[offset:(offset + BATCH_SIZE), :]
                _, c = session.run([optimizer, loss_function],feed_dict={X: batch_x, Y : batch_y})

        score = session.run(accuracy, feed_dict={X: test_imgs, Y: test_labels})
        #print('Accuracy: ',score)
    return score,
  1. 所以,现在我们准备实现遗传算法:我们的适应度函数将是一个最大值函数(weights=(1.0,)),我们使用伯努利分布(bernoulli.rvs)初始化二进制字符串,个体的长度为 L= 19,种群由 20 个个体组成。这一次,我们选择了有序交叉,其中从第一个父代选择一个子串并将其复制到子代的相同位置;剩余位置由第二个父代填充,确保子串中的节点不重复。我们保留了之前的变异操作符 mutShuffleIndexes;锦标赛选择方法为 selRoulette,它通过轮盘选择方法进行选择(我们选择 k 个个体,并从中选择适应度最高的个体)。这一次,我们没有自己编码遗传算法,而是使用了 DEAP 的 eaSimple 算法,这是基本的遗传算法:
population_size = 20
num_generations = 3
creator.create("FitnessMax", base.Fitness, weights = (1.0,))
creator.create("Individual", list , fitness = creator.FitnessMax)
toolbox = base.Toolbox()
toolbox.register("binary", bernoulli.rvs, 0.5)
toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.binary, n = L)
toolbox.register("population", tools.initRepeat, list , toolbox.individual)
toolbox.register("mate", tools.cxOrdered)
toolbox.register("mutate", tools.mutShuffleIndexes, indpb = 0.8)
toolbox.register("select", tools.selRoulette)
toolbox.register("evaluate", evaluateModel)
popl = toolbox.population(n = population_size)

import time
t = time.time()
result = algorithms.eaSimple(popl, toolbox, cxpb = 0.4, mutpb = 0.05, ngen = num_generations, verbose = True)
t1 = time.time() - t
print(t1)
  1. 算法将需要一些时间;在 i7 配备 NVIDIA 1070 GTX GPU 的机器上,大约需要 1.5 小时。最好的三个解决方案如下:
best_individuals = tools.selBest(popl, k = 3)
for bi in best_individuals:
    print(bi)

LSTM 优化的遗传算法

在遗传 CNN 中,我们使用遗传算法来估计最佳的 CNN 架构;在遗传 RNN 中,我们将使用遗传算法来寻找 RNN 的最佳超参数、窗口大小和隐藏单元数。我们将找到能够减少 均方根误差 (RMSE) 的参数。

超参数窗口大小和单元数再次被编码为二进制字符串,窗口大小使用 6 位,单元数使用 4 位。因此,完整编码的染色体将是 10 位。LSTM 使用 Keras 实现。

我们实现的代码来自 github.com/aqibsaeed/Genetic-Algorithm-RNN

  1. 必要的模块已导入。这一次,我们使用 Keras 来实现 LSTM 模型:
import numpy as np
import pandas as pd
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split as split

from keras.layers import LSTM, Input, Dense
from keras.models import Model

from deap import base, creator, tools, algorithms
from scipy.stats import bernoulli
from bitstring import BitArray

np.random.seed(1120)
  1. 我们需要的 LSTM 数据集必须是时间序列数据;我们使用来自 Kaggle 的风力发电预测数据 (www.kaggle.com/c/GEF2012-wind-forecasting/data):
data = pd.read_csv('train.csv')
data = np.reshape(np.array(data['wp1']),(len(data['wp1']),1))

train_data = data[0:17257]
test_data = data[17257:]
  1. 定义一个函数,根据选定的 window_size 准备数据集:
def prepare_dataset(data, window_size):
    X, Y = np.empty((0,window_size)), np.empty((0))
    for i in range(len(data)-window_size-1):
        X = np.vstack([X,data[i:(i + window_size),0]])
        Y = np.append(Y,data[i + window_size,0])   
    X = np.reshape(X,(len(X),window_size,1))
    Y = np.reshape(Y,(len(Y),1))
    return X, Y
  1. train_evaluate 函数为给定个体创建 LSTM 网络并返回其 RMSE 值(适应度函数):
def train_evaluate(ga_individual_solution):   
    # Decode genetic algorithm solution to integer for window_size and num_units
    window_size_bits = BitArray(ga_individual_solution[0:6])
    num_units_bits = BitArray(ga_individual_solution[6:]) 
    window_size = window_size_bits.uint
    num_units = num_units_bits.uint
    print('\nWindow Size: ', window_size, ', Num of Units: ', num_units)

    # Return fitness score of 100 if window_size or num_unit is zero
    if window_size == 0 or num_units == 0:
        return 100, 

    # Segment the train_data based on new window_size; split into train and validation (80/20)
    X,Y = prepare_dataset(train_data,window_size)
    X_train, X_val, y_train, y_val = split(X, Y, test_size = 0.20, random_state = 1120)

    # Train LSTM model and predict on validation set
    inputs = Input(shape=(window_size,1))
    x = LSTM(num_units, input_shape=(window_size,1))(inputs)
    predictions = Dense(1, activation='linear')(x)
    model = Model(inputs=inputs, outputs=predictions)
    model.compile(optimizer='adam',loss='mean_squared_error')
    model.fit(X_train, y_train, epochs=5, batch_size=10,shuffle=True)
    y_pred = model.predict(X_val)

    # Calculate the RMSE score as fitness score for GA
    rmse = np.sqrt(mean_squared_error(y_val, y_pred))
    print('Validation RMSE: ', rmse,'\n')

    return rmse,
  1. 接下来,我们使用 DEAP 工具定义个体(由于染色体是通过二进制编码的字符串(10 位)表示的,所以我们使用伯努利分布),创建种群,使用有序交叉,使用 mutShuffleIndexes 变异,并使用轮盘选择法来选择父代:
population_size = 4
num_generations = 4
gene_length = 10

# As we are trying to minimize the RMSE score, that's why using -1.0\. 
# In case, when you want to maximize accuracy for instance, use 1.0
creator.create('FitnessMax', base.Fitness, weights = (-1.0,))
creator.create('Individual', list , fitness = creator.FitnessMax)

toolbox = base.Toolbox()
toolbox.register('binary', bernoulli.rvs, 0.5)
toolbox.register('individual', tools.initRepeat, creator.Individual, toolbox.binary, n = gene_length)
toolbox.register('population', tools.initRepeat, list , toolbox.individual)

toolbox.register('mate', tools.cxOrdered)
toolbox.register('mutate', tools.mutShuffleIndexes, indpb = 0.6)
toolbox.register('select', tools.selRoulette)
toolbox.register('evaluate', train_evaluate)

population = toolbox.population(n = population_size)
r = algorithms.eaSimple(population, toolbox, cxpb = 0.4, mutpb = 0.1, ngen = num_generations, verbose = False)
  1. 我们得到最佳解决方案,如下所示:
best_individuals = tools.selBest(population,k = 1)
best_window_size = None
best_num_units = None

for bi in best_individuals:
    window_size_bits = BitArray(bi[0:6])
    num_units_bits = BitArray(bi[6:]) 
    best_window_size = window_size_bits.uint
    best_num_units = num_units_bits.uint
    print('\nWindow Size: ', best_window_size, ', Num of Units: ', best_num_units)
  1. 最后,我们实现了最佳 LSTM 解决方案:
X_train,y_train = prepare_dataset(train_data,best_window_size)
X_test, y_test = prepare_dataset(test_data,best_window_size)

inputs = Input(shape=(best_window_size,1))
x = LSTM(best_num_units, input_shape=(best_window_size,1))(inputs)
predictions = Dense(1, activation='linear')(x)
model = Model(inputs = inputs, outputs = predictions)
model.compile(optimizer='adam',loss='mean_squared_error')
model.fit(X_train, y_train, epochs=5, batch_size=10,shuffle=True)
y_pred = model.predict(X_test)

rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print('Test RMSE: ', rmse)

耶!现在,你已经拥有了最好的 LSTM 网络来预测风能。

总结

本章介绍了一种有趣的自然启发算法家族:遗传算法。我们涵盖了各种标准优化算法,涵盖了从确定性模型到基于梯度的算法,再到进化算法。我们讨论了自然选择的生物学过程。接着,我们学*了如何将优化问题转换为适合遗传算法的形式。遗传算法中的两个关键操作——交叉和变异也得到了阐述。虽然无法广泛涵盖所有交叉和变异方法,但我们学*了其中一些流行的方式。

我们将所学应用于三种非常不同的优化问题。我们用它来猜一个单词。例子是一个五个字母的单词;如果我们使用简单的暴力搜索,搜索空间会是61⁵。我们使用遗传算法优化了 CNN 架构;同样注意,假设有19个可能的位,搜索空间是2¹⁹。然后,我们用它来寻找 LSTM 网络的最优超参数。

在下一章中,我们将讨论另一个引人入胜的学*范式:强化学*。这是另一种自然的学*范式,因为在自然界中,我们通常没有监督学*;相反,我们通过与环境的互动来学*。同样,在这里,智能体除了从环境中获得的奖励和惩罚外,什么也不被告知。

第六章:物联网的强化学*

强化学*RL)与监督学*和无监督学*有很大不同。它是大多数生物学*的方式——与环境互动。在本章中,我们将研究强化学*中使用的不同算法。随着章节的进展,你将完成以下内容:

  • 了解什么是强化学*,以及它与监督学*和无监督学*的不同

  • 学*强化学*的不同元素

  • 了解强化学*在现实世界中的一些迷人应用

  • 了解用于训练强化学*(RL)智能体的 OpenAI 接口

  • 了解 Q 学*并用它来训练一个强化学*智能体

  • 了解深度 Q 网络并用它来训练一个智能体玩 Atari 游戏

  • 了解策略梯度算法并运用它

介绍

你是否曾观察过婴儿如何学会翻身、坐起、爬行,甚至站立?你有没有看过小鸟如何学会飞翔——父母把它们从巢里扔出去,它们拍打一段时间,慢慢学会飞行。所有这些学*都包含了以下的一个组成部分:

  • 试错法:婴儿尝试不同的方法,许多次未能成功,最终才能做到。

  • 目标导向:所有的努力都指向一个特定的目标。人类婴儿的目标可以是爬行,幼鸟的目标可以是飞翔。

  • 与环境的互动:它们得到的唯一反馈来自环境。

这个 YouTube 视频是一个美丽的视频,展示了一个孩子学*爬行以及中间的各个阶段:www.youtube.com/watch?v=f3xWaOkXCSQ

人类婴儿学*爬行或幼鸟学*飞行都是自然界中强化学*的例子。

强化学*(在人工智能中)可以定义为一种从与环境互动中进行目标导向学*和决策的计算方法,在某些理想化条件下进行。让我们详细说明这一点,因为我们将使用各种计算机算法来执行学*——这是一种计算方法。在我们考虑的所有例子中,智能体(学*者)都有一个具体的目标,试图实现——这是一个目标导向的方法。强化学*中的智能体没有被给予任何明确的指示,它只从与环境的互动中学*。正如下面的图所示,与环境的互动是一个循环过程。智能体可以感知环境的状态,智能体可以对环境执行特定的、定义明确的动作;这会导致两件事:首先,环境状态发生变化,其次,生成奖励(在理想条件下)。这个循环继续进行:

智能体与环境之间的互动

与监督学*不同,智能体并没有被提供任何示例。智能体不知道正确的动作是什么。与无监督学*不同,智能体的目标不是在输入数据中寻找某种固有的结构(尽管学*过程可能会发现某些结构,但这不是目标);相反,它的目标是最大化奖励(从长远来看)。

RL 术语

在学*不同的算法之前,让我们先了解一下 RL 术语。为了举例说明,我们可以考虑两个例子:一个智能体在迷宫中找到路线,另一个智能体驾驶自动驾驶汽车SDC)。这两个例子在下图中进行了说明:

两个示例 RL 场景

在深入之前,让我们熟悉一些常见的 RL 术语:

  • 状态 s:状态可以看作是一个表示所有可能环境状态的集合(或表示符)。状态可以是连续的,也可以是离散的。例如,在一个智能体寻找迷宫路径的案例中,状态可以由一个 4 × 4 的数组表示,其中0代表空白块,1代表被智能体占据的块,X代表不能占据的状态;这里的状态是离散的。对于一个驾驶方向盘的智能体来说,状态就是 SDC 前方的视图。图像包含连续值的像素。

  • 动作 a(s):动作是智能体在某一特定状态下可以执行的所有可能操作的集合。可能的动作集合,a,取决于当前的状态,s。动作可能导致状态的变化,也可能不会。动作可以是离散的,也可以是连续的。在迷宫中,智能体可以执行五个离散动作:[上, 下, 左, 右, 不变]。另一方面,SDC 智能体可以在一个连续的角度范围内旋转方向盘。

  • 奖励 r(s, a, s'):当智能体选择一个动作时,环境返回的标量值。它定义了目标;如果动作将智能体带到目标附*,智能体会得到更高的奖励,否则会得到较低(甚至负)的奖励。我们如何定义奖励完全取决于我们——以迷宫为例,我们可以将奖励定义为智能体当前位置与目标之间的欧几里得距离。SDC 智能体的奖励可以是汽车在道路上(正奖励)或在道路外(负奖励)。

  • 策略 π(s):它定义了每个状态与在该状态下采取的动作之间的映射。策略可以是确定性的——即对于每个状态都有一个明确的策略。例如,对于迷宫代理,策略可以是如果上方的格子是空的,则向上移动。策略也可以是随机的——即某个动作是由某个概率决定的。它可以通过简单的查找表来实现,或者可以是一个依赖于当前状态的函数。策略是强化学*代理的核心。在本章中,我们将学*帮助代理学*策略的不同算法。

  • 价值函数 V(s):它定义了一个状态在长期中的好坏。它可以被看作是代理从状态s开始,未来能够积累的奖励的总量。你可以将其视为长期的好处,而不是奖励的即时好处。你认为哪个更重要,最大化奖励还是最大化价值函数?是的,你猜对了:就像下棋时,我们有时会牺牲一个兵去换取几步后赢得比赛一样,代理应该尝试最大化价值函数。价值函数通常有两种考虑方式:

    • 价值函数 V^π(s):它是跟随策略π时状态的好坏。数学上,在状态s时,它是从跟随策略π中预期的累积奖励:

  • 价值-状态函数(或 Q 函数) Q^π(s, a):它是状态 s 下采取动作 a,然后跟随策略 π 时的好坏。数学上,我们可以说,对于一个状态-动作对 (s, a),它是从状态 s 中采取动作 a,然后跟随策略 π 所得到的预期累积奖励:

γ是折扣因子,它的值决定了我们在比较即时奖励和后续奖励时对即时奖励赋予多大的重要性。折扣因子的高值决定了代理能看到多远的未来。许多成功的强化学*算法中,γ的理想选择值通常为0.97

  • 环境模型:它是一个可选元素,模拟环境的行为,并包含环境的物理规律;换句话说,它定义了环境将如何表现。环境模型由转移到下一个状态的概率定义。

强化学*问题在数学上被公式化为马尔可夫决策过程MDP),并且遵循马尔可夫性质——即当前状态完全表征世界的状态

深度强化学*

强化学*算法可以根据它们所迭代/逼*的内容分为两类:

  • 基于价值的方法:在这些方法中,算法选择最大化价值函数的动作。这里,智能体学*预测一个给定状态或动作的好坏。因此,目标是找到最优的价值。一个基于价值的方法的例子是 Q 学*。例如,考虑我们的强化学*智能体在迷宫中的情况:假设每个状态的值是从该方格到达目标所需步数的负数,那么,在每个时间步,智能体将选择一个动作,带它到达具有最优值的状态,如下图所示。所以,从值为-6的状态开始,它将移动到-5-4-3-2-1,最终到达值为0的目标:

具有每个方格值的迷宫世界

  • 基于策略的方法:在这些方法中,算法预测最大化价值函数的最佳策略。目标是找到最优策略。一个基于策略的方法的例子是策略梯度。在这里,我们*似策略函数,从而将每个状态映射到最佳的对应动作。

我们可以使用神经网络作为函数逼*器来获取策略或价值的*似值。当我们使用深度神经网络作为策略逼*器或价值逼*器时,我们称之为深度强化学*DRL)。在最*的研究中,DRL 取得了非常成功的结果,因此,在本章中,我们将重点讨论 DRL。

一些成功的应用

在过去几年中,强化学*在各种任务中取得了成功,尤其是在游戏和机器人领域。在学*其算法之前,让我们先了解一些强化学*的成功案例:

  • AlphaGo Zero:由谷歌的 DeepMind 团队开发的 AlphaGo Zero,通过完全没有人类知识的方式掌握围棋,从一个完全空白的起点开始(tabula rasa)。AlphaGo Zero 使用一个神经网络来同时*似走棋概率和价值。这个神经网络以原始的棋盘表示为输入,使用由神经网络引导的蒙特卡洛树搜索来选择走棋。强化学*算法将前瞻性搜索整合到训练循环中。它使用 40 个区块的残差 CNN 训练了 40 天,在此过程中,它进行了大约 2900 万场比赛(一个非常庞大的数字!)。该神经网络在谷歌云上使用 TensorFlow 进行了优化,配备了 64 个 GPU 工作节点和 19 个 CPU 参数服务器。你可以在这里访问论文:www.nature.com/articles/nature24270

  • AI 控制的滑翔机:微软开发了一种控制系统,可以在多种不同的自动驾驶硬件平台上运行,如 Pixhawk 和 Raspberry Pi 3。它可以通过自动寻找并搭乘自然发生的上升气流,使滑翔机在空中飞行而不使用发动机。该控制器帮助滑翔机自行操作;它检测并利用上升气流在没有发动机或人员帮助的情况下飞行。他们将其实现为部分可观察的 MDP(马尔可夫决策过程)。他们采用贝叶斯强化学*,并使用蒙特卡罗树搜索来寻找最佳动作。他们将整个系统分为两个级别的规划者——一个基于经验做出决策的高级规划者和一个利用贝叶斯强化学*实时检测并捕捉上升气流的低级规划者。您可以在微软新闻上查看滑翔机的操作:news.microsoft.com/features/science-mimics-nature-microsoft-researchers-test-ai-controlled-soaring-machine/

  • 运动行为:在论文《丰富环境中运动行为的出现》arxiv.org/pdf/1707.02286.pdf)中,DeepMind 的研究人员为代理提供了丰富且多样的环境。这些环境提供了不同难度等级的挑战。代理面临的困难按顺序递增,这促使代理在没有执行任何奖励工程的情况下学会了复杂的运动技能。

模拟环境

由于强化学*(RL)涉及试错过程,因此在模拟环境中首先训练我们的 RL 代理是很有意义的。虽然有大量的应用可以用于创建环境,但一些流行的应用包括以下内容:

  • OpenAI gym:它包含了一系列我们可以用来训练 RL 代理的环境。在本章中,我们将使用 OpenAI gym 接口。

  • Unity ML-Agents SDK:它允许开发者通过易于使用的 Python API,将使用 Unity 编辑器创建的游戏和模拟转换为可以通过深度强化学*(DRL)、进化策略或其他机器学*方法训练智能代理的环境。它与 TensorFlow 兼容,并提供训练二维/三维以及虚拟现实(VR)/增强现实(AR)游戏中智能代理的能力。您可以在此了解更多:github.com/Unity-Technologies/ml-agents

  • Gazebo:在 Gazebo 中,我们可以构建具有基于物理模拟的三维世界。Gazebo 与机器人操作系统ROS)和 OpenAI Gym 接口一起使用,称为 gym-gazebo,可以用来训练 RL 代理。有关更多信息,您可以参考白皮书:erlerobotics.com/whitepaper/robot_gym.pdf

  • Blender学*环境:这是 Blender 游戏引擎的 Python 接口,它也可以在 OpenAI gym 上使用。它以 Blender 为基础。Blender 是一个免费的三维建模软件,集成了游戏引擎,提供了一套易于使用、功能强大的工具,用于创建游戏。它提供了一个 Blender 游戏引擎的接口,而游戏本身是在 Blender 中设计的。我们可以创建自定义虚拟环境,以便在特定问题上训练强化学*(RL)智能体(github.com/LouisFoucard/gym-blender)。

OpenAI gym

OpenAI gym 是一个开源工具包,用于开发和比较 RL 算法。它包含多种模拟环境,可用于训练智能体并开发新的 RL 算法。首先,你需要安装gym。对于 Python 3.5 及以上版本,可以使用pip安装gym

pip install gym

OpenAI gym 支持多种环境,从简单的基于文本的到三维环境。最新版本中支持的环境可以分为以下几类:

  • 算法:它包含涉及执行计算任务的环境,如加法运算。尽管我们可以轻松地在计算机上进行计算,但作为一个 RL 问题,这些问题的有趣之处在于智能体仅通过示例学*这些任务。

  • Atari:此环境提供各种经典的 Atari/街机游戏。

  • Box2D:它包含二维机器人任务,如赛车代理或双足机器人行走。

  • 经典控制:包含经典的控制理论问题,如平衡推车摆杆。

  • MuJoCo:这是一个专有的物理引擎(你可以获得一个为期一个月的免费试用)。它支持多种机器人模拟任务。该环境包含物理引擎,因此用于训练机器人任务。

  • 机器人学:这个环境也使用 MuJoCo 的物理引擎。它模拟基于目标的任务,如取物和影子手机器人任务。

  • 玩具文本:这是一个基于文本的简单环境,非常适合初学者。

要获取这些组下所有环境的完整列表,你可以访问:gym.openai.com/envs/#atari。OpenAI 接口的最棒之处在于,所有环境都可以通过相同的最小接口进行访问。要获取你安装中所有可用环境的列表,你可以使用以下代码:

from gym import envs
print(envs.registry.all())

这将提供所有已安装环境的列表及其环境 ID,ID 为字符串类型。你还可以在gym注册表中添加你自己的环境。要创建一个环境,我们使用make命令,并将环境名称作为字符串传递。例如,要创建一个使用 Pong 环境的游戏,我们需要的字符串是Pong-v0make命令创建环境,而reset命令用于激活该环境。reset命令将环境恢复到初始状态。该状态以数组形式表示:

import gym
env = gym.make('Pong-v0')
obs = env.reset()
env.render()

Pong-v0的状态空间由一个 210×160×3 的数组表示,这实际上代表了 Pong 游戏的原始像素值。另一方面,如果你创建一个Go9×9-v0环境,状态则由一个 3×9×9 的数组定义。我们可以使用render命令来可视化环境。下图展示了Pong-v0Go9x9-v0环境在初始状态下的渲染环境:

Pong-v0Go9x9-v0的渲染环境

render命令会弹出一个窗口。如果你想在内联显示环境,可以使用 Matplotlib 的内联模式,并将render命令更改为plt.imshow(env.render(mode='rgb_array'))。这将在 Jupyter Notebook 中内联显示环境。

环境包含action_space变量,它决定了环境中可能的动作。我们可以使用sample()函数随机选择一个动作。选择的动作可以通过step函数影响环境。step函数在环境上执行所选动作;它返回改变后的状态、奖励、一个布尔值表示游戏是否结束,以及一些有助于调试但在与强化学*智能体互动时不会用到的环境信息。以下代码展示了一个 Pong 游戏,其中智能体执行一个随机动作。我们在每个时间步将状态存储在一个数组frames中,以便稍后查看游戏:

frames = [] # array to store state space at each step
for _ in range(300):
    frames.append(env.render(mode='rgb_array'))
    obs,reward,done, _ = env.render(env.action_space.sample())
    if done:
        break

这些帧可以借助 Matplotlib 和 IPython 中的动画函数,在 Jupyter Notebook 中以持续播放的 GIF 风格图像显示:

import matplotlib.animation as animation
from JSAnimation.Ipython_display import display_animation
from IPython.display import display

patch = plt.imshow(frames[0])
plt.axis('off')

def animate(i)
    patch.set_data(frames[i])

anim = animation.FuncAnimation(plt.gcf(), animate, \
        frames=len(frames), interval=100)

display(display_animation(anim, default_mode='loop')

通常情况下,训练一个智能体需要大量的步骤,因此在每个步骤保存状态空间是不可行的。我们可以选择在前述算法中的每 500 步(或任何其他你希望的步数)后进行存储。相反,我们可以使用 OpenAI gym 的包装器将游戏保存为视频。为此,我们首先需要导入包装器,然后创建环境,最后使用 Monitor。默认情况下,它将存储 1、8、27、64 等的每个视频,以及每 1,000^(次)的回合(回合数为完美的立方数);每次训练默认保存在一个文件夹中。实现此功能的代码如下:

import gym
from gym import wrappers
env = gym.make('Pong-v0')
env = wrappers.Monitor(env, '/save-mov', force=True)
# Follow it with the code above where env is rendered and agent
# selects a random action

如果你想在下次训练中使用相同的文件夹,可以在Monitor方法调用中选择force=True选项。最后,我们应该使用close函数关闭环境:

env.close()

前述代码可以在OpenAI_practice.ipynb Jupyter Notebook 中找到,位于 GitHub 的第六章,物联网强化学*文件夹内。

Q-learning

在他的博士论文《从延迟奖励中学*》中,Watkins 在 1989 年提出了 Q 学*的概念。Q 学*的目标是学*一个最优的动作选择策略。给定一个特定的状态,s,并采取一个特定的动作,a,Q 学*试图学*状态s的值。在最简单的版本中,Q 学*可以通过查找表来实现。我们维护一个表,记录环境中每个状态(行)和动作(列)的值。算法试图学*这个值——即在给定状态下采取特定动作的好坏。

我们首先将 Q 表中的所有条目初始化为0;这确保了所有状态都有相同的(因此是平等的)价值。后来,我们观察采取特定动作所获得的奖励,并根据奖励更新 Q 表。Q 值的更新是动态进行的,通过贝尔曼方程来帮助实现,方程如下:

这里,α是学*率。以下是基本的 Q 学*算法:

简单的 Q 学*算法

如果你有兴趣,你可以在这里阅读 Watkins 的 240 页博士论文:www.cs.rhul.ac.uk/~chrisw/new_thesis.pdf

学*结束时,我们将拥有一个好的 Q 表,并且有最优策略。这里有一个重要问题:我们如何选择第二步的动作?有两种选择;首先,我们随机选择一个动作。这使得我们的智能体能够以相等的概率探索所有可能的动作,但同时忽略了它已经学到的信息。第二种方式是我们选择具有最大值的动作;最初,所有动作的 Q 值相同,但随着智能体的学*,一些动作将获得高值,另一些则获得低值。在这种情况下,智能体正在利用它已经学到的知识。那么,哪个更好呢:探索还是利用?这就是所谓的探索-利用权衡。解决这个问题的一种自然方式是依赖于智能体已经学到的知识,但有时也需要进行探索。这是通过使用ε-贪婪算法实现的。基本的想法是,智能体以概率ε随机选择动作,而以概率(1-ε)利用在之前的回合中学到的信息。该算法大多数时候(1-ε)选择最好的选项(贪婪),但有时(ε)会做出随机选择。现在让我们尝试在一个简单的问题中实现我们学到的东西。

使用 Q 表的出租车下车

简单的 Q 学*算法涉及维护一个大小为 m×n 的表,其中 m 为状态总数,n 为可能的动作总数。因此,我们从玩具文本组中选择了一个问题,因为它们的 state 空间和 action 空间都很小。为了便于说明,我们选择了 Taxi-v2 环境。我们的智能体目标是选择一个位置的乘客并将其送到另一个位置。智能体成功送客后会获得 +20 分,每走一步会失去 1 分。如果进行非法的接送操作,还会扣除 10 分。状态空间中有墙壁(用 | 表示)和四个位置标记,分别是 RGYB。出租车用框表示:接送位置可以是这四个标记中的任何一个。接客点用蓝色表示,送客点用紫色表示。Taxi-v2 环境的状态空间大小为 500,动作空间大小为 6,因此 Q 表的大小为 500×6=3000 个条目:

出租车接送环境

在出租车接送环境中,出租车用黄色框表示。位置标记 R 是接客点,G 是送客点:

  1. 我们首先导入必要的模块并创建我们的环境。由于这里只需要创建查找表,因此使用 TensorFlow 并不是必需的。如前所述,Taxi-v2 环境有 500 种可能的状态和 6 种可能的动作:
import gym
import numpy as np
env = gym.make('Taxi-v2')
obs = env.reset()
env.render()
  1. 我们将大小为 (300×6) 的 Q 表初始化为全零,并定义超参数:γ 为折扣因子,α 为学*率。我们还设定了最大轮次(一个轮次意味着从重置到完成=True 的一次完整运行)和智能体将在每一轮中学*的最大步数:
m = env.observation_space.n # size of the state space
n = env.action_space.n # size of action space
print("The Q-table will have {} rows and {} columns, resulting in \
     total {} entries".format(m,n,m*n))

# Intialize the Q-table and hyperparameters
Q = np.zeros([m,n])
gamma = 0.97
max_episode = 1000
max_steps = 100
alpha = 0.7
epsilon = 0.3
  1. 现在,对于每一轮,我们选择具有最高值的动作,执行该动作,并根据收到的奖励和未来的状态,使用贝尔曼方程更新 Q 表:
for i in range(max_episode):
    # Start with new environment
    s = env.reset()
    done = False
    for _ in range(max_steps):
        # Choose an action based on epsilon greedy algorithm
        p = np.random.rand()
        if p > epsilon or (not np.any(Q[s,:])):
            a = env.action_space.sample() #explore
        else:
            a = np.argmax(Q[s,:]) # exploit
        s_new, r, done, _ = env.step(a) 
        # Update Q-table
        Q[s,a] = (1-alpha)*Q[s,a] + alpha*(r + gamma*np.max(Q[s_new,:]))
        #print(Q[s,a],r)
        s = s_new
        if done:
            break
  1. 现在我们来看一下学*型智能体是如何工作的:
s = env.reset()
done = False
env.render()
# Test the learned Agent
for i in range(max_steps):
 a = np.argmax(Q[s,:])
 s, _, done, _ = env.step(a)
 env.render()
 if done:
 break 

下图展示了在一个特定示例中智能体的行为。空车用黄色框表示,载有乘客的车用绿色框表示。从图中可以看出,在给定的情况下,智能体在 11 步内完成接送乘客,目标位置标记为 (B),目的地标记为 (R):

使用学*到的 Q 表,智能体接送乘客

酷吧?完整的代码可以在 GitHub 上找到,文件名为 Taxi_drop-off.ipynb

Q 网络

简单的 Q 学*算法涉及维持一个大小为 m×n 的表格,其中 m 是状态的总数,n 是可能动作的总数。这意味着我们不能将其用于大规模的状态空间和动作空间。一个替代方法是用神经网络替换表格,作为一个函数逼*器,逼*每个可能动作的 Q 函数。在这种情况下,神经网络的权重存储着 Q 表格的信息(它们将给定状态与相应的动作及其 Q 值匹配)。当我们用深度神经网络来逼* Q 函数时,我们称其为 深度 Q 网络 (DQN)。

神经网络以状态为输入,计算所有可能动作的 Q 值。

使用 Q-网络进行出租车下车

如果我们考虑前面的 出租车下车 示例,我们的神经网络将由 500 个输入神经元组成(状态由 1×500 的 one-hot 向量表示),以及 6 个输出神经元,每个神经元代表给定状态下某一特定动作的 Q 值。神经网络将在此处逼*每个动作的 Q 值。因此,网络应该经过训练,使得其逼*的 Q 值与目标 Q 值相同。目标 Q 值由贝尔曼方程给出,如下所示:

我们训练神经网络,使得目标 Q 和预测 Q 之间的平方误差最小化——也就是说,神经网络最小化以下损失函数:

目标是学*未知的 Q[target] 函数。通过反向传播更新 QNetwork 的权重,以使损失最小化。我们使神经网络 QNetwork 来逼* Q 值。它是一个非常简单的单层神经网络,具有提供动作及其 Q 值(get_action)、训练网络(learnQ)以及获取预测 Q 值(Qnew)的方法:

class QNetwork:
    def __init__(self,m,n,alpha):
        self.s = tf.placeholder(shape=[1,m], dtype=tf.float32)
        W = tf.Variable(tf.random_normal([m,n], stddev=2))
        bias = tf.Variable(tf.random_normal([1, n]))
        self.Q = tf.matmul(self.s,W) + bias
        self.a = tf.argmax(self.Q,1)

        self.Q_hat = tf.placeholder(shape=[1,n],dtype=tf.float32)
        loss = tf.reduce_sum(tf.square(self.Q_hat-self.Q))
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=alpha)
        self.train = optimizer.minimize(loss)
        init = tf.global_variables_initializer()

        self.sess = tf.Session()
        self.sess.run(init)

    def get_action(self,s):
        return self.sess.run([self.a,self.Q], feed_dict={self.s:s})

    def learnQ(self,s,Q_hat):
        self.sess.run(self.train, feed_dict= {self.s:s, self.Q_hat:Q_hat})

    def Qnew(self,s):
        return self.sess.run(self.Q, feed_dict={self.s:s})

我们现在将这个神经网络集成到之前训练 RL 代理解决 出租车下车 问题的代码中。我们需要进行一些更改;首先,OpenAI 步骤和重置函数返回的状态只是状态的数字标识符,所以我们需要将其转换为一个 one-hot 向量。此外,我们不再使用 Q 表格更新,而是从 QNetwork 获取新的 Q 预测值,找到目标 Q 值,并训练网络以最小化损失。代码如下:

QNN = QNetwork(m,n, alpha)
rewards = []
for i in range(max_episode):
 # Start with new environment
 s = env.reset()
 S = np.identity(m)[s:s+1]
 done = False
 counter = 0
 rtot = 0
 for _ in range(max_steps):
 # Choose an action using epsilon greedy policy
 a, Q_hat = QNN.get_action(S) 
 p = np.random.rand()
 if p > epsilon:
 a[0] = env.action_space.sample() #explore

 s_new, r, done, _ = env.step(a[0])
 rtot += r
 # Update Q-table
 S_new = np.identity(m)[s_new:s_new+1]
 Q_new = QNN.Qnew(S_new) 
 maxQ = np.max(Q_new)
 Q_hat[0,a[0]] = r + gamma*maxQ
 QNN.learnQ(S,Q_hat)
 S = S_new
 #print(Q_hat[0,a[0]],r)
 if done:
 break
 rewards.append(rtot)
print ("Total reward per episode is: " + str(sum(rewards)/max_episode))

这本应该做得很好,但正如你所看到的,即使训练了1,000个回合,网络的奖励依然很低,如果你查看网络的表现,似乎它只是随便走动。是的,我们的网络什么都没学到;表现比 Q 表还差。这也可以从训练过程中的奖励图表验证——理想情况下,随着代理的学*,奖励应该增加,但这里并没有发生这种情况;奖励像是围绕平均值的随机漫步(该程序的完整代码可以在Taxi_drop-off_NN.ipynb文件中找到,文件在 GitHub 上可用):

代理在学*过程中每个回合获得的总奖励

发生了什么?为什么神经网络没有学到东西,我们能改进它吗?

假设出租车需要往西走去接客,而代理随机选择了西行;代理获得了奖励,网络会学*到,在当前状态下(通过一个独热编码表示),西行是有利的。接下来,考虑另一个与此相似的状态(相关状态空间):代理再次做出西行动作,但这次却获得了负奖励,所以现在代理会忘记之前学到的东西。因此,相似的状态-动作对但目标不同会混淆学*过程。这被称为灾难性遗忘。问题的产生是因为连续状态高度相关,因此,如果代理按顺序学*(如本例),这个高度相关的输入状态空间会妨碍代理的学*。

我们可以打破输入数据与网络之间的关联吗?可以:我们可以构建一个回放缓冲区,在这里我们首先存储每个状态、其对应的动作、连续奖励和结果状态(状态、动作、奖励、新状态)。在这种情况下,动作是完全随机选择的,从而确保了动作和结果状态的多样性。回放缓冲区最终将由这些元组(SARS')组成一个大列表。接下来,我们将这些元组随机地输入到网络中(而不是按顺序输入);这种随机性将打破连续输入状态之间的关联。这被称为经验回放。它不仅解决了输入状态空间中的关联问题,还使我们能够多次从相同的元组中学*,回顾稀有事件,并且通常能更好地利用经验。从某种意义上说,通过使用回放缓冲区,我们已经减少了监督学*中的问题(回放缓冲区作为输入输出数据集),其中输入的随机采样确保了网络能够进行泛化。

我们的方法的另一个问题是,我们立即更新目标 Q。这也会导致有害的相关性。请记住,在 Q 学*中,我们试图最小化Q[target]与当前预测的Q之间的差异。这个差异被称为时序差分TD)误差(因此 Q 学*是一种TD 学*)。目前,我们立即更新我们的Q[target],因此目标与我们正在更改的参数之间(通过Q[pred]进行的权重)存在相关性。这几乎就像在追逐一个移动的目标,因此不会给出一个通用的方向。我们可以通过使用固定的 Q 目标来解决这个问题——即使用两个网络,一个用于预测Q,另一个用于目标Q。这两个网络在架构上完全相同,预测 Q 网络在每一步中都会改变权重,而目标 Q 网络的权重会在固定的学*步骤后更新。这提供了一个更加稳定的学*环境。

最后,我们做一个小小的改变:目前,我们的 epsilon 在整个学*过程中都有一个固定值。但在现实生活中并非如此。最初,当我们一无所知时,我们会进行大量探索,但随着我们变得熟悉,我们倾向于采取已学到的路径。在我们的 epsilon 贪婪算法中也可以做到这一点,通过随着网络在每个回合中学*,逐步改变 epsilon 的值,使得 epsilon 随时间减少。

配备了这些技巧后,现在我们来构建一个 DQN 来玩 Atari 游戏。

DQN 来玩 Atari 游戏

我们将在这里学*的 DQN 基于 DeepMind 的论文(web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf)。DQN 的核心是一个深度卷积神经网络,它以游戏环境的原始像素为输入(就像任何人类玩家看到的一样),每次捕捉一屏幕,并将每个可能动作的值作为输出。值最大的动作就是选择的动作:

  1. 第一步是获取我们所需的所有模块:

import gym
import sys
import random
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from datetime import datetime
from scipy.misc import imresize
  1. 我们选择了 OpenAI Atari 游戏列表中的 Breakout 游戏——你可以尝试其他 Atari 游戏的代码;你可能唯一需要更改的地方是预处理步骤。Breakout 的输入空间——即我们的输入空间——由 210×160 个像素组成,每个像素有 128 种可能的颜色。这是一个非常庞大的输入空间。为了减少复杂性,我们将选择图像中的一个感兴趣区域,将其转换为灰度图像,并将其调整为大小为80×80的图像。我们通过preprocess函数来实现这一点:
def preprocess(img):
    img_temp = img[31:195] # Choose the important area of the image
    img_temp = img_temp.mean(axis=2) # Convert to Grayscale#
    # Downsample image using nearest neighbour interpolation
    img_temp = imresize(img_temp, size=(IM_SIZE, IM_SIZE), interp='nearest')
    return img_temp

以下截图展示了预处理前后的环境:

原始环境的大小为 210×160(彩色图像),处理后的环境大小为 80×80(灰度图像)。

  1. 从前面的图示中可以看到,无法判断球是下落还是上升。为了解决这个问题,我们将四个连续的状态(由于四个独特的动作)结合为一个输入。我们定义了一个函数update_state,它将当前环境的观察结果追加到前一个状态数组中:
def update_state(state, obs):
    obs_small = preprocess(obs)
    return np.append(state[1:], np.expand_dims(obs_small, 0), axis=0)

该函数将处理过的新状态追加到切片的状态中,确保网络的最终输入由四个帧组成。在以下截图中,你可以看到这四个连续的帧。这是我们 DQN 的输入:

DQN 的输入是四个连续的游戏状态(帧)

  1. 我们创建了一个在class DQN中定义的 DQN;它由三层卷积层组成,最后一层卷积层的输出被展平,然后是两个全连接层。该网络和之前的情况一样,试图最小化Q[target]Q[predicted]之间的差异。在代码中,我们使用的是 RMSProp 优化器,但你也可以尝试其他优化器:
def __init__(self, K, scope, save_path= 'models/atari.ckpt'):
    self.K = K
    self.scope = scope
    self.save_path = save_path
    with tf.variable_scope(scope):
        # inputs and targets
        self.X = tf.placeholder(tf.float32, shape=(None, 4, IM_SIZE, IM_SIZE), name='X')
        # tensorflow convolution needs the order to be:
        # (num_samples, height, width, "color")
        # so we need to tranpose later
        self.Q_target = tf.placeholder(tf.float32, shape=(None,), name='G')
        self.actions = tf.placeholder(tf.int32, shape=(None,), name='actions')
        # calculate output and cost
        # convolutional layers
        Z = self.X / 255.0
        Z = tf.transpose(Z, [0, 2, 3, 1])
        cnn1 = tf.contrib.layers.conv2d(Z, 32, 8, 4, activation_fn=tf.nn.relu)
        cnn2 = tf.contrib.layers.conv2d(cnn1, 64, 4, 2, activation_fn=tf.nn.relu)
        cnn3 = tf.contrib.layers.conv2d(cnn2, 64, 3, 1, activation_fn=tf.nn.relu)
        # fully connected layers
        fc0 = tf.contrib.layers.flatten(cnn3)
        fc1 = tf.contrib.layers.fully_connected(fc0, 512)
        # final output layer
        self.predict_op = tf.contrib.layers.fully_connected(fc1, K)
        Qpredicted = tf.reduce_sum(self.predict_op * tf.one_hot(self.actions, K),
     reduction_indices=[1])
        self.cost = tf.reduce_mean(tf.square(self.Q_target - Qpredicted))
        self.train_op = tf.train.RMSPropOptimizer(0.00025, 0.99, 0.0, 1e-6).minimize(self.cost)

我们在接下来的步骤中讨论了该类所需的必要方法:

  1. 我们添加了一个方法来返回预测的 Q 值:
def predict(self, states):
    return self.session.run(self.predict_op, feed_dict={self.X: states})
  1. 我们需要一个方法来确定具有最大值的动作。在这个方法中,我们还实现了 epsilon-贪婪策略,且 epsilon 的值在主代码中会发生变化:
def sample_action(self, x, eps):
    """Implements epsilon greedy algorithm"""
    if np.random.random() < eps:
        return np.random.choice(self.K)
    else:
        return np.argmax(self.predict([x])[0])
  1. 我们需要一个方法来更新网络的权重,以最小化损失。该函数可以定义如下:
 def update(self, states, actions, targets):
     c, _ = self.session.run(
         [self.cost, self.train_op],
         feed_dict={
         self.X: states,
         self.Q_target: targets,
         self.actions: actions
         })
     return c
  1. 将模型权重复制到固定的 Q 网络中:
def copy_from(self, other):
    mine = [t for t in tf.trainable_variables() if t.name.startswith(self.scope)]
    mine = sorted(mine, key=lambda v: v.name)
    theirs = [t for t in tf.trainable_variables() if t.name.startswith(other.scope)]
    theirs = sorted(theirs, key=lambda v: v.name)
    ops = []
    for p, q in zip(mine, theirs):
        actual = self.session.run(q)
        op = p.assign(actual)
        ops.append(op)
    self.session.run(ops)
  1. 除了这些方法,我们还需要一些辅助函数来保存学*到的网络、加载保存的网络,并设置 TensorFlow 会话:
def load(self):
    self.saver = tf.train.Saver(tf.global_variables())
    load_was_success = True
    try:
        save_dir = '/'.join(self.save_path.split('/')[:-1])
        ckpt = tf.train.get_checkpoint_state(save_dir)
        load_path = ckpt.model_checkpoint_path
        self.saver.restore(self.session, load_path)
    except:
        print("no saved model to load. starting new session")
        load_was_success = False
    else:
        print("loaded model: {}".format(load_path))
        saver = tf.train.Saver(tf.global_variables())
        episode_number = int(load_path.split('-')[-1])

def save(self, n):
    self.saver.save(self.session, self.save_path, global_step=n)
    print("SAVED MODEL #{}".format(n))

def set_session(self, session):
    self.session = session
    self.session.run(tf.global_variables_initializer())
    self.saver = tf.train.Saver()
  1. 为了实现 DQN 算法,我们使用一个learn函数;它从经验重放缓冲区中随机选择一个样本,并使用目标 Q 网络中的目标 Q 来更新 Q 网络:
def learn(model, target_model, experience_replay_buffer, gamma, batch_size):
    # Sample experiences
    samples = random.sample(experience_replay_buffer, batch_size)
    states, actions, rewards, next_states, dones = map(np.array, zip(*samples))
    # Calculate targets
     next_Qs = target_model.predict(next_states)
     next_Q = np.amax(next_Qs, axis=1)
     targets = rewards +     np.invert(dones).astype(np.float32) * gamma * next_Q
    # Update model
     loss = model.update(states, actions, targets)
     return loss
  1. 好了,所有的元素都准备好了,现在让我们决定 DQN 的超参数并创建我们的环境:
# Some Global parameters
MAX_EXPERIENCES = 500000
MIN_EXPERIENCES = 50000
TARGET_UPDATE_PERIOD = 10000
IM_SIZE = 80
K = 4 # env.action_space.n

# hyperparameters etc
gamma = 0.97
batch_sz = 64
num_episodes = 2700
total_t = 0
experience_replay_buffer = []
episode_rewards = np.zeros(num_episodes)
last_100_avgs = []
# epsilon for Epsilon Greedy Algorithm
epsilon = 1.0
epsilon_min = 0.1
epsilon_change = (epsilon - epsilon_min) / 700000

# Create Atari Environment
env = gym.envs.make("Breakout-v0")

# Create original and target Networks
model = DQN(K=K, scope="model")
target_model = DQN(K=K, scope="target_model")
  1. 最后,以下是调用并填充经验重放缓冲区、逐步执行游戏并在每一步训练模型网络以及每四步训练target_model的代码:
with tf.Session() as sess:
    model.set_session(sess)
    target_model.set_session(sess)
    sess.run(tf.global_variables_initializer())
    model.load()
    print("Filling experience replay buffer...")
    obs = env.reset()
    obs_small = preprocess(obs)
    state = np.stack([obs_small] * 4, axis=0)
    # Fill experience replay buffer
    for i in range(MIN_EXPERIENCES):
        action = np.random.randint(0,K)
        obs, reward, done, _ = env.step(action)
        next_state = update_state(state, obs)
        experience_replay_buffer.append((state, action, reward, next_state, done))
        if done:
            obs = env.reset()
            obs_small = preprocess(obs)
            state = np.stack([obs_small] * 4, axis=0)
        else:
            state = next_state
        # Play a number of episodes and learn
        for i in range(num_episodes):
            t0 = datetime.now()
            # Reset the environment
            obs = env.reset()
            obs_small = preprocess(obs)
            state = np.stack([obs_small] * 4, axis=0)
            assert (state.shape == (4, 80, 80))
            loss = None
            total_time_training = 0
            num_steps_in_episode = 0
            episode_reward = 0
            done = False
            while not done:
                # Update target network
                if total_t % TARGET_UPDATE_PERIOD == 0:
                    target_model.copy_from(model)
                    print("Copied model parameters to target network. total_t = %s, period = %s" % (total_t, TARGET_UPDATE_PERIOD))
                # Take action
                action = model.sample_action(state, epsilon)
                obs, reward, done, _ = env.step(action)
                obs_small = preprocess(obs)
                next_state = np.append(state[1:], np.expand_dims(obs_small, 0), axis=0)
                episode_reward += reward
                # Remove oldest experience if replay buffer is full
                if len(experience_replay_buffer) == MAX_EXPERIENCES:
                    experience_replay_buffer.pop(0)
                    # Save the recent experience
                    experience_replay_buffer.append((state, action, reward, next_state, done))

                # Train the model and keep measure of time
                t0_2 = datetime.now()
                loss = learn(model, target_model, experience_replay_buffer, gamma, batch_sz)
                dt = datetime.now() - t0_2
                total_time_training += dt.total_seconds()
                num_steps_in_episode += 1
                state = next_state
                total_t += 1
                epsilon = max(epsilon - epsilon_change, epsilon_min)
                duration = datetime.now() - t0
                episode_rewards[i] = episode_reward
                time_per_step = total_time_training / num_steps_in_episode
                last_100_avg = episode_rewards[max(0, i - 100):i + 1].mean()
                last_100_avgs.append(last_100_avg)
                print("Episode:", i,"Duration:", duration, "Num steps:", num_steps_in_episode, "Reward:", episode_reward, "Training time per step:", "%.3f" % time_per_step, "Avg Reward (Last 100):", "%.3f" % last_100_avg,"Epsilon:", "%.3f" % epsilon)
                if i % 50 == 0:
                    model.save(i)
                sys.stdout.flush()

#Plots
plt.plot(last_100_avgs)
plt.xlabel('episodes')
plt.ylabel('Average Rewards')
plt.show()
env.close()

我们可以看到,现在奖励随着回合数增加,最终的平均奖励是20,尽管它可能更高,但我们仅仅学*了几千个回合,甚至我们的重放缓冲区的大小在(50,000 到 5,000,000)之间:

智能体学*过程中的平均奖励

  1. 让我们看看我们的智能体在学*了大约 2,700 个回合后是如何表现的:
env = gym.envs.make("Breakout-v0")
frames = []
with tf.Session() as sess:
    model.set_session(sess)
    target_model.set_session(sess)
    sess.run(tf.global_variables_initializer())
    model.load()
    obs = env.reset()
    obs_small = preprocess(obs)
    state = np.stack([obs_small] * 4, axis=0)
    done = False
    while not done:
        action = model.sample_action(state, epsilon)
        obs, reward, done, _ = env.step(action)
        frames.append(env.render(mode='rgb_array'))
        next_state = update_state(state, obs)
        state = next_state

你可以在这里看到学*过的智能体的视频:www.youtube.com/watch?v=rPy-3NodgCE

很酷,对吧?没有告诉它任何信息,它仅仅通过 2,700 个回合学会了如何玩一款不错的游戏。

有一些方法可以帮助你更好地训练智能体:

  • 由于训练需要大量时间,除非你有强大的计算资源,否则最好保存模型并重新启动保存的模型。

  • 在代码中,我们使用了Breakout-v0和 OpenAI gym,在这种情况下,环境中会对连续(随机选择的1234)帧重复相同的步骤。你也可以选择BreakoutDeterministic-v4,这是 DeepMind 团队使用的版本;在这里,步骤会恰好在连续的四帧中重复。因此,智能体在每第四帧后看到并选择动作。

双重 DQN

现在,回想一下,我们使用最大值操作符来选择一个动作并评估这个动作。这可能导致一个可能并非最理想的动作被高估。我们可以通过将选择和评估解耦来解决这个问题。通过 Double DQN,我们有两个权重不同的 Q 网络;这两个网络都通过随机经验进行学*,但一个用于通过 epsilon-greedy 策略来确定动作,另一个用于确定其值(因此,计算目标 Q 值)。

为了更清楚地说明,让我们先看一下 DQN 的情况。选择具有最大 Q 值的动作;设W为 DQN 的权重,那么我们正在做的是:

上标W表示用于*似 Q 值的权重。在 Double DQN 中,方程变为如下:

请注意变化:现在,动作是通过使用权重为W的 Q 网络选择的,并且最大 Q 值是通过使用权重为W'的 Q 网络预测的。这减少了过估计,有助于我们更快且更可靠地训练智能体。你可以在这里访问Deep Reinforcement Learning with Double Q-Learning论文:www.aaai.org/ocs/index.php/AAAI/AAAI16/paper/download/12389/11847

对战 DQN

对战 DQN 将 Q 函数解耦为价值函数和优势函数。价值函数和之前讨论的相同;它表示状态的价值,与动作无关。另一方面,优势函数提供了动作a在状态s中的相对效用(优势/好处)的度量:

在 Dueling DQN 中,使用相同的卷积层来提取特征,但在后期阶段,它被分成两个独立的网络,一个提供价值,另一个提供优势。随后,两个阶段通过聚合层重新组合,以估计 Q 值。这确保了网络为价值函数和优势函数生成独立的估计值。这种价值和优势的解耦直觉是,对于许多状态,估计每个动作选择的价值并非必要。例如,在赛车中,如果前方没有车,那么选择“向左转”或“向右转”就没有必要,因此在给定状态下无需估计这些动作的价值。这使得网络可以学*哪些状态是有价值的,而不必为每个状态确定每个动作的效果。

在聚合层中,价值和优势被结合在一起,使得可以从给定的Q中唯一地恢复出VA。这是通过强制要求优势函数估计器在所选动作下的优势为零来实现的:

这里,θ是通用卷积特征提取器的参数,αβ是优势和值估计器网络的参数。Dueling DQN 也是由谷歌 DeepMind 团队提出的。你可以在arXiv上阅读完整的论文:arxiv.org/abs/1511.06581。作者发现,用平均操作替换先前的max操作可以提高网络的稳定性。在这种情况下,优势的变化速度仅与均值变化速度相同。因此,在他们的结果中,使用了以下给出的聚合层:

以下截图展示了 Dueling DQN 的基本架构:

Dueling DQN 的基本架构

策略梯度

在基于 Q 学*的方法中,我们在估计价值/Q 函数之后生成策略。在基于策略的方法中,如策略梯度方法,我们直接逼*策略。

按照之前的方法,我们在这里使用神经网络来逼*策略。在最简单的形式下,神经网络通过使用最陡梯度上升法调整权重来学*选择最大化奖励的动作策略,因此得名“策略梯度”。

在策略梯度中,策略由一个神经网络表示,其输入是状态的表示,输出是动作选择的概率。该网络的权重是我们需要学*的策略参数。自然会产生一个问题:我们应该如何更新这个网络的权重?由于我们的目标是最大化奖励,因此可以理解,我们的网络试图最大化每个回合的期望奖励:

在这里,我们使用了一个参数化的随机策略 π——也就是说,策略决定了在给定状态 s 的情况下选择动作 a 的概率,神经网络的参数是 θR 代表一个回合中所有奖励的总和。然后,使用梯度上升法更新网络参数:

这里,η 是学*率。通过策略梯度定理,我们得到如下公式:

因此,替代最大化期望回报,我们可以使用损失函数作为对数损失(将期望动作和预测动作作为标签和 logits),并将折扣奖励作为权重来训练网络。为了增加稳定性,研究发现添加基线有助于减少方差。最常见的基线形式是折扣奖励的总和,结果如下所示:

基线 b(s[t]) 如下所示:

这里,γ 是折扣因子。

为什么选择策略梯度?

首先,政策梯度方法像其他基于策略的方法一样,直接估计最优策略,无需存储额外的数据(经验回放缓冲区)。因此,它的实现非常简单。其次,我们可以训练它来学*真正的随机策略。最后,它非常适合连续动作空间。

使用策略梯度玩 Pong 游戏

让我们尝试使用策略梯度来玩 Pong 游戏。这里的实现灵感来自 Andrej Karpathy 的博客文章,文章地址:karpathy.github.io/2016/05/31/rl/。回想一下,在 Breakout 游戏中,我们使用了四个游戏帧堆叠在一起作为输入,从而使代理能够了解游戏的动态;而在这里,我们使用连续两帧游戏图像的差异作为输入。因此,我们的代理同时拥有当前状态和前一个状态的信息:

  1. 首先,和往常一样,我们需要导入必要的模块。我们导入了 TensorFlow、Numpy、Matplotlib 和 gym 作为环境:
import numpy as np
import gym
import matplotlib.pyplot as plt
import tensorflow as tf
from gym import wrappers
%matplotlib inline
  1. 我们构建了我们的神经网络,即 PolicyNetwork;它以游戏状态作为输入,并输出动作选择概率。在这里,我们构建了一个简单的两层感知器,且没有偏置。weights 使用 Xavier 初始化方法随机初始化。隐藏层使用 ReLU 激活函数,输出层使用 softmax 激活函数。我们使用稍后定义的 tf_discount_rewards 方法来计算基线。最后,我们使用 TensorFlow 的 tf.losses.log_loss 来计算预测的动作概率,并选择一个热编码的动作向量作为标签,同时将折扣奖励和方差校正后的奖励作为权重:
class PolicyNetwork(object):
    def __init__(self, N_SIZE, h=200, gamma=0.99, eta=1e-3, decay=0.99, save_path = 'models1/pong.ckpt' ):
        self.gamma = gamma
        self.save_path = save_path
        # Placeholders for passing state....
        self.tf_x = tf.placeholder(dtype=tf.float32, shape=[None, N_SIZE * N_SIZE], name="tf_x")
        self.tf_y = tf.placeholder(dtype=tf.float32, shape=[None, n_actions], name="tf_y")
        self.tf_epr = tf.placeholder(dtype=tf.float32, shape=[None, 1], name="tf_epr")

        # Weights
        xavier_l1 = tf.truncated_normal_initializer(mean=0, stddev=1\. / N_SIZE, dtype=tf.float32)
        self.W1 = tf.get_variable("W1", [N_SIZE * N_SIZE, h], initializer=xavier_l1)
        xavier_l2 = tf.truncated_normal_initializer(mean=0, stddev=1\. / np.sqrt(h), dtype=tf.float32)
        self.W2 = tf.get_variable("W2", [h, n_actions], initializer=xavier_l2)

        #Build Computation
        # tf reward processing (need tf_discounted_epr for policy gradient wizardry)
        tf_discounted_epr = self.tf_discount_rewards(self.tf_epr)
        tf_mean, tf_variance = tf.nn.moments(tf_discounted_epr, [0], shift=None, name="reward_moments")
        tf_discounted_epr -= tf_mean
        tf_discounted_epr /= tf.sqrt(tf_variance + 1e-6)

        #Define Optimizer, compute and apply gradients
        self.tf_aprob = self.tf_policy_forward(self.tf_x)
        loss = tf.losses.log_loss(labels = self.tf_y,
        predictions = self.tf_aprob,
        weights = tf_discounted_epr)
        optimizer = tf.train.AdamOptimizer()
        self.train_op = optimizer.minimize(loss)
  1. 该类有方法来计算动作概率(tf_policy_forwardpredict_UP),使用 tf_discount_rewards 计算基线,更新网络权重(update),并最终设置会话(set_session),然后加载和保存模型:
def set_session(self, session):
    self.session = session
    self.session.run(tf.global_variables_initializer())
    self.saver = tf.train.Saver()

def tf_discount_rewards(self, tf_r): # tf_r ~ [game_steps,1]
    discount_f = lambda a, v: a * self.gamma + v;
    tf_r_reverse = tf.scan(discount_f, tf.reverse(tf_r, [0]))
    tf_discounted_r = tf.reverse(tf_r_reverse, [0])
    return tf_discounted_r

def tf_policy_forward(self, x): #x ~ [1,D]
    h = tf.matmul(x, self.W1)
    h = tf.nn.relu(h)
    logp = tf.matmul(h, self.W2)
    p = tf.nn.softmax(logp)
    return p

def update(self, feed):
    return self.session.run(self.train_op, feed)

def load(self):
    self.saver = tf.train.Saver(tf.global_variables())
    load_was_success = True 
    try:
        save_dir = '/'.join(self.save_path.split('/')[:-1])
        ckpt = tf.train.get_checkpoint_state(save_dir)
        load_path = ckpt.model_checkpoint_path
        print(load_path)
        self.saver.restore(self.session, load_path)
    except:
        print("no saved model to load. starting new session")
        load_was_success = False
    else:
        print("loaded model: {}".format(load_path))
        saver = tf.train.Saver(tf.global_variables())
        episode_number = int(load_path.split('-')[-1])

def save(self):
    self.saver.save(self.session, self.save_path, global_step=n)
    print("SAVED MODEL #{}".format(n))

def predict_UP(self,x):
    feed = {self.tf_x: np.reshape(x, (1, -1))}
    aprob = self.session.run(self.tf_aprob, feed);
    return aprob
  1. 现在,PolicyNetwork已经创建,我们为游戏状态创建了一个preprocess函数;我们不会处理完整的 210×160 状态空间——而是将其缩减为 80×80 的二值状态空间,最后将其展平:
# downsampling
def preprocess(I):
    """ 
    prepro 210x160x3 uint8 frame into 6400 (80x80) 1D float vector 
    """
    I = I[35:195] # crop
    I = I[::2,::2,0] # downsample by factor of 2
    I[I == 144] = 0 # erase background (background type 1)
    I[I == 109] = 0 # erase background (background type 2)
    I[I != 0] = 1 # everything else (paddles, ball) just set to 1
    return I.astype(np.float).ravel()
  1. 让我们定义一些变量,来保存状态、标签、奖励和动作空间大小。我们初始化游戏状态并实例化策略网络:
# Create Game Environment
env_name = "Pong-v0"
env = gym.make(env_name)
env = wrappers.Monitor(env, '/tmp/pong', force=True)
n_actions = env.action_space.n # Number of possible actions
# Initializing Game and State(t-1), action, reward, state(t)
states, rewards, labels = [], [], []
obs = env.reset()
prev_state = None

running_reward = None
running_rewards = []
reward_sum = 0
n = 0
done = False
n_size = 80
num_episodes = 2500

#Create Agent
agent = PolicyNetwork(n_size)
  1. 现在我们开始实施策略梯度算法。对于每一集,智能体首先进行游戏,存储状态、奖励和选择的动作。一旦游戏结束,它就会使用所有存储的数据来进行训练(就像监督学*一样)。然后它会重复这一过程,直到达到你想要的集数:
with tf.Session() as sess:
    agent.set_session(sess)
    sess.run(tf.global_variables_initializer())
    agent.load()
    # training loop
    done = False
    while not done and n< num_episodes:
        # Preprocess the observation
        cur_state = preprocess(obs)
        diff_state = cur_state - prev_state if prev_state isn't None else np.zeros(n_size*n_size)
        prev_state = cur_state

        #Predict the action
        aprob = agent.predict_UP(diff_state) ; aprob = aprob[0,:]
        action = np.random.choice(n_actions, p=aprob)
        #print(action)
        label = np.zeros_like(aprob) ; label[action] = 1

        # Step the environment and get new measurements
        obs, reward, done, info = env.step(action)
        env.render()
        reward_sum += reward

        # record game history
        states.append(diff_state) ; labels.append(label) ; rewards.append(reward)

        if done:
            # update running reward
            running_reward = reward_sum if running_reward is None else         running_reward * 0.99 + reward_sum * 0.01    
            running_rewards.append(running_reward)
            #print(np.vstack(rs).shape)
            feed = {agent.tf_x: np.vstack(states), agent.tf_epr: np.vstack(rewards), agent.tf_y: np.vstack(labels)}
            agent.update(feed)
            # print progress console
            if n % 10 == 0:
                print ('ep {}: reward: {}, mean reward: {:3f}'.format(n, reward_sum, running_reward))
            else:
                print ('\tep {}: reward: {}'.format(n, reward_sum))

            # Start next episode and save model
            states, rewards, labels = [], [], []
            obs = env.reset()
            n += 1 # the Next Episode

            reward_sum = 0
            if n % 50 == 0:
                agent.save()
            done = False

plt.plot(running_rewards)
plt.xlabel('episodes')
plt.ylabel('Running Averge')
plt.show()
env.close()
  1. 在训练了 7,500 集后,智能体开始赢得一些游戏。在训练了 1,200 集后,胜率有所提高,达到了 50%。经过 20,000 集训练后,智能体已经能够赢得大部分游戏。完整代码可在 GitHub 的 Policy gradients.ipynb 文件中找到。你可以在这里查看智能体经过 20,000 集学*后进行的游戏:youtu.be/hZo7kAco8is。请注意,这个智能体学会了在自己的位置附*振荡;它还学会了将自己运动产生的力量传递给球,并且知道只有通过进攻性击球才能击败对手。

演员-评论家算法

在策略梯度方法中,我们引入了基线来减少方差,但依然是动作和基线(仔细看:方差是预期奖励的总和,换句话说,它是状态的好坏或其价值函数)同时变化。是不是应该将策略评估和价值评估分开呢?这正是演员-评论家方法的思想。它由两个神经网络组成,一个用于*似策略,称为演员网络,另一个用于*似价值,称为评论家网络。我们在策略评估和策略改进步骤之间交替进行,从而实现更稳定的学*。评论家使用状态和动作值来估计价值函数,接着用它来更新演员的策略网络参数,使得整体性能得以提升。下图展示了演员-评论家网络的基本架构:

演员-评论家架构

总结

在本章中,我们学*了强化学*(RL)以及它与监督学*和无监督学*的区别。本章的重点是深度强化学*(DRL),在该方法中,深度神经网络用于*似策略函数或价值函数,甚至两者兼而有之。本章介绍了 OpenAI Gym,这是一个提供大量环境来训练 RL 代理的库。我们学*了基于价值的方法,如 Q-learning,并利用它训练一个代理来接载和放下出租车中的乘客。我们还使用了 DQN 来训练一个代理玩 Atari 游戏。接着,本章介绍了基于策略的方法,特别是策略梯度。我们讨论了策略梯度背后的直觉,并使用该算法训练一个 RL 代理来玩 Pong 游戏。

在下一章中,我们将探索生成模型,并学*生成对抗网络背后的秘密。

第七章:物联网的生成模型

机器学*ML)和人工智能AI)几乎触及了所有与人类相关的领域。农业、音乐、健康、国防——你找不到一个没有 AI 印记的领域。AI/ML 的巨大成功,除了计算能力的存在,还依赖于大量数据的生成。大多数生成的数据是未标注的,因此理解数据的内在分布是一个重要的机器学*任务。正是在这一点上,生成模型发挥了作用。

在过去的几年中,深度生成模型在理解数据分布方面取得了巨大成功,并已应用于各种领域。最受欢迎的两种生成模型是 变分自编码器VAEs)和 生成对抗网络GANs)。

在本章中,我们将学* VAEs 和 GANs,并使用它们生成图像。阅读完本章后,你将掌握以下内容:

  • 了解生成网络与判别网络之间的区别

  • 了解 VAEs

  • 理解 GANs 的直观功能

  • 实现一个基本的 GAN,并用它生成手写数字

  • 了解 GAN 最受欢迎的变种——深度卷积 GAN

  • 在 TensorFlow 中实现深度卷积 GAN,并用它生成人脸

  • 了解 GANs 的进一步修改和应用

介绍

生成模型是深度学*模型中的一个令人兴奋的新分支,它通过无监督学*进行学*。其主要思想是生成具有与给定训练数据相同分布的新样本;例如,一个在手写数字上训练的网络可以生成新的数字,这些数字不在数据集中,但与其相似。从形式上讲,我们可以说,如果训练数据遵循分布 Pdata,那么生成模型的目标是估计概率密度函数 Pmodel,该函数与 Pdata 相似。

生成模型可以分为两种类型:

  • 显式生成模型:在这些模型中,概率密度函数 Pmodel 被显式定义并求解。密度函数可能是可处理的,像 PixelRNN/CNN 这种情况,或者是密度函数的*似,像 VAE 这种情况。

  • 隐式生成模型:在这些模型中,网络学*从 Pmodel 中生成一个样本,而无需显式地定义它。GANs 就是这种类型的生成模型的一个例子。

在本章中,我们将探讨 VAE(一种显式生成模型)和 GAN(一种隐式生成模型)。生成模型可以有效地生成逼真的样本,并可用于执行超分辨率、上色等任务。对于时间序列数据,我们甚至可以用它们进行模拟和规划。最后,生成模型还可以帮助我们理解数据的潜在表示。

使用 VAEs 生成图像

在第四章《物联网深度学*》中,你应该对自编码器及其功能有所了解。VAE 是一种自编码器;在这里,我们保留了(训练过的)解码器部分,可以通过输入随机的潜在特征z来生成类似于训练数据的样本。现在,如果你还记得,在自编码器中,编码器的作用是生成低维特征,z

自编码器的架构

VAE 关注的是从潜在特征z中找到似然函数 p(x):

这是一个难以处理的密度函数,无法直接优化;相反,我们通过使用简单的高斯先验 p(z) 并使编码器解码器网络具有概率性质来获得下界:

VAE 的架构

这使我们能够定义一个可处理的对数似然的下界,如下所示:

在前述的公式中,θ表示解码器网络的参数,φ表示编码器网络的参数。通过最大化这个下界,网络得以训练:

下界中的第一个项负责输入数据的重构,第二个项则用于使*似后验分布接*先验分布。训练完成后,编码器网络作为识别或推理网络,而解码器网络则作为生成器。

你可以参考 Diederik P Kingma 和 Max Welling 在 2014 年 ICLR 会议上发表的论文《Auto-Encoding Variational Bayes》(arxiv.org/abs/1312.6114)中的详细推导。

TensorFlow 中的 VAE

现在让我们来看一下 VAE 的实际应用。在这个示例代码中,我们将使用标准的 MNIST 数据集,并训练一个 VAE 来生成手写数字。由于 MNIST 数据集较为简单,编码器和解码器网络将仅由全连接层组成;这将帮助我们集中精力于 VAE 架构。如果你计划生成复杂的图像(例如 CIFAR-10),你需要将编码器和解码器网络修改为卷积和反卷积网络:

  1. 与之前的所有情况一样,第一步是导入所有必要的模块。在这里,我们将使用 TensorFlow 的高级 API,tf.contrib,来构建全连接层。注意,这样我们就避免了单独声明每一层的权重和偏置的麻烦:
import numpy as np
import tensorflow as tf

import matplotlib.pyplot as plt
%matplotlib inline

from tensorflow.contrib.layers import fully_connected
  1. 我们读取数据。MNIST 数据集可以在 TensorFlow 教程中找到,所以我们直接从那里获取:
# Load MNIST data in a format suited for tensorflow.
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
n_samples = mnist.train.num_examples
n_input = mnist.train.images[0].shape[0]
  1. 我们定义了VariationalAutoencoder类;这个类是核心代码。它包含用于定义编码器和解码器网络的方法。编码器生成潜在特征z的均值和方差,分别称为z_muz_sigma。利用这些,取一个样本Z。然后,潜在特征z被传递给解码器网络生成x_hat。网络通过 Adam 优化器最小化重建损失和潜在损失的总和。该类还定义了重建、生成、转换(到潜在空间)和单步训练的方法:
class VariationalAutoencoder(object):
    def __init__(self,n_input, n_z,
        learning_rate=0.001, batch_size=100):
        self.batch_size = batch_size
        self.n_input = n_input
        self.n_z = n_z

        # Place holder for the input 
        self.x = tf.placeholder(tf.float32, shape = [None, n_input])

        # Use Encoder Network to determine mean and 
        # (log) variance of Gaussian distribution in the latent space
        self.z_mean, self.z_log_sigma_sq = self._encoder_network()
        # Draw a sample z from Gaussian distribution
        eps = tf.random_normal((self.batch_size, n_z), 0, 1, dtype=tf.float32)
        # z = mu + sigma*epsilon
        self.z = tf.add(self.z_mean,tf.multiply(tf.sqrt(tf.exp(self.z_log_sigma_sq)), eps))
        # Use Decoder network to determine mean of
        # Bernoulli distribution of reconstructed input
        self.x_hat = self._decoder_network()

        # Define loss function based variational upper-bound and 
        # corresponding optimizer
        # define generation loss
        reconstruction_loss = \
            -tf.reduce_sum(self.x * tf.log(1e-10 + self.x_hat)
            + (1-self.x) * tf.log(1e-10 + 1 - self.x_hat), 1)
        self.reconstruction_loss = tf.reduce_mean(reconstruction_loss)

        latent_loss = -0.5 * tf.reduce_sum(1 + self.z_log_sigma_sq 
            - tf.square(self.z_mean)- tf.exp(self.z_log_sigma_sq), 1)
        self.latent_loss = tf.reduce_mean(latent_loss)
        self.cost = tf.reduce_mean(reconstruction_loss + latent_loss) 
        # average over batch
        # Define the optimizer
        self.optimizer = tf.train.AdamOptimizer(learning_rate).minimize(self.cost)

        # Initializing the tensor flow variables
        init = tf.global_variables_initializer()
        # Launch the session
        self.sess = tf.InteractiveSession()
        self.sess.run(init)

    # Create encoder network
    def _encoder_network(self):
        # Generate probabilistic encoder (inference network), which
        # maps inputs onto a normal distribution in latent space.
        layer_1 = fully_connected(self.x,500,activation_fn=tf.nn.softplus) 
        layer_2 = fully_connected(layer_1, 500, activation_fn=tf.nn.softplus) 
        z_mean = fully_connected(layer_2,self.n_z, activation_fn=None)
        z_log_sigma_sq = fully_connected(layer_2, self.n_z, activation_fn=None)
        return (z_mean, z_log_sigma_sq)

    # Create decoder network
    def _decoder_network(self):
        # Generate probabilistic decoder (generator network), which
        # maps points in the latent space onto a Bernoulli distribution in the data space.
        layer_1 = fully_connected(self.z,500,activation_fn=tf.nn.softplus) 
        layer_2 = fully_connected(layer_1, 500, activation_fn=tf.nn.softplus) 
        x_hat = fully_connected(layer_2, self.n_input, activation_fn=tf.nn.sigmoid)

        return x_hat

    def single_step_train(self, X):
        _,cost,recon_loss,latent_loss = self.sess.run([self.optimizer,         self.cost,self.reconstruction_loss,self.latent_loss],feed_dict={self.x: X})
        return cost, recon_loss, latent_loss

    def transform(self, X):
        """Transform data by mapping it into the latent space."""
        # Note: This maps to mean of distribution, we could alternatively
        # sample from Gaussian distribution
        return self.sess.run(self.z_mean, feed_dict={self.x: X})

    def generate(self, z_mu=None):
        """ Generate data by sampling from latent space.

        If z_mu isn't None, data for this point in latent space is
        generated. Otherwise, z_mu is drawn from prior in latent 
        space. 
        """
        if z_mu is None:
            z_mu = np.random.normal(size=n_z)
            # Note: This maps to mean of distribution, we could alternatively    
            # sample from Gaussian distribution
        return self.sess.run(self.x_hat,feed_dict={self.z: z_mu})

    def reconstruct(self, X):
        """ Use VAE to reconstruct given data. """
        return self.sess.run(self.x_hat, feed_dict={self.x: X})
  1. 在所有成分准备好之后,我们开始训练我们的 VAE。我们通过train函数来完成这项任务:
def train(n_input,n_z, learning_rate=0.001,
    batch_size=100, training_epochs=10, display_step=5):
    vae = VariationalAutoencoder(n_input,n_z, 
        learning_rate=learning_rate, 
        batch_size=batch_size)
    # Training cycle
    for epoch in range(training_epochs):
        avg_cost, avg_r_loss, avg_l_loss = 0., 0., 0.
        total_batch = int(n_samples / batch_size)
        # Loop over all batches
        for i in range(total_batch):
            batch_xs, _ = mnist.train.next_batch(batch_size)
            # Fit training using batch data
            cost,r_loss, l_loss = vae.single_step_train(batch_xs)
            # Compute average loss
            avg_cost += cost / n_samples * batch_size
            avg_r_loss += r_loss / n_samples * batch_size
            avg_l_loss += l_loss / n_samples * batch_size
        # Display logs per epoch step
        if epoch % display_step == 0:
            print("Epoch: {:4d} cost={:.4f} Reconstruction loss = {:.4f} Latent Loss = {:.4f}".format(epoch,avg_cost,avg_r_loss,avg_l_loss))
     return vae
  1. 在以下截图中,您可以看到潜在空间大小为 10 的 VAE 的重建数字(左)和生成的手写数字(右):

  1. 正如之前讨论的,编码器网络将输入空间的维度降低。为了更清楚地说明这一点,我们将潜在空间的维度降为 2。以下是二维 z 空间中每个标签的分离情况:

  1. 来自潜在空间维度为 2 的 VAE 的重建和生成数字如下所示:

从前面的截图(右)可以看到有趣的一点是,改变二维z的值会导致不同的笔画和不同的数字。完整的代码可以在 GitHub 上的Chapter 07文件夹中的VariationalAutoEncoders_MNIST.ipynb文件里找到:

tf.contrib.layers.fully_connected(
    inputs,
    num_outputs,
    activation_fn=tf.nn.relu,
    normalizer_fn=None,
    normalizer_params=None,
    weights_initializer=intializers.xavier_intializer(),
    weights_regularizer= None, 
    biases_initializer=tf.zeros_intializer(),
    biases_regularizer=None,
    reuse=None,
    variables_collections=None,
    outputs_collections=None,
    trainable=True,
    scope=None
)

contrib层是 TensorFlow 中包含的一个更高级的包。它提供了构建神经网络层、正则化器、总结等操作。在前面的代码中,我们使用了tf.contrib.layers.fully_connected()操作,定义在tensorflow/contrib/layers/python/layers/layers.py中,它添加了一个全连接层。默认情况下,它创建代表全连接互连矩阵的权重,默认使用 Xavier 初始化。同时,它也创建了初始化为零的偏置。它还提供了选择归一化和激活函数的选项。

GANs

GANs 是隐式生成网络。在 Quora 的一个会议中,Facebook 人工智能研究总监兼纽约大学教授 Yann LeCun 将 GANs 描述为过去 10 年中机器学*领域最有趣的想法。目前,关于 GAN 的研究非常活跃。过去几年的主要 AI/ML 会议上,报道了大多数与 GAN 相关的论文。

GAN 由 Ian J. Goodfellow 和 Yoshua Bengio 在 2014 年的论文《生成对抗网络》(arxiv.org/abs/1406.2661)中提出。它们的灵感来自于两人对抗的游戏场景。就像游戏中的两个玩家一样,GAN 中有两个网络——一个称为判别网络,另一个称为生成网络——彼此对抗。生成网络试图生成与输入数据相似的数据,而判别网络则必须识别它所看到的数据是来自真实数据还是伪造数据(即由生成器生成)。每次判别器发现真实输入和伪造数据之间的分布差异时,生成器都会调整其权重以减少这种差异。总结来说,判别网络试图学*伪造数据和真实数据之间的边界,而生成网络则试图学*训练数据的分布。随着训练的结束,生成器学会生成与输入数据分布完全相似的图像,判别器则无法再区分二者。GAN 的总体架构如下:

GAN 的架构

现在让我们深入探讨 GAN 是如何学*的。判别器和生成器轮流进行学*。学*过程可以分为两个步骤:

  1. 在这里,判别器D(x),进行学*。生成器G(z),用于从随机噪声z(它遵循某种先验分布P(z))生成假图像生成器生成的假图像和来自训练数据集的真实图像都被输入到判别器,然后判别器执行监督学*,试图将假图像与真实图像区分开。如果Pdata 是训练数据集的分布,那么判别器网络试图最大化其目标,使得当输入数据为真实数据时,D(x)接* 1,当输入数据为假数据时,接* 0。这可以通过对以下目标函数执行梯度上升来实现:

  1. 在下一步,生成器网络进行学*。它的目标是欺骗判别器网络,让它认为生成的G(z)是真实的,也就是说,迫使D(G(z))接* 1。为了实现这一目标,生成器网络最小化以下目标:

这两个步骤会依次重复进行。一旦训练结束,判别器将无法再区分真实数据和伪造数据,生成器也变得非常擅长生成与训练数据非常相似的数据。嗯,说起来容易做起来难:当你尝试使用 GAN 时,你会发现训练并不是很稳定。这是一个开放的研究问题,已经提出了许多 GAN 的变种来解决这一问题。

在 TensorFlow 中实现一个基础的 GAN

在这一部分,我们将编写一个 TensorFlow 代码来实现一个 GAN,正如我们在上一节所学的那样。我们将为判别器和生成器使用简单的 MLP 网络。为了简便起见,我们将使用 MNIST 数据集:

  1. 像往常一样,第一步是添加所有必要的模块。由于我们需要交替访问和训练生成器和判别器的参数,我们将为了清晰起见在当前代码中定义我们的权重和偏置。使用 Xavier 初始化权重并将偏置初始化为零总是更好的做法。因此,我们还从 TensorFlow 导入执行 Xavier 初始化的方法:from tensorflow.contrib.layers import xavier_initializer
# import the necessaey modules
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import os
from tensorflow.contrib.layers import xavier_initializer
%matplotlib inline
  1. 让我们读取数据并定义超参数:
# Load data
from tensorflow.examples.tutorials.mnist import input_data
data = input_data.read_data_sets('MNIST_data', one_hot=True)

# define hyperparameters
batch_size = 128
Z_dim = 100
im_size = 28
h_size=128
learning_rate_D = .0005
learning_rate_G = .0006
  1. 我们定义生成器和判别器的训练参数,并为输入X和潜在变量Z定义占位符:
#Create Placeholder for input X and random noise Z
X = tf.placeholder(tf.float32, shape=[None, im_size*im_size])
Z = tf.placeholder(tf.float32, shape=[None, Z_dim])
initializer=xavier_initializer()

# Define Discriminator and Generator training variables
#Discriminiator
D_W1 = tf.Variable(initializer([im_size*im_size, h_size]))
D_b1 = tf.Variable(tf.zeros(shape=[h_size]))

D_W2 = tf.Variable(initializer([h_size, 1]))
D_b2 = tf.Variable(tf.zeros(shape=[1]))

theta_D = [D_W1, D_W2, D_b1, D_b2]

#Generator
G_W1 = tf.Variable(initializer([Z_dim, h_size]))
G_b1 = tf.Variable(tf.zeros(shape=[h_size]))

G_W2 = tf.Variable(initializer([h_size, im_size*im_size]))
G_b2 = tf.Variable(tf.zeros(shape=[im_size*im_size]))

theta_G = [G_W1, G_W2, G_b1, G_b2]
  1. 既然我们已经设置了占位符和权重,我们定义一个函数来生成来自Z的随机噪声。在这里,我们使用均匀分布来生成噪声;有些人也尝试过使用高斯噪声——要做到这一点,你只需要将随机函数从uniform改为normal
def sample_Z(m, n):
    return np.random.uniform(-1., 1., size=[m, n])
  1. 我们构建判别器和生成器网络:
def generator(z):
    """ Two layer Generator Network Z=>128=>784 """
    G_h1 = tf.nn.relu(tf.matmul(z, G_W1) + G_b1)
    G_log_prob = tf.matmul(G_h1, G_W2) + G_b2
    G_prob = tf.nn.sigmoid(G_log_prob)
    return G_prob

def discriminator(x):
    """ Two layer Discriminator Network X=>128=>1 """
    D_h1 = tf.nn.relu(tf.matmul(x, D_W1) + D_b1)
    D_logit = tf.matmul(D_h1, D_W2) + D_b2
    D_prob = tf.nn.sigmoid(D_logit)
    return D_prob, D_logit
  1. 我们还需要一个辅助函数来绘制生成的手写数字。以下函数会将生成的 25 个样本以 5×5 的网格展示:
def plot(samples):
    """function to plot generated samples"""
    fig = plt.figure(figsize=(10, 10))
    gs = gridspec.GridSpec(5, 5)
    gs.update(wspace=0.05, hspace=0.05)
    for i, sample in enumerate(samples):
        ax = plt.subplot(gs[i])
        plt.axis('off')
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        ax.set_aspect('equal')
        plt.imshow(sample.reshape(28, 28), cmap='gray')
    return fig
  1. 现在,我们定义 TensorFlow 操作来生成生成器的样本,并从判别器获取对假输入和真实输入数据的预测:
G_sample = generator(Z)
D_real, D_logit_real = discriminator(X)
D_fake, D_logit_fake = discriminator(G_sample)
  1. 接下来,我们为生成器和判别器网络定义交叉熵损失,并交替最小化它们,同时保持其他权重参数冻结:
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_real, labels=tf.ones_like(D_logit_real)))
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, labels=tf.zeros_like(D_logit_fake)))
D_loss = D_loss_real + D_loss_fake
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, labels=tf.ones_like(D_logit_fake)))

D_solver = tf.train.AdamOptimizer(learning_rate=learning_rate_D).minimize(D_loss, var_list=theta_D)
G_solver = tf.train.AdamOptimizer(learning_rate=learning_rate_G).minimize(G_loss, var_list=theta_G)
  1. 最后,让我们在 TensorFlow 会话中执行训练:
sess = tf.Session()
sess.run(tf.global_variables_initializer())
GLoss = []
DLoss = []
if not os.path.exists('out/'):
    os.makedirs('out/')

for it in range(100000):
    if it % 100 == 0:
        samples = sess.run(G_sample, feed_dict={Z: sample_Z(25, Z_dim)})
        fig = plot(samples)
        plt.savefig('out/{}.png'.format(str(it).zfill(3)), bbox_inches='tight')
        plt.close(fig)
    X_mb, _ = data.train.next_batch(batch_size)
    _, D_loss_curr = sess.run([D_solver, D_loss], feed_dict={X: X_mb, Z: sample_Z(batch_size, Z_dim)})
    _, G_loss_curr = sess.run([G_solver, G_loss], feed_dict={Z: sample_Z(batch_size, Z_dim)})
    GLoss.append(G_loss_curr)
    DLoss.append(D_loss_curr)
    if it % 100 == 0:
        print('Iter: {} D loss: {:.4} G_loss: {:.4}'.format(it,D_loss_curr, G_loss_curr))

print('Done')
  1. 在以下屏幕截图中,你可以看到生成和判别网络的损失是如何变化的:

生成和判别网络的损失

  1. 让我们还看看在不同的训练轮次中生成的手写数字:

手写数字

尽管手写数字已经足够好,但我们可以看到仍有很多改进的空间。研究人员用来稳定性能的一些方法如下:

  • 将输入图像从(0,1)标准化到(-1,1)。而且,生成器最终输出的激活函数不再使用 sigmoid,而是使用双曲正切激活函数。

  • 我们可以通过最大化损失log D来替代最小化生成器损失log 1-D;这可以通过在训练生成器时反转标签来实现,例如(将真实标签转为假标签,假标签转为真实标签)。

  • 另一种方法是存储以前生成的图像,并通过从中随机选择来训练判别器。(没错,你猜对了——这类似于我们在第六章《物联网的强化学*》中学到的经验回放缓冲区。)

  • 人们还尝试过只有当生成器或判别器的损失超过某个阈值时才更新它们。

  • 在判别器和生成器的隐藏层中,使用 Leaky ReLU 激活函数,而不是 ReLU。

深度卷积生成对抗网络

2016 年,Alec Radford 等人 提出了 GAN 的一种变体,叫做 深度卷积生成对抗网络 (DCGAN)。 (完整论文链接:arxiv.org/abs/1511.06434。)他们将 MLP 层替换为卷积层,并在生成器和判别器网络中都加入了批量归一化。我们将在这里使用名人图像数据集实现 DCGAN。你可以从 mmlab.ie.cuhk.edu.hk/projects/CelebA.html 下载 ZIP 文件 img_align_celeba.zip。我们将利用我们在第二章《物联网的数据访问与分布式处理》中创建的 loader_celebA.py 文件来解压并读取图像:

  1. 我们将导入所有需要的模块的语句:
import loader
import os
from glob import glob
import numpy as np
from matplotlib import pyplot
import tensorflow as tf
%matplotlib inline
  1. 我们使用 loader_celebA.py 解压 img_align_celeba.zip。由于图像数量非常庞大,我们使用该文件中定义的 get_batches 函数来生成用于训练网络的批次:
loader.download_celeb_a()

# Let's explore the images
data_dir = os.getcwd()
test_images = loader.get_batch(glob(os.path.join(data_dir, 'celebA/*.jpg'))[:10], 56, 56)
pyplot.imshow(loader.plot_images(test_images))

在接下来的内容中,您可以看到数据集的图像:

  1. 我们定义了判别器网络。它由三个卷积层组成,分别使用 64128256 个 5×5 大小的滤波器。前两个层使用 2 的步幅,第三个卷积层使用 1 的步幅。所有三个卷积层都使用 leakyReLU 作为激活函数。每个卷积层后面都跟有一个批量归一化层。第三个卷积层的结果会被拉平,并传递到最后一个全连接(密集)层,该层使用 sigmoid 激活函数:
def discriminator(images, reuse=False):
    """
    Create the discriminator network
    """
    alpha = 0.2

    with tf.variable_scope('discriminator', reuse=reuse):
        # using 4 layer network as in DCGAN Paper

        # First convolution layer
        conv1 = tf.layers.conv2d(images, 64, 5, 2, 'SAME')
        lrelu1 = tf.maximum(alpha * conv1, conv1)

        # Second convolution layer
        conv2 = tf.layers.conv2d(lrelu1, 128, 5, 2, 'SAME')
        batch_norm2 = tf.layers.batch_normalization(conv2, training=True)
        lrelu2 = tf.maximum(alpha * batch_norm2, batch_norm2)

        # Third convolution layer
        conv3 = tf.layers.conv2d(lrelu2, 256, 5, 1, 'SAME')
        batch_norm3 = tf.layers.batch_normalization(conv3, training=True)
        lrelu3 = tf.maximum(alpha * batch_norm3, batch_norm3)

        # Flatten layer
        flat = tf.reshape(lrelu3, (-1, 4*4*256))

        # Logits
        logits = tf.layers.dense(flat, 1)

        # Output
        out = tf.sigmoid(logits)

        return out, logits
  1. 生成器网络是判别器的反向;生成器的输入首先会传递给一个包含 2×2×512 单元的全连接层。全连接层的输出会被重塑,以便我们将其传递给卷积堆栈。我们使用 tf.layers.conv2d_transpose() 方法来获取转置卷积的输出。生成器有三个转置卷积层。除了最后一个卷积层外,所有层都使用 leakyReLU 作为激活函数。最后一个转置卷积层使用双曲正切激活函数,以确保输出位于 (-11) 的范围内:
def generator(z, out_channel_dim, is_train=True):
    """
    Create the generator network
    """
    alpha = 0.2

    with tf.variable_scope('generator', reuse=False if is_train==True else True):
        # First fully connected layer
        x_1 = tf.layers.dense(z, 2*2*512)

        # Reshape it to start the convolutional stack
        deconv_2 = tf.reshape(x_1, (-1, 2, 2, 512))
        batch_norm2 = tf.layers.batch_normalization(deconv_2, training=is_train)
        lrelu2 = tf.maximum(alpha * batch_norm2, batch_norm2)

        # Deconv 1
        deconv3 = tf.layers.conv2d_transpose(lrelu2, 256, 5, 2, padding='VALID')
        batch_norm3 = tf.layers.batch_normalization(deconv3, training=is_train)
        lrelu3 = tf.maximum(alpha * batch_norm3, batch_norm3)

        # Deconv 2
        deconv4 = tf.layers.conv2d_transpose(lrelu3, 128, 5, 2, padding='SAME')
        batch_norm4 = tf.layers.batch_normalization(deconv4, training=is_train)
        lrelu4 = tf.maximum(alpha * batch_norm4, batch_norm4)

        # Output layer
        logits = tf.layers.conv2d_transpose(lrelu4, out_channel_dim, 5, 2, padding='SAME')

        out = tf.tanh(logits)

        return out
  1. 我们定义了计算模型损失的函数,该函数定义了生成器和判别器的损失,并返回它们:
def model_loss(input_real, input_z, out_channel_dim):
    """
    Get the loss for the discriminator and generator
    """

    label_smoothing = 0.9

    g_model = generator(input_z, out_channel_dim)
    d_model_real, d_logits_real = discriminator(input_real)
    d_model_fake, d_logits_fake = discriminator(g_model, reuse=True)

    d_loss_real = tf.reduce_mean(
        tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_real,
                                                labels=tf.ones_like(d_model_real) * label_smoothing))
    d_loss_fake = tf.reduce_mean(
        tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake,
                                                labels=tf.zeros_like(d_model_fake)))

    d_loss = d_loss_real + d_loss_fake

    g_loss = tf.reduce_mean(
        tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake,
                                                labels=tf.ones_like(d_model_fake) * label_smoothing))

    return d_loss, g_loss
  1. 接下来,我们需要定义优化器,以便判别器和生成器能够顺序学*。为此,我们利用tf.trainable_variables()获取所有训练变量的列表,然后首先优化仅判别器的训练变量,再优化生成器的训练变量:
def model_opt(d_loss, g_loss, learning_rate, beta1):
    """
    Get optimization operations
    """
    t_vars = tf.trainable_variables()
    d_vars = [var for var in t_vars if var.name.startswith('discriminator')]
    g_vars = [var for var in t_vars if var.name.startswith('generator')]

    # Optimize
    with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)): 
        d_train_opt = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(d_loss, var_list=d_vars)
        g_train_opt = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(g_loss, var_list=g_vars)

    return d_train_opt, g_train_opt
  1. 现在,我们已经具备了训练 DCGAN 所需的所有要素。为了更好地观察生成器的学*过程,我们定义了一个辅助函数,来显示生成器网络在学*过程中生成的图像:
def generator_output(sess, n_images, input_z, out_channel_dim):
    """
    Show example output for the generator
    """
    z_dim = input_z.get_shape().as_list()[-1]
    example_z = np.random.uniform(-1, 1, size=[n_images, z_dim])

    samples = sess.run(
        generator(input_z, out_channel_dim, False),
        feed_dict={input_z: example_z})

    pyplot.imshow(loader.plot_images(samples))
    pyplot.show()
  1. 最后,进入训练部分。在这里,我们使用之前定义的ops来训练 DCGAN,并将图像按批次输入到网络中:
def train(epoch_count, batch_size, z_dim, learning_rate, beta1, get_batches, data_shape, data_files):
    """
    Train the GAN
    """
    w, h, num_ch = data_shape[1], data_shape[2], data_shape[3]
    X = tf.placeholder(tf.float32, shape=(None, w, h, num_ch), name='input_real') 
    Z = tf.placeholder(tf.float32, (None, z_dim), name='input_z')
    #model_inputs(data_shape[1], data_shape[2], data_shape[3], z_dim)
    D_loss, G_loss = model_loss(X, Z, data_shape[3])
    D_solve, G_solve = model_opt(D_loss, G_loss, learning_rate, beta1)

    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        train_loss_d = []
        train_loss_g = []
        for epoch_i in range(epoch_count):
            num_batch = 0
            lossD, lossG = 0,0
            for batch_images in get_batches(batch_size, data_shape, data_files):

                # values range from -0.5 to 0.5 so we scale to range -1, 1
                batch_images = batch_images * 2
                num_batch += 1

                batch_z = np.random.uniform(-1, 1, size=(batch_size, z_dim))

                _,d_loss = sess.run([D_solve,D_loss], feed_dict={X: batch_images, Z: batch_z})
                _,g_loss = sess.run([G_solve,G_loss], feed_dict={X: batch_images, Z: batch_z})

                lossD += (d_loss/batch_size)
                lossG += (g_loss/batch_size)
                if num_batch % 500 == 0:
                    # After every 500 batches
                    print("Epoch {}/{} For Batch {} Discriminator Loss: {:.4f} Generator Loss: {:.4f}".
                          format(epoch_i+1, epochs, num_batch, lossD/num_batch, lossG/num_batch))

                    generator_output(sess, 9, Z, data_shape[3])
            train_loss_d.append(lossD/num_batch)
            train_loss_g.append(lossG/num_batch)

    return train_loss_d, train_loss_g
  1. 现在我们来定义数据的参数并进行训练:
# Data Parameters
IMAGE_HEIGHT = 28
IMAGE_WIDTH = 28
data_files = glob(os.path.join(data_dir, 'celebA/*.jpg'))

#Hyper parameters
batch_size = 16
z_dim = 100
learning_rate = 0.0002
beta1 = 0.5
epochs = 2
shape = len(data_files), IMAGE_WIDTH, IMAGE_HEIGHT, 3
with tf.Graph().as_default():
    Loss_D, Loss_G = train(epochs, batch_size, z_dim, learning_rate, beta1, loader.get_batches, shape, data_files)

每处理一个批次,你可以看到生成器的输出在不断改善:

DCGAN 生成器输出随着学*的进展而改善

GAN 的变体及其酷炫应用

在过去几年里,已经提出了大量的 GAN 变体。你可以通过github.com/hindupuravinash/the-gan-zoo访问完整的 GAN 变体列表。在这一部分,我们将列出一些更受欢迎和成功的变体。

Cycle GAN

2018 年初,伯克利 AI 研究实验室发表了一篇名为《使用循环一致对抗网络进行未配对图像到图像的转换》的论文(arXiv 链接:arxiv.org/pdf/1703.10593.pdf)。这篇论文的特别之处不仅在于它提出了一种新的架构——CycleGAN,并且具有更好的稳定性,还因为它展示了这种架构可以用于复杂的图像转换。以下图示展示了 CycleGAN 的架构,两个部分分别突出显示了在计算两个对抗损失时起作用的生成器判别器

CycleGAN 的架构

CycleGAN 由两个 GAN 组成。它们分别在两个不同的数据集上进行训练,xPdata 和yPdata。生成器被训练来执行映射,即G[A]: x→yG[B]: y→x。每个判别器的训练目标是能够区分图像x和变换后的图像GB,从而得出两个变换的对抗损失函数,定义如下:

第二个步骤如下:

两个 GAN 的生成器以循环方式连接在一起,这样如果一个生成器的输出被输入到另一个生成器中,并且将对应的输出反馈到第一个生成器,我们就会得到相同的数据。我们通过一个例子来解释这个过程;假设生成器 AG[A])输入一张图像 x,那么输出就是转换后的图像 GA。这个转换后的图像现在输入到生成器 BG[B]),GB)≈x,结果应该是原始图像 x。类似地,我们也会有 GA≈y。这一过程通过引入循环损失项得以实现:

因此,网络的目标函数如下:

在这里,λ 控制着两个目标之间的相对重要性。它们还在经验缓冲区中保留了先前的图像,用于训练判别器。在下面的截图中,你可以看到论文中报告的从 CycleGANs 获得的一些结果:

CycleGAN 的结果(来自原始论文)

作者展示了 CycleGANs 可以用于以下几种情况:

  • 图像转换:例如将马变成斑马,反之亦然。

  • 分辨率增强:当 CycleGAN 用包含低分辨率和超分辨率图像的数据集进行训练时,能够在输入低分辨率图像时执行超分辨率。

  • 风格迁移:给定一张图像,可以将其转换为不同的绘画风格

GANs 的应用

GANs 确实是很有趣的网络;除了你已经看到的应用,GANs 还在许多其他令人兴奋的应用中得到了探索。接下来,我们列举了一些:

  • 音乐生成:MIDINet 是一种卷积 GAN,已被证明可以生成旋律。你可以参考这篇论文:arxiv.org/pdf/1703.10847.pdf

  • 医学异常检测:AnoGAN 是一种由 Thomas Schlegl 等人展示的 DCGAN,用于学*正常解剖变异性的流形。他们成功地训练了网络,能够标记视网膜光学相干断层扫描图像中的异常。如果你对这项工作感兴趣,可以在 arXiv 上阅读相关论文:arxiv.org/pdf/1703.05921.pdf

  • 使用 GANs 进行人脸向量运算:在 Indico Research 和 Facebook 共同研究的论文中,他们展示了使用 GANs 进行图像运算的可能性。例如,戴眼镜的男人不戴眼镜的男人 + 不戴眼镜的女人 = 戴眼镜的女人。这是一篇有趣的论文,你可以在 Arxiv 上阅读更多内容:arxiv.org/pdf/1511.06434.pdf

  • 文本到图像合成:生成对抗网络(GAN)已被证明可以根据人类书写的文本描述生成鸟类和花卉的图像。该模型使用了 DCGAN,并结合了混合字符级卷积递归网络。该工作的详细信息可以在论文《生成对抗文本到图像合成》中找到。论文的链接是arxiv.org/pdf/1605.05396.pdf

总结

这是一个有趣的章节,希望你在阅读时能像我写这篇文章时一样享受其中。目前这是研究的热点话题。本章介绍了生成模型及其分类,即隐式生成模型和显式生成模型。首先介绍的生成模型是变分自编码器(VAE);它们是显式生成模型,旨在估计密度函数的下界。我们在 TensorFlow 中实现了 VAE,并用其生成了手写数字。

本章随后转向了一个更为流行的显式生成模型:生成对抗网络(GAN)。详细解释了 GAN 架构,特别是判别器网络和生成器网络如何相互竞争。我们使用 TensorFlow 实现了一个 GAN,用于生成手写数字。本章还介绍了 GAN 的成功变种:DCGAN,并实现了一个 DCGAN,用于生成名人图像。本章还介绍了最*提出的 GAN 变种——CycleGAN 的架构细节,以及它的一些酷应用。

本章标志着本书第一部分的结束。到目前为止,我们集中讨论了不同的机器学*(ML)和深度学*(DL)模型,这些模型是我们理解数据并用于预测/分类以及其他任务所需的。从下一章开始,我们将更多地讨论数据本身,以及在当前物联网驱动的环境中,我们如何处理这些数据。

在下一章,我们将探讨分布式处理,这是处理大量数据时的必需技术,并介绍两种提供分布式处理的平台。

第八章:物联网的分布式 AI

分布式计算环境的进步和互联网的全球普及导致了分布式人工智能DAI)的出现。在本章中,我们将了解两个框架,一个是 Apache 的机器学*库MLlib),另一个是 H2O.ai,它们都为大规模、流式数据提供分布式和可扩展的机器学*ML)。本章将以 Apache 的 Spark 介绍开始,它是事实上的分布式数据处理系统。本章将涵盖以下主题:

  • Spark 及其在分布式数据处理中的重要性

  • 理解 Spark 架构

  • 学* MLlib

  • 在深度学*管道中使用 MLlib

  • 深入了解 H2O.ai 平台

介绍

物联网(IoT)系统会生成大量数据;虽然在许多情况下可以从容分析数据,但对于某些任务(如安全、欺诈检测等),这种延迟是不可接受的。在这种情况下,我们需要的是一种在指定时间内处理大量数据的方法——解决方案是 DAI,多个集群中的机器以分布式方式处理大数据(数据并行)和/或训练深度学*模型(模型并行)。有许多方式可以执行 DAI,大多数方法是建立在或围绕 Apache Spark 的基础上。Apache Spark 于 2010 年发布,并采用 BSD 许可协议,如今它是大数据领域最大的开源项目。它帮助用户创建一个快速且通用的集群计算系统。

Spark 运行在 Java 虚拟机上,使得它可以在任何安装了 Java 的机器上运行,无论是笔记本电脑还是集群。它支持多种编程语言,包括 Python、Scala 和 R。许多深度学*框架和 API 围绕 Spark 和 TensorFlow 构建,旨在简化分布式人工智能(DAI)任务,例如TensorFlowOnSparkTFoS)、Spark MLlib、SparkDl 和 Hydrogen Sparkling(结合了 H2O.ai 和 Spark)。

Spark 组件

Spark 使用主从架构,其中一个中央协调器(称为Spark 驱动程序)和多个分布式工作节点(称为Spark 执行器)。驱动程序进程创建一个SparkContext对象,并将用户应用程序分解成更小的执行单元(任务)。这些任务由工作节点执行。工作节点之间的资源由集群 管理器管理。下图展示了 Spark 的工作原理:

Spark 的工作原理

现在让我们逐一了解 Spark 的不同组件。下图展示了构成 Spark 的基本组件:

构成 Spark 的组件

让我们简要看看在本章中我们将使用的一些组件,如下所示:

  • 弹性分布式数据集弹性分布式数据集RDDs)是 Spark 中的主要 API。它们表示一个不可变的、分区的数据集合,可以并行操作。更高层的 API 如 DataFrames 和 DataSets 是建立在 RDD 之上的。

  • 分布式变量:Spark 有两种类型的分布式变量:广播变量和累加器。它们由用户定义的函数使用。累加器用于将所有执行器中的信息聚合成共享结果。广播变量则是集群中共享的变量。

  • DataFrames:它是一个分布式的数据集合,非常类似于 pandas 中的 DataFrame。它们可以从各种文件格式中读取,并使用单个命令对整个 DataFrame 执行操作。它们分布在集群中。

  • :Spark 内置了用于 MLlib 和图形处理(GraphX)的库。在本章中,我们将使用基于 Spark 框架的 MLlib 和 SparkDl。我们将学*如何应用它们来进行机器学*预测。

Spark 是一个大话题,本书无法提供有关 Spark 的更多细节。我们建议感兴趣的读者参考 Spark 文档:spark.apache.org/docs/latest/index.html

Apache MLlib

Apache Spark MLlib 为机器学*提供了强大的计算环境。它提供了一个大规模分布式架构,使得运行机器学*模型更加快速高效。这还不是全部;它是开源的,并且拥有一个不断壮大的活跃社区,持续改进并提供最新的功能。它提供了流行的机器学*算法的可扩展实现,涵盖以下算法:

  • 分类:逻辑回归、线性支持向量机、朴素贝叶斯

  • 回归:广义线性回归

  • 协同过滤:交替最小二乘法

  • 聚类:K 均值

  • 分解:奇异值分解和主成分分析

它已被证明比 Hadoop MapReduce 更快。我们可以使用 Java、Scala、R 或 Python 编写应用程序。它还可以轻松与 TensorFlow 集成。

MLlib 中的回归

Spark MLlib 内置了回归的方法。为了能够使用 Spark 的内置方法,您需要在您的集群(独立集群或分布式集群)上安装pyspark。可以使用以下方式进行安装:

pip install pyspark

MLlib 库具有以下回归方法:

  • 线性回归:我们在前面的章节中已经学*了线性回归;我们可以使用在pyspark.ml.regression中定义的LinearRegression类来使用这种方法。默认情况下,它使用最小化平方误差和正则化。它支持 L1 和 L2 正则化以及它们的组合。

  • 广义线性回归:Spark MLlib 提供了指数族分布的子集,如高斯分布、泊松分布等。回归是通过GeneralizedLinearRegression类实例化的。

  • 决策树回归:可以使用DecisionTreeRegressor类进行决策树回归预测。

  • 随机森林回归:作为一种流行的机器学*方法,它们在RandomForestRegressor类中定义。

  • 梯度提升树回归:我们可以使用GBTRegressor类来使用决策树的集成方法。

此外,MLlib 还支持使用AFTSurvivalRegressionIsotonicRegression类进行生存回归和等温回归。

借助这些类,我们可以在不到 10 行代码的情况下构建回归(或如下一节所示的分类)机器学*模型。基本步骤如下:

  1. 构建 Spark 会话。

  2. 实现数据加载管道:加载数据文件,指定格式,并将其读取到 Spark DataFrame 中。

  3. 确定要用作输入的特征以及目标(可选择将数据集拆分为训练集/测试集)。

  4. 实例化所需的类对象。

  5. 使用fit()方法并将训练数据集作为参数。

  6. 根据所选择的回归器,你可以查看学*到的参数并评估拟合的模型。

我们使用线性回归模型来预测波士顿房价数据集(www.cs.toronto.edu/~delve/data/boston/bostonDetail.html),数据集为csv格式:

  1. 导入必要的模块。我们将使用LinearRegressor来定义线性回归类,使用RegressionEvaluator来评估训练后的模型,使用VectorAssembler来将特征合并为一个输入向量,使用SparkSession来启动 Spark 会话:
from pyspark.ml.regression import LinearRegression as LR
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.evaluation import RegressionEvaluator

from pyspark.sql import SparkSession
  1. 接下来,使用SparkSession类启动一个 Spark 会话,代码如下:
spark = SparkSession.builder \
 .appName("Boston Price Prediction") \
 .config("spark.executor.memory", "70g") \
 .config("spark.driver.memory", "50g") \
 .config("spark.memory.offHeap.enabled",True) \
 .config("spark.memory.offHeap.size","16g") \
 .getOrCreate()
  1. 现在我们来读取数据;首先从给定路径加载数据,定义我们要使用的格式,最后将其读取到 Spark DataFrame 中,具体如下:
house_df = spark.read.format("csv"). \
    options(header="true", inferschema="true"). \
    load("boston/train.csv")
  1. 你可以看到现在内存中加载的 DataFrame 及其结构,见下图:

  1. 类似于 pandas DataFrame,Spark DataFrame 也可以通过单一命令进行处理。让我们通过以下截图来进一步了解我们的数据集:

  1. 接下来,我们定义要用于训练的特征;为此,我们使用VectorAssembler类。我们定义来自house_df DataFrame 的列,将其合并为输入特征向量和相应的输出预测(类似于定义X_trainY_train),然后执行相应的转换,具体如下:
vectors = VectorAssembler(inputCols = ['crim', 'zn','indus','chas',
    'nox','rm','age','dis', 'rad', 'tax',
    'ptratio','black', 'lstat'],
    outputCol = 'features')
vhouse_df = vectors.transform(house_df)
vhouse_df = vhouse_df.select(['features', 'medv'])
vhouse_df.show(5)

  1. 数据集随后被分割为训练/测试数据集,代码如下所示:
train_df, test_df = vhouse_df.randomSplit([0.7,0.3])
  1. 现在数据集已经准备好,我们实例化LinearRegression类,并将其拟合到训练数据集上,如下所示:
regressor = LR(featuresCol = 'features', labelCol='medv',\
    maxIter=20, regParam=0.3, elasticNetParam=0.8)
model = regressor.fit(train_df)
  1. 我们可以获得线性回归的结果系数,如下所示:
print("Coefficients:", model.coefficients)
print("Intercept:", model.intercept)

  1. 该模型在训练数据集上提供了 RMSE 值为4.73r2值为0.71,共进行了21次迭代:
modelSummary = model.summary
print("RMSE is {} and r2 is {}"\ 
   .format(modelSummary.rootMeanSquaredError,\
    modelSummary.r2))
print("Number of Iterations is ",modelSummary.totalIterations)
  1. 接下来,我们在测试数据集上评估我们的模型;我们得到的 RMSE 为5.55,R2 值为0.68
model_evaluator = RegressionEvaluator(predictionCol="prediction",\
    labelCol="medv", metricName="r2")
print("R2 value on test dataset is: ",\
    model_evaluator.evaluate(model_predictions))
print("RMSE value is", model.evaluate(test_df).rootMeanSquaredError)

一旦工作完成,你应该使用stop()方法停止 Spark 会话。完整代码可以在Chapter08/Boston_Price_MLlib.ipynb中找到。r2值较低和 RMSE 较高的原因是我们考虑了训练数据集中的所有特征作为输入特征向量,而其中许多特征对房价的预测没有显著作用。尝试减少特征,保留与价格高度相关的特征。

MLlib 中的分类

MLlib 还提供了多种分类器;它提供了二项和多项逻辑回归分类器。决策树分类器、随机森林分类器、梯度提升树分类器、多层感知器分类器、线性支持向量机分类器和朴素贝叶斯分类器都得到了支持。每个分类器都在其各自的类中定义;有关详细信息,请参阅spark.apache.org/docs/2.2.0/ml-classification-regression.html。基本步骤与我们在回归案例中学到的相同;唯一的区别是,现在,模型评估的标准是准确率,而不是 RMSE 或 r2。

本节将为你展示如何使用 Spark MLlib 的逻辑回归分类器实现葡萄酒质量分类问题:

  1. 对于这个分类问题,我们将使用通过LogisticRegressor类提供的逻辑回归。像之前的例子一样,VectorAssembler将用于将输入特征合并为一个向量。在我们之前看到的葡萄酒质量数据集中(第一章,物联网与人工智能的原理与基础),质量是一个介于 0 到 10 之间的整数,我们需要对其进行处理。在这里,我们将使用StringIndexer来处理。

Spark 的一个伟大特点是我们可以将所有的预处理步骤定义为一个管道。当有大量的预处理步骤时,这非常有用。这里我们只有两个预处理步骤,但为了展示如何形成管道,我们将使用Pipeline类。我们将所有这些模块导入作为第一步,并创建一个 Spark 会话,代码如下所示:

from pyspark.ml.classification import LogisticRegression as LR
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.feature import StringIndexer
from pyspark.ml import Pipeline

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("Wine Quality Classifier") \
    .config("spark.executor.memory", "70g") \
    .config("spark.driver.memory", "50g") \
    .config("spark.memory.offHeap.enabled",True) \
    .config("spark.memory.offHeap.size","16g") \
    .getOrCreate()
  1. 我们将加载并读取winequality-red.csv数据文件,如下所示:
wine_df = spark.read.format("csv"). \
    options(header="true",\
    inferschema="true",sep=';'). \
    load("winequality-red.csv")
  1. 我们处理给定数据集中的quality标签,将其拆分为三个不同的类别,并将其作为新的quality_new列添加到现有的 Spark DataFrame 中,代码如下所示:
from pyspark.sql.functions import when
wine_df = wine_df.withColumn('quality_new',\
    when(wine_df['quality']< 5, 0 ).\
    otherwise(when(wine_df['quality']<8,1)\
    .otherwise(2)))
  1. 尽管修改后的质量quality_new已经是一个整数,我们可以直接将其用作标签。在这个例子中,我们添加了StringIndexer将其转换为数字索引,目的是为了说明。你可以使用StringIndexer将字符串标签转换为数字索引。我们还使用VectorAssembler将各列组合成一个特征向量。两个阶段通过Pipeline结合在一起,如下所示:
string_index = StringIndexer(inputCol='quality_new',\
    outputCol='quality'+'Index')
vectors = VectorAssembler(inputCols = \
    ['fixed acidity','volatile acidity',\
    'citric acid','residual sugar','chlorides',\
    'free sulfur dioxide', 'total sulfur dioxide', \
    'density','pH','sulphates', 'alcohol'],\
    outputCol = 'features')

stages = [vectors, string_index]

pipeline = Pipeline().setStages(stages)
pipelineModel = pipeline.fit(wine_df)
pl_data_df = pipelineModel.transform(wine_df)
  1. 在流水线处理之后得到的数据随后被拆分成训练数据集和测试数据集,如以下代码所示:
train_df, test_df = pl_data_df.randomSplit([0.7,0.3])
  1. 接下来,我们实例化LogisticRegressor类,并使用fit方法在训练数据集上进行训练,如下所示:
classifier= LR(featuresCol = 'features', \
    labelCol='qualityIndex',\
    maxIter=50)
model = classifier.fit(train_df)
  1. 在以下截图中,我们可以看到学*到的模型参数:

  1. 模型的准确率为 94.75%。我们还可以在以下代码中看到其他评估指标,如precision(精确度)、recall(召回率)、F 值、真阳性率和假阳性率:
modelSummary = model.summary

accuracy = modelSummary.accuracy
fPR = modelSummary.weightedFalsePositiveRate
tPR = modelSummary.weightedTruePositiveRate
fMeasure = modelSummary.weightedFMeasure()
precision = modelSummary.weightedPrecision
recall = modelSummary.weightedRecall
print("Accuracy: {} False Positive Rate {} \
    True Positive Rate {} F {} Precision {} Recall {}"\
    .format(accuracy, fPR, tPR, fMeasure, precision, recall))

我们可以看到,使用 MLlib 的葡萄酒质量分类器的性能与我们之前的方法相当。完整代码可以在 GitHub 仓库中的Chapter08/Wine_Classification_MLlib.pynb文件找到。

使用 SparkDL 进行迁移学*

前面几节讲解了如何使用 Spark 框架和其 MLlib 处理机器学*问题。然而,在大多数复杂任务中,深度学*模型提供了更好的性能。Spark 支持 SparkDL,这是一个在 MLlib 上运行的更高级 API。它在后台使用 TensorFlow,并且还需要 TensorFrames、Keras 和 TFoS 模块。

在本节中,我们将使用 SparkDL 进行图像分类。这将帮助你熟悉 Spark 对图像的支持。对于图像,正如我们在第四章《物联网的深度学*》中学到的,卷积神经网络CNNs)是事实上的选择。在第四章《物联网的深度学*》中,我们从头开始构建了 CNN,并且还了解了一些流行的 CNN 架构。CNN 的一个非常有趣的特点是,每个卷积层都学*识别图像中的不同特征,起到特征提取器的作用。较低的卷积层会过滤出基本形状,比如线条和圆形,而较高的层则过滤出更抽象的形状。这个特点可以用来利用在一组图像上训练的 CNN 来分类另一组类似领域的图像,只需要更改顶部的全连接层。这种技术被称为迁移学*。根据新数据集图像的可用性以及两个领域之间的相似性,迁移学*可以显著帮助减少训练时间,并且减少对大规模数据集的需求。

在 2016 年 NIPS 教程中,AI 领域的关键人物之一 Andrew Ng 提到,迁移学*将是下一个商业成功的推动力。在图像领域,通过使用在 ImageNet 数据上训练的 CNN 进行迁移学*,已经在其他领域的图像分类中取得了巨大的成功。目前,许多研究正在致力于将迁移学*应用于其他数据领域。您可以通过 Sebastian Ruder 的这篇博客文章了解迁移学*的基础:ruder.io/transfer-learning/

我们将采用 Google 提出的 CNN 架构 InceptionV3(arxiv.org/pdf/1409.4842.pdf),并使用在 ImageNet 数据集(www.image-net.org)上训练的模型,来识别道路上的车辆(目前我们仅限于公交车和小汽车)。

在开始之前,请确保以下模块已安装在您的工作环境中:

  • PySpark

  • TensorFlow

  • Keras

  • TFoS

  • TensorFrames

  • Wrapt

  • Pillow

  • pandas

  • Py4J

  • SparkDL

  • Kafka

  • Jieba

这些可以通过pip install命令在您的独立机器或集群中的机器上安装。

接下来,您将学*如何使用 Spark 和 SparkDL 进行图像分类。我们通过 Google 图像搜索截图了两种不同的花朵,雏菊和郁金香;雏菊有 42 张图片,郁金香有 65 张图片。在下面的截图中,您可以看到雏菊的样本截图:

以下截图展示了郁金香的样本图片:

我们的数据集太小,因此如果从头开始构建 CNN,它无法提供任何有用的性能。在这种情况下,我们可以利用迁移学*。SparkDL 模块提供了一个简单方便的方式,通过DeepImageFeaturizer类使用预训练模型。它支持以下 CNN 模型(在 ImageNet 数据集(www.image-net.org)上预训练):

  • InceptionV3

  • Xception

  • ResNet50

  • VGG16

  • VGG19

我们将使用 Google 的 InceptionV3 作为我们的基础模型。完整的代码可以从 GitHub 仓库中的Chapter08/Transfer_Learning_Sparkdl.ipynb访问:

  1. 在第一步中,我们需要为 SparkDL 库指定环境。这是一个重要的步骤;如果没有它,内核将无法知道从哪里加载 SparkDL 包:
import os
SUBMIT_ARGS = "--packages databricks:spark-deep-learning:1.3.0-spark2.4-s_2.11 pyspark-shell"
os.environ["PYSPARK_SUBMIT_ARGS"] = SUBMIT_ARGS

即使在某些操作系统上通过pip安装 SparkDL 时,仍需要指定操作系统环境或 SparkDL。

  1. 接下来,让我们启动一个SparkSession,如下所示的代码:
from pyspark.sql import SparkSession
spark = SparkSession.builder \
    .appName("ImageClassification") \
    .config("spark.executor.memory", "70g") \
    .config("spark.driver.memory", "50g") \
    .config("spark.memory.offHeap.enabled",True) \
    .config("spark.memory.offHeap.size","16g") \
    .getOrCreate()
  1. 我们导入必要的模块并读取数据图像。除了读取图像路径,我们还将标签分配给 Spark DataFrame 中的每个图像,如下所示:
import pyspark.sql.functions as f
import sparkdl as dl
from pyspark.ml.image import ImageSchema
from sparkdl.image import imageIO
dftulips = ImageSchema.readImages('data/flower_photos/tulips').\
    withColumn('label', f.lit(0))
dfdaisy = ImageSchema.readImages('data/flower_photos/daisy').\
    withColumn('label', f.lit(1))
  1. 接下来,你可以看到两个数据框的前五行。第一列包含每张图像的路径,第二列显示其标签(是否属于雏菊(标签 1)或属于郁金香(标签 0)):

  1. 我们使用randomSplit函数将两个图像数据集划分为训练集和测试集(这始终是一种良好的做法)。通常,人们选择 60%—40%、70%—30%或 80%—20%的训练-测试数据集比例。我们在这里选择了 70%—30%的划分。为了训练,我们将两种花的训练图像合并到trainDF数据框中,将测试数据集图像合并到testDF数据框中,如下所示:
trainDFdaisy, testDFdaisy = dfdaisy.randomSplit([0.70,0.30],\
        seed = 123)
trainDFtulips, testDFtulips = dftulips.randomSplit([0.70,0.30],\
        seed = 122)
trainDF = trainDFdaisy.unionAll(trainDFtulips)
testDF = testDFdaisy.unionAll(testDFtulips)
  1. 接下来,我们使用InceptionV3作为特征提取器,后接逻辑回归分类器来构建管道。我们使用trainDF数据框来训练模型:
from pyspark.ml.classification import LogisticRegression
from pyspark.ml import Pipeline

vectorizer = dl.DeepImageFeaturizer(inputCol="image",\
        outputCol="features", modelName="InceptionV3")
logreg = LogisticRegression(maxIter=20, labelCol="label")
pipeline = Pipeline(stages=[vectorizer, logreg])
pipeline_model = pipeline.fit(trainDF)
  1. 现在我们在测试数据集上评估训练好的模型。我们可以看到,在测试数据集上,使用以下代码我们得到了 90.32%的准确率:
predictDF = pipeline_model.transform(testDF) #predict on test dataset

from pyspark.ml.evaluation import MulticlassClassificationEvaluator as MCE
scoring = predictDF.select("prediction", "label")
accuracy_score = MCE(metricName="accuracy")
rate = accuracy_score.evaluate(scoring)*100
print("accuracy: {}%" .format(round(rate,2)))
  1. 这里是两个类别的混淆矩阵:

在不到 20 行代码的情况下,我们成功训练了模型并获得了 90.32%的良好准确率。请记住,这里使用的数据集是原始数据;通过增加数据集中的图像数量,并过滤掉低质量的图像,你可以提高模型的表现。你可以从官方 GitHub 仓库了解更多关于深度学*库 SparkDL 的内容:github.com/databricks/spark-deep-learning

介绍 H2O.ai

H2O 是由 H2O.ai 开发的一个快速、可扩展的机器学*和深度学*框架,采用开源 Apache 许可证发布。根据公司提供的详细信息,超过 9000 个组织和 80,000+名数据科学家使用 H2O 来满足他们的机器学*/深度学*需求。它使用内存压缩技术,即使在一个小型的机器集群中,也能处理大量数据。它支持 R、Python、Java、Scala 和 JavaScript 接口,甚至还内置了一个 Web 界面。H2O 可以在独立模式下运行,也可以在 Hadoop 或 Spark 集群中运行。

H2O 包括大量的机器学*算法,如广义线性模型、朴素贝叶斯、随机森林、梯度提升和深度学*算法。H2O 的最佳特点是,用户可以在几行代码中构建成千上万个模型,比较结果,甚至进行超参数调优。H2O 还拥有更好的数据预处理工具。

H2O 需要 Java,因此,请确保你的系统上已安装 Java。你可以使用PyPi安装 H2O 以在 Python 中使用,以下是安装代码:

pip install h2o

H2O AutoML

H2O 最令人兴奋的功能之一是AutoML,自动化机器学*。它旨在开发一个易于使用的机器学*接口,可以供非专业人士使用。H2O AutoML 自动化了训练和调优大量候选模型的过程。其界面设计如此简便,用户只需要指定数据集、输入输出特征以及他们希望在训练的总模型数量或时间限制方面设置的任何约束条件。其余的工作由 AutoML 本身完成;在指定的时间限制内,它会识别出表现最好的模型,并提供一个排行榜。通常,已训练的所有模型的集成模型——堆叠集成模型,会占据排行榜的顶端。对于高级用户,有大量选项可供使用;这些选项的详细信息以及它们的各种功能可以在docs.h2o.ai/h2o/latest-stable/h2o-docs/automl.html上找到。

要了解更多关于 H2O 的信息,您可以访问他们的网站:h2o.ai

H2O 中的回归

我们将首先展示如何在 H2O 中进行回归。我们将使用与之前在 MLlib 中使用的相同数据集,即波士顿房价数据集,来预测房屋的价格。完整的代码可以在 GitHub 上找到:Chapter08/boston_price_h2o.ipynb

  1. 完成该任务所需的模块如下:
import h2o
import time
import seaborn
import itertools
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from h2o.estimators.glm import H2OGeneralizedLinearEstimator as GLM
from h2o.estimators.gbm import H2OGradientBoostingEstimator as GBM
from h2o.estimators.random_forest import H2ORandomForestEstimator as RF
%matplotlib inline
  1. 在导入必要的模块后,第一步是启动一个h2o服务器。我们通过h2o.init()命令来完成这一操作。它首先检查是否已有现有的h2o实例,如果没有,它将启动一个新的实例。还可以通过指定 IP 地址和端口号作为init()函数的参数来连接到现有集群。在下面的截图中,您可以看到在独立系统上执行init()的结果:

  1. 接下来,我们使用h2oimport_file函数读取数据文件。它将文件加载到 H2O 数据框中,可以像使用 pandas 数据框一样轻松处理。我们可以很容易地使用cor()方法找到h2o数据框中不同输入特征之间的相关性:
boston_df = h2o.import_file("../Chapter08/boston/train.csv", destination_frame="boston_df")

plt.figure(figsize=(20,20))
corr = boston_df.cor()
corr = corr.as_data_frame()
corr.index = boston_df.columns
#print(corr)
sns.heatmap(corr, annot=True, cmap='YlGnBu',vmin=-1, vmax=1)
plt.title("Correlation Heatmap")

以下是波士顿房价数据集中不同特征之间相关性图的输出:

  1. 现在,和往常一样,我们将数据集分为训练集、验证集和测试集。定义要作为输入特征使用的特征(x):
train_df,valid_df,test_df = boston_df.split_frame(ratios=[0.6, 0.2],\
         seed=133)
features =  boston_df.columns[:-1]
  1. 完成这项工作后,过程非常简单。我们只需实例化来自 H2O 库的回归模型类,并使用train()函数,传入训练集和验证集数据作为参数。在train函数中,我们还需要指定输入特征(x)和输出特征(y)。在本例中,我们将所有可用的特征作为输入特征,房价medv作为输出特征。通过简单的打印语句,我们可以查看训练后模型的特征。接下来,你可以看到一个广义线性回归模型的声明及其在训练集和验证集上的训练结果:
model_glm = GLM(model_id='boston_glm')
model_glm.train(training_frame= train_df,\
         validation_frame=valid_df, \
         y = 'medv', x=features)
print(model_glm)

  1. 训练后,下一步是检查在测试数据集上的表现,这可以通过model_performance()函数轻松完成。我们也可以将其应用于任何数据集:训练集、验证集、测试集或某些新的类似数据集:
test_glm = model_glm.model_performance(test_df)
print(test_glm)

  1. 如果我们想使用梯度提升估计回归模型或随机森林回归模型,我们将实例化相应的类对象;接下来的步骤保持不变。不同的是输出参数;在梯度提升估计器和随机森林回归的情况下,我们还会学*不同输入特征的相对重要性:
#Gradient Boost Estimator
model_gbm = GBM(model_id='boston_gbm')
model_gbm.train(training_frame= train_df, \
        validation_frame=valid_df, \
        y = 'medv', x=features)

test_gbm = model_gbm.model_performance(test_df)

#Random Forest
model_rf = RF(model_id='boston_rf')
model_rf.train(training_frame= train_df,\
        validation_frame=valid_df, \
        y = 'medv', x=features)

test_rf = model_rf.model_performance(test_df)
  1. 机器学*和深度学*中最难的部分是选择正确的超参数。在 H2O 中,通过其H2OGridSearch类,任务变得相当简单。以下代码片段在之前定义的梯度提升估计器上执行网格搜索,搜索超参数深度:
from h2o.grid.grid_search import H2OGridSearch as Grid
hyper_params = {'max_depth':[2,4,6,8,10,12,14,16]}
grid = Grid(model_gbm, hyper_params, grid_id='depth_grid')
grid.train(training_frame= train_df,\
        validation_frame=valid_df,\
        y = 'medv', x=features)
  1. H2O 的最佳部分是使用 AutoML 自动寻找最佳模型。让我们要求它在 10 个模型中为我们搜索,并且将时间限制为 100 秒。AutoML 将在这些参数下构建 10 个不同的模型,不包括堆叠集成模型。它将在最多 100 秒内运行,最终训练出堆叠集成模型:
from h2o.automl import H2OAutoML as AutoML
aml = AutoML(max_models = 10, max_runtime_secs=100, seed=2)
aml.train(training_frame= train_df, \
        validation_frame=valid_df, \
        y = 'medv', x=features)
  1. 我们回归任务的排行榜如下:

排行榜中的不同模型可以通过各自的model_id进行访问。最佳模型通过 leader 参数进行访问。在我们的例子中,aml.leader代表最佳模型,即所有模型的堆叠集成模型。我们可以使用h2o.save_model函数将最佳模型保存为二进制格式或 MOJO 格式。

H2O 中的分类

相同的模型可以用于 H2O 中的分类,只需做一个改动;我们需要使用asfactor()函数将输出特征从数值型转为类别型。我们将对红酒的质量进行分类,并使用我们之前的红酒数据库(第三章,物联网中的机器学*)。我们需要导入相同的模块并启动 H2O 服务器。完整代码可在Chapter08/wine_classification_h2o.ipynb文件中找到:

  1. 下面是导入必要模块并启动 H2O 服务器的代码:
import h2o
import time
import seaborn
import itertools
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from h2o.estimators.glm import H2OGeneralizedLinearEstimator as GLM
from h2o.estimators.gbm import H2OGradientBoostingEstimator as GBM
from h2o.estimators.random_forest import H2ORandomForestEstimator as RF

%matplotlib inline

h2o.init()
  1. 下一步是读取数据文件。我们首先修改输出特征以适应两个类别(好酒和坏酒),然后使用asfactor()函数将其转换为类别变量。这是 H2O 中的一个重要步骤;因为我们在回归和分类中使用相同的类对象,它们要求回归时输出标签为数值型,分类时输出标签为类别型,如代码块所示:
wine_df = h2o.import_file("../Chapter08/winequality-red.csv",\
        destination_frame="wine_df")    
features = wine_df.columns[:-1]
print(features)
wine_df['quality'] = (wine_df['quality'] > 7).ifelse(1,0)
wine_df['quality'] = wine_df['quality'].asfactor()
  1. 接下来,将数据集分为训练集、验证集和测试集。我们将训练集和验证集输入到广义线性估计器中,唯一的改动是;我们需要指定family=binomial参数,因为这里我们只有两类分类:好酒和坏酒。如果你有超过两个类别,使用family=multinomial。记住,指定这个参数是可选的;H2O 会自动检测输出特征:
train_df,valid_df,test_df = wine_df.split_frame(ratios=[0.6, 0.2],\
        seed=133)    

model_glm = GLM(model_id='wine_glm', family = 'binomial')
model_glm.train(training_frame= train_df, \
        validation_frame=valid_df,\
        y = 'quality', x=features)
print(model_glm)
  1. 经过训练后,你可以查看模型在所有性能指标上的表现:准确率、精确度、召回率、F1 值和 AUC,甚至是混淆矩阵。你可以获取三种数据集(训练集、验证集和测试集)的所有指标。以下是从广义线性估计器获取的测试数据集的指标:

  1. 在不改变前面的代码的情况下,我们可以进行超参数调优并使用 H2O 的 AutoML 来获得更好的模型:
from h2o.automl import H2OAutoML as AutoML
aml = AutoML(max_models = 10, max_runtime_secs=100, seed=2)
aml.train(training_frame= train_df, \
        validation_frame=valid_df, \
        y = 'quality', x=features)

我们看到,在红酒质量分类中,最佳模型是 XGBoost。

概要

随着物联网的普及,生成的数据正在以指数速度增长。这些数据通常是非结构化的,且数量庞大,常被称为大数据。为应对庞大的数据集,已经提出了大量的框架和解决方案。一个有前景的解决方案是 DAI,将模型或数据分布在一组机器上。我们可以使用分布式 TensorFlow 或 TFoS 框架来进行分布式模型训练。*年来,出现了一些易于使用的开源解决方案。两个最受欢迎且成功的解决方案是 Apache Spark 的 MLlib 和 H2O.ai 的 H2O。在本章中,我们展示了如何在 MLlib 和 H2O 中为回归和分类任务训练机器学*模型。Apache Spark 的 MLlib 支持 SparkDL,后者为图像分类和检测任务提供了出色的支持。本章使用 SparkDL 通过预训练的 InceptionV3 对花卉图像进行分类。另一方面,H2O.ai 的 H2O 在处理数值型和表格数据时表现良好。它提供了一个有趣且实用的 AutoML 功能,甚至允许非专家用户在很少的细节输入下调优并搜索大量的机器学*/深度学*模型。本章涵盖了如何使用 AutoML 进行回归和分类任务的示例。

当在一组机器上工作时,充分利用这些分布式平台是非常有利的。随着计算和数据以可承受的价格转移到云端,将机器学*任务迁移到云端是有意义的。接下来的一章将介绍不同的云平台,以及如何使用它们来分析由您的物联网设备生成的数据。

第九章:个人和家庭物联网

现在,您已经掌握了机器学*ML)和深度学*DL)的知识,并学*了如何将其应用于大数据、图像任务、文本任务和时间序列数据,是时候探索您所学的算法和技术在实际中的应用了。本章及接下来的两章将集中介绍一些具体的案例研究。本章将重点讨论个人和家庭物联网IoT)的使用案例。我们将在本章中讨论以下内容:

  • 成功的物联网应用

  • 可穿戴设备及其在个人物联网中的作用

  • 如何通过机器学*监测心脏

  • 什么让家成为智能家居

  • 智能家居中使用的设备

  • 人工智能在预测人类活动识别中的应用

个人物联网

个人物联网主要由可穿戴设备主导,这些技术设备设计为佩戴在身体上,并与智能手机上的应用程序配合使用。第一款可穿戴设备是由美国 Time Computer Inc.(当时称为汉密尔顿手表公司)生产的 Pulsar 计算器手表。那时它是一款不连接互联网的独立设备。随着互联网的普及,能够连接互联网的可穿戴设备成为了一种潮流。预计可穿戴设备市场将从 2016 年约3.25亿台增长至 2020 年超过8.3亿台:

该图展示了 2016 至 2021 年间全球可穿戴设备的数量(数据来源:Statista)。随着越来越多的设备在线连接并持续生成数据,AI/ML 工具成为分析这些数据并做出明智决策的自然选择。在本节中,您将了解一些成功的个人物联网应用。

MIT 的 SuperShoes

当你用一只手拿着手机并通过Google Maps导航时,你有多少次觉得这种方式很繁琐?你有多少次希望能有一双能带你去任何地方的魔法拖鞋?由MIT Media Lab开发的 SuperShoes(www.media.mit.edu/projects/supershoes/overview/)几乎就像这些魔法拖鞋;它们使用户无需查看智能手机屏幕即可在街道上导航。

SuperShoes 具有灵活的鞋垫,在脚趾下嵌入了振动马达。它们通过无线连接到智能手机上的应用程序。该应用程序不仅允许用户与 SuperShoes 进行交互,还将用户的喜好、不喜欢的东西、爱好、商店、食物、人物、兴趣等存储在云账户中。振动马达产生的触觉刺激与用户进行沟通。一旦用户在应用程序中输入目的地,鞋子就开始工作。如果左脚趾有触觉刺激,用户应该向左转;如果右脚趾有触觉刺激,用户应该向右转。如果没有触觉刺激,用户则需要继续直行。如果两个脚趾都反复有触觉刺激,说明用户已经到达目的地。

除了导航,它还可以推荐附*的兴趣地点;用户可以在云端更新他们的喜好和不喜欢的事物。根据用户的喜好和不喜欢,SuperShoes 在用户接*推荐地点时会给出提示(两只脚趾同时被刺激)。SuperShoes 的另一个有趣功能是它可以提醒用户;如果用户附*有任务,它会提醒用户。

制作这款鞋所需的硬件非常简单,主要包括以下内容:

  • 三个振动触觉装置刺激脚趾

  • 用于感应步态的电容传感器

  • 一块微控制器,用于接收应用程序的命令,并相应地控制触觉刺激装置

  • 用于与智能手机连接的蓝牙设备

  • 为整个系统提供电力的电池

这一魔法由应用程序中的软件代码实现。您可以在此网站了解更多关于 SuperShoes 的信息:dhairyadand.com/works/supershoes

持续血糖监测

人工智能的一个主要应用领域是物联网(IoT)在医疗保健中的应用,其中最成功的商业应用之一就是持续监测人体血糖水平。雅培的 FreeStyle CGM、DexCom CGM 和美敦力的 CGM 是一些市面上可用的品牌。

连续血糖监测CGM)使糖尿病患者能够实时检查自己体内的血糖水平。它帮助他们在一段时间内监测血糖数据,这些数据还可以用于预测未来的血糖水平,从而帮助他们应对低血糖等情况。

在 CGM 中,通常将传感器放置在腹部皮肤下或粘贴在手臂背部。传感器将读数发送到连接的呼叫器或智能手机应用程序。该应用程序具有基于人工智能的算法,可以告知用户任何临床相关的血糖模式。数据的可用性不仅帮助用户主动管理血糖的升高和下降,还可以提供关于饮食、锻炼或疾病对血糖水平影响的见解。

传感器的使用寿命通常为 7 到 14 天,通常这个时间足够医疗人员了解个人的生活方式,并相应地建议调整。

使用 CGM 数据预测低血糖

一旦一个人拥有 CGM 数据,就可以使用 AI/ML 对其进行分析,以收集更多信息或预测低血糖。在本节中,我们将看到如何使用前面章节中学到的算法来构建一个血糖预测系统。

我们将基于 Sparacino 等人发表的论文《血糖浓度可以通过连续血糖监测传感器时间序列提前预测》(10.1109/TBME.2006.889774)构建我们的预测器。

在论文中,CGM 时间序列的血糖数据由一个时间序列模型描述;论文考虑了两种模型,一个是简单的一阶多项式,另一个是一阶自回归模型。模型参数在每个采样时间 t[s] 对过去的血糖数据进行拟合。这里,我们将使用在第三章《物联网中的机器学*》中学到的 scikit 线性回归器来实现简单的一阶多项式:

  1. 我们导入模块 pandas 来读取csv文件,Numpy 用于数据处理,Matplotlib 用于绘图,scikit-learn 用于线性回归器,如下所示:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
%matplotlib inline
  1. 将从 CGM 获得的数据保存在数据文件夹中并读取。我们需要两个值:血糖读数和对应的时间。我们使用的数据在两个 CSV 文件中提供,分别是ys.csv(包含血糖值)和ts.csv(包含对应的时间),如下所示:
# Read the data
ys = pd.read_csv('data/ys.csv')
ts = pd.read_csv('data/ts.csv')
  1. 根据论文,我们定义了预测模型的两个参数ph,即预测时间范围,和mu,即遗忘因子。有关这两个参数的更多细节,请参考前面提到的论文:
# MODEL FIT AND PREDICTION

# Parameters of the predictive model. ph is Prediction horizon, mu is Forgetting factor.
ph = 10 
mu = 0.98
  1. 我们创建数组来保存预测值,如下所示:
n_s = len(ys)

# Arrays to hold predicted values
tp_pred = np.zeros(n_s-1) 
yp_pred = np.zeros(n_s-1)
  1. 现在我们读取 CGM 数据,模拟实时采集,并预测ph分钟后的血糖水平。所有过去的数据都用于确定模型参数,但每个数据点的贡献不同,由分配给它的个体权重mu^k(表示距当前采样时间k时刻的样本权重)决定:
# At every iteration of the for loop a new sample from CGM is acquired.
for i in range(2, n_s+1):
    ts_tmp = ts[0:i]
    ys_tmp = ys[0:i]
    ns = len(ys_tmp)

    # The mu**k assigns the weight to the previous samples.
    weights = np.ones(ns)*mu
    for k in range(ns):
        weights[k] = weights[k]**k
    weights = np.flip(weights, 0)
    # MODEL
    # Linear Regression.
    lm_tmp = LinearRegression() 
    model_tmp = lm_tmp.fit(ts_tmp, ys_tmp, sample_weight=weights)

    # Coefficients of the linear model, y = mx + q 
    m_tmp = model_tmp.coef_
    q_tmp = model*tmp.intercept

*    # PREDICTION
    tp = ts.iloc[ns-1,0] + ph
    yp = m_tmp*tp + q_tmp

    tp_pred[i-2] = tp 
    yp_pred[i-2] = yp
  1. 我们可以看到预测滞后于实际数据。正常的血糖水平在70180之间。低于70,患者可能会出现低血糖,超过180则可能导致高血糖。让我们来看一下我们预测数据的图示:
# PLOT
# Hypoglycemia threshold vector. 
t_tot = [l for l in range(int(ts.min()), int(tp_pred.max())+1)]
hypoglycemiaTH = 70*np.ones(len(t_tot)) 
#hyperglycemiaTH = 180*np.ones(len(t_tot))

fig, ax = plt.subplots(figsize=(10,10))
fig.suptitle('Glucose Level Prediction', fontsize=22, fontweight='bold')
ax.set_title('mu = %g, ph=%g ' %(mu, ph))
ax.plot(tp_pred, yp_pred, label='Predicted Value') 
ax.plot(ts.iloc[:,0], ys.iloc[:,0], label='CGM data') 
ax.plot(t_tot, hypoglycemiaTH, label='Hypoglycemia threshold')
#ax.plot(t_tot, hyperglycemiaTH, label='Hyperglycemia threshold')
ax.set_xlabel('time (min)')
ax.set_ylabel('glucose (mg/dl)')
ax.legend()

  1. 以下代码的 RMSE 误差将为 27:
from sklearn.metrics import mean_squared_error as mse
print("RMSE is", mse(ys[1:],yp_pred))

代码位于Chapter09/Hypoglycemia_Prediction.ipynb笔记本中。葡萄糖预测系统已经出现在许多商业产品中。你也可以根据我们刚刚创建的模型来做一个类似的系统。你还可以使用人工神经网络做类似的预测,结果可能会更好(参考 www.ncbi.nlm.nih.gov/pubmed/20082589)。

心脏监测器

另一个非常有用的个人 AI 应用在物联网中的例子是心脏病的检测。现在有很多可穿戴设备可以用来监测和记录心率。这些数据可以用来预测任何有害的心脏状况。在这里,我们将采用 AI/ML 工具来预测心脏心律失常,这是一组心率不规律的情况;它可以是过快(每分钟超过 100 次)或过慢(每分钟低于 60 次)。使用的数据来自UCI 机器学*数据集archive.ics.uci.edu/ml/datasets/heart+Disease。该数据集包含 76 个属性,但并非所有属性都用于预测疾病的存在;每一行数据都有一个目标字段。它有五个可能的值 0–4,值 0 表示健康的心脏,其他任何值都表示有疾病。为了更好的准确性,问题可以被划分为二分类问题。该代码灵感来源于 Mohammed Rashad 的 GitHub 链接,并且以 GNU GPL 3.0 许可证共享:github.com/MohammedRashad/Deep-Learning-and-Wearable-IoT-to-Monitor-and-Predict-Cardiac-Arrhytmia。完整的代码可以通过 GitHub 仓库中的Chapter09/Heart_Disease_Prediction.ipynb文件访问:

  1. 和往常一样,第一步是导入必要的模块。由于我们现在要对患者进行心脏病与否的分类,因此我们需要一个分类器。为了简便起见,我们使用SVC分类器。你也可以尝试使用 MLP 分类器,代码如下所示:
# importing required libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from sklearn.svm import SVC
from sklearn import metrics
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
  1. 接下来,读取数据集,对数据集进行预处理,选择你将考虑的属性。我们从 76 个属性中选择了 13 个,然后将目标值从多类值转换为二分类。最后,数据被分割为训练集和测试集,如下所示:
# reading csv file and extracting class column to y.
dataset = pd.read_csv("data.csv")
dataset.fillna(dataset.mean(), inplace=True)

dataset_to_array = np.array(dataset)
label = dataset_to_array[:,57] # "Target" classes having 0 and 1
label = label.astype('int')
label[label>0] = 1 # When it is 0 heart is healthy, 1 otherwise

# extracting 13 features
dataset = np.column_stack((
    dataset_to_array[:,4] , # pain location
    dataset_to_array[:,6] , # relieved after rest
    dataset_to_array[:,9] , # pain type 
    dataset_to_array[:,11], # resting blood pressure
    dataset_to_array[:,33], # maximum heart rate achieve
    dataset_to_array[:,34], # resting heart rate 
    dataset_to_array[:,35], # peak exercise blood pressure (first of 2 parts) 
    dataset_to_array[:,36], # peak exercise blood pressure (second of 2 parts) 
    dataset_to_array[:,38], # resting blood pressure 
    dataset_to_array[:,39], # exercise induced angina (1 = yes; 0 = no) 
    dataset.age, # age 
    dataset.sex , # sex
    dataset.hypertension # hyper tension
 ))

print ("The Dataset dimensions are : " , dataset.shape , "\n")

# dividing data into train and test data
X_train, X_test, y_train, y_test = train_test_split(dataset, label, random_state = 223)
  1. 现在,我们定义要使用的模型。这里我们使用的是支持向量分类器,通过fit函数来训练数据集:
model = SVC(kernel = 'linear').fit(X_train, y_train)
  1. 让我们来看一下它在测试数据集上的表现:
model_predictions = model.predict(X_test)
# model accuracy for X_test 
accuracy = metrics.accuracy_score(y_test, model_predictions)
print ("Accuracy of the model is :" , 
    accuracy , "\nApproximately : ", 
    round(accuracy*100) , "%\n")
  1. 你可以看到,它提供了 74%的准确率,使用 MLP,我们可以进一步提高准确率。但请记住,在使用 MLP 分类器之前,务必对所有输入特征进行归一化处理。以下是我们训练的支持向量分类器在测试数据集上的混淆矩阵:
#creating a confusion matrix
cm = confusion_matrix(y_test, model_predictions)

import pandas as pd
import seaborn as sn
import matplotlib.pyplot as plt
%matplotlib inline
df_cm = pd.DataFrame(cm, index = [i for i in "01"],
columns = [i for i in "01"])
plt.figure(figsize = (10,7))
sn.heatmap(df_cm, annot=True)

以下输出显示了测试数据集的混淆矩阵:

你可以在相同的数据集上训练你的模型,并使用训练好的模型为你的朋友、家人或客户预测心脏状况。

数字助手

数字助手是最早设想的人工智能应用之一。最初的数字助手尝试并未取得成功。然而,随着智能手机的出现和普及,今天我们有了大量的数字助手,它们提供拨打电话、写短信、安排日程,甚至为你上网搜索等服务。你可以让它们推荐附*的餐厅、酒吧或其他类似的地方。

以下是一些流行的数字助手:

  • Siri:由苹果开发,允许用户拨打电话、添加日程、播放音乐或视频,甚至发送短信。如今,几乎所有的苹果产品上都提供语音激活界面。

  • Cortana:由微软创建,帮助你通过基于时间、地点甚至人物的提醒保持日程安排。你可以让 Cortana 为你订午餐,或者使用它与之合作的其他应用程序。它与 Edge 集成,并启用了一个配备 Cortana 的语音激活扬声器。

  • Alexa:由亚马逊开发,适用于亚马逊 Echo 智能音响。它可以播放音乐、制定待办事项清单、为你设置闹钟、播放有声书,并提供关于股票、天气等的实时信息。它还支持语音互动。

  • Google Assistant:这是一个语音控制的智能助手。它提供持续的对话功能,即一旦你开始讲话,它会在没有触发词的情况下继续听取后续请求,而无需重复说嘿,Google。它还可以识别不同人的语音特征,并根据个人的喜好和厌恶量身定制回答。它不仅可以在安卓智能手机上使用,还可以在 Google Home 上使用。

2018 年,谷歌更进一步,发布了 Google Duplex,它可以为你打电话并预定约会。它说话像人类一样,并且在交谈时能够理解上下文。

物联网与智能家居

我的一个密友一直担心他年迈的母亲,母亲独自待在家中,而他、妻子和孩子们都外出。当母亲的健康状况开始恶化时,他向我求助。解决方案很简单;他在每个房间安装了 CCTV 摄像头,并与手机应用程序进行了连接。摄像头通过互联网联网,现在,无论他身在何处,都可以查看家中的情况,确保母亲的安好。

闭路电视(CCTV)、智能照明、智能音箱等连接到互联网帮助家庭自动化许多任务,您获得的是智能家居。目前大多数智能家居系统通过语音命令界面工作,您可以使用一组命令控制特定设备。例如,在 Amazon 的 Echo Dot 中,您可以要求其搜索或播放特定歌曲。您可以要求 Apple 的 Siri 使用手机打电话给朋友,都可以通过简单的语音界面。大多数这些设备都在某种形式上使用 AI/ML,但通过使用 AI/ML,家庭自动化可以进一步发展。例如,对于我的朋友,可以训练 AI 系统从视频中识别活动,或者检测家庭入侵。可能性是无限的。有了正确的数据和足够的计算能力,您只受想象的限制。

在本节中,我们将看到一些现有的家庭自动化产品,并探讨如何进一步利用 AI 来增强自动化。

人体活动识别

最受关注的智能家居应用之一是人体活动识别HAR)。有许多公司试图开发能够追踪体育活动及其相应热量消耗的应用程序。健康与健身无疑是一个大生意。除了在健身和健康方面的应用外,HAR 在老年护理或康复中也很有用。进行 HAR 已有许多方法,以下是其中两种:

  • 使用摄像头(或雷达或类似设备)记录人类活动,并使用深度学*方法对其进行分类

  • 个人使用可穿戴传感器(类似于智能手机中的加速计)记录其数据,并用于预测活动

这两种方法各有优缺点。我们将在以下章节详细讨论它们。

使用可穿戴传感器进行 HAR

大量供应商提供带有健身追踪器的可穿戴手表和手镯。这些手表和手镯配备了 GPS、加速度计、陀螺仪、心率传感器和/或环境光传感器。通过传感器融合,它们结合这些传感器的输出来对活动进行预测。由于数据的时间性质,这是一个具有挑战性的时间序列分类任务。

Fitbit (www.fitbit.com/smarttrack),作为健身追踪器领域的领先公司,使用了它称之为SmartTrack的技术,能够识别连续运动或轻微运动的活动。它利用运动的强度和模式对活动进行分类。它将活动分为七类,如下所示:

  • 步行

  • 跑步

  • 有氧运动

  • 椭圆机

  • 户外骑行

  • 运动

  • 游泳

Apple Watch (www.apple.com/in/apple-watch-series-4/workout/) 对 Fitbit 构成了强有力的竞争。它运行在 iOS 操作系统上,具备跌倒检测功能,以及许多其他健康跟踪特性。通过分析手腕的轨迹和冲击加速度,它可以检测到是否发生跌倒,并能够发起紧急呼叫。Apple Watch 默认将活动分为三类:步行、锻炼和站立。锻炼(运动)则进一步分类到其他领域,如室内跑步、户外跑步、滑雪、单板滑雪、瑜伽,甚至徒步旅行。

如果你想尝试使用智能手机传感器制作类似的应用程序,首先你需要的数据。接下来,我们展示了一个使用随机森林实现的 HAR 方法,代码改编自罗切斯特大学的数据科学家 Nilesh Patil 的 GitHub 链接:github.com/nilesh-patil/human-activity-recognition-smartphone-sensors

数据集来自论文 Davide Anguita, Alessandro Ghio, Luca Oneto, Xavier Parra 和 Jorge L. Reyes-Ortiz. A Public Domain Dataset for Human Activity Recognition Using Smartphones. 2013 年 21 届欧洲人工神经网络、计算智能与机器学*研讨会,ESANN 2013。比利时布鲁日,2013 年 4 月 24-26 日。

可在 UCI ML 网站上获取:archive.ics.uci.edu/ml/datasets/Human+Activity+Recognition+Using+Smartphones#

数据集中的每一条记录包含:

  • 来自加速度计的三轴加速度(总加速度)以及估算的身体加速度

  • 来自陀螺仪的三轴角速度

  • 一个包含 561 个特征的向量,包含时域和频域变量

  • 它的活动标签

  • 实验执行者的标识符

数据被分类为六个类别:

  • 躺卧

  • 坐姿

  • 站立

  • 步行

  • 向下行走

  • 向上行走

  1. 在这里,我们使用 scikit-learn 的随机森林分类器对数据进行分类。实现所需的模块在第一步中被导入:
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

from sklearn.ensemble import RandomForestClassifier as rfc
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
%matplotlib inline
  1. 我们读取数据并将其分为traintest数据集,如下所示:
data = pd.read_csv('data/samsung_data.txt',sep='|')
train = data.sample(frac=0.7,
        random_state=42)
test = data[~data.index.isin(train.index)]

X = train[train.columns[:-2]]
Y = train.activity
  1. 数据包含 561 个特征,但并非所有特征都同等重要。我们可以通过制作一个简单的随机森林分类器来选择更重要的特征,仅选择最重要的特征。在此实现中,分为两个步骤。首先,我们获取重要特征的列表,并按重要性降序排列。然后,通过网格超参数调优找到特征的数量和特征。超参数调优的结果显示在曲线上。我们可以看到,大约 20 个特征后,使用以下代码,袋外OOB)精度没有显著提高:
randomState = 42
ntree = 25

model0 = rfc(n_estimators=ntree,
random_state=randomState,
n_jobs=4,
warm_start=True,
oob_score=True)
model0 = model0.fit(X, Y)

# Arrange the features in ascending order
model_vars0 = pd.DataFrame(
    {'variable':X.columns,
    'importance':model0.feature_importances_})

model_vars0.sort_values(by='importance',
    ascending=False,
    inplace=True)

# Build a feature vector with most important 25 features

n = 25
cols_model = [col for col in model_vars0.variable[:n].values]

  1. 我们还可以在下图中看到前 25 个特征的平均重要性:

  1. 同样,我们可以超调树的数量参数。在这里,我们将特征限制为四个重要特征:
n_used = 4
cols_model = [col for col in model_vars0.variable[:n_used].values]\
     + [model_vars0.variable[6]]
X = train[cols_model]
Y = train.activity

ntree_determination = {}
for ntree in range(5,150,5):
    model = rfc(n_estimators=ntree,
        random_state=randomState,
        n_jobs=4,
        warm_start=False,
        oob_score=True)
model = model.fit(X, Y)
ntree_determination[ntree]=model.oob_score_

  1. 因此,我们可以看到,具有大约四个重要特征和50棵树的随机森林能够提供良好的 OOB 准确率。因此,我们的最终模型如下:
model2 = rfc(n_estimators=50,
    random_state=randomState,
    n_jobs=4,
    warm_start=False,
    oob_score=True)
model2 = model2.fit(X, Y)
  1. 这样会得到 94%的测试数据准确率。接下来,您可以看到测试数据集的混淆矩阵:
test_actual = test.activity
test_pred = model2.predict(test[X.columns])
cm = confusion_matrix(test_actual,test_pred)
sns.heatmap(data=cm,
     fmt='.0f',
     annot=True,
     xticklabels=np.unique(test_actual),
     yticklabels=np.unique(test_actual))

完整代码和数据探索可在 GitHub 仓库中找到,文件路径为Chapter09/Human_activity_recognition_using_accelerometer.ipynb。使用加速度计数据的优势在于,它是从可穿戴设备收集的,因此无需在现场进行安装。另一个优势是它是文本数据,因此比视频数据所需的计算资源更少。

来自视频的 HAR

检测人类活动的另一种方法是通过视频。在这种情况下,我们必须使用诸如 CNN 这样的深度学*模型来获得良好的结果。Ivan Laptev 和 Barbara Caputo 提供了一个适用于分类视频的良好数据集,网址为:www.nada.kth.se/cvap/actions/。该数据集包含六种类型的动作:步行、慢跑、跑步、拳击、挥手和拍手,且在不同的场景下录制。每个视频都是使用 25fps 的相机录制的,空间分辨率为 160 × 120,平均长度为四秒。数据集中总共有 599 个视频,每个类别大约 100 个。

视频数据的一个问题是它在计算上非常昂贵,因此,减少数据集的大小非常重要,以下是几种减少数据集的方法:

  • 由于颜色在活动检测中并不起作用,因此可以将图像从三通道彩色图像转换为二维灰度图像。

  • 视频时长为四秒,且每秒 25 帧,其中大部分帧包含冗余数据,因此我们可以将帧数从(25 × 4 = 100)帧减少到每秒 5 帧,从而得到 20 帧。(如果每个视频提取的总帧数固定,会更好)。

  • 将单个帧的空间分辨率从 160 × 120 降低。

接下来,在建模时,我们应该使用三维卷积层。假设我们每个视频只取了 20 帧,并将每帧的大小缩减到 128 × 128,那么一个样本将是:20 × 128 × 128 × 1,这对应的是一个具有单通道的 20 × 128 × 128 的体积。

智能照明

提到智能家居时,首先想到的家庭自动化应用是使用智能灯。目前大多数智能照明系统都提供了通过智能手机应用或互联网来控制灯光的开关,以及灯光的强度。有些还允许你改变灯光的颜色/色调。运动探测灯是今天几乎所有家庭的标准配置,能够在检测到任何运动后自动开启:

听力障碍人士使用的智能灯根据情况变化颜色

使用人工智能,我们可以使这些智能灯更加智能。在紧急情况下,它们可以被编程成相互协作,引导你走向正确的出口。对于听力障碍的人,智能灯可以用来替代警报,例如,火灾警报响起时显示红灯,遇到入室盗窃时显示橙灯,当有人按门铃时显示欢迎的绿色灯。借助如果这样,那么那样IFTTT)等服务,你可以建立更智能、更复杂的支持系统。

IFTTT 提供一个免费的服务来控制你的设备。一个设备(或服务)的动作可以触发一个或多个其他设备。使用起来非常简单,你只需在 IFTTT 网站创建一个小程序:ifttt.com,选择你想用作触发器的设备(点击选择)或服务,并将其与 IFTTT 账户关联。接下来,你选择(点击选择)当触发器激活时,哪个服务或设备应该采取行动。该网站包含成千上万的预制小程序,让你的工作更加轻松。

个性化智能灯系统的算法

这些只是你可以用现有智能灯做的一些例子。但如果你喜欢冒险并准备将新传感器与这些智能灯进行接口,你可以为自己打造一盏个性化的灯,它会根据人的精神活动改变其色调/强度。当你感到困倦时,它会变暗,而在你工作时,它则会全亮,但当你和朋友们交谈、共度时光时,它会提供一个愉悦的色调。听起来有些离谱?其实不然,你可以首先使用一种 AI 算法,通过视频(或可穿戴的健身追踪器)检测人体活动,并将其分类为三种状态:工作、休闲和睡眠,然后利用其输出控制智能灯的色调/强度。

家庭监控

家庭监控是一个非常有用且迫切需要的应用。随着单亲家庭和老龄化人口的增加,安全和监控的需求不仅限于外部环境,还需要在家庭内部进行。许多公司正在尝试使用视频提供室内监控。一个成功的实施案例是由 DeepSight AILabs 公司开发的,他们开发了专有软件SuperSecure;这是一个与任何闭路电视系统、任何摄像头、任何分辨率兼容的通用后期解决方案,将其转变为AI 驱动的智能监控解决方案,以高准确度检测潜在威胁,并触发即时警报以挽救生命和保护资产。

当您尝试实施自己的家庭监控时,我们在使用视频实施 HAR 时讨论的要点也同样适用。

智能家居仍处于初级阶段,主要原因是它们涉及高昂的拥有成本和互连设备的不灵活性。通常,一个特定的系统完全由一个公司管理。如果某些原因导致公司关闭,消费者就会陷入困境。解决方案将是允许开源家庭自动化硬件和软件。有关家庭自动化领域的挑战和机遇的一篇有趣文章是微软研究的一篇文章,Home Automation in the Wild: Challenges and Opportunities (www.microsoft.com/en-us/research/publication/home-automation-in-the-wild-challenges-and-opportunities/)。

总结

本章的重点是个人和家庭的 AI 驱动物联网解决方案。智能手机的大规模使用使得可穿戴传感器成为每个人都可以接触到的设备,导致了大量的个人应用的出现。在这一章节中,我们探讨并实施了一些成功的个人和家庭 AI 驱动的物联网解决方案。我们了解了麻省理工学院的 SuperShoes,这是一种可以自主找到目的地路径的鞋子。我们了解了连续血糖监测系统,并编写了代码来预测高血糖。本章还展示了个性化心率监测器如何实施。

尽管智能家居仍处于初级阶段,本章探讨了一些最流行和有用的智能家居解决方案。介绍了 HAR,这是一种位于智能家居和个人物联网边界的应用。我们使用 scikit-learn 编写了一些代码,用于根据加速度计获取的数据分类活动。本章介绍了一些酷炫的智能照明应用,并讨论了使用视频进行家庭监控。

在接下来的章节中,我们将研究一些案例研究,其中从物联网传感器获得的数据被用来提高工业生产和效率。

第十章:工业物联网的 AI 应用

来自不同背景的公司如今意识到人工智能AI)的重要性,因此正在将其纳入到各自的生态系统中。本章重点介绍一些成功的 AI 驱动的工业物联网解决方案。到本章结束时,您将涵盖以下内容:

  • AI 驱动的物联网解决方案如何改变行业

  • 不同行业提供 AI 驱动的数据分析,以提高生产效率、优化物流和改善客户体验

  • 预防性维护

  • 基于飞机引擎传感器数据实现预防性维护的代码

  • 电力负荷预测

  • 实现一个 TensorFlow 代码来执行短期负荷预测

AI 驱动的工业物联网简介

物联网、机器人技术、大数据和机器学*ML)的融合正在为工业企业创造巨大的机遇,也带来了显著的挑战。

低成本传感器、多云平台和强大的边缘基础设施的可用性使得各行业采用 AI 变得更加容易且具有盈利性。这种 AI 驱动的工业物联网正在改变公司提供产品和服务、以及与客户和合作伙伴互动的方式。

AI 驱动的工业物联网的一个有前景的领域是预防性维护。直到现在,工业公司通常采用反应式维护方式,即根据固定的时间表进行维护(如每六个月),或者仅在某些设备停止运作时才进行维护。例如,一家物流公司可能会对其车队中的每辆车辆进行半年一次的服务检查,并按计划更换某些部件或整车。这种反应性维护常常浪费时间且费用高昂。应用 AI 算法在问题发生之前预测异常和故障,可以节省大量时间。

另一个 AI 驱动的工业物联网可以实现奇迹的领域是人类与机器人之间的协作。机器人已经是工业物联网生态系统的一部分;它们在生产线和仓库中工作,执行那些特别重复或对人类工人来说危险的任务。目前矿业行业中使用的半自动化卡车、火车和装载机通常是通过预设程序、固定轨道和/或远程人工操作来引导的。

在许多工业场景中,云计算所引入的延迟可能是不可接受的,在这种情况下,需要边缘计算基础设施。

为了让您了解 AI 驱动的工业物联网的普及和应用,以下是一些提供工业物联网服务和解决方案的热门 AI 初创企业名单:

  • Uptake Technologies Inc:总部位于芝加哥的初创公司,由 Brad Keywell 于 2014 年共同创立,开发用于监控和分析工业设备实时数据的软件,并利用这些数据提高机械的性能和维护效率。该公司计划将其业务扩展到能源、铁路、石油天然气、矿业和风能等重型行业 (www.uptake.com/)。

  • C3.ai:由 Thomas Siebel 领导的领先大数据、物联网和人工智能应用提供商,在 Forrester Research 2018 年工业物联网波段报告中被评为物联网平台领导者。公司成立于 2009 年,成功地为能源管理、网络效率、欺诈检测和库存优化等领域的工业提供服务 (c3.ai)。

  • Alluvium:由《Machine Learning for Hackers》的作者 Drew Conway 于 2015 年创立,Alluvium 使用机器学*和人工智能帮助工业公司实现运营稳定并改善生产。他们的旗舰产品 Primer 帮助公司从传感器的原始和精炼数据中识别有用的洞察力,使他们能够在操作故障发生之前预测它们 (alluvium.io)。

  • Arundo Analytics:由 Jakob Ramsøy 领导,成立于 2015 年,Arundo Analytics 提供将实时数据与机器学*和其他分析模型相连接的服务。他们的产品可以扩展已部署的模型,创建和管理实时数据管道 (www.arundo.com)。

  • Canvass Analytics:它通过基于实时运营数据的预测分析,帮助企业做出关键的业务决策。Canvass AI 平台从工业设备、传感器和操作系统生成的数百万个数据点中提炼出模式和相关性,从而创造新的洞察力。由 Humera Malik 领导,Canvass Analytics 成立于 2016 年 (www.canvass.io)。

这还不是结束,像 Amazon 和 Google 这样的软件技术巨头也在物联网领域投入大量资金和基础设施。Google 利用预测建模来降低数据中心成本,而 PayPal 则使用机器学*来发现欺诈交易。

一些有趣的应用案例

来自不同背景的大量公司正在意识到将数据分析和人工智能融入其生态系统的重要性和影响。从提高运营、供应链和维护效率到提升员工生产力,再到创建新的商业模式、产品和服务,人工智能已经在各个领域得到了探索。以下,我们列出了一些人工智能驱动物联网在工业中的有趣应用案例:

  • 预测性维护:在预测性维护中,使用人工智能算法预测设备故障的发生时间。这样,公司可以提前进行维护,从而减少停机时间。在接下来的部分中,我们将详细讨论预防性维护如何帮助行业,以及它可以采取的各种方式。

  • 资产追踪:也叫资产管理,这是一种跟踪关键物理资产的方法。通过跟踪关键资产,公司可以优化物流、维持库存水平并发现任何低效之处。传统上,资产追踪限于在资产上添加 RFID 或条形码,从而跟踪其位置。然而,凭借人工智能算法的支持,现在可以进行更积极的资产追踪。例如,风力发电站可以感知风速、风向甚至温度的变化,并利用这些参数将每台风机调整到最佳方向,以最大化发电量。

  • 车队管理与维护:运输行业在车队管理中已经使用人工智能约十年,通过优化路线来提高效率。如今,许多低成本传感器的可用性和边缘计算设备的进步使运输公司能够收集并利用这些传感器接收到的数据,不仅能够通过更好的车对车通信和预防性维护优化物流,还能提高安全性。通过安装如困倦检测等系统,因疲劳或分心导致的危险行为能够被检测到,司机会被提醒采取应对措施。

使用人工智能的预测性维护

重型机械和设备是任何行业的支柱,和所有物理对象一样,它们会逐渐退化、老化并发生故障。最初,公司通常采取反应式维护,即在设备发生故障后才进行维护。这往往会导致计划外的停机时间。对于任何行业来说,计划外的停机时间都会导致资源紧张,显著降低效率、生产力,进而减少利润。为了解决这些问题,行业开始转向预防性维护。

在预防性维护中,定期按预定的时间间隔进行例行检查。预防性维护要求记录设备及其定期维护计划。第三次工业革命中,计算机引入到工业领域,使得维护和更新这些记录变得更加容易。虽然预防性维护可以避免大多数计划外的停机,但它仍然不是最理想的替代方案,因为定期检查可能会造成不必要的开支。下图展示了四次工业革命的示例:

根据创作共用许可协议共享的图像:(commons.wikimedia.org/wiki/File:Industry_4.0.png)

当前自动化和数字化的趋势推动了第四次工业革命,也称为工业 4.0。这使得企业能够部署机器对机器M2M)和机器对人M2H)的通信,以及基于 AI 的分析算法,从而实现预测性维护,能够利用历史数据预测设备故障的发生。预测性维护策略极大地简化了企业资源的维护和管理。

预测性维护的主要思想是根据条件监测数据预测设备何时可能发生故障。传感器用于监控设备在正常运行中的状态和性能,依据设备的不同,可能会使用不同类型的传感器。以下是一些常见的条件监测参数/传感器值:

  • 振动传感器,主要用于检测泵和电机的错位、不平衡、机械松动或磨损。

  • 电流/电压传感器,用于测量供电电机的电流和电压。

  • 超声波分析,用于检测管道系统或储罐中的泄漏,或可动部件的机械故障以及电气设备的故障。

  • 红外热成像,用于识别温度波动。

  • 用于检测液体质量的传感器(例如,在葡萄酒的情况下,传感器用于检测葡萄酒中不同元素的存在)。

实施预测性维护,最重要的是识别需要监控的条件。接着,部署所需的传感器来监控这些条件。最后,收集来自传感器的数据以构建模型。

使用长短期记忆(LSTM)进行预测性维护。

为了演示预测性维护,我们将使用 Azure ML 提供的模拟数据(gallery.azure.ai/Collection/Predictive-Maintenance-Template-3)。数据集包含以下三个文件:

根据数据源提供的数据描述,训练数据(train_FD001.txt)由多个多变量时间序列组成,以周期作为时间单位,每个周期有 21 个传感器读数。可以假设每个时间序列是从不同的相同类型引擎生成的。假定每个引擎在开始时具有不同程度的初始磨损和制造差异,这些信息未知于用户。在这些模拟数据中,引擎被假定在每个时间序列开始时正常运行。在一定时间段内开始退化,退化进展并增加幅度。当达到预定义的阈值时,引擎被认为不再适合继续操作。换句话说,每个时间序列中的最后一个周期可以视为相应引擎的故障点。以样本训练数据为例,id=1 的引擎在周期 192 失败,而 id=2 的引擎在周期 287 失败。

测试数据(test_FD001.txt)与训练数据具有相同的数据结构。唯一的区别在于数据不指示故障发生的时间点(换句话说,最后一个时间段不表示故障点)。以样本测试数据为例,id=1 的引擎从周期 1 运行到周期 31。未显示该引擎在故障前还能运行多少个周期。

地面真实数据(RUL_FD001.txt)提供了测试数据中引擎剩余工作周期的数量。以示例地面真实数据为例,测试数据中 id=1 的引擎在故障前可以再运行 112 个周期。

由于这是时间序列数据,我们将使用长短期记忆网络LSTM)来分类引擎在某个时间段内是否会失败。这里呈现的代码基于 Umberto Griffo 提供的实现,GitHub 链接如下:(github.com/umbertogriffo/Predictive-Maintenance-using-LSTM):

  1. 实施预测性维护所需的模块在第一步导入。我们还为随机计算设置种子,以便结果可重现:
import keras
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os

# Setting seed for reproducibility
np.random.seed(1234) 
PYTHONHASHSEED = 0

from sklearn import preprocessing
from sklearn.metrics import confusion_matrix, recall_score, precision_score
from keras.models import Sequential,load_model
from keras.layers import Dense, Dropout, LSTM
  1. 让我们读取数据并分配列名,如下所示的代码:
# read training data - It is the aircraft engine run-to-failure data.
train_df = pd.read_csv('PM_train.txt', sep=" ",
         header=None)
train_df.drop(train_df.columns[[26, 27]], 
        axis=1, 
        inplace=True)
train_df.columns = ['id', 'cycle', 'setting1',
         'setting2', 'setting3', 's1', 's2',
         's3', 's4', 's5', 's6', 's7', 's8',
         's9', 's10', 's11', 's12', 's13', 
        's14', 's15', 's16', 's17', 's18', 
        's19', 's20', 's21']

train_df = train_df.sort_values(['id','cycle'])

# read test data - It is the aircraft engine operating data without failure events recorded.
test_df = pd.read_csv('PM_test.txt', 
        sep=" ", header=None)
test_df.drop(test_df.columns[[26, 27]], 
        axis=1, 
        inplace=True)
test_df.columns = ['id', 'cycle', 'setting1', 
        'setting2', 'setting3', 's1', 's2', 's3',
         's4', 's5', 's6', 's7', 's8', 's9', 
        's10', 's11', 's12', 's13', 's14',
         's15', 's16', 's17', 's18', 's19', 
        's20', 's21']

# read ground truth data - It contains the information of true remaining cycles for each engine in the testing data.
truth_df = pd.read_csv('PM_truth.txt', 
        sep=" ", 
        header=None)
truth_df.drop(truth_df.columns[[1]], 
        axis=1, 
        inplace=True)
  1. 作为第一步,我们预测引擎在某个时间段内是否会失败,因此我们的标签将是 10,即这将是一个二元分类问题。为了创建二元标签,我们预处理数据并创建一个新的剩余寿命RUL)标签。我们还创建一个名为 label1 的二元变量,指示特定引擎是否会在 w1 个周期内失败。最后,数据(非传感器)被归一化,如下所示:
# Data Labeling - generate column RUL(Remaining Usefull Life or Time to Failure)
rul = pd.DataFrame(train_df.groupby('id')
        ['cycle'].max()).reset_index()
rul.columns = ['id', 'max']
train_df = train_df.merge(rul, 
        on=['id'], 
        how='left')
train_df['RUL'] = train_df['max'] -     train_df['cycle']
train_df.drop('max', 
        axis=1, 
        inplace=True)

# Let us generate label columns for training data
# we will only use "label1" for binary classification, 
# The question: is a specific engine going to fail within w1 cycles?
w1 = 30
w0 = 15
train_df['label1'] = np.where(train_df['RUL'] <= w1, 1, 0 )

# MinMax normalization (from 0 to 1)
train_df['cycle_norm'] = train_df['cycle']
cols_normalize = train_df.columns.difference
        (['id','cycle','RUL','label1'])
min_max_scaler = preprocessing.MinMaxScaler()
norm_train_df = pd.DataFrame(min_max_scaler.
        fit_transform(train_df[cols_normalize]), 
        columns=cols_normalize, 
        index=train_df.index)
join_df = train_df[train_df.columns.
        difference(cols_normalize)].
        join(norm_train_df)
train_df = join_df.reindex(columns = train_df.columns)

train_df.head()

  1. 对测试数据集执行类似的预处理操作,唯一的变化是——RUL 值是从真实数据中获取的:
# MinMax normalization (from 0 to 1)
test_df['cycle_norm'] = test_df['cycle']
norm_test_df = pd.DataFrame(
        min_max_scaler.
        transform(test_df[cols_normalize]), 
        columns=cols_normalize,     
         index=test_df.index)
test_join_df = test_df[test_df.
        columns.difference(cols_normalize)].
        join(norm_test_df)
test_df = test_join_df.
        reindex(columns = test_df.columns)
test_df = test_df.reset_index(drop=True)

# We use the ground truth dataset to generate labels for the test data.
# generate column max for test data
rul = pd.DataFrame(test_df.
        groupby('id')['cycle'].max()).
        reset_index()
rul.columns = ['id', 'max']
truth_df.columns = ['more']
truth_df['id'] = truth_df.index + 1
truth_df['max'] = rul['max'] + truth_df['more']
truth_df.drop('more', 
        axis=1, 
        inplace=True)

# generate RUL for test data
test_df = test_df.merge(truth_df, 
        on=['id'], how='left')
test_df['RUL'] = test_df['max'] - test_df['cycle']
test_df.drop('max', 
        axis=1, 
        inplace=True)

# generate label columns w0 and w1 for test data
test_df['label1'] = np.where
        (test_df['RUL'] <= w1, 1, 0 )
test_df.head()

  1. 由于我们使用 LSTM 进行时间序列建模,我们创建了一个函数,该函数将根据窗口大小生成要输入 LSTM 的序列。我们选择的窗口大小为50。我们还需要一个函数来生成相应的标签:
# function to reshape features into 
# (samples, time steps, features) 
def gen_sequence(id_df, seq_length, seq_cols):
    """ Only sequences that meet the window-length
    are considered, no padding is used. This 
    means for testing we need to drop those which 
    are below the window-length. An alternative
    would be to pad sequences so that
    we can use shorter ones """

    # for one id we put all the rows in a single matrix
    data_matrix = id_df[seq_cols].values
    num_elements = data_matrix.shape[0]
    # Iterate over two lists in parallel.
    # For example id1 have 192 rows and 
    # sequence_length is equal to 50
    # so zip iterate over two following list of 
    # numbers (0,112),(50,192)
    # 0 50 -> from row 0 to row 50
    # 1 51 -> from row 1 to row 51
    # 2 52 -> from row 2 to row 52
    # ...
    # 111 191 -> from row 111 to 191
    for start, stop in zip(range(0, num_elements-seq_length), range(seq_length, num_elements)):
        yield data_matrix[start:stop, :]

def gen_labels(id_df, seq_length, label):
    # For one id we put all the labels in a 
    # single matrix.
    # For example:
    # [[1]
    # [4]
    # [1]
    # [5]
    # [9]
    # ...
    # [200]] 
    data_matrix = id_df[label].values
    num_elements = data_matrix.shape[0]
    # I have to remove the first seq_length labels
    # because for one id the first sequence of 
    # seq_length size have as target
    # the last label (the previus ones are 
    # discarded).
    # All the next id's sequences will have 
    # associated step by step one label as target. 
    return data_matrix[seq_length:num_elements, :]
  1. 现在让我们为我们的数据生成训练序列和对应的标签,代码如下:
# pick a large window size of 50 cycles
sequence_length = 50

# pick the feature columns 
sensor_cols = ['s' + str(i) for i in range(1,22)]
sequence_cols = ['setting1', 'setting2', 
        'setting3', 'cycle_norm']
sequence_cols.extend(sensor_cols)

# generator for the sequences
seq_gen = (list(gen_sequence
        (train_df[train_df['id']==id], 
        sequence_length, sequence_cols)) 
        for id in train_df['id'].unique())

# generate sequences and convert to numpy array
seq_array = np.concatenate(list(seq_gen)).
        astype(np.float32)
print(seq_array.shape)

# generate labels
label_gen = [gen_labels(train_df[train_df['id']==id], 
        sequence_length, ['label1']) 
        for id in train_df['id'].unique()]
label_array = np.concatenate(label_gen).
        astype(np.float32)
print(label_array.shape)
  1. 我们现在构建一个包含两个 LSTM 层和一个全连接层的 LSTM 模型。该模型用于二分类训练,因此它尝试减少二元交叉熵损失。我们使用Adam优化器来更新模型参数:
nb_features = seq_array.shape[2]
nb_out = label_array.shape[1]

model = Sequential()

model.add(LSTM(
     input_shape=(sequence_length, nb_features),
     units=100,
     return_sequences=True))
model.add(Dropout(0.2))

model.add(LSTM(
     units=50,
     return_sequences=False))
model.add(Dropout(0.2))

model.add(Dense(units=nb_out,
     activation='sigmoid'))
model.compile(loss='binary_crossentropy', 
    optimizer='adam', 
    metrics=['accuracy'])

print(model.summary())

  1. 我们训练模型,结果如下所示:
history = model.fit(seq_array, label_array, 
        epochs=100, batch_size=200, 
        validation_split=0.05, verbose=2,
         callbacks = [keras.callbacks.
            EarlyStopping(monitor='val_loss', 
            min_delta=0, patience=10, 
            verbose=0, mode='min'),
        keras.callbacks.
            ModelCheckpoint
            (model_path,monitor='val_loss',     
            save_best_only=True, 
            mode='min', verbose=0)])    
  1. 经过训练的模型在测试数据集上的准确率为 98%,在验证数据集上的准确率为 98.9%。精度值为0.96,召回率为1.0,F1 分数为0.98。不错吧!下图显示了训练模型的这些结果:

我们可以使用相同的数据来预测飞机发动机的 RUL 值,也就是预测发动机的故障时间。这将是一个回归问题,我们现在可以使用 LSTM 模型进行回归。最初的步骤将与之前相同,但从第五步开始会有所变化。虽然生成的输入数据序列将与之前保持一致,但目标将不再是二进制标签,而是使用 RUL 作为回归模型的目标:

  1. 我们使用相同的gen_labels()函数创建目标值。我们还使用gen_sequence()函数创建了一个验证集:
# generate labels
label_gen = [gen_labels(train_df[train_df['id']==id],
        sequence_length, ['RUL']) 
        for id in train_df['id'].unique()]
label_array = np.concatenate(label_gen).astype(np.float32)

# val is a list of 192 - 50 = 142 bi-dimensional array 
# (50 rows x 25 columns)
val=list(gen_sequence(train_df[train_df['id']==1], 
        sequence_length, sequence_cols))
  1. 创建一个 LSTM 模型。我们在训练过程中使用r2作为评估指标,因此我们使用 Keras 的自定义指标功能和我们自己的指标函数:
def r2_keras(y_true, y_pred):
     """Coefficient of Determination 
     """
     SS_res = K.sum(K.square( y_true - y_pred ))
     SS_tot = K.sum(K.square( y_true - K.mean(y_true) ) )
     return ( 1 - SS_res/(SS_tot + K.epsilon()) )

# Next, we build a deep network. 
# The first layer is an LSTM layer with 100 units followed by 
# another LSTM layer with 50 units. 
# Dropout is also applied after each LSTM layer to control 
# overfitting. 
# Final layer is a Dense output layer with single unit and linear 
# activation since this is a regression problem.
nb_features = seq_array.shape[2]
nb_out = label_array.shape[1]

model = Sequential()
model.add(LSTM(
     input_shape=(sequence_length, nb_features),
     units=100,
     return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(
     units=50,
     return_sequences=False))
model.add(Dropout(0.2))
model.add(Dense(units=nb_out))
model.add(Activation("linear"))
model.compile(loss='mean_squared_error', optimizer='rmsprop',metrics=['mae',r2_keras])

print(model.summary())

  1. 在训练数据集上训练模型,结果如下所示:
# fit the network
history = model.fit(seq_array, label_array, epochs=100, 
    batch_size=200, validation_split=0.05, verbose=2,
    callbacks = [keras.callbacks.EarlyStopping
    (monitor='val_loss', min_delta=0, patience=10, 
    verbose=0, mode='min'),
    keras.callbacks.ModelCheckpoint
    (model_path,monitor='val_loss', 
    save_best_only=True, mode='min', 
    verbose=0)])
  1. 训练后的模型在测试数据集上提供了r2值为 0.80,在验证数据集上为 0.72。我们可以通过超参数调优来改善我们的结果。接下来,您可以看到训练过程中训练和验证数据集的损失:

要运行此代码,请确保您的 TensorFlow 版本为 1.4 及以上,Keras 版本为 2.1.2。如果您的 Keras 版本更高,请首先使用pip uninstall keras卸载它,然后使用pip install keras==2.1.2重新安装。

完整的代码,包括二分类和回归模型,已上传至 GitHub 仓库,Chapter10/Predictive_Maintenance_using_LSTM.ipynb。我们还可以创建一个模型,确定故障是否会在不同的时间窗口发生,例如,故障发生在窗口(1,w[0])或窗口(w[0+1], w[1])天内,依此类推。这将变成一个多分类问题,数据需要相应地进行预处理。你可以在 Azure AI Gallery 中阅读更多关于这个预测性维护模板的信息:gallery.azure.ai/Experiment/Predictive-Maintenance-Step-2A-of-3-train-and-evaluate-regression-models-2

预测性维护的优缺点

根据 GE 的一份调查报告(www.gemeasurement.com/sites/gemc.dev/files/ge_the_impact_of_digital_on_unplanned_downtime_0.pdf),停机时间会对石油和天然气行业的性能产生负面影响。这不仅仅是石油和天然气行业,所有行业都是如此。因此,为了减少停机时间,提高效率,采用预测性维护至关重要。然而,建立预测性维护系统的成本相当高,但一旦预测性维护系统建立得当,它有助于提供多个具有成本效益的好处,例如:

  • 最小化设备维护所需的时间

  • 最小化因维护而丧失的生产时间

  • 最后,备件成本也得到了最小化

成功的预测性维护可以在积极的方面重塑整个公司

工业中的电力负荷预测

电力目前是国内和工业领域最重要的能源载体。由于与燃料不同,电力难以存储且存储成本高,因此需要精确地将电力的生成与需求进行耦合。因此,电力负荷预测至关重要。根据时间范围(预测范围),电力负荷预测可分为以下三类:

  • 短期负荷预测:预测的时间范围为一小时到几周

  • 中期负荷预测:预测持续时间从几周到几个月不等

  • 长期负荷预测:在这里,预测从几个月到几年

根据需求和应用,可能需要规划其中一个或所有之前的负荷预测类别。*年来,短期负荷预测STLF)领域进行了大量的研究工作。STLF 能够通过提供准确的未来负荷预测手段来帮助工业界,从而有助于精确规划、降低运营成本,进而提高利润并提供更可靠的电力供应。STLF 基于历史数据(通过智能电表获取)和预测的天气条件,预测未来的能源需求。

负荷预测问题是一个回归问题。它可以作为时间序列问题或静态模型进行建模。将负荷预测建模为时间序列数据是最常见的选择。使用时间序列建模,我们可以使用标准的机器学*时间序列模型,如 ARIMA,或者我们可以利用深度学*模型,如循环神经网络和 LSTM。

有关电力负荷预测中使用的各种策略和模型的全面回顾,请参考这篇论文:

Fallah, S., Deo, R., Shojafar, M., Conti, M., and Shamshirband, S. (2018). 智能能源管理网格中的能源负荷预测的计算智能方法:现状、未来挑战和研究方向。Energies, 11(3), 596。

使用 LSTM 进行 STLF

在这里,我们展示了使用 LSTM 进行短期负荷预测的代码。用于训练和测试的数据来自 UCI ML 网站(archive.ics.uci.edu/ml/datasets/Individual+household+electric+power+consumption#)。STLF 的代码已从 GitHub 进行适配(github.com/demmojo/lstm-electric-load-forecast):

  1. 我们导入必要的模块并设置随机种子,代码如下所示:
import time
from keras.layers import LSTM
from keras.layers import Activation, Dense, Dropout
from keras.models import Sequential, load_model
from numpy.random import seed

from tensorflow import set_random_seed
set_random_seed(2) # seed random numbers for Tensorflow backend
seed(1234) # seed random numbers for Keras
import numpy as np
import csv
import matplotlib.pyplot as plt

%matplotlib inline
  1. 定义加载数据并将其转换为适合 LSTM 输入的序列的工具函数:
def load_data(dataset_path, sequence_length=60, prediction_steps=5, ratio_of_data=1.0):
    # 2075259 is the total number of measurements 
    # from Dec 2006 to Nov 2010
    max_values = ratio_of_data * 2075259

    # Load data from file
    with open(dataset_path) as file:
        data_file = csv.reader(file, delimiter=";")
        power_consumption = []
        number_of_values = 0
        for line in data_file:
            try:
                power_consumption.append(float(line[2]))
                number_of_values += 1
            except ValueError:
                pass

            # limit data to be considered by 
            # model according to max_values
            if number_of_values >= max_values: 
                break

    print('Loaded data from csv.')
    windowed_data = []
    # Format data into rolling window sequences
    # for e.g: index=0 => 123, index=1 => 234 etc.
    for index in range(len(power_consumption) - sequence_length): 
            windowed_data.append(
            power_consumption[
            index: index + sequence_length])

    # shape (number of samples, sequence length)
    windowed_data = np.array(windowed_data)

    # Center data
    data_mean = windowed_data.mean()
    windowed_data -= data_mean
    print('Center data so mean is zero 
            (subtract each data point by mean of value: ', 
            data_mean, ')')
    print('Data : ', windowed_data.shape)

    # Split data into training and testing sets
    train_set_ratio = 0.9
    row = int(round(train_set_ratio * windowed_data.shape[0]))
    train = windowed_data[:row, :]

    # remove last prediction_steps from train set
    x_train = train[:, :-prediction_steps] 
    # take last prediction_steps from train set
    y_train = train[:, -prediction_steps:] 
    x_test = windowed_data[row:, :-prediction_steps]

    # take last prediction_steps from test set
    y_test = windowed_data[row:, -prediction_steps:] 

    x_train = np.reshape(x_train, 
            (x_train.shape[0], x_train.shape[1], 1))
    x_test = np.reshape(x_test, 
            (x_test.shape[0], x_test.shape[1], 1))

    return [x_train, y_train, x_test, y_test, data_mean]
  1. 构建 LSTM 模型,我们所构建的模型包含两个 LSTM 层和一个全连接层:
def build_model(prediction_steps):
    model = Sequential()
    layers = [1, 75, 100, prediction_steps]
    model.add(LSTM(layers[1], 
        input_shape=(None, layers[0]), 
        return_sequences=True)) # add first layer
    model.add(Dropout(0.2)) # add dropout for first layer
    model.add(LSTM(layers[2], 
        return_sequences=False)) # add second layer
    model.add(Dropout(0.2)) # add dropout for second layer
    model.add(Dense(layers[3])) # add output layer
    model.add(Activation('linear')) # output layer 
    start = time.time()
    model.compile(loss="mse", optimizer="rmsprop")
    print('Compilation Time : ', time.time() - start)
    return model
  1. 训练模型,代码如下所示:
def run_lstm(model, sequence_length, prediction_steps):
    data = None
    global_start_time = time.time()
    epochs = 1
    ratio_of_data = 1 # ratio of data to use from 2+ million data points
    path_to_dataset = 'data/household_power_consumption.txt'

    if data is None:
        print('Loading data... ')
        x_train, y_train, x_test, y_test, result_mean = load_data(path_to_dataset, sequence_length,
                                                                  prediction_steps, ratio_of_data)
    else:
        x_train, y_train, x_test, y_test = data

    print('\nData Loaded. Compiling...\n')

    model.fit(x_train, y_train, batch_size=128, epochs=epochs, validation_split=0.05)
    predicted = model.predict(x_test)
    # predicted = np.reshape(predicted, (predicted.size,))
    model.save('LSTM_power_consumption_model.h5') # save LSTM model

    plot_predictions(result_mean, prediction_steps, predicted, y_test, global_start_time)

    return None

sequence_length = 10 # number of past minutes of data for model to consider
prediction_steps = 5 # number of future minutes of data for model to predict
model = build_model(prediction_steps)
run_lstm(model, sequence_length, prediction_steps)
  1. 从以下图表中,我们可以看到我们的模型做出了良好的预测:

完整的代码可以在 GitHub 上找到:Chapter10/Electrical_load_Forecasting.ipynb

总结

在这一章中,我们看到,AI 驱动的物联网对工业界产生了重大影响。从制造业、物流、农业、采矿到新产品和服务的创造,AI 触及了各个领域。我们可以合理假设,AI 驱动的工业物联网将改变并颠覆当前的商业流程和模式,带来更好的发展。

下一章将展示 AI 和物联网如何帮助塑造更好的城市。

第十一章:智能城市物联网(AI for Smart Cities IoT)

本章将向读者介绍智能城市。通过案例研究,将展示如何将本书中学到的概念应用于开发各种智能城市组件。在阅读本章时,您将学*以下内容:

  • 什么是智能城市?

  • 智能城市的基本组件

  • 全球各地的城市正在实施智能解决方案

  • 建设智能城市面临的挑战

  • 编写代码以从旧金山犯罪数据中检测犯罪描述

我们为什么需要智能城市?

根据联合国数据(population.un.org/wup/DataQuery/),到 2050 年底,世界人口将达到 97 亿(9.7 × 10⁹)。预计其中约 70%将是城市人口,许多城市的人口将超过 1000 万。这是一个巨大的数字,随着人口的增长,我们不仅迎来了新的机会,同时也面临许多独特的挑战:

预测的世界人口(数据来源:联合国)

最困难的挑战是如何让所有居民都能获得资源和能源,同时避免环境恶化。目前,城市消耗了全球 75%的资源和能源,并产生了 80%的温室气体;尽管绿色能源的趋势在增长,但我们都知道地球的资源,如食物和水,是有限的。另一个关键挑战是行政管理;随着人口的增加,需要采取策略来防止卫生问题、缓解交通拥堵和遏制犯罪。

许多这些问题可以通过使用 AI 支持的物联网来解决。利用技术进步可以为城市居民创造新的体验,并使他们的日常生活更加舒适和安全。这催生了智能城市的概念。

根据 techopedia(www.techopedia.com/definition/31494/smart-city)的定义,智能城市是利用信息和通信技术提升城市服务(如能源和交通)质量和性能的城市,从而减少资源消耗、浪费和整体成本。Deakin 和 AI Waer 列出了四个有助于定义智能城市的因素:

  • 在城市基础设施中使用广泛的电子和数字技术

  • 利用信息和通信技术ICT)来转变生活和工作环境

  • 将 ICT 嵌入政府系统

  • 实施将人们和信息通信技术(ICT)结合在一起的实践和政策,以促进创新并增强其提供的知识

因此,智能城市不仅拥有信息通信技术(ICT),而且还以一种积极影响居民的方式使用技术。

Deakin 和 AI Waer 的论文定义了智慧城市,并聚焦于所需的转型:

Deakin, M., 和 Al Waer, H.(2011)。从智能到智慧城市《智能建筑国际》, 3**(3), 140-152

人工智能AI)与物联网(IoT)共同具有应对城市人口过度增长所带来的关键挑战的潜力;它们可以帮助解决交通管理、医疗、能源危机等诸多问题。物联网数据和人工智能技术可以改善智慧城市中市民和企业的生活。

智慧城市的组成部分

智慧城市有许多 AI 驱动的物联网技术应用场景,从维护更健康的环境到提升公共交通和安全。在下面的图表中,您可以看到一些智慧城市的应用场景:

智慧城市组成部分

本节将概述一些最受欢迎的应用场景——其中一些已经在全球各地的智慧城市中得到了实施。

智能交通管理

人工智能和物联网可以实施智能交通解决方案,确保智慧城市的居民能够尽可能安全、高效地从一个地点到达另一个地点。

洛杉矶,作为世界上最为拥堵的城市之一,已实施智能交通解决方案以控制交通流量。它已安装了路面传感器和闭路电视摄像头,将关于交通流量的实时更新发送到中央交通管理系统。来自传感器和摄像头的数据流被分析,并通知用户交通拥堵和信号灯故障的情况。2018 年 7 月,洛杉矶市在每个交叉口进一步安装了先进交通控制器ATC)机柜。通过启用车与基础设施通信V2I)和 5G 连接功能,这使得它们能够与具备交通信号灯信息功能的汽车(如 Audi A4 或 Q7)进行通信。您可以通过洛杉矶智慧交通系统的官方网站了解更多信息(dpw.lacounty.gov/TNL/ITS/)。

启动嵌入传感器的自动化车辆可以提供车辆的位置和速度;它们可以与智能交通信号灯直接通信,防止交通拥堵。此外,利用历史数据,可以预测未来的交通情况,并加以利用防止可能的拥堵。

智能停车

住在城市中的任何人都一定感受到过找停车位的困难,尤其是在假期期间。智能停车可以缓解这一难题。通过在停车位地面嵌入路面传感器,智能停车解决方案能够判断停车位是否空闲,并创建实时停车地图。

阿德莱德市在 2018 年 2 月安装了智能停车系统,并推出了一款移动应用程序:Park Adelaide,该应用将为用户提供准确的实时停车信息。该应用可以让用户定位、支付,甚至远程延长停车时长。它还会提供可用停车位的导航、停车管理信息,并在停车时段即将结束时发出提醒。阿德莱德市的智能停车系统旨在改善交通流量,减少交通拥堵,并减少碳排放。智能停车系统的详细信息可通过阿德莱德市官网查看(www.cityofadelaide.com.au/city-business/why-adelaide/adelaide-smart-city/smart-parking)。

旧金山交通管理局SAFTA)实施了 SFpark 智能停车系统(sfpark.org)。他们使用无线传感器实时检测计时停车位的占用情况。SFpark 于 2013 年推出,已将工作日的温室气体排放减少了 25%,交通量下降,驾驶员寻找停车位的时间减少了 50%。SAFTA 还报告称,通过简化停车缴费过程,损失因停车计时器损坏而减少,停车相关收入增加了约 190 万美元。

在伦敦,西敏市(iotuk.org.uk/smart-parking/#1463069773359-c0d6f90f-4dca)与 Machina Research(machinaresearch.com/login/?next=/forecasts/usecase/)合作,在 2014 年建立了智能停车系统。此前,驾驶员平均需要等待 12 分钟,导致了拥堵和污染,但自从安装了智能停车系统后,驾驶员无需等待,可以通过手机找到可用的停车位。这不仅减少了拥堵和污染,还提高了收入。

智能废物管理

垃圾收集及其妥善管理和处理是城市的重要服务。城市人口的增长要求采用更好的智能垃圾管理方法。一个智能城市应全面解决其垃圾管理问题。采用人工智能(AI)进行智能回收和垃圾管理可以提供可持续的垃圾管理系统。在 2011 年,芬兰公司 ZenRobotics(zenrobotics.com/)展示了如何利用计算机视觉和人工智能(机器人)来训练机器人,从传送带上分拣和挑选可回收材料。自那时以来,我们取得了长足的进展;许多公司提供智能垃圾管理解决方案,城市和建筑物也开始采纳这些方案。领导者和社区建设者对于智能城市基础设施部署的潜在好处有着日益增长的意识。

巴塞罗那的垃圾管理系统(ajuntament.barcelona.cat/ecologiaurbana/en/services/the-city-works/maintenance-of-public-areas/waste-management-and-cleaning-services/household-waste-collection)是一个很好的案例研究。他们在垃圾桶上安装了传感器和设备,这些设备可以向有关部门发送警报通知,一旦垃圾桶即将被填满,相关部门就会派遣垃圾收集车。他们在每个地区都保持有单独的垃圾桶,分别用于纸张、塑料、玻璃和食品垃圾。巴塞罗那的管理部门已经建立了一个由地下真空管道连接的容器网络,这些管道能够吸走垃圾并将其送到处理单元;这也消除了垃圾车收集垃圾的需求。

另一个好的案例研究是丹麦的垃圾管理(www.smartbin.com/tdc-denmark-cisco-showcase-the-future-of-smart-city-waste-collection/),由 SmartBin 提供。SmartBin 与丹麦最大的电信服务商 TDC 及思科(Cisco)合作,为一系列垃圾桶安装了传感器,这些传感器与城市数字平台集成。此外,路灯和交通信号灯也安装了传感器,这些传感器将数据发送到市政厅的控制台。这些传感器收集的实时数据帮助清洁服务更有效地规划垃圾收集路线;他们只需要去那些需要清空的地方。

在阿联酋沙迦安装了十个太阳能驱动的 Bigbelly 垃圾桶,并配备了 Wi-Fi 单元;他们计划在不久的将来部署数百个这样的智能垃圾桶,以实现可持续发展目标。

智能警务

不幸的是,犯罪是无处不在的。每个城市都有警察力量,致力于抓捕罪犯并降低犯罪率。智能城市同样需要警务:智能警务,指的是执法机构采用基于证据、数据驱动的策略,这些策略既有效、又高效、且经济。智能警务的概念大约于 2009 年出现,主要受限于预算约束。推动智能警务概念的根本思想来自赫尔曼·戈德斯坦(威斯康星大学,1979 年)。他认为,警察不应将犯罪事件视为孤立事件,而应将其视为具有历史和未来的公开问题系统。

在美国,司法援助局BJA)资助了许多智能警务倡议SPI),根据其研究结果,这些倡议显著减少了暴力犯罪。SPI 侧重于警察与研究合作伙伴的合作,研究合作伙伴负责持续的数据收集和分析,监控数据,参与解决方案的开发,并评估其影响。这些倡议帮助警察识别出以下内容:

  • 犯罪热点

  • 常犯罪犯

新加坡也已启动了其智能国家计划。几乎在城市的每个角落都安装了摄像头和传感器。通过这些设备获取的视频流,能够识别出哪些地方有人在禁烟区吸烟,或者从高楼上闲逛。摄像头使当局能够监控人群密度、公共场所的清洁情况,甚至追踪所有登记车辆的确切位置。这些摄像头的影像流被输入到一个名为虚拟新加坡的在线平台,该平台提供有关城市实时运作的信息。

智能照明

街灯是必要的,但它们消耗大量能源。智能照明系统可以帮助提高街灯的能效。除此之外,灯柱还可以配备额外的传感器,或作为 Wi-Fi 网络热点。

一个可以帮助在任何城市安装智能照明的发明是 CitySense (www.tvilight.com/citysense/),这是一款屡获殊荣的街道灯运动传感器,具有集成的无线照明控制功能。CitySense 为恶劣的外部环境设计,提供按需自适应照明。灯具可以根据行人、自行车或汽车的存在调整亮度。它通过实时网状网络触发邻*灯光,并在人体周围创建一个安全的光圈。它还配备智能过滤器,能够过滤掉由小动物或树木移动引起的干扰。该系统可以自动检测任何灯具故障,并触发维护请求。荷兰的梵高村已将 CitySense 应用于其智能街道照明系统。

值得一提的是巴塞罗那的照明大师计划,它报告了街道照明功耗的显著降低。大约在 2014 年,城市的大多数灯柱都安装了 LED 灯,并且在灯柱中安装了物联网驱动的传感器。传感器会在街道空旷时自动调暗灯光,这有助于降低能源消耗。此外,这些灯柱还充当 Wi-Fi 网络热点,并配备了监测空气质量的传感器。

智能治理

智能城市的主要目的是为其居民创造一个舒适便捷的生活。因此,智能城市基础设施在没有智能治理的情况下是不完整的。智能治理意味着利用信息和通信技术,通过不同利益相关者(包括政府和公民)之间更好的合作来提高决策质量。智能治理可以被视为智能、开放和参与性政府的基础。这需要重新塑造政府、公民和其他社会行为者的角色,并探索新技术来构建新的治理模式,包括新的关系、新的流程和新的政府结构。智能治理能够利用数据、证据和其他资源来改进决策,并能够交付满足公民需求的成果。这将提升决策过程并提高公共服务的质量。

使物联网适应智能城市及其必要步骤

建设智能城市不是一项一日之功,也不是某个人或某个组织的工作。这需要许多战略合作伙伴、领导者,甚至是公民的共同协作。这种协作的动态超出了本书的范围,但由于本书面向人工智能爱好者和工程师,我们来探讨一下人工智能社区能做些什么,哪些领域为我们提供了职业或创业的机会。任何物联网平台都必然需要以下内容:

  • 用于收集数据的智能设备网络(传感器、摄像头、执行器等)

  • 可以从低功耗物联网设备收集数据、存储数据并将其安全地转发到云端的现场(云)网关

  • 用于汇聚多个数据流并将其分发到数据湖和控制应用程序的流数据处理器

  • 存储所有原始数据的数据湖,甚至包括那些当前看似无价值的数据

  • 一个可以清理和结构化收集到的数据的数据仓库

  • 用于分析和可视化传感器收集数据的工具

  • 用于基于长期数据分析自动化城市服务的 AI 算法和技术,并寻找提高控制应用程序性能的方法

  • 用于向物联网执行器发送命令的控制应用程序

  • 用于连接智能设备和公民的用户应用程序

除此之外,还会有关于安全性和隐私的问题,服务提供商必须确保这些智能服务不会对市民的福祉构成威胁。服务本身应该易于使用和操作,以便市民能够采纳它们。

如你所见,这为 AI 工程师提供了很多就业机会。物联网生成的数据需要被处理,要真正从中受益,我们需要超越监控和基础分析。AI 工具将被用来识别传感器数据中的模式和隐藏的关联。利用机器学*/人工智能工具分析历史传感器数据可以帮助识别趋势,并基于这些趋势创建预测模型。然后,这些模型可以被控制应用使用,向物联网设备的执行器发送命令。

建设智能城市的过程将是一个迭代的过程,每次迭代都会增加更多的处理和分析。以智能交通信号灯为例,让我们看看如何逐步改进它。

与传统交通信号灯相比,我们的智能交通信号灯会根据交通流量调整信号时间。我们可以使用历史交通数据来训练模型,以揭示交通模式并调整信号时间,从而最大化平均车速,避免拥堵。这种独立的智能交通信号灯很好,但还不够。如果一个地区发生了拥堵,那如果路上的驾驶员可以被告知避免这条路线,那就太好了。为了实现这一点,我们可以增加一个额外的处理系统;它通过交通信号灯的传感器数据识别拥堵,并使用车辆或驾驶员智能手机的 GPS,告知靠*拥堵区域的驾驶员避开该路线。

下一步,交通信号灯可以增加更多传感器,比如可以监测空气质量的传感器,然后训练模型以确保在达到临界空气质量之前生成警报。

开放数据的城市

在过去的十年里,世界各地的许多城市都建立了开放数据门户。这些开放数据门户不仅帮助市民保持信息通畅,对于 AI 开发者来说也是一大福音,因为数据是驱动 AI 的动力。我们来看看一些有趣的数据门户及其提供的数据。

这篇文章在《福布斯》上列出了 90 个拥有开放数据的美国城市: https://www.forbes.com/sites/metabrown/2018/04/29/city-governments-making-public-data-easier-to-get-90-municipal-open-data-portals/#4542e6f95a0d

亚特兰大市 大亚特兰大快速交通局数据

亚特兰大都市快速交通局 (MARTA) 发布了实时公共交通数据,旨在为开发者提供机会,开发定制的 web 和移动应用程序。MARTA 平台为开发者提供了资源,供他们访问数据并利用这些数据开发应用程序 (www.itsmarta.com/app-developer-resources.aspx)。

通用交通数据格式 (GTFS) 用于提供数据。GTFS 是一个用于公共交通时刻表和地理信息的标准格式。它由一系列文本文件组成,每个文件描述了交通信息的特定方面:停靠点、路线、行程及类似的计划数据。

MARTA 还通过 RESTful API 提供数据。要访问 API,你需要安装 MARTA-Python,这是一个用于访问 MARTA 实时 API 的 Python 库。可以使用 pip 安装该 Python 库:

pip install tox

在使用 API 之前,你需要注册并申请 API 密钥 (www.itsmarta.com/developer-reg-rtt.aspx)。API 密钥将存储在 MARTA_API_KEY 环境变量中。要设置 MARTA_API_KEY,你可以使用以下命令:

在 Windows 上,使用以下命令:

set MARTA_API_KEY=<your_api_key_here>

在 Linux/MAC 上,使用以下命令:

export MARTA_API_KEY=<your_api_key_here>

它提供了两个主要的封装函数 get_buses()get_trains(),这两个函数都接受关键字参数来过滤结果:

from marta.api import get_buses, get_trains

# To obtain list of all buses
all_buses = get_buses()

# To obtain a list of buses by route
buses_route = get_buses(route=1)

# To obtain list of all trains
trains = get_trains()

# To obtain list of trains specified by line
trains_red = get_trains(line='red')

# To obtain list of trains by station
trains_station = get_trains(station='Midtown Station')

# To obtain list of trains by destination
trains_doraville = get_trains(station='Doraville')

# To obtain list of trains by line, station, and destination
trains_all = get_trains(line='blue', 
            station='Five Points Station', 
            destination='Indian Creek')

get_buses()get_trains() 函数分别返回 BusTrain 字典对象。

芝加哥 "Things Array" 数据

Things Array (AoT) 项目于 2016 年启动,项目内容包括在灯杆上安装一个传感器盒网络。传感器收集有关环境和城市活动的实时数据。生成的数据可以通过批量下载以及 API 提供给开发者和爱好者。

传感器部署在多个地理区域,每个部署区域被称为 项目,其中最大部署位于芝加哥,属于名为芝加哥的项目。

部署的物理设备被称为 节点,每个节点通过其独特的序列号 VSN 进行标识。这些节点通过网络连接在一起。节点中包含 传感器,这些传感器观察环境的各个方面,如温度、湿度、光强度和颗粒物。传感器记录的信息称为 观测数据

这些观测数据存在冗余,并可以通过 API 获取原始形式的数据。节点和观测、传感器和观测之间存在一对多的关系。项目、节点和传感器之间也存在多对多的关系。有关 AoT 项目的完整数据和详细信息,可以从芝加哥市开放数据门户访问:data.cityofchicago.org/

使用旧金山犯罪数据检测犯罪

旧金山市也有一个开放数据门户(datasf.org/opendata/),提供来自不同部门的在线数据。在本节中,我们使用提供大约 12 年(从 2003 年 1 月到 2015 年 5 月)的犯罪报告数据集,数据涵盖了旧金山市所有社区,并训练一个模型来预测发生的犯罪类别。共有 39 个离散的犯罪类别,因此这是一个多类别分类问题。

我们将使用 Apache 的 PySpark,并利用其易于使用的文本处理功能来处理这个数据集。所以第一步是创建一个 Spark 会话:

  1. 第一步是导入必要的模块并创建 Spark 会话:
from pyspark.ml.classification import LogisticRegression as LR
from pyspark.ml.feature import RegexTokenizer as RT
from pyspark.ml.feature import StopWordsRemover as SWR
from pyspark.ml.feature import CountVectorizer
from pyspark.ml.feature import OneHotEncoder, StringIndexer, VectorAssembler
from pyspark.ml import Pipeline
from pyspark.sql.functions import col
from pyspark.sql import SparkSession

spark = SparkSession.builder \
        .appName("Crime Category Prediction") \
        .config("spark.executor.memory", "70g") \
        .config("spark.driver.memory", "50g") \
        .config("spark.memory.offHeap.enabled",True) \
        .config("spark.memory.offHeap.size","16g") \
        .getOrCreate()
  1. 我们加载存储在 csv 文件中的数据集:
data = spark.read.format("csv"). \
        options(header="true", inferschema="true"). \
        load("sf_crime_dataset.csv")

data.columns

  1. 数据包含九个列:[Dates, Category, Descript, DayOfWeek, PdDistrict, Resolution, Address, X, Y],我们只需要 CategoryDescript 字段来构建训练集和测试集:
drop_data = ['Dates', 'DayOfWeek', 'PdDistrict', 'Resolution', 'Address', 'X', 'Y']
data = data.select([column for column in data.columns if column not in drop_data])

data.show(5)

  1. 现在我们拥有的数据集包含文本数据,因此我们需要进行文本处理。三个重要的文本处理步骤是:对数据进行分词、去除停用词以及将词向量化。我们将使用 RegexTokenizer,它使用正则表达式将句子分词成一个单词列表,由于标点符号或特殊字符不会对含义产生任何影响,我们只保留包含字母数字内容的单词。像 the 这样的词在文本中非常常见,但对上下文没有任何意义。我们可以使用内置的 StopWordsRemover 类去除这些词(也叫做 停用词)。我们使用标准的停用词 ["http","https","amp","rt","t","c","the"]。最后,通过使用 CountVectorizer,我们将单词转换为数字向量(特征)。正是这些数字特征将作为输入来训练模型。我们的数据输出是 Category 列,但它也是文本的,并且有 36 个不同的类别,因此我们需要将其转换为独热编码向量;PySpark 的 StringIndexer 可以轻松完成这个转换。我们将所有这些转换步骤添加到数据 Pipeline 中:
# regular expression tokenizer
re_Tokenizer = RT(inputCol="Descript", 
            outputCol="words", pattern="\\W")    

# stop words
stop_words = ["http","https","amp","rt","t","c","the"] 
stop_words_remover = SWR(inputCol="words", 
            outputCol="filtered").setStopWords(stop_words)

# bag of words count
count_vectors = CountVectorizer(inputCol="filtered",
         outputCol="features", vocabSize=10000, minDF=5)

#One hot encoding the label
label_string_Idx = StringIndexer(inputCol = "Category", 
                outputCol = "label")

# Create the pipeline
pipeline = Pipeline(stages=[re_Tokenizer, stop_words_remover,
             count_vectors, label_string_Idx])

# Fit the pipeline to data.
pipeline_fit = pipeline.fit(data)
dataset = pipeline_fit.transform(data)

dataset.show(5)

  1. 现在,数据已经准备好,我们将其分成训练集和测试集:
# Split the data randomly into training and test data sets.
(trainingData, testData) = dataset.randomSplit([0.7, 0.3], seed = 100)
print("Training Dataset Size: " + str(trainingData.count()))
print("Test Dataset Size: " + str(testData.count()))
  1. 让我们为此拟合一个简单的逻辑回归模型。在测试数据集上,它提供了 97% 的准确率。耶!
# Build the model
logistic_regrssor = LR(maxIter=20, 
                regParam=0.3, elasticNetParam=0)
# Train model with Training Data
model = logistic_regrssor.fit(trainingData)

# Make predictions on Test Data
predictions = model.transform(testData)

# evaluate the model on test data set
evaluator = MulticlassClassificationEvaluator(predictionCol="prediction")
evaluator.evaluate(predictions)

完整的代码可在 GitHub 仓库 Chapter11/SF_crime_category_detection.ipynb Jupyter Notebook 中找到。

挑战与好处

人工智能正在改变城市的运作、交付和维护公共设施的方式,从照明、交通到连接性和健康服务。然而,采用技术时,如果技术之间无法高效协同工作或与其他城市服务无法整合,可能会成为阻碍因素。因此,考虑到改造性解决方案非常重要。

另一个需要关注的重要事项是合作。为了让城市真正从智能城市的潜力中受益,必须改变思维方式。各级政府应进行长期规划,并跨多个部门协作。所有人——包括技术专家、地方政府、企业、环保人士以及公众——都必须携手合作,才能让城市成功转型为智能城市。

尽管预算可能是一个大问题,但世界各地多个城市成功实施智能城市组件的成果表明,通过正确的实施,智能城市更加经济。智能城市的转型不仅创造了就业机会,还能帮助保护环境、减少能源支出并创造更多收入。巴塞罗那市就是一个典型的例子;通过实施物联网系统,预计创造了 47,000 个就业岗位,节省了 4,250 万欧元的水费,并通过智能停车每年额外创收 3,650 万欧元。我们可以清楚地看到,城市可以从利用人工智能驱动的物联网解决方案的技术进步中受益良多。

概要

基于人工智能的物联网解决方案可以帮助连接城市并管理多个基础设施和公共服务。本章介绍了智能城市的多种应用场景,从智能照明和道路交通到互联公共交通和废物管理。从成功的案例研究中,我们还了解到,智能城市可以减少能源成本、优化自然资源的使用、提高城市安全性,并创造更健康的环境。本章列出了一些开放的城市数据门户及其提供的信息。我们使用本书中学到的工具,对 12 年间旧金山的犯罪报告数据进行分类。最后,本章讨论了建设智能城市的一些挑战与好处。

第十二章:综合所有内容

现在,我们已经理解并实现了不同的人工智能AI)/机器学*ML)算法,是时候将它们结合起来,了解每种算法最适合哪种类型的数据,并同时理解每种数据类型所需的基本预处理。在本章结束时,你将了解以下内容:

  • 可以输入模型的不同类型数据

  • 如何处理时间序列数据

  • 文本数据的预处理

  • 可以对图像数据进行的不同转换

  • 如何处理视频文件

  • 如何处理语音数据

  • 云计算选项

处理不同类型的数据

数据有各种各样的形式、大小和类型:推文、每日股价、每分钟的心跳信号、相机拍摄的照片、通过 CCTV 获取的视频、音频录音等。每种数据都包含信息,当这些数据经过正确的处理并与合适的模型结合使用时,我们可以分析这些数据,并获得关于潜在模式的高级信息。在本节中,我们将介绍每种数据类型在输入模型之前所需的基本预处理,以及可以使用的模型。

时间序列建模

时间是许多有趣的人类行为的基础,因此,AI 驱动的物联网系统必须知道如何处理时间相关的数据。时间可以显式地表示,例如,通过定期捕获数据,其中时间戳也是数据的一部分,或者隐式地表示,例如,在语音或书面文本中。能够捕捉时间相关数据中固有模式的方法称为时间序列建模

在定期间隔捕获的数据是时间序列数据,例如,股价数据就是时间序列数据。让我们来看一下苹果股票价格数据;这些数据可以从 NASDAQ 网站下载(www.nasdaq.com/symbol/aapl/historical)。或者,你可以使用pandas_datareader模块直接下载数据,通过指定数据源来实现。要在你的工作环境中安装pandas_datareader,可以使用以下命令:

pip install pandas_datareader
  1. 以下代码从 2010 年 1 月 1 日到 2015 年 12 月 31 日,从 Yahoo Finance 下载苹果公司股票价格:
import datetime
from pandas_datareader import DataReader
%matplotlib inline

Apple = DataReader("AAPL", "yahoo", 
        start=datetime.datetime(2010, 1, 1), 
        end=datetime.datetime(2015,12,31)) 
Apple.head()
  1. 下载的 DataFrame 提供了每个工作日的HighLowOpenCloseVolumeAdj Close值:

  1. 现在我们来绘制图表,如下所示:
close = Apple['Adj Close']
plt.figure(figsize= (10,10))
close.plot()
plt.ylabel("Apple stocj close price")
plt.show()

要能够建模时间序列数据,我们需要识别几个要素:趋势、季节性和稳定性。

  1. 趋势是指找出在平均情况下,测量值是否随着时间的推移而减少(或增加)。找到趋势最常见的方法是绘制移动平均,如下所示:
moving_average = close.rolling(window=20).mean()

plt.figure(figsize= (10,10))
close.plot(label='Adj Close')
moving_average.plot(label='Moving Average Window 20')
plt.legend(loc='best')
plt.show()

  1. 我们可以看到,使用窗口为 20,上升和下降趋势。 对于时间序列建模,我们应该去趋势化数据。 去趋势化可以通过从原始信号中减去趋势(移动平均值)来完成。 另一种流行的方法是使用一阶差分方法,即取相邻数据点之间的差异:
fod = close.diff()
plt.figure(figsize= (10,10))
fod.plot(label='First order difference')
fod.rolling(window=40).mean().\
        plot(label='Rolling Average')
plt.legend(loc='best')
plt.show()

  1. 季节性是与时间相关的高低规律重复出现的模式(例如,正弦系列)。 最简单的方法是在数据中找到自相关。 找到季节性后,您可以通过将数据差分到与季节长度相对应的时间滞来移除它:
# Autocorrelation
plt.figure(figsize= (10,10))
fod.plot(label='First order difference')
fod.rolling(window=40).mean().\
        plot(label='Rolling Average')
fod.rolling(window=40).corr(fod.shift(5)).\
        plot(label='Auto correlation')
plt.legend(loc='best')
plt.show()

  1. 最后一件事是确保系列是否平稳,即系列的均值不再是时间的函数。 数据的平稳性对于时间序列建模至关重要。 我们通过消除数据中存在的任何趋势或季节性来实现平稳性。 一旦数据平稳,我们可以使用回归模型对其进行建模。

传统上,时间序列数据是使用自回归和移动平均模型(如 ARMA 和 ARIMA)进行建模的。 要了解更多关于时间序列建模的信息,感兴趣的读者可以参考这些书籍:

  • Pandit, S. M., and Wu, S. M. (1983). 带应用的时间序列与系统分析(Vol. 3). 纽约:约翰·威利。

  • Brockwell, P. J., Davis, R. A., and Calder, M. V. (2002). 时间序列和预测简介(Vol. 2). 纽约:斯普林格出版社。

对于任何时间序列数据,平稳性都是一个重要的特性,无论您是使用传统的时间序列建模还是深度学*模型。 这是因为,如果一个系列具有平稳性(即使是弱平稳性),那么它意味着数据在时间上具有相同的分布,因此可以在时间上进行估计。 如果您计划使用诸如 RNN 或 LSTM 之类的深度学*模型,则在确认时间序列的平稳性后,此外,您需要对数据进行归一化,并使用滑动窗口转换将系列转换为输入-输出对,以便进行回归。 使用 scikit-learn 库和 NumPy 可以非常容易地完成这一操作:

  1. 让我们对close DataFrame 进行归一化。 归一化确保数据位于01之间。 请注意,以下图与前述步骤 3close DataFrame 的图表相同,但y轴比例现在不同:
# Normalization
from sklearn.preprocessing import MinMaxScaler
def normalize(data):
    x = data.values.reshape(-1,1)
    pre_process = MinMaxScaler()
    x_normalized = pre_process.fit_transform(x)
    return x_normalized

x_norm = normalize(close)

plt.figure(figsize= (10,10))
pd.DataFrame(x_norm, index = close.index).plot(label="Normalized Stock prices")
plt.legend(loc='best')
plt.show()

  1. 我们定义了一个window_transform()函数,它将数据系列转换为一系列输入-输出对。 例如,您想构建一个 RNN,该 RNN 将前五个值作为输出,并预测第六个值。 然后,您选择window_size = 5
# Create window from the normalized data
def window_transform(series, window_size):
    X = []
    y = []

    # Generate a sequence input/output pairs from series
    # x= <s1,s2,s3,s4,s5,... s_n> y = s_n+1 and so on
    for i in range(len(series) - window_size):
    X.append(series[i:i+window_size])
    y.append(series[i+window_size])

    # reshape each 
    X = np.asarray(X)
    X.shape = (np.shape(X)[0:2])
    y = np.asarray(y)
    y.shape = (len(y),1)

    return X,y

window_size = 7
X,y = window_transform(x_norm,window_size = window_size)

请参阅 GitHub 存储库,Chapter-12/time_series_data_preprocessing.ipynb,查看本节的完整代码。

预处理文本数据

语言在我们日常生活中扮演着非常重要的角色。对我们来说,阅读书面文本是非常自然的,但计算机呢?它们能读取文本吗?我们能让深度学*模型根据旧的模式生成新的文本吗?例如,如果我说,“昨天,我在星巴克喝了 ____”,我们大多数人都能猜出空白处是“咖啡”,但是我们的深度学*模型能做到吗?答案是肯定的;我们可以训练我们的深度学*模型来猜测下一个词(或字符)。然而,深度学*模型运行在计算机上,而计算机只理解二进制,只有 0 和 1。因此,我们需要一种方法来处理文本数据,以便将其转换为计算机易于处理的形式。此外,尽管“cat”、“CAT”和“Cat”有不同的 ASCII 表示,但它们表示的意思相同;这对我们来说很容易理解,但要让模型将它们视为相同,我们需要对文本数据进行预处理。本节将列出文本数据的必要预处理步骤,您将学*如何在 Python 中实现:

  1. 在这一节中,我们将考虑来自我最喜欢的科幻小说《基地》的小段文本,作者是艾萨克·阿西莫夫。该文本位于foundation.txt文件中。第一步是,读取文本:
f = open('foundation.txt')
text = f.read()
print(text)
  1. 文本处理的下一步是清洗数据。我们保留文本中相关的部分。在大多数情况下,标点符号不会为文本增加额外的含义,因此我们可以放心地将其去除:
# clean data
import re
# remove Punctuation
text = re.sub(r"[^a-zA-Z0-9]", " ", text) 
print(text)
  1. 清洗数据后,我们需要对文本进行规范化处理。在文本处理过程中,规范化文本意味着将所有文本转换为相同的大小写,通常是小写。为了遵循惯例,我们将文本转换为小写:
# Normalize text
# Convert to lowercase
text = text.lower() 
print(text)
  1. 一旦文本被规范化,下一步是对文本进行分词。我们可以将文本分割为单词令牌或句子令牌。为了做到这一点,您可以使用split函数,或者使用功能强大的 NLTK 模块。如果您的系统中没有安装 NLTK,可以通过pip install nltk进行安装。在接下来的例子中,我们使用 NLTK 的单词分词器来完成这项任务:
import os
import nltk
nltk.download('punkt') 
from nltk.tokenize import word_tokenize

# Split text into words using NLTK
words_nltk = word_tokenize(text)
print(words_nltk)
  1. 根据您拥有的文本类型和所做的工作,您可能需要去除停用词。停用词是出现在大多数文本样本中的词,因此不会为文本的上下文或意义增加任何信息。例如,the、a 和 an。您可以声明自己的停用词,也可以使用 NLTK 提供的停用词。在这里,我们从文本中移除english语言的stopwords
from nltk.corpus import stopwords
nltk.download('stopwords')
#Remove stop words
words = [w for w in words \
        if w not in stopwords.words("english")]

  1. 另一个可以在文本数据上进行的操作是词干提取和词形还原。这些操作用于将单词转换为规范形式:
from nltk.stem.porter import PorterStemmer

# Reduce words to their stems
stemmed = [PorterStemmer().stem(w) for w in words]
print(stemmed)

from nltk.stem.wordnet import WordNetLemmatizer

# Reduce words to their root form
lemmed = [WordNetLemmatizer().lemmatize(w) for w in words]
print(lemmed)

您可以通过 GitHub 访问包含此代码的笔记本:Chapter12/text_processing.ipynb

图像数据增强

Python 有 OpenCV,它为图像提供了非常好的支持。OpenCV 可以从 Conda 通道和 PyPi 下载安装。一旦使用 OpenCV 的imread()函数读取图像,图像就表示为一个数组。如果图像是彩色的,则通道以 BGR 顺序存储。数组中的每个元素表示相应像素值的强度(这些值的范围在 0 到 255 之间)。

假设你已经训练了一个模型来识别一个球:你给它展示一个网球,它能识别为球。接下来我们展示的这张球的图像是在缩放之后拍摄的:我们的模型还能识别它吗?一个模型的效果取决于它所训练的数据集,因此,如果模型在训练时看到了缩放后的图像,它将很容易识别出缩放后的球是一个球。一种确保数据集中有这些图像的方法是隐式地包含这些变化图像,然而,由于图像是作为数组表示的,我们可以进行数学变换来重新缩放、翻转、旋转,甚至改变强度。在现有训练图像上执行这些变换以生成新图像的过程叫做数据增强。使用数据增强的另一个好处是,您可以增加训练数据集的大小(当与数据生成器一起使用时,我们可以获得无限的图像)。

大多数深度学*库都提供了标准的 API 来进行数据增强。在 Keras(keras.io/preprocessing/image/)中,有ImageDataGenerator,在 TensorFlow-TfLearn 中,我们有ImageAugmentation。TensorFlow 也有操作符来执行图像转换和变换(www.tensorflow.org/api_guides/python/image)。在这里,我们将看看如何使用 OpenCV 强大的库进行数据增强,并创建我们自己的数据生成器:

  1. 我们导入了必要的模块:OpenCV 用于读取和处理图像,numpy用于矩阵操作,Matplotlib 用于可视化图像,shuffle来自 scikit-learn 用于随机打乱数据,以及 Glob 用于查找目录中的文件:
import cv2 # for image reading and processsing
import numpy as np
from glob import glob
import matplotlib.pyplot as plt
from sklearn.utils import shuffle
%matplotlib inline
  1. 我们读取了必要的文件。对于这个示例,我们从 Google 图像搜索中下载了一些前美国总统巴拉克·奥巴马的图像:
img_files = np.array(glob("Obama/*"))
  1. 我们创建了一个函数,可以在图像中随机引入以下任意变形:在 0 到 50 度范围内随机旋转,随机改变图像强度,随机将图像水平和垂直平移最多 50 个像素,或者随机翻转图像:
def distort_image(img, rot = 50, shift_px = 40):
    """
    Function to introduce random distortion: brightness, flip,
    rotation, and shift 
    """
    rows, cols,_ = img.shape
    choice = np.random.randint(5)
    #print(choice)
    if choice == 0: # Randomly rotate 0-50 degreee
        rot *= np.random.random() 
        M = cv2.getRotationMatrix2D((cols/2,rows/2), rot, 1)
        dst = cv2.warpAffine(img,M,(cols,rows))
    elif choice == 1: # Randomly change the intensity
        hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
        ratio = 1.0 + 0.4 * (np.random.rand() - 0.5)
        hsv[:, :, 2] = hsv[:, :, 2] * ratio
        dst = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
    elif choice == 2: # Randomly shift the image in horizontal and vertical direction
        x_shift,y_shift = np.random.randint(-shift_px,shift_px,2)
        M = np.float32([[1,0,x_shift],[0,1,y_shift]])
        dst = cv2.warpAffine(img,M,(cols,rows))
    elif choice == 3: # Randomly flip the image
        dst = np.fliplr(img)
    else:
        dst = img

    return dst
  1. 在下图中,您可以看到前述函数在我们数据集中随机选择的图像上的结果:

  1. 最后,您可以使用 Python 的yield创建数据生成器,生成您需要的任意数量的图像:
# data generator
def data_generator(samples, batch_size=32, validation_flag = False):
    """
    Function to generate data after, it reads the image files, 
    performs random distortions and finally 
    returns a batch of training or validation data
    """
    num_samples = len(samples)
    while True: # Loop forever so the generator never terminates
 shuffle(samples)
        for offset in range(0, num_samples, batch_size):
            batch_samples = samples[offset:offset+batch_size]
            images = []

            for batch_sample in batch_samples:
                if validation_flag: # The validation data consists only of center image and without distortions
                    image = cv2.imread(batch_sample)
                    images.append(image)
                    continue
                else: # In training dataset we introduce distortions to augment it and improve performance
                    image = cv2.imread(batch_sample)
                    # Randomly augment the training dataset to reduce overfitting
                    image = distort_image(image)
                    images.append(image)

        # Convert the data into numpy arrays
        X_train = np.array(images)

        yield X_train 

train_generator = data_generator(img_files,  batch_size=32)

Chapter12/data_augmentation.ipynb文件包含了这一部分的代码。

处理视频文件

视频本质上是静态图像(帧)的集合,因此,如果我们能够从视频中提取图像,就可以将我们信任的 CNN 网络应用到这些图像上。唯一需要做的就是将视频转换为帧列表:

  1. 我们首先导入必要的模块。我们需要 OpenCV 来读取视频并将其转换为帧。我们还需要math模块进行基本的数学运算,以及 Matplotlib 来可视化这些帧:
import cv2 # for capturing videos
import math # for mathematical operations
import matplotlib.pyplot as plt # for plotting the images
%matplotlib inline
  1. 我们使用 OpenCV 函数读取视频文件,并通过属性标识符5来获取视频的帧率(docs.opencv.org/2.4/modules/highgui/doc/reading_and_writing_images_and_video.html#videocapture-get):
videoFile = "video.avi" # Video file with complete path
cap = cv2.VideoCapture(videoFile) # capturing the video from the given path
frameRate = cap.get(5) #frame rate
  1. 我们通过read()函数逐帧读取视频中的所有帧。虽然我们每次只读取一帧,但我们只保存每秒的第一帧。这样,我们可以覆盖整个视频,同时减少数据大小:
count = 0
while(cap.isOpened()):
    frameId = cap.get(1) #current frame number
    ret, frame = cap.read()
    if (ret != True):
        break
    if (frameId % math.floor(frameRate) == 0):
        filename ="frame%d.jpg" % count
        count += 1
        cv2.imwrite(filename, frame)

cap.release()
print ("Finished!")
  1. 让我们来可视化一下我们保存的第五帧:
img = plt.imread('frame5.jpg') # reading image using its name
plt.imshow(img)

本代码所用的视频文件来自 Ivan Laptev 和 Barbara Caputo 维护的网站(www.nada.kth.se/cvap/actions/)。代码可以在 GitHub 上找到:Chapter12/Video_to_frames.ipynb

使用 CNN 进行视频分类的最佳论文之一是 Andrej Karpathy 等人撰写的大规模视频分类与卷积神经网络。你可以在这里访问:www.cv-foundation.org/openaccess/content_cvpr_2014/html/Karpathy_Large-scale_Video_Classification_2014_CVPR_paper.html

音频文件作为输入数据

另一个有趣的数据类型是音频文件。将语音转换为文本或分类音频声音的模型以音频文件作为输入。如果你想处理音频文件,那么你需要使用librosa模块。处理音频文件的方法有很多;我们可以将其转换为时间序列并使用循环神经网络。另一种取得良好结果的方法是将其作为一维或二维图案,并训练 CNN 进行分类。一些采用这种方法的优秀论文如下:

  • Hershey, S., Chaudhuri, S., Ellis, D. P., Gemmeke, J. F., Jansen, A., Moore, R. C., 和 Slaney, M. (2017 年 3 月). 用于大规模音频分类的 CNN 架构. 在声学、语音和信号处理(ICASSP)2017 年 IEEE 国际会议(第 131-135 页)。IEEE.

  • Palaz, D., Magimai-Doss, M., 和 Collobert, R. (2015). 基于 CNN 的语音识别系统分析,使用原始语音作为输入。在第十六届国际语音通信协会年会上。

  • Zhang, H., McLoughlin, I., and Song, Y. (2015, April). 使用卷积神经网络进行稳健的声音事件识别。在《声学、语音与信号处理》(ICASSP),2015 年 IEEE 国际会议中(第 559-563 页)。IEEE。

  • Costa, Y. M., Oliveira, L. S., and Silla Jr, C. N. (2017). 使用声谱图对卷积神经网络进行音乐分类的评估。应用软计算,52,28–38。

我们将使用librosa模块读取音频文件,并将其转换为一维声音波形和二维声谱图。你可以通过以下方式在你的 Anaconda 环境中安装librosa

pip install librosa
  1. 在这里,我们将导入numpymatplotliblibrosa。我们将从librosa数据集中获取示例音频文件:
import librosa
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# Get the file path to the included audio example
filename = librosa.util.example_audio_file()
  1. librosa的加载函数返回音频数据,表示为一维 NumPy 浮点数组的时间序列。我们可以将它们用作时间序列,甚至用作 CNN 的一维模式:
input_length=16000*4
def audio_norm(data):
    # Function to Normalize
    max_data = np.max(data)
    min_data = np.min(data)
    data = (data-min_data)/(max_data-min_data) 
    return data

def load_audio_file(file_path, 
            input_length=input_length):
    # Function to load an audio file and 
    # return a 1D numpy array 
    data, sr = librosa.load(file_path, sr=None)

    max_offset = abs(len(data)-input_length)
    offset = np.random.randint(max_offset)
    if len(data)>input_length:
        data = data[offset:(input_length+offset)]
    else:
        data = np.pad(data, (offset, 
            input_size - len(data) - offset), 
            "constant")    

    data = audio_norm(data)
    return data
  1. 以下是归一化后的一维音频波形图:
data_base = load_audio_file(filename)
fig = plt.figure(figsize=(14, 8))
plt.title('Raw wave ')
plt.ylabel('Amplitude')
plt.plot(np.linspace(0, 1, input_length), data_base)
plt.show()

  1. librosa还有一个melspectrogram函数,我们可以使用它来生成梅尔声谱图,该图可以作为 CNN 的二维图像使用:
def preprocess_audio_mel_T(audio, sample_rate=16000, 
        window_size=20, #log_specgram
        step_size=10, eps=1e-10):

    mel_spec = librosa.feature.melspectrogram(y=audio,
             sr=sample_rate, n_mels= 256)
    mel_db = (librosa.power_to_db(mel_spec,
         ref=np.max) + 40)/40
    return mel_db.T

def load_audio_file2(file_path,
             input_length=input_length):
    #Function to load the audio file  
    data, sr = librosa.load(file_path, sr=None)

    max_offset = abs(len(data)-input_length)
    offset = np.random.randint(max_offset)
    if len(data)>input_length:
        data = data[offset:(input_length+offset)]
    else:
        data = np.pad(data, (offset, 
            input_size - len(data) - offset),
            "constant")

    data = preprocess_audio_mel_T(data, sr)
    return data
  1. 这是相同音频信号的梅尔声谱图:
data_base = load_audio_file2(filename)
print(data_base.shape)
fig = plt.figure(figsize=(14, 8))
plt.imshow(data_base)

你可以在 GitHub 仓库中的Chapter12/audio_processing.ipynb文件找到示例的代码文件。

云计算

将 AI 算法应用于物联网生成的数据需要计算资源。随着大量云平台以具有竞争力的价格提供服务,云计算提供了一个具有成本效益的解决方案。在如今众多的云平台中,我们将讨论三大云平台提供商,它们占据了大部分市场份额:Amazon Web ServiceAWS)、Google Cloud PlatformGCP)和 Microsoft Azure。

AWS

Amazon 提供几乎所有云计算功能,从云数据库、云计算资源,到云分析,甚至是建立安全数据湖的空间。它的物联网核心允许用户将设备连接到云端。它提供一个统一的仪表盘,可以用来控制你注册的服务。它按小时收费,提供这些服务已有* 15 年。Amazon 不断升级其服务,提供更好的用户体验。你可以通过其网站了解更多关于 AWS 的信息:aws.amazon.com/

它允许新用户免费使用其许多服务整整一年。

Google Cloud Platform

Google Cloud Platform (cloud.google.com/) 也提供了众多服务。它提供云计算、数据分析、数据存储,甚至云 AI 产品,用户可以使用这些预训练模型和服务来生成自定义的模型。该平台允许按分钟计费。它提供企业级安全服务。Google Cloud 控制台是访问和控制所有 GCP 服务的唯一入口。GCP 为第一年提供 $300 的信用额度,让你可以免费访问其所有服务。

Microsoft Azure

Microsoft Azure 也提供了各种云服务。Microsoft 云服务的最大特点是其易用性;你可以轻松地将其与现有的 Microsoft 工具集成。与 AWS 相比,它声称成本低五倍。像 AWS 和 GCP 一样,Azure 也提供了价值 $200 的一年免费试用。

你可以使用这些云服务来开发、测试和部署你的应用程序。

总结

本章重点提供了处理不同类型数据的工具以及如何为深度学*模型准备这些数据。我们从时间序列数据开始。本章接着详细介绍了文本数据如何进行预处理。本章展示了如何执行数据增强,这是一项对图像分类和物体检测至关重要的技术。接着,我们进入了视频处理部分;我们展示了如何从视频中提取图像帧。然后,本章介绍了音频文件;我们从音频文件中生成了时间序列和梅尔频谱图。最后,我们讨论了云平台,并介绍了三大云服务提供商的功能和服务。

posted @ 2025-07-17 15:21  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报