总目录

Python机器学习实践指南

TensorFlow机器学习项目实战

TensorFlow技术解析与实战

目　　录

版权信息

版权声明

内容提要

作者简介

审阅者简介

译者简介

译者序

前言

第1章 Python机器学习的生态系统

1.1 数据科学/机器学习的工作流程

1.1.1 获取

1.1.2 检查和探索

1.1.3 清理和准备

1.1.4 建模

1.1.5 评估

1.1.6 部署

1.2 Python库和功能

1.2.1 获取

1.2.2 检查

1.2.3 准备

1.2.4 建模和评估

1.2.5 部署

1.3 设置机器学习的环境

1.4 小结

第2章 构建应用程序，发现低价的公寓

2.1 获取公寓房源数据

使用import.io抓取房源数据

2.2 检查和准备数据

2.2.1 分析数据

2.2.2 可视化数据

2.3 对数据建模

2.3.1 预测

2.3.2 扩展模型

2.4 小结

第3章 构建应用程序，发现低价的机票

3.1 获取机票价格数据

3.2 使用高级的网络爬虫技术检索票价数据

3.3 解析DOM以提取定价数据

通过聚类技术识别异常的票价

3.4 使用IFTTT发送实时提醒

3.5 整合在一起

3.6 小结

第4章 使用逻辑回归预测IPO市场

4.1 IPO市场

4.1.1 什么是IPO

4.1.2 近期IPO市场表现

4.1.3 基本的IPO策略

4.2 特征工程

4.3 二元分类

4.4 特征的重要性

4.5 小结

第5章 创建自定义的新闻源

5.1 使用Pocket应用程序，创建一个监督训练的集合

5.1.1 安装Pocket的Chrome扩展程序

5.1.2 使用Pocket API来检索故事

5.2 使用embed.ly API下载故事的内容

5.3 自然语言处理基础

5.4 支持向量机

5.5 IFTTT与文章源、Google表单和电子邮件的集成

通过IFTTT设置新闻源和Google表单

5.6 设置你的每日个性化新闻简报

5.7 小结

第6章 预测你的内容是否会广为流传

6.1 关于病毒性，研究告诉我们了些什么

6.2 获取分享的数量和内容

6.3 探索传播性的特征

6.3.1 探索图像数据

6.3.2 探索标题

6.3.3 探索故事的内容

6.4 构建内容评分的预测模型

6.5 小结

第7章 使用机器学习预测股票市场

7.1 市场分析的类型

7.2 关于股票市场，研究告诉我们些什么

7.3 如何开发一个交易策略

7.3.1 延长我们的分析周期

7.3.2 使用支持向量回归，构建我们的模型

7.3.3 建模与动态时间扭曲

7.4 小结

第8章 建立图像相似度的引擎

8.1 图像的机器学习

8.2 处理图像

8.3 查找相似的图像

8.4 了解深度学习

8.5 构建图像相似度的引擎

8.6 小结

第9章 打造聊天机器人

9.1 图灵测试

9.2 聊天机器人的历史

9.3 聊天机器人的设计

9.4 打造一个聊天机器人

9.5 小结

第10章 构建推荐引擎

10.1 协同过滤

10.1.1 基于用户的过滤

10.1.2 基于项目的过滤

10.2 基于内容的过滤

10.3 混合系统

10.4 构建推荐引擎

10.5 小结

欢迎来到异步社区！

返回总目录

版权信息

书名：Python机器学习实践指南

ISBN：978-7-115-44906-1

本书由人民邮电出版社发行数字版。版权所有，侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用，未经授权，不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟，与我们共同保护知识产权。

如果购买者有侵权行为，我们可能对该用户实施包括但不限于关闭该帐号等维权措施，并可能追究法律责任。

•　著　　　　[美] Alexander T. Combs

译　　　　黄　申

责任编辑　陈冀康

•　人民邮电出版社出版发行　　北京市丰台区成寿寺路11号

邮编　100164 　电子邮件　315@ptpress.com.cn

网址　http://www.ptpress.com.cn

•　读者服务热线：(010)81055410

反盗版热线：(010)81055315

版权声明

Copyright © Packt Publishing 2016. First published in the English language under the title Python Machine Learning Blueprints.

All Rights Reserved.

本书由美国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可，对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有，侵权必究。

内容提要

机器学习是近年来渐趋热门的一个领域，同时Python语言经过一段时间的发展也已逐渐成为主流的编程语言之一。本书结合了机器学习和Python语言两个热门的领域，通过易于理解的项目详细讲述了如何构建真实的机器学习应用程序。

全书共有10章。第1章讲解了Python机器学习的生态系统，剩余9章介绍了众多与机器学习相关的算法，包括聚类算法、推荐引擎等，主要包括机器学习在公寓、机票、IPO市场、新闻源、内容推广、股票市场、图像、聊天机器人和推荐引擎等方面的应用。

本书适合Python程序员、数据分析人员、对算法感兴趣的读者、机器学习领域的从业人员及科研人员阅读。

作者简介

Alexander T. Combs是一位经验丰富的数据科学家、策略师和开发人员。他有金融数据抽取、自然语言处理和生成，以及定量和统计建模的背景。他目前是纽约沉浸式数据科学项目的一名全职资深讲师。

审阅者简介

Kushal Khandelwal是一位数据科学家和全栈开发人员。他的兴趣包括构建可扩展的机器学习和图像处理的软件应用。他擅长Python编码，并对各种开源项目做出了积极的贡献。他目前担任Truce.in的技术主管，这是一家以农民为中心的创业公司，Kushal致力于创建可扩展的Web应用程序来帮助农民。

译者简介

黄申博士，现任IBM研究院资深科学家，毕业于上海交通大学计算机科学与工程专业，师从俞勇教授。微软学者、IBM ExtremeBlue天才计划成员。长期专注于大数据相关的搜索、推荐、广告以及用户精准化领域。曾在微软亚洲研究院、eBay中国、沃尔玛1号店（现京东1号店）和大润发飞牛网担任要职，带队完成了若干公司级的战略项目。同时发表了20多篇国际论文，并拥有10多项国际专利，《计算机工程》特邀审稿专家，《Elasticsearch实战》中文版的译者，2016年出版的《大数据架构商业之路》一书销量和口碑双赢，续作《大数据架构和算法实现之路》将于2017年中出版。2015年，因对业界做出卓越贡献，获得美国政府颁发的“美国杰出人才”称号。

译者序

谈到为什么要翻译这本书，还是一段机缘巧合。那是2015年的下半年，当时我正在撰写自己的原创书籍《大数据架构商业之路：从业务需求到技术方案》。在那本书中，我希望结合一个创业的故事，展示各个阶段可能遇到的大数据课题、业务需求，以及相对应的技术方案，甚至是实践解析。其中，最挑战的部分莫过于案例的分析到技术方案，再到框架编码的逐步展开。因为之前对于这种写作模式没有相关的经验，让人很是苦恼。我也搜寻了市面上相关的中英文书籍，可惜并未发现特别好的范例作为参考。

一次偶然的机会，我在Amazon.com上发现了Alexander T. Combs的《Python Machine Learning Blueprints》。当时此书尚未出版，还是试读本。在阅读样章之后我发现这种写作模式就是我想要的，没有太多的理论和说教，而是结合我们日常生活都会经历的方方面面，包括房产、金融、旅游和电子商务等，提供了可以直接上手的教学内容，让读者可以身临其境，乐在其中，轻松了解机器学习的实用知识。这正是我想要学习的风格！于是我采纳了这种模式，并结合自己的项目经验，一口气完成了《大数据架构商业之路：从业务需求到技术方案》一书。上市之后，读者对这种理论和案例相结合的方式很是赞许。所以，我对《Python Machine Learning Blueprints》一书心存感激，对它何时上市也很是关注。

终于，2016年的 7 月底，该书的英文版正式发行。我迫不及待地阅读完了原版，和当初试读的感觉一样，这是一本很有创意的书，而且Python和机器学习都是最近几年的技术热点，如果能将这么棒的内容介绍给广大国内的读者，那是多么令人激动的事情！于是，我抱着试试看的心态，联系了人民邮电出版社的编辑陈冀康老师。很幸运，当时此书还没有译者，陈老师审阅我的试译稿之后也表示满意，于是我很荣幸地成为了此书的译者。

不过在翻译的过程中，我也发现了不少细节上的疑问，于是我主动联系了原书的作者Alexander，他总是非常仔细地解答这些问题，使得我信心大增，可以确保译文尽可能地贴近原文。而编辑陈老师也对此举表示了充分的肯定。在此，我要对Alexander和陈老师的帮助表示衷心的感谢。当然，我也要感谢父母和妻儿的支持，为了此书，我陪伴你们的时间更少了，而你们丝毫没有怨言，让我可以安心地完成每次的写作。

在翻译此书的岁月中，Python、机器学习及其应用在国内外都获得了空前的关注，相关的社区也保持了非常好的活跃度，相信这个技术方向在将来还有很大的空间。希望本书能帮助到每一位热爱Python和机器学习的朋友，为中国的人工智能事业尽一份绵薄之力。如果您对本书中的技术细节感兴趣，可以通过如下渠道联系我，很期待和大家的互动和交流。

QQ　　36638279

微信　　18616692855

邮箱　　s_huang790228@hotmail.com

LinkedIn　　https://cn.linkedin.com/in/shuang790228

扫一扫就能微信联系作者：

个人

公众号

前言

机器学习正在迅速成为数据驱动型世界的一个必备模块。许多不同的领域如机器人、医学、零售和出版等，都需要依赖这门技术。在这本书中，你将学习如何一步步构建真实的机器学习应用程序。

通过易于理解的项目，你将学习如何处理各种类型的数据，如何以及何时应用不同的机器学习技术，包括监督学习和无监督学习。

本书中的每个项目都同时提供了教学和实践。例如，你将学习如何使用聚类技术来发现低价的机票，以及如何使用线性回归找到一间便宜的公寓。本书以通俗易懂、简洁明了的方式，教你如何使用机器学习来收集、分析并操作大量的数据。

本书涵盖的内容

第1章，Python机器学习的生态系统，深入Python，它有一个深度活跃的开发者社区，而且许多开发者来自科学社区。这为Python提供了丰富的科学计算库。在本章中，我们将讨论这些关键库的特性以及如何准备你的环境，以最好地利用它们。

第2章，构建应用程序，发现低价的公寓，指导我们构建第一个机器学习应用程序，我们从一个最小但实际的例子开始：建设应用程序来识别低价的公寓。到本章结束，我们将创建一个应用程序，使得寻找合适的公寓变得更容易点。

第3章，构建应用程序，发现低价的机票，演示了如何构建应用程序来不断地监测票价。一旦出现异常价格，应用程序将提醒我们，可以快速采取行动。

第4章，使用逻辑回归预测IPO市场，展示了我们如何使用机器学习决定哪些IPO值得仔细研究，而哪些可以直接跳过。

第5章，创建自定义的新闻源，介绍如何构建一个系统，它会了解你对于新闻的品味，而且每天都可以为你提供个性化的新闻资讯。

第6章，预测你的内容是否会广为流传，检查一些被大家广泛分享的内容，并试图找到这种内容相对于其他人们不愿分享的内容有哪些特点。

第7章，使用机器学习预测股票市场，讨论如何构建和测试交易策略。当你试图设计属于自己的系统时，有无数的陷阱要避免，这是一个几乎不可能完成的任务。但是，这个过程有很多的乐趣，而且有的时候，它甚至可以帮你盈利。

第8章，建立图像相似度的引擎，帮助你构建高级的、基于图像的深度学习应用。我们还将涵盖深度学习的算法来了解为什么它们是如此的重要，以及为什么它们成为了最近研究的热点。

第9章，打造聊天机器人，演示如何从头构建一个聊天机器人。读完之后，你将了解更多关于该领域的历史及其未来前景。

第10章，构建推荐引擎，探讨不同类型的推荐系统。我们将看到它们在商业中是如何实现和运作的。我们还将实现自己的推荐引擎来查找GitHub资料库。

阅读本书需要准备什么

你需要的是Python 3.x和建立真实机器学习项目的渴望。你可以参考随本书的详细代码列表。

本书的读者

本书的目标读者包括了解数据科学的Python程序员、数据科学家、架构师，以及想要构建完整的、基于Python的机器学习系统的人员。

约定

在这本书中，你会发现许多文本样式，以区分不同种类的信息。这里是某些样式的例子和它们的含义。

文本中的代码、数据库表名称、文件夹名称、文件名、文件扩展名、路径名、虚构的URL、用户输入和Twitter句柄如下所示：“这点可以通过在我们的数据框上调用.corr()来实现。”

代码块的格式设置如下。

<category>
<pattern>I LIKE TURTLES</pattern>
<template>I feel like this whole <set name="topic">turle</set>
thing could be a problem. What do you like about them?</template>
</category>

任何命令行输入或输出的写法如下。

sp = pd.read_csv(r'/Users/alexcombs/Downloads/spy.csv')
sp.sort_values('Date', inplace=True)

新术语和重要词语以粗体显示。

读者反馈

我们非常欢迎读者的反馈。让我们知道你对这本书有什么想法——你喜欢哪些内容或不喜欢哪些内容。读者的反馈对我们而言很重要，因为它有助于我们打造各种主题，而且让你获益更多。

对于一般的反馈，通过电子邮件feedback@packtpub.com发送，并在消息的主题中提及书的标题。

如果你擅长某个专业的主题，并且你有兴趣撰写或合著一本书，请参阅我们的作者指南www.packtpub.com/authors。

客户支持

现在你是一名自豪的Packt书籍所有者，我们将做一些事情来帮助你从这次购买中获得最大收益。

下载示例代码

在http://www.packtpub.com，你可以通过自己的账户来下载此书的示例代码文件。如果你在其他地方购买此书，你可以访问http://www.packtpub.com/support并注册，我们将文件直接发送给你。

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

1．使用你的电子邮件地址和密码登录或注册我们的网站。

2．将光标指针悬停在顶部的SUPPORT选项卡上。

3．单击Code Downloads & Errata。

4．在搜索框Search中输入书籍的名称。

5．选择你要下载代码文件的图书。

6．在下拉菜单中，选择你在哪里购买的此书。

7．单击Code Download。

在Packt Publishing的网站上，你也可以单击该书主页上的Code Files按钮来下载代码文件。可以在搜索框中输入图书的名称来访问其主页。请注意，你需要登录到你的Packt账户。

一旦文件下载完毕，请确保使用以下软件的最新版本来解压缩或提取文件夹。

- Windows版WinRAR / 7-Zip。

- Mac版Zipeg / iZip / UnRarX。

- Linux版7-Zip / PeaZip。

该书的代码包也托管在GitHub上：https://github.com/packtpublishing/pythonmachin elearningblueprints。我们还有丰富的来自其他书籍的代码包和视频，位于https://github.com/ PacktPublishing/。去看一下吧！

勘误

虽然我们已经采取一切谨慎的措施，以确保内容的准确性，但错误在所难免。如果你在我们的书中发现一个错误——也许在正文中，也许在代码中——请向我们报告，我们将非常感激。这样，你可以让其他读者避免挫折，并帮助我们改进本书的后续版本。如果你发现任何错误，请访问这个链接进行报告：http://www.packtpub.com/submit-errata，选择你的书，单击Errata Submission Form链接，然后输入错误的详细信息。一旦此勘误通过验证，你的提交将被接受，勘误信息将被上传到我们的网站或添加到任何该主题Errata部分现有的勘误表。

要查看以前提交的勘误，请访问https://www.packtpub.com/books/content/support并在搜索字段中输入书籍的名称。所需信息将出现在Errata部分中。

盗版行为

在互联网上出现正版材料的盗版，是所有媒体面临的一个持续性的问题。在Packt，我们非常重视版权和许可的保护。如果你在互联网上，发现我们作品任何形式的非法副本，请立即向我们提供地址或网站名称，以便我们请求补偿。

请通过copyright@packtpub.com与我们联系，并提供疑似盗版材料的链接。

我们感谢你的帮助，这样可以保护我们的作者，并让我们继续为你提供宝贵的内容。

疑问

如果你对本书的任何方面有问题，可以通过questions@packtpub.com与我们联系，我们将尽最大努力解决这个问题。

第1章　Python机器学习的生态系统

机器学习正在迅速改变我们的世界。作为人工智能的核心，我们几乎每天都会读到机器学习如何改变日常的生活。一些人认为它会带领我们进入一个风格奇异的高科技乌托邦；而另一些人认为我们正迈向一个高科技天启时代，将与窃取我们工作机会的机器人和无人机敢死队进行持久的战争。不过，虽然权威专家们可能会喜欢讨论这些夸张的未来，但更为平凡的现实是，机器学习正在快速成为我们日常生活的固定装备。随着我们微小但循序渐进地改进自身与计算机以及周围世界之间的互动，机器学习正在悄悄地改善着我们的生活。

如果你在Amazon.com这样的在线零售商店购物，使用Spotify或Netflix这样的流媒体音乐或电影服务，甚至只是执行一次Google搜索，你就已经触碰到了机器学习的应用。使用这些服务的用户会产生数据，这些数据会被收集、汇总并送入模型，而模型最终会为每个用户创建个性化的体验来完善服务。

想要深入到机器学习应用的开发中，现在就是一个理想的时机。你会发现，Python是开发这些应用的理想选择。Python拥有一个深度的、活跃的开发者社区，许多开发者也来自科学家的社区。这为Python提供了一组丰富的科学计算库。在本书中，我们将讨论并使用这些来自Python科学栈的库。

在接下来的章节中，我们将一步步学习如何建立各种不同的机器学习应用。但是，在真正开始之前，我们将使用本章剩下的篇幅讨论这些关键库的特性，以及如何准备能充分利用它们的环境。

我们将在本章中介绍以下主题。

- 数据科学/机器学习的工作流程。

- 工作流中每个阶段的库。

- 设置你的环境。

1.1　数据科学/机器学习的工作流程

打造机器学习的应用程序，与标准的工程范例在许多方面都是类似的，不过有一个非常重要的方法有所不同：需要将数据作为原材料来处理。数据项目成功与否，很大程度上依赖于你所获数据的质量，以及它是如何被处理的。由于数据的使用属于数据科学的领域，理解数据科学的工作流程对于我们也有所帮助：整个过程要按照图1-1中的顺序，完成六个步骤：获取，检查和探索，清理和准备，建模，评估和最后的部署。

在这个过程中，还经常需要绕回到之前的步骤，例如检查和准备数据，或者是评估和建模，但图1-1所示的内容可以描述该过程较高层次的抽象。

图1-1

现在让我们详细讨论每一个步骤。

1.1.1　获取

机器学习应用中的数据，可以来自不同的数据源，它可能是通过电子邮件发送的CSV文件，也可能是从服务器中拉取出来的日志，或者它可能需要构建自己的Web爬虫。数据也可能存在不同的格式。在大多数情况下，它是基于文本的数据，但稍后将看到，构建处理图像甚至视频文件的机器学习应用，也是很容易的。不管是什么格式，一旦锁定了某种数据，那么了解该数据中有什么以及没有什么，就变得非常重要了。

1.1.2　检查和探索

一旦获得了数据，下一步就是检查和探索它们。在这个阶段中，主要的目标是合理地检查数据，而实现这一点的最好办法是发现不可能或几乎不可能的事情。举个例子，如果数据具有唯一的标识符，检查是否真的只有一个；如果数据是基于价格的，检查是否总为正数；无论数据是何种类型，检查最极端的情况。它们是否有意义？一个良好的实践是在数据上运行一些简单的统计测试，并将数据可视化。此外，可能还有一些数据是缺失的或不完整的。在本阶段注意到这些是很关键的，因为需要在稍后的清洗和准备阶段中处理它。只有进入模型的数据质量好了，模型的质量才能有保障，所以将这一步做对是非常关键的。

1.1.3　清理和准备

当所有的数据准备就绪，下一步是将它转化为适合于模型使用的格式。这个阶段包括若干过程，例如过滤、聚集、输入和转化。所需的操作类型将很大程度上取决于数据的类型，以及所使用的库和算法的类型。例如，对于基于自然语言的文本，其所需的转换和时间序列数据所需的转换是非常不同的。全书中，我们将会看到一些转换的的例子。

1.1.4　建模

一旦数据的准备完成后，下一阶段就是建模了。在这个阶段中，我们将选择适当的算法，并在数据上训练出一个模型。在这个阶段，有许多最佳实践可以遵循，我们将详细讨论它们，但是基本的步骤包括将数据分割为训练、测试和验证的集合。这种数据的分割可能看上去不合逻辑——尤其是在更多的数据通常会产生更好的模型这种情况下——但正如我们将看到的，这样做可以让我们获得更好的反馈，理解该模型在现实世界中会表现得如何，并避免建模的大忌：过拟合。

1.1.5　评估

一旦模型构建完成并开始进行预测，下一步是了解模型做得有多好。这是评估阶段试图回答的问题。有很多的方式来衡量模型的表现，同样，这在很大程度上依赖于所用数据和模型的类型，不过就整体而言，我们试图回答这样的问题：模型的预测和实际值到底有多接近。有一堆听上去令人混淆的名词，例如根均方误差、欧几里德距离，以及F1得分，但最终，它们还是实际值与预估值之间的距离量度。

1.1.6　部署

一旦模型的表现令人满意，那么下一个步骤就是部署了。根据具体的使用情况，这个阶段可能有不同的形式，但常见的场景包括将其作为另一个大型应用程序中的某个功能特性，一个定制的Web应用程序，甚至只是一个简单的cron作业。

1.2　Python库和功能

现在，我们已经对数据科学工作流的每一步有了初步的理解，下面来看看在每一步中，存在哪些有用的Python库和功能可供选择。

1.2.1　获取

访问数据常见的方式之一是通过REST风格的API接口，需要知道的库是Python Request库（http://www.python-requests.org/en/latest/）。它被称为给人类使用的HTTP，为API的交互提供了一个整洁和简单的方式。

让我们来看一个使用Requests进行交互的例子，它从GitHub的API中拉取数据。在这里，我们将对该API进行调用，并请求某个用户的starred库列表。

import requests
r = requests.get(r"https://api.github.com/users/acombs/starred")
r.json()

这个请求将以JSON文档的形式，返回用户已经标记为starred的所有存储库以及它们的属性。图1-2是上述调用后输出结果的一个片段。

图1-2

Requests库有数量惊人的特性——这里无法全部涵盖，我建议你看看上面提供的链接所指向的文档。

1.2.2　检查

由于数据检查是机器学习应用开发中关键的一步，我们现在来深入了解几个库，它们将在此项任务中很好地为我们服务。

1．Jupyter记事本

许多库有助于减轻数据检查过程的工作负荷。首先是带有IPython（http://ipython.org/）的Jupyter记事本。这是一个全面的、交互式的计算环境，对于数据探索是非常理想的选择。和大多数开发环境不同，Jupyter记事本是一个基于Web的前端（相对于IPython的内核而言），被分成单个的代码块或单元。根据需要，单元可以单独运行，也可以一次全部运行。这使得开发人员能够运行某个场景，看到输出结果，然后回到代码，做出调整，再看看所产生的变化——所有这些都无需离开记事本。图1-3是在Jupyter记事本中进行交互的样例。

图1-3

请注意，我们在这里做了一系列的事情，并不仅仅是和IPython的后端进行交互，而且也和终端shell进行了交互。这个特定的实例运行了Python 3.5的内核，但如果你愿意，也可以很容易地运行Python 2.X的内核。在这里，我们已经引入了Python os库，并进行了一次调用，找到当前的工作目录（单元＃2），你可以看到输入代码单元格下方的输出。然后，我们在单元#3中使用os库改变了这个目录，但是在单元＃4中停止使用os库，而是开始使用基于Linux的命令。这是通过在单元前添加！符号来完成的。在单元＃6中可以看到，我们甚至能够将shell的输出保存到一个Python变量（file_two）。这是一个很棒的功能，使文件操作变成了一项简单的任务。

现在，让我们来看看使用该记事本所进行的一些简单的数据操作。这也是我们首次介绍另一个不可或缺的库：pandas。

2．Pandas

Pandas是一个卓越的数据分析工具。根据Pandas的文档（http://pandas.pydata.org/pandas-docs/version/0.17.1/）：

它有一个更广泛的目标，就是成为任何语言中，最强大和灵活的开源数据分析/操作工具。

即使它还没有达到这个目标，也不会差得太远。现在让我们来看看。

import os
import pandas as pd
import requests

PATH = r'/Users/alexcombs/Desktop/iris/'

r=
requests.get('https://archive.ics.uci.edu/ml/machine-learning-databases/iri
s/iris.data')

with open(PATH + 'iris.data', 'w') as f:
f.write(r.text)

os.chdir(PATH)

df = pd.read_csv(PATH + 'iris.data', names=['sepal length', 'sepal width',
'petal length', 'petal width', 'class'])

df.head()

前面的代码和屏幕截图如图1-4所示，我们已经从https://archive.ics.uci.edu/ml/datasets/Iris下载了一个经典的机器学习数据集：iris.data，并将其写入iris目录。这实际上是一个CSV文件，通过Pandas，我们进行了一个调用并读取了该文件。我们还增加了列名，因为这个特定的文件缺一个标题行。如果该文件已经包含了一个标题行，Pandas会自动解析并反映这一点。和其他CSV库相比，Pandas将其变为一个简单的操作。

图1-4

解析文件只是该库的一个小功能。对适合于单台机器的数据集而言，Pandas是个终极的工具，这有点像Excel。就像流行的电子表格程序，操作的基本单位是表格形式的数据列和行。在Pandas的术语中，数据列称为系列（Series），而表格称为数据框（DateFrame）。

使用之前截屏中同样的iris数据框，让我们来看看几个常见的操作。

df['sepal length']

前面的代码生成图1-5的输出。

图1-5

第一个操作是通过列名，从数据框中选择某一列。执行数据切片的另一种方式是使用.ix[row,column]标注。让我们使用下面这个标注，来选择前两列和前四行。

df.ix[:3, :2]

前面的代码生成图1-6的输出。

使用.ix标注和Python列表切片的语法，我们能够选择该数据框中的一小片。现在，让我们更进一步，使用列表迭代器并只选择描述width的列。

df.ix[:3, [x for x in df.columns if 'width' in x]]

前面的代码生成图1-7所示的输出。

图1-6

图1-7

我们在这里所做的是创建一个列表，该列表是所有列的一个子集。前面的df.columns返回所有列的列表，而我们的迭代使用了一个条件查询，只选择标题中含有width字样的列。显然，在这种情况下，我们可以很容易地拼写出希望在列表中出现的列，但是这里展示了处理大规模数据集时该库所具有的能力。

我们已经看到了，如何基于其在数据框中的位置，来选择数据的分片，现在来看看另一种选择数据的方法。这次，我们将根据某些特定的条件，来选择数据的一个子集。我们首先列出所有可用的唯一类，然后选择其中之一。

df['class'].unique()

前面的代码生成图1-8的输出。

图1-8

df[df['class']=='Iris-virginica']

在图1-9所示最右侧的一列中，我们可以看到数据框只包含Iris-virginica类的数据。事实上，选择之后图1-11中数据框的大小是50行，比图1-10中原来的150行要小一些。

df.count()

df[df['class']=='Iris-virginica'].count()

图1-9

图1-10

图1-11

我们还可以看到，在左侧的索引保留了原始行号。现在，可以将这些数据保存为一个新的数据框并重置索引，如下面的代码和截图1-12所示。

virginica = df[df['class']=='Iris-virginica'].reset_index(drop=True)
virginica

图1-12

我们通过在某个列上放置条件来选择数据，现在来添加更多的条件。我们将回到初始的数据框，并使用两个条件选择数据。

df[(df['class']=='Iris-virginica')&(df['petal width']>2.2)]

上述代码生成图1-13的输出。

图1-13

数据框现在只包含来自Iris-virginica类、而且花瓣宽度大于2.2的数据。

现在，让我们使用Pandas，从虹膜数据集中获取一些快速的描述性统计数据。

df.describe()

上述代码生成图1-14的输出。

图1-14

随着数据框的.describe()方法被调用，我们收到了各相关列的描述性统计信息（请注意，类别信息被自动删除了，因为它在这里是不相关的）。如果想要更为详细的信息，还可以传入自定义的百分比。

df.describe(percentiles=[.20,.40,.80,.90,.95])

上述代码生成图1-15的输出。

图1-15

接下来，让我们检查这些特征之间是否有任何相关性。这可以通过在数据框上调用.corr()来完成。

df.corr()

上述代码生成图1-16的输出。

图1-16

默认地，系统返回每个行-列对中的Pearson相关系数。通过传递方法的参数，还可以切换到Kendall's tau或Spearman's秩相关系数（例如，.corr(method="spearman")或.corr(method="kendall")）。

3．可视化

目前为止，我们已经看到如何选择数据框的某一部分，并从数据中获取汇总的统计信息，现在让我们学习如何通过可视化的方式来观测数据。不过首先要回答的问题是，为什么要花费心思进行可视化的视察呢？来看一个例子就能明白这是为什么了。

表1-1展示了四组不同序列的x值和y值的汇总统计。

表1-1

序列的x和y

取值

x的平均值

9

y的平均值

7.5

序列的x样本方差

11

序列的y样本方差

4.1

x和y之间的相关性

0.816

回归线

y=3.00+0.500x

基于四组序列拥有相同的汇总统计，我们可能会认为这些系列的可视化看上去也是相似。我们当然是错误的，非常错误。这四个序列是安斯库姆四重奏的一部分，他们被刻意制造出来用于说明可视化数据检查的重要性。每个序列绘制在图1-17中。

安斯库姆四重奏的网址：https://en.wikipedia.org/wiki/Anscombe%27s_quartet。

显然，经过可视化的观察之后，我们不再会认为这些数据集是相同的。所以，现在我们能理解可视化的重要性了，下面来看看一对用于可视化的、很有价值的Python库。

图1-17

Matplotlib库

我们将要看到的第一个库是matplotlib。这是Python绘图库的鼻祖了。最初人们创建它是为了仿效MATLAB的绘图功能，现在它自己已经发展成为特性完善的库了，并拥有超多的功能。对于那些没有MATLAB背景的使用者，可能很难理解所有这些部件是如何共同协作来创造图表的。

我们将所有的部件拆分为多个逻辑模块，便于大家理解都发生了些什么。在深入理解matplotlib之前，让我们先设置Jupyter记事本，以便看清每个图像。要做到这一点，需要将以下几行添加到import声明中。

import matplotlib.pyplot as plt
plt.style.use('ggplot')
%matplotlib inline
import numpy as np

第一行引入了matplotlib，第二行将风格设置为近似R中的ggplot库（这需要matplotlib 1.41），第三行设置插图，让它们在记事本中可见，而最后一行引入了numpy。本章稍后，我们将在一些操作中使用numpy。

现在，让我们使用下面的代码，在鸢尾花Iris数据集上生成第一个图：

fig, ax = plt.subplots(figsize=(6,4))
ax.hist(df['petal width'], color='black');
ax.set_ylabel('Count', fontsize=12)
ax.set_xlabel('Width', fontsize=12)
plt.title('Iris Petal Width', fontsize=14, y=1.01)

前面的代码生成图1-18中的输出。

图1-18

即使是在这个简单的例子中，也发生了很多事情，让我们来逐行分析。第一行创建了宽度为6英寸和高度为4英寸的一个插图。然后，我们通过调用.hist()并传入数据，依照iris数据框绘制了花瓣宽度的直方图。这里还将直方图中柱子的颜色设置为black（黑色）。接下来的两行分别在y轴和x轴上放置标签，最后一行为全图设置了标题。其中使用y轴的参数调整了标题在y轴方向相对于图片顶部的位置，并微微增加了默认字体的大小。这使得我们从花瓣宽度的数据得到了一个很漂亮的直方图。现在，让我们进一步扩展，为iris数据集的每一列生成直方图。

fig, ax = plt.subplots(2,2, figsize=(6,4))

ax[0][0].hist(df['petal width'], color='black');
ax[0][0].set_ylabel('Count', fontsize=12)
ax[0][0].set_xlabel('Width', fontsize=12)
ax[0][0].set_title('Iris Petal Width', fontsize=14, y=1.01)

ax[0][1].hist(df['petal length'], color='black');
ax[0][1].set_ylabel('Count', fontsize=12)
ax[0][1].set_xlabel('Lenth', fontsize=12)
ax[0][1].set_title('Iris Petal Lenth', fontsize=14, y=1.01)

ax[1][0].hist(df['sepal width'], color='black');
ax[1][0].set_ylabel('Count', fontsize=12)
ax[1][0].set_xlabel('Width', fontsize=12)
ax[1][0].set_title('Iris Sepal Width', fontsize=14, y=1.01)

ax[1][1].hist(df['sepal length'], color='black');
ax[1][1].set_ylabel('Count', fontsize=12)
ax[1][1].set_xlabel('Length', fontsize=12)
ax[1][1].set_title('Iris Sepal Length', fontsize=14, y=1.01)

plt.tight_layout()

上述代码的输出显示如图1-19所示。

图1-19

显然，这不是最有效的编码方法，但是对于展示matplotlib是如何工作的很有用处。请注意，我们现在是通过ax数组来绘制四个子插图，而不是之前例子中的单一子插图对象ax。新增加的代码是调用plt.tight_layout()，该方法将很好地自动调整子插图，以避免排版上显得过于拥挤。

现在来看看matplotlib所提供的一些其他类型的画图模式。一个有用的类型是散点图。这里，我们将在x轴和y轴分布绘画花瓣宽度和花瓣长度。

fig, ax = plt.subplots(figsize=(6,6))
ax.scatter(df['petal width'],df['petal length'], color='green')
ax.set_xlabel('Petal Width')
ax.set_ylabel('Petal Length')
ax.set_title('Petal Scatterplot')

上述的代码生成了图1-20所示的输出。

图1-20

如前所述，我们可以添加多个子插图，来检视每个方面。

我们可以考察的另一种类型是简单的线图。这里来看看花瓣长度的插图。

fig, ax = plt.subplots(figsize=(6,6))
ax.plot(df['petal length'], color='blue')
ax.set_xlabel('Specimen Number')
ax.set_ylabel('Petal Length')
ax.set_title('Petal Length Plot')

上述的代码生成了图1-21所示的输出。

基于这个简单的线图，我们已经可以看到对于每个类别存在鲜明的长度差别——请记住样本数据集在每个类别拥有50个排序的样例。这就告诉我们，花瓣长度很可能是用于区分类别的一个有用特征。

图1-21

让我们来看看matplotlib库中最后一个类型的图表：条形图。这也许是最为常见的图表之一。这里将使用三类鸢尾花中每个特征的平均值绘制一个条形图，而且为了让其更有趣，我们将使用堆积条形图，它附带了若干新的matplotlib特性。

fig, ax = plt.subplots(figsize=(6,6))
bar_width = .8
labels = [x for x in df.columns if 'length' in x or 'width' in x]
ver_y = [df[df['class']=='Iris-versicolor'][x].mean() for x in labels]
vir_y = [df[df['class']=='Iris-virginica'][x].mean() for x in labels]
set_y = [df[df['class']=='Iris-setosa'][x].mean() for x in labels]
x = np.arange(len(labels))
ax.bar(x, vir_y, bar_width, bottom=set_y, color='darkgrey')
ax.bar(x, set_y, bar_width, bottom=ver_y, color='white')
ax.bar(x, ver_y, bar_width, color='black')
ax.set_xticks(x + (bar_width/2))
ax.set_xticklabels(labels, rotation=-70, fontsize=12);
ax.set_title('Mean Feature Measurement By Class', y=1.01)
ax.legend(['Virginica','Setosa','Versicolor'])

上述的代码生成图1-22所示的输出。

图1-22

为了生成条形图，我们需要将x和y的值传递给.bar()方法。在这种情况下，x值将只是我们感兴趣的特征的长度的数组，在这个例子中是4，或者是数据框中列的数量。函数np.arange()是产生这个数值的简单方法，但也可以轻松地手动输入这个数组。由于我们不想在x轴显示1到4，因此调用了.set_xticklabels()方法并传入想要显示的列名。为了让x轴的标签对齐，我们还需要调整标签之间的间隔。这就是为什么将xticks设置为x加上bar_width值的一半，而我们先前已经将bar_width设置为0.8。这里y值来自每个类别中特征的平均值。然后，通过调用.bar()绘制每个插图。需要注意的是，我们为每个序列传入一个bottom参数，这个参数将该序列的y点最小值设置为其下面那个序列的y点最大值。这就能创建堆积条形图。最后，添加了一个图例来描述每个序列。按照从顶部到底部条形放置的顺序，我们依次在图例中插入了相应的名称。

Seaborn库

我们接下来将看到的可视化库被称为seaborn（http://stanford.edu/~mwaskom/software/seaborn/index.html）。它是专门为统计可视化而创建的库。事实上，seaborn可以和pandas数据框完美地协作，框中的列是特征而行是观测的样例。这种数据框的风格被称为整洁的数据，而且它是机器学习应用中最常见的形式。

现在让我们来看看seaborn的能力。

import seaborn as sns
sns.pairplot(df, hue="class")

仅仅通过这两行代码，我们就可以得到图1-23所示的输出。

图1-23

就在刚刚我们详细地讨论了matplotlib错综复杂的细微之处，而生成这张图的简单性却显而易见。仅仅使用了两行代码，所有的特征都已经被绘画出来，彼此对照并标上了正确的标签。那么，当seaborn使得这种可视化变得如此简单的时候，学习matplotlib是在浪费时间吗？幸运的是，情况并非如此，seaborn是建立在matplotlib之上的。事实上，我们可以使用所学的matplotlib知识来修改并使用seaborn。让我们来看看另一个可视化的例子。

fig, ax = plt.subplots(2, 2, figsize=(7, 7))
sns.set(style='white', palette='muted')
sns.violinplot(x=df['class'], y=df['sepal length'], ax=ax[0,0])
sns.violinplot(x=df['class'], y=df['sepal width'], ax=ax[0,1])
sns.violinplot(x=df['class'], y=df['petal length'], ax=ax[1,0])
sns.violinplot(x=df['class'], y=df['petal width'], ax=ax[1,1])
fig.suptitle('Violin Plots', fontsize=16, y=1.03)
for i in ax.flat:
plt.setp(i.get_xticklabels(), rotation=-90)
fig.tight_layout()

以上代码行生成图1-24所示的输出。

图1-24

这里，我们为4个特征分别生成了小提琴图。小提琴图显示了特征的分布情况。例如，我们可以很容易地看到类别irissetosa的花瓣长度高度聚集在1～2厘米之间，而类别iris-virginica分散在4～7厘米之间。我们还可以看到，之前在构建matplotlib图形时使用了许多相同的代码。这里主要的区别在于加入了sns.plot()调用来取代之前的ax.plot()调用 。我们还使用了fig.suptitle()方法，在所有的子图上添加了一个总标题，而不是在每个单独的子图上各自添加标题。另一个明显的添加部分，是每个子图的遍历取代了之前xticklabels的轮换。我们调用ax.flat()，遍历每个子图的轴，并使用.setp()设置特定的属性。这可以让我们不再需要像之前matplotlib子图代码那样，单独地敲打ax[0][0]…ax[1][1]，并设置属性。

我们在这里使用的图是一个很好的开始，但是你可以使用matplotlib和seaborn创建上百种不同风格的图形。我强烈建议深入研究这两个库的文档，这将是非常值得的。

1.2.3　准备

我们已经学到了很多有关检查数据的内容，现在让我们开始学习如何处理和操作数据。这里你将了解 pandas的Series.map()、Series.apply()、DataFrame.apply()、DataFrame.applymap()和 DataFrame.groupby()方法。这些对于处理数据而言是非常有价值的，而且在特征工程的机器学习场景下特别有用，我们将在后面的章节详细地讨论这个概念。

1．Map

Map方法适用于序列数据，所以在我们的例子中将用它来转变数据框的某个列，它就是一个pandas的序列 。假设我们觉得类别的名字太长了，并且希望使用特殊的3字母代码系统对其进行编码。为了实现这点，我们将使用map方法并将一个Python字典作为其参数。这里将为每个单独的鸢尾花类型传入替换的文本。

df['class'] = df['class'].map({'Iris-setosa': 'SET', 'Iris-virginica':
'VIR', 'Iris-versicolor': 'VER'})
df

前面的代码生成图1-25的输出。

图1-25

下面来看看这里做了些什么。我们在现有class列的每个值上运行了map的方法。由于每个值都能在Python字典中找到，所以它会被添加到被返回的序列。我们为返回序列赋予了相同的class名，所以它替换了原有的class列。如果我们选择了一个不同的名字，例如short class，那么这一列会被追加到数据框，然后我们将有初始的class列外加新的short class列。

我们还可以向map方法传入另一个序列或函数，来执行对某个列的转变，但这个功能在apply 方法也是可用的，下面这节会讨论该方法。字典的功能是map方法所独有的，这也是选择map而不是apply进行单列转变的最常见原因。现在让我们来看看apply方法。

2．Apply

Apply的方法让我们既可以在数据框上工作，也可以在序列上工作。我们将从一个也能使用map的例子开始，然后再讨论只能使用apply的示例。

继续使用iris数据框，让我们根据花瓣的宽度来创建新的列。之前我们看到花瓣宽度的平均值为1.3。现在，在数据框中创建一个新的列——宽花瓣，它包含一个基于petal width列的二进制值。如果花瓣宽度等于或宽于中值，那么我们将其编码为1，而如果它小于中值，我们将其编码为0。为了实现这点，这里将在petal width这列使用apply方法。

df['wide petal'] = df['petal width'].apply(lambda v: 1 if v >= 1.3 else 0)
df

前面的代码生成图1-26所示的输出。

图1-26

这里发生了几件事情，让我们一步一步来看。首先，我们为所要创建的列名简单地使用了列选择的语法，向数据框追加一个新的列，在这个例子中是wide petal。我们将这个新列设置为apply方法的输出。这里在petal width列上运行apply，并返回了wide petal列的相应值。Apply方法作用于petal width列的每个值。如果该值大于或等于1.3，函数返回1；否则，返回0。这种类型的转换在机器学习领域是相当普遍的特征工程转变，所以最好熟悉如何执行它。

现在让我们来看看如何在数据框上使用apply，而不是在一个单独的序列上。现在将基于petal area来创建一个新的特征。

df['petal area'] = df.apply(lambda r: r['petal length'] * r['petal width'],
axis=1)
df

前面的代码生成图1-27的输出。

图1-27

请注意，这里不是在一个序列上调用apply，而是在整个数据框上。此外正是由于在整个数据框上调用了apply，我们传送了axis=1的参数来告诉pandas，我们要对行运用函数。如果传入了axis=0，那么该函数将对列进行操作。这里，每列都是被顺序地处理，我们选择将petal length的值和petal width的值相乘。得到的序列就将成为数据框中的petal area列。这种能力和灵活性使得pandas成为了数据操作不可或缺的工具。

3．Applymap

我们已经学习了列的操作，并解释了如何在行上运作，不过，假设你想对数据框里所有的数据单元执行一个函数，那又该怎么办呢？这时applymap就是合适的工具了。这里看一个例子。

df.applymap(lambda v: np.log(v) if isinstance(v, float) else v)

前面的代码生成图1-28的输出。

图1-28

在这里，我们在数据框上调用了applymap，如果某个值是float类型的的实例，那么就会获得该值的对数（np.log()利用numpy库返回该值）。这种类型的检查，可以防止系统返回一个错误信息，或者是为字符串型的class列或整数形的wide petal列返回浮动值。Applymap的常见用法是根据一定的条件标准来转变或格式化每一个单元。

4．Groupby

现在，让我们来看一个非常有用，但对于新pandas用户往往难以理解的操作——数据框.groupby()方法。我们将逐步分析若干例子，来展示这个最为重要的功能。

这个groupby操作就如其名——它基于某些你所选择的类别对数据进行分组。让我们使用iris数据集来看一个简单的例子。这里将回到之前的步骤，重新导入最初的iris数据集，并运行第一个groupby操作。

df.groupby('class').mean()

前面的代码生成图1-29所示的输出。

图1-29

系统按照类别对数据进行了划分，并且提供了每个特征的均值。让我们现在更进一步，得到每个类别完全的描述性统计信息。

df.groupby('class').describe()

前面的代码生成图1-30所示的输出。

图1-30

现在我们可以看到每个class完整的分解。再来看看其他一些可执行的groupby操作。之前，我们看出花瓣长度和宽度在不同类之间有一些比较明显的区别，这里让我们看看如何使用groupby来发现这一点。

df.groupby('petal width')['class'].unique().to_frame()

前面的代码生成图1-31所示的输出。

在这个例子中，我们通过和每个唯一类相关联的花瓣宽度，对类别进行分组。这里测量组的数量还是可管理的，但是如果这个数量将要增大很多，那么我们很可能需要将测量分割为不同的范围。正如之前看到的，这点可以使用apply方法来完成。

图1-31

现在来看一个自定义的聚集函数。

df.groupby('class')['petal width']\
.agg({'delta': lambda x: x.max() - x.min(), 'max': np.max, 'min': np.min})

前面的代码生成图1-32所示的输出。

图1-32

在这段代码中，我们根据类别来分组花瓣宽度的时候，使用np.max和np.min这两个函数（两个np函数来自numpy库），以及返回最大花瓣宽度减去最小花瓣宽度的lambda函数。这些都以字典的形式，传递给.agg()方法，以此返回一个将字典键值作为列名的数据框。可以仅仅运行函数本身或者传递函数的列表，不过列的名称所含信息量就更少了[1]。

我们只是刚刚接触了groupby方法的一些功能，还有很多东西要学习，所以我建议你阅读这里的文档：http://pandas.pydata.org/pandas-docs/stable/。

对于准备阶段中如何操纵和准备数据，我们现在有了扎实的基本理解，而下一步就是建模。这里即将讨论Python机器学习生态系统中最为主要的一些库。

1.2.4　建模和评估

对于统计建模和机器学习，Python有许多很优秀的、文档详实的库供选择。下面只谈及最流行的几个库。

1．Statsmodels

我们要介绍的第一个库是statsmodels（http://statsmodels.sourceforge.net/）。

Statsmodels是用于探索数据、估计模型，并运行统计检验的Python包。在这里，让我们使用它来构建一个简单的线性回归模型，为setosa类中花萼长度和花萼宽度之间的关系进行建模。

首先，通过散点图来目测这两者的关系。

fig, ax = plt.subplots(figsize=(7,7))
ax.scatter(df['sepal width'][:50], df['sepal length'][:50])
ax.set_ylabel('Sepal Length')
ax.set_xlabel('Sepal Width')
ax.set_title('Setosa Sepal Width vs. Sepal Length', fontsize=14,
y=1.02)

前面的代码生成图1-33所示的输出。

图1-33

我们可以看到，似乎有一个正向的线性关系，也就是说，随着花萼宽度的增加，花萼长度也会增加。接下来我们使用statsmodels，在这个数据集上运行一个线性回归模型，来预估这种关系的强度。

import statsmodels.api as sm

y = df['sepal length'][:50]
x = df['sepal width'][:50]
X = sm.add_constant(x)

results = sm.OLS(y, X).fit()
print(results.summary())

前面的代码生成图1-34所示的输出。

图1-34所示的屏幕截图显示了这个简单回归模型的结果。由于这是一个线性回归，该模型的格式为Y = Β0+Β1X，其中B0为截距而B1是回归系数。在这里，最终公式是Sepal Length = 2.6447 + 0.6909 × Sepal Width。我们也可以看到，该模型的R2值是一个可以接受的0.558，而p值 （Prob）是非常显著的——至少对于这个类而言。

图1-34

现在让我们使用结果对象来绘制回归线。

fig, ax = plt.subplots(figsize=(7,7))
ax.plot(x, results.fittedvalues, label='regression line')
ax.scatter(x, y, label='data point', color='r')
ax.set_ylabel('Sepal Length')
ax.set_xlabel('Sepal Width')
ax.set_title('Setosa Sepal Width vs. Sepal Length', fontsize=14,
y=1.02)
ax.legend(loc=2)

前面的代码生成图1-35所示的输出。

通过绘制results.fittedvalues，我们可以获取从模型所得的回归线。

在statsmodels包中，还有一些其他的统计函数和测试模块，我希望你能去探索它们。对于Python中标准的统计建模而言，这是一个非常有用的包。接下来，让我们开始学习Python机器学习包中的王者：scikit-learn。

2．scikit-learn

scikit-learn是一个令人惊喜的Python库，作者们为其设计了无与伦比的文档，为几十个算法提供了统一的API接口。它建立在Python科学栈的核心模块之上，也就是NumPy、SciPy、pandas和matplotlib。scikit-learn覆盖的一些领域包括：分类、回归、聚类、降维、模型选择和预处理。

图1-35

我们来看看几个例子。首先，使用iris数据建立一个分类器，然后学习如何利用scikit-learn 的工具来评估得到的模型。

在scikit-learn中打造机器学习模型的第一步，是理解数据应该如何构建。独立变量应该是一个数字型的n×m维的矩阵X、一个因变量y和n×1维的向量。该y向量可以是连续的数字，也可以是离散的数字，还可以是离散的字符串类型。然后将这些向量传递到指定分类器的.fit()方法。这是使用scikit-learn最大的好处，每个分类器都尽最大可能地使用同样的方法。如此一来，它们的交换使用易如反掌。

让我们来看看在第一个例子中，如何实现。

from sklearn.ensemble import RandomForestClassifier
from sklearn.cross_validation import train_test_split

clf = RandomForestClassifier(max_depth=5, n_estimators=10)

X = df.ix[:,:4]
y = df.ix[:,4]

X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=.3)

clf.fit(X_train,y_train)

y_pred = clf.predict(X_test)

rf = pd.DataFrame(list(zip(y_pred, y_test)), columns=['predicted',
'actual'])
rf['correct'] = rf.apply(lambda r: 1 if r['predicted'] ==
r['actual'] else 0, axis=1)
rf

前面的代码生成图1-36的输出。

图1-36

现在，让我们来看看下面的代码。

rf['correct'].sum()/rf['correct'].count()

这会生成图1-37的输出。

图1-37

在前面的几行代码中，我们建立、训练并测试了一个分类器，它在Iris数据集上具有95％的准确度。这里逐项分析每个步骤。在代码的前两行，我们做了几个导入，前两个是从scikit-learn，值得庆幸的是在import语句中其名字缩短为sklearn了。第一个导入的是一个随机森林分类器，第二个导入的是一个将数据分成训练组和测试组的模块。出于某些原因，这种数据切分在机器学习应用的构建中是很关键的。我们将在以后的章节讨论这些，现在只需要知道这是必需的。模块train_test_split还会打乱数据的先后顺序，这也是非常重要的，因为原有的顺序可能包含误导实际预测的信息。

在这本书中，我们将使用最新的Python版本，撰写本书的时候是版本3.5。如果你使用的Python是版本2.x，你需要添加额外的import语句，让整数的除法和Python 3.x中的一样运作。没有这一行，你的准确度将被报告为0，而不是95%。该行是：

from __future__ import division

在import语句之后，第一行看上去很奇怪的代码实例化了我们的分类器，这个例子中是随机森林分类器。这里选择一个使用10个决策树的森林，而每棵树最多允许五层的判定深度。如此实施的原因是为了避免过拟合（overfitting），我们将在后面的章节中深入讨论这个话题。

接下来的两行创建了X矩阵和y向量。初始的iris数据框包含四个特征：花瓣的宽度和长度，以及花萼的宽度和长度。这些特征被选中并成为独立特征矩阵X。最后一列，iris类别的名称，就成为了因变的y向量。

然后这些被传递到train_test_split方法，该方法将数据打乱并划分为四个子集，X_train，X_test，y_train和y_test。参数test_size被设置为0.3，这意味着数据集的30％将被分配给X_test和y_test部分，而其余的将被分配到训练的部分，X_train和y_train。

接下来，使用训练数据来拟合我们的模型。一旦模型训练完毕，再通过测试数据来调用分类器的预测方法。请记住，测试数据是分类器没有处理过的数据。预测的返回结果是预估标签的列表。然后，我们创建对应实际标签与预估标签的数据框。最终，我们加和正确的预测次数，并将其除以样例的总数，从而看出预测的准确率。现在让我们看看哪些特征提供了最佳的辨别力或者说预测能力。

f_importances = clf.feature_importances_
f_names = df.columns[:4]
f_std = np.std([tree.feature_importances_ for tree in
clf.estimators_], axis=0)

zz = zip(f_importances, f_names, f_std)
zzs = sorted(zz, key=lambda x: x[0], reverse=True)

imps = [x[0] for x in zzs]
labels = [x[1] for x in zzs]
errs = [x[2] for x in zzs]
plt.bar(range(len(f_importances)), imps, color="r", yerr=errs,
align="center")
plt.xticks(range(len(f_importances)), labels);

从图1-38可以看出，正如我们根据之前可视化分析所作出的预期，花瓣的长度和宽度对于区分iris的类别而言，具有更好的辨别力。不过，这些数字究竟来自哪里？随机森林有一个名为.feature_importances_的方法，它返回特征在决策树中划分叶子节点的相对能力。如果一个特征能够将分组一致性地、干净拆分成不同的类别，那么它将具有很高的特征重要性。这个数字的总和将始终为1。也许你注意到，在这里我们已经包括了标准差，它将有助于说明每个特征有多么的一致。这是如此生成的：对于每个特征，获取每10棵决策树的特征重要性，并计算标准差。

图1-38

现在，让我们看看另一个使用scikit-learn的例子。现在，切换分类器并使用支持向量机（SVM）。

from sklearn.multiclass import OneVsRestClassifier
from sklearn.svm import SVC
from sklearn.cross_validation import train_test_split

clf = OneVsRestClassifier(SVC(kernel='linear'))

X = df.ix[:,:4]
y = np.array(df.ix[:,4]).astype(str)

X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=.3)
clf.fit(X_train,y_train)

y_pred = clf.predict(X_test)

rf = pd.DataFrame(list(zip(y_pred, y_test)), columns=['predicted',
'actual'])
rf['correct'] = rf.apply(lambda r: 1 if r['predicted'] ==
r['actual'] else 0, axis=1)
rf

前面的代码生成图1-39的输出。

图1-39

现在，让我们执行下面这行代码。

rf['correct'].sum()/rf['correct'].count()

前面的代码生成图1-40的输出。

图1-40

这里，我们将模型切换为支持向量机，而没有改变代码的本质。唯一的变化是引入了SVM而不是随机森林，以及实例化分类器的那一行代码（标签y需要一个小小的格式改变，这是因为SVM无法像随机森林分类器那样，将这些标签解释为NumPy的字符串）。

这些仅仅是scikit-learn能力的一小部分，但它应该可以说明这个伟大的工具对于机器学习应用而言强大的功能和力量。还有许多其他的机器学习库，我们在这里没有机会讨论，不过会在后面的章节中探讨，这里我强烈建议，如果你是第一次使用机器学习库，而又想要一个强大的通用工具，scikit-learn将是你明智的选择。

1.2.5　部署

将一个机器学习模型放入生产环境时，有许多可用的选项。它基本上取决于应用程序的性质。部署小到在本地机器上运行cron作业，大到在Amazon EC2实例上部署全面的实现。

这里不会深入具体实施的细节，不过全书中我们将有机会研究不同的部署实例。

1.3　设置机器学习的环境

本章已经介绍了一些可以通过pip（Python的包管理器）单独安装的库。不过，我强烈建议你安装预打包的解决方案，例如Continuum's Anaconda Python发行版。这是一个单一的可执行程序，包含几乎所有需要的软件包和依赖者。而且，因为这个发行版是针对Python科学栈的用户，它本质上是一个一劳永逸的解决方案。

Anaconda也包括软件包管理器，使得包的更新变得如此简单。

只需简单地键入conda update <package_name>，那么库就会被更新到最近的稳定版本。

1.4　小结

在本章中，我们介绍了数据科学/机器学习的工作流程。我们学习了如何让数据一步步地通过流水线的每个阶段，从最初的获取一直到最终的部署。本章还涵盖了Python科学栈中最重要的一些功能库及其关键特性。

现在，我们将利用这方面的知识和经验，开始创造独特的、有价值的机器学习应用程序。在下一章，你将看到如何运用回归模型来发现一个便宜的公寓，让我们开始吧！

[1]　译者注：就是将函数以列表的形式，而不是字典的形式进行传送。这样就缺乏"delta"、"min"和"max"这样的键值作为列名，自动生成的列名就不会有太多的含义。

第2章　构建应用程序，发现低价的公寓

在上一章中，我们学习了使用数据的基本要素。现在，我们将运用这些知识，构建第一个机器学习的应用程序。我们将从一个规模很小，但是非常实际的例子开始：建立一个应用程序来识别定价较低的公寓。

如果你曾经找过公寓，你就会明白这个过程可能是多么令人沮丧。它不仅耗费时间，而且即使当你发现一个自己喜欢的公寓，你怎么知道它就是合适的公寓？

你可能在心里设定了目标预算和区域。但是，如果你和我是同一类人，那么你也许愿意做一些权衡。例如，我住在纽约市，那么靠近地铁站这样的便利设施毫无疑问是一个很大的加分项。但是，这点到底值多少钱？我是否应该拿有电梯的住所和靠近火车站的住所进行交换？步行到火车站多少分钟？抵得过走上楼梯吗？租房的时候，有几十个这样的问题需要考虑。那么，如何使用机器学习来帮助我们进行决策呢？

本章的剩余部分会探索这一点。我们不能得到所有问题的答案（稍后你会更清楚其中的原因），不过在本章的结尾，我们将创建一个应用程序，使得找公寓这个问题变得稍微简单一点。

我们将在本章讨论以下主题。

- 获取公寓的房源数据。

- 检查和准备数据。

- 可视化数据。

- 构建回归模型。

- 预测。

2.1　获取公寓房源数据

在20世纪70年代初，如果你想购买股票，就需要聘请经纪人，他们会收取你将近1%的固定佣金。如果你想购买一张机票，你需要联系旅行社代理，他们将赚取大约7%的佣金。如果你想出售一间房子，你会联系一个房地产代理，他们赚取6％的佣金。在2016年，你基本上可以免费地做前两者。而对于最后一项，情况仍然和20世纪70年代的一样，保持不变。

为什么是这种情况？更重要的是，这些与机器学习有什么关系？现实是，这一切都归结于数据，以及谁能够访问它。

你可能想象着通过API或爬取房地产网站，就能够很容易地访问珍贵的地产房源数据。你错了，如果你打算遵守这些网站的条款和条件的话。房地产数据受到房地产经纪人国家协会（NAR）的严格控制，由他们运行多项房源服务（MLS）。这是一种聚合房源数据的服务，只有经纪人和代理商可以使用它，而且还需要花费巨资。所以，可以想象，他们不太希望任何人都能大量地下载这些数据。

这是不幸的，因为开放这些数据无疑会催生许多有价值的消费者应用程序。对于占家庭预算最大比重的购买决策而言，这点看上去尤其重要。

话虽如此，也不是完全没有希望。虽然依据条款所言，直接从MLS提供商获取数据是被禁止的，但是我们可以利用第三方工具来拉取数据。

现在，我们来看一个有用的工具，它可以帮助我们获取所需的数据。

使用import.io抓取房源数据

有许多优秀的、基于Python的库用于抓取网页，包括requests、Beautiful Soup和Scrapy。我们将探讨其中的一些，后面的章节还会讨论更多。为了达到此处的目的，我们将使用免费的替代方案：

Import.io（http://www.import.io）是一个免费的、基于Web的服务，它会自动抓取网页。这是一个很好的选择，让我们可以避免从头开始创建一个网络爬虫。好在，它为房地产的房源数据提供了一个示例API接口，数据来自Zillow.com。

图2-1的图片来自http://www.import.io/examples。在import.io的搜索框中输入Zillow.com，检索Zillow数据的样例。

图2-1

他们所提供的数据是有关旧金山的，不过在我们的例子中将使用纽约。为了更换城市，需要使用我们感兴趣的数据所在的网址，来替换演示所提供的网址。

为了实现这点，我们可以打开一个单独的浏览器选项卡，并导航到Zillow.com。在那里执行一个公寓搜索。让我们将公寓搜索限制在曼哈顿地区，价格在$1500到$3000之间。如图2-2所示。

图2-2

一旦有结果返回，我们需要从浏览器地址栏中复制Zillow.com站点的URL，并将其粘贴到之前选项卡中import.io的提取框中。

复制图2-2中Zillow.com地址栏中的URL。并将其粘贴到import.io的提取框中，如图2-3所示。

图2-3

单击左上角的提取数据（Extract Data）按钮，你将看到一个结果表，只显示你想要的数据。

现在，我们可以通过单击“下载CSV”（Download CSV）按钮，轻松地下载这些数据。弹出的对话框会问我们需要下载多少页，从结果页可以看出在Zillow的搜索返回了2640条结果，我们需要下载106页来获得整个数据集。而Import.io仅仅允许我们下载20页，现在也只能如此了。

2.2　检查和准备数据

我们现在有一个包含500套公寓的数据集。来看看其中有什么。首先在Jupyter记事本中，使用pandas导入数据。

import pandas as pd
import re
import numpy as np
import matplotlib.pyplot as plt

plt.style.use('ggplot')
%matplotlib inline

pd.set_option("display.max_columns", 30)
pd.set_option("display.max_colwidth", 100)
pd.set_option("display.precision", 3)

# Use the file location of your Import.io csv
CSV_PATH = r"/Users/alexcombs/Downloads/magic.csv"

df = pd.read_csv(CSV_PATH)
df.columns

上述代码生成图2-4中的输出。

图2-4

最后一行df.columns为数据提供了列标题的输出。此外，让我们使用df.head().T查看数据的某些样本。在行结束处的.T语法将转置我们的数据框并垂直地显示它，如图2-5所示。

图2-5

我们已经可以看出数据有一些缺失值（NaN）。需要多个操作来标准化此数据。数据集中的列（或者说是图2-5中转置后的行）表示了每个Zillow房源的单项数据。看起来似乎有两种类型的房源—— 一种类型是单个单元，而另一种类型是多个单元。

这两种类型可以在图2-6中看到。

图2-6

这两个房源对应于在Zillow.com上所看到的图像，如图2-7所示。

图2-7

拆分这些的关键是listingtype_value这个列头。我们将数据拆分为单一的单元，Apartment for Rent，以及多个单元，Apartments for Rent：

# multiple units
mu = df[df['listingtype_value'].str.contains ('Apartments For')]

# single units
su = df[df['listingtype_value'].str.contains('Apartment For')]

现在来看看每种房源类型的数量。

len（mu）

上述代码生成以下输出。

161
len(su)

上述代码生成以下输出。

339

由于大多数房源属于单一单元的类型，我们现在将从此开始。

接下来，我们需要将数据格式化为标准结构。例如，至少需要为卧室数、浴室数、平方英尺和地址各准备一列。

从之前的观察中可以发现，我们已经有一个清晰的价格列，那就是pricelarge_value_prices。幸运的是，该列中没有缺失值，因此我们不会因为缺少数据而丢失任何的房源。

卧室和浴室的数量以及平方英尺将需要一些解析，因为它们全都挤在单一的列中。让我们解决这个问题。

先来看一下该列。

su['propertyinfo_value']

上述代码生成如图2-8所示的输出。

图2-8

看上去，数据似乎总是包括卧室和浴室的数量，偶尔也会包含例如年份这样的额外信息。在我们继续解析之前，先来检验一下这个假设。

# 检查没有包含'bd'或'Studio'的行数
len(su[~(su['propertyinfo_value'].str.contains('Studio')\
|su['propertyinfo_value'].str.contains('bd'))])

上述代码生成以下输出。

0

现在来看看下面几行代码。

#检查没有包含'ba'的行数
len(su[~(su['propertyinfo_value'].str.contains('ba'))])

上述代码生成以下输出。

6

看来有几行缺少浴室数量的数据。出现这种情况的原因有多种，我们可以使用一些方法来解决这个问题。一种就是填充或插补这些缺失的数据点。

关于缺失数据的主题很可能讨论一整章甚至是一本书，这里我建议投入一些时间来理解这个课题，它是建模过程中一个关键的组成部分。然而，这并非此处讨论的主要目的，所以我们将假设数据的缺失是随机的，即使删除这些没有浴室信息的房源，也不会使得我们的样本产生不恰当的偏向。

# 选择拥有浴室的房源 [1]
no_baths = su[~(su['propertyinfo_value'].str.contains('ba'))]

# 再排除那些缺失了浴室信息的房源
sucln = su[~su.index.isin(no_baths.index)]

现在我们可以继续解析卧室和浴室信息：

# 使用项目符号进行切分
def parse_info(row):
if not 'sqft' in row:
br, ba = row.split('')[:2]
sqft = np.nan
else:
br, ba, sqft = row.split('.')[:3]
return pd.Series({'Beds': br, 'Baths': ba, 'Sqft': sqft})

attr = sucln['propertyinfo_value'].apply(parse_info)

attr

上述代码生成图2-9的输出。

图2-9

这里我们做了些什么？我们在propertyinfo_value列上运行了apply函数。然后该操作返回一个数据框，其中每个公寓属性都会成为单独的列。在最终完成之前，还有几个额外的步骤。我们需要在取值中删除字符串（bd、ba和sqft），并且需要将这个新的数据框和原始的数据进行连接。让我们现在就这么做吧。

#在取值中将字符串删除
attr_cln = attr.applymap(lambda x: x.strip().split(' ')[0] if
isinstance (x,str) else np.nan)

attr_cln

上述代码生成图2-10的输出。

图2-10

让我们来看看下面的代码。

sujnd = sucln.join(attr_cln)

sujnd.T

上述代码生成图2-11的输出。

到了这个时刻，各方面的数据集开始聚集在一起了。我们可以基于卧室的数量、浴室的数量和面积的平方英尺数，来测试关于公寓价值的假设。但是，正如行业专家所说，房地产的区域最为关键。让我们采取和之前相同的属性解析方法，并将其应用到公寓的地址上。

如果可能，我们还将尝试提取楼层的信息。这里我们假设一个模式，其中一个数字后面跟随一个字母，而该数字就表示建筑物的楼层。

# parse out zip, floor
def parse_addy(r):
so_zip = re.search(', NY(\d+)', r)
so_flr = re.search('(?:APT|#)\s+(\d+)[A-Z]+,', r)
if so_zip:
zipc = so_zip.group(1)
else:
zipc = np.nan
if so_flr:
flr = so_flr.group(1)
else:
flr = np.nan
return pd.Series({'Zip':zipc, 'Floor': flr})

flrzip = sujnd['routable_link/_text'].apply(parse_addy)

suf = sujnd.join(flrzip)

suf.T

图2-11

上述代码生成图2-12的输出。

图2-12

正如你所看到的，当楼层和邮编信息出现的时候，我们能够成功地解析出它们。这使我们从333个房源中获得了320个带有邮政编码信息的房源和164个带有楼层信息的房源。

最终进行一点清理，然后我们即将开始检查这个数据集。

# 我们将数据减少为所感兴趣的那些列
sudf = suf[['pricelarge_value_prices', 'Beds', 'Baths', 'Sqft', 'Floor',
'Zip']]

# 我们还会清理奇怪的列名，并重置索引
sudf.rename(columns={'pricelarge_value_prices':'Rent'}, inplace=True)

sudf.reset_index(drop=True, inplace=True)

sudf

上述代码生成图2-13的输出。

图2-13

2.2.1　分析数据

到了这个阶段，数据已经是我们分析时所需要的格式了。让我们从一些总体的统计数据分析开始。

sudf.describe()

上述代码生成图2-14的输出。

图2-14

这里可以看到租金的统计细分。不要忘记我们从Zillow的原始数据中，只选择了每月价格在1500到3000美元之间的公寓。在这里无法看到的是卧室和浴室的平均数量，或者楼层的平均数。导致这个现象的问题有两个。第一个问题涉及卧室。我们需要所有的数据都为数值型才能获得统计。可以将工作室公寓认定为一个零卧室的公寓（实际也确实如此），来解决这个问题。

# 我们将出现的'Studio'替换为0
sudf.loc[:,'Beds'] = sudf['Beds'].map(lambda x: 0 if 'Studio' in x else x)

sudf

上述代码生成图2-15的输出。

图2-15

这解决了第一个问题，但我们还有另一个问题。任何需要统计数据的列必须是数值类型。正如你在图2-16的截图所见，情况并非如此。

图2-16

sudf.info()

上述代码生成图2-16的输出。

我们可以通过更改数据类型来解决这个问题，如下面的代码所示。

# 让我们解决列中数据类型的问题
sudf.loc[:,'Rent'] = sudf['Rent'].astype(int)
sudf.loc[:,'Beds'] = sudf['Beds'].astype(int)

# 存在半间浴室的情况，因此需要浮点型
sudf.loc[:,'Baths'] = sudf['Baths'].astype(float)

# 存在NaNs，需要浮点型，但是首先要将逗号替换掉
sudf.loc[:,'Sqft'] = sudf['Sqft'].str.replace(',','')

sudf.loc[:,'Sqft'] = sudf['Sqft'].astype(float)
sudf.loc[:,'Floor'] = sudf['Floor'].astype(float)

让我们执行下面的这行代码并看看结果如何。

sudf.info()

上述代码生成图2-17的输出。

图2-17

让我们执行下面的代码行，以便得到最终的统计数据。

sudf.describe()

上述代码生成图2-18的输出。

图2-18

租金、卧室、浴室和平方英尺的数字都看起来不错，但是Floor楼层这一列似乎有些问题。在纽约，确实有很多非常高的建筑，但我想没有超过1000层的。

快速看过数据之后，你会发现APT 1107A给了我们这个结果。很可能，这是一个11层的公寓，但是为了安全性以及一致性，我们会放弃这个房源。幸运的是，这是唯一超出30楼的房源，所以我们的数据仍然是完好的状态。

# 索引标号318是有问题的房源，这里放弃它
sudf = sudf.drop([318])

sudf.describe()

上述代码生成图2-19的输出。

图2-19

我们的数据现在看起来不错，接下来继续分析的步骤。让我们生成数据的透视图，首先通过邮政编码和卧室数量来检视价格的情况。Pandas有一个.pivot_table()函数，使这个操作变得很容易。

sudf.pivot_table('Rent', 'Zip', 'Beds', aggfunc='mean')

上述代码生成图2-20的输出。

图2-20

此操作可让我们按照邮政编码来查看平均价格。正如你所见，随着房间数量的增加，我们将看到越来越少的房源，NaN值就是很好的证明。为了进一步探究其原因，我们可以基于房源的数量进行透视。

sudf.pivot_table('Rent', 'Zip', 'Beds', aggfunc='count')

上述代码生成图2-21的输出。

图2-21

从图2-21可以看出，根据邮政编码和卧室数量的维度来分析，我们的数据是稀疏的。这是不幸的，理想情况下，我们应该需要更多的数据。尽管如此，我们仍然可以进行分析。

现在要通过可视化的方式来检视手头的数据。

2.2.2　可视化数据

由于目前的数据是基于邮政编码的，因此最好的可视化方法是使用热图[2]。如果你不熟悉热图，那么简单地来理解它只是按照色谱来表示数据的可视化。现在，让我们使用名为folium的Python映射库来实现这一点（https://github.com/python-visualization/folium）。

由于缺少包含两到三间卧室的公寓，让我们缩减数据集，聚焦到工作室和一间卧室的房源。

su_lt_two = sudf[sudf['Beds']<2]

现在我们将继续创建可视化。

import folium

map = folium.Map(location=[40.748817, -73.985428], zoom_start=13)
map.geo_json(geo_path=r'/Users/alexcombs/Downloads/nyc.json',
data=su_lt_two,
columns=['Zip', 'Rent'],
key_on='feature.properties.postalCode',
threshold_scale=[1700.00, 1900.00, 2100.00, 2300.00, 2500.00,
2750.00],
fill_color='YlOrRd', fill_opacity=0.7, line_opacity=0.2,
legend_name='Rent (%)',
reset=True)
map.create_map(path='nyc.html')

上述代码生成图2-22的输出。

这里发生了很多事情，所以让我们一步一步来分析。导入folium后，我们创建了一个.Map()对象。为了使地图居中，还需要传入坐标和缩放级别。我在Google上搜索了帝国大厦的坐标（你需要使用经度的正负符号），并调整缩放，使帝国大厦出现在我想要居中的地方。

下一行代码需要一个称为GeoJSON文件的东西。这是一个表示地理属性的开放格式来。通过搜索NYC GeoJSON文件，我找到了一个，特别是它还包含了邮政编码的映射。一旦传入了GeoJSON文件与邮政编码之后，你还需要传入数据框。

然后你需要引用键列（在这个例子中为Zip）以及你希望用于热图的列。在我们的例子中将使用租金的中位数。其他选项用于确定颜色的调色板、颜色改变的取值以及某些用于调整图例和着色的其他参数。最后一行代码确定了输出文件的名称。

图2-22

如果你在本地机器上使用这些代码，你可能会在Chrome浏览器中遇到一个问题。阴影部分似乎不正常。Chrome认为其是跨域请求，因此拒绝执行它，而且由于此，你将无法看到热图的叠加部分。Internet Explorer和Safari浏览器应该可以正常显示。

随着热图完成，我们可以感受到哪些地区有更高的或更低的租金。如果你租房的时候关注某个特定的区域，这将很有帮助。不过，让我们继续使用回归建模，进行更为深入的分析。

2.3　对数据建模

让我们开始使用一个和两个卧室的数据集。我们将观察邮政编码和卧室数量对于出租价格的影响。这里将使用两个包：第一个是statsmodels，我们在前一章简要讨论过，而第二个包：patsy（https://patsy.readthedocs.org/en/latest/index.html）和statsmodels搭档使用，使工作更轻松。在运行回归的时候，Patsy让我们可以使用R风格的公式。

让我们现在开始吧。

import patsy
import statsmodels.api as sm

f = 'Rent ~ Zip + Beds'
y, X = patsy.dmatrices(f, su_lt_two, return_type='dataframe')

results = sm.OLS(y, X).fit()
print(results.summary())

上述代码生成图2-23的输出。

图2-23

通过这几行代码，我们刚刚运行了第一个机器学习算法。

虽然大多数人不倾向于将线性回归视为机器学习，但它实际上就是机器学习。线性回归是一种监督式的机器学习。在这种情况下，监督只是意味着我们为训练集提供了输出值[3]。

现在，让我们解释这其中发生的事情。在引入包之后，有两行和patsy模块相关的代码。第一行是我们将要使用的公式。在左手边（波浪号之前）是反应或因变量，也就是Rent。在右手边，是独立或预测变量，就是Zip和Beds。这个公式表示，我们想知道邮政编码和卧室数量将如何影响出租价格。

然后我们的公式将和包含相应列名的数据框一起，传递给patsy.dmatrices()。然后设置Patsy，让它返回一个数据框，其中X矩阵由预测变量组成，而y向量由响应变量组成。这些将被传递给sm.OLS()，之后调用.fit()来运行我们的模型。最后，打印出模型的结果。

如你所见，输出的结果提供了大量的信息。让我们从最上面的部分开始吧。可以看到模型包括了262个观察样本，调整后的R2为0.283，F-statistic为1.21e-10，具有统计的显着性。这里显著性是指什么？它意味着我们所创建的模型，仅仅使用卧室数量和邮政编码，就已经能够解释约三分之一的价格差异。这是一个满意的结果吗？为了更好地回答这个问题，让我们来看看输出的中间部分。

中间部分为我们提供了模型中每个自变量的有关信息。从左到右，我们可以看到以下信息：变量、变量在模型中的系数、标准误差、t统计量、t统计量的p值，以及95%的置信区间。

这一切告诉我们什么？如果看p值这一列，我们可以确定独立变量从统计的角度来看是否具有意义。在回归模型中具有统计学意义，这意味着一个独立变量和响应变量之间的关系不太可能是偶然发生的。通常，统计学家使用0.05的p值来确定这一点。一个0.05的p值意味着我们看到的结果只有5%的可能性是偶然发生的。就这里的输出而言，卧室的数量显然是有意义的。那邮政编码怎么样呢？

首先要注意的是，我们的截距代表了10001的邮政编码。建立线性回归模型的时候，是需要截距的。截距就是回归线和y轴交叉的地方。Statsmodels会自动选择一个预测变量作为截距。在这里，它决定使用纽约的切尔西地区（10001）[4]。

就像卧室的数量，截距在统计上是显着的。但是，其他邮政编码又怎么样呢？

在大多数情况下，它们并不显著。不过，让我们来看看显著的几个。邮政编码——10027、10029和10035——都是非常显著的，并且都具有很高的负置信区间。这告诉我们，和切尔西地区一个类似的公寓相比，这些地区往往会有较低的租金价格。

因为切尔西被认为是纽约的一个时尚之地，而另三个街区都在哈林区[5]及其附近——它们当然不会被认为是时尚的地方——模型与我们对真实世界的直觉，是相吻合的。

现在让我们使用这个模型进行一些预测。

2.3.1　预测

假设根据前面的分析，我们对三个特定的邮政编码感兴趣：10002、10003和10009。我们应该如何使用已有的模型，来确定为某个公寓支付多少钱呢？下面来看看吧。

首先，需要理解模型的输入是什么样子，这样我们才知道如何输入一组新的值。让我们来看看X矩阵。

X.head()

上述代码生成图2-24的输出。

图2-24

我们可以看到，输入是用所谓的虚拟变量进行编码的。由于邮政编码不是数字的，所以为了表示这个特征，系统使用了虚拟编码。如果某个公寓在10003中，那么该列将被编码为1，而所有其他邮政编码都被编码为0。而卧室是数值型的，所以系统将根据实际的数字对其进行编码。现在，让我们创建自己的输入行进行预测。

to_pred_idx = X.iloc [0] .index
to_pred_zeros = np.zeros（len（to_pred_idx））
tpdf = pd.DataFrame（to_pred_zeros，index = to_pred_idx，columns = ['value']）

tpdf

上述代码生成图2-25的输出。

图2-25

我们刚刚使用了X矩阵的索引，并用零填充数据。现在让我们填入一些实际的值。我们要对一个位于10009区域的、包含一间卧室的公寓进行估价。

tpdf.loc['Intercept'] = 1
tpdf.loc['Beds'] = 1
tpdf.loc['Zip[T.10009]'] = 1

tpdf

对于线性回归，截距值必须设置为1，模型才能返回正确的统计值。

上述代码生成图2-26的输出。

图2-26

这里我们可以看到截距和10009邮政编码已经被设置为1了。

在图2-27中，我们可以看到卧室的数量也已经被设置为1了。

图2-27

我们已经将特征设置为了适当的值，现在使用该模型返回一个预测。

results.predict(tpdf['value'])

上述代码生成如下输出。

2529.5604669841355

请记住，results是我们保存模型的变量名。这个模型对象有一个.predict()方法，我们使用自己的输入值调用该方法。正如你可以看到的，模型返回了预测的值。

如果我们想要在条件中增加一间卧室怎么办？

来改变一下输入并看看结果。

tpdf['value'] = 0
tpdf.loc['Intercept'] = 1
tpdf.loc['Beds'] = 2
tpdf.loc['Zip[T.10009]'] = 1

tpdf

上述代码生成了图2-28的输出。

图2-28

我们可以看到卧室数量已经被更新为2。

现在，我们将再次运行预测。

results.predict(tpdf['value'])

上述代码生成以下输出。

2738.035104645339

看起来，额外增加的卧室每个月将花费我们大约200美元。如果我们选择10002地区呢？让我们在代码中实现这个。

tpdf['value'] = 0
tpdf.loc['Intercept'] = 1
tpdf.loc['Beds'] = 2
tpdf.loc['Zip[T.10002]'] = 1

results.predict(tpdf['value'])

上述代码生成以下输出。

2651.1763504369078

根据我们的模型，如果选择10002而不是10009地区，我们可以在两卧室的公寓上少花一些钱。

2.3.2　扩展模型

到了目前这个阶段，我们只检视了邮政编码、卧室和出租价格之间的关系。虽然这个模型有一定的解释能力，但是，我们的数据集太小，使用的特征也太少，无法充分地观测房地产估值这个复杂的市场。

然而，幸运的是，我们即将向该模型添加更多的数据和特征，而且可以使用完全相同的框架来扩展我们的分析。

未来可扩展的探索包括利用Foursquare或Yelp API所提供的餐馆和酒吧数据，或者是Walk Score这类供应商所提供的可步行性和交通便利性指标。

要扩展这个模型有很多的方法，我建议你在一个方向上持续努力，例如探索各种指标。随着每天更多的数据被发布，你的模型会不断地改善。

2.4　小结

在本章中，你学习了如何获取房地产列表上的数据，利用pandas的功能来操作和清理数据，通过热图来可视化地检视数据，最后，构建并使用回归模型来为公寓估价。

目前为止，我们只是刚刚接触了机器学习的表层。在下面的章节中，我们会继续探索不同的算法和应用。

下一章将探讨如何使用聚类算法寻找极其稀有的、折扣力度非常之大的机票。

[1]　这行代码中，作者使用的注释和变量名令人困惑，但是最终效果是一样的。

[2]　译者注：热图可以帮助显示基于地理位置的信息，所以适合邮编相关的分析。

[3]　译者注：或者说是目标值。

[4]　译者注：位于美国纽约曼哈顿的切尔西区域，以画廊、音乐、文学等各类艺术而闻名。

[5]　译者注：纽约的黑人区。

第3章　构建应用程序，发现低价的机票

让我们谈谈错误。它们是生活的一部分，每个人都会犯错——即使是航空公司也如此。

在2014年的某个下午，我正在阅读Twitter上的订阅消息，而我所关注的其中一个账号是美国一家主要的航空公司，它们所提供的到欧洲的机票价格明显低于正常值。当时，从纽约到维也纳最便宜的票价也要大约800美元。然而，该航空公司在某些日期的推广票价介于350美元到450美元之间。这似乎好得让人难以置信，不过确有可能是真的。我偶然发现了行业所说的错误票价。

在旅行老手和里程贩子的超级秘密社会里，这是众所周知的，航空公司偶尔——而且意外地——贴出不包括燃料附加费的票价。

值得注意的是，这不是他们所犯的唯一一类错误。你可能会期望先进的算法为每个航班更新票价，它会考虑到大量的因素而不至于犯错。在大多数情况下，你是对的。但是，由于遗留系统的存在，以及处理多个飞行运营商和管辖区域的复杂性，错误确实会发生。

既然你知道这些票价真的存在，你怎么能得到它们呢？当然是通过机器学习了！由于这种机票通常只会持续几个小时然后就消失了，所以我们要建立一个应用程序，持续监控票价。一旦出现异常价格，应用程序将产生一个提醒，然后我们就可以快速地采取行动了。

我们将在本章讨论以下主题。

- 在网上获取机票价格。

- 使用先进的网络抓取技术检索票价数据。

- 解析文档对象模型以提取价格。

- 使用聚类技术识别异常票价。

- 使用IFTTT发送实时文本提醒。

3.1　获取机票价格数据

好在，机票价格数据比房地产数据更容易获得，所以不难找。免费的、付费的API数据源以及许多网站都提供这些数据。我测试了其中一些服务，但最后，只有一个提供的数据格式是可用的。这种格式使我们很容易提前几个月就能找到价格最低的航班。

在我告诉你是哪个服务之前，让我先向你展示一下典型的航班搜索界面，如图3-1所示。

图3-1

对于我们的目标而言，这种类型的接口其问题在于——所有的API接口有相同的问题——我们需要对所感兴趣的全部日期、全部可能的行程长度、每个机场都执行查询。虽然这样做是可行的，但是做法太笨拙了，而且需要耗费大量的精力。

好在，有一个更好的方法。Google提供了一个鲜为人知的工具，被称为航班查询器。此工具可让你查看在数月的时间内，从一个地区到另一个地区的最低票价。

图3-2是搜索从纽约到欧洲、行程为8～12天的例子。返回的城市按照价格来排序，从最低价到最高价。

图3-2

对于查找错误票价，这种格式是非常理想的，原因是返回的结果按照价格排序，覆盖整个区域，时间跨度有60天。这能确保异常的票价出现时，会自动跑到列表的顶部。

这是个好消息。坏消息是Google使代码拉取数据这件事情变得相当有挑战性。幸运的是，使用一些聪明的编码，我们仍然可以得到所需的数据。

3.2　使用高级的网络爬虫技术检索票价数据

我们已经在前面的章节中学习了如何使用request库来检索网页。正如我之前所说，它是一个了不起的工具，但不幸的是，这里无法工作。我们想要爬取的页面是完全基于AJAX的。异步JavaScript（AJAX）这个方法从服务器获取数据，而不必重新加载整个页面。这意味着，需要使用浏览器来检索数据。虽然这个听起来好像需要大量的额外工作，不过有两个库，当一起使用它们的时候，这就会成为一个轻量级的任务。

这两个库是Selenium和PhantomJS。Selenium是一个强大的工具，它可以自动化Web浏览器，而PhantomJS是一个浏览器。为什么使用PhantomJS而不是Firefox或Chrome呢？PhantomJS是所谓的无头浏览器，意思是它没有可视化的用户界面。这使得它非常精简，成为我们理想的选择。

要安装PhantomJS， 你可以从http://phantomjs.org/download.html下载可执行文件或者源码。至于Selenium，它可以通过pip来安装。

我们还需要另一个名为BeautifulSoup4的库来解析页面中的数据。如果你还没有安装这个，也可以使用pip安装它。

完成这些安装之后，让我们开始动手吧。我们将在Jupyter记事本里工作。Jupyter最适合探索性的分析。稍后，当探索完成之后，我们会继续在文本编辑器中工作。文本编辑器更适合编写我们想要部署为应用程序的代码。

首先，导入这些库。

import pandas as pd
import numpy as np

from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import
DesiredCapabilities
from bs4 import BeautifulSoup

import matplotlib.pyplot as plt
%matplotlib inline

接下来，我们将设置代码以实例化浏览器对象。正是这个对象将为我们拉取页面。你可以在浏览器中搜索并复制URL，以此来选择想要的机场或地区。在这里，我会查找从纽约机场到几个亚洲城市的行程。

url =
"https://www.google.com/flights/explore/#explore;f=JFK,EWR,LGA;t=
HND,NRT,TPE,HKG,KIX;s=1;li=8;lx=12;d=2016-04-01"

driver = webdriver.PhantomJS()

dcap = dict(DesiredCapabilities.PHANTOMJS)
dcap["phantomjs.page.settings.userAgent"] = ("Mozilla/5.0
(Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/46.0.2490.80 Safari/537.36")

driver = webdriver.PhantomJS(desired_capabilities=dcap,
service_args=['--ignore- ssl-errors=true'])

driver.implicitly_wait(20)
driver.get(url)

我们需要向接收请求的服务器发送一个用户代理。你可以使用我在此列出的代理，或者如果你愿意，也可以将其替换为自己的代理。进入到解析阶段的时候，这点变得尤为重要。如果你在普通的浏览器中使用某代理来选择文档对象模型（DOM）元素，然后在代码中传递了另一个不同的代理，那么你解析页面的时候也许会碰到问题，因为DOM可能是与用户代理相关的。

你可以通过Google搜索“what is my user agent?”，来找到自己的用户代理。如果你计划将其用于其他的爬虫，请复制这个信息，然后在上述代码中使用它。

运行上述代码之后，你可以使用下面这行代码来保存页面的截图。请检查这个截图，以确保一切看起来正常。

driver.save_screenshot(r'flight_explorer.png')

如果一切都是按计划完成，你应该可以看到输出为True，并且生成一个与所抓取页面相关的图像文件。在普通的Web浏览器中，它看起来就像原有的页面。

接下来，我们将继续解析页面以提取定价信息。

3.3　解析DOM以提取定价数据

DOM是形成网页结构的元素集合。如果你曾经查看过网页的源代码，你就已经看到了DOM的各个模块。它们包括例如body、div、class和id这样的元素和标签。我们需要处理这些元素来提取所需的数据。

让我们来看看Google网页的DOM。为了查看其内容，请在该页面上单击右键，并单击“检查元素”。对于Firefox或Chrome浏览器，这个操作应该是相同的。这将打开开发人员选项卡，允许你查看页面的源信息。打开之后，在左上角挑选元素选择器，并单击其中一个价格栏跳转到相应的元素。如图3-3所示。

图3-3

你注意到的第一件事情可能是，发现div标签中没有任何定价数据。如果将鼠标悬停在价格栏上，将出现一个显示费用的提示，但这一切都是使用JavaScript完成的，并不是DOM。事实上，唯一可用的信息是价格栏的高度。那么，我们如何获得不在那里的数据呢？靠推断！

页面确实给了我们足够的线索来推断价格，那就是使用价格栏的高度。你会注意到，为每个城市所列出的是最好的票价。你可以在条形图的左手边看到它。此div提供了文本类型的价格，如图3-4所示的屏幕截图。

你还会注意到，每个城市都有一个条匹配了最低的票价。相比其他的条，这个条加入了更暗的阴影来突出显示。因为有一个独特的类来产生这种颜色，因此我们能够对它进行定位。一旦我们找到了它，就可以使用它的高度除以价格来确定每个像素所对应的价格。使用这种方法，推导出每个航班的价格就成为了一个简单的数学练习。

图3-4

让我们现在编写代码。

第一步是将页面源文件提供给BeautifulSoup。

s = BeautifulSoup(driver.page_source, "lxml")

然后我们可以获取所有最佳价格的列表。

best_price_tags = s.findAll('div', 'FTWFGDB-w-e')
best_prices = []
for tag in best_price_tags:
best_prices.append(int(tag.text.replace('$','')))

由于拥有最便宜票价的城市上升到了最高的排名，我们可以直接使用它。

best_price = best_prices[0]

接下来，我们将得到包含每个条的高度的列表。

best_height_tags = s.findAll('div', 'FTWFGDB-w-f')
best_heights = []
for t in best_height_tags:
best_heights.append(float(t.attrs['style']\
.split('height:') [1].replace ('px;','')))

同样，我们只需要第一个。

best_height = best_heights[0]

然后我们可以计算每个高度像素所对应的价格。

pph = np.array(best_price)/np.array(best_height)

接下来，我们将检索每个城市所有航班的价格条的高度。

cities = s.findAll('div', 'FTWFGDB-w-o')

hlist=[]
for bar in cities[0]\
.findAll('div', 'FTWFGDB-w-x'):
hlist.append(float(bar['style']\
.split('height: ')[1]\
.replace('px;','')) *pph)
fares = pd.DataFrame(hlist, columns=['price'])

任务完成了，我们现在有一个数据框，包含了两个月内最便宜的票价。下面来看看。

fares.min()

上述代码生成图3-5的输出。

图3-5

我们的最低票价应该与页面上看到的一样，而事实确实如此。现在再看看完整的列表，如图3-6所示。

图3-6

一切看起来不错。我们现在可以继续建立异常值检测了。

通过聚类技术识别异常的票价

机票全天都在不断地更新。如果我们试图确定远低于正常的票价，不使用机器学习的技术怎么行？这看上去似乎是相当简单，但是当你开始思考可用的选项时，它很快就变得比预期复杂得多。

一个选择是获得每个城市的价格并设置一个阈值，如果它们跌到比阈值还低的价格，你就发送一个提醒。这可能行得通，不过是将比目前最低价格少一定量的百分比设置为提醒的条件，还是将具体的美元金额设置为提醒条件？还有，如何设置它？如果由于季节性因素导致票价自然下降，又该怎么办呢？也许你可以检查每个价格条和中间值相比偏离了多少。如果价格接近平稳的时候，出现一个很小幅度的下降呢？也许你可以对比每个价格条与其相邻条的高度。如果错误票价出现在不止一天中，又会如何？正如你所见，这件任务不是看上去那么简单的。鉴于此，我们如何避免为每个城市存储定价数据、处理季节性因素，并试图设置阈值的烦恼呢？这里使用聚类算法。

有许多聚类算法可用，但是对于这里所处理的数据类型，我们将使用被称为基于密度的空间聚类算法（DBSCAN），它适合带有噪声数据的应用。这是一种非常有效的算法，倾向于使用和人类相同的方式来识别点的集群。图3-7是来自scikit-learn文档中的可视化图像。它演示了DBSCAN在不同数据分布范围内的有效性。

图3-7

你可以看到，它是相当强大的。让我们现在讨论一下算法的工作原理。

为了理解DBSCAN算法，首先我们需要讨论两个参数的设置，以使得算法能够运作。第一个参数称为epsilon。此参数确定在同一聚类中两个点彼此之间的距离。如果epsilon设置得非常大，那么任何两个点则更可能聚集在一起。第二个参数称为最小点数。这是创建聚类所需点的最小数量（包括当前点）。如果最小的点数是1，那么每个点都将成为一个聚类。如果最小点数大于1，那么有可能某些点就不隶属于任何聚类。然后这些点就被认作噪声——也就是DBSCAN中的N。

DBSCAN算法是如下进行的。从所有点的集合中随机选择一个点。从这一个点出发，搜索所有方向上的和当前点相距epsilon距离的范围。如果在epsilon距离的范围内，存在等于或多于最小点数的点，那么这个范围内所有的点就隶属于一个聚集（图3-7中的彩色区域）。针对每个新加入该聚类的点，重复该过程。继续此操作，直到没有任何新的点可以添加到此聚类。此时，第一个聚类就完成了。现在，从已经完成的聚类之外，随机选择新的点再次开始。重复同样的过程，直到没有新的聚类可以形成。

我们已经了解了算法的工作原理，现在将其应用到机票的数据上。我们将首先创建一个简单的图像来检视票价。

fig, ax = plt.subplots(figsize=(10,6))
plt.scatter(np.arange(len(fares['price'])),fares['price'])

上述代码的输出如图3-8所示。

图3-8

我们可以看到票价平稳了几个星期，然后开始急剧上升。大多数人可能将这些看作 4个主要的聚类。现在编写代码来识别和显示这些集群。

首先，我们将设置一个price数据框，然后可以将DBSCAN对象传入其中。

px = [x for x in fares['price']]
ff = pd.DataFrame(px, columns=['fare']).reset_index()

然后，我们需要为聚类导入几个库。

from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler

最后，下面的代码将DBSCAN算法应用于票价数据并输出一个可视化图像。

X = StandardScaler().fit_transform(ff)
db = DBSCAN(eps=.5, min_samples=1).fit(X)

labels = db.labels_
clusters = len(set(labels))
unique_labels = set(labels)
colors = plt.cm.Spectral(np.linspace(0, 1, len(unique_labels)))
plt.subplots(figsize=(12,8))

for k, c in zip(unique_labels, colors):
class_member_mask = (labels == k)
xy = X[class_member_mask]
plt.plot(xy[:, 0], xy[:, 1], 'o', markerfacecolor=c,
markeredgecolor='k', markersize=14)

plt.title("Total Clusters: {}".format(clusters), fontsize=14,
y=1.01)

让我们逐行地解释。在第一行，我们使用StandardScaler()方法，这个对象将获取数据，对每个点减去平均值，然后除以标准差[1]。这个步骤使所有的数据位于相同的基础之上，并为算法读取了这些数据。标准化之后的数据被传递给了DBSCAN对象。这里设置了前面讨论的两个参数。我们将eps或者epsilon距离设置为0.5，并将min_samples设置为1。下一行代码将labels设置为算法的labels数组输出。每个点（因为min_points设置为1）都将关联一个聚类ID。这些聚类将被标记为从0到n-1，其中n是聚类的总数。接下来的两行代码获得聚类的总数及其唯一标签，而以colors开头的代码为我们的图像生成了有颜色的图。剩余的代码对图像进行了设置，包括对每个聚类应用唯一的颜色，并使用聚类的总数为图像设置标题。

让我们来看看图3-9中票价数据的输出。

图3-9

正如你所见，算法已经确定了四个不同的聚类，这正是我们所期望的。我刚刚告诉你该算法使用这些参数运作得有多好，而现在就准备修改参数了。为什么要破坏完美的结果？好吧，让我们来看看几个引入虚构票价的场景。

使用目前的票价系列和相同的参数，让我们再引入一个新的票价。

首先，将序列中的数据点#10替换掉。我们将其从656美元更改为600美元，如图3-10所示。

图3-10

你会注意到在接近图形底部的位置，这个点形成了自己的聚类。然而，尽管这个票价与其他的价格明显分开，但是还不足以引起我们注意。

让我们增加epsilon参数，这样我们只会聚出两个分组：典型的票价和不正常的票价。

我们现在保持相同的虚构票价，不过将epsilon增加到1.5，如图3-11所示。

图3-11

你可以看到现在有两个聚类。我们600美元的票价已经被放在了主聚类，而在图顶部最右侧的票价已经形成了自己的聚类。这看起来很合理，因为最右边的票价是一个明显的异常值。让我们再测试一下，需要多远的距离才能将虚构的票价放在自己的聚类里？

图3-12的截图展示了让虚构票价进一步远离后的结果。

在图3-12中，将其删除到550美元，我们可以看到它仍然是主聚类的一部分。

图3-13中，将其降至545美元，会使其形成自己的聚类。这似乎是一个合理的水平，不过现在让我们使用其他城市运行另外几个场景试试看。

图3-14是东京成田机场的数据序列。其中有个单一的聚类，这是我们所希望的。现在换一个虚构的票价。我们将序列中#45的票价从970美元替换为600美元。这是一个大幅的下降——远远超过之前数据序列中由于111美元下跌而触发一个新的集群——但它显然在通常的价格范围内，所以我们不想形成一个新的聚类。

图3-12

图3-13

图3-14

从图3-15中你可以看到没有形成新的聚类。虚构的票价和其左右邻近的两个票价之间有如此大的距离，为什么还会导致这种情况？这是因为我们正在处理整个序列，而不仅仅是每个点最近的邻居。最有可能的是，虚构的数据点受到其左边点的影响而加入到聚类。让我们再试一个场景。让我们将一个票价置换到右边——进一步远离左下角的聚类，如图3-16所示。

图3-15

图3-16

这里我们将#55的票价从1176美元换成700美元。这会导致一个新的聚类。票价是在全系列票价的范围之内，但是现在它当然是一个异常点。然而，我们很可能不想被告知这种异常。

由于我们不希望在每次有多个集群时都被提醒，因此需要为希望被告知的场景设置规则。

首先，由于我们正在寻找错误的票价，因此期望它们等于所显示的最低价格。我们可以按照聚类来分组并检索最低的价格。

pf = pd.concat([ff, pd.DataFrame(db.labels_,
columns=['cluster'])], axis=1)
pf

图3-17

上述代码生成图3-17的输出。

以下代码将按照聚类来分组并显示分组中最低的价格和成员的数量。

rf=pf.groupby('cluster')['fare'].agg(['min','count'])
rf

上述代码生成图3-18的输出。

这里我们还预计错误聚类将小于主聚类。我们对错误聚类的大小设置一个限制，要求它小于总数的百分之10。在这种情况下，它将少于七个票价。这个数字将根据不同聚类的数量和大小而变化，但这应该是一个可行的数字。为了查看分位数的细节，可以使用下面这行代码。

rf.describe([.10,.25,.5,.75,.9])

上述代码生成图3-19的输出。

图3-18

图3-19

让我们再添加一个条件。为最低价集群和次低价集群之间设置一个最小距离。这样做将防止像在图3-20中看到的情况。

图3-20

这里的费用是最低的，但它只是比其他聚类范围低那么一点点。设置最小距离将减少我们收到的误报。让我们先将最小距离设置为100美元。

现在我们有了自己的异常检测规则，下面来看看如何将全部的模块组合起来，完成实时票价提醒应用程序。

3.4　使用IFTTT发送实时提醒

为了有机会获得这些便宜的票价，当它们出现时我们需要几乎实时地知道这一情况。为了实现这一点，我们将使用一个名为If This Then That的服务（IFTTT）。这是一项免费服务，允许你使用一系列的触发器和动作，将大量的服务连接在一起。想要保存Instagram上你所喜欢的所有照片到iPhone？一个特定的人每次发布Tweet时，你都想收到一个电子邮件？想要将你的Facebook更新发布到Twitter？IFTTT可以实现所有这一切，甚至更多。

如果要开始使用IFTTT，步骤如下。

1．在http://www.ifttt.com注册账户。

2．在https://ifttt.com/maker注册Maker频道。

3．在https://ifttt.com/sms注册SMS频道。

Maker频道允许你发送和接收HTTP请求，而SMS频道允许你发送和接受SMS消息。

创建账户并激活两个频道之后，请单击主页上的My Recepes，然后再单击Create a Recipe，如图3-21所示。

图3-21

然后搜索并选择Maker频道，如图3-22所示。

图3-22

接下来，选择Receive a web request，如图3-23所示。

图3-23

然后我们将创建一个名为fare_alert的事件，如图3-24所示。

图3-24

接下来，我们将设置that，如图3-25所示。

图3-25

搜索SMS并选择它。然后选择Send me an SMS，如图3-26所示。

图3-26

之后，我们将填写fare_alert的字段。确保替换掉原文中的花括号[2]，如图3-27所示。

图3-27

一旦完成，你就可以使用城市和机票价格自定义消息，如图3-28所示。

图3-28

为了测试这里的设置是否奏效，请访问http://www.ifttt.com/maker，单击How to Trigger Events。然后填写fare_alert的事件，并将测试城市和票价放在value1和value2的框中，如图3-29所示。

图3-29

最后，单击Test It，你应该会在短短几秒钟内收到一条短信。

现在，所有的模块都已经就绪，是时候将它们整合到单个脚本中，让其进行24×7小时的票价监控。

3.5　整合在一起

之前，我们都在Jupyter记事本里工作，但现在需要部署应用程序了，我们会切换到文本编辑器中工作。记事本对于探索性分析和可视化是很棒的，不过运行后台作业最好使用一个简单的.py文件。所以让我们开始吧。

我们将从包的引入开始。如果你尚未安装这些包，可能需要通过pip安装它们。

import sys
import pandas as pd
import numpy as np

import requests

from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import
DesiredCapabilities
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

from bs4 import BeautifulSoup

from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler

import schedule
import time

接下来，我们将创建一个函数来拉取数据并运行聚类算法。请注意，当我们获取数据的时候，会存在一个明显的等待。这是因为页面有AJAX的异步请求，我们需要增加一个等待时间，以确保继续下一步之前页面的定价数据已经被返回。如果由于某种原因，抓取仍然失败，那么这里将发送一个文本作为提醒。

def check_flights():
url = "https://www.google.com/flights/explore/#explore;f=JFK,
EWR,LGA;t= HND,NRT,TPE,HKG,KIX;s=1;li=8;lx=12;d=2016-04-01"

driver = webdriver.PhantomJS()

dcap = dict(DesiredCapabilities.PHANTOMJS)
dcap["phantomjs.page.settings.userAgent"] = \
("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36")

driver = webdriver.PhantomJS(desired_capabilities=dcap,
service_args=['--ignore-
ssl-errors= true'])
driver.get(url)

wait = WebDriverWait(driver, 20)
wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR,
"span.FTWFGDB-v-c")))

s = BeautifulSoup(driver.page_source, "lxml")

best_price_tags = s.findAll('div', 'FTWFGDB-w-e')

# 检查爬虫是否工作正常——如果失败了就发送提醒并关闭
if len(best_price_tags) < 4:
print('Failed to Load Page Data')
requests.post('https://maker.ifttt.com/trigger/fare_alert/with/key/MY_SECRE
T_KEY',data={"value1": "script", "value2": "failed",
"value3": ""})
sys.exit(0)
else:
print('Successfully Loaded Page Data')

best_prices = []
for tag in best_price_tags:
best_prices.append(int(tag.text.replace('$', '')))

best_price = best_prices[0]

best_height_tags = s.findAll('div', 'FTWFGDB-w-f')
best_heights = []
for t in best_height_tags:
best_heights.append(float(t.attrs['style']
.split('height:')[1].replace ('px;', '')))

best_height = best_heights[0]

# 每个高度像素对应的价格
pph = np.array(best_price)/np.array(best_height)

cities = s.findAll('div', 'FTWFGDB-w-o')

hlist = []
for bar in cities[0].findAll('div', 'FTWFGDB-w-x'):
hlist.append(float(bar['style'].split('height: ')[1]
.replace('px;', '')) * pph)

fares = pd.DataFrame(hlist, columns=['price'])
px = [x for x in fares['price']]
ff = pd.DataFrame(px, columns=['fare']).reset_index()

# 开始聚类
X = StandardScaler().fit_transform(ff)
db = DBSCAN(eps=1.5, min_samples=1).fit(X)

labels = db.labels_
clusters = len(set(labels))

pf = pd.concat([ff, pd.DataFrame(db.labels_, columns= ['cluster'])], axis=1)

rf = pf.groupby('cluster')['fare']\
.agg(['min', 'count']).sort_values('min', scending=True)

现在我们将检查规则是否被触发。如果是，就发送接收文本的请求。

# 设置我们的规则
# 必须有多于1个的聚类
# 聚类的最小值必须等于最低的票价
# 聚类的大小必须小于全部数量的百分之10
# 聚类必须比次低的价格聚类少100美元及以上
if clusters > 1\
and ff['fare'].min() == rf.iloc[0]['min']\
and rf.iloc[0]['count'] < rf['count'].quantile(.10)\
and rf.iloc[0]['fare'] + 100 < rf.iloc[1]['fare']:
city = s.find('span', 'FTWFGDB-v-c').text
fare = s.find('div', 'FTWFGDB-w-e').text
requests.post('https://maker.ifttt.com/trigger/fare_alert/with/key/MY_SECRE
T_KEY', data={"value1": city, "value2": fare, "value3": ""})
else:
print('no alert triggered')

最终，我们将设置一个调度器。它每60分钟运行一次这里的代码。

# 设置调度器，每60分钟运行一次我们的代码
schedule.every(60).minutes.do(check_flights)

while 1:
schedule.run_pending()
time.sleep(1)

这样做应该就完工了。我们现在可以将其保存为fare_alerter.py，并在命令行中运行它。它将持续运行并每60分钟检查一次票价。如果出现了错误的机票，我们将是第一个知道的！

请注意，这是此类代码的最基本实现。为了创建一个合理的实现，应该使用良好的日志记录来代替这里列出的打印语句。有关如何实现日志记录的更多信息，请参见https://docs.python.org/3.4/howto/logging.html#logging-basic-tutorial。

3.6　小结

本章讨论了很多内容。我们学会了如何在Web上找到最好的机票数据，如何使用DOM来查找和解析HTML元素，如何将数据聚集为有意义的组，最后是如何通过IFTTT的Web请求，从代码发送文本提醒。虽然我们在这里讨论的是机票相关的内容，但是所做的工作几乎可以重用于任何你想关注的定价。

如果你决定对机票使用这个程序，我希望它能为你提供许多快乐的旅行！

下一章会讨论如何使用分类算法来帮助预测IPO的市场。

[1]　译者注：这种标准化的方法称为Z-Score。

[2]　译者注：你即将看到的花括号是保留字，用于传递参数。

第4章　使用逻辑回归预测IPO市场

在20世纪90年代末，获得了对的IPO（首次公开募股）就像赢得彩票一样。对于一些技术公司而言，第一天的回报是它们最初发行价格的很多倍。如果你幸运地得到了一个配额，那就会获得一笔意外之财。这里列出在那个时期，第一天表现最棒的几家公司。

- VA Linux上涨697%，12/09/1999。

- Globe.com上涨606%，11/13/1998。

- Foundry Networks上涨525%，9/28/1999。

虽然互联网疯狂的日子已经离我们远去，但IPO仍然可能在第一天给予超额的回报。以下是在过去几年，第一天交易涨幅超过100%的几个。

- Seres Therapeutics上涨185%，06/26/2015。

- Audro Biotech上涨147%，4/15/2015。

- Shake Shack上涨118%，1/30/2015。

正如你所看到的，这仍然是一个值得关注的市场。在本章中，我们将仔细观察IPO市场。我们将看看如何使用机器学习来帮助自己决策，哪些IPO值得仔细研究，而哪些是可以忽略的。

我们将在本章中讨论以下主题。

- IPO市场。

- 数据清洗和特征工程。

- 使用逻辑回归的二元分类。

- 模型评估。

- 特征的重要性。

4.1　IPO市场

在我们开始建模之前，先讨论一下什么是IPO（或首次公开募股），以及关于这个市场，研究的结果能告诉我们些什么。之后，我们将讨论一些可以应用的策略。

4.1.1　什么是IPO

首次公开募股是一家私人公司成为上市公司的过程。公开发行为公司募集资金，并让公众通过购买其股票，获得投资该公司的机会。

虽然具体实施有些不同，但在典型的发行过程中，一家公司会列出一家或多家承销其发行的投资银行。这意味着那些银行向公司保证，在发行当天他们将购买所有以IPO价格提供的股份。当然，承销的银行不打算自己保留全部的股份。在发行公司的帮助下，他们去做所谓的路演，吸引机构客户的兴趣。这些客户可以预订股份，表示他们有意在IPO当天购买股票。这是一个非约束性合同，因为发行的价格直到IPO的当天才最终确定。然后，承销商将根据客户们所表达的感兴趣程度，设定发行的价格。

从我们的角度来看，非常有趣的地方在于：研究表明IPO一直被系统性地低估。有许多理论解释为什么会发生这种情况，以及为什么低估的范围会随着时间而变化，不过可以肯定的是，研究已经显示出每年有“数十亿美元留在桌子上”。

在IPO中，“留在桌子上的钱”是指股票的发行价和第一天收盘价之间的差价。

在我们继续之前，还应该谈一谈发行价和开盘价之间的区别。虽然偶然的情况下你可以通过经纪人的交易，以发行价获得IPO，但作为一个普通的公众，你基本上不得不以开盘价（通常更高）来购买IPO。我们将在这个假设下构建模型。

4.1.2　近期IPO市场表现

现在来看看IPO市场的表现。我们将从IPOScoop.com拉取数据。这是一项为即将到来的IPO提供评级的服务。请访问https://www.iposcoop.com/scoop-track-record-from-2000-to-present/并单击页面底部的按钮，下载一个电子表格。我们将其加载到pandas，并使用Jupyter记事本运行一些可视化。

首先，进行整个章节都需要的导入环节。然后，我们会像下面这样拉取数据。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from patsy import dmatrix
from sklearn.ensemble import RandomForestClassifier
from sklearn import linear_model
%matplotlib inline
ipos = pd.read_csv(r'/Users/alexcombs/Downloads/ipo_data.csv',
encoding='latin-1')
ipos

上述代码生成图4-1的输出。

图4-1

这里我们可以看到，对于每个IPO都有一些不少的信息：发行日期、发行者、发行价格、开盘价格以及价格的变化。让我们先按照年份来探索表现的数据。

我们首先需要进行一些清理工作，以正确地格式化所有的列。这里将去掉美元和百分比符号。

ipos = ipos.applymap(lambda x: x if not '$' in str(x) else
x.replace('$',''))
ipos = ipos.applymap(lambda x: x if not '%' in str(x) else
x.replace('%',''))
ipos

上述代码生成图4-2的输出。

图4-2

接下来，我们将修正所有列的数据类型。目前它们都是对象，但是对于即将执行的聚合和其他操作而言，我们需要数值类型。使用下面这行代码来查看列的数据类型。

ipos.info()

上述代码生成图4-3的输出。

图4-3

在我们的数据中有一些'N/C'的值，首先需要将其替换。之后就可以更改数据类型了。

ipos.replace('N/C',0, inplace=True)
ipos['Date'] = pd.to_datetime(ipos['Date'])
ipos['Offer Price'] = ipos['Offer Price'].astype('float')
ipos['Opening Price'] = ipos['Opening Price'].astype('float')
ipos['1st Day Close'] = ipos['1st Day Close'].astype('float')
ipos['1st Day % Px Chng '] = ipos['1st Day % Px Chng '].astype('float')
ipos['$ Chg Close'] = ipos['$ Chg Close'].astype('float')
ipos['$ Chg Opening'] = ipos['$ Chg Opening'].astype('float')
ipos['Star Ratings'] = ipos['Star Ratings'].astype('int')

请注意，这会抛出一个错误，如图4-4所示。

图4-4

这意味着我们的日期中有一个格式是不正确的。基于上述的堆栈跟踪信息发现问题，然后修复它。

ipos[ipos['Date']=='11/120']

发现这个错误后，我们将观察图4-5的输出。

图4-5

正确的日期应该是11/20/2012，因此我们将对其设置正确的值并重新运行前面的数据类型修订。之后，一切都可以顺利进行。

ipos.loc[1660, 'Date'] = '2012-11-20'

ipos['Date'] = pd.to_datetime(ipos['Date'])
ipos['Offer Price'] = ipos['Offer Price'].astype('float')
ipos['Opening Price'] = ipos['Opening Price'].astype('float')
ipos['1st Day Close'] = ipos['1st Day Close'].astype('float')
ipos['1st Day % Px Chng '] = ipos['1st Day % Px Chng']
.astype('float')
ipos['$ Chg Close'] = ipos['$ Chg Close'].astype('float')
ipos['$ Chg Opening'] = ipos['$ Chg Opening'].astype('float')
ipos['Star Ratings'] = ipos['Star Ratings'].astype('int')

ipos.info()

上述代码生成图4-6的输出。

图4-6

现在，终于可以开始我们的探索了。这里从第一天的平均收益百分比开始。

ipos.groupby(ipos['Date'].dt.year)['1st Day % Px Chng ']\
.mean().plot(kind= 'bar', figsize=(15,10), color='k', title='1st Day Mean
IPO Percentage Change')

上述代码生成图4-7的输出。

图4-7

这里都是近年来一些正向的百分比。让我们现在来看看与平均值相比较，中位数的表现又是如何。

ipos.groupby(ipos['Date'].dt.year)['1st Day % Px Chng ']\
.median().plot(kind='bar', figsize=(15,10), color='k', title='1st Day
Median IPO Percentage Change')

上述代码生成图4-8的输出。

图4-8

通过平均值和中位数的对比，我们可以清楚地看到，一些较大的异常值造成了回报分布的偏斜。让我们来仔细观察一下。

ipos['1st Day % Px Chng '].describe()

上述代码生成图4-9的输出。

图4-9

现在我们还可以将其绘制成图。

ipos['1st Day % Px Chng '].hist(figsize=(15,7), bins=100, color='grey')

上述代码生成图4-10的输出。

图4-10

从图4-10，我们可以看到大多数回报集中在零附近，但有个长尾一直拖到右侧，那里有一些真正的全垒打[1]发行价。

我们已经看过第一天的百分比变化，就是从发行价到当天收盘价的差距，但正如我前面所指出的，很少有机会能够以发行价买入。既然如此，现在让我们来看看开盘价到收盘价的收益率。它有助于我们理解这个问题：所有的收益都是给了那些拿到发行价的人，还是说在第一天人们仍然有机会冲入并获得超高的回报？

为了回答这个问题，我们首先创建两个新的列。

ipos['$ Chg Open to Close'] = ipos['$ Chg Close'] - ipos['$ Chg Opening']
ipos['% Chg Open to Close'] = (ipos['$ Chg Open to Close']/ipos['Opening
Price']) * 100

上面的代码生成图4-11的输出。

图4-11

接下来，我们将生成统计信息。

ipos['% Chg Open to Close'].describe()

上面的代码生成图4-12的输出。

图4-12

即刻，这些数据看起来就令人怀疑了。虽然首次公开募股有可能在开盘后下跌，但是跌幅几乎达到99%，似乎是不太现实的。经过一番调查，我们发现好像两个表现最差的发行者实际上是不好的数据点。当处理现实世界的数据时，往往情况就是如此，所以我们将更正这些并重新生成数据。

ipos.loc[440, '$ Chg Opening'] = .09
ipos.loc[1264, '$ Chg Opening'] = .01
ipos.loc[1264, 'Opening Price'] = 11.26

ipos['$ Chg Open to Close'] = ipos['$ Chg Close'] - ipos['$ Chg Opening']
ipos['% Chg Open to Close'] = (ipos['$ Chg Open to Close']/ipos['Opening
Price']) * 100

ipos['% Chg Open to Close'].describe()

上述代码生成图4-13的输出。

图4-13

这次损失下降到40％，看起来仍然让人觉得怀疑，不过仔细观察之后，发现它是Zillow的IPO。Zillow开盘炒得异常火热，但在收盘前很快就跌到了地板上。这告诉我们，坏数据点似乎已经被清理完毕了。

现在将继续前进，希望我们已清除了大部分的错误。

ipos['% Chg Open to Close'].hist(figsize=(15,7), bins=100, color='grey')

上述代码生成图4-14的输出。

图4-14

最后，我们可以看到开盘价到收盘价变化的分布形状，和发行价到收盘价变化的分布相比，有着明显的差异。平均值和中位值都有显著的下降，而且紧贴着原点右侧的条形看上去有一个健康的梯度，而原点左侧的条形似乎也按照比例进行了增长[2]。注意，右边的长尾没有之前那么明显了，但仍然是值得注意的，所以还有一丝希望。

4.1.3　基本的IPO策略

现在我们对市场有了一些感觉，这里来探讨几项策略。如果我们以其开盘价购买每个IPO股票，然后在收盘时卖出，那么最终收益如何？我们看一下2015年迄今的数据。

ipos[ipos['Date']>='2015-01-01']['$ Chg Open to Close'].describe()

上述代码生成图4-15的输出。

图4-15

ipos[ipos['Date']>='2015-01-01']['$ Chg Open to Close'].sum()

上述代码生成图4-16的输出。

图4-16

让我们拆分一下盈利的交易和亏损的交易。

ipos[(ipos['Date']>='2015-01-01')&(ipos['$ Chg Open to Close']>0)]['$ Chg
Open to Close'].describe()

上述代码生成图4-17的输出[3]。

图4-17

ipos[(ipos['Date']>='2015-01-01')&(ipos['$ Chg Open to Close']<0)]['$ Chg
Open to Close'].describe()

上述代码生成图4-18的输出[4]。

图4-18

所以，我们可以看到，如果2015年投资每一个IPO，我们将会忙于投资147家IPO，大约一半使我们挣钱，而另一半使我们损失了钱。整体上还是有利润的，因为盈利IPO的收益最终弥补了损失的钱。当然，这里假设没有交易差额或佣金成本，在现实世界中这些都是不可避免的。然而，这显然不是发家致富的法宝，因为平均回报率低于1%。

交易差额是指对于目标股票，你尝试买入或卖出的价格和订单实际执行价格之间的差异。

让我们看看是否可以使用机器学习来帮助改善这个最基本的方法。一个合理的策略似乎是瞄准图4-14中那长长的右尾，所以我们将聚焦于此。

4.2　特征工程

在交易开始后，什么会影响股票的表现？最近整体市场的表现，或者是承销商的威望都可能会影响它。也许交易日的星期几或月份很重要。在模型中考虑和囊括这些因素被称为特征工程，而且特征的建模几乎和用于构建模型的数据一样重要。如果你的特征没有信息含量，那么模型根本不会有价值。

让我们开始这个过程，添加一些我们觉得可能会影响IPO表现的特征。

先从获取标普500指数的数据开始。这可能是普通美国市场最好的代表。我们可以从Yahoo! Finance下载，网址是https://finance.yahoo.com/q/hp?s=%5EGSPC&amp;a= 00&amp;b=1&amp;c=2000&amp;d=11&amp;e=17&amp;f=2015&amp;g=d。然后，我们可以使用pandas导入数据。

sp = pd.read_csv(r'/Users/alexcombs/Downloads/spy.csv')
sp.sort_values('Date', inplace=True)
sp.reset_index(drop=True, inplace=True)
sp

上述代码生成图4-19的输出。

图4-19

因为整体市场在过去一周的表现会在逻辑上影响某个股票，因此让我们将其添加到这里的DataFrame中。我们将计算标普500昨日收盘价相对于其七天前收盘价的变化百分比。

def get_week_chg(ipo_dt):
try:
day_ago_idx = sp[sp['Date']==str(ipo_dt.date())].index[0] - 1
week_ago_idx = sp[sp['Date']==str(ipo_dt.date())].index[0] - 8
chg = (sp.iloc[day_ago_idx]['Close'] - \
sp.iloc[week_ago_idx]['Close'])/(sp.iloc[week_ago_idx]['Close'])
return chg * 100
except:
print('error', ipo_dt.date())

ipos['SP Week Change'] = ipos['Date'].map(get_week_chg)

上述代码生成图4-20的输出。

图4-20

运行代码后，系统提示我们有几个日期对应的数据执行失败了，这表明IPO的日期可能存在一些错误。检查这些日期相关的IPO发现它们当天是关闭的状态。这里是纠正错误的一个示例和代码。

ipos[ipos['Date']=='2009-08-01']

上述代码生成图4-21的输出。

图4-21

EM的实际IPO日期是2009年的8月12日，所以将其纠正，此外，经过一番研究，我们也发现了其他错误数据的真正发行日期并做了修正。

ipos.loc[1175, 'Date'] = pd.to_datetime('2009-08-12')
ipos.loc[1660, 'Date'] = pd.to_datetime('2012-11-20')
ipos.loc[2251, 'Date'] = pd.to_datetime('2015-05-21')
ipos.loc[2252, 'Date'] = pd.to_datetime('2015-05-21')

再次运行该函数，它将正确地添加所有发行股票的一周变化情况。

ipos['SP Week Change'] = ipos['Date'].map(get_week_chg)

现在，让我们添加一项新的指标，即标准普尔500指数在IPO前一天收盘时到IPO首日开盘时这个期间内，变化的百分比。

def get_cto_chg(ipo_dt):
try:
today_open_idx = sp[sp['Date']==str(ipo_dt.date())].index[0]
yday_close_idx = sp[sp['Date']==str(ipo_dt.date())].index[0] - 1
chg = (sp.iloc[today_open_idx]['Open'] - \
sp.iloc[yday_close_idx] ['Close'])/(sp.iloc[yday_close_idx]['Close'])
return chg * 100
except:
print('error', ipo_dt)
ipos['SP Close to Open Chg Pct'] = ipos['Date'].map(get_cto_chg)

上述代码生成图4-22的输出。

图4-22

现在，让我们来整理承销商的数据。这需要一些工作量。我们将执行一系列的步骤。首先，为主承销商添加一列。接下来，会对数据进行标准化。最后，我们将添加一列，表示参与承销商的总数。

首先，我们通过数据中字符串的拆分和空格的删除，解析出主承销商。

ipos['Lead Mgr'] = ipos['Lead/Joint-Lead Mangager'].map(lambda x:
x.split('/')[0])
ipos['Lead Mgr'] = ipos['Lead Mgr'].map(lambda x: x.strip())

接下来，打印出不同的主承销商，这样可以看出为了规范银行的名称，需要进行多少清理工作。

for n in pd.DataFrame(ipos['Lead Mgr'].unique(),
columns=['Name']).sort ('Name')['Name']:
print(n)

上述代码生成图4-23的输出。

图4-23

有两种方法可以做到这一点。第一种方法，毫无疑问是两个方法中更容易的那个，就是相信我们为你所做的工作，只是复制和粘贴下面的代码。另一种方法是执行大量迭代的字符串部分匹配，并且由你自己来纠正。强烈建议使用第一种选项。

ipos.loc[ipos['Lead Mgr'].str.contains('Hambrecht'),'Lead Mgr'] = 'WR
Hambrecht+Co.'
ipos.loc[ipos['Lead Mgr'].str.contains('Edwards'), 'Lead Mgr'] = 'AG
Edwards'
ipos.loc[ipos['Lead Mgr'].str.contains('Edwrads'), 'Lead Mgr'] = 'AG
Edwards'
ipos.loc[ipos['Lead Mgr'].str.contains('Barclay'), 'Lead Mgr'] = 'Barclays'
ipos.loc[ipos['Lead Mgr'].str.contains('Aegis'), 'Lead Mgr'] = 'Aegis
Capital'
ipos.loc[ipos['Lead Mgr'].str.contains('Deutsche'), 'Lead Mgr'] = 'Deutsche
Bank'
ipos.loc[ipos['Lead Mgr'].str.contains('Suisse'), 'Lead Mgr'] = 'CSFB'
ipos.loc[ipos['Lead Mgr'].str.contains('CS.?F'), 'Lead Mgr'] = 'CSFB'
ipos.loc[ipos['Lead Mgr'].str.contains('^Early'), 'Lead Mgr'] =
'EarlyBirdCapital'
ipos.loc[325,'Lead Mgr'] = 'Maximum Captial'
ipos.loc[ipos['Lead Mgr'].str.contains('Keefe'), 'Lead Mgr'] = 'Keefe,
Bruyette & Woods'
ipos.loc[ipos['Lead Mgr'].str.contains('Stan'), 'Lead Mgr'] = 'Morgan
Stanley'
ipos.loc[ipos['Lead Mgr'].str.contains('P. Morg'), 'Lead Mgr'] = 'JP Morgan'
ipos.loc[ipos['Lead Mgr'].str.contains('PM'), 'Lead Mgr'] = 'JP Morgan'
ipos.loc[ipos['Lead Mgr'].str.contains('J\.P\.'), 'Lead Mgr'] = 'JP Morgan'
ipos.loc[ipos['Lead Mgr'].str.contains('Banc of'), 'Lead Mgr'] = 'Banc of
America'
ipos.loc[ipos['Lead Mgr'].str.contains('Lych'), 'Lead Mgr'] = 'BofA Merrill
Lynch'
ipos.loc[ipos['Lead Mgr'].str.contains('Merrill$'), 'Lead Mgr'] = 'Merrill
Lynch'
ipos.loc[ipos['Lead Mgr'].str.contains('Lymch'), 'Lead Mgr'] = 'Merrill
Lynch'
ipos.loc[ipos['Lead Mgr'].str.contains('A Merril Lynch'), 'Lead Mgr'] =
'BofA Merrill Lynch'
ipos.loc[ipos['Lead Mgr'].str.contains('Merril '), 'Lead Mgr'] = 'Merrill
Lynch'
ipos.loc[ipos['Lead Mgr'].str.contains('BofA$'), 'Lead Mgr'] = 'BofA
Merrill Lynch'
ipos.loc[ipos['Lead Mgr'].str.contains('SANDLER'), 'Lead Mgr'] = 'Sandler
O'neil + Partners'
ipos.loc[ipos['Lead Mgr'].str.contains('Sandler'), 'Lead Mgr'] = 'Sandler
O'Neil + Partners'
ipos.loc[ipos['Lead Mgr'].str.contains('Renshaw'), 'Lead Mgr'] = 'Rodman &
Renshaw'
ipos.loc[ipos['Lead Mgr'].str.contains('Baird'), 'Lead Mgr'] = 'RW Baird'
ipos.loc[ipos['Lead Mgr'].str.contains('Cantor'), 'Lead Mgr'] = 'Cantor
Fitzgerald'
ipos.loc[ipos['Lead Mgr'].str.contains('Goldman'), 'Lead Mgr'] = 'Goldman
Sachs'
ipos.loc[ipos['Lead Mgr'].str.contains('Bear'), 'Lead Mgr'] = 'Bear
Stearns'
ipos.loc[ipos['Lead Mgr'].str.contains('BoA'), 'Lead Mgr'] = 'BofA Merrill
Lynch'
ipos.loc[ipos['Lead Mgr'].str.contains('Broadband'), 'Lead Mgr'] =
'Broadband Capital'
ipos.loc[ipos['Lead Mgr'].str.contains('Davidson'), 'Lead Mgr'] = 'DA
Davidson'
ipos.loc[ipos['Lead Mgr'].str.contains('Feltl'), 'Lead Mgr'] = 'Feltl & Co.'
ipos.loc[ipos['Lead Mgr'].str.contains('China'), 'Lead Mgr'] = 'China
International'
ipos.loc[ipos['Lead Mgr'].str.contains('Cit'), 'Lead Mgr'] = 'Citigroup'
ipos.loc[ipos['Lead Mgr'].str.contains('Ferris'), 'Lead Mgr'] = 'Ferris
Baker Watts'
ipos.loc[ipos['Lead Mgr'].str.contains('Friedman|Freidman|FBR'), 'Lead
Mgr'] = 'Friedman Billings Ramsey'
ipos.loc[ipos['Lead Mgr'].str.contains('^I-'), 'Lead Mgr'] = 'I-Bankers'
ipos.loc[ipos['Lead Mgr'].str.contains('Gunn'), 'Lead Mgr'] = 'Gunn Allen'
ipos.loc[ipos['Lead Mgr'].str.contains('Jeffer'), 'Lead Mgr'] = 'Jefferies'
ipos.loc[ipos['Lead Mgr'].str.contains('Oppen'), 'Lead Mgr'] =
'Oppenheimer'
ipos.loc[ipos['Lead Mgr'].str.contains('JMP'), 'Lead Mgr'] = 'JMP
Securities'
ipos.loc[ipos['Lead Mgr'].str.contains('Rice'), 'Lead Mgr'] = 'Johnson
Rice'
ipos.loc[ipos['Lead Mgr'].str.contains('Ladenburg'), 'Lead Mgr'] =
'Ladenburg Thalmann'
ipos.loc[ipos['Lead Mgr'].str.contains('Piper'), 'Lead Mgr'] = 'Piper
Jaffray'
ipos.loc[ipos['Lead Mgr'].str.contains('Pali'), 'Lead Mgr'] = 'Pali
Capital'
ipos.loc[ipos['Lead Mgr'].str.contains('Paulson'), 'Lead Mgr'] = 'Paulson
Investment Co.'
ipos.loc[ipos['Lead Mgr'].str.contains('Roth'), 'Lead Mgr'] = 'Roth
Capital'
ipos.loc[ipos['Lead Mgr'].str.contains('Stifel'), 'Lead Mgr'] = 'Stifel
Nicolaus'
ipos.loc[ipos['Lead Mgr'].str.contains('SunTrust'), 'Lead Mgr'] = 'SunTrust
Robinson'
ipos.loc[ipos['Lead Mgr'].str.contains('Wachovia'), 'Lead Mgr'] =
'Wachovia'
ipos.loc[ipos['Lead Mgr'].str.contains('Wedbush'), 'Lead Mgr'] = 'Wedbush
Morgan'
ipos.loc[ipos['Lead Mgr'].str.contains('Blair'), 'Lead Mgr'] = 'William
Blair'
ipos.loc[ipos['Lead Mgr'].str.contains('Wunderlich'), 'Lead Mgr'] =
'Wunderlich'
ipos.loc[ipos['Lead Mgr'].str.contains('Max'), 'Lead Mgr'] = 'Maxim Group'
ipos.loc[ipos['Lead Mgr'].str.contains('CIBC'), 'Lead Mgr'] = 'CIBC'
ipos.loc[ipos['Lead Mgr'].str.contains('CRT'), 'Lead Mgr'] = 'CRT Capital'
ipos.loc[ipos['Lead Mgr'].str.contains('HCF'),'Lead Mgr'] = 'HCFP Brenner'
ipos.loc[ipos['Lead Mgr'].str.contains('Cohen'), 'Lead Mgr'] = 'Cohen & Co.'
ipos.loc[ipos['Lead Mgr'].str.contains('Cowen'), 'Lead Mgr'] = 'Cowen & Co.'
ipos.loc[ipos['Lead Mgr'].str.contains('Leerink'), 'Lead Mgr'] = 'Leerink
Partners'
ipos.loc[ipos['Lead Mgr'].str.contains('Lynch\xca'), 'Lead Mgr'] = 'Merrill
Lynch'

此过程完成之后，你可以再次运行以下代码来查看更新后的列表。

for n in pd.DataFrame(ipos['Lead Mgr'].unique(),
columns=['Name']).sort_values ('Name')['Name']:
print(n)

上述代码生成图4-24的输出。

图4-24

我们可以看到，列表现在是整齐划一的了。这点完成后，我们将增加承销商的数量。

ipos['Total Underwriters'] = ipos['Lead/Joint-Lead Mangager'].map(lambda x:
len(x.split('/')))

接下来，我们将添加几个日期相关的特征。这里加入星期几和月份。

ipos['Week Day'] = ipos['Date'].dt.dayofweek.map({0:'Mon', 1:'Tues',
2:'Wed',\
3:'Thurs', 4:'Fri', 5:'Sat', 6:'Sun'})
ipos['Month'] = ipos['Date'].map(lambda x: x.month)
ipos['Month'] = ipos['Month'].map({1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr',
5:'May', 6:'Jun',7:'Jul',\
8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'})
ipos

上述代码生成图4-25的输出。

图4-25

如果所有的事情都是按预期进行，我们的DataFrame应该看起来像图4-25那样。我们现在补充几个最终的特征，涉及发行价和开盘价之间的变化，以及发行价和收盘价之间的变化。

ipos['Gap Open Pct'] = (ipos['$ Chg Opening'].astype('float')/ipos['Opening
Price'].astype('float')) * 100
ipos['Open to Close Pct'] = (ipos['$ Chg Close'].astype('float') -\
ipos['$ Chg Opening'].astype('float'))/\
ipos['Opening Price'].astype('float') * 100

现在，特征准备就绪了。如果遇到我们认为有用的、可能会改善模型的数据，我们总是可以加入更多的特征。不过，在这里让我们以这些特征开始。

将这些特征提供给模型之前，我们需要考虑选择哪些特征。我们必须非常小心，不要在添加时特征时“泄露”了信息。这是一个常见的错误，当向模型提供信息的时候，所用的数据在当时其实是无法获得的，这时候就会发生信息“泄露”。例如，将收盘价添加到我们的模型将使结果完全无效。如果这样做，实际上我们是为模型提供了它试图预测的答案。通常，泄露的错误比这个例子更微妙一些，但无论如何，我们需要注意这点。

我们将添加以下特征。

- 月份（Month）。

- 星期几（Week Day）。

- 主要承销商（Lead Mgr）。

- 承销商总数（Total Underwriters）。

- 发行价到开盘价的差距百分比（Gap Open Pct）。

- 发行价到开盘价的美元变化量（$ Chg Opening）。

- 发行价（Offer Price）。

- 开盘价（Opening Price）。

- 标准普尔指数从收盘到开盘的变化百分比（SP Close to Open Chg Pct）。

- 标准普尔指数前一周的变化（SP Week Change）。

完善模型所需的全部特征后，我们将其准备好以供模型使用。我们将使用Patsy库。如果需要，可以使用pip安装Patsy。Patsy以原始的形式获取数据，并将其转换为适用于统计模型构建的矩阵。

from patsy import dmatrix
X = dmatrix('Month + Q("Week Day") + Q("Total Underwriters") + Q("Gap Open
Pct") + Q("$ Chg Opening") +\
Q("Lead Mgr") + Q("Offer Price") + Q("Opening Price") +\
Q("SP Close to Open Chg Pct") + Q("SP Week Change")', data=ipos,
return_type='dataframe')
X

上述代码生成图4-26的输出。

我们可以看到Patsy已经将分类型数据重新配置为多列，而将连续的数据保存在单个列中。这种操作被称为虚构编码。在这种格式中，每个月都会得到属于自己的列。对于每个代理而言同样如此。例如，如果特定的IPO样例（某一行）在May这个月发行，那么它在May这个列的值就为1，而该行所有其他月份的列值都为0。对于分类型的特征，总是有n-1个特征列。被排除的列成为了基线，而其他的将和这个基线进行比较。

最后，Patsy还添加了一个截距列。这是回归模型正常运行所需的第一个列。

图4-26

这步完成后，我们将进入建模阶段。

4.3　二元分类

我们试图预测IPO是否值得购买，而不是尝试准确地预测第一天的总收益是多少。这里，我们应该指出这里所做的不是投资建议，其目的只是为了说明某个案例。请不要使用这个模型开始随意地进行IPO交易。后果会非常严重。现在，为了预测二进制的结果（即1或0，是或否），我们将从称为逻辑回归（logistic regression）的模型开始。逻辑回归使用了逻辑函数。这是很理想的选择，因为逻辑回归有几个数学属性，使其易于使用，如图4-27所示。

图4-27

由于逻辑函数的形式，它特别适合于提供概率的估计，以及依据这些估计的二进制响应。任何大于0.5的被分类为1，而任何低于0.5的被分类为0。这些1和0可以对应任何我们想要分类的事物，不过在这个应用程序中，它将决定我们对于发行股，是买入（1）还是不买入（0）。

继续下一步，在我们的数据上使用这个模型。我们通过有点不同寻常的方式，将数据进行拆分。机器学习模型的标准做法是随机地决定哪些实例作为模型的训练数据，而哪些实例被用作测试数据。但是，由于这里的数据是基于时间的，因此我们将使用除今年（2015年）之外的所有数据进行训练。然后在2015年年初至今的数据上进行测试。

# 2188是第一个2015年数据的索引号，数据是按照日期排序的
X_train, X_test = X[:2188], X[2188:]
y_train = ipos['$ Chg Open to Close'][:2188].map(lambda x: 1 if x >= 1 else 0)
y_test = ipos['$ Chg Open to Close'][2188:].map(lambda x: 1 if x >= 1 else 0)

使用上面的代码，我们将数据分成训练集和测试集。请注意我们主观地为正向结果设置了1美元的阈值。这是为了贯彻这个策略：瞄准长尾中的赢家，而不是任何收益大于0的收盘。

现在，我们将拟合该模型，方法如下所示。

clf = linear_model.LogisticRegression()
clf.fit(X_train, y_train)

上面的代码将生成图4-28的输出。

图4-28

我们现在可以在预留的2015年数据上，评估模型的表现。

clf.score(X_test, y_test)

上面的代码将生成图4-29的输出。

从图4-29中，我们可以看出82%的预测是准确的。对于2015年而言，这当然是优于我们基本策略的预测，但这也可能是带有误导性的结果，因为实际上获益超过1美元的IPO，其比例是非常低的。这意味着对于所有IPO都将其预测猜测为0，也会给我们相同的结果，不过，这里让我们比较一下两者的区别。

首先我们的基本策略如下。

ipos[(ipos['Date']>='2015-01-01')]['$ Chg Open to Close'].describe()

上述代码生成图4-30的输出。

图4-29

图4-30

接下来，我们将处理预测的结果。首先使用结果设置一个数据框，然后输出它们。

pred_label = clf.predict(X_test)
results=[]
for pl, tl, idx, chg in zip(pred_label, y_test, y_test.index,
ipos.ix[y_test.index]['$ Chg Open to Close']):
if pl == tl:
results.append([idx, chg, pl, tl, 1])
else:
results.append([idx, chg, pl, tl, 0])
rf = pd.DataFrame(results, columns=['index', '$ chg', 'predicted',
'actual', 'correct'])
rf

上述代码生成图4-31的输出。

rf[rf['predicted']==1]['$ chg'].describe()

上述代码生成图4-32的输出。

图4-31

图4-32

所以，总数从147次买入降到6次买入。我们的平均值从0.23美元上涨到2.99美元，但我们的中位数从0美元下降到−0.02美元。让我们看看回报的图表。

fig, ax = plt.subplots(figsize=(15,10))
rf[rf['predicted']==1]['$ chg'].plot(kind='bar')
ax.set_title('Model Predicted Buys', y=1.01)
ax.set_ylabel('$ Change Open to Close')
ax.set_xlabel('Index')

上述代码生成图4-33的输出。

图4-33

从图4-33来看，似乎我们赢得了年中一次很大的收益，以及几次较小的收益和损失。我们对模型的测试还不是很充分。我们可能只是非常幸运地抓住了这次胜利。还需要评估模型的鲁棒性。我们可以通过几个方面来实现这点，这里仅仅做两件事情。首先，我们将阈值从1美元降至0.25美元，看看模型怎么反应。

X_train, X_test = X[:2188], X[2188:]
y_train = ipos['$ Chg Open to Close'][:2188].map(lambda x: 1 if x >= .25 else 0)
y_test = ipos['$ Chg Open to Close'][2188:].map(lambda x: 1 if x >= .25 else 0)
clf = linear_model.LogisticRegression()
clf.fit(X_train, y_train)
clf.score(X_test, y_test)

上述代码生成图4-34的输出。

图4-34

现在我们来检查一下结果。

pred_label = clf.predict(X_test)
results=[]
for pl, tl, idx, chg in zip(pred_label, y_test, y_test.index,
ipos.ix[y_test.index]['$ Chg Open to Close']):
if pl == tl:
results.append([idx, chg, pl, tl, 1])
else:
results.append([idx, chg, pl, tl, 0])
rf = pd.DataFrame(results, columns=['index', '$ chg', 'predicted',
'actual', 'correct'])
rf[rf['predicted']==1]['$ chg'].describe()

上述代码生成图4-35的输出。

从结果来看，我们的准确率和平均值都下降了。但是，我们的统计数量从6上升到了25，而且仍然远高于基础策略的结果。让我们再做一个测试。现在将2014年的数据从训练数据中删除，并将其加入测试数据中。

X_train, X_test = X[:1900], X[1900:]
y_train = ipos['$ Chg Open to Close'][:1900].map(lambda x: 1 if x >= .25 else 0)
y_test = ipos['$ Chg Open to Close'][1900:].map(lambda x: 1 if x >= .25 else 0)
clf = linear_model.LogisticRegression()
clf.fit(X_train, y_train)
clf.score(X_test, y_test)

上述代码生成图4-36的输出。

图4-35

图4-36

再次检查结果。

pred_label = clf.predict(X_test)
results=[]
for pl, tl, idx, chg in zip(pred_label, y_test, y_test.index,
ipos.ix[y_test.index]['$ Chg Open to Close']):
if pl == tl:
results.append([idx, chg, pl, tl, 1])
else:
results.append([idx, chg, pl, tl, 0])
rf = pd.DataFrame(results, columns=['index', '$ chg', 'predicted',
'actual', 'correct'])
rf[rf['predicted']==1]['$ chg'].describe()

上述代码生成图4-37的输出。

图4-37

随着2014年的数据放入测试集合，我们可以看到虽然平均值有所下降，但模型的表现仍然要好于投资每一笔IPO的简单方法，具体如表4-1所示。

表4-1

模型

交易次数

整体收益

每次交易的平均收益

2014-2015 niave

435

61

0.14

2014-2015 .25 LR

72

63.14

0.88

2015 naive

147

33.74

0.23

2015 .25 LR

25

45.52

1.82

2015 1 LR

6

25.2

4.2

现在让我们继续观察，模型中哪些特征是最重要的。

4.4　特征的重要性

哪些特征增加了一个发行股未来成功的概率？不幸的是，对于这个问题没有简单的答案。不过，我们将探讨两种方法，来评估这一点。由于我们建立模型时采用的是逻辑回归，所以可以观察每个特征参数的相关系数。请记住逻辑函数使用的是以下形式。

ln(p/1-p) = B_0 + B_1x

这里，p表示正向结果的概率，B_0是截距，B_1是特征的系数。一旦我们拟合了模型，就可以检查这些系数。现在立即获取它们。

f fv = pd.DataFrame(X_train.columns, clf.coef_.T).reset_index()
fv.columns = ['Coef', 'Feature']
fv.sort_values('Coef', ascending=0).reset_index(drop=True)

上述代码生成图4-38的输出。

对于分类型的特征，特征系数上的正符号告诉我们当这个特征存在时，相对于基线而言它增加了正向结果的概率。对于连续性的特征，正号表示该特征值的增加，会导致正向结果的概率增加。系数的大小告诉我们概率增加的幅度。让我们来看看星期几这个特征。

fv[fv['Feature'].str.contains('Week Day')]

上述代码生成图4-39的输出。

图4-38

图4-39

从图4-38中，我们可以看出没有星期五。这意味着星期五是所有其他同类特征用于比较的基线。我们也看到了根据这里的模型，周四增加了IPO成功的几率。

重要的是，系数并不代表相对于基线，概率实际增加了多少。为了得到这个值，我们必须取其指数。相对于周五，周四概率的增加为e(0.053885) = 1.055。这意味着如果保持所有其他因素不变，周四出现一个成功IPO的可能性比星期五的高出5.5%。我们也可以看到对于IPO而言，星期一是最糟糕的一天，e(-0.132437) = 0.876，也就是出现成功IPO的概率下降约12.4%。

回到特征的重要性，你很可能认为，在这个时候可以拿出具有最大正系数的那些特征，将它们扔到模型里，然后你就会拥有主宰新股市场的一切了。别急着下结论。

让我们看看基于正系数大小的前两个特征。

ipos[ipos['Lead Mgr'].str.contains('Keegan|Towbin')]

上述代码生成图4-40的输出。

图4-40

我们的前两个特征代表了四次IPO的总和。这就是为什么很难从逻辑回归模型提取信息，特别是这么复杂的模型。

然而，并不是没有任何希望了。我们可以利用另一种称为随机森林分类器的模型，来获得重要性的度量。本章不会深入说明这个模型是如何运作的，但它会给出和逻辑回归模型相似的结果，此外，它还将附带提供一个非常好的总结，告诉我们哪些特征对正向结果的影响最大。

使用与前面相同的训练和测试数据集，我们将拟合随机森林分类器。

clf_rf = RandomForestClassifier(n_estimators=1000)
clf_rf.fit(X_train, y_train)
f_importances = clf_rf.feature_importances_
f_names = X_train
f_std = np.std([tree.feature_importances_ for tree in clf_rf.estimators_],
axis=0)
zz = zip(f_importances, f_names, f_std)
zzs = sorted(zz, key=lambda x: x[0], reverse=True)
imps = [x[0] for x in zzs[:20]]
labels = [x[1] for x in zzs[:20]]
errs = [x[2] for x in zzs[:20]]
plt.subplots(figsize=(15,10))
plt.bar(range(20), imps, color="r", yerr=errs, align="center")
plt.xticks(range(20), labels, rotation=-70);

上述代码生成图4-41的输出。

图4-41

这里的输出向我们提供了一个包含每个特征误差条的排序列表，用于表明特征的重要性。观察这份列表，以“gap opening percentage”和“dollar change from opening”开头的排序看上去非常合理。

4.5　小结

本章我们已经讨论了很多方面的内容，但只是简单地了解了如何构建这类模型。希望你已经更好地理解了建模的过程，从清理数据，到进行特征工程，到测试数据。希望你可以使用这些信息自行扩展模型并加以改进。

在下一章中，我们将注意力转向另一个不同的领域，也就是从数值型数据切换到基于文本的数据。

[1]　译者注：这里形容非常成功的发行。

[2]　译者注：之所以作者认为这样的分布更健康，是因为它更符合常见的正态分布假设。

[3]　译者注：该图中的max和图4-15中的max应该一致，应该是笔误。

[4]　译者注：该图中的min和图4-15中的min应该一致，可能是笔误。

第5章　创建自定义的新闻源

我很爱阅读。甚至可以说有些狂热。曾经有些日子，我仔细阅读了上百篇的文章。尽管如此，我还是经常发现自己读不完搜索到的信息。我总是怀疑自己错过了一些有趣的事情，从而导致知识库中永远存在一些空白！

如果你患有类似的症状，不要害怕，因为在本章中，我要揭露一个简单的窍门，帮助你找到想要阅读的所有文章，同时让你避免在大量不感兴趣的内容上浪费时间。

在这一章的结尾，你将学会如何构建一个能理解你对新闻喜好的系统，并每天向你发送一个私人定制的新闻通讯。

下列是我们将在本章涵盖的内容。

- 使用Pocket应用程序创建监督训练的集合。

- 利用Pocket API来获取故事。

- 使用embed.ly API来提取故事主体。

- 自然语言处理的基础。

- 支持向量机。

- IFTTT与RSS源以及Google表单的集成。

- 建立每日的个性化新闻通讯。

5.1　使用Pocket应用程序，创建一个监督训练的集合

在我们可以创建自己对新闻稿的喜好模型之前，需要用于训练的数据。这些训练数据将被输入到我们的模型中，以教导该模型区分我们感兴趣的和不感兴趣的文章。为了构建这个语料库，我们需要标注大量与这些兴趣相关的文章。对于每篇文章，我们将其标记为“y”或“n”。这将指示该文章是否应该出现在发送给我们的每日摘要中。

为了简化这个过程，我们将使用Pocket应用程序。Pocket是一个允许你保存故事以供稍后阅读的应用程序。你只需安装浏览器扩展插件，然后当希望保存故事的时候，单击浏览器工具栏中的Pocket图标。这篇文章就被保存到了你的个人资料库。对于我们的目的而言，Pocket一个强大的功能是它还能够保存你所选择的标签。我们将使用此功能，将感兴趣的文章标记为“y”，而不感兴趣的文章标记为“n”。

5.1.1　安装Pocket的Chrome扩展程序

我们在这里使用Google Chrome，其他浏览器应该类似。对于Chrome，进入Google App Store，然后查找Extensions部分，如图5-1所示。

图5-1

单击蓝色的Add to Chrome按钮。如果你已经有一个Pocket账户了，那么请登录，如果你还没有账户，请继续注册（免费）。一旦完成，你应该可以看到浏览器右上角的Pocket图标。图标将变灰，不过一旦有你想要保存的文章，就可以单击它。文章保存之后，它就会变成红色。

如图5-2所示，在右上角可以看到灰色的图标。当图标被单击时，它变为红色，表示文章已经被保存。

图5-2

现在有趣的部分开始了！开始保存所有你看到的文章。对于有趣的那些打上“y”的标签，对于无趣的那些打上“n”的标签。这需要一点工作量。监督学习最终结果的好坏取决于你的训练集，所以你需要标记数百篇文章来获得好的效果。如果在保存某篇文章时你忘记给它打标签了，那么你可以去这个网站对其进行标记：http://www.get.pocket.com。

5.1.2　使用Pocket API来检索故事

现在你已经很勤奋地将文章都保存到了Pocket，下一步是检索它们。为了实现这一点，我们将使用Pocket API。你可以在https://getpocket.com/developer/apps/new注册一个新账户[1]。如图5-3所示，单击左上角的Create New App并填写详细信息以获取你的API密钥。请务必选择所有的权限，这样你才可以添加、更改和检索文章。

图5-3

一旦填写完毕并提交，你将收到CONSUMER KEY。你可以在左上角的My Apps下到它。看上去就如图5-4的截屏所示，不过显然你会得到一个真正的密钥。

图5-4

一旦设置完毕，你就可以进入到下一步，进行授权的设置。我们现在就开始。它要求你输入用户密钥和重定向的网址。重定向网址可以是任何链接。这里我使用自己的Twitter账户。

import requests
auth_params = {'consumer_key': 'MY_CONSUMER_KEY', 'redirect_uri':
'https://www.twitter.com/acombs'}
tkn = requests.post('https://getpocket.com/v3/oauth/request',
data=auth_params)
tkn.content

这将产生如图5-5的输出。

图5-5

该输出将包含你下一步所需的编码[2]。将以下内容放在浏览器的地址栏中。

https://getpocket.com/auth/authorize?request_token=some_long_code&redirect_uri=https%3A//www.twitter.com/acombs[3]

如果你将重定向的URL更改为你自己的网址，请务必对其进行编码。对于此有一些可用的资源。一个选择是使用Python的urllib库，另一个选择是使用免费的在线资源。

此时，你应该会看到一个授权屏幕。继续并批准授权，然后我们就可以进入下一步。

usr_params = {'consumer_key':'my_consumer_key', 'code':
'some_long_code'}
usr = requests.post('https://getpocket.com/v3/oauth/authorize',
data= usr_params)
usr.content

这里我们将使用图5-6的输出编码[4]进入检索故事的环节。

图5-6

首先，我们检索标记为“n”的故事。

no_params = {'consumer_key':'my_consumer_key', 'access_token':
'some_super_long_code',
'tag': 'n'}
no_result = requests.post('https://getpocket.com/v3/get',
data=no_params)
no_result.text

上述代码生成图5-7的输出。

图5-7

注意在这里，我们通过所有标记为“n”的文章获得了一个很长的JSON字符串。其中有若干个主键，不过现在我们只对URL感兴趣。我们将依据此，继续创建一个URL的列表。

no_jf = json.loads(no_result.text)
no_jd = no_jf['list']
no_urls=[]
for i in no_jd.values():
no_urls.append(i.get('resolved_url'))
no_urls

上述代码生成图5-8的输出。

图5-8

这个列表包含所有我们不感兴趣的故事的URL。现在，让我们将它放入一个DataFrame对象并将其如此标记。

import pandas
no_uf = pd.DataFrame(no_urls, columns=['urls'])
no_uf = no_uf.assign(wanted = lambda x: 'n')
no_uf

上述代码生成图5-9的输出。

图5-9

现在，不想要的故事已经就绪了。让我们对感兴趣的故事进行同样的处理。

ye_params = {'consumer_key': 'my_consumer_key', 'access_token':
'some_super_long_token',
'tag': 'y'}
yes_result = requests.post('https://getpocket.com/v3/get',
data=yes_params)
yes_jf = json.loads(yes_result.text)
yes_jd = yes_jf['list']
yes_urls=[]
for i in yes_jd.values():
yes_urls.append(i.get('resolved_url'))
yes_uf = pd.DataFrame(yes_urls, columns=['urls'])
yes_uf = yes_uf.assign(wanted = lambda x: 'y')
yes_uf

上述代码生成图5-10的输出。

图5-10

现在我们有两种类型的故事作为训练的数据，将它们连接到同一个DataFrame。

df = pd.concat([yes_uf, no_uf])
df.dropna(inplace=1)
df

上述代码生成图5-11的输出。

图5-11

现在，我们已经将所有的网址和相应的标签都放入同一个数据框中，接下来要下载每篇文章的HTML内容。我们将使用另一个称为embed.ly的免费服务。

5.2　使用embed.ly API下载故事的内容

我们拥有了所有故事的URL，但不幸的是这对于机器训练来说还不够。现在需要整篇文字的内容。如果我们需要为数十个网站创建自己的爬虫，那么这可能会成为一个巨大的挑战。我们必须编写代码来定位文章的正文，同时小心回避围绕正文的所有其他无关内容。幸运的是，有一些免费的服务将为我们实现这个目标。这里将使用embed.ly，不过你也可以使用一些其他的服务。

第一步是注册embed.ly API的访问。你可以在https://app.embed.ly/signup执行此操作。这是一个很直接的过程。一旦确认注册，你将收到一个API密钥。这就是你所需要的全部。只需在HTTP请求中使用此密钥。现在开始吧。

import urllib
def get_html(x):
qurl = urllib.parse.quote(x)
rhtml = requests.get('https://api.embedly.com/1/extract?url=' +
qurl + '&key=some_api_key')
ctnt = json.loads(rhtml.text).get('content')
return ctnt
df.loc[:,'html'] = df['urls'].map(get_html)
df.dropna(inplace=1)
df

上述代码生成图5-12的输出。

图5-12

使用这个，每个故事的HTML内容就能准备好了。

我们需要向模型提供纯文本而不是HTML，所以这里将使用解析器来剥离HTML的标签。

from bs4 import BeautifulSoup
def get_text(x):
soup = BeautifulSoup(x, 'lxml')
text = soup.get_text()
return text
df.loc[:,'text'] = df['html'].map(get_text)
df

上述代码生成图5-13的输出。

图5-13

有了这个，我们的训练集就准备完毕了。现在可以继续讨论如何将这些文本转换为模型可以使用的格式。

5.3　自然语言处理基础

如果机器学习模型只能操作数值型的数据，那么我们如何将文本转换成数值的表示？这是自然语言处理（NLP）的重点。在我们处理数据之前，需要简要了解一下NLP的原理。

一开始我们不使用从Pocket收集来的数据，而是一个最简化的例子，以确保阐明NLP的工作原理。一旦这些都清楚了，我们就可以将其应用于新闻源的语料库。

我们将从一个包含三句话的小语料库开始。

- The new kitten played with the other kittens

- She ate lunch

- She loved her kitten

我们首先将语料库转换为词袋（BOW）的表示。这里将跳过预处理。将语料库转换为词袋表示，包括获取每个单词及其数量，来创建词条-文档的矩阵。在词条-文档的矩阵中，每个唯一的单词对应于一列，而每个文档对应于一行。两者的交点是这个单词在该文档中出现的次数，如表5-1所示。

表5-1

the

new

kitten

played

with

other

kittens

she

ate

lunch

loved

her

1

1

1

1

1

1

1

1

0

0

0

0

0

2

0

0

0

0

0

0

0

1

1

1

0

0

3

0

0

1

0

0

0

0

1

0

0

1

1

请注意，仅仅分析了这三个短句，我们就已经有了12个特征[5]。可以想象，如果处理真实的文档，例如新闻稿，甚至是书籍，那么特征的数量将爆炸式地增长到数十万。为了缓解这个问题，我们可以采取一系列的步骤，删除对分析几乎没有价值的特征。

我们可以采取的第一步是删除停用词。这些单词是如此的普通，它们通常无法告诉你关于文档的内容。常见的英语停用词的示例是“the”、“is”、“at”、“which”和“on”。我们将删除这些词并重新计算词条-文档矩阵，如表5-2所示。

表5-2

new

kitten

played

kittens

ate

lunch

loved

1

1

1

1

1

0

0

0

2

0

0

0

0

1

1

0

3

0

1

0

0

0

0

1

如你在表5-2中所见，特征的数量从12个减少到7个。这很棒，但我们可以更进一步。这里可以执行取词干或词形还原来进一步减少特征。请注意在我们的矩阵中，同时存在“kitten”和“kittens”。使用取词干或词形还原的技术，我们可以将这两者合并为“kitten”，如表5-3所示。

表5-3

new

kitten

play

eat

lunch

love

1

1

2

1

0

0

0

2

0

0

0

1

1

0

3

0

1

0

0

0

1

我们的新矩阵合并了“kittens”和“kitten”，不过这里还进行了其他的修改。我们去掉了“played”和“loved”的后缀，“ate”变成了“eat”。为什么呢？ 这就是词形还原所做的事情。如果你还记得小学的语法课，我们学过从带词尾的形式转变为词的基本形式。现在，如果词形还原是将一个词变换为它的基本形式，那什么是取词干呢？取词干也有同样的目标，不过它使用的方法没有那么复杂。这种方法有时可以产生虚构的单词而不是实际的基本形式。例如，在词形还原中，如果你变换词“ponies”，你会得到“pony”；而使用取词干的技术，你会得到“poni”。

现在，让我们进一步对矩阵应用另一个变换。到目前为止，我们使用了每个单词的简单计数，但是可以采用某个算法以做得更好，它就像数据的过滤器，以提升对于每个文档而言更为特殊的单词。该算法称为词频-逆文档频率或tf-idf。

我们为矩阵中的每个词条计算tf-idf的值。这里来算几个例子。对于文档1中的“new”一词，词频只是1。逆文档频率计算为文档总数除以出现该词的文档数，再取log。对于“new”来说就是log(3/1)，或0.4471。所以对于完整的tf-idf值，我们使用tf × idf，在这里它是1 × 0.4471，或者就是0.4471。对于文档1中的单词“kitten”，tf-idf是2 × log(3/2)，或0.3522。

为剩余的词条和文档完成同样的操作，我们获得了表5-4。

表5-4

new

kitten

play

eat

lunch

love

1

0.4471

0.3522

0.4471

0

0

0

2

0

0

0

0.4471

0.4471

0

3

0

0.1761

0

0

0

0.4471

为什么要做这些？为了获得较高的tf-idf值，一个词条需要在较少的文档中，出现较高的次数。这样，我们可以认为文件由具有高tf-idf值的词条所表示。

使用目前的数据框，我们将训练集转换为tf-idf矩阵。

from sklearn.feature_extraction.text import TfidfVectorizer
vect = TfidfVectorizer(ngram_range=(1,3), stop_words='english', min_df=3)
tv = vect.fit_transform(df['text'])

通过这三行，我们将所有文档转换为tf-idf向量。有几点需要注意。我们传入了一些参数：ngram_range、stop_words和 min_df。让我们逐个来讨论。

首先，ngram_range表示文档是如何被分词的。在之前的例子中，我们将每个单词作为分词，但在这里我们使用每一个、每二个到每三个词组成的序列作为分词。以第二句话为例，“She ate lunch.”此时忽略停用词。那么这句话的ngram将是：“she”、“she ate”、“she ate lunch”、“ate”、“ate lunch”和“lunch”。

接下来的选项是stop_words。我们传入“English”来删除所有的英语停用词。如前所述，这将删除所有缺乏信息含量的词条。

最后的选项是min_df。这里会删除所有文档频率少于三的单词。加入此操作会移除那些非常罕见的词条，并减少矩阵的规模。

现在我们的文章语料库是可供模型操作的数值格式，下面继续将其输送给分类器。

5.4　支持向量机

我们将在本章中使用一个新的分类器，即线性支持向量机。支持向量机的算法使用“最大边缘超平面”，试图对数据点进行线性分离并归类。这是口头上的描述，下面让我们来看看这究竟是什么意思。

假设有两类数据，而我们想用一条线来分隔它们（这里只处理两个特征或维度）。放置这条线最有效的方法是什么？如图5-14所示。

图5-14

在图5-14中，线H_1不能有效地区分两个类，所以我们可以不考虑这条。线H_2能够清楚地区分它们，但是H_3确保了最大的空余边缘。这意味着该线在两个类间最近点的当中，而这些点被称为支持向量。它们也可以看作是图5-15中的虚线。

图5-15

但是，如果数据不能被如此清晰地分到各个类中，那又该怎么办？如果点之间有重叠呢？在这种情况下也有办法。一种是使用所谓的softmargin SVM。这个公式仍然使边缘最大化，但是有一个权衡的策略：如果点错误地落在边缘的某一侧，那么对这样的点进行惩罚。另一种是使用所谓的kernel技巧。这种方法将数据转换到更高维度的空间，让这些数据可以被线性的分割。

如图5-16所示，其中有两个类，不能用单线性平面分开。

图5-16

不过，内核的某种实现可以将图5-16的图像映射到更高维度，如图5-17所示。这允许数据被线性分割。

我们已经将一维特征空间映射到二维特征空间。该映射简单地取每个x值并将其映射到x，x^2。这个变换允许我们添加线性分割的平面。

介绍完这些，现在让我们将tf-idf矩阵传送给SVM模型。

from sklearn.svm import LinearSVC
clf = LinearSVC()
model = clf.fit(tv, df['wanted'])

这个tv参数是我们的矩阵，而df ['wanted']是我们的标签列表。记住标签是'y'或'n'，表示我们是否对文章感兴趣。一旦运行完成，模型就训练完毕了。

图5-17

本章中尚未进行的一个步骤就是正式地评估我们的模型。你总是应该保留一份数据来评估模型，但由于我们需要不断更新模型，并且每天评估它，在这一章我们将跳过这一步。记得这通常是一个糟糕的想法[6]。

现在让我们继续建立每日的新闻源。

5.5　IFTTT与文章源、Google表单和电子邮件的集成

我们使用Pocket来构建训练集，可是现在需要一个流式的文章源来运行训练后的模型。为了完成这项任务，我们将再次使用IFTT、Google表单，以及一个允许我们使用Google表单的Python库。

通过IFTTT设置新闻源和Google表单

希望此时你已经有一个IFTTT账户，如果没有请现在去申请。更多详细信息，请参阅第 3 章——构建应用程序，发现低价的机票。一旦完成此操作后，你需要设置它与文章源和Google表单的集成。

在图5-18中，首先单击Channels，搜索feed，然后单击它进行设置。

在图5-19中，你只需要单击Connect。

图5-18

图5-19

接下来，再次单击右上角的Channels。这次搜索Google Drive，如图5-20所示。

图5-20

单击Google Drive。它会带你到选择Google账户进行连接的页面。选择账户，然后单击Allow允许IFTT访问你的Google Drive账户。完成后，你应该看到图5-21的内容。

图5-21

现在，我们的频道已连接，可以设置自己的文章源了。单击My Recipes，然后单击Create a Recipe。这将让你来到这一步，如图5-22所示。

图5-22

单击this。搜索feed，然后单击它。你会来到这一步，如图5-23所示。

图5-23

在图5-23中，单击New feed item，你应该看到图5-24的内容。

图5-24

然后，将URL添加到输入框中，并单击Create Trigger。一旦这步完成，你会回到添加that的动作，如图5-25所示。

图5-25

单击that，搜索Google Drive，然后单击其图标。一旦完成这步，你会来到这个界面，如图5-26所示。

图5-26

我们希望新闻项目能够流入Google Drive的电子表单，因此请单击Add row to spreadsheet。然后，你可以自定义电子表单，如图5-27所示。

图5-27

我将这个电子表单命名为NewStories，并将其放在名为IFTTT的Google Drive文件夹中。单击Create Action来完成这个Recipe，很快你就会看到新闻项目不断地流入Google Drive的电子表单。注意，它只会添加新进入的新闻项目，而不会添加在创建Google工作表时就已经存在的新闻项目。我建议增加一定数量的信息源。你需要为每个源头创建单独的Recipe。最好为训练集中的网站添加信息源，例如，你使用Pocket保存的那些网站。

让新闻故事在表单中累积一两天。很快，它们就应该看起来像图5-28那样。

图5-28

幸运的是，其中包括了全文的HTML内容。这意味着我们不必再使用embed.ly为每篇文章下载其内容。我们仍然需要从Google表单中下载文章，然后处理其中的文本以删除HTML标记，但这一切都可以相当容易地完成。

为了拉取表单中的文章，我们将使用一个名为gspread的Python库。它可以通过pip安装。完成后，你需要按照指示设置oauth2。可以在http://gspread.readthedocs. org/en/latest/oauth2.html找到如何设置。完成后，你会下载一个JSON格式的凭据文件。一旦你有了这个文件，就能使用client_email键找到电子邮件地址。然后，你需要向该电子邮件共享正在接受新闻故事的NewStories电子表单。只需单击表单右上角的蓝色Share按钮，然后在其中粘贴电子邮件地址就行了。如果运行下列代码，最终你会在Gmail账户中收到未能发送的邮件，但这是预期的结果。请确保在以下的代码中换入你自己的文件路径以及文件名称。

import gspread
from oauth2client.client import SignedJwtAssertionCredentials
json_key = json.load(open(r'/PATH_TO_KEY/KEY.json'))
scope = ['https://spreadsheets.google.com/feeds']
credentials = SignedJwtAssertionCredentials(json_key['client_email'],
json_key['private_key'].encode(), scope)
gc = gspread.authorize(credentials)

现在，如果一切顺利，运行应该没有错误。接下来，你可以下载新闻故事了。

ws = gc.open("NewStories")
sh = ws.sheet1
zd = list(zip(sh.col_values(2),sh.col_values(3), sh.col_values(4)))
zf = pd.DataFrame(zd, columns=['title','urls','html'])
zf.replace('', pd.np.nan, inplace=True)
zf.dropna(inplace=True)
zf

上述代码生成图5-29的输出。

图5-29

如此一来，我们从信息源中下载了所有的文章，并将它们放入了DataFrame对象。现在需要去掉HTML标签。可以使用之前用过的函数来获取文本。然后我们将使用tf-idf向量转换器转变它。

zf.loc[:,'text'] = zf['html'].map(get_text) zf.reset_index(drop=True,
inplace=True)
test_matrix = vect.transform(zf['text'])
test_matrix

上述代码生成图5-30的输出。

图5-30

这里，我们看到向量化是成功的。现在将这个传递给模型，以获取结果。

results = pd.DataFrame(model.predict(test_matrix),
columns = ['wanted'])

上述代码生成图5-31的输出。

图5-31

可以看到，每个故事都有一个预测的结果。让我们来查阅故事的内容，以此来评估结果的准确性。

rez = pd.merge(results,zf, left_index=True, right_index=True)
rez

上述代码生成图5-32的输出。

图5-32

图5-32中两篇文章的预测肯定是对的，因为我的兴趣不在High School Musical和Full House。

此时，我们可以通过查看结果和纠正错误来改进模型。你需要根据自己的判断来决定每个预测准确与否，这里列出我的判断和修改结果。

change_to_no = [130, 145, 148, 163, 178, 199, 219, 222, 223, 226, 235, 279,
348, 357, 427, 440, 542, 544, 546, 568, 614, 619, 660, 668, 679, 686, 740,
829]
change_to_yes = [0, 9, 29, 35, 42, 71, 110, 190, 319, 335, 344, 371, 385,
399, 408, 409, 422, 472, 520, 534, 672]
for i in rez.iloc[change_to_yes].index:
rez.iloc[i]['wanted'] = 'y'
for i in rez.iloc[change_to_no].index:
rez.iloc[i]['wanted'] = 'n'
rez

上述代码生成图5-33的输出。

图5-33

看上去好像有修正了很多错误，但对于超过900篇文章的评价而言，可以说改得非常少了。通过这些更正，我们现在可以将其反馈到模型中进一步提升它。将这些结果添加到之前的训练数据中，然后重建模型。

combined = pd.concat([df[['wanted', 'text']], rez[['wanted',
'text']]])
combined

上述代码生成图5-34的输出。

图5-34

现在我们重建模型。

tvcomb = vect.fit_transform(combined['text'], combined['wanted'])
model = clf.fit(tvcomb, combined['wanted'])

我们通过所有可用的数据重新训练了模型。随着时间的推移你将获得更多的结果，因此可以进行多次这样的操作。你添加的训练数据越多，预测的结果会越好。

我们假设此时你已经有一个训练有素的模型了，并准备开始使用它。下面看看如何部署它来建立个性化的新闻源。

5.6　设置你的每日个性化新闻简报

为了使用新闻故事来创建个人电子邮件，我们将再次使用IFTTT。如我们在第3章——构建应用程序，发现低价的机票——所做，我们将使用Maker频道发送POST请求。不过，这一次的有效载荷将是我们的新闻故事。如果你尚未设置Maker频道，请先完成这一步。具体的操作指令可以在第3章中找到。你还应设置Gmail频道。一旦完成后，我们将添加一个Recipe来组合这两个。

首先，在IFTTT主页单击Create Recipe。然后，搜索Maker Channel，如图5-35所示。

图5-35

选择this，然后选择Receive a web request，如图5-36所示。

图5-36

然后，为该请求提供一个名称。我在这里使用news_event，如图5-37所示。

图5-37

最后，单击Create Trigger来完成所有步骤。接下来，单击that来设置电子邮件。搜索Gmail并单击图5-38所示的图标。

图5-38

选择Gmail后，单击Send an e-mail，如图5-39所示。在这里，你可以定制化自己的电子邮件消息。

图5-39

输入你的电子邮件地址，主题，最后在电子邮件的正文中包括Value1。我们将通过POST请求传递故事标题和链接。单击Create Recipe最终完成这项操作。

现在，我们已经准备就绪，可以生成一个按计划运行的脚本，自动发送我们感兴趣的文章。我们将为此创建一个单独的脚本，不过对于现有的代码，还需要序列化向量转化器和模型。

import pickle
pickle.dump(model, open
(r'/Users/alexcombs/Downloads/news_model_pickle.p', 'wb'))
pickle.dump(vect, open
(r'/Users/alexcombs/Downloads/news_vect_pickle.p', 'wb'))

通过这些代码，我们已经保存了所需的模型。在新的脚本中，我们将读取这些模型来生成新的预测。我们将使用相同的计划库运行第3章的代码。整合所有这些，我们将获得如下的脚本。

# 进行包的导入
import pandas as pd

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import LinearSVC

import schedule
import time

import pickle

import json

import gspread

import requests
from bs4 import BeautifulSoup

from oauth2client.client import SignedJwtAssertionCredentials

# 创建我们的抓取函数
def fetch_news():
try:
vect = pickle.load(open(r'/Users/alexcombs/Downloads/
news_vect_pickle.p', 'rb'))
model = pickle.load(open(r'/Users/alexcombs/Downloads/
news_model_pickle.p', 'rb'))

json_key = json.load(open(r'/Users/alexcombs/Downloads/
APIKEY.json'))
scope = ['https://spreadsheets.google.com/feeds']
credentials = SignedJwtAssertionCredentials(json_key
['client_email'], json_key['private_key'].encode(), scope)
gc = gspread.authorize(credentials)

ws = gc.open("NewStories")
sh = ws.sheet1
zd = list(zip(sh.col_values(2), sh.col_values(3),
sh.col_values(4)))
zf = pd.DataFrame(zd, columns=['title', 'urls', 'html'])
zf.replace('', pd.np.nan, inplace=True)
zf.dropna(inplace=True)

def get_text(x):
soup = BeautifulSoup(x, 'lxml')
text = soup.get_text()
return text

zf.loc[:, 'text'] = zf['html'].map(get_text)

tv = vect.transform(zf['text'])
res = model.predict(tv)

rf = pd.DataFrame(res, columns=['wanted'])
rez = pd.merge(rf, zf, left_index=True, right_index=True)

news_str = ''
for t, u in zip(rez[rez['wanted'] == 'y']['title'],
rez[rez['wanted'] == 'y']['urls']):
news_str = news_str + t + '\n' + u + '\n'

payload = {"value1": news_str}
r = requests.post('https://maker.ifttt.com/trigger/
news_event/with/ key/IFTTT_KEY', data=payload)

# 清理工作表
lenv = len(sh.col_values(1))
cell_list = sh.range('A1:F' + str(lenv))
for cell in cell_list:
cell.value = ""
sh.update_cells(cell_list)
print(r.text)
except:
print('Failed')

schedule.every(480).minutes.do(fetch_news)
while 1:
schedule.run_pending()
time.sleep(1)

这个脚本所做的事情是，每4小时运行一次，从Google表单下载新闻故事，通过模型对这些故事进行预测，再向IFTTT发送POST请求来生成电子邮件，该邮件包含了模型预测我们会感兴趣的故事，然后在最终，它会清除电子表格中的故事，以便下一封电子邮件只会发送新的故事。

恭喜！你现在拥有自己的个性化新闻源了！

5.7　小结

在本章中，我们学习了在训练机器学习模型时，如何使用文本数据。我们还学习了NLP和支持向量机的基础知识。下一章，我们将进一步深入这些技能，并尝试预测什么样的内容会广为流传。

[1]　译者注：这个账户是使用API的开发者账户，和之前保存文章的普通账户有所不同。

[2]　译者注：图5-5中some_long_code所代表的部分。

[3]　将链接中的some_long_code替换为你获得的编码。

[4]　这里的输出编码some_super_long_code是虚构的，只是用于示意。

[5]　译者注：这里每个唯一的单词表示一维特征。

[6]　译者注：作者的原意是通常情况下，不应该跳过评估的环节。

第6章　预测你的内容是否会广为流传

一切都始于一场赌注。2001年，Jonah Peretti，那时还是麻省理工学院的研究生，拖延了他的毕业进程。他没有写论文，而是决定接受耐克公司提供的机会，打造一双个性化的运动鞋。根据当时推出的最新项目，任何人都可以在耐克的新网站——NIKEiD进行这样的设计。唯一的问题是，至少从耐克的角度来看，像Peretti所请求的那样，在耐克鞋上打出“血汗工厂”[1]的字眼，是不能予以考虑的。Peretti通过一系列发给耐克公司的电子邮件表示抗议，他指出“血汗工厂”一词并不属于该公司明令禁止条款中的任何类别，因此不应该导致他的请求被拒绝。

Peretti发现他与耐克的客服代表之间来来回回的邮件非常有意思，而且觉得别人可能也会感兴趣，所以将这些信转发给一些亲近的朋友。几天之内，电子邮件已经进入了全世界的各种收件箱。主流的媒体，例如Time、Salon、The Guardian，甚至Today show的节目都谈论到了这个。Peretti是整个病毒式传播的中心。

不久之后，开始讨论的问题变成这种事情是否可以复制。他的朋友，Cameron Marlow，一直在准备写有关病毒式传播的博士论文，并且非常肯定这样的事情对于任何人来说都很难刻意为之的。Marlow和 Peretti打赌，Peretti不能重复耐克事件这样的成功。Peretti迎接了挑战。

一晃15年过去了，Jonah Peretti领导着一个名为BuzzFeed的网站，它已经成为了病毒式内容的同义词。2015年该网站拥有超过7700万的独立访问者，在总触达率的排名中它高于纽约时报。我认为Peretti赢得了这场赌注。

但他究竟是怎么做的呢？ Peretti如何发明秘密公式，来创建像野火一般蔓延的内容？在本章中，我们将试图解开其中一些奥秘。我们将观察一些广为流传的内容，并尝试找到它们的共同点，看看它们和那些人们不太愿意分享的内容相比，到底有什么区别。

我们将在本章讨论以下主题。

- 关于病毒性，研究告诉我们了些什么？

- 获得被共享的内容和数量。

- 探索可共享性的特征。

- 构建预测性的内容评分模型。

6.1　关于病毒性，研究告诉我们了些什么

了解共享行为是一件很重要的事情。随着消费者对传统广告的日益麻木，推送应该不再限于简单的触达，而是应该讲述更有吸引力的故事。这种尝试的成功在社交性分享中变得越来越明显。为什么要花费力气做这些呢？因为对于一个品牌而言，每一次分享都意味着触达了一个消费者——而且没有花费1分钱。

鉴于其价值，几个研究人员观察了分享行为，希望理解是什么激励人们这么去做。

研究人员发现了以下几点原因。

- 为他人提供实用的价值（利他主义动机）。

- 将自身和某些想法以及概念相关联（自我认同动机）。

- 通过共同的情感与他人联系（公社动机）。

关于最后一个动机，一个经过特别精心设计的研究查看了7,000份来自《纽约时报》的内容，以观察情感对分享行为的影响。他们发现，简单的情感和情绪不足以解释分享行为，但当分享与情绪响应相结合时，解释力更大。例如，虽然悲伤具有很强的负面性，但被认为是低响应状态。另一方面，愤怒既具有负面性，又具有相匹配的高响应状态。鉴于此，使读者产生悲伤情绪的故事，其产生的进一步传播要比使读者产生愤怒情绪的故事产生少得多，如图6-1所示。

这篇文章包含了动机方面的研究。然而，如果我们保持这些因素不变，其他属性将如何影响某段内容的病毒式传播？其中一些因素可能包括：标题的语句、标题的长度、标题的词性、内容的长度、发帖的社交网络、主题、主题的时间轴等等。毫无疑问，一个人可以花费毕生的精力研究这种现象。然而，现在，我们只是花费接下来30页左右的内容进行相关研究。

图6-1

6.2　获取分享的数量和内容

在开始探索哪些特征会使得内容易于共享之前，我们手头上需要足够的内容。我们还需要每份内容在各种社交网络上的分享次数。幸运的是，获取这些并没有多大困难。我会使用网站ruzzit.com。

这是一个相对较新的网站——它仍处于beta测试阶段[2]，但它会跟踪最常被分享的内容，这正是我们需要的，如图6-2所示。

我们将从页面中抓取内容——不幸的是，没有可以直接使用的API接口。而且，因为该网站使用了无限滚动的机制，我们需要使用第3章的老朋友，Selenium和PhantomJS。现在开始抓取吧。

图6-2

我们将从最初需要导入的包开始。

import requests
import pandas as pd
import numpy as np
import json
import time
from selenium import webdriver
pd.set_option('display.max_colwidth', 200)

接下来，我们将设置Selenium浏览器。这里仅选择该站点过去一年的文章来生成URL网址列表。需要设定浏览器的大小，以便获得标准的桌面外观，并且每隔15秒请求一次，这事关礼仪[3]。我们也将向下滚动相当于50页的内容（每页有10篇文章）。

browser = webdriver.PhantomJS()
browser.set_window_size(1080,800)
browser.get("http://www.ruzzit.com/en-US/Timeline?media=Articles&timeline=
Year1&networks=All")
time.sleep(3)
pg_scroll_count = 50
while pg_scroll_count:
browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(15)
pg_scroll_count -= 1
titles = browser.find_elements_by_class_name("article_title")
link_class = browser.find_elements_by_class_name("link_read_more_article")
stats = browser.find_elements_by_class_name("ruzzit_statistics_area")

在最后一节中，我们选择了分析所需的页面元素。接下来，需要进一步解析它们以获取文本信息。

我在分析中去除Twitter所提供的分享次数。该公司在2015年年底决定从其标准API中删除此项数据。鉴于此，其展示的次数不太可靠。为了避免数据被污染的风险，最好直接去除这些信息。

all_data = []
for title, link, stat in zip(titles, link_class, stats):
all_data.append((title.text,\
link.get_attribute("href"),\
stat.find_ element_by_class_name("col-md-
12").text.split(' shares')[0],
stat.find_element_by_class_name("col-md-
12").text.split('tweets\n')
[1].split('likes\n0')[0],
stat.find_element_by_class_name("col-md-
12").text.split('1's\n')[1].split(' pins')[0],
stat.find_element_by_class_name("col-md-
12").text.split('pins\n')[1]))

接下来，我们将它放入一个数据框。

df = pd.DataFrame(all_data, columns=['title', 'link', 'fb', 'lnkdn',
'pins', 'date'])
df

上述代码生成图6-3的输出。

图6-3

这是一个好的开始，但我们需要清理数据。你会注意到所有的链接都是通过ruzzit.com的重定向。我们将通过跟踪链接，检索原始站点的链接来解决这个问题，具体如下。

df = df.assign(redirect = df['link'].map(lambda x: requests.get(x).url))

此行代码使用requests库检索故事的真实URL（重定向之后的），如图6-4所示。

图6-4

如果现在检查数据框DataFrame，我们可以看到该网站的原始链接。你会注意到在第一行有CNN的主页。仔细研究后，发现有17个故事指向某个网站的主页。这是因为它们已被删除。另一个原因是一些链接指向的是图像而不是文章。

以下代码将识别这两个问题，并删除有问题的行。

def check_home(x):
if '.com' in x:
if len(x.split('.com')[1]) < 2:
return 1
else:
return 0
else:
return 0
def check_img(x):
if '.gif' in x or '.jpg' in x:
return 1
else:
return 0
df = df.assign(pg_missing = df['pg_missing'].map(check_home))
df = df.assign(img_link = df['redirect'].map(check_img))
dfc = df[(df['img_link']!=1)&(df['pg_missing']!=1)]
dfc

上述代码生成图6-5的输出。

图6-5

现在让我们进行下一步，获取完整的文章和其他元数据。就如上一章，我们将使用embed.ly中的API接口。如果对于其设置你需要帮助，请返回前一章参看详细介绍。这里将使用embed.ly来检索文章的标题、HTML和一些附加数据，例如引用和图像。

def get_data(x):
try:
data = requests.get('https://api.embedly.com/1/extract?
key=SECRET_ KEY7&url=' + x)
json_data = json.loads(data.text)
return json_data
except:
print('Failed')
return None
dfc = dfc.assign(json_data = dfc['redirect'].map(get_data))
dfc

上述代码生成图6-6的输出。

图6-6

现在每篇文章都有一列JSON数据。这里将解析这个JSON数据，抽取出我们有兴趣探索的每个特征列。先从基本信息开始：网站、标题、HTML和图片数量。

def get_title(x):
try:
return x.get('title')
except:
return None
def get_site(x):
try:
return x.get('provider_name')
except:
return None
def get_images(x):
try:
return len(x.get('images'))
except:
return None
def get_html(x):
try:
return x.get('content')
except:
return None
dfc = dfc.assign(title = dfc['json_data'].map(get_title))
dfc = dfc.assign(site = dfc['json_data'].map(get_site))
dfc = dfc.assign(img_count = dfc['json_data'].map(get_images))
dfc = dfc.assign(html = dfc['json_data'].map(get_html))
dfc

上述代码生成图6-7的输出。

图6-7

大多数行都成功抽取了页面的HTML内容，但是有一些却没有返回任何值。检查了空白的行之后，我们发现它们似乎主要来自BuzzFeed这个网站。这是合理的，因为这种页面主要是图片和小测验。这是一个小小的烦恼，我们不得不将就着用一下。

现在让我们取出HTML并将其转换为文本。这里将使用BeautifulSoup库为我们实现这个目标。

from bs4 import BeautifulSoup
def text_from_html(x):
try:
soup = BeautifulSoup(x, 'lxml')
return soup.get_text()
except:
return None
dfc = dfc.assign(text = dfc['html'].map(text_from_html))
dfc

上述代码生成图6-8的输出。

图6-8

现在添加额外的特征。我们将添加页面上第一个图像中最突出的颜色。由embed.ly生成的JSON数据保存了每个图像的RGB值，这些值体现了对应图像的颜色，所以这将是一个简单的任务。

import matplotlib.colors as mpc
def get_rgb(x):
try:
if x.get('images'):
main_color = x.get('images')[0].get('colors') [0].get('color')
return main_color
except:
return None
def get_hex(x):
try:
if x.get('images'):
main_color = x.get('images')[0].get('colors') [0].get('color')
return mpc.rgb2hex([(x/255) for x in main_color])
except:
return None
dfc = dfc.assign(main_hex = dfc['json_data'].map(get_hex))
dfc = dfc.assign(main_rgb = dfc['json_data'].map(get_rgb))
dfc

上述代码生成图6-9的输出。

图6-9

我们提取出第一个图像中最突出的颜色并保存其RGB值，同时也将其转换为HEX十六进制值。稍后检查图像颜色的时候也会使用这个信息。

我们几乎完成了数据的处理部分，不过还需要转换一些从Ruzzit获取的数字。我们所拥有的分享次数是用于显示目的，而不是用于分析的格式，如图6-10所示。

图6-10

我们需要清理fb、lnkdn、pins和date（日期）`列，将它们从字符串表示转化为数字类型，如下所示。

def clean_counts(x):
if 'M' in str(x):
d = x.split('M')[0]
dm = float(d) * 1000000
return dm
elif 'k' in str(x):
d = x.split('k')[0]
dk = float(d.replace(',','')) * 1000
return dk
elif ',' in str(x):
d = x.replace(',','')
return int(d)
else:
return x
dfc = dfc.assign(fb = dfc['fb'].map(clean_counts))
dfc = dfc.assign(lnkdn = dfc['lnkdn'].map(clean_counts))
dfc = dfc.assign(pins = dfc['pins'].map(clean_counts))
dfc = dfc.assign(date = pd.to_datetime(dfc['date'], dayfirst=True))
dfc

上述代码生成图6-11的输出。

图6-11

最后，我们将添加最后一个特征，每列的字数统计。我们可以通过空格来切分文本，然后采取最终的计数。操作如下。

def get_word_count(x):
if not x is None:
return len(x.split(' '))
else:
return None
dfc = dfc.assign(word_count = dfc['text'].map(get_word_count))
dfc

上述代码生成图6-12的输出。

随着我们的数据准备就绪，现在可以开始进行分析了。我们将尝试寻找什么样的特征会使内容具有更高的传播度。

图6-12

6.3　探索传播性的特征

我们在这里收集的故事代表了在过去一年中，大约500个传播度最高的作品。我们将尝试解构这些文章来寻找使它们广为流传的共同特征。先从图像数据开始。

6.3.1　探索图像数据

让我们来看看每个故事中包含的图片数量。我们运行一个数值统计，然后绘制图表。

dfc['img_count'].value_counts().to_frame('count')

上述代码生成图6-13的输出。

图6-13

现在，让我们绘制这些信息。

fig, ax = plt.subplots(figsize=(8,6))
y = dfc['img_count'].value_counts().sort_index()
x = y.sort_index().index
plt.bar(x, y, color='k', align='center')
plt.title('Image Count Frequency', fontsize=16, y=1.01)
ax.set_xlim(-.5,5.5)
ax.set_ylabel('Count')
ax.set_xlabel('Number of Images')

上述代码生成图6-14的输出。

图6-14中的数字已经令人惊讶了。绝大多数的故事里都有五张图片，而只有一张甚至没有图片的故事是相当罕见的。

因此，我们发现人们倾向于分享包含大量图片的内容。下面来看看这些图像中最常见的颜色。

mci = dfc['main_hex'].value_counts().to_frame('count')
mci

上述代码生成图6-15的输出。

图6-14

图6-15

图6-15看上去不是很有帮助，因为我们并不理解HEX值代表什么颜色。不过，这里可以使用pandas中的一个新功能，称为conditional formatting，它可以帮助我们。

mci['color'] = ' '
def color_cells(x):
return 'background-color: ' + x.index
mci.style.apply(color_cells, subset=['color'], axis=0)
mci

上述代码生成图6-16的输出。

这当然很有帮助。我们可以看到一些颜色，例如淡蓝色、黑色和绿色（这里是以灰度渲染的），但颜色的粒度是如此之细，总共有超过450 个唯一的值。让我们使用一点聚类的技术将其转化成更容易管理的范围。由于这里有每个颜色的RBG值，我们可以创建一个三维空间，并使用K-means算法来聚集它们。我不会在这里讨论算法的细节，不过它是一个相当简单的迭代算法，它通过测量每个数据点到到中心点的距离来生成聚类，并迭代式地重复该过程。算法需要我们选择k的值，或者说是期望的聚类数量。由于RGB值的范围是从0到256，我们将使用256的平方根，也就是16。如此一来，我们可获得一个可管理的数量，同时保留调色板的特点。

我们首先将RGB值拆分成单独的列，如下所示。

def get_csplit(x):
try:
return x[0], x[1], x[2]
except:
return None, None, None
dfc['reds'], dfc['greens'], dfc['blues'] =
zip(*dfc['main_rgb'].map (get_csplit))

接下来，我们将使用它来运行我们的K-means模型并获取中心值。

from sklearn.cluster import KMeans
clf = KMeans(n_clusters=16)
clf.fit(dfc[['reds', 'greens', 'blues']].dropna())
clusters = pd.DataFrame(clf.cluster_centers_, columns=['r', 'g', 'b'])
clusters

上述代码生成图6-17的输出。

图6-16

图6-17

现在，从每页的首张图片中，我们获得了16个最受欢迎的主流颜色。接下来使用pandas 的DataFrame.style()方法以及我们刚刚创建的为单元格填色的函数，来看看这些主流颜色长什么样子。这里需要将索引设置为等于三列中十六进制值那列，以使用我们的color_cells函数，具体如下。

def hexify(x):
rgb = [round(x['r']), round(x['g']), round(x['b'])]
hxc = mpc.rgb2hex([(x/255) for x in rgb])
return hxc
clusters.index = clusters.apply(hexify, axis=1)
clusters['color'] = ' '
clusters.style.apply(color_cells, subset=['color'], axis=0)

上述代码生成图6-18的输出。

图6-18

那么，你看到了。这些是广为流传的内容中最常见的颜色（至少在第一张图像中）。比预期更单调一点：虽然有一些蓝色和红色，但多数还是棕色这种灰蒙蒙的色调。

现在让我们继续检视故事的标题。

6.3.2　探索标题

我们将从创建一个函数开始，使用它来检查最常见的元组。将其设置好之后，将来在正文上也可以使用它。

from nltk.util import ngrams
from nltk.corpus import stopwords
import re
def get_word_stats(txt_series, n, rem_stops=False):
txt_words = []
txt_len = []
for w in txt_series:
if w is not None:
if rem_stops == False:
word_list = [x for x in ngrams(re.findall('[a-z0-
9']+', w.lower()), n)]
else:
word_list = [y for y in ngrams([x for x in
re.findall('[a-z0-9']+', w.lower())\
if x not in stopwords.words('english')], n)]
word_list_len = len(list(word_list))
txt_words.extend(word_list)
txt_len.append(word_list_len)
return pd.Series(txt_words).value_counts().to_frame('count'),
pd.DataFrame (txt_len, columns=['count'])

这里有很多要解释，所以让我们逐步分析。我们创建了一个函数并接收Series、一个整数和一个布尔值作为输入。整数决定了我们将用于n元语法解析的n， 而布尔值决定我们是否排除停用词。函数返回每行[4]的元组[5]数目和每个元组的频率。

下面让我们在标题上运行这个函数，暂时保持停用词。先从一元语法开始。

hw,hl = get_word_stats(dfc['title'], 1, 0)
hl

上述代码生成图6-19的输出。

现在，每个标题的字数都有了，让我们来看看其统计信息。

hl.describe()

上述代码生成图6-20的输出。

图6-19

图6-20

我们可以看到传播广泛的故事其标题长度的中位数恰好在11个字。让我们来看看最常用的那些单词，如图6-21所示。

这种信息不是很有价值，但它符合我们的期望。让我们来看看二元语法的同类信息。

hw,hl = get_word_stats(dfc['title'], 2, 0)
hw

上述代码生成图6-22的输出。

图6-21

图6-22

这肯定是更有趣。从中可以看到标题中的某些部分反复出现。最突出的两个是(donald, trump[6])和(die，at)。Trump是有道理的，因为他做了一些抓眼球的声明，但令人惊讶的是看到关于死亡的标题。快速浏览过去一年的头条新闻，发现一些知名人物最近去世了，所以这也有一定的意义。

现在让我们去掉停用词，再次运行代码。

hw,hl = get_word_stats(dfc['title'], 2, 1)
hw

上述代码生成图6-23的输出。

再次，我们看到了许多期待的东西。看起来如果我们改变数字的解析方式（用单个标识符，例如[number]，来替换每一个数字），可能会看到更多这样的元组排名靠前。如果你愿意尝试，我会把这个练习留给你。

让我们再来看看三元语法。

hw,hl = get_word_stats(dfc['title'], 3, 0)

上述代码生成图6-24的输出。

图6-23

图6-24

看来，元组包括的词语越多，标题越来越像经典的BuzzFeed风格。让我们看看事实是否如此。我们还没看过哪个网站产生的病毒式传播故事最多，这里通过图表来看看BuzzFeed是否领先。

dfc['site'].value_counts().to_frame()

上述代码生成图6-25的输出。

图6-25

我们可以清楚地看到，BuzzFeed在名单中占主导地位，和第二位Huffington Post拉开了明显的距离，而这个网站也有Jonah Peretti的参与。看起来研究病毒式传播的科学可以产生巨大的收益。

到目前为止，我们已经检视了图像和标题，接下来继续观察故事的正文。

6.3.3　探索故事的内容

在上一节中，我们创建了一个函数来发现故事标题中常见的n元语法，现在应用这个函数来探索故事的完整内容。

我们将这样开始：去除停用词，使用二元组。因为与故事主体相比，标题是非常短小的，所以包含停止词是有一定意义的，但在故事正文中，通常去除它们是更合理的做法。

hw,hl = get_word_stats(dfc['text'], 2, 1)
hw

上述代码生成图6-26的输出。

有趣的是，我们在标题中看到的轻松愉快的元组在这里完全消失了。正文充满了关于恐怖主义、政治和种族关系的讨论。

标题的内容是轻松愉快的，而正文的内容却是黑暗而富有争议的，这怎么可能？我的猜测是“13 Puppies Who Look Like Elvis”比“The History of US Race Relations”这类文章的字数要少的多[7]。

让我们再来看看一个实验。这次将评估故事正文的三元组。

hw,hl = get_word_stats(dfc['text'], 3, 1)
hw

上述代码生成图6-27的输出。

我们似乎突然进入了广告和社交活动的领域。有了这些，让我们继续构建内容评分的预测模型。

图6-26

图6-27

6.4　构建内容评分的预测模型

现在让我们使用所学到的东西来创建一个模型，它可以根据给定的内容，预估其被分享的次数。我们将使用前文已经创建的特征，以及几个额外的特征。

理想情况下，我们需要一个更大的内容样本，特别是有更多分享次数的内容。尽管这样，我们还是就手头上的数据进行操作。

我们将使用一种称为随机森林回归（random forest regression）的算法。在前面的章节中，我们看过随机森林一个更典型的实现，就是基于分类的模型。而这里，我们将使用回归并尝试预测分享的次数。当然，我们也可以将分享次数划分为不同的范围，转化成分类问题，但是对于连续变量的处理，最好还是使用回归的技术。

首先，我们将创建一个极其简单的模型。使用的特征包括图像的数量、网站和字数。我们将使用Facebook的Like数来训练模型。

首先导入sci-kit学习库，然后这样预处理数据：删除包含null值的行，重置行的索引编号，最后将数据框切分为训练集和测试集。

from sklearn.ensemble import RandomForestRegressor
all_data = dfc.dropna(subset=['img_count', 'word_count'])
all_data.reset_index(inplace=True, drop=True)
train_index = []
test_index = []
for i in all_data.index:
result = np.random.choice(2, p=[.65,.35])
if result == 1:
test_index.append(i)
else:
train_index.append(i)

我们使用一个随机数生成器来确定哪些行的内容（基于它们的索引）将被放置在哪个集合中，其概率的分配大约为2/3和1/3。设定这样的概率，可以确保我们获得的训练样本数量约为测试样本数量的两倍。如下所示，我们可以将其打印出来。

print('test length:', len(test_index), '\ntrain length:', len(train_index))

上述代码生成图6-28的输出。

图6-28

现在，我们将继续准备数据。接下来，需要为不同的网站设置分类型的编码。目前，DataFrame对象含有每个站点用字符串表示的名称。我们需要使用虚构的编码。这将为每个站点创建一个列。如果该行源于该特定的网站，那么该列将用1填充；所有对应其他网站的列用0填充。如下处理。

sites = pd.get_dummies(all_data['site'])
sites

上述代码生成图6-29的输出。

虚构的编码可以在图6-29中看到。

现在我们继续将数据分成训练集和测试集，如下所示。

y_train = all_data.iloc[train_index]['fb'].astype(int)
X_train_nosite = all_data.iloc[train_index][['img_count', 'word_count']]
X_train = pd.merge(X_train_nosite, sites.iloc[train_index],
left_index=True, right_index=True)
y_test = all_data.iloc[test_index]['fb'].astype(int)
X_test_nosite = all_data.iloc[test_index][['img_count', 'word_count']]
X_test = pd.merge(X_test_nosite, sites.iloc[test_index], left_index=True,
right_index=True)

图6-29

这样，我们设置了X_test、X_train、y_test和y_train变量。现在使用它们构建模型。

clf = RandomForestRegressor(n_estimators=1000)
clf.fit(X_train, y_train)

通过这两行代码，我们训练了模型。下面，使用这个模型来预测测试集中的数据，能够收获多少Facebook的Like。

y_actual = y_test
deltas = pd.DataFrame(list(zip(y_pred, y_actual, (y_pred –
y_actual)/(y_actual))), columns=['predicted', 'actual', 'delta'])
deltas

上述代码生成图6-30的输出。

在这里，我们看到并排的预测值、实际值和差值百分比。下面看看这个结果的描述性统计。

deltas['delta'].describe()

上述代码生成图6-31的输出。

图6-30

图6-31

这看起来很惊人。错误的中位数是0！好吧，不幸的是，这不是一个特别有用的信息，因为在两侧正和负的错误都存在，并且它们会中和平均值，就是我们在这里所看的。让我们来看一个更具信息性的指标来评估这个模型。我们将把均方根误差（也就是标准差）比上实际平均值。

首先，来说明一下为什么这样计算更有价值，让我们在两个示例的数据序列上运行以下场景。

a = pd.Series([10,10,10,10])
b = pd.Series([12,8,8,12])
np.sqrt(np.mean((b-a)**2))/np.mean(a)

这将产生图6-32的输出。

现在将其与平均值进行比较。

(b-a).mean()

这将产生图6-33的输出。

图6-32

图6-33

显然前者是更有意义的统计[8]。现在为我们的模型运行这个统计。

np.sqrt(np.mean((y_pred-y_actual)**2))/np.mean(y_actual)

上述代码生成图6-34的输出。

图6-34

突然发现，我们的模型没有那么优秀了[9]。让我们为模型添加另一个特征。来看看添加单词的计数是否会有助于模型的提升。我们将使用计数的向量转化器来做到这一点。和之前对于网站名称的处理很像，我们将单个词和n元语法转换成特征。

from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer(ngram_range=(1,3))
X_titles_all = vect.fit_transform(all_data['title'])
X_titles_train = X_titles_all[train_index]
X_titles_test = X_titles_all[test_index]
X_test = pd.merge(X_test, pd.DataFrame(X_titles_test.toarray(),
index=X_test.index), left_index=True, right_index=True)
X_train = pd.merge(X_train, pd.DataFrame(X_titles_train.toarray(),
index=X_train.index), left_index=True, right_index=True)

在这些代码行中，我们将新的n元语法特征加入到现有的特征中。现在重新训练模型，看看是否有任何改进。

clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
deltas = pd.DataFrame(list(zip(y_pred, y_actual, (y_pred -
y_actual)/(y_actual))), columns=['predicted', 'actual', 'delta'])
deltas

上述代码生成图6-35的输出。

图6-35

再次使用以下方式检查错误的情况。

np.sqrt(np.mean((y_pred-y_actual)**2))/np.mean(y_actual)

这将产生图6-36的输出。

看来模型有了一定的改进。让我们为模型继续添加一个特征。添加标题的字数如下。

all_data = all_data.assign(title_wc = all_data['title'].map(lambda x:
len(x.split(' '))))
X_train = pd.merge(X_train, all_data[['title_wc']], left_index=True,
right_index=True)
X_test = pd.merge(X_test, all_data[['title_wc']], left_index=True,
right_index=True)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
np.sqrt(np.mean((y_pred-y_actual)**2))/np.mean(y_actual)

上述代码生成图6-37的输出。

图6-36

图6-37

看来，每项特征都适度改进了我们的模型。当然还有更多可以添加到模型中的特征。例如，我们可以增加星期几发布和发布的具体时间，还可以通过在标题上运行正则表达式来确定某篇文章是否采用了清单体[10]，或者可以检查每篇文章的情绪。对于病毒式传播的建模而言，这些只是探索潜在重要的特征的开始。为了继续减少模型中的错误，我们肯定还需要不断进步。

我还应该提到，目前针对我们的模型只做了最粗略的测试。每种测量方法都应该运行多次以获得更准确的错误率表示。对于之前两个模型，可能并没不存在统计上可辨别的差异，因为我们只执行了一次测试。

6.5　小结

在本章中，我们研究了病毒式传播的共同特征，以及如何使用随机森林回归构建模型，并预测传播性。我们还学习了怎样结合多种类型的特征，以及如何将数据分成训练集和测试集。

希望你能利用这里学到的知识，建立下一个病毒式传播的王国。如果这还不行，也许下一章关于股市机会的掌控将会是非常有用的。

[1]　译者注：耐克认为Peretti这样做是含沙射影地指向耐克工厂。

[2]　译者注：至少在原著作者撰写本书的时候是如此。

[3]　译者注：频繁地爬取网站内容将对其服务器造成不必要的压力，作者认为这样做是不合礼仪的。

[4]　译者注：也就是每篇文章。

[5]　译者注：这里的元组是指n元语法生成的元组。

[6]　译者注：Donald Trump是美国2016年总统大选的候选人之一。

[7]　译者注：作者的意思是标题通常长度都非常短，而正文的长度相互之间差异很大，所以造成了统计结果迥然。

[8]　译者注：作者的意思是，他所建议的统计方式更能体现数据的波动。

[9]　译者注：在这里，和平均值相比，较大的波动意味着较大的预测误差。

[10]　译者注：清单体（Listicle）是英文中的新词，它是现代信息传播最吸引眼球的方式之一。

第7章　使用机器学习预测股票市场

在生物学中，有一个相当知名的现象，称为红色皇后的竞赛。这个想法是每个有机体都加入一种不能获得巨大优势的竞争，而只是能够跟上不断变化的、充满对立生物的环境。

这个词来自Lewis Carol的“Through the Looking Glass”一书：“现在，这里，你要理解，拼命的奔跑，你才可以保持在原地。”

这种现象的一个例子是耐抗生素的超级细菌之兴起，例如MRSA。随着我们研发出越来越强大的抗生素，这些细菌也在进化越来越好的防御系统，以打败我们的药物。

这似乎和股票市场没什么关系，但是同样的现象每天都在金融市场出现。就像有生命的有机体那样，市场每天都在演化，今天还能成立的事情，明天可能就完全行不通了。

例如，一篇偶尔发布的报道会提醒金融界存在一种现象，该现象是基于某些有利可图的异常情况。通常这样现象是一些外部施加的、现实世界约束的下游效应。例如年终税收导致的亏损卖出。由于税法的性质，对于交易者而言，在年底卖出他们的亏损股票是很合理的。接近年底的时候，这将对亏损的股票产生价格下行的压力，使得该股票低于其合理的市场价值。这也意味着1月份来临的时候，下行的压力没有了，这种股票面临的是向上的压力，原因是新的资金再次投入这些被低估的资产。但是，一旦这种现象被广播，对于交易者而言，他们只有做到先行一步才有意义，也就是在12月底买入股票，并在1月卖给其他交易者。这些试图获得早期优势的新交易者，现在进入了市场并稀释了盈利的效果。

他们正在缓解年底的抛售压力，并减少了1月份的购买动力。这种效应本质上是随着利润一起套利。曾经奏效的操作不再灵验，交易者开始放弃这个战略，并继续寻找下一个新的事物。为了避免“不进则退”，交易者必须快速地适应。

在本章中，我们将花一些时间讨论如何构建和测试交易策略。不过，我们将花更多的时间，研究如何“不”这样做。在设计自己的系统时，有无数的陷阱需要规避，这几乎是一个不可能完成的任务。然而，这个过程可以有很多的乐趣——有时，它甚至可以创造利润。

我们将在本章讨论以下主题。

- 市场分析的类型。

- 关于股票市场，研究告诉我们些什么？

- 如何开发一个交易系统。

- 构建和评估你的机器学习模型。

请不要使用本章中的信息做傻事。不要拿你无法承受的金额去冒险。如果你决定使用任何这里学习的内容进行交易，你都要为自己的行为负责。这里所学的知识都不应该被视为任何类型的投资建议，我不会对你的行为承担任何责任。

7.1　市场分析的类型

刚开始，让我们先讨论一些涉及金融市场的关键术语和分析方法。虽然有无数的金融工具，如股票、债券、交易型开放式指数基金（ETF）、汇市、互惠掉换等，这里我们将讨论的内容限于股票和股票市场。股票只是上市公司所有权的一小部分。当公司的未来前景被看好时，人们就预期股票的价格会增长，而前景不被看好时，预期价格就会下跌。

投资者一般属于下列这两个阵营之一。第一阵营相信基本面的分析。基本面分析师通过公司的财务状况来寻找信息，查明市场对公司的股票是否低估。这些投资者关注各种因素，例如营收、盈利和现金流。他们也观察与这些值有关的许多比率。很多时候，这涉及查看两家公司财务对比的情况。

第二阵营的投资者是技术分析师。技术分析师认为股票的股价已经反映了所有可用的公共信息，而研究基本面很大程度上是浪费时间。他们相信，通过查阅历史价格——股票图表——一个人可以看到价格可能上涨、下跌或停滞不前的区间。一般来说，他们觉得这些图表为揭示投资者的心理提供了线索。

这两个群体的共同点是一个潜在的信念，它认为正确的分析可以让我们获得利润。不过，这是真的吗？

7.2　关于股票市场，研究告诉我们些什么

在过去50年中，股票市场最有影响力的理论也许是有效市场假设。这个理论是由Eugene Fama发明的，它认为市场是理性的，所有可用的信息都充分反映在股票价格上。因此，某位投资者不可能在风险调整的基础上始终“打败市场”。有效市场假说通常被认为具有三种形式：弱式、半强式和强式。

在弱式下，市场是有效的意义在于，投资者无法使用过去的价格信息来预测未来的价格。股票反映出的信息是相对较快的。此外，虽然技术分析是无效的，在某些情况下基本面分析可能是有效的。

在半强式中，价格以无偏的方式，立即反映出所有相关的最新公开信息。这种情况下，技术分析和基本面分析都无法奏效。最后，在强式中，股票价格反映所有公开和私密信息。

基于这些理论，想利用市场的模式来赚钱，希望并不大。幸运的是，虽然市场整体上是以有效的形态运作，但是人们还是发现了一些不太高效的地方。往往大多数现象都是短暂的，不过有些确是一直存在。最普遍的——即使是根据Fama的理论——是动量交易策略的优秀表现。

那么，什么是动量交易策略呢？其主旨有很多变体，但其基本思想是按照股票前一段时间的回报，将它们从高到低进行排名。买入表现最佳的股票并持有一段时间，然后在一定时期之后重复该过程。一个典型的长期动量交易策略可能是购买标准普尔500指数中，过去一年的表现排名前25的股票，持有它们一年，然后重复这个过程。

这听起来像一个极其简单的策略——确实如此。但是，它已经在不断地返回和人们预期不一致的结果。为什么呢？你可以想象，很多研究都有观测这种效应，其假设是关于人们如何处理新的信息，存在某些内在的、系统性的偏差。研究表明他们短期内对新闻反应不足，然而长期来看对新闻又反应过度了。

随着越来越多的交易者学习这个理论并涌入市场，这种效应会实现获利吗？近年来有一些证据显露出来，但仍不明朗。无论如何，其效果是真实的，并且持续时间远远超过有效市场假设当前所能解释的那部分，所以还有希望。有了这种微小的希望，让我们继续下一步，看看如何发现属于自己的异常。

7.3　如何开发一个交易策略

我们首先专注于技术方面，以此开始策略的研发。来看看在过去的几年中，标准普尔500指数的表现。我们将使用pandas的功能来导入数据。这让我们可以访问多个股票数据来源，包括Yahoo!和Google。

首先，需要安装datareader包。这可以使用命令行通过pip安装：pip install pandas_datareader。

然后，我们将继续设置包的导入，如下所示。

import pandas as pd
from pandas_datareader import data, wb
import matplotlib.pyplot as plt

%matplotlib inline
pd.set_option('display.max_colwidth', 200)

现在，我们将获取SPY ETF的数据。它代表了标准普尔500的股票。我们将拉取从2010年初到2016年3月初的数据。

import pandas_datareader as pdr

start_date = pd.to_datetime('2010-01-01')
stop_date = pd.to_datetime('2016-03-01')

spy = pdr.data.get_data_yahoo('SPY', start_date, stop_date)

上述代码生成图7-1的输出。

图7-1

现在可以绘制这些数据了。我们只选择收盘价，如下所示。

spy_c = spy['Close']

fig, ax = plt.subplots(figsize=(15,10))
spy_c.plot(color='k')
plt.title("SPY", fontsize=20)

上述代码生成图7-2的输出。

图7-2

在图7-2中，我们看到了选定时期内，标准普尔500指数日收盘价的价格图。让我们进行一点分析，看看如果投资这个ETF，该期间内的回报将是多少。

我们先拉取首个开盘日的数据。

first_open = spy['Open'].iloc[0]
first_open

上述代码生成图7-3的输出。

接下来，让我们得到该期间最后一天的收盘价。

last_close = spy['Close'].iloc[-1]
last_close

这将导致图7-4的输出。

图7-3

图7-4

最后，让我们看看整个时期的变化。

last_close - first_open

上述代码生成图7-5的输出。

图7-5

因此，看起来在这个时期开始的时候，购入100股股票会花费我们大约11,237美元，该时期结束时，相同的100股股份价值约为19,811美元。这笔交易将给我们带来超过76％的收益。相当不错了。

现在让我们看看同一时期内，盘中交易的收益。这个操作假设我们在每日开盘时买入股票，并在当天收盘时卖出股票。

spy['Daily Change'] = pd.Series(spy['Close'] - spy['Open'])

这行代码将提供每天从开盘到收盘的变化。让我们来看看。

spy['Daily Change']

上述代码生成图7-6的输出。

图7-6

现在让我们将这段时期的变化加和。

spy['Daily Change'].sum()

上述代码生成图7-7的输出。

图7-7

所以，你可以看到，我们的收益已经从超过85点的增长，下降到刚刚过41点的增长。哎哟！一半以上的市场收益来自于这段时期内整日整夜地持有股票。

隔夜交易的回报率优于盘中交易的回报率，但是波动性又如何呢？人们总是在风险调整的基础上判断回报的，所以让我们来看看基于标准差，隔夜交易和盘中交易相比较各自表现如何。

我们可以使用NumPy来计算盘中交易的标准差，具体如下。

np.std(spy['Daily Change'])

上述代码生成图7-8的输出。

图7-8

现在，让我们计算隔夜交易的标准差。

spy['Overnight Change'] = pd.Series(spy['Open'] - spy['Close'].shift(1))
np.std(spy['Overnight Change'])

上述代码生成图7-9的输出。

图7-9

因此，隔夜交易与盘中交易相比具有较低的波动性。然而，并不是所有的波动性都是相等的。让我们比较两种策略，在下跌交易日的平均变化。

首先，让我们来看看下跌交易日的每日变化。

spy[spy['Daily Change']<0]['Daily Change'].mean()

上述代码生成图7-10的输出。

图7-10

现在，我们来看看下跌交易日的隔夜变化。

spy[spy['Overnight Change']<0]['Overnight Change'].mean()

上述代码生成图7-11的输出。

图7-11

再次，我们看到隔夜交易策略的平均下降幅度小于盘中交易策略的。

到目前为止，我们都是观测的数据点，现来看看回报。这将有助于在更现实的背景下讨论我们的收益和损失。继续前面的三个策略[1]，我们将为每个场景构建一个pandas数据序列：每日回报（昨日收盘到今日收盘的价格变化）、盘中回报（当日开盘到收盘的价格变化）和隔夜回报（昨日收盘到今日开盘的价格变化），具体如下。

daily_rtn = ((spy['Close'] –
spy['Close'].shift(1))/spy['Close'].shift(1))*100
id_rtn = ((spy['Close'] - spy['Open'])/spy['Open'])*100
on_rtn = ((spy['Open'] - spy['Close'].shift(1))/spy['Close'].shift(1))*100

我们所做的是使用pandas.shift()方法以当天的数据序列减去前面一天的数据序列。例如，对于前面代码中的第一个Series，每天我们从当日收盘价中减去前一日的收盘价。由于是计算差价，所以新的Series所包含的数据点会少一个。如果打印出新的Series，你可以看到以下内容。

daily_rtn

上述代码生成图7-12的输出。

图7-12

现在来看看所有三个策略的统计信息。我们将创建一个函数，它将接收每个回报的数据序列，然后打印出摘要性的结果。我们要得到每一次获利、亏损和盈亏平衡交易的统计数据，以及名为夏普比率（Sharpe ratio）的东西。我之前说过，回报是根据风险调整后的基础来判断的。这正是夏普比率将要提供给我们的。它是一种考虑回报的波动性，来比较回报的方法。这里，我们使用调整过的夏普比率来计算年化比率。

def get_stats(s, n=252):
s = s.dropna()
wins = len(s[s>0])
losses = len(s[s<0])
evens = len(s[s==0])
mean_w = round(s[s>0].mean(), 3)
mean_l = round(s[s<0].mean(), 3)
win_r = round(wins/losses, 3)
mean_trd = round(s.mean(), 3)
sd = round(np.std(s), 3)
max_l = round(s.min(), 3)
max_w = round(s.max(), 3)
sharpe_r = round((s.mean()/np.std(s))*np.sqrt(n), 4)
cnt = len(s)
print('Trades:', cnt,\
'\nWins:', wins,\
'\nLosses:', losses,\
'\nBreakeven:', evens,\
'\nWin/Loss Ratio', win_r,\
'\nMean Win:', mean_w,\
'\nMean Loss:', mean_l,\
'\nMean', mean_trd,\
'\nStd Dev:', sd,\
'\nMax Loss:', max_l,\
'\nMax Win:', max_w,\
'\nSharpe Ratio:', sharpe_r)

现在让我们在每个策略上运行相关的代码并查看统计信息。这里将从买入并持有的策略（每日回报）开始，然后再切换到另外两个，具体如下。

get_stats(daily_rtn)

上述代码生成图7-13的输出。

图7-13

get_stats(id_rtn)

上述代码生成图7-14的输出。

图7-14

get_stats(on_rtn)

上述代码生成图7-15的输出。

图7-15

如你所见，在三个策略中，买入并持有的策略具有最高的平均回报率以及最高的回报率标准差。它也包含了最大的单日下跌（亏损）。还有一点值得注意的是，即使隔夜策略和盘中策略有着几乎相同的平均回报，其波动性明显较小。因此，隔夜策略的夏普比率要高于盘中策略的。

到目前阶段，我们拥有一个相当不错的基准线了，可以用它来比较我们后续的策略。现在，我要告诉你一个新的策略，它将绝对性地击败目前所有的三个策略。

让我们来看看这个新的神秘策略的统计数据，如图7-16所示。

图7-16

有了这个策略，我的夏普比率几乎是买入并持有策略的三倍，并明显地降低了波动性，增加了最大收益，并将最大损失降低近一半。

我是如何设计这种战胜市场的策略的？请稍等一下……在测试的时间段内，对于隔夜策略我生成了1,000次随机信号（买入或者不买入），然后选择表现最好的一个。这给了我最好的1000次随机信号组合。

这显然不是战胜市场的方式。那么，为什么我这样做呢？我这样做是为了证明，如果你测试足够多的策略，事实是你偶然会遇到一些似乎是很棒的策略。这就是所谓的数据挖掘谬误，是交易策略开发中的真正风险。这就是为什么某个策略和现实世界的行为相对应是如此的重要——而行为，由于一些现实的约束而产生了系统性的偏差。如果你想在交易中占有优势，不要和市场进行交易，而是与市场的参与者进行交易。

我们要占优势，就要深入地理解人们对某些情况如何做出反应。

7.3.1　延长我们的分析周期

现在延伸我们的分析。首先，从标准普尔500指数拉取自2000年开始的数据。

start_date = pd.to_datetime('2000-01-01')
stop_date = pd.to_datetime('2016-03-01')
sp = pdr.data.get_data_yahoo('SPY', start_date, stop_date)

让我们看看这个图表。

fig, ax = plt.subplots(figsize=(15,10))
sp['Close'].plot(color='k')
plt.title("SPY", fontsize=20)

上述代码生成图7-17的输出。

图7-17

在图7-17中，我们看到了从2000年开始到2016年3月1日期间，SPY的价格变化。当时一定存在很多波动，市场同时经历了相对的高点和低点。

让我们在这个新扩展的时间段内，获取三个基本策略的基准线。

首先，让我们为每个策略设置变量，如下所示。

long_day_rtn = ((sp['Close'] –
sp['Close'].shift(1))/sp['Close'].shift(1))*100

long_id_rtn = ((sp['Close'] - sp['Open'])/sp['Open'])*100
long_on_rtn = ((sp['Open'] –
sp['Close'].shift(1))/sp['Close'].shift(1))*100

现在，让我们看看每个策略的总体数据。

1．首先是每日回报。

(sp['Close'] - sp['Close'].shift(1)).sum()

上述代码生成图7-18的输出。

图7-18

2．然后是盘中回报。

(sp['Close'] - sp['Open']).sum()

上述代码生成图7-19的输出。

图7-19

3．最后是隔夜回报。

(sp['Open'] - sp['Close'].shift(1)).sum()

上述代码生成图7-20的输出。

图7-20

现在，让我们看看每种策略的统计数据。

4．首先，我们得到每日回报的统计量。

get_stats(long_day_rtn)

上述代码生成图7-21的输出。

图7-21

5．接下来，我们获取盘中回报的统计量。

get_stats(long_id_rtn)

上述代码生成图7-22的输出。

图7-22

6．最后，我们得到隔夜回报的统计量。

get_stats(long_on_rtn)

上述代码生成图7-23的输出。

我们可以看到，在更长的考察时间内，三者之间的差异更加显著。如果我们在过去16年间，只在白天持有标准普尔ETF，那么我们会亏钱。如果我们只在夜间持有ETF，回报就会得到超过50%的改善！[2]当然，这里假设没有交易成本、没有税收，每次买入卖出都是完美衔接，但无论如何，这是一个了不起的发现。

图7-23

7.3.2　使用支持向量回归，构建我们的模型

现在我们有一个基线用于比较，接下来构建第一个回归模型。我们将从一个非常基本的模型开始，只使用股票的前一天的收盘价值来预测第二天的收盘价。我们将使用支持向量回归来构建此模型。有了这些，下面开始建立模型。

第一步是为包含每一天价格的历史记录设置DataFrame对象。在这个模型中，我们将包含过去的20个收盘，如下所示。

for i in range(1, 21, 1):
sp.loc[:,'Close Minus ' + str(i)] = sp['Close'].shift(i)
sp20 = sp[[x for x in sp.columns if 'Close Minus' in x or x ==
'Close']].iloc[20:,]
sp20

上述代码生成图7-24的输出。

图7-24

这个代码在同一行给出了每天及其前20个交易日的收盘价。

这将形成我们为模型所提供的X数组的基础。但是，在完全就绪之前，还有几个额外的步骤。

首先，我们将颠倒这些列，这样从左到右就是最早时间到最晚时间的顺序，如下所示。

sp20 = sp20.iloc[:,::-1]
sp20

上述代码生成图7-25的输出。

图7-25

现在，让我们导入支持向量机，并设置训练和测试矩阵，以及每个数据点的目标向量。

from sklearn.svm import SVR
clf = SVR(kernel='linear')
X_train = sp20[:-1000]
y_train = sp20['Close'].shift(-1)[:-1000]
X_test = sp20[-1000:]
y_test = sp20['Close'].shift(-1)[-1000:]

我们只有4000多个数据点可以使用，并选择使用最后的1000个作为测试。现在让我们拟合模型，并使用它来测试样本之外的数据，具体如下。

model = clf.fit(X_train, y_train)
preds = model.predict(X_test)

现在我们有自己的预测了，将它们与实际的数据进行比较。

tf = pd.DataFrame(list(zip(y_test, preds)), columns=['Next Day Close',
'Predicted Next Close'], index=y_test.index)
tf

上述代码生成图7-26的输出。

图7-26

评估模型的性能

让我们来看看模型的性能。如果预测的当日收盘价高于当日开盘价，那么我们就会在当天开盘时买入。然后我们会在当天收盘时卖出。

接下来，我们将向DataFrame对象添加一些额外的数据点来计算结果，如下所示。

cdc = sp[['Close']].iloc[-1000:]
ndo = sp[['Open']].iloc[-1000:].shift(-1)
tf1 = pd.merge(tf, cdc, left_index=True, right_index=True)
tf2 = pd.merge(tf1, ndo, left_index=True, right_index=True)
tf2.columns = ['Next Day Close', 'Predicted Next Close', 'Current Day
Close', 'Next Day Open']
tf2

上述代码生成图7-27的输出。

图7-27

在这里，我们将添加以下代码来获取收益和亏损的信号量。

def get_signal(r):
if r['Predicted Next Close'] > r['Next Day Open']:
return 1
else:
return 0
def get_ret(r):
if r['Signal'] == 1:
return ((r['Next Day Close'] - r['Next Day Open'])/r['Next
Day Open']) * 100
else:
return 0
tf2 = tf2.assign(Signal = tf2.apply(get_signal, axis=1))
tf2 = tf2.assign(PnL = tf2.apply(get_ret, axis=1))
tf2

上述代码生成图7-28的输出。

图7-28

现在来看看，我们是否能够只使用价格的历史来成功地预测第二天的价格。我们先从计算所获得的信号量点数开始，如下所示。

(tf2[tf2['Signal']==1]['Next Day Close'] - tf2[tf2['Signal']==1]['Next Day
Open']).sum()

上述代码生成图7-29的输出。

图7-29

目前为止看上去不太妙。但是，和被测试的时期有关吗？我们从不独立地评估模型。在最近的1,000天中，基本的盘中策略生成了有多少点？

(sp['Close'].iloc[-1000:] - sp['Open'].iloc[-1000:]).sum()

上述代码生成图7-30的输出。

图7-30

因此，看起来我们的新策略失败了，甚至没有比过基本的盘中买入策略。让我们拿到完整的统计数据来比较两者。

首先，这段时期的基本盘中策略统计如下。

get_stats((sp['Close'].iloc[-1000:] –
sp['Open'].iloc[-1000:])/sp['Open'].iloc [-1000:] * 100)

上述代码生成图7-31的输出。

图7-31

现在，我们模型的结果如下。

get_stats(tf2['PnL'])

上述代码生成图7-32的输出。

图7-32

这看起来很糟糕。如果我们修改交易策略怎么样？如果只有在预测值比开盘值高出一定的程度之上，才进行买入交易，那又会怎么样？这样做有帮助吗？让我们试试看。我们将使用修改的信号量重新运行策略如下。

def get_signal(r):
if r['Predicted Next Close'] > r['Next Day Open'] + 1:
return 1
else:
return 0
def get_ret(r):
if r['Signal'] == 1:
return ((r['Next Day Close'] - r['Next Day Open'])/r['Next
Day Open']) * 100
else:
return 0
tf2 = tf2.assign(Signal = tf2.apply(get_signal, axis=1))
tf2 = tf2.assign(PnL = tf2.apply(get_ret, axis=1))
(tf2[tf2['Signal']==1]['Next Day Close'] - tf2[tf2['Signal']==1]['Next Day
Open']).sum()

上述代码生成图7-33的输出。

图7-33

现在的统计如下。

get_stats(tf2['PnL'])

上述代码生成图7-34的输出。

图7-34

我们已经从糟糕到更糟糕了。看来，如果过去的价格历史表明好事要来临了，你可以做恰恰相反的预期。我们似乎已经使用这个模型开发了一个逆向的指标。如果我们继续探索会怎样？让我们看看如果翻转这个模型，收益会是什么样子，也就是说当模型预测强劲的收益时，我们不交易，相反，当模型预测亏损时，我们反而进行交易，具体如下。

def get_signal(r):
if r['Predicted Next Close'] > r['Next Day Open'] + 1:
return 0
else:
return 1
def get_ret(r):
if r['Signal'] == 1:
return ((r['Next Day Close'] - r['Next Day Open'])/r['Next Day Open']) * 100
else:
return 0
tf2 = tf2.assign(Signal = tf2.apply(get_signal, axis=1))
tf2 = tf2.assign(PnL = tf2.apply(get_ret, axis=1))
(tf2[tf2['Signal']==1]['Next Day Close'] - tf2[tf2['Signal']==1]['Next Day
Open']).sum()

上述代码生成图7-35的输出。

图7-35

让我们获取统计数据。

get_stats(tf2['PnL'])

这将输出图7-36的结果。

图7-36

看起来我们确实拥有一个逆向指标。当我们的模型预测下一交易日会有收益的时候，市场表现明显不佳（至少在我们的测试期间内）。在大多数情况下这是否都成立？不见得。市场倾向于从逆转的体系转变到趋势持续的体系。让我们在不同的时期，重新运行模型来进一步测试它。

X_train = sp20[:-2000]
y_train = sp20['Close'].shift(-1)[:-2000]
X_test = sp20[-2000:-1000]
y_test = sp20['Close'].shift(-1)[-2000:-1000]
model = clf.fit(X_train, y_train)
preds = model.predict(X_test)
tf = pd.DataFrame(list(zip(y_test, preds)), columns=['Next Day Close',
'Predicted Next Close'], index=y_test.index)
cdc = sp[['Close']].iloc[-2000:-1000]
ndo = sp[['Open']].iloc[-2000:-1000].shift(-1)
tf1 = pd.merge(tf, cdc, left_index=True, right_index=True)
tf2 = pd.merge(tf1, ndo, left_index=True, right_index=True)
tf2.columns = ['Next Day Close', 'Predicted Next Close', 'Current Day
Close', 'Next Day Open']
def get_signal(r):
if r['Predicted Next Close'] > r['Next Day Open'] + 1:
return 0
else:
return 1
def get_ret(r):
if r['Signal'] == 1:
return ((r['Next Day Close'] - r['Next Day Open'])/r['Next
Day Open']) * 100
else:
return 0
tf2 = tf2.assign(Signal = tf2.apply(get_signal, axis=1))
tf2 = tf2.assign(PnL = tf2.apply(get_ret, axis=1))
(tf2[tf2['Signal']==1]['Next Day Close'] - tf2[tf2['Signal']==1]['Next Day
Open']).sum()

上述代码生成图7-37的输出。

图7-37

因此，我们可以看到，新的模型和新的测试时间段返回的分数超过了33点。让我们将此结果与相同时间段的盘中策略进行比较。

(sp['Close'].iloc[-2000:-1000] - sp['Open'].iloc[-2000:-1000]).sum()

这将产生图7-38的输出。

图7-38

因此，在新的测试时段中，我们的逆向模型似乎表现出明显的优势。

到了现阶段，我们还可以对这个模型做一些扩展。我们甚至还没有开始使用技术指标或者模型中的基本数据，而且我们将交易限制在一天。所有这些都可以进行调整和扩展。然而，这里我想介绍另一个使用完全不同算法的模型。该算法称为动态时间规整（dynamic time warping）。它所做的事情是向你提供一个表示两个时间序列之间相似性的度量。

7.3.3　建模与动态时间扭曲

开始之前，我们需要从命令行使用pip安装fastdtw库，命令是pip install fastdtw。

完成后，我们将导入需要的附加库，如下所示。

from scipy.spatial.distance import euclidean
from fastdtw import fastdtw

接下来，我们将创建一个函数，该函数将接受两个序列并返回它们之间的距离。

def dtw_dist(x, y):
distance, path = fastdtw(x, y, dist=euclidean)
return distance

现在，我们将16年的时间序列数据分成不同的期间，每个期间长度为5天。我们为每个期间配上一个附加的点。这将用于创建我们的x和y数据，具体如下。

tseries = []
tlen = 5
for i in range(tlen, len(sp), tlen):
pctc = sp['Close'].iloc[i-tlen:i].pct_change()[1:].values * 100
res = sp['Close'].iloc[i-tlen:i+1].pct_change()[-1] * 100
tseries.append((pctc, res))

我们可以看看第一个序列，了解数据的样子。

tseries[0]

上述代码生成图7-39的输出。

图7-39

现在有了每个序列，我们就可以通过算法运行它们，来获得每个序列相对于其他序列的距离度量。

dist_pairs = []
for i in range(len(tseries)):
for j in range(len(tseries)):
dist = dtw_dist(tseries[i][0], tseries[j][0])
dist_pairs.append((i,j,dist,tseries[i][1], tseries[j][1]))

一旦我们有了这些，就可以将其放入一个DataFrame对象。我们将删除相互距离为零的序列，因为它们代表了相同的序列。我们还会根据序列的日期进行排序，只观测第一个序列在时间上排第二个序列之前的那些。

dist_frame = pd.DataFrame(dist_pairs, columns=['A','B','Dist', 'A Ret', 'B
Ret'])
sf =
dist_frame[dist_frame['Dist']>0].sort_values(['A','B']).reset_index(drop=1)
sfe = sf[sf['A']<sf['B']]

最后，我们将交易限制到相互距离小于1，而第一个序列的回报为正的情况。

winf = sfe[(sfe['Dist']<=1)&(sfe['A Ret']>0)] winf

上述代码生成图7-40的输出。

图7-40

让我们看看排名靠前的模式，在绘制后是什么样子。

plt.plot(np.arange(4), tseries[6][0])

上述代码生成图7-41的输出。

图7-41

现在，我们将绘制第二个。

plt.plot(np.arange(4), tseries[598][0])

上面的代码将生成图7-42的输出。

图7-42

从图7-41和图7-42中可以看出，曲线几乎相同，这正是我们想要的。我们打算尝试找到所有在第二天获得正收益的曲线。然后，一旦我们发现某个曲线与这些有利可图的曲线之一非常相似，我们就会买入，以期待另一次盈利。

现在构造一个函数来评估我们的交易。对于相似的历史曲线，只要能返回正向的盈利，我们就会买入。如果发生无法盈利的情况，我们将删除它们。

excluded = {}
return_list = []
def get_returns(r):
if excluded.get(r['A']) is None:
return_list.append(r['B Ret'])
if r['B Ret'] < 0:
excluded.update({r['A']:1})
winf.apply(get_returns, axis=1);

现在所有交易的回报都存储于return_list，让我们评估最终的结果。

get_stats(pd.Series(return_list))

上述代码生成图7-43的输出。

这些结果是迄今为止我们看到的最好结果。盈利/亏损比例和平均值远高出其他的模型。看来，这个新模型可能行得通，特别是与之前的模型相比。

现在，为了进一步检视该模型，我们应该通过其他的时间段来探索其鲁棒性。周期超过四天是否会改善模型？我们是否应该总是排除产生亏损的模式？还有很多额外的问题可以探索，但我会将它作为练习留给读者。如果你确实使用了这些技术，就知道我们只是浅尝辄止，还需要更多额外的周期测试，以适当地检验这些模型。

图7-43

7.4　小结

在这一章中，我们研究了股市。我们学会了如何使用机器学习来制定交易策略。我们使用支持向量回归构建了第一个策略，使用动态时间规整构建了第二个策略。

毫无疑问，本章的内容本身就可以写一本书。交易策略中许多最重要的模块，我们甚至都没有涵盖。这些包括投资组合建设、风险缓释和资金管理。对于任何真正的策略而言，这些都是最根本的——可能比交易信号更为重要。

希望这将成为你自己探索的起点。但是，我需要再次提醒你，“战胜市场”是一项几乎不可能完成的任务。在这个市场中，你将与世界上最聪明的人们竞争。如果你决定尝试，我祝你好运。如果结果不是你想象的那样，请记住我提醒过你！

在下一章中，我们将讨论如何构建一个计算图像相似度的引擎。

[1]　译者注：这里存在笔误，之前只提到了两个策略。

[2]　译者注：此处作者的意思是，和购买并持有相比，如果每天晚间买入并且在每天早上卖出，你将获得额外的50%盈利。

第8章　建立图像相似度的引擎

在我们的旅程中，到目前为止已经和数字以及文本打了很多交道。在本章中，我们将进入图像的世界。虽然看上去这似乎需要一些更高级的法术，但是我可以向你保证，将图像转换为机器可读的格式和转换文本一样简单。

我们将从图像机器学习的“hello world”开始：数字的识别。不过，到本章结束时，我们将构建一个高级的、基于图像的深度学习应用。要达到怎样的境界，我们才能建立这个高级应用？当然，这就是为什么要寻找我们的灵兽[1]！

我们还将花费相当多的时间来讨论深度学习算法，并理解为什么它们是如此重要，为什么大家都在热议它们。

本章将讨论以下主题。

- 图像的机器学习。

- 处理图像。

- 查找相似的图像。

- 理解深度学习。

- 构建图像相似度引擎。

8.1　图像的机器学习

尽管有关文本和数值型数据的机器学习应用是讨论得最多的，还有大量的类似应用是关于图像的。其中许多集中在具有深远影响的前沿研究领域。

如今，我们习惯于运行Google搜索，从三个星期前阅读的一个新闻故事来找到一些奇怪的文本片段，并将第一项作为期待的结果。现在，想象一下，如果我们能够对图片、电影和GIF动图做同样的事情，那会怎样？例如，让我们假设你在六个月前看到一个GIF，它是完全适合你当前的Slack[2]聊天，但你所能记住的只是它有一只美洲驼，而一个男子试图喂它。现在，你很难找到这张动图，因为大多数图片搜索需要利用标签和图像周围的文本。然而在过去几年中，像Google这样的公司，在机器为图像内容打标签的方面取得了惊人的进步。当这项研究完全公布于众让大家都来使用，它会从根本上改变我们搜索图像的方式。

在Google Research的这篇博文中（http://googleresearch.blogspot.com/ 2014/11/a-picture-is-worth-thousand-coherent.html），他们描述了实现这一目标的进展，并阐述了完成它的一些技术挑战。例如，在图8-1中，可以看到炉子上有两个比萨饼。

图8-1

为了像这样标记图像，不仅需要理解每个对象，还需要理解它们之间的关系。即使那样，标签还需要与自然语言的短语匹配——也就是匹配人类的描述。例如，即使在技术上是正确的，一个人也永远不会将这张图标注为“在两个彼此相邻的比萨饼之下的烤箱”。

图像机器学习的另一种应用是人脸识别。你可能看过最近的新闻讨论了Facebook的DeepFace技术。据报道，这个应用程序是如此的先进，即使图像是人们的后脑勺，它也可以识别某个人——而且它识别的准确度近乎完美。

虽然这似乎对整个社会有可怕的影响，但是对于Facebook而言，这是价值连城的技术。一旦某个人在单张照片中被加注了标签，那么不再需要其他附加的标签——未来所有的照片的标注都是自动进行的。

应该指出这是一项艰巨的任务。一个人头发、服饰、年龄的改变——更别提每张照片中不同的拍摄角度和光线——都使得这项任务变得极具挑战性，甚至对人类也是如此。事实上，就像计算机已经开始在游戏中（如Alpha Go）超越人类，它们也开始在这种识别任务中赶超我们。

这种新的机器自我掌控水平来自于相对较新的一类算法，其术语是“深度学习”。本章稍后将仔细探讨深度学习，让你可以理解它与其他算法的不同之处，以及为什么它是如此的成功。然而，现在我们将从头开始，先来了解一下处理图像的基础知识。

8.2　处理图像

当我们第一次介绍自然语言处理时，你理解了需要执行某种变换，才能以数字的方式表示文字。我们通过创建词条-文档的矩阵做到了这点。如今我们正在处理图片，需要执行另一种变换，然后用数字形式呈现图像。

让我们来看看图8-2中的几个手写数字。

图8-2

这些特定数字取自MNIST的手写数字数据库（是的，这些都是真实的手写）。这个数据库包含数万个像这样的数字，采集自美国人口普查局员工和高中生的手写样本。

假设现在我们想使用机器学习来识别这些数字。我们如何使用数字来表示这些手写体？

一种方法可能是将图像中的每个像素映射到相同大小的数字矩阵中。然后，我们可以通过矩阵中的值来表示该像素的一些属性。实际上，这正是人们处理的方法。

每个数字图像都被缩放并且居中定位到给定大小的画布上（28像素×28像素或64像素×64像素），然后每个像素的颜色强度在矩阵中被表示为0到1之间的值，其中1是纯黑色，0是纯白色。此过程称为灰度缩放。

有了这个简单的方法，我们将一个真实世界中的“事物”变成一个数字化的表示，该表示可以用于我们的机器学习算法中。

下面来看个例子。我们将MNIST数据库加载到scikit-learn。

from sklearn import datasets
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
digits = datasets.load_digits()
def display_img(img_no):
fig, ax = plt.subplots()
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.matshow(digits.images[img_no], cmap = plt.cm.binary);
display_img(0)

上述代码生成图8-3的输出。

图8-3

在上面的代码中，我们加载了必要的包，然后是数字数据集，最后，我们使用matplotlib展示了第一个数字。这是一个粗略的表示，因为它被缩小到了8×8，或总共64个像素。运行以下命令，可以看到实际的矩阵表示。

digits.images[0]

上述代码生成图8-4的输出。

图8-4

这些特定的数字在0和16之间缩放，但可以看到，它们和图像中每个像素的颜色强度是相互关联的。如果在算法中应用这些，我们需要将8×8的矩阵展开为长度64的单个向量。如下所示。

digits.data[0].shape

上述代码生成图8-5的输出。

图8-5

然后这将成为我们训练集中的一行——也就是特征向量。使用以下命令，我们也可以看到和数据相关联的标签。

digits.target[0]

上述代码生成图8-6的输出。

图8-6

现在我们准备就绪，可以将这个图像数据输入算法了。

到目前为止，我们只讨论了如何使用黑白图像，不过令人惊讶的是，处理彩色图片也是一样的简单。每个像素可以使用三个特征来表示，每个RGB值对应一个特征。或者，如果倾向于保留单个特征，那么可以取三个RGB值的平均值。

现在我们已经将图像转换成机器可以处理的表示，接下来看看我们可以使用哪些算法。

8.3　查找相似的图像

通常，MNIST的数字数据库是用于分类任务，即给定手写数字，找到它对应的目标标签。这里，我们将以不同的方式使用它。对于一张给定的图像，我们将尝试在数据集中，找到与它最相似的另外一张图像。这是一个无监督学习的任务，而不是一个监督学习的任务，因为我们不会使用标签进行训练。

本书之前介绍过一个处理文本特征的算法，我们将从这个算法开始。该算法是余弦相似性。回想一下，这个算法计算X矩阵中每一行的单位向量。每行和其他的行进行点乘，为我们提供每对向量之间的余弦夹角。最终的结果是，我们拥有了一个单独的指标，它告诉我们两张图片有多么的“接近”。现在来看看具体如何操作。

首先，我们将导入需要的库。

import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity

然后，我们将计算第一个图像（索引号为0）和所有其他图像之间的相似度。

X = digits.data
co_sim = cosine_similarity(X[0].reshape(1,-1), X)

当我们输入一维数组时，scikit-learn需要我们重塑数组。

最后，我们将结果放在一个pandas的DataFrame对象中，并查看该结果。

cosf = pd.DataFrame(co_sim).T
cosf.columns = ['similarity']
cosf.sort_values('similarity', ascending=False)

这将产生图8-7的输出。

我们可以看到第一行的值是1—— 一个完美的相似度——因为这是我们的原图。下面是按照相似度排序的所有其他图像。下面看看编号877的图像。我们将使用之前创建的函数来显示它。

display_img(877)

这将产生图8-8的输出。

图8-7

图8-8

可以看出，这个图像也是一个0，表明我们在正确的轨道上前进。让我们并排对比编号0的图像与编号877的图像，如图8-9所示。

我们可以看到，这两个零是非常相似的。我打赌他们是由同一个人书写的。

现在，让我们做点有趣的事情。和0差异最大的是什么呢？下面来看看。最不相似的图像是编号1626。

display_img(1626)

这将产生图8-10的输出。

图8-9

图8-10

令人震惊的是，我们刚刚确认了二进制编码。与1相反的是，事实上，就是一个0，哈哈！

让我们来看看在图像机器学习应用中经常采纳的另一种算法。它被称为卡方核（chi-squared kernel），和余弦相似度计算类似，它将给出一个标量值，告诉我们两个向量之间的相似度，如图8-11所示。

让我们来看看卡方核的相似度和之前运行的余弦相似度，这两者相比较如何。

from sklearn.metrics.pairwise import chi2_kernel
k_sim = chi2_kernel(X[0].reshape(1,-1), X)
kf = pd.DataFrame(k_sim).T
kf.columns = ['similarity']
kf.sort_values('similarity', ascending=False)

上述代码生成图8-12的输出。

图8-11

图8-12

虽然顺序与余弦相似度的结果有些不同，但排名靠前的值大多数都是相同。

让我们来看看卡方核测量出来的最相似图像。

display_img(1167)

这将产生图8-13的输出。

图8-13

再次，我们找到一个与原图非常相似的0。

为什么要在两者（余弦相似度与卡方核）中选择一个？你可以使用任何一个并得到相似度的结果——如我们已经看到的那样——余弦相似度已经被证明是自然语言处理工作的首选，而卡方核是处理图像任务的直接选择。

到现在为止，你有理由怀疑我们怎么可能通过仅含一位数字的微小黑框和白框，就确定图像是比萨饼还是猴子，或者摩托车什么的。根据我们目前所做的，似乎还不能收集足够的信息来辨别图片是什么。事实上，这是也是真的。

只使用字母的计数，我们几乎不可能区分《白鲸记》和《傲慢与偏见》。同样的道理，我们需要添加另外一层抽象来辨别图像。对于文本而言，这意味着使用单词和单词的组合，而对于视觉信息，类似地，我们使用像素的聚集。这些像素的聚集形成所谓的视觉词汇，这种方法被称为视觉词包——bag of visual words。之所以选择了这样的术语，是因为文本处理使用了词包——bag of words——并忽略了单词之间的顺序，而视觉词包使用了同样的方式，忽略了视觉词汇的空间顺序。

现在，为了进一步说明视觉词包的过程，让我们在更高的层面来理解这个概念。因为视觉词包的起源来自纹理识别，我们将使用图8-14的的例子。

图8-14

这里，我们有三种不同的纹理。每个包括一系列重复的纹理单元，这些单元被称为textrons。这些形成了视觉词汇的基础，如图8-15所示。

图8-15

然后，每个样本可以表示为这些特征上的直方图，如图8-16所示。

图8-16

正如我们在图8-16所看到的，这些可以和词包中文档针对单词的直方图进行类比。

现在，我应该指出，这种描述主要是概念性的。涉及视觉词包的实现时，存在更多的事项。完整的细节超出了本书的讨论范围，不过其中一些工作应该包括选择兴趣点作为视觉词汇，以及使特征尺度不变的规范化。

一旦完成预处理，就像文本处理那样，我们要计算相似度矩阵，然后将数据输入分类算法。再次，像文本处理那样，算法通常是SVM。

8.4　了解深度学习

虽然视觉词包方法的表现令人印象深刻，但是最近，利用所谓深度学习的新方法已然出现。深度学习为人工智能领域带来了新的生机，因为它不断地在提升我们的基准线。本章稍后，我们将在图像相似度引擎中使用深度学习，不过，首先我们将介绍究竟什么是深度学习，以及为什么它是如此重要的突破。

深度学习源于已经存在了几十年的算法。这些算法，称为感知器，对人类大脑中的神经元进行建模。

在参加生物学课程时，你可能已经学习过神经元是如何工作的。基本的信息如下。

- 每个神经元连接到其他神经元组成的网络。

- 当神经元激发时，它将信号发送到和它连接的神经元。

- 接收这个信号的神经元，根据一些已建立的激活阈值，或者激发，或者不激发。

这是感知模型的基础，如图8-17所示。

图8-17

我们可以将神经元视为一个单独的决策单元。假设神经元的任务是决定我们是否应该接受一个新的工作机会。在这个场景中，我们的相关输入可能是工作地点、薪酬、对未来经理的印象，以及办公室环境等。这种情况下，我们有一个二元决策：1，接受这份工作，或0，不接受。每一项输入都帮助我们确定是否接受该工作，但是，很显然，它们不会具有同样的影响力。例如，如果办公室是10分中的满分，但工资只能拿到10分中的2分，我们很可能拒绝这个机会。反之，如果办公室是10分中的2分，而工资是10分中的满分，我们很可能会接受它。因此，在决策过程中，薪酬比办公室的环境更重要。在感知器的语言中，我们会说薪酬这个输入的权重更高。对所有的输入—— x变量——修改它们的权重—— w变量，我们将获得一个值，它会触发或不触发我们的函数。

从数学上来讲，我们有图8-18的公式。

图8-18

到目前为止，我们已经讨论了感知器如何担当一个决策单元，但是还没有讨论学习是如何进行的。

为了理解学习的过程，让我们试着用感知器学习一些决策规则。

我们要学习的第一条规则称为AND函数。AND函数是像这样工作的：有两个输入，当两个输入都为正时，我们希望函数输出1。如果一个输入是负的，或者两个都是负的，我们希望它输出0。我们将权重设置为0到1之间的随机数。

让我们开始学习的过程。这里X_1的输入等于1，X_2的输入等于−1。W_1被随机设为0.8，W_2被随机设为0.2。因为两者都必须是正的，输出才为1，所以阈值是大于1的任何值。

因此，我们有W_1×X_1+W_2×X_2为1×0.8+-1×0.4=0.8-0.4=0.4。现在，因为我们期望输出为0，所以说误差为0.4。现在我们将尝试改进模型，将这些错误推回到输入，让其更新权重。为此，我们将使用图8-19的公式轮流进行评估。

图8-19

这里，wi是第i个输入的权重，t是目标结果，o是实际结果。其中，我们的目标结果是0，实际结果是0.4。现在忽略n项。它是学习率，决定了更新幅度应该有多大或多小。现在，我们假设它被设置为1。

让我们看看x_1如何更新它的重量。因此，我们有1×(0-0.4)×1，它等于-0.4。这是w的差值，因此，更新公式1，我们有0.8-0.4，这给出了w_1的新权重为0.4。因此，x_1的权重下降。那么x_2的权重又如何？

让我们来看看。这个是1×(0-0.4)×（-1），等于0.4。然后，我们获得了0.2+0.4=0.6，以此来更新w_2。从此可以看到，两个权重相互接近了，这正是我们所希望的。如果给定一个足够小的学习率，然后继续运行这个过程，那么模型就会收敛，我们就将学会AND函数。

虽然这个模型看上去极其简单——没错，它也有明显的局限性（例如，无法学习XOR函数）——但它是当今深度学习框架的基石。人们对这种模型进行渐进的创新，提高了其学习复杂表达的能力，包括结合S形函数代替阶梯函数、堆叠神经元形成分层网络，以及更好的方法来向下层分配错误。

总之，这些更新使得模型不仅可以学习非线性表示（学习XOR函数所必需的），还可以学习任何其他模式。通过堆叠多层的神经元—— 一层的输出作为另一层的输入——每个更高的层次都能够识别更复杂的数据表示。

在图8-20中，我们看到了这个过程在人脸识别任务中是如何进行的。

图8-20

图8-20中从左到右，我们可以在深度学习网络的每个隐藏层中，追踪不断增加的表达复杂性。

在图8-21中，我们看到的网络只有一个隐藏层，不过，拥有多个隐藏层是常规的实践做法，而它也是深度学习这一术语的来源。

我们可以认为这就像盲人摸象的比喻。

“感受到一条腿的盲人说大象就像一根柱子；感受到尾巴的那个人说大象就像一条绳子；感受到躯干的那个人说大象就像一棵树；感受到耳朵的那个人说大象就像一把扇子；感受到肚子的那个人说大象就像一面墙；感受到象牙的那个人说大象就像实心管。”

——维基百科。

图8-21

没有一个人能够纵览全局，仅仅通过片面的观察，他们无法正确地说出正在评估的是什么。但是，将同样的属性结合起来，反复观察，再加上被告知它是头大象，那么下一次遇见柱子 + 绳子 + 树干 + 扇子 + 墙 + 实心管的组合，我们可能认为它就是一头大象。

现在我们对深度学习有了更好的理解，下面将继续使用深度学习来创建应用程序。

8.5　构建图像相似度的引擎

如果你一直读到现在，那么现在可以开始享受成果了。我们将利用人工智能的强大力量做一些真正重要的事情。我们将找到你的灵兽。嗯，不完全是。我们将使用它，在CIFAR-10图像数据集中找到最像你的猫。CIFAR-10是什么？这个数据集是用作计算机视觉研究标准的一组图像。它包括10个分类，成千上万的图像，例如狗、青蛙，飞机和汽车等等。不过，对我们而言最重要的是，其中包括猫。

现在，我们需要理解在所有讨论的算法中，深度学习往往是最强大的，但同时对于“家庭使用者”也是最不友好的。我的意思是，相对于其他算法，深度学习通常需要更长的训练时间、更多的计算能力。事实上，这也是神经网络架构没有被快速接受的原因之一。随着较便宜的GPU（图形处理单元）出现，它才成为研究人员的可选项之一。

鉴于此，我们将采取两大措施来减少模型的处理时间。

第一个措施是利用GraphLab Create。它是一个很流行的大规模机器学习框架。它提供了一个很好的API，有点像pandas和scikit-learn的结合。通常，它是收费的服务——需要许可证，但是对于学术用途是免费的，似乎还包括了Bootcamps和大规模在线公开课程（MOOC），如Coursera。请务必阅读该站点的说明细节。

Graphlab的安装很简单。细节可以在这里找到：https://dato.com/download/ install-graphlab-create-command-line.html。基本上，它只需要填写表单就可以获取许可证，然后复制并粘贴它提供的pip命令。

注意，你必须使用Python 2.7。Python 3目前还不支持这些。

缩短处理时间的下一个措施是使用被称为迁移学习的东西。迁移学习的基本思想是利用大规模的、高度训练的、针对某项具体任务的深度学习网络的能力。然后，我们砍掉这些网络的最高层，以使用较低层作为特征，让其运作在没有受过训练的任务中。请记住，在事物的综合表示方面，较低层的特殊性更少。在数字识别的世界中，较低层可以表示环状或直线，而较高层更多的是关注0或1。在大象识别的世界中，低层更多的是关于扇子和树干。在迁移学习中，我们可以提取这些较低级别的特征，以便将它们应用到新的领域——那些它们没有专门受训过的领域。

本节中的很多信息都参考了有关机器学习基础的Coursera课程，可用这里访问：https://www. coursera.org/learn/ml-foundations。如果你有兴趣深入研究这些材料，我强烈推荐你阅读它。

有了这些，让我们开始编码。灵兽等着我们呢。

我们将从包的引入开始。

import graphlab
graphlab.canvas.set_target('ipynb')

接下来，我们从CIFAR-10数据集中加载将要使用的一组图像。

gl_img =
graphlab.SFrame('http://s3.amazonaws.com/dato-datasets/coursera/ deep_learni
ng/image_train_data')
gl_img

这将产生图8-22的输出。

图8-22

你会看到几列标识信息，例如编号和标签，你还会注意到名为“深度特征”的列。这些特征是从一个大规模的、经过训练的深度学习网络提取而来。稍后我们将解释如何使用它们。现在，让我们继续。

使用下面的代码，我们可以观察一下图像。

gl_img['image'][0:5].show()

这将产生图8-23的输出。

图8-23

不幸的是，图片非常小，但你可以调整它们的大小，让其看得稍微清楚些。

graphlab.image_analysis.resize(gl_img['image'][2:3], 96,96).show()

这将产生图8-24的输出。

现在，所有用于比较的图片都已经加载完毕了，我们只需要加载自己的照片。我会加载自己的一张照片，它应该适用于这个任务。当然，你需要在该位置加载你自己的。

img = graphlab.Image('/Users/alexcombs/Downloads/profile_pic.jpg')
ppsf = graphlab.SArray([img])
ppsf = graphlab.image_analysis.resize(ppsf, 32,32)
ppsf.show()

这将产生图8-25的输出。

图8-24

图8-25

接下来，我们需要将该图像放入包含之前训练图像的数据框中。但是，要做到这一点，我们首先需要提取其特征。具体如下。

ppsf = graphlab.SFrame(ppsf).rename({'X1': 'image'})
ppsf

上述代码生成图8-26的输出。

现在，我们将提取图像的深度特征。

ppsf['deep_features'] = deep_learning_model.extract_features(ppsf)
ppsf

上述代码生成图8-27的输出。

图8-26

图8-27

此刻，我们只需要一些最后的操作，就能让自己的照片与训练的图片拥有同样的格式。

ppsf['label'] = 'me'
gl_img['id'].max()

上述代码生成图8-28的输出。

图8-28

我们看到数据框中最大的ID编号为49,970。我们为自己照片分配的编号为50,000，这样做只是为了便于记忆。

ppsf['id'] = 50000
ppsf

上述代码生成图8-29的输出。

图8-29

差不多快好了，现在我们将使用这些列来连接所有的内容。

labels = ['id', 'image', 'label', 'deep_features']
part_train = gl_img[labels]
new_train = part_train.append(ppsf[labels])
new_train.tail()

这将产生图8-30的输出。

图8-30

好吧，我们现在有一个很大的数据框架，包含所有的图像，以及表示为向量的图像深度特征。此时，我们可以使用非常简单的模型来找到最相似的图像。

我们首先使用k-最近邻居模型，将某个随机选取的猫和集合中其他的猫进行比较，感觉一下我们的模型效果如何。

knn_model =
graphlab.nearest_neighbors.create(new_train,features=['deep_ features'],
label='id')

上述代码生成图8-31的输出。

图8-31

让我们看看被测试的小猫。

cat_test = new_train[-2:-1]
graphlab.image_analysis.resize(cat_test ['image'], 96,96).show()

这将产生图8-32的输出。

图8-32

因此，这个可爱的像素化小怪物就是我们的测试主角。让我们找到和它面貌相似的其他小猫。

sim_frame = knn_model.query(cat_test)
sim_frame

上述代码生成图8-33的输出。

图8-33

最后让我们看看匹配上的小猫们。

def reveal_my_twin(x):
return gl_img.filter_by(x['reference_label'],'id')
spirit_animal = reveal_my_twin(knn_model.query(cat_test))
spirit_animal['image'].show()

这将导致图8-34的输出。

图8-34

这就是结果。每个图像是一只猫，我觉得它们看起来都很像被测试的小猫。

让我们再运行一次，选择另一只小猫。这里我省略了代码，只是给你看看被测试的猫和相似查找的结果。

首先，这是被测试的对象，如图8-35所示。

图8-35

现在是相似度匹配的结果，如图8-36所示。

图8-36

对于编号145的图片，我必须承认这个匹配相当不错。然而，现在，到了我们一直期待的时刻。让我们揭示我的动物双胞胎。

me_test = new_train[-1:]
graphlab.image_analysis.resize(me_test['image'], 96,96).show()

这将产生图8-37的输出。

图8-37

现在，我们运行一次搜索，查找和我的照片最匹配的图片。

sim_frame = knn_model.query(me_test)
sim_frame

上述代码生成图8-38的输出。

图8-38

现在，我们加载结果。

graphlab.image_analysis.resize(spirit_animal['image'][0:1], 96,96).show()

这将产生图8-39的输出。

图8-39

我觉得还行。我认为自己戴的帽子决定了一切，但我一定会接受它作为我的灵兽。

8.6　小结

通过本章，我们在计算机视觉应用的机器学习领域中进行了一次宏伟的旅程。我们讨论了一些这个领域使用的技术，以及如何使用这些技术创建可行的应用。我们还阐述了深度学习的原理，以及如何将其应用于特征提取和分类。最重要的是，你现在有一个非常科学的方法来寻找你的灵兽。仅仅这一点就值回票价了。

在下一章中，我们将了解如何创建一个聊天机器人的应用程序。

[1]　译者注：灵兽或者精神动物，指对于每个人的精神而言，都有一种动物的天性与其对应，有些人相信找到自己的天性可以增强和发展自我。

[2]　时下非常流行的一种企业聊天工具，包括了聊天群组、大规模工具集成、文件整合、统一搜索等功能。

第9章　打造聊天机器人

几乎从电脑诞生的那天开始，我们就幻想有朝一日能够与它们交谈。电影告诉大家，这些机器将是拥有超高智能的代理。除了能够进行对话，这些机器人还能观察我们的情绪，甚至在必要的时候违抗我们的命令，就像在电影《2001：太空奥德赛》中那样。

在电影问世11年之后，随着iPhone 4S的发布苹果公司向全世界介绍了Siri。对于任何使用过Siri的人来说很显然的事实是，如果我们想面对电影中HAL 9000单位所展示的那般智能，那么还有很漫长的路要走。但是，尽管这些代理——或聊天机器人——过去表现笨拙，该领域仍然在迅速发展。

在本章中，我们将学习如何从头打造一个聊天机器人。一路走来，我们将了解更多关于该领域的历史及其未来前景。

我们将在本章讨论以下主题。

- 图灵测试。

- 聊天机器人的历史。

- 聊天机器人的设计。

- 创建一个聊天机器人。

9.1　图灵测试

1950年，Alan Turing在一篇著名的论文中提出了这样的问题：“机器能思考吗？”作为脚踏实地的天才，他决定不去正面地回答这个问题，而是将其放入“其他人头脑之问题”的框架中。这个问题提出了另一个疑问：“我们如何真正知道其他人是否拥有和我们一样的头脑？”因为我们只能观察他们的行为——而不是他们内心的活动，我们必须相信他们像我们一样。Turing提出，如果机器表现出它好像是智能的，那么其实我们也应该这样看。简而言之，这是图灵测试。如果机器通过了这个测试，那就意味着我们要相信该机器是和自己一样的同类人。

你可能想知道目前为止，是否有程序曾经成功地通过图灵测试。答案是，真的没有官方图灵测试。最接近官方活动的事情是Loebner奖。这是一个每年举行的比赛，对评委觉得最像人类的聊天机器人，颁发适度的奖励。如果这听起来过于严格和学术，它可以被描述为任何事物。据说，Loebner赞助比赛是因为他鄙视任何形式的劳动。

希望目前为止，你已经意识到任何关于“通过图灵测试”的声明更多的是假象，而不是实际的科学。尽管如此，图灵的初始概念是非常有价值的。

9.2　聊天机器人的历史

我还记得那天父亲让我坐在他的IBM XT面前，看着它黑色和绿色闪烁的监视器，并让我和“它”说话。对话大致是这样的：

>Hello? How are you feeling today?
>Hi. Who is this?
>Hi there… how are you today?
>I'm fine. You didn't tell me who you are.
>We should be discussing you, not me.
>We should? Why?
>Let's change focus a bit… Tell me about your family.
>You are impossible to have a conversation with.
>Can you elaborate on that?
>You are killing me.
>Perhaps you would like me to be killing you.
>Ok, you are freaking me out.
>Perhaps you're really talking about yourself?
>What?? That makes no sense.
>How would an answer to that help you?
>END
>Very Interesting.
>QUIT
>Thank you for talking with me.
>#$###$#@@...

和我交谈的“它”是一个名为ELIZA的程序。由Joseph Weizenbaum于20世纪60年代中期，在麻省理工学院人工智能实验室编写。他创建ELIZA来模仿罗杰斯心理治疗师[1]的反应。虽然无论从何种层面来理解，这些反应都是很滑稽的，但是该程序能够说服一些用户，让他们觉得正在和一个真实的人聊天。考虑到它的代码不足200行，并使用随机化和正则表达式进行鹦鹉学舌般的应答，这已经是一个了不起的壮举了。即使在今天，这个简单的程序仍然是主流的做法。如果你问Siri ELIZA是谁，她会告诉你她是一位好朋友以及出色的心理医生。

如果说ELIZA是聊天机器人的雏形，之后我们又看到了什么？最近这些年，新型的聊天机器人爆发式地增长，最值得注意的是Cleverbot。

Cleverbot于1997年在互联网上问世。从那时起，这个机器人已经进行了数亿次的对话。不像早期的聊天机器人，随着每次对话的进行，Cleverbot（机如其名）似乎变得更加智能。对于该算法确切的工作原理，相关细节很难找到，据说是将所有的对话记录到数据库中，并通过识别数据库中最类似的问题和回应，来找到最合适的答复。

我在图9-1所示的屏幕截图中，提出了一个无意义的问题，你可以看到Cleverbot根据字符串的匹配，发现了和这个问题中所讨论对象相类似的事物。

图9-1

我继续相关的话题，如图9-2所示。

我再一次得到相似的答复。

你还会注意到，话题可以在多个会话中持续。对于我的回答，它要求我提供更多的细节，并证明我的答案是合理的。看上去，这是使Cleverbot更聪明的方法之一。

图9-2

虽然聊天机器人学习人类可能很有趣，但它们也有黑暗面。

就在这一年，微软在Twitter上发布了一个名为Tay的聊天机器人。微软邀请人们向Tay提出问题，Tay会根据她的“个性”做出回应。显然，微软对这个机器人进行了特定的编程，让其看上去像一个19岁的美国女孩。她希望成为你的虚拟“好友”，唯一的问题是她听上去像一个年轻的极端主义者。

由于一系列令人难以置信的煽动性推文，微软被迫在Twitter上关闭了Tay，并发布道歉：

“现在很多人都知道，星期三我们推出了一款名为Tay的聊天机器人。对于Tay意外发布的攻击性和伤害性的推文，我们非常抱歉，这并不代表我们的立场或者我们支持谁。现在Tay已经下线，对于和我们的原则以及价值观相冲突的恶意内容，需要及时地预测，只有当我们有信心可以更好地预测时，才会让Tay再次回归。”

——2016年3月25日微软官方博客

显然，想要在未来对公众发布聊天机器人的品牌，应该从这次事件中吸取教训。

毫无疑问，品牌商们正在接受聊天机器人。从Facebook到Taco Bell[2]都在进入这个领域。

来看看TacoBot，如图9-3所示。

图9-3

是的，这是一个真实的例子，尽管发生了Tay那样的事件，未来的UI还是很有可能成为TacoBot这样的东西。最后一个例子可能进一步解释这是为什么。

Quartz最近推出了一个将新闻转变为对话的应用程序。在聊天中你的代入感更好，就像倾听一个朋友述说新闻，而不是让系统简单地排出故事列表。如图9-4所示。

David Gasca，Twitter的产品经理，在Medium的一篇文章中描述了他使用该应用的经历。他描述了会话的本质如何触发了只有人与人之间才有的那种感觉。下面的内容是有关他在这个应用中遇到广告时的感觉：

“与简单的展示广告不同，和我的应用对话时，我觉得欠它些什么：我就想单击。在潜意识的层次，我觉得需要互动，不能让应用程序失望：因为应用程序给了我这些内容。到目前为止它都很棒，我也喜欢GIF的动图。我也许应该单击这个广告，因为应用程序正在很耐心地询问我。”

如果这种体验是普遍的——我也期望它是——那么这可能是广告界的一件大事，毫无疑问，广告的利润将推动UI的设计：

“机器人的行为越像一个真实的人，它就会越像真人一样被对待。”

——Mat Webb，技术专家，《Mind Hacks》一书的合著者

到目前为止，你可能很想知道这些东西是如何运作的，所以让我们继续吧！

图9-4

9.3　聊天机器人的设计

初始的ELIZA应用程序是200行出头的代码。Python NLTK的实现也是差不多的短小精悍。在NLTK网站的这个链接（http://www.nltk.org/modules/nltk/chat/ eliza.html），可以看到一些摘要。这里我也转载了以下内容。

# Natural Language Toolkit: Eliza
#
# Copyright (C) 2001-2016 NLTK Project
# Authors: Steven Bird <stevenbird1@gmail.com>
# Edward Loper <edloper@gmail.com>
# URL: <http://nltk.org/>
# For license information, see LICENSE.TXT
# Based on an Eliza implementation by Joe Strout
<joe@strout.net>,
# Jeff Epler <jepler@inetnebr.com> and Jez Higgins
<mailto:jez@jezuk.co.uk>.
# a translation table used to convert things you say into
things the
# computer says back, e.g. "I am" --> "you are"
from __future__ import print_function
from nltk.chat.util import Chat, reflections
# a table of response pairs, where each pair consists of a
# regular expression, and a list of possible responses,
# with group-macros labelled as %1, %2.
pairs = ((r'I need (.*)',("Why do you need %1?", "Would it
really help you to get %1?","Are you sure you need
%1?")),(r'Why don't you (.*)',
("Do you really think I don't %1?","Perhaps eventually
I will %1.","Do you really want me to %1?")),
[snip](r'(.*)\?',("Why do you ask that?", "Please consider
whether you can answer your own question.",
"Perhaps the answer lies within yourself?",
"Why don't you tell me?")),
(r'quit',("Thank you for talking with me.","Good-bye.",
"Thank you, that will be $150. Have a good day!")),
(r'(.*)',("Please tell me more.","Let's change focus a bit...
Tell me about your family.","Can you elaborate on
that?","Why do you say that %1?","I see.",
"Very interesting.","%1.","I see. And what does that
tell you?","How does that make you feel?",
"How do you feel when you say that?"))
)
eliza_chatbot = Chat(pairs, reflections)
def eliza_chat():
print("Therapist\n---------")
print("Talk to the program by typing in plain English,
using normal upper-")
print('and lower-case letters and punctuation. Enter "quit"
when done.')
print('='*72)
print("Hello. How are you feeling today?")
eliza_chatbot.converse()
def demo():
eliza_chat()
if __name__ == "__main__":
demo()

从这段代码中可以看出，它解析了输入的文本，然后将其与一系列的正则表达式进行匹配。一旦输入匹配成功，就会返回一个随机的答复（有时回应也会带上输入的一部分）。所以，像“I need a taco”这样的问题有时会触发“Would it really help you to get a taco?”这样的回答。显然，答案是肯定的，而且幸运的是，现在这个时代的技术已经可以为你提供这项服务了（祝福你，TacoBot），当然还是在早期发展阶段。令人震惊的是，有些人确实相信ELIZA是一个真实的人。

但是，更先进的机器人又怎样呢？它们是如何构建的？

令人惊讶的是，你遇到的大多数聊天机器人并没有使用机器学习的技术，他们使用的是所谓的基于检索的模型。这意味着答复是根据问题和上下文而预定义好的。这些机器人最常见的架构是被称为人工智能标记语言（AIML）的东西。AIML是一种基于XML的模式，它表示机器人应该如何与用户的输入进行交互。它只是一个更高级版本的ELIZA。

让我们来看看如何使用AIML来生成答复。首先，预处理所有的输入以使其标准化。这意味着当你输入“Waaazzup ???”时，它被映射到“WHAT IS UP”。这个预处理步骤通过多种方式，将针对同样事物的不同说法变成一个输入，这样就可以在输入上运行单个规则。标点符号等在这个阶段也被去除了。一旦完成，输入就匹配上了对应的规则。以下是模板的示例。

<category>
<pattern>WHAT IS UP</pattern>
<template>The sky, duh. Pfft. Humans...</template>
</category>

这是基本的设置，你也可以使用通配符、随机化和优先级模式。例如，以下模式使用了通配符匹配。

<category>
<pattern>* FOR ME<pattern>
<template>I'm a bot. I don't <star/>. Ever.</template>
</category>

这里，*通配符匹配了FOR ME之前的一个或多个单词，然后在返回的输出模板中重复这些内容。如果用户输入“Dance for me!”，回复就是“I'm a bot. I don't dance. Ever.”。

正如你所见，这些规则并没有产生任何接近于真实智力的东西，但有一些技巧让人们产生了幻觉。一个更棒的能力是根据话题的内容，产生相应的答复。

例如，以下是找到话题的规则。

<category>
<pattern>I LIKE TURTLES</pattern>
<template>I feel like this whole <set name="topic">turle</set>
thing could be a problem. What do you like about them?
</template>
</category>

一旦设置了话题，那么就可以匹配上特定于该上下文的规则。

<topic name="turtles">

<category>
<pattern>* SHELL IS *</pattern>
<template>I dislike turtles primarily because of their shells.
What other creepy things do you like about turtles?
</template>
</category>

<category>
<pattern>* HIDE *</pattern>
<template>I wish like a turtle that I could hide from this conversation.
</template>
</category>

</topic>

让我们看看这个交互大致是什么样子的。

>I like turtles!
>I feel like this whole turtle thing could be a problem. What do you like about them?
>I like how they hide in their shell
>I wish like a turtle I could hide from this conversation.

你可以看到，谈话的连续性增强了现实感。

你可能认为在深度学习的时代，这不是最先进的技术，你是对的。虽然大多数机器人都是基于规则的，但下一代的聊天机器人正在浮现，它们是基于神经网络的。

2015年，Google的Oriol Vinyas和Quoc Le发表了一篇论文（https://arxiv.org/ pdf/1506.05869v1.pdf），描述了基于序列到序列模型的神经网络构造。这种类型的模型将输入序列，例如“ABC”，映射到诸如“XYZ”的输出序列。这些输入和输出可以是从一种语言到另一种语言的翻译。然而，在他们的工作案例中，训练数据不再是语言翻译，而是技术支持的对话脚本和电影中的对白。虽然这两个模型的结果都很有趣，但基于电影对白模型的互动却更加引人注目。

图9-5是从论文中截取的交互样例。

图9-5

这些都不是由人类显式编码的或存在于训练集中，然而，看着这些就像和真人交谈，不禁让人有点恐惧。让我们看看更多的内容，如图9-6所示。

图9-6

注意，模型回答时似乎考虑了性别（他，她）、地理（英格兰）和职业（球员）。即使是有关意义、伦理和道德的问题，表现得也不错，如图9-7所示。

图9-7

会话还在继续，如图9-8所示。

图9-8

如果这个对话脚本没有让你对未来产生轻微的恐惧感，那么你可能已经是某种形式的人工智能产物了。

我真心推荐你阅读整篇论文。它写得不是过于技术化，而且肯定能让你了解这项技术目前发展的进度。

我们谈了很多关于聊天机器人的历史、类型和设计，现在让我们继续，创建一个属于自己的机器人吧！

9.4　打造一个聊天机器人

现在，了解了聊天机器人有哪些可能性，你很可能想打造一个最好的、最先进的、Google级别的机器人，对吧？好吧，现在不要想着那些了，因为我们会做相反的事情！我们将建立史上最糟糕的机器人！

让我告诉你为什么。要创建一个与Google所造的机器人相媲美的聊天机器人，需要大量的硬件和时间。在你的MacBook Pro上，无论使用何种真实训练集进行学习，模型通常都要耗费一个月或两个月的时间，你是无法速成它的。这意味着你不得不在Amazon的AWS上租用一些时间，而且也不是随随便便什么机器都能满足需求的。这类机器需要有一些强力的性能规格，最好是GPU。尝试这些当然是好事情。但是，如果你的目标只是建立一些酷炫、吸引人的东西，那么下面的内容就可以帮助到你。

我也应该提前警告你，虽然Cleverbot不是微软的Tay，但对话的口味可能还是会有点重。如果你很容易被冒犯，那你可能需要另一个不同的训练集。

好，让我们开始吧！

首先，和往常一样，我们需要训练数据。一如既往，这是整个过程中最具挑战性的一步。幸运的是，我找到了一个超赞的会话数据库。网站notsocleverbot.com让人们提交他们与Cleverbot之间最荒谬的对话。有了这个训练集，夫复何求？

让我们来看看Cleverbot和该站点用户之间的对话示例，如图9-9所示。

图9-9

所以，我们将从这里开始。首先，我们需要从这个网站下载对话脚本。为此，我们将使用和第2章相同的工具import.io，如图9-10所示。

图9-10

你只需将链接粘贴到页面上的表单中即可。输入的格式就像这样：http://www.notsoc leverbot.com/index.php?page=1。

一旦提交，网站import.io就将处理请求并返回一个页面，就像图9-11这样。

图9-11

如果从这里看一切正常，请单击右上角附近的粉色“Done”按钮。

网站import.io将处理该页面，然后将带你到图9-12这样的界面。

图9-12

接下来，单击中间的Show URL Generator按钮，如图9-13所示。

图9-13

接下来，你可以设置要下载的数字范围。例如，1～20，每次1个。显然，你抓取的页面越多，这个模型就会越好。但是，请记住，你的请求会对服务器造成压力，所以使用时请体贴一些[3]。

完成后，单击Add to List并在文本框中单击Return，然后你应该能够单击Save。它将开始运行，完成之后，你可以将数据下载为CSV文件。

接下来，我们将使用Jupyter记事本来检查和处理数据。首先导入 pandas和Python正则表达式的库re。我们还要设置pandas的选项来加宽列，以便我们可以更好地看清数据。

import pandas as pd
import re
pd.set_option('display.max_colwidth',200)

现在，我们将加载数据。

df = pd.read_csv('/Users/alexcombs/Downloads/nscb.csv')
df

上述代码生成图9-14所示的输出。

图9-14

由于我们只对第一列的对话数据感兴趣，将其拆解出来。

convo = df.iloc[:,0]
convo

上述代码生成图9-15所示的输出。

图9-15

从图9-15，我们可以看到用户和Cleverbot之间的交互，以及对话的启动。为了获取我们所需的格式，需要将数据解析成问题和答复的配对。我们不一定关心谁说了什么，但我们关心将每个答复和问题对应起来。稍后我们将理解这是为什么。现在让我们在文本上执行一些正则表达式。

clist = []
def qa_pairs(x):
cpairs = re.findall(": (.*?)(?:$|\n)", x)
clist.extend(list(zip(cpairs, cpairs[1:])))
convo.map(qa_pairs);
convo_frame = pd.Series(dict(clist)).to_frame().reset_index()
convo_frame.columns = ['q', 'a']

上述代码将产生图9-16所示的输出。

图9-16

好吧，这里有很多代码。刚刚发生了什么？我们首先创建了一个列表来保存问题和答复的元组。然后我们通过一个函数来切分对话，使用正则表达式将它们变为匹配的对。

最后，我们将列标记为q和a，并将所有的数据放入一个pandas的DataFrame。

我们现在将应用一点算法的魔术，为用户的输入匹配最接近的问题。

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
vectorizer = TfidfVectorizer(ngram_range=(1,3))
vec = vectorizer.fit_transform(convo_frame['q'])

我们在上面的代码中所做的是，导入TfidfVectorization库和余弦相似度的库。然后我们使用训练数据来创建tf-idf矩阵。现在可以使用这个来转换我们自己的新问题，并测量该问题与训练集中现有问题间的相似度。

我们在第5章详细讨论了余弦相似度和tf-idf算法，所以如果你想了解它们运作的细节，请参考第5章。

让我们现在获取相似度分数。

my_q = vectorizer.transform(['Hi. My name is Alex.'])
cs = cosine_similarity(my_q, vec)
rs = pd.Series(cs[0]).sort_values(ascending=0)
top5 = rs.iloc[0:5]
top5

上述代码生成图9-17的输出。

图9-17

我们在这里看什么？这里列出了和我提出的问题最接近的前五个问题，以及它们之间的余弦相似度。左边是索引编号，右边是余弦相似度。让我们来看看这些问题。

convo_frame.iloc[top5.index]['q']

这将产生图9-18所示的输出。

图9-18

我们可以看到，没有一个问题和我的输入是完全相同的，但肯定有一些相似之处。

再来看看答复。

rsi = rs.index[0]
rsi
convo_frame.iloc[rsi]['a']

上述代码将产生图9-19所示的输出。

图9-19

好吧，我们的机器人似乎已经表明了态度。让我们再进一步。

我们将创建一个方便的函数，这样可以轻松地测试一系列的语句。

def get_response(q):
my_q = vectorizer.transform([q])
cs = cosine_similarity(my_q, vec)
rs = pd.Series(cs[0]).sort_values(ascending=0)
rsi = rs.index[0]
return convo_frame.iloc[rsi]['a']
get_response('Yes, I am clearly more clever than you will ever be!')

这将产生图9-20所示的输出。

图9-20

显然，我们创造了一个怪物，所以继续吧。

get_response('You are a stupid machine. Why must I prove anything to you?')

这将产生图9-21所示的输出。

图9-21

我很享受这个。让我们继续前进。

get_response('My spirit animal is a menacing cat. What is yours?')

图9-22

我回答说：

get_response('I mean I didn't actually name it.')

这将产生图9-23的输出。

图9-23

继续：

get_response('Do you have a name suggestion?')

这将产生图9-24所示的输出。

图9-24

我回答：

get_response('I think it might be a bit aggressive for a kitten')

这将产生图9-25所示的输出。

图9-25

我试图平息局面：

get_response('No need to involve the police.')

这将产生图9-26所示的输出。

图9-26

最后：

get_response('And I you, Cleverbot')

这将产生图9-27所示的输出。

图9-27

显然，这可能是我近期最好的对话之一了：包括机器人和真实的人。

现在我们已经创建了这个基于蛋糕[4]的智能，这里要设置一下，以便我们可以通过短信的方式与其聊天。

为了达到这个目的，我们还需要一些东西。第一个是twilio账户。他们将提供一个免费的账户，让我们发送和接收短信，如图9-28所示。

图9-28

跳转到http://www.twilio.com的网页，单击注册一个免费的开发者API密钥。你会设置一些登录凭据，然后他们将向你的手机发送短信以确认你的号码。设置完成后，你就可以在Quickstart文档中找到详细信息。请确保你在左上角的下拉菜单中选择了Python，如图9-29所示。

图9-29

从Python代码发送消息很简单，但是你需要请求一个twilio 的号码。在代码中，你将使用这个号码发送和接收消息。接收消息稍微有点复杂，因为它需要你运行一个Web服务器。文档很简洁，所以设置它不会让你太难熬。你需要一个公共Web能访问的Flask[5]服务器URL地址，并将其粘贴到你管理twilio号码的区域之下方。只需单击号码，它就会带你到粘贴网址的地方，如图9-30所示。

图9-30

一旦这些都设置完成，你只需要确保Flask Web服务器启动并运行。这里，我压缩了所有的代码，可以用于你的Flask应用程序。

from flask import Flask, request, redirect
import twilio.twiml
import pandas as pd
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
app = Flask(__name__)
PATH_TO_CSV = 'your/path/here.csv'
df = pd.read_csv(PATH_TO_CSV)
convo = df.iloc[:,0]
clist = []
def qa_pairs(x):
cpairs = re.findall(": (.*?)(?:$|\n)", x)
clist.extend(list(zip(cpairs, cpairs[1:])))
convo.map(qa_pairs);
convo_frame = pd.Series(dict(clist)).to_frame().reset_index()
convo_frame.columns = ['q', 'a']
vectorizer = TfidfVectorizer(ngram_range=(1,3))
vec = vectorizer.fit_transform(convo_frame['q'])
@app.route("/", methods=['GET', 'POST'])
def get_response():
input_str = request.values.get('Body')
def get_response(q):
my_q = vectorizer.transform([input_str])
cs = cosine_similarity(my_q, vec)
rs = pd.Series(cs[0]).sort_values(ascending=0)
rsi = rs.index[0]
return convo_frame.iloc[rsi]['a']
resp = twilio.twiml.Response()
if input_str:
resp.message(get_response(input_str))
return str(resp)
else:
resp.message('Something bad happened here.')
return str(resp)

看起来有很多的事情，但实际上我们使用了和之前相同的代码，只是现在我们抓取了twilio发送的POST数据——消息正文——而不是之前手动输入get_request函数的数据。

如果所有都按计划进行，你应该已经拥有了自己的搞怪好友，你可以随时向它发送短消息，还有什么比这更棒的？

9.5　小结

在本章中，我们对聊天机器人进行了全面的探讨。很明显，我们是这类应用程序的弄潮儿。希望本章激励了你，创建自己的机器人，但如果没有，至少你对这些应用的原理，以及它们如何塑造我们的未来，有了更加丰满的理解。

我会让应用程序说出结束语。

get_response("Say goodbye, Clevercake")

这将产生图9-31的输出。

图9-31

在下一章中，我们将进入推荐引擎的世界。

[1]　译者注：罗杰斯，著名的心理学家，罗氏心理治疗法的创始人。

[2]　译者注：美国非常知名的连锁餐饮品牌。

[3]　译者注：作者的意思是，不要在短时间内抓取大量网页，否则会对www.notsocleverbot.com产生过大的流量压力。

[4]　暗合刚刚出现的“clevercake”。

[5]　译者注：Flask是一个使用 Python 编写的轻量级 Web 应用框架。

第10章　构建推荐引擎

像许多事情一样，该话题源于沮丧和僵硬的鸡尾酒。这是一个星期六，两个年轻人晚上依旧没有和女生约会。他们坐在一起喝了很多酒，相互分享了很多悲伤的事情，渐渐地这两个哈佛大一新生开始思考一个问题。如果不是依靠随机的机会，而是使用一个电脑算法来发现合适的女孩，这会怎么样？

他们觉得匹配人的关键是创建一组问题。每个人都会在头几次尴尬的约会上寻找一些信息，而这组问题就是为大家提供这样的信息。使用这些问卷进行候选人的匹配，那么没有任何希望的约会就被消灭在萌芽之中。这个过程将是相当有效的。

想法是向波士顿和全国的大学生推销他们的新服务。马上，他们就这么做了。

不久之后，他们建立的数字配对服务取得了巨大的成功。它受到全国媒体的关注，并在后面的几年内生成了数万的匹配。

事实上，这家公司是如此的成功，一家更大的公司为了使用其技术，最终将其收购。

如果你认为我可能在谈论OkCupid，那么你会错了——差不多有40年的误差。我所说的公司是在1965年开始的——当时，匹配的计算是通过IBM 1401大型机上的穿孔卡来实现的。它花了三天时间来运行计算。

奇怪的是，OkCupid和1965年的前辈，Compatibility Research公司，之间是有关联的。Compatibility Research的联合创始人是Jeff Tarr，他的女儿Jennifer Tarr是OkCupid联合创始人Chris Coyne的妻子。世界真的很小。

为什么这些与构建推荐引擎的内容相关？这是因为该系统很可能是第一个推荐引擎。虽然大多数人认为推荐引擎作为工具，是用于寻找和他们喜好紧密相关的产品、音乐和电影，但最原始的应用是找到潜在的伴侣。关于这些系统如何工作，它提供了一个很好的参考框架。

在本章中，我们将探讨不同类型的推荐系统。我们将了解它们的商业化实现，以及内部的工作原理。最后，我们会实现自己的推荐引擎，找到适合的GitHub资料库。

本章将讨论以下主题。

- 协同过滤。

- 基于内容的过滤。

- 混合系统。

- 构建推荐引擎。

10.1　协同过滤

在2012年初，爆出了这样一则新闻故事：一位男子进入一家Target[1]商店，挥舞着手中的一叠优惠券，这些都是Target邮寄给他还在读高中的女儿的。他来的目的是谴责经理，因为这套优惠券都是诸如婴儿服装、配方奶和幼儿家具这类商品专享的。

听到顾客的投诉，经理再三道歉。他感觉很糟糕，想在几天后通过电话跟进，解释这是怎么回事。这个时候，反而是这位父亲在电话里进行了道歉。看来他的女儿确实是怀孕了。她的购物习惯泄露了她的这个秘密。

出卖这位女生的算法很可能是，至少部分是，基于协同过滤。

那么，什么是协同过滤？

协同过滤（collaborative filtering）是基于这样的想法，在某处总有和你趣味相投的人。假设你和趣味相投的人们评价方式都非常类似，而且你们都已经以这种方式评价了一组特定的项目，此外，你们每个人对其他人尚未评价的项目也有过评价。正如已经假设的那样，你们的口味是类似的，因此可以从趣味相投的人们那里，提取具有很高评分而你尚未评价的项目，作为给你的推荐，反之亦然。在某种程度上，这点和数字化配对非常相像，但结果是你喜欢的歌曲或产品，而不是与异性的约会。

对于怀孕的高中生这个案例，当她购买了无味的乳液、棉球和维生素补充剂[2]之后，她可能就和那些稍后继续购买婴儿床和尿布的人匹配上了。

10.1.1　基于用户的过滤

让我们通过一个例子来看看实践中这是如何运作的。

我们将从被称为效用矩阵（utility matrx）的东西开始。它和词条-文档矩阵相类似，不过这里我们表示的是产品和用户，而不再是词条和文档。

这里，我们假设有顾客A到D，以及他们所评分的一组产品，评分从0到5，如表10-1所示。

表10-1

Snarky's Potato Chips

SoSo SmoothLotion

DufflyBeer

BetterTapWater

XXLargeLivin' Football Jersey

SnowyCottonBalls

Disposos'Diapers

A

4

5

3

5

B

4

4

5

C

2

2

1

D

5

3

5

4

之前我们看到，当想要查找类似的项目时，可以使用余弦相似度。让我们在这里试试。我们将为用户A发现最相似的其他顾客。由于这里的向量是稀疏的，包含了许多未评分的项目，我们将在这些缺失的地方输入一些默认值。这里填入0。我们从用户A和用户B的比较开始。

from sklearn.metrics.pairwise import cosine_similarity
cosine_similarity(np.array([4,0,5,3,5,0,0]).reshape(1,-1),\
np.array([0,4,0,4,0,5,0]).reshape(1,-1))

上述代码生成图10-1的输出。

图10-1

我们可以看到，这两者没有很高的相似性，这是有道理的，因为他们没有多少共同的评分。

现在来看看用户C与用户A的比较。

cosine_similarity(np.array([4,0,5,3,5,0,0]).reshape(1,-1),\
np.array([2,0,2,0,1,0,0]).reshape(1,-1))

上述代码生成图10-2的输出。

图10-2

这里，我们看到他们有很高的相似度（记住1是完美的相似度）， 尽管他们对同样产品的评价有所不同。为什么得到了这样的结果？[3]问题在于我们对没有评分的产品，选择使用0分。它表示强烈的（负的）一致性。在这种情况下，0不是中性的。

那么，如何解决这个问题？

我们可以做的是重新生成每位用户的评分，并使得平均分变为0或中性，而不是为缺失值简单地使用0。我们拿出每位用户的评分，将其减去该用户所有打分的平均值。例如，对于用户A，他打分的平均值为17/4，或4.25。然后我们从用户A提供的每个单独评分中减去这个值。

一旦完成，我们继续找到其他用户的平均值，从他们的每个评分中减去该均值，直到对每位用户完成该项操作。

这个过程后将产生表10-2。请注意，每行的用户评分总和为0 （这里忽略四舍五入带来的问题）。

表10-2

Snarky's Potato Chips

SoSo SmoothLotion

DufflyBeer

BetterTapWater

XXLargeLivin' Football Jersey

SnowyCottonBalls

Disposos'Diapers

A

- 0 .25

0 .75

-1.25

0 .75

B

- 0 .33

- 0 .33

0 .66

C

0 .33

0 .33

- 0 .66

D

0 .75

-1.25

0 .75

- 0 .25

让我们在新的数据集上尝试余弦相似度。再次将用户A和用户B、C进行比较。

首先，A和B之间的比较如下。

cosine_similarity(np.array([-.25,0,.75,-1.25,.75,0,0])\
.reshape(1,-1),\
np.array([0,-.33,0,-.33,0,.66,0])\
.reshape(1,-1))

上述代码生成图10-3的输出。

图10-3

现在，我们试试看A和C。

cosine_similarity(np.array([-.25,0,.75,-1.25,.75,0,0])\
.reshape(1,-1),\
np.array([.33,0,.33,0,-.66,0,0])\
.reshape(1,-1))

上述代码生成图10-4的输出。

图10-4

我们可以看到，A和B之间的相似度略有增加，而A和C之间的相似度显著下降。这正是我们所希望的。

这种中心化的过程除了帮助我们处理缺失值之外，还有其他好处，例如帮助我们处理不同严苛程度的打分者，现在每位打分者的平均分都是0了。注意，这个公式等价于Pearson相关系数，取值落在−1和1之间。

让我们现在采用这个框架，使用它来预测产品的评分。我们将示例限制为三位用户X、Y和Z，我们将预测X尚未评价，而和X非常相似的Y和Z已经评过的产品，对于X而言会得到多少分。

我们先从每位用户的基本评分开始，如表10-3所示。

表10-3

Snarky's Potato Chips

SoSo SmoothLotion

DufflyBeer

BetterTapWater

XXLargeLivin' Football Jersey

SnowyCottonBalls

Disposos'Diapers

X

4

3

4

Y

3.5

2.5

4

4

Z

4

3.5

4.5

4.5

接下来，我们将中心化这些评分，如表10-4所示。

表10-4

Snarky's Potato Chips

SoSo SmoothLotion

DufflyBeer

BetterTapWater

XXLargeLivin' Football Jersey

SnowyCottonBalls

Disposos'Diapers

X

0 .33

- 0 .66

0 .33

?

Y

0

-1

0 .5

0 .5

Z

- 0 .125

- 0 .625

0 .375

0 .375

现在，我们想知道用户X会给Disposos' Diapers打多少分。我们可以根据用户评分中心化之后的余弦相似度获得权重，并通过这些权重对用户Y和用户Z的评分进行加权计算。

让我们先得到用户Y和X的相似度。

user_x = [0,.33,0,-.66,0,33,0]
user_y = [0,0,0,-1,0,.5,.5]
cosine_similarity(np.array(user_x).reshape(1,-1),\
np.array(user_y). reshape(1,-1))

上述代码生成图10-5的输出。

图10-5

现在计算用户Z和X的相似度。

user_x = [0,.33,0,-.66,0,33,0]
user_z = [0,-.125,0,-.625,0,.375,.375]

cosine_similarity(np.array(user_x).reshape(1,-1),\
np.array(user_z).reshape(1,-1))

上述代码生成图10-6的输出。

图10-6

因此，我们现在有一个用户X和用户Y之间的相似度（0.42447212），以及用户A和用户Z之间的相似度（0.46571861）。

将它们整合起来，我们通过每位用户与X之间的相似度，对每位用户的评分进行加权，然后除以总相似度。

(0.42447212 × (4) +0.46571861 × (4.5)) / (0.42447212 +0.46571861) = 4.26

我们可以看到用户X对Disposos' Diapers 的预估评分为4.26（不低啊，最好发张优惠券！）。

10.1.2　基于项目的过滤

到目前为止，我们只了解了基于用户的协同过滤，但还有一个可用的方法。在实践中，这种方法远优于基于用户的过滤[4]，它被称为基于项目的过滤。这是它的工作原理：每个被评分项目与所有其他项目相比较，找到最相似的项，而不是根据评分历史将每位用户和所有其他用户相匹配。同时，也是使用中心化余弦相似度。

让我们来看看它是如何工作的。

再次，我们有一个效用矩阵。这一次，我们将看看用户对歌曲的评分。每一列是一位用户，而每一行是一首歌曲，如表10-5所示。

表10-5

U1

U2

U3

U4

U5

S1

2

4

5

S2

3

3

S3

1

5

4

S4

4

4

4

S5

3

5

现在，假设我们想知道U3对于S5的评分。这里，我们会根据用户对歌曲的评分来寻找类似的歌曲，而不是寻找类似的用户。

让我们来看一个例子。

首先，我们从每行歌曲的中心化开始，并计算其他每首歌曲和目标歌曲（即S5）的余弦相似度，参见表10-6。

表10-6

U1

U2

U3

U4

U5

CntrdCoSim

S1

-1.66

0 .33

1.33

0 .98

S2

0

0

0

S3

-2.33

1.66

0 .66

0 .72

S4

0

0

0

0

S5

-1

?

1

1

你可以看到，最右边的列是其他每行相对行S5的中心化余弦相似度。

接下来需要选择一个数字，k，这是我们为预测U3对歌曲的评分，所要使用的最近邻居数量。在这个简单的例子中，我们使用k = 2。

我们可以看到对于歌曲S5，S1和S3是和它最相似的，所以我们将使用U3对这两首歌的评分（分别为4和5）。

现在让我们计算评分。

(0.98 × (4) +0.72 × (5)) / (0.98 +0.72) = 4.42

因此，通过基于项目的协同过滤，我们可以看到U3很可能给S5打出高分4.42。

之前，我提到基于用户的过滤不如基于项目的过滤有效。这是为什么呢？

很有可能，你的朋友和你有共同的爱好，但是你们每个人都有自己喜欢，而别人毫无兴趣的领域。

例如，也许你们都喜欢“权力的游戏”这部电视剧，但你的朋友也喜欢Norwegian death metal重金属乐队。而你死也不愿意听这种音乐。如果你们在许多方面类似——除了death metal——那么基于用户的推荐，你仍然会看到很多关于乐队的推荐，其名称都包括火焰、斧头、头骨和大头棒这样的字眼。使用基于项目的过滤，很可能会避免让你看到这些推荐。

让我们用快速的代码示例，来总结这个问题的讨论。

首先，我们将创建DataFrame示例。

import pandas as pd
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

df = pd.DataFrame({'U1':[2 , None, 1, None, 3], 'U2': [None, 3, None, 4,
None],\
'U3': [4, None, 5, 4, None], 'U4': [None, 3, None, 4,
None], 'U5': [5, None, 4, None, 5]})

df.index = ['S1', 'S2', 'S3', 'S4', 'S5']

df

上述代码生成图10-7的输出。

图10-7

我们现在将创建一个函数，它将读取用户和项目的评分矩阵。对于给定的项目和用户，该函数将返回基于协同过滤的预测评分。

def get_sim(ratings, target_user, target_item, k=2):
centered_ratings = ratings.apply(lambda x: x - x.mean(), axis=1)
csim_list = []
for i in centered_ratings.index:
csim_list.append(cosine_similarity(np.nan_to_num(centered_ratings.loc[i,:].
values).reshape(1, -1),
np.nan_to_num(centered_ratings.loc[target_item,:]).reshape(1, -1)).item())
new_ratings = pd.DataFrame({'similarity': csim_list, 'rating':
ratings[target_user]}, index=ratings.index)
top = new_ratings.dropna().sort_values('similarity',
ascending=False)[:k].copy()
top['multiple'] = top['rating'] * top['similarity']
result = top['multiple'].sum()/top['similarity'].sum()
return result

现在可以传入我们的值，并获得用户对项目的预测评分。

get_sim(df, 'U3', 'S5', 2)

上述代码生成图10-8的输出。

图10-8

我们可以看到这与之前的分析相符。

到目前为止，我们在进行比较时，将用户和项目作为整个的实体，但是现在，让我们继续了解另一种方法，它将我们的用户和项目分解为所谓的特征集合。

10.2　基于内容的过滤

作为一个音乐家，Tim Westergren花了几年时间倾听其他有天赋的音乐家的作品，想知道为什么他们永远不能拔尖。他们的音乐很好，和你在电台收听到的那些一样好。然而，不知何故，他们从来没有大的突破。他想，一定是因为他们的音乐没有在足够的、合适的人们面前展示。

Tim最终退出了音乐家的工作，开始从事电影背景音乐的作曲。在那里，他开始思考每一块音乐自己独特的结构或DNA，并可以将其分解为不同的组成部分。

思考一番之后，他开始考虑围绕这个想法创建一家公司，建立一系列音乐的基因组。他的一位朋友曾经创建并出售了一家公司，Tim让他来运作这个想法。Tim的朋友喜欢他的想法，并开始帮助他写一个商业计划，并为该项目收集了首轮融资。行动开始了。

在接下来的几年里，他们雇用了一小群音乐家，对上百万首音乐细致地编写了几乎400个不同的特征，每个特征从0到5进行打分——所有都是通过手，或者说是通过耳朵进行的。每首3到4分钟长的歌曲需要几乎半小时的评级。

这些特征包括如此的参数：如领唱歌手的声音有多么的沉重，或节奏是每分钟多少拍。他们花费了近一年的时间完成了首个原型。它完全使用Excel中的VBA宏构建，花了差不多4分钟才返回一次推荐结果。但是，最后，它成功了，运作得非常好。

我们现在知道这家公司就是Pandora Music，你很可能已经听说过或使用过其产品，因为每天它有来自世界各地数百万的用户。毫无疑问，它是基于内容过滤的成功范例。

在基于内容的过滤中，不再将每首歌曲视为一个不可分割的单位，而是将它变成特征向量，然后就可以使用我们的老朋友余弦相似度进行比较。

不仅歌曲可以被分解成为特征向量，听众也可以被转化为特征向量。听众的品味描述成为了空间中的向量，使我们可以测量他们的品味描述和歌曲本身之间的相似程度。

对于Tim Westergren来说，这是神奇的，因为不像其他推荐引擎依赖于音乐的人气，这个系统的推荐是基于固有的结构相似性。也许有人从来没有听过歌曲X，但如果他们喜欢歌曲Y，那么他们应该喜欢歌曲X，因为这两首歌在基因上是几乎相同的。这就是基于内容的过滤。

10.3　混合系统

我们已经学习了推荐系统的两种主要形式。但是，需要注意的是，在任何大规模生产环境中，推荐引擎可能同时利用这两项技术。这被称为混合系统，人们喜欢混合系统的原因是，它有助于消除使用单一系统时可能存在的缺点。这两个系统在一起，创建了更强大的解决方案。

让我们检查每种类型的利弊。

协同过滤的优点如下。

- 没有必要手动创建特征。

协同过滤的缺点如下。

- 如果没有大量的项目和用户，它不能正常工作。

- 当项目数量远远超过可能被购买的数量时[5]，效用矩阵会有稀疏性。

基于内容的过滤的优点如下。

它不需要大量的用户。

基于内容的过滤的缺点如下。

- 定义正确的特征可能是一个挑战。

- 缺乏“意外的惊喜”[6]。

当一家公司缺乏大量的用户群，基于内容的过滤是更好的选择，但是随着公司的增长，加入协同过滤可以帮助我们为用户提供更多的“惊喜”。

现在你已经熟悉推荐引擎的类型和内部工作原理了，让我们开始构建自己的引擎吧。

10.4　构建推荐引擎

我喜欢的一件事是偶遇一个非常有用的GitHub资源库。有非常多的资源库，包括人为管理的机器学习教程，几十行使用ElasticSearch的代码包等等。麻烦的是，找到这些库远比想象的困难。幸运的是，我们现在懂得利用GitHub的API，在一定程度上帮助我们发现这些代码的珍宝。

我们将使用GitHub API，创建基于协同过滤的推荐引擎。这个计划是获得所有我已经加上了星号的资料库，然后得到这些库的全部创作者。然后再获取这些作者添加过星号的所有资料库。一旦完成，我们可以比较已加星标的资料库，找到和我最相似的用户（如果你自己也运行GitHub的资料库，我建议查找和你最相似的用户）。一旦发现了最相似的GitHub用户，我们可以使用他们所加星的（而我没有加过星号的）资料库来生成一组推荐。

让我们开始吧。首先，我们将导入需要的库。

import pandas as pd
import numpy as np
import requests
import json

现在，你需要开立一个GitHub账户，并为一些资料库打上星号，但你不需要注册开发人员项目。你可以从个人资料中获取授权令牌，它允许你使用API。你也可以在代码中使用它，但其限制相当严格，对于我们的示例用处不大。

为了创建用于API的令牌，请访问以下URL https://github.com/settings/ tokens。在这里，你将在右上角看到一个按钮，如图10-9所示。

图10-9

你需要单击Generate new token按钮。一旦完成，你需要将提供的令牌复制到以下代码中。请确保这两者都包含于引号中。

myun = YOUR_GITHUB_HANDLE
mypw = YOUR_PERSONAL_TOKEN

现在，我们将创建一个函数，它将拉取你已加星标的每个资料库的名称。

my_starred_repos = []
def get_starred_by_me():
resp_list = []
last_resp = ''
first_url_to_get = 'https://api.github.com/user/starred'
first_url_resp = requests.get(first_url_to_get, auth= (myun,mypw))
last_resp = first_url_resp
resp_list.append(json.loads(first_url_resp.text))

while last_resp.links.get('next'):
next_url_to_get = last_resp.links['next']['url']
next_url_resp = requests.get(next_url_to_get, auth= (myun,mypw))
last_resp = next_url_resp
resp_list.append(json.loads(next_url_resp.text))

for i in resp_list:
for j in i:
msr = j['html_url']
my_starred_repos.append(msr)

这里有很多操作，但实质上，我们就是查询API以获取自己加过星标的资料库。GitHub使用分页，而不是在一次调用中返回所有结果。因此，我们需要检查从每个响应返回的.links。只要有下一个链接可以调用，我们就继续这样做。

接下来，我们只需要调用创建的函数。

get_starred_by_me()

然后，我们可以看到已加星标资料库的完整列表。

my_starred_repos

此代码将产生类似于图10-10的输出。

图10-10

接下来，我们需要解析每个已加星标资料库的用户名，这样就可以检索他们曾经标记的库。

my_starred_users = []
for ln in my_starred_repos:
right_split = ln.split('.com/')[1]
starred_usr = right_split.split('/')[0]
my_starred_users.append(starred_usr)

my_starred_users

上述代码生成图10-11的输出。

图10-11

现在，我们已经获得了所有加星标资料库的作者，下面需要检索他们所加标的库，以下函数将会实现这一点。

starred_repos = {k:[] for k in set(my_starred_users)}
def get_starred_by_user(user_name):
starred_resp_list = []
last_resp = ''
first_url_to_get = 'https://api.github.com/users/'+ user_name +'/starred'
first_url_resp = requests.get(first_url_to_get, auth= (myun,mypw))
last_resp = first_url_resp
starred_resp_list.append(json.loads(first_url_resp.text))

while last_resp.links.get('next'):
next_url_to_get = last_resp.links['next']['url']
next_url_resp = requests.get(next_url_to_get, auth= (myun,mypw))
last_resp = next_url_resp

starred_resp_list.append(json.loads(next_url_resp.text))

for i in starred_resp_list:
for j in i:
sr = j['html_url']
starred_repos.get(user_name).append(sr)

这个函数的工作方式与我们之前调用的函数几乎相同，但它调用了不同的端点。它会将之前作者们加星标的资料库添加到一个字典，我们稍后将使用该字典。

让我们现在调用它。运行可能需要几分钟，具体取决于作者们加标的资料库数量。实际上，我自己的数据超过了4,000个加标的资料库。

for usr in list(set(my_starred_users)):
print(usr)
try:
get_starred_by_user(usr)
except:
print('failed for user', usr)

上述代码生成图10-12的输出。

图10-12

请注意，在调用它之前，我将已加星标的用户列表变为了一个集合。我发现了一些重复的用户，这是由于在一个用户句柄下对多个资料库加了星标，所以将列表转化为集合很有意义，它会去除重复的调用。

我们现在需要为所有被加标的资料库，构建一个特征集。

repo_vocab = [item for sl in list(starred_repos.values()) for item in sl]

接下来，由于多个用户会标注同一个资料库，我们将其转换为一个集合，以删除可能存在的多个重复。

repo_set = list(set(repo_vocab))

让我们看看这产生了多少库。

len(repo_vocab)

上面的代码生成了图10-13的输出。

我加标的资料库已经超过80个了，而所有相关的用户对超过12,000个唯一的资料库加过星标。你可以想象，如果我们按照同样的方法进一步获取相关的资料库[7]，那会有多少。

图10-13

现在，我们有了完整的特征集，或着说资料库的词汇，我们对于每位用户和每个资料库的组合创建一个二进制向量，如果该用户对该库有加星标，那么为1，否则为0。

all_usr_vector = []
for k,v in starred_repos.items():
usr_vector = []
for url in repo_set:
if url in v:
usr_vector.extend([1])
else:
usr_vector.extend([0])
all_usr_vector.append(usr_vector)

我们刚刚做的是检查每位用户，看看他们是否为词汇集中的资料库打过星标。如果打过，值就设置为1，如果没有就是0。

此时，我们有12,378个项目（资料库），79位用户，以及他们之间的二进制向量。让我们将这些放入一个DataFrame。行索引将是我们已加星标的用户句柄，而列将是资料库的词汇。

df = pd.DataFrame(all_usr_vector, columns=repo_set,
index=starred_ repos.keys())
df

上述代码生成图10-14的输出。

接下来，为了将我们自己与其他用户进行比较，需要向数据框中添加自己的那行。

my_repo_comp = []
for i in df.columns:
if i in my_starred_repos:
my_repo_comp.append(1)
else:
my_repo_comp.append(0)

mrc = pd.Series(my_repo_comp).to_frame('acombs').T
mrc

图10-14

上述代码生成图10-15的输出。

图10-15

我们现在需要添加适当的列名并将其连接到其他数据框。

mrc.columns = df.columns

fdf = pd.concat([df, mrc])

fdf

上述代码生成图10-16的输出。

图10-16

你可以看到，在图10-16的截图中，我也被添加到DataFrame。

现在，我们只需要计算自己和其他用户之间的相似性。这次我们将使用pearsonr函数，它需要从scipy导入。

from scipy.stats import pearsonr

sim_score = {}
for i in range(len(fdf)):
ss = pearsonr(fdf.iloc[-1,:], fdf.iloc[i,:])
sim_score.update({i: ss[0]})

sf = pd.Series(sim_score).to_frame('similarity')
sf

上述代码生成图10-17的输出。

图10-17

我们刚刚所做的是将DataFrame中最后一个向量和其他向量进行比较，并生成中心化余弦相似度（Pearson相关系数）[8]。一些值是NaN（不是数字），因为他们没有给任何项目标记星号，导致在计算中除以了零。

现在让我们对这些值进行排序，以返回最相似用户的索引编号。

sf.sort_values('similarity', ascending=False)

上述代码生成图10-18的输出。

图10-18

这些是最相似的用户，因此，我们可以使他们来推荐自己可能喜欢的资料库。来看看这些用户，以及他们都标记了哪些我们可能喜欢的资料库。

你可以忽略具有完美相似度分数的第一个用户，这是我们自己。按照列表找下去，三个最接近的匹配是用户31、用户5和用户71。让我们看看每个人。

fdf.index[31]

上述代码生成图10-19的输出。

图10-19

让我们来看看这是谁，以及他们的资料库是什么。

从https://github.com/lmcinnes，我们可以看到资料库属于谁。

这是hdbscan的作者—— 一个优秀的库—— 他恰好也是scikit-learn和matplotlib的贡献者，如图10-20所示。

图10-20

让我们看看他对哪些库加了星标。有几种方法来做到这点：我们可以使用自己的代码，或者只是单击他们图片下方的星星。让我们两者都试一下，只是比较并确保一切都是对的。

首先通过代码：

fdf.iloc[31,:][fdf.iloc[31,:]==1]

上面的代码生成图10-21的输出。

图10-21

我们看到13个被标记的资料库。让我们将其和GitHub网站提供的那些进行比较，如图10-22所示。

图10-22

在这里，我们可以看到它们是完全相同的。还要注意，我们可以记录自己和这位用户都标记的库：他们是标记为Unstar[9]的那些。

不幸的是，只有13个标星的资料库，没有足够的数据来生成推荐。

下一位相似的用户，实际上是一个朋友和前同事，Charles Chi。

fdf.index[5]

上述代码生成图10-23的输出。

图10-23

他的GitHub描述文件如图10-24所示。

图10-24

在这里，我们看到了他加过星标的资料库，如图10-25所示。

图10-25

Charles已经标记了27个库，所以肯定可以从中发现一些好的建议。

最后，让我们来看看第三个最相似的用户。

fdf.index[71]

这将产生图10-26的输出。

图10-26

用户Artem Ruster已经发布了近500个资料库，如图10-27所示。

图10-27

我们可以在图10-28中看到他已加星标的资料库。

图10-28

这绝对是产生推荐内容的沃土。让我们现在开始，使用这三个链接产生一些推荐。

首先，我们需要收集他们已经加星标，而我没有加星标的链接。我们将创建一个DataFrame，放入我和三位相似用户已加星标的资料库。

all_recs =
fdf.iloc[[31,5,71,79],:][fdf.iloc[[31,5,71,79],:]==1].fillna(0).T

上述代码生成图10-29的输出。

图10-29

如果看起来好像全是零，不用担心，这是一个稀疏矩阵，所以大多数都将是0。让我们看看是否存在我们几个都已加星标的资料库。

all_recs[(all_recs==1).all(axis=1)]

此代码将产生图10-30的输出。

图10-30

可以看到，不出意外的，我们都喜欢scikit-learn。让我们看看其他几位标记了哪些我没标记的。先创建一个排除我的数据框，然后，查询共同的加标资料库。

str_recs_tmp = all_recs[all_recs['acombs']==0].copy()
str_recs = str_recs_tmp.iloc[:,:-1].copy()
str_recs

上述代码生成图10-31的输出。

图10-31

好吧，看起来我没有错失任何超级资料库。让我们看看是否存在两位共同加标的库。为了找到这些，我们只是将行的内容加和。

str_recs[str_recs.sum(axis=1)>1]

上述代码生成图10-32的输出。

图10-32

这看起来很有希望，因为有一些资料库，被cchi和rushter都加过星号。看看库的名称，似乎有许多“很棒”（awesome）的项目在其中。也许我应该跳过推荐引擎，直接使用关键字搜索“awesome”。

到目前为止，不得不说我对结果印象深刻。这些肯定是我感兴趣的库，我一定会仔细看看。

现在，我们使用协同过滤生成了推荐，然后通过聚集执行了一点额外的过滤。如果想更进一步，我们可以按照每个被推荐项目收到的星星数来排序。你可以通过GitHub API进行另一次调用，来实现这一点。有一个端点会提供此类信息。

为了改进结果，可以做的另一件事情是添加基于内容的过滤。这是我们前面所讨论的混合步骤。我们需要为自己的库创建一组特征，而这些特征可以表明我们的兴趣。一种方法是对加标资料库的名称以及描述进行分词，来创建一个特征集。

这里是我打过星标的库，如图10-33所示。

图10-33

你可以想象，这将生成一组单词特征，我们可以用其审查基于协同过滤的那些推荐。这将包括很多词汇，如Python、Machine Learning和Data Science等。这将确保与我们不太相似的用户仍然可以提供基于自身兴趣的推荐。它也会减少推荐的“意外之喜”，你需要考虑到这点。例如，有可能某些资料库不同于当前我所标星的库，然而我对它其实很感兴趣。这当然只是一种可能性。

从数据框的角度看，基于内容过滤的步骤会是什么样子？列将是单词特征（n元语法），行将是从协同过滤步骤产生而来的资料库。我们只需使用自己的库，再次运行相似性比较的过程。

10.5　小结

在本章中，我们了解了推荐引擎。我们学习了当今使用的两个主要类型的系统：协同过滤和基于内容的过滤。我们还了解了如何将它们一起使用并形成混合系统。我们也花了一些时间讨论每类系统的利弊。最后，我们学习了如何使用GitHub API，从头开始一步一步地构建推荐引擎。

我希望你使用本章中的指导来构建自己的推荐引擎，而且我希望你能找到许多对你有用的资源。我知道我找到了一些自己肯定会使用的东西。

在构建推荐引擎的旅途上，在使用本书所介绍的全部机器学习技术的旅途上，我祝你好运。机器学习打开了一个新的世界，充满了可能性，未来它将影响我们生活中的几乎每一个领域，而你，已经迈出了进入这个新世界的第一步。

[1]　译者注：Target是美国知名的零售连锁店。

[2]　译者注：这些是刚刚怀孕的母亲会经常购买的。

[3]　译者注：作者的意思是，A和C对相同项目的评分有较大差距，他们不应该有如此高的相似度。

[4]　译者注：这点不能一概而论，要根据具体的应用场景而言。

[5]　译者注：推荐引擎一般用于电子商务，所以效用矩阵里的评分通常代表购买。在其他应用场景中，“被购买”等同于“被评分”。

[6]　译者注：推荐引擎有一个重要的评估指标是能否为用户带来意外的惊喜，为其推荐他们没有想到，但确实喜欢的物品。

[7]　译者注：作者介绍的数据获取方法类似网络爬虫，从作者自己开始，获取加标的库，查看其作者，再获取该作者加标的库，如此往复，可以获取海量数据。

[8]　译者注：根据维基百科的定义，中心化之后，向量间的余弦相似度和Pearson相关系数是等价的。

[9]　由于自己标记为star，才会出现unstar的操作选项。

欢迎来到异步社区！

异步社区的来历

异步社区(www.epubit.com.cn)是人民邮电出版社旗下IT专业图书旗舰社区，于2015年8月上线运营。

异步社区依托于人民邮电出版社20余年的IT专业优质出版资源和编辑策划团队，打造传统出版与电子出版和自出版结合、纸质书与电子书结合、传统印刷与POD按需印刷结合的出版平台，提供最新技术资讯，为作者和读者打造交流互动的平台。

社区里都有什么？

购买图书

我们出版的图书涵盖主流IT技术，在编程语言、Web技术、数据科学等领域有众多经典畅销图书。社区现已上线图书1000余种，电子书400多种，部分新书实现纸书、电子书同步出版。我们还会定期发布新书书讯。

下载资源

社区内提供随书附赠的资源，如书中的案例或程序源代码。

另外，社区还提供了大量的免费电子书，只要注册成为社区用户就可以免费下载。

与作译者互动

很多图书的作译者已经入驻社区，您可以关注他们，咨询技术问题；可以阅读不断更新的技术文章，听作译者和编辑畅聊好书背后有趣的故事；还可以参与社区的作者访谈栏目，向您关注的作者提出采访题目。

灵活优惠的购书

您可以方便地下单购买纸质图书或电子图书，纸质图书直接从人民邮电出版社书库发货，电子书提供多种阅读格式。

对于重磅新书，社区提供预售和新书首发服务，用户可以第一时间买到心仪的新书。

用户帐户中的积分可以用于购书优惠。100积分=1元，购买图书时，在里填入可使用的积分数值，即可扣减相应金额。

特别优惠

购买本电子书的读者专享异步社区优惠券。 使用方法：注册成为社区用户，在下单购书时输入“57AWG”，然后点击“使用优惠码”，即可享受电子书8折优惠（本优惠券只可使用一次）。

纸电图书组合购买

社区独家提供纸质图书和电子书组合购买方式，价格优惠，一次购买，多种阅读选择。

社区里还可以做什么？

提交勘误

您可以在图书页面下方提交勘误，每条勘误被确认后可以获得100积分。热心勘误的读者还有机会参与书稿的审校和翻译工作。

写作

社区提供基于Markdown的写作环境，喜欢写作的您可以在此一试身手，在社区里分享您的技术心得和读书体会，更可以体验自出版的乐趣，轻松实现出版的梦想。

如果成为社区认证作译者，还可以享受异步社区提供的作者专享特色服务。

会议活动早知道

您可以掌握IT圈的技术会议资讯，更有机会免费获赠大会门票。

加入异步

扫描任意二维码都能找到我们：

异步社区

微信订阅号

微信服务号

官方微博

QQ群：436746675

社区网址：www.epubit.com.cn

官方微信：异步社区

官方微博：@人邮异步社区，@人民邮电出版社-信息技术分社

投稿&咨询：contact@epubit.com.cn

目    录

版权信息

版权声明

内容提要

作者简介

审稿人简介

前言

第1章 探索和转换数据

1.1 TensorFlow的主要数据结构——张量

1.1.1 张量的属性——阶、形状和类型

1．张量的阶

2．张量的形状

3．张量的数据类型

1.1.2 创建新的张量

1．从numpy数组到TensorFlow张量和从TensorFlow张量到numpy数组

2．有用的方法

1.1.3 动手工作——与TensorFlow交互

1.2 处理计算工作流——TensorFlow的数据流图

1.2.1 建立计算图

1.2.2 数据供给

1.2.3 变量

1.2.4 保存数据流图

1．计算图序列化语言——协议缓存

2．数据流图构建示例

1.3 运行我们的程序——会话

1.4 基本张量方法

1.4.1 简单矩阵运算

1．约简

2．张量分割

1.4.2 序列

1.4.3 张量形状变换

1.4.4 数据流结构和结果可视化——TensorBoard

1．TensorBoard怎样工作

2．与TensorBoard的GUI交互

1.5 从磁盘读取信息

1.5.1 列表格式——CSV

1.5.2 读取图像数据

1.5.3 加载和处理图像

1.5.4 读取标准TensorFlow格式

1.6 小结

第2章 聚类

2.1 从数据中学习——无监督学习

2.2 聚类的概念

2.3 k均值

2.3.1 k均值的机制

2.3.2 算法迭代判据

2.3.3 k均值算法拆解

2.3.4 k均值的优缺点

2.4 k最近邻

2.4.1 k最近邻算法的机制

2.4.2 k-nn的优点和缺点

2.5 有用的库和使用示例

2.5.1 matplotlib绘图库

2.5.2 scikit-learn数据集模块

2.5.3 人工数据集类型

1．块状数据集

2．环形数据集

2.6 例1——对人工数据集的k均值聚类

2.6.1 数据集描述和加载

2.6.2 模型架构

2.6.3 损失函数描述和优化循环

2.6.4 停止条件

2.6.5 结果描述

2.6.6 每次迭代中的质心变化

2.6.7 完整源代码

2.6.8 k均值用于环状数据集

2.7 例2——对人工数据集使用最近邻算法

2.7.1 数据集生成

2.7.2 模型结构

2.7.3 损失函数描述

2.7.4 停止条件

2.7.5 结果描述

2.7.6 完整源代码

2.8 小结

第3章 线性回归

3.1 单变量线性模型方程

3.2 选择损失函数

3.3 最小化损失函数

3.3.1 最小方差的全局最小值

3.3.2 迭代方法：梯度下降

3.4 示例部分

3.4.1 TensorFlow中的优化方法——训练模块

3.4.2 tf.train.Optimizer类

3.4.3 其他Optimizer实例类型

3.5 例1——单变量线性回归

3.5.1 数据集描述

3.5.2 模型结构

3.5.3 损失函数描述和Optimizer

3.5.4 停止条件

3.5.5 结果描述

3.5.6 完整源代码

3.6 例2——多变量线性回归

3.6.1 有用的库和方法

3.6.2 Pandas库

3.6.3 数据集描述

3.6.4 模型结构

3.6.5 损失函数和Optimizer

3.6.6 停止条件

3.6.7 结果描述

3.6.8 完整源代码

3.7 小结

第4章 逻辑回归

4.1 问题描述

4.2 Logistic函数的逆函数——Logit函数

4.2.1 伯努利分布

4.2.2 联系函数

4.2.3 Logit函数

4.2.4 对数几率函数的逆函数——Logistic函数

1．Logistic函数作为线性模型的泛化

2．Logistic函数的属性

3．损失函数

4.2.5 多类分类应用——Softmax回归

1．损失函数

2．为迭代方法进行数据正则化

3．一位有效输出表示

4.3 例1——单变量逻辑回归

4.3.1 有用的库和方法

4.3.2 数据集描述和加载

1．CHDAGE数据集

2．CHDAGE数据集格式

3．数据集加载和预处理实现

4.3.3 模型结构

4.3.4 损失函数描述和优化器循环

4.3.5 停止条件

4.3.6 结果描述

4.3.7 完整源代码

4.3.8 图像化表示

4.4 例2——基于skflow单变量逻辑回归

4.4.1 有用的库和方法

4.4.2 数据集描述

4.4.3 模型结构

4.4.4 结果描述

4.4.5 完整源代码

4.5 小结

第5章 简单的前向神经网络

5.1 基本概念

5.1.1 人工神经元

1．最简单的人工神经元——感知器

2．感知器算法

5.1.2 神经网络层

1．神经网络激活函数

2．梯度和反向传播算法

3．最小化损失函数：梯度下降

4．神经网络的选择-分类vs回归

5.1.3 有用的库和方法

1．TensorFlow激活函数

2．TensorFlow中损失优化方法

3．Sklearn预处理函数

5.2 例1——非线性模拟数据回归

5.2.1 数据集描述和加载

5.2.2 数据集预处理

5.2.3 模型结构——损失函数描述

5.2.4 损失函数优化器

5.2.5 准确度和收敛测试

5.2.6 完整源代码

5.2.7 结果描述

5.3 例2——通过非线性回归，对汽车燃料效率建模

5.3.1 数据集描述和加载

5.3.2 数据预处理

5.3.3 模型架构

5.3.4 准确度测试

5.3.5 结果描述

5.3.6 完整源代码

5.4 例3——多类分类：葡萄酒分类

5.4.1 数据集描述和加载

5.4.2 数据集预处理

5.4.3 模型架构

5.4.4 损失函数描述

5.4.5 损失函数优化器

5.4.6 收敛性测试

5.4.7 结果描述

5.4.8 完整源代码

5.5 小结

第6章 卷积神经网络

6.1 卷积神经网络的起源

6.1.1 卷积初探

1．连续卷积

2．离散卷积

3．核与卷积

4．卷积操作的解释

5．在TensorFlow中使用卷积

6．其他的卷积操作

7．示例代码——对灰度图像采用卷积操作

8．卷积核对结果的影响

6.1.2 降采样操作——池化

1．降采样层的属性

2．不变性

3．降采样层的性能

4．在TensorFlow中使用池化操作

5．其他的池化操作

6．示例代码

6.1.3 提高效率——dropout操作

1．在TensorFlow中应用dropout

2．代码示例

6.1.4 卷积类型层构建办法

1．卷积层

2．降采样层

6.2 例1——MNIST数字分类

6.2.1 数据集描述和加载

6.2.2 数据预处理

6.2.3 模型结构

6.2.4 损失函数描述

6.2.5 损失函数优化器

6.2.6 准确性测试

6.2.7 结果描述

6.2.8 完整源代码

6.3 例2——CIFAR10数据集的图像分类

6.3.1 数据集描述和加载

6.3.2 数据集预处理

6.3.3 模型结构

6.3.4 损失函数描述和优化器

6.3.5 训练和准确性测试

6.3.6 结果描述

6.3.7 完整源代码

6.4 小结

第7章 循环神经网络和LSTM

7.1 循环神经网络

7.1.1 梯度爆炸和梯度消失

7.1.2 LSTM神经网络

1．门操作——一种基本组件

2．操作步骤

7.1.3 其他RNN结构

7.1.4 TensorFlow LSTM有用的类和方法

1．tf.nn.rnn_cell.BasicLSTMCell类

2．MultiRNNCell（RNNCell）类

3．learn.ops.split_squeeze(dim, num_split, tensor_in)

7.2 例1——能量消耗、单变量时间序列数据预测

7.2.1 数据集描述和加载

7.2.2 数据预处理

7.2.3 模型结构

7.2.4 损失函数描述

7.2.5 收敛检测

7.2.6 结果描述

7.2.7 完整源代码

7.3 例2——创作巴赫风格的曲目

7.3.1 字符级模型

7.3.2 字符串序列和概率表示

7.3.3 使用字符对音乐编码——ABC音乐格式

1．ABC格式的构成

2．哥德堡变奏曲

7.3.4 有用的库和方法

1．保存和恢复变量和模型

2．加载和保存数据的伪代码

7.3.5 数据集描述和加载

7.3.6 网络训练

7.3.7 数据集预处理

1．字典定义

2．模型架构

7.3.8 损失函数描述

7.3.9 停止条件

7.3.10 结果描述

7.3.11 完整源代码

7.4 小结

第8章 深度神经网络

8.1 深度神经网络的定义

8.2 深度网络结构的历史变迁

8.2.1 LeNet 5

8.2.2 Alexnet

8.2.3 VGG模型

8.2.4 第一代Inception模型

8.2.5 第二代Inception模型

8.2.6 第三代Inception模型

8.2.7 残差网络（ResNet）

8.2.8 其他的深度神经网络结构

8.3 例子——VGG艺术风格转移

8.3.1 有用的库和方法

8.3.2 数据集描述和加载

8.3.3 数据集预处理

8.3.4 模型结构

8.3.5 损失函数

1．内容损失函数

2．损失函数优化循环

8.3.6 收敛性测试

8.3.7 程序执行

8.3.8 完整源代码

8.4 小结

第9章 规模化运行模型——GPU和服务

9.1 TensorFlow中的GPU支持

9.2 打印可用资源和设备参数

9.2.1 计算能力查询

9.2.2 选择CPU用于计算

9.2.3 设备名称

9.3 例1——将一个操作指派给GPU

9.4 例2——并行计算Pi的数值

9.4.1 实现方法

9.4.2 源代码

9.5 分布式TensorFlow

9.5.1 分布式计算组件

1．作业

2．任务

3．服务器

4．综合回顾

9.5.2 创建TensorFlow集群

1．ClusterSpec定义格式

2．创建tf.train.Sever

9.5.3 集群操作——发送计算方法到任务

9.5.4 分布式编码结构示例

9.6 例3——分布式Pi计算

9.6.1 服务器端脚本

9.6.2 客户端脚本

9.7 例4——在集群上运行分布式模型

9.8 小结

第10章 库的安装和其他技巧

10.1 Linux安装

10.1.1 安装要求

10.1.2 Ubuntu安装准备（安装操作的前期操作）

2．测试安装是否成功

4．Virtualenv安装方法

5．环境测试

6．Docker安装方法

10.1.4 Linux下从源码安装TensorFlow

1．安装Git代码版本管理器

2．安装Bazel构建工具

4．安装CUDA系统包

6．配置TensorFlow的构建环境

7．构建TensorFlow

10.2 Windows安装

10.2.1 经典的Docker工具箱方法

10.2.2 安装步骤

1．下载Docker工具箱安装文件

2．创建Docker机器

10.3 MacOS X安装

10.4 小结

欢迎来到异步社区！

返回总目录

版权信息

书名：TensorFlow机器学习项目实战

ISBN：978-7-115-46362-3

本书由人民邮电出版社发行数字版。版权所有，侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用，未经授权，不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟，与我们共同保护知识产权。

如果购买者有侵权行为，我们可能对该用户实施包括但不限于关闭该帐号等维权措施，并可能追究法律责任。

•　著　　　　［阿根廷］ Rodolfo Bonnin

译　　　　姚鹏鹏

责任编辑　陈冀康

•　人民邮电出版社出版发行　　北京市丰台区成寿寺路11号

邮编　100164 　电子邮件　315@ptpress.com.cn

网址　http://www.ptpress.com.cn

•　读者服务热线：(010)81055410

反盗版热线：(010)81055315

版权声明

Copyright ©2016 Packt Publishing. First published in the English language under the title Building Machine Learning Projects with TensorFlow.

All rights reserved.

本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可，对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有，侵权必究。

内容提要

TensorFlow是Google所主导的机器学习框架，也是机器学习领域研究和应用的热门对象。

本书主要介绍如何使用TensorFlow库实现各种各样的模型，旨在降低学习门槛，并为读者解决问题提供详细的方法和指导。全书共10章，分别介绍了TensorFlow基础知识、聚类、线性回归、逻辑回归、不同的神经网络、规模化运行模型以及库的应用技巧。

本书适合想要学习和了解 TensorFlow 和机器学习的读者阅读参考。如果读者具备一定的C++和Python的经验，将能够更加轻松地阅读和学习本书。

作者简介

Rodolfo Bonnin是一名系统工程师，同时也是阿根廷国立理工大学的博士生。他还在德国斯图加特大学进修过并行编程和图像理解的研究生课程。

他从2005年开始研究高性能计算，并在2008年开始研究和实现卷积神经网络，编写过一个同时支持CPU和GPU的神经网络前馈部分。最近，他一直在进行使用神经网络进行欺诈模式检测的工作，目前正在使用ML技术进行信号分类。

感谢我的妻子和孩子们，尤其感谢他们在我写这本书时表现出的耐心。感谢本书的审稿人，他们让这项工作更专业化。感谢Marcos Boaglio，他安装调试了设备，以使我能完成这本书。

审稿人简介

Niko Gamulin是CloudMondo的高级软件工程师，CloudMondo是美国的一家创业公司，在那里他开发并实现了系统的预测行为模型。他曾开发过深度学习模型，用于满足各种应用。2015年他从卢布尔雅那大学获得电气工程博士学位。他的研究集中在创建流失预测的机器学习模型。

我要感谢我最棒的女儿Agata，她激励我更多地了解、学习这一过程。同时也要感谢Ana，她是世界上最好的妻子。

前言

近年来，机器学习已经从科学和理论专家的技术资产转变为IT领域大多数大型企业日常运营中的常见主题。

这种现象开始于可用数据量的爆炸：从2005年到2011年，出现了多种廉价的数据捕获设备（具有集成GPS、数百万像素相机和重力传感器的手机）以及普及的新型高维数据捕获装置（3D LIDAR和光学系统，IOT设备的爆炸等），它们使得访问前所未有的大量信息成为了可能。

此外，在硬件领域，摩尔定律的尽头已经近在咫尺，于是促使大量并行设备的开发，这让用于训练同一模型的数据能够成倍增长。

硬件和数据可用性方面的进步使研究人员能够重新审视先驱者基于视觉的神经网络架构（卷积神经网络等）的工作，将它们用于许多新的问题。这都归功于具备普遍可用性的数据以及强悍的计算能力。

为了解决这些新的问题，机器学习的从业者，创建了许多优秀的机器学习包，如Keras、Scikyt-learn、Theano、Caffe和Torch。它们每个都拥有一个特定的愿景来定义、训练和执行机器学习模型。

2015 年 11 月 9 日，Google公司进入了机器学习领域，决定开源自己的机器学习框架TensorFlow，Google内部许多项目都以此为基础。首次发布的是0.5版本，这与其他版本相比有一些缺点，这些在后面讨论。不能运行分布式模型就是其中很突出的一个缺点。

于是，这个小故事带我们来到了今天，TensorFlow成为了该领域开发人员的主要竞争对手之一。因为使用它的项目数量增加，对于任何数据科学的从业者来说，它作为一个工具箱的重要性正在逐步提高。

在本书中，我们将使用TensorFlow库实现各种各样的模型，旨在降低读者的学习门槛，并为解决问题提供详细的方法。

本书包含哪些内容

第1章，探索和转换数据，帮助读者理解TensorFlow应用程序的主要组件和其包含的主要的数据探索方法。

第2章，聚类，告诉你怎样定义相似性标准，并将数据元素分组为不同的类。

第3章，线性回归，帮助读者定义第一个数学模型来解释不同的现象。

第4章，逻辑回归，是用非常强大而简单的数学函数建模非线性现象的第一步。

第5章，简单的前向神经网络，帮助你理解主要组件和神经网络的机制。

第6章，卷积神经网络，解释了最近重新发现的一组特殊网络的功能和实际应用。

第7章，循环神经网络和LSTM，详细地解释了这个对时序数据非常有用的框架。

第8章，深度神经网络，提供混合类型神经网络的最新发展的概述。

第9章，规模化运行模型——GPU和服务，解释怎样通过将工作划分为协调单元来解决
更复杂的问题。

第10章，库的安装和其他技巧，涵盖在Linux、Windows和Mac架构上安装TensorFlow的流程，并向你介绍一些有用的代码技巧，可以简化日常任务。

读这本书你需要什么

软件需求（包括版本）

硬件规格

操作系统需求

TensorFlow 1.0，Jupyter Notebook

任何x86电脑

Ubuntu Linux 16.04

本书目标读者

本书面向希望机器学习任务的结果更快、更高效的数据分析师、数据科学家和研究人员。对于那些想要寻找一个用TensorFlow进行复杂数值计算的清晰指南的人来说，他们会发现本书非常有用。本书也适用于想要在各种场景中应用TensorFlow的开发人员。本书期望读者有一些C++和Python的经验。

约定

在本书中，你会发现一些不同的文本样式，用以区别不同种类的信息。下面举例说明。正文中的代码段、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter句柄如下所示：“我们可以通过使用include指令包括其他上下文。”

代码段的格式如下：

>>> import tensorflow as tf
>>> tens1 = tf.constant([[[1,2],[2,3]],[[3,4],[5,6]]])
>>> print sess.run(tens1)[1,1,0]
5

当我们想提醒你注意代码块的特定部分时，相关的行或部分会加粗显示：

>>> import tensorflow as tf
>>> tens1 = tf.constant([[[1,2],[2,3]],[[3,4],[5,6]]])
>>> print sess.run(tens1)[1,1,0]
5

命令行输入写成如下的形式：

# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
/etc/asterisk/cdr_mysql.conf

新术语和重要词语以粗体显示。例如，你在屏幕上看到的字，在菜单栏或对话框中，出现在文本中，如下所示：“单击下一步按钮可以转到下一个界面。”

这个图标表示警告或需要特别注意的内容。

这个图标表示提示或者技巧。

读者反馈

欢迎提出反馈。如果你对本书有任何想法，喜欢它什么，不喜欢它什么，请让我们知道。要写出真正对大家有帮助的书，了解读者的反馈很重要。

一般的反馈，请发送电子邮件至feedback@packtpub.com，并在邮件主题中包含书名。

如果你有某个主题的专业知识，并且有兴趣写成或帮助促成一本书，请参考我们的作者指南http://www.packtpub.com/authors。

客户支持

现在，你是一位自豪的Packt图书的读者，我们会尽全力帮你充分利用你手中的书。

下载示例代码

你可以从异步社区（www.epubit.com.cn）下载本书最新版的示例代码。

当你下载完之后，请确保你使用如下最新版的软件来解压该文件：

- WinRAR / 7-Zip for Windows；

- Zipeg / iZip / UnRarX for Mac；

- 7-Zip / PeaZip for Linux。

本书的代码同时维护于GitHub上https://github.com/PacktPublishing/Building-Machine- Learning-Projects-with-TensorFlow。我们在https://github.com/PacktPublishing/上维护了很多数据的代码和视频。欢迎查看！

勘误

虽然我们已尽力确保本书内容正确，但错误仍旧在所难免。如果你在我们的书中发现错误，不管是文本还是代码，希望能告知我们，我们不胜感激。这样做可以减少其他读者的困扰，帮助我们改进本书的后续版本。如果你发现任何错误，请访问http://www.packtpub.com/ submit-errata提交，选择你的书，单击勘误表提交表单的链接，并输入详细说明。勘误一经核实，你的提交将被接受，此勘误将被上传到本公司网站或添加到现有勘误表。从http://www.packtpub.com/support选择书名就可以查看现有的勘误表。

如果想要查看已经提交的勘误表，请登录https://www.packtpub.com/books/content/support 并在搜索框中输入书名，你所查找的信息就会显示在Errata部分了。

侵权行为

互联网上的盗版是所有媒体都要面对的问题。Packt非常重视保护版权和许可证。如果你发现我们的作品在互联网上被非法复制，不管以什么形式，都请立即为我们提供位置地址或网站名称，以便我们可以寻求补救。

请把可疑盗版材料的链接发到copyright@packtpub.com。非常感谢你帮助我们保护作者，以及保护我们给你带来有价值内容的能力。

问题

如果你对本书内容存有疑问，不管是哪个方面，都可以通过questions@packtpub.com联系我们，我们将尽最大努力来解决。

第1章　探索和转换数据

TensorFlow是一个开源软件库，用于使用数据流图进行数值计算。图中的节点表示数学运算，而图边表示在它们之间传递的多维数据数组（张量，tensor）。

该库包括各种功能，使你能够实现和探索用于图像和文本处理的前沿卷积神经网络（CNN）和循环神经网络（RNN）架构。由于复杂计算以图形的形式表示，TensorFlow可以用作一个框架，使你能够轻松开发自己的模型，并在机器学习领域中使用它们。

它还能够在最不同的环境中运行，从CPU到移动处理器，包括高度并行的GPU计算，并且新的服务架构能够运行所有命名选项的非常复杂的混合，见表1-1。

表1-1　TensorFlow

向量（Tensor）

操作（Operation）

图（Graph）

运行时(CPU、GPU、移动设备等)

1.1　TensorFlow的主要数据结构——张量

TensorFlow基于张量数据管理。张量是数学领域的概念，并且被开发为向量和矩阵的线性代数项的泛化。

具体到TensorFlow中，一个张量就是一个张量类的实例，是绑定了相关运算的一个特定类型的多维数组。

1.1.1　张量的属性——阶、形状和类型

之前已经介绍过，TensorFlow使用张量数据结构来表征所有的数据。所有的张量都有一个静态的类型和动态的维数。所以你能够实时地改变一个张量的内部结构。

张量的另一个属性就是只有张量类型的对象才能在计算图的节点中传递。

我们开始来讨论张量的其他属性（从此处开始，我们所有说的张量都是TensorFlow中的张量对象）。

1．张量的阶

张量的阶（rank）表征了张量的维度，但是跟矩阵的秩（rank）不一样。它表示张量的维度的质量。

阶为1的张量等价于向量，阶为2的向量等价于矩阵。对于一个阶为2的张量，通过t[i, j]就能获取它的每个元素。对于一个阶为3的张量，需要通过t[i, j, k]进行寻址，以此类推，见表1-2。

表1-2　张量的阶

阶

数学实体

代码示例

0

Scalar

scalar = 1000

1

Vector

vector = [2, 8, 3]

2

Matrix

matrix = [[4, 2, 1], [5, 3, 2], [5, 5, 6]]

3

3-tensor

tensor = [[[4], [3], [2]], [[6], [100], [4]], [[5], [1], [4]]]

n

n-tensor

…

在下面这个例子中，我们创建了一个张量，并获取其元素：

>>> import tensorflow as tf
>>> tens1 = tf.constant([[[1,2],[2,3]],[[3,4],[5,6]]])
>>> print sess.run(tens1)[1,1,0]
5

这个张量的阶是3，因为该张量包含的矩阵中的每个元素，都是一个向量。

2．张量的形状

TensorFlow文档使用三个术语来描述张量的维度：阶（rank），形状（shape）和维数（dimension number）。表1-3展示了它们彼此之间的关系。

表1-3　三者之间的关系

阶

形状

维数

例子

0

[]

0-D

4

1

[D0]

1-D

[2]

2

[D0,D1]

2-D

[6,2]

3

[D0,D1,D2]

3-D

[7,3,2]

n

[D0,D1, …, Dn-1]

n-D

形为[D0, D1,　… Dn-1]的张量

图1-1的例子中，我们创建了一个三阶张量，并打印出它的形状。

图1-1　三阶张量

3．张量的数据类型

除了维度，张量还有一个确定的数据类型。你可以把表1-4中的任意一个类型指派给向量。

表1-4　张量数据类型

数据类型

Python 类型

描述

DT_FLOAT

tf.float32

32位浮点型

DT_DOUBLE

tf.float64

64位浮点型

DT_INT8

tf.int8

8位有符号整型

DT_INT16

tf.int16

16位有符号整型

DT_INT32

tf.int32

32位有符号整型

DT_INT64

tf.int64

64位有符号整型

DT_UINT8

tf.uint8

8位无符号整型

DT_STRING

tf.string

可变长度的字节数组，每一个张量元素都是一个字节数组

DT_BOOL

tf.bool

布尔型

1.1.2　创建新的张量

我们既可以创建我们自己的张量，也可以从著名的Python库numpy中继承。下面的例子中，我们创建了一些numpy数组，并对它们进行了简单的数学操作：

import tensorflow as tf
import numpy as np
x = tf.constant(np.random.rand(32).astype(np.float32))
y= tf.constant ([1,2,3])

1．从numpy数组到TensorFlow张量和从TensorFlow张量到numpy数组

TensorFlow与numpy是可互操作的，通常调用eval()函数会返回numpy对象。该函数可以用作标准数值工具。

我们一定要注意张量对象只是一个操作结果的符号化句柄，所以它并不持有该操作的结果。因此，我们必须使用eval()方法来获得实际的值。该方法等价于Session.run(tesnsor_ to_eval)。

本例中，我们会创建两个numpy数组，并将它们转化成张量：

import tensorflow as tf #we import tensorflow
import numpy as np   #we import numpy
sess = tf.Session() #start a new Session Object
x_data = np.array([[1.,2.,3.],
[3.,2.,6.]]) # 2x3 matrix
x = tf.convert_to_tensor(x_data, dtype=tf.float32) #Finally, we create the
#tensor, starting from the fload 3x matrix

2．有用的方法

tf.convert_to_tensor``：该方法将Python对象转化为tensor对象。它的输入可以是tensor对象、numpy数组、Python列表和Python标量。

1.1.3　动手工作——与TensorFlow交互

与大多数Python的模块一样，TensorFlow允许使用Python的交互式控制台。

在图1-2中，我们调用Python解释器（在终端对话框输入Python调用），并创建一个常量类型的张量。然后再次调用它，Python解释器显示张量的形状和类型。

图1-2　在Python解释器中运行TensorFlow

我们还可以使用IPython解释器，这将允许我们采用一种类似于笔记本式工具的格式，例如Jupyter，如图1-3所示。

图1-3　IPython对话框

以交互方式运行TensorFlow会话时，我们最好使用InteractiveSession对象。

与正常的tf.Session类不同，tf.InteractiveSession类将其自身设置为构建时的默认会话。因此，当你尝试评估张量或运行一个操作时，不必传递一个Session对象来指示它所引用的会话。

1.2　处理计算工作流——TensorFlow的数据流图

TensorFlow的数据流图（data flow graph）符号化地表示了模型的计算是如何工作的。

简单地说，数据流图是完整的TensorFlow计算，如图1-4所示。图中的节点（node）表示操作（operation），而边（edge）表示各操作之间流通的数据。

图1-4　在TensorBoard中一个简单的数据流图示例

通常，节点实现数学运算，同时也表示数据或变量的供给（feed），或输出结果。

边描述节点之间的输入/输出关系。这些数据边缘专门传输张量。节点被分配给计算设备，并且一旦其输入边缘上的所有张量都到位，则开始异步地并行执行。

所有的操作（operation）都拥有一个名字，可以表示一个抽象的计算（例如，矩阵求逆或者相乘）。

1.2.1　建立计算图

计算图（computation graph）通常并不需要直接构建Graph类对象，而是由用户在创建张量（tensor）和操作（operation）的时候自动创建的。TensorFlow张量构造函数（如tf.constant()）将向默认的计算图添加必要的元素。其他TensorFlow操作也同样如此。

例如，语句“c = tf.matmul（a，b）”创建一个MatMul类型的操作，它接受张量a和b作为输入，并产生c作为输出。

有用的操作对象方法如下：

- tf.Operation.type：返回操作的类型（例如，MatMul）；

- tf.Operation.inputs：返回表示操作的输入张量对象列表；

- tf.Graph.get_operations()：返回计算图中的操作列表；

- tf.Graph.version：返回计算图的版本信息。

1.2.2　数据供给

TensorFlow还提供了一种将张量直接注入到图内任何操作中的数据供给（feed）机制。

feed用张量值临时替换操作的输出。将feed的数据作为参数传入run函数。feed只在调用它的方法内有效。最常见的用例是，通过使用tf.placeholder()创建特定的feed操作的方法。

1.2.3　变量

在大多数计算中，会多次执行计算图。大多数张量的生存周期不会超过单次执行周期。然而，变量是一种特殊的操作，它返回一个持久的、可变的张量的句柄，存活于多次计算图执行之中。对于TensorFlow的机器学习应用，模型的参数通常存储在变量中，并且在运行模型的训练阶段被更新。

初始化变量时，只需以张量作为参数，传入Variable对象构造函数中。

在下面例子中，我们用长度为1000的零数组初始化一个变量：

b = tf.Variable(tf.zeros([1000]))

1.2.4　保存数据流图

数据流图是使用Google的协议缓存（protocol buffers）编写的，因此可以用各种语言读取。

1．计算图序列化语言——协议缓存

协议缓存（protocol buffers）是一种语言中立、平台中立、可扩展的结构化数据序列化机制。首先定义数据结构，然后使用特定的代码（各种编程语言都可以）读写它。

（1）有用的方法

tf.Graph.as_graph_def(from_version=None, add_shapes=False)：返回一个序列化的计算图表示GraphDef 。

（2）参数

- from_version：设置该参数，返回的GraphDef增加了自本版本后定义的节点。

- add_shapes：如果为真，那么每个节点都增加张量形状。

2．数据流图构建示例

本例中，我们将构建一个非常简单的数据流图，并观察生成的protobuffer文件：

import tensorflow as tf
g = tf.Graph()
with g.as_default():
import tensorflow as tf
sess = tf.Session()
W_m = tf.Variable(tf.zeros([10, 5]))
x_v = tf.placeholder(tf.float32, [None, 10])
result = tf.matmul(x_v, W_m)
print g.as_graph_def()

生成的protobuffer（简略后）如下：

node {
name: "zeros"
op: "Const"
attr {
key: "dtype"
value {
type: DT_FLOAT
}
}
attr {
key: "value"
value {
tensor {
dtype: DT_FLOAT
tensor_shape {
dim {
size: 10
}
dim {
size: 5
}
}
float_val: 0.0
}
}
}
}
...
node {
name: "MatMul"
op: "MatMul"
input: "Placeholder"
input: "Variable/read"
attr {
key: "T"
value {
type: DT_FLOAT
}
}
...
}
versions {
producer: 8
}

1.3　运行我们的程序——会话

客户端程序通过创建会话（Session）与TensorFlow系统交互。Session对象是运行环境的表示。Session对象开始为空，当程序员创建不同的操作和张量时，它们将被自动添加到Session，直到Run方法被调用，才开始运算。

Run方法输入是需要计算的操作，以及一组可选的张量，用来代替图中某些节点的输出。

如果我们调用这个方法，并且有命名操作所依赖的操作，Session对象将执行所有这些操作，然后继续执行命名操作。

用以下简单的代码可以创建一个会话：

s = tf.Session()

1.4　基本张量方法

本节中，我们将探讨TensorFlow支持的一些基本方法。它们将有利于初步数据探索和为并行计算做准备。

1.4.1　简单矩阵运算

TensorFlow支持许多常见的矩阵运算，如转置、乘法、获取行列式和逆。

下面简单演示一下怎样使用这些函数。

In [1]: import tensorflow as tf
In [2]: sess = tf.InteractiveSession()
In [3]: x = tf.constant([[2, 5, 3, -5],
...:                   [0, 3,-2, 5],
...:                   [4, 3, 5, 3],
...:                   [6, 1, 4, 0]])

In [4]: y = tf.constant([[4, -7, 4, -3, 4],
...:                   [6, 4,-7, 4, 7],
...:                   [2, 3, 2, 1, 4],
...:                   [1, 5, 5, 5, 2]])
In [5]: floatx = tf.constant([[2., 5., 3., -5.],
...:                        [0., 3.,-2., 5.],
...:                        [4., 3., 5., 3.],
...:                        [6., 1., 4., 0.]])
In [6]: tf.transpose(x).eval() # Transpose matrix
Out[6]:
array([[ 2, 0, 4, 6],
[ 5, 3, 3, 1],
[ 3, -2, 5, 4],
[-5, 5, 3, 0]], dtype=int32)

In [7]: tf.matmul(x, y).eval() # Matrix multiplication
Out[7]:
array([[ 39, -10, -46, -8, 45],
[ 19, 31, 0, 35, 23],
[ 47, 14, 20, 20, 63],
[ 38, -26, 25, -10, 47]], dtype=int32)

In [8]: tf.matrix_determinant(floatx).eval() # Matrix determinant
Out[8]: 818.0

In [9]: tf.matrix_inverse(floatx).eval() # Matrix inverse
Out[9]:
array([[-0.00855745, 0.10513446, -0.18948655, 0.29584351],
[ 0.12958434,   0.12224938, 0.01222495, -0.05134474],
[-0.01955992, -0.18826403, 0.28117359, -0.18092911],
[-0.08557458,   0.05134474, 0.10513448, -0.0415648 ]], dtype=float32)

In [10]: tf.matrix_solve(floatx, [[1],[1],[1],[1]]).eval() # Solve Matrix
system
Out[10]:
array([[ 0.20293398],
[ 0.21271393],
[-0.10757945],
[ 0.02933985]], dtype=float32)

1．约简

约简（reduction）是一种跨维度张量操作，计算结果比原张量缩减一个维度。

支持的操作包括（具有相同的参数）product，minimum，maximum，mean，all，any和accumulate_n。

In [1]: import tensorflow as tf

In [2]: sess = tf.InteractiveSession()
In [3]: x = tf.constant([[1, 2, 3],
...:                   [3, 2, 1],
...:                   [-1,-2,-3]])
In [4]:

In [4]: boolean_tensor = tf.constant([[True, False, True],
...:                   [False, False, True],
...:                   [True, False, False]])

In [5]: tf.reduce_prod(x, reduction_indices=1).eval() # reduce prod
Out[5]: array([ 6, 6, -6], dtype=int32)

In [6]: tf.reduce_min(x, reduction_indices=1).eval() # reduce min
Out[6]: array([ 1, 1, -3], dtype=int32)

In [7]: tf.reduce_max(x, reduction_indices=1).eval() # reduce max
Out[7]: array([ 3, 3, -1], dtype=int32)

In [8]: tf.reduce_mean(x, reduction_indices=1).eval() # reduce mean
Out[8]: array([ 2, 2, -2], dtype=int32)

In [9]: tf.reduce_all(boolean_tensor, reduction_indices=1).eval() # reduce
all
Out[9]: array([False, False, False], dtype=bool)

In [10]: tf.reduce_any(boolean_tensor, reduction_indices=1).eval() # reduce
any
Out[10]: array([ True, True, True], dtype=bool)

2．张量分割

张量分割是张量一个维度减小的过程，并且所得到的元素由索引行确定，如图1-5所示。如果索引行中的某些元素重复，则对拥有重复索引的索引进行操作。

图1-5　张量分割的解释

索引数组大小应该与索引数组的维度0的大小相同，并且它们必须增加1。

In [1]: import tensorflow as tf
In [2]: sess = tf.InteractiveSession()
In [3]: seg_ids = tf.constant([0,1,1,2,2]); # Group indexes : 0|1,2|3,4
In [4]: tens1 = tf.constant([[2, 5, 3, -5],
...:                       [0, 3,-2, 5],
...:                       [4, 3, 5, 3],
...:                       [6, 1, 4, 0],
...:                       [6, 1, 4, 0]]) # A sample constant matrix

In [5]: tf.segment_sum(tens1, seg_ids).eval() # Sum segmentation
Out[5]:
array([[ 2, 5, 3, -5],
[ 4, 6, 3, 8],
[12, 2, 8, 0]], dtype=int32)

In [6]: tf.segment_prod(tens1, seg_ids).eval() # Product segmentation
Out[6]:
array([[ 2, 5, 3, -5],
[ 0,  9, -10, 15],
[ 36, 1, 16,  0]], dtype=int32)

In [7]: tf.segment_min(tens1, seg_ids).eval() # minimun value goes to group
Out[7]:
array([[ 2, 5, 3, -5],
[ 0, 3, -2, 3],
[ 6, 1, 4, 0]], dtype=int32)

In [8]: tf.segment_max(tens1, seg_ids).eval() # maximum value goes to group
Out[8]:
array([[ 2, 5, 3, -5],
[ 4, 3, 5, 5],
[ 6, 1, 4, 0]], dtype=int32)

In [9]: tf.segment_mean(tens1, seg_ids).eval() # mean value goes to group
Out[9]:
array([[ 2, 5, 3, -5],
[ 2, 3, 1, 4],
[ 6, 1, 4, 0]], dtype=int32)

1.4.2　序列

序列实用程序包括诸如argmin和argmax（显示维度的最小和最大值），listdiff（显示列表之间的交集的补码），where（显示张量上的真实值的索引）和unique（在列表上去除重复的元素）。

In [1]: import tensorflow as tf
In [2]: sess = tf.InteractiveSession()
In [3]: x = tf.constant([[2, 5, 3, -5],
...:                   [0, 3,-2, 5],
...:                   [4, 3, 5, 3],
...:                   [6, 1, 4, 0]])
In [4]: listx = tf.constant([1,2,3,4,5,6,7,8])
In [5]: listy = tf.constant([4,5,8,9])

In [6]:

In [6]: boolx = tf.constant([[True,False], [False,True]])

In [7]: tf.argmin(x, 1).eval() # Position of the maximum value of columns
Out[7]: array([3, 2, 1, 3])

In [8]: tf.argmax(x, 1).eval() # Position of the minimum value of rows
Out[8]: array([1, 3, 2, 0])

In [9]: tf.listdiff(listx, listy)[0].eval() # List differences
Out[9]: array([1, 2, 3, 6, 7], dtype=int32)

In [10]: tf.where(boolx).eval() # Show true values
Out[10]:
array([[0, 0],
[1, 1]])

In [11]: tf.unique(listx)[0].eval() # Unique values in list
Out[11]: array([1, 2, 3, 4, 5, 6, 7, 8], dtype=int32)

1.4.3　张量形状变换

这些类型的函数与矩阵形状相关。它们用于调整不匹配的数据结构，并快速获取数据张量的信息。这对于运行时决定处理策略时很有用。

在下面的例子中，我们将从二阶张量开始，并打印一些关于它的信息。然后我们将探讨修改矩阵的操作（添加或删除维度），例如squeeze和expand_dims。

In [1]: import tensorflow as tf
In [2]: sess = tf.InteractiveSession()
In [3]: x = tf.constant([[2, 5, 3, -5],
...:                   [0, 3,-2, 5],
...:                   [4, 3, 5, 3],
...:                   [6, 1, 4, 0]])

In [4]: tf.shape(x).eval() # Shape of the tensor
Out[4]: array([4, 4], dtype=int32)

In [5]: tf.size(x).eval() # size of the tensor
Out[5]: 16

In [6]: tf.rank(x).eval() # rank of the tensor
Out[6]: 2

In [7]: tf.reshape(x, [8, 2]).eval() # converting to a 10x2 matrix
Out[7]:
array([[ 2, 5],
[ 3, -5],
[ 0, 3],
[-2, 5],
[ 4, 3],
[ 5, 3],
[ 6, 1],
[ 4, 0]], dtype=int32)

In [8]: tf.squeeze(x).eval() # squeezing
Out[8]:
array([[ 2, 5, 3, -5],
[ 0, 3, -2, 5],
[ 4, 3, 5, 3],
[ 6, 1, 4, 0]], dtype=int32)

In [9]: tf.expand_dims(x,1).eval() #Expanding dims
Out[9]:
array([[[ 2, 5, 3, -5]],
[[ 0, 3, -2, 5]],
[[ 4, 3,  5, 3]],
[[ 6, 1,  4, 0]]], dtype=int32)

对于一个很大的数据集，我们可能并不需要全部的信息。这时候我们可以使用张量的切片（slicing）和连接（joining）。这样我们能够节省下很多不必要的内存开销。

在以下示例中，我们将提取矩阵切片、拆分、添加填充（add padding），以及打包（pack）和解包（unpack）行。

In [1]: import tensorflow as tf
In [2]: sess = tf.InteractiveSession()
In [3]: t_matrix = tf.constant([[1,2,3],
...:                          [4,5,6],
...:                          [7,8,9]])
In [4]: t_array = tf.constant([1,2,3,4,9,8,6,5])
In [5]: t_array2= tf.constant([2,3,4,5,6,7,8,9])

In [6]: tf.slice(t_matrix, [1, 1], [2,2]).eval() # cutting an slice
Out[6]:
array([[5, 6],
[8, 9]], dtype=int32)

In [7]: tf.split(0, 2, t_array) # splitting the array in two
Out[7]:
[<tf.Tensor 'split:0' shape=(4,) dtype=int32>,
<tf.Tensor 'split:1' shape=(4,) dtype=int32>]

In [8]: tf.tile([1,2],[3]).eval() # tiling this little tensor 3 times
Out[8]: array([1, 2, 1, 2, 1, 2], dtype=int32)

In [9]: tf.pad(t_matrix, [[0,1],[2,1]]).eval() # padding
Out[9]:
array([[0, 0, 1, 2, 3, 0],
[0, 0, 4, 5, 6, 0],
[0, 0, 7, 8, 9, 0],
[0, 0, 0, 0, 0, 0]], dtype=int32)

In [10]: tf.concat(0, [t_array, t_array2]).eval() #concatenating list
Out[10]: array([1, 2, 3, 4, 9, 8, 6, 5, 2, 3, 4, 5, 6, 7, 8, 9],
dtype=int32)

In [11]: tf.pack([t_array, t_array2]).eval() # packing
Out[11]:
array([[1, 2, 3, 4, 9, 8, 6, 5],
[2, 3, 4, 5, 6, 7, 8, 9]], dtype=int32)

In [12]: sess.run(tf.unpack(t_matrix)) # Unpacking, we need the run method
to view the tensors
Out[12]:
[array([1, 2, 3], dtype=int32),
array([4, 5, 6], dtype=int32),
array([7, 8, 9], dtype=int32)]

In [13]: tf.reverse(t_matrix, [False,True]).eval() # Reverse matrix
Out[13]:
array([[3, 2, 1],
[6, 5, 4],
[9, 8, 7]], dtype=int32)

1.4.4　数据流结构和结果可视化——TensorBoard

可视化汇总信息是任何一个数据科学家工具箱的重要组成部分。

TensorBoard是一个软件实用程序，它支持数据流图的图形表示，还可以在仪表板（dashboard）上解释结果。它的数据通常来自日志数据，如图1-6所示。

可以将图的所有张量和操作信息写入日志。TensorBoard会从日志信息中分析这些数据，并将其以图形的方式呈现给用户。TensorBoard可以在会话运行的过程中查看。

想要启动TensorBoard，可使用如图1-7所示的命令行。

1．TensorBoard怎样工作

我们构建的每个计算图，TensorFlow都有一个实时记录机制，以便保存模型拥有的几乎所有信息。

图1-6　TensorBoard GUI

图1-7　启动TensorBoard

然而，模型构建器必须考虑到有些信息的维度甚至高达上百维，以便稍后用作分析工具。

为了保存所有必需的信息，TensorFlow API使用数据输出对象，称为汇总（summaries）。

这些汇总将结果写入TensorFlow事件文件（event file），该文件收集会话运行期间生成的所有必需数据。在图1-8中，我们将直接在生成的事件日志目录上运行TensorBoard。

图1-8　运行TensorBoard

（1）添加汇总节点

TensorFlow会话中的汇总，都是由SummaryWriter对象写入。此方法的函数名为：

tf.train.SummaryWriter. init (logdir, graph_def=None)

该方法将在参数的路径中创建一个SummaryWriter和一个事件文件。

SummaryWriter的构造函数将在logdir目录中创建一个新的事件文件。当你调用以下方法（add_summary()、add_session_log()、add_event()或add_graph()）的时候，该事件文件将会添加一个Event类型的协议缓存。

如果将graph_def协议缓存传递给构造函数，它将会被添加到事件文件（这等效于稍后调用add_graph()）。

当运行TensorBoard时，它将从文件中读取图形定义，并以图形方式显示，以便与其交互。

首先，创建想要汇总数据的TensorFlow图，并决定要在哪些节点进行汇总操作。

TensorFlow中的操作只有在你运行它们的时候，或者另一个操作依赖于它的输出的时候才运行。我们刚刚创建的汇总节点是图形的外设：当前运行的任何操作都不取决于它们。因此，要生成汇总，我们需要运行所有这些汇总节点。手动管理它们将是乏味的，因此使用tf.merge_all_summaries将它们组合到单个操作中，生成所有汇总数据。

然后，你可以运行合并的汇总操作，这将在特定的步骤生成一个序列化的汇总协议缓存（protobuf）对象与所有的汇总数据。最后，要将此汇总数据写入磁盘，将Summary protobuf传递给tf.train.SummaryWriter。

SummaryWriter在其构造函数中使用logdir，这个logdir是非常重要的，所有的事件都会被写入这个目录。此外，SummaryWriter可以选择在其构造函数中使用GraphDef。如果SummaryWriter接收到一个GraphDef，那么TensorBoard也将可视化你的数据流图。

现在你已经改动了图表并拥有一个SummaryWriter，那就可以开始运行网络了！如果需要，你可以每一步运行合并汇总，并记录大量训练数据。但是这样容易造成数据过多，所以比较合适的方法是每n步执行合并汇总操作。

（2）常见汇总操作

这是不同的汇总类型的列表，和它们构造函数的参数：

- tf.scalar_summary (tag, values, collections=None, name=None)

- tf.image_summary (tag, tensor, max_images=3, collections=None, name=None)

- tf.histogram_summary (tag, values, collections=None, name=None)

（3）特殊汇总功能

这些是特殊汇总函数，用于合并不同操作的值：

- tf.merge_summary (inputs, collections=None, name=None)

- tf.merge_all_summaries (key='summaries')

最后，作为可视化的一个辅助，TensorBoard中使用各种图标表示不同的操作或者变量。这里有一个节点符号表，见表1-5。

表1-5　节点符号表

图标

含义

高级节点表示名称域。双击展开高级节点

彼此未连接的编号节点的顺序

彼此连接的编号节点的顺序

单个操作节点

常数节点

汇总节点

表示操作之间的数据流

表示操作之间的控制依赖

表示输出操作节点可以突变输入张量

2．与TensorBoard的GUI交互

通过平移和缩放使用计算图。单击并拖动即可平移，使用滚动手势可以进行缩放。双击一个节点，或单击其“+”按钮，可展开代表一组操作的名称域，如图1-9所示。为了在缩放和平移时轻松跟踪当前视点，右下角有一个缩略图。

图1-9　TensorBoard的节点展开

要关闭打开的节点，可再次双击它，或单击它的“−”按钮。你也可以单击一次以选择节点，节点将变成更暗的颜色，详细信息和连接的节点将出现在TensorBoard右上角的信息卡中。

选择对于 high-degree 节点的理解也很有帮助，选择任意节点，则与它的其余连接的相应节点也会被选中，这使得在进行例如查看哪一个节点是否已保存等操作时非常容易。

单击详情卡片中的一个节点名称时会选中该节点，必要的话，视角会自动平移以使该节点可见。

最后，使用图例上方的颜色菜单，你可以给你的图表选择两个颜色配色方案。默认的结构视图下，当两个high-level节点颜色一样时，其会以相同的彩虹色彩出现，而结构唯一的节点颜色是灰色。还有一个视图则展示了不同的操作运行于什么设备之上。名称域被恰当地根据其中的操作节点的设备来按比例着色。

1.5　从磁盘读取信息

TensorFlow可以读取许多常用的标准格式，包括大家耳熟能详的CSV、图像文件（JPG和PNG格式）和标准TensorFlow格式。

1.5.1　列表格式——CSV

为了读CSV格式，TensorFlow构建了自己的方法。与其他库（如pandas）相比，读取一个简单的CSV文件的过程有点复杂。

读取CSV文件需要几个准备步骤。首先，我们必须创建一个文件名队列对象与我们将使用的文件列表，然后创建一个TextLineReader。使用此行读取器，剩余的操作将是解码CSV列，并将其保存于张量。如果我们想将同质数据混合在一起，可以使用pack方法。

鸢尾花（Iris）数据集或Fisher's Iris数据集是分类问题的一个常用的基准。它是由Ronald Fisher 1936年在论文《The use of multiple measurements in taxonomic problems》中引入的多变量数据集。Ronald Fisher用其做线性判别分析的示例。

数据集包括3种鸢尾（分别是山鸢尾、变色鸢尾和维吉尼亚鸢尾），各50个样本。在每个样品中测量4个特征：萼片和花瓣的长度和宽度，以厘米计。基于这4个特征的组合，Fisher开发了线性判别模型来区分物种（你可以在本书的代码包中获取此数据集的.csv文件）。

为了读取CSV文件，首先下载下来，并将其放在Python可执行文件的相同目录中。

在下面的代码示例中，我们将从知名的Iris数据库中读取并打印前5个记录。

import tensorflow as tf
sess = tf.Session()
filename_queue = tf.train.string_input_producer(
tf.train.match_filenames_once("./*.csv"),
shuffle=True)
reader = tf.TextLineReader(skip_header_lines=1)
key, value = reader.read(filename_queue)
record_defaults = [[0.], [0.], [0.], [0.], [""]]
col1, col2, col3, col4, col5 = tf.decode_csv(value,
record_defaults=record_defaults) # Convert CSV records to tensors. Each
#column maps to one tensor.
features = tf.pack([col1, col2, col3, col4])

tf.initialize_all_variables().run(session=sess)
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(coord=coord, sess=sess)

for iteration in range(0, 5):
example = sess.run([features])
print(example)
coord.request_stop()
coord.join(threads)

输出如图1-10所示。

图1-10　输出

1.5.2　读取图像数据

TensorFlow能够以图像格式导入数据，这对于面向图像的模型非常有用，因为这些模型的输入往往是图像。TensorFlow支持的图像格式是JPG和PNG，程序内部以uint8张量表示，每个图像通道一个二维张量，如图1-11所示。

图1-11　样例图形

1.5.3　加载和处理图像

本例中，我们会加载一个样例图像，并对其进行一些处理，最后将其保存。

import tensorflow as tf
sess = tf.Session()
filename_queue =
tf.train.string_input_producer(tf.train.match_filenames_once("./blue_jay.jp
g"))
reader = tf.WholeFileReader()
key, value = reader.read(filename_queue)
image=tf.image.decode_jpeg(value)
flipImageUpDown=tf.image.encode_jpeg(tf.image.flip_up_down(image))
flipImageLeftRight=tf.image.encode_jpeg(tf.image.flip_left_right(image))
tf.initialize_all_variables().run(session=sess)
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(coord=coord, sess=sess)
example = sess.run(flipImageLeftRight)
print example
file=open ("flippedUpDown.jpg", "wb+")
file.write (flipImageUpDown.eval(session=sess))
file.close()
file=open ("flippedLeftRight.jpg", "wb+")
file.write (flipImageLeftRight.eval(session=sess))
file.close()

打印示例行将逐行汇总显示图像中RGB值，如图1-12所示。

图1-12　图像中的RGB值

最终的图片如图1-13所示。

图1-13　原始图像和转变后的图像对比（向上翻转和向左翻转）

1.5.4　读取标准TensorFlow格式

另一种方法是将任意数据转换为 TensorFlow 官方格式。这种方法将简化混合或者匹配数据集与网络结构。

你可以编写一个小程序，获取你的数据，将它填充到一个示例协议缓存，序列化协议缓存成一个字符串，然后使用tf.python_io.TFRecordWriter类将该字符串写入一个TFRecords文件。

要读取 TFRecords 的文件，可以使用 tf.TFRecordReader 的 tf.parse_single_example 解析器。parse_single_example操作将示例协议缓存解析为张量。

1.6　小结

在本章中，我们学习了TensorFlow的主要数据结构和简单操作，并简要介绍了计算图的各个部分。

这些操作将是后续章节的基础。数据科学家可以根据当前数据的整体特性，决定使用简单的模型，或者是使用更复杂的工具。

在下一章中，我们将开始构建和运行计算图，并将使用本章中介绍的一些方法解决问题。

第2章　聚类

在本章中，我们将会使用上一章节学习的数据转化操作，并且使用聚类（clustering）技术，从给定的数据中发掘有趣的模式，将数据分组。

在处理过程中，我们将会用到两个新的工具，scikit-learn和matplotlib库。其中scikit-learn库能够生成特定结构的数据集，而matplotlib库可以对数据和模型作图。

本章包含如下主题：

- 理解聚类的概念和原理，并将其与分类（classification）比较；

- 使用scikit-learn库生成数据集，并使用matplotlib库生成专业化的图表；

- 实现k均值聚类算法；

- 实现k最近邻算法，并将其与k均值比较。

2.1　从数据中学习——无监督学习

本章中，我们会学习两个无监督学习（unsupervised learning）的例子。

无监督学习可以从给定的数据集中找到感兴趣的模式（pattern）。无监督学习，一般不给出模式的相关信息。所以，无监督学习算法需要自动探索信息是怎样组成的，并识别数据中的不同结构。

2.2　聚类的概念

对于没有标签（unlabeled）的数据，我们首先能做的，就是寻找具有相同特征的数据，将它们分配到相同的组。

为此，数据集可以分成任意数量的段（segment），其中每个段都可以用它的成员的质量中心（质心，centroid）来替代表示。

为了将不同的成员分配到相同的组中，我们需要定义一下，怎样表示不同元素之间的距离（distance）。在定义距离之后，我们可以说，相对于其他的质心，每个类成员都更靠近自己所在类的质心。

在图2-1中，我们可以看到典型的聚类算法的结果和聚类中心的表示。

图2-1　简单聚类算法的输出

2.3　k均值

k均值（k-means）是一种常见的聚类算法，并且比较容易实现。它非常直接，一般是用于分析数据的第一步。经过该处理，我们能够得到一些关于数据集的先验知识。

2.3.1　k均值的机制

k均值算法试图将给定的数据分割为k个不相交的组（group）或者簇（cluster），每个簇的指标就是该组所有成员的均值。这个点通常称为质心，指具有相同名称的算术实体，并且可以被表示为任意维度中的向量。

k均值是一个朴素的方法，因为它在不知道簇的数量的前提下，寻找合适的质心。

要想知道多少个簇能够比较好地表示给定数据，一个常用的方法是Elbow方法。

2.3.2　算法迭代判据

此方法的判据和目标是最小化簇成员到包含该成员的簇的实际质心的平方距离的总和。这也称为惯性最小化，k均值的损失函数如下：

2.3.3　k均值算法拆解

k均值算法的机制可以由图2-2所示的流程图展示。

图2-2　k均值流程简单流程图

算法流程可以简化如下。

① 对于未分类的样本，首先随机以k个元素作为起始质心。为了简洁，也可以简化该算法，取元素列表中的前k个元素作为质心。

② 计算每个样本跟质心的距离，并将该样本分配给距离它最近的质心所属的簇，重新计算分配好后的质心。从图中，你能看到质心在像真正的质心移动。

③ 在质心改变之后，它们的位移将引起各个距离改变，因此需要重新分配各个样本。

④ 在停止条件满足之前，不断重复第二步和第三步。

可以使用不同类型的停止条件。

- 我们可以选择一个比较大的迭代次数N，这样我们可能会遭遇一些冗余的计算。也可以选择N小一些，但是在这种情况下，如果本身质点不稳定，收敛过程慢，那么我们得到的结果就不能让人信服。这种停止条件也可以用作最后的手段，以防我们有一个非常漫长的迭代过程。

- 另外还有一种停止条件。如果已经没有元素从一个类转移到另一个类，意味着迭代的结束。

k均值示意图如图2-3所示。

图2-3　k均值示意图

2.3.4　k均值的优缺点

该方法的优点：

- 扩展性很好（大部分的计算都可以并行计算）；

- 应用范围广。

但是简单是有成本的（没有银弹规则）：

- 它需要先验知识（可能的聚类的数量应该预先知道）；

- 异常值影响质心的结果，因为算法并没有办法剔除异常值；

- 由于我们假设该图是凸的和各向同性的，所以对于非圆状的簇，该算法表现不是很好。

2.4　k最近邻

k最近邻（k-nearest neighbors，简写为k-nn）是一种简单而经典的聚类方法。该方法只需查看周围点的类别信息，并且假设所有的样本都属于已知的类别。其流程图如图2-4所示，示意图如图2-5所示。

图2-4　k最近邻流程图

图2-5　k-nn示意图

2.4.1　k最近邻算法的机制

k-nn有多种实现方式，本章中我们会使用半监督（Semi Supervised）方式。我们有一个训练集，它已经有了类别信息，随后我们猜测给定样本所该具有的类别信息。

在图2-4中，我们可以看到算法的步骤分解。可以通过以下步骤概括：

① 设定训练集的数据类别信息。

② 然后读取下一个要分类的样本，并计算从新样本到训练集的每个样本的欧几里得距离。

③ 同欧几里得距离上最近的样本来确定新样本的类别信息。确定的方式就是最近的k个样本的投票。

④ 重复以上步骤，直到所有测试样本都确定了类别。

2.4.2　k-nn的优点和缺点

该方法的优点如下。

- 简单；无需调整参数。

- 无训练过程；我们只需要更多地训练样本来改进模型。

缺点是：计算成本高（必须计算训练集点和每个新样本之间的所有距离）。

2.5　有用的库和使用示例

我们将会在本部分讨论有用的库。

2.5.1　matplotlib绘图库

数据绘图是数据科学学科的一个组成部分。为此，我们需要一个非常强大的框架，以能够绘制我们的结果。对于这个任务，matplotlib中没有通用框架来解决，我们使用matplotlib库。

在matplotlib官方网站（http://matplotlib.org/），matplotlib的定义是：

“matplotlib是一个Python下的2D绘图库，能够在跨平台的交互环境中产生各种硬拷贝格式的印刷级的图形。”

下面进行合成数据绘图示例。本例中，我们将会产生一个包含100个随机数的列表，用matplotlib绘制这100个数据，并生成图像文件。

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
with tf.Session() as sess:
fig, ax = plt.subplots()
ax.plot(tf.random_normal([100]).eval(), tf.random_normal([100]
).eval(),'o')
ax.set_title('Sample random plot for TensorFlow')
plt.savefig("result.png")

绘图结果如图2-6所示。

图2-6　TensorFlow生成数据的matplotlib绘图

查看matplotlib绘图模块的更多的用法，请参考：http://matplotlib.org/。

2.5.2　scikit-learn数据集模块

TensorFlow当前还没有集成易于生成人工数据集的方法。因此，我们会使用sklearn库来帮忙。

在scikit-learn的官方网站（https://scikit-learn.org/stable/），scikit-learn的介绍是：

“scikit-learn（原名 scikits.learn）是一个基于Python编程语言的开源机器学习库。它具有各种分类，回归和聚类算法，并可以与Python数值和科学计算库NumPy以及SciPy互操作。”

本例中，我们会使用scikit-learn的数据集模块，来生成和加载各种人工数据集。

查看更多的scikit-learn数据集模块的说明和解释，请参考：http://scikit-learn.org/stable/datasets/。

2.5.3　人工数据集类型

1．块状数据集

我们将会使用如下的人工数据集，如图2-7所示。

图2-7　块状、环状和月牙状数据集

这个数据集用于测试聚类算法。该数据集是特意设计，专门用于测试聚类算法的准确程度的。

以下是生成块状数据集的方法：

sklearn.datasets.make_blobs(n_samples=100, n_features=2, centers=3,
cluster_std=1.0, center_box=(-10.0, 10.0), shuffle=True,
random_state=None)

n_samples是数据的数目，n_features是特征数据的列的数目（维度），centers是类的中心，cluster_std是标准差，center_box是随机生成数据中心时中心的边界，shuffle指是否打乱样品，random_state是随机种子。

2．环形数据集

这是一个圆环套圆环的数据集。这是一个非线性可分的问题，所以需要使用非线性模型。对于这种数据集，简单的算法如k-means就不能处理。

以下是生成环形数据集的方法：

sklearn.datasets.make_circles（n_samples = 100，shuffle = True，noise = None，
random_state = None，factor = 0.8）

n_samples是数据的数目，shuffle指数据是否发乱，noise是添加到圆形数据集上的随机噪声数据，random_state是随机种子，factor是环形数据间的比例因子。

2.6　例1——对人工数据集的k均值聚类

2.6.1　数据集描述和加载

本章中，我们使用的是人工数据集，它的生成方式如下：

centers = [(-2, -2), (-2, 1.5), (1.5, -2), (2, 1.5)]
data, features = make_blobs (n_samples=200, centers=centers, n_features =
2, cluster_std=0.8, shuffle=False, random_state=42)

通过matplotlib绘制该数据集：

ax.scatter(np.asarray(data).transpose()[0],
np.asarray(data).transpose()[1], marker = 'o', s = 250)
plt.plot()

最终结果如图2-8所示。

图2-8　块状数据集散点图

2.6.2　模型架构

Points变量用来存放数据集的点的坐标，centroids变量用于存放每个组质心的坐标，clustering_assignments变量用来存放为每个数据元素分配的类的索引。

比如说，clustering_assignments[2]=1表示数据data[2]的数据点被分配到1类，而1类的质心坐标通过访问centroids[1]得到。

points=tf.Variable(data)
cluster_assignments = tf.Variable(tf.zeros([N], dtype=tf.int64))
centroids = tf.Variable(tf.slice(points.initialized_value(), [0,0], [K,2]))

然后，我们可以通过matplotlib库绘制出质心的位置：

fig, ax = plt.subplots()
ax.scatter(np.asarray(centers).transpose()[0],
np.asarray(centers).transpose()[1], marker = 'o', s = 250)
plt.show()

图2-9是绘制结果。

图2-9　中心点的位置

2.6.3　损失函数描述和优化循环

然后我们对所有的质心做N次复制，对每个样本点做K次复制，这样样本点和质心的形状都是N×K×2，我们就可以计算每一个样本到每一个质心点之间在所有维度上的距离。

rep_centroids = tf.reshape(tf.tile(centroids, [N, 1]), [N, K, 2])
rep_points = tf.reshape(tf.tile(points, [1, K]), [N, K, 2])
sum_squares = tf.reduce_sum(tf.square(rep_points - rep_centroids),
reduction_indices=2)

然后我们对所有维度求和，得到和最小的那个索引（这个索引就是每个点所属的新的类）：

best_centroids = tf.argmin(sum_squares, 1)

centroids也会在每个迭代之后由bucket_mean函数更新，具体请查看完整的源码。

2.6.4　停止条件

本例的停止条件是所有的质心不再变化：

did_assignments_change = tf.reduce_any(tf.not_equal(best_centroids,
cluster_assignments))

此处，我们使用control_dependencies来控制是否更新质心：

with tf.control_dependencies([did_assignments_change]):
do_updates = tf.group(
centroids.assign(means),
cluster_assignments.assign(best_centroids))

2.6.5　结果描述

当程序结束的时候，我们得到如图2-10所示的输出。

图2-10　k均值运行结果

2.6.6　每次迭代中的质心变化

图2-10是一次迭代之中的质心变化，而图2-11是不同迭代中质心的变化。

图2-11　不同迭代中的质心变化

2.6.7　完整源代码

完整源代码如下：

import tensorflow as tf
import numpy as np
import time

import matplotlib
import matplotlib.pyplot as plt

from sklearn.datasets.samples_generator import make_blobs
from sklearn.datasets.samples_generator import make_circles

DATA_TYPE = 'blobs'

# Number of clusters, if we choose circles, only 2 will be enough
if (DATA_TYPE == 'circle'):
K=2
else:
K=4

# Maximum number of iterations, if the conditions are not met
MAX_ITERS = 1000

start = time.time()

centers = [(-2, -2), (-2, 1.5), (1.5, -2), (2, 1.5)]
if (DATA_TYPE == 'circle'):
data, features = make_circles(n_samples=200, shuffle=True, noise= 0.01, factor=0.4)
else:
data, features = make_blobs (n_samples=200, centers=centers, n_features = 2, cluster_std=0.8, shuffle=False, random_state=42)

fig, ax = plt.subplots()
ax.scatter(np.asarray(centers).transpose()[0], np.asarray(centers).transpose()[1], marker = 'o', s = 250)
plt.show()

fig, ax = plt.subplots()
if (DATA_TYPE == 'blobs'):
ax.scatter(np.asarray(centers).transpose()[0], np.asarray(centers).transpose()[1], marker = 'o', s = 250)
ax.scatter(data.transpose()[0], data.transpose()[1], marker = 'o', s = 100, c = features, cmap=plt.cm.coolwarm )
plt.plot()

points=tf.Variable(data)
cluster_assignments = tf.Variable(tf.zeros([N], dtype=tf.int64))

centroids = tf.Variable(tf.slice(points.initialized_value(), [0,0], [K,2]))

sess = tf.Session()
sess.run(tf.initialize_all_variables())

sess.run(centroids)

rep_centroids = tf.reshape(tf.tile(centroids, [N, 1]), [N, K, 2])
rep_points = tf.reshape(tf.tile(points, [1, K]), [N, K, 2])
sum_squares = tf.reduce_sum(tf.square(rep_points - rep_centroids),
reduction_indices=2)
best_centroids = tf.argmin(sum_squares, 1)
did_assignments_change = tf.reduce_any(tf.not_equal(best_centroids, cluster_assignments))

def bucket_mean(data, bucket_ids, num_buckets):
total = tf.unsorted_segment_sum(data, bucket_ids, num_buckets)
count = tf.unsorted_segment_sum(tf.ones_like(data), bucket_ids, num_buckets)
return total / count
means = bucket_mean(points, best_centroids, K)
with tf.control_dependencies([did_assignments_change]):
do_updates = tf.group(
centroids.assign(means),
cluster_assignments.assign(best_centroids))

changed = True
iters = 0
fig, ax = plt.subplots()
if (DATA_TYPE == 'blobs'):
colourindexes=[2,1,4,3]
else:
colourindexes=[2,1]
while changed and iters < MAX_ITERS:
fig, ax = plt.subplots()
iters += 1
[changed, _] = sess.run([did_assignments_change, do_updates])
[centers, assignments] = sess.run([centroids, cluster_assignments])
ax.scatter(sess.run(points).transpose()[0], sess.run(points).transpose()[1],
marker = 'o', s = 200, c = assignments, cmap=plt.cm.coolwarm )
ax.scatter(centers[:,0],centers[:,1], marker = '^', s = 550, c = colourindexes, cmap=plt.cm.plasma)
ax.set_title('Iteration ' + str(iters))
plt.savefig("kmeans" + str(iters) +".png")
ax.scatter(sess.run(points).transpose()[0], sess.run(points).transpose()[1], marker = 'o', s = 200, c = assignments, cmap=plt.cm.coolwarm )
plt.show()
end = time.time()
print ("Found in %.2f seconds" % (end-start)), iters, "iterations"
print "Centroids:"
print centers
print "Cluster assignments:", assignments

2.6.8　k均值用于环状数据集

对于环状数据，我们知道，每个类不能由简单的均值替代。如图2-12所示，两个圆共享同一个质心，或者两个质心非常接近，这样我们就不能预测出一个清晰的输出。

对于本数据集，我们只用两个类，以确保该算法的缺点能够被读者理解，如图2-13所示。

图2-12　环状数据集

图2-13　k均值算法应用于环状数据集

正如我们所见，初始质心向样本最集中的地方漂移，能够将数据线性分割。这是这个算法最大的限制之一。要处理非线性可分的数据集，我们可以尝试其他的分类方法，如基于密度的抗噪聚类方法（density based spatial clustering of applications with noise，DBSCAN），但这已经超过本书的范围。

2.7　例2——对人工数据集使用最近邻算法

本例中，我们使用的数据集是上面的算法（k均值）不能正确分类的问题。

2.7.1　数据集生成

本例中的数据集跟上例一样，还是两类，但是这次我们会加大数据的噪声（从0.01到0.12）：

data, features = make_circles(n_samples=N, shuffle=True, noise=0.12, factor=0.4)

训练集的数据绘制如图2-14所示。

图2-14　生成环状数据集

2.7.2　模型结构

这里面的变量除了存放原始数据，还有一个列表，用来存放为每个测试数据预测的测试结果。

2.7.3　损失函数描述

在聚类问题中，我们使用的距离描述跟前面一样，都是欧几里得距离。在每一个聚类的循环中，计算测试点与每个存在的训练点之间的距离，找到最接近那个训练点的索引，使用该索引寻找最近邻的点的类：

distances = tf.reduce_sum(tf.square(tf.sub(i , tr_data)),reduction_indices=1)
neighbor = tf.arg_min(distances,0)

2.7.4　停止条件

本例中，当处理完测试集中所有的样本后，整个过程结束。

2.7.5　结果描述

图2-15是k-nn算法的结果。我们可以看到，至少在有限数据集的范围内，该算法比无重叠、块状优化、k均值方法的效果好。

图2-15　k-nn对环状数据集的结果

2.7.6　完整源代码

完整源代码如下：

import tensorflow as tf
import numpy as np
import time

import matplotlib
import matplotlib.pyplot as plt

from sklearn.datasets.samples_generator import make_circles

N=210
K=2
# Maximum number of iterations, if the conditions are not met
MAX_ITERS = 1000
cut=int(N*0.7)

start = time.time()

data, features = make_circles(n_samples=N, shuffle=True, noise= 0.12, factor=0.4)
tr_data, tr_features= data[:cut], features[:cut]
te_data,te_features=data[cut:], features[cut:]

fig, ax = plt.subplots()
ax.scatter(tr_data.transpose()[0], tr_data.transpose()[1], marker = 'o', s = 100, c = tr_features, cmap=plt.cm.coolwarm )
plt.plot()

points=tf.Variable(data)
cluster_assignments = tf.Variable(tf.zeros([N], dtype=tf.int64))

sess = tf.Session()
sess.run(tf.initialize_all_variables())

test=[]

for i, j in zip(te_data, te_features):
distances = tf.reduce_sum(tf.square(tf.sub(i , tr_data)),reduction_indices=1)
neighbor = tf.arg_min(distances,0)
test.append(tr_features[sess.run(neighbor)])
print test
fig, ax = plt.subplots()
ax.scatter(te_data.transpose()[0], te_data.transpose()[1], marker = 'o', s = 100,
c = test, cmap=plt.cm.coolwarm )
plt.plot()

end = time.time()
print ("Found in %.2f seconds" % (end-start))
print "Cluster assignments:", test

2.8　小结

在本章中，我们学习了一些目前可以实现的简单模型，并在细节部分做到了尽可能的详细。

从现在起，我们掌握了如何生成人工数据集，这使我们能够快速测试一个模型对于不同数据集的有效性，从而评估它们的优点和缺点。

此外，我们已经实现了第一个迭代方法并进行了收敛性测试。下面的章节中，我们会用类似的方式实现其他模型，但使用更精细和更精确的方法。

在下一章中，我们将使用线性函数解决分类问题，并且第一次使用来自训练集的信息来学习数据特征。这是有监督学习（supervised learning）的目标，而且这对于解决许多现实生活中的问题更有用。

第3章　线性回归

本章中，我们将通过最小化误差和损失函数，用一条线来拟合给定的点，学习机器学习项目的一般流程。

在前面的章节中，我们已经看过几种不同的问题和许多不同的解决方法。它们有一个共同的特点，就是根据已有的带标签的数据，定性地给出新数据的标签。这种问题，在社科领域最常见。

另一类常见的问题需要给出一个方程（提前建模好了的）的确切的数值。比如在物理学领域，我们需要根据温度和湿度的历史数据，预测未来的温度和湿度，我们把这类要得到确切数值的问题称为回归分析（regression analysis）。

具体到线性回归，我们通过线性方程表示输入跟输出量之间的关系。

3.1　单变量线性模型方程

正如之前所说，线性回归，是寻找一条直线，使得所有的点到这条直线的距离总和最短。

这种关系用经典的线性方程可以表示如下：

模型方法有如下的形式：

这里，β_0也称作偏差（bias），是当x为0的时候，方程的值， β_1是建模的那条直线的斜率。变量x通常称作自变量，而y一般称作因变量。有时也分别称作回归量和响应量。

下面生成人工数据集。在本例中，我们会随机生成一个近似采样随机分布，使得β_1=2.0、β_0=0.2，并加入一个噪声，噪声的最大振幅为0.4。

In[]:
#Indicate the matplotlib to show the graphics inline
%matplotlib inline
import matplotlib.pyplot as plt # import matplotlib
import numpy as np # import numpy
trX = np.linspace(-1, 1, 101) # Linear space of 101 and [-1,1]
#Create The y function based on the x axis
trY = 2 * trX + np.random.randn(*trX.shape) * 0.4 + 0.2
plt.figure() # Create a new figure
plt.scatter(trX,trY) #Plot a scatter draw of the random datapoints
# Draw one line with the line function
plt.plot (trX, .2 + 2 * trX)

结果如图3-1所示。

图3-1　带噪声的样本和线性函数

3.2　选择损失函数

跟其他的机器学习方法一样，线性回归要首先选择一个误差函数（error function，又称为损失函数，cost function）。该函数的值，表征模型对于问题的适合程度。

在线性回归中最常用的损失函数是最小方差（least squares）。

要想计算最小方差，我们首先要做的一件事就是怎样计算这个“差”，也就是点到建模直线的距离。所以我们首先定义一个函数来计算每对xn、yn跟建模直线之间的距离。

对于二维回归，我们已知的是多对（X_0,Y_0）、（X_1,Y_1）…（Xn,Yn），我们需要做的就是通过最小化以下函数，找到β_0和β_1的值。

简单点说，这个求和就表示预测值和真实值之间的欧几里得距离的和。

使用这个操作的原因是，平方差的求和能够给我们一个唯一且简单的全局数值。首先是通过相减得到预测值和真实值之间的差距，通过平方获得一个正数值，用于惩罚远离直线的点。

3.3　最小化损失函数

下一步就是选择一个办法最小化损失函数。在微积分中，我们学过，想要获得局部最小值，可以对参数求偏导，并让偏导等于0。这个方法有两个要求，第一是偏导存在，第二最好是凸函数。可以证明最小方差函数满足这两个条件。这对避免局部最小值的问题，非常有帮助。最小方差损失函数如图3-2所示。

图3-2　最小方差损失函数

3.3.1　最小方差的全局最小值

我们通过矩阵的形式来计算最小方差的解：

此处，J是损失函数，参数的解如下：

3.3.2　迭代方法：梯度下降

梯度下降方法是在机器学习领域使用得最多的优化方法。该方法沿着梯度的反方向，寻找损失函数的局部最小值。

对于二维的线性回归，我们先随机定义一个权值θ，作为线性方程的系数，然后使用如下方程，循环迭代更新θ 的值。

该式子的机制很简单。我们从一个初始值开始，沿着方程改变最大方向的反方向移动。α 被称作步长（step），会影响每次迭代移动的大小。

最后一步是跳出迭代的测试。以下两种情况满足其中之一就跳出循环：两次迭代之间的差值大于一个epsilon或者迭代次数达到。

如果方程是非凸的，建议多找几个初始值运用梯度下降法，最后选择损失函数值最低的那个参数作为结果。因为，非凸函数的最小值，可能是局部最小值，最终的结果依赖于初始值。所以推荐选多个初始值。

3.4　示例部分

首先我们讨论有用的库和模块。

3.4.1　TensorFlow中的优化方法——训练模块

训练或者参数优化是机器学习工作流中重要的组成部分。

在TensorFlow中有一个tf.train模块专门来解决这个问题。在该模块中，集成了很多种数据科学家常用的优化策略。这个模块提供的主要对象被称作Optimizer（优化器）。

3.4.2　tf.train.Optimizer类

优化器（Optimizer）类用来计算损失函数的梯度，并将它们应用于同一模型的不同变量。我们最常用的优化方法有梯度下降（gradient descent）、Adam和Adagrad。

要注意，Optimizer这个类本身是不能被初始化的，只能初始化它的子类。

正如我们之前所讨论的，TensorFlow可以通过符号语言定义函数，所以梯度也可以通过符号语言的方式使用，这能够提高结果的准确性并丰富对数据的操作。

使用Optimizer类，我们要遵循以下步骤：

① 为需要优化的参数，创建一个Optimizer（本例中为梯度下降）。

opt = GradientDescentOptimizer(learning_rate= [learning rate])

② 创建一个操作，调用minimize方法，最小化损失函数。

optimization_op = opt.minimize(cost, var_list=[variables list])

minimize函数要遵循以下的格式：

tf.train.Optimizer.minimize(loss, global_step=None, var_list=None,
gate_gradients=1, aggregation_method=None,
colocate_gradients_with_ops=False, name=None)

主要的参数如下。

- loss：存放损失函数每轮迭代后的数值。

- global_step：Optimizer执行一次迭代，加一。

- var_list：需要优化的变量。

事实上，optimizer方法内部调用了compute_gradients( )和apply_gradients( )两个方法。如果你想在使用梯度之前，对梯度进行额外的处理，可以先使用compute_gradients( )方法计算出梯度，然后调用apply_gradients( )方法。如果我们想只使用一步来训练，我们一定需要按照opt_op.run( )的方式执行run方法。

3.4.3　其他Optimizer实例类型

以下是其他Optimizer实例类型。

- tf.train.AdagradOptimizer：自适应梯度Optimizer，学习率随着时间递减；

- tf.train.AdadeltaOptimizer：增强版Adagrad，学习率不再绝对随着时间衰减；

- tf.train.MomentumOptimizer：实现Momentum优化算法；

- TensorFlow还实现了其他的Optimizer，如tf.train.AdamOptimizer, tf.train.FtrlOptimizer, tf.train.RMSPropOptimizer。

3.5　例1——单变量线性回归

我们将用一个例子将之前所学的知识串联起来。本例中，我们首先会创建一个近似线性分布；之后，我们会使用一个线性方程，通过最小化误差函数（本例中为最小方差），来拟合这个分布。

利用训练后的模型，对于一个新的样本，我们能够预测出它的输出。

3.5.1　数据集描述

对于这个样例，我们会通过一个线性方程和一些噪声，生成一个人工数据集：

import TensorFlow as tf
import numpy as np
trX = np.linspace(-1, 1, 101)
trY = 2 * trX + np.random.randn(*trX.shape) * 0.4 + 0.2 # create a y value
which is approximately linear but with some random noise

使用以下代码，我们同时绘制出散点图和理想的线性方程。

import matplotlib.pyplot as plt
plt.scatter(trX,trY)
plt.plot (trX, .2 + 2 * trX)

结果如图3-3所示。

图3-3　带噪声的样本和线性函数

3.5.2　模型结构

1）首先创建一个变量来保存x和y轴的数值。然后我们符号化定义输入x和权值w相乘的操作。

2）生成一些变量并赋予初始值，来启动模型。

In[]:
X = tf.placeholder("float", name="X") # create symbolic variables
Y = tf.placeholder("float", name = "Y")

3）我们为模型先声明一个name_scope。我们能够把这个域（scope）内的变量和操作看作一个同质实体。在这个域内，我们先定义一个方程，用来计算变量x乘上权重（斜率），加上偏差。然后我们定义一个用来存放权重（斜率）和偏差的变量。这些变量在计算过程中不断变化，最后将定义的model的返回值赋给y_model变量。

从下面的TensorBoard中，我们可以看到损失函数（CostFunction）的操作。放大Model区域，我们可以看到乘法操作和加法操作，还可以看到参数变量b0和b1，还有对模型的梯度操作，如图3-4所示。

图3-4　Model域结构图

3.5.3　损失函数描述和Optimizer

在损失函数（cost function）中，我们创建一个域（scope）来包含所有的操作，使用之前创建的y_model来计算y轴值。

with tf.name_scope("CostFunction"):
cost = (tf.pow(Y-y_model, 2)) # use sqr error for cost

定义Optimizer的时候，我们首先初始化一个GradientDescentOptimizer，并将步长设置为0.05，这是一个经验值，有助于收敛。

train_op = tf.train.GradientDescentOptimizer(0.05).minimize(cost)

创建一个会话，并将初始化的变量保存起来，方便我们在TensorBoard上查看。本例中，我们会将每次迭代的最后一个误差结果作为一个标量保存起来。我们也需要将TensorFlow生成的图结构保存，用于以后查看。

sess = tf.Session()
init = tf.initialize_all_variables()
tf.train.write_graph(sess.graph,
'/home/ubuntu/linear','graph.pbtxt')
cost_op = tf.scalar_summary("loss", cost)
merged = tf.merge_all_summaries()
sess.run(init)
writer = tf.train.SummaryWriter('/home/ubuntu/linear',
sess.graph)

在模型训练阶段，我们设置迭代100次，每次我们通过将样本输入模型，进行梯度下降操作。每次迭代之后，绘制出模型曲线，并将最后的误差值存入summary。

In[]:
for i in range(100):
for (x, y) in zip(trX, trY):
sess.run(train_op, feed_dict={X: x, Y: y})
summary_str = sess.run(cost_op, feed_dict={X: x, Y: y})
writer.add_summary(summary_str, i)
b0temp=b.eval(session=sess)
b1temp=w.eval(session=sess)
plt.plot (trX, b0temp + b1temp * trX )

绘图结果如图3-5所示；我们可以看到设定初始值后，模型很快收敛到一个不错的结果。

图3-5　绘图结果

放大CostFunction域，我们能够看到乘方和减操作，如图3-6所示，它们也被写入了summary中。

图3-6　CostFunction域结构图

3.5.4　停止条件

本例的停止条件为执行迭代100次。

3.5.5　结果描述

现在我们来检查参数的结果，打印run的w和b的变量的输出。

printsess.run(w) # Should be around 2
printsess.run(b) #Should be around 0.2
2.09422
0.256044

我们可以图像化地查看我们的数据结果，并绘制出最终直线，如图3-7所示。

plt.scatter(trX,trY)
plt.plot (trX, testb + trX * testw)

图3-7　数据结果和最终直线

现在我们在TensorBoard中查看数据结果。

TensorBoard的启用，需要指定日志目录，执行以下命令：

$ tensorboard --logdir=.

TensorBoard会加载日志目录中的事件和图形文件，并监听6006口。你可以在浏览器中输入“localhost:6006”，然后就能在浏览器中看到类似于图3-8的TensorBoard的仪表盘。

图3-8　TensorBoard的仪表盘

3.5.6　完整源代码

完整源代码如下：

import matplotlib.pyplot as plt # import matplotlib
import numpy as np # import numpy
import tensorflow as tf
import numpy as np

trX = np.linspace(-1, 1, 101) #Create a linear space of 101 points between
1 and 1
trY = 2 * trX + np.random.randn(*trX.shape) * 0.4 + 0.2 #Create The y
function based on the x axis
plt.figure() # Create a new figure
plt.scatter(trX,trY) #Plot a scatter draw of the random datapoints
plt.plot (trX, .2 + 2 * trX) # Draw one line with the line function

get_ipython().magic(u'matplotlib inline')

import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np

trX = np.linspace(-1, 1, 101)
trY = 2 * trX + np.random.randn(*trX.shape) * 0.4 + 0.2 # create a y value
which is approximately linear but with some random noise

plt.scatter(trX,trY)
plt.plot (trX, .2 + 2 * trX)

X = tf.placeholder("float", name="X") # create symbolic variables
Y = tf.placeholder("float", name = "Y")

withtf.name_scope("Model"):

def model(X, w, b):
returntf.mul(X, w) + b # We just define the line as X*w + b0

w = tf.Variable(-1.0, name="b0") # create a shared variable
b = tf.Variable(-2.0, name="b1") # create a shared variable
y_model = model(X, w, b)

withtf.name_scope("CostFunction"):
cost = (tf.pow(Y-y_model, 2)) # use sqr error for cost function

train_op = tf.train.GradientDescentOptimizer(0.05).minimize(cost)

sess = tf.Session()
init = tf.initialize_all_variables()
tf.train.write_graph(sess.graph, '/home/ubuntu/linear','graph.pbtxt')
cost_op = tf.scalar_summary("loss", cost)
merged = tf.merge_all_summaries()
sess.run(init)
writer = tf.train.SummaryWriter('/home/ubuntu/linear', sess.graph)

fori in range(100):
for (x, y) in zip(trX, trY):
sess.run(train_op, feed_dict={X: x, Y: y})
summary_str = sess.run(cost_op, feed_dict={X: x, Y: y})
writer.add_summary(summary_str, i)
b0temp=b.eval(session=sess)
b1temp=w.eval(session=sess)
plt.plot (trX, b0temp + b1temp * trX )

printsess.run(w) # Should be around 2
printsess.run(b) #Should be around 0.2

plt.scatter(trX,trY)
plt.plot (trX, sess.run(b) + trX * sess.run(w))

3.6　例2——多变量线性回归

在本例中，我们处理的问题会超过一个变量。

该数据包含1993个样本，采集的是波士顿郊区的房价。每个样本包括13个变量和该地区的平均地价。

我们用的样本跟原始样本唯一不一样的地方就是去除了一个变量（b），这个变量是按照种族化描述不同的郊区。

除此之外，我们将会选择对我们有用的变量，来建模线性方程。

3.6.1　有用的库和方法

本部分我们会介绍一些有用的库和方法。这些库和方法不属于TensorFlow，我们将在本例以及本书的后续例子中使用。这对我们解决不同的问题会非常有用。

3.6.2　Pandas库

当我们想快速读取常规大小的数据文件的时候，创建读缓存区和其他的机制可能会造成额外的开支。这是我们在现实生活中常见的问题，Pandas库可以来处理这种问题。

Pandas官网（pandas.pydara.org）这样介绍Pandas：

“Pandas是一款开源的，基于BSD协议的Python库，能够提供高性能、易用的数据结构和数据分析工具。”它具有以下特点：

- 能够从CSV文件、文本文件、MS Excel、SQL数据库，甚至是用于科学用途的HDF5格式。

- CSV文件加载能够自动识别列头，支持列的直接寻址。

- 数据结构自动转换为NumPy的多维阵列。

3.6.3　数据集描述

数据集由CSV文件存储，我们用Panda库打开。本数据集包含以下变量。

- CRIM：城镇人均犯罪率。

- ZN：住宅用地超过 25000 sq.ft. 的比例。

- INDUS：城镇非零售商用土地的比例。

- CHAS：Charles河空变量（如果边界是河流，则为1；否则为0）。

- NOX：一氧化氮浓度。

- RM：住宅平均房间数。

- AGE：1940 年之前建成的自用房屋比例。

- DIS：到波士顿5个中心区域的加权距离。

- RAD：辐射性公路的靠近指数。

- TAX：每 10000 美元的全值财产税率。

- PTRATIO：城镇师生比例。

- LSTAT：人口中地位低下者的比例。

- MEDV：自住房的平均房价，以千美元计。

我们可以通过以下的简单代码读取数据，并查看详细数据：

import tensorflow.contrib.learn as skflow
fromsklearn import datasets, metrics, preprocessing
import numpy as np
import pandas as pd

df = pd.read_csv("data/boston.csv", header=0)
printdf.describe()

这将会给出该数据集变量的统计概述，如图3-9所示。前6个结果如下：

CRIM         ZN         INDUS        CHAS         NOX              RM \
Count   506.000000   506.000000   506.000000   506.000000   506.000000
506.000000
mean      3.613524    11.363636    11.136779     0.069170     0.554695
6.284634
std       8.601545    23.322453     6.860353     0.253994     0.115878
0.702617
min       0.006320     0.000000     0.460000     0.000000     0.385000
3.561000
25%       0.082045     0.000000     5.190000     0.000000     0.449000
5.885500
50%       0.256510     0.000000     9.690000     0.000000     0.538000
6.208500
75%       3.677082    12.500000    18.100000     0.000000     0.624000
6.623500
max      88.976200   100.000000    27.740000     1.000000     0.871000
8.780000

图3-9　数据集变量的统计概述

3.6.4　模型结构

本例模型虽然简单，但是它包含我们处理更复杂模型的所有元素。

在如图3-10所示的结构图中，我们能够看到，解决一个机器学习问题的不同模块：模型、损失函数和梯度。TensorFlow有一个非常有用的功能，就是能够自动对模型和方程求导。

图3-10　模型结构图

此处，我们可以找到之前部分w、b的定义和模型线性方程，如图3-11所示。

X = tf.placeholder("float", name="X") # create symbolic variables
Y = tf.placeholder("float", name = "Y")

withtf.name_scope("Model"):
w = tf.Variable(tf.random_normal([2], stddev=0.01), name="b0") # create
a shared variable
b = tf.Variable(tf.random_normal([2], stddev=0.01), name="b1") # create
a shared variable
def model(X, w, b):
returntf.mul(X, w) + b # We just define the line as X*w + b0
y_model = model(X, w, b)

图3-11　模型（Model）域结构图

3.6.5　损失函数和Optimizer

在本例中，我们使用最常见的均方误差作为损失函数，跟上例不同的是，这次是多变量；因此我们使用reduce_mean函数，来平衡多个维度的误差值。其在TensorBoard中的结构，如图3-12所示。

with tf.name_scope("CostFunction"):
cost = tf.reduce_mean(tf.pow(Y-y_model, 2)) # use sqr error for cost
function
train_op = tf.train.AdamOptimizer(0.1).minimize(cost)
for a in range (1,10):
cost1=0.0
for i, j in zip(xvalues, yvalues):
sess.run(train_op, feed_dict={X: i, Y: j})
cost1+=sess.run(cost, feed_dict={X: i, Y: i})/506.00
#writer.add_summary(summary_str, i)
xvalues, yvalues = shuffle (xvalues, yvalues)

图3-12　损失函数域展开

3.6.6　停止条件

本例的停止条件比较简单，只要等待所有的样本执行完特定的次数，就可以结束训练。

3.6.7　结果描述

结果如下：

1580.53295174
[ 2.25225258 1.30112672]
[ 0.80297691 0.22137061]
1512.3965525
[ 4.62365675 2.90244412]
[ 1.16225874 0.28009811]
1495.47174799
[ 6.52791834 4.29297304]
[ 0.824792270.17988272]
...
1684.6247849
[ 29.71323776 29.96078873]
[-0.68271929 -0.13493828]
1688.25864746
[ 29.78564262 30.09841156]
[-0.58272243 -0.08323665]
1684.27538102
[ 29.75390816 30.13044167]
[-0.59861398 -0.11895057]

从这个结果中，我们能够看到在训练阶段，模型直线将会很快收敛到下面的直线：

Price=0.6 × Industry+29.75
Price = 0.1 × Age+30.13

3.6.8　完整源代码

完整源代码如下：

import matplotlib.pyplot as plt
import tensorflow as tf
import tensorflow.contrib.learn as skflow
from sklearn.utils import shuffle
import numpy as np
import pandas as pd

df = pd.read_csv("data/boston.csv", header=0)
printdf.describe()

f, ax1 = plt.subplots()
plt.figure() # Create a new figure

y = df['MEDV']

for i in range (1,8):
number = 420 + i
ax1.locator_params(nbins=3)
ax1 = plt.subplot(number)
plt.title(list(df)[i])
ax1.scatter(df[df.columns[i]],y) #Plot a scatter draw of the datapoints
plt.tight_layout(pad=0.4, w_pad=0.5, h_pad=1.0)

X = tf.placeholder("float", name="X") # create symbolic variables
Y = tf.placeholder("float", name = "Y")

with tf.name_scope("Model"):

w = tf.Variable(tf.random_normal([2], stddev=0.01), name="b0") # create
a shared variable
b = tf.Variable(tf.random_normal([2], stddev=0.01), name="b1") # create
a shared variable

def model(X, w, b):
return tf.mul(X, w) + b # We just define the line as X*w + b0

y_model = model(X, w, b)

with tf.name_scope("CostFunction"):
cost = tf.reduce_mean(tf.pow(Y-y_model, 2)) # use sqr error for cost
function

train_op = tf.train.AdamOptimizer(0.001).minimize(cost)

sess = tf.Session()
init = tf.initialize_all_variables()
tf.train.write_graph(sess.graph, '/home/bonnin/linear2','graph.pbtxt')
cost_op = tf.scalar_summary("loss", cost)
merged = tf.merge_all_summaries()
sess.run(init)
writer = tf.train.SummaryWriter('/home/bonnin/linear2', sess.graph)

xvalues = df[[df.columns[2], df.columns[4]]].values.astype(float)
yvalues = df[df.columns[12]].values.astype(float)
b0temp=b.eval(session=sess)
b1temp=w.eval(session=sess)

for a in range (1,50):
cost1=0.0
for i, j in zip(xvalues, yvalues):
sess.run(train_op, feed_dict={X: i, Y: j})
cost1+=sess.run(cost, feed_dict={X: i, Y: i})/506.00
#writer.add_summary(summary_str, i)
xvalues, yvalues = shuffle (xvalues, yvalues)
print (cost1)
b0temp=b.eval(session=sess)
b1temp=w.eval(session=sess)
print (b0temp)
print (b1temp)

3.7　小结

在本章中，我们使用TensorFlow的训练函数构建了第一个完整的模型。除此之外，我们还成功构建了一个多变量的线性模型来计算多于一维数据的回归。除此之外，我们还使用了TensorBoard观察变量在训练阶段的中间过程。

在下一章中，我们将继续使用非线性模型，这让我们更加接近神经网络的领域。神经网络是TensorFlow的主要支持领域，也是最能充分发挥TensorFlow能力的地方。

第4章　逻辑回归

在之前的章节中，我们学习了如何线性回归建模。线性回归模型中，通过最小化误差函数，计算出权重和偏差。

但是这种方法的使用范围有很大的限制，只有那些结果是连续变量的问题可以使用线性回归。

但是，如果我们面对的是离散的变量呢？比如，一个特征的是否出现；是否是金色头发；就医者是否得病。

这些问题是本章要解决的问题。

4.1　问题描述

线性回归的目标是基于一个连续方程预测一个值，而本章的目标是预测一个样本属于某个确定类的概率。

本章中，我们将会使用一个泛化的线性模型来解决回归问题。不同于之前的线性回归，我们这次的目标是解决一个分类问题，也就是将观察值贴上某个标签，或者是分入某个预先定义的类。

图4-1给我们展示了回归问题与分类问题的区别。第一幅图中（线性回归），当输入x连续变化的时候，y也是连续变化的。

但是，在第二幅图中就不一样了。不管输入x怎么变化，y只有两种可能性。左边部分数据趋向于0，右边部分数据趋向于1。

逻辑回归（logistic regression）的这个术语容易让人产生疑惑，明明要处理的是分类问题，为什么叫作回归？回归应该寻找一个连续值，而分类寻找的是离散值。

理解的关键就是，我们不仅仅是寻找一个表示类的离散值，我们还寻找表示属于该类可能性的连续值。

图4-1　回归与分类的区别

4.2　Logistic函数的逆函数——Logit函数

在我们开始学习Logistic函数之前，我们先复习一个它的逆函数Logit函数，Logit函数和Logistic函数息息相关，它们好多的性质是关联在一起的。

Logit函数的变量需要是一个概率p，更确切地说，需要是伯努利分布的事件概率。

4.2.1　伯努利分布

伯努利分布又称为二项分布，也就是说它只能表示成功和失败两种情况。

- 取1，表示成功，以概率p表示。

- 取0，表示失败，概率是q =1−p。

一个服从伯努利分布的随机变量，其概率函数可以被表述为：

Pr(X=1)=1−Pr(X=0)=1−q = p

什么样的情况可以用伯努利分布来表示呢？当我们只有两种选择的时候（特征是否存在，事件是否发生，现象是否具有因果性，等等）。

4.2.2　联系函数

在我们建立广义线性模型之前，我们先要从线性函数开始，从独立变量映射到一个概率分布。

既然操作的是二值选项，我们自然会选择刚刚提到的伯努利分布，而连接函数则是对数几率函数。

4.2.3　Logit函数

我们能够使用的一个函数，就是对发生率取对数，该函数被称作Logit函数：

logit(p) = log

这里面的（p/1−p）被称作事件的发生率，对其取对数，所以称为对数几率函数，简称对数函数，标记为Logit函数。

由图4-2我们可以看到，该函数实现了从区间[0,1]到区间（−∞,+∞）之间的映射。那么我们只要将y用一个输入的线性函数替换，那么就实现了输入的线性变化和区间[0,1]之间的映射。

图4-2　Logit函数

4.2.4　对数几率函数的逆函数——Logistic函数

我们计算一下对数几率函数的逆函数：

logit−1(α) = logistic(α) =

这是一个Sigmoid函数。

Logistic函数将使得我们能够在我们的回归任务表示为二项选择。

Sigmoid函数的图形表示如图4-3所示。

图4-3　Sigmoid函数的图形表示

1．Logistic函数作为线性模型的泛化

Logistic函数σ(t)的定义如下：

一般的解释就是t为一个独立变量，该函数将t映射到区间[0,1]之间。但是我们提升了这个模型，将t转变为变量x的一个线性映射（当x是一个多变量的向量时，t就是该向量中各个元素的线性组合）。

我们可以将t表示如下：

t=wx+b

我们就能够得到以下方程：

logit(p) = In

对于所有的元素，我们计算了回归方程，得出如下概率。

图4-4展示了任意范围的变量是如何映射到区间[0,1]的。而[0,1]之间的数，就可以被表示为一个时间发生的可能性。

图4-4　变量映射到[0,1]

线性函数的参数起什么作用呢？它们可以改变直线的斜率和Sigmoid函数零的位置。通过调整线性方程中的参数，来缩小预测值与真实值之间的差距。

2．Logistic函数的属性

函数空间中每个曲线都可以被描述成它所应用的可能目标。具体到Logistic函数：

- 事件的可能性p依赖于一个或者多个变量。比如，根据之前的资历，预测获奖的可能性。

- 对于特定的观察，估算事件发生的可能性。

- 预测改变独立变量对二项响应的影响。

- 通过计算可能性，将观测分配到某个确定的类。

3．损失函数

前面的章节中，我们学习了近似的函数，它能够得到样本属于某个确定类的可能性。为了计算预测与真实结果的切合程度，我们需要仔细选择损失函数。

损失函数可以表达为：

该损失函数的主要性质就是偏爱相似行为，而当误差超过0.5的时候，惩罚会急剧增加。

4.2.5　多类分类应用——Softmax回归

到现在为止，我们只是对二类分类进行处理，能够得到发生或者不发生的概率p。

当我们面对多于二类的情况，通常有两种方法：一对多和一对所有。

- 第一类技术计算多个模型。针对每个类都计算一个“一vs所有（one against all）”的概率。

- 第二类技术只计算出一个概率集合，每个概率表示属于其中某一类的可能性。

- 第二种技术的输出是Softmax回归格式，这是Logistic回归对于n类的泛化。

于是，我们就要对我们的训练样本做出改变，从二值标签（），到向量标签（）。其中K是类的数目，y可以取K个不同的值，而不限于2。

对于这个技术，我们针对每一个测试输出X，估算出P(y=k|x),其中k=1,1,K。Softmax回归的输出将会是一个K维的向量，每个数值对应的是该属于当前类的概率。

在图4-5中，我们表示了由输入到逻辑回归发生率p的映射。

图4-5　由输入到逻辑回归
发生率p的映射

1．损失函数

Softmax的损失函数是一种改动的交叉熵函数，也是非线性的，惩罚混乱度高的情况，偏爱混乱度低的情况。

这里，c是类的索引，i是训练样品的索引，当yc为1表示当前样品属于类c。

展开该表达式，我们能够得到：

2．为迭代方法进行数据正则化

正如我们在之前的部分所看到的，逻辑回归使用梯度下降方法来最小化损失函数。图4-6为数据正则化。

图4-6　数据正则化

该方法对特征数据的分布和形式敏感。

正是这个原因，我们要先对数据进行预处理，以期待获得更好、更快的收敛结果。

我们不去考虑正则化的理论解释。直觉地来看，数据正则化的时候，相当于平滑误差表面，使得梯度下降迭代更快地收敛到最小误差。

3．一位有效输出表示

为了能让Softmax作为回归函数的损失函数，我们必须使用一种编码类型，叫作一位有效（one hot）。这种编码方式把一个表示类的数值，转变成一个阵列，而原来表示类的数值列表，经过编码后将成为阵列列表。阵列的长度是列表中最大数值的数目，对应的该类的数目加1的那个元素被置为1，其他的元素都是0。

比如，用于表示以下的列表[1,3,2,4]，它的one hot编码如下：

[[0 1 0 0 0]
[0 0 0 1 0]
[0 0 1 0 0]
[0 0 0 0 1]]

4.3　例1——单变量逻辑回归

在第一个例子中，我们将会估计心脏病发生的可能性，使用单变量逻辑回归，输入是患者的年龄。

4.3.1　有用的库和方法

自从版本0.8之后，TensorFlow就提供了产生one hot结果的工具。

tf.one_hot(indices, depth, on_value=1, off_value=0, axis=None,
dtype=tf.float32, name=None)

这种方法生成one hot编码的数据结构，可以识别数据、生成轴、数据类型等。

在输出的张量中，指示值是on_value，默认为1，其他的是off_value，默认为0。

dtype是生成的张量的数据类型，默认的是float 32类型。

depth变量表示结果将会有多少行。我们认为它的值应该是max（indices）+1，但是也会被截取。

TensorFlow中Softmax的实现使用的是tf.nn.log_softmax函数，遵循以下形式：

tf.nn.log_softmax(logits, name=None)

此处，参数如下。

- logits：一个张量，格式可以为float 32，float 64，二维数据，形状是[batch_size, num_classes]。

- name：操作的名称（可选）。

操作返回一个张量，类型和形状都跟Logits相同。

4.3.2　数据集描述和加载

我们学习的第一个例子是拟合一个回归，输入是变量，输出只有两种可能的结果。

1．CHDAGE数据集

在第一个简单的例子中，我们将使用一个简单的数据集，该数据集取自书籍；Applied Logistic Regression-Third Edition, David W. Hosmer Jr., Stanley Lemeshow, Rodney X. Sturdivant, by Wiley。

绘制出年龄，冠心病的发病与否。100个病人。该表格包含了身份变量（ID），年龄分段（AGEGRP）。输出结果CHD，0的时候表示没有冠心病，1的时候表示得了冠心病。一般来说，可以用任何数字表示，但是实际中，我们发现使用0和1最方便。我们把这个数据集称作CHDAGE数据集。

2．CHDAGE数据集格式

CHDAGE数据集是一个二列的CSV文件，我们可以从一个外部的仓库下载。

在第一章，探索和转换数据，我们使用了TensorFlow自带的方法来读取该数据集。在本章中，我们将会用另外的一个非常流行的库来读取该数据集。

这样做的原因是，该数据集有100个元组，最好能够将它们用一行代码读入。我们拥有简单而强大的免费的工具，由pandas库提供。

在本工程的初始阶段，我们开始加载CHDAGE数据集，打印出重要的统计量，然后进行预处理。

在打印出数据之后，我们建立一个带激活函数的模型。激活函数选择Softmax函数，在二类的时候，这就是一个Logistic函数；这就是当只有两类的时候（疾病到底存不存在）。

3．数据集加载和预处理实现

首先，我们引入需要的库，将matplotlib库设置成内置（如果你使用Jupyter的话）模式。

>>> import pandas as pd
>>> import numpy as np
>>> %matplotlib inline
>>> import matplotlib.pyplot as plt

我们使用pandas库来读取数据，检查数据集的统计信息：

>>> df = pd.read_csv("data/CHD.csv", header=0)
>>> print df.describe()

age    chd
count    100.000000    100.00000
mean    44.380000    0.43000
std    11.721327    0.49757
min    20.000000    0.00000
25%    34.750000    0.00000
50%    44.000000    0.00000
75%    55.000000    1.00000
max    69.000000    1.00000

然后处理数据，并绘制数据，使得我们对数据有大致的了解。

plt.figure() # Create a new figure
plt.scatter(df['age'],df['chd']) #Plot a scatter draw of the random datapoints

绘图结果如图4-7所示。

图4-7　冠心病与年龄关系散点图

4.3.3　模型结构

此处，我们将会描述如何建立该模型。

learning_rate = 0.8 #Learning speed
batch_size = 100 #number of samples for the batch
display_step = 2 #number of steps before showing progress

此处，我们为数据流图创建初始变量和占位符。单变量x和y都是浮点型变量。

x = tf.placeholder("float", [None, 1]) # Placeholder for the 1D data
y = tf.placeholder("float", [None, 2]) # Placeholder for the classes (2)

现在，我们创建线性模型，它们在模型被拟合的过程中将会不断地变化。

W = tf.Variable(tf.zeros([1, 2]))
b = tf.Variable(tf.zeros([2]))

最后，我们创建激活函数，并且将其作用于线性方程之上。

activation = tf.nn.softmax(tf.matmul(x, W) + b)

4.3.4　损失函数描述和优化器循环

此处，我们定义交叉相关函数作为损失函数，定义优化器操作，选择梯度下降。这将会在后续章节中展开。现在我们把它当作一个黑盒，使它最小化。

cost = tf.reduce_mean(-tf.reduce_sum(y*tf.log(activation), reduction_indices=1))
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)
init = tf.initialize_all_variables()
#Iterate through all the epochs
for epoch in range(training_epochs):
avg_cost = 0.
total_batch = 400/batch_size
# Loop over all batches

for i in range(total_batch):
# Transform the array into a one hot format
temp=tf.one_hot(indices = df['chd'].values, depth=2, on_value = 1, off_value = 0, axis = -1 , name = "a")
batch_xs, batch_ys = (np.transpose([df['age']])-44.38)/11.721327, temp
# Fit training using batch data
sess.run(optimizer, feed_dict={x: batch_xs.astype(float), y: batch_ys.eval()})

# Compute average loss, suming the corrent cost divided by the batch
total number
avg_cost += sess.run(cost, feed_dict={x: batch_xs.astype(float), y: batch_ys.eval()})/total_batch

4.3.5　停止条件

本例中，当迭代次数满足的时候，程序停止。

4.3.6　结果描述

该程序的输出如下：

Epoch: 0001 cost= 0.638730764
[ 0.04824295 -0.04824295]
[[-0.17459483 0.17459483]]
Epoch: 0002 cost= 0.589489654
[ 0.08091066 -0.08091066]
[[-0.29231569 0.29231566]]
Epoch: 0003 cost= 0.565953553
[ 0.10427245 -0.10427245]
[[-0.37499282 0.37499279]]
Epoch: 0004 cost= 0.553756475
[ 0.12176144 -0.12176143]
[[-0.43521613 0.4352161 ]]
Epoch: 0005 cost= 0.547019333
[ 0.13527818 -0.13527818]
[[-0.48031801 0.48031798]]

不同迭代周期中的拟合函数结果如图4-8所示。

图4-8　不同变量下的拟合函数

4.3.7　完整源代码

完整源代码如下：

import pandas as pd
import numpy as np
get_ipython().magic(u'matplotlib inline')
import matplotlib.pyplot as plt
import tensorflow as tf

df = pd.read_csv("data/CHD.csv", header=0)

# Parameters

learning_rate = 0.2
training_epochs = 5
batch_size = 100
display_step = 1
sess = tf.Session()
b=np.zeros((100,2))
#print pd.get_dummies(df['admit']).values[1]
print sess.run(tf.one_hot(indices = [1, 3, 2, 4], depth=5, on_value = 1, off_
value = 0, axis = 1 , name = "a"))
#print a.eval(session=sess)

# tf Graph Input

x = tf.placeholder("float", [None, 1])
y = tf.placeholder("float", [None, 2])
# Create model
# Set model weights
W = tf.Variable(tf.zeros([1, 2]))
b = tf.Variable(tf.zeros([2]))

# Construct model
activation = tf.nn.softmax(tf.matmul(x, W) + b)
# Minimize error using cross entropy
cost = tf.reduce_mean(-tf.reduce_sum(y*tf.log(activation), reduction_indices=1)) # Cross entropy
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost) # Gradient Descent

# Initializing the variables
init = tf.initialize_all_variables()

# Launch the graph

with tf.Session() as sess:
tf.train.write_graph(sess.graph, './graphs','graph.pbtxt')
sess.run(init)
writer = tf.train.SummaryWriter('./graphs', sess.graph)
#Initialize the graph structure

graphnumber=321

#Generate a new graph
plt.figure(1)

#Iterate through all the epochs
for epoch in range(training_epochs):
avg_cost = 0.
total_batch = 400/batch_size
# Loop over all batches

for i in range(total_batch):
# Transform the array into a one hot format

temp=tf.one_hot(indices = df['chd'].values, depth=2, on_value = 1, off_value = 0, axis = -1 , name = "a")
batch_xs, batch_ys = (np.transpose([df['age']])-44.38)/11.721327, temp

# Fit training using batch data
sess.run(optimizer, feed_dict={x: batch_xs.astype(float), y: batch_ys.eval()})

# Compute average loss, suming the corrent cost divided by the batch total number
avg_cost += sess.run(cost, feed_dict={x: batch_xs.astype(float), y: batch_ys.eval()})/total_batch
# Display logs per epoch step

if epoch % display_step == 0:
print "Epoch:", '%05d' % (epoch+1), "cost=", "{:.8f}".format(avg_cost)

#Generate a new graph, and add it to the complete graph

trX = np.linspace(-30, 30, 100)
print (b.eval())
print (W.eval())
Wdos=2*W.eval()[0][0]/11.721327
bdos=2*b.eval()[0]

# Generate the probabiliy function
trY = np.exp(-(Wdos*trX)+bdos)/(1+np.exp(-(Wdos*trX)+bdos) )

# Draw the samples and the probability function, whithout the normalization
plt.subplot(graphnumber)
graphnumber=graphnumber+1

#Plot a scatter draw of the random datapoints
plt.scatter((df['age']),df['chd'])
plt.plot(trX+44.38,trY) #Plot a scatter draw of the random datapoints
plt.grid(True)

#Plot the final graph
plt.savefig("test.svg")

4.3.8　图像化表示

使用TensorBoard工具，我们能够观察到模型结构图4-9。请注意，在一半的操作图形中，我们定义了主要的全局操作（doftmas），以及应用于其他项的梯度，这些是执行最小化损失函数所需要的。这是下面章节要讨论的主题。

图4-9　模型结构图

4.4　例2——基于skflow单变量逻辑回归

本例中，我们将会探索单变量的回归问题，不过要用一个新的库。这将会简化我们的创建流程，叫作skflow。

4.4.1　有用的库和方法

在机器学习领域，会有很多库的变种。我们在第2章聚类中介绍了sklearn。

就在TensorFlow发布后不久，skflow就发布了。skflow的目标是以模拟sklearn的接口运行TensorFlow。这样比在TensorFlow的会话环境中运行会简化掉很多工作。

下面的实例中，我们将会重复之前的回归，不过以skflow的接口运行。

在本例中，我们将会学习如何生成一个具体的非常有组织的回归模型，只要设定一个日子文件夹作为参数即可。

4.4.2　数据集描述

数据集跟之前一样，我们使用pandas库加载数据：

import pandas as pd

df = pd.read_csv("data/CHD.csv", header=0)
print df.describe()

4.4.3　模型结构

模型结构的代码在my_model中：

def my_model(X, y):
return skflow.models.logistic_regression(X, y)

X1 =a.fit_transform(df['age'].astype(float))
y1 = df['chd'].values
classifier = skflow.TensorFlowEstimator(model_fn=my_model, n_classes=2)

展开图4-10中的逻辑回归（logistic regress）域，我们能看到图4-11中的softmax分类器。

图4-10　逻辑回归

图4-11　softmax分类器

4.4.4　结果描述

score = metrics.accuracy_score(df['chd'].astype(float),
classifier.predict(X))
print("Accuracy: %f" % score)

输出结果还不错（因为模型简单），准确率是74%。

Accuracy: 0.74000

4.4.5　完整源代码

完整的源代码如下：

import tensorflow.contrib.learn as skflow
from sklearn import datasets, metrics, preprocessing
import numpy as np
import pandas as pd

df = pd.read_csv("data/CHD.csv", header=0)
print df.describe()

def my_model(X, y):
return skflow.models.logistic_regression(X, y)

a = preprocessing.StandardScaler()

X1 =a.fit_transform(df['age'].astype(float))

y1 = df['chd'].values

classifier = skflow.TensorFlowEstimator(model_fn=my_model, n_classes=2)
classifier.fit(X1,y1 , logdir='/tmp/logistic')

score = metrics.accuracy_score(df['chd'].astype(float),
classifier.predict(X))
print("Accuracy: %f" % score)

4.5　小结

本章中，我们学习了Logistic函数。我们使用该函数对分类问题建模，为输入预测它所属于的类。

我们还学习了如何用pandas库读取基于文本的数据。

此外，我们还学习了如何使用skflow库，该库是TensorFlow库的一个很好的补充。

下一章中，我们将会开始学习更复杂的结构，并进入TensorFlow最擅长的领域：人工神经网络。我们会学习怎样使用人工神经网络对现实世界的数据进行建模、训练和测试。

第5章　简单的前向神经网络

神经网络是Tensorflow最擅长的机器学习领域。TensorFlow拥有一套符号引擎，它使得训练复杂模型变得更简单和方便。通过这套符号引擎，我们能够实现许多的模型结构和算法。

本章中，我们处理的问题相对于之前的章节，输入变量的数目将会变多，也会变得更加复杂。我们将学习和掌握处理这类问题的方法。

本章中，我们将会覆盖以下内容：

① 神经网络的基本概念；

② 神经网络用于回归非线性合成函数；

③ 使用非线性回归预测汽车燃料效率；

④ 学习葡萄酒分类——一种多类分类。

5.1　基本概念

在正式地使用和建立神经网络之前，我们需要学习以下神经网络的基本概念，这对于我们后面的学习非常有用。

5.1.1　人工神经元

人工神经元就是使用一个数学函数来对生物的神经元建模。

简单来说，一个人工神经元就是接受一个或者多个输入（训练数据），对它们加和，并产生一个输出。一般来说，这里面的加和指的是加权求和（每个输入乘上权重，并加上一个偏差），然后将加和的输入传递给一个非线性函数（一般称作激活函数或者转移函数）。

1．最简单的人工神经元——感知器

感知器是实现人工神经元最简单的方法，它的历史可以追溯到20世纪50年代，在20世纪60年代的时候，首次被实现。

简单来说，感知器就是一个二元分类函数，它将输入映射到一个二元输出，如图5-1所示。

图5-1　单层感知器

2．感知器算法

简化版的感知器算法如下：

① 以一个随机分布初始化权值和偏差（通常比较小）；

② 选择一个输入向量，并将其放入神经网络中；

③ 将输入与权重相乘，并加上偏差，计算网络的输出y'；

④ 感知器的函数如下：

⑤ 如果y′≠y，将权重wi加上Δw=yxi；

⑥ 返回第②步。

5.1.2　神经网络层

我们可以对单层的感知器进行泛华，将它们堆积起来，并互相连接，如图5-2所示。但这就带来一个问题，这样的线性组合出来的模型还只是一个线性分类器，对于复杂的非线性分类，这种方式并不能正确拟合，这个问题需要激活函数来解决。

1．神经网络激活函数

单独的单变量线性分类器并不能带来神经网络的强悍性能。就算那些不是很复杂的机器学习问题都会涉及多变量和非线性，所以我们常常要用其他的转移函数来替代感知器中原本的转移函数。

图5-2　连接单层感知器

有很多非线性函数可以用来做激活函数，从而表征不同的非线性模型。在输入同样的变量的时候，不同的激活函数有不同的响应。常用的激活函数如下：

- Logistic：典型的激活函数，在计算分类的概率时非常有用。

- Tanh：跟Sigmoid函数很像，但是范围是[−1,1]，而不是[0,1]。

- Relu：修正线性函数，该函数主要是为了对抗梯度消失。也就是当梯度反向传播到第一层的时候，梯度容易趋近于0或者一个极小值。

f(x)=max(0, x)

在我们计算总的误差的时候，因为是一整个函数作用于输入数据，所以我们要调整这个方程中的所有变量，来最小化方程。

怎样最小化误差呢？正如我们在优化部分所学，我们通过损失函数的梯度来最小化误差。

如果我们的网络拥有多层权重和转移函数，我们最终需要通过链式法则来求导所有参数的梯度。

2．梯度和反向传播算法

在感知器的学习阶段，我们按照每个权重对误差的“责任”，按比例调整权重。

在更复杂的网络中，误差的责任被分散在整个结构的所有操作之中。

3．最小化损失函数：梯度下降

我们由图5-3理解一下损失函数。

4．神经网络的选择-分类vs回归

神经网络既可以被用于回归问题，也可以被用于分类问题。不同的地方在于结构的最后一层。如果需要的结果是一个数值，那么就不要连接标准函数，如 sigmoid。如果是这样的话，我们得到的就是一个连续值。

图5-3　梯度下降

5.1.3　有用的库和方法

本章中我们会用到TensorFlow中一些新的函数，下面是我们需要的最重要的函数：

1．TensorFlow激活函数

最常用的激活函数如下：

- tf.sigmoid(x)：标准的sigmoid函数；

- tf.tanh(x)：双曲正切函数；

- tf.nn.relu(x)：修正线性函数；

TensorFlow中其他的函数：

- tf.nn.elu(x)：指数线性单元；如果输入小于0，返回exp(x)−1；否则，返回x；

- tf.softsign(x)：返回x/(abs(x)+1)；

- tf.nn.bias_add(value,bias)：增加一个bias到value。

2．TensorFlow中损失优化方法

- tf.train.GradientDescentOptimizer(learning_rate, use_locking, name)：原始梯度下降方法，唯一参数就是学习率。

- tf.train.AdagradOptimizer：自适应调整学习率，累加历史梯度的平方，作为分母，防止有些方向的梯度值过大，提高优化效率，善于处理稀疏梯度。

- tf.train.AdadeltaOptimizer：扩展AdaGrad优化方法，只累加最近的梯度值，而不对整个历史上的梯度值进行累加。

- tf.train.AdamOptimizertf.train.AdamOptimizer. (learningrate, beta1, beta2, epsilon, use locking, name)：梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam是自适应矩估计（Adaptive Moment Estimation）的首字母缩写。

3．Sklearn预处理函数

我们看一些下面的Sklearn数据预处理函数：

- preprocessing.StandardScaler()：数据正规化（Normalization）是机器学习估计的一个常见要求，为了模型能更好地收敛，我们通常会将数据集预处理到一个零均值单位方差的高斯状分布。通常，我们会将数据的各个维度都减去它的均值，然后乘上一个非零的数。这个非零的数就是数据集的标准差。对于该任务，我们直接使用StandardScaler，它已经实现了我们上面提到的操作。它也保留了变换操作，让我们可以直接用在测试集上。

- StandardScaler .fit_transform()：将数据调整到所需要的形式。StandardScaler对象会存储数据变化的变量，这样我们可以把数据解正规化到原先的格式。

- cross_validation.train_test_split：该方法能够将数据集分割成训练集和测试集。我们只需要提供两者的比例，该方法能够自动帮我们处理。

5.2　例1——非线性模拟数据回归

人工神经网络的应用包含了很多的分类任务，但是事实上，很多这些分类任务都是用回归来实现的。

分类和回归的网络结构差的并不多，都可以使用多变量的输入，以及线性或者非线性的激活函数。

在一些例子中，唯一要变的就是在输出层，连接上Sigmoid状的函数，该函数能够表征结果为各个类别的可能性。

在第一个例子中，我们使用一个简单的带噪声的二次方程生成样本。模型我们使用带一个隐含层的神经网络，然后检测预测值跟真实值的距离远近。

5.2.1　数据集描述和加载

本例中，我们使用生成的数据集，这跟第3章线性回归类似。

我们这次选择的方程是一个二次方程，并加上随机噪声，这有助于帮助我们测试回归的泛化能力。

核心代码如下：

import numpy as np
trainsamples = 200
testsamples = 60
dsX = np.linspace(-1, 1, trainsamples + testsamples).transpose()
dsY = 0.4* pow(dsX,2) +2 * dsX + np.random.randn(*dsX.shape) * 0.22 + 0.8

5.2.2　数据集预处理

本例中的数据集不需要预处理，因为它是我们人工生成的，具有更好的性能，比如能够保证数据范围是(−1,1)。

5.2.3　模型结构——损失函数描述

本例中的损失函数使用均方误差，由以下代码实现：

cost = tf.pow(py_x-Y, 2)/(2)

5.2.4　损失函数优化器

本例中，我们使用梯度下降作为损失函数优化器，可以用以下代码实现：

train_op = tf.train.AdamOptimizer(0.5).minimize(cost)

5.2.5　准确度和收敛测试

predict_op = tf.argmax(py_x, 1)
cost1 += sess.run(cost, feed_dict={X: [[x1]], Y: y1}) / testsamples

5.2.6　完整源代码

完整源代码如下：

import tensorflow as tf
import numpy as np
from sklearn.utils import shuffle
%matplotlib inline
import matplotlib.pyplot as plt
trainsamples = 200
testsamples = 60
#Here we will represent the model, a simple imput, a hidden layer of
sigmoid activation
def model(X, hidden_weights1, hidden_bias1, ow):
hidden_layer = tf.nn.sigmoid(tf.matmul(X, hidden_weights1)+ b)
return tf.matmul(hidden_layer, ow)
dsX = np.linspace(-1, 1, trainsamples + testsamples).transpose()
dsY = 0.4* pow(dsX,2) +2 * dsX + np.random.randn(*dsX.shape) * 0.22 + 0.8
plt.figure() # Create a new figure
plt.title('Original data')
plt.scatter(dsX,dsY) #Plot a scatter draw of the datapoints
X = tf.placeholder("float")
Y = tf.placeholder("float")
# Create first hidden layer
hw1 = tf.Variable(tf.random_normal([1, 10], stddev=0.1))
# Create output connection
ow = tf.Variable(tf.random_normal([10, 1], stddev=0.0))
# Create bias
b = tf.Variable(tf.random_normal([10], stddev=0.1))
model_y = model(X, hw1, b, ow)
# Cost function
cost = tf.pow(model_y-Y, 2)/(2)
# construct an optimizer
train_op = tf.train.GradientDescentOptimizer(0.05).minimize(cost)
# Launch the graph in a session
with tf.Session() as sess:
tf.initialize_all_variables().run() #Initialize all variables
for i in range(1,100):
dsX, dsY = shuffle (dsX.transpose(), dsY) #We randomize the samples
to mplement a better training
trainX, trainY =dsX[0:trainsamples], dsY[0:trainsamples]
for x1,y1 in zip (trainX, trainY):
sess.run(train_op, feed_dict={X: [[x1]], Y: y1})
testX, testY = dsX[trainsamples:trainsamples + testsamples],
dsY[0:trainsamples:trainsamples+testsamples]
cost1=0.
for x1,y1 in zip (testX, testY):
cost1 += sess.run(cost, feed_dict={X: [[x1]], Y: y1}) /
testsamples
if (i%10 == 0):
print "Average cost for epoch " + str (i) + ":" + str(cost1)

5.2.7　结果描述

生成的人工数据的散点图如图5-4所示。

图5-4　人工数据散点图

由以下每次迭代的结果，我们知道该实现结果非常好，甚至在第一次迭代的时候就取得了不错的结果。

Average cost for epoch 1:[[ 0.00753353]]
Average cost for epoch 2:[[ 0.00381996]]
Average cost for epoch 3:[[ 0.00134867]]
Average cost for epoch 4:[[ 0.01020064]]
Average cost for epoch 5:[[ 0.00240157]]
Average cost for epoch 6:[[ 0.01248318]]
Average cost for epoch 7:[[ 0.05143405]]
Average cost for epoch 8:[[ 0.00621457]]
Average cost for epoch 9:[[ 0.0007379]]

5.3　例2——通过非线性回归，对汽车燃料效率建模

本例中，我们会进入一个新的领域：解决非线性问题。该领域是神经网络附加值最大的领域。开始该旅程之前，我们会对几个汽车型号的燃料效率建模。该问题的输入是多个变量，只有非线性模型才能取得比较好的结果。

5.3.1　数据集描述和加载

对该问题，我们会分析一个著名的、标准的、数据组织得很好的数据集。我们有一个多变量的输入（有连续的，也有离散的），预测每加仑英里数（mpg）。

这是一个玩具级例子，但是这个例子会铺开通往更复杂问题的道路。而且该例子还有个好处，就是被大量研究过。

数据集由如下的列组成。

- mpg：每加仑英里数，连续。

- cylinders：气缸，多值离散。

- displacement：排量，连续。

- horsepower：马力，连续。

- weight：车重，连续。

- acceleration：加速度，连续。

- model year：年份，多值离散。

- origin：产地，多值离散。

- car name：车名，字符串（不会被使用）。

我们不会对该数据做详细的分析，我们只是想看一下每一个连续的变量都跟目标变量的增减相关，如图5-5所示。

图5-5　不同的变量和燃烧效率的关系散点图

5.3.2　数据预处理

本例中，我们会使用一个前面描述的sklearn中的StandardScaler对象：

- scaler = preprocessing.StandardScaler()

- X_train = scaler.fit_transform(X_train)

5.3.3　模型架构

我们需要建立的是一个多变量输入，单变量输出的前向神经网络，如图5-6所示。

图5-6　模型架构

5.3.4　准确度测试

我们用以下代码实现准确度测试：

score =
metrics.mean_squared_error(regressor.predict(scaler.transform(X_test)),
y_test)
print(" Total Mean Squared Error: " + str(score))

5.3.5　结果描述

Step #99, avg. train loss: 182.33624
Step #199, avg. train loss: 25.09151
Step #300, epoch #1, avg. train loss: 11.92343
Step #400, epoch #1, avg. train loss: 11.20414
Step #500, epoch #1, avg. train loss: 5.14056
Total Mean Squared Error: 15.0792258911

5.3.6　完整源代码

完整源代码如下：

%matplotlib inline
import matplotlib.pyplot as plt
import pandas as pd

from sklearn import datasets, cross_validation, metrics
from sklearn import preprocessing
from tensorflow.contrib import skflow

# Read the original dataset
df = pd.read_csv("data/mpg.csv", header=0)
# Convert the displacement column as float
df['displacement']=df['displacement'].astype(float)
# We get data columns from the dataset
# First and last (mpg and car names) are ignored for X
X = df[df.columns[1:8]]
y = df['mpg']

plt.figure() # Create a new figure

for i in range (1,8):
number = 420 + i
ax1.locator_params(nbins=3)
ax1 = plt.subplot(number)
plt.title(list(df)[i])
ax1.scatter(df[df.columns[i]],y) #Plot a scatter draw of the
datapoints
plt.tight_layout(pad=0.4, w_pad=0.5, h_pad=1.0)
# Split the datasets

X_train, X_test, y_train, y_test = cross_validation.train_test_split(X, y,

test_size=0.25)

# Scale the data for convergency optimization
scaler = preprocessing.StandardScaler()

# Set the transform parameters
X_train = scaler.fit_transform(X_train)

# Build a 2 layer fully connected DNN with 10 and 5 units respectively
regressor = skflow.TensorFlowDNNRegressor(hidden_units=[10, 5],
steps=500, learning_rate=0.051, batch_size=1)

# Fit the regressor
regressor.fit(X_train, y_train)

# Get some metrics based on the X and Y test data
score =
metrics.mean_squared_error(regressor.predict(scaler.transform(X_test)),
y_test)

print(" Total Mean Squared Error: " + str(score))

5.4　例3——多类分类：葡萄酒分类

本部分，我们会处理一个更复杂的数据集，预测葡萄酒的原产地。

5.4.1　数据集描述和加载

这个数据集包含了3种不同起源的葡萄酒，每个数据有13个属性。这些葡萄酒都由意大利制造，但是原产地不一样。

数据变量：

- Alcohol：酒精度。

- Malic acid：苹果酸。

- Ash：灰。

- Alcalinity of ash：灰分的碱度。

- Magnesium：镁含量。

- Total phenols：总酚类。

- Flavanoids：黄烷类。

- Nonflavanoid phenols：非类黄烷酚。

- Proanthocyanins：原花青素。

- Color intensity：颜色强度。

- Hue：色相。

- OD280/OD315 of diluted wines：稀释葡萄酒的OD280 / OD315。

- Proline：脯氨酸。

读取该数据集，我们只需要调用pandas库即可。

df = pd.read_csv("./wine.csv", header=0)

5.4.2　数据集预处理

因为该csv文件从1开始，为数据减去该偏差：

y = df['Wine'].values-1

对于其结果，我们使用一位有效编码：

Y = tf.one_hot(indices = y, depth=3, on_value = 1., off_value = 0., axis =
1 , name = "a").eval()

不同变量跟原产地关系散点图如图5-7所示。同样，我们会选择预先打乱数据：

图5-7　不同变量跟原产地关系散点图

X, Y = shuffle (X, Y)
scaler = preprocessing.StandardScaler()
X = scaler.fit_transform(X)

5.4.3　模型架构

该模型只需要一个单层的全链接神经网络：

- x = tf.placeholder(tf.float32, [None, 12])。

- W = tf.Variable(tf.zeros([12, 3]))。

- b = tf.Variable(tf.zeros([3]))。

- y = tf.nn.softmax(tf.matmul(x, W) + b)。

5.4.4　损失函数描述

我们选择交叉熵作为损失函数：

y_ = tf.placeholder(tf.float32, [None, 3])
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y),
reduction_indices=[1]))

5.4.5　损失函数优化器

我们又一次选择梯度下降作为损失函数的优化器。

train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy)

5.4.6　收敛性测试

在收敛性测试中，我们将正确预测的结果算作1，错误预测的结果算作0，计算它们的均值，作为该模型的准确度。

correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print(accuracy.eval({x: Xt, y_: Yt}))

5.4.7　结果描述

正如我们所见，准确度会随着迭代的次数而波动，但一直是一个大于90%的准确率，远高于30%的基准线（如果我们随机猜测一个0～3的结果）。

0.973684
0.921053
0.921053
0.947368
0.921053

5.4.8　完整源代码

完整源代码如下：

sess = tf.InteractiveSession()
import pandas as pd
# Import data
from tensorflow.examples.tlutorials.mnist import input_data
from sklearn.utils import shuffle

from sklearn import preprocessing

flags = tf.app.flags
FLAGS = flags.FLAGS

df = pd.read_csv("./wine.csv", header=0)
print (df.describe())
#df['displacement']=df['displacement'].astype(float)
X = df[df.columns[1:13]].values
y = df['Wine'].values-1
Y = tf.one_hot(indices = y, depth=3, on_value = 1., off_value = 0., axis =
1 , name = "a").eval()

X, Y = shuffle (X, Y)

scaler = preprocessing.StandardScaler()
X = scaler.fit_transform(X)

# Create the model
x = tf.placeholder(tf.float32, [None, 12])
W = tf.Variable(tf.zeros([12, 3]))
b = tf.Variable(tf.zeros([3]))
y = tf.nn.softmax(tf.matmul(x, W) + b)

# Define loss and optimizer
y_ = tf.placeholder(tf.float32, [None, 3])
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y),
reduction_indices=[1]))
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy)
# Train
tf.initialize_all_variables().run()
for i in range(100):
X,Y =shuffle (X, Y, random_state=1)

Xtr=X[0:140,:]
Ytr=Y[0:140,:]

Xt=X[140:178,:]
Yt=Y[140:178,:]
Xtr, Ytr = shuffle (Xtr, Ytr, random_state=0)
#batch_xs, batch_ys = mnist.train.next_batch(100)
batch_xs, batch_ys = Xtr , Ytr
train_step.run({x: batch_xs, y_: batch_ys})
cost = sess.run (cross_entropy, feed_dict={x: batch_xs, y_: batch_ys})
# Test trained model
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print(accuracy.eval({x: Xt, y_: Yt}))

5.5　小结

在本章中，我们开始学习TensorFlow最强的功能：神经网络模型。

通过使用生成模型和实验模型，我们同时实现了神经网络的回归和分类。

在下一章中，我们将会深入新的模型结构，并且将神经网络模型用到其他的知识领域，如将卷积神经网络用于计算机视觉。

第6章　卷积神经网络

很多最新的神经网络模型都包含卷积神经网络。在很多领域有它们的身影，但是用得最多的地方是图像分类和特征提取。

本章中将包括以下主题：

- 了解卷积函数和卷积网络的运行原理，掌握构建卷积网络的操作；

- 在图像数据上使用卷积神经网络，并掌握提升准确率的数据预处理技术；

- 使用卷积神经网络对MNIST数据集进行分类；

- 使用卷积神经网络对CIFAR数据集进行分类。

6.1　卷积神经网络的起源

Fukushima教授在1980年发表了一篇文章，提出了一种新识别器，这是卷积网络的最早实现。该网络是一种自组织的神经网络，拥有平移和扭曲不变性。

1986年Rumelhart等人在反向传播（back propagation）那篇论文的书籍版中提到了这个思想。在1988年的一篇关于语言识别中的暂态信号的文章中，也使用了卷积神经网络。

1988年，LeCun在他的文章《基于梯度学习的文本分类》中回顾了之前的卷积神经网络的设计，提出了LeNet-5网络，该网络还可以用于手写数字识别。当时几种变种SVM算法在手写数字识别上取得最好的效果，但是LeNet-5仍然超过了它们。

后来在2003年，有一篇文章《基于层次神经网络的图像解释》对该模型做了扩展。但是，本章中，我们主要使用LeCun的LeNet架构。

6.1.1　卷积初探

要想理解卷积操作，我们首先学习卷积函数，然后再将该函数应用于信息领域。

遵循卷积的发展历史，我们首先从连续域开始。

1．连续卷积

卷积最早的历史能够追溯到18世纪；通俗点讲，卷积就是两个操作在时间维度上的融合。

用数学公式可以表示如下：

如果我们从算法的角度来理解这个概念，这个操作能够被分解为如下步骤：

1）信号翻转：对应这个部分的符号。

2）信号平移：对应于函数g中的参数t。

3）信号相乘：f和g的相乘。

4）信号积分：这个没有前3个部分直观，因为结果中的每个瞬时值都是一个积分的结果，如图6-1所示。

图6-1　卷积操作的步骤

2．离散卷积

卷积的使用范围可以被延展到离散域，对应的数学表示为：

3．核与卷积

当我们在离散域进行离散操作时，核（kernel）就是一个非常重要的概念。

核是一个n×m的矩阵，n和m是每个维度上的元素数目，通常n = m。

卷积操作包括：首先将相应的元素乘上核，每个像素乘一次，然后把所有的值相加，最后把相加后的结果赋值给中心像素。

移动卷积矩阵，应用同样的操作，直到所有的元素都被遍历。

在图6-2的示例中，我们拥有一个图像和一个3×3的核，如图6-2所示。这种3×3的核在图像处理中非常常见。

图6-2　3×3卷积核操作结构图

4．卷积操作的解释

在学习过连续和离散域的卷积操作之后，我们来学习卷积在机器学习中的应用。

卷积核的作用在于强化或者隐藏模式（pattern）。通过训练好的参数（在下面的例子中是手动设置），我们可以发现模式，如不同维度的方向和边缘。我们也可以通过模糊核去除我们不想要的细节和异常点。

正如LeCun在他的论文中所说：

“卷积网络可以被认为本身集成了特征提取器。”

这使得卷积神经网络可以被看成一种先进的数据处理方式；对于一个特定的数据集，我们可以非常灵活地将卷积神经放在整个模型的前面部分。

5．在TensorFlow中使用卷积

TensorFlow为卷积操作提供了很多方法。典型的形式是conv2d操作。

tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu,data_format, name=None)

其参数的使用方法如下。

- input：这是输入的张量，卷积操作就是在input上进行。该张量限定了四维的格式，默认的顺序如下：[batch, in_height, in_width, in_channels]。batch是你准备的图像的数量。这个顺序叫作NHWC。还有一个选择是NCWH。比如，单个100×100像素的彩色图像有如下shape：[1,100,100,3]。

- filter：这个张量表示了一个核或者滤波器，格式如下：[filter_height, filter_width, in_channels, out_channels]

- strides：这是4个整型数据的列表。该变量表示每个维度上窗口滑动的步长。

- Padding：可以选择为SAME或者VALID。SAME会保留原始张量的维度，而VALID将会改变输出张量的大小。

- data_format：这个参数表示张量的组织方式（NHWC或者NCWH）。

6．其他的卷积操作

TensorFlow提供了很多有用卷积操作的方法，其他几种如下。

- tf.nn.conv2d_transpose：conv2d的梯度操作，在解卷积网络中经常使用。

- tf.nn.conv1d：执行一维卷积操作，输入是一个三维张量和一个滤波张量。

- tf.nn.conv3d：执行三维卷积操作，输入是一个五维张量和一个滤波张量。

7．示例代码——对灰度图像采用卷积操作

在示例代码中，我们会读取一个GIF格式的灰度图像。在内存会存成一个RGB三个通道值都相等的三通道张量。我们会使用卷积操作（设置一个卷积核）将图像转成一个真灰度矩阵，并将结果输出成一个JPEG格式的文件。

注意，我们可以微调kernel变量，来观察其对结果的影响。

下面是样例代码：

import tensorflow as tf

#Generate the filename queue, and read the gif files contents
filename_queue = tf.train.string_input_producer(
tf.train.match_filenames_once("data/test.gif"))
reader = tf.WholeFileReader()
key, value = reader.read(filename_queue)
image=tf.image.decode_gif(value)

#Define the kernel parameters
kernel=tf.constant(
[
[[[-1.]],[[-1.]],[[-1.]]],
[[[-1.]],[[8.]],[[-1.]]],
[[[-1.]],[[-1.]],[[-1.]]]
]
)

#Define the train coordinator
coord = tf.train.Coordinator()

with tf.Session() as sess:
tf.initialize_all_variables().run()
threads = tf.train.start_queue_runners(coord=coord)
#Get first image
image_tensor = tf.image.rgb_to_grayscale(sess.run([image])[0])
#apply convolution, preserving the image size
imagen_convoluted_tensor=tf.nn.conv2d(tf.cast(image_tensor, tf.float32),kernel,[1,1,1,1],"SAME")
#Prepare to save the convolution option
file=open ("blur2.jpeg", "wb+")
#Cast to uint8 (0..255), previous scalation, because the convolution could alter #the scale of the final image
out=tf.image.encode_jpeg(tf.reshape(tf.cast(
imagen_convoluted_tensor/tf.reduce_max(
imagen_convoluted_tensor)*255.,
tf.uint8), tf.shape(imagen_convoluted_tensor.eval()[0]).eval()))
file.close()
coord.request_stop()
coord.join(threads))

8．卷积核对结果的影响

你可以观察到核变量是怎样影响输出的结果的。第一幅图是原始图像，如图6-3所示。

从左到右、从上到下的卷积核依次是“模糊（blur）”“向下Sobel（一种自上向下寻找边缘的算法）”“emboss（增强角边缘）”“outline（自己写的滤波器）”，如图6-4所示。

图6-3　原始图像

图6-4　不同卷积核计算结果：左上角模糊blur、右上角向下Sobel、左下角emboss、右下角outline

6.1.2　降采样操作——池化

TensorFlow中的降采样（subsampling）操作通过使用池化（pool）方法实现。该操作的基本思想就是用局部区域里面的一个值代替整个区域，常见的是max_pool和avg_pool两种方法。其中max_pool使用局部区域的最大值代替该区域所有元素，而avg_pool使用均值代替。

在图6-5中，你可以看到在一个16×16的矩阵上使用2×2的核。本例中使用的是最大值代替整个2×2区域。

图6-5　max_pool示例

这种类型的池化操作也不是一成不变的。在LeCun的论文中，汇聚操作需要在原来的像素上乘上一个参数再加上一个偏差。

1．降采样层的属性

降采样层的目标跟卷积层多少有些类似。它们都是降低信息的数量和复杂性的同时保存重要的信息元素。降采样操作是对重要信息的一种压缩表示。

2．不变性

降采样层同样可以将一个复杂的信息简单化表示，通过滑动图像上的滤波器，我们将图像用其更有价值的部分表示，甚至能减少到大小为1的像素。相反的，该属性也让模型失去了位置特性信息。

3．降采样层的性能

降采样层实现起来比较快，因为它大致上就是通过几组比较保留了一些信息，剔除了一些信息。

4．在TensorFlow中使用池化操作

首先，我们分析最常用的池化操作——max_pool。它的用法如下：

tf.nn.max_pool(value, ksize, strides, padding, data_format, name)

该方法的用法跟conv2d类似，参数解释如下：

- value：四维的float 32类型张量，shape为[batch length,height,width,channels]。

- ksize：整型列表，每个元素卷积窗口在每个维度上的大小。

- strides：窗口在每个维度上的滑动步长。

- data_format：数据的组织结构，可以为NWC或者NCHW。

- padding：VALID或者SAME。

5．其他的池化操作

- tf.nn.avg_pool：返回窗口中的均值。

- tf.nn.max_pool_with_argmax：除了返回max_pool的结果，还返回最大值的索引。

- tf.nn.avg_pool3d：三维的avg_pool操作，输入需要一个额外的深度信息。

- tf.nn.max_pool3d：跟avg_pool3d操作类似，不过执行的是max操作。

6．示例代码

在下面的代码中，我们读入一个原始图像：

import tensorflow as tf

#Generate the filename queue, and read the gif files contents
filename_queue =
tf.train.string_input_producer(tf.train.match_filenames_once("data/test.gif"))
reader = tf.WholeFileReader()
key, value = reader.read(filename_queue)
image=tf.image.decode_gif(value)

#Define the coordinator
coord = tf.train.Coordinator()

def normalize_and_encode (img_tensor):
image_dimensions = tf.shape(img_tensor.eval()[0]).eval()
return tf.image.encode_jpeg(tf.reshape(tf.cast(img_tensor, tf.uint8),
image_dimensions))

with tf.Session() as sess:
maxfile=open ("maxpool.jpeg", "wb+")
avgfile=open ("avgpool.jpeg", "wb+")
tf.initialize_all_variables().run()
threads = tf.train.start_queue_runners(coord=coord)

image_tensor = tf.image.rgb_to_grayscale(sess.run([image])[0])

maxed_tensor=tf.nn.avg_pool(tf.cast(image_tensor,
tf.float32),[1,2,2,1],[1,2,2,1],"SAME")
averaged_tensor=tf.nn.avg_pool(tf.cast(image_tensor,
tf.float32),[1,2,2,1],[1,2,2,1],"SAME")

maxfile.write(normalize_and_encode(maxed_tensor).eval())
avgfile.write(normalize_and_encode(averaged_tensor).eval())
coord.request_stop()
maxfile.close()
avgfile.close()
coord.join(threads)

在如图6-6和图6-7所示的图像中，我们看到了比原始图像小的图像。第一幅采用的是max_pool操作，第二个是avg_pool操作。正如你看到的一样，两个图像看上去相同，但其实还是有差别的。将最大值替代为均值之后，所有像素的点都变得小了或者相等。

图6-6　max_pool操作后的效果

图6-7　avg_pool操作后的效果

6.1.3　提高效率——dropout操作

训练大规模神经网络经常遇到的一个问题就是过拟合（overfitting），也就是训练结果对于训练样本很友好，但是对于测试集不行。也就是说，模型的泛化效果不理想。

基于此，科学家发明了dropout操作。这个操作随机选择一些权重，将它们赋值为0。

这个方法最大的优势就是避免了所有的神经元，同步地优化它们的权重。这种操作避免了所有的神经元收敛到同样的结果，从而达到了“解关联（decorrelating）”的作用。

dropout的第二个优点就是能使得隐藏单元的激活变得稀疏，这是一个非常好的特性。

在图6-8中，我们展示了一个全链接的多层神经网络，并将dropout操作嵌入其中。

1．在TensorFlow中应用dropout

为了使用dropout操作，TensorFlow实现了一个方法tf.nn.dropout，它的使用方式如下：

tf.nn.dropout(x,keep_prob,noise_shape,seed,name)

参数的意义如下。

- x：原始张量。

- keep_prob：dropout的概率，输出的非0元素是原来的“1/keep_prob”倍。

- noise_shape：这是一个4个元素的列表，用来决定某个维度是否使用dropout。

图6-8　dropout操作示例

2．代码示例

在本例中，我们对一个向量使用dropout操作。从图6-9所示的案例中，我们可以看到，有0.5概率的元素被置零了。对于没有被置零的元素，它们的值都乘了2（1/0.5，其中0.5是dropout的可能性）。

图6-9　代码示例

有一个可能会让你惊讶的地方：对于没有丢弃（drop）掉的元素，它们乘上了一个因子。这个操作的作用是为了维系原来的网络。

6.1.4　卷积类型层构建办法

构建卷积神经网络层，有许多实践和方法，很多已经成为深度神经网络中的准规范（quasi-canonical）。

为了方便构建卷积神经网络层，我们在这里介绍一些简单的构建函数。

1．卷积层

这是一个卷积层的例子，一个卷积操作连接一个偏差（bias），结果送给一个激活函数，激活函数的输出作为这个卷积层的输出。Relu操作是一个非常常用的激活函数。

def conv_layer(x_in, weights, bias, strides=1):
x = tf.nn.conv2d(x, weights, strides=[1, strides, strides, 1],
padding='SAME')
x = tf.nn.bias_add(x_in, bias)
return tf.nn.relu(x)

2．降采样层

通过调用max_pool函数建立降采样层，按需求设置参数：

def maxpool2d(x, k=2):
return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, k, k, 1],
padding='SAME')

6.2　例1——MNIST数字分类

本部分，我们介绍MNIST数据集。该数据集是模式识别的一个非常知名的数据集。最早的时候，该数据集是用来训练识别手写数字的神经网络的。

原始的数据集有60 000个数字用来做训练，另外10 000个做测试，我们这里使用它的一个子集。

图6-10是LeNet-5的结构，这是解决这个问题第一个知名的卷积神经网络。

图6-10　LeNet-5的结构

6.2.1　数据集描述和加载

MNIST是一个简单并易于读取的数据集，但是处理起来却没有那么简单。现在，有很多种算法可以解决这个问题。在例子中，我们建立一个简单的网络，并能取得远高于10%（随机猜测）的结果。

为了获取MNIST数据集，我们需要使用TensorFlow中用于处理MNIST的方法。

通过以下两行代码，我们能够获取完整的MNIST数据集。

plt.imshow(mnist.train.images[0].reshape((28, 28), order='C'),cmap='Greys', interpolation='nearest')

在如图6-11所示的图像中，我们能够看到数据集的数据结构。

图6-11　MNIST数据集数据结构

通过下面的代码（见图6-12），我们能够打开和探索MNIST数据集。

图6-12　加载和探索MNIST数据集

要想打印一个字符（在Jupyter Notebook中），我们首先需要重新组织一下图像，生成一个28×28的矩阵，通过如下代码可以绘制一个“数字”图像。

图6-13是这行代码应用于不同的数据集元素的结果。

图6-13　MNIST中不同数据元素的图像示例

6.2.2　数据预处理

本例中，我们不做任何的数据预处理；但是如果对数据集进行增补，如平移、旋转或者扭曲，可以提高分类效果。

6.2.3　模型结构

此处，我们看一下为构建这个结构所用的不同的层：

首先是定义了一个权重和偏差的字典：

weights = {
'wc1': tf.Variable(tf.random_normal([5, 5, 1, 32])),
'wc2': tf.Variable(tf.random_normal([5, 5, 32, 64])),
'wd1': tf.Variable(tf.random_normal([7*7*64, 1024])),
'out': tf.Variable(tf.random_normal([1024, n_classes]))
}
biases = {
'bc1': tf.Variable(tf.random_normal([32])),
'bc2': tf.Variable(tf.random_normal([64])),
'bd1': tf.Variable(tf.random_normal([1024])),
'out': tf.Variable(tf.random_normal([n_classes]))
}

每一个weight都会匹配一个对应的bias。

然后定义了链接层，一个接着另一个：

conv_layer_1 = conv2d(x_in, weights['wc1'], biases['bc1'])

conv_layer_1 = subsampling(conv_layer_1, k=2)

conv_layer_2 = conv2d(conv_layer_1, weights['wc2'], biases['bc2'])

conv_layer_2 = subsampling(conv_layer_2, k=2)

fully_connected_layer = tf.reshape(conv_layer_2, [-1,
weights['wd1'].get_shape().as_list()[0]])
fully_connected_layer = tf.add(tf.matmul(fully_connected_layer,
weights['wd1']), biases['bd1'])
fully_connected_layer = tf.nn.relu(fully_connected_layer)

fully_connected_layer = tf.nn.dropout(fully_connected_layer, dropout)

prediction_output = tf.add(tf.matmul(fully_connected_layer,
weights['out']), biases['out'])

对于每一个weight，我们还要使用一个偏差（bias）来做常数偏移。

然后我们就可以定义卷积层和降采样层了，降采样层连接在卷积层后面。

6.2.4　损失函数描述

损失函数为交叉熵的均值，这是softmax回归常用的一种损失函数。

cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(pred, y))

6.2.5　损失函数优化器

本例中，我们使用一个改进的优化器AdamOptimizer，并将学习率设置为0.001。

optimizer = tf.train.AdamOptimizer
(learning_rate=learning_rate).minimize(cost)

6.2.6　准确性测试

比较真实值和预测值，相同为0，不同为1。对上一步得到的结果取均值，就是我们的准确值。准确值将会在0～1。

correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

6.2.7　结果描述

该例子的结果非常简单，我们只取了其中10 000个样本做训练集。虽然结果并不理想，但是显然比随机猜（十分之一）高。

Optimization Finished!
Testing Accuracy: 0.382812

6.2.8　完整源代码

完整源代码如下：

import tensorflow as tf
%matplotlib inline
import matplotlib.pyplot as plt
# Import MINST data
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
# Parameters
learning_rate = 0.001
training_iters = 2000
batch_size = 128
display_step = 10

# Network Parameters
n_input = 784 # MNIST data input (img shape: 28*28)
n_classes = 10 # MNIST total classes (0-9 digits)
dropout = 0.75 # Dropout, probability to keep units

# tf Graph input
x = tf.placeholder(tf.float32, [None, n_input])
y = tf.placeholder(tf.float32, [None, n_classes])
keep_prob = tf.placeholder(tf.float32) #dropout (keep probability)

#plt.imshow(X_train[1202].reshape((20, 20), order='F'), cmap='Greys',
interpolation='nearest')

# Create some wrappers for simplicity
def conv2d(x, W, b, strides=1):
# Conv2D wrapper, with bias and relu activation
x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1],
padding='SAME')
x = tf.nn.bias_add(x, b)
return tf.nn.relu(x)
def maxpool2d(x, k=2):
# MaxPool2D wrapper
return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, k, k, 1],
padding='SAME')
# Create model
def conv_net(x, weights, biases, dropout):
# Reshape input picture
x = tf.reshape(x, shape=[-1, 28, 28, 1])

# Convolution Layer
conv1 = conv2d(x, weights['wc1'], biases['bc1'])
# Max Pooling (down-sampling)
conv1 = maxpool2d(conv1, k=2)

# Convolution Layer
conv2 = conv2d(conv1, weights['wc2'], biases['bc2'])
# Max Pooling (down-sampling)
conv2 = maxpool2d(conv2, k=2)

# Fully connected layer
# Reshape conv2 output to fit fully connected layer input
fc1 = tf.reshape(conv2, [-1, weights['wd1'].get_shape().as_list()[0]])
fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
fc1 = tf.nn.relu(fc1)
# Apply Dropout
fc1 = tf.nn.dropout(fc1, dropout)

# Output, class prediction
out = tf.add(tf.matmul(fc1, weights['out']), biases['out'])
return out
# Store layers weight & bias
weights = {
# 5x5 conv, 1 input, 32 outputs
'wc1': tf.Variable(tf.random_normal([5, 5, 1, 32])),
# 5x5 conv, 32 inputs, 64 outputs
'wc2': tf.Variable(tf.random_normal([5, 5, 32, 64])),
# fully connected, 7*7*64 inputs, 1024 outputs
'wd1': tf.Variable(tf.random_normal([7*7*64, 1024])),
# 1024 inputs, 10 outputs (class prediction)
'out': tf.Variable(tf.random_normal([1024, n_classes]))
}

biases = {
'bc1': tf.Variable(tf.random_normal([32])),
'bc2': tf.Variable(tf.random_normal([64])),
'bd1': tf.Variable(tf.random_normal([1024])),
'out': tf.Variable(tf.random_normal([n_classes]))
}

# Construct model
pred = conv_net(x, weights, biases, keep_prob)

# Define loss and optimizer
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(pred, y))
optimizer =
tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

# Evaluate model
correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

# Initializing the variables
init = tf.initialize_all_variables()

# Launch the graph
with tf.Session() as sess:
sess.run(init)
step = 1
# Keep training until reach max iterations
while step * batch_size < training_iters:
batch_x, batch_y = mnist.train.next_batch(batch_size)
test = batch_x[0]
fig = plt.figure()
plt.imshow(test.reshape((28, 28), order='C'), cmap='Greys',
interpolation='nearest')
print (weights['wc1'].eval()[0])
plt.imshow(weights['wc1'].eval()[0][0].reshape(4, 8), cmap='Greys',
interpolation='nearest')
# Run optimization op (backprop)
sess.run(optimizer, feed_dict={x: batch_x, y: batch_y,
keep_prob: dropout})
if step % display_step == 0:
# Calculate batch loss and accuracy
loss, acc = sess.run([cost, accuracy], feed_dict={x: batch_x,
y: batch_y,
keep_prob: 1.})
print "Iter " + str(step*batch_size) + ", Minibatch Loss= " + \
"{:.6f}".format(loss) + ", Training Accuracy= " + \
"{:.5f}".format(acc)
step += 1
print "Optimization Finished!"

# Calculate accuracy for 256 mnist test images
print "Testing Accuracy:", \
sess.run(accuracy, feed_dict={x: mnist.test.images[:256],
y: mnist.test.labels[:256],
keep_prob: 1.})

6.3　例2——CIFAR10数据集的图像分类

在本例中，我们将会处理一个图像处理领域非常流行的数据集CIFAR10。该数据集虽然简单，但是非常通用。本例中，我们将会构建一个简单的CNN模型，通过使用该模型，我们学习处理分类问题的一般模型结构。

6.3.1　数据集描述和加载

数据集包含了40 000个32×32个像素的图像。包含以下的类：飞机、摩托、飞鸟、猫、鹿、狗、青蛙、马、船和卡车。本例中，我们只取前10 000个图像使用。

图6-14是从该数据集中挑选出来的几幅图像：

图6-14　CIFAR数据集中的部分图像

6.3.2　数据集预处理

我们首先把原始数据结构调整为[10000, 3, 32, 32]，然后再把该数据结构转化成需要的结构。将第一个通道的颜色数据移动到最后一个通道。

datadir='data/cifar-10-batches-bin/'
plt.ion()
G = glob.glob (datadir + '*.bin')
A = np.fromfile(G[0],dtype=np.uint8).reshape([10000,3073])
labels = A [:,0]
images = A [:,1:].reshape([10000,3,32,32]).transpose (0,2,3,1)
plt.imshow(images[14])
print labels[11]
images_unroll = A [:,1:]

6.3.3　模型结构

在这个模型中，我们接连使用了卷积和池化操作，最后使用了一个平展层和逻辑回归。使用逻辑回归的目的是计算当前的样品属于每个类的概率。

def conv_model (X, y):
X= tf. reshape(X, [-1, 32, 32, 3])
with tf.variable_scope('conv_layer1'):
h_conv1=tf.contrib.layers.conv2d(X, num_outputs=16,
kernel_size=[5,5], activation_fn=tf.nn.relu)#print (h_conv1)
h_pool1=max_pool_2x2(h_conv1)#print (h_pool1)
with tf.variable_scope('conv_layer2'):
h_conv2=tf.contrib.layers.conv2d(h_pool1, num_outputs=16,
kernel_size=[5,5], activation_fn=tf.nn.relu)
#print (h_conv2)
h_pool2=max_pool_2x2(h_conv2)
h_pool2_flat = tf.reshape(h_pool2, [-1,8*8*16 ])
h_fc1 = tf.contrib.layers.stack(h_pool2_flat,
tf.contrib.layers.fully_connected ,[96,48], activation_fn=tf.nn.relu )

return skflow.models.logistic_regression(h_fc1,y)

6.3.4　损失函数描述和优化器

使用函数如下：

classifier = skflow.TensorFlowEstimator(model_fn=conv_model, n_classes=10,
batch_size=100, steps=2000, learning_rate=0.01)

6.3.5　训练和准确性测试

通过这两行命令，我们能够开始拟合模型和计算训练模型的准确率。

%time classifier.fit(images, labels, logdir='/tmp/cnn_train/')
%time score =metrics.accuracy_score(labels, classifier.predict(images))

6.3.6　结果描述

表6-1是运行结果。

表6-1　运行结果

Parameter

Result1

Result2

CPU times

user 35min 6s

user 39.8s

Sys

1min 50s

7.19s

Total

36min 57s

47s

Wall time

25min 3s

32.5s

Accuracy

0.612200

6.3.7　完整源代码

完整源代码[1]如下：

import glob
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import tensorflow.contrib.learn as skflow
from sklearn import metrics
from tensorflow.contrib import learn

datadir='data/cifar-10-batches-bin/'
plt.ion()
G = glob.glob (datadir + '*.bin')
A = np.fromfile(G[0],dtype=np.uint8).reshape([10000,3073])
labels = A [:,0]
images = A [:,1:].reshape([10000,3,32,32]).transpose (0,2,3,1)
plt.imshow(images[15])
print labels[11]
images_unroll = A [:,1:]
def max_pool_2x2(tensor_in):
return tf.nn.max_pool(tensor_in, ksize= [1,2,2,1], strides= [1,2,2,1],
padding='SAME')

def conv_model (X, y):
X= tf. reshape(X, [-1, 32, 32, 3])
with tf.variable_scope('conv_layer1'):
h_conv1=tf.contrib.layers.conv2d(X, num_outputs=16,
kernel_size=[5,5], activation_fn=tf.nn.relu)#print (h_conv1)
h_pool1=max_pool_2x2(h_conv1)#print (h_pool1)
with tf.variable_scope('conv_layer2'):
h_conv2=tf.contrib.layers.conv2d(h_pool1, num_outputs=16,
kernel_size=[5,5], activation_fn=tf.nn.relu)
#print (h_conv2)
h_pool2=max_pool_2x2(h_conv2)
h_pool2_flat = tf.reshape(h_pool2, [-1,8*8*16 ])
h_fc1 = tf.contrib.layers.stack(h_pool2_flat,
tf.contrib.layers.fully_connected ,[96,48], activation_fn=tf.nn.relu )
return skflow.models.logistic_regression(h_fc1,y)

images = np.array(images,dtype=np.float32)
classifier = skflow.Estimator(model_fn=conv_model, n_classes=10,
batch_size=100, steps=2000, learning_rate=0.01)

%time classifier.fit(images, labels, logdir='/tmp/cnn_train/')
%time score =metrics.accuracy_score(labels, classifier.predict(images))

6.4　小结

在本章中，我们学习了卷积神经网络。卷积神经网络是最新的神经网络模型的重要组成部分。通过这个工具，我们能够处理更复杂的数据集，也能让我们理解最先进的模型。

下一章中，我们会学习另一类最新的神经网络，它也是最新的神经网络的一部分：递归神经网络。

[1]　本段代码在新的TensorFlow上无法运行，所以这里用Keras来运行程序。——译者注

第7章　循环神经网络和LSTM

回顾一下我们已知的传统神经网络模型，可以发现训练和预测阶段都以静态的方式运行。也就说，对于每一个输入，给出一个输出，而不考虑其序列间的关系。不同于我们之间的预测模型，循环神经网络不仅依赖于当前的输入，而且依赖于先前的输入。

本章包括如下内容：

- 理解循环神经网络运行原理，掌握构建循环神经网络的操作类型。

- 解释更高级递归神经网络模型（如LSTM）的思想。

- 应用TensorFlow中的LSTM模型预测能耗周期。

- 学习巴赫谱曲。

7.1　循环神经网络

知识不能凭空出现。很多新的点子来自于旧知识的重新组合，这很值得机器学习模仿。但是，传统的神经网络模型不会将之前的知识传递到当前的状态。

我们可以通过循环神经网络（recurrent neural networks，RNN）来实现这个想法。循环神经网络是一种序列化的神经网络，它可以将已有的信息重复利用。RNN中一个主要假设就是当前的信息依赖于先前的信息。在图7-1中，我们可以看到一个RNN基本元素，元胞（Cell）的示意图。

图7-1　元胞示意图

一个元胞包含3个主要元素，输入（xt）、状态和输出（ht）。但是正如我们之前所说，元胞并非一个独立的状态，它保留状态信息，传递给下个元胞。在图7-2中，展示了一个展开的RNN元胞。本图解释了信息怎样从初始状态，经过一系列中间状态，转移到最终状态hn。

图7-2　展开的元胞

在定义了元胞间的流动方式之后，我们要做的就是研究每一个RNN的元胞本身的运行方式。最基本的标准RNN中，只有一个简单的神经网络层。该层的输入除了原本的输入还有前一个样本的状态。将两者拼接后，通过tanh操作，输出新的状态h(t +1)，如图7-3所示。

图7-3　只有一个简单的神经网络层元胞的内部示意图

理论上，经过多次的数据迭代，这样的结构就能够提取出序列间的依赖信息。但是序列的长度使得上下文之间的关联变得困难。下面我们以“设计师懂得如何设计漂亮的建筑”为例：这句结构看上去很简单，但是要关联设计师和建筑这两个概念，则需要非常多的训练实例。这个问题就属于梯度的爆炸和消失。

7.1.1　梯度爆炸和梯度消失

事实上，循环神经网络在做反向传播的时候，会遇到跟深度神经网络类似的问题，不同的是深度网络引发的原因是层数多，而循环神经网络是因为步长太长。这种级联的梯度计算，在传递到最后的阶段，容易造成两种后果，要么梯度弱小到一个没有意义的非常小的值，要么弥散到超出参数的边界。这也是LSTM结构出现的原因。

7.1.2　LSTM神经网络

长短期记忆网络（Long-Short Term Memory，LSTM）是RNN的一个特例，它特殊的结构使得它能够拥有更长的依赖能力。也就是说，它比一般的RNN能够记住更长时间周期上的信息模式（pattern）。

1．门操作——一种基本组件

为了更好地理解LSTM元胞的运作方式，我们首先介绍LSTM中的主要操作块：门操作（the gate operation）。

这种操作一般都是多变量输入，我们只让其中一部分输入通过，拦住其他部分。我们可以把它理解成一个信息的滤波器，作用就是获取并记住需要的信息元素。

为了实现这个功能，我们使用多变量控制向量（用箭头标注），与神经网络通过Sigmoid激活函数相连，如图7-4所示。通过使用控制向量和Sigmoid函数，我们将会得到一个类二值向量（binary like vector）。

图7-4　门操作示例

我们通过多个开关符号来表示这个功能。

定义完这个二值向量后，我们通过将输入乘上该向量，使得输入中的一部分能够通过。该操作使用一个三角符号表示，箭头指向信息通往的方向，如图7-5所示。

图7-5　门结构示意图

图7-6就是一个常见的LSTM元胞结构。它包含了3个门操作，来保护和控制元胞状态。

该操作既能够丢弃低（希望是没用的）状态数据，又能够向状态中加入（希望是有用的）新的数据。

图7-6解释了LSTM元胞中所有操作的概念。

对于输入：

- 由于优化了权重，所以通过训练，元胞的状态将能够记住更长期的信息。

- 短期的信息h(t)也会被直接结合进输入。由于它没有经过加权，所以它对最终结果的影响会更大。

输出可以看成集成了所有门操作的应用。

图7-6　常见的LSTM元胞结构

2．操作步骤

本节中，我们将会介绍LSTM Cell的每个子部分。

（1）第一部分——遗忘数据（输入门）

在此部分，处理的数据是上个样本的短期记忆和本次样本的输入。将这两个数据结合后，将其输入进一个开关函数，该函数用多变量Sigmoid函数实现。Sigmoid函数的输出将作为守门人，放进或者阻止一部分先前知识。具体如图7-7所示。

图7-7　输入门

（2）第二部分——保持、更新状态数据（见图7-8）

现在，我们使用滤波器来解决允许什么信息通过或者拒绝什么样的半永久状态。

在本阶段，我们将决定怎样将新信息和半新信息相结合，嵌入到新的状态。新的长期状态信息由两块相加而成。第一块是上一步得到的过滤后的长期状态信息，第二块是根据上个元胞的和本元胞的输入xt获得的更新信息。

为了正规化和xt，我们将其拼接后通过一个tanh的激活函数，这可以将新的信息正规化到（−1,1）。

图7-8　保持、更新数据状态

（3）第三部分——输出滤波后的元胞状态（见图7-9）

下面解释短期记忆。跟长期记忆相同的是，输入同样结合了本次的和上次的短期状态信息，以使得能够获取新的信息。不同的是，这次点乘上的是长期记忆的状态，该状态通过tanh函数正规化到（−1,1）。

图7-9　输出滤波后的元胞状态

7.1.3　其他RNN结构

本章中，我们主要介绍了RNN中用得最多的模型LSTM。但是RNN家族还有其他变种，例如：

- 带窥视孔（peepholes）的LSTM：这种元胞中，元胞的门与元胞的状态直接连接在一起。

- 门循环单元（Gate Recurrent Unit，GRU）：这种元胞中，忘记部分和输入门结合在一起，把状态并入隐藏状态，这样简化了网络的训练。

7.1.4　TensorFlow LSTM有用的类和方法

本部分，我们会学习构建LSTM层的主要的类和方法，并且我们会将它们用在本书的实例中。

1．tf.nn.rnn_cell.BasicLSTMCell类

这是一个最简单的LSTM循环神经网络的元胞，仅仅带一个遗忘门，而不带其他的新颖的特征，如窥视孔（peepholes）。窥视孔的作用在于让各种门可以观察到元胞状态。

下面是主要参数：

- num_units：整型变量，LSTM cell的数目；

- forget_bias：浮点型变量，偏差（默认值为1）。该参数作用于遗忘门，用于第一次迭代中减少信息的损失；

- activation：是内部状态的损失函数（默认值为标准tanh）。

2．MultiRNNCell（RNNCell）类

在本例中，我们会用多个元胞来记住历史信息，所以我们会级联式堆积多个元胞。因此，我们需要MultiRNNCell类，如图7-10所示。

MultiRNNCell(cells, state_is_tuple=False)

这是MultiRNNCell的构造函数，主要的参数就是cells。这些cell是需要堆积的RNNCell类的实例。

3．learn.ops.split_squeeze(dim, num_split, tensor_in)

这个函数对输入在某个维度上进行切割。输入一个张量，对该张量在dim维度上切割成num_split个其他维度数值一样的张量。

图7-10　MultiRNNCell

- dim：该参数表示需要切割的维度。

- num_split：该参数表示分割数目。

- tensor_in：该参数是输入张量。

7.2　例1——能量消耗、单变量时间序列数据预测

本例中，我们将会解决一个回归类问题。我们的数据集是家庭某个时间范围内的电力消耗。我们很容易推测，这种数据一定会存在某种模式（比如因为要准备早餐和打开电脑，早晨的用电量会上升，下午会有少许下降，晚上的时候，随着大家都把灯打开，数据又会上升，直到夜里，数据慢慢降为零，这样一直保持到第二天早上）。

7.2.1　数据集描述和加载

在本例中，我们会使用电力负荷数据集。

该数据集的官方描述如下：

“本数据集没有缺失数据。

数据的值是15分钟内的kW数。如果想获取kW·h的数据，需要将当前数据除以4。每一行表示一个客户。一些客户是在2011年之后创建的，所以他们之前的消耗为零。

所有的时间都是葡萄牙时间。每天都有96个度量（24×4）。每年的3月份只有23小时，这时夜里一点到两点之间的值都为零，而每年的8月份会有25小时，这是夜里一点到两点的数据是两个小时的累加。”

为了简化我们的模型，我们只取了一个客户的完整的用电数据，并将其转换成标准的CSV格式。数据文件放置在本章代码文件夹的数据子文件夹中。

使用下面的代码，我们能够读取用户数据，并且将其绘制出来。

import pandas as pd
from matplotlib import pyplot as plt
df = pd.read_csv("data/elec_load.csv", error_bad_lines=False)
plt.subplot()
plot_test, = plt.plot(df.values[:1500], label='Load')
plt.legend(handles=[plot_test])

图7-11中（我们取了前1500个样品），我们能够看到在初始的时候有一个过渡状态，这可能是刚开始测量的时候，还不稳定。之后，我们就能明显看到，用电量的高低起伏呈现明显的周期性。

图7-11　绘制用户数据

我们能够大致地看出，每100个采样呈现一个循环，这跟我们每天96个采样点吻合。

7.2.2　数据预处理

为了使得反向传播更容易收敛，我们一般会先对输入数据进行正规化，如图7-12所示。

我们使用最经典的放缩和数据中心化方法，也就是对每个数据减去平均值，并按最大值进行放缩（将最大值限定到某个固定值，如1）。

可以使用pandas库中的describe( )查看数据的快速统计汇总。

Load
Count    140256.000000
mean        145.332503
std          48.477976
min           0.000000
25%         106.850998
50%         151.428571
75%         177.557604
max         338.218126

图7-12　正规化后的数据

7.2.3　模型结构

下面我们会简单地描述一下电力消耗随时间变化的模型。

我们使用的模型连接了10个串联的LSTM multicell，每个multicell的输出都是一个线性回归。（本例中，通过最近的5个值预测下一个值）。

def lstm_model(time_steps, rnn_layers, dense_layers=None):
def lstm_cells(layers):
return
[tf.nn.rnn_cell.BasicLSTMCell(layer['steps'],state_is_tuple=True)
for layer in layers]

def dnn_layers(input_layers, layers):
return input_layers

def _lstm_model(X, y):
stacked_lstm = tf.nn.rnn_cell.MultiRNNCell(lstm_cells(rnn_layers),
state_is_tuple=True)
x_ = learn.ops.split_squeeze(1, time_steps, X)
output, layers = tf.nn.rnn(stacked_lstm, x_, dtype=dtypes.float32)
output = dnn_layers(output[-1], dense_layers)
return learn.models.linear_regression(output, y)

return _lstm_model

图7-13是主要的操作模块，由learn模块创建，我们可以看到，里面有RNN操作、优化器、线性回归以及输出。

图7-13　主要操作模块

在图7-14中，我们展开RNN模块，除了级联的LSTM cell，还有输入序列。其他的操作由learn包添加。

然后我们完善模型的定义，本模型是一个回归模型。

regressor = learn.TensorFlowEstimator(model_fn=lstm_model(
TIMESTEPS, RNN_LAYERS, DENSE_LAYERS),
n_classes=0,
verbose=2, steps=TRAINING_STEPS,
optimizer='Adagrad',
learning_rate=0.03,
batch_size=BATCH_SIZE)

图7-14　RNN域内的结构图

7.2.4　损失函数描述

我们使用均方误差作为本例的损失函数。均方误差是用于回归问题的经典误差函数。

rmse = np.sqrt(((predicted - y['test']) ** 2).mean(axis=0))

7.2.5　收敛检测

我们为当前模型使用如下的拟合函数：

regressor.fit(X['train'], y['train'], monitors=[validation_monitor],
logdir=LOG_DIR)

我们将会得到如下很好的误差值。作为练习，我们可以尝试着不对数据做正规化，直接将数据丢入模型，我们会发现，结果变差了。

在命令对话框的输出是：

MSE：0.001139

我们通过这个数据就能够知道误差是怎样随着迭代次数的增加而衰减的，如图7-15所示。

图7-15　误差随迭代次数的增加而衰减

7.2.6　结果描述

如图7-16所示，真实值和预测值都被绘制在图中。我们会发现，对于循环模型，均误差损失函数能够取得很好的结果。

图7-16　结果描述

7.2.7　完整源代码

下面是完整的源代码：

import numpy as np
import pandas as pd
import tensorflow as tf
from matplotlib import pyplot as plt

from tensorflow.python.framework import dtypes
from tensorflow.contrib import learn

import logging
logging.basicConfig(level=logging.INFO)

from tensorflow.contrib import learn
from sklearn.metrics import mean_squared_error
predicted = regressor.predict(X['test'])
rmse = np.sqrt(((predicted - y['test']) ** 2).mean(axis=0))
score = mean_squared_error(predicted, y['test'])
print ("MSE: %f" % score)
#plot_predicted, = plt.plot(array[:1000], label='predicted')
plt.subplot()
plot_predicted, = plt.plot(predicted, label='predicted')
plot_test, = plt.plot(y['test'], label='test')
plt.legend(handles=[plot_predicted, plot_test])

LOG_DIR = './ops_logs'
TIMESTEPS = 5
RNN_LAYERS = [{'steps': TIMESTEPS}]
DENSE_LAYERS = None
TRAINING_STEPS = 10000
BATCH_SIZE = 100
PRINT_STEPS = TRAINING_STEPS / 100

def lstm_model(time_steps, rnn_layers, dense_layers=None):
def lstm_cells(layers):
return
[tf.nn.rnn_cell.BasicLSTMCell(layer['steps'],state_is_tuple=True)
for layer in layers]

def dnn_layers(input_layers, layers):
return input_layers

def _lstm_model(X, y):
stacked_lstm = tf.nn.rnn_cell.MultiRNNCell(lstm_cells(rnn_layers), state_is_tuple=True)
x_ = learn.ops.split_squeeze(1, time_steps, X)
output, layers = tf.nn.rnn(stacked_lstm, x_, dtype=dtypes.float32)
output = dnn_layers(output[-1], dense_layers)
return learn.models.linear_regression(output, y)

return _lstm_model

regressor = learn.TensorFlowEstimator(model_fn=lstm_model(TIMESTEPS,
RNN_LAYERS, DENSE_LAYERS), n_classes=0,
verbose=2, steps=TRAINING_STEPS,
optimizer='Adagrad',
learning_rate=0.03,
batch_size=BATCH_SIZE)

df = pd.read_csv("data/elec_load.csv", error_bad_lines=False)
plt.subplot()
plot_test, = plt.plot(df.values[:1500], label='Load')
plt.legend(handles=[plot_test])

print df.describe()
array=(df.values- 147.0) /339.0
plt.subplot()
plot_test, = plt.plot(array[:1500], label='Normalized Load')
plt.legend(handles=[plot_test])
listX = []
listy = []
X={}
y={}

for i in range(0,len(array)-6):
listX.append(array[i:i+5].reshape([5,1]))
listy.append(array[i+6])

arrayX=np.array(listX)
arrayy=np.array(listy)

X['train']=arrayX[0:12000]
X['test']=arrayX[12000:13000]
X['val']=arrayX[13000:14000]

y['train']=arrayy[0:12000]
y['test']=arrayy[12000:13000]
y['val']=arrayy[13000:14000]

# print y['test'][0]
# print y2['test'][0]

#X1, y2 = generate_data(np.sin, np.linspace(0, 100, 10000), TIMESTEPS,
seperate=False)
# create a lstm instance and validation monitor
validation_monitor = learn.monitors.ValidationMonitor(X['val'], y['val'],
every_n_steps=PRINT_STEPS,
early_stopping_rounds=1000)

regressor.fit(X['train'], y['train'], monitors=[validation_monitor],
logdir=LOG_DIR)

predicted = regressor.predict(X['test'])
rmse = np.sqrt(((predicted - y['test']) ** 2).mean(axis=0))
score = mean_squared_error(predicted, y['test'])
print ("MSE: %f" % score)

#plot_predicted, = plt.plot(array[:1000], label='predicted')

plt.subplot()
plot_predicted, = plt.plot(predicted, label='predicted')

plot_test, = plt.plot(y['test'], label='test')
plt.legend(handles=[plot_predicted, plot_test])

7.3　例2——创作巴赫风格的曲目

在本例中，我们将循环神经网络用于字符序列，也称作字符 RNN模型。

本例中，我们的训练集是巴赫的哥德堡变奏曲，数据以字符的格式给出，通过将其输入网络，RNN学习变奏曲的结构，最后基于学习到的结构谱出新的曲目。

本例中的许多想法和概念来自于论文《循环神经网络的可视化和理解》(Visualizing and Understanding Recurrent Networks , https://arxiv.org/abs/1506.02078), 以及文章《高效得不可思议的循环神经网络》（The Unreasonable Effectiveness of

recurrent neural networks, http://karpathy.github.io/2015/05/21/ rnn-effectiveness/）

7.3.1　字符级模型

正如我们之前所说，字符RNN模型可以处理字符序列。这种模型的输入可以是各种语言，包括：

- 程序代码。

- 不同人的语言（用于建模某个作者的写作风格）。

- 科学论文（tex）等。

7.3.2　字符串序列和概率表示

RNN的输入需要一个清晰并且直接的表示。基于此，我们选择了一位有效（one hot）的表示方式。一位有效的编码对于输出的可能性是有限的情况（也就是分类的情况），使用起来非常方便，可以直接用来与Softmax函数的值比较。字符输出正是这样一种情况，因为字符总共10个，是有限的。

这样，模型的输入就是一个字符串序列，而模型的输出是一个阵列（array）序列。阵列的长度跟字典的大小相同，每个元素的数字的含义是，给定序列前一个的字符，当前位置取该字符的可能性。

在图7-17中，我们可以看到一个简单的配置模型。输入就是一位有效的编码方式，然后通过神经网络预测输出。

图7-17　配置模型

7.3.3　使用字符对音乐编码——ABC音乐格式

当我们寻求一种输入格式的时候，我们希望能够尽可能简单而又结构同质（structurally homogeneous）。

遍观各种音乐的表示，ABC音乐格式是非常不错的选择。因为它不仅结构简单，而且只用了很少的字符。而且所有的字符还都是ASCII字符集的子集。

1．ABC格式的构成

一个ABC格式文件由两部分组成：文件头和文件体。

- 文件头：文件头包含多个键值对，如X:[序号]，T:[歌曲名]，M:[拍号]，K:[调号]，C:[创作人]。

- 文件体：文件体紧接在文件头之后，包含每个小节的内容，每个小节之间用“|”字符分开。

ABC记谱法还有其他的元素，但是在下面的例子中，你只要知道这种格式是怎样工作的，哪怕没有经过音乐训练也可以。

X:1
T:Notes
M:C
L:1/4
K:C
C, D, E, F,|G, A, B, C|D E F G|A B c d|e f g a|b c' d' e'|f' g' a' b'|]

上面的文本所承载音乐的五线谱如图7-18所示。

图7-18　五线谱

2．哥德堡变奏曲

巴赫的哥德堡变奏曲是一首咏叹曲，由30个变奏组成，以巴赫的学生（Johann Gottlieb Goldberg）命名。该学生可能是该曲目的主要演奏者。

X:1
T:Variation no. 1
C:J.S.Bach
M:3/4
L:1/16
Q:500
V:2 bass
K:G
[V:1]GFG2- GDEF GAB^c |d^cd2- dABc defd |gfg2- gfed ^ceAG|
[V:2]G,,2B,A, B,2G,2G,,2G,2 |F,,2F,E, F,2D,2F,,2D,2 |E,,2E,D,
E,2G,2A,,2^C2|
% (More parts with V:1 and V:2)

图7-19展示的是变奏曲Nr 1的第一部分，通过学习这一部分可帮助我们理解将要模拟的文件结构。

图7-19　变奏曲Nr 1的第一部分

7.3.4　有用的库和方法

该部分，我们会介绍本例中新增的函数。

1．保存和恢复变量和模型

实际应用机器学习的时候，有一个非常重要的需求就是保存和恢复整个模型。TensorFlow通过tf.train.Saver对象来实现这一功能。

该对象的主要方法如下：

- tf.train.Saver（args）：这是构造函数，下面是其主要参数。

- var_list：这是所有需要保存的变量的列表。例如，{ firstvar: var1, secondvar: var2}。如果为none，保存所有的对象。

- max_to_keep：该变量表示维护的检查点（checkpoint）的最大数目。

- write_version：该变量表示格式的版本，实际上，现在只有1是合法的。

- tf.train,Saver.save：用于保存构造函数中添加的变量。它需要启动图的session。被保存的变量必须已初始化。其主要参数如下。

- session：用于保存变量的对话。

- save_path：检查点文件的路径。

- global_step：全局训练步骤的标识。

- tf.train.Saver.restore：这个方法能够恢复保存过的变量，包括两个主要参数。

- session：用于数据恢复的会话。

- save_path：save方法返回的路径，可以调用latest_checkpoint()，也可以自己提供。

2．加载和保存数据的伪代码

此处，我们仅仅演示一下如何保存和获取两个变量数据。

（1）变量保存

以下代码用于创建和保存变量：

# Create some variables.
simplevar = tf.Variable(..., name="simple")
anothervar = tf.Variable(..., name="another")
…
# Add ops to save and restore all the variables.
saver = tf.train.Saver()
# Later, launch the model, initialize the variables, do some work, save the
# variables to disk.
with tf.Session() as sess:
sess.run(tf.initialize_all_variables())
# Do some work with the model.
..
# Save the variables to disk.
save_path = saver.save(sess, "/tmp/model.ckpt")

（2）变量恢复

下面的代码用于恢复变量：

saver = tf.train.Saver()
# Later, launch the model, use the saver to restore variables from disk, and
# do some work with the model.
with tf.Session() as sess:
#Work with the restored model....

7.3.5　数据集描述和加载

对于这个数据集，我们使用30个作品，产生1000个实例，并且随机打乱。

import random
input = open('input.txt', 'r').read().split('X:')
for i in range (1,1000):
print "X:" + input[random.randint(1,30)] +
"\n_____________________________________\n"

7.3.6　网络训练

该网络的训练数据是30部ABC格式的音乐作品。

ABC格式的原始文档可以从该网站获取：http://www.barfly. dial.pipex.com/Goldbergs.abc。

我们使用以下的小程序生成训练样本。

对于这个数据集，我们拥有30个作品，我们将其按照随机分布，生成1000个实例的列表：

import random
input = open('original.txt', 'r').read().split('X:')
for i in range (1,1000):
print "X:" + input[random.randint(1,30)] +
"\n_____________________________________\n"

然后我们运行如下命令，生成数据集：

python generate_dataset.py > input.txt

7.3.7　数据集预处理

原始数据并不能直接拿来使用。为了使用该数据，我们首先需要定义字典。

1．字典定义

数据处理的第一步是找到原文档中所有的字符，这能让我们确定维度，并对每个字符按照一位有效（one-hot）的格式进行编码。

图7-20是所有ABC音乐格式文档的字符，包括了音乐符号和标点符号。

图7-20　所有ABC音乐格式文档的字符

2．模型架构

我们使用一个多层LSTM来实现该模型，设置其初始状态为零。

cell_fn = rnn_cell.BasicLSTMCell
cell = cell_fn(args.rnn_size, state_is_tuple=True)
self.cell = cell = rnn_cell.MultiRNNCell([cell] * args.num_layers,
state_is_tuple=True)
self.input_data = tf.placeholder(tf.int32, [args.batch_size,
args.seq_length])
self.targets = tf.placeholder(tf.int32, [args.batch_size,
args.seq_length])
self.initial_state = cell.zero_state(args.batch_size, tf.float32)
with tf.variable_scope('rnnlm'):
softmax_w = tf.get_variable("softmax_w", [args.rnn_size,
args.vocab_size])
softmax_b = tf.get_variable("softmax_b", [args.vocab_size])
with tf.device("/cpu:0"):
embedding = tf.get_variable("embedding", [args.vocab_size,
args.rnn_size])
inputs = tf.split(1, args.seq_length,
tf.nn.embedding_lookup(embedding, self.input_data))
inputs = [tf.squeeze(input_, [1]) for input_ in inputs]
def loop(prev, _):
prev = tf.matmul(prev, softmax_w) + softmax_b
prev_symbol = tf.stop_gradient(tf.argmax(prev, 1))
return tf.nn.embedding_lookup(embedding, prev_symbol)
outputs, last_state = seq2seq.rnn_decoder(inputs,
self.initial_state, cell, loop_function=loop if infer else None,
scope='rnnlm')
output = tf.reshape(tf.concat(1, outputs), [-1, args.rnn_size])

7.3.8　损失函数描述

损失函数由函数loss_by_exampled定义。该损失函数用来度量概率分布的预测结果与样本的契合程度，专业名称是困惑度（perplexity）。这种度量在语言模型中大量使用：

self.logits = tf.matmul(output, softmax_w) + softmax_b
self.probs = tf.nn.softmax(self.logits)
loss = seq2seq.sequence_loss_by_example([self.logits],
[tf.reshape(self.targets, [-1])],
[tf.ones([args.batch_size * args.seq_length])],
args.vocab_size)
self.cost = tf.reduce_sum(loss) / args.batch_size / args.seq_length

7.3.9　停止条件

当epochs的数值和batch的数值同时达到时，程序迭代完成。代码块为：

if (e==args.num_epochs-1 and b == data_loader.num_batches-1)

7.3.10　结果描述

首先执行训练脚本，代码为：

python train.py

然后执行采样程序，代码为：

python sample.py

只要给定一个X:1作为序列的首字符，我们就可以按照深度（推荐为3）和长度（推荐为512）进行作曲了。

下面的曲谱是由网站http://www.drawthedots.com/实现的，如图7-21所示。我们只需要将结果输入到该网站，该网站即为我们自动生成五线曲谱。

图7-21　自动生成的乐曲曲谱

7.3.11　完整源代码

下面是完整的源代码（train.py）：

from __future__ import print_function
import numpy as np
import tensorflow as tf

import argparse
import time
import os
from six.moves import cPickle
from utils import TextLoader
from model import Model
class arguments:
def __init__(self):
return
def main():
args = arguments()
train(args)
def train(args):
args.data_dir='data/'; args.save_dir='save'; args.rnn_size =64;
args.num_layers=1; args.batch_size=50;args.seq_length=50
args.num_epochs=5;args.save_every=1000; args.grad_clip=5.
args.learning_rate=0.002; args.decay_rate=0.97
data_loader = TextLoader(args.data_dir, args.batch_size,
args.seq_length)
args.vocab_size = data_loader.vocab_size
with open(os.path.join(args.save_dir, 'config.pkl'), 'wb') as f:
cPickle.dump(args, f)
with open(os.path.join(args.save_dir, 'chars_vocab.pkl'), 'wb') as f:
cPickle.dump((data_loader.chars, data_loader.vocab), f)
model = Model(args)
with tf.Session() as sess:
tf.initialize_all_variables().run()
saver = tf.train.Saver(tf.all_variables())
for e in range(args.num_epochs):
sess.run(tf.assign(model.lr, args.learning_rate *(args.decay_rate ** e)))
data_loader.reset_batch_pointer()
state = sess.run(model.initial_state)
for b in range(data_loader.num_batches):
start = time.time()
x, y = data_loader.next_batch()
feed = {model.input_data: x, model.targets: y}
for i, (c, h) in enumerate(model.initial_state):
feed[c] = state[i].c
feed[h] = state[i].h
train_loss, state, _ = sess.run([model.cost,model.final_state, model.train_op], feed)
end = time.time()
print("{}/{} (epoch {}), train_loss = {:.3f}, time/batch ={:.3f}" \
.format(e * data_loader.num_batches + b,
args.num_epochs * data_loader.num_batches,
e, train_loss, end - start))
if (e==args.num_epochs-1 and b ==
data_loader.num_batches-1): # save for the last result
checkpoint_path = os.path.join(args.save_dir,'model.ckpt')
saver.save(sess, checkpoint_path, global_step = e *data_loader.num_batches + b)
print("model saved to {}".format(checkpoint_path))

if __name__ == '__main__':
main()

下面是完整的源代码（model.py）：

import tensorflow as tf
from tensorflow.python.ops import rnn_cell
from tensorflow.python.ops import seq2seq
import numpy as np

class Model():
def __init__(self, args, infer=False):
self.args = args
if infer: #When we sample, the batch and sequence lenght are = 1
args.batch_size = 1
args.seq_length = 1
cell_fn = rnn_cell.BasicLSTMCell #Define the internal cell structure
cell = cell_fn(args.rnn_size, state_is_tuple=True)
self.cell = cell = rnn_cell.MultiRNNCell([cell] * args.num_layers, state_is_tuple=True)
#Build the inputs and outputs placeholders, and start with a zero internal values
self.input_data = tf.placeholder(tf.int32, [args.batch_size, args.seq_length])
self.targets = tf.placeholder(tf.int32, [args.batch_size, args.seq_length])
self.initial_state = cell.zero_state(args.batch_size, tf.float32)
with tf.variable_scope('rnnlm'):
softmax_w = tf.get_variable("softmax_w", [args.rnn_size, args.vocab_size]) #Final w
softmax_b = tf.get_variable("softmax_b", [args.vocab_size]) #Final bias
with tf.device("/cpu:0"):
embedding = tf.get_variable("embedding", [args.vocab_size, args
.rnn_size])
inputs = tf.split(1, args.seq_length, tf.nn.embedding_lookup(embedding, self.input_data))
inputs = [tf.squeeze(input_, [1]) for input_ in inputs]
def loop(prev, _):
prev = tf.matmul(prev, softmax_w) + softmax_b
prev_symbol = tf.stop_gradient(tf.argmax(prev, 1))
return tf.nn.embedding_lookup(embedding, prev_symbol)
outputs, last_state = seq2seq.rnn_decoder(inputs, self.initial_state, cell, loop_function=loop if infer else None, scope='rnnlm')
output = tf.reshape(tf.concat(1, outputs), [-1, args.rnn_size])
self.logits = tf.matmul(output, softmax_w) + softmax_b
self.probs = tf.nn.softmax(self.logits)
loss = seq2seq.sequence_loss_by_example([self.logits],
[tf.reshape(self.targets, [-1])],
[tf.ones([args.batch_size * args.seq_length])],
args.vocab_size)
self.cost = tf.reduce_sum(loss) / args.batch_size / args.seq_length
self.final_state = last_state
self.lr = tf.Variable(0.0, trainable=False)
tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, tvars),
args.grad_clip)
optimizer = tf.train.AdamOptimizer(self.lr)
self.train_op = optimizer.apply_gradients(zip(grads, tvars))
def sample(self, sess, chars, vocab, num=200, prime='START', sampling_type=1):
state = sess.run(self.cell.zero_state(1, tf.float32))
for char in prime[:-1]:
x = np.zeros((1, 1))
x[0, 0] = vocab[char]
feed = {self.input_data: x, self.initial_state:state}
[state] = sess.run([self.final_state], feed)
def weighted_pick(weights):
t = np.cumsum(weights)
s = np.sum(weights)
return(int(np.searchsorted(t, np.random.rand(1)*s)))
ret = prime
char = prime[-1]
for n in range(num):
x = np.zeros((1, 1))
x[0, 0] = vocab[char]
feed = {self.input_data: x, self.initial_state:state}
[probs, state] = sess.run([self.probs, self.final_state], feed)
p = probs[0]
sample = weighted_pick(p)
pred = chars[sample]
ret += pred
char = pred
return ret

下面是完整的源代码（utils.py）：

import codecs
import os
import collections
from six.moves import cPickle
import numpy as np

class TextLoader():
def __init__(self, data_dir, batch_size, seq_length, encoding='utf-8'):
self.data_dir = data_dir
self.batch_size = batch_size
self.seq_length = seq_length
self.encoding = encoding

input_file = os.path.join(data_dir, "input.txt")
vocab_file = os.path.join(data_dir, "vocab.pkl")
tensor_file = os.path.join(data_dir, "data.npy")

if not (os.path.exists(vocab_file) and
os.path.exists(tensor_file)):
print("reading text file")
self.preprocess(input_file, vocab_file, tensor_file)
else:
print("loading preprocessed files")
self.load_preprocessed(vocab_file, tensor_file)
self.create_batches()
self.reset_batch_pointer()

def preprocess(self, input_file, vocab_file, tensor_file):
with codecs.open(input_file, "r", encoding=self.encoding) as f:
data = f.read()
counter = collections.Counter(data)
count_pairs = sorted(counter.items(), key=lambda x: -x[1])
self.chars, _ = zip(*count_pairs)
self.vocab_size = len(self.chars)
self.vocab = dict(zip(self.chars, range(len(self.chars))))
with open(vocab_file, 'wb') as f:
cPickle.dump(self.chars, f)
self.tensor = np.array(list(map(self.vocab.get, data)))
np.save(tensor_file, self.tensor)

def load_preprocessed(self, vocab_file, tensor_file):
with open(vocab_file, 'rb') as f:
self.chars = cPickle.load(f)
self.vocab_size = len(self.chars)
self.vocab = dict(zip(self.chars, range(len(self.chars))))
self.tensor = np.load(tensor_file)
self.num_batches = int(self.tensor.size / (self.batch_size *
self.seq_length))

def create_batches(self):
self.num_batches = int(self.tensor.size / (self.batch_size *
self.seq_length))

self.tensor = self.tensor[:self.num_batches * self.batch_size * self.seq
_length]
xdata = self.tensor
ydata = np.copy(self.tensor)
ydata[:-1] = xdata[1:]
ydata[-1] = xdata[0]
self.x_batches = np.split(xdata.reshape(self.batch_size, -1), self.num_batches, 1)
self.y_batches = np.split(ydata.reshape(self.batch_size, -1), self.num_batches, 1)

def next_batch(self):
x, y = self.x_batches[self.pointer], self.y_batches[self.pointer]
self.pointer += 1
return x, y

def reset_batch_pointer(self):
self.pointer = 0

7.4　小结

本章中，我们学习了最新的神经网络架构——循环神经网络。随着本章的结束，我们也完成了对主流机器学习方法的探索。

在后面的章节中，我们将会研究怎样将不同的神经网络层组合，以达到更好的效果。此外，我们还会覆盖一些有趣的实验模型。

第8章　深度神经网络

本章中，我们会学习当前深度学习领域中优秀的模型：深度神经网络（Deep neural networks，DNN）。

8.1　深度神经网络的定义

这个领域正面临着快速的变化，每天我们都能听到DNN被用于解决新的问题，比如计算机视觉、自动驾驶、语音识别和文本理解等。

在前面的章节中，我们已经使用过DNN技术的相关技术，尤其是卷积神经网络。

出于实践的角度，我们这儿所说的深度学习和深度神经网络是指深度超过几个相似层的神经网络结构，一般能够到达几十层，或者是由一些复杂的模块组合而成。

8.2　深度网络结构的历史变迁

本部分，我们会回顾深度学习的每一个里程碑，从LeNet 5开始。

8.2.1　LeNet 5

神经网络领域在20世纪八九十年代还很寂静。尽管有很多努力，但是那时的网络结构还比较简单，而且往往需要巨大的（通常是达不到的）计算资源。

大概在1998年，在贝尔实验室，Yan LeCun在研究手写数字分类的时候，提出了卷积神经网络。卷积神经网络是当前的深度神经网络的基石之一，我们已经在第6章，卷积神经网络中有所介绍。

在那个年代，SVM和其他的机器学习工具才是处理这类问题的主流，但是CNN的那篇奠基之作，表明神经网络也能够达到甚至是超过当时最好的处理结果。

8.2.2　Alexnet

经过多年的中断（尽管在此期间，LeCun继续将他的神经网络模型延伸到其他任务，如人脸和对象检测），神经网络终于迎来了复苏。结构化数据和计算机处理能力的爆炸性增长使得深度学习成为可能。过去要训练数月的网络，现在能够在比较短的时间内训练完成。

来自多家公司和大学的多个团队开始用深度神经网络处理各种问题，包括图像识别。其中，一个著名的比赛叫作ImageNet图像分类，Alexnet就是为了这个测试所开发，如图8-1所示。

图8-1　Alexnet结构

Alexnet可以被看作LeNet 5的扩展，也就是说第一层用的是卷积神经网络，然后连接上一个不常用的最大池化层，然后是几个全链接层，在最后一层输出概率。

8.2.3　VGG模型

图像分类挑战的另一个主要的竞争者是牛津大学的VGG团队。

VGG（Visual Geometry Group，可视化几何团队）网络结构的主要特点，就是减小了卷积滤波的大小，只用一个3×3的滤波器，并将它们不断组合，如图8-2所示。

这种小型化的滤波器是对LeNet以及其继任者Alexnet的一个突破，这两个的网络滤波器都是设为11×11。小型化滤波器的操作引领了一个新的潮流，并且一直延续到现在。

但是尽管滤波器变小了，但是总体参数依然非常大（通常有几百万个参数），所以还需要改进。

图8-2　VGG的参数数目

8.2.4　第一代Inception模型

在Alexnet和VGG统治了深度学习一两年之后，谷歌公司发布了他们的深度学习模型——Inception。到现在为止，Inception已经发布了好几个版本。

第一个版本的Inception是GoogLeNet，如图8-3所示。从图上看，它的结构模型很深，但是本质上它是通过堆叠9个基本上没有怎么改变的Inception模块。

尽管如此复杂，但是相比于Alexnet，Inception减少了参数的数量，增加了准确率。

Inception的可解释性和可扩展性相对于Alexnet也有所增加。因为事实上，该模型结构就是堆叠相似的结构。

图8-3　第一代Inception模型

8.2.5　第二代Inception模型

2015年的时候，深度学习还有一个问题，就是随着一次又一次的迭代，深度神经网络稳定性不够好。

为了理解这个稳定性的问题，首先，我们回忆一下前面例子中使用过的标准化，包含中心化，以及将标准差归一化。主要是为了反向传播（back propagation）的梯度。

在真正大型数据集的训练过程中发生的是，在经过多次训练的例子之后，不同的振荡频率开始放大平均参数值，如共振现象，我们称之为协方差漂移。

这就是使用批量标准化的主要原因。

为了简化过程描述，批量标准化不仅仅应用于输入层，它应用于每一层，在它们开始影响或者漂移数值之前。

这就是Inception V2的主要特点，谷歌公司在2015年1月份发布，提升了GoogLeNet。

8.2.6　第三代Inception模型

时间来到2015年12月，Inception的结构又经历了一轮新的迭代。发布之间的月份差异使我们了解到新迭代的发展速度。

新的架构有如下调整：

- 降低了卷积滤波器的大小，最大是3×3；

- 增加了网络的深度；

- 使用宽度增加技术加了每层提高特征组合。

图8-4说明了如何解释改进的Inception模块。

图8-4　改进的Inception模块

图8-5是整个V3模型的结构图，由许多新的Inception模块的实例拼接而成。

图8-5　Inception V3模型

8.2.7　残差网络（ResNet）

残差神经网络结构出现在2015年12月（基本上跟Inception V3的发布时间差不多）。ResNet带来了一个简单但是很新颖的想法，不仅仅使用卷积层的输出，还要结合上原始输入层的输出。

在图8-6中，我们可以看到一个简单的ResNet的模块：3个卷积层的堆积和一个relu操作。

图8-6　残差网络一般结构

卷积的部分包括了一个从256降到64的操作，使用一个3×3的滤波层来控制特征数目，增补一个1×1的层。在最近的发展中，ResNet也被用在少于30层的网络结构中。

8.2.8　其他的深度神经网络结构

有许多最先发展的深度神经网络的结构；实际上，这个领域每天的变化都是如此之快，几乎每天多少都有几个优秀的结构冒出来。下面是几个最有前途的网络结构：

- SqueezeNet：该网络为了简化Alexnet的参数数量，宣称可以达到50×参数数量的降低。

- 高效神经网络(Efficient Neural NetworkEnet)：用于减少浮点操作，实现实时的神经网络。

- Fractalnet：用于实现非常深的深度网络。该网络不使用残差结构，而是实现一种分形（fractal）结构。

8.3　例子——VGG艺术风格转移

本例中，我们会实现Leon Gatys的论文《艺术风格的神经网络算法》。

源代码由Anish Athalye提供（http://www.anishathalye.com/）。

注意的是，本例并没有训练的部分。我们会加载一个由VLFeat提供的已经训练好的系数矩阵。该矩阵已经经过了大量的训练，我们可以直接使用，避免了大量的训练计算。

8.3.1　有用的库和方法

- 使用scipy.io.loadmat加载参数文件。

- 第一个有用的库是scipy的io模块，我们使用该模块来加载MATLAB软件的mat文件格式。

- 使用方法。

scipy.io.loadmat(file_name, mdict=None, appendmat=True, **kwargs)

- 返回处理参数。

- mat_dict：返回的是一个字典，变量名对应字典的“键”，变量值对应字典的“值”。

8.3.2　数据集描述和加载

本例的方法，我们使用一个已经训练好的数据集，也就是使用Imagenet数据集训练好的VGG神经网络的参数。

8.3.3　数据集预处理

因为本例使用的是训练好的参数矩阵，所以除了初始化数据集，没有太多的数据集预处理工作。

8.3.4　模型结构

本模型结构主要分为两个部分：风格和内容。

对于最终胜出的图像，我们需要不带最终全链接层的VGG网络。

8.3.5　损失函数

本结构的参数损失函数分为两个部分，对于最终图像，一部分是内容，另一部分是风格。

1．内容损失函数

内容损失函数在content_loss函数里面：

# content loss
content_loss = content_weight * (2 * tf.nn.l2_loss(
net[CONTENT_LAYER] - content_features[CONTENT_LAYER]) /
content_features[CONTENT_LAYER].size)

2．损失函数优化循环

优化循环的代码如下：

best_loss = float('inf')
best = None
with tf.Session() as sess:
sess.run(tf.initialize_all_variables())
for i in range(iterations):
last_step = (i == iterations - 1)
print_progress(i, last=last_step)
train_step.run()

if (checkpoint_iterations and i % checkpoint_iterations ==
0) or last_step:
this_loss = loss.eval()
if this_loss < best_loss:
best_loss = this_loss
best = image.eval()
yield (
(None if last_step else i),
vgg.unprocess(best.reshape(shape[1:]), mean_pixel)
)

8.3.6　收敛性测试

本例中的收敛性测试就是检查迭代次数是否达到（写在迭代参数里面）。

8.3.7　程序执行

为了执行该程序，我们需要选择一个好的迭代次数，100是一个不错的经验值。推荐8GB以上的可用内存。

python neural_style.py --content examples/2-content.jpg --styles
examples/2-style1.jpg --checkpoint-iterations=100 --iterations=1000 --
checkpoint-output=out%s.jpg --output=outfinal

上述命令的结果如图8-7所示。

图8-7　风格转移中间过程

该命令行的输出如下：

Iteration 1/1000
Iteration 2/1000
Iteration 3/1000
Iteration 4/1000
...
Iteration 999/1000
Iteration 1000/1000
content loss: 908786
style loss: 261789
tv loss: 25639.9
total loss: 1.19621e+06

8.3.8　完整源代码

完整源代码如下（neural_style.py）：

import os

import numpy as np
import scipy.misc

from stylize import stylize

import math
from argparse import ArgumentParser

# default arguments
CONTENT_WEIGHT = 5e0
STYLE_WEIGHT = 1e2
TV_WEIGHT = 1e2
LEARNING_RATE = 1e1
STYLE_SCALE = 1.0
ITERATIONS = 100
VGG_PATH = 'imagenet-vgg-verydeep-19.mat'

def build_parser():
parser = ArgumentParser()
parser.add_argument('--content',
dest='content', help='content image',
metavar='CONTENT', required=True)
parser.add_argument('--styles',
dest='styles',
nargs='+', help='one or more style images',
metavar='STYLE', required=True)
parser.add_argument('--output',
dest='output', help='output path',
metavar='OUTPUT', required=True)
parser.add_argument('--checkpoint-output',
dest='checkpoint_output', help='checkpoint output format',
metavar='OUTPUT')
parser.add_argument('--iterations', type=int,
dest='iterations', help='iterations (default %(default)s)',
metavar='ITERATIONS', default=ITERATIONS)
parser.add_argument('--width', type=int,
dest='width', help='output width',
metavar='WIDTH')
parser.add_argument('--style-scales', type=float,
dest='style_scales',
nargs='+', help='one or more style scales',
metavar='STYLE_SCALE')
parser.add_argument('--network',
dest='network', help='path to network parameters (default
%(default)s)',
metavar='VGG_PATH', default=VGG_PATH)
parser.add_argument('--content-weight', type=float,
dest='content_weight', help='content weight (default
%(default)s)',
metavar='CONTENT_WEIGHT', default=CONTENT_WEIGHT)
parser.add_argument('--style-weight', type=float,
dest='style_weight', help='style weight (default %(default)s)',
metavar='STYLE_WEIGHT', default=STYLE_WEIGHT)
parser.add_argument('--style-blend-weights', type=float,
dest='style_blend_weights', help='style blending weights',
nargs='+', metavar='STYLE_BLEND_WEIGHT')
parser.add_argument('--tv-weight', type=float,
dest='tv_weight', help='total variation regularization weight
(default %(default)s)',
metavar='TV_WEIGHT', default=TV_WEIGHT)
parser.add_argument('--learning-rate', type=float,
dest='learning_rate', help='learning rate (default
%(default)s)',
metavar='LEARNING_RATE', default=LEARNING_RATE)
parser.add_argument('--initial',
dest='initial', help='initial image',
metavar='INITIAL')
parser.add_argument('--print-iterations', type=int,
dest='print_iterations', help='statistics printing frequency',
metavar='PRINT_ITERATIONS')
parser.add_argument('--checkpoint-iterations', type=int,
dest='checkpoint_iterations', help='checkpoint frequency',
metavar='CHECKPOINT_ITERATIONS')
return parser

def main():
parser = build_parser()
options = parser.parse_args()

if not os.path.isfile(options.network):
parser.error("Network %s does not exist. (Did you forget to
download it?)" % options.network)

content_image = imread(options.content)
style_images = [imread(style) for style in options.styles]

width = options.width
if width is not None:
new_shape = (int(math.floor(float(content_image.shape[0]) /
content_image.shape[1] * width)), width)
content_image = scipy.misc.imresize(content_image, new_shape)
target_shape = content_image.shape
for i in range(len(style_images)):
style_scale = STYLE_SCALE
if options.style_scales is not None:
style_scale = options.style_scales[i]
style_images[i] = scipy.misc.imresize(style_images[i], style_scale
*

target_shape[1] / style_images[i].shape[1])

style_blend_weights = options.style_blend_weights
if style_blend_weights is None:
# default is equal weights
style_blend_weights = [1.0/len(style_images) for _ in style_images]
else:
total_blend_weight = sum(style_blend_weights)
style_blend_weights = [weight/total_blend_weight
for weight in style_blend_weights]

initial = options.initial
if initial is not None:
initial = scipy.misc.imresize(imread(initial),
content_image.shape[:2])

if options.checkpoint_output and "%s" not in options.checkpoint_output:
parser.error("To save intermediate images, the checkpoint output "
"parameter must contain `%s` (e.g. `foo%s.jpg`)")

for iteration, image in stylize(
network=options.network,
initial=initial,
content=content_image,
styles=style_images,
iterations=options.iterations,
content_weight=options.content_weight,
style_weight=options.style_weight,
style_blend_weights=style_blend_weights,
tv_weight=options.tv_weight,
learning_rate=options.learning_rate,
print_iterations=options.print_iterations,
checkpoint_iterations=options.checkpoint_iterations
):
output_file = None
if iteration is not None:
if options.checkpoint_output:
output_file = options.checkpoint_output % iteration
else:
output_file = options.output
if output_file:
imsave(output_file, image)

def imread(path):
return scipy.misc.imread(path).astype(np.float)

def imsave(path, img):
img = np.clip(img, 0, 255).astype(np.uint8)
scipy.misc.imsave(path, img)

if __name__ == '__main__':
main()

完整源代码如下（Stilize.py）：

import vgg

import tensorflow as tf
import numpy as np

from sys import stderr

CONTENT_LAYER = 'relu4_2'
STYLE_LAYERS = ('relu1_1', 'relu2_1', 'relu3_1', 'relu4_1', 'relu5_1')

try:
reduce
except NameError:
from functools import reduce

def stylize(network, initial, content, styles, iterations,
content_weight, style_weight, style_blend_weights, tv_weight,
learning_rate, print_iterations=None, checkpoint_iterations=None):
"""
Stylize images.

This function yields tuples (iteration, image); `iteration` is None
if this is the final image (the last iteration). Other tuples are
yielded
every `checkpoint_iterations` iterations.
:rtype: iterator[tuple[int|None,image]]
"""
shape = (1,) + content.shape
style_shapes = [(1,) + style.shape for style in styles]
content_features = {}
style_features = [{} for _ in styles]

# compute content features in feedforward mode
g = tf.Graph()
with g.as_default(), g.device('/cpu:0'), tf.Session() as sess:
image = tf.placeholder('float', shape=shape)
net, mean_pixel = vgg.net(network, image)
content_pre = np.array([vgg.preprocess(content, mean_pixel)])
content_features[CONTENT_LAYER] = net[CONTENT_LAYER].eval(
feed_dict={image: content_pre})

# compute style features in feedforward mode
for i in range(len(styles)):
g = tf.Graph()
with g.as_default(), g.device('/cpu:0'), tf.Session() as sess:
image = tf.placeholder('float', shape=style_shapes[i])
net, _ = vgg.net(network, image)
style_pre = np.array([vgg.preprocess(styles[i], mean_pixel)])
for layer in STYLE_LAYERS:
features = net[layer].eval(feed_dict={image: style_pre})
features = np.reshape(features, (-1, features.shape[3]))
gram = np.matmul(features.T, features) / features.size
style_features[i][layer] = gram

# make stylized image using backpropogation
with tf.Graph().as_default():
if initial is None:
noise = np.random.normal(size=shape, scale=np.std(content) * 0.1)
initial = tf.random_normal(shape) * 0.256
else:
initial = np.array([vgg.preprocess(initial, mean_pixel)])
initial = initial.astype('float32')
image = tf.Variable(initial)
net, _ = vgg.net(network, image)

# content loss
content_loss = content_weight * (2 * tf.nn.l2_loss(
net[CONTENT_LAYER] - content_features[CONTENT_LAYER]) /
content_features[CONTENT_LAYER].size)
# style loss
style_loss = 0
for i in range(len(styles)):
style_losses = []
for style_layer in STYLE_LAYERS:
layer = net[style_layer]
_, height, width, number = map(lambda i: i.value,
layer.get_shape())
size = height * width * number
feats = tf.reshape(layer, (-1, number))
gram = tf.matmul(tf.transpose(feats), feats) / size
style_gram = style_features[i][style_layer]
style_losses.append(2 * tf.nn.l2_loss(gram - style_gram) /
style_gram.size)
style_loss += style_weight * style_blend_weights[i] *
reduce(tf.add, style_losses)
# total variation denoising
tv_y_size = _tensor_size(image[:,1:,:,:])
tv_x_size = _tensor_size(image[:,:,1:,:])
tv_loss = tv_weight * 2 * (
(tf.nn.l2_loss(image[:,1:,:,:] - image[:,:shape[1]-1,:,:])
/
tv_y_size) +
(tf.nn.l2_loss(image[:,:,1:,:] - image[:,:,:shape[2]-1,:])
/
tv_x_size))
# overall loss
loss = content_loss + style_loss + tv_loss

# optimizer setup
train_step = tf.train.AdamOptimizer(learning_rate).minimize(loss)

def print_progress(i, last=False):
stderr.write('Iteration %d/%d\n' % (i + 1, iterations))
if last or (print_iterations and i % print_iterations == 0):
stderr.write(' content loss: %g\n' % content_loss.eval())
stderr.write(' style loss: %g\n' % style_loss.eval())
stderr.write(' tv loss: %g\n' % tv_loss.eval())
stderr.write(' total loss: %g\n' % loss.eval())

# optimization
best_loss = float('inf')
best = None
with tf.Session() as sess:
sess.run(tf.initialize_all_variables())
for i in range(iterations):
last_step = (i == iterations - 1)
print_progress(i, last=last_step)
train_step.run()

if (checkpoint_iterations and i % checkpoint_iterations ==
0) or last_step:
this_loss = loss.eval()
if this_loss < best_loss:
best_loss = this_loss
best = image.eval()
yield (
(None if last_step else i),
vgg.unprocess(best.reshape(shape[1:]), mean_pixel)
)

def _tensor_size(tensor):
from operator import mul
return reduce(mul, (d.value for d in tensor.get_shape()), 1)
vgg.py
import tensorflow as tf
import numpy as np
import scipy.io

def net(data_path, input_image):
layers = (
'conv1_1', 'relu1_1', 'conv1_2', 'relu1_2', 'pool1',

'conv2_1', 'relu2_1', 'conv2_2', 'relu2_2', 'pool2',

'conv3_1', 'relu3_1', 'conv3_2', 'relu3_2', 'conv3_3',
'relu3_3', 'conv3_4', 'relu3_4', 'pool3',

'conv4_1', 'relu4_1', 'conv4_2', 'relu4_2', 'conv4_3',
'relu4_3', 'conv4_4', 'relu4_4', 'pool4',

'conv5_1', 'relu5_1', 'conv5_2', 'relu5_2', 'conv5_3',
'relu5_3', 'conv5_4', 'relu5_4'
)

data = scipy.io.loadmat(data_path)
mean = data['normalization'][0][0][0]
mean_pixel = np.mean(mean, axis=(0, 1))
weights = data['layers'][0]

net = {}
current = input_image
for i, name in enumerate(layers):
kind = name[:4]
if kind == 'conv':
kernels, bias = weights[i][0][0][0][0]
# matconvnet: weights are [width, height, in_channels,
out_channels]
# tensorflow: weights are [height, width, in_channels,
out_channels]
kernels = np.transpose(kernels, (1, 0, 2, 3))
bias = bias.reshape(-1)
current = _conv_layer(current, kernels, bias)
elif kind == 'relu':
current = tf.nn.relu(current)
elif kind == 'pool':
current = _pool_layer(current)
net[name] = current

assert len(net) == len(layers)
return net, mean_pixel

def _conv_layer(input, weights, bias):
conv = tf.nn.conv2d(input, tf.constant(weights), strides=(1, 1, 1, 1),
padding='SAME')
return tf.nn.bias_add(conv, bias)

def _pool_layer(input):
return tf.nn.max_pool(input, ksize=(1, 2, 2, 1), strides=(1, 2, 2, 1),
padding='SAME')

def preprocess(image, mean_pixel):
return image - mean_pixel

def unprocess(image, mean_pixel):
return image + mean_pixel

8.4　小结

在本章中，我们学习了不同的深度神经网络结构。

我们学习了近几年来知名的结构VGG，并学会了如何使用该结构进行艺术风格转移。

下一章中，我们学习机器学习技术最有用的技术：图像处理单元（Graphical Processing Units）。我们将会学习怎样在TensorFlow中使用GPU支持，并且将其的运行时间与只有CPU运行的时间做对比。

第9章　规模化运行模型——GPU和服务

直到现在为止，我们都还只是在一个主机的CPU上运行程序，这意味着，我们最多只能在几个处理核上运行程序（低端的电脑为2核或4核，高端的处理器能多达16核）。

然而，即使如此强大的计算资源在面对庞大的计算任务时，还有时力不从心。

因此，我们需要开发一种分布式的训练和运行模型的方式，这就是分布式TensorFlow的意图。

在本章中，你将会学到：

- 如何查看TensorFlow拥有的计算资源；

- 如何将任务分配给一个计算节点中不同的计算单元；

- 如何记录CPU操作日志；

- 如何在拥有很多分布式计算单元的集群上分布计算。

9.1　TensorFlow中的GPU支持

TensorFlow拥有CPU和GPU的原生支持，如图9-1所示。因此，TensorFlow可以为不同的计算使用一个版本。

图9-1　TensorFlow中的GPU支持

9.2　打印可用资源和设备参数

在我们开始使用操作之前，TensorFlow可以帮我们反映出可用的计算资源。这样我们才能将我们的操作选择性分配到可用资源上。

9.2.1　计算能力查询

为了获取一台机器上的计算元素，你可以使用log_device_placement，当建立一个TensorFlow会话的时候，按照这种方式：

python
>>>Import tensorflow as tf
>>>sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))

该命令的输出如图9-2所示。

图9-2　命令输出

这个输出主要显示了，对于不同的CUDA库的信息，然后就是CPU的名字GRID K520，以及GPU的计算性能。

上面这段很长的输出，主要是告诉我们所用的CUDA版本、GPU的名称和GPU的计算能力。

9.2.2　选择CPU用于计算

如果你的机器有GPU可以使用，但是你还是想要用CPU，我们可以通过方法tf.Graph.device选择计算资源。

该函数的使用方法如下：

with tf.Graph.device(device_name_or_function):

该方法的参数是处理单元的字符串，或者是返回处理单元字符串的函数，或者为空。返回值是赋值了处理单元的上下文管理器。

如果参数是一个函数，那么每次操作就调用该函数，以决定在哪个处理单元上执行操作。这对于结合所有操作很有帮助。

9.2.3　设备名称

为了简化识别每个计算设备，我们使用了统一的命名格式：

设备ID格式

计算设备命名示例如下。

- “/cpu:0”：机器中的第一个CPU。

- “/gpu:0”：机器中的GPU，如果你有一个的话。

- “/gpu:1”：机器中的第二个GPU，以此类推。

如果机器中有GPU，而且没有任何其他的设置，TensorFlow会默认选择GPU作为计算设备。

9.3　例1——将一个操作指派给GPU

本例中，我们会使用两个张量，使用GPU加载，并在CUDA的环境中对两个张量相加（安装方法在第10章中介绍，如图9-3所示。

此处，我们可以看到两个常量的相加，在/gpu:0上实现计算。因为这个时候，会优先选择gpu0。正如上面所说，如果机器中存在GPU设备，TensorFlow将会优先选择GPU作为计算设备。

图9-3　张量相加

9.4　例2——并行计算Pi的数值

本例中，我们将学习并行计算，实现蒙特卡洛近似计算Pi。

蒙特卡洛方法就是使用随机数值序列来进行估算。

本例中，我们会随机产生很多样例抛入正方形中，如图9-4所示。我们知道，落入圆中的样本数量跟落入正方形中的样本数量跟两者的面积比例相同。

图9-4　随机面积计算方法

为了解决这个问题，我们随机生成样本，计算圆圈内的点和落在方框内的点的比例。

该计算假设分布是均匀的，那么两者中的样本比例应该跟两者的面积比例相同。

我们使用如下比例：

用面积比例计算Pi

由以上比例，我们能够推断出，落入圆圈中的样本和落入方块中的样本比例也应该是0.785。

另一个事实就是，我们生成的样本越多，那么我们计算的准确度应该会越高。这告诉我们，当我们增加GPU的数量时，我们可以增加样本，因此增加准确率。

进一步减少计算的方法就是限制（X,Y）坐标的范围，限制到(0,1)，这样我们生成的方式更直接。我们只要计算（圆的半径）。

9.4.1　实现方法

本方法只用CPU做计算；但是在本试验中，我们操作服务器中的GPU资源（本例中为4个），然后接受采样结果，做最终采样计算，如图9-5所示。

图9-5　计算任务时间线

本方法的收敛速度很慢，是o(n1/2)，但我们的例子不是追求速度，只是做一个最简单的示例。

在上面例子中，通过生成样本并进行计算，我们看到了怎样进行并行计算的操作。

9.4.2　源代码

完整的源代码如下：

import tensorflow as tf
import numpy as np
c = []
#Distribute the work between the GPUs
for d in ['/gpu:0', '/gpu:1', '/gpu:2', '/gpu:3']:
#Generate the random 2D samples
i=tf.constant(np.random.uniform(size=10000), shape=[5000,2])
with tf.Session() as sess:
tf.initialize_all_variables()
#Calculate the euclidean distance to the origin
distances=tf.reduce_sum(tf.pow(i,2),1)
#Sum the samples inside the circle
tempsum =
sess.run(tf.reduce_sum(tf.cast(tf.greater_equal(tf.cast(1.0,tf.float64),dis
tances),tf.float64)))
#append the current result to the results array
c.append( tempsum)
#Do the final ratio calculation on the CPU
with tf.device('/cpu:0'):
with tf.Session() as sess:
sum = tf.add_n(c)
print (sess.run(sum/20000.0)*4.0)

9.5　分布式TensorFlow

分布式TensorFlow可以计算拥有多个计算节点的集群，无缝在节点间分配任务。

在进行分布式计算之前，需要先设置分布式计算环境。因为我们需要计算的是大数据的、可扩展的计算，所以设置环境会不同于之前的计算。

9.5.1　分布式计算组件

本部分，我们描述一个分布式TensorFlow的设置，从最细粒度的任务元素到整个集群描述。

1．作业

一个作业（job）可以被拆分成多个具有相同目的的任务（task），一般有两种类型的作业。

- 参数服务器（parameter server）作业：保存和更新变量，发布各个参数节点的初始值和当前值。

- 工作（work）作业：进行计算操作。

2．任务

任务（task）是作业的分割，每个任务都会属于一个作业（job）。在作业内部，不同的任务用索引标识。一般来说，索引为0的任务是主任务或者协调任务。

3．服务器

服务器（sever）是为物理计算设备抽象出来的逻辑对象。每一个服务器都被排他性地分配给一个任务。

4．综合回顾

图9-6展示了一个集群中所有的部分。

图9-6　TensorFlow集群设置元素

图9-6中包含了两个作业，分别是参数服务器作业和工作作业。TensorFlow为各个部分之间的通信创建grpC（谷歌远程过程调用，google remote procedure call）通信通道。对于每个作业类型，都有几个不同的任务，能够将作业域问题的子问题分开解决。

9.5.2　创建TensorFlow集群

分布式集群编程的第一个任务就是创建一个ClusterSpec对象，用来包含真实的服务器的实际地址和端口，这是整个集群的一部分。

每个服务器只能对应一个任务，但是在一台机器上可以有多个任务（因为可能有多个GPU设备）。对于每个任务，做如下操作：

- 创建tf.train.ClusterSpec对象来描述一个集群中所有的任务。

- 将ClusterSpec对象传递给tf.train.Server的构造函数，将当前任务关联起一个作业名和任务索引。

1．ClusterSpec定义格式

ClusterSpec对象使用协议缓存（protocol buffer）格式，这是一种基于JSON的特殊格式。

格式如下：

{
"job1 name": [
"task0 server uri",
"task1 server uri"
...
]
...
"jobn name"[
"task0 server uri",
"task1 server uri"
]})
...

从上面的描述，我们可以知道下段创建了一个集群，一个参数服务器作业和3个工作作业。

tf.train.ClusterSpec({
"worker": [
"wk0.example.com:2222",
"wk1.example.com:2222",
"wk2.example.com:2222"
],
"ps": [
"ps0.example.com:2222",
]})

2．创建tf.train.Sever

定义ClusterSpec只是配置集群操作的第一步。下面我们使用tf.train.Server类创建一个本地服务器。

该类的构造函数需要的参数是：一个集群对象，一个作业名，还有一个任务索引：

server = tf.train.Server(cluster, job_name="local", task_index=[Number of
server])

9.5.3　集群操作——发送计算方法到任务

为了开始集群学习操作，我们需要学习操作资源的寻址。

首先，假设我们已经建立了一个集群，集群中有不同的作业和任务资源。集群中的任意资源的ID是符合如下格式的字符串：

想要调用上下文管理器中的资源，可以用如下的关键词和结构。

with tf.device("/job:ps/task:1"):
[代码块]

"/job:ps/task:1"字符串标识我们所调用的资源，在上下文管理器中使用。

图9-7展示了一个集群设置的示例，包含了所有的部分寻址的名称。

图9-7　服务器各元素命名

9.5.4　分布式编码结构示例

下面的示例代码设置了一个服务器和一个工作作业，并将不同的任务分配给不同的资源进行操作。

#Address the Parameter Server task
with tf.device("/job:ps/task:1"):
weights = tf.Variable(...)
bias = tf.Variable(...)

#Address the Parameter Server task
with tf.device("/job:worker/task:1"):
#... Generate and train a model
layer_1 = tf.nn.relu(tf.matmul(input, weights_1) + biases_1)
logits = tf.nn.relu(tf.matmul(layer_1, weights_2) + biases_2)
train_op = ...

#Command the main task of the cluster
with tf.Session("grpc://worker1.cluster:2222") as sess:
for i in range(100):
sess.run(train_op)

9.6　例3——分布式Pi计算

本例中，我们会改变我们的视角，从一个拥有多个计算资源的服务器，到一个拥有多个服务器的集群。集群中每台服务器又各自拥有多个计算资源。

在分布式版本执行时，会需要一些设置，由图9-8说明。

图9-8　分布式调度运行

9.6.1　服务器端脚本

该脚本将会在各个计算节点被执行。本例中，我们会使用两个工作作业，假设我们在localhost上用命令行指明索引号。如果你想在网络上的节点运行它们，只需要将localhost的地址替换成集群空间的地址。

源代码如下：

import tensorflow as tf
tf.app.flags.DEFINE_string("index", "0","Server index")
FLAGS = tf.app.flags.FLAGS
print FLAGS.index
cluster = tf.train.ClusterSpec({"local": ["localhost:2222",
"localhost:2223"]})
server = tf.train.Server(cluster, job_name="local",
task_index=int(FLAGS.index))
server.join()

在本机上按如下命令执行该脚本：

python start_server.py -index=0 #Server task 0
python start_server.py -index=1 #Server task 1

如果不出意外，server上的输出如图9-9所示。

图9-9　服务器端初始化

9.6.2　客户端脚本

然后，我们会使用客户端脚本。客户端脚本将会发送产生随机数字的任务到集群成员，进行最终的Pi的计算，几乎跟GPU的例子相同。

import tensorflow as tf
import numpy as np

tf.app.flags.DEFINE_integer("numsamples", "100","Number of samples per
server")
FLAGS = tf.app.flags.FLAGS

print ("Sample number per server: " + str(FLAGS.numsamples) )
cluster = tf.train.ClusterSpec({"local": ["localhost:2222",
"localhost:2223"]})
#This is the list containing the sumation of samples on any node
c=[]

def generate_sum():
i=tf.constant(np.random.uniform(size=FLAGS.numsamples*2),
shape=[FLAGS.numsamples,2])
distances=tf.reduce_sum(tf.pow(i,2),1)
return
(tf.reduce_sum(tf.cast(tf.greater_equal(tf.cast(1.0,tf.float64),distances),
tf.int32)))

with tf.device("/job:local/task:0"):
test1= generate_sum()

with tf.device("/job:local/task:1"):
test2= generate_sum()
#If your cluster is local, you must replace localhost by the address of the
first node
with tf.Session("grpc://localhost:2222") as sess:
result = sess.run(tf.cast(test1 +
test2,tf.float64)/FLAGS.numsamples*2.0)
print(result)

9.7　例4——在集群上运行分布式模型

本例将会教大家怎样在一个分布式TensorFlow上运行一个例子。

本例中，我们会完成一个简单的任务，但是我们会包含机器学习所有的步骤。

本例中，我们会创建一个参数服务器，用来存储和分发线性方程的参数（本例中只有w和b_0）。除此之外还有两个工作作业来协作训练参数，并基于最后一次的结果不断更新结果。

分布式训练集群设置如图9-10所示。

图9-10　分布式训练集群设置

源代码如下：

import tensorflow as tf
import numpy as np
from sklearn.utils import shuffle

# Here we define our cluster setup via the command line
tf.app.flags.DEFINE_string("ps_hosts", "",
"Comma-separated list of hostname:port pairs")
tf.app.flags.DEFINE_string("worker_hosts", "",
"Comma-separated list of hostname:port pairs")

# Define the characteristics of the cluster node, and its task index
tf.app.flags.DEFINE_string("job_name", "", "One of 'ps', 'worker'")
tf.app.flags.DEFINE_integer("task_index", 0, "Index of task within the
job")

FLAGS = tf.app.flags.FLAGS

def main(_):
ps_hosts = FLAGS.ps_hosts.split(",")
worker_hosts = FLAGS.worker_hosts.split(",")

# Create a cluster following the command line paramaters.
cluster = tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})
# Create the local task.
server = tf.train.Server(cluster,
job_name=FLAGS.job_name,
task_index=FLAGS.task_index)

if FLAGS.job_name == "ps":
server.join()
elif FLAGS.job_name == "worker":

# Assigns ops to the local worker by default.
with tf.device(tf.train.replica_device_setter(
worker_device="/job:worker/task:%d" % FLAGS.task_index,
cluster=cluster)):

#Define the training set, and the model parameters, loss function and
training operation
trX = np.linspace(-1, 1, 101)
trY = 2 * trX + np.random.randn(*trX.shape) * 0.4 + 0.2 # create a y
value
X = tf.placeholder("float", name="X") # create symbolic variables
Y = tf.placeholder("float", name = "Y")

def model(X, w, b):
return tf.mul(X, w) + b # We just define the line as X*w + b0

w = tf.Variable(-1.0, name="b0") # create a shared variable
b = tf.Variable(-2.0, name="b1") # create a shared variable
y_model = model(X, w, b)

loss = (tf.pow(Y-y_model, 2)) # use sqr error for cost function
global_step = tf.Variable(0)

train_op = tf.train.AdagradOptimizer(0.8).minimize(
loss, global_step=global_step)

#Create a saver, and a summary and init operation
saver = tf.train.Saver()
summary_op = tf.merge_all_summaries()
init_op = tf.initialize_all_variables()

# Create a "supervisor", which oversees the training process.
sv = tf.train.Supervisor(is_chief=(FLAGS.task_index == 0),
logdir="/tmp/train_logs",
init_op=init_op,
summary_op=summary_op,
saver=saver,
global_step=global_step,
save_model_secs=600)

# The supervisor takes care of session initialization, restoring from
# a checkpoint, and closing when done or an error occurs.
with sv.managed_session(server.target) as sess:
# Loop until the supervisor shuts down
step = 0
while not sv.should_stop() :
# Run a training step asynchronously.
# See `tf.train.SyncReplicasOptimizer` for additional details on
how to
# perform *synchronous* training.
for i in range(100):
trX, trY = shuffle (trX, trY, random_state=0)
for (x, y) in zip(trX, trY):
_, step = sess.run([train_op, global_step],feed_dict={X: x,
Y: y})
#Print the partial results, and the current node doing the
calculation
print ("Partial result from node: " + str(FLAGS.task_index) + ",
w: " + str(w.eval(session=sess))+ ", b0: " + str(b.eval(session=sess)))
# Ask for all the services to stop.
sv.stop()

if __name__ == "__main__":
tf.app.run()

对于参数服务器作业使用如下命令：

python trainer.py --ps_hosts=localhost:2222 --
worker_hosts=localhost:2223,localhost:2224 --job_name=ps -task_index=0
he first

对于第一个工作作业使用如下命令：

python trainer.py --ps_hosts=localhost:2222 --
worker_hosts=localhost:2223,localhost:2224 --job_name=worker -
task_index=0

对于第二个工作作业使用如下命令：

python trainer.py --ps_hosts=localhost:2222 --
worker_hosts=localhost:2223,localhost:2224 --job_name=worker --
task_index=1

9.8　小结

本章中，我们学习了两种用TensorFlow实现分布式高性能计算的方法，一种是单机版，另一种是集群版。

下一章中，我们将会学习怎样在多种环境和工具中安装TensorFlow。

第10章　库的安装和其他技巧

有几种安装TensorFlow的方法。谷歌公司为不同的结构、不同的操作系统和图像处理卡（Graphics Processing Unit，GPU）准备了不同的安装包。虽然在GPU上运行机器学习任务会更快，但是两个选项都提供了：

- CPU：这将会在本机器的各个CPU的核上处理数据。

- GPU：这个功能能够在更强大的NVIDIA公司的CUDA上运行。其实还有其他许多的架构，如Vulkan，但是它们不像CUDA那么流行，所以还不能成为标准。

本章中，我们将会学习：

- 如何在3种不同的操作系统上安装TensorFlow（Linux、Windows和MacOS X）。

- 如何测试安装是否成功，如何运行示例程序，如何开发你的脚本。

- 额外的资源，在编写机器学习程序的时候简化我们的工作。

10.1　Linux安装

首先，我们要说明，正如你所知道的那样，Linux王国有许多的变体，它们都有自己独特的包管理办法，基于此，我们选择使用Ubuntu 16.04发行版。无疑，这是最流行的Linux发行版，而且Ubuntu 16.04还是一个LTS（Long Term Support，长期支持）版本。也就是说，桌面版拥有3年的支持，服务器版拥有5年的支持。这意味着，我们的基础软件在2021年前都能获得支持！

你可以在网站https://wiki.ubuntu.com/LTS获得更多的LTS信息。

Ubuntu，即使是一个面向新手的版本，也能满足对于TensorFlow的所有技术支持，而且面向新手的版本还拥有最多的用户支持。基于此，我们会介绍使用该版本的所有步骤。Ubuntu下的步骤跟基于Debian的发行版的步骤类似。

在写作本书的时候，TensorFlow还不能支持32位的Linux版本。所以，首先确认，你所用的是64位版本的Linux。

10.1.1　安装要求

对于这种TensorFlow的安装，你有两个选择：

- 一个运行在云上基于AMD-64的镜像；

- 一台兼容AMD-64指令的电脑（通常称作64位处理器）。

AWS上，有一个非常适合的ami镜像，编码是ami-cf68e0d8，既能运行在CPU上，也能运行在GPU上。

10.1.2　Ubuntu安装准备（安装操作的前期操作）

因为我们使用的是最新的Ubuntu 16.04，我们需要确保所有的包都是更新到最新的版本，然后还要有最小的Python的支持环境。

我们执行如下命令行：

sudo apt-get upgrade -y
sudo pip install –upgrade
https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.9.0-cp27-n
one-linux_x86_64.whl

然后，你就会发现，所有的依赖包都会依次下载。如果不出意外，你的屏幕将会显示如图10-1所示。

图10-1　使用pip安装时的输出

2．测试安装是否成功

安装完成之后，我们可以做一个小的测试，调用Python解释器，然后引入TensorFlow库，定义两个数字常量，对它们加和。

sudo pip install -upgrade
https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.10.0rc0-cp
27-none-linux_x86_64.whl

还有许多其他版本的预编译TensorFlow包。

它们的安装网址要替换成如下格式：

https://storage.googleapis.com/tensorflow/linux/[processor
type]/tensorflow-[version]-cp[python version]-none-linux_x86_64.whl

这里面的“[processor type]”是GPU或者CPU，“[version]”是TensorFlow的版本（实际是0.11），“[python version]”是Python的版本，可选2.7、3.4或者3.5。

4．Virtualenv安装方法

本部分，我们将会解释如何使用Virtualenv工具安装TensorFlow。

Virtualenv的官网（virtualenv.pypa.io）对Virtualenv的介绍如下：

“Virtualenv是用来创建独立的Python环境的工具。（……）通过创建一个独立的安装目录，创造了一个独立的安装环境，不跟其他的Virtualenv共享库（可以选择是否使用已经安装好的全局库）。”

通过该工具，我们能够将TensorFlow安装在一个独立的环境之中，而不影响系统中的其他库，其他库也不会影响到TensorFlow的安装。

安装Virtualenv有如下简单几步（在Linux终端中）：

1）设置LC_ALL变量：

sudo apt-get install python-virtualenv

3）安装Virtualenv包：

virtualenv --system-site-packages ~/tensorflow

4）然后就可以使用新的TensorFlow了，要记得激活TensorFlow的环境：

source ~/tensorflow/bin/activate

5）通过pip安装TensorFlow包：

pip install –upgrade
https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.9.0-cp27-n
one-linux_x86_64.whl

通过这个办法，你可以在一台电脑里面安装TensorFlow的不同版本。

5．环境测试

下面我们来为TensorFlow做一个最简单的测试。

首先，需要激活刚创建的TensorFlow环境：

python
>>> import tensorflow as tf
>>> a = tf.constant(2)
>>> b = tf.constant(3)
>>> with tf.Session() as sess:
print(sess.run(a * b))
6

当所有的操作完成之后，如果想回到正常的环境，可以激活（deactivate）环境：

(tensorflow)$ deactivate

6．Docker安装方法

我们还可以通过最近的一种技术“容器”来安装TensorFlow。

容器在某种程度上跟Virtualenv有些类似。在Docker中，你也会拥有一个新的虚拟环境。主要的区别就是虚拟化的层次。

它将应用程序和其依赖包含在一个包中，并且这些封装的容器可以同时运行在一个通用层上，Docker引擎又在主机操作系统上运行。

Docker主架购如图10-2所示。

图10-2　Docker主架构（图片来源-https://www.docker.com/products/docker-engine）

（1）安装Docker

首先，我们通过apt包安装Docker：

sudo apt-get install docker.io

（2）以普通用户身份运行Docker

本部分，我们创建一个Docker组，然后将一个用户加入这个组：

sudo groupadd docker

当你运行这段程序的时候，可能会遇到错误；提示用户组“docker”已经存在。你可安全忽略这个错误。

然后将当前用户添加到该Docker用户组：

sudo usermod -aG docker [your user]

该命令不应该返回任何输出。

（3）重启

本步之后，我们使用reboot来应用这个上面的操作。

（4）测试Docker的安装

重启之后，我们调用Docker的Hello World例子测试Docker是否安装成功，如图10-3所示。

$ docker run hello-world

图10-3　Docker Hello World 容器

（5）在容器中运行TensorFlow

然后我们运行（如果还没安装，先安装）TensorFlow的二进制镜像（本例中使用的是vanilla CPU镜像）：

docker run -it -p 8888:8888 gcr.io/tensorflow/tensorflow

安装完成之后，在Jupyter notebook中运行TensorFlow。

许多例子程序都是Jupyter notebook格式的。为了执行这些程序，你需要首先安装和使用Jupyter notebook。主页是jupyter.org。Jupyter notebook的初始界面如图10-4所示。

图10-4　Jupyter notebook主界面

10.1.4　Linux下从源码安装TensorFlow

现在，我们可以进入最复杂的、对开发者最友好的TensorFlow安装方法——源码安装。首先我们需要使用几个工具来下载和编译TensorFlow。

1．安装Git代码版本管理器

Git是最著名的代码版本管理器之一。Google的很多开源代码都是发布在Github上。

为了下载TensorFlow的源代码，我们要首先安装Git源码管理器。

为了在Linux（Ubuntu 16.04）下安装Git，执行以下命令：

$ sudo apt-get install git

2．安装Bazel构建工具

Bazel（bazel.io）是谷歌公司的软件构建工具，在谷歌内部已经使用了超过7年。2015年9月份，Bazel对外发布。

Bazel被用作TensorFlow的主构建工具，所以为了执行高级的任务，我们首先要学一些关于该工具的基本知识。

相比于其他工具，如Gradle，Bazel的优势体现在：

- 支持多种语言，如C++、Java、Python等；

- 支持创建Android和iOS应用，甚至是Docker镜像；

- 支持使用多种代码库，如Github、Maven等；

- 通过API实现定制规则的扩展。

① 添加Bazel包源

首先，我们要为可用仓库（repository）添加Bazel仓库，并将其配置进apt工具。因为apt工具是Ubuntu操作系统下的依赖管理工具。

② 更新和安装Bazel

我们拥有了包的源文件之后，就可以通过apt-get工具安装并更新Bazel：

echo -e "blacklist nouveau\nblacklist lbm-nouveau\noptions nouveau
modeset=0\nalias nouveau off\nalias lbm-nouveau off\n" | sudo tee
/etc/modprobe.d/blacklist-nouveau.conf
sudo update-initramfs -u
$ sudo reboot (a reboot will occur)

4．安装CUDA系统包

第一步是从仓库安装所需要的包：

sudo apt-get install -y linux-source linux-headers-`uname -r`
nvidia-graphics-drivers-361
nvidia-cuda-dev
sudo apt install nvidia-cuda-toolkit
sudo apt-get install libcupti-dev

如果你是在云镜像上安装Tensorflow，请执行以下命令：

sudo apt-get install linux-image-extra-virtual

① 创建替换位置

目前TensorFlow安装需要一个严格的文件结构，所以我们必须在文件系统上为CUDA配置类似的结构。

执行如下命令行：

sudo mkdir /usr/local/cuda
cd /usr/local/cuda
sudo ln -s /usr/lib/x86_64-linux-gnu/ lib64
sudo ln -s /usr/include/ include
sudo ln -s /usr/bin/ bin
sudo ln -s /usr/lib/x86_64-linux-gnu/ nvvm
sudo mkdir -p extras/CUPTI
cd extras/CUPTI
sudo ln -s /usr/lib/x86_64-linux-gnu/ lib64
sudo ln -s /usr/include/ include
sudo ln -s /usr/include/cuda.h /usr/local/cuda/include/cuda.h
sudo ln -s /usr/include/cublas.h /usr/local/cuda/include/cublas.h
sudo ln -s /usr/include/cudnn.h /usr/local/cuda/include/cudnn.h
sudo ln -s /usr/include/cupti.h
/usr/local/cuda/extras/CUPTI/include/cupti.h
sudo ln -s /usr/lib/x86_64-linux-gnu/libcudart_static.a
/usr/local/cuda/lib64/libcudart_static.a
sudo ln -s /usr/lib/x86_64-linux-gnu/libcublas.so
/usr/local/cuda/lib64/libcublas.so
sudo ln -s /usr/lib/x86_64-linux-gnu/libcudart.so
/usr/local/cuda/lib64/libcudart.so
sudo ln -s /usr/lib/x86_64-linux-gnu/libcudnn.so
/usr/local/cuda/lib64/libcudnn.so
sudo ln -s /usr/lib/x86_64-linux-gnu/libcufft.so
/usr/local/cuda/lib64/libcufft.so
sudo ln -s /usr/lib/x86_64-linux-gnu/libcupti.so
/usr/local/cuda/extras/CUPTI/lib64/libcupti.so

② 安装cuDNN

TensorFlow还使用了额外的cuDNN包来加速深度神经网络操作。

我们先下载cuDNN包：

sudo cp cuda/lib64/libcudnn* /usr/local/cuda/lib64
git clone https://github.com/tensorflow/tensorflow

图10-5　获取TensorFlow源码

6．配置TensorFlow的构建环境

我们首先进入TensorFlow的主目录：

./configure

脚本会向你询问关于配置的所有问题（大部分都可以直接按Enter或者回答Yes），如图10-6所示。

图10-6　CUDA配置

如果你是在AWS上安装，你需要执行以下命令：

TF_UNOFFICIAL_SETTING=1　./configure

7．构建TensorFlow

在完成所有的准备步骤后，我们需要编译TensorFlow。下面的命令值得注意，因为它指向一个教程。我们编译这个例子是因为这个例子提供了一些最简单的安装测试。

运行如下命令：

bazel-bin/tensorflow/cc/tutorials_example_trainer --use_gpu

图10-7是该命令的输出。

图10-7　TensorFlow GPU测试

10.2　Windows安装

本部分讲解如何在Windows操作系统下安装TensorFlow。首先，Windows操作系统对于TensorFlow生态系统来说不是一个好的选择，但是我们还是可以在Windows操作系统下使用TensorFlow。

10.2.1　经典的Docker工具箱方法

该方法主要使用Docker工具，Windows系统的大部分发行版（从Windows 7开始，同样需要64位系统）都支持。

为了使用Docker（具体是指VirtualBox），你需要打开VT-X的扩展。该操作需要在BIOS下完成。

10.2.2　安装步骤

此处，我们会列出Windows下，通过docker安装TensorFlow的具体操作步骤。

1．下载Docker工具箱安装文件

Docker工具箱的安装文件的URL是“https://github.com/docker/toolbox/releases/download/ v1.12.0/DockerToolbox-1.12.0.exe”。

执行该文件之后，安装界面显示如图10-8所示。

图10-8　Docker工具箱安装初始界面

单击Next按钮，界面如图10-9所示。

图10-9　Docker工具箱路径选择

然后选择我们安装所需要的组件，如图10-10所示。

图10-10　Docker安装包选择

在各种安装操作完成之后，Docker的安装也就完成了，如图10-11所示。

图10-11　Docker安装的最后界面

2．创建Docker机器

为了创建一个机器，我们在Docker的终端执行如下命令：

docker-machine create vdocker -d virtualbox

然后，在命令窗口中输入如下命令：

FOR /f "tokens=*" %i IN ('docker-machine env --shell cmd vdocker') DO
%i docker run -it b.gcr.io/tensorflow/tensorflo

该命令将会打印和读取并运行刚创建的虚拟机所需要的各种变量，如图10-12所示。

图10-12　Docker初始化镜像

然后，在安装好的TensorFlow容器中，我们就可以像之前在Linux中那样安装和运行TensorFlow了。

docker run -it -p 8888:8888 gcr.io/tensorflow/tensorflow

如果不想在Jupyter中运行，而是想直接在对话窗口中运行，你可以这样运行Docker镜像：

run -it -p 8888:8888 gcr.io/tensorflow/ tensorflow bash

10.3　MacOS X安装

现在我们开始安装MacOS X下的TensorFlow。安装的过程跟Linux下类似。我们是基于OS X E1 Captioan版本。我们使用Python 2.7，不用GPU支持。

本安装需要超级用户权限。

现在，我们安装pip包管理器，使用easy_install安装。easy_install是MacOS下Python的包管理工具，默认包含在操作系统中。

在终端中执行以下命令，如图10-13所示。

$ sudo easy_install pip

图10-13　使用easy_install安装pip

然后我们安装六模块，这是一个兼容模块，用来帮助Python 2支持Python 3编程。

安装六模块，执行如下命令，如图10-14所示。

$ sudo easy_install --upgrade six

图10-14　安装六模块

安装六模块之后，我们安装TensorFlow包，只要执行以下命令，如图10-15所示。

sudo pip install -ignore-packages six
https://storage.googleapis.com/tensorflow/mac/cpu/tensorflow-0.10.0-py2-non
e-any.whl

图10-15　安装TensorFlow包

然后我们安装numpy包，在EI Caption中需要此操作，如图10-16所示。

sudo easy_install numpy

图10-16　安装numpy包

之后我们就可以在Python中引入TensorFlow模块，并运行一些简单的例子了，如图10-17所示。

图10-17　引入TensorFlow模块

10.4　小结

在本章中，我们学习了安装TensorFlow的几种办法。

有些架构和处理器在当前的TensorFlow中并不支持，但是每个月TensorFlow都在添加新的支持。让我们一起期待使用该技术的领域越来越多。

欢迎来到异步社区！

异步社区的来历

异步社区(www.epubit.com.cn)是人民邮电出版社旗下IT专业图书旗舰社区，于2015年8月上线运营。

异步社区依托于人民邮电出版社20余年的IT专业优质出版资源和编辑策划团队，打造传统出版与电子出版和自出版结合、纸质书与电子书结合、传统印刷与POD按需印刷结合的出版平台，提供最新技术资讯，为作者和读者打造交流互动的平台。

社区里都有什么？

购买图书

我们出版的图书涵盖主流IT技术，在编程语言、Web技术、数据科学等领域有众多经典畅销图书。社区现已上线图书1000余种，电子书400多种，部分新书实现纸书、电子书同步出版。我们还会定期发布新书书讯。

下载资源

社区内提供随书附赠的资源，如书中的案例或程序源代码。

另外，社区还提供了大量的免费电子书，只要注册成为社区用户就可以免费下载。

与作译者互动

很多图书的作译者已经入驻社区，您可以关注他们，咨询技术问题；可以阅读不断更新的技术文章，听作译者和编辑畅聊好书背后有趣的故事；还可以参与社区的作者访谈栏目，向您关注的作者提出采访题目。

灵活优惠的购书

您可以方便地下单购买纸质图书或电子图书，纸质图书直接从人民邮电出版社书库发货，电子书提供多种阅读格式。

对于重磅新书，社区提供预售和新书首发服务，用户可以第一时间买到心仪的新书。

用户帐户中的积分可以用于购书优惠。100积分=1元，购买图书时，在里填入可使用的积分数值，即可扣减相应金额。

特别优惠

购买本电子书的读者专享异步社区优惠券。 使用方法：注册成为社区用户，在下单购书时输入“57AWG”，然后点击“使用优惠码”，即可享受电子书8折优惠（本优惠券只可使用一次）。

纸电图书组合购买

社区独家提供纸质图书和电子书组合购买方式，价格优惠，一次购买，多种阅读选择。

社区里还可以做什么？

提交勘误

您可以在图书页面下方提交勘误，每条勘误被确认后可以获得100积分。热心勘误的读者还有机会参与书稿的审校和翻译工作。

写作

社区提供基于Markdown的写作环境，喜欢写作的您可以在此一试身手，在社区里分享您的技术心得和读书体会，更可以体验自出版的乐趣，轻松实现出版的梦想。

如果成为社区认证作译者，还可以享受异步社区提供的作者专享特色服务。

会议活动早知道

您可以掌握IT圈的技术会议资讯，更有机会免费获赠大会门票。

加入异步

扫描任意二维码都能找到我们：

异步社区

微信订阅号

微信服务号

官方微博

QQ群：436746675

社区网址：www.epubit.com.cn

官方微信：异步社区

官方微博：@人邮异步社区，@人民邮电出版社-信息技术分社

投稿&咨询：contact@epubit.com.cn

目    录

版权信息

内容提要

序

前言

第一篇 基础篇

第1章 人工智能概述

1.1 什么是人工智能

1.2 什么是深度学习

1.3 深度学习的入门方法

1.4 什么是TensorFlow

1.5 为什么要学TensorFlow

1.5.1 TensorFlow的特性

1.5.2 使用TensorFlow的公司

1.5.3 TensorFlow的发展

1.6 机器学习的相关赛事

1.6.1 ImageNet的ILSVRC

1.6.2 Kaggle

1.6.3 天池大数据竞赛

1.7 国内的人工智能公司

1.8 小结

第2章 TensorFlow环境的准备

2.1 下载TensorFlow 1.1.0

2.2 基于pip的安装

2.2.1 Mac OS环境准备

2.2.2 Ubuntu/Linux环境准备

2.2.3 Windows环境准备

2.3 基于Java的安装

2.4 从源代码安装

2.5 依赖的其他模块

2.5.1 numpy

2.5.2 matplotlib

2.5.3 jupyter

2.5.4 scikit-image

2.5.5 librosa

2.5.6 nltk

2.5.7 keras

2.5.8 tflearn

2.6 小结

第3章 可视化TensorFlow

3.1 PlayGround

3.1.1 数据

3.1.2 特征

3.1.3 隐藏层

3.1.4 输出

3.2 TensorBoard[2]

3.2.1 SCALARS面板

3.2.2 IMAGES面板

3.2.3 AUDIO面板

3.2.4 GRAPHS面板

3.2.5 DISTRIBUTIONS面板

3.2.6 HISTOGRAMS面板

3.2.7 EMBEDDINGS面板

3.3 可视化的例子

3.3.1 降维分析

3.3.2 嵌入投影仪

3.4 小结

第4章 TensorFlow基础知识

4.1 系统架构

4.2 设计理念

4.3 编程模型[5]

4.3.1 边

4.3.2 节点

4.3.3 其他概念

4.4 常用API

4.4.1 图、操作和张量

4.4.2 可视化

4.5 变量作用域

4.5.1 variable_scope示例

4.5.2 name_scope示例

4.6 批标准化

4.6.1 方法

4.6.2 优点

4.6.3 示例

4.7 神经元函数及优化方法

4.7.1 激活函数

4.7.2 卷积函数

4.7.3 池化函数

4.7.4 分类函数

4.7.5 优化方法

4.8 模型的存储与加载

4.8.1 模型的存储与加载

4.8.2 图的存储与加载

4.9 队列和线程

4.9.1 队列

4.9.2 队列管理器

4.9.3 线程和协调器

4.10 加载数据

4.10.1 预加载数据

4.10.2 填充数据

4.10.3 从文件读取数据

4.11 实现一个自定义操作

4.11.1 步骤

4.11.2 最佳实践

4.12 小结

第5章 TensorFlow源代码解析

5.1 TensorFlow的目录结构

5.1.1 contirb

5.1.2 core

5.1.3 examples

5.1.4 g3doc

5.1.5 python

5.1.6 tensorboard

5.2 TensorFlow源代码的学习方法

5.3 小结

第6章 神经网络的发展及其TensorFlow实现

6.1 卷积神经网络

6.2 卷积神经网络发展

6.2.1 网络加深

6.2.2 增强卷积层的功能

6.2.3 从分类任务到检测任务

6.2.4 增加新的功能模块

6.3 MNIST的AlexNet实现

6.3.1 加载数据

6.3.2 构建网络模型

6.3.3 训练模型和评估模型

6.4 循环神经网络[15]

6.5 循环神经网络发展

6.5.1 增强隐藏层的功能

6.5.2 双向化及加深网络

6.6 TensorFlow Model Zoo

6.7 其他研究进展

6.7.1 强化学习

6.7.2 深度森林

6.7.3 深度学习与艺术

6.8 小结

第7章 TensorFlow的高级框架

7.1 TFLearn

7.1.1 加载数据

7.1.2 构建网络模型

7.1.3 训练模型

7.2 Keras

7.2.1 Keras的优点

7.2.2 Keras的模型[3]

7.2.3 Keras的使用

7.3 小结

第二篇 实战篇

第8章 第一个TensorFlow程序

8.1 TensorFlow的运行方式

8.1.1 生成及加载数据

8.1.2 构建网络模型

8.1.3 训练模型

8.2 超参数的设定

8.3 小结

第9章 TensorFlow在MNIST中的应用

9.1 MNIST数据集简介

9.1.1 训练集的标记文件

9.1.2 训练集的图片文件

9.1.3 测试集的标记文件

9.1.4 测试集的图片文件

9.2 MNIST的分类问题

9.2.1 加载数据

9.2.2 构建回归模型

9.2.3 训练模型

9.2.4 评估模型

9.3 训练过程的可视化

9.4 MNIST的卷积神经网络[4]

9.4.1 加载数据

9.4.2 构建模型

9.4.3 训练模型和评估模型

9.5 MNIST的循环神经网络[5]

9.5.1 加载数据

9.5.2 构建模型

9.5.3 训练数据及评估模型

9.6 MNIST的无监督学习

9.6.1 自编码网络[7]

9.6.2 TensorFlow的自编码网络实现[10]

9.7 小结

第10章 人脸识别

10.1 人脸识别简介

10.2 人脸识别的技术流程

10.2.1 人脸图像采集及检测

10.2.2 人脸图像预处理

10.2.3 人脸图像特征提取

10.2.4 人脸图像匹配与识别

10.3 人脸识别的分类

10.3.1 人脸检测

10.3.2 人脸关键点检测

10.3.3 人脸验证

10.3.4 人脸属性检测

10.4 人脸检测[2]

10.4.1 LFW数据集

10.4.2 数据预处理

10.4.3 进行检测

10.5 性别和年龄识别[8]

10.5.1 数据预处理

10.5.2 构建模型

10.5.3 训练模型

10.5.4 验证模型

10.6 小结

第11章 自然语言处理

11.1 模型的选择

11.2 英文数字语音识别[3]

11.2.1 定义输入数据并预处理数据

11.2.2 定义网络模型

11.2.3 训练模型

11.2.4 预测模型

11.3 智能聊天机器人

11.3.1 原理

11.3.2 最佳实践

11.4 小结

第12章 图像与语音的结合

12.1 看图说话模型

12.1.1 原理

12.1.2 最佳实践

12.2 小结

第13章 生成式对抗网络

13.1 生成式对抗网络的原理

13.2 生成式对抗网络的应用

13.3 生成式对抗网络的实现[4]

13.4 生成式对抗网络的改进

13.5 小结

第三篇 提高篇

第14章 分布式TensorFlow

14.1 分布式原理

14.1.1 单机多卡和分布式

14.1.2 分布式部署方式

14.2 分布式架构[3]

14.2.1 客户端、主节点和工作节点的关系

14.2.2 客户端、主节点和工作节点的交互过程

14.3 分布式模式

14.3.1 数据并行

14.3.2 同步更新和异步更新

14.3.3 模型并行

14.4 分布式API[12]

14.5 分布式训练代码框架

14.6 分布式最佳实践[13]

14.7 小结

第15章 TensorFlow线性代数编译框架XLA

15.1 XLA的优势

15.2 XLA的工作原理

15.3 JIT编译方式

15.3.1 打开JIT编译

15.3.2 将操作符放在XLA设备上

15.4 JIT编译在MNIST上的实现

15.5 小结

第16章 TensorFlow Debugger[1]

16.1 Debugger的使用示例

16.2 远程调试方法

16.3 小结

第17章 TensorFlow和Kubernetes结合

17.1 为什么需要Kubernetes

17.2 分布式TensorFlow在Kubernetes中的运行

17.2.1 部署及运行

17.2.2 其他应用

17.3 小结

第18章 TensorFlowOnSpark

18.1 TensorFlowOnSpark的架构[2]

18.2 TensorFlowOnSpark在MNIST上的实践[5]

18.3 小结

第19章 TensorFlow移动端应用

19.1 移动端应用原理

19.1.1 量化[2]

19.1.2 优化矩阵乘法运算

19.2 iOS系统实践

19.2.1 环境准备

19.2.2 编译演示程序并运行

19.2.3 自定义模型的编译及运行

19.3 Android系统实践[14]

19.3.1 环境准备

19.3.2 编译演示程序并运行

19.3.3 自定义模型的编译及运行

19.4 树莓派实践

19.5 小结

第20章 TensorFlow的其他特性

20.1 TensorFlow Serving[1]

20.2 TensorFlow Flod[7]

20.3 TensorFlow计算加速

20.3.1 CPU加速

20.3.2 TPU加速和FPGA加速

20.4 小结

第21章 机器学习的评测体系

21.1 人脸识别的性能指标

21.2 聊天机器人的性能指标

21.3 机器翻译的评价方法

21.3.1 BLEU

21.3.2 METEOR

21.4 常用的通用评价指标

21.4.1 ROC和AUC

21.4.2 AP和mAP

21.5 小结

附录A 公开数据集

A.1 图片数据集

A.2 人脸数据集

A.3 视频数据集

A.4 问答数据集

A.5 自动驾驶数据集

A.6 年龄、性别数据集

附录B 项目管理经验小谈

B.1 管理的激进与保守问题

B.1.1 激进派

B.1.2 保守派

B.1.3 保守派和激进派的区别

B.2 公司效率损失及规避

B.3 小结

欢迎来到异步社区！

返回总目录

版权信息

书名：TensorFlow技术解析与实战

ISBN：978-7-115-45613-7

本书由人民邮电出版社发行数字版。版权所有，侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用，未经授权，不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟，与我们共同保护知识产权。

如果购买者有侵权行为，我们可能对该用户实施包括但不限于关闭该帐号等维权措施，并可能追究法律责任。

•　著　　　　李嘉璇

责任编辑　杨海玲

•　人民邮电出版社出版发行　　北京市丰台区成寿寺路11号

邮编　100164 　电子邮件　315@ptpress.com.cn

网址　http://www.ptpress.com.cn

•　读者服务热线：(010)81055410

反盗版热线：(010)81055315

内容提要

TensorFlow 是谷歌公司开发的深度学习框架，也是目前深度学习的主流框架之一。本书从深度学习的基础讲起，深入TensorFlow框架原理、模型构建、源代码分析和网络实现等各个方面。全书分为基础篇、实战篇和提高篇三部分。基础篇讲解人工智能的入门知识，深度学习的方法，TensorFlow的基础原理、系统架构、设计理念、编程模型、常用API、批标准化、模型的存储与加载、队列与线程，实现一个自定义操作，并进行TensorFlow源代码解析，介绍卷积神经网络（CNN）和循环神经网络（RNN）的演化发展及其TensorFlow实现、TensorFlow的高级框架等知识；实战篇讲解如何用TensorFlow写一个神经网络程序并介绍TensorFlow实现各种网络（CNN、RNN和自编码网络等），并对MINIST数据集进行训练，讲解TensorFlow在人脸识别、自然语言处理、图像和语音的结合、生成式对抗网络等方面的应用；提高篇讲解TensorFlow的分布式原理、架构、模式、API，还会介绍TensorFlow XLA、TensorFlow Debugger、TensorFlow和Kubernetes结合、TensorFlowOnSpark、TensorFlow移动端应用，以及TensorFlow Serving、TensorFlow Fold和TensorFlow计算加速等其他特性。最后，附录中列出一些可供参考的公开数据集，并结合作者的项目经验介绍项目管理的一些建议。

本书深入浅出，理论联系实际，实战案例新颖，基于最新的TensorFlow 1.1版本，涵盖TensorFlow的新特性，非常适合对深度学习和TensorFlow感兴趣的读者阅读。

恭喜你选择TensorFlow，它是最流行的深度学习框架，没有之一。 我相信这是一本能让你坚持看到最后一页的技术书。

谨以本书献给我的挚爱—文森特•梵高先生，他毕生用画作所代表的 对生活的美好追求，是我在无数个黑夜中的灵魂伴侣。

序

今天深度学习已经渗透到互联网技术和产品的方方面面，它从学术界的一个研究课题变成了被工业界最广泛应用的关键技术。对于每一个程序员，我认为都应该或多或少了解和掌握深度学习。对于初学者来说，从TensorFlow入手是很好的起点。TensorFlow有谷歌的强大支持，并且有广泛的社区。

本书的作者李嘉璇曾是百度的一名优秀工程师，一位非常勤奋的女生。她在工作之余致力于人工智能的研究，对深度学习框架的架构、应用及编程进行深入钻研，并利用深度学习做图像处理、情感分析、文本挖掘等项目。更为难得的是，她在繁忙的工作之外积极创建TensorFlow及深度学习交流社区，同时也活跃于国内各大技术社区。这本书更是她投入了很多个不眠之夜编写而成。

鉴于这样的背景，我认为这本书非常适合希望入门深度学习的程序员。他们可以将本书作为一本入门和实践的书籍阅读。读者可以从本书中了解基本的深度学习原理、典型的模型、大量的TensorFlow源代码以及成功的应用范例。从本书出发，读者可以循序渐进，逐步深入，在工作实践中加以运用，领略深度学习的美妙。

余凯

地平线机器人创始人，前百度深度学习实验室主任

前言

缘起

2017年2月，TensorFlow的首届开发者峰会（2017 TensorFlow Dev Summit）在美国的加利福尼亚州举行。在会上，谷歌公司宣布正式发布TensorFlow 1.0版本。本书就是基于最新的1.1.0版本来介绍TensorFlow的技术解析和实战。

人工智能大潮来了。2016年，AlphaGo击败围棋大师李世石后，人工智能的应用仿佛一夜之间遍地开花。在科技潮流的大环境中，现在硅谷的用人单位越来越倾向于雇用既懂理论（思考者）又懂编程（执行者）的工程师。思考者的日常工作是阅读文献以求产生思路，而执行者则是编写代码来实现应用。但是要成为一名真正的工程师，学习机器学习是将思考者和执行者相结合的最快途径。

众所周知，人工智能是高级计算智能最宽泛的概念，机器学习是研究人工智能的一个工具，深度学习是机器学习的一个子集，是目前研究领域卓有成效的学习方法。深度学习的框架有很多，而TensorFlow将神经网络、算法这些平时停留在理论层面的知识，组织成一个平台框架，集合了神经网络的各个算法函数组成一个工具箱，让广大工程师可以专心建造自己的目标领域的“轮子”，而且TensorFlow是基于Python语言的，极易上手，这些优势迅速吸引了全世界的工程师。

我曾经也是一名前后端开发工程师，更专注于后端工程方向，而潜心研究深度学习和TensorFlow后，我被TensorFlow深深地迷住了。我发现它对各行各业将会有很深远的影响，并且会大大地解放劳动力。

与传统工程师的主要工作——实现产品需求或者设计高可用性架构不同，深度学习让人总结和抽象人类是怎样理解和看待问题的，并把这种方式教给机器。例如，在AlphaGo的研究中，人们需要先抽象出人类思考围棋的方式，然后将这种方式抽象成算法，并且配合人类大脑构造中神经网络的传输来实现这些算法。这时，工程师不会再写实现业务需求的逻辑代码，而是深度学习中将神经网络的“黑盒”和模型效果非常好却缺乏“可解释性”的特性相结合，在次次实验中尽量找出规律。记得美国前总统肯尼迪在宣布登月计划时曾说：“我们选择去月球，不是因为它简单，而是因为它困难。”今天，我相信，所有致力于人工智能方向的工程师之所以自豪地去研究，也不是因为它简单，而是因为它困难。我们研究它，是因为立足于现在这个点往前看，我们看不到已经建好的高楼大厦，看到的是一片等待我们去发掘的空旷的大地，而这个发掘过程需要的是十足的远见、决心、勇气和信心。

我在学习的过程中，由于深度学习的资料英文的居多，在理解上走了不少弯路。我把学到的知识和原理用心整理并用文字表述出来，写成这本书，希望能帮助没有接触过深度学习的广大程序员迅速上手，而不再被英文阅读理解挡在门外。说实话，TensorFlow的文档以及API接口是比较抽象的，再加上有一些从工程方向转入深度学习的人以前没有过深度学习的经验，所以如果带着工程类程序研发的思维去学习，甚至是实现业务逻辑需求的思维去学习，效果会很差。我希望这本书能为读者呈现一个通俗易懂、形象生动的TensorFlow，使读者迅速走入深度学习的世界。

在本书的写作过程中，为了能充分挤出时间，深夜当我困倦时，我常常让自己以最不舒服的方式入睡，希望能尽量少睡，以此增加仔细钻研的时间。有时我还会打开电视，将音量设置为静音，感受房间中电视背景光闪烁的动感，以此提醒自己时间的流动。刚开始我会坐在工作台前写作，累了又会抱着笔记本坐在床上继续写作，有时会写着写着不知不觉地睡着，凌晨三四点钟又醒来，感受黑夜里的那片安宁，心情顿时平静，再次投入到钻研中。每每有灵感，都非常激动；每每再次深入一个概念，增删易稿，把原理逼近真相地讲透，都让我非常有成就感。

面向的读者

我素来不爱探究数学公式的推导原理，对符号也很茫然，只是在必须要用时才对这些公式进行详细的推导，但是我却对这些原理在应用层面如何使用出奇地感兴趣。本书的目标就是带读者进入造“应用轮子”的大门。我会以最少的数学公式讲清楚如何用TensorFlow实现CNN、RNN，如何在实战中使用TensorFlow进行图片分类、人脸识别和自然语言处理等，以及如何将想训练的数据、想实现的应用亲手做出来。

同时，Python语言是一门相当高级的语言，有“可执行的伪代码”的美誉，可以用极少的代码行去完成一个复杂的功能，同时Python还有极为丰富的第三方库，让全世界很多工程师的开发工作变得异常简单。TensorFlow是用Python语言实现的框架，对很多学生来说非常容易上手，当然，如果是有开发经验的工程师，就更容易学会。如果说设计神经网络模型像是盖一栋大楼，那么TensorFlow强大的API用起来会让人感觉就像搭积木一样容易。因此，懂点儿Python，即便不怎么懂数学和算法原理也没关系，尽管跟着我一起学便是。

在翻译学上有一个概念叫作“平行语料库”，这个概念来自制作于公元前196年的古埃及罗塞塔石碑，石碑上用希腊文字、古埃及文字和当时的通俗体文字刻了同样的内容。在本书进行某个概念的讲解时，虽然是用Python代码作示范，但TensorFlow前端开发同时也支持多种上层语言，本书讲解过程中也会兼顾到用C++、Java、Go语言做开发的读者。

我希望，本书成为不同领域的读者进入人工智能领域的“垫脚石”，也希望所有的读者在人生路上能利用TensorFlow这个工具大放异彩。

我有很重的强迫症，因此，在编写本书的过程中，阅读了国内外很多与TensorFlow相关的资料，对本书的目录结构和框架经过很多次反复琢磨和调整；在写完之后，我又从头到尾地读过好几遍，并且和了解TensorFlow不同方面的人反复交流，根据建议又反复修改。这一切就是希望它能通俗易懂，把读者快速领入深度学习的大门。

这扇门的背后是异彩纷呈的，身怀这门技艺的人是应该非常自豪的，但这扇门的背后也是非常辛苦的，有时数据需要自己去想办法解决，还需要每天看论文，知晓最新科研成果，给自己以启发，反复地做实验，研究算法和模型，寻求提升和解决方法，经常会遇到在很长一段时间没有思路的情况。但是，只要做的东西是开创的，令人称赞的，就会开心地享受这个过程。

我专为本书读者建立了一个QQ交流群（320420130），希望在群里与大家深入讨论和交流学习过程中遇到的问题，也希望与大家分享最新的研究成果。

致谢

非常感谢谷歌大脑的工程师Jeff Dean，在得知我目前正在写这本书的时候，他特地发了邮件鼓励我：“听说你写了一本关于TensorFlow的书，真是太好了。希望你很享受学习TensorFlow的这段经历，并享受运用TensorFlow完成各种任务的这种体验。我非常高兴你为中文社区写这本书。”[1]这让我更坚定了传播TensorFlow深度学习的决心。

感谢百度硅谷AI实验室资深科学家王益老师关于AI on Kubernetes的建议。

感谢在百度工作时的同事陈后江，在写作过程中，我们有时在周末的深夜还进行讨论，印象最深的一次是在大冬天晚上，我们恰好都在外面，相互通了20多分钟电话，手冻得像冰棒似的。还要感谢童牧晨玄，他也是深度学习领域的爱好者，对关键的概念理解得非常透彻，能十分精准地讲出原理。

非常感谢《Redis实战》一书的译者黄健宏，他对技术写作有很丰富的经验。和他聊书总是能聊到凌晨以后，讨论到畅快处，甚至聊到天亮，他对问题的思考就像是“演杂技”一样，精准又恰到好处；同时，他又是一个非常让人感到温暖和踏实的朋友。

非常感谢iOS资深开发者唐巧，他在国内社区乐于分享的精神造福了很多的技术从业者，也正是他的推荐让我和本书的编辑杨海玲老师结下了这段美好的情谊。

非常感谢人民邮电出版社的杨海玲编辑，她最开始想到这个写作方向，我们一起一点一点地讨论书的内容，确认书的写作框架。在写作过程中，她的细致、专业、独到的见解也为本书增色不少。她对内容严谨和认真的态度令人动容。

非常感谢中科院计算所刘昕博士对本书第6章神经网络的发展提出的建议；感谢曾经的百度同事毕骁鹏对第8章、第9章、第13章、第14章、第20章、第21章提出的极为细致的建议，尤其是他擅长GPU和FPGA的部分，对本书的硬件加速提供了很多建议；感谢中科院智能信息处理重点实验室常务副主任山世光对第10章人脸识别部分提出的建议；感谢刘元震对本书第11章提出的建议；感谢我的好朋友容器专家苗立尧对第17章提出的建议；感谢百度地图导航专家梁腾腾对第19章移动端开发给予的极为细致的建议；感谢阿里巴巴数据科学与技术研究院高级专家孙亮博士对整本书的结构和知识点提出的建议。

感谢我的好朋友吴丽明，曾经那么帮助过我；感谢我的好朋友饶志臻先生，一直诱惑我买苹果设备，有个硬件发烧友真的很幸福；感谢我的闺蜜谢禹曦，好久没有和你聚餐了，甚是思念。

最后，还得感谢一位流行歌手——“火星弟弟”华晨宇，他在舞台上那一次次创意和感染力的演出深深地吸引了我，他在台下那认真刻苦作曲改歌的样子也激励着我，每次想到他的事迹，都给我极大的鼓励。

非常感谢本书的每一位读者，本书的完成过程非常辛苦但也充满甜蜜。我在“知乎”（ID：李嘉璇）和网站（tf.greatgeekgrace.com）上也会回答关于“人工智能”的各类问题，希望通过内容的更新与读者不断交流。另外，由于水平有限，在内容上表述上难免也有遗漏和疏忽，也恳请读者多多指正。

李嘉璇

2017年4月于北京石景山

个人博客：blog.greatgeekgrace.com

TensorFlow交流社区：tf. greatgeekgrace.com

电子邮箱：qiyueli_2013@163.com

[1]　Jeff Dean的邮件原文是：“It’s great that you've written a book about TensorFlow. I hope you enjoyed the experience in learning about TensorFlow and how to accomplish various tasks. I'm glad that you’re making your book available for the Chinese speaking community.”。

第一篇　基础篇

著名历史学家斯塔夫里阿诺斯在《全球通史》中，曾以15世纪的航海在“物理上”连通“各大洲”作为标志将人类历史划分为两个阶段。在我正在写作的《互联网通史》中，我把互联网这个“信息上”连通“人类个体”的物件作为划分人类历史的标志。而随着人工智能最近的崛起，我们又该思考重新划分了，因为人工智能将会在“信息上”连通“各个物体”。到那时各个物体都有“智能”，如智能汽车、智能电视、扫地机器人、智能音响等智能家居，想象极度的智能下，屋子里的电器和家居都可能和我们有简单的交互。

深度学习领域之所以异军突起，是因为传统的研发思维，如架构、组件化、大规模并发、存储与计算等，已经是技术红海了，而每位工程师都应该学习机器学习，是因为它带给工程师全新的开发思维，工程师可以用自己的代码让机器更加“聪明”。

第1章　人工智能概述

有人说，人工智能在世界范围的流行，是因为那盘围棋。2016年3月，谷歌公司的AlphaGo向韩国棋院围棋九段大师李世石发起挑战，而这棋局走法的可能性有361!种，最终AlphaGo战胜了这场“棋局数比可见宇宙中的原子数还多”的智力游戏。2015年11月9日（在距这场比赛前4个月），谷歌公司开源了它的第二代深度学习系统TensorFlow，也就是AlphaGo的基础程序。

1.1　什么是人工智能

什么是人工智能（artificial intelligence，AI）？要了解这个问题，我们先来看看人工智能的几个应用。

1．微软小冰

相信很多朋友手机里都有关注“微软小冰”的公众号，这是微软（亚洲）互联网工程院的一款人工智能伴侣虚拟机器人，跟它聊天时你会发现，小冰有时回答得非常切中你的心意，而有时逻辑上表达却有点儿对不上上下文，所以你觉得它时而回答得不错像人，时而又一眼看穿它是个机器人。这种能否判断对方究竟是人还是机器人的思维实验，叫作“图灵测试”。

图灵测试是计算机科学之父英国人艾伦·图灵提出的，这是一种测试机器是否具备人类智能的方法。图灵设计了一种“模仿游戏”：远处的人在一段规定的时间内，根据两个实体——电脑和人类对他提出的各种问题来判断对方是人类还是电脑。[1]具体过程如图1-1所示。C向A和B提出问题，由C来判断对方是人类还是电脑。通过一系列这样的测试，从电脑被误判断为人的概率就可以测出电脑的智能程度，电脑越被误判成人，说明智能程度就越高。

图1-1

这种情感对话能力就是人工智能的一个方向。而现在微软小冰更是可以通过文本、图像、视频和语音与人类展开交流，逐渐具备能看、能听和能说的各种人工智能感官，并且能够和人类进行双向同步交互。

2．人脸识别

现在许多电脑开机密码、支付宝的刷脸支付、客流的闸机通行都有采用人脸识别技术。目前市面上也有许多人脸识别考勤机。很多公司已经采用了人脸闸机打卡签到技术，当有人刷脸打卡签到时，识别出这个人的面部特征，考勤机会将其与公司的员工信息进行比对，完成身份识别，确认后，便可开闸放行。

更进一步讲，人脸识别中还可以识别出人物的年龄、性别、是否佩戴眼镜、是否有笑容、情绪欢乐或悲伤，以及眼睛、鼻子、嘴等关键部位，这就是人脸关键点检测。图1-2就是人脸关键点检测的一个示例。

图1-2

国内有一些公司在人脸识别上已经达到了先进水平，如云从科技、旷视科技、商汤科技等。旷视科技的Face++有目前世界一流的人脸追踪、识别、分析等服务应用，面向开发者的云平台及API、SDK，已经可以直接调用。

以上是人工智能应用的两个例子。百度百科上给出的人工智能的解释是：“它是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。人工智能是计算机科学的一个分支，它企图了解智能的实质，并生产出一种新的能以人类智能相似的方式做出反应的智能机器，该领域的研究包括机器人、语言识别、图像识别、自然语言处理和专家系统等。”[2]

简而言之，人工智能就是研究用计算机来实现人类的智能，例如，去模仿人类的知觉、推理、学习能力等，从而让计算机能够像人一样思考和行动，有图像识别（机器识别出猫猫狗狗）、人机对话（机器感知到人类的语义和情感，并给出反馈）、围棋的人机对弈（AlphaGo、Master等让机器自己思考去下棋）等。

国际上的谷歌、苹果、亚马逊、微软等巨大公司都在“两条腿走路”，一方面在做研发项目，如“谷歌大脑”（Google Brain），另一方面同时发力智能家居，如“Google Home智能音箱”，希望把设备当成人来交流。国内的阿里、腾讯、百度、搜狗、地平线等公司以及很多不同领域的创业公司也都在积累的大量数据上，开始尝试训练出高效的模型，不断优化业务指数。

那么，机器是如何实现人类的智力的呢？其实，机器主要是通过大量的训练数据进行训练，程序不断地进行自我学习和修正来训练出一个模型，而模型的本质就是一堆参数，用上千万、上亿个参数来描述业务的特点，如“人脸”“房屋地段价格”“用户画像”的特点，从而接近人类智力。这个过程一般采用的是机器学习以及机器学习的子集——深度学习（deep learning），也就是结合深度神经网络的方法来训练。所以说，深度学习方法是能够迅速实现人工智能很有效的工具。

AlphaGo的原理

20年前，IBM的“深蓝”计算机打败人类象棋高手的情景仿佛还历历在目。20年后，人工智能挑战最难的棋类——围棋棋局也成功了。那么AlphaGo是如何下棋的呢？我们知道，传统计算机的下棋方法，一般采取贪婪算法，用Alpha-Beta修剪法配合Min-Max算法。而AlphaGo采用了蒙特卡洛树搜索法（Monte Carlo tree search，MCTS）和深度卷积神经网络（deep convolutional neural network，DCNN）相结合。模型中涉及的主要网络及作用如下。

●　估值网络（value network，也称盘面评估函数）：计算出盘面的分数。

●　策略网络（policy network）：计算对于下每一个棋子的概率和胜率。它评估对手和自己可能下的位置，对可能的位置进行评估和搜寻。

训练模型的主要过程分为以下4步。

（1）采用分类的方法得到直接策略。

（2）直接策略对历史棋局资料库进行神经网络学习，得到习得策略。

（3）采用强化学习的方法进行自我对局来得到改良策略。

（4）用回归的方法整体统计后得到估值网络。

这里的神经网络部分都采用的是深度卷积神经网络，在自我对局的部分采用的是蒙特卡洛树状搜寻法（MCTS）。

更详细的论文见谷歌公司发表在《自然》（Nature）上的论文《Mastering the game of Go with deep neural networks and tree search》。

1.2　什么是深度学习

深度学习，顾名思义，需要从“深度”和“学习”两方面来谈。

1．深度

深度学习的前身是人工神经网络（artificial neural network，ANN），它的基本特点就是试图模仿人脑的神经元之间传递和处理信息的模式。神经网络这个词本身可以指生物神经网络和人工神经网络。在机器学习中，我们说的神经网络一般就是指人工神经网络。

图1-3给出的是一个最基本的人工神经网络的3层模型。

图1-3

人工神经网络由各个层组成，输入层（input layer）输入训练数据，在输出层（output layer）输出计算结果，中间有1个或多个隐藏层（hidden layer），使输入数据向前传播到输出层。“深度”一词没有具体的特指，一般就是要求隐藏层很多（一般指5层、10层、几百层甚至几千层）。

人工神经网络的构想源自对人类大脑的理解——神经元的彼此联系。二者也有不同之处，人类大脑的神经元是按照特定的物理距离连接的，而人工神经网络有独立的层和连接，还有数据传播方向。

例如，我们拿一张图片，对它做一些预处理，如图像居中、灰度调整、梯度锐化、去除噪声、倾斜度调整等，就可以输入到神经网络的第一层。然后，第一层会自己提取这个图像的特征，把有用的特征向下传递，直到最后一层，然后输出结果。这就是一次前向传播（forword propagation）。

最后一层的输出要给出一个结论，例如，在分类问题中，要告诉我们到底输入的图像是哪个类别，一般它会给出一个“概率向量”。如图1-4所示，列出了这只猫所属品种的前5个概率值。

图1-4

人工神经网络的每一层由大量的节点（神经元）组成，层与层之间有大量连接，但是层内部的神经元一般相互独立。深度学习的目的就是要利用已知的数据学习一套模型，使系统在遇见未知的数据时也能够做出预测。这个过程需要神经元具备以下两个特性。

（1）激活函数（activation function）：这个函数一般是非线性函数，也就是每个神经元通过这个函数将原有的来自其他神经元的输入做一个非线性变化，输出给下一层神经元。激活函数实现的非线性能力是前向传播（forword propagation）很重要的一部分。

（2）成本函数（cost function）：用来定量评估在特定输入值下，计算出来的输出结果距离这个输入值的真实值有多远，然后不断调整每一层的权重参数，使最后的损失值最小。这就是完成了一次反向传播（backword propagation）。损失值越小，结果就越可靠。

神经网络算法的核心就是计算、连接、评估、纠错和训练，而深度学习的深度就在于通过不断增加中间隐藏层数和神经元数量，让神经网络变得又深又宽，让系统运行大量数据，训练它。

2．学习

什么是“学习”？有一些成语可以概括：举一反三、闻一知十、触类旁通、问牛知马、融会贯通等。计算机的学习和人类的学习类似，我们平时大量做题（训练数据），不断地经过阶段性考试（验证数据）的检验，用这些知识和解题方法（模型）最终走向最终（测试数据）的考场。

最简单也最普遍的一类机器学习算法就是分类（classification）。对于分类，输入的训练数据有特征（feature），有标记（label），在学习中就是找出特征和标记间的映射关系（mapping），通过标记来不断纠正学习中的偏差，使学习的预测率不断提高。这种训练数据都有标记的学习，称为有监督学习（supervised learning）。

无监督学习（unsupervised learning）则看起来非常困难。无监督学习的目的是让计算机自己去学习怎样做一些事情。因此，所有数据只有特征而没有标记。

无监督学习一般有两种思路：一是在训练时不为其指定明确的分类，但是这些数据会呈现出聚群的结构，彼此相似的类型会聚集在一起。计算机通过把这些没有标记的数据分成一个个组合，就是聚类（clustering）；二是在成功时采用某种形式的激励制度，即强化学习（reinforcement learning，RL）。对强化学习来说，它虽然没有标记，但有一个延迟奖赏与训练相关，通过学习过程中的激励函数获得某种从状态到行动的映射。强化学习一般用在游戏、下棋（如前面提到的AlphaGo）等需要连续决策的领域。（6.7.1节会讲解强化学习的应用。）

有人可能会想，难道就只有有监督学习和无监督学习这两种非黑即白的关系吗？二者的中间地带就是半监督学习（semi-supervised learning）。对于半监督学习，其训练数据一部分有标记，另一部分没有标记，而没标记数据的数量常常极大于有标记数据的数量（这也符合现实，大部分数据没有标记，标记数据的成本很大）。它的基本规律是：数据的分布必然不是完全随机的，通过结合有标记数据的局部特征，以及大量没标记数据的整体分布，可以得到比较好的分类结果。

因此，“学习”家族的整体构造如图1-5所示[3]。

图1-5

关于有监督学习和无监督学习在实战中的应用，会在本书“实战篇”中介绍。

1.3　深度学习的入门方法

要想入门深度学习，需要两个工具，即算法知识和大量的数据，外加一台计算机，如果有GPU就更好了，但是因为许多入门初学者的条件有限，没有GPU也可以，本书的许多讲解都是基于Mac笔记本完成的。

我把深度学习的入门过程整理成图1-6所示的7个步骤。

图1-6

下面就来详细介绍一下这7个步骤。

1．学习或者回忆一些数学知识

因为计算机能做的就只是计算，所以人工智能更多地来说还是数学问题[4]。我们的目标是训练出一个模型，用这个模型去进行一系列的预测。于是，我们将训练过程涉及的过程抽象成数学函数：首先，需要定义一个网络结构，相当于定义一种线性非线性函数；接着，设定一个优化目标，也就是定义一种损失函数（loss function）。

而训练的过程，就是求解最优解及次优解的过程。在这个过程中，我们需要掌握基本的概率统计、高等数学、线性代数等知识，如果学过就最好，没学过也没关系，仅仅知道原理和过程即可，有兴趣的读者可以涉猎一些推导证明。

2．掌握经典机器学习理论与基本算法

这些基本算法包括支持向量机、逻辑回归、决策树、朴素贝叶斯分类器、随机森林、聚类算法、协同过滤、关联性分析、人工神经网络和BP算法、PCA、过拟合与正则化等。[5]

在本书“实战篇”的第8章到第13章的例子中也有贯穿这些算法知识，保证读者可以用它写出一个小的TensorFlow程序。

3．掌握一种编程工具（语言）

Python语言是一种解释型、面向对象、动态数据类型的高级程序设计语言。Python是很多新入门的程序员的入门编程语言，也是很多老程序员后来必须掌握的编程语言。我们需要重点掌握使用线性代数库和矩阵的操作，尤其是Numpy、Pandas第三方库，也要多试试机器学习的库，如sklearn，做一些SVM及逻辑回归的练习。这对直接上手写TensorFlow程序大有裨益。

有些工业及学术领域的读者还可能擅长MATLAB或R，其实现算法的思想和Python也很类似。

同时考虑到许多读者是使用C++、Java、Go语言的，TensorFlow还提供了和Python“平行语料库”的接口。虽然本书是主要是基于Python讲解的，对于其他语言的原理和应用API也都非常类似，读者把基础掌握后，只需要花很短的时间就能使用自己擅长的语言开发。另外对于Java语言的同学，本书第18章会讲解TensorFlowOnSpark，第19章会讲到TensorFlow的移动端开发。

4．研读经典论文，关注最新动态和研究成果

一些经典论文是必读的。例如，要做手写数字识别，若采用LeNet，要先阅读一下LeNet的学术论文；要做物体目标检测的训练，若选定MSCNN框架，可以先读MSCNN相关的论文。那么，论文从哪里找呢？那么多论文应该读哪篇呢？

下面以GoogleNet的TensorFlow实现为例。在GitHub[6]上，一般在开头的描述中就会说明这个模型所依据的论文，如图1-7所示。

顺着这篇论文阅读，可以大致了解这个网络的实现原理，对迅速上手应用有很大的作用。同时，我在第6章也会对LeNet、AlexNet、ResNet这几个常见的网络进行讲解，帮助读者举一反三。

图1-7

很多做模式识别的工作者之所以厉害，是因为他们有过很多、很深的论文积累，对模型的设计有很独到的见解，而他们可能甚至一行代码也不会写，而工程（写代码）能力在工作中很容易训练。许多工程方向的软件工程师，工作模式常常在实现业务逻辑和设计架构系统上，编码能力很强，但却缺少论文积累。同时具有这两种能力的人，正是硅谷一些企业目前青睐的人才。

读者平时还可以阅读一些博客、笔记，以及微信公众号、微博新媒体资讯等，往往一些很流行的新训练方法和模型会很快在这些媒体上发酵，其训练神经网络采用的一些方法可能有很大的启发性。

5．自己动手训练神经网络

接着，就是要选择一个开源的深度学习框架。选择框架时主要考虑哪种框架用的人多。人气旺后，遇到问题很容易找到答案；GitHub上关于这个框架的项目和演示会非常多；相关的论文也会层出不穷；在各个QQ群和微信群的活跃度会高；杂志、公众号、微博关注的人也会很多；行业交流和技术峰会讨论的话题也多；也能享受到国内外研究信息成果的同步。

目前这个阶段，TensorFlow因为背靠谷歌公司这座靠山，再加上拥有庞大的开发者群体，而且采用了称为“可执行的伪代码”的Python语言，更新和发版速度着实非常快。目前TensorFlow已经升级到1.1版，在性能方面也有大幅度提高，而且新出现的Debugger、Serving、XLA特性也是其他框架所不及的。此外，一些外围的第三方库（如Keras、TFLearn）也基于它实现了很多成果，并且Keras还得到TensorFlow官方的支持。TensorFlow支持的上层语言也在逐渐增多，对于不同工程背景的人转入的门槛正在降低。

在GitHub[7]上有一个关于各种框架的比较，从建模能力、接口、模型部署、性能、架构、生态系统、跨平台等7个方面进行比较，TensorFlow也很占综合优势。截至2017年1月，TensorFlow的star数已经超过了其他所有框架的总和，如图1-8所示。

因此，从目前来看，投身TensorFlow是一个非常好的选择，掌握TensorFlow在找工作时是一个非常大的加分项。

图1-8

接下来就是找一个深度神经网络，目前的研究方向主要集中在视觉和语音两个领域。初学者最好从计算机视觉入手，因为它不像语音等领域需要那么多的领域知识，结果也比较直观。例如，用各种网络模型来训练手写数字（MNIST）及图像分类（CIFAR）的数据集。

6．深入感兴趣或者工作相关领域

人工智能目前的应用领域很多，主要是计算机视觉和自然语言处理，以及各种预测等。对于计算机视觉，可以做图像分类、目标检测、视频中的目标检测等；对于自然语言处理，可以做语音识别、语音合成、对话系统、机器翻译、文章摘要、情感分析等，还可以结合图像、视频和语音，一起发挥价值。

更可以深入某一个行业领域。例如，深入医学行业领域，做医学影像的识别；深入淘宝的穿衣领域，做衣服搭配或衣服款型的识别；深入保险业、通信业的客服领域，做对话机器人的智能问答系统；深入智能家居领域，做人机的自然语言交互；等等。

7．在工作中遇到问题，重复4~6步

在训练中，准确率、坏案例（bad case）、识别速度等都是可能遇到的瓶颈。训练好的模型也不是一成不变的，需要不断优化，也需要结合具体行业领域和业务进行创新，这时候就要结合最新的科研成果，调整模型，更改模型参数，一步步更好地贴近业务需求。

1.4　什么是TensorFlow

想想，在机器学习流行之前，我们是如何做与语音和图像相关的识别的？大多数是基于规则的系统。例如，做自然语言处理，需要很多语言学的知识；再如，1997年的IBM的深蓝计算机对战国际象棋，也需要很多象棋的知识。

当以统计方法为核心的机器学习方法成为主流后，我们需要的领域知识就相对少了。重要的是做特征工程（feature engineering），然后调一些参数，根据一些领域的经验来不断提取特征，特征的好坏往往就直接决定了模型的好坏。这种方法的一大缺点是，对文字等抽象领域，特征还相对容易提取，而对语音这种一维时域信号和图像这种二维空域信号等领域，提取特征就相对困难。

深度学习的革命性在于，它不需要我们过多地提取特征，在神经网络的每一层中，计算机都可以自动学习出特征。为了实现深度学习中运用的神经网络，TensorFlow这样的深度学习开源工具就应运而生。我们可以使用它来搭建自己的神经网络。这就有点儿类似于PHP开发当中的CodeIgniter框架，Java开发当中的SSH三大框架，Python开发当中的Tornado、Django框架，C++当中的MFC、ACE框架。框架的主要目的就是提供一个工具箱，使开发时能够简化代码，呈现出来的模型尽可能简洁易懂。

1.5　为什么要学TensorFlow

首先，TensorFlow的一大亮点是支持异构设备分布式计算（heterogeneous distributed computing）。

何为异构？信息技术当中的异构是指包含不同的成分，有异构网络（如互联网，不同厂家的硬件软件产品组成统一网络且互相通信）、异构数据库（多个数据库系统的集合，可以实现数据的共享和透明访问[8]）。这里的异构设备是指使用CPU、GPU等核心进行有效地协同合作；与只依靠CPU相比，性能更高，功耗更低。

那何为分布式？分布式架构目的在于帮助我们调度和分配计算资源（甚至容错，如某个计算节点宕机或者太慢），使得上千万、上亿数据量的模型能够有效地利用机器资源进行训练。

图1-9给出的是开源框架TensorFlow的标志。

图1-9

TensorFlow支持卷积神经网络（convolutional neural network，CNN）和循环神经网络（recurrent neural network，RNN），以及RNN的一个特例长短期记忆网络（long short-term memory，LSTM），这些都是目前在计算机视觉、语音识别、自然语言处理方面最流行的深度神经网络模型。

下面参考《The Unreasonable Effectiveness of Recurrent Neural Networks》[9]这篇文章梳理了一个有效框架应该具有的功能。

- Tensor库是对CPU/GPU透明的，并且实现了很多操作（如切片、数组或矩阵操作等）。这里的透明是指，在不同设备上如何运行，都是框架帮用户去实现的，用户只需要指定在哪个设备上进行哪种运算即可。

- 有一个完全独立的代码库，用脚本语言（最理想的是Python）来操作Tensors，并且实现所有深度学习的内容，包括前向传播/反向传播、图形计算等。

- 可以轻松地共享预训练模型（如Caffe的模型及TensorFlow中的slim模块）。

- 没有编译过程。深度学习是朝着更大、更复杂的网络发展的，因此在复杂图算法中花费的时间会成倍增加。而且，进行编译的话会丢失可解释性和有效进行日志调试的能力。

在我看来，在目前的深度学习的研究领域主要有以下3类人群。

- 学者。主要做深度学习的理论研究，研究如何设计一个“网络模型”，如何修改参数以及为什么这样修改效果会好。平时的工作主要是关注科研前沿和进行理论研究、模型实验等，对新技术、新理论很敏感。

- 算法改进者。这些人为了把现有的网络模型能够适配自己的应用，达到更好的效果，会对模型做出一些改进，把一些新算法改进应用到现有模型中。这类人主要是做一些基础的应用服务，如基础的语音识别服务、基础的人脸识别服务，为其他上层应用方提供优良的模型。

- 工业研究者。这类人群不会涉及太深的算法，主要掌握各种模型的网络结构和一些算法实现。他们更多地是阅读优秀论文，根据论文去复现成果，然后应用到自己所在的工业领域。这个层次的人也是现在深度学习研究的主流人群。

我相信本书的读者也大都是第二类和第三类人群，且以第三类人群居多。

而在工业界，TensorFlow将会比其他框架更具优势。工业界的目标是把模型落实到产品上，而产品的应用领域一般有两个：一是基于服务端的大数据服务，让用户直接体验到服务端强大的计算能力（谷歌云平台及谷歌搜索功能）；二是直接面向终端用户的移动端（Android系统）以及一些智能产品的嵌入式。

坐拥Android的市场份额和影响力的谷歌公司，在这两个方向都很强大。此外，谷歌力推的模型压缩和8位低精度数据存储（详见第 19 章）不仅对训练系统本身有优化作用，在某种程度上也能使算法在移动设备上的部署获益，这些优化举措将会使存储需求和内存带宽要求降低，并且使性能得到提升，对移动设备的性能和功耗非常有利。

如果一个框架的用户生态好，用的人就会很多，而用的人多会让用户生态更繁荣，用的人也就会更多。这庞大的用户数就是TensorFlow框架的生命力。

截至2017年1月，与Caffe、Theano、Torch、MXNet等框架相比，TensorFlow在GitHub上Fork数和Star数都是最多的，如图1-10所示。

图1-10

图1-11展示了截至2017年2月，近些年几大机器学习框架的流行程度。

图1-11

1.5.1　TensorFlow的特性

在TensorFlow官方网站[10]上，着重介绍了TensorFlow的6大优势特性。

- 高度的灵活性（deep flexibility）。TensorFlow是一个采用数据流图（data flow graph），用于数值计算的开源软件库。只要计算可以表示为一个数据流图，就可以使用TensorFlow，只需要构建图，书写计算的内部循环即可。因此，它并不是一个严格的“神经网络库”。用户也可以在TensorFlow上封装自己的“上层库”，如果发现没有自己想要的底层操作，用户也可以自己写C++代码来丰富。关于封装的“上层库”，TensorFlow现在有很多开源的上层库工具，极大地减少了重复代码量，在第7章中会详细介绍。

- 真正的可移植性（true portability）。TensorFlow可以在CPU和GPU上运行，以及在台式机、服务器、移动端、云端服务器、Docker容器等各个终端运行。因此，当用户有一个新点子，就可以立即在笔记本上进行尝试。

- 将科研和产品结合在一起（connect research and production）。过去如果将一个科研的机器学习想法应用到商业化的产品中，需要很多的代码重写工作。现在TensorFlow提供了一个快速试验的框架，可以尝试新算法，并训练出模型，大大提高了科研产出率。

- 自动求微分（auto-differentiation）。求微分是基于梯度的机器学习算法的重要一步。使用TensorFlow后，只需要定义预测模型的结构和目标函数，将两者结合在一起后，添加相应的数据，TensorFlow就会自动完成计算微分操作。

- 多语言支持（language options）。TensorFlow提供了Python、C++、Java接口来构建用户的程序，而核心部分是用C++实现的，如图1-12所示。第4章中会着重讲解TensorFlow的架构。用户也可以使用Jupyter Notebook[11]来书写笔记、代码，以及可视化每一步的特征映射（feature map）。用户也可以开发更多其他语言（如Go、Lua、R等）的接口。

图1-12

- 最优化性能（maximize performance）。假如用户有一台32个CPU内核、4个GPU显卡的机器，如何将计算机的所有硬件计算资源全部发挥出来呢？TensorFlow给予线程、队列、分布式计算等支持，可以让用户将TensorFlow的数据流图上的不同计算元素分配到不同的设备上，最大化地利用硬件资源。关于线程和队列，将在4.9节中介绍；关于分布式，将在第14章介绍。

1.5.2　使用TensorFlow的公司

除了谷歌在自己的产品线上使用TensorFlow外，国内的京东、小米等公司，以及国外的Uber、eBay、Dropbox、Airbnb等公司，都在尝试使用TensorFlow。图1-13是摘自TensorFlow官方网站的日益壮大的公司墙。

图1-13

1.5.3　TensorFlow的发展

2016年4月，TensorFlow的0.8版本就支持了分布式、支持多GPU运算。2016年6月，TensorFlow的0.9版本改进了对移动设备的支持。2017年2月，TensorFlow的1.0正式版本中，增加了Java和Go的实验性API，以及专用编译器XLA和调试工具Debugger，还发布了tf.transform，专门用来数据预处理。并且还推出了“动态图计算”TensorFlow Fold，这是被评价为“第一次清晰地在设计理念上领先”[12]。

用户还可以使用谷歌公司的PaaS TensorFlow产品Cloud Machine Learning来做分布式训练。现在也已经有了完整的TensorFlow Model Zoo。

另外，TensorFlow出色的版本管理和细致的官方文档手册，以及很容易找到解答的繁荣的社区，应该能让用户用起来相当顺手。

截至2017年3月，用TensorFlow作为生产平台和科研基础研发已经越来越坚实可靠。

1.6　机器学习的相关赛事

说到机器学习，不得不提到每年的一些挑战赛。近年来取得好成绩的队伍，常常是使用深度学习的方法的。正是这些赛事激励着全世界科学家不断采用更优化的方法提高算法结果的准确率，也引领着年度的深度学习探索方向。

1.6.1　ImageNet的ILSVRC

ILSVRC（ImageNet Large Scale Visual Recognition Challenge，大规模视觉识别挑战赛）是用来大规模评估对象检测和图像识别的算法的挑战赛。从2010年开始，至2016年已举办7届。ImageNet是目前世界上最大的图像识别数据库，拥有超过1500万张有标记的高分辨率图像的数据集，这些图像分属于大概22 000个类别。ILSVRC使用ImageNet的一个子集，分为1 000种类别，每种类别中都有大约1 000张图像。总之，大约有120万张训练图像，5万张验证图像和15万张测试图像。[13]图1-14所示为ImageNet的官方网站。

图1-14

ILSVRC每年邀请谷歌、微软、百度等IT企业使用ImageNet，测试他们图片分类系统运行情况。过去几年中，该系统的图像识别功能大大提高，出错率仅为约5%（比人眼还低，人眼的识别错误率大概在5.1%[14]）。在2015年，ILSVRC的错误率已经降低到了3.57%[15]，采用152层的ResNet获得了2015年分类任务的第一名。ILSVRC历年的Top-5错误率如图1-15所示。

在ImageNet上，习惯性地报告两个错误率：Top-1和Top-5。Top-1错误率是指，预测输出的概率最高的类别，是否和人工标记的类别一致，如果不一致，此时的概率。Top-5错误率是指，预测输出的概率最高的前5个类别当中，有没有和人工标记的类别一致，当5个都不一致时的概率。例如在图片分类任务下，对一张图片进行预测，输出这张图片分类概率最高的5个类别，只要有一个预测的类别和人工标注的类别标记一致，就是认为正确。当5个都不一致发生的概率就是Top-5错误率。

图1-15

值得自豪的是，在刚刚过去的ILSVRC 2016上，中国学术界和工业界科研团队包揽了多项冠军[16]。

- CUImage（商汤科技联合港中文）：目标检测第一。

- Trimps-Soushen（公安部三所）：目标定位第一。

- CUvideo（商汤科技联合港中文）：视频中物体检测子项目第一。

- NUIST（南京信息工程大学）：视频中的物体探测两个子项目第一。

- Hikvvision（海康威视）：场景分类第一。

- SenseCUSceneParsing（商汤科技联合港中文）：场景分析第一。

1.6.2　Kaggle

如果说ILSVRC企业参加的居多，那Kaggle这个平台则更多地面向个人开发者。图 1-16展示的是Kaggle的官方网站首页。

Kaggle成立于2010年，是一个进行数据发掘、数据分析和预测竞赛的在线平台。与Kaggle合作之后，一家公司可以提供一些数据，进而提出一个问题，Kaggle网站上的计算机科学家和数学家（也就是现在的数据科学家）将领取任务，提供潜在的解决方案。最终胜出的解决方案可以获得3万美元到25万美元的奖励。也就是说，Kaggle也是一个众包理念，利用全世界的人才来解决一个大问题。

图1-16

Kaggle这个比赛非常适合学生参加，因为一般在校学生可能拿不到很多数据。此外，Kaggle不仅对参赛者有算法能力上的要求，而且能锻炼参赛者对数据的“嗅觉”，使参赛者从数据本身问题出发来寻求解决方案。

1.6.3　天池大数据竞赛

“天池”是阿里搭建的一个大数据竞赛平台，图1-17展示的是它的官方网站页面。

图1-17

这个平台上一般会有一些穿衣搭配挑战、新浪微博互动预测、用户重复购买行为预测等赛事。平台提供的“赛题攻略”对新手入门有很大的引领作用。如果在一些项目上取得不错的成绩，还有丰厚的奖金，以及进入阿里巴巴的工作机会。

1.7　国内的人工智能公司

近年来，国内涌现出一批做人工智能的公司，很多原有的互联网公司也开始试水人工智能方向。虽然不可否认人工智能领域还是有一些泡沫存在，但是这个技术领域的井喷点确实来临了，确切地说是科研成果的井喷点。我们要做的就是加快科研成果向产品的转化速度。

国内的腾讯、阿里、百度三大公司在人工智能研究和商业化探索方面走得最早。腾讯优图是腾讯的人工智能开放平台；阿里云ET是阿里巴巴的智能机器人；百度主要在无人驾驶汽车和手机百度客户端的基于“自然语言的人机交互界面”的“度秘”上发力。这些都是人工智能在产业界应用的探索。此外，还有搜狗、云从科技、商汤科技、昆仑万维、格灵深瞳等公司，都在人工智能领域纷纷发力。

下面我们就来介绍国内几家比较有特色的做人工智能的公司。

（1）陌上花科技：衣+（dress+）。提供图像识别、图像搜索、物体追踪检测、图片自动化标记、图像视频智能分析、边看边买、人脸识别和分析等服务。其官方网站的首页如图1-18所示。

图1-18

（2）旷视科技：Face++。以人脸识别精度著称，并且提供人工智能开放平台。目前已经和美图秀秀、魔漫相机合作，实现美白、瘦脸、五官美化等美颜效果。此外，还和支付宝合作，未来有望推出“Smile to Pay”。其官方网站首页如图1-19所示。

图1-19

（3）科大讯飞。主要提供语音识别解决方案，以及语音合成、语言云（分词、词性标注、命名实体识别、依存句法分析、语义角色标注等）等语音扩展服务，有完善的SDK及多种语言实现的API。其官方网站首页如图1-20所示。

图1-20

（4）地平线。嵌入式人工智能的领导者，致力于提供高性能、低功耗、低成本、完整开放的嵌入式人工智能解决方案。其官方网站首页如图1-21所示。

图1-21

1.8　小结

本章主要介绍了人工智能、机器学习、深度学习的关系，以及深度学习的学习步骤，分析了这个领域的相关人群，以及这个领域的重要赛事。然后，全面介绍了TensorFlow的作用、特性，并介绍了国内做人工智能的公司，讲述了目前在产业界进行的探索，和提供给开发者的一些基础平台。

[1]　参考百度百科 “图灵测试”。

[2]　参考百度百科 “人工智能”。

[3]　参考威斯康星大学麦迪逊分校一个ppt的第14页：http://pages.cs.wisc.edu/～jerryzhu/pub/sslicml07.pdf。

[4]　这里，一些人担心人工智能超越人类还会产生哲学和伦理问题。我认为做这种讨论还为时尚早，严谨的数据基础是要突破的主要方向。

[5]　推荐读者阅读李航老师的《统计学习方法》，很快就能入门。

[6]　https://github.com/tensorflow/models/tree/master/inception

[7]https://github.com/zer0n/deepframeworks

[8]　参考百度百科 “异构数据库”。

[9]　http://karpathy.github.io/2015/05/21/rnn-effectiveness/

[10]　https://www.tensorflow.org/

[11]　http://ipython.org/notebook.html

[12]　参考论文《Deep Leaning with Dynamic Computation Graphs》：https://openreview.net/pdf?id=ryrGawqex。

[13]　参考论文《ImageNet Classification with Deep Convolutional Neural Networks》：http://www.cs.toronto.edu/～fritz/absps/ imagenet.pdf。

[14]　数据出自论文《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》：https://arxiv.org/abs/1502.01852。

[15]　数据出自论文《Deep Residual Learning for Image Recognition》：https://arxiv.org/abs/1512.03385。

第2章　TensorFlow环境的准备

本章的主要任务就是准备TensorFlow环境。与安装其他软件（如Caffe）相比，TensorFlow极容易安装，环境部署极为轻松。

接下来我们先介绍下载TensorFlow代码仓库，然后介绍基于pip的安装方式、基于Java的安装方式以及使用Bazel的源代码编译安装方式。

2.1　下载TensorFlow 1.1.0

2017年5月，TensorFlow已经开放到1.1.0-rc2版本，支持多种操作系统。接下来我们就用 1.1.0 版本来介绍TensorFlow的环境准备过程。

我们从GitHub代码仓库中将1.1.0版本的TensorFlow源代码下载下来，在Tags中选择1.1.0版本将跳转到1.1.0版本的代码仓库[1]，如图2-1所示。

图2-1

根据图2-2下载解压之后即得到源代码，我们将其保存在本地目录tensorflow-1.1.0中。

图2-2

2.2　基于pip的安装

pip是Python的包管理工具，主要用于PyPI[2]（Python Packet Index）上的包。命令简洁方便，包种类丰富，社区完善，并且拥有轻松升级/降级包的能力。

2.2.1　Mac OS环境准备

Mac OS是本书所讲内容依赖的环境，机器配置如图2-3所示。

图2-3

首先需要依赖Python环境，以及pip命令。这在Mac和Linux系统中一般都有。这里使用的Python版本是2.7.12。TensorFlow 1.1.0版本兼容Python 2和Python 3，读者可以用适合自己的Python环境。

1．安装virtualenv

virtualenv是Python的沙箱工具，用于创建独立的Python环境。我们毕竟是在自己机器上做实验，为了不来回修改各种环境变量，这里用virtualenv为TensorFlow创建一套“隔离”的Python运行环境。

首先，用pip安装virtualenv：

$ pip install virtualenv --upgrade

安装好后创建一个工作目录，这里直接在home下创建了一个tensorflow文件夹：

$ virtualenv --system-site-packages ~/tensorflow

然后进入该目录，激活沙箱：

$ cd ~/tensorflow
$ source bin/activate
(tensorflow) $

2．在virtualenv里安装TensorFlow

进入沙箱后，执行下面的命令来安装TensorFlow：

(tensorflow) $ pip install tensorflow==1.1.0

默认安装所需的依赖，直至安装成功。

3．运行TensorFlow

照着官方文档录入一个简单例子：

(tensorflow) $ python
Python 2.7.12 (default, Oct 11 2016, 05:16:02)
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> import tensorflow as tf
>>> hello = tf.constant('Hello,TensorFlow!')
>>> sess = tf.Session()
>>> print sess.run(hello)
Hello, TensorFlow!

恭喜，TensorFlow环境已经安装成功了。

注意，每次需要运行TensorFlow程序时，都需要进入tensorflow目录，然后执行source bin/activate命令来激活沙箱。

2.2.2　Ubuntu/Linux环境准备

使用Ubuntu/Linux的读者可以照着Mac OS的环境准备，先安装virtualenv的沙盒环境，再用pip安装TensorFlow软件包。

TensorFlow的Ubuntu/Linux安装分为CPU版本和GPU版本，下面来分别介绍。

（1）安装仅支持CPU的版本，直接安装如下：

$ pip install tensorflow==1.1.0

（2）安装支持GPU的版本的前提是已经安装了CUDA SDK，直接使用下面的命令：

$ pip install tensorflow-gpu==1.1.0

2.2.3　Windows环境准备

TensorFlow 1.1.0版本支持Windows 7、Windows 10和Server 2016。因为使用Windows PowerShell代替CMD，所以下面的命令均在PowerShell下执行。这里使用的是Windows 10系统，使用微软小娜呼唤出PowerShell，如图2-4所示。

图2-4

1．安装Python

TensorFlow在Windows上只支持64位Python 3.5.x，可以通过Python Releases for Windows[3]或Python 3.5 from Anaconda下载并安装Python 3.5.2（注意选择正确的操作系统）。下载后，安装界面如图2-5所示，注意勾选“Add Python 3.5 to PATH”。

图2-5

选择Customize installation（自定义安装），进入下一步。如图2-6所示，可以看出Python包自带pip命令。

图2-6

然后，等待安装完成，再到PowerShell中输入python，看到进入终端的命令提示则代表python安装成功。在“开始”->“所有程序”下也可以找到Python终端。安装成功后的界面如图2-7所示。

图2-7

TensorFlow的Windows安装也分为CPU版本和GPU版本，下面来分别介绍。

（1）CPU版本安装。在PowerShell中执行如下命令，默认安装TensorFlow 1.1.0版本及相关依赖。

C:\> pip install tensorflow==1.1.0

安装完成后如图2-8所示。

图2-8

（2）GPU版本安装。如果读者的机器支持安装GPU版本，请先安装如下两个驱动：CUDA[4]和CuDNN[5]（后者需要注册NVIDIA用户，并加入CuDNN开发组，然后填若干问卷，才可以下载）。选择下载版本时要注意与CUDA版本匹配。解压后保存至CUDA的安装目录下。然后，安装GPU版本，安装命令如下：

C:\> pip install tensorflow-gpu==1.1.0

2．运行TensorFlow

在微软小娜中，搜索“python”，直接模糊匹配，调出命令窗口，输入测试代码：

>>>import tensorflow as tf
>>>sess = tf.Session()
>>>a = tf.constant(10)
>>>b = tf.constant(22)
>>>print(sess.run(a + b))
32

正确输出结果32，安装完毕。

2.3　基于Java的安装

基于Java的方式安装，可以参照TensorFlow官方GitHub的安装方法[6]。

我们需要下载JAR（Java ARchive）libtensorflow-1.1.0-rc2.jar和运行TensorFlow需要的本地库。这些都可以直接从官方GitHub上下载，如图2-9所示。

图2-9

这里仍然用Mac OS X系统，下载后的文件如下：

libtensorflow-1.1.0-rc2.jar
libtensorflow_jni-cpu-darwin-x86_64-1.1.0-rc2.tar.gz

对libtensorflow_jni-cpu-darwin-x86_64-1.1.0-rc2.tar.gz进行解压，解压到当前目录jni。

tar zxvf libtensorflow_jni-cpu-darwin-x86_64-1.1.0-rc2.tar.gz -C ./jni

这样就完成了TensorFlow的Java安装。下面我们写一个例子来测试一下，看能否正确输出TensorFlow的版本。将下面代码写入文件，命名为MyClass.java。

import org.tensorflow.TensorFlow;

public class MyClass {
public static void main(String[] args) {
System.out.println("I'm using TensorFlow version: " +  TensorFlow.version());
}
}

然后进行编译：

javac -cp libtensorflow-1.1.0-rc2.jar MyClass.java

最后执行，成功输出所采用的TensorFlow版本，如图2-10所示。

图2-10

2.4　从源代码安装

从源代码编译安装，需要使用Bazel编译工具。我们先安装Bazel工具。在需要依赖的JDK 8配好之后，在Mac笔记本上直接执行下面命令，安装版本是0.4.4：

brew install bazel

其他操作系统（如Ubuntu）的计算机对Bazel的安装，可以采用apt-get等方式。

先进入tensorflow-1.1.0的源代码目录，运行./configure脚本会出现所采用的Python路径、是否用HDFS、是否用Google Cloud Platform等选项，读者可以根据自己的需要进行配置，或者直接按“回车”采用默认配置。

下面我们演示使用CPU版本的编译。具体如下：

➜  tensorflow-1.1.0 ./configure
Please specify the location of python．[Default is /usr/local/bin/python]:
Please specify optimization flags to use during compilation [Default is -march=native]:
Do you wish to use jemalloc as the malloc implementation? (Linux only) [Y/n]
jemalloc enabled on Linux
Do you wish to build TensorFlow with Google Cloud Platform support? [y/N]
No Google Cloud Platform support will be enabled for TensorFlow
Do you wish to build TensorFlow with Hadoop File System support? [y/N]
No Hadoop File System support will be enabled for TensorFlow
Do you wish to build TensorFlow with the XLA just-in-time compiler (experimental)? [y/N]
No XLA support will be enabled for TensorFlow
Found possible Python library paths:
/usr/local/Cellar/python/2.7.12_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages
/Library/Python/2.7/site-packages
Please input the desired Python library path to use． Default is [/usr/local/Cellar/ python/2.7.12_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages]

Using python library path: /usr/local/Cellar/python/2.7.12_2/Frameworks/Python. framework/Versions/2.7/lib/python2.7/site-packages
Do you wish to build TensorFlow with OpenCL support? [y/N]
No OpenCL support will be enabled for TensorFlow
Do you wish to build TensorFlow with CUDA support? [y/N]
No CUDA support will be enabled for TensorFlow
Configuration finished

随后，我们执行bazel编译命令，因为编译时需要耗费大量的内存，加入--local_resources 2048,4,1.0来限制内存大小。具体如下：

bazel build --local_resources 2048,4,1.0 -c opt //tensorflow/tools/pip_package:build_ pip_package
bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg

然后进入/tmp/tensorflow_pkg，可以看到生成的文件tensorflow-1.1.0-cp27-cp27m-macosx_10_12_intel.whl，直接安装如下：

pip install /tmp/tensorflow_pkg/tensorflow-1.1.0-cp27-cp27m-macosx_10_12_intel.whl

使用GPU版本的编译需要配置中选择使用CUDA，然后填写对应的CUDA SDK版本等，其他步骤均相同。

2.5　依赖的其他模块

TensorFlow在运行中需要做一些矩阵运算，时常会用到一些第三方模块，此外，在处理音频、自然语言时需要也要用到一些模块，建议一并安装好。本书“实战篇”中会大量用到这些扩展。

下面我们就来简单介绍TensorFlow依赖的一些模块。

2.5.1　numpy

numpy是用来存储和处理大型矩阵的科学计算包，比Python自身的嵌套列表结构（nested list structure）要高效的多。它包括：

- 一个强大的N维数组对象Array；

- 比较成熟的函数库；

- 用于整合C/C++和Fortran代码的工具包；

- 实用的线性代数、傅里叶变换和随机数生成函数。

numpy模块的安装方法如下：

pip install numpy --upgrade

2.5.2　matplotlib

matplotlib是Python最著名的绘图库，它提供了一整套和MATLAB相似的命令API，十分适合交互式地进行制图。用它可以画出美丽的线图、散点图、等高线图、条形图、柱状图、3D图等，而且还可以方便地将它作为绘图控件，嵌入GUI应用程序中。在后面的实例中，需要可视化地展现训练结果或者中间的特征映射，就很方便。

matplotlib模块的安装方法如下：

pip install matplotlib --upgrade

2.5.3　jupyter

jupyter notebook是Ipython的升级版，能够在浏览器中创建和共享代码、方程、说明文档。界面相当友好，功能也很强大。其实，jupyter实际就是一个基于Tornado框架的Web应用，使用MQ进行消息管理。

jupyter模块的安装方法如下：

pip install jupyter --upgrade

打开jupyter notebook：

jupyter notebook

出现如下显示：

[W 06:02:13.434 NotebookApp] Widgets are unavailable．Please install widgetsnbextension or ipywidgets 4.0
[I 06:02:13.454 NotebookApp] Serving notebooks from local directory: /Users/baidu/ Downloads/tensorflow-0.12/tensorflow
[I 06:02:13.454 NotebookApp] 0 active kernels
[I 06:02:13.454 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/
[I 06:02:13.454 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).

浏览器自动打开，启动成功，界面如图 2-11 所示。其中，在 tensorflow-1.1.0/tensorflow/ examples/udacity下有许多扩展名为.ipynb的示例文件，读者可以自行在浏览器中打开和学习。

图2-11

2.5.4　scikit-image

scikit-image[7]有一组图像处理的算法，可以使过滤一张图片变得很简单，非常适合用于对图像的预处理。

scikit-image模块的安装方法如下：

pip install scikit-image --upgrade

2.5.5　librosa

librosa是用Python进行音频特征提取的第三方库，有很多方式可以提取音频特征。

librosa模块的安装如下：

pip install librosa --upgrade

2.5.6　nltk

nltk[8]模块中包含着大量的语料库，可以很方便地完成很多自然语言处理的任务，包括分词、词性标注、命名实体识别（NER）及句法分析。

nltk的安装方法：

pip install nltk --upgrade

安装完成后，需要导入nltk工具包，下载nltk数据源，如下：

>>> import nltk
>>> nltk.download()

2.5.7　keras

Keras是第一个被添加到TensorFlow核心中的高级别框架，成为Tensorflow的默认API。第7章中会详细讲解Keras的使用。

keras模块的安装方法如下：

pip install keras --upgrade

2.5.8　tflearn

TFLearn是另一个支持TensorFlow的第三方框架，第7章中会详细讲解TFLearn的使用。

tflearn模块的安装方法如下：

pip install git+https://github.com/tflearn/tflearn.git

2.6　小结

本章介绍了TensorFlow环境的准备，分别讲解了使用pip命令、Java JAR文件、用Bazel工具对源代码进行编译这3种安装方式，以及在pip安装方式下，在Mac、Ubuntu/Linux、Windows系统上如何安装CPU版本和GPU版本的TensorFlow。

最后，讲了一些常用扩展的作用和安装，这些扩展在本书的“实战篇”中会用到。

[1]　https://github.com/tensorflow/tensorflow/tree/v1.1.0

[2]　https://pypi.python.org/pypi

[3]　https://www.python.org/downloads/windows/

[4]　https://developer.nvidia.com/cuda-downloads

[5]　https://developer.nvidia.com/cudnn

[6]　https://github.com/tensorflow/tensorflow/tree/master/tensorflow/java

[7]　http://scikit-image.org/

[8]　http://www.nltk.org/

第3章　可视化TensorFlow

可视化是认识程序的最直观方式。在做数据分析时，可视化一般是数据分析最后一步的结果呈现。把可视化放到“基础篇”，是为了让读者在安装完成后，就能先看一下TensorFlow到底有哪些功能，直观感受一下深度学习的学习成果，让学习目标一目了然。

3.1　PlayGround

PlayGround[1]是一个用于教学目的的简单神经网络的在线演示、实验的图形化平台，非常强大地可视化了神经网络的训练过程。使用它可以在浏览器里训练神经网络，对Tensorflow有一个感性的认识。

PlayGround界面从左到右由数据（DATA）、特征（FEATURES）、神经网络的隐藏层（HIDDEN LAYERS）和层中的连接线和输出（OUPUT）几个部分组成，如图3-1所示。

图3-1

3.1.1　数据

在二维平面内，点被标记成两种颜色。深色（电脑屏幕显示为蓝色）代表正值，浅色（电脑屏幕显示为黄色）代表负值。这两种颜色表示想要区分的两类，如图3-2所示。

图3-2

网站提供了4种不同形态的数据，分别是圆形、异或、高斯和螺旋，如图3-3所示。神经网络会根据所给的数据进行训练，再分类规律相同的点。

图3-3

PlayGournd中的数据配置非常灵活，可以调整噪声（noise）的大小。图3-4展示的是噪声为0、25和50时的数据分布。

图3-4

PlayGournd中也可以改变训练数据和测试数据的比例（ratio）。图3-5展示的是训练数据和测试数据比例为1 : 9和9 : 1时的情况。

图3-5

此外，PlayGournd中还可以调整输入的每批（batch）数据的多少，调整范围可以是1～30，就是说每批进入神经网络数据的点可以1～30个，如图3-6所示。

图3-6

3.1.2　特征

接下来我们需要做特征提取（feature extraction），每一个点都有X_1和X_2两个特征，由这两个特征还可以衍生出许多其他特征，如X_1X_1、X_2X_2、X_1X_2、sin(X_1)、sin(X_2)等，如图3-7所示。

图3-7

从颜色上，X_1左边浅色（电脑屏幕显示为黄色）是负，右边深色（电脑屏幕显示为蓝色）是正，X_1表示此点的横坐标值。同理，X_2上边深色是正，下边浅色是负，X_2表示此点的纵坐标值。X_1X_1是关于横坐标的“抛物线”信息，X_2X_2是关于纵坐标的“抛物线”信息，X_1X_2是“双曲抛物面”的信息，sin(X_1)是关于横坐标的“正弦函数”信息，sin(X_2)是关于纵坐标的“正弦函数”信息。

因此，我们要学习的分类器（classifier）就是要结合上述一种或者多种特征，画出一条或者多条线，把原始的蓝色和黄色数据分开。

3.1.3　隐藏层

我们可以设置隐藏层的多少，以及每个隐藏层神经元的数量，如图3-8所示。

图3-8

隐藏层之间的连接线表示权重（weight），深色（蓝色）表示用神经元的原始输出，浅色（黄色）表示用神经元的负输出。连接线的粗细和深浅表示权重的绝对值大小。鼠标放在线上可以看到具体值，也可以修改值，如图3-9所示。

图3-9

修改值时，同时要考虑激活函数，例如，当换成Sigmoid时，会发现没有负向的黄色区域了，因为Sigmoid的值域是(0,1)，如图3-10所示。

图3-10

下一层神经网络的神经元会对这一层的输出再进行组合。组合时，根据上一次预测的准确性，我们会通过反向传播给每个组合不同的权重。组合时连接线的粗细和深浅会发生变化，连接线的颜色越深越粗，表示权重越大。

3.1.4　输出

输出的目的是使黄色点都归于黄色背景，蓝色点都归于蓝色背景，背景颜色的深浅代表可能性的强弱。

我们选定螺旋形数据，7个特征全部输入，进行试验。选择只有3个隐藏层时，第一个隐藏层设置8个神经元，第二个隐藏层设置4个神经元，第三个隐藏层设置2个神经元。训练大概2分钟，测试损失（test loss）和训练损失（training loss）就不再下降了。训练完成时可以看出，我们的神经网络已经完美地分离出了橙色点和蓝色点，如图3-11所示。

图3-11

假设我们只输入最基本的前4个特征，给足多个隐藏层，看看神经网络的表现。假设加入6个隐藏层，前4层每层有8个神经元，第五层有6个神经元，第六层有2个神经元。结果如图3-12所示。

图3-12

我们发现，通过增加神经元的个数和神经网络的隐藏层数，即使没有输入许多特征，神经网络也能正确地分类。但是，假如我们要分类的物体是猫猫狗狗的图片，而不是肉眼能够直接识别出特征的黄点和蓝点呢？这时候怎样去提取那些真正有效的特征呢？

有了神经网络，我们的系统自己就能学习到哪些特征是有效的、哪些是无效的，通过自己学习的这些特征，就可以做到自己分类，这就大大提高了我们解决语音、图像这种复杂抽象问题的能力。

3.2　TensorBoard[2]

TensorBoard是TensorFlow自带的一个强大的可视化工具，也是一个Web应用程序套件。TensorBoard目前支持7种可视化，即SCALARS、IMAGES、AUDIO、GRAPHS、DISTRIBUTIONS、HISTOGRAMS和EMBEDDINGS。这7种可视化的主要功能如下。

- SCALARS：展示训练过程中的准确率、损失值、权重/偏置的变化情况。

- IMAGES：展示训练过程中记录的图像。

- AUDIO：展示训练过程中记录的音频。

- GRAPHS：展示模型的数据流图，以及训练在各个设备上消耗的内存和时间。

- DISTRIBUTIONS：展示训练过程中记录的数据的分布图。

- HISTOGRAMS：展示训练过程中记录的数据的柱状图。

- EMBEDDINGS：展示词向量（如Word2vec）后的投影分布。

TensorBoard通过运行一个本地服务器，来监听6006端口。在浏览器发出请求时，分析训练时记录的数据，绘制训练过程中的图像。在9.3节的MNIST示例中，会逐一讲解TensorBoard的图像绘制，让读者更好地了解训练的过程中发生了什么。本节我们就先看一下TensorBoard能够绘制出哪些东西。

TensorBoard的可视化界面如图3-13所示。

图3-13

从图3-13中可以看到，在标题处有上述几个可视化面板，下面通过一个示例，分别介绍这些可视化面板的功能。

这里，我们运行手写数字识别的入门例子，如下：

python tensorflow-1.1.0/tensorflow/examples/tutorials/mnist/mnist_with_summaries.py

然后，打开TensorBoard面板：

tensorboard ––logdir=/tmp/tensorflow/mnist/logs/mnist_with_summaries

这时，输出：

Starting TensorBoard 39 on port 6006
(You can navigate to http://192.168.0.101:6006)

我们就可以在浏览器中打开http://192.168.0.101:6006，查看面板的各项功能。

3.2.1　SCALARS面板

SCALARS面板的左边是一些选项，包括Split on undercores（用下划线分开显示）、Data downloadlinks（数据下载链接）、Smoothing（图像的曲线平滑程度）以及Horizontal Axis（水平轴）的表示，其中水平轴的表示分3种（STEP代表迭代次数，RELATIVE代表按照训练集和测试集的相对值，WALL代表按照时间），如图3-14左边所示。图3-14右边给出了准确率和交叉熵损失函数值的变化曲线（迭代次数是1000次）。

图3-14

SCALARS面板中还绘制了每一层的偏置（biases）和权重（weights）的变化曲线，包括每次迭代中的最大值、最小值、平均值和标准差，如图3-15所示。

图3-15

3.2.2　IMAGES面板

图3-16展示了训练数据集和测试数据集经过预处理后图片的样子。

图3-16

3.2.3　AUDIO面板

AUDIO面板是展示训练过程中处理的音频数据。这里暂时没有找到合适的例子，读者了解即可。

3.2.4　GRAPHS面板

GRAPHS面板是对理解神经网络结构最有帮助的一个面板，它直观地展示了数据流图。图 3-17 所示界面中节点之间的连线即为数据流，连线越粗，说明在两个节点之间流动的张量（tensor）越多。

图3-17

在GRAPHS面板的左侧，可以选择迭代步骤。可以用不同Color（颜色）来表示不同的Structure（整个数据流图的结构），或者用不同Color来表示不同Device（设备）。例如，当使用多个GPU时，各个节点分别使用的GPU不同。

当我们选择特定的某次迭代（如第899次）时，可以显示出各个节点的Compute time（计算时间）以及Memory（内存消耗），如图3-18所示。

图3-18

3.2.5　DISTRIBUTIONS面板

DISTRIBUTIONS面板和接下来要讲的HISTOGRAMS面板类似，只不过是用平面来表示来自特定层的激活前后、权重和偏置的分布。图3-19展示的是激活之前和激活之后的数据分布。

图3-19

3.2.6　HISTOGRAMS面板

HISTOGRAMS主要是立体地展现来自特定层的激活前后、权重和偏置的分布。图3-20展示的是激活之前和激活之后的数据分布。

图3-20

3.2.7　EMBEDDINGS面板

EMBEDDINGS面板在MNIST这个示例中无法展示，在3.3节中我们会用Word2vec例子来看一下这个面板的词嵌入投影仪。

3.3　可视化的例子

词嵌入（word embedding）在机器学习中非常常见，可以应用在自然语言处理、推荐系统等其他程序中。下面我们就以Word2vec为例来看看词嵌入投影仪的可视化。

TensorFlow的Word2Vec有basic、optimised这两个版本，我们重点来看这两个版本的可视化表示。

3.3.1　降维分析

本节将以GitHub上的一段代码[3]为例，讲述可视化的思路。

Word2vec采用text8[4]作为文本的训练数据集。这个文本中只包含a～z字符和空格，共27种字符。我们重点讲述产生的结果可视化的样子以及构建可视化的过程。这里我们采用的是Skip-gram模型，即根据目标词汇预测上下文。也就是说，给定n个词围绕着词w，用w来预测一个句子中其中一个缺漏的词c，以概率p(c|w)来表示。最后生成的用t-SNE降维呈现词汇接近程度的关系如图3-21所示。

图3-21

在word2vec_basic.py中，从获得数据到最终得到可视化的结果的过程分为5步。

（1）下载文件并读取数据。主要是read_data函数，它读取输入的数据，输出一个list，里面的每一项就是一个词。

def read_data(filename):
with zipfile.ZipFile(filename) as f:
data = tf.compat.as_str(f.read(f.namelist()[0])).split()
return data

这里的data就类似于['fawn', 'homomorphism', 'nordisk', 'nunnery']。

（2）建立一个词汇字典。这里首先建立了一个词汇字典，字典里是对应的词和这个词的编码。

vocabulary_size = 50000

def build_dataset(words):
count = [['UNK', -1]]
count.extend(collections.Counter(words).most_common(vocabulary_size - 1))
dictionary = dict()
for word, _ in count:
dictionary[word] = len(dictionary)
data = list()
unk_count = 0
for word in words:
if word in dictionary:
index = dictionary[word]
else:
index = 0  # dictionary['UNK']
unk_count += 1
data.append(index)
count[0][1] = unk_count
reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
return data, count, dictionary, reverse_dictionary

data, count, dictionary, reverse_dictionary = build_dataset(words)

dictionary里存储的就是词与这个词的编码；reverse_dictionary是反过来的dictionary，对应的是词的编码与这个词；data是list，存储的是词对应的编码，也就是第一步中得到的词的list，转化为词的编码表示；count中存储的是词汇和词频，其中重复数量少于49 999个词，用'UNK'来代表稀有词。具体示例如下：

data [5239, 3084, 12, 6, 195, 2, 3137, 46, 59, 156]
count [['UNK', 418391], ('the', 1061396), ('of', 593677), ('and', 416629),
('one', 411764), ('in', 372201), ('a', 325873), ('to', 316376), ('zero', 264975),
('nine', 250430)]
dictionary {'fawn': 0, 'homomorphism': 1, 'nordisk': 2, 'nunnery': 3, 'chthonic':
4, 'sowell': 5, 'sonja': 6, 'showa': 7, 'woods': 8, 'hsv': 9}
reverse_dictionary {0: 'fawn', 1: 'homomorphism', 2: 'nordisk', 3: 'nunnery', 4:
'chthonic', 5: 'sowell', 6: 'sonja', 7: 'showa', 8: 'woods', 9: 'hsv'}

（3）产生一个批次（batch）的训练数据。这里定义generate_batch函数，输入batch_size、num_skips和skip_windows，其中batch_size是每个batch的大小，num_skips代表样本的源端要考虑几次，skip_windows代表左右各考虑多少个词，其中skip_windows*2=num_skips。最后返回的是batch和label，batch的形状是[batch_size]，label的形状是[batch_size, 1]，也就是用一个中心词来预测一个周边词。

举个例子。假设我们的句子是“我在写一首歌”，我们将每一个字用dictionary中的编码代替，就变成了[123, 3084, 12, 6, 195, 90]，假设这里的window_size是3，也就是只预测上文一个词，下文一个词，假设我们的generate_batch函数从3084出发，源端重复2次，那么batch就是[3084 3084 12 12 6 6 195 195]，3084的上文是123，下文是12；12的上文是3084，下文是6；6的上文是12，下文是195；195的上文是6，下文是90。因此，对应输出的label就是：

[[ 123]
[12]
[3084]
[    6]
[  12]
[ 195]
[   6]
[   90]]

（4）构建和训练模型。这里我们构建一个Skip-gram模型，具体模型搭建可以参考Skip-gram的相关论文。执行结果如下：

Found and verified text8.zip
Data size 17005207 # 共有17005207个单词数
Most common words (+UNK) [['UNK', 418391], ('the', 1061396), ('of', 593677),
('and', 416629), ('one', 411764)]
Sample data [5239, 3084, 12, 6, 195, 2, 3137, 46, 59, 156] ['anarchism', 'originated',
'as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against']
3084 originated -> 5239 anarchism
3084 originated -> 12 as
12 as -> 3084 originated
12 as -> 6 a
6 a -> 195 term
6 a -> 12 as
195 term -> 6 a
195 term -> 2 of
Initialized
Average loss at step  0 :  263.743347168
Nearest to a: following, infantile, professor, airplane, retreat, implicated,
ideological, epstein,
Nearest to will: apokryphen, intercity, casta, nsc, commissioners, conjuring,
stockholders, bureaucrats,
Nearest to this: option, analgesia, quelled, maeshowe, comers, inevitably, kazan, burglary,
Nearest to in: embittered, specified, deicide, pontiff, omitted, edifice, levitt, cordell,
Nearest to world: intelligible, unguarded, pretext, cinematic, druidic, agm, embarks,
cingular,
Nearest to use: hab, tabula, estates, laminated, battle, loyola, arcadia, discography,
Nearest to from: normans, zawahiri, harrowing, fein, rada, incorrect, spandau, insolvency,
Nearest to people: diligent, tum, cour, komondor, lecter, sadly, barnard, ebony,
Nearest to it: fulfilled, referencing, paullus, inhibited, myra, glu, perpetuation,
theologiae,
Nearest to united: frowned, turkey, profusion, personifications, michelangelo,
sisters, okeh, claypool,
Nearest to new: infanta, fen, mizrahi, service, monrovia, mosley, taxonomy, year,
Nearest to seven: tilsit, prefect, phyla, varied, reformists, bc, berthe, acceptance,
Nearest to also: pri, navarrese, abandonware, env, plantinga, radiosity, oops, manna,
Nearest to about: lorica, nchen, closing, interpret, smuggler, viceroyalty, barsoom, caving,
Nearest to his: introduction, mania, rotates, switzer, elvis, warped, chilli,
etymological,
Nearest to and: robson, fun, paused, scent, clouds, insulation, boyfriend, agreeable,
Average loss at step  2000 :  113.878970229
Average loss at step  4000 :  53.0354625027
Average loss at step  6000 :  33.5644974816
Average loss at step  8000 :  23.246792558
Average loss at step  10000 :  17.7630081813

（5）用t-SNE降维呈现。这里我们将上一步训练的结果做了一个t-SNE降维处理，最终用Matplotlib绘制出图形，图形见图3-19。代码如下：

def plot_with_labels(low_dim_embs, labels, filename='tsne.png'):
assert low_dim_embs.shape[0] >= len(labels), "More labels than embeddings"
plt.figure(figsize=(18, 18))  # in inches
for i, label in enumerate(labels):
x, y = low_dim_embs[i, :]
plt.scatter(x, y)
plt.annotate(label,
xy=(x, y),
xytext=(5, 2),
textcoords='offset points',
ha='right',
va='bottom')

plt.savefig(filename)

try:
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000)
plot_only = 500
low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only, :])
labels = [reverse_dictionary[i] for i in xrange(plot_only)]
plot_with_labels(low_dim_embs, labels)

except ImportError:
print("Please install sklearn, matplotlib, and scipy to visualize embeddings.")

小知识

t-SNE是流形学习（manifold Learning）方法的一种。它假设数据是均匀采样于一个高维空间的低维流形，流形学习就是找到高维空间中的低维流形，并求出相应的嵌入映射，以实现维数约简或者数据可视化。流形学习方法分为线性的和非线性的两种。线性的流形学习方法如主成份分析（PCA），非线性的流形学习方法如等距特征映射（Isomap）、拉普拉斯特征映射（Laplacian eigenmaps，LE）、局部线性嵌入（Locally-linear embedding，LLE）等。

3.3.2　嵌入投影仪

在3.2节中我们说到，在TensorBorad的面板中还有一个EMBEDDINGS面板，用于交互式可视化和分析高维数据。对于上面的word2vec_basic.py文件，我们只是做了一个降维分析，下面我们就来看看TensorBorad在词嵌入中的投影。这里采用官方GitHub开源实现上的例子[5]进行讲解。

这里我们自定义了两个操作（operator，OP）：SkipgramWord2vec和NegTrainWord2vec。为什么需要自定义操作以及如何定义一个操作将在4.10节介绍。操作需要先编译，然后执行。这里采用Mac OS系统，在g++命令后加上-undefined dynamic_lookup参数：

TF_INC=$(python -c 'import tensorflow as tf; print(tf.sysconfig.get_include())')
g++ -std=c++11 -shared word2vec_ops.cc word2vec_kernels.cc -o word2vec_ops.so -fPIC -I $TF_INC -O2 -D_GLIBCXX_USE_CXX11_ABI=0

在当前目录下生成word2vec_ops.so文件，然后执行word2vec_optimized.py，生成的模型和日志文件位于/tmp/，我们执行：

tensorboard --logdir=/tmp/

访问http://192.168.0.101:6006/，得到的EMBEDDINGS面板如图3-22所示。

图3-22

在EMBEDDINGS面板左侧的工具栏中，可以选择降维的方式，有T-SNE、PCA和CUSTOM的降维方式，并且可以做二维/三维的图像切换。例如，切换到t-SNE降维工具，可以手动调整Dimension（困惑度）、Learning rate（学习率）等参数，最终生成10 000个点的分布，如图3-23所示。

图3-23

在EMBEDDINGS面板的右侧，可以采用正则表达式匹配出某些词，直观地看到词之间的余弦距离或欧式距离的关系，如图3-24所示。

图3-24

任意选择一个点，如8129，选择“isolate 101 points”按钮，将会展示出100个在空间上最接近被选择点的词，也可以调整展示的词的数量，如图3-25所示。

图3-25

3.4　小结

可视化是研究深度学习的一个重要方向，有利于我们直观地探究训练过程中的每一步发生的变化。TensorFlow提供了强大的工具TensorBoard，不仅有完善的API接口，而且提供的面板也非常丰富。在4.3.2节我们会讲解实现TensorBoard的API。在第17章我们还会讲到TensorFlow的调试工具，调试和可视化配合起来，有利于精准地调整模型。

[1]　http://playground.tensorflow.org/

[2]　本节内容参考https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tensorboard/README.md。

[3]　https://github.com/tensorflow/tensorflow/blob/master/tensorflow/examples/tutorials/word2vec/ word2vec_basic.py

[4]　http://mattmahoney.net/dc/textdata

[5]　https://github.com/tensorflow/models/blob/master/tutorials/embedding/word2vec_optimized.py

第4章　TensorFlow基础知识

本章主要参考TensorFlow官方网站上的新手入门[1]和扩展教程[2]，讲解TensorFlow的基本概念。本章从系统架构、设计理念、编程模型、常用API、存储与加载模型、线程及队列、加载数据、自定义操作等多个方面进行讲解，相信通过本章的学习，读者会对TensorFlow的全貌有一个基本的认识。本章的学习对理解TensorFlow的原理和实战非常重要，读者需要用心揣摩。

4.1　系统架构

图4-1给出的是TensorFlow的系统架构，自底向上分为设备层和网络层、数据操作层、图计算层、API层、应用层，其中设备层和网络层、数据操作层、图计算层是TensorFlow的核心层。[3]

下面就自底向上详细介绍一下TensorFlow的系统架构。最下层是网络通信层和设备管理层。网络通信层包括gRPC（google Remote Procedure Call Protocol）和远程直接数据存取（Remote Direct Memory Access，RDMA），这都是在分布式计算时需要用到的。设备管理层包括TensorFlow分别在CPU、GPU、FPGA等设备上的实现，也就是对上层提供了一个统一的接口，使上层只需要处理卷积等逻辑，而不需要关心在硬件上的卷积的实现过程。

其上是数据操作层，主要包括卷积函数、激活函数等操作（参见4.7节）。再往上是图计算层，也是我们要了解的核心，包含本地计算图和分布式计算图的实现（本章会做基础知识的梳理，包括图的创建、编译、优化和执行，本书的“实战篇”会介绍图的具体应用，第 15章会介绍分布式计算图的实现）。再往上是API层和应用层（4.4节和4.7节会介绍重点常用的API的Python实现以及一些其他语言的实现，本书的“实战篇”会重点讲解调用API层对深度学习各种网络模型的实现）。

图4-1

4.2　设计理念

TensorFlow的设计理念主要体现在以下两个方面。

（1）将图的定义和图的运行完全分开。因此，TensorFlow被认为是一个“符号主义”的库。

我们知道，编程模式通常分为命令式编程（imperative style programming）和符号式编程（symbolic style programming）。命令式编程就是编写我们理解的通常意义上的程序，很容易理解和调试，按照原有逻辑执行。符号式编程涉及很多的嵌入和优化，不容易理解和调试，但运行速度相对有所提升。现有的深度学习框架中，Torch是典型的命令式的，Caffe、MXNet采用了两种编程模式混合的方法，而TensorFlow完全采用符号式编程。

符号式计算一般是先定义各种变量，然后建立一个数据流图，在数据流图中规定各个变量之间的计算关系，最后需要对数据流图进行编译，但此时的数据流图还是一个空壳儿，里面没有任何实际数据，只有把需要运算的输入放进去后，才能在整个模型中形成数据流，从而形成输出值。[4]

例如：

t = 8 + 9
print(t)

在传统的程序操作中，定义了t的运算，在运行时就执行了，并输出17。而在TensorFlow中，数据流图中的节点，实际上对应的是TensorFlow API中的一个操作，并没有真正去运行：

import tensorflow as tf
t = tf.add(8, 9)
print(t) # 输出 Tensor("Add_1:0", shape=(), dtype=int32)

定义了一个操作，但实际上并没有运行。

（2）TensorFlow中涉及的运算都要放在图中，而图的运行只发生在会话（session）中。开启会话后，就可以用数据去填充节点，进行运算；关闭会话后，就不能进行计算了。因此，会话提供了操作运行和Tensor求值的环境。例如：

import tensorflow as tf

# 创建图
a = tf.constant([1.0, 2.0])
b = tf.constant([3.0, 4.0])
c = a * b

# 创建会话
sess = tf.Session()

# 计算c
print sess.run(c) # 进行矩阵乘法，输出[3., 8.]
sess.close()

了解了设计理念，接下来看一下TensorFlow的编程模型。

4.3　编程模型[5]

TensorFlow是用数据流图做计算的，因此我们先创建一个数据流图（也称为网络结构图），如图4-2所示，看一下数据流图中的各个要素。

图4-2讲述了TensorFlow的运行原理。图中包含输入（input）、塑形（reshape）、Relu层（Relu layer）、Logit层（Logit layer）、Softmax、交叉熵（cross entropy）、梯度（gradient）、SGD训练（SGD Trainer）等部分，是一个简单的回归模型。

它的计算过程是，首先从输入开始，经过塑形后，一层一层进行前向传播运算。Relu层（隐藏层）里会有两个参数，即Wh1和bh1，在输出前使用ReLu（Rectified Linear Units）激活函数做非线性处理。然后进入Logit层（输出层），学习两个参数Wsm和bsm。用Softmax来计算输出结果中各个类别的概率分布。用交叉熵来度量两个概率分布（源样本的概率分布和输出结果的概率分布）之间的相似性。然后开始计算梯度，这里是需要参数Wh1、bh1、Wsm和bsm，以及交叉熵后的结果。随后进入SGD训练，也就是反向传播的过程，从上往下计算每一层的参数，依次进行更新。也就是说，计算和更新的顺序为bsm、Wsm、bh1和 Wh1。

图4-2[6]

顾名思义，TensorFlow是指“张量的流动”。TensorFlow的数据流图是由节点（node）和边（edge）组成的有向无环图（directed acycline graph，DAG）。TensorFlow由Tensor和Flow两部分组成，Tensor（张量）代表了数据流图中的边，而Flow（流动）这个动作就代表了数据流图中节点所做的操作。

4.3.1　边

TensorFlow的边有两种连接关系：数据依赖和控制依赖[7]。其中，实线边表示数据依赖，代表数据，即张量。任意维度的数据统称为张量。在机器学习算法中，张量在数据流图中从前往后流动一遍就完成了一次前向传播（forword propagation），而残差[8]从后向前流动一遍就完成了一次反向传播（backword propagation）。

还有一种特殊边，一般画为虚线边，称为控制依赖（control dependency），可以用于控制操作的运行，这被用来确保happens-before关系，这类边上没有数据流过，但源节点必须在目的节点开始执行前完成执行。常用代码如下：

tf.Graph.control_dependencies(control_inputs)

TensorFlow支持的张量具有表4-1所示的数据属性。

表4-1

数据类型

Python类型

描述

DT_FLOAT

tf.float32

32位浮点型

DT_DOUBLE

tf.float64

64位浮点型

DT_INT64

tf.int64

64位有符号整型

DT_INT32

tf.int32

32位有符号整型

DT_INT16

tf.int16

16位有符号整型

DT_INT8

tf.int8

8位有符号整型

DT_UINT8

tf.uint8

8位无符号整型

DT_STRING

tf.string

可变长度的字节数组，每一个张量元素都是一个字节数组

DT_BOOL

tf.bool

布尔型

DT_COMPLEX64

tf.complex64

由两个32位浮点数组成的复数：实部和虚部

DT_QINT32

tf.qint32

用于量化[9]操作的32位有符号整型

DT_QINT8

tf.qint8

用于量化操作的8位有符号整型

DT_QUINT8

tf.quint8

用于量化操作的8位无符号整型

有关图及张量的实现的源代码均位于tensorflow-1.1.0/tensorflow/python/framework/ops.py，后面会详细讲。

4.3.2　节点

图中的节点又称为算子，它代表一个操作（operation，OP），一般用来表示施加的数学运算，也可以表示数据输入（feed in）的起点以及输出（push out）的终点，或者是读取/写入持久变量（persistent variable）的终点。表4-2列举了一些TensorFlow实现的算子。算子支持表4-1所示的张量的各种数据属性，并且需要在建立图的时候确定下来。

表4-2[10]

类别

示例

数学运算操作

Add、Subtract、Multiply、Div、Exp、Log、Greater、Less、Equal……

数组运算操作

Concat、Slice、Split、Constant、Rank、Shape、Shuffle……

矩阵运算操作

MatMul、MatrixInverse、MatrixDeterminant……

有状态的操作

Variable、Assign、AssignAdd……

神经网络构建操作

SoftMax、Sigmoid、ReLU、Convolution2D、MaxPool,……

检查点操作

Save、Restore

队列和同步操作

Enqueue、Dequeue、MutexAcquire、MutexRelease……

控制张量流动的操作

Merge、Switch、Enter、Leave、NextIteration

与操作相关的代码位于tensorflow-1.1.0/tensorflow/python/ops/目录下。以数学运算为例，代码为上述目录下的math_ops.py，里面定义了add、subtract、multiply、scalar_mul、div、divide、truediv、floordiv等数学运算，每个函数里面调用了gen_math_ops.py中的方法，这个文件是在编译（安装时）TensorFlow时生成的，位于Python库site-packages/tensorflow/python/ops/gen_math_ops.py中，随后又调用了tensorflow-1.1.0/tensorflow/core/kernels/下面的核函数实现。再例如，数组运算的代码位于tensorflow-1.1.0/tensorflow/python/ops/array_ops.py中，里面定义了concat、split、slice、size、rank等运算，每个函数都调用了gen_array_ops.py中的方法，这个文件也是在编译TensorFlow时生成的，位于Python库site-packages/tensorflow/python/ops/gen_array_ops.py中，随后又调用了tensorflow-1.1.0/tensorflow/core/kernels/下面的核函数实现。

4.3.3　其他概念

除了边和节点，TensorFlow还涉及其他一些概念，如图、会话、设备、变量、内核等。下面就分别介绍一下。

1．图

把操作任务描述成有向无环图。那么，如何构建一个图呢？构建图的第一步是创建各个节点。具体如下：

import tensorflow as tf

# 创建一个常量运算操作，产生一个 1×2 矩阵
matrix1 = tf.constant([[3., 3.]])

# 创建另外一个常量运算操作，产生一个 2×1 矩阵
matrix2 = tf.constant([[2.],[2.]])

# 创建一个矩阵乘法运算 ，把matrix1和matrix2作为输入
# 返回值product代表矩阵乘法的结果
product = tf.matmul(matrix1, matrix2)

2．会话

启动图的第一步是创建一个Session对象。会话（session）提供在图中执行操作的一些方法。一般的模式是，建立会话，此时会生成一张空图，在会话中添加节点和边，形成一张图，然后执行。

要创建一张图并运行操作的类，在Python的API中使用tf.Session，在C++ 的API中使用tensorflow::Session。示例如下：

with tf.Session() as sess:
result = sess.run([product])
print result

在调用Session对象的run()方法来执行图时，传入一些Tensor，这个过程叫填充（feed）；返回的结果类型根据输入的类型而定，这个过程叫取回（fetch）。

与会话相关的源代码位于tensorflow-1.1.0/tensorflow/python/client/session.py。

会话是图交互的一个桥梁，一个会话可以有多个图，会话可以修改图的结构，也可以往图中注入数据进行计算。因此，会话主要有两个API接口：Extend和Run。Extend操作是在Graph中添加节点和边，Run操作是输入计算的节点和填充必要的数据后，进行运算，并输出运算结果。

3．设备

设备（device）是指一块可以用来运算并且拥有自己的地址空间的硬件，如GPU和CPU。TensorFlow为了实现分布式执行操作，充分利用计算资源，可以明确指定操作在哪个设备上执行。具体如下：

with tf.Session() as sess:
# 指定在第二个gpu上运行
with tf.device("/gpu:1"):
matrix1 = tf.constant([[3., 3.]])
matrix2 = tf.constant([[2.],[2.]])
product = tf.matmul(matrix1, matrix2)

与设备相关的源代码位于tensorflow-1.1.0/tensorflow/python/framework/device.py。

4．变量

变量（variable）是一种特殊的数据，它在图中有固定的位置，不像普通张量那样可以流动。例如，创建一个变量张量，使用tf.Variable()构造函数，这个构造函数需要一个初始值，初始值的形状和类型决定了这个变量的形状和类型：

# 创建一个变量，初始化为标量0
state = tf.Variable(0, name="counter")

创建一个常量张量：

input1 = tf.constant(3.0)

TensorFlow 还提供了填充机制，可以在构建图时使用tf.placeholder()临时替代任意操作的张量，在调用Session对象的run()方法去执行图时，使用填充数据作为调用的参数，调用结束后，填充数据就消失。代码示例如下：

input1 = tf.placeholder(tf.float32)
input2 = tf.placeholder(tf.float32)
output = tf.multiply(input1, input2)
with tf.Session() as sess:
print sess.run([output], feed_dict={input1:[7.], input2:[2.]})
# 输出 [array([ 14.], dtype=float32)]

与变量相关的源代码位于tensorflow/tensorflow/python/ops/variables.py。

5．内核

我们知道操作（operation）是对抽象操作（如matmul或者add）的一个统称，而内核（kernel）则是能够运行在特定设备（如CPU、GPU）上的一种对操作的实现。因此，同一个操作可能会对应多个内核。

当自定义一个操作时，需要把新操作和内核通过注册的方式添加到系统中。4.10节会用一个示例来讲解如何自定义一个操作。

4.4　常用API

了解TensorFlow的API有助于在应用时得心应手。下面介绍的是常用API，在后面的示例中基本上都会用到。这里主要介绍基于Python的API，基于其他语言的API也大同小异，最重要的是理解API的功能及其背后的原理。[11]

4.4.1　图、操作和张量

TensorFlow的计算表现为数据流图，所以tf.Graph类中包含一系列表示计算的操作对象（tf.Operation），以及在操作之间流动的数据——张量对象（tf.Tensor）。与图相关的API均位于tf.Graph类中，参见表4-3。

表4-3

操作

描述

tf.Graph.init()

创建一个空图

tf.Graph.as_default()

将某图设置为默认图，并返回一个上下文管理器。如果不显式添加一个默认图，系统会自动设置一个全局的默认图。所设置的默认图，在模块范围内定义的节点都将默认加入默认图中

tf.Graph.device(device_name_or_function)

定义运行图所使用的设备，并返回一个上下文管理器

tf.Graph.name_scope(name)

为节点创建层次化的名称，并返回一个上下文管理器

tf.Operation类代表图中的一个节点，用于计算张量数据。该类型由节点构造器（如tf.matmul()或者Graph.create_op()）产生。例如，c = tf.matmul(a, b)创建一个Operation类，其类型为MatMul的操作类。与操作相关的API均位于tf.Operation类中，参见表4-4。

表4-4

操作

描述

tf.Operation.name

操作的名称

tf.Operation.type

操作的类型，如MatMul

tf.Operation.inputstf.Operation.outputs

操作的输入与输出

tf.Operation.control_inputs

操作的依赖

tf.Operation.run(feed_dict=None, session=None)

在会话中运行该操作

tf.Operation.get_attr(name)

获取操作的属性值

tf.Tensor类是操作输出的符号句柄，它不包含操作输出的值，而是提供了一种在tf.Session中计算这些值的方法。这样就可以在操作之间构建一个数据流连接，使TensorFlow能够执行一个表示大量多步计算的图形。与张量相关的API均位于tf.Tensor类中，参见表4-5。

表4-5

操作

描述

tf.Tensor.dtype

张量的数据类型

tf.Tensor.name

张量的名称

tf.Tensor.value_index

张量在操作输出中的索引

tf.Tensor.graph

张量所在的图

tf.Tensor.op

产生该张量的操作

tf.Tensor.consumers()

返回使用该张量的操作列表

tf.Tensor.eval(feed_dict=None, session=None)

在会话中求张量的值，需要使用sess.as_default()或者eval(session=sess)

tf.Tensor.get_shape()

返回用于表示张量的形状（维度）的类TensorShape

tf.Tensor.set_shape(shape)

更新张量的形状

tf.Tensor.device

设置计算该张量的设备

4.4.2　可视化

在第3章中，我们讲解了可视化面板的功能，但如何编写可视化的程序呢？可视化时，需要在程序中给必要的节点添加摘要（summary），摘要会收集该节点的数据，并标记上第几步、时间戳等标识，写入事件文件（event file）中。tf.summary.FileWriter类用于在目录中创建事件文件，并且向文件中添加摘要和事件，用来在TensorBoard中展示。9.3节将详细讲解可视化的过程。

表4-6给出了可视化常用的API操作。

表4-6

API

描述

tf.summary.FileWriter.init(logdir, graph=None, max_queue= 10, flush_secs=120, graph_def=None)

创建FileWriter和事件文件，会在logdir中创建一个新的事件文件

tf.summary.FileWriter.add_summary(summary, global_step=None)

将摘要添加到事件文件

tf.summary.FileWriter.add_event(event)

向事件文件中添加一个事件

tf.summary.FileWriter.add_graph(graph, global_step=None, graph_def=None)

向事件文件中添加一个图

tf.summary.FileWriter.get_logdir()

获取事件文件的路径

tf.summary.FileWriter.flush()

将所有事件都写入磁盘

tf.summary.FileWriter.close()

将事件写入磁盘，并关闭文件操作符

tf.summary.scalar(name, tensor, collections=None)

输出包含单个标量值的摘要

tf.summary.histogram(name, values, collections=None)

输出包含直方图的摘要

tf.summary.audio(name, tensor, sample_rate, max_outputs=3, collections=None)

输出包含音频的摘要

tf.summary.image(name, tensor, max_outputs=3, collections= None)

输出包含图片的摘要

tf.summary.merge(inputs, collections=None, name=None)

合并摘要，包含所有输入摘要的值

4.5　变量作用域

在TensorFlow中有两个作用域（scope），一个是name_scope，另一个是variable_scope。它们究竟有什么区别呢？简而言之，variable_scope主要是给variable_name加前缀，也可以给op_name加前缀；name_scope是给op_name加前缀。下面我们就来分别介绍。

4.5.1　variable_scope示例

variable_scope变量作用域机制在TensorFlow中主要由两部分组成：

v = tf.get_variable(name, shape, dtype, initializer) # 通过所给的名字创建或是返回一个变量
tf.variable_scope(<scope_name>) # 为变量指定命名空间

当tf.get_variable_scope().reuse == False时，variable_scope作用域只能用来创建新变量：

with tf.variable_scope("foo"):
v = tf.get_variable("v", [1])
v2 = tf.get_variable("v", [1])
assert v.name == "foo/v:0"

上述程序会抛出ValueError错误，因为v这个变量已经被定义过了，但tf.get_variable_scope().reuse默认为False，所以不能重用。

当tf.get_variable_scope().reuse == True时，作用域可以共享变量：

with tf.variable_scope("foo") as scope:
v = tf.get_variable("v", [1])
with tf.variable_scope("foo", reuse=True):
#也可以写成：
#scope.reuse_variables()
v1 = tf.get_variable("v", [1])
assert v1 == v

1．获取变量作用域

可以直接通过tf.variable_scope()来获取变量作用域：

with tf.variable_scope("foo") as foo_scope:
v = tf.get_variable("v", [1])
with tf.variable_scope(foo_scope)
w = tf.get_variable("w", [1])

如果在开启的一个变量作用域里使用之前预先定义的一个作用域，则会跳过当前变量的作用域，保持预先存在的作用域不变。

with tf.variable_scope("foo") as foo_scope:
assert foo_scope.name == "foo"
with tf.variable_scope("bar")
with tf.variable_scope("baz") as other_scope:
assert other_scope.name == "bar/baz"
with tf.variable_scope(foo_scope) as foo_scope2:
assert foo_scope2.name == "foo"  # 保持不变

2．变量作用域的初始化

变量作用域可以默认携带一个初始化器，在这个作用域中的子作用域或变量都可以继承或者重写父作用域初始化器中的值。方法如下：

with tf.variable_scope("foo", initializer=tf.constant_initializer(0.4)):
v = tf.get_variable("v", [1])
assert v.eval() == 0.4  # 被作用域初始化
w = tf.get_variable("w", [1], initializer=tf.constant_initializer(0.3)):
assert w.eval() == 0.3  # 重写初始化器的值
with tf.variable_scope("bar"):
v = tf.get_variable("v", [1])
assert v.eval() == 0.4  # 继承默认的初始化器
with tf.variable_scope("baz", initializer=tf.constant_initializer(0.2)):
v = tf.get_variable("v", [1])
assert v.eval() == 0.2  # 重写父作用域的初始化器的值

上面讲的是variable_name，那对于op_name呢？在variable_scope作用域下的操作，也会被加上前缀：

with tf.variable_scope("foo"):
x = 1.0 + tf.get_variable("v", [1])
assert x.op.name == "foo/add"

variable_scope主要用在循环神经网络（RNN）的操作中，其中需要大量的共享变量。

4.5.2　name_scope示例

TensorFlow中常常会有数以千计的节点，在可视化的过程中很难一下子展示出来，因此用name_scope为变量划分范围，在可视化中，这表示在计算图中的一个层级。name_scope会影响op_name，不会影响用get_variable()创建的变量，而会影响通过Variable()创建的变量。因此：

with tf.variable_scope("foo"):
with tf.name_scope("bar"):
v = tf.get_variable("v", [1])
b = tf.Variable(tf.zeros([1]), name='b')
x = 1.0 + v
assert v.name == "foo/v:0"
assert b.name == "foo/bar/b:0"
assert x.op.name == "foo/bar/add"

可以看出，tf.name_scope()返回的是一个字符串，如上述的"bar"。name_scope对用get_variable()创建的变量的名字不会有任何影响，而Variable()创建的操作会被加上前缀，并且会给操作加上名字前缀。

4.6　批标准化

批标准化（batch normalization，BN）是为了克服神经网络层数加深导致难以训练而诞生的。我们知道，深度神经网络随着网络深度加深，训练起来会越来越困难，收敛速度会很慢，常常会导致梯度弥散问题（vanishing gradient problem）。

统计机器学习中有一个ICS（Internal Covariate Shift）理论，这是一个经典假设：源域（source domain）和目标域（target domain）的数据分布是一致的。也就是说，训练数据和测试数据是满足相同分布的。这是通过训练数据获得的模型能够在测试集获得好的效果的一个基本保障。

Covariate Shift是指训练集的样本数据和目标样本集分布不一致时，训练得到的模型无法很好地泛化（generalization）。它是分布不一致假设之下的一个分支问题，也就是指源域和目标域的条件概率是一致的，但是其边缘概率不同。的确，对于神经网络的各层输出，在经过了层内操作后，各层输出分布就会与对应的输入信号分布不同，而且差异会随着网络深度增大而加大，但是每一层所指向的样本标记（label）仍然是不变的。

解决思路一般是根据训练样本和目标样本的比例对训练样本做一个矫正。因此，通过引入批标准化来规范化[12]某些层或者所有层的输入，从而固定每层输入信号的均值与方差。

4.6.1　方法

批标准化一般用在非线性映射（激活函数）之前，对x=Wu+b做规范化，使结果（输出信号各个维度）的均值为0，方差为1。让每一层的输入有一个稳定的分布会有利于网络的训练。

4.6.2　优点

批标准化通过规范化让激活函数分布在线性区间，结果就是加大了梯度，让模型更加大胆地进行梯度下降，于是有如下优点：

- 加大探索的步长，加快收敛的速度；

- 更容易跳出局部最小值；

- 破坏原来的数据分布，一定程度上缓解过拟合。

因此，在遇到神经网络收敛速度很慢或梯度爆炸[13]（gradient explode）等无法训练的情况下，都可以尝试用批标准化来解决。

4.6.3　示例

我们对每层的Wx_plus_b进行批标准化，这个步骤放在激活函数之前：

# 计算Wx_plus_b的均值和方差，其中axes=[0]表示想要标准化的维度
fc_mean, fc_var = tf.nn.moments(Wx_plus_b, axes=[0], )
scale = tf.Variable(tf.ones([out_size]))
shift = tf.Variable(tf.zeros([out_size]))
epsilon = 0.001
Wx_plus_b = tf.nn.batch_normalization(Wx_plus_b, fc_mean, fc_var, shift,
scale, epsilon)
# 也就是在做：
# Wx_plus_b = (Wx_plus_b - fc_mean) / tf.sqrt(fc_var + 0.001)
# Wx_plus_b = Wx_plus_b * scale + shift

更多关于批标准化的理论可以查看Sergey Ioffe和Christian Szegedy的论文《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》[14]。

4.7　神经元函数及优化方法

本节主要介绍TensorFlow中构建神经网络所需的神经元函数，包括各种激活函数、卷积函数、池化函数、损失函数、优化器等。读者阅读时，务必把本节介绍的常用API记熟，这有利于在“实战篇”轻轻松松地构建神经网络进行训练。

4.7.1　激活函数

激活函数（activation function）运行时激活神经网络中某一部分神经元，将激活信息向后传入下一层的神经网络。神经网络之所以能解决非线性问题（如语音、图像识别），本质上就是激活函数加入了非线性因素，弥补了线性模型的表达力，把“激活的神经元的特征”通过函数保留并映射到下一层。

因为神经网络的数学基础是处处可微的，所以选取的激活函数要能保证数据输入与输出也是可微的。那么激活函数在TensorFlow中是如何表达的呢？

激活函数不会更改输入数据的维度，也就是输入和输出的维度是相同的。TensorFlow中有如下激活函数，它们定义在tensorflow-1.1.0/tensorflow/python/ops/nn.py文件中，这里包括平滑非线性的激活函数，如sigmoid、tanh、elu、softplus和softsign，也包括连续但不是处处可微的函数relu、relu6、crelu和relu_x，以及随机正则化函数dropout：

tf.nn.relu()
tf.nn.sigmoid()
tf.nn.tanh()
tf.nn.elu()
tf.nn.bias_add()
tf.nn.crelu()
tf.nn.relu6()
tf.nn.softplus()
tf.nn.softsign()
tf.nn.dropout() # 防止过拟合，用来舍弃某些神经元

上述激活函数的输入均为要计算的x（一个张量），输出均为与x数据类型相同的张量。常见的激活函数有sigmoid、tanh、relu和softplus这4种。下面我们就来逐一讲解。

（1）sigmoid函数。这是传统神经网络中最常用的激活函数之一（另一个是tanh），对应的公式和图像如图4-3所示。

使用方法如下：

a = tf.constant([[1.0, 2.0], [1.0, 2.0], [1.0, 2.0]])
sess = tf.Session()
print sess.run(tf.sigmoid(a))

图4-3

sigmoid函数的优点在于，它的输出映射在(0,1)内，单调连续，非常适合用作输出层，并且求导比较容易。但是，它也有缺点，因为软饱和性[15]，一旦输入落入饱和区，f '(x)就会变得接近于0，很容易产生梯度消失[16]。

（2）tanh函数。对应的公式和图像如图4-4所示。

图4-4

tanh函数也具有软饱和性。因为它的输出以0为中心，收敛速度比sigmoid要快。但是仍无法解决梯度消失的问题。

（3）relu函数是目前最受欢迎的激活函数。softplus可以看作是ReLU的平滑版本。relu定义为f(x)=max(x,0)。softplus定义为f(x)=log(1+exp(x))。

由图4-5可见，relu在x<0时硬饱和。由于x>0时导数为1，所以，relu能够在x>0时保持梯度不衰减，从而缓解梯度消失问题，还能够更快地收敛，并提供了神经网络的稀疏表达能力。但是，随着训练的进行，部分输入会落到硬饱和区，导致对应的权重无法更新，称为“神经元死亡”。

图4-5

使用示例如下：

a = tf.constant([-1.0, 2.0])
with tf.Session() as sess:
b = tf.nn.relu(a)
print sess.run(b)

除了relu本身外，TensorFlow还定义了relu6，也就是定义在min(max(features, 0), 6)的tf.nn.relu6(features, name=None)，以及crelu，也就是tf.nn.crelu(features, name=None)。

（4）dropout函数。一个神经元将以概率keep_prob决定是否被抑制。如果被抑制，该神经元的输出就为0；如果不被抑制，那么该神经元的输出值将被放大到原来的1/keep_prob倍。[17]

在默认情况下，每个神经元是否被抑制是相互独立的。但是否被抑制也可以通过noise_shape来调节。当noise_shape[i] == shape(x)[i]时，x中的元素是相互独立的。如果shape(x) = [k, l, m, n]，x中的维度的顺序分别为批、行、列和通道，如果noise_shape = [k, 1, 1, n]，那么每个批和通道都是相互独立的，但是每行和每列的数据都是关联的，也就是说，要不都为0，要不都还是原来的值。

使用示例如下：

a = tf.constant([[-1.0, 2.0, 3.0, 4.0]])
with tf.Session() as sess:
b = tf.nn.dropout(a, 0.5, noise_shape = [1,4])
print sess.run(b)
b = tf.nn.dropout(a, 0.5, noise_shape = [1,1])
print sess.run(b)

激活函数的选择

当输入数据特征相差明显时，用tanh的效果会很好，且在循环过程中会不断扩大特征效果并显示出来。当特征相差不明显时，sigmoid效果比较好。同时，用sigmoid和tanh作为激活函数时，需要对输入进行规范化，否则激活后的值全部都进入平坦区，隐层的输出会全部趋同，丧失原有的特征表达。而relu会好很多，有时可以不需要输入规范化来避免上述情况。

因此，现在大部分的卷积神经网络都采用relu作为激活函数。我估计大概有85%～90%的神经网络会采用ReLU，10%～15%的神经网络会采用tanh，尤其用在自然语言处理上。

4.7.2　卷积函数

卷积函数是构建神经网络的重要支架，是在一批图像上扫描的二维过滤器。9.4.1 节会详细讲解卷积的过程。卷积函数定义在tensorflow-1.1.0/tensorflow/python/ops下的nn_impl.py和nn_ops.py文件中。

tf.nn.convolution(input, filter, padding, strides=None,
dilation_rate=None, name=None, data_format=None)
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None,
data_format= None, name=None)
tf.nn.depthwise_conv2d (input, filter, strides, padding, rate=None, name=None,
data_format=None)
tf.nn.separable_conv2d (input, depthwise_filter, pointwise_filter, strides, padding,
rate=None, name=None, data_format=None)
tf.nn.atrous_conv2d(value, filters, rate, padding, name=None)
tf.nn.conv2d_transpose(value, filter, output_shape, strides, padding='SAME',
data_format='NHWC', name=None)
tf.nn.conv1d(value, filters, stride, padding, use_cudnn_on_gpu=None,
data_format= None, name=None)
tf.nn.conv3d(input, filter, strides, padding, name=None)
tf.nn.conv3d_transpose(value, filter, output_shape, strides, padding='SAME', name=None)

下面就分别加以说明。

（1）tf.nn.convolution(input, filter, padding, strides=None, dilation_rate=None, name=None, data_format =None)这个函数计算N维卷积的和。

（2）tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)这个函数的作用是对一个四维的输入数据input和四维的卷积核filter进行操作，然后对输入数据进行一个二维的卷积操作，最后得到卷积之后的结果。

def conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None,
data_format=None, name=None)
# 输入：
#   input：一个Tensor。数据类型必须是float32或者float64
#   filter：一个Tensor。数据类型必须是input相同
#   strides：一个长度是4的一维整数类型数组，每一维度对应的是input中每一维的对应移动步数，
#   比如，strides[1]对应input[1]的移动步数
#   padding：一个字符串，取值为SAME或者VALID
#   padding='SAME'：仅适用于全尺寸操作，即输入数据维度和输出数据维度相同
#   padding='VALID'：适用于部分窗口，即输入数据维度和输出数据维度不同
#   use_cudnn_on_gpu：一个可选布尔值，默认情况下是True
#   name：（可选）为这个操作取一个名字
# 输出：一个Tensor，数据类型是input相同

使用示例如下：

input_data = tf.Variable( np.random.rand(10,9,9,3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 2), dtype = np.float32)
y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')

打印出tf.shape(y)的结果是[10 9 9 2]。

（3）tf.nn.depthwise_conv2d (input, filter, strides, padding, rate=None, name=None,data_format= None) 这个函数输入张量的数据维度是[batch, in_height, in_width, in_channels]，卷积核的维度是[filter_height, filter_width, in_channels, channel_multiplier]，在通道in_channels上面的卷积深度是1，depthwise_conv2d函数将不同的卷积核独立地应用在in_channels的每个通道上（从通道1到通道channel_multiplier），然后把所以的结果进行汇总。最后输出通道的总数是in_channels * channel_multiplier。

使用示例如下：

input_data = tf.Variable( np.random.rand(10, 9, 9, 3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 5), dtype = np.float32)
y = tf.nn.depthwise_conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')

这里打印出tf.shape(y)的结果是[10 9 9 15]。

（4）tf.nn.separable_conv2d (input, depthwise_filter, pointwise_filter, strides, padding, rate=None, name=None, data_format=None)是利用几个分离的卷积核去做卷积。在这个API中，将应用一个二维的卷积核，在每个通道上，以深度channel_multiplier进行卷积。

def separable_conv2d (input, depthwise_filter, pointwise_filter, strides, padding,
rate=None, name=None, data_format=None)
# 特殊参数：
#   depthwise_filter：一个张量。数据维度是四维[filter_height, filter_width, in_channels,
#   channel_multiplier]。其中，in_channels的卷积深度是1
#   pointwise_filter：一个张量。数据维度是四维[1, 1, channel_multiplier * in_channels,
#   out_channels]。其中，pointwise_filter是在depthwise_filter卷积之后的混合卷积

使用示例如下：

input_data = tf.Variable( np.random.rand(10, 9, 9, 3), dtype = np.float32 )
depthwise_filter = tf.Variable( np.random.rand(2, 2, 3, 5), dtype = np.float32)
pointwise_filter = tf.Variable( np.random.rand(1, 1, 15, 20), dtype = np.float32)
# out_channels >= channel_multiplier * in_channels
y = tf.nn.separable_conv2d(input_data, depthwise_filter, pointwise_filter,
strides = [1, 1, 1, 1], padding = 'SAME')

这里打印出tf.shape(y)的结果是[10 9 9 20]。

（5）tf.nn.atrous_conv2d(value, filters, rate, padding, name=None)计算Atrous卷积，又称孔卷积或者扩张卷积。

使用示例如下：

input_data = tf.Variable( np.random.rand(1,5,5,1), dtype = np.float32 )
filters = tf.Variable( np.random.rand(3,3,1,1), dtype = np.float32)
y = tf.nn.atrous_conv2d(input_data, filters, 2, padding='SAME')

这里打印出tf.shape(y)的结果是[1 5 5 1]。

（6）tf.nn.conv2d_transpose(value, filter, output_shape, strides, padding='SAME', data_format='NHWC', name=None)[18]在解卷积网络（deconvolutional network）中有时称为“反卷积”，但实际上是conv2d的转置，而不是实际的反卷积。

def conv2d_transpose(value, filter, output_shape, strides, padding='SAME',
data_format='NHWC', name=None)
# 特殊参数：
#   output_shape：一维的张量，表示反卷积运算后输出的形状
# 输出：和value一样维度的Tensor

使用示例如下：

x = tf.random_normal(shape=[1,3,3,1])
kernel = tf.random_normal(shape=[2,2,3,1])
y = tf.nn.conv2d_transpose(x,kernel,output_shape=[1,5,5,3],
strides=[1,2,2,1],padding="SAME")

这里打印出tf.shape(y)的结果是[1 5 5 3]。

（7）tf.nn.conv1d(value, filters, stride, padding, use_cudnn_on_gpu=None, data_format=None, name=None)和二维卷积类似。这个函数是用来计算给定三维的输入和过滤器的情况下的一维卷积。不同的是，它的输入是三维，如[batch, in_width, in_channels]。卷积核的维度也是三维，少了一维filter_height，如 [filter_width, in_channels, out_channels]。stride是一个正整数，代表卷积核向右移动每一步的长度。

（8）tf.nn.conv3d(input, filter, strides, padding, name=None)和二维卷积类似。这个函数用来计算给定五维的输入和过滤器的情况下的三维卷积。和二维卷积相对比：

- input的shape中多了一维in_depth，形状为Shape[batch, in_depth, in_height, in_width, in_channels]；

- filter的shape中多了一维filter_depth，由filter_depth, filter_height, filter_width构成了卷积核的大小；

- strides中多了一维，变为[strides_batch, strides_depth, strides_height, strides_width, strides_channel]，必须保证strides[0] = strides[4] = 1。

（9）tf.nn.conv3d_transpose(value, filter, output_shape, strides, padding='SAME', name=None)和二维反卷积类似，不再赘述。

4.7.3　池化函数

在神经网络中，池化函数一般跟在卷积函数的下一层，它们也被定义在tensorflow-1.1.0/ tensorflow/python/ops下的nn.py和gen_nn_ops.py文件中。

tf.nn.avg_pool(value, ksize, strides, padding, data_format='NHWC', name=None)
tf.nn.max_pool(value, ksize, strides, padding, data_format='NHWC', name=None)
tf.nn.max_pool_with_argmax(input, ksize, strides, padding, Targmax=None, name=None)
tf.nn.avg_pool3d(input, ksize, strides, padding, name=None)
tf.nn.max_pool3d(input, ksize, strides, padding, name=None)
tf.nn.fractional_avg_pool(value, pooling_ratio, pseudo_random=None, overlapping=None,
deterministic=None, seed=None, seed2=None, name=None)
tf.nn.fractional_max_pool(value, pooling_ratio, pseudo_random=None, overlapping=None,
deterministic=None, seed=None, seed2=None, name=None)
tf.nn.pool(input, window_shape, pooling_type, padding, dilation_rate=None, strides=None,
name=None, data_format=None)

池化操作是利用一个矩阵窗口在张量上进行扫描，将每个矩阵窗口中的值通过取最大值或平均值来减少元素个数。每个池化操作的矩阵窗口大小是由ksize指定的，并且根据步长strides决定移动步长。下面就分别来说明。

（1）tf.nn.avg_pool(value, ksize, strides, padding, data_format='NHWC', name=None)。这个函数计算池化区域中元素的平均值。

def avg_pool(value, ksize, strides, padding, data_format='NHWC', name=None)
# 输入：
#   value：一个四维的张量。数据维度是[batch, height, width, channels]
#   ksize：一个长度不小于4的整型数组。每一位上的值对应于输入数据张量中每一维的窗口对应值
#   strides：一个长度不小于4的整型数组。该参数指定滑动窗口在输入数据张量每一维上的步长
#   padding：一个字符串，取值为SAME或者VALID
#   data_format: 'NHWC'代表输入张量维度的顺序，N为个数，H为高度，W为宽度，C为通道数（RGB三
#   通道或者灰度单通道）
#   name（可选）：为这个操作取一个名字
# 输出：一个张量，数据类型和value相同

使用示例如下：

input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)

y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
output = tf.nn.avg_pool(value = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1],
padding ='SAME')

上述代码打印出tf.shape(output)的结果是[10 6 6 10]。计算输出维度的方法是：shape(output) = (shape(value) - ksize + 1) / strides。

（2）tf.nn.max_pool(value, ksize, strides, padding, data_format='NHWC', name=None)。这个函数是计算池化区域中元素的最大值。

使用示例如下：

input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = np.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)
y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
output = tf.nn.max_pool(value = y, ksize = [1, 2, 2, 1], strides = [1, 1, 1, 1],
padding ='SAME')

上述代码打印出tf.shape(output)的结果是[10 6 6 10]。

（3）tf.nn.max_pool_with_argmax(input, ksize, strides, padding, Targmax = None, name=None)。这个函数的作用是计算池化区域中元素的最大值和该最大值所在的位置。

在计算位置argmax的时候，我们将input铺平了进行计算，所以，如果input = [b, y, x, c]，那么索引位置是(( b height + y) width + x) * channels + c。

使用示例如下，该函数只能在GPU下运行，在CPU下没有对应的函数实现：

input_data = tf.Variable( np.random.rand(10,6,6,3), dtype = tf.float32 )
filter_data = tf.Variable( np.random.rand(2, 2, 3, 10), dtype = np.float32)

y = tf.nn.conv2d(input_data, filter_data, strides = [1, 1, 1, 1], padding = 'SAME')
output, argmax = tf.nn.max_pool_with_argmax(input = y, ksize = [1, 2, 2, 1],
strides = [1, 1, 1, 1], padding = 'SAME')

返回结果是一个张量组成的元组（output, argmax），output表示池化区域的最大值；argmax的数据类型是Targmax，维度是四维。

（4）tf.nn.avg_pool3d()和tf.nn.max_pool3d()分别是在三维下的平均池化和最大池化。

（5）tf.nn.fractional_avg_pool()和tf.nn.fractional_max_pool()分别是在三维下的平均池化和最大池化。

（6）tf.nn.pool(input, window_shape, pooling_type, padding, dilation_rate=None, strides=None, name=None, data_format=None)。这个函数执行一个N维的池化操作。

4.7.4　分类函数

TensorFlow中常见的分类函数主要有sigmoid_cross_entropy_with_logits、softmax、log_softmax、softmax_cross_entropy_with_logits等，它们也主要定义在tensorflow-1.1.0/tensorflow/python/ops的nn.py和nn_ops.py文件中。

tf.nn.sigmoid_cross_entropy_with_logits(logits, targets, name=None)
tf.nn.softmax(logits, dim=-1, name=None)
tf.nn.log_softmax(logits, dim=-1, name=None)
tf.nn.softmax_cross_entropy_with_logits(logits, labels, dim=-1, name=None)
tf.nn.sparse_softmax_cross_entropy_with_logits(logits, labels, name=None)

下面我们就逐一讲解。

（1）tf.nn.sigmoid_cross_entropy_with_logits(logits, targets, name=None)：

def sigmoid_cross_entropy_with_logits(logits, targets, name=None):
# 输入：logits:[batch_size, num_classes],targets:[batch_size, size].logits用最后一
# 层的输入即可
# 最后一层不需要进行sigmoid运算，此函数内部进行了sigmoid操作
# 输出：loss [batch_size, num_classes]

这个函数的输入要格外注意，如果采用此函数作为损失函数，在神经网络的最后一层不需要进行sigmoid运算。

（2）tf.nn.softmax(logits, dim=-1, name=None)计算Softmax激活，也就是softmax = exp(logits) / reduce_sum(exp(logits), dim)。

（3）tf.nn.log_softmax(logits, dim=-1, name=None)计算log softmax激活，也就是logsoftmax = logits - log(reduce_sum(exp(logits), dim))。

（4）tf.nn.softmax_cross_entropy_with_logits(_sentinel=None, labels=None, logits=None, dim=-1, name =None)：

def softmax_cross_entropy_with_logits(logits, targets, dim=-1, name=None):
# 输入：logits and labels 均为[batch_size, num_classes]
# 输出： loss:[batch_size], 里面保存的是batch中每个样本的交叉熵

（5）tf.nn.sparse_softmax_cross_entropy_with_logits(logits, labels, name=None) ：

def sparse_softmax_cross_entropy_with_logits(logits, labels, name=None):
# logits是神经网络最后一层的结果
# 输入：logits: [batch_size, num_classes]  labels: [batch_size]，必须在[0, num_classes]
# 输出：loss [batch_size]，里面保存是batch中每个样本的交叉熵

4.7.5　优化方法

如何加速神经网络的训练呢？目前加速训练的优化方法基本都是基于梯度下降的，只是细节上有些差异。梯度下降是求函数极值的一种方法，学习到最后就是求损失函数的极值问题。

TensorFlow提供了很多优化器（optimizer），我们重点介绍下面这8个：

class tf.train.GradientDescentOptimizer
class tf.train.AdadeltaOptimizer
class tf.train.AdagradOptimizer
class tf.train.AdagradDAOptimizer
class tf.train.MomentumOptimizer
class tf.train.AdamOptimizer
class tf.train.FtrlOptimizer
class tf.train.RMSPropOptimizer

这8个优化器对应8种优化方法，分别是梯度下降法（BGD和SGD）、Adadelta法、Adagrad法（Adagrad和AdagradDAO）、Momentum法（Momentum和Nesterov Momentum）、Adam、Ftrl法和RMSProp法，其中BGD、SGD、Momentum和Nesterov Momentum是手动指定学习率的，其余算法能够自动调节学习率。

下面就介绍其中几种优化方法。

1．BGD法

BGD的全称是batch gradient descent，即批梯度下降。这种方法是利用现有参数对训练集中的每一个输入生成一个估计输出yi，然后跟实际输出yi比较，统计所有误差，求平均以后得到平均误差，以此作为更新参数的依据。它的迭代过程为：

（1）提取训练集中的所有内容{x_1, …, xn}，以及相关的输出yi；

（2）计算梯度和误差并更新参数。

这种方法的优点是，使用所有训练数据计算，能够保证收敛，并且不需要逐渐减少学习率；缺点是，每一步都需要使用所有的训练数据，随着训练的进行，速度会越来越慢。

那么，如果将训练数据拆分成一个个批次（batch），每次抽取一批数据来更新参数，是不是会加速训练呢？这就是最常用的SGD。

2．SGD法

SGD的全称是stochastic gradient descent，即随机梯度下降。因为这种方法的主要思想是将数据集拆分成一个个批次（batch），随机抽取一个批次来计算并更新参数，所以也称为MBGD（minibatch gradient descent）。

SGD在每一次迭代计算mini-batch的梯度，然后对参数进行更新。与BGD相比，SGD在训练数据集很大时，仍能以较快的速度收敛。但是，它仍然会有下面两个缺点。

（1）由于抽取不可避免地梯度会有误差，需要手动调整学习率（learning rate），但是选择合适的学习率又比较困难。尤其在训练时，我们常常想对常出现的特征更新速度快一些，而对不常出现的特征更新速度慢一些，而SGD在更新参数时对所有参数采用一样的学习率，因此无法满足要求。

（2）SGD容易收敛到局部最优，并且在某些情况下可能被困在鞍点。

为了解决学习率固定的问题，又引入了Momentum法。

3．Momentum法

Momentum是模拟物理学中动量的概念，更新时在一定程度上保留之前的更新方向，利用当前的批次再微调本次的更新参数，因此引入了一个新的变量v（速度），作为前几次梯度的累加。因此，Momentum能够更新学习率，在下降初期，前后梯度方向一致时，能够加速学习；在下降的中后期，在局部最小值的附近来回震荡时，能够抑制震荡，加快收敛。

4．Nesterov Momentum法

Nesterov Momentum法由Ilya Sutskever在Nesterov工作的启发下提出的，是对传统Momentum法的一项改进，其基本思路如图4-6所示。

图4-6[19]

标准Momentum法首先计算一个梯度（短的1号线），然后在加速更新梯度的方向进行一个大的跳跃（长的1号线）；Nesterov项首先在原来加速的梯度方向进行一个大的跳跃（2号线），然后在该位置计算梯度值（3号线），然后用这个梯度值修正最终的更新方向（4号线）。

上面介绍的优化方法都需要我们自己设定学习率，接下来介绍几种自适应学习率的优化方法。

5．Adagrad法

Adagrad法能够自适应地为各个参数分配不同的学习率，能够控制每个维度的梯度方向。这种方法的优点是能够实现学习率的自动更改：如果本次更新时梯度大，学习率就衰减得快一些；如果这次更新时梯度小，学习率衰减得就慢一些。

6．Adadelta法

Adagrad法仍然存在一些问题：其学习率单调递减，在训练的后期学习率非常小，并且需要手动设置一个全局的初始学习率。Adadelta法用一阶的方法，近似模拟二阶牛顿法，解决了这些问题。

7．RMSprop法

RMSProp法与Momentum法类似，通过引入一个衰减系数，使每一回合都衰减一定比例。在实践中，对循环神经网络（RNN）效果很好。

8．Adam法

Adam的名称来源于自适应矩估计[20]（adaptive moment estimation）。Adam法根据损失函数针对每个参数的梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。

9．各个方法的比较

Karpathy在MNIST数据集上用上述几个优化器做了一些性能比较，发现如下规律[21]：在不怎么调整参数的情况下，Adagrad法比SGD法和Momentum法更稳定，性能更优；精调参数的情况下，精调的SGD法和Momentum法在收敛速度和准确性上要优于Adagrad法。

各个优化器的损失值比较结果如图4-7所示。

图4-7

各个优化器的测试准确率比较如图4-8所示。

图4-8

各个优化器的训练准确率比较如图4-9所示。

图4-9

想要更深入研究各种优化方法，可以参考《An overview of gradient descent optimization algorithms》[22]。

4.8　模型的存储与加载

训练好一个神经网络模型后，我们就希望能够将其应用在预测数据上。那么，如何把模型存储起来呢？同时，对于一个已经存储起来的模型，在将其应用在预测数据上时又如何加载呢？

TensorFlow的API提供了以下两种方式来存储和加载模型。

（1）生成检查点文件（checkpoint file），扩展名一般为.ckpt，通过在tf.train.Saver对象上调用Saver.save()生成。它包含权重和其他在程序中定义的变量，不包含图结构。如果需要在另一个程序中使用，需要重新创建图形结构，并告诉TensorFlow如何处理这些权重。

（2）生成图协议文件（graph proto file），这是一个二进制文件，扩展名一般为.pb，用tf.train.write_graph()保存，只包含图形结构，不包含权重，然后使用tf.import_graph_def()来加载图形。

下面我们就分“模型存储”和“图存储”来介绍这两种方式。在TensorFlow的高级API，如Keras中，也提供了更高级的语句来保存和加载模型，在7.2.3节中会介绍。

4.8.1　模型的存储与加载

模型存储主要是建立一个tf.train.Saver()来保存变量，并且指定保存的位置，一般模型的扩展名为.ckpt。

下面我们定义一个新的神经网络，含两个全连接层和一个输出层，来训练MNIST数据集，并把训练好的模型存储起来。我们用MNIST数据集来说明。[23]

1．加载数据及定义模型

加载数据及定义模型的代码如下：

# 加载数据
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
trX, trY, teX, teY = mnist.train.images, mnist.train.labels, mnist.test.images,
mnist.test.labels

X = tf.placeholder("float", [None, 784])
Y = tf.placeholder("float", [None, 10])
# 初始化权重参数
w_h = init_weights([784, 625])
w_h2 = init_weights([625, 625])
w_o = init_weights([625, 10])

# 定义权重函数
def init_weights(shape):
return tf.Variable(tf.random_normal(shape, stddev=0.01))

# 定义模型
def model(X, w_h, w_h2, w_o, p_keep_input, p_keep_hidden):
# 第一个全连接层
X = tf.nn.dropout(X, p_keep_input)
h = tf.nn.relu(tf.matmul(X, w_h))

h = tf.nn.dropout(h, p_keep_hidden)
# 第二个全连接层
h2 = tf.nn.relu(tf.matmul(h, w_h2))
h2 = tf.nn.dropout(h2, p_keep_hidden)

return tf.matmul(h2, w_o) #输出预测值

生成网络模型，得到预测值，代码如下:

p_keep_input = tf.placeholder("float")
p_keep_hidden = tf.placeholder("float")
py_x = model(X, w_h, w_h2, w_o, p_keep_input, p_keep_hidden)

定义损失函数，代码如下：

cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x, Y))
train_op = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cost)
predict_op = tf.argmax(py_x, 1)

接下来我们就要训练刚才定义好的模型，并把每一轮训练得到的参数都存储下来。

2．训练模型及存储模型

首先，我们定义一个存储路径，这里就用当前路径下的ckpt_dir目录，代码如下：

ckpt_dir = "./ckpt_dir"
if not os.path.exists(ckpt_dir):
os.makedirs(ckpt_dir)

定义一个计数器，为训练轮数计数，代码如下：

# 计数器变量，设置它的trainable=False，不需要被训练
global_step = tf.Variable(0, name='global_step', trainable=False)

当定义完所有变量后，调用tf.train.Saver()来保存和提取变量，其后面定义的变量将不会被存储，代码如下：

# 在声明完所有变量后，调用tf.train.Saver
saver = tf.train.Saver()
# 位于tf.train.Saver之后的变量将不会被存储
non_storable_variable = tf.Variable(777)

训练模型并存储，如下：

with tf.Session() as sess:
tf.initialize_all_variables().run()

start = global_step.eval() # 得到global_step的初始值
print("Start from:", start)

for i in range(start, 100):
# 以128作为batch_size
for start, end in zip(range(0, len(trX), 128), range(128, len(trX)+1, 128)):
sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end],
p_keep_input: 0.8, p_keep_hidden: 0.5})

global_step.assign(i).eval() # 更新计数器
saver.save(sess, ckpt_dir + "/model.ckpt", global_step=global_step) # 存储模型

于是，在训练的过程中，ckpt_dir下会出现16个文件，其中有5个model.ckpt-{n}.data- 00000-of-00001文件，是训练过程中保存的模型，5个model.ckpt-{n}.meta文件，是训练过程中保存的元数据（TensorFlow默认只保存最近5个模型和元数据，删除前面没用的模型和元数据），5个model.ckpt-{n}.index文件，{n}代表迭代次数，以及1个检查点文本文件，里面保存着当前模型和最近的5个模型，内容如下：

model_checkpoint_path: "model.ckpt-60"
all_model_checkpoint_paths: "model.ckpt-56"
all_model_checkpoint_paths: "model.ckpt-57"
all_model_checkpoint_paths: "model.ckpt-58"
all_model_checkpoint_paths: "model.ckpt-59"
all_model_checkpoint_paths: "model.ckpt-60"

那么，假如在训练某个模型时突然因为某种原因，脚本停止运行了，或者机器重启了，是不是就要从头开始训练呢？我们知道，训练一个神经网络的时间都比较长，少则几个小时，多则几天，甚至几周。如果能将之前训练的参数保存下来，就可以在出现意外状况时接着上一次的地方开始训练。此外，每个固定的轮数在检查点保存一个模型（.ckpt文件），也有利于随时将模型拿出来进行预测，用前几次的预测效果就可以估计出神经网络究竟设计得怎么样。

3．加载模型

如果有已经训练好的模型变量文件，可以用saver.restore来进行模型加载：

with tf.Session() as sess:
tf.initialize_all_variables().run()

ckpt = tf.train.get_checkpoint_state(ckpt_dir)
if ckpt and ckpt.model_checkpoint_path:
print(ckpt.model_checkpoint_path)
saver.restore(sess, ckpt.model_checkpoint_path) # 加载所有的参数
# 从这里开始就可以直接使用模型进行预测，或者接着继续训练了

4.8.2　图的存储与加载

当仅保存图模型时，才将图写入二进制协议文件中，例如：

v = tf.Variable(0, name='my_variable')
sess = tf.Session()
tf.train.write_graph(sess.graph_def, '/tmp/tfmodel', 'train.pbtxt')

当读取时，又从协议文件中读取出来：

with tf.Session() as _sess:
with gfile.FastGFile("/tmp/tfmodel/train.pbtxt",'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
_sess.graph.as_default()
tf.import_graph_def(graph_def, name='tfgraph')

4.9　队列和线程

和TensorFlow中的其他组件一样，队列（queue）本身也是图中的一个节点，是一种有状态的节点，其他节点，如入队节点（enqueue）和出队节点（dequeue），可以修改它的内容。例如，入队节点可以把新元素插到队列末尾，出队节点可以把队列前面的元素删除。本节主要介绍队列、队列管理器、线程和协调器的有关知识。

4.9.1　队列

TensorFlow中主要有两种队列，即FIFOQueue和RandomShuffleQueue，它们的源代码实现在tensorflow-1.1.0/tensorflow/python/ops/data_flow_ops.py中。

1．FIFOQueue

FIFOQueue创建一个先入先出队列。例如，我们在训练一些语音、文字样本时，使用循环神经网络的网络结构，希望读入的训练样本是有序的，就要用FIFOQueue。关于循环神经网络的讲解参见9.5节。

我们先创建一个含有队列的图：

import tensorflow as tf

# 创建一个先入先出队列,初始化队列插入0.1、0.2、0.3三个数字
q = tf.FIFOQueue(3, "float")
init = q.enqueue_many(([0.1, 0.2, 0.3],))
# 定义出队、+1、入队操作
x = q.dequeue()
y = x + 1
q_inc = q.enqueue([y])

然后开启一个会话，执行2次q_inc操作，随后查看队列的内容：

with tf.Session() as sess:
sess.run(init)
quelen =  sess.run(q.size())
for i in range(2):
sess.run(q_inc) # 执行2次操作，队列中的值变为0.3,1.1,1.2

quelen =  sess.run(q.size())
for i in range(quelen):
print (sess.run(q.dequeue())) # 输出队列的值

最终结果如下：

0.3
1.1
1.2

2．RandomShuffleQueue

RandomShuffleQueue创建一个随机队列，在出队列时，是以随机的顺序产生元素的。例如，我们在训练一些图像样本时，使用CNN的网络结构，希望可以无序地读入训练样本，就要用RandomShuffleQueue，每次随机产生一个训练样本。关于CNN的实战讲解参见第10章。

RandomShuffleQueue在TensorFlow使用异步计算时非常重要。因为TensorFlow的会话是支持多线程的，我们可以在主线程里执行训练操作，使用RandomShuffleQueue作为训练输入，开多个线程来准备训练样本，将样本压入队列后，主线程会从队列中每次取出mini-batch的样本进行训练。详细的例子将在4.10节中详细讲解。

下面我们创建一个随机队列，队列最大长度为10，出队后最小长度为2：

q = tf.RandomShuffleQueue(capacity=10, min_after_dequeue=2, dtypes="float")

然后开启一个会话，执行10次入队操作，8次出队操作：

sess = tf.Session()
for i in range(0, 10): #10次入队
sess.run(q.enqueue(i))

for i in range(0, 8): # 8次出队
print(sess.run(q.dequeue()))

发现结果确实是乱序的：

5.0
0.0
6.0
9.0
2.0
3.0
1.0
4.0

我们尝试修改入队次数为12次，再运行，发现程序阻断不动，或者我们尝试修改出队次数为10次，即不保留队列最小长度，发现队列输出8次结果后，在终端仍然阻断了。现象如图4-10所示，在箭头处卡在不动。这种情况称为阻断。

图4-10

阻断一般发生在：

- 队列长度等于最小值，执行出队操作；

- 队列长度等于最大值，执行入队操作。

只有队列满足要求后，才能继续执行。可以通过设置会话在运行时的等待时间来解除阻断：

run_options = tf.RunOptions(timeout_in_ms = 10000)    # 等待10秒
try:
sess.run(q.dequeue(), options=run_options)
except tf.errors.DeadlineExceededError:
print('out of range')

上面的例子都是在会话的主线程中进行入队操作。当数据量很大时，入队操作从硬盘中读取数据，放入内存中，主线程需要等待入队操作完成，才能进行训练操作。会话中可以运行多个线程，我们使用线程管理器QueueRunner创建一系列的新线程进行入队操作，让主线程继续使用数据，即训练网络和读取数据是异步的，主线程在训练网络，另一个线程在将数据从硬盘读入内存。

4.9.2　队列管理器

我们创建一个含有队列的图：

q = tf.FIFOQueue(1000, "float")
counter = tf.Variable(0.0)    # 计数器
increment_op = tf.assign_add(counter, tf.constant(1.0))    # 操作：给计数器加1
enqueue_op = q.enqueue([counter]) # 操作：计数器值加入队列

创建一个队列管理器QueueRunner，用这两个操作向队列q中添加元素。目前我们只使用一个线程：

qr = tf.train.QueueRunner(q, enqueue_ops=[increment_op, enqueue_op] * 1)

启动一个会话，从队列管理器qr中创建线程：

#主线程
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
enqueue_threads = qr.create_threads(sess, start=True)  # 启动入队线程
#主线程
for i in range(10):
print (sess.run(q.dequeue()))

输出结果如下：

4.0
9.0
12.0
15.0
18.0
22.0
25.0
27.0
32.0
35.0

不是我们期待的自然数列，并且线程被阻断。这是因为加1操作和入队操作不同步，可能加1操作执行了很多次之后，才会进行一次入队操作。另外，因为主线程的训练（出队操作）和读取数据的线程的训练（入队操作）是异步的，主线程会一直等待数据送入。

QueueRunner 有一个问题就是：入队线程自顾自地执行，在需要的出队操作完成之后，程序没法结束。这样就要使用tf.train.Coordinator来实现线程间的同步，终止其他线程。

4.9.3　线程和协调器

使用协调器（coordinator）来管理线程：

# 主线程
sess = tf.Session()
sess.run(tf.global_variables_initializer())

# Coordinator：协调器，协调线程间的关系可以视为一种信号量，用来做同步
coord = tf.train.Coordinator()

# 启动入队线程，协调器是线程的参数
enqueue_threads = qr.create_threads(sess, coord = coord,start=True)

# 主线程
for i in range(0, 10):
print(sess.run(q.dequeue()))

coord.request_stop()# 通知其他线程关闭
coord.join(enqueue_threads) # join操作等待其他线程结束，其他所有线程关闭之后，这一函数才能返回

发现上述代码能正常运行，返回结果，并结束。但我们发现，在关闭队列线程后，再执行出队操作，就会抛出tf.errors.OutOfRange错误。把coord.request_stop()和主线程的出队操作q.dequeue()调换位置，如下：

coord.request_stop()

# 主线程
for i in range(0, 10):
print(sess.run(q.dequeue()))

coord.join(enqueue_threads)

这种情况就需要使用tf.errors.OutOfRangeError来捕捉错误，终止循环：

coord.request_stop()

# 主线程
for i in range(0, 10):
try:
print(sess.run(q.dequeue()))
except tf.errors.OutOfRangeError:
break

coord.join(enqueue_threads)

所有队列管理器被默认加在图的tf.GraphKeys.QUEUE_RUNNERS集合中。

4.10　加载数据

TensorFlow作为符号编程框架，需要先构建数据流图，再读取数据，随后进行模型训练。TensorFlow官方网站给出了以下读取数据3种方法[24]。

- 预加载数据（preloaded data）：在TensorFlow图中定义常量或变量来保存所有数据。

- 填充数据（feeding）：Python产生数据，再把数据填充后端。

- 从文件读取数据（reading from file）：从文件中直接读取，让队列管理器从文件中读取数据。

4.10.1　预加载数据

预加载数据的示例如下：

x1 = tf.constant([2, 3, 4])
x2 = tf.constant([4, 0, 1])
y = tf.add(x1, x2)

这种方式的缺点在于，将数据直接嵌在数据流图中，当训练数据较大时，很消耗内存。

4.10.2　填充数据

使用sess.run()中的feed_dict参数，将Python产生的数据填充给后端。

import tensorflow as tf
# 设计图
a1 = tf.placeholder(tf.int16)
a2 = tf.placeholder(tf.int16)
b = tf.add(x1, x2)
# 用Python产生数据
li1 = [2, 3, 4]
li2 = [4, 0, 1]
# 打开一个会话，将数据填充给后端
with tf.Session() as sess:
print sess.run(b, feed_dict={a1: li1, a2: li2})

填充的方式也有数据量大、消耗内存等缺点，并且数据类型转换等中间环节增加了不小开销。这时最好用第三种方法，在图中定义好文件读取的方法，让TensorFlow自己从文件中读取数据，并解码成可使用的样本集。

4.10.3　从文件读取数据

从文件读取数据分为如下两个步骤：

（1）把样本数据写入TFRecords二进制文件；

（2）再从队列中读取。

我们以MNIST数据集为例来说明，如何把MNIST的数据转换成TFRecords文件。

TFRecords是一种二进制文件，能更好地利用内存，更方便地复制和移动，并且不需要单独的标记文件。接下来我们就看一下如何转换，具体代码参见 tensorflow-1.1.0/tensorflow/examples/ how_tos/reading_data/convert_to_records.py。

1．生成TFRecords文件

我们定义主函数，给训练、验证、测试数据集做转换：

def main(unused_argv):
# 获取数据
data_sets = mnist.read_data_sets(FLAGS.directory,
dtype=tf.uint8, # 注意，这里的编码是uint8
reshape=False,
validation_size=FLAGS.validation_size)

# 将数据转换为tf.train.Example类型，并写入TFRecords文件
convert_to(data_sets.train, 'train')
convert_to(data_sets.validation, 'validation')
convert_to(data_sets.test, 'test')

转换函数convert_to的主要功能是，将数据填入到tf.train.Example的协议缓冲区（protocol buffer）中，将协议缓冲区序列化为一个字符串，通过tf.python_io.TFRecordWriter 写入TFRecords文件。

def convert_to(data_set, name):
images = data_set.images
labels = data_set.labels
num_examples = data_set.num_examples # 55000个训练数据，5000个验证数据，10000个测试数据

if images.shape[0] != num_examples:
raise ValueError('Images size %d does not match label size %d.' %
(images.shape[0], num_examples))
rows = images.shape[1] # 28
cols = images.shape[2] # 28
depth = images.shape[3] # 1，是黑白图像，所以是单通道

filename = os.path.join(FLAGS.directory, name + '.tfrecords')
print('Writing', filename)
writer = tf.python_io.TFRecordWriter(filename)

for index in range(num_examples):
image_raw = images[index].tostring()

# 写入协议缓冲区中，height、width、depth、label编码成int64类型，image_raw编码成二进制
example = tf.train.Example(features=tf.train.Features(feature={
'height': _int64_feature(rows),
'width': _int64_feature(cols),
'depth': _int64_feature(depth),
'label': _int64_feature(int(labels[index])),
'image_raw': _bytes_feature(image_raw)}))

writer.write(example.SerializeToString()) # 序列化为字符串
writer.close()

编码函数如下：

def _int64_feature(value):
return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

def _bytes_feature(value):
return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

运行结束后，在/tmp/data下生成3个文件，即train.tfrecords、validation.tfrecords和test.tfrecords。

2．从队列中读取

一旦生成了TFRecords文件，接下来就可以使用队列读取数据了。主要分为3步：

（1）创建张量，从二进制文件读取一个样本；

（2）创建张量，从二进制文件随机读取一个mini-batch；

（3）把每一批张量传入网络作为输入节点。

代码参见tensorflow-1.1.0/tensorflow/examples/how_tos/reading_data/fully_connected_reader.py。

首先我们定义从文件中读取并解析一个样本：

def read_and_decode(filename_queue): # 输入文件名队列
reader = tf.TFRecordReader()
_, serialized_example = reader.read(filename_queue)
features = tf.parse_single_example( # 解析example
serialized_example,
# 必须写明features里面的key的名称
features={
'image_raw': tf.FixedLenFeature([], tf.string), # 图片是string类型
'label': tf.FixedLenFeature([], tf.int64),  # 标记是int64类型
})
# 对于BytesList，要重新进行解码，把string类型的0维Tensor变成uint8类型的一维Tensor
image = tf.decode_raw(features['image_raw'], tf.uint8)
image.set_shape([mnist.IMAGE_PIXELS])
# Tensor("input/DecodeRaw:0", shape=(784,), dtype=uint8)

# image张量的形状为：Tensor("input/sub:0", shape=(784,), dtype=float32)
image = tf.cast(image, tf.float32) * (1．/ 255) - 0.5

# 把标记从uint8类型转换为int32类型
# label张量的形状为Tensor("input/Cast_1:0", shape=(), dtype=int32)
label = tf.cast(features['label'], tf.int32)

return image, label

接下来使用tf.train.shuffle_batch将前面生成的样本随机化，获得一个最小批次的张量：

def inputs(train, batch_size, num_epochs):

# 输入参数:
#  train: 选择输入训练数据/验证数据
#  batch_size: 训练的每一批有多少个样本
#  num_epochs: 过几遍数据，设置为0/None表示永远训练下去
"""
返回结果：A tuple (images, labels)
* images: 类型float, 形状[batch_size, mnist.IMAGE_PIXELS]，范围[-0.5, 0.5].
* labels： 类型int32，形状[batch_size]，范围 [0, mnist.NUM_CLASSES]
注意tf.train.QueueRunner 必须用tf.train.start_queue_runners()来启动线程
"""
if not num_epochs: num_epochs = None
# 获取文件路径，即/tmp/data/train.tfrecords, /tmp/data/validation.records
filename = os.path.join(FLAGS.train_dir,
TRAIN_FILE if train else VALIDATION_FILE)

with tf.name_scope('input'):
# tf.train.string_input_producer返回一个QueueRunner，里面有一个FIFOQueue
filename_queue = tf.train.string_input_producer(
[filename], num_epochs=num_epochs) # 如果样本量很大，可以分成若干文件，把文件名列表传入

image, label = read_and_decode(filename_queue)

# 随机化example，并把它们规整成batch_size大小
# tf.train.shuffle_batch生成了RandomShuffleQueue，并开启两个线程
images, sparse_labels = tf.train.shuffle_batch(
[image, label], batch_size=batch_size, num_threads=2,
capacity=1000 + 3 * batch_size,
min_after_dequeue=1000) # 留下一部分队列，来保证每次有足够的数据做随机打乱

return images, sparse_labels

最后，我们把生成的batch张量作为网络的输入，进行训练：

def run_training():
with tf.Graph().as_default():
# 输入images和labels
images, labels = inputs(train=True, batch_size=FLAGS.batch_size,
num_epochs=FLAGS.num_epochs)

# 构建一个从推理模型来预测数据的图
logits = mnist.inference(images,
FLAGS.hidden1,
FLAGS.hidden2)

loss = mnist.loss(logits, labels)  # 定义损失函数

# Add to the Graph operations that train the model.
train_op = mnist.training(loss, FLAGS.learning_rate)

# 初始化参数，特别注意：string_input_producer内部创建了一个epoch计数变量，
# 归入tf.GraphKeys.LOCAL_VARIABLES集合中，必须单独用initialize_local_variables()初始化
init_op = tf.group(tf.global_variables_initializer(),
tf.local_variables_initializer())

sess = tf.Session()

sess.run(init_op)

# Start input enqueue threads.
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)

try:
step = 0
while not coord.should_stop(): # 进入永久循环
start_time = time.time()
_, loss_value = sess.run([train_op, loss])
duration = time.time() - start_time

# 每100次训练输出一次结果
if step % 100 == 0:
print('Step %d: loss = %.2f (%.3f sec)' % (step, loss_value, duration))
step += 1
except tf.errors.OutOfRangeError:
print('Done training for %d epochs, %d steps.' % (FLAGS.num_epochs, step))
finally:
coord.request_stop() # 通知其他线程关闭

coord.join(threads)
sess.close()

输出结果如下：

Step 0: loss = 2.29 (0.193 sec)
Step 100: loss = 2.05 (0.030 sec)
Step 200: loss = 1.63 (0.022 sec)
Step 300: loss = 1.38 (0.025 sec)
Step 400: loss = 0.94 (0.027 sec)
Step 500: loss = 0.86 (0.027 sec)
Step 600: loss = 0.68 (0.024 sec)
Step 700: loss = 0.67 (0.028 sec)
Step 800: loss = 0.62 (0.026 sec)
Step 900: loss = 0.56 (0.028 sec)
Step 1000: loss = 0.49 (0.018 sec)
Done training for 2 epochs, 1100 steps.

数据集大小为5 5000，2轮训练，共110 000个数据，batch_size大小为100，故训练次数为1 100次，每100次训练输出一次结果，共输出11次结果。

如上所述，我们总结出TensorFlow使用TFRecords文件训练样本的步骤：

（1）在生成文件名队列中，设定epoch数量；

（2）训练时，设定为无穷循环；

（3）在读取数据时，如果捕捉到错误，终止。

4.11　实现一个自定义操作

尽管TensorFlow自己提供了足够多的操作，初学者甚至中高级的读者都可以直接用手册中的API来实现自己的业务需求。想创建一个不包含在现有TensorFlow库中的操作，可以先试试用现有的Python操作的组合能不能实现，如果现有操作的组合不能实现，或者能够实现但是效率不高，再或者因为发现在XLA框架中难以自己融合（对XLA的讲解参见第16章），想手工融合几个操作，如何实现呢？

本节内容较难，需要熟练掌握C++语言，并且对张量的流动和前向传播和反向传播有很深的理解。建议初学者跳过这部分内容，看完全书之后再看本节。

4.11.1　步骤

要自定义一个操作，最简单的是需要以下3步。

（1）在C++文件（*_ops.cc文件）中注册新的操作。这里定义了操作功能的接口规范，如操作的名称、输入和输出以及属性等。

（2）在C++文件（*_kernels.cc文件）中实现这个操作。也就是，对上一步中操作注册规范的具体实现，可以实现在如CPU、GPU等多个内核上。

（3）测试操作。编译出该操作的库文件（*_ops.so文件），然后在Python中使用这个操作。

因此，要创建一个新的操作，需要掌握一些C++语言的知识。

4.11.2　最佳实践

下面以词嵌入的例子来说明，源代码参见https://github.com/tensorflow/models/blob/master/ tutorials/embedding/word2vec_optimized.py。在3.3.2节中，我们以嵌入投影仪的可视化讲过这个Word2vec的可视化例子。

第一步，我们创建word2vec_ops.cc来注册两个操作，即SkipgramWord2vec和NegTrainWord2vec，代码如下：

#include "tensorflow/core/framework/op.h"

namespace tensorflow {

REGISTER_OP("SkipgramWord2vec")
.Output("vocab_word: string")
.Output("vocab_freq: int32")
.Output("words_per_epoch: int64")
.Output("current_epoch: int32")
.Output("total_words_processed: int64")
.Output("examples: int32")
.Output("labels: int32")
.SetIsStateful()
.Attr("filename: string")
.Attr("batch_size: int")
.Attr("window_size: int = 5")
.Attr("min_count: int = 5")
.Attr("subsample: float = 1e-3")
.Doc(R"doc(
Parses a text file and creates a batch of examples.

vocab_word: A vector of words in the corpus.
vocab_freq: Frequencies of words．Sorted in the non-ascending order.
words_per_epoch: Number of words per epoch in the data file.
current_epoch: The current epoch number.
total_words_processed: The total number of words processed so far.
examples: A vector of word ids.
labels: A vector of word ids.
filename: The corpus's text file name.
batch_size: The size of produced batch.
window_size: The number of words to predict to the left and right of the target.
min_count: The minimum number of word occurrences for it to be included in the
vocabulary.
subsample: Threshold for word occurrence．Words that appear with higher
frequency will be randomly down-sampled．Set to 0 to disable.
)doc");

REGISTER_OP("NegTrainWord2vec")
.Input("w_in: Ref(float)")
.Input("w_out: Ref(float)")
.Input("examples: int32")
.Input("labels: int32")
.Input("lr: float")
.SetIsStateful()
.Attr("vocab_count: list(int)")
.Attr("num_negative_samples: int")
.Doc(R"doc(
Training via negative sampling.

w_in: input word embedding.
w_out: output word embedding.
examples: A vector of word ids.
labels: A vector of word ids.
vocab_count: Count of words in the vocabulary.
num_negative_samples: Number of negative samples per example.
)doc");

}

第二步，我们将这两个操作在CPU设备上进行实现，生成word2vec_kernels.cc文件，代码如下：

#include "tensorflow/core/framework/op.h"
#include "tensorflow/core/framework/op_kernel.h"
#include "tensorflow/core/lib/core/stringpiece.h"
#include "tensorflow/core/lib/gtl/map_util.h"
#include "tensorflow/core/lib/random/distribution_sampler.h"
#include "tensorflow/core/lib/random/philox_random.h"
#include "tensorflow/core/lib/random/simple_philox.h"
#include "tensorflow/core/lib/strings/str_util.h"
#include "tensorflow/core/platform/thread_annotations.h"
#include "tensorflow/core/util/guarded_philox_random.h"

namespace tensorflow {

// 预先计算的示例数
const int kPrecalc = 3000;
// 处理前读入句子的字数
const int kSentenceSize = 1000;

namespace {

bool ScanWord(StringPiece* input, string* word) {
str_util::RemoveLeadingWhitespace(input);
StringPiece tmp;
if (str_util::ConsumeNonWhitespace(input, &tmp)) {
word->assign(tmp.data(), tmp.size());
return true;
} else {
return false;
}
}

}

class SkipgramWord2vecOp : public OpKernel {
public:
explicit SkipgramWord2vecOp(OpKernelConstruction* ctx)
: OpKernel(ctx), rng_(&philox_) {
string filename;
OP_REQUIRES_OK(ctx, ctx->GetAttr("filename", &filename));
OP_REQUIRES_OK(ctx, ctx->GetAttr("batch_size", &batch_size_));
OP_REQUIRES_OK(ctx, ctx->GetAttr("window_size", &window_size_));
OP_REQUIRES_OK(ctx, ctx->GetAttr("min_count", &min_count_));
OP_REQUIRES_OK(ctx, ctx->GetAttr("subsample", &subsample_));
OP_REQUIRES_OK(ctx, Init(ctx->env(), filename));

mutex_lock l(mu_);
example_pos_ = corpus_size_;
label_pos_ = corpus_size_;
label_limit_ = corpus_size_;
sentence_index_ = kSentenceSize;
for (int i = 0; i < kPrecalc; ++i) {
NextExample(&precalc_examples_[i].input, &precalc_examples_[i].label);
}
}

void Compute(OpKernelContext* ctx) override {
Tensor words_per_epoch(DT_INT64, TensorShape({}));
Tensor current_epoch(DT_INT32, TensorShape({}));
Tensor total_words_processed(DT_INT64, TensorShape({}));
Tensor examples(DT_INT32, TensorShape({batch_size_}));
auto Texamples = examples.flat<int32>();
Tensor labels(DT_INT32, TensorShape({batch_size_}));
auto Tlabels = labels.flat<int32>();
{
mutex_lock l(mu_);
for (int i = 0; i < batch_size_; ++i) {
Texamples(i) = precalc_examples_[precalc_index_].input;
Tlabels(i) = precalc_examples_[precalc_index_].label;
precalc_index_++;
if (precalc_index_ >= kPrecalc) {
precalc_index_ = 0;
for (int j = 0; j < kPrecalc; ++j) {
NextExample(&precalc_examples_[j].input,
&precalc_examples_[j].label);
}
}
}
words_per_epoch.scalar<int64>()() = corpus_size_;
current_epoch.scalar<int32>()() = current_epoch_;
total_words_processed.scalar<int64>()() = total_words_processed_;
}
ctx->set_output(0, word_);
ctx->set_output(1, freq_);
ctx->set_output(2, words_per_epoch);
ctx->set_output(3, current_epoch);
ctx->set_output(4, total_words_processed);
ctx->set_output(5, examples);
ctx->set_output(6, labels);
}

private:
struct Example {
int32 input;
int32 label;
};

int32 batch_size_ = 0;
int32 window_size_ = 5;
float subsample_ = 1e-3;
int min_count_ = 5;
int32 vocab_size_ = 0;
Tensor word_;
Tensor freq_;
int64 corpus_size_ = 0;
std::vector<int32> corpus_;
std::vector<Example> precalc_examples_;
int precalc_index_ = 0;
std::vector<int32> sentence_;
int sentence_index_ = 0;

mutex mu_;
random::PhiloxRandom philox_ GUARDED_BY(mu_);
random::SimplePhilox rng_ GUARDED_BY(mu_);
int32 current_epoch_ GUARDED_BY(mu_) = -1;
int64 total_words_processed_ GUARDED_BY(mu_) = 0;
int32 example_pos_ GUARDED_BY(mu_);
int32 label_pos_ GUARDED_BY(mu_);
int32 label_limit_ GUARDED_BY(mu_);

//{example_pos_，label_pos_}是下一个示例的光标。example_pos_在corpus_结尾处换行
// 对每个例子，我们为标记随机生成[label_pos_，label_limit]
void NextExample(int32* example, int32* label) EXCLUSIVE_LOCKS_REQUIRED(mu_) {
while (true) {
if (label_pos_ >= label_limit_) {
++total_words_processed_;
++sentence_index_;
if (sentence_index_ >= kSentenceSize) {
sentence_index_ = 0;
for (int i = 0; i < kSentenceSize; ++i, ++example_pos_) {
if (example_pos_ >= corpus_size_) {
++current_epoch_;
example_pos_ = 0;
}
if (subsample_ > 0) {
int32 word_freq = freq_.flat<int32>()(corpus_[example_pos_]);
// See Eq．5 in http://arxiv.org/abs/1310.4546
float keep_prob =
(std::sqrt(word_freq / (subsample_ * corpus_size_)) + 1) *
(subsample_ * corpus_size_) / word_freq;
if (rng_.RandFloat() > keep_prob) {
i--;
continue;
}
}
sentence_[i] = corpus_[example_pos_];
}
}
const int32 skip = 1 + rng_.Uniform(window_size_);
label_pos_ = std::max<int32>(0, sentence_index_ - skip);
label_limit_ =
std::min<int32>(kSentenceSize, sentence_index_ + skip + 1);
}
if (sentence_index_ != label_pos_) {
break;
}
++label_pos_;
}
*example = sentence_[sentence_index_];
*label = sentence_[label_pos_++];
}

Status Init(Env* env, const string& filename) {
string data;
TF_RETURN_IF_ERROR(ReadFileToString(env, filename, &data));
StringPiece input = data;
string w;
corpus_size_ = 0;
std::unordered_map<string, int32> word_freq;
while (ScanWord(&input, &w)) {
++(word_freq[w]);
++corpus_size_;
}
if (corpus_size_ < window_size_ * 10) {
return errors::InvalidArgument("The text file ", filename,
" contains too little data: ",
corpus_size_, " words");
}
typedef std::pair<string, int32> WordFreq;
std::vector<WordFreq> ordered;
for (const auto& p : word_freq) {
if (p.second >= min_count_) ordered.push_back(p);
}
LOG(INFO) << "Data file: " << filename << " contains " << data.size()
<< " bytes, " << corpus_size_ << " words, " << word_freq.size()
<< " unique words, " << ordered.size()
<< " unique frequent words.";
word_freq.clear();
std::sort(ordered.begin(), ordered.end(),
[](const WordFreq& x, const WordFreq& y) {
return x.second > y.second;
});
vocab_size_ = static_cast<int32>(1 + ordered.size());
Tensor word(DT_STRING, TensorShape({vocab_size_}));
Tensor freq(DT_INT32, TensorShape({vocab_size_}));
word.flat<string>()(0) = "UNK";
static const int32 kUnkId = 0;
std::unordered_map<string, int32> word_id;
int64 total_counted = 0;
for (std::size_t i = 0; i < ordered.size(); ++i) {
const auto& w = ordered[i].first;
auto id = i + 1;
word.flat<string>()(id) = w;
auto word_count = ordered[i].second;
freq.flat<int32>()(id) = word_count;
total_counted += word_count;
word_id[w] = id;
}
freq.flat<int32>()(kUnkId) = corpus_size_ - total_counted;
word_ = word;
freq_ = freq;
corpus_.reserve(corpus_size_);
input = data;
while (ScanWord(&input, &w)) {
corpus_.push_back(gtl::FindWithDefault(word_id, w, kUnkId));
}
precalc_examples_.resize(kPrecalc);
sentence_.resize(kSentenceSize);
return Status::OK();
}
};

REGISTER_KERNEL_BUILDER(Name("SkipgramWord2vec").Device(DEVICE_CPU),
SkipgramWord2vecOp);

class NegTrainWord2vecOp : public OpKernel {
public:
explicit NegTrainWord2vecOp(OpKernelConstruction* ctx) : OpKernel(ctx) {
base_.Init(0, 0);

OP_REQUIRES_OK(ctx, ctx->GetAttr("num_negative_samples", &num_samples_));

std::vector<int32> vocab_count;
OP_REQUIRES_OK(ctx, ctx->GetAttr("vocab_count", &vocab_count));

std::vector<float> vocab_weights;
vocab_weights.reserve(vocab_count.size());
for (const auto& f : vocab_count) {
float r = std::pow(static_cast<float>(f), 0.75f);
vocab_weights.push_back(r);
}
sampler_ = new random::DistributionSampler(vocab_weights);
}

~NegTrainWord2vecOp() { delete sampler_; }

void Compute(OpKernelContext* ctx) override {
Tensor w_in = ctx->mutable_input(0, false);
OP_REQUIRES(ctx, TensorShapeUtils::IsMatrix(w_in.shape()),
errors::InvalidArgument("Must be a matrix"));
Tensor w_out = ctx->mutable_input(1, false);
OP_REQUIRES(ctx, w_in.shape() == w_out.shape(),
errors::InvalidArgument("w_in.shape == w_out.shape"));
const Tensor& examples = ctx->input(2);
OP_REQUIRES(ctx, TensorShapeUtils::IsVector(examples.shape()),
errors::InvalidArgument("Must be a vector"));
const Tensor& labels = ctx->input(3);
OP_REQUIRES(ctx, examples.shape() == labels.shape(),
errors::InvalidArgument("examples.shape == labels.shape"));
const Tensor& learning_rate = ctx->input(4);
OP_REQUIRES(ctx, TensorShapeUtils::IsScalar(learning_rate.shape()),
errors::InvalidArgument("Must be a scalar"));

auto Tw_in = w_in.matrix<float>();
auto Tw_out = w_out.matrix<float>();
auto Texamples = examples.flat<int32>();
auto Tlabels = labels.flat<int32>();
auto lr = learning_rate.scalar<float>()();
const int64 vocab_size = w_in.dim_size(0);
const int64 dims = w_in.dim_size(1);
const int64 batch_size = examples.dim_size(0);
OP_REQUIRES(ctx, vocab_size == sampler_->num(),
errors::InvalidArgument("vocab_size mismatches: ", vocab_size,
" vs．", sampler_->num()));

// v_in的梯度累加器
Tensor buf(DT_FLOAT, TensorShape({dims}));
auto Tbuf = buf.flat<float>();

Tensor g_buf(DT_FLOAT, TensorShape({}));
auto g = g_buf.scalar<float>();

// 在下面的循环中，每个负样本需要两个随机32位值
// 我们为每个样本保留8个值，防止基础实现发生变化
auto rnd = base_.ReserveSamples32(batch_size * num_samples_ * 8);
random::SimplePhilox srnd(&rnd);

for (int64 i = 0; i < batch_size; ++i) {
const int32 example = Texamples(i);
DCHECK(0 <= example && example < vocab_size) << example;
const int32 label = Tlabels(i);
DCHECK(0 <= label && label < vocab_size) << label;
auto v_in = Tw_in.chip<0>(example);

// 正样本: 预测标记
//   前向传播: x = v_in' * v_out
//          l = log(sigmoid(x))
//   反向传播: dl/dx = g = sigmoid(-x)
//          dl/d(v_in) = g * v_out'
//          dl/d(v_out) = v_in' * g
{
auto v_out = Tw_out.chip<0>(label);
auto dot = (v_in * v_out).sum();
g = (dot.exp() + 1.f).inverse();
Tbuf = v_out * (g() * lr);
v_out += v_in * (g() * lr);
}

// 负样本:
//   前向传播: x = v_in' * v_sample
//          l = log(sigmoid(-x))
//   反向传播: dl/dx = g = -sigmoid(x)
//          dl/d(v_in) = g * v_out'
//          dl/d(v_out) = v_in' * g
for (int j = 0; j < num_samples_; ++j) {
const int sample = sampler_->Sample(&srnd);
if (sample == label) continue;  // Skip.
auto v_sample = Tw_out.chip<0>(sample);
auto dot = (v_in * v_sample).sum();
g = -((-dot).exp() + 1.f).inverse();
Tbuf += v_sample * (g() * lr);
v_sample += v_in * (g() * lr);
}

v_in += Tbuf;
}
}

private:
int32 num_samples_ = 0;
random::DistributionSampler* sampler_ = nullptr;
GuardedPhiloxRandom base_;
};

REGISTER_KERNEL_BUILDER(Name("NegTrainWord2vec").Device(DEVICE_CPU), NegTrainWord2vecOp);

}

第三步，编译出该操作的类文件，并做测试。

我们需要在特定的头文件目录下编译，使用Python提供的get_include获取头文件目录，然后使用C++编译器（如g++）将操作编译成动态库，如下：

TF_INC=$(python -c 'import tensorflow as tf; print(tf.sysconfig.get_include())')
g++ -std=c++11 -shared word2vec_ops.cc word2vec_kernels.cc -o word2vec_ops.so -fPIC -I
$TF_INC -O2 -D_GLIBCXX_USE_CXX11_ABI=0

TensorFlow的Python API提供了tf.load_op_library函数来加载动态库，并向TensorFlow框架注册操作。load_op_library返回一个包含操作和内核的Python模块。于是，我们测试如下：

import tensorflow as tf
word2vec = tf.load_op_library('word2vec_ops.so')
with tf.Session(''):
word2vec.skipgram_word2vec(filename='text8', batch_size=500, window_size=5,
min_count=5, subsample=0.001)

成功输出了：

SkipgramWord2vec(vocab_word=<tf.Tensor 'SkipgramWord2vec:0' shape=<unknown> dtype=
string>, vocab_freq=<tf.Tensor 'SkipgramWord2vec:1' shape=<unknown> dtype=int32>,
words_per_epoch=<tf.Tensor 'SkipgramWord2vec:2' shape=<unknown> dtype=int64>, current_
epoch=<tf.Tensor 'SkipgramWord2vec:3' shape=<unknown> dtype=int32>, total_words_
processed=<tf.Tensor 'SkipgramWord2vec:4' shape=<unknown> dtype=int64>, examples=
<tf.Tensor 'SkipgramWord2vec:5' shape=<unknown> dtype=int32>, labels=<tf.Tensor
'SkipgramWord2vec:6' shape=<unknown> dtype=int32>)

说明我们注册的自定义操作成功了。

4.12　小结

本章主要讲解了TensorFlow的基础知识，包括系统架构、设计理念、基本概念，以及常用的API、神经元函数和神经网络，还介绍了存储与加载模型的方法以及线程和队列的知识（这些内容很有利于在第14章对分布式的理解），最后介绍了自定义操作的方法。本章是全书中最重要的。

[1]　https://www.tensorflow.org/get_started/get_started

[2]　https://www.tensorflow.org/extend/architecture

[3]　本节内容参考https://www.tensorflow.org/extend/architecture。

[4]　这一段参考http://www.xue163.com/5800/10381/58008965.html。

[5]　本节内容参考TensorFlow白皮书《TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systems》：http://download.tensorflow.org/paper/whitepaper2015.pdf。

[6]　本图参考https://www.tensorflow.org/images/tensors_flowing.gif 。

[7]　源自图论里的DAG、数据依赖、控制依赖的相关描述。

[8]　在数理统计中，残差是指实际观察值与训练的估计值之间的差。

[9]　量化是数字信号处理领域的一个概念，是指将信号的连续取值（或者大量可能的离散取值）近似为有限多个（或较少的）离散值的过程（参考百度百科 “量化”）。

[10]　本表的内容参考TensorFlow白皮书《TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systems》：http://download.tensorflow.org/paper/whitepaper2015.pdf。

[11]　基于Java和Go语言的API还在完善，基于Java语言的API参见https://www.tensorflow.org/api_docs/java/reference/org/ tensorflow/package-summary。基于Go语言的API参见https://godoc.org/github.com/tensorflow/tensorflow/tensorflow/go。

[12]　规范化，这里也可以称为标准化，是将数据按比例缩放，使之落入一个小的特定区间。这里是指将数据减去平均值，再除以标准差。

[13]　梯度爆炸与梯度消失相反，如果是梯度非常大，链式求导后乘积就变得很大，使权重变得非常大，产生指数级爆炸。

[14]　https://arxiv.org/abs/1502.03167

[15]　软饱和是指激活函数h(x)在取值趋于无穷大时，它的一阶导数趋于0。硬饱和是指当|x| > c时，其中c为常数，f '(x)=0。relu就是一类左侧硬饱和激活函数。

[16]　梯度消失是指在更新模型参数时采用链式求导法则反向求导，越往前梯度越小。最终的结果是到达一定深度后梯度对模型的更新就没有任何贡献了。

[17]　dropout在论文中最早被提出时是这么做的：在训练的时候用概率p丢弃，然后在预测的时候，所有参数按比例缩小，也就是乘以p。在各种深度学习框架（如Keras、TensorFlow）的实现中，都是用反向ropout来代替dropout。也就是这里所说的，在训练的时候一边dropout，然后再按比例放大，也就是乘以1/p，然后在预测的时候，不做任何处理。

[18]　源代码位于tensorflow-1.1.0/tensorflow/python/ops/nn_ops.py。

[19]　本图出自Geoffrey Hinton的Coursera公开课“Neural Networks for Machine Learning”第6章：https://www.coursera.org/ learn/neural-networks。

[20]　矩估计就是利用样本矩来估计总体中相应的参数。如果一个随机变量X服从某种分布，X的一阶矩是E(X)，也就是样本平均值，X的二阶矩是E(X2)，也就是样本平方的平均值。

[21]　这一结论和图4-7至图4-9参考http://sebastianruder.com/optimizing-gradient-descent/。

[22]　http://sebastianruder.com/optimizing-gradient-descent/

[23]　本节代码参考https://github.com/nlintz/TensorFlow-Tutorials/blob/master/10_save_restore_net.py。为了讲解方便，这里对代码顺序略微做了调整。

[24]　https://www.tensorflow.org/versions/r0.10/how_tos/reading_data/#preloaded_data

第5章　TensorFlow源代码解析

在了解了TensorFlow的基本原理、编程模型和常用API后，我们一起梳理一下TensorFlow的源代码，以便更深入地理解TensorFlow的设计，为今后学习各种模型示例做准备。

源代码解析往往是学习一门新技术时，能够整体理解其框架的重要途径，相信本章会是很多程序员最喜爱的一章。

5.1　TensorFlow的目录结构

我们仍然以TensorFlow 1.1.0版本为例，看看TensorFlow的代码结构。

进入tensorflow-1.1.0目录，代码结构如下：

├── ACKNOWLEDGMENTS # TensorFlow版权声明
├── ADOPTERS.md # 使用 TensorFlow的人员或组织列表
├── AUTHORS # TensorFlow作者的官方列表
├── BUILD
├── CONTRIBUTING.md # TensorFlow贡献指导
├── ISSUE_TEMPLATE.md # 提ISSUE的模板
├── LICENSE # 版权许可
├── README.md
├── RELEASE.md # 每次发版的change log
├── WORKSPACE # 配置移动端开发环境
├── bower.BUILD
├── configure
├── models.BUILD
├── tensorflow # 主目录，后面分析的重点
├── third_party # 第三方库，包括eigen3（特征运算的库，包括SVD、LU分解等）、gpus（支持cuda）、hadoop、jpeg、llvm、py、sycl
├── tools # 构建cuda支持
└── util

其中，最重要的源代码保存在tensorflow目录中。tensorflow目录的结构如下：

├── BUILD
├── __init__.py
├── c
├── cc # 采用C++进行训练的样例
├── compiler
├── contrib # 将常用功能封装在一起的高级API
├── core # C++实现的主要目录
├── examples # 各种示例，本书后续讲的例子主要就在这个目录中
├── g3doc # 针对C++、Python版本的代码文档
├── go
├── java
├── opensource_only # 声明目录
├── python # Python实现的主要目录
├── stream_executor # 流处理
├── tensorboard # App、Web支持，以及脚本支持
├── tensorflow.bzl
├── tf_exported_symbols.lds
├── tf_version_script.lds
├── tools # 一些工具杂项
├── user_ops
└── workspace.bzl

下面我们就简单介绍几个重点目录。

5.1.1　contirb

contrib目录中保存的是将常用的功能封装成的高级API。但是这个目录并不是官方支持的，很有可能在高级API完善后被官方迁移到核心的TensorFlow目录中或去掉，现在有一部分包（package）在https://github.com/tensorflow/models 有了更完整的体现。这里重点介绍几个常用包。

- framework：很多函数（如get_variables、get_global_step）都在这里定义，还有一些废弃或者不推荐（deprecated）的函数。

- layers：这个包主要有initializers.py、layers.py、optimizers.py、regularizers.py、summaries.py等文件。initializers.py中主要是做变量初始化的函数。layers.py中有关于层操作和权重偏置变量的函数。optimizers.py中包含损失函数和global_step 张量的优化器操作。regularizers.py中包含带有权重的正则化函数。summaries.py中包含将摘要操作（见 4.4.2节可视化API）添加到tf.GraphKeys.SUMMARIES集合中的函数。

- learn：这个包是使用TensorFlow进行深度学习的高级API，包括完成训练模型和评估模型、读取批处理数据和队列功能的API封装。

- rnn：这个包提供了额外的RNN Cell，也就是对RNN隐藏层的各种改进，如LSTMBlockCell、GRUBlockCell、FusedRNNCell、GridLSTMCell、AttentionCellWrapper等。

- seq2seq：这个包提供了建立神经网络seq2seq层和损失函数的操作。

- slim：TensorFlow-Slim （TF-Slim）是一个用于定义、训练和评估TensorFlow中的复杂模型的轻量级库。在使用中可以将TF-Slim与TensorFlow的原生函数和tf.contrib中的其他包进行自由组合。TF-Slim现在已经被逐渐迁移到TensorFlow开源的Models[1]中，这里包含了几种广泛使用的卷积神经网络图像分类模型的代码，可以从头训练模型或者预训练模型开始微调。TF-Slim非常有用，在12.1节中会用到。

5.1.2　core

这个目录中保存的都是C语言的文件，是TensorFlow的原始实现。

├── BUILD
├── common_runtime # 公共运行库
├── debug
├── distributed_runtime # 分布式执行模块，含有grpc session、grpc worker、 grpc master等
├── example
├── framework # 基础功能模块
├── graph
├── kernels # 一些核心操作在CPU、CUDA内核上的实现
├── lib # 公共基础库
├── ops
├── platform # 操作系统实现相关文件
├── protobuf # .proto文件，用于传输时的结构序列化
├── public # API的头文件目录
├── user_ops
└── util

Protocol Buffers是谷歌公司创建的一个数据序列化[2]（serialization）工具，可以用于结构化数据序列化，很适合作为数据存储或者RPC数据交换的格式。定义完协议缓冲区后，将生成.pb.h和.pb.cc文件，其中定义了相应的get、set以及序列化和反序列化函数。TensorFlow的几个核心proto文件graph_def.proto、node_def.proto、op_def.proto都保存在framework目录中。构图时先构建graph_def，存储下来，然后在实际计算时再转成如图、节点、操作等的内存对象。

下面以tensorflow-1.1.0/tensorflow/core/framework/node_def.proto为例来说明定义proto文件的过程。node_def.proto定义中指定了设备（device）操作（op）以及操作的属性（attr）。代码如下：

syntax = "proto3";

package tensorflow;
option cc_enable_arenas = true;
option java_outer_classname = "NodeProto";
option java_multiple_files = true;
option java_package = "org.tensorflow.framework";

import "tensorflow/core/framework/attr_value.proto";

message NodeDef {
string name = 1; # 操作的名称
string op = 2; # 操作的名称
repeated string input = 3; # 每个input 指明了当前节点来自哪个节点的第几个张量，
# 格式是node:index
string device = 4; # 指定device方法
map<string, AttrValue> attr = 5; # 其中AttrValue又是另外一个协议缓冲区
};

framework目录中还有node_def_builder.h、node_def_builder.cc、node_def_util.h、node_def_util_test.cc等文件，这都是为了在C++里能操作上面代码中定义的node_def.proto的protobuf 结构。

5.1.3　examples

examples目录中给出了深度学习的一些例子，包括MNIST、Word2vec、Deepdream、Iris、HDF5的一些例子，对入门非常有帮助。此外，这个目录中还有TensorFlow在Android系统上的移动端实现，以及一些扩展为.ipynb的文档教程，可以用jupyter打开（使用方式参见2.4.3节）。

5.1.4　g3doc

TensorFlow的文档是用Markdown在维护的，并存放在g3doc中。g3doc目录可以认为是TensorFlow的离线手册，非常好用。

g3doc/api_docs目录中的任何内容都是从代码中的注释生成的，不应该直接编辑。脚本tools/docs/gen_docs.sh是用来生成API文档的。如果无参数调用，它只重新生成Python API文档（即操作的文档，包括用Python和C ++定义的）。如果传递了-a，运行脚本时还会重新生成C++ API的文档。这个脚本必须从tools/docs目录调用，如果使用参数-a，需要安装doxygen[3]。

5.1.5　python

第4章中介绍的很多函数的实现都是在python这个目录中。例如，4.7节中的激活函数、卷积函数、池化函数、损失函数、优化方法等。

5.1.6　tensorboard

tensorboard目录中是实现TensorFlow图表可视化工具的代码，代码是基于Tornado[4]来实现网页端可视化的。

5.2　TensorFlow源代码的学习方法

如何高效地学习TensorFlow源代码呢？很多人在接触TensorFlow后会首先问这个问题。下面我就介绍一下TensorFlow源代码的学习步骤。

（1）了解自己要研究的基本领域，如图像分类、物体检测、语音识别等，了解对应这个领域所用的技术，如卷积神经网络（convolutional neural network，CNN）和循环神经网络（recurrent neural network，RNN），知道实现的基本原理。

（2）尝试运行GitHub上对应的基本模型[5]，其目录结构如下：

├── AUTHORS
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── WORKSPACE
├── autoencoder
├── compression
├── differential_privacy
├── im2txt
├── inception
├── lm_1b
├── namignizer
├── neural_gpu
├── neural_programmer
├── next_frame_prediction
├── resnet
├── slim
├── street
├── swivel
├── syntaxnet
├── textsum
├── transformer
├── tutorials
└── video_prediction

如果研究领域是计算机视觉，可以看代码中的如下几个目录：compresssion（图像压缩）、im2txt（图像描述）、inception（对ImageNet数据集用Inception V3架构去训练和评估）、resnet（残差网络）、slim（图像分类）和street（路标识别或验证码识别）。

如果研究领域是自然语言处理，可以看lm_1b（语言模型）、namignizer（起名字）、swivel（使用Swivel算法转换词向量）、syntaxnet（分词和语法分析）、textsum（文本摘要）以及tutorials目录里的word2vec（词转换为向量）。

这些都是教科书式的代码，看懂学懂对今后自己实现模型大有裨益。尝试运行上述模型，并对模型进行调试和调参。当你完整阅读完MNIST或者CIFAR10整个项目的逻辑后，就会掌握TensorFlow项目架构。

这里，我着重说一下slim目录。

slim目录中的TF-Slim是图像分类的一个库，它包含用于定义、训练和评估复杂模型的轻量级高级API。可以用于训练和评估的几个广泛使用的CNN图像分类模型，如lenet、alexnet、vgg、inception_v1、inception_v2、inception_v3、inception_v4、resnet_v1、resnet_v2等，这些模型都位于slim/nets中，具体如下：

├── alexnet.py
├── alexnet_test.py
├── cifarnet.py
├── inception.py
├── inception_resnet_v2.py
├── inception_resnet_v2_test.py
├── inception_utils.py
├── inception_v1.py
├── inception_v1_test.py
├── inception_v2.py
├── inception_v2_test.py
├── inception_v3.py
├── inception_v3_test.py
├── inception_v4.py
├── inception_v4_test.py
├── lenet.py
├── nets_factory.py
├── nets_factory_test.py
├── overfeat.py
├── overfeat_test.py
├── resnet_utils.py
├── resnet_v1.py
├── resnet_v1_test.py
├── resnet_v2.py
├── resnet_v2_test.py
├── vgg.py
└── vgg_test.py

TF-Slim包含的脚本可以让人从头训练模型或从预先训练的网络开始训练模型并微调，这些脚本位于slim/scripts，具体如下：

├── finetune_inception_v1_on_flowers.sh
├── finetune_inception_v3_on_flowers.sh
├── train_cifarnet_on_cifar10.sh
└── train_lenet_on_mnist.sh

TF-Slim还包含用于下载标准图像数据集，将其转换为TensorFlow支持的TFRecords格式，这些脚本位于slim/datasets，具体如下：

├── cifar10.py
├── dataset_factory.py
├── dataset_utils.py
├── download_and_convert_cifar10.py
├── download_and_convert_flowers.py
├── download_and_convert_mnist.py
├── flowers.py
├── imagenet.py
└── mnist.py

随后可以轻松地在任何上述数据集上训练任何模型。

（3）结合要做的项目，找到相关的论文，自己用TensorFlow实现这篇论文的内容，这会让你有一个质的飞跃。

上面介绍的学习方法在本书“实战篇”中会再结合例子进行讲解。

5.3　小结

本章简略地解析了TensorFlow源代码，介绍了它的主要的目录结构，以及一些模块的位置和功能，还介绍了TensorFlow的源代码学习方法，建议读者可以自己多去研究。

[1]　https://github.com/tensorflow/models/tree/master/slim

[2]　序列化是指将对象的状态信息转换为可以存储或传输的形式的过程。

[3]　内容参考官方网站https://www.tensorflow.org/community/documentation。

[4]　http://www.tornadoweb.org/en/stable/

[5]　https://github.com/tensorflow/models

第6章　神经网络的发展及其TensorFlow实现

卷积神经网络（convolutional neural network，CNN）的演进从LeNet到AlexNet，再到VggNet、GoogLeNet，最后到ResNet，演进的方式有一定规律，并且也在ImageNet LSVRC竞赛上用120万张图片、1000类标记上取得了很好的成绩。循环神经网络（recurrent neural networks，RNN）的演进从Vanilla RNN到隐藏层结构精巧的GRU和LSTM，再到双向和多层的Deep Bidirectional RNN。本章主要介绍这些神经网络模型的结构和演化脉络，并且尝试用TensorFlow去构建这些网络，为读者将来自己设计网络或者根据读到的论文构建网络模型打下基础。

6.1　卷积神经网络

卷积神经网络（CNN），属于人工神经网络的一种，它的权值共享（weight sharing）的网络结构显著降低了模型的复杂度，减少了权值的数量，是目前语音分析和图像识别领域研究热点。

在传统的识别算法中，我们需要对输入的数据进行特征提取和数据重建，而卷积神经网络可以直接将图片作为网络的输入，自动提取特征，并且对图片的变形（如平移、比例缩放、倾斜）等具有高度不变形。

那什么是卷积（convolution）呢？卷积是泛函分析中的一种积分变换的数学方法，通过两个函数f和g生成第三个函数的一种数学算子，表征函数f与g经过翻转和平移的重叠部分的面积。①设f(x)和g(x)是R_1上的两个可积函数，做积分后的新函数就称为函数f与g的卷积：

我们知道，神经网络（neural networks，NN）的基本组成包括输入层、隐藏层、输出层。卷积神经网络的特点在于隐藏层分为卷积层和池化层（pooling layer，又叫下采样层）。卷积层通过一块块卷积核（conventional kernel）在原始图像上平移来提取特征，每一个特征就是一个特征映射；而池化层通过汇聚特征后稀疏参数来减少要学习的参数，降低网络的复杂度，池化层最常见的包括最大值池化（max pooling）和平均值池化（average pooling），如图 6-1所示。

图6-1[1]

卷积核在提取特征映射时的动作称为padding，其有两种方式，即SAME和VALID。由于移动步长（Stride）不一定能整除整张图的像素宽度，我们把不越过边缘取样称为Valid Padding，取样的面积小于输入图像的像素宽度；越过边缘取样称为Same Padding，取样的面积和输入图像的像素宽度一致。在图6-2中，左边为Valid Padding，右边为Same Padding。

图6-2[2]

关于卷积神经网络的推导和实现可参考相关论文[3]。

6.2　卷积神经网络发展

神经网络的发展过程如图6-3所示。本图参考了中国科学院计算技术研究所刘昕博士整理的卷积神经网络结构演化的历史。

图6-3

卷积神经网络发展的起点是神经认知机（neocognitron）模型，当时已经出现了卷积结构。第一个卷积神经网络模型LeCun诞生于1989年，其发明人是LeCun。学习卷积神经网络的读本是Lecun的论文[4]，在这篇论文里面较为详尽地解释了什么是卷积神经网络，并且阐述了为什么要卷积，为什么要降采样，径向基函数（radial basis function，RBF）怎么用，等等。

1998年LeCun提出了LeNet，但随后卷积神经网络的锋芒逐渐被SVM等手工设计的特征的分类器盖过。随着ReLU和Dropout的提出，以及GPU和大数据带来的历史机遇，卷积神经网络在2012年迎来了历史性突破——AlexNet。

如图6-3所示，AlexNet之后卷积神经网络的演化过程主要有4个方向的演化：一个是网络加深，二是增强卷积层的功能，三是从分类任务到检测任务，四是增加新的功能模块。

下面就简单讲述各个阶段的几个网络的结构及特点。

6.2.1　网络加深

1．LeNet

LeNet的论文详见http://vision.stanford.edu/cs598_spring07/papers/Lecun98.pdf。LeNet包含的组件如下。

- 输入层：32×32。

- 卷积层：3个。

- 降采样层：2个。

- 全连接层：1个。

- 输出层（高斯连接）：10个类别（数字0～9的概率）。

LeNet的网络结构如图6-4所示。

图6-4

下面就介绍一下各个层的用途及意义。

（1）输入层。输入图像尺寸为32×32。这要比MNIST数据集中的字母（28×28）还大，即对图像做了预处理reshape操作。这样做的目的是希望潜在的明显特征，如笔画断续、角点，能够出现在最高层特征监测卷积核的中心。

（2）卷积层（C1, C3, C5）。卷积运算的主要目的是使原信号特征增强，并且降低噪音。在一个可视化的在线演示示例[5]中，我们可以看出不同的卷积核输出特征映射的不同，如图 6-5所示。

（3）下采样层（S2, S4）。下采样层主要是想降低网络训练参数及模型的过拟合程度。通常有以下两种方式。

- 最大池化（max pooling）：在选中区域中找最大的值作为采样后的值。

- 平均值池化（mean pooling）：把选中的区域中的平均值作为采样后的值。

（4）全连接层（F6）。F6是全连接层，计算输入向量和权重向量的点积，再加上一个偏置。随后将其传递给sigmoid函数，产生单元i的一个状态。

（5）输出层。输出层由欧式径向基函数（Euclidean radial basis function）单元组成，每个类别（数字的0～9）对应一个径向基函数单元，每个单元有84个输入。也就是说，每个输出RBF单元计算输入向量和该类别标记向量之间的欧式距离。距离越远，RBF输出越大。

经过测试，采用LeNet，6万张原始图片的数据集，错误率能够降低到0.95%；54万张人工处理的失真数据集合并上6万张原始图片的数据集，错误率能够降低到0.8%。[6]

接着，历史转折发生在2012年，Geoffrey Hinton和他的学生Alex Krizhevsky在ImageNet竞赛中一举夺得图像分类的冠军，刷新了图像分类的记录，通过比赛回应了对卷积方法的质疑。比赛中他们所用网络称为AlexNet。

图6-5

2．AlexNet

AlexNet在2012年的ImageNet图像分类竞赛中，Top-5错误率为15.3%；2011年的冠军是采用基于传统浅层模型方法，Top-5错误率为25.8%。AlexNet也远远超过2012年竞赛的第二名，错误率为26.2%。AlexNet的论文详见Alex Krizhevsky、Ilya Sutskever和Geoffrey E．Hinton的《ImageNet Classification with Deep Convolutional Neural Networks》[7]。

AlexNet的结构如图6-6所示。图中明确显示了两个GPU之间的职责划分：一个GPU运行图中顶部的层次部分，另一个GPU运行图中底部的层次部分。GPU之间仅在某些层互相通信。

图6-6

AlexNet由5个卷积层、5个池化层、3个全连接层，大约5000万个可调参数组成。最后一个全连接层的输出被送到一个1000维的softmax层，产生一个覆盖1000类标记的分布。

AlexNet之所以能够成功，让深度学习卷积的方法重回到人们视野，原因在于使用了如下方法。

- 防止过拟合：Dropout、数据增强（data augmentation）。

- 非线性激活函数：ReLU。

- 大数据训练：120万（百万级）ImageNet图像数据。

- GPU实现、LRN（local responce normalization）规范化层的使用。

要学习如此多的参数，并且防止过拟合，可以采用两种方法：数据增强和Dropout。

（1）数据增强：增加训练数据是避免过拟合的好方法，并且能提升算法的准确率。当训练数据有限的时候，可以通过一些变换从已有的训练数据集中生成一些新数据，来扩大训练数据量。通常采用的变形方式以下几种，具体效果如图6-7所示。

- 水平翻转图像（又称反射变化，flip）。

- 从原始图像（大小为256×256）随机地平移变换（crop）出一些图像（如大小为224×224）。

- 给图像增加一些随机的光照（又称光照、彩色变换、颜色抖动）。

（2）Dropout。AlexNet做的是以0.5的概率将每个隐层神经元的输出设置为0。以这种方式被抑制的神经元既不参与前向传播，也不参与反向传播。因此，每次输入一个样本，就相当于该神经网络尝试了一个新结构，但是所有这些结构之间共享权重。因为神经元不能依赖于其他神经元而存在，所以这种技术降低了神经元复杂的互适应关系。因此，网络需要被迫学习更为健壮的特征，这些特征在结合其他神经元的一些不同随机子集时很有用。如果没有Dropout，我们的网络会表现出大量的过拟合。Dropout使收敛所需的迭代次数大致增加了一倍。

图6-7

Alex用非线性激活函数relu代替了sigmoid，发现得到的SGD的收敛速度会比sigmoid/tanh快很多。

单个GTX 580 GPU只有3 GB内存，因此在其上训练的数据规模有限。从AlexNet结构图可以看出，它将网络分布在两个GPU上，并且能够直接从另一个GPU的内存中读出和写入，不需要通过主机内存，极大地增加了训练的规模。

6.2.2　增强卷积层的功能

1．VGGNet

VGGNet可以看成是加深版本的AlexNet，参见Karen Simonyan和Andrew Zisserman 的论文《Very Deep Convolutional Networks for Large-Scale Visual Recognition》[8]。

VGGNet和下文中要提到的GoogLeNet是2014年ImageNet竞赛的第二名和第一名，Top-5错误率分别为7.32%和6.66%[9]。VGGNet也是5个卷积组、2层全连接图像特征、1层全连接分类特征，可以看作和AlexNet一样总共8个部分。根据前5个卷积组，VGGNet论文中给出了A～E这5种配置，如图6-8所示。卷积层数从8（A配置）到16（E配置）递增。VGGNet不同于AlexNet的地方是：VGGNet使用的层更多，通常有16～19层，而AlexNet只有8层。VGGNet的结构如图6-8所示。

从VGGNet的论文中可以看出，随着卷积层从8到16的一步步加深，通过加深卷积层数已经到达了准确率提升的瓶颈。从论文中给出的结果（如图6-9所示）可以看到，再加深模型，错误率已经很难再降低了。

图6-8

模型

ILSVRC-2012上Top-5分类任务错误率（%）

验证集

测试集

16层VGG

7.5%

7.4%

19层VGG

7.5%

7.3%

VGG和SVM结合

7.1%

7.0%

图6-9

2．GoogLeNet

提到GoogleNet，我们首先要说起NIN（Network in Network）的思想[10]（详见Min Lin和Qiang Chen和Shuicheng Yan的论文《Network In Network》），它对传统的卷积方法做了两点改进：将原来的线性卷积层（linear convolution layer）变为多层感知卷积层（multilayer perceptron）；将全连接层的改进为全局平均池化。这使得卷积神经网络向另一个演化分支——增强卷积模块的功能的方向演化，2014年诞生了GoogLeNet（即Inception V1）。谷歌公司提出的GoogLeNet是2014年ILSVRC挑战赛的冠军，它将Top-5的错误率降低到了6.67%。GoogLeNet的更多内容详见Christian Szegedy和Wei Liu等人的论文《Going Deeper with Convolutions》[11]。

GoogLeNet的网络的中段结构如图6-10所示。

图6-10

论文中介绍了如何发现Inception模型的最优结构，如图6-11所示。原始的设计见图6-11的左侧，使用1×1、3×3、5×5的卷积核对应图像的区域，然后连接起来到全连接层。降维后的设计如图6-11的右侧，使用1×1的卷积核进行降维，在全连接层将1×1、3×3、5×5的卷积结果连接起来。这样做使网络的宽度和深度均可扩大。使用了Inception模型的结构可以有2～3倍的加速。

图6-11

GoogLeNet的主要思想是围绕“深度”和“宽度”去实现的。

（1）深度。层数更深，论文中采用了22层。为了避免梯度消失问题，GoogLeNet巧妙地在不同深度处增加了两个损失函数来避免反向传播时梯度消失的现象。

（2）宽度。增加了多种大小的卷积核，如1×1、3×3、5×5，但并没有将这些全都用在特征映射上，都结合起来的特征映射厚度将会很大。但是采用了图6-11右侧所示的降维的Inception模型，在3×3、5×5卷积前，和最大池化后都分别加上了1×1的卷积核，起到了降低特征映射厚度的作用。

3．ResNet

把网络加深和增强卷积模块功能两个演化方向相结合，诞生了ResNet（Residual Network，残差网络）。ReNet是2015年ILSVRC竞赛中不依赖外部数据的物体检测和物体识别两个项目的冠军，是由MSRA何凯明团队提出的，训练深达152层的网络。同时，MSRA也是2015年ImageNet竞赛的大赢家，在分类、检测、定位以及COCO数据集上的检测（detection）和分隔（segmentation），都获得了冠军。图 6-12 展示的是一个 34 层的 ResNet 的结构。残差网络的更多内容详见Kaiming He、Xiangyu Zhang、Shaoqing Ren和Jian Sun的论文《Deep Residual Learning for Image Recognition》[12]。

按照一般的经验，只要没有发生梯度消失或者梯度爆炸，并且不过拟合，网络应该是越深越好。但是，论文作者在CIFAR10上训练网络时却发现，层数从20层增加到56层，错误率上升了，准确率下降了，如图6-13所示。

图6-12

图6-13

这种情况被称为网络退化（network degradation）。为此ResNet中引入了一个shortcut结构（指图6-12中卷积层之间的带箭头的曲线部分），将输入跳层传递与卷积的结果相加。

关于ResNet中残差（residual）的含义，其实是如果能用几层网络去逼近一个复杂的非线性映射H(x)来预测图片的分类，那么同样可以用这几层网络去逼近它的残差函数（residual function）F(x)= H(x)−x，并且我们认为优化残差映射要比直接优化H(x)简单。

6.2.3　从分类任务到检测任务

从分类任务到检测任务这个演化方向经历了从R-CNN到Fast R-CNN，再到Faster R-CNN的演化。

R-CNN可以看作是Region Proposal Networks[13]（RPN）和CNN结合的力作。在ImageNet、VOC、MSCOCO数据集上都曾经取得过很好的效果。但它的主要缺点是重复计算，因为最后建议的区域（region）有几千个，多数都是互相重叠的，重叠的部分会被多次重复提取特性。

Fast R-CNN是R-CNN的加速版本，将最后建议的区域映射到CNN的最后一个卷积层的特征映射上，这样一张图片只需要提取一次特征，大大提高了速度。但Fast R-CNN的速度瓶颈在RPN上。此外，Fast R-CNN支持多类物体的同时检测，其行人与车辆检测技术就是汽车高级辅助驾驶系统的关键技术之一。

Fater-R-CNN将RPN也交给CNN来做，于是速度更快，可以达到实时。详见Shaoqing Ren、Kaiming He、Ross Girshick和Jian Sun的论文《Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks》[14]。

关于检测任务如图片目标检测、视频目标检测（VID）的更多资料请读者自行查阅。

6.2.4　增加新的功能模块

增加新的功能模块这个演化方向主要涉及FCN（反卷积）、STNet、CNN与RNN/LSTM的混合架构。这部分涉及的知识较深，更多资料请读者自行查阅。

6.3　MNIST的AlexNet实现

接下来我们就试试用TensorFlow来构建一个网络模型（这里以AlexNet为例）。构建好模型后，使用MNIST数据来看看训练结果如何。

现在我就一步步介绍如何将一个好的开源模型（如AlexNet）在TensorFlow上实现。MNIST在TensorFlow的例子中是用CNN去训练的，而我们把原来普通的CNN更改成AlexNet。这里我主要是参考代码https://github.com/aymericdamien/TensorFlow-Examples/blob/master/examples/ 3_NeuralNetworks/convolutional_network.py和https://github.com/tensorflow/models/blob/master/ tutorials/image/alexnet/alexnet_benchmark.py，然后根据AlexNet的网络结构图来实现。

同时，一次完整的训练模型和评估模型的过程一般分为3个步骤：加载数据，定义网络模型，训练模型和评估模型。接下来就分成这3个部分讲解我们的代码。

6.3.1　加载数据

在加载数据的过程中，我们还要定义模型的超参数、模型所用的网络的参数以及数据的输入。如下：

import tensorflow as tf

# 输入数据
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)

# 定义网络的超参数
learning_rate = 0.001
training_iters = 200000
batch_size = 128
display_step = 10

# 定义网络的参数
n_input = 784  # 输入的维度(img shape: 28×28)
n_classes = 10 # 标记的维度 (0-9 digits)
dropout = 0.75 # Dropout的概率，输出的可能性

# 输入占位符
x = tf.placeholder(tf.float32, [None, n_input])
y = tf.placeholder(tf.float32, [None, n_classes])
keep_prob = tf.placeholder(tf.float32) #dropout

6.3.2　构建网络模型

接下来我们定义AlexNet需要用到的卷积、池化和规范化操作。为了简单，我们将这些功能封装起来。代码如下：

# 定义卷积操作
def conv2d(name, x, W, b, strides=1):
x = tf.nn.conv2d(x, W, strides=[1, strides, strides, 1], padding='SAME')
x = tf.nn.bias_add(x, b)
return tf.nn.relu(x, name=name) # 使用relu激活函数

# 定义池化层操作
def maxpool2d(name, x, k=2):
return tf.nn.max_pool(x, ksize=[1, k, k, 1], strides=[1, k, k, 1],
padding='SAME', name=name)

# 规范化操作
def norm(name, l_input, lsize=4):
return tf.nn.lrn(l_input, lsize, bias=1.0, alpha=0.001 / 9.0,
beta=0.75, name=name)

定义所有的网络参数（网络参数的具体值详见AlexNet的结构图，参见图6-6），如下：

# 定义所有的网络参数
weights = {
'wc1': tf.Variable(tf.random_normal([11, 11, 1, 96])),
'wc2': tf.Variable(tf.random_normal([5, 5, 96, 256])),
'wc3': tf.Variable(tf.random_normal([3, 3, 256, 384])),
'wc4': tf.Variable(tf.random_normal([3, 3, 384, 384])),
'wc5': tf.Variable(tf.random_normal([3, 3, 384, 256])),
'wd1': tf.Variable(tf.random_normal([4*4*256, 4096])),
'wd2': tf.Variable(tf.random_normal([4096, 4096])),
'out': tf.Variable(tf.random_normal([4096, 10]))
}
biases = {
'bc1': tf.Variable(tf.random_normal([96])),
'bc2': tf.Variable(tf.random_normal([256])),
'bc3': tf.Variable(tf.random_normal([384])),
'bc4': tf.Variable(tf.random_normal([384])),
'bc5': tf.Variable(tf.random_normal([256])),
'bd1': tf.Variable(tf.random_normal([4096])),
'bd2': tf.Variable(tf.random_normal([4096])),
'out': tf.Variable(tf.random_normal([n_classes]))
}

定义AlexNet的网络模型：

# 定义整个网络
def alex_net(x, weights, biases, dropout):
# Reshape input picture
x = tf.reshape(x, shape=[-1, 28, 28, 1])

# 第一层卷积
# 卷积
conv1 = conv2d('conv1', x, weights['wc1'], biases['bc1'])
# 下采样
pool1 = maxpool2d('pool1', conv1, k=2)
# 规范化
norm1 = norm('norm1', pool1, lsize=4)

# 第二层卷积
# 卷积
conv2 = conv2d('conv2', norm1, weights['wc2'], biases['bc2'])
# 最大池化（向下采样）
pool2 = maxpool2d('pool2', conv2, k=2)
# 规范化
norm2 = norm('norm2', pool2, lsize=4)

# 第三层卷积
# 卷积
conv3 = conv2d('conv3', norm2, weights['wc3'], biases['bc3'])
# 下采样
pool3 = maxpool2d('pool3', conv3, k=2)
# 规范化
norm3 = norm('norm3', pool3, lsize=4)

# 第四层卷积
conv4 = conv2d('conv4', norm3, weights['wc4'], biases['bc4'])
# 第五层卷积
conv5 = conv2d('conv5', conv4, weights['wc5'], biases['bc5'])
# 下采样
pool5 = maxpool2d('pool5', conv5, k=2)
# 规范化
norm5 = norm('norm5', pool5, lsize=4)

# 全连接层1
fc1 = tf.reshape(norm5, [-1, weights['wd1'].get_shape().as_list()[0]])
fc1 = tf.add(tf.matmul(fc1, weights['wd1']), biases['bd1'])
fc1 = tf.nn.relu(fc1)
# dropout
fc1 = tf.nn.dropout(fc1, dropout)

#全连接层2
fc2 = tf.reshape(fc1, [-1, weights['wd2'].get_shape().as_list()[0]])
fc2 = tf.add(tf.matmul(fc2, weights['wd2']), biases['bd2'])
fc2 = tf.nn.relu(fc2)
# dropout
fc2 = tf.nn.dropout(fc2, dropout)

# 输出层
out = tf.add(tf.matmul(fc2, weights['out']), biases['out'])
return out

构建模型，定义损失函数和优化器，并构建评估函数：

# 构建模型
pred = alex_net(x, weights, biases, keep_prob)

# 定义损失函数和优化器
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(pred, y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

# 评估函数
correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

6.3.3　训练模型和评估模型

训练模型和评估模型的代码如下：

# 初始化变量
init = tf.global_variables_initializer()

with tf.Session() as sess:
sess.run(init)
step = 1
# 开始训练，直到达到training_iters，即200000
while step * batch_size < training_iters:
batch_x, batch_y = mnist.train.next_batch(batch_size)
sess.run(optimizer, feed_dict={x: batch_x, y: batch_y,
keep_prob: dropout})
if step % display_step == 0:
# 计算损失值和准确度，输出
loss, acc = sess.run([cost, accuracy], feed_dict={x: batch_x,
y: batch_y,
keep_prob: 1.})
print("Iter " + str(step*batch_size) + ", Minibatch Loss= " + \
"{:.6f}".format(loss) + ", Training Accuracy= " + \
"{:.5f}".format(acc))
step += 1
print("Optimization Finished!")

# 计算测试集的准确度
print("Testing Accuracy:", \
sess.run(accuracy, feed_dict={x: mnist.test.images[:256],
y: mnist.test.labels[:256],
keep_prob: 1.}))

输出结果如下：

Iter 1280, Minibatch Loss= 397151.531250, Training Accuracy= 0.43750
Iter 2560, Minibatch Loss= 358224.781250, Training Accuracy= 0.48438
Iter 3840, Minibatch Loss= 208550.218750, Training Accuracy= 0.70312

我们也可以像实现AlexNet的模型这样，用TensorFlow实现其他网络（如VGGNet、GoogLeNet、ResNet），具体实现的步骤我们总结如下：

（1）仔细研读该网络的论文，理解每一层的输入/输出值以及网络结构；

（2）按照加载数据，定义网络模型，训练模型和评估模型这样的步骤实现网络。

读者可以从简单的VGGNet开始动手试试。https://github.com/tensorflow/models/tree/master/ slim/nets有所有这几个经典网络的实现方式。

6.4　循环神经网络[15]

循环神经网络主要是自然语言处理（natural language processing，NLP）应用的一种网络模型。它不同于传统的前馈神经网络（feed-forward neural network，FNN），循环神经网络在网络中引入了定性循环，使信号从一个神经元传递到另一个神经元并不会马上消失，而是继续存活。这就是循环神经网络名称的来历。

在传统的神经网络中，输入层到输出层的每层直接是全连接的，但是层内部的神经元彼此之间没有连接。这种网络结构应用到文本处理时却有难度。例如，我们要预测某个单词的下一个单词是什么，就需要用到前面的单词。循环神经网络的解决方式是，隐藏层的输入不仅包括上一层的输出，还包括上一时刻该隐藏层的输出。理论上，循环神经网络能够包含前面的任意多个时刻的状态，但实践中，为了降低训练的复杂性，一般只处理前面几个状态的输出。

循环神经网络的特点在于它是按时间顺序展开的，下一步会受本步处理的影响，网络模型如图6-14所示。在计算时间为2的那步时，输入层和前一步（时间为1）的数据都会对时间为2的那步的输出值有影响。

图6-14[16]

循环神经网络的训练也是使用误差反向传播（backpropagation，BP）算法，并且参数w1、w2和w3是共享的。但是，其在反向传播中，不仅依赖当前层的网络，还依赖前面若干层的网络，这种算法称为随时间反向传播（backpropagation through time，BPTT）算法。BPTT算法是BP算法的扩展，可以将加载在网络上的时序信号按层展开，这样就使得前馈神经网络的静态网络转化为动态网络。

6.5　循环神经网络发展

循环神经网络的发展如图6-15所示。

图6-15

最初Vanilla RNN的改进和CNN的改进类似，也是朝着两个方向演化：一是隐藏层的功能逐渐增强，二是网络的双向化及加深。本节内容参考《Recurrent Neural Networks, Part 1–Introduction
to RNNs》[17]。

6.5.1　增强隐藏层的功能

1．简单RNN

简单RNN（Simple RNN，SRNN）是一个3层网络，在隐藏层（也叫上下文层）增加了上下文单元。图6-16中的CONTEXT(t)是隐藏层，CONTEXT(t-1)是上下文单元。上下文单元节点与隐藏层中的节点的连接及权值都是固定的。

图6-16

如图6-16所示，假设当前是t时刻，则分3步来预测P(wm)：

- 单词wm-1映射到词向量，记作INPUT(t)；

- 连接上一次训练的隐藏层CONTEXT(t-1)，通过sigmoid激活函数生成当前t时刻的CONTEXT(t)；

- 利用softmax函数，预测P(wm)。

2．LSTM

一般的RNN只能与前面若干序列有关，若超过十步，就很容易产生梯度消失或者梯度爆炸问题。产生梯度消失是因为导数的链式法则导致了连乘，造成梯度指数级消失。通过引入单元（cell）结构，得到了RNN的改进模型长短期记忆（Long-Short Term Memory，LSTM）模型，这个模型可以解决梯度消失的问题，如图6-17所示。

可以看出，把图6-14中RNN隐藏层中的黑圆圈换成图6-17里LSTM中的Block，就得到LSTM模型了。这个Block里面主要有1个单元（cell），3个门（gate）。

图6-17[18]

- 单元（cell）：主要有一个状态参数，用来记录状态。

- 输入门（input gate）和输出门（output gate）：对参数的输入、输出进行处理。

- 忘记门（forget gate）：用来设置选择性遗忘的权重，原始RNN在这里权重是1。

3．GRU

GRU[19]（Gated Recurrent Unit Recurrent Neural Network）中在隐藏层上不同距离处的单词对当前的隐藏层的状态的影响不同，距离越远的影响越小。在每个前面的状态对当前的隐藏层的状态的影响上进行了距离加权，距离越远权值越小。同时，在发生误差时，仅仅对产生误差的对应单词的权重进行更新。

如图6-18所示，GRU的思想和LSTM的十分相似。GRU有两个门，即重置门r和更新门z。重置门决定如何组合新输入和之前的记忆，更新门决定留下多少之前的记忆。如果把重置门都设为1，更新门都设为0，就得到普通的RNN模型。

图6-18

4．CW-RNN[20]

CW-RNN（Clockwork RNN）是一种使用时钟频率驱动的RNN。它将隐藏层分为几个组，通过不同的隐藏层组工作在不同的时钟频率下来解决长时间依赖问题。每一组按照自己规定的时钟频率对输入进行处理。将时钟时间离散化，不同的时间点不同的隐藏层组工作，所有的隐藏层组在每一步不会都同时工作，这样就加快网络的训练。此外，时钟周期大的组的神经元速度慢，周期小的速度快，连接方向是周期大的连接到周期小的，周期小的不会连接到周期大的。CW-RNN的结构图如图6-19所示。

图6-19

如图6-19所示，隐藏层中的神经元会被划分为若干个组，记为g，每一组中的神经元个数相同，记为k，并为每一个组分配一个时钟周期Ti∈{T_1, T 2,…, T g}，每一个组内的所有神经元是全连接的，但是组j到组i是循环连接，并需要满足Tj大于Ti。如图6-19所示，这些组按照时钟周期递增从左到右进行排序，即T_1<T_2<…<Tg，连接方向便是从右到左，从速度慢的组连接到速度快的组。

6.5.2　双向化及加深网络

1．双向RNN

双向RNN[21]（Bidirectional RNN）假设当前（第t步）的输出不仅与前面的序列有关，而且与后面的序列有关。原始的双向RNN是一个相对较简单的RNN，由两个RNN上下叠加在一起组成。输出由这两个RNN的隐藏层的状态决定，如图6-20所示。

图6-20

目前的双向RNN有所发展，例如，将双向的思想和LSTM、GRU结合，变成双向LSTM、双向GRU等。

2．深度双向RNN

“双向”与“深度”结合，就产生了深度双向RNN（Deep Bidirectional RNN），深度双向RNN与双向RNN类似，但在隐藏层叠加了多层，使每一步的输入有多层网络，有更强大的表达与学习能力，但需要更多的训练数据。图 6-21 左侧表示了一个多个隐藏层的 RNN，右侧表示深度双向LSTM（DBLSTM）。

图6-21[22]

本节大致介绍了RNN的发展脉络。除此之外，RNN在训练的过程的学习算法也逐渐丰富，如BPTT（Back Propagation Through Time）、RTRL（Real-time Recurrent Learning）、EKF（Extended Kalman Filter）等，读者可以自行查阅相关资料进行学习。

6.6　TensorFlow Model Zoo

TensorFlow的模型都位于https://github.com/tensorflow/models 。正如5.2节中介绍的，这个目录中有很多图像和语音处理的模型，可以直接拿来用。这些模型的检查点文件（参考 4.8.1节ckpt模型文件的保存）有的被打成压缩包，可以直接下载，当作预训练模型使用，如表6-1所示。

表6-1

模型

检查点

Inception V1

inception_v1_2016_08_28.tar.gz

Inception V2

inception_v2_2016_08_28.tar.gz

Inception V3

inception_v3_2016_08_28.tar.gz

Inception V4

inception_v4_2016_09_09.tar.gz

Inception-ResNet-v2

inception_resnet_v2.tar.gz

ResNet 50

resnet_v1_50.tar.gz

ResNet 101

resnet_v1_101.tar.gz

ResNet 152

resnet_v1_152.tar.gz

VGG16

vgg_16.tar.gz

VGG19

vgg_19.tar.gz

此外，我们知道，Caffe因为开源时间比较久，有很多训练好的模型，读者可以利用它作为自己训练项目的预训练模型，大大地减少训练时间和迭代次数。Caffe的模型位于Caffe Model Zoo中，我们可以用工具[23]将Caffe的模型转换为TensorFlow的模型。

6.7　其他研究进展

了解了CNN和RNN的发展，现在再来看看深度学习还有什么令人激动的研究进展。目前TensorFlow的拥趸巨多，这些新的进展一旦出现，在开源社区如GitHub上就会立刻有人将其实现，并且很多也会慢慢合并到Keras等第三方框架中，让开发人员运用新的算法更加得心应手。

6.7.1　强化学习

强化学习（reinforcement learning）是机器学习大家族中的一个分支，并且随着和深度神经网络相结合，体现出更强大的特性，并且在AlphaGo的改良策略网络（policy network）中，也用到了强化学习的方法。

强化学习介于有监督学习和无监督学习之间。在强化学习中，只有很少的标记（奖励），这些标记还有延迟。模型通过这些奖励，不断学习在环境中的行为。

强化学习主要用在游戏、下棋、博弈这类有得分并且有很多步操作的活动中，主要是用来做连续决策。强化学习大家族中有很多方法，如Q-learning、Sarsa、Policy Gradient、Actor Critic等，一般包括算法更新和思维决策两个部分。如果与神经网络相结合，可以采用深度Q网络（Deep Q Network，DQN）。目前也有很多使用Keras的开源实现，读者可以自行查找。

6.7.2　深度森林

我们知道，使用深度神经网络训练模型时，首先需要大量的训练数据，尤其是很多标记数据，并且依赖强大的计算设施和超参数的调节。周志华教授在其论文《Deep Forest：Towards An Alternative to Deep Neural Networks》[24]中提出了一种基于树的新方法——多粒度级联森林（multi-grained cascade forest，gcForest）。gcForest在只有少量数据的情况下也可以训练，并且超参数比深度神经网络少得多，对超参数设定来说性能健壮性也很高，所以使用gcForest训练起来很容易。此外，对于不同规模或者不同数据，也能使用默认设定取得很好的结果。

这为我们使用深度神经网络之外的方法打开了一扇门。未来的深度学习中也许会出现更多神经网络方向外的思路。

6.7.3　深度学习与艺术

在绘画领域，有一个非常著名的“艺术风格的神经网络算法”（A Neural Algorithm of Artistic Style）[25]。这个算法主要是进行绘画风格的迁移。例如，把梵高、毕加索的绘画风格应用在选定的照片上。也就是说，它可以把一幅图片的风格和内容分开，从而把A图片的风格和B图片的内容组合起来，生成一幅风格化的内容图片。在美图秀秀、魔漫相机、脸萌等软件上，也有类似的玩法。这个算法也已经有了采用TensorFlow的开源实现[26]。读者可以自己尝试，探究参数并了解原理。

在音乐领域，也可以利用深度学习来进行创作。例如，用大量的MIDI音频旋律作为训练数据，采用RNN来生成一段旋律，有了这段旋律，就可以作为后续创作的种子和灵感，如果输入大量的巴赫、贝多芬、肖邦的乐曲，还可以模仿他们的作曲风格做出旋律。用音乐作曲已经有了TensorFlow的开源实现[27]。

扩展到其他艺术领域，我们知道，一个创作者的天赋或者勤奋的大量积累是创作的灵感来源，甚至有时候需要从生活中的其他事物中寻找灵感。记得大张伟曾经说过他的创作过程就是去听几个GB的歌去找灵感，每月要去听几千首的歌。深度学习可以帮你创作一些旋律小样，希望可以作为灵感的种子。

6.8　小结

本章主要介绍了卷积神经网络和循环神经网络的发展过程，描述了发展的脉络和方向，对后来研究中设计模型结构很有启发意义。此外，本章还介绍了深度学习研究中的其他进展，如强化学习、深度森林、深度学习在音乐和绘画上的进展，都是目前很火的研发方向。本书还举例了TensorFlow对AlexNet的实现，以及TensorFlow目前完善的预训练模型社区TensorFlow Model Zoo，读者熟练后，可以找到跟自己业务模型相近的预训练模型进行训练。除此之外，迁移学习、One Shot学习、目标检测、视觉跟踪、机器人技术目标分割等也是研究的热点，读者可以选择自己钻研的行业潜心研究。

[1]　本图参考https://classroom.udacity.com/courses/ud730/lessons/6370362152/concepts/63798118170923。

[2]　本图参考https://classroom.udacity.com/courses/ud730/lessons/6370362152/concepts/63798118170923。

[3]　http://cogprints.org/5869/1/cnn_tutorial.pdf

[4]　http://yann.lecun.com/exdb/publis/pdf/lecun-98.pdf

[5]　https://graphics.stanford.edu/courses/cs178/applets/convolution.html

[6]　数据出自Yann LeCun、Leon Bottou、Yoshua Bengio和Patrick Haffner的论文《GradientBased Learning Applied to Document Recognition》：http://vision.stanford.edu/cs598_spring07/papers/Lecun98.pdf。

[7]　https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf

[8]　http://www.robots.ox.ac.uk/~vgg/research/very_deep/

[9]　数据来源于竞赛结果官网：http://image-net.org/challenges/LSVRC/2014/results。

[10]　https://arxiv.org/abs/1312.4400 。

[11]　https://arxiv.org/abs/1409.4842 。

[12]　https://arxiv.org/abs/1512.03385

[13]　RPNs是指从任意尺寸的图片中得到一系列的带有识别出物体概率分数的建议区域。具体流程是使用一个小的网络在最后卷积得到的特征映射上进行滑动扫描，这个滑动的网络每次与特征映射上的窗口进行全连接，然后映射到一个低维向量，如256D或512D，最后将这个低维向量送入到两个全连接层，即box回归层（box-regression layer）和box分类层（box- classification layer）。

[14]　https://arxiv.org/abs/1506.01497

[15]　本节代码参考https://github.com/aymericdamien/TensorFlow-Examples/blob/master/examples/3_NeuralNetworks/recurrent_network.py，并做了相应修改。

[16]　本图出自Alex Graves的论文《Supervised Sequence Labelling with Recurrent Neural Networks》：http://www.cs.toronto. edu/～graves/preprint.pdf。

[17]　http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/

[18]　本图出自Alex Graves的论文《Supervised Sequence Labelling with Recurrent Neural Networks》：http://www.cs.toronto. edu/～graves/preprint.pdf

[19]　详见Junyoung Chung、Caglar Gulcehre、KyungHyun Cho、Yoshua Bengio等人的论文《Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling》：https://arxiv.org/abs/1412.3555。

[20]　本小节参考Jan Koutník、Klaus Greff、Faustino Gomez、Jurgen Schmidhuber的论文《A Clockwork RNN》：https:// arxiv.org/pdf/1402.3511.pdf。

[21]　Mike Schuster和Kuldip K. Paliwal的论文《Bidirectional Recurrent Neural Networks》。

[22]　图出自Alex Graves, Navdeep Jaitly and Abdel-rahman Mohamed的论文《HYBRID SPEECH RECOGNITION WITH DEEP BIDIRECTIONAL LSTM》：https://www.cs.toronto.edu/～graves/asru_2013.pdf

[23]　https://github.com/ethereon/caffe-tensorflow

[24]　https://arxiv.org/abs/1702.08835

[25]　Leon A. Gatys, Alexander S. Ecker,Matthias Bethge的论文：https://arxiv.org/pdf/1508.06576v2.pdf

[26]　https://github.com/anishathalye/neural-style

[27]　https://github.com/tensorflow/magenta

第7章　TensorFlow的高级框架

得益于TensorFlow社区的繁荣，诞生出许多高质量的元框架（metaframework），如Keras、TFLearn、TensorLayer等。使用元框架能够大大减少编写TensorFlow代码的工作量，方便开发者快速搭建网络模型，并且使代码简单、可读性强。

本章我们主要讲解官方默认支持的Keras和老牌的TFLearn提供的高级API。

7.1　TFLearn

TFLearn是一个建立在TensorFlow顶部的模块化的深度学习框架，它为TensorFlow提供更高级的API，以便于快速实验，同时保持完全透明和兼容。

在6.2节中我们已经用原生的TensorFlow代码完成了AlexNet。读者可能已经感受到其代码的冗长，下面我们就用TFLearn框架，看看如何将代码写得简洁。[1]

7.1.1　加载数据

这里用的是牛津大学的鲜花数据集[2]（Flower Dataset）。这个数据集提供了17个类别的鲜花数据，每个类别80张图片，并且图片有大量的姿态和光的变化。

注意，在代码的开始需要导入用到的与卷积、池化、规范化相关的类，方法如下：

import tflearn
from tflearn.layers.core import input_data, dropout, fully_connected
from tflearn.layers.conv import conv_2d, max_pool_2d
from tflearn.layers.normalization import local_response_normalization
from tflearn.layers.estimator import regression

import tflearn.datasets.oxflower17 as oxflower17
X, Y = oxflower17.load_data(one_hot=True, resize_pics=(227, 227))

7.1.2　构建网络模型

构建AlexNet网络模型时，直接使用TFLearn中的卷积、池化、规范化、全连接、dropout函数来构建即可。方法如下：

# 构建AlexNet网络
network = input_data(shape=[None, 227, 227, 3])
network = conv_2d(network, 96, 11, strides=4, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = conv_2d(network, 256, 5, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = conv_2d(network, 384, 3, activation='relu')
network = conv_2d(network, 384, 3, activation='relu')
network = conv_2d(network, 256, 3, activation='relu')
network = max_pool_2d(network, 3, strides=2)
network = local_response_normalization(network)
network = fully_connected(network, 4096, activation='tanh')
network = dropout(network, 0.5)
network = fully_connected(network, 4096, activation='tanh')
network = dropout(network, 0.5)
network = fully_connected(network, 17, activation='softmax')
network = regression(network, optimizer='momentum',
loss='categorical_crossentropy',
learning_rate=0.001) # 回归操作，同时规定网络所使用的学习率、损失函数和优化器

7.1.3　训练模型

构建完模型之后，就可以训练模型了。这里我们加了一步，就是假设有训练好或训练到一半的AlexNet模型的检查点文件，直接载入，方法如下：

model = tflearn.DNN(network, checkpoint_path='model_alexnet',
max_checkpoints=1, tensorboard_verbose=2)
model.fit(X, Y, n_epoch=1000, validation_set=0.1, shuffle=True,
show_metric=True, batch_size=64, snapshot_step=200,
snapshot_epoch=False, run_id='alexnet_oxflowers17')

可见，代码简洁得让人惊讶。这就是第三方库的优点。

7.2　Keras

Keras是一个高级的Python神经网络框架，其文档详见https://keras.io/ 。Keras已经被添加到TensorFlow中，成为其默认的框架，为TensorFlow提供更高级的API。

如果读者不想了解TensorFlow的细节，只需要模块化，那么Keras是一个不错的选择。如果将TensorFlow比喻为编程界的Java或C++，那么Keras就是编程界的Python。它作为TensorFlow的高层封装，可以与TensorFlow联合使用，用它快速搭建原型。

另外，Keras兼容两种后端，即Theano和TensorFlow，并且其接口形式和Torch有几分相像。掌握Keras可以大幅提升对开发效率和网络结构的理解。

7.2.1　Keras的优点

Keras是高度封装的，非常适合新手使用，代码更新速度比较快，示例代码也比较多，文档和讨论区也比较完善。最重要的是，Keras是TensorFlow官方支持的。当机器上有可用的GPU时，代码会自动调用GPU进行并行计算。

Keras官方网站上描述了它的几个优点，具体如下。

- 模块化：模型的各个部分，如神经层、成本函数、优化器、初始化、激活函数、规范化都是独立的模块，可以组合在一起来创建模型。

- 极简主义：每个模块都保持简短和简单。

- 易扩展性：很容易添加新模块，因此Keras适于做进一步的高级研究。

- 使用Python语言：模型用Python实现，非常易于调试和扩展。

7.2.2　Keras的模型[3]

Keras的核心数据结构是模型。模型是用来组织网络层的方式。模型有两种，一种叫Sequential模型，另一种叫Model模型。Sequential模型是一系列网络层按顺序构成的栈，是单输入和单输出的，层与层之间只有相邻关系，是最简单的一种模型。Model模型是用来建立更复杂的模型的。

这里先介绍简单的Sequential模型的使用（7.2.3 节将会以一个示例来介绍Model模型）。首先是加载数据，这里我们假设数据已经加载完毕，是X_train, Y_train和X_test, Y_test。然后构建模型：

from keras.models import Sequential
from keras.layers import Dense, Activation
model = Sequential()
model.add(Dense(output_dim=64, input_dim=100))
model.add(Activation("relu"))
model.add(Dense(output_dim=10))
model.add(Activation("softmax"))

然后，编译模型，同时指明损失函数和优化器：

model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy'])

最后，训练模型和评估模型：

model.fit(X_train, Y_train, nb_epoch=5, batch_size=32)
loss_and_metrics = model.evaluate(X_test, Y_test, batch_size=32)

这就是一个最简单的模型的使用。如果要搭建复杂的网络，可以使用Keras的Model模型，它能定义多输出模型、含有共享层的模型、共享视觉模型、图片问答模型、视觉问答模型等。在Keras的源代码的examples文件夹里还有更多的例子，有兴趣的读者可以参考。

7.2.3　Keras的使用

我们下载Keras代码[4]到本地目录，将下载后的目录命名为keras。Keras源代码中包含很多示例，例如：

- CIFAR10——图片分类（使用CNN和实时数据）；

- IMDB——电影评论观点分类（使用LSTM）；

- Reuters——新闻主题分类（使用多层感知器）；

- MNIST——手写数字识别（使用多层感知器和CNN）；

- OCR——识别字符级文本生成（使用LSTM）。

这里我们主要用MNIST示例进行讲解。后续在第13章中，我们仍会以Keras框架来讲解原理及代码实现。

1．安装

Keras的安装非常简单，不依赖操作系统，建议大家直接通过pip命令安装：

pip install keras

安装完成后，需要选择依赖的后端，在~/.keras/keras.json下修改最后一行backend对应的值即可。修改后的文件如下：

{
"image_dim_ordering": "tf",
"epsilon": 1e-07,
"floatx": "float32",
"backend": "tensorflow"
}

2．实现卷积神经网络[5]

用Keras实现一个网络模型，主要分为加载数据、模型构建、模型编译、模型训练、模型评估或者模型预测几步。下面我们就用最简单的MNIST示例来看如何用Keras实现一个卷积神经网络（CNN）。

首先，定义好超参数以及加载数据，如下：

batch_size = 128
nb_classes = 10 # 分类数
nb_epoch = 12 # 训练轮数

# 输入图片的维度
img_rows, img_cols = 28, 28
# 卷积滤镜的个数
nb_filters = 32
# 最大池化，池化核大小
pool_size = (2, 2)
# 卷积核大小
kernel_size = (3, 3)

(X_train, y_train), (X_test, y_test) = mnist.load_data()

if K.image_dim_ordering() == 'th':
# 使用Theano的顺序：(conv_dim1, channels, conv_dim2, conv_dim3)
X_train = X_train.reshape(X_train.shape[0], 1, img_rows, img_cols)
X_test = X_test.reshape(X_test.shape[0], 1, img_rows, img_cols)
input_shape = (1, img_rows, img_cols)
else:
# 使用TensorFlow的顺序：(conv_dim1, conv_dim2, conv_dim3, channels)
X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1)
X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 1)
input_shape = (img_rows, img_cols, 1)

X_train = X_train.astype('float32')
X_test = X_test.astype('float32')
X_train /= 255
X_test /= 255

# 将类向量转换为二进制类矩阵
Y_train = np_utils.to_categorical(y_train, nb_classes)
Y_test = np_utils.to_categorical(y_test, nb_classes)

下面来构建模型，这里用2个卷积层、1个池化层和2个全连接层来构建，如下：

model = Sequential()

model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1],
border_mode='valid',
input_shape=input_shape))
model.add(Activation('relu'))
model.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1]))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=pool_size))
model.add(Dropout(0.25))

model.add(Flatten())
model.add(Dense(128))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(nb_classes))
model.add(Activation('softmax'))

随后，用model.compile()函数编译模型，采用多分类的损失函数，用Adadelta算法做优化方法，如下：

model.compile(loss='categorical_crossentropy',
optimizer='adadelta',
metrics=['accuracy'])

然后，开始用model.fit()函数训练模型，输入训练集和测试数据，以及batch_size和nb_epoch参数，如下：

model.fit(X_train, Y_train, batch_size=batch_size, nb_epoch=nb_epoch,
verbose=1, validation_data=(X_test, Y_test))

最后，用model.evaluate()函数来评估模型，输出测试集的损失值和准确率，如下：

score = model.evaluate(X_test, Y_test, verbose=0)
print('Test score:', score[0])
print('Test accuracy:', score[1])

计算出的损失值和准确率如下：

Test score: 0.0327563833317
Test accuracy: 0.9893

这是一个非常简单的例子。尽管模型架构是不变的，但是读者要将其应用到自己的开发领域，一般是先读懂对应的神经网络论文，然后用这个架构去搭建和训练模型。

3．模型的加载及保存

Keras的save_model和load_model方法可以将Keras模型和权重保存在一个HDF5文件中，这里面包括模型的结构、权重、训练的配置（损失函数、优化器）等。如果训练因为某种原因中止，就用这个HDF5文件从上次训练的地方重新开始训练。

keras/tests目录中的test_model_saving.py文件中给出了加载和保存模型的方式。test_model_saving.py文件的内容如下：

from keras.models import save_model, load_model

def test_sequential_model_saving():
model = Sequential()
model.add(Dense(2, input_dim=3))
model.add(RepeatVector(3))
model.add(TimeDistributed(Dense(3)))
model.compile(loss=objectives.MSE,
optimizer=optimizers.RMSprop(lr=0.0001),
metrics=[metrics.categorical_accuracy],
sample_weight_mode='temporal')
x = np.random.random((1, 3))
y = np.random.random((1, 3, 3))
model.train_on_batch(x, y)

out = model.predict(x)
_, fname = tempfile.mkstemp('.h5')  # 创建一个HDFS 5文件
save_model(model, fname)

new_model = load_model(fname)
os.remove(fname)

out2 = new_model.predict(x)
assert_allclose(out, out2, atol=1e-05)

# 检测新保存的模型和之前定义的模型是否一致
x = np.random.random((1, 3))
y = np.random.random((1, 3, 3))
model.train_on_batch(x, y)
new_model.train_on_batch(x, y)
out = model.predict(x)
out2 = new_model.predict(x)
assert_allclose(out, out2, atol=1e-05)

如果只是希望保存模型的结构，而不包含其权重及训练的配置（损失函数、优化器），可以使用下面的代码将模型序列化成json或者yaml文件：

json_string = model.to_json()
json_string = model.to_yaml()

保存完成后，还可以手动编辑，并且使用如下语句进行加载：

from keras.models import model_from_json
model = model_from_json(json_string)
model = model_from_yaml(yaml_string)

如果仅需要保存模型的权重，而不包含模型的结构，可以使用save_weights和load_weights语句来保存和加载：

model.save_weights('my_model_weights.h5')
model.load_weights('my_model_weights.h5')

7.3　小结

本章主要介绍了TensorFlow的高质量的元框架——Keras和TFLearn。当读者熟练使用TensorFlow去构建神经网络后，会发现TensorFlow的确过于灵活，且代码十分冗长。使用元框架提供的高级API能够可以极大地提高开发效率，很容易做一些模型实验。在第11章和第13章中都会用Keras和TFLearn里的高级API来构建网络。

[1]　本节代码参考https://github.com/tflearn/tflearn/blob/master/examples/images/alexnet.py。

[2]　http://www.robots.ox.ac.uk/～vgg/data/flowers/17/

[3]　本节的示例参考Keras的官方文档：https://keras.io/。

[4]　https://github.com/fchollet/keras

[5]　本节代码参考https://github.com/fchollet/keras/blob/master/examples/mnist_cnn.py。

第二篇　实战篇

经过基础篇的学习，相信读者对TensorFlow的基本概念已经掌握得很好了，而实战是一个程序员的自我修养。

俄罗斯著名的戏剧和表演理论家康斯坦丁·斯坦尼斯拉夫斯基在他的著作《演员的自我修养》里主张体验，让演员和角色合二为一，这样才能把角色内心生活的一切不可捉摸的细微变化和全部深度，艺术地表达出来。只有这样的艺术才能完全抓住观众的心，使观众弄明白舞台上所发生的一切，丰富观众的内心的经验，在他们心中留下时间无法磨灭的痕迹。而人的情感一定是依照天性自然发生的，是不经意间达到的，却不是随时能通过外在变化（如哭泣、面部表情）来表达，这时就需要表演者对戏剧角色的动作行为深入研究，然后揣摩出内心的情感特征，让“动作”和“心理”相互影响，激发出天性，使动作的表现更加真实。

学者式的研究与学习表演有很大相似之处。学习表演的对象是剧本中的人物，将剧本和角色剖析成不同层次的单元；这里研究的对象是TensorFlow，最终希望能够服务于我们所做的工作上。那么，从哪些方面学习才能真正掌握它呢？基础篇相当于将TensorFlow剖析成不同的单元，并讲解了“剧本”（卷积神经网络）发展的脉络。在本篇中，我们就要通过“动作”（各种例子）去揣摩不同单元是如何构成每一种网络的例子，并且细腻地知道每个单元对做不同任务的网络的影响。

实践出真知，现在我们这就进入实战演练。

第8章　第一个TensorFlow程序

理解TensorFlow的运行方式对后面几章的具体实战非常重要。本章就用一个简单的例子来讲解TensorFlow的运行方式。

8.1　TensorFlow的运行方式

TensorFlow的运行方式分如下4步：

（1）加载数据及定义超参数；

（2）构建网络；

（3）训练模型；

（4）评估模型和进行预测。

下面我们以一个神经网络为例，讲解TensorFlow的运行方式。在这个例子中，我们构造一个满足一元二次函数y = ax^2+b的原始数据，然后构建一个最简单的神经网络，仅包含一个输入层、一个隐藏层和一个输出层。通过TensorFlow将隐藏层和输出层的weights和biases的值学习出来，看看随着训练次数的增加，损失值是不是不断在减小。

8.1.1　生成及加载数据

首先来生成输入数据。我们假设最后要学习的方程为y = x^2 − 0.5，我们来构造满足这个方程的一堆x和y，同时加入一些不满足方程的噪声点。

import tensorflow as tf
import numpy as np
# 构造满足一元二次方程的函数
x_data = np.linspace(-1,1,300)[:, np.newaxis] # 为了使点更密一些，我们构建了300个点，分布在-1到1区间，直接采用np生成等差数列的方法，并将结果为300个点的一维数组，转换为300×1的二维数组
noise = np.random.normal(0, 0.05, x_data.shape) # 加入一些噪声点，使它与x_data的维度一致，并且拟合为均值为0、方差为0.05的正态分布
y_data = np.square(x_data) - 0.5 + noise # y = x^2 – 0.5 + 噪声

接下来定义x和y的占位符来作为将要输入神经网络的变量：

xs = tf.placeholder(tf.float32, [None, 1])
ys = tf.placeholder(tf.float32, [None, 1])

8.1.2　构建网络模型

这里我们需要构建一个隐藏层和一个输出层。作为神经网络中的层，输入参数应该有4个变量：输入数据、输入数据的维度、输出数据的维度和激活函数。每一层经过向量化（y = weights×x + biases）的处理，并且经过激活函数的非线性化处理后，最终得到输出数据。

下面来定义隐藏层和输出层，示例代码如下：

def add_layer(inputs, in_size, out_size, activation_function=None):
# 构建权重：in_size×out_size大小的矩阵
weights = tf.Variable(tf.random_normal([in_size, out_size]))
# 构建偏置：1×out_size的矩阵
biases = tf.Variable(tf.zeros([1, out_size]) + 0.1)
# 矩阵相乘
Wx_plus_b = tf.matmul(inputs, weights) + biases
if activation_function is None:
outputs = Wx_plus_b
else:
outputs = activation_function(Wx_plus_b)
return outputs # 得到输出数据
# 构建隐藏层，假设隐藏层有20个神经元
h1 = add_layer(xs, 1, 20, activation_function=tf.nn.relu)
# 构建输出层，假设输出层和输入层一样，有1个神经元
prediction = add_layer(h1, 20, 1, activation_function=None)

接下来需要构建损失函数：计算输出层的预测值和真实值间的误差，对二者差的平方求和再取平均，得到损失函数。运用梯度下降法，以0.1的学习速率最小化损失：

# 计算预测值和真实值间的误差
loss = tf.reduce_mean(tf.reduce_sum(tf.square(ys - prediction),
reduction_indices=[1]))
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

8.1.3　训练模型

我们让TensorFlow训练1000次，每50次输出训练的损失值：

init = tf.global_variables_initializer() # 初始化所有变量
sess = tf.Session()
sess.run(init)

for i in range(1000): # 训练1000次
sess.run(train_step, feed_dict={xs: x_data, ys: y_data})
if i % 50 == 0: # 每50次打印出一次损失值
print(sess.run(loss, feed_dict={xs: x_data, ys: y_data}))

输出结果如下，在打印出的20次结果中，可以看出损失值是趋于变小的：

4.62726
0.00609592
0.00468114
0.00430631
0.004184
0.0041371
0.00411622
0.0040998
0.00408824
0.00407396
0.00405857
0.00404454
0.00403032
0.00401612
0.00399823
0.00397677
0.00396069
0.0039459
0.00392994
0.00391947

以上就是最简单的利用TensorFlow的神经网络训练一个模型的过程，目标就是要训练出权重值来使模型拟合y = x^2 − 0.5的系数1和−0.5，通过损失值越来越小，可以看出训练参数越来越逼近目标结果。按照标准的步骤，接下来应该评估模型，就是把学习出来的系数weights、biase进行前向传播后和真值y = x^2 − 0.5的结果系数进行比较，根据相近程度计算准确率。这里省略了评估过程。

在第9章将进行对MNIST数据集在各种神经网络上的训练。

8.2　超参数的设定

所谓超参数（hyper-parameters），就是指机器学习模型里的框架参数。与权重参数不同的是，它是需要手动设定、不断试错的。

学习率（learning rate）是一个最常设定的超参数。学习率设置得越大，训练时间越短，速度越快；而学习率设置得越小，训练得准确度越高。那么，如何确定一个比较好的学习率呢？只能通过实验的方法。例如，先设置0.01，观察损失值的变化，然后尝试0.001、0.0001，最终确定一个比较合适的学习率。

我们也可以设置可变的学习率。那么，怎样才算是准确率不再提高，应该停止训练了呢？例如，在训练过程中记录最佳的准确率，在连续n轮（epoch）没达到最佳的准确率时，便可以认为准确率不再提高，就可以停止训练，称为“early stopping”，这个策略叫作 “no-improvement-in-n”规则（例如，我们设置连续10轮准确率不再变动，就认为不再提高）。此时，让学习率减半；下一次满足时，再让学习率减半。这样，在逐渐解决最优解时，我们的学习率越来越小，准确度就越来越高。

mini-batch大小是另一个最常设定的超参数。每批大小决定了权重的更新规则。例如，大小为32时，就是把32个样本的梯度全部计算完，然后求平均值，去更新权重。批次越大训练的速度越快，可以利用矩阵、线性代数库来加速，但是权重更新频率略低。批次越小训练的速度就慢。那么，如何选择批次大小呢？也需要结合机器的硬件性能以及数据集的大小来设定。

正则项系数（regularization parameter，λ）是另一个常用的超参数。但是，设定没有太多可遵循的规则，一般凭经验。一般来说，如果在较复杂的网络发现出现了明显的过拟合（在训练数据准确率很高但测试数据准确率反而下降），可以考虑增加此项。初学者可以一开始设置为0，然后确定好一个比较好的学习率后，再给λ一个值，随后根据准确率再进行精细调整。

8.3　小结

本章主要介绍了如何用TensorFlow构建一个神经网络。构建神经网络主要分为4个步骤：构造数据、构建网络、训练模型、评估及预测模型。此外，还介绍了一些超参数设定的经验和技巧。

第9章　TensorFlow在MNIST中的应用

MNIST[1]（Mixed National Institute of Standards and Technology）是一个入门级的计算机视觉数据集，数据集中都是美国中学生手写的数字。它的训练集包含6万张图片，测试集包含1万张图片，并且数字已经进行过预处理和格式化，做了大小调整并居中，图片尺寸也固定为28×28。这个数据集很小，但训练速度很快，而且收敛效果也很好，非常适合作为实战的例子去学习。

接下来我们就以MNIST数据集为例，尝尽TensorFlow在深度学习中的各种应用。

9.1　MNIST数据集简介

MNIST数据集是NIST数据集的子集，包含以下4个文件。

- train-labels-idx1-ubyte.gz：训练集标记文件（28 881字节）。

- train-images-idx3-ubyte.gz：训练集图片文件（9 912 422 字节）。

- t10k-labels-idx1-ubyte.gz：测试集标记文件（4 542字节）。

- t10k-images-idx3-ubyte.gz：测试集图片文件（1 648 877字节）。

MNIST数据集包括训练集的图片和标记数据，以及测试集的图片和标记数据，在测试集包含的10 000个样例中，前5 000个样例取自原始的NIST训练集，后5 000个取自原始的NIST测试集，因此前5 000个预测起来更容易些。

下面具体讲解它们的格式[2]。

9.1.1　训练集的标记文件

训练集标记文件train-labels-idx1-ubyt的格式如下：

[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000801(2049) magic number (MSB first)
0004     32 bit integer  60000            number of items
0008     unsigned byte   ??               label
0009     unsigned byte   ??               label
........
xxxx     unsigned byte   ??               label

其中，MSB（most significant bit，最高有效位），在二进制数中，MSB是最高加权位，与十进制数字中最左边的一位类似[3]。通常，MSB位于二进制数的最左侧。MSB first指的是最高有效位在前。

这里magic number是指写入ELF格式（Executable and Linkable Format）的ELF头文件中的常量，检查这个数和自己设定的是否一致能够判断出文件是否损坏。

9.1.2　训练集的图片文件

训练集的图片文件train-images-idx3-ubyte的格式如下：

[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000803(2051) magic number
0004     32 bit integer  60000            number of images
0008     32 bit integer  28               number of rows
0012     32 bit integer  28               number of columns
0016     unsigned byte   ??               pixel
0017     unsigned byte   ??               pixel
........
xxxx     unsigned byte   ??               pixel

pixel（像素）的取值范围是0-255，0-255代表背景色（白色），255代表前景色（黑色）。

9.1.3　测试集的标记文件

测试集的标记文件t10k-labels-idx1-ubyte的格式如下：

[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000801(2049) magic number (MSB first)
0004     32 bit integer  10000            number of items
0008     unsigned byte   ??               label
0009     unsigned byte   ??               label
........
xxxx     unsigned byte   ??               label

9.1.4　测试集的图片文件

测试集的图片文件t10k-labels-idx1-ubyte的格式如下：

[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000803(2051) magic number
0004     32 bit integer  10000            number of images
0008     32 bit integer  28               number of rows
0012     32 bit integer  28               number of columns
0016     unsigned byte   ??               pixel
0017     unsigned byte   ??               pixel
........
xxxx     unsigned byte   ??               pixel

已经有各种方法被应用在MNIST这个训练集上，接下来我们就一起来探讨这些方法。讨论这些方法有助于大家了解神经网络的基本设计思想和TensorFlow的工作流程。这些方法的代码位于tensorflow-1.1.0/tensorflow/examples/tutorials/mnist/下。

- mnist_softmax.py：MNIST采用Softmax回归训练。

- fully_connected_feed.py：MNIST采用Feed数据方式训练。

- mnist_with_summaries.py：MNIST使用卷积神经网络（CNN），并且训练过程可视化。

- mnist_softmax_xla.py.py：MNIST使用XLA框架（参见第15章）。

我们先从一个简单的Softmax回归模型开始。

9.2　MNIST的分类问题

Softmax回归可以解决两种以上的分类，该模型是Logistic回归模型在分类问题上的推广。对于要识别0～9这10类数字，首选Softmax回归。MNIST的Softmax回归源代码位于tensorflow- 1.1.0/tensorflow/examples/tutorials/mnist/mnist_softmax.py。

9.2.1　加载数据

我们需要导入input_data.py文件，使用tensorflow.contrib.learn中的read_data_sets来加载数据，代码如下：

from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
# 加载数据
mnist = input_data.read_data_sets(FLAGS.data_dir, one_hot=True)

其中，FLAGS.data_dir是MNIST所在的路径，用户可以自己指定；one_hot标记则是指一个长度为n的数组，只有一个元素是1.0，其他元素是0.0（例如，在n为4的情况下，标记2对应的one_hot标记就是 0.0 0.0 1.0 0.0）。

使用one_hot的直接原因是，我们使用0～9个类别的多分类的输出层是softmax层，它的输出是一个概率分布，从而要求输入的标记也以概率分布的形式出现，进而可以计算交叉熵。

9.2.2　构建回归模型

构建回归模型，我们需要输入原始真实值（group truth），计算采用softmax函数拟合后的预测值，并且定义损失函数和优化器：

# 定义回归模型
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
y = tf.matmul(x, W) + b # 预测值

在这里，我们要求TensorFlow用梯度下降算法以0.5的学习率最小化交叉熵。在这里也可以采用其他优化器，只需要调整tf.train.GradientDescentOptimizer即可。

# 定义损失函数和优化器
y_ = tf.placeholder(tf.float32, [None, 10]) # 输入的真实值的占位符

# 我们用tf.nn.softmax_cross_entropy_with_logits 来计算预测值y与真实值y_的差值，并取均值
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y, logits=y_))
# 采用SGD作为优化器
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

9.2.3　训练模型

已经设置好了模型，在训练之前，需要先初始化我们创建的变量，以及在会话中启动模型。

#这里使用InteractiveSession()来创建交互式上下文的TensorFlow会话
#与常规会话不同的是，交互式会话会成为默认会话
#方法（如tf.Tensor.eval和tf.Operation.run）都可以使用该会话来运行操作（OP）
sess = tf.InteractiveSession()
tf.global_variables_initializer().run()

我们让模型循环训练1000次，在每次循环中我们都随机抓取训练数据中100个数据点，来替换之前的占位符。

# Train
for _ in range(1000):
batch_xs, batch_ys = mnist.train.next_batch(100)
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

这种训练方式称为随机训练（stochastic training），使用SGD方法进行梯度下降，也就是每次从训练数据集中随机抓取一小部分数据进行梯度下降训练。正如我们在4.7.5节中讲到的，与每次对所有训练数据进行计算的BGD相比，SGD既能够学习到数据集的总体特征，又能够加速训练过程。

模型训练好了之后，如何来评估模型的性能呢？

9.2.4　评估模型

tf.argmax(y,1)返回的是模型对任一输入x预测到的标记值，tf.argmax(y_,1)代表正确的标记值。我们用tf.equal来检测预测值和真实值是否匹配，并且将预测后得到的布尔值转化成浮点数，并取平均值。代码如下：

# 评估训练好的模型
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1)) # 计算预测值和真实值
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) # 布尔型转化为浮点数，并取平均值，得到准确率
# 计算模型在测试集上的准确率
print(sess.run(accuracy, feed_dict={x: mnist.test.images,
y_: mnist.test.labels}))

评估模型输出的结果为：

0.9179

也就是说，最终的准确率是 91.7%。接下来我们用卷积神经网络（CNN）模型，配合TensorBoard可视化工具，直观地看看在训练过程中都发生了什么，以及能不能得到更高的准确率。

9.3　训练过程的可视化

TensorFlow为我们提供了现成的神经网络的可视化例子。下面我们就以MNIST为例，看看训练过程中究竟发生了什么。源代码详见tensorflow-1.1.0/tensorflow/examples/tutorials/mnist/mnist_ with_summaries.py。

我们知道，采用TensorBoard可视化的原理在于，在训练的过程中，记录下结构化的数据，然后运行一个本地服务器，监听6006端口。当在浏览器中请求页面时，分析记录的数据，绘制成统计图表及计算图展示出来。

我们先直接运行脚本，看会有哪些结果：

python mnist_with_summaries.py

得到的命令行输出如下：

Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes.
Extracting /tmp/tensorflow/mnist/input_data/train-images-idx3-ubyte.gz
Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes.
Extracting /tmp/tensorflow/mnist/input_data/train-labels-idx1-ubyte.gz
Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes.
Extracting /tmp/tensorflow/mnist/input_data/t10k-images-idx3-ubyte.gz
Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes.
Extracting /tmp/tensorflow/mnist/input_data/t10k-labels-idx1-ubyte.gz
Accuracy at step 0: 0.1168
Accuracy at step 10: 0.6744
Accuracy at step 20: 0.803
Accuracy at step 30: 0.8496
Accuracy at step 40: 0.8822
Accuracy at step 50: 0.8857
Accuracy at step 60: 0.8897
Accuracy at step 70: 0.8883
Accuracy at step 80: 0.8884
Accuracy at step 90: 0.8974
Adding run metadata for 99
Accuracy at step 100: 0.9062
# 此处有省略
Adding run metadata for 899
Accuracy at step 900: 0.9643
Accuracy at step 910: 0.9665
Accuracy at step 920: 0.9662
Accuracy at step 930: 0.9678
Accuracy at step 940: 0.966
Accuracy at step 950: 0.9685
Accuracy at step 960: 0.9687
Accuracy at step 970: 0.9686
Accuracy at step 980: 0.9651
Accuracy at step 990: 0.9665
Adding run metadata for 999

这个例子会把它训练过程中的数据存储在/tmp/tensorflow/mnist 目录中，这个路径可以通过命令行参数--log_dir指定。在/tmp/tensorflow/mnist中运行tree命令，结果如下：

├── input_data # 存放训练数据
│   ├── t10k-images-idx3-ubyte.gz
│   ├── t10k-labels-idx1-ubyte.gz
│   ├── train-images-idx3-ubyte.gz
│   └── train-labels-idx1-ubyte.gz
└── logs # 训练结果日志
└── mnist_with_summaries
├── test # 测试集结果日志
│   └── events.out.tfevents.1484255500.baidudeMacBook-Pro.local
└── train # 训练集结果日志
└── events.out.tfevents.1484255499.baidudeMacBook-Pro.local

运行tensorboard命令，打开浏览器，查看训练的可视化结果。此时，需要加上参数logdir，标明日志文件的存储路径，方法如下：

tensorboard --logdir=/tmp/tensorflow/mnist/logs/mnist_with_summaries

而这个路径是在创建摘要的文件写入符（FileWriter）时指定的，如下：

# sess.graph是图的定义；本句的含义是使图可视化
file_writer = tf.summary.FileWriter('/path/to/logs', sess.graph)

输出结果如下：

Starting TensorBoard 39 on port 6006
(You can navigate to http://192.168.0.101:6006)

在浏览器中打开http://192.168.0.101:6006就进入了可视化的操作界面，如图9-1所示。

图9-1

接下来就可以进入各个面板查看数据流图和直方图了，这部分内容请参考3.2节中关于TersorBoard可视化面板的介绍。

我们曾在4.4.2节讲解过与可视化实现相关的API，这里我们就用本节的例子来具体看看如何实现可视化。

从图9-1可以看出，给一个张量添加多个摘要描述的函数为variable_summaries，如下：

def variable_summaries(var):
"""对一个张量添加多个摘要描述"""
with tf.name_scope('summaries'):
mean = tf.reduce_mean(var)
tf.summary.scalar('mean', mean) # 均值
with tf.name_scope('stddev'):
stddev = tf.sqrt(tf.reduce_mean(tf.square(var - mean)))
tf.summary.scalar('stddev', stddev) # 标准差
tf.summary.scalar('max', tf.reduce_max(var)) # 最大值
tf.summary.scalar('min', tf.reduce_min(var)) # 最小值
tf.summary.histogram('histogram', var)

这里绘制出的每一层的均值、标准差、最大值和最小值在SCALARS面板中，如图9-2所示。

图9-2

在构建网络模型的过程中，对weights和biases均调用variable_summaries，并对每一层采用tf.summary.histogram绘制张量经过激活函数前后的变化。

def nn_layer(input_tensor, input_dim, output_dim, layer_name, act=tf.nn.relu):
# 为确保计算图中各个层的分组，给每一层添加一个name_scope
with tf.name_scope(layer_name):
with tf.name_scope('weights'):
weights = weight_variable([input_dim, output_dim])
variable_summaries(weights)
with tf.name_scope('biases'):
biases = bias_variable([output_dim])
variable_summaries(biases)
with tf.name_scope('Wx_plus_b'):
preactivate = tf.matmul(input_tensor, weights) + biases
tf.summary.histogram('pre_activations', preactivate) # 激活前的直方图
activations = act(preactivate, name='activation')
tf.summary.histogram('activations', activations) # 激活后的直方图
return activations

绘制出的图形在HISTOGRAMS面板中，如图9-3所示。

绘制准确率和交叉熵采用的方法为：

tf.summary.scalar('cross_entropy', cross_entropy)
tf.summary.scalar('accuracy', accuracy)

绘制出的图形在SCALARS面板中，如图9-4所示。

图9-3

图9-4

读者可以自行探索更多的可视化组合方法，充分利用好TensorBoard工具。我们接下来就讲解CNN在TensorFlow上如何构建。

9.4　MNIST的卷积神经网络[4]

本节我们将学习用TensorFlow搭建一个卷积神经网络（CNN）模型，并用它来训练MNIST数据集。

同样，构建的流程也是先加载数据，再构建网络模型，最后训练和评估模型。

9.4.1　加载数据

先导入必要的库，如下：

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

这一步和上一节MNIST的回归模型相同，不再赘述。

9.4.2　构建模型

构建一个CNN模型，需要以下几步。

（1）定义输入数据并预处理数据。这里，我们首先读取数据MNIST，并分别得到训练集的图片和标记的矩阵，以及测试集的图片和标记的矩阵。代码如下：

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
trX, trY, teX, teY = mnist.train.images, mnist.train.labels, mnist.test.images, mnist.test.labels

其中，mnist是TensorFlow的tensorflow.contrib.learn中的Datasets，其值如下：

Datasets(train=<tensorflow.contrib.learn.python.learn.datasets.mnist.DataSet object at 0x110987cd0>, validation=<tensorflow.contrib.learn.python.learn.datasets. mnist.DataSet object at 0x110987d10>, test=<tensorflow.contrib.learn.python.learn. datasets.mnist.DataSet object at 0x110987d50>)

trX、trY,、teX、teY是数据的矩阵表现，其值类似于：

[[ 0.  0.  0. ...,  0.  0.  0.]
[ 0.  0.  0. ...,  0.  0.  0.]
[ 0.  0.  0. ...,  0.  0.  0.]
...,
[ 0.  0.  0. ...,  0.  0.  0.]
[ 0.  0.  0. ...,  0.  0.  0.]
[ 0.  0.  0. ...,  0.  0.  0.]]

接着，需要处理输入的数据，把上述trX和teX的形状变为[-1,28,28,1]，-1表示不考虑输入图片的数量，28×28是图片的长和宽的像素数，1是通道（channel）数量，因为MNIST的图片是黑白的，所以通道是1，如果是RGB彩色图像，通道是3。

trX = trX.reshape(-1, 28, 28, 1)  # 28x28x1 input img
teX = teX.reshape(-1, 28, 28, 1)  # 28x28x1 input img

X = tf.placeholder("float", [None, 28, 28, 1])
Y = tf.placeholder("float", [None, 10])

（2）初始化权重与定义网络结构。这里，我们将要构建一个拥有3个卷积层和3个池化层，随后接1个全连接层和1个输出层的卷积神经网络。首先定义初始化权重的函数：

def init_weights(shape):
return tf.Variable(tf.random_normal(shape, stddev=0.01))

初始化权重方法如下，我们设置卷积核的大小为3×3：

w = init_weights([3, 3, 1, 32])            # patch大小为3×3，输入维度为1，输出维度为32
w2 = init_weights([3, 3, 32, 64])          # patch大小为3×3，输入维度为32，输出维度为64
w3 = init_weights([3, 3, 64, 128])         # patch大小为3×3，输入维度为64，输出维度为128
w4 = init_weights([128 * 4 * 4, 625])     # 全连接层，输入维度为 128 × 4 × 4,是上一层的输出数据又三维的转变成一维， 输出维度为625
w_o = init_weights([625, 10])  # 输出层，输入维度为 625, 输出维度为10，代表10类(labels)

随后，定义一个模型函数，代码如下：

# 神经网络模型的构建函数，传入以下参数
# X：输入数据
# w：每一层的权重
# p_keep_conv，p_keep_hidden：dropout要保留的神经元比例

def model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden):
# 第一组卷积层及池化层，最后dropout一些神经元
l1a = tf.nn.relu(tf.nn.conv2d(X, w, strides=[1, 1, 1, 1], padding='SAME'))
# l1a shape=(?, 28, 28, 32)
l1 = tf.nn.max_pool(l1a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# l1 shape=(?, 14, 14, 32)
l1 = tf.nn.dropout(l1, p_keep_conv)

# 第二组卷积层及池化层，最后dropout一些神经元
l2a = tf.nn.relu(tf.nn.conv2d(l1, w2, strides=[1, 1, 1, 1], padding='SAME'))
# l2a shape=(?, 14, 14, 64)
l2 = tf.nn.max_pool(l2a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# l2 shape=(?, 7, 7, 64)
l2 = tf.nn.dropout(l2, p_keep_conv)

# 第三组卷积层及池化层，最后dropout一些神经元
l3a = tf.nn.relu(tf.nn.conv2d(l2, w3, strides=[1, 1, 1, 1], padding='SAME'))
# l3a shape=(?, 7, 7, 128)
l3 = tf.nn.max_pool(l3a, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# l3 shape=(?, 4, 4, 128)
l3 = tf.reshape(l3, [-1, w4.get_shape().as_list()[0]])    # reshape to (?, 2048)
l3 = tf.nn.dropout(l3, p_keep_conv)

# 全连接层，最后dropout一些神经元
l4 = tf.nn.relu(tf.matmul(l3, w4))
l4 = tf.nn.dropout(l4, p_keep_hidden)

# 输出层
pyx = tf.matmul(l4, w_o)
return pyx #返回预测值

我们定义dropout的占位符——keep_conv，它表示在一层中有多少比例的神经元被保留下来。生成网络模型，得到预测值，如下：

p_keep_conv = tf.placeholder("float")
p_keep_hidden = tf.placeholder("float")
py_x = model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden) #得到预测值

接下来，定义损失函数，这里我们仍然采用tf.nn.softmax_cross_entropy_with_logits来比较预测值和真实值的差异，并做均值处理；定义训练的操作（train_op），采用实现RMSProp算法的优化器tf.train.RMSPropOptimizer，学习率为0.001，衰减值为0.9，使损失最小；定义预测的操作（predict_op）。具体如下：

cost = tf.reduce_mean(tf.nn. softmax_cross_entropy_with_logits(logits=py_x, labels=Y))
train_op = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cost)
predict_op = tf.argmax(py_x, 1)

9.4.3　训练模型和评估模型

先定义训练时的批次大小和评估时的批次大小，如下：

batch_size = 128
test_size = 256

在一个会话中启动图，开始训练和评估：

# Launch the graph in a session
with tf.Session() as sess:
# you need to initialize all variables
tf. global_variables_initializer().run()

for i in range(100):
training_batch = zip(range(0, len(trX), batch_size),
range(batch_size, len(trX)+1, batch_size))
for start, end in training_batch:
sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end],
p_keep_conv: 0.8, p_keep_hidden: 0.5})

test_indices = np.arange(len(teX)) # Get A Test Batch
np.random.shuffle(test_indices)
test_indices = test_indices[0:test_size]

print(i, np.mean(np.argmax(teY[test_indices], axis=1) ==
sess.run(predict_op, feed_dict={X: teX[test_indices],
p_keep_conv: 1.0,
p_keep_hidden: 1.0})))

结果如下：

0 0.96484375
1 0.984375
2 0.98828125
3 0.9921875
4 0.9921875
5 1.0
6 0.99609375
7 0.9921875
8 0.9921875
9 0.98046875
10 0.9921875
11 0.984375
12 0.984375
13 1.0
14 0.9921875
15 0.9921875
16 0.98828125
17 0.9921875
18 1.0
19 1.0
20 0.99609375
# 此处有省略
71 0.98828125
72 0.99609375
73 1.0
74 0.99609375
75 1.0
76 0.9921875
77 0.9921875
78 0.99609375
79 0.99609375
80 1.0
81 0.99609375
82 0.9921875
83 0.99609375
84 0.99609375
85 0.99609375
86 0.99609375
87 0.9765625
88 0.99609375
89 0.984375
90 0.984375
91 0.98828125
92 1.0
93 0.99609375
94 0.9921875
95 1.0
96 0.99609375
97 0.99609375
98 0.9921875
99 0.9921875

上面输出了训练的次数和准确度的关系。可以看出，当训练 100 轮后精确率已经接近99.22%。

通过回归模型和卷积神经网络模型，可以看出卷积神经网络的效果真的是非常好。如果把循环神经网络（RNN）应用再MNIST上会怎么样呢？

9.5　MNIST的循环神经网络[5]

本节学习用TensorFlow搭建一个循环神经网络（RNN）模型，并用它来训练MNIST数据集。

RNN在自然语言处理领域的已下几个方向应用得非常成功：

- 机器翻译；

- 语音识别；

- 图像描述生成（把RNN和CNN结合，根据图像的特征生成描述，在第12章用“看图说话”讲解）；

- 语言模型与文本生成，即利用生成的模型预测下一个单词的可能性。

更多的资料读者可参考Alex Graves的论文《Supervised Sequence Labelling with Recurrent Neural Networks》[6]。

9.5.1　加载数据

这一步和9.2.1节MNIST的回归模型相同，请读者参考，不再赘述。

9.5.2　构建模型

首先，设置训练的超参数，分别设置学习率、训练次数和每轮训练的数据大小：

# 设置训练的超参数
lr = 0.001
training_iters = 100000
batch_size = 128

为了使用RNN来分类图片，我们把每张图片的行看成是一个像素序列（sequence）。因为MNIST图片的大小是28×28像素，所以我们把每一个图像样本看成一行行的序列。因此，共有（28个元素的序列）×（28行），然后每一步输入的序列长度是28，输入的步数是28步。下面定义RNN的参数：

# 神经网络的参数
n_inputs = 28  # 输入层的n
n_steps = 28  # 28长度
n_hidden_units = 128  # 隐藏层的神经元个数
n_classes = 10  # 输出的数量，即分类的类别，0～9个数字，共有10个

定义输入数据及权重：

# 输入数据占位符
x = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
y = tf.placeholder(tf.float32, [None, n_classes])

# 定义权重
weights = {
# (28, 128)
'in': tf.Variable(tf.random_normal([n_inputs, n_hidden_units])),
# (128, 10)
'out': tf.Variable(tf.random_normal([n_hidden_units, n_classes]))
}
biases = {
# (128, )
'in': tf.Variable(tf.constant(0.1, shape=[n_hidden_units, ])),
# (10, )
'out': tf.Variable(tf.constant(0.1, shape=[n_classes, ]))
}

定义RNN模型：

def RNN(X, weights, biases):

# 把输入的X转换成X ==> (128 batch * 28 steps, 28 inputs)
X = tf.reshape(X, [-1, n_inputs])

# 进入隐藏层
# X_in = (128 batch * 28 steps, 128 hidden)
X_in = tf.matmul(X, weights['in']) + biases['in']
# X_in ==> (128 batch, 28 steps, 128 hidden)
X_in = tf.reshape(X_in, [-1, n_steps, n_hidden_units])
# 这里采用基本的LSTM循环网络单元：basic LSTM Cell
lstm_cell = tf.contrib.rnn.BasicLSTMCell(n_hidden_units, forget_bias=1.0,
state_is_tuple=True)
# 初始化为零值，lstm单元由两个部分组成：(c_state, h_state)
init_state = lstm_cell.zero_state(batch_size, dtype=tf.float32)

# dynamic_rnn 接收张量(batch, steps, inputs)或者(steps, batch, inputs)作为X_in
outputs, final_state = tf.nn.dynamic_rnn(lstm_cell, X_in, initial_state=
init_state, time_major=False)

results = tf.matmul(final_state[1], weights['out']) + biases['out']
return results

定义损失函数和优化器，优化器采用AdamOptimizer。

pred = RNN(x, weights, biases)
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=y))
train_op = tf.train.AdamOptimizer(lr).minimize(cost)

定义模型预测结果及准确率计算方法：

correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

9.5.3　训练数据及评估模型

在一个会话中启动图，开始训练，每20次输出1次准确率的大小：

with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
step = 0
while step * batch_size < training_iters:
batch_xs, batch_ys = mnist.train.next_batch(batch_size)
batch_xs = batch_xs.reshape([batch_size, n_steps, n_inputs])
sess.run([train_op], feed_dict={
x: batch_xs,
y: batch_ys,
})
if step % 20 == 0:
print(sess.run(accuracy, feed_dict={
x: batch_xs,
y: batch_ys,
}))
step += 1

结果如下：

0.117188
0.640625
0.726562
0.695312
0.859375
0.773438
0.90625
# 此处有省略
0.960938
0.945312
0.953125
0.976562
0.96875
0.953125
0.96875
0.960938
0.96875
0.960938
0.976562
0.96875
0.921875
0.96875
0.984375
0.9375

可以看出，准确率接近93%，准确率不如CNN模型。

9.6　MNIST的无监督学习

本节将介绍基于无监督学习的一个简单应用——自编码器（autoencoder），并学习用TensorFlow搭建一个自编码网络，并用它在MNIST数据集上训练。

9.6.1　自编码网络[7]

前面讲到的都是有监督学习，它的重要特征是数据都是有标记的，无标记的数据应该用什么样的网络模型来学习呢？下面我们就介绍一个网络模型——自编码网络。

自编码网络模型如图9-5所示。[8]

自编码网络的作用是将输入样本压缩到隐藏层，然后解压，在输出端重建样本。最终输出层神经元数量等于输入层神经元的数量。

这里面主要有两个过程：压缩和解压。压缩依靠的是输入数据（图像、文字、声音）本身存在不同程度的冗余信息，自动编码网络通过学习去掉这些冗余信息，把有用的特征输入到隐藏层中。这里和主成分分析（principal components analysis，PCA[9]）有些类似，要找到可以代表源数据的主要成分。其实，如果激活函数不使用sigmoid等非线性函数，而使用线性函数，就是PCA模型。可以想象，如果数据都是完全随机、相互独立、同分布的，自编码网络就很难学习到一个有效的压缩模型。

图9-5②

压缩过程一方面要限制隐藏神经元的数量，来学习一些有意义的特征，另一方面还希望神经元大部分时间是被抑制的，当神经元的输出接近1时认为是被激活的，接近0时认为是被抑制的。希望部分神经元处于被抑制状态，这种规则称为稀疏性限制。

多个隐藏层的主要作用是，如果输入的数据是图像，第一层会学习如何识别边，第二层会学习如何去组合边，从而构成轮廓、角等，更高层会学习如何去组合更有意义的特征。例如，如果输入数据是人脸图像的话，更高层会学习如何识别和组合眼睛、鼻子、嘴等人脸器官。

9.6.2　TensorFlow的自编码网络实现[10]

下面我们还以MNIST数据集为例，讲解一下自编码器的运用。

1．加载数据

先导入必要的库，如下：

import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

2．构建模型

首先，设置训练的超参数，包括学习率、训练的轮（epoch）数（全部数据训完一遍成为一轮）、每次训练的数据多少、每隔多少轮显示一次训练结果：

# 设置训练超参数
learning_rate = 0.01 # 学习率
training_epochs = 20 # 训练的轮数
batch_size = 256 # 每次训练的数据多少
display_step = 1 # 每隔多少轮显示一次训练结果

还要设置其他参数变量，表示从测试集中选择10张图片去验证自动编码器的结果：

examples_to_show = 10

然后定义输入数据，这里是无监督学习，所以只需要输入图片数据，不需要标记数据。

X = tf.placeholder("float", [None, n_input])

随后初始化权重与定义网络结构。我们设计这个自动编码网络含有两个隐藏层，第一个隐藏层神经元为256个，第二个隐藏层神经元为128个。定义网络参数如下：

# 网络参数
n_hidden_1 = 256 # 第一个隐藏层神经元个数，也是特征值个数
n_hidden_2 = 128 # 第二个隐藏层神经元个数，也是特征值个数
n_input = 784 # 输入数据的特征值个数：28×28=784

初始化每一层的权重和偏置，如下：

weights = {
'encoder_h1': tf.Variable(tf.random_normal([n_input, n_hidden_1])),
'encoder_h2': tf.Variable(tf.random_normal([n_hidden_1, n_hidden_2])),
'decoder_h1': tf.Variable(tf.random_normal([n_hidden_2, n_hidden_1])),
'decoder_h2': tf.Variable(tf.random_normal([n_hidden_1, n_input])),
}
biases = {
'encoder_b1': tf.Variable(tf.random_normal([n_hidden_1])),
'encoder_b2': tf.Variable(tf.random_normal([n_hidden_2])),
'decoder_b1': tf.Variable(tf.random_normal([n_hidden_1])),
'decoder_b2': tf.Variable(tf.random_normal([n_input])),
}

接着，定义自动编码模型的网络结构，包括压缩和解压两个过程：

# 定义压缩函数
def encoder(x):
# Encoder Hidden layer with sigmoid activation #1
layer_1 = tf.nn.sigmoid(tf.add(tf.matmul(x, weights['encoder_h1']),
biases['encoder_b1']))
# Decoder Hidden layer with sigmoid activation #2
layer_2 = tf.nn.sigmoid(tf.add(tf.matmul(layer_1, weights['encoder_h2']),
biases['encoder_b2']))
return layer_2

# 定义解压函数
def decoder(x):
# Encoder Hidden layer with sigmoid activation #1
layer_1 = tf.nn.sigmoid(tf.add(tf.matmul(x, weights['decoder_h1']),
biases['decoder_b1']))
# Decoder Hidden layer with sigmoid activation #2
layer_2 = tf.nn.sigmoid(tf.add(tf.matmul(layer_1, weights['decoder_h2']),
biases['decoder_b2']))
return layer_2
# 构建模型
encoder_op = encoder(X)
decoder_op = decoder(encoder_op

接着，我们构建损失函数和优化器。这里的损失函数用“最小二乘法”对原始数据集和输出的数据集进行平方差并取均值运算；优化器采用RMSPropOptimizer。

# 得出预测值
y_pred = decoder_op
# 得出真实值，即输入值
y_true = X

# 定义损失函数和优化器
cost = tf.reduce_mean(tf.pow(y_true - y_pred, 2))
optimizer = tf.train.RMSPropOptimizer(learning_rate).minimize(cost)

3．训练数据及评估模型

在一个会话中启动图，开始训练和评估：

with tf.Session() as sess:
sess.run(init)
total_batch = int(mnist.train.num_examples/batch_size)
# 开始训练
for epoch in range(training_epochs):

for i in range(total_batch):
batch_xs, batch_ys = mnist.train.next_batch(batch_size)
# Run optimization op (backprop) and cost op (to get loss value)
_, c = sess.run([optimizer, cost], feed_dict={X: batch_xs})
# 每一轮，打印出一次损失值
if epoch % display_step == 0:
print("Epoch:", '%04d' % (epoch+1), "cost=", "{:.9f}".format(c))

print("Optimization Finished!")

# 对测试集应用训练好的自动编码网络
encode_decode = sess.run(y_pred, feed_dict={X: mnist.test.images[:examples_ to_show]})
# 比较测试集原始图片和自动编码网络的重建结果
f, a = plt.subplots(2, 10, figsize=(10, 2))
for i in range(examples_to_show):
a[0][i].imshow(np.reshape(mnist.test.images[i], (28, 28))) #测试集
a[1][i].imshow(np.reshape(encode_decode[i], (28, 28))) # 重建结果
f.show()
plt.draw()
plt.waitforbuttonpress()

终端输出了每一轮的损失值，结果如下：

Epoch: 0001 cost= 0.210102022
Epoch: 0002 cost= 0.175847366
Epoch: 0003 cost= 0.161052987
Epoch: 0004 cost= 0.149544969
Epoch: 0005 cost= 0.142014906
Epoch: 0006 cost= 0.135422051
Epoch: 0007 cost= 0.131084189
Epoch: 0008 cost= 0.127759427
Epoch: 0009 cost= 0.124004595
Epoch: 0010 cost= 0.123085082
Epoch: 0011 cost= 0.117432065
Epoch: 0012 cost= 0.119511291
Epoch: 0013 cost= 0.115581676
Epoch: 0014 cost= 0.113403663
Epoch: 0015 cost= 0.110742018
Epoch: 0016 cost= 0.111147717
Epoch: 0017 cost= 0.105923556
Epoch: 0018 cost= 0.105761752
Epoch: 0019 cost= 0.103263445
Epoch: 0020 cost= 0.101153791
Optimization Finished!

可以看出随着训练次数的增多，损失值趋于减少。

测试集的图片和经过自动编码器重建特征后的图片对比如图9-6所示。上面一行是测试集的图片，下面一行对应的是经过自动编码器重建后的结果。

图9-6

9.7　小结

本章主要介绍了TensorFlow在手写数字数据集MNIST的图像识别中的应用。以MNIST数据集为例，讲解了最简单的Softmax回归、卷积神经网络（CNN）、循环神经网络（RNN）、自动编码器模型的构建和训练方式。同时应用tf.summary.FileWriter()可视化的API来配合TensorBoard的可视化展现。

[1]　http://yann.lecun.com/exdb/mnist/。

[2]　格式内容参考官方网站http://yann.lecun.com/exdb/mnist/ 。

[3]　参见百度百科 “MSB”：http://baike.baidu.com/link?url=r3DrWE4OHhsdq-u0u8D_pJ9_24Kmzl5jwrhdBB0C1azd7nxCz IfM9BkPVbPF_os2gIaJu0pLctzwG7RpxEKUKK。

[4]　本节代码参考https://github.com/nlintz/TensorFlow-Tutorials/blob/master/05_convolutional_net.py。

[5]　本节代码参考https://github.com/aymericdamien/TensorFlow-Examples/blob/master/examples/3_Neural Networks/recurrent_ network.py，并做了相应修改。

[6]　http://www.cs.toronto.edu/~graves/preprint.pdf

[7]　本节参考UFLDL的文章：http://ufldl.stanford.edu/wiki/index.php/Autoencoders_and_Sparsity。

[8]　本图参考http://ufldl.stanford.edu/wiki/index.php/Autoencoders_and_Sparsity。

[9]　主成分分析（principal components analysis，PCA）是一种分析、简化数据集的技术。经常用于减少数据集的维数，同时保持数据集中对方差贡献最大的特征。这是通过保留低阶主成分，忽略高阶主成分做到的。是最常用的线性降维方法。（出自维基百科“主成分分析”。）

[10]　本节代码主要参考https://github.com/aymericdamien/TensorFlow-Examples/blob/master/examples/3_NeuralNetworks/ autoencoder.py。

第10章　人脸识别

人脸识别是基于人的脸部特征信息进行身份识别的一种生物识别技术。用摄像机或摄像头采集含有人脸的图像或视频流，并自动在图像中检测和跟踪人脸，进而对检测　到的人脸进行一系列与脸部相关的技术处理，包括人脸检测、人脸关键点检测、人脸验证等。

在《麻省理工科技评论》（MIT Technology Review）发布的2017年全球十大突破性技术榜单中，支付宝的“刷脸支付”（Paying with Your Face）成功入围，并且其评论称，该技术提供了一种安全并且十分方便的支付方式，并且已经处于成熟期。

10.1　人脸识别简介

现在很多App都应用了人脸识别技术，让用户体验“刷脸认证”“眨眼支付”等。人脸识别具有很多天然的优势。

- 非强制性：采集方式不容易被察觉，被识别的人脸图像可以主动获取。

- 非接触性：用户不需要与设备接触。

- 并发性：能够同时进行多个人脸的检测、跟踪和识别。

在深度学习出现以前，人脸识别方法一般分为两个步骤：高维人工特征提取和降维。传统的人脸识别技术主要是基于可见光图像的人脸识别。但这种方式有很多缺陷，例如，同一个人在姿势、光照等发生变化时，会使识别率大大降低。目前，深度学习+大数据（海量的有标注人脸数据）成为人脸识别领域的主流技术路线。

采用神经网络的人脸识别技术，可以通过大量样本图像训练来得到识别模型，不需要人工选取特征，而是在样本的训练过程中自行学习。它的识别准确率极高，可以达到99%。

下面我们就介绍基于神经网络的人脸识别技术的识别流程。

10.2　人脸识别的技术流程

人脸识别系统一般主要包括4个组成部分，分别为人脸图像采集及检测、人脸图像预处理、人脸图像特征提取以及人脸图像匹配与识别。

10.2.1　人脸图像采集及检测

人脸识别的第一步就是人脸的图像采集及检测。人脸图像采集是指通过摄像镜头把人脸图像采集下来，如静态图像、动态图像、不同的位置、不同表情等。当用户在采集设备的拍摄范围内时，采集设备会自动搜索并拍摄。

人脸检测属于目标检测（object detection）的一部分，主要涉及以下两个方面：

（1）对要检测的目标对象进行概率统计，从而得到待检测对象的一些特征，建立起目标检测模型；

（2）用得到的模型来匹配输入的图像，如果有匹配则输出匹配的区域，没有就什么也不做。

人脸检测是人脸识别的预处理的一部分，即在图像中准确标定出人脸的位置和大小。人脸图像中包含的模式特征十分丰富，如直方图特征、颜色特征、模板特征、结构特征及哈尔特征（Haar-like feature）等。人脸检测就是把这其中有用的信息挑出来，并利用这些特征实现人脸检测。

在人脸检测算法中，有模板匹配模型、Adaboost模型等，其中Adaboost模型在速度与精度的综合性能上表现最好。该算法的特点就是训练慢，检测快，基本上可以达到视频流实时检测效果。

10.2.2　人脸图像预处理

人脸图像预处理是基于人脸检测的结果，对图像进行处理，为后面的特征提取服务。系统获取的人脸图像可能受到各种条件的限制和随机干扰，需要进行缩放、旋转、拉伸、光线补偿、灰度变换、直方图均衡化、规范化、几何校正、过滤以及锐化等图像预处理。

10.2.3　人脸图像特征提取

人脸图像特征提取就是将人脸图像信息数字化，将一张人脸图像转变为的一串数字（一般称为特征向量）。例如，对一张脸，找到它的眼睛左边、嘴唇右边、鼻子、下巴等位置，利用特征点间的欧氏距离、曲率和角度等提取出特征分量，最终把相关的特征连接成一个长的特征向量。

10.2.4　人脸图像匹配与识别

人脸图像匹配与识别就是把提取的人脸图像的特征数据与数据库中存储的人脸特征模板进行搜索匹配，根据相似程度对身份信息进行判断，设定一个阈值，当相似度超过这一阈值，则把匹配得到的结果输出。这一过程又分为两类：一类是确认，是一对一（1:1）进行图像比较，换句话说就是证明“你就是你”，一般用在金融的核实身份和信息安全领域；另一类是辨认，是一对多（1:N）进行图像匹配，也就是说在N个人中找到你，一般的N可以是一个视频流，只要人走进识别范围就完成识别工作，一般用在安防领域。

10.3　人脸识别的分类

在人脸识别领域，主要有以下4个细分方向。

- 人脸检测；

- 人脸关键点检测；

- 人脸验证；

- 人脸属性检测。

10.3.1　人脸检测

人脸检测是指检测并定位图片中的人脸，返回高精度的人脸框坐标。人脸检测是对人脸进行分析和处理的第一步。早期的检测过程称为“滑动窗口”，也就是选择图像中的某个矩形区域作为滑动窗口，在这个窗口中提取一些特征对这个图像区域进行描述，最后根据这些特征描述来判断这个窗口是不是人脸。人脸检测的过程就是不断遍历需要观察的窗口。例如，检测结果就如图10-1所示。

图10-1

10.3.2　人脸关键点检测

人脸关键点检测是指定位并返回人脸五官与轮廓的关键点坐标位置（如图10-2所示）。关键点包括人脸轮廓、眼睛、眉毛、嘴唇以及鼻子轮廓。现在某些人脸识别公司，如Face++能提供高精度的关键点，最多可达106点。无论是静态图片还是动态视频流，均能完美贴合人脸。

图10-2

人脸关键点定位技术主要有级联形回归（cascaded shape regression，CSR），目前人脸识别一般是基于DeepID网络结构。DeepID网络结构和卷积神经网络结构类似，主要区别在倒数第二层，DeepID网络结构有一个DeepID层，它与卷积层4和最大池化层3相连，由于卷积神经网络层数越高视野域越大，这种连接方式可以既考虑局部的特征，又考虑全局的特征，如图10-3所示。

图10-3[1]

10.3.3　人脸验证

人脸验证是指分析两张人脸属于同一个人的可能性大小。输入两张人脸，得到一个置信度分数和相应的阈值，以便评估相似度。图10-4是我调用Face++的人脸验证在线接口得到的结果。对比结果为：是同一个人的可能性很高。

图10-4

10.3.4　人脸属性检测

人脸属性检测包括人脸属性辨识和人脸情绪分析。例如，在https://www.betaface.com/wpa/ 可以进行人脸识别在线测试，可以给出人的年龄、是否有胡子、情绪（高兴、正常、生气、愤怒）、性别、是否带眼镜、肤色等。图10-5中给出的是我的一张照片的测试结果，因为化妆和灯光的原因，结果并不是很准确。

图10-5

人脸识别可以应用在很多方面。例如，美图秀秀之类的美颜应用，世纪佳缘等相亲应用中的查看与潜在配偶的“面相”相似度，以及支付领域的“刷脸支付”和安防领域的“人脸鉴权”。同时，国内的Face++和商汤科技等公司，都提供了人脸识别的相应SDK，供开发者调用。

下面和大家一起做两个练习：一个是关于人脸检测的，另一个是人脸的性别和年龄识别。

10.4　人脸检测[2]

本节示例是TensorFlow的人脸识别实现，参考了Florian Schroff、Dmitry Kalenichenko和James Philbin的论文《FaceNet：A Unified Embedding for Face Recognition and Clustering》[3]。本节的人脸检测的过程参考了https://github.com/davidsandberg/facenet/wiki/Validate-on-lfw 。

我们先把代码下载下来：

git clone --recursive https://github.com/davidsandberg/facenet.git

10.4.1　LFW数据集

这里采用的数据集是LFW（Labeled Faces in the Wild Home）数据集[4]。这个数据集是由美国马萨诸塞大学阿姆斯特分校计算机视觉实验室整理的。它包含13 233张图片，共5 749人，其中4 096人只有一张图片，1 680人的图片多于一张，每张图片尺寸是250×250。

我们将下载后的数据集解压放在$YOURHOME/facenet/datasets/lfw/raw，这里的$YOURHOME写成你自己的目录。下载后的数据集如下：

drwxr-xr-x@   3 jiaxuan  staff   102B 10  7  2007 AJ_Cook
drwxr-xr-x@   3 jiaxuan  staff   102B 10  7  2007 AJ_Lamas
drwxr-xr-x@   3 jiaxuan  staff   102B 10  7  2007 Aaron_Eckhart
drwxr-xr-x@   3 jiaxuan  staff   102B 10  7  2007 Aaron_Guiel
drwxr-xr-x@   3 jiaxuan  staff   102B 10  7  2007 Aaron_Patterson
drwxr-xr-x@   6 jiaxuan  staff   204B 10  7  2007 Aaron_Peirsol
drwxr-xr-x@   3 jiaxuan  staff   102B 10  7  2007 Aaron_Pena
drwxr-xr-x@   4 jiaxuan  staff   136B 10  7  2007 Aaron_Sorkin
drwxr-xr-x@   3 jiaxuan  staff   102B 10  7  2007 Aaron_Tippin

人脸图片位于上述每个人物名字的文件夹下，命名方式为“名字_xxxx.jpg”。例如，AJ_Cook文件夹中的图片的文件名为AJ_Cook_0001.jpg。

10.4.2　数据预处理

在图像识别中，数据预处理是很重要的一步。这里使用facenet源代码下的align模块去校准。校准代码见https://github.com/davidsandberg/facenet/blob/master/src/align/align_dataset_mtcnn.py。我们需要将检测所使用的数据集校准为和预训练模型所使用的数据集大小一致。

为了能正确运行校准程序，需要设置一下环境变量：

export PYTHONPATH=$YOURHOME/facenet/src

校准命令如下：

for N in {1..4}; do python src/align/align_dataset_mtcnn.py $YOURHOME/facenet
/datasets/lfw/raw $YOURHOME/facenet/datasets/lfw/lfw_mtcnnpy_160 --image_size 160
--margin 32 --random_order --gpu_memory_fraction 0.25 & done

这里采用GitHub上提供的预训练模型20170216-091149.zip[5]，采用的训练集是MS-Celeb-1M数据集[6]。MS-Celeb-1M是微软的一个非常大的人脸识别数据库，它是从名人榜上选择前100万的名人，然后通过搜索引擎采集每个名人大约100张人脸图片而形成的。这个预训练模型的准确率已经达到0.993±0.004。

我们将下载后的模型解压到$YOURHOME/facenet/models/facenet/20170216-091149，里面包含的文件如下：

model-20170216-091149.ckpt-250000.data-00000-of-00001
model-20170216-091149.ckpt-250000.index
model-20170216-091149.met

10.4.3　进行检测

进入facenet目录，用如下命令运行脚本：

python src/validate_on_lfw.py datasets/lfw/lfw_mtcnnpy_160 models

得到的结果如下：

Model directory: /media/data/DeepLearning/models/facenet/20170216-091149/
Metagraph file: model-20170216-091149.meta
Checkpoint file: model-20170216-091149.ckpt-250000
Runnning forward pass on LFW images
Accuracy: 0.993+-0.004
Validation rate: 0.97533+-0.01352 @ FAR=0.00100
Area Under Curve (AUC): 0.999
Equal Error Rate (EER): 0.008

为了和基准进行比较，这里采用facenet/data/pairs.txt文件，它是官方随机生成的数据，里面包含匹配和不匹配的人名和图片编号。匹配的人名和图片编号示例如下：

Abel_Pacheco  1  4

表示Abel_Pacheco的第1张和第4张是一个人。

不匹配的人名和图片编号示例如下：

Abdel_Madi_Shabneh  1  Dean_Barker  1

表示Abdel_Madi_Shabneh的第1张和Dean_Barker的第1张不是一个人。

下面我们看一下validate_on_lfw.py是如何检测人脸的。可以说分为4步，具体如下：

def main(args):

with tf.Graph().as_default():

with tf.Session() as sess:

# 1. 读入之前的pairs.txt文件
# 读入后如[['Abel_Pacheco', '1', '4']
# ['Akhmed_Zakayev', '1', '3'] ['Slobodan_Milosevic', '2', 'Sok_An', '1']]
pairs = lfw.read_pairs(os.path.expanduser(args.lfw_pairs))

# 获取文件路径和是否匹配的关系对
paths, actual_issame = lfw.get_paths(os.path.expanduser(args.lfw_dir),
pairs, args.lfw_file_ext)

# 2. 加载模型
print('Model directory: %s' % args.model_dir)
meta_file, ckpt_file = facenet.get_model_filenames(os.path.expanduser
(args.model_dir))

print('Metagraph file: %s' % meta_file)
print('Checkpoint file: %s' % ckpt_file)
facenet.load_model(args.model_dir, meta_file, ckpt_file)

# 获取输入、输出的张量
images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0")
embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0")
phase_train_placeholder = tf.get_default_graph().get_tensor_by_name
("phase_train:0")

image_size = images_placeholder.get_shape()[1]
embedding_size = embeddings.get_shape()[1]

# 3. 使用前向传播来验证
print('Runnning forward pass on LFW images')
batch_size = args.lfw_batch_size
nrof_images = len(paths)
nrof_batches = int(math.ceil(1.0*nrof_images / batch_size)) # 总共的批次数
emb_array = np.zeros((nrof_images, embedding_size))
for i in range(nrof_batches):
start_index = i*batch_size
end_index = min((i+1)*batch_size, nrof_images)
paths_batch = paths[start_index:end_index]
images = facenet.load_data(paths_batch, False, False, image_size)
feed_dict = { images_placeholder:images, phase_train_placeholder:False }
emb_array[start_index:end_index,:] = sess.run(embeddings,
feed_ dict=feed_dict)

# 4. 这里计算准确率和验证率，使用了十折交叉验证的方法
tpr, fpr, accuracy, val, val_std, far = lfw.evaluate(emb_array,
actual_issame, nrof_folds=args.lfw_nrof_folds)

print('Accuracy: %1.3f+-%1.3f' % (np.mean(accuracy), np.std(accuracy)))
print('Validation rate: %2.5f+-%2.5f @ FAR=%2.5f' % (val, val_std, far))

# 得到auc值
auc = metrics.auc(fpr, tpr)
print('Area Under Curve (AUC): %1.3f' % auc)
# 得到等错误率（eer）
eer = brentq(lambda x: 1. - x - interpolate.interp1d(fpr, tpr)(x), 0., 1.)
print('Equal Error Rate (EER): %1.3f' % eer)

这里采用十折交叉验证[7]（10-fold cross validation）的方法来测试算法的准确性。十折交叉验证是常用的精度测试方法，具体策略是：将数据集分成10份，轮流将其中9份做训练集，1份做测试集，10次的结果的均值作为对算法精度的估计，一般还需要进行多次10折交叉验证求均值，例如，10次10折交叉验证，再求其均值，作为对算法准确性的估计。

10.5　性别和年龄识别[8]

示例中数据集采用Adience数据集[9]。Adience数据集包含26 580张图片，总共含有2 284类，涉及的年龄范围有8个区段（0～2、4～6、8～13、15～20、25～32、38～43、48～53、60～），并且这个数据集含有噪声、姿势、光照等变化，尽可能真实地反映现实世界。

下载后的Adience数据集如下：

AdienceBenchmarkOfUnfilteredFacesForGenderAndAgeClassification
├── aligned # 经过剪裁和对齐的数据
├── faces  # 原始数据
├── fold_0_data.txt
├── fold_1_data.txt
├── fold_2_data.txt
├── fold_3_data.txt
├── fold_4_data.txt
├── fold_frontal_0_data.txt
├── fold_frontal_1_data.txt
├── fold_frontal_2_data.txt
├── fold_frontal_3_data.txt
└── fold_frontal_4_data.txt

fold_0_data.txt至fold_4_data.txt里面是全部数据的标记，fold_frontal_0_data.txt至fold_frontal_4_data.txt 里面是仅使用近似正面姿态的面部的标记，它们都包含表 10-1 所示的数据。其中user_id是用户的Flickr[10]帐户ID；original_image是图片的文件名；face_id是一个人的标识符，标记为同一个人；x、y、dx、dy给出一个围绕一个人脸的边框。tilt_ang是切斜角度，fiducial_yaw_angle是基准偏移角度，fiducial_score是基准分数。

表10-1

user_id

original_image

face_id

age

gender

x

y

dx

dy

tilt_ang

fiducial_yaw_angle

fiducial_score

113445054@N07

11763777465_11d01c34ce_o.jpg

1322

(25,32)

m

1102

296

357

357

-15

0

59

10.5.1　数据预处理

使用脚本[11]把数据处理成TFRecords的格式，处理后的文件列表如下：

-rw-r--r--   1 root  staff    55M  3 17 03:47 train-00000-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:48 train-00001-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:48 train-00002-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:48 train-00003-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:49 train-00004-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:47 train-00005-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:48 train-00006-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:48 train-00007-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:48 train-00008-of-00010
-rw-r--r--   1 root  staff    55M  3 17 03:49 train-00009-of-00010
-rw-r--r--   1 root  staff    30M  3 17 03:47 validation-00000-of-00002
-rw-r--r--   1 root  staff    30M  3 17 03:47 validation-00001-of-00002

下面看一下它是如何处理的。这里借助了https://github.com/GilLevi/AgeGenderDeep Learning/Folds文件夹。在这个文件夹中，已经对训练集和测试集进行了划分和标注，以“性别”为例，划分后的文件如下：

10069023@N00/landmark_aligned_face.1924.10335948845_0d22490234_o.jpg 0
114841417@N06/landmark_aligned_face.489.12077468164_8545fe9215_o.jpg 1
7464014@N04/landmark_aligned_face.961.10109081873_8060c8b0a5_o.jpg 1

也就是以“空格”划分了“图片名称路径”列表和“性别”。

我们借助这个文件下提供的gender_train.txt和gender_val.txt中的图片列表把原有的Adience数据集处理成TFRecords文件，其中图片处理为大小为256×256的JPEG编码的RGB图像。处理代码参见https://github.com/dpressel/rude-carnie/blob/master/preproc.py。

下面就来介绍一下关键代码。首先为每一个样例建立一个proto：

def _convert_to_example(filename, image_buffer, label, height, width):
"""
为每一个样例建立一个proto
参数如下：
filename: 文件名（如上面所述的文件名称路径列表）
image_buffer: string, JPEG编码的RGB图像
label: 标记的真实值
height: 目标高度 256
width: 目标宽度 256
"""

example = tf.train.Example(features=tf.train.Features(feature={
'image/class/label': _int64_feature(label),
'image/filename': _bytes_feature(os.path.basename(filename)),
'image/encoded': _bytes_feature(image_buffer),
'image/height': _int64_feature(height),
'image/width': _int64_feature(width)
}))
return example

随后，将tf.python_io.TFRecordWriter写入TFRecords文件，输出文件为output_file。如下：

writer = tf.python_io.TFRecordWriter(output_file)
example = _convert_to_example(filename, image_buffer, label, height, width)
writer.write(example.SerializeToString())
writer.close()

4.10.3节曾详细讲解过如何从文件中读取数据，并生成TFRecords文件，本节是一个实际应用。读者可以复习一下4.10.3节的内容。

这样，我们就把原始数据处理成了大小为256×256的JPEG编码的RGB图像，生成TFRecords文件，并且将TFRecords文件分为训练集和测试集。

10.5.2　构建模型

这里的年龄和性别的训练模型是参考Gil Levi和Tal Hassner的论文《Age and Gender Classification Using Convolutional Neural Networks》[12]构建的。年龄和性别的构建模型的代码在https://github.com/dpressel/rude-carnie/blob/master/model.py中。

为了方便生成卷积网络，这里直接使用TensorFlow的高级API——tensorflow.contrib.slim，tensorflow.contrib.slim是对常见网络和一些功能的封装，调用起来很方便，避免了自己写大量代码，让代码结构简洁。方法如下：

def levi_hassner(nlabels, images, pkeep, is_training):

weight_decay = 0.0005
weights_regularizer = tf.contrib.layers.l2_regularizer(weight_decay)
with tf.variable_scope("LeviHassner", "LeviHassner", [images]) as scope:

with tf.contrib.slim.arg_scope(
[convolution2d, fully_connected],
weights_regularizer=weights_regularizer,
biases_initializer=tf.constant_initializer(1.),
weights_initializer=tf.random_normal_initializer(stddev=0.005),
trainable=True):
with tf.contrib.slim.arg_scope(
[convolution2d],
weights_initializer=tf.random_normal_initializer(stddev=0.01)):

conv1 = convolution2d(images, 96, [7,7], [4, 4], padding='VALID',
biases_initializer=tf.constant_initializer(0.), scope='conv1')
pool1 = max_pool2d(conv1, 3, 2, padding='VALID', scope='pool1')
norm1 = tf.nn.local_response_normalization(pool1, 5, alpha=0.0001,
beta=0.75, name='norm1')
conv2 = convolution2d(norm1, 256, [5, 5], [1, 1], padding='SAME',
scope='conv2')
pool2 = max_pool2d(conv2, 3, 2, padding='VALID', scope='pool2')
norm2 = tf.nn.local_response_normalization(pool2, 5, alpha=0.0001,
beta=0.75, name='norm2')
conv3 = convolution2d(norm2, 384, [3, 3], [1, 1], biases_initializer=
tf.constant_initializer(0.), padding='SAME', scope='conv3')
pool3 = max_pool2d(conv3, 3, 2, padding='VALID', scope='pool3')
flat = tf.reshape(pool3, [-1, 384*6*6], name='reshape')
full1 = fully_connected(flat, 512, scope='full1')
drop1 = tf.nn.dropout(full1, pkeep, name='drop1')
full2 = fully_connected(drop1, 512, scope='full2')
drop2 = tf.nn.dropout(full2, pkeep, name='drop2')

with tf.variable_scope('output') as scope:

weights = tf.Variable(tf.random_normal([512, nlabels], mean=0.0, stddev=
0.01), name='weights')
biases = tf.Variable(tf.constant(0.0, shape=[nlabels], dtype=tf.float32),
name='biases')
output = tf.add(tf.matmul(drop2, weights), biases, name=scope.name)
return output

10.5.3　训练模型

定义好网络模型后，接下来就进行训练。训练模型代码在https://github.com/dpressel/rude-carnie/ blob/master/train.py中。

下面就以“性别”的训练为例，修改文件中的相应参数，如train_dir。训练过程的关键代码如下：

def main(argv=None):
with tf.Graph().as_default():

model_fn = select_model(FLAGS.model_type)
# 打开元数据文件md.json，这个文件是在预处理数据时生成的。找出nlabels和epoch的大小
input_file = os.path.join(FLAGS.train_dir, 'md.json')
print(input_file)
with open(input_file, 'r') as f:
md = json.load(f)

images, labels, _ = distorted_inputs(FLAGS.train_dir, FLAGS.batch_size,
FLAGS.image_size, FLAGS.num_
preprocess_threads)
logits = model_fn(md['nlabels'], images, 1-FLAGS.pdrop, True)
total_loss = loss(logits, labels)

train_op = optimizer(FLAGS.optim, FLAGS.eta, total_loss)
saver = tf.train.Saver(tf.global_variables())
summary_op = tf.summary.merge_all()

sess = tf.Session(config=tf.ConfigProto(
log_device_placement=FLAGS.log_device_placement))

tf.global_variables_initializer().run(session=sess)

# 本例可以输入预训练模型Inception V3，此处可以用来微调 Inception V3
if FLAGS.pre_model:
inception_variables = tf.get_collection(
tf.GraphKeys.VARIABLES, scope="Inception V3")
restorer = tf.train.Saver(inception_variables)
restorer.restore(sess, FLAGS.pre_model)

if FLAGS.pre_checkpoint_path:
if tf.gfile.Exists(FLAGS.pre_checkpoint_path) is True:
print('Trying to restore checkpoint from %s' % FLAGS.pre_checkpoint_ path)
restorer = tf.train.Saver()
tf.train.latest_checkpoint(FLAGS.pre_checkpoint_path)
print('%s: Pre-trained model restored from %s' %
(datetime.now(), FLAGS.pre_checkpoint_path))

# 将ckpt文件存储在run-{pid}目录中
run_dir = '%s/run-%d' % (FLAGS.train_dir, os.getpid())

checkpoint_path = '%s/%s' % (run_dir, FLAGS.checkpoint)
if tf.gfile.Exists(run_dir) is False:
print('Creating %s' % run_dir)
tf.gfile.MakeDirs(run_dir)

tf.train.write_graph(sess.graph_def, run_dir, 'model.pb', as_text=True)

tf.train.start_queue_runners(sess=sess)

summary_writer = tf.summary.FileWriter(run_dir, sess.graph)
steps_per_train_epoch = int(md['train_counts'] / FLAGS.batch_size)
num_steps = FLAGS.max_steps if FLAGS.epochs < 1 else FLAGS.epochs *
steps_per_train_epoch
print('Requested number of steps [%d]' % num_steps)

for step in xrange(num_steps):
start_time = time.time()
_, loss_value = sess.run([train_op, total_loss])
duration = time.time() - start_time

assert not np.isnan(loss_value), 'Model diverged with loss = NaN'

if step % 10 == 0:
num_examples_per_step = FLAGS.batch_size
examples_per_sec = num_examples_per_step / duration
sec_per_batch = float(duration)

format_str = ('%s: step %d, loss = %.3f (%.1f examples/sec; %.3f '
'sec/batch)')
print(format_str % (datetime.now(), step, loss_value,
examples_per_sec, sec_per_batch))

# 每10步记录一次摘要文件，保存一个检查点文件
if step % 10 == 0:
summary_str = sess.run(summary_op)
summary_writer.add_summary(summary_str, step)

if step % 10 == 0 or (step + 1) == num_steps:
saver.save(sess, checkpoint_path, global_step=step)

进行了100次迭代后生成的检查点文件位于run-{pid}（进程号）的目录中，如下：

➜  run-28892 $ tree -L 1
.
├──── checkpoint
├──── checkpoint-100.data-00000-of-00001
├──── checkpoint-100.index
├──── checkpoint-100.meta
├──── checkpoint-60.data-00000-of-00001
├──── checkpoint-60.index
├──── checkpoint-60.meta
├──── checkpoint-70.data-00000-of-00001
├──── checkpoint-70.index
├──── checkpoint-70.meta
├──── checkpoint-80.data-00000-of-00001
├──── checkpoint-80.index
├──── checkpoint-80.meta
├──── checkpoint-90.data-00000-of-00001
├──── checkpoint-90.index
├──── checkpoint-90.meta
├──── events.out.tfevents.1489700787.baidudeMacBook-Pro.local
└──── model.pb

10.5.4　验证模型

接着我用自己的一张图片（如图10-6所示）来看看我们训练的模型是否准确。源代码位于https://github.com/dpressel/rude-carnie/blob/master/guess.py 。

图10-6

关键代码如下：

def classify(sess, label_list, softmax_output, coder, images, image_file):

print('Running file %s' % image_file) # 输入的图片文件
image_batch = make_batch(image_file, coder, not FLAGS.single_look)
batch_results = sess.run(softmax_output, feed_dict={images:image_batch.eval()})
output = batch_results[0]
batch_sz = batch_results.shape[0]
for i in range(1, batch_sz):
output = output + batch_results[i]

output /= batch_sz
best = np.argmax(output) # 最可能的性别分类
best_choice = (label_list[best], output[best])
print('Guess @ 1 %s, prob = %.2f' % best_choice)

nlabels = len(label_list)
return best_choice

结果非常好，它输出了性别是“F”：

Guess @ 1 F, prob = 0.59

微软也推出了脸部图片识别性别和年龄的网站（http://how-old.net/ ），非常好玩。除了通过图片识别出年龄和性别，这个网站还可以根据问题搜索图片，结果相当准确。例如，我搜索了“士大夫”，会得到图10-7所示的结果。

图10-7

10.6　小结

本章主要讲解了TensorFlow在工业界（人脸识别方向）的应用。本章首先介绍了人脸识别的原理及技术流程，人脸识别的分类，接着结合最常见的案例，讲解了用TensorFlow实现人脸检测和从人脸识别性别和年龄。

本章介绍了人工智能在视觉领域的主要应用，人工智能主要的关注点在于能使计算机模拟人的感官，正如人的“眼耳鼻口舌”，人工智能在自然语言处理领域也有很大的突破。我们在下一章会介绍。

[1]　本图出自《Deep Learning Face Representation from Predicting 10,000 Classes》：http://mmlab.ie.cuhk. edu.hk/pdf/ YiSun_CVPR14.pdf。

[2]　本节代码参考https://github.com/davidsandberg/facenet。

[3]　https://arxiv.org/abs/1503.03832

[4]　LFW数据集的下载地址为http://vis-www.cs.umass.edu/lfw/。

[5]　https://drive.google.com/file/d/0B5MzpY9kBtDVTGZjcWkzT3pldDA/view

[6]　https://www.microsoft.com/en-us/research/project/ms-celeb-1m-challenge-recognizing-one-million-celebrities-real-world/

[7]　参考百度百科 “十折交叉验证”。

[8]　本节代码参考https://github.com/dpressel/rude-carnie。

[9]　http://www.openu.ac.il/home/hassner/Adience/data.html#agegender

[10]　https://www.flickr.com/

[11]　https://github.com/dpressel/rude-carnie/blob/master/preproc.py

[12]　http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.722.9654&rep=rep1&type=pdf

第11章　自然语言处理

自然语言处理[1]是计算机科学领域与人工智能领域中的另一个重要方向，其中很重要的一点就是语音识别（speech recognition）。语音识别要解决的问题是让计算机能够“听懂”人类的语音，将语音中包含的文字信息“提取”出来。

与语言相关的技术可以应用在很多地方。例如，日本的富国生命保险公司花费170万美元安装人工智能系统，把客户的语言转换为文本，并分析这些词是正面的还是负面的。这些自动化工作将帮助人类更快地处理保险业务。除此之外，现在的人工智能公司也在把智能客服作为重点的研究方向。

与图像识别不同，在自然语言处理中输入的往往是一段语音或者一段文字，输入数据的长短是不确定的，并且它与上下文有很密切的关系，所以常用的是循环神经网络（recurrent neural network，RNN）模型。

11.1　模型的选择

下面我们就来介绍使用不同输入和不同数据时，分别适用哪种模型以及如何应用。

在图11-1中，每一个矩形是一个向量，箭头则表示函数（如矩阵相乘）。最下面一行为输入向量，最上面一行为输出向量，中间一行是RNN的状态。

图11-1中从左到右分别表示以下几种情况。

（1）一对一：没有使用RNN，如Vanilla模型，从固定大小的输入得到固定大小输出（应用在图像分类）。

（2）一对多：以序列输出（应用在图片描述，输入一张图片输出一段文字序列，这种往往需要CNN和RNN相结合，也就是图像和语言相结合，详见第12章）。

图11-1

（3）多对一：以序列输入（应用在情感分析，输入一段文字，然后将它分类成积极或者消极情感，如淘宝下某件商品的评论分类），如使用LSTM。

（4）多对多：异步的序列输入和序列输出（应用在机器翻译，如一个RNN读取一条英文语句，然后将它以法语形式输出）。

（5）多对多：同步的序列输入和序列输出（应用在视频分类，对视频中每一帧打标记）。

我们注意到，在上述讲解中，因为中间RNN的状态的部分是固定的，可以多次使用，所以不需要对序列长度进行预先特定约束。更详细的讨论参见Andrej Karpathy的文章《The Unreasonable Effectiveness of Recurrent Neural Networks》[2]。

自然语言处理通常包括语音合成（将文字生成语音）、语音识别、声纹识别（声纹鉴权），以及它们的一些扩展应用，以及文本处理，如分词、情感分析、文本挖掘等。

11.2　英文数字语音识别[3]

本节我们就用语音识别的例子来说明TensorFlow在自然语言处理上的应用。这里我们将使用TensorFlow机器学习库，用20行Python代码创建一个超简单的语音识别器。

在这个例子中，我们构建一个LSTM循环神经网络，用TFLearn第三方库来训练一个英文数字口语数据集。

我们采用spoken numbers pcm数据集[4]，这个数据集中包含许多人阅读的0～9这几个数字的英文的音频。分为男声和女声，一段音频（wav文件）中只有一个数字对应的英文的声音。标识方法是{数字}_人名_xxx。如下：

9_Vicki_400.wav
9_Victoria_100.wav

下面我们就来训练一个简单的英文口语数字识别模型，在普通Mac上训练，使其能够在一分钟内达到98%的准确率。

11.2.1　定义输入数据并预处理数据

首先，需要将语音处理成能够读取的矩阵形式。这里面用到了梅尔频率倒谱系数（Mel frequency cepstral coefficents，MFCC）特征向量，MFCC是一种在自动语音和说话人识别中广泛使用的特征。

import tflearn
import speech_data
import tensorflow as tf

learning_rate = 0.0001
training_iters = 300000  # 迭代次数
batch_size = 64

width = 20  # MFCC特征
height = 80  # 最大发音长度
classes = 10  # 数字类别

batch = word_batch = speech_data.mfcc_batch_generator(batch_size) # 生成每一批MFCC语音
X, Y = next(batch)
trainX, trainY = X, Y
testX, testY = X, Y

对语言做分帧、取对数、逆矩阵等操作后，生成的MFCC就代表这个语音的特征。

11.2.2　定义网络模型

读者会发现，用tflearn真是很简洁，只用4行代码就定义好了一个LSTM模型：

net = tflearn.input_data([None, width, height])
net = tflearn.lstm(net, 128, dropout=0.8)
net = tflearn.fully_connected(net, classes, activation='softmax')
net = tflearn.regression(net, optimizer='adam', learning_rate=learning_rate,
loss='categorical_crossentropy')

11.2.3　训练模型

接下来训练模型，并把模型存储下来：

model = tflearn.DNN(net, tensorboard_verbose=0)
while 1: #training_iters
model.fit(trainX, trainY, n_epoch=10, validation_set=(testX, testY),
show_ metric=True,batch_size=batch_size)
_y=model.predict(X)
model.save("tflearn.lstm.model")

11.2.4　预测模型

任意输入一个语音文件，进行预测：

demo_file = "5_Vicki_260.wav"
demo=speech_data.load_wav_file(speech_data.path + demo_file)
result=model.predict([demo])
result=numpy.argmax(result)
print("predicted digit for %s : result = %d "%(demo_file,result))

结果输出如下：

predicted digit for 5_Vicki_260.wav : result = 5

结果很准确，确实这个音频的数字就是“5”。

语音识别可以用在智能输入法、会议的快速录入、语音控制系统、智能家居等领域。除了语音识别之外，如果对方能给出应答就更好了，这就是下面要讲的“智能聊天机器人”。

11.3　智能聊天机器人

现在很多公司都把未来方向压在了“自然语言的人机交互”上，而这其实就是“智能聊天机器人”。例如，苹果的Siri、微软的Cortana与小冰、Google Now、百度的度秘、亚马逊的蓝牙音箱Amazon Echo内置的语音助手Alexa、Facebook推出的语音助手M等。智能聊天机器人的商业价值有两个方面。

- 通过和用户的“语音机器人”的对话，将用户引导到对应的服务上面。

- 作为今后智能硬件和智能家居的嵌入式应用。例如，当用户和一个智能椅子进行对话时，用户说：“椅子，你调高一点，把靠背放平。”这个椅子的语音系统就可能搭载了上述大公司开发的智能聊天机器人系统。

智能聊天机器人的发展经历了3代不同的技术。

- 第一代是基于特征工程。有大量的逻辑判断，如if then; else then。

- 第二代是基于检索库。给定一个问题或者聊天，从检索库中找到与已有答案最匹配的答案。

- 第三代是基于深度学习。采用seq2seq+Attention模型，经过大量的训练，根据输入生成相应的输出。

下面我们就来看看基于深度学习的聊天机器人的seq2seq+Attention模型原理和构建方法。

11.3.1　原理

seq2seq模型是一个翻译模型，主要是把一个序列翻译成另一个序列。它的基本思想是用两个RNNLM，一个作为编码器，另一个作为解码器，组成RNN编码器-解码器。

在文本处理领域，我们常用编码器-解码器（encoder-decoder）框架，如图11-2所示。

图11-2

这是一种适合处理由一个上下文（context）生成一个目标（target）的通用处理模型。因此，对于一个句子对<X, Y>，当输入给定的句子X，通过编码器-解码器框架来生成目标句子Y。X和Y可以是不同语言，这就是机器翻译；X和Y可以是对话的问句和答句，这就是聊天机器人；X和Y可以是图片和这个图片的对应描述（这就是第12章要讲的看图说话）。

X由x_1、x_2等单词序列组成，Y也由y_1、y_2等单词序列组成。编码器对输入的X进行编码，生成中间语义编码C，然后解码器对中间语义编码C进行解码，在每个i时刻，结合已经生成的y_1, y_2,…, yi-1的历史信息生成Yi。但是，这个框架有一个缺点，就是生成的句子中每一个词采用的中间语义编码是相同的，都是C。因此，在句子比较短的时候，还能比较贴切，句子长时，就明显不合语义了。

在实际实现聊天系统的时候，一般编码器和解码器都采用RNN模型以及RNN模型的改进模型LSTM。当句子长度超过30以后，LSTM模型的效果会急剧下降，一般此时会引入Attention模型，对长句子来说能够明显提升系统效果。

Attention机制是认知心理学层面的一个概念，它是指当人在做一件事情的时候，会专注地做这件事而忽略周围的其他事。例如，人在专注地看这本书，会忽略旁边人说话的声音。这种机制应用在聊天机器人、机器翻译等领域，就把源句子中对生成句子重要的关键词的权重提高，产生出更准确的应答。

增加了Attention模型的编码器-解码器框架如图11-3所示。

现在的中间语义编码变成了不断变化的Ci，能够生产更准确的目标Yi。

图11-3

11.3.2　最佳实践

下面就来做一个智能聊天机器人。[5]

这里我们使用康奈尔大学的Corpus数据集[6]（Cornell Movie Dialogs Corpus），里面含有600多部电影的对白。对白示例如下：

L1045 +++$+++ u0 +++$+++ m0 +++$+++ BIANCA +++$+++ They do not!
L1044 +++$+++ u2 +++$+++ m0 +++$+++ CAMERON +++$+++ They do to!

我们首先关注如何处理聊天数据，一般步骤如下。

（1）先把数据集整理成“问”和“答”的文件，生成.enc（问句）和.dec（答句）文件，如下：

├──── test.dec # 测试集答句
├──── test.enc # 测试集问句
├──── train.dec # 训练集答句
├──── train.enc # 训练集问句

train.enc问句示例如下：

Gosh, if only we could find Kat a boyfriend...
C'esc ma tete. This is my head
How is our little Find the Wench A Date plan progressing?

（2）创建词汇表，然后把问句和答句转换成对应的id形式。词汇表的文件里面有2万个词汇，如下：

├──── vocab20000.dec # 答句的词汇表
└──── vocab20000.enc # 问句的词汇表

词汇表的内容如下：

_PAD
_GO
_EOS
_UNK
.
'
,
I
?
you
the
to
a
s
t
it

其中_GO、_EOS、_UNK、_PAD是在seq2seq模型中使用的特殊标记，用来填充标记对话：_GO 标记对话开始；_EOS标记对话结束；_UNK标记未出现在词汇表中的字符，用来替换稀有词汇；_PAD是用来填充序列，保证批次中的序列有相同的长度。

转换成的ids文件如下：

├──── test.enc.ids20000
├──── train.dec.ids20000
└──── train.enc.ids20000

问句和答句转换成的ids文件中，每一行是一个问句或答句，每一行中的每一个id代表问句或答句中对应位置的词，格式如下：

185 4 4 4 146 131 5 1144 39 313 53 102 1176 12042 4 2020 9 2691 9
792 15 4
7518 4
2993 49 88 109 54 13 765 466 252 4 4 4

（3）采用编码器-解码器框架进行训练。

1．定义训练参数

这里，我们将参数写到一个专门的文件seq2seq.ini中，如下：

[strings]
# 模式: train，test，serve
mode = train
train_enc = data/train.enc
train_dec = data/train.dec
test_enc = data/test.enc
test_dec = data/test.enc
# 模型文件和词汇表的存储路径
working_directory = working_dir/
[ints]
# 词汇表大小
enc_vocab_size = 20000
dec_vocab_size = 20000
# LSTM层数
num_layers = 3
# 每层大小，可以取值：128, 256, 512, 1024
layer_size = 256

max_train_data_size = 0
batch_size = 64
# 每多少次迭代存储一次模型
steps_per_checkpoint = 300
[floats]
learning_rate = 0.5 # 学习速率
learning_rate_decay_factor = 0.99 # 学习速率下降系数
max_gradient_norm = 5.0

2．定义网络模型

下面来定义seq2seq模型，该模型的代码在seq2seq_model.py中，这个代码基于TensorFlow 0.12版本，读者可以重新安装试验。定义一个seq2seq+Attention模型类[7]，里面主要包含3个函数：

（1）初始化模型的函数（init）；

（2）训练模型的函数（step）；

（3）获取下一批次训练数据的函数（get_batch）。

我们首先来看如何初始化模型，如下：

class Seq2SeqModel(object):

def __init__(self, source_vocab_size, target_vocab_size, buckets, size,
num_layers, max_gradient_norm, batch_size, learning_rate,
learning_rate_decay_factor, use_lstm=False,
num_samples=512, forward_only=False):
"""构建模型

参数：
source_vocab_size: 问句词汇表大小
target_vocab_size: 答句词汇表大小
buckets: (I,O), 其中I指定最大输入长度，O指定最大输出长度
size: 每一层的神经元数量
num_layers: 模型层数
max_gradient_norm: 梯度将被削减到最大的规范
batch_size: 批次大小。用于训练和预测的批次大小，可以不同
learning_rate: 学习速率
learning_rate_decay_factor: 调整学习速率
use_lstm: 使用LSTM 单元来代替GRU 单元
num_samples: 使用softmox的样本数
forward_only: 是否仅构建前向传播
"""
self.source_vocab_size = source_vocab_size
self.target_vocab_size = target_vocab_size
self.buckets = buckets
self.batch_size = batch_size
self.learning_rate = tf.Variable(float(learning_rate), trainable=False)
self.learning_rate_decay_op = self.learning_rate.assign(
self.learning_rate * learning_rate_decay_factor)
self.global_step = tf.Variable(0, trainable=False)

output_projection = None
softmax_loss_function = None
# 如果样本量比词汇表的量小，那么要用抽样的softmax
if num_samples > 0 and num_samples < self.target_vocab_size:
w = tf.get_variable("proj_w", [size, self.target_vocab_size])
w_t = tf.transpose(w)
b = tf.get_variable("proj_b", [self.target_vocab_size])
output_projection = (w, b)

def sampled_loss(inputs, labels):
labels = tf.reshape(labels, [-1, 1])
return tf.nn.sampled_softmax_loss(w_t, b, inputs, labels, num_samples,
self.target_vocab_size)
softmax_loss_function = sampled_loss

# 构建RNN
single_cell = tf.nn.rnn_cell.GRUCell(size)
if use_lstm:
single_cell = tf.nn.rnn_cell.BasicLSTMCell(size)
cell = single_cell
if num_layers > 1:
cell = tf.nn.rnn_cell.MultiRNNCell([single_cell] * num_layers)

# Attention模型
def seq2seq_f(encoder_inputs, decoder_inputs, do_decode):
return tf.nn.seq2seq.embedding_attention_seq2seq(
encoder_inputs, decoder_inputs, cell,
num_encoder_symbols=source_vocab_size,
num_decoder_symbols=target_vocab_size,
embedding_size=size,
output_projection=output_projection,
feed_previous=do_decode)

# 给模型填充数据
self.encoder_inputs = []
self.decoder_inputs = []
self.target_weights = []
for i in xrange(buckets[-1][0]):
self.encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in xrange(buckets[-1][1] + 1):
self.decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))
self.target_weights.append(tf.placeholder(tf.float32, shape=[None],
name="weight{0}".format(i)))

# targets的值是解码器偏移1位
targets = [self.decoder_inputs[i + 1]
for i in xrange(len(self.decoder_inputs) - 1)]

# 训练模型的输出
if forward_only:
self.outputs, self.losses = tf.nn.seq2seq.model_with_buckets(
self.encoder_inputs, self.decoder_inputs, targets,
self.target_weights, buckets, lambda x, y: seq2seq_f(x, y, True),
softmax_loss_function=softmax_loss_function)

if output_projection is not None:
for b in xrange(len(buckets)):
self.outputs[b] = [
tf.matmul(output, output_projection[0]) + output_projection[1]
for output in self.outputs[b]
]
else:
self.outputs, self.losses = tf.nn.seq2seq.model_with_buckets(
self.encoder_inputs, self.decoder_inputs, targets,
self.target_weights, buckets,
lambda x, y: seq2seq_f(x, y, False),
softmax_loss_function=softmax_loss_function)

# 训练模型时，更新梯度
params = tf.trainable_variables()
if not forward_only:
self.gradient_norms = []
self.updates = []
opt = tf.train.GradientDescentOptimizer(self.learning_rate)
for b in xrange(len(buckets)):
gradients = tf.gradients(self.losses[b], params)
clipped_gradients, norm = tf.clip_by_global_norm(gradients,
max_gradient_norm)
self.gradient_norms.append(norm)
self.updates.append(opt.apply_gradients(
zip(clipped_gradients, params), global_step=self.global_step))

self.saver = tf.train.Saver(tf.all_variables())

接着，定义运行模型的每一步：

def step(self, session, encoder_inputs, decoder_inputs, target_weights,
bucket_id, forward_only):
"""运行模型的每一步
参数:
session: tensorflow session
encoder_inputs: 问句向量序列
decoder_inputs: 答句向量序列
target_weights: target weights
bucket_id: 输入的bucket_id
forward_only: 是否只做前向传播

"""

encoder_size, decoder_size = self.buckets[bucket_id]
if len(encoder_inputs) != encoder_size:
raise ValueError("Encoder length must be equal to the one in bucket,"
" %d != %d." % (len(encoder_inputs), encoder_size))
if len(decoder_inputs) != decoder_size:
raise ValueError("Decoder length must be equal to the one in bucket,"
" %d != %d." % (len(decoder_inputs), decoder_size))
if len(target_weights) != decoder_size:
raise ValueError("Weights length must be equal to the one in bucket,"
" %d != %d." % (len(target_weights), decoder_size))

# 输入填充
input_feed = {}
for l in xrange(encoder_size):
input_feed[self.encoder_inputs[l].name] = encoder_inputs[l]
for l in xrange(decoder_size):
input_feed[self.decoder_inputs[l].name] = decoder_inputs[l]
input_feed[self.target_weights[l].name] = target_weights[l]

last_target = self.decoder_inputs[decoder_size].name
input_feed[last_target] = np.zeros([self.batch_size], dtype=np.int32)

# 输出填充： 与是否有后向传播有关
if not forward_only:
output_feed = [self.updates[bucket_id],
self.gradient_norms[bucket_id],
self.losses[bucket_id]]
else:
output_feed = [self.losses[bucket_id]]
for l in xrange(decoder_size):
output_feed.append(self.outputs[bucket_id][l])

outputs = session.run(output_feed, input_feed)
if not forward_only:
return outputs[1], outputs[2], None  #有后向传播下的输出： 梯度，损失值, None
else:
return None, outputs[0], outputs[1:]  # 仅有前向传播下的输出：None，损失值, outputs

接下来是get_batch函数，它的主要作用是为训练的每一步（step）产生一个批次的数据。

def get_batch(self, data, bucket_id):
"""
这个函数的作用是从指定的桶中获取一个批次的随机数据，在训练的每步（step）中使用
参数：
data：长度为（self.buckets）的元组，其中每个元素都包含用于创建批次的输入和输出数据对的列表
bucket_id：整数，从哪个bucket获取本批次
返回：
一个包含三项的元组（encoder_inputs，decode_inputs，target_weights）
"""

3．训练模型

修改seq2seq.ini文件中的mode值，当值为“train”时，可以运行execute.py进行训练。关键逻辑代码如下：

def train():
# 准备数据集
print("Preparing data in %s" % gConfig['working_directory'])
enc_train, dec_train, enc_dev, dec_dev, _, _ = data_utils.prepare_custom_data
(gConfig['working_directory'],gConfig['train_enc'],gConfig['train_dec'],
gConfig['test_enc'],gConfig['test_dec'],gConfig['enc_vocab_size'],
gConfig['dec_vocab_size'])

config = tf.ConfigProto()
config.gpu_options.allocator_type = 'BFC'

with tf.Session(config=config) as sess:
# 构建模型
print("Creating %d layers of %d units." % (gConfig['num_layers'], gConfig
['layer_size']))
model = create_model(sess, False)

# 把数据读入桶（bucket）中，并计算桶的大小
print ("Reading development and training data (limit: %d)."
% gConfig['max_train_data_size'])
dev_set = read_data(enc_dev, dec_dev)
train_set = read_data(enc_train, dec_train, gConfig['max_train_data_size'])
train_bucket_sizes = [len(train_set[b]) for b in xrange(len(_buckets))]
train_total_size = float(sum(train_bucket_sizes))
train_buckets_scale = [sum(train_bucket_sizes[:i + 1]) / train_total_size
for i in xrange(len(train_bucket_sizes))]

# 开始训练循环
step_time, loss = 0.0, 0.0
current_step = 0
previous_losses = []
while True:

# 随机生成一个0-1的数，在生成bucket_id中使用
random_number_01 = np.random.random_sample()
bucket_id = min([i for i in xrange(len(train_buckets_scale))
if train_buckets_scale[i] > random_number_01])

# 获取一个批次的数据，并进行一步训练
start_time = time.time()
encoder_inputs, decoder_inputs, target_weights = model.get_batch(
train_set, bucket_id)
_, step_loss, _ = model.step(sess, encoder_inputs, decoder_inputs,
target_weights, bucket_id, False)
step_time += (time.time() - start_time) / gConfig['steps_per_checkpoint']
loss += step_loss / gConfig['steps_per_checkpoint']
current_step += 1

# 保存检查点文件，打印统计数据
if current_step % gConfig['steps_per_checkpoint'] == 0:

perplexity = math.exp(loss) if loss < 300 else float('inf')
print ("global step %d learning rate %.4f step-time %.2f perplexity "
"%.2f" % (model.global_step.eval(), model.learning_rate.eval(),
step_time, perplexity))
# 如果损失值在最近3次内没有再降低，就减小学习率.
if len(previous_losses) > 2 and loss > max(previous_losses[-3:]):
sess.run(model.learning_rate_decay_op)
previous_losses.append(loss)
# 保存检查点文件，并把计数器和损失值归零
checkpoint_path = os.path.join(gConfig['working_directory'], "seq2seq.ckpt")
model.saver.save(sess, checkpoint_path, global_step=model.global_step)
step_time, loss = 0.0, 0.0

4．验证模型

修改seq2seq.ini文件中的mode值，当值为“test”时，可以运行execute.py进行测试。关键逻辑代码如下：

def decode():
with tf.Session() as sess:
# 建立模型，并定义超参数batch_size
model = create_model(sess, True)
model.batch_size = 1  # 这里一次只解码一个句子

# 加载词汇表文件
enc_vocab_path = os.path.join(gConfig['working_directory'],"vocab%d.enc" %
gConfig['enc_vocab_size'])
dec_vocab_path = os.path.join(gConfig['working_directory'],"vocab%d.dec" %
gConfig['dec_vocab_size'])

enc_vocab, _ = data_utils.initialize_vocabulary(enc_vocab_path)
_, rev_dec_vocab = data_utils.initialize_vocabulary(dec_vocab_path)

# 对标准输入的句子进行解码
sys.stdout.write("> ")
sys.stdout.flush()
sentence = sys.stdin.readline()
while sentence:
# 得到输入句子的token-ids
token_ids = data_utils.sentence_to_token_ids(tf.compat.as_bytes(sentence),
enc_vocab)
# 计算这个token_ids属于哪一个桶（bucket）
bucket_id = min([b for b in xrange(len(_buckets))
if _buckets[b][0] > len(token_ids)])
# 将句子送入到模型中
encoder_inputs, decoder_inputs, target_weights = model.get_batch(
{bucket_id: [(token_ids, [])]}, bucket_id)

_, _, output_logits = model.step(sess, encoder_inputs, decoder_inputs,
target_weights, bucket_id, True)
# 这是一个贪心的解码器，输出只是output_logits的argmaxes。
outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]
# 如果输出中有EOS符号，在EOS处切断
if data_utils.EOS_ID in outputs:
outputs = outputs[:outputs.index(data_utils.EOS_ID)]
# 打印出与输出句子对应的法语句子
print(" ".join([tf.compat.as_str(rev_dec_vocab[output]) for output in
outputs]))
print("> ", end="")
sys.stdout.flush()
sentence = sys.stdin.readline()

我们训练了417次后，生成了大小为209 MB的seq2seq.ckpt-417.data-00000-of-00001模型文件，开始进行测试，结果如下（行首有“>”的是我的输入，没有的是机器人的输出）：

> Hello
Hi .
> I love you.
Yeah .
> What
What?
> Sunny day
What?

如果再输入复杂的语句，机器人的表现就不尽如人意了。这只是个模型的简单演示实现，侧重于关注聊天机器人的原理。有兴趣的读者可以以优质的中文对话语料训练一个简易版的中文对话机器人。

前面介绍了基于文字的智能聊天机器人，如果再结合上语音识别，就产生了可以直接对话的机器人。它的系统架构[8]如图11-4所示。

图11-4

国内的智能聊天机器人有很多。例如，图灵机器人公司，着力于提高对话和语义准确度，提升中文语境下的智能程度；竹间智能科技也在研究有记忆、自学习的情感机器人，致力于机器人可以真正理解多模式多渠道的信息，并给予高度拟人化的回应，希望能以最理想的自然语言交流模式交流；此外，腾讯公司也有很多的社交对话数据，在聊天机器人方面可能会有更大的潜力。

目前国内微信应用应该是有最庞大的自然语言交流的语料库，那么微信如果在这方面发力，利用它庞大真实的数据，结合它的小程序希望成为所有服务的入口的目的，有很多事情可以想象。例如，未来的场景可能是，我们对着“微信助手”说，定个石锅拌饭，它就直接跳到外卖的小程序，很容易完成下单，用完即走。

11.4　小结

本章主要介绍了TensorFlow在自然语言处理中的应用，以英文数字语言识别和智能聊天机器人为例，讲解了如何处理数据集，构建网络，进行训练和预测。关于自然语言处理还有很多方面，如文本情感分析、信息检索与问答系统、机器翻译、语言合成等，读者也可以多进行尝试。

[1]　广义的自然语言处理包含语音处理及文本处理，狭义的单指理解和处理文本。本书均指广义的概念。

[2]　http://karpathy.github.io/2015/05/21/rnn-effectiveness/

[3]　本节代码参考https://github.com/pannous/tensorflow-speech-recognition/blob/master/speech2text-tflearn.py。

[4]　http://pannous.net/spoken_numbers.tar

[5]　本节代码参考https://github.com/suriyadeepan/easy_seq2seq 。需要依赖TensorFlow 0.12.1环境，请读者自行适配。

[6]　http://www.cs.cornell.edu/~cristian/Cornell_Movie-Dialogs_Corpus.html

[7]　参考论文《Grammar as a Foreign Language》：http://arxiv.org/abs/1412.7449 。

[8]　参见《中国人工智能学会通讯》2016年第6卷第1期。

第12章　图像与语音的结合

斯坦福大学人工智能实验室的李飞飞教授在2017年极客大会上曾经讲过，实现人工智能要有3个要素：语法（syntax）、语义（semantics）和推理（inference），如图12-1所示。

图12-1

语言和视觉是人工智能界非常关注的点，也就是说，在语言和视觉层面，通过语法（对语言来说是语法解析，对视觉来说是三维结构的解析）和语义（对语言来说是语义，对视觉来说是物体动作的含义）作为模型的输入训练数据，最终实现推理的能力，也就是把训练中学习到的能力应用到工作中去，从新的数据中推断出结论。[1]

12.1　看图说话模型

将图像和语言融合，就是“看图说话”。看图说话的目标是，输入一张图片，希望我们训练的看图说话模型能够根据图像给出描述图像内容的自然语言，讲出一个故事。这是一个很大的挑战，因为这需要在图像信息和文本信息这两种不同形式的信息之间进行“翻译”。

本节我们以TensorFlow的官方模型[2]为例，讲解如何训练一个看图说话的模型。这个模型要达到的目标是：我们给出一张图片，机器要给出“A person on a beach flying a kite”的描述，如图12-2所示。

图12-2

12.1.1　原理

看图说话模型采用的是编码器-解码器框架，先将图像编码成固定的中间矢量，然后解码成自然语言的描述。编码器-解码器框架如图12-3所示。这里编码器采用的是Inception V3图像识别模型，解码器采用的是LSTM网络。

图12-3

在图12-3中，{s_0, s_1,…, sn-1}是字幕的词，{wes_0, wes_1,…, wesn-1}是它们对应的词嵌入向量，LSTM的输出{p_1, p_2,…, pn}是由句子中的下一个词生成的概率分布。{log p_1(s_1), log p_2(s_2),…, log pn(sn)}是正确词在每一个步骤的对数似然，这几个值的总和取负数是我们模型的最小化目标。

12.1.2　最佳实践

这里使用微软的Microsoft COCO Caption数据集[3]，每张图片有5个标题文字。Microsoft COCO Caption数据集有两部分，即2014年版本和2015年版本。这里采用的是2014年版本[4]。

为了能够直接使用预训练的Inception V3模型，这里采用TensorFlow-Slim图像分类库[5]。

这里直接用现成的代码来训练模型，主要讲解构建模型的过程。训练的代码在tensormodels/im2txt/im2txt/train.py中，构建模型的代码在tensormodels/im2txt/im2txt/show_ and_tell_model.py中。

ShowAndTellModel的build函数中说明了构建模型的过程：

class ShowAndTellModel(object):
def build(self):

self.build_inputs() # 构建输入数据
self.build_image_embeddings()  # 采用Inception V3构建图像模型，并且输出图片嵌入向量
self.build_seq_embeddings() # 构建输入序列的embeddings
self.build_model() # 将CNN和LSTM串联起来，构建完整模型
self.setup_inception_initializer() # 载入Inception V3预训练模型
self.setup_global_step() # 记录全局的迭代次数

我们看看训练时是如何做的：

def main(unused_argv):
assert FLAGS.input_file_pattern, "--input_file_pattern is required"
assert FLAGS.train_dir, "--train_dir is required"

model_config = configuration.ModelConfig()
model_config.input_file_pattern = FLAGS.input_file_pattern
model_config.inception_checkpoint_file = FLAGS.inception_checkpoint_file
training_config = configuration.TrainingConfig()

# 创建训练结果的存储路径
train_dir = FLAGS.train_dir
if not tf.gfile.IsDirectory(train_dir):
tf.logging.info("Creating training directory: %s", train_dir)
tf.gfile.MakeDirs(train_dir)

# 建立TensorFlow数据流图
g = tf.Graph()
with g.as_default():
# 构建模型
model = show_and_tell_model.ShowAndTellModel(
model_config, mode="train", train_inception=FLAGS.train_inception)
model.build()

# 定义学习率
learning_rate_decay_fn = None
if FLAGS.train_inception:
learning_rate = tf.constant(training_config.train_inception_learning_rate)
else:
learning_rate = tf.constant(training_config.initial_learning_rate)
if training_config.learning_rate_decay_factor > 0:
num_batches_per_epoch = (training_config.num_examples_per_epoch /
model_config.batch_size)
decay_steps = int(num_batches_per_epoch *
training_config.num_epochs_per_decay)

def _learning_rate_decay_fn(learning_rate, global_step):
return tf.train.exponential_decay(
learning_rate,
global_step,
decay_steps=decay_steps,
decay_rate=training_config.learning_rate_decay_factor,
staircase=True)

learning_rate_decay_fn = _learning_rate_decay_fn

# 定义训练的操作
train_op = tf.contrib.layers.optimize_loss(
loss=model.total_loss,
global_step=model.global_step,
learning_rate=learning_rate,
optimizer=training_config.optimizer,
clip_gradients=training_config.clip_gradients,
learning_rate_decay_fn=learning_rate_decay_fn)

saver = tf.train.Saver(max_to_keep=training_config.max_checkpoints_to_keep)

# 训练
tf.contrib.slim.learning.train(
train_op,
train_dir,
log_every_n_steps=FLAGS.log_every_n_steps,
graph=g,
global_step=model.global_step,
number_of_steps=FLAGS.number_of_steps,
init_fn=model.init_fn,
saver=saver)

最后运行tensormodels/im2txt/im2txt/run_inference.py来预测生成的模型。输入一张图片，看看它给出的描述。输入图12-4所示的图片。

图12-4

结果如下：

Captions for image COCO_val2014_000000224477.jpg:
0) a man riding a wave on top of a surfboard . (p=0.040413)
1) a person riding a surf board on a wave (p=0.017452)
2) a man riding a wave on a surfboard in the ocean . (p=0.005743)

它给出了3个带概率分布的句子，意思表达得比较准确，而且语法也合乎自然逻辑。

我们也希望今后看图说话能够给出更长的描述。现在有些公司的自动化新闻写作机器人已经可以根据视频直播结合一些历史数据，写作关于奥运、体育等类型的稿件了。

12.2　小结

本章主要讲述了图像和自然语言相结合的应用——看图说话。这个模型结合了卷积神经网络Inception V3模型和循环神经网络LSTM模型。实现过程中，首先将图像转换成图像嵌入向量，然后用类似于词嵌入的方法来训练。关于图像和语言相结合的例子还有图像语义标注、图像语义分析等，读者可以自行实践。

[1]　参考论文《The Syntax, Semantics and Inference Mechanism in Natural Language》：http://www.aaai.org/Papers/Symposia/Fall/1996/FS-96-04/FS96-04-010.pdf。

[2]　https://github.com/tensorflow/models/tree/master/im2txt

[3]　http://mscoco.org/ 。Microsoft COCO Caption数据集是建立在Microsoft Common Objects in Context（COCO）数据集的工作基础上的。COCO含有超过30万张图片，200万个标记实体。Microsoft COCO Caption是对原COCO数据集中约33万张图片，使用亚马逊公司的Mechanical Turk服务，人工地为每张图片生成了至少5句标注，标注语句总共超过了150万句。

[4]　2014年版本中训练集有82 783张图片，验证集有40 504张图片和测试集有40 775 张图片。

[5]　https://github.com/tensorflow/models/tree/master/slim

第13章　生成式对抗网络

生成式对抗网络（generative adversarial network，GAN）是由谷歌公司在2014年提出的一个网络模型，主要灵感来自于二人博弈中的零和博弈，也是目前最火热的非监督深度学习的代表。“GAN之父”Ian J. Goodfellow也被公认为人工智能的顶级专家。

Yann Lecun在Quora上答题时曾说，他最激动的深度学习进展是生成式对抗网络。

13.1　生成式对抗网络的原理

生成式对抗网络包含一个生成模型（generative model，G）和一个判别模型（discriminative model，D）。本节内容参考了Ian J. Goodfellow、Jean Pouget-Abadie、Mehdi Mirza、Bing Xu、David Warde-Farley、Sherjil Ozair、Aaron Courville、Yoshua Bengio的论文《Generative Adversarial Networks》[1]。

生成式对抗网络的网络结构如图13-1所示。

图13-1

生成式对抗网络主要解决的问题是如何从训练样本中学习出新样本。生成模型就是负责训练出样本的分布，如果训练样本是图片就生成相似的图片，如果训练样本是文章句子就生成相似的文章句子。判别模型是一个二分类器，用来判断输入样本是真实数据还是训练生成的样本。

生成式对抗网络的优化是一个二元极小极大博弈（minimax two-player game）问题，它的目的是使生成模型的输出再输入给判别模型时，判别模型很难判断是真实数据还是虚假数据。训练好的生成模型，有能力把一个噪声向量转化成和训练集类似的样本。

具体到每一个生成式对抗网络的模型，有很多种结构，不过整体思路是不变的，如图13-2所示。

图13-2[2]

读者也可以设计自己的GAN网络架构。我们主要讲解辅助分类器生成式对抗网络（auxiliary classifier GAN，AC-GAN）的实现。

13.2　生成式对抗网络的应用

生成式对抗网络取得的成果有很多，目前在生成数字和生成人脸图像方面表现都非常好，目前也是深度学习研究的一个重要思路。图13-3给出的是训练好的生成式对抗网络的生成模型产生出来的一些样本。

图13-3[3]

13.3　生成式对抗网络的实现[4]

我们拿AC-GAN作为例子，看如何在MNIST数据集上实现生成式对抗网络。这个实现代码是以Augustus Odena、Christopher Olah和Jonathon Shlens的论文《Conditional Image Synthesis With Auxiliary Classifier GANs》[5]为基础的。

正如图13-2所示，我们通过噪声，让生成模型G生成虚假数据，然后和真实数据一起送到判别模型D当中，判别模型一方面输出这个数据的真/假，另一方面输出这个图片的分类（对于MNIST来说就是0～9）。我们现在来看具体的代码实现。

首先定义生成模型，目的是要生成一对（z, L）数据，其中z是噪声向量，L是（1, 28, 28）的图像空间，如下：

def build_generator(latent_size):
cnn = Sequential()

cnn.add(Dense(1024, input_dim=latent_size, activation='relu'))
cnn.add(Dense(128 * 7 * 7, activation='relu'))
cnn.add(Reshape((128, 7, 7)))

# 上采样，图像尺寸变为 14×14
cnn.add(UpSampling2D(size=(2, 2)))
cnn.add(Convolution2D(256, 5, 5, border_mode='same',
activation='relu', init='glorot_normal'))

# 上采样，图像尺寸变为 28×28
cnn.add(UpSampling2D(size=(2, 2)))
cnn.add(Convolution2D(128, 5, 5, border_mode='same',
activation='relu', init='glorot_normal'))

# 规约到1个通道
cnn.add(Convolution2D(1, 2, 2, border_mode='same',
activation='tanh', init='glorot_normal'))

# 生成模型的输入层，特征向量
latent = Input(shape=(latent_size, ))

# 生成模型的输入层，标记
image_class = Input(shape=(1,), dtype='int32')

cls = Flatten()(Embedding(10, latent_size, init='glorot_normal')(image_class))

h = merge([latent, cls], mode='mul')

fake_image = cnn(h) # 输出虚假图片

return Model(input=[latent, image_class], output=fake_image)

接下来我们定义判别模型，输入（1, 28, 28）的图片，输出有两个值，一个是判别模型认为这张图片是否是虚假图片，另一个是判别模型认为这张图片所属的分类。

def build_discriminator():
# 采用激活函数Leaky ReLU来替换标准的卷积神经网络中的激活函数
cnn = Sequential()

cnn.add(Convolution2D(32, 3, 3, border_mode='same', subsample=(2, 2),
input_shape=(1, 28, 28)))
cnn.add(LeakyReLU())
cnn.add(Dropout(0.3))

cnn.add(Convolution2D(64, 3, 3, border_mode='same', subsample=(1, 1)))
cnn.add(LeakyReLU())
cnn.add(Dropout(0.3))

cnn.add(Convolution2D(128, 3, 3, border_mode='same', subsample=(2, 2)))
cnn.add(LeakyReLU())
cnn.add(Dropout(0.3))

cnn.add(Convolution2D(256, 3, 3, border_mode='same', subsample=(1, 1)))
cnn.add(LeakyReLU())
cnn.add(Dropout(0.3))

cnn.add(Flatten())

image = Input(shape=(1, 28, 28))

features = cnn(image)

# 有两个输出
# 输出真假值，范围在0～1
fake = Dense(1, activation='sigmoid', name='generation')(features)
# 辅助分类器，输出图片的分类
aux = Dense(10, activation='softmax', name='auxiliary')(features)

return Model(input=image, output=[fake, aux])

下面开始写训练的过程，我们进行50轮（epoch），并把权重保存下来，每轮也把虚假数据生成的图片保存下来，便于观察虚假数据的演化过程，代码如下：

if __name__ == '__main__':

# 定义超参数
nb_epochs = 50
batch_size = 100
latent_size = 100

# 优化器的学习率
adam_lr = 0.0002
adam_beta_1 = 0.5

# 构建判别网络
discriminator = build_discriminator()
discriminator.compile(
optimizer=Adam(lr=adam_lr, beta_1=adam_beta_1),
loss=['binary_crossentropy', 'sparse_categorical_crossentropy']
)

# 构建生成式网络
generator = build_generator(latent_size)
generator.compile(optimizer=Adam(lr=adam_lr, beta_1=adam_beta_1),
loss='binary_crossentropy')

latent = Input(shape=(latent_size, ))
image_class = Input(shape=(1,), dtype='int32')

# 生成虚假图片
fake = generator([latent, image_class])

# 生成组合模型
discriminator.trainable = False
fake, aux = discriminator(fake)
combined = Model(input=[latent, image_class], output=[fake, aux])

combined.compile(optimizer=Adam(lr=adam_lr, beta_1=adam_beta_1),
loss=['binary_crossentropy', 'sparse_categorical_crossentropy']
)

# 将mnist数据转化为(..., 1, 28, 28) 维度，并且取值范围为[-1, 1]
(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train = (X_train.astype(np.float32) - 127.5) / 127.5
X_train = np.expand_dims(X_train, axis=1)

X_test = (X_test.astype(np.float32) - 127.5) / 127.5
X_test = np.expand_dims(X_test, axis=1)

nb_train, nb_test = X_train.shape[0], X_test.shape[0]

train_history = defaultdict(list)
test_history = defaultdict(list)

for epoch in range(nb_epochs):
print('Epoch {} of {}'.format(epoch + 1, nb_epochs))

nb_batches = int(X_train.shape[0] / batch_size)
progress_bar = Progbar(target=nb_batches)

epoch_gen_loss = []
epoch_disc_loss = []

for index in range(nb_batches):
progress_bar.update(index)
# 产生一个批次的噪声数据
noise = np.random.uniform(-1, 1, (batch_size, latent_size))

# 获取一个批次的真实数据
image_batch = X_train[index * batch_size:(index + 1) * batch_size]
label_batch = y_train[index * batch_size:(index + 1) * batch_size]

# 生成一些噪声标记
sampled_labels = np.random.randint(0, 10, batch_size)

# 产生一个批次的虚假图片
generated_images = generator.predict(
[noise, sampled_labels.reshape((-1, 1))], verbose=0)

X = np.concatenate((image_batch, generated_images))
y = np.array([1] * batch_size + [0] * batch_size)
aux_y = np.concatenate((label_batch, sampled_labels), axis=0)

epoch_disc_loss.append(discriminator.train_on_batch(X, [y, aux_y]))

# 产生两个批次的噪声和标记
noise = np.random.uniform(-1, 1, (2 * batch_size, latent_size))
sampled_labels = np.random.randint(0, 10, 2 * batch_size)

# 我们训练生成模型来欺骗判别模型，所以将输出的真/假都设为真
trick = np.ones(2 * batch_size)

epoch_gen_loss.append(combined.train_on_batch(
[noise, sampled_labels.reshape((-1, 1))], [trick, sampled_labels]))

print('\nTesting for epoch {}:'.format(epoch + 1))

# 评估测试集

# 产生一个新批次的噪声数据
noise = np.random.uniform(-1, 1, (nb_test, latent_size))

sampled_labels = np.random.randint(0, 10, nb_test)
generated_images = generator.predict(
[noise, sampled_labels.reshape((-1, 1))], verbose=False)

X = np.concatenate((X_test, generated_images))
y = np.array([1] * nb_test + [0] * nb_test)
aux_y = np.concatenate((y_test, sampled_labels), axis=0)

# 看看判别模型是否能判别
discriminator_test_loss = discriminator.evaluate(X, [y, aux_y], verbose=False)

discriminator_train_loss = np.mean(np.array(epoch_disc_loss), axis=0)

# 创建两个批次新的噪声数据
noise = np.random.uniform(-1, 1, (2 * nb_test, latent_size))
sampled_labels = np.random.randint(0, 10, 2 * nb_test)

trick = np.ones(2 * nb_test)

generator_test_loss = combined.evaluate(
[noise, sampled_labels.reshape((-1, 1))],
[trick, sampled_labels], verbose=False)

generator_train_loss = np.mean(np.array(epoch_gen_loss), axis=0)

# 把损失值等性能指标记录下来，并输出
train_history['generator'].append(generator_train_loss)
train_history['discriminator'].append(discriminator_train_loss)

test_history['generator'].append(generator_test_loss)
test_history['discriminator'].append(discriminator_test_loss)

print('{0:<22s} | {1:4s} | {2:15s} | {3:5s}'.format('component',
*discriminator.metrics_names))
print('-' * 65)

ROW_FMT = '{0:<22s} | {1:<4.2f} | {2:<15.2f} | {3:<5.2f}'
print(ROW_FMT.format('generator (train)', *train_history['generator'][-1]))
print(ROW_FMT.format('generator (test)', *test_history['generator'][-1]))
print(ROW_FMT.format('discriminator (train)',
*train_history ['discriminator'][-1]))
print(ROW_FMT.format('discriminator (test)',
*test_history ['discriminator'][-1]))

# 每一个eopch保存一次权重
generator.save_weights('params_generator_epoch_{0:03d}.hdf5'.format
(epoch), True)
discriminator.save_weights('params_discriminator_epoch_{0:03d}.hdf5'.
format(epoch), True)

# 生成一些可视化的虚假的数字来看演化过程
noise = np.random.uniform(-1, 1, (100, latent_size))

sampled_labels = np.array([[i] * 10 for i in range(10)]).reshape(-1, 1)

generated_images = generator.predict([noise, sampled_labels], verbose=0)

# 整理到一个方格中
img = (np.concatenate([r.reshape(-1, 28)
for r in np.split(generated_images, 10)
], axis=-1) * 127.5 + 127.5).astype(np.uint8)

Image.fromarray(img).save('plot_epoch_{0:03d}_generated.png'.format(epoch))

pickle.dump({'train': train_history, 'test': test_history},
open('acgan-history.pkl', 'wb'))

训练结束后，会创建以下3类文件。

- paramsdiscriminator_epoch{{epoch_number}}.hdf5：判别模型的权重参数。

- paramsgenerator_epoch{{epoch_number}}.hdf5：生成模型的权重参数。

- plotepoch{{epoch_number}}_generated.png：产生的一些虚假数据的图片。

在训练过程中，刚开始看到的图像是杂乱的。例如，第一轮结束后的图像如图13-4所示。

图13-4

在5轮后，可以看到一些差不多的图像，如图13-5所示。

图13-5

在训练15轮后，可以得到一些良好的图像，如图13-6所示。

图13-6

事实上，在15轮左右，训练损失就已经收敛到纳什均衡点（Nash equilibrium point）。

13.4　生成式对抗网络的改进

生成式对抗网络（generative adversarial network，GAN）在无监督学习上是非常有效的。但是，常规的生成式对抗网络的判别器使用的是Sigmoid交叉熵损失函数，这在学习过程中可能导致梯度消失。生成式对抗网络的一个改进是Wasserstein生成式对抗网络（Wasserstein generative adversarial network，WGAN），它使用Wasserstein距离度量而不是Jensen-Shannon散度（Jensen-Shannon divergence，JSD）。生成式对抗网络的另一个改进是使用最小二乘生成式对抗网络（least squares generative adversarial network，LSGAN），它的判别模型采用最小平方损失函数（least squares loss function）。关于更多它们之前的区别，读者可参考Sebastian Nowozin、Botond Cseke和Ryota Tomioka的论文《f-GAN：Training Generative Neural Samplers using Variational Divergence Minimization》[6]。

13.5　小结

本章讲解了2017年初最令学术界惊喜的进展——生成式对抗网络，包括它的原理、目前的应用以及多个变种，并且以AC-GAN作为例子使用TensorFlow进行了实现，在MNIST上训练可以看到不错的效果。生成式对抗网络的改进主要有两个方向：一是度量距离的算法，二是采用不同的损失函数。

[1]　https://arxiv.org/abs/1406.2661

[2]　本图出自Augustus Odena、Christopher Olah和Jonathon Shlens的论文《Conditional Image Synthesis with Auxiliary Classifier GANs》：https://arxiv.org/pdf/1610.09585.pdf。

[3]　本图出自Ian J. Goodfellow、Jean Pouget-Abadie、Mehdi Mirza、Bing Xu、David Warde-Farley、Sherjil Ozair、Aaron Courville和Yoshua Bengio的论文《Generative Adversarial Networks》：https://arxiv.org/abs/1406.2661 。

[4]　本节代码参考https://github.com/fchollet/keras/blob/master/examples/mnist_acgan.py。

[5]　https://arxiv.org/abs/1610.09585

[6]　https://arxiv.org/abs/1606.00709

第三篇　提高篇

终于学完基础篇和实战篇了，相信读者已经掌握了先阅读论文了解原理，然后复现模型、调整模型，最后用自己的数据训练模型的一整套方法。下面我们进入提高篇，提高篇主要着力于在训练集数据量极大、网络模型极大（也就是参数极多）的情况下，我们应该采用什么样的分布式架构设计，以及如何配合Kubernetes、Spark等工具来做训练。

除此之外，还介绍几个TensorFlow非常有潜力的新特性，如线性代数编译框架XLA、调试工具Debugger、TensorFlow在移动端上的应用、生产环境工具Serving、动态计算图工具Flod等。最后介绍一些机器学习的评测体系。

TensorFlow本身是一把非常好用的锤子，基础篇介绍了锤子的结构，实战篇我们学会了用锤子砸核桃的动作，如果砸的进度非常慢怎么办？用分布式并行，配合分布式集群的管理Kubernetes。如果核桃特别多（训练数据非常多）得无法存储怎么办？用Spark作为访问分布式文件系统上数据的方式。如何优化计算图本身呢？用XLA框架。砸得不对怎么调试呢？用Debugger。如果在手掌上（资源有限的移动端）砸核桃怎么办？有一天要登台表演砸核桃，是不是应该多准备几个砸的姿势（生产环境工具Serving）？砸得好不好如何来评价（机器学习的评测体系）呢？

相信通过对这些内容的学习，读者会对TensorFlow框架的各个方面有更深入的了解。让我们快开始吧，我曾用这篇中的知识面试过很多人，学懂这些知识可能有机会“秒杀”面试官。

第14章　分布式TensorFlow

TensorFlow的一大亮点就是支持分布式计算。分布式TensorFlow是由高性能的gRPC库作为底层技术来支持的。本章我们就来学习分布式TensorFlow所支持的架构和适用场景。

本章前3节主要参考了Martín Abadi、Ashish Agarwal和Paul Barham等的论文《TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systems》[1]。

14.1　分布式原理

首先，我们介绍TensorFlow的分布式原理。TensorFlow的分布式集群由多个服务器进程和客户端进程组成。TensorFlow有几种部署方式，如单机多卡和分布式（多机多卡），一般我们把多机多卡的部署称为TensorFlow的分布式。本节先介绍单机多卡和分布式的区别，随后介绍分布式的部署方式。

14.1.1　单机多卡和分布式

单机多卡是指单台服务器有多块GPU。假设一台机器上有4块GPU，单机多GPU的训练过程如下。

（1）在单机单GPU的训练中，数据是一个批次（batch）一个批次地训练的。在单机多GPU中，一次处理4个批次的数据，每个GPU处理一个批次的数据计算。

（2）变量，也就是参数，保存在CPU上，数据由CPU分发给4个GPU，在GPU上完成计算，得到每个批次要更新的梯度。

（3）在CPU上收集完4个GPU上要更新的梯度，计算一下平均梯度，然后更新参数。

（4）继续第2步和第3步，循环这个过程。

这个过程的处理速度取决于最慢的那个GPU的速度。如果4个GPU的处理速度差不多，处理速度就相当于单机单GPU的速度的4倍减去数据在CPU和GPU之间传输的开销。但是，这样进行并行训练，运算能力还是限制在单机上。

分布式是指训练在多个工作节点（worker）上。工作节点是指实现计算的一个单元，如果计算服务器是单卡，一般就是指这台服务器；如果计算服务器是多卡，还可以根据多个GPU划分多个工作节点。当数据量大到超过一台机器的处理能力时，必须使用分布式。

分布式TensorFlow底层的通信是gRPC（google remote procedure call）。gRPC是谷歌开源的一个高性能、跨语言的RPC框架。RPC协议，即远程过程调用协议，是指通过网络从远程计算机程序上请求服务。也就是说，假设你在本机上执行一段代码num=add(a, b)，它被调用后，得到一个返回结果，你感觉这段代码是在本机上执行的，但实际情况是，本机上的add方法是将参数打包发送给远程服务器，由远程服务器运行add方法，将返回的结果再打包返回给本机客户端的。

14.1.2　分布式部署方式

在分布式运行的情况下，我们需要有多个计算单元（工作节点），后端的服务器可以部署为单工作节点和多工作节点。

1．单工作节点部署

单工作节点部署是在每台服务器上运行一个工作节点，假设服务器有4个GPU，一个工作节点可以访问4块GPU卡，这时需要在代码中使用tf.device()指定运行操作的设备。

单工作节点部署的优势是在单机多个GPU间需要通信的情况下，效率更高。例如，可以实现RNN的模型并行。单工作节点部署的劣势是需要手动在代码中指定设备。

2．多工作节点部署

多工作节点是指一台服务器上可以运行多个工作节点。部署有以下两种方法。

（1）设置CUDA_VISIBLE_DEVICES环境变量，限制各个工作节点只可见一个GPU，启动进程时添加环境变量即可。例如，每个工作节点只能访问一个GPU，在代码中不需要额外指定[2]。示例如下：

CUDA_VISIBLE_DEVICES='' python ./distributed_supervisor.py --ps_hosts=
127.0.0.1:2222,127.0.0.1:2223 --worker_hosts=127.0.0.1:2224,127.0.0.1:2225
--job_name=ps --task_index=0
CUDA_VISIBLE_DEVICES='' python ./distributed_supervisor.py
--ps_hosts=127.0.0.1:2222,127.0.0.1:2223 --worker_hosts=127.0.0.1:2224,
127.0.0.1:2225 --job_name=ps --task_index=1
CUDA_VISIBLE_DEVICES='0' python ./distributed_supervisor.py
--ps_hosts=127.0.0.1:2222,127.0.0.1:2223 --worker_hosts=127.0.0.1:2224,
127.0.0.1:2225 --job_name=worker --task_index=0
CUDA_VISIBLE_DEVICES='1' python ./distributed_supervisor.py
--ps_hosts=127.0.0.1:2222,127.0.0.1:2223 --worker_hosts=127.0.0.1:2224,
127.0.0.1:2225 --job_name=worker --task_index=1

（2）使用tf.device()指定使用特定的GPU。

多工作节点部署的优势是代码简单，提高GPU使用率。多工作节点部署的劣势是工作节点间如果需要通信就不能利用本地GPU通信的优势，而且部署时需要部署多个工作节点。

14.2　分布式架构[3]

了解了分布式的原理之后，我们来看一下分布式架构的组成。分布式架构主要由客户端（client）和服务端（server）组成，服务端又包括主节点（master）和工作节点（worker）两者组成。我们需要关注客户端、主节点和工作节点这三者间的关系和它们的交互过程。

14.2.1　客户端、主节点和工作节点的关系

简单地来说，在TensorFlow中，客户端通过会话来联系主节点，实际的工作交由工作节点实现。每个工作节点占据一台设备（是TensorFlow具体计算的硬件抽象，即CPU或GPU）。在单机模式下，客户端、主节点和工作节点都在同一台服务器上；在分布式模式下，它们可以位于不同的服务器上。

图14-1展示了这三者之间的关系。

图14-1[4]

1．客户端

客户端用于建立TensorFlow计算图，并建立与集群进行交互的会话层。因此，代码中只要包含Session()就是客户端。一个客户端可以同时与多个服务端相连，同时一个服务端也可以与多个客户端相连。

2．服务端

服务端是一个运行了tf.train.Server实例的进程，是TensorFlow执行任务的集群（cluster）的一部分，并有主节点服务（Master service，也叫主节点）和工作节点服务（Worker service，也叫工作节点）之分。运行中由一个主节点进程和数个工作节点进程组成，主节点进程和工作节点进程之间通过接口通信。单机多卡和分布式都是这种结构，因此只需要更改它们之间通信的接口就可以实现单机多卡和分布式的切换。

3．主节点服务

主节点服务实现了tensorflow::Session接口，通过RPC服务程序来远程连接工作节点，与工作节点的服务进程中的工作任务进行通信。在TensorFlow服务端中，一般是task_index为0的作业（job）。

4．工作节点服务

工作节点服务实现了worker_service.proto接口，使用本地设备对部分图进行计算。在TensorFlow服务端中，所有工作节点都包含工作节点的服务逻辑。每个工作节点负责管理一个或者多个设备。工作节点也可以是本地不同端口的不同进程，或者多台服务器上的多个进程。14.6节中会用本地不同端口的两个进程来模拟两个工作节点的部署。

在运行TensorFlow的分布式时，我们首先需要创建一个TensorFlow集群（cluster）对象。集群是TensorFlow分布式执行的任务集，由一个或者多个作业（job）组成，而每个作业又由一个或多个具有相同目的的任务（task）组成。每个任务一般由一个工作进程来执行。由此可知，作业是任务的集合，集群是作业的集合。

在分布式机器学习框架（包括TensorFlow在内）中，一般把作业划分为参数作业（parameter job）和工作节点作业（worker job）。参数作业运行的服务器称为参数服务器（parameter server，PS），负责管理参数的存储和更新；工作节点作业负责管理无状态且主要从事计算的任务，如运行操作。

当模型越来越大，模型的参数越来越多，多到一台机器的性能不够完成对模型参数的更新的时候，就需要把参数分开放到不同的机器去存储和更新。参数服务器可以是由多台机器组成的集群，这就有点儿类似于分布式存储架构，涉及数据的同步、一致性等，参数可以存储为键值（key-value）的形式，或者理解为一个分布式的键值内存数据库，然后再加上一些参数更新的操作。[5]

因此，参数的存储和更新是在参数作业中进行的，模型的计算是在工作节点作业中进行的。TensorFlow的分布式实现作业间的数据传输，也就是参数作业到工作节点作业的前向传播，以及工作节点作业到参数作业的反向传播。

任务相当于是一个特定的TensorFlow服务器的独立进程，该进程属于特定的作业并在作业中拥有对应的序号。在大多数情况下，一个任务对应一个工作节点。

图14-2展示了我对集群内各种关系的理解。

图14-2

14.2.2　客户端、主节点和工作节点的交互过程

任务整体执行流程如图14-3所示，左边是单机多卡的交互，右边是分布式的交互。

图14-3[6]

14.3　分布式模式

知道了分布式的架构以及客户端、主节点和工作节点的关系，我们来看看分布式的具体运行模式是什么。在训练一个模型的过程中，有哪些部分可以分开，放在不同的机器上运行呢？这里就介绍两种模式：数据并行和模型并行。

14.3.1　数据并行

数据并行的原理很简单，如图14-4所示。其中CPU主要负责梯度平均和参数更新，而GPU1和GPU2主要负责训练模型副本（model replica）。这里称作“模型副本”是因为它们都是基于训练样例的子集训练得到的，模型之间具有一定的独立性。

图14-4①

具体的训练步骤如下。[7]

（1）在GPU1和GPU2上分别定义模型网络结构。

（2）对于单个GPU，分别从数据管道读取不同的数据块，然后进行前向传播，计算出损失，再计算当前变量的梯度。

（3）把所有GPU输出的梯度数据转移到CPU上，先进行梯度求平均操作，然后进行模型变量的更新。

（4）重复第1步至第3步，直到模型变量收敛为止。

数据并行的目的主要是提高SGD的效率。例如，假如每次SGD的mini-batch大小是1000个样本，那么如果切成10份，每份100个，然后将模型复制10份，就可以在10个模型上同时计算。

但是，因为10个模型的计算速度可能是不一致的，有的快有的慢，那么在CPU更新变量的时候，是应该等待这一mini-batch全部计算完成，然后求和取平均来更新呢，还是让一部分先计算完的就先更新，后计算完的将前面的覆盖呢？这就引出了同步更新和异步更新的问题。

14.3.2　同步更新和异步更新

分布式随机梯度下降法是指，模型参数可以分布式地存储在不同的参数服务器上，工作节点可以并行地训练数据并且能够和参数服务器通信获取模型参数。更新参数也分为同步和异步两种方式，即为异步随机梯度下降法（Async-SGD）和同步随机梯度下降法（Sync-SGD），如图14-5所示。

图14-5[8]

同步随机梯度下降法（也称同步更新、同步训练）的含义是在进行训练时，每个节点上的工作任务需要读入共享参数，执行并行的梯度计算，同步需要等待所有工作节点把局部的梯度算好，然后将所有共享参数进行合并、累加，再一次性更新到模型的参数；下一个批次中，所有工作节点拿到模型更新后的参数再进行训练。

这种方案的优势是，每个训练批次都考虑了所有工作节点的训练情况，损失下降比较稳定；劣势是，性能瓶颈在于最慢的工作节点上。在异构设备中，工作节点性能常常不同，这个劣势非常明显。

异步随机梯度下降法（也称异步更新、异步训练）的含义是每个工作节点上的任务独立计算局部梯度，并异步更新到模型的参数中，不需要执行协调和等待操作。

这种方案的优势优势是，性能不存在瓶颈；劣势是，每个工作节点计算的梯度值发送回参数服务器会有参数更新的冲突，一定程度上会影响算法的收敛速度，在损失下降过程中抖动较大。

同步更新和异步更新如何选择？有没有优化方式呢？

同步更新和异步更新的实现区别主要在于更新参数服务器的参数的策略。在数据量小，各个节点的计算能力比较均衡的情况下，推荐使用同步模式；在数据量很大，各个机器的计算性能参差不齐的情况下，推荐使用异步模式。具体使用哪一种还可以看实验结果，一般数据量足够大的情况下异步更新效果会更好。

为了解决有些工作节点计算比较慢的问题，可以使用多一些工作节点。例如，让工作节点总数变为n+n×5%，n为集群工作节点数。异步更新可以设定为在接受到n个工作节点的参数后，可以直接更新参数服务器上的模型参数，进入下一个批次的模型训练。计算比较慢的节点上训练出来的参数直接被丢弃。我们称这种方法为带备份的Sync-SGD（Sync-SGD with backup）。

在Jianmin Chen、Xinghao Pan、Rajat Monga、Samy Bengio和Rafal Jozefowicz的论文《Revisiting Distributed Synchronous SGD》中，作者曾基于ImageNet数据集用TensorFlow的Async-SGD、Sync-SGD、带备份的Sync-SGD模式做了1000种图片的分类训练。实验环境分别为50、100和200个工作节点，运行在NVIDIA K40 GPU上。图14-6展示的是50、100和200个工作节点，用上述3种模型的训练结果。

图14-6[9]

可以看出增加两个备份节点，带备份的Sync-SGD模型可以快速提升模型训练速度。从图14-6所示的50个工作节点的情况可以看出，Sync-SGD模型比Async-SGD模型大概提升了25%的训练速度，以及0.48%的准确度。

而且随着工作节点数目的增多，训练时间会极大缩短。如图14-7所示，采用Async-SGD算法，分别用25、50、100和200个节点，200个节点的训练时间是采用25个节点的训练时间的1/8，说明分布式的TensorFlow能够提升大规模训练的效率。

工作节点数

测试准确率（%）

时间（小时）

25

78.94

184.9

50

78.83

97.67

100

78.44

51.97

200

78.04

22.94

图14-7[10]

理解了参数更新的机制，还有很重要的一步，训练时数据是如何分发到工作节点上的呢？

同步更新与异步更新有图内模式（in-graph pattern）和图间模式（between-graph pattern）两种模式，是独立于图内（in-graph）和图间（between-graph）的概念，也就是说无论是图内还是图间都可以实现同步更新和异步更新，只是实现代码上会有些差异。

图内复制（in-graph replication）是指所有操作（operation）都在同一个图中，用一个客户端来生成图，然后把所有操作分配到集群的所有参数服务器和工作节点上。图内复制和单机多卡有点类似，是扩展到了多机多卡，但是数据分发还是在客户端一个节点上。这种方式的优势是计算节点只需要调用join()函数等待任务，客户端随时提交数据就可以训练。但劣势是训练数据的分发在一个节点上，要分发给不同的工作节点，严重影响并发训练速度。因此，在数据量很大的情况下，不推荐使用这种模式。

图间复制（between-graph replication）与图内复制对应，是指每一个工作节点创建一个图，训练的参数保存在参数服务器，数据不用分发，各个工作节点独立计算，计算完成后，把要更新的参数告诉参数服务器，参数服务器来更新参数。这种模式的优势是不需要数据分发，各个工作节点都会创建图和读取数据进行训练。劣势是工作节点既是图的创建者又是计算任务的执行者，如果某个工作节点宕机会影响集群的工作。这种模式是在数据量在TB级的时候，并发性能很高。因此，大数据相关的深度学习还是推荐使用图间模式。在14.6节的对MNIST进行分布式训练的例子中，我们就采用了这种方式。

14.3.3　模型并行

还可以对模型进行切分，让模型的不同部分执行在不同的设备上，这样一个批次样本可以在不同的设备上同时执行。为了充分利用同一台设备的计算能力，TensorFlow会尽量让相邻的计算在同一台设备上完成来节省网络开销。如图14-8所示，这是一个LSTM模型，展示一个批次的样本在设备1、设备2、设备3同时训练，分别执行模型的不同部分，分别训练出P1、P2、P3三个不同的参数。

图14-8[11]

本节的模型并行和数据并行，说明在TensorFlow中，计算可以分离，参数也可以分离。可以在每个设备上分配计算节点，然后让其对应的参数也在该设备上，让计算和参数放在一起。

14.4　分布式API[12]

创建集群的方法是为每一个任务（task）启动一个服务（工作节点服务或者主节点服务）。这些任务可以分布在不同的机器上，也可以同一台机器启动多个任务，使用不同的GPU来运行。每个任务会完成以下工作。

（1）创建一个tf.train.ClusterSpec，用于对集群中的所有任务进行描述，该描述内容对所有任务应该是相同的。

（2）创建一个tf.train.Server，用于创建一个服务，并运行相应作业上的计算任务。

TensorFlow的分布式开发API主要包括以下几个。

（1）tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})。创建TensorFlow集群描述信息，其中ps和worker为作业名称，ps_hosts和worker_hosts为该作业的任务所在节点的地址信息。表14-1中个给出了两个示例，tf.train.ClusterSpec的传入参数是作业和任务之间的关系映射，该映射关系中的任务是通过IP地址和端口号表示的。

表14-1

tf.train.ClusterSpec结构

可用任务

tf.train.ClusterSpec( { "local":
["localhost:2222", "localhost:2223" ] } )

/job:local/task:0
/job:local/task:1

tf.train.ClusterSpec ( {
"worker": [
"worker0.example.com:2222",
"worker1.example.com:2222",
"worker2.example.com:2222"
],
"ps": [
"ps0.example.com:2222",
"ps1.example.com:2222"
] } )

/job:worker/task:0
/job:worker/task:1
/job:worker/task:2
/job:ps/task:0
/job:ps/task:1

（2）tf.train.Server(cluster, job_name, task_index)。创建一个服务（主节点服务或者工作节点服务），用于运行相应作业上的计算任务，运行的任务在task_index指定的机器上启动。例如，在本地的2222和2223两个端口上配置不同的任务：

# 在任务0:
cluster = tf.train.ClusterSpec({"local": ["localhost:2222", "localhost:2223"]})
server = tf.train.Server(cluster, job_name="local", task_index=0)
# 在任务1:
cluster = tf.train.ClusterSpec({"local": ["localhost:2222", "localhost:2223"]})
server = tf.train.Server(cluster, job_name="local", task_index=1)

但是，这种做法还需要手动配置节点，无法实现动态的扩容或缩容。当集群规模比较大时，就需要使用自动化的管理节点、监控节点的工具，如集群管理工具Kubernetes（第17章中会讲解TensorFlow在Kubernetes上的部署和应用）。

（3）tf.device(device_name_or_function)。设定在指定的设备上执行张量运算，指定代码运行在CPU或GPU上。示例如下：

#指定在task0所在的机器上执行Tensor的操作运算
with tf.device("/job:ps/task:0"):
weights_1 = tf.Variable(...)
biases_1 = tf.Variable(...)

14.5　分布式训练代码框架

下面展示将如何创建一个TensorFlow服务器集群，以及如何在该集群中分布式计算一个数据流图。TensorFlow分布式集群的所有节点执行的代码都是相同的。分布式任务代码具有固定的结构：

# 第1步：命令行参数解析，获取集群的信息ps_hosts和worker_hosts，
# 以及当前节点的角色信息job_name和task_index。例如:
tf.app.flags.DEFINE_string("ps_hosts", "", "Comma-separated list of hostname:port pairs")
tf.app.flags.DEFINE_string("worker_hosts", "", "Comma-separated list of hostname:port
pairs")
tf.app.flags.DEFINE_string("job_name", "", "One of 'ps', 'worker'")
tf.app.flags.DEFINE_integer("task_index", 0, "Index of task within the job")
FLAGS = tf.app.flags.FLAGS
ps_hosts = FLAGS.ps_hosts.split(",")
worker_hosts = FLAGS.worker_hosts(",")

# 第2步：创建当前任务节点的服务器
cluster = tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})
server = tf.train.Server(cluster, job_name=FLAGS.job_name, task_index=FLAGS.task_index)

# 第3步：如果当前节点是参数服务器，则调用server.join()无休止等待；如果是工作节点，则执行第4步
if FLAGS.job_name == "ps":
server.join()

# 第4步：构建要训练的模型，构建计算图
elif FLAGS.job_name == "worker":
# build tensorflow graph model

# 第5步：创建tf.train.Supervisor来管理模型的训练过程
# 创建一个supervisor来监督训练过程
sv = tf.train.Supervisor(is_chief=(FLAGS.task_index == 0), logdir="/tmp/train_logs")
# supervisor负责会话初始化和从检查点恢复模型
sess = sv.prepare_or_wait_for_session(server.target)
# 开始循环，直到supervisor停止
while not sv.should_stop()
# 训练模型

现在我们就根据本节所讲的TensorFlow分布式训练代码框架，来看看如何对MNIST进行分布式训练。对于上述代码框架，我们要编写的主要有两部分：构建TensorFlow图模型的代码，以及每一步执行训练的代码。

14.6　分布式最佳实践[13]

本节采用图14-9所示的结构对MNIST数据集进行分布式训练。我们在本机上开设3个端口作为分布式工作节点的部署，2222端口为参数服务器，2223端口为工作节点0，2224端口为工作节点1。参数服务器执行参数更新任务，工作节点0和工作节点1执行图模型训练计算任务。

图14-9

我们先来运行代码，看看结果什么样。开启3个终端，分别运行：

python mnist_replica.py --job_name="ps" --task_index=0
python mnist_replica.py --job_name="worker" --task_index=0
python mnist_replica.py --job_name="worker" --task_index=1

在开启参数服务器（ps）的终端里，结果如下：

job name = ps
task index = 0
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job ps -> {0 -> localhost:2222}
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job worker -> {0 -> localhost:2223, 1 -> localhost:2224}
I tensorflow/core/distributed_runtime/rpc/grpc_server_lib.cc:217] Started server
with target: grpc://localhost:2222

然后该进程挂起，等待工作节点服务中的进程开始训练。

我们一共进行200次迭代，工作节点1执行了169次迭代，计算输出如下：

job name = worker
task index = 0
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job ps -> {0 -> localhost:2222}
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job worker -> {0 -> localhost:2223, 1 -> localhost:2224}
I tensorflow/core/distributed_runtime/rpc/grpc_server_lib.cc:217] Started server
with target: grpc://localhost:2223
Worker 0: Initializing session...
I tensorflow/core/distributed_runtime/master_session.cc:994] Start master session
0d791a02977e5701 with config:
device_filters: "/job:ps"
device_filters: "/job:worker/task:0"
allow_soft_placement: true

Worker 0: Session initialization complete.
Training begins @ 1483516057.489495
1483516057.518419: Worker 0: training step 1 done (global step: 0)
1483516057.541053: Worker 0: training step 2 done (global step: 1)
1483516057.569677: Worker 0: training step 3 done (global step: 2)
1483516057.584578: Worker 0: training step 4 done (global step: 3)
1483516057.646970: Worker 0: training step 5 done (global step: 4)
# ……中间略去
1483516059.286596: Worker 0: training step 166 done (global step: 197)
1483516059.291600: Worker 0: training step 167 done (global step: 198)
1483516059.297347: Worker 0: training step 168 done (global step: 199)
1483516059.303738: Worker 0: training step 169 done (global step: 200)
Training ends @ 1483516059.303808
Training elapsed time: 1.814313 s
After 200 training step(s), validation cross entropy = 1235.56

工作节点2执行了34次迭代，结果如下：

job name = worker
task index = 1
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job ps -> {0 -> localhost:2222}
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job worker -> {0 -> localhost:2223, 1 -> localhost:2224}
I tensorflow/core/distributed_runtime/rpc/grpc_server_lib.cc:217] Started server
with target: grpc://localhost:2224
Worker 1: Waiting for session to be initialized...
I tensorflow/core/distributed_runtime/master_session.cc:994] Start master session
92e671f3dd1ffd05 with config:
device_filters: "/job:ps"
device_filters: "/job:worker/task:1"
allow_soft_placement: true

Worker 1: Session initialization complete.
Training begins @ 1483516058.803010
1483516058.832164: Worker 1: training step 1 done (global step: 121)
1483516058.844464: Worker 1: training step 2 done (global step: 123)
1483516058.860988: Worker 1: training step 3 done (global step: 126)
1483516058.873543: Worker 1: training step 4 done (global step: 128)
1483516058.884758: Worker 1: training step 5 done (global step: 130)
# ……中间略去
1483516059.152332: Worker 1: training step 30 done (global step: 176)
1483516059.167606: Worker 1: training step 31 done (global step: 178)
1483516059.177215: Worker 1: training step 32 done (global step: 180)
1483516059.301384: Worker 1: training step 33 done (global step: 182)
1483516059.309557: Worker 1: training step 34 done (global step: 202)
Training ends @ 1483516059.309638
Training elapsed time: 0.506628 s
After 200 training step(s), validation cross entropy = 1235.56

下面我们一起来看一下如何用代码实现在MNIST上进行分布式训练。

首先，定义一些常量，用于构建数据流图：

flags = tf.app.flags
flags.DEFINE_string("data_dir", "/tmp/mnist-data", "Directory for storing mnist data")
# 只下载数据，不做其他操作
flags.DEFINE_boolean("download_only", False,
"Only perform downloading of data; Do not proceed to "
"session preparation, model definition or training")
# task_index从0开始。0代表用来初始化变量的第一个任务
flags.DEFINE_integer("task_index", None,
"Worker task index, should be >= 0. task_index=0 is "
"the master worker task the performs the variable "
"initialization ")
# 每台机器的GPU个数，这里在前述Mac笔记本上运行，因此为0
flags.DEFINE_integer("num_gpus", 0,
"Total number of gpus for each machine."
"If you don't use GPU, please set it to '0'")
# 在同步训练模式下，设置收集的工作节点的数量。默认就是工作节点的总数
flags.DEFINE_integer("replicas_to_aggregate", None,
"Number of replicas to aggregate before parameter update"
"is applied (For sync_replicas mode only; default: "
"num_workers)")
flags.DEFINE_integer("hidden_units", 100,
"Number of units in the hidden layer of the NN")
# 训练的次数
flags.DEFINE_integer("train_steps", 200,
"Number of (global) training steps to perform")
flags.DEFINE_integer("batch_size", 100, "Training batch size")
flags.DEFINE_float("learning_rate", 0.01, "Learning rate")
# 使用同步训练/异步训练
flags.DEFINE_boolean("sync_replicas", False,
"Use the sync_replicas (synchronized replicas) mode, "
"wherein the parameter updates from workers are aggregated "
"before applied to avoid stale gradients")
# 如果服务器已经存在，采用gRPC协议通信；如果不存在，采用进程间通信
flags.DEFINE_boolean(
"existing_servers", False, "Whether servers already exists. If True, "
"will use the worker hosts via their GRPC URLs (one client process "
"per worker host). Otherwise, will create an in-process TensorFlow "
"server.")
# 参数服务器主机
flags.DEFINE_string("ps_hosts","localhost:2222",
"Comma-separated list of hostname:port pairs")
# 工作节点主机
flags.DEFINE_string("worker_hosts", "localhost:2223,localhost:2224",
"Comma-separated list of hostname:port pairs")
# 本作业是工作节点还是参数服务器
flags.DEFINE_string("job_name", None,"job name: worker or ps")

FLAGS = flags.FLAGS

IMAGE_PIXELS = 28

下面我们就从命令行参数中读取参数服务器和工作节点的主机信息，用tf.train.ClusterSpec来创建TensorFlow的集群描述。

# 读取集群的描述信息
ps_spec = FLAGS.ps_hosts.split(",")
worker_spec = FLAGS.worker_hosts.split(",")

# 创建TensorFlow集群描述对象
cluster = tf.train.ClusterSpec({
"ps": ps_spec,
"worker": worker_spec})

为本地执行的任务创建TensorFlow的Server对象。

if not FLAGS.existing_servers:
# 创建本地Sever对象，从tf.train.Server这个定义开始，每个节点开始不同
# 根据执行的命令的参数（作业名字）不同，决定了这个任务是哪个任务
# 如果作业名字是ps，进程就加入这里，作为参数更新的服务，等待其他工作节点给它提交参数更新的数据
# 如果作业名字是worker，就执行后面的计算任务
server = tf.train.Server(cluster, job_name=FLAGS.job_name,
task_index=FLAGS.task_index)
# 如果是参数服务器，直接启动即可。这时,进程就会阻塞在这里
# 下面的tf.train.replica_device_setter代码会将参数指定给ps_server保管
if FLAGS.job_name == "ps":
server.join()

下面需要处理工作节点：

# 找出worker的主节点，即task_index为0的点
is_chief = (FLAGS.task_index == 0)
# 如果使用gpu
if FLAGS.num_gpus > 0:
if FLAGS.num_gpus < num_workers:
raise ValueError("number of gpus is less than number of workers")
gpu = (FLAGS.task_index % FLAGS.num_gpus)
# 分配worker到指定的gpu上运行
worker_device = "/job:worker/task:%d/gpu:%d" % (FLAGS.task_index, gpu)
# 如果使用cpu：
elif FLAGS.num_gpus == 0:
# 把cpu分配给worker
cpu = 0
worker_device = "/job:worker/task:%d/cpu:%d" % (FLAGS.task_index, cpu)

我们使用tf.train.replica_device_setter将涉及变量的操作分配到参数服务器上，并使用CPU；将涉及非变量的操作分配到工作节点上，使用上一步worker_device的值。

# 在这个with语句之下定义的参数，会自动分配到参数服务器上去定义
如果有多个参数服务器，就轮流循环分配
with tf.device(
tf.train.replica_device_setter(
worker_device=worker_device,
ps_device="/job:ps/cpu:0",
cluster=cluster)):
# 定义全局步长，默认值为0
global_step = tf.Variable(0, name="global_step", trainable=False)

# 定义隐藏层参数变量，这里是全连接神经网络隐藏层
hid_w = tf.Variable(
tf.truncated_normal(
[IMAGE_PIXELS * IMAGE_PIXELS, FLAGS.hidden_units],
stddev=1.0 / IMAGE_PIXELS),
name="hid_w")
hid_b = tf.Variable(tf.zeros([FLAGS.hidden_units]), name="hid_b")

# 定义Softmax回归层的参数变量
sm_w = tf.Variable(
tf.truncated_normal(
[FLAGS.hidden_units, 10],
stddev=1.0 / math.sqrt(FLAGS.hidden_units)),
name="sm_w")
sm_b = tf.Variable(tf.zeros([10]), name="sm_b")

# 定义模型输入数据变量
x = tf.placeholder(tf.float32, [None, IMAGE_PIXELS * IMAGE_PIXELS])
y_ = tf.placeholder(tf.float32, [None, 10])

# 构建隐藏层
hid_lin = tf.nn.xw_plus_b(x, hid_w, hid_b)
hid = tf.nn.relu(hid_lin)

# 构建损失函数和优化器
y = tf.nn.softmax(tf.nn.xw_plus_b(hid, sm_w, sm_b))
cross_entropy = -tf.reduce_sum(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))

# 异步训练模式：自己计算完梯度就去更新参数，不同副本之间不会去协调进度
opt = tf.train.AdamOptimizer(FLAGS.learning_rate)

# 同步训练模式
if FLAGS.sync_replicas:
if FLAGS.replicas_to_aggregate is None:
replicas_to_aggregate = num_workers
else:
replicas_to_aggregate = FLAGS.replicas_to_aggregate
# 使用SyncReplicasOptimizer作为优化器，并且是在图间复制情况下
# 在图内复制情况下将所有的梯度平均就可以了
opt = tf.train.SyncReplicasOptimizer(
opt,
replicas_to_aggregate=replicas_to_aggregate,
total_num_replicas=num_workers,
name="mnist_sync_replicas")

train_step = opt.minimize(cross_entropy, global_step=global_step)

if FLAGS.sync_replicas:
local_init_op = opt.local_step_init_op
if is_chief:
# 所有的进行计算的工作节点里的一个主工作节点（chief）
# 这个主节点负责初始化参数、模型的保存、概要的保存等
local_init_op = opt.chief_init_op

ready_for_local_init_op = opt.ready_for_local_init_op

# 同步训练模式所需的初始令牌和主队列
chief_queue_runner = opt.get_chief_queue_runner()
sync_init_op = opt.get_init_tokens_op()

init_op = tf.global_variables_initializer()
train_dir = tempfile.mkdtemp()

if FLAGS.sync_replicas:
# 创建一个监管程序，用于统计训练模型过程中的信息
# logdir是保存和加载模型的路径
# 启动就会去这个logdir目录看是否有检查点文件，有的话就自动加载
# 没有就用init_op指定的初始化参数
# 主工作节点（chief）负责模型参数初始化等工作
# 在这个过程中，其他工作节点等待主节点完成初始化工作，初始化完成后，一起开始训练数据
# global_step的值是所有计算节点共享的
# 在执行损失函数最小值的时候会自动加1，通过global_step能知道所有计算节点一共计算了多少步
sv = tf.train.Supervisor(
is_chief=is_chief,
logdir=train_dir,
init_op=init_op,
local_init_op=local_init_op,
ready_for_local_init_op=ready_for_local_init_op,
recovery_wait_secs=1,
global_step=global_step)
else:
sv = tf.train.Supervisor(
is_chief=is_chief,
logdir=train_dir,
init_op=init_op,
recovery_wait_secs=1,
global_step=global_step)
# 在创建会话时，设置属性allow_soft_placement为True
# 所有的操作会默认使用其被指定的设备，如GPU
# 如果该操作函数没有GPU实现时，会自动使用CPU设备
sess_config = tf.ConfigProto(
allow_soft_placement=True,
log_device_placement=False,
device_filters=["/job:ps", "/job:worker/task:%d" % FLAGS.task_index])

# 主工作节点（chief），即task_index为0的节点将会初始化会话
# 其余的工作节点会等待会话被初始化后进行计算
if is_chief:
print("Worker %d: Initializing session..." % FLAGS.task_index)
else:
print("Worker %d: Waiting for session to be initialized..." %
FLAGS.task_index)

if FLAGS.existing_servers:
server_grpc_url = "grpc://" + worker_spec[FLAGS.task_index]
print("Using existing server at: %s" % server_grpc_url)

# 创建TensorFlow会话对象，用于执行TensorFlow图计算
# prepare_or_wait_for_session需要参数初始化完成且主节点也准备好后，才开始训练
sess = sv.prepare_or_wait_for_session(server_grpc_url, config=sess_config)
else:
sess = sv.prepare_or_wait_for_session(server.target, config=sess_config)

print("Worker %d: Session initialization complete." % FLAGS.task_index)

if FLAGS.sync_replicas and is_chief:
sess.run(sync_init_op)
sv.start_queue_runners(sess, [chief_queue_runner])

# 执行分布式模型训练
time_begin = time.time()
print("Training begins @ %f" % time_begin)

local_step = 0
while True:
# 读入MNIST的训练数据，默认每批次为100张图片
batch_xs, batch_ys = mnist.train.next_batch(FLAGS.batch_size)
train_feed = {x: batch_xs, y_: batch_ys}

_, step = sess.run([train_step, global_step], feed_dict=train_feed)
local_step += 1

now = time.time()
print("%f: Worker %d: training step %d done (global step: %d)" %
(now, FLAGS.task_index, local_step, step))

if step >= FLAGS.train_steps:
break

time_end = time.time()
print("Training ends @ %f" % time_end)
training_time = time_end - time_begin
print("Training elapsed time: %f s" % training_time)

# 读入MNIST的验证数据，计算验证的交叉熵
val_feed = {x: mnist.validation.images, y_: mnist.validation.labels}
val_xent = sess.run(cross_entropy, feed_dict=val_feed)
print("After %d training step(s), validation cross entropy = %g" %
(FLAGS.train_steps, val_xent))

14.7　小结

本章主要介绍了TensorFlow的分布式原理、分布式架构、分布式模式等知识，以及TensorFlow实现分布式所需要的API，整理了TensorFlow分布式训练代码的框架结构，最后以MNIST为例讲解了实现分布式的方法。

[1]　https://arxiv.org/abs/1603.04467

[2]　代码参考 https://github.com/tobegit3hub/tensorflow_examples/tree/master/distributed_tensorflow。

[3]　本节内容参考https://www.tensorflow.org/extend/architecture。

[4]　本图出自https://www.tensorflow.org/extend/architecture。

[5]　关于参数服务器的更多编程框架参见李沐的文章《Parameter Server for Distributed Machine Learning》：http://www. cs.cmu.edu/~muli/file/ps.pdf。

[6]　本图出自论文《TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systems》：https://arxiv.org/ abs/1603.04467v1。

[7]　本图出自https://www.tensorflow.org/tutorials/deep_cnn 。

[8]　本图出自论文《TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systems》：https://arxiv.org/ abs/1603.04467v1。

[9]　本图出自论文《Revisiting Distributed Synchronous SGD》：https://arxiv.org/abs/1604.00981 。

[10]　本图参考Jianmin Chen、Xinghao Pan和Rajat Monga等人的论文《Revisiting Distributed Synchronous SGD》：https:// arxiv.org/abs/1604.00981。

[11]　本图出自Martın Abadi、Ashish Agarwal和Paul Barham等人的论文《TensorFlow: Large-Scale Machine Learning on Heterogeneous Distributed Systems》：https://arxiv.org/abs/1603.04467v1 。

[12]　本节内容部分参考https://www.tensorflow.org/deploy/distributed 。

[13]　本节代码参考tensorflow-1.1.0/tensorflow/tools/dist_test/python/mnist_replica.py。

第15章　TensorFlow线性代数编译框架XLA

XLA（Accelerated Linear Algebra）是用于线性代数领域的专用编译器（domain-specific compiler），用于优化TensorFlow计算。XLA通过即时（just-in-time，JIT）编译或提前（ahead-of-time，AOT）编译来进行实验，尤其有助于面向硬件加速的开发者。XLA框架目前还是处于试验阶段的。

本章我们主要讲述XLA的优势、工作原理和XLA的一些应用。[1]

15.1　XLA的优势

XLA是一个线性代数的领域专用编译器，能在执行速度、内存的使用、对自定义操作的依赖、移动端的内存占用和可移植性等方面优化了TensorFlow的计算。

- 提高执行速度。通过编译子图来减少生命周期较短的操作的执行时间，通过融合管道化的操作来减少内存占用。

- 提高内存的使用。分析和规划内存的使用需求，消除许多中间结果的缓存。

- 减少对自定义操作的依赖。通过提高自动化融合底层操作（low-level op）的性能，达到原先需要手动融合自定义操作（custom op）的效果。

- 减少移动端的内存占用（移动端的应用详见第19章）。一是通过提前（AOT）编译子图来减少TensorFlow的执行时间，二是通过共享头文件对（如xxx.o和xxx.h）被其他程序直接链接。这两个操作能够使移动端预测的内存占用减少几个数量级。

- 提高可移植性。可以用XLA为新的硬件设备开发一个新的后端，使TensorFlow不需要更改很多代码就可以用在新的硬件设备上。

15.2　XLA的工作原理

学过C语言的人可能知道，LLVM是一个编译器的框架系统，用C++编写而成，用于优化以任意编程语言编写的程序的编译时间（compile time）、链接时间（link time）、运行时间（run time）以及空闲时间（idle time）。[2]

在基于LLVM的编译器中，前端负责解析、验证和诊断输入代码中的错误，然后将解析的代码转换为LLVM中间表示（intermediate representation，IR）。该IR通过一系列分析和优化过程来改进代码，然后发送到代码生成器中，以产生本地机器代码。如图15-1所示，这是一个非常直接的三相设计的LLVM实现。设计中最重要的是LLVM IR，在编译器中IR被用来表示代码。

图15-1[3]

XLA的输入语言称为HLO IR，XLA使用在HLO中定义的图形，并将它们编译成各种体系结构的机器指令。图15-2展示的是XLA中的编译过程。[4]

图15-2③

如图15-2所示，XLA首先进行目标无关的优化和分析，如公共子表达式消除（common subexpression elimination，CSE）、目标无关的操作融合（如将多个操作融合成一个操作）和运行时内存的缓冲区分析等。

接着，XLA将HLO计算发送到后端。后端执行进一步的HLO级目标相关的优化和分析。例如，XLA GPU后端可以执行对GPU编程模型有益的操作融合，并且确定如何将计算划分成流。

下一步是生成目标特定的代码。XLA里面的CPU和GPU后端使用LLVM进行中间表示、优化及代码生成。这些后端用LLVM IR来表示XLA HLO计算。

XLA目前支持在x86-64和NVIDIA GPU上进行JIT编译，以及在x86-64和ARM上进行AOT编译。因此，AOT编译方式更适合移动端和嵌入式的深度学习使用。下面我们就以JIT编译为例进行说明。

15.3　JIT编译方式

TensorFlow的XLA JIT编译器通过XLA编译和运行TensorFlow计算图的一部分。与标准TensorFlow实现相比，XLA可以将多个操作（内核）融合到少量编译内核中，融合操作符可以减少存储器带宽需求并提高性能。

通过XLA运行TensorFlow计算有两种方法，一是打开CPU或GPU设备上的JIT编译，二是将操作符放在XLA_CPU或XLA_GPU设备上。

15.3.1　打开JIT编译

打开JIT编译可以有两种方式。下面是在会话上打开，这种方式会把所有可能的操作符编程成XLA计算。用法示例如下：

config = tf.ConfigProto()
config.graph_options.optimizer_options.global_jit_level = tf.OptimizerOptions.ON_1
sess = tf.Session(config=config)

另一种方式是为一个或多个操作符手动打开JIT编译。这是通过使用属性_XlaCompile = true标记要编译的操作符来完成的。用法示例如下：

jit_scope = tf.contrib.compiler.jit.experimental_jit_scope
x = tf.placeholder(np.float32)
with jit_scope():
y = tf.add(x, x)

15.3.2　将操作符放在XLA设备上

目前有效的设备是XLA_CPU或XLA_GPU，示例如下：

with tf.device("/job:localhost/replica:0/task:0/device:XLA_GPU:0"):
output = tf.add(input1, input2)

15.4　JIT编译在MNIST上的实现

下面我们就用MNIST的softmax版本来尝试使用XLA和不使用XLA的差异。代码位于tensorflow-1.1.0/tensorflow/examples/tutorials/mnist/mnist_softmax_xla.py中。

不使用XLA来运行时，如下：

python mnist_softmax_xla.py --xla=false

运行完成后生成时间线文件timeline.ctf.json，使用Chrome跟踪事件分析器（在浏览器中访问chrome://tracing），打开该时间线文件，呈现的时间线如图15-3所示。

图15-3

图15-3中最左侧一列列出了本机的4个GPU。可以清晰地看到图中MatMul操作符，跨越4个CPU的时间消耗情况。

让我们使用XLA来训练模型，如下：

TF_XLA_FLAGS=--xla_generate_hlo_graph=.* python mnist_softmax_xla.py

运行完成后，得到的时间线图像如图15-4所示。

图15-4

我们看看如何调用JIT编译，关键的训练代码如下。下面讲解了如何开启XLA的JIT编译，以及如何将训练追踪写入时间线文件。

config = tf.ConfigProto()
jit_level = 0
if FLAGS.xla:
# 开启XLA的JIT编译
jit_level = tf.OptimizerOptions.ON_1

config.graph_options.optimizer_options.global_jit_level = jit_level
run_metadata = tf.RunMetadata()
sess = tf.Session(config=config)
tf.global_variables_initializer().run(session=sess)
# 训练
train_loops = 1000
for i in range(train_loops):
batch_xs, batch_ys = mnist.train.next_batch(100)

# 在最后一次循环中，创建时间线文件，可以用chrome://tracing/打开和分析
if i == train_loops - 1:
sess.run(train_step,
feed_dict={x: batch_xs, y_: batch_ys},
options=tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE),
run_metadata=run_metadata)
trace = timeline.Timeline(step_stats=run_metadata.step_stats)
trace_file = open('xlatimeline.ctf.json', 'w')
trace_file.write(trace.generate_chrome_trace_format())
else:
sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

目前XLA框架还处于试验阶段，AOT主要应用场景是一些内存较小的嵌入式设备、手机、树莓派等，对于性能要求较高的读者可以做进一步探索和发现。

15.5　小结

本节主要讲述了TensorFlow的线性代数编译框架XLA的工作原理，JIT编译和AOT编译的适用范围，重点讲解了JIT编译方式的两种实现方法。最后用MNIST数据集展现了如何使用JIT编译。XLA是TensorFlow 1.0版本加入的新特性，还有待完善。本章重点面向开发中对性能要求较高的开发者。

[1]　本章内容主要参考TensorFlow官方网站：https://www.tensorflow.org/versions/master/experimental/xla/。

[2]　参考百度百科 “LLVM”：http://baike.baidu.com/link?url=0c67Vs4dGqctjTSoQ9xpF2yWxmUxaj8SN4UhPAtq4t3xtai22h9-L3IY_qSuabj9FYyJyfXvrqbllf80Sg19s_。

[3]　本图参考http://www.aosabook.org/en/llvm.html。

[4]　本图参考TensorFlow官方网站：https://www.tensorflow.org/versions/master/experimental/xla/。

第16章　TensorFlow Debugger[1]

TensorFlow Debugger（tfdbg）是TensorFlow的专用调试器。它使用断点和计算机图形化来展现实时数据流，提供了运行TensorFlow图形的内部结构和状态的可视化。这种可视化非常有助于在训练和推理期间调试各种类型的模型错误。

本章会通过讲解如何调试TensorFlow模型开发中一种常见的错误类型——非数字（nan）和无限值（inf）导致的训练失败，来展示tfdbg命令行界面（command line interface，CLI）的功能。而这对一般调试器（如C++的gdb或者Python的pdb）来说，是很难调试的。

此外，TensorFlow这种使用符号式编程的语言本身就以难调试而闻名，没有经验的开发者常常很难直观地感受数据流图在做什么，因此，出现了问题就难以定位，更谈不上说高级的优化任务。

16.1　Debugger的使用示例

本节我们以一个错误运行的MNIST训练为例，看看如何通过TensorFlow Debugger来找到出错的地方，并改正。源代码位于tensorflow-1.1.0/tensorflow/python/debug/examples/debug_mnist.py。我们先不加调试器，直接执行，如下：

python -m tensorflow.python.debug.examples.debug_mnist

也可以进入tensorflow-1.1.0/tensorflow/python/debug/examples执行：

python debug_mnist.py

经过10次训练，结果如下：

Accuracy at step 0: 0.1113
Accuracy at step 1: 0.3712
Accuracy at step 2: 0.098
Accuracy at step 3: 0.098
Accuracy at step 4: 0.098
Accuracy at step 5: 0.098
Accuracy at step 6: 0.098
Accuracy at step 7: 0.098
Accuracy at step 8: 0.098
Accuracy at step 9: 0.098

可以看出，准确率在第一次训练时有所上升，后面一直保持在较低的水平。

下面我们就用TensorFlow Debugger来尝试调试。仅需要在原有的文件中加上下面3行代码，就可以在每次调用run()之前和之后调用基于终端的用户界面（UI），来控制执行和检查图的内部状态：

from tensorflow.python import debug as tf_debug

sess = tf_debug.LocalCLIDebugWrapperSession(sess)
sess.add_tensor_filter("has_inf_or_nan", tf_debug.has_inf_or_nan)

在这个例子中，我们为张量值注册了一个过滤器has_inf_or_nan，它能够判断出图的任何中间张量中是否有nan或inf值。

现在我们就开启调试模式（debug），找出准确率无法提高的原因。首先执行：

python -m tensorflow.python.debug.examples.debug_mnist –debug

也可以通过如下方式执行：

python debug_mnist.py --debug=True

这时就进入了Debugger的界面，如图16-1所示。

图16-1

这就是我们说的运行开始的UI（run-start UI）。在tfdbg>后面可以输入交互式的命令，如run()（或者缩写r），就可以进入运行结束后UI（run-end UI），如图16-2所示。

图16-2

在图16-2中，可以看到，数值并没有异常。然后，可以通过如下命令连续运行10次：

tfdbg> run -t 10

可以使用下面的命令直到找出在图形中的第一个nan或者inf值（这类似于调试中的打断点）：

tfdbg> run -f has_inf_or_nan

结果如图16-3所示。

图16-3

第一行的灰底字（电脑屏幕中显示为红字），表示tfdbg在调用run()后立即停止，生成了通过指定过滤器has_inf_or_nan的中间张量。如图16-3所示，在第4次调用run()期间，有36个中间张量包含inf或者nan值，首次出现在cross_entropy/Log:0。

单击图中的cross_entropy/Log:0，并且单击下划线的node_info菜单项，仔细看一下这个节点的输入张量，并且看看里面是否有0值。方法如下：

tfdbg> pt softmax/Softmax:0
tfdbg> /0\.000

果然是有0值，如图16-4所示。

图16-4

用ni命令的-t标志进行追溯，方法如下：

ni -t cross_entropy/Log

追溯结果如图16-5所示，可以看到debug_mnist.py文件的第102行是罪魁祸首。

图16-5

于是，我们对tf.log的输入值进行裁剪，将这个问题解决：

diff = y_ * tf.log(tf.clip_by_value(y, 1e-8, 1.0))

再次运行后，准确率不再低值徘徊，恢复正常。

TensorFlow Debugger是一个非常好用的调试工具，还有很多交互式命令，感兴趣的读者可以进一步参考tfdbg的命令行接口教程[2]。

16.2　远程调试方法

上面的使用示例是在本地的调试方法。但通常情况下，数据是在远程机器上训练，更一般的情况是，深度学习越来越倾向于在云端训练，本地不能访问训练过程中的数据。那么，这种情况下如何进行模型的调试呢？

这时采用tfdbg的offline_analyzer。设置一个本地和远程机器都能访问的共享目录，如/home/somebody/tfdbg_dumps_1。然后，通过debug_utils.watch_graph函数设置运行时的参数选项。在运行session.run()时就会将中间张量和运行时的图像转储到共享目录中。方法如下：

from tensorflow.python.debug import debug_utils

# 此处代码如：构建图，生成session对象等。已省略
run_options = tf.RunOptions()
debug_utils.watch_graph(
run_options,
session.graph,
debug_urls=["file:///home/somebody/tfdbg_dumps_1"])  # 共享目录的位置
# 这里如果用多个客户端执行run，应该使用多个不同的共享目录
session.run(fetches, feed_dict=feeds, options=run_options)

这样，在本地终端上就可以使用tfdbg的offline_analyzer来加载和检查共享目录中的数据。方法如下：

python -m tensorflow.python.debug.cli.offline_analyzer \
--dump_dir=/home/somebody/tfdbg_dumps_1

另一种更简单和灵活的方法是使用会话的包装器函数DumpingDebugWrapperSession来在共享目录中产生训练中的累积文件。直接这样使用：

from tensorflow.python.debug import debug_utils

sess = tf_debug.DumpingDebugWrapperSession(
sess, "/home/somebody/tfdbg_dumps_1/", watch_fn=my_watch_fn)

16.3　小结

本章主要介绍了TensorFlow Debugger，这是一个非常有用的调试工具。在训练神经网络的过程中，开发者常常会因为各种原因，写错某个参数或者变量的值，虽然这时候训练过程不会报错，但是会导致损失值很长时间都不下降，模型收敛很慢。本章还介绍了远程调试的方法。以往面对这种情况，开发人员需要费很大力气去查找原因。使用TensorFlow Debugger可以在早期的一两次迭代中，观察张量的正确与否，给调试带来极大帮助。Debugger是TensorFlow 1.0新加入的特性，目前还在不断完善，并且有望和TensorBoard结合，更大限度地简化调试过程。

[1]　本章内容主要参考https://www.tensorflow.org/programmers_guide/debugger。

[2]　https://www.tensorflow.org/programmers_guide/debugger

第17章　TensorFlow和Kubernetes结合

在AlphaGo中，每个实验使用1 000个节点，每个节点有4个GPU，也就是使用了4 000个GPU。在Siri中，每个实验2个节点，也就是使用了8个GPU。可想而知，AI研究的进行依赖于海量数据的计算，同时也离不开高性能计算资源的支持。

在第14章中我们已经讲解了TensorFlow的分布式原理以及部署方式。随着海量数据的出现和模型参数的增多，我们必然需要更大的集群来运行模型，这样最大的好处在于把原本可能需要周级别的训练时间缩短到天级别甚至小时级别。未来的模型训练面对的都是上亿数据和上亿参数，稳定的计算能力和管理便捷的集群环境至关重要。Kubernetes是目前应用最广泛的容器集群管理工具之一，它可以为对分布式TensorFlow的监控、调度等生命周期管理提供所需的保障。

17.1　为什么需要Kubernetes

有过大数据集群开发经验的人都知道，尽管TensorFlow有自己的分布式方案，但仍需要手动把每台机器运行起来，当机器量是几台或十几台的时候，可能压力不大，但当机器量达到上千台时，就需要一样东西来进行管理和调度，进行自动化部署、调度、扩容和缩容处理，甚至当一些任务意外退出后，还需要控制自动重启。Kubernetes就提供了这样的解决方案。

Kubernetes官方[1]的解释是：Kubernetes是一个用于容器集群的自动化部署、扩容以及运维的开源平台，它可以提供任务调度、监控、失败重启等功能。

另外，因为TensorFlow和Kubernetes都是谷歌公司的开源产品，所以非常容易在它们之间搭起桥梁，并且谷歌云平台[2]也在推出平台化的解决方案。

17.2　分布式TensorFlow在Kubernetes中的运行

下面我们就来介绍在Kubernetes中运行分布式TensorFlow的方法。本节首先学习如何部署Kubernetes环境，接着在搭建好的环境中运行分布式TensorFlow，并用MNIST来训练。

17.2.1　部署及运行

我们需要先安装Kubernetes。这里是用Mac来演示的，其他操作系统也大同小异。

我们用Minikube来创建本地Kubernetes集群。安装Minikube需要预先安装VirtualBox虚拟机，读者可以从官网[3]上直接下载安装，注意选择对应的操作系统版本即可。图17-1是我选用的版本。

图17-1

Minikube用Go语言编写，发布形式是一个独立的二进制文件，所以只需要下载下来，然后放在对应的位置即可。因此安装Minikube，只需要一条命令：

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.14.0/
minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/

Kubernetes提供了一个客户端kubectl，可直接通过kubectl以命令行的方式与集群交互。

安装kubectl的方法如下：

curl -Lo kubectl http://storage.googleapis.com/kubernetes-release/release/v1.5.1/
bin/darwin/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/

下面在Minikube中启动 Kubernetes集群，如图17-2所示。

图17-2

可以观察到VirtualBox中也启动了相应的虚拟机，如图17-3所示。

图17-3

我们采用Docker Hub[4]上的最新镜像tensorflow/tensorflow（基于TensorFlow的1.0版本）。

首先，配置参数服务器的部署（deployment）文件，命名为tf-ps-deployment.json。代码如下：

{
"apiVersion": "extensions/v1beta1",
"kind": "Deployment",
"metadata": {
"name": "tensorflow-ps2"
},
"spec": {
"replicas": 2,
"template": {
"metadata": {
"labels": {
"name": "tensorflow-ps2",
"role": "ps"
}
},
"spec": {
"containers": [
{
"name": "ps",
"image": "tensorflow/tensorflow",
"ports": [
{
"containerPort": 2222
}
]
}
]
}
}
}
}

配置参数服务器的服务（Service）文件，命名为tf-ps-service.json，代码如下：

{
"apiVersion": "v1",
"kind": "Service",
"spec": {
"ports": [
{
"port": 2222,
"targetPort": 2222
}
],
"selector": {
"name": "tensorflow-ps2"
}
},
"metadata": {
"labels": {
"name": "tensorflow",
"role": "service"
},
"name": "tensorflow-ps2-service"
}
}

配置计算服务器的部署文件，命名为tf-worker-deployment.json，代码如下：

{
"apiVersion": "extensions/v1beta1",
"kind": "Deployment",
"metadata": {
"name": "tensorflow-worker2"
},
"spec": {
"replicas": 2,
"template": {
"metadata": {
"labels": {
"name": "tensorflow-worker2",
"role": "worker"
}
},
"spec": {
"containers": [
{
"name": "worker",
"image": "tensorflow/tensorflow",
"ports": [
{
"containerPort": 2222
}
]
}
]
}
}
}
}

配置计算服务器的服务文件，命名为tf-worker-service.json，代码如下：

{
"apiVersion": "v1",
"kind": "Service",
"spec": {
"ports": [
{
"port": 2222,
"targetPort": 2222
}
],
"selector": {
"name": "tensorflow-worker2"
}
},
"metadata": {
"labels": {
"name": "tensorflow-worker2",
"role": "service"
},
"name": "tensorflow-wk2-service"
}
}

执行以下命令：

kubectl create -f tf-ps-deployment.json
kubectl create -f tf-ps-service.json
kubectl create -f tf-worker-deployment.json
kubectl create -f tf-worker-service.json

分别输出以下结果：

deployment "tensorflow-ps2" created
service " tensorflow-ps2-service" created
deployment "tensorflow-worker2" created
service "tensorflow-wk2-service" created

稍等片刻，运行kubectl get pod，可以看到参数服务器和计算服务器全部创建完成，如图17-4所示。

图17-4

下面我们进入每个服务器（Pod）中，部署好需要运行的mnist_replica.py文件。

首先查看以下2台ps_host的 IP地址，如图17-5所示。

图17-5

然后查看2台worker_host的IP地址，如图17-6所示。

图17-6

打开4个终端，分别进入4个Pod当中，命令如下：

kubectl exec -ti tensorflow-ps2-3073558082-3b08h /bin/bash
kubectl exec -ti tensorflow-ps2-3073558082-4x3j2 /bin/bash
kubectl exec -ti tensorflow-worker2-3070479207-k6z8f /bin/bash
kubectl exec -ti tensorflow-worker2-3070479207-6hvsk /bin/bash

通过下面的方式将mnist_replica.py分别部署到4个Pod中，如下：

curl https://raw.githubusercontent.com/tensorflow/tensorflow/master/
tensorflow/tools/dist_test/python/mnist_replica.py -o mnist_replica.py

在参数服务器的两个容器中分别执行：

python mnist_replica.py --ps_hosts=172.17.0.16:2222,172.17.0.17:2222 --worker_
hosts=172.17.0.3:2222,172.17.0.8:2222 --job_name="ps" --task_index=0

python mnist_replica.py --ps_hosts=172.17.0.16:2222,172.17.0.17:2222 --worker_
hosts=172.17.0.3:2222,172.17.0.8:2222 --job_name="ps" --task_index=1

在计算服务器的两个容器中分别执行：

python mnist_replica.py --ps_hosts=172.17.0.16:2222,172.17.0.17:2222 --worker_
hosts=172.17.0.3:2222,172.17.0.8:2222 --job_name="worker" --task_index=0

python mnist_replica.py --ps_hosts=172.17.0.16:2222,172.17.0.17:2222 --worker_
hosts=172.17.0.3:2222,172.17.0.8:2222 --job_name="worker" --task_index=1

执行输出与14.6节的输出类似。一共执行200次迭代，工作节点1（172.17.0.3:2222）执行了144次迭代，如下：

job name = worker
task index = 0
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize
GrpcChannelCache for job ps -> {0 -> localhost:2222}
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job worker -> {0 -> localhost:2223, 1 -> localhost:2224}
I tensorflow/core/distributed_runtime/rpc/grpc_server_lib.cc:217] Started server
with target: grpc://localhost:2223
Worker 0: Initializing session...
I tensorflow/core/distributed_runtime/master_session.cc:994] Start master session
0d791a02977e5701 with config:
device_filters: "/job:ps"
device_filters: "/job:worker/task:0"
allow_soft_placement: true

Worker 0: Session initialization complete.
Training begins @ 1483516057.489495
1483516057.518419: Worker 0: training step 1 done (global step: 0)
1483516057.541053: Worker 0: training step 2 done (global step: 1)
1483516057.569677: Worker 0: training step 3 done (global step: 2)
1483516057.584578: Worker 0: training step 4 done (global step: 3)
1483516057.646970: Worker 0: training step 5 done (global step: 4)
# ……中间略去
1483516059.286596: Worker 0: training step 141 done (global step: 197)
1483516059.291600: Worker 0: training step 142 done (global step: 198)
1483516059.297347: Worker 0: training step 143 done (global step: 199)
1483516059.303738: Worker 0: training step 144 done (global step: 200)
Training ends @ 1483516059.303808
Training elapsed time: 1.614513 s
After 200 training step(s), validation cross entropy = 1235.56

工作节点2（172.17.0.8:2222）执行了56次迭代，输出如下：

job name = worker
task index = 1
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job ps -> {0 -> localhost:2222}
I tensorflow/core/distributed_runtime/rpc/grpc_channel.cc:200] Initialize GrpcChannel
Cache for job worker -> {0 -> localhost:2223, 1 -> localhost:2224}
I tensorflow/core/distributed_runtime/rpc/grpc_server_lib.cc:217] Started server
with target: grpc://localhost:2224
Worker 1: Waiting for session to be initialized...
I tensorflow/core/distributed_runtime/master_session.cc:994] Start master session
92e671f3dd1ffd05 with config:
device_filters: "/job:ps"
device_filters: "/job:worker/task:1"
allow_soft_placement: true

Worker 1: Session initialization complete.
Training begins @ 1483516058.803010
1483516058.832164: Worker 1: training step 1 done (global step: 121)
1483516058.844464: Worker 1: training step 2 done (global step: 123)
1483516058.860988: Worker 1: training step 3 done (global step: 126)
1483516058.873543: Worker 1: training step 4 done (global step: 128)
1483516058.884758: Worker 1: training step 5 done (global step: 130)
# ……中间略去
1483516059.152332: Worker 1: training step 52 done (global step: 176)
1483516059.167606: Worker 1: training step 53 done (global step: 178)
1483516059.177215: Worker 1: training step 54 done (global step: 180)
1483516059.301384: Worker 1: training step 55 done (global step: 182)
1483516059.309557: Worker 1: training step 56 done (global step: 202)
Training ends @ 1483516059.309638
Training elapsed time: 0.536126 s
After 200 training step(s), validation cross entropy = 1235.56

在这个例子中，更好的方式是把需要执行的源代码以及训练数据和测试数据放在持久卷（persistent volume）中，在多个Pod间实现共享，从而避免在每一个Pod中分别部署。

对应TensorFlow的GPU的Docker集群部署，Nvidia官方提供了nvidia-docker的方式，原理主要是利用宿主机上的GPU设备，将它映射到容器中。更多与部署相关的内容读者可参考https://github.com/NVIDIA/nvidia-docker。

17.2.2　其他应用

训练好模型之后可以将它打包制作成环境独立的镜像，这样能够极大地方便测试人员部署一致的环境，也便于对不同版本的模型做标记、比较不同模型的准确率，从整体上降低测试、部署上线等的工作复杂性，具有很大的优势。

17.3　小结

将Kubernete与TensorFlow结合，借助Kubernetes提供的稳定计算环境，对TensorFlow集群进行便捷的管理，降低了搭建大规模深度学习平台的难度，这也是社区非常推崇的部署方案。本章主要讲述了用Kubernetes管理TensorFlow集群的方法，以及在Kubernetes上部署分布式TensorFlow的方式，最后采用MNIST的分布式例子进行了实践。

[1]　https://kubernetes.io/

[2]　https://cloud.google.com/

[3]　https://www.virtualbox.org/

[4]　https://hub.docker.com/r/tensorflow/tensorflow/

第18章　TensorFlowOnSpark

在第14章我们讲了TensorFlow的分布式运行，在第17章又介绍了使用Kubernetes集群对TensorFlow节点进行调度、监控和失败重启等功能。我们知道，Hadoop生态的大数据系统一般可以分为Yarn、HDFS和MapReduce计算框架，TensorFlow本身的分布式就相当于MapReduce计算框架部分，而Kubernetes就相当于Yarn调度系统。本章要讲的TensorFlowOnSpark是利用远程直接内存访问（Remote Direct Memory Access，RDMA）解决了存储功能和调度，实现了深度学习和大数据的融合。

TensorFlowOnSpark（TFoS）是雅虎推出的开源项目[1]，支持使用Apache Spark集群进行分布式TensorFlow训练和预测。其实，TensorFlow的程序并不能直接作为Spark的程序运行，TensorFlowOnSpark提供了一个程序来进行桥接，本质上是每个Spark Executor启动一个对应的TensorFlow进程，然后通过远程进程通信（RPC）进行交互。

18.1　TensorFlowOnSpark的架构[2]

要把一个训练程序改到使用Spark的集群上运行，在运用TensorFlowOnSpark后，就只需要改非常少量的代码（官方认为不到10行）。TensorFlowOnSpark通过下面的步骤来管理Spark集群。

（1）预留：为在Executor上执行的每个TensorFlow进程保留一个端口，并启动数据消息的监听器。

（2）启动：在Executor上启动Tensorflow主函数。

（3）数据获取：这里提供了两种不同的模式来提取训练数据和测试数据。

- Readers和QueueRunners：利用TensorFlow的Readers和QueueRunners机制直接从HDFS文件中读取数据文件。Spark不涉及访问数据。

- Feeding：将Spark RDD数据发送到TensorFlow节点，随后的数据将通过feed_dict机制传入TensorFlow图中。

（4）关闭：关闭Executor上的TensorFlow计算节点和参数服务节点。

TensorFlowOnSpark系统的架构如图18-1所示。

图18-1[3]

TFoS曾做过一个图像分类的实验，结果非常令人鼓舞。以同一准确度作为评判标准，准确度达到0.730，单计算节点工作需要46小时，双计算节点需要22.5小时，4计算节点需要13小时，8计算节点需要7.5小时，如图18-2所示。因此，实现了接近模型训练的近线性可扩展性。

图18-2[4]

下面我们就以MNIST数据集为例，看看如何在Spark上进行部署、训练以及预测。

18.2　TensorFlowOnSpark在MNIST上的实践[5]

采用Standalone模式的Spark集群，仅需要一台计算机就够，下面以此为例看看如何应用TensorFlowOnSpark。

首先，安装Spark和Hadoop，这里所用的计算机的操作系统是OS X 10.10.5，并且已经部署好了Java 1.8.0的JDK，从http://spark.apache.org/downloads.html下载Spark，这里选用2.1.0版本，从http://hadoop.apache.org/#Download+Hadoop 下载Hadoop，这里选用2.7.3版本。在2017年3月，这个框架对TensorFlow 0.12.1版本的支持较好，但对TensorFlow 1.0 版本尚有些问题，还需要读者自己适配好。

安装后，修改必要的配置文件，设置环境变量后，启动Hadoop：

$HADOOP_HOME/sbin/start-all.sh

然后，检出TensorFlowOnSpark源代码，如下：

git clone --recurse-submodules https://github.com/yahoo/TensorFlowOnSpark.git
cd TensorFlowOnSpark
git submodule init
git submodule update --force
git submodule foreach --recursive git clean -dfx

接着，将源代码部分打包，供提交任务时使用，如下：

cd TensorFlowOnSpark/src
zip -r ../tfspark.zip *

设置TensorFlowOnSpark根目录的环境变量，接下来会用到：

cd TensorFlowOnSpark
export TFoS_HOME=$(pwd)

接着，启动Spark 主节点（master）：

${SPARK_HOME}/sbin/start-master.sh

配置两个工作节点（worker）实例，通过master-spark-URL和主节点连接：

export MASTER=spark://$(hostname):7077
export SPARK_WORKER_INSTANCES=2
export CORES_PER_WORKER=1
export TOTAL_CORES=$((${CORES_PER_WORKER}*${SPARK_WORKER_INSTANCES}))
${SPARK_HOME}/sbin/start-slave.sh -c $CORES_PER_WORKER -m 3G ${MASTER}

接下来，提交任务，将MNIST的zip文件转换为HDFS上的RDD数据集：

${SPARK_HOME}/bin/spark-submit \
--master ${MASTER} --conf spark.ui.port=4048 --verbose \
${TFoS_HOME}/examples/mnist/mnist_data_setup.py \
--output examples/mnist/csv \
--format csv

运行完毕后，可以通过如下命令看到处理过的数据集：

hadoop fs -ls hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv
Found 2 items
drwxr-xr-x   - jiaxuan supergroup           0 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/test
drwxr-xr-x   - jiaxuan supergroup           0 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train

可以查看保存后的图片和标记向量，分别共有10份：

hadoop fs -ls hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels

-rw-r--r--   1 jiaxuan supergroup           0 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/_SUCCESS
-rw-r--r--   1 jiaxuan supergroup       204800 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00000
-rw-r--r--   1 jiaxuan supergroup       245760 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00001
-rw-r--r--   1 jiaxuan supergroup       245760 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00002
-rw-r--r--   1 jiaxuan supergroup       245760 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00003
-rw-r--r--   1 jiaxuan supergroup       245760 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00004
-rw-r--r--   1 jiaxuan supergroup       245760 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00005
-rw-r--r--   1 jiaxuan supergroup       245760 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00006
-rw-r--r--   1 jiaxuan supergroup       245760 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00007
-rw-r--r--   1 jiaxuan supergroup       245760 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00008
-rw-r--r--   1 jiaxuan supergroup       229120 2017-03-10 04:27
hdfs://localhost:9000/user/jiaxuan/examples/mnist/csv/train/labels/part-00009

MNIST训练集共有60 000条数据，有10个文件，每个文件有6 000条数据左右，里面存储的格式和标记向量格式如图18-3所示。

图18-3

图片向量格式如图18-4所示。

图18-4

这里，我们主要是把训练集和测试集分别保存成RDD数据，具体代码参见${TFoS_HOME}/ examples/mnist/mnist_data_setup.py。关键代码如下：

writeMNIST(sc, "mnist/train-images-idx3-ubyte.gz", "mnist/train-labels-idx1-
ubyte.gz", args.output + "/train", args.format, args.num_partitions)
writeMNIST(sc, "mnist/t10k-images-idx3-ubyte.gz", "mnist/t10k-labels-idx1-
ubyte.gz", args.output + "/test", args.format, args.num_partitions)

调用writeMNIST函数，将RDDs保存为特定格式：

def writeMNIST(sc, input_images, input_labels, output, format, num_partitions):
"""将MNIST图像和标记向量写入HDFS上"""
with open(input_images, 'rb') as f:
images = numpy.array(mnist.extract_images(f))

with open(input_labels, 'rb') as f:
labels = numpy.array(mnist.extract_labels(f, one_hot=True))

shape = images.shape
print("images.shape: {0}".format(shape))         # 60000 x 28 x 28
print("labels.shape: {0}".format(labels.shape))  # 60000 x 10

imageRDD = sc.parallelize(images.reshape(shape[0], shape[1] * shape[2]),
num_partitions)
labelRDD = sc.parallelize(labels, num_partitions)

output_images = output + "/images"
output_labels = output + "/labels"

# 将RDDs保存为特定格式
if format == "pickle":
imageRDD.saveAsPickleFile(output_images)
labelRDD.saveAsPickleFile(output_labels)
elif format == "csv":
imageRDD.map(toCSV).saveAsTextFile(output_images)
labelRDD.map(toCSV).saveAsTextFile(output_labels)

接着，提交训练任务，开始训练，命令如下，我们最终在HDFS上生成了mnist_model：

${SPARK_HOME}/bin/spark-submit \
--master ${MASTER} \
--py-files
${TFoS_HOME}/tfspark.zip,${TFoS_HOME}/examples/mnist/spark/mnist_dist.py \
--conf spark.cores.max=${TOTAL_CORES} \
--conf spark.task.cpus=${CORES_PER_WORKER} \
--conf spark.executorEnv.JAVA_HOME="$JAVA_HOME" \
${TFoS_HOME}/examples/mnist/spark/mnist_spark.py \
--cluster_size ${SPARK_WORKER_INSTANCES} \
--images examples/mnist/csv/train/images \
--labels examples/mnist/csv/train/labels \
--format csv \
--mode train \
--model mnist_model

这里的mnist_dist.py主要是构建TensorFlow分布式任务，其中定义了分布式任务的主函数，也就是启动TensorFlow的主函数map_fun，采用的数据获取方式是Feeding。这里用到的TensorFlowOnSpark代码主要是获取TensorFlow集群和服务器实例，如下：

cluster, server = TFNode.start_cluster_server(ctx, 1, args.rdma)

其中TFNode调用我们刚才打包好的tfspark.zip中的TFNode.py文件。

mnist_spark.py文件是我们训练的主程序，体现了TensorFlowOnSpark的部署步骤，如下：

sc = SparkContext(conf=SparkConf().setAppName("mnist_spark"))
executors = sc._conf.get("spark.executor.instances")
num_executors = int(executors) if executors is not None else 1
num_ps = 1
……
cluster = TFCluster.reserve(sc, args.cluster_size, num_ps, args.tensorboard,
TFCluster.InputMode.SPARK) #1.为在Executor执行上的每个TensorFlow进程保留一个端口
cluster.start(mnist_dist.map_fun, args) # 2.启动Tensorflow主函数
if args.mode == "train":
cluster.train(dataRDD, args.epochs) # 3.训练
else:
labelRDD = cluster.inference(dataRDD) # 预测
labelRDD.saveAsTextFile(args.output)
cluster.shutdown() # 4.关闭Executor上的TensorFlow计算节点和参数服务节点

预测的过程也类似，运行如下命令：

${SPARK_HOME}/bin/spark-submit \
--master ${MASTER} \
--py-files
${TFoS_HOME}/tfspark.zip,${TFoS_HOME}/examples/mnist/spark/mnist_dist.py \
--conf spark.cores.max=${TOTAL_CORES} \
--conf spark.task.cpus=${CORES_PER_WORKER} \
--conf spark.executorEnv.JAVA_HOME="$JAVA_HOME" \
${TFoS_HOME}/examples/mnist/spark/mnist_spark.py \
--cluster_size ${SPARK_WORKER_INSTANCES} \
--images examples/mnist/csv/test/images \
--labels examples/mnist/csv/test/labels \
--mode inference \
--format csv \
--model mnist_model \
--output predictions

最终输出的预测文件如下：

2017-03-10T23:29:17.009563 Label: 7, Prediction: 7
2017-03-10T23:29:17.009677 Label: 2, Prediction: 2

其实，除了单机版的Standalone模式外，官方网站上还介绍了在Amazon EC2上运行以及在Hadoop集群上采用YARN模式运行，请读者自行参考。

18.3　小结

本章主要介绍了雅虎公司的开源工具TensorFlowOnSpark的架构及使用。读者只需要改很少的代码就可以将TensorFlow和Spark结合，真正实现大数据的深度学习训练。

[1]　https://github.com/yahoo/TensorFlowOnSpark

[2]　本节内容参考https://github.com/yahoo/TensorFlowOnSpark 。

[3]　本图参考http://yahoohadoop.tumblr.com/post/157196317141/open-sourcing-tensorflowonspark-distributed-deep 。

[4]　本图引自http://yahoohadoop.tumblr.com/post/157196317141/open-sourcing-tensorflowonspark-distributed-deep 。

[5]　本节实践过程参考https://github.com/yahoo/TensorFlowOnSpark/wiki/GetStarted_standalone 。

第19章　TensorFlow移动端应用

深度学习在声频、图像、视频处理上已经取得了令人印象深刻的进步，但它通常运行在功能强大的计算机上，如果需要运行在手机等移动设备或者树莓派等嵌入式平台上呢？

TensorFlow目前是最有竞争力成为未来主流的深度学习框架，谷歌公司不仅为自己研发的操作系统——Android提供了TensorFlow移动端支持，而且对iOS和树莓派也提供了移动端支持。

19.1　移动端应用原理

在移动端或者嵌入式设备上应用深度学习，有两种方式：一是将模型运行在云端服务器上，向服务器发送请求，接收服务器响应；二是在本地运行模型。一般来说，采用后者的方式，也就是在PC上训练好一个模型，然后将其放在移动端上进行预测。

使用本地运行模型原因在于，首先，向服务端请求数据的方式可行性差。移动端的资源（如网络、CPU、内存资源）是很稀缺的。例如，在网络连接不良或者丢失的情况下，向服务端发送连续的数据的代价就变得非常高昂。其次，运行在本地的实时性更好。但问题是，一个模型大小动辄几百兆，且不说把它安装到移动端需要多少网络资源，就是每次预测时需要的内存资源也是很多的。那么，要在性能相对较弱的移动/嵌入式设备（如没有加速器的ARM CPU）上高效运行一个CNN，应该怎么做呢？这就衍生出了很多加速计算的方向，其中重要的两个方向是对内存空间和速度的优化。采用的方式一是精简模型，既可以节省内存空间，也可以加快计算速度；二是加快框架的执行速度，影响框架执行速度主要有两方面的因素，即模型的复杂度和每一步的计算速度。

精简模型主要是使用更低的权重精度，如量化（quantization）或权重剪枝（weight pruning）。剪枝是指剪小权重的连接，把所有权值连接低于一个阈值的连接从网络里移除。

而加速框架的执行速度一般不会影响模型的参数，是试图优化矩阵之间的通用乘法（GEMM）运算，因此会同时影响卷积层（卷积层的计算是先对数据进行im2col[1]运算，再进行GEMM运算）和全连接层。

下面我们就分别来介绍。

19.1.1　量化[2]

量化（quantitative），这里不是指金融上的量化交易，而是指离散化。量化是一个总括术语，是用比32位浮点数更少的空间来存储和运行模型，并且TensorFlow量化的实现屏蔽了存储和运行细节。

神经网络训练时要求速度和准确率，训练通常在GPU上进行，所以使用浮点数影响不大。但是在预测阶段，使用浮点数会影响速度。量化可以在加快速度的同时，保持较高的精度。

量化网络的动机主要有两个。最初的动机是减小模型文件的大小。模型文件往往占据很大的磁盘空间，例如，6.6节中介绍的模型，每个模型都接近200 MB，模型中存储的是分布在大量层中的权值。在存储模型的时候用8位整数，模型大小可以缩小为原来32位的25%左右。在加载模型后运算时转换回32位浮点数，这样已有的浮点计算代码无需改动即可正常运行。

量化的另一个动机是降低预测过程需要的计算资源。这在嵌入式和移动端非常有意义，能够更快地运行模型，功耗更低。从体系架构的角度来说，8位的访问次数要比32位多，在读取8位整数时只需要32位浮点数的1/4的内存带宽，例如，在32位内存带宽的情况下，8位整数可以一次访问4个，32位浮点数只能1次访问1个。而且使用SIMD指令（19.2节会加速介绍该指令集），可以在一个时钟周期里实现更多的计算。另一方面，8位对嵌入式设备的利用更充分，因为很多嵌入式芯片都是8位、16位的，如单片机、数字信号处理器（DSP芯片），8位可以充分利用这些。

此外，神经网络对于噪声的健壮性很强，因为量化会带来精度损失（这种损失可以认为是一种噪声），并不会危害到整体结果的准确度。

那能否用低精度格式来直接训练呢？答案是，大多数情况下是不能的。因为在训练时，尽管前向传播能够顺利进行，但往往反向传播中需要计算梯度。例如，梯度是0.2，使用浮点数可以很好地表示，而整数就不能很好地表示，这会导致梯度消失。因此需要使用高于8位的值来计算梯度。因此，正如在本节一开始介绍的那样，在移动端训练模型的思路往往是，在PC上正常训练好浮点数模型，然后直接将模型转换成8位，移动端是使用8位的模型来执行预测的过程。

下面我们就以8位精度的存储和计算来说明。

1．量化示例

我们举个将GoogleNet模型转换成8位模型的例子，看看模型的大小减小多少，以及用它预测的结果怎么样。

从官方网站上下载[3]训练好的GoogleNet模型，解压后，放在/tmp目录下，然后执行：

bazel build tensorflow/tools/quantization:quantize_graph
bazel-bin/tensorflow/tools/quantization/quantize_graph \
--input=/tmp/classify_image_graph_def.pb \
--output_node_names="softmax" --output=/tmp/quantized_graph.pb \
--mode=eightbit

生成量化后的模型quantized_graph.pb大小只有23 MB，是原来模型classify_image_graph_def.pb（91 MB）的1/4。它的预测效果怎么样呢？执行：

bazel build tensorflow/examples/label_image:label_image
bazel-bin/tensorflow/examples/label_image/label_image \
--image=/tmp/cropped_panda.jpg \
--graph=/tmp/quantized_graph.pb \
--labels=/tmp/imagenet_synset_to_human_label_map.txt \
--input_width=299 \
--input_height=299 \
--input_mean=128 \
--input_std=128 \
--input_layer="Mul:0" \
--output_layer="softmax:0"

运行结果如图19-1所示，可以看出8位模型预测的结果也很好。

图19-1

2．量化过程的实现

TensorFlow的量化是通过将预测的操作转换成等价的8位版本的操作来实现的。量化操作过程如图19-2所示。

图19-2中左侧是原始的Relu操作，输入和输出均是浮点数。右侧是量化后的Relu操作，先根据输入的浮点数计算最大值和最小值，然后进入量化（Quantize）操作将输入数据转换成8位。一般来讲，在进入量化的Relu（QuantizedRelu）处理后，为了保证输出层的输入数据的准确性，还需要进行反量化（Dequantize）的操作，将权重再转回32位精度，来保证预测的准确性。也就是整个模型的前向传播采用8位整数运行，在最后一层之前加上一个反量化层，把8位转回32位作为输出层的输入。

实际上，我们会在每个量化操作（如QuantizedMatMul、QuantizedRelu等）的后面执行反量化操作（Dequantize），如图19-3左侧所示，在QuantizedMatMul后执行反量化和量化操作可以相互抵消。因此，如图19-3右侧所示，在输出层之前做一次反量化操作就可以了。

图19-2

图19-3

3．量化数据的表示

将浮点数转换为8位的表示实际上是一个压缩问题。实际上，权重和经过激活函数处理过的上一层的输出（也就是下一层的输入）实际上是分布在一个范围内的值。量化的过程一般是找出最大值和最小值后，将分布在其中的浮点数认为是线性分布，做线性扩展。因此，假设最小值是-10.0f，最大值是30.0f，那量化后的结果如表19-1所示。

表19-1

量化后的值

原始的浮点数

0去

-10.0

255去

30.0

128

10.0

19.1.2　优化矩阵乘法运算

谷歌公司开源了一个小型独立的低精度通用矩阵乘法（General Matrix to Matrix Multiplication，GEMM）库——gemmlowp[4]。

19.2　iOS系统实践

本节先带领读者编译完成在iOS系统上需要的TensorFlow程序，然后真实地编译一个能在Xcode模拟器中运行的图片识别模型，随后讲解使用自己的数据，如何在PC端训练好一个模型，经过3道工序的模型优化后，编译成iOS支持的模型，并生成iOS工程文件安装在iPhone上运行。

19.2.1　环境准备

需要运行在操作系统Mac OS X上的集成开发工具Xcode，7.3（含）以上版本即可。图19-4是笔者所用的版本。

图19-4

随后，需要编译包含TensorFlow核心的静态库，在tensorflow-1.1.0目录下运行：

tensorflow/contrib/makefile/download_dependencies.sh

运行时会把相应的依赖库下载到tensorflow/contrib/makefile/downloads/目录下：

├─── eigen        # C++开源矩阵计算工具
├─── gemmlowp    # 小型独立的低精度通用矩阵乘法（GEMM）库
├─── googletest # 谷歌开源的C++测试框架
├─── protobuf    # 谷歌开源的数据交换格式协议
└─── re2          # 谷歌开源的正则表达式库

19.2.2　编译演示程序并运行

在tensorflow-1.1.0目录下运行：

tensorflow/contrib/makefile/build_all_ios.sh

经过编译后会生成一个静态库，位于tensorflow/contrib/makefile/gen/lib下，如下：

├─── ios_ARM64
├─── ios_ARMV7
├─── ios_ARMV7S
├─── ios_I386
├─── ios_X86_64
└─── libtensorflow-core.a

这时就可以在Xcode的模拟器上以及iOS设备上运行App的预测示例了。

在TensorFlow的iOS示例[5]中，共有3个目录，代表3个示例，其中benchmark目录是预测的基准示例，simple目录是图片的预测示例，camera目录是视频流实时预测示例。

从官方网站[6]上下载Inception V1模型，这是在ImageNet上训练好的能识别1000类图片的识别模型。

把解压后的Inception V1模型分别复制到benchmark、simple、camera下的data目录中，分别进入这3个目录，运行目录下后缀名为xcodeproj的文件。如图19-5所示，进入simple目录中运行。

图19-5

这个程序识别的图像是我的一张书包照片（如图19-6所示），将它放在tensorflow/contrib/ ios_examples/simple/data目录下，并命名为grace_hopper.jpg。

图19-6

如图19-7所示，选择iPhone 7 Plus模拟器，并点击左上角运行标志，编译完成后，模拟器的页面会出现一个“Run Model”按钮，每单击一次按钮就进行一次预测，预测结果见Xcode的控制台。

图19-7

可以看出，识别结果背包（backpack）的概率是0.754，第二个预测结果是邮袋（mailbag），概率是0.191。

19.2.3　自定义模型的编译及运行

如果我们想要训练自己的模型在手机端做预测，该如何做呢？这里我们假设想在手机上有一个实时花卉识别模型，当打开App时，摄像头对准某束花，App立刻告诉我这束花的品种。[7]

我们从官方网站下载花卉数据[8]。解压放在/tmp下后，可以看到郁金香（tulips）、玫瑰（roses）、蒲公英（dandelion）、向日葵（sunflowers）、雏菊（daisy）5种花卉的文件目录，每个目录中存放着近800张该花卉品种的图片，如下：

├──── LICENSE.txt
├──── daisy
├──── dandelion
├──── roses
├──── sunflowers
└──── tulips

1．训练原始模型

我们使用TensorFlow官方网站提供的预训练好的Inception V3模型[9]在此花卉数据集上进行训练。在项目根目录tensorflow-1.1.0下执行：

python tensorflow/examples/image_retraining/retrain.py \
--bottleneck_dir=/tmp/bottlenecks/ \
--how_many_training_steps 10 \
--model_dir=/tmp/inception \
--output_graph=/tmp/retrained_graph.pb \
--output_labels=/tmp/retrained_labels.txt \
--image_dir /tmp/flower_photos

训练完成后，可以在/tmp下看到生成的模型文件retrained_graph.pb（大小为83 MB）和标签文件retrained_labels.txt。

我们看到，上述命令行中存储和使用了“瓶颈”（bottlenecks）文件。瓶颈是用于描述实际进行分类的最终输出层之前的层（倒数第二层）的非正式术语。倒数第二层已经被训练得很好，因此瓶颈值会是一个有意义且紧凑的图像摘要，并且包含足够的信息使分类器做出选择。因此，在第一次训练的过程中，retrain.py文件的代码会先分析所有的图片，计算每张图片的瓶颈值并存储下来。因为每张图片在训练的过程中会被使用多次，因此在下一次使用的过程中，可以不必重复计算。这里用tulips/9976515506_d496c5e72c.jpg为例，生成的瓶颈文件为tulips/ 9976515506_d496c5e72c.jpg.txt，内容如图19-8所示。

图19-8

2．编译成iOS支持的模型[10]

这里，从原始模型到iOS支持的模型将经过3个阶段的处理。首先是去掉iOS系统不支持的操作，并优化模型；然后将模型进行量化，权重变为8位的常数，缩小模型；最后对模型做一个内存映射。我们接下来分步骤细讲。

（1）去掉iOS不支持的操作并优化模型。

因为移动设备的内存资源稀缺，并且还需要下载应用程序，所以默认情况下，iOS版本的TensorFlow仅支持在预测阶段中常见的且没有很大的外部依赖关系的操作。官方网站维护了一份支持的操作列表[11]。

目前有个操作DecodeJpeg不被支持，这个操作是用来对JPEG格式的图片进行解码的，但是它的实现依赖于libjpeg，libjpeg在iOS上的支持非常麻烦，并且还会增加二进制的占用空间。同时，对于本次的应用来说，我们希望从摄像头中实时识别出花卉的种类，直接处理相机的图像缓冲区，不需要先存成JPEG文件，然后再解码。

而恰好我们基于预训练模型Inception v3是从图片数据集中训练而得，模型是包含了DecodeJpeg操作。所以需要把输入数据直接供给（feed）发生在Decode后的Mul操作来绕过Decode操作。

而恰好我们基于预训练模型Inception V3训练的模型包含了DecodeJpeg操作，所以把输入数据直接填充（feed）到发生在解码后的Mul操作来绕过解码操作。

这个步骤也会做一些优化来加速预测，例如，将显式批处理规范化（explicit batch normalization）操作合并到卷积权重中，以减少计算次数。命令如下：

bazel build tensorflow/python/tools:optimize_for_inference
bazel-bin/tensorflow/python/tools/optimize_for_inference \
--input=/tmp/retrained_graph.pb \
--output=/tmp/optimized_graph.pb \
--input_names=Mul \
--output_names=final_result

在/tmp/生成模型optimized_graph.pb（大小为83 MB），可以通过label_image命令来预测，验证模型的有效性，如下：

bazel-bin/tensorflow/examples/label_image/label_image \
--output_layer=final_result \
--labels=/tmp/retrained_labels.txt \
--image=/tmp/flower_photos/daisy/5547758_eea9edfd54_n.jpg \
--graph=/tmp/optimized_graph.pb
--input_layer=Mul

结果如图19-9所示，模型还是有效的。

图19-9

（2）量化模型。

尽管苹果系统在.ipa包中分发应用程序，所有应用程序中的资源都会使用zip压缩。 但通常模型不能很好地压缩，因此将模型权重从浮点数转化为整数（范围为0～255），会损失一些准确度，但通常小于1%。运行命令如下：

bazel build tensorflow/tools/quantization:quantize_graph
bazel-bin/tensorflow/tools/quantization/quantize_graph \
--input=/tmp/optimized_graph.pb \
--output=/tmp/rounded_graph.pb \
--output_node_names=final_result \
--mode=weights_rounded

在/tmp下生成模型rounded_graph.pb，大小为83MB，但是经过zip命令压缩能达到23 MB。

（3）内存映射。

我们需要处理的最后一个步骤是内存映射[12]（memory mapping）。如果App把83 MB的模型全部一次性加载到内存缓冲区，光加载这一步就会对iOS的RAM上施加很大的压力，同时，操作系统会不可预知地杀死使用内存过多的应用程序。我们需要使用的模型权值缓冲区是只读的，因此可以把它映射到内存中。当有内存压力时，操作系统可以直接释放些内存，来避免系统直接崩溃。

如何来做呢？需要重新排列模型，使权重能够分部分逐块地从主GraphDef加载到内存中。运行命令如下：

bazel build tensorflow/contrib/util:convert_graphdef_memmapped_format
bazel-bin/tensorflow/contrib/util/convert_graphdef_memmapped_format \
--in_graph=/tmp/rounded_graph.pb \
--out_graph=/tmp/mmapped_graph.pb

这时会在/tmp下生成模型mmapped_graph.pb（大小为83 MB）。

3．生成iOS工程文件并运行

我们使用19.2.2节中粗略介绍过的视频流实时预测的演示程序的例子[13]，将模型文件和标记文件复制到tensorflow-1.1.0/tensorflow/contrib/ios_examples/camera/data目录下。

然后修改tensorflow-1.1.0/tensorflow/contrib/ios_examples/camera下的文件CameraExample ViewController.mm，更改要加载的模型文件名称、输入图片的尺寸、操作节点的名字以及缩放像素大小等。如下：

static NSString* model_file_name = @"mmapped_graph";
static NSString* model_file_type = @"pb";

const bool model_uses_memory_mapping = true;

static NSString* labels_file_name = @"retrained_labels";
static NSString* labels_file_type = @"txt";
// 以下尺寸需要和模型训练时相匹配
const int wanted_input_width = 299;
const int wanted_input_height = 299;
const int wanted_input_channels = 3;
const float input_mean = 128.0f;
const float input_std = 128.0f;
const std::string input_layer_name = "Mul";
const std::string output_layer_name = "final_result";

最后连上iPhone手机，就可以双击tensorflow-1.1.0/tensorflow/contrib/ios_examples/camera/ camera_example.xcodeproj编译并运行了。运行后，会在手机上安装好App，打开App并找到玫瑰花来识别，识别结果如图19-10所示。

图19-10

这里概率并不是很高，原因是我在训练的时候迭代次数只设置了10次，设置10 000次后，可以使识别概率达到99%以上。

那么，这个安装到iOS系统上的工程文件里面究竟包含了什么，才达到用摄像头实时识别的目的呢?生成的打包好的工程文件位于/Users/lijiaxuan/Library/Developer/Xcode/DerivedData/ camera_example-dwwvzqamrwtfblfprxmxpvwasgin/Build/Products/Debug-iphoneos下，如图19-11所示。

图19-11

我们打开CameraExample.app文件一探究竟。里面就包含了安装在iPhone上的可执行文件CameraExample以及以资源文件方式存储的模型文件mmapped_graph.pb和标记文件retrained_labels.txt，如图19-12所示。

图19-12

19.3　Android系统实践[14]

本节先带领读者准备好Android系统的编译环境，然后再真实地编译一个能在Android手机上运行的图片识别模型，随后讲解使用自己的数据，如何在PC端训练好一个模型，经过模型优化后，编译成Android支持的模型，并生成Android apk文件安装在Android手机上运行。

19.3.1　环境准备

下面就一步一步地介绍如何在有摄像头的Android设备上运行TensorFlow的图片分类器。我们先来搭建环境，需要依赖Java、Android SDK、Android NDK、Bazel。这里仍然使用Mac Pro笔记本进行演示，其他操作系统的搭建环境与此类似。

1．搭建Java环境

从Oracle官方网站下载JDK 1.8版本[15]，得到jdk-8u111-macosx-x64.dmg文件，双击进行安装，如图19-13所示。

图19-13

安装完毕后，设置Java的环境变量，如下：

JAVA_HOME=`/usr/libexec/java_home`
export JAVA_HOME

2．搭建Android SDK环境

从Android官方网站[16]下载Android SDK，这里使用的是25.0.2版本，得到文件android- sdk_r25.0.2-macosx.zip，然后直接解压，放在~/Library/Android/sdk目录下。解压后里面的目录如下：

├──── build-tools
├──── extras
├──── patcher
├──── platform-tools    # 各版本SDK。有根据API Level划分的SDK版本
├──── platforms
├──── sources
├──── system-images
├──── temp              # 临时文件夹，一般在SDK更新安装时用到
└──── tools             # 各版本通用的SDK工具。含有adb、aapt、aidl、dx等文件

3．搭建Android NDK环境

从Android官方网站[17]下载Android NDK的Mac OS X版本，得到android-ndk-r13b-darwin-x86_64.zip文件，直接解压即可。解压后得到的目录如下：

├──── CHANGELOG.md
├──── build
├──── ndk-build
├──── ndk-depends
├──── ndk-gdb
├──── ndk-stack
├──── ndk-which
├──── platforms
├──── prebuilt
├──── python-packages
├──── shader-tools
├──── simpleperf
├──── source.properties
├──── sources
└──── toolchains

4．搭建Bazel

直接使用brew安装bazel，如下：

brew install bazel

若已安装，用如下方式更新：

brew upgrade bazel

19.3.2　编译演示程序并运行

至此，编译Android手机的apk文件需要用的环境都已经搭建好了。下面就来编译一个TensorFlow的图片分类演示程序，并在Android手机上运行。相关演示程序在https://github.com/ tensorflow/tensorflow/tree/master/tensorflow/examples/android。

1．编译演示程序

首先，修改tensorflow-1.1.0的根目录中的WORKSPACE文件。将android_sdk_repository和android_ndk_repository的配置改为用户自己的安装目录及版本。具体如下：

android_sdk_repository(
name = "androidsdk",
api_level = 25,
build_tools_version = "25.0.2",
# Replace with path to Android SDK on your system
path = "~/Library/Android/sdk",
)

android_ndk_repository(
name="androidndk",
path="~/Downloads/android-ndk-r13b",
api_level=23)

修改完毕后，在根目录用bazel构建：

bazel build //tensorflow/examples/android:tensorflow_demo

运行结果如下：

Target //tensorflow/examples/android:tensorflow_demo up-to-date:
bazel-bin/tensorflow/examples/android/tensorflow_demo_deploy.jar
bazel-bin/tensorflow/examples/android/tensorflow_demo_unsigned.apk
bazel-bin/tensorflow/examples/android/tensorflow_demo.apk
INFO: Elapsed time: 39.357s, Critical Path: 15.21s

在编译成功之后，默认会在tensorflow-1.1.0/bazel-bin/tensorflow/examples/android 目录下面生成我们想要的TensorFlow演示程序，如图19-14所示。

图19-14

下面我们就把生成的apk文件安装到Android手机上并运行。

2．运行

将生成的apk文件传输到手机上，利用手机摄像头看看效果。这里采用的是小米Note手机，搭载Android 6.0.1版本，并且需要开启“开发者模式”。将手机用数据线与计算机相连，进入SDK所在的目录下，进入platform-tools文件夹，找到adb命令，并执行：

./adb install tensorflow-0.12/bazel-bin/tensorflow/examples/android/tensorflow_demo.apk

这样就将tensorflow_demo.apk自动安装到手机上了，在手机上会生成图19-15所示的两个App图标。

打开TF Detect的App，App会调起手机摄像头，对摄像头返回的数据流进行实时监测，监测结果如图19-16所示。

图19-15

图19-16

看起来精度并不是很高，与手机摄像头的像素有关系。读者可以定制自己的图片分类器，训练好模型后，重新编译apk即可。

19.3.3　自定义模型的编译及运行

本节和19.2.3节中的步骤基本相同，仍然分为3步：训练原始模型、编译成Android系统支持的模型，以及生成Andriod apk文件并运行。

其中，训练原始模型和编译成Android系统支持的模型的过程都是相同的，因为都是使用项目根目录下的tensorflow/python/tools/optimize_for_inference.py、tensorflow/tools/quantization/ quantize_graph.py、tensorflow/contrib/util/convert_graphdef_memmapped_format.cc分别对模型进行优化。这里，我们直接将第一步生成的原始模型文件retrained_graph.pb和标记文件retrained_labels.txt放在tensorflow/examples/android/assets目录下。

需要修改tensorflow/examples/android/src/org/tensorflow/demo/TensorFlowImageClassifier.java中要加载的模型文件名称，输入图片的尺寸、操作节点的名字和如何缩放像素大小等，这和19.2.3节非常类似，只是是用Java语言实现的。方法如下：

private static final int INPUT_SIZE = 299;
private static final int IMAGE_MEAN = 128;
private static final float IMAGE_STD = 128;
private static final String INPUT_NAME = "Mul:0";
private static final String OUTPUT_NAME = "final_result:0";
private static final String MODEL_FILE = "file:///android_asset/retrained_graph.pb";
private static final String LABEL_FILE = "file:///android_asset/retrained_labels.txt";

然后重新编译apk，连接上Android手机后，安装apk，方法如下：

bazel build //tensorflow/examples/android:tensorflow_demo
adb install -r -g bazel-bin/tensorflow/examples/android/tensorflow_demo.apk

运行结果仍如图19-10所示。

19.4　树莓派实践

TensorFlow还可以在树莓派（Raspberry Pi）上运行，树莓派是只有信用卡大小的微型电脑，系统基于Linux，它也有音频和视频功能。现在有很多树莓派的应用，例如，输入1万张自己的面部图片，在树莓派上训练人脸识别的模型，教会它认识你后，可以在你进入家门后，帮你开灯、播放音乐等各种功能。树莓派上的编译方法和直接在Linux环境上使用十分相似，读者可以自己参考文档。

19.5　小结

本章讲解了移动端模型的应用原理，重点讲解了量化这一重要思想，介绍了TensorFlow在iOS和Android移动端的应用，并采用TensorFlow的工具对生成的原始模型进行优化、量化和内存映射。其原理都是借助有摄像头的手机来做分类和物体识别。随着移动端手机性能的提高，在移动端做深度学习会有很大前景。

[1]　im2col的主要功能是对索引的图像块重排列为矩阵列。它是先将一个大矩阵，重叠地划分为多个子矩阵，对每个子矩阵序列化成向量，最后得到另一个矩阵。

[2]　本节参考自官方网站《How to Quantize Neural Networks with TensorFlow》：https://www.tensorflow.org/performance/quantization 。

[3]　下载路径http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz。

[4]　库地址为https://github.com/google/gemmlowp 。

[5]　代码位于https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/ios_examples/ 。

[6]　模型的下载地址为https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip 。

[7]　本节讲解的内容部分参考官方网站https://www.tensorflow.org/tutorials/image_retraining 。

[8]　http://download.tensorflow.org/example_images/flower_photos.tgz

[9]　模型地址为http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz 。

[10]本小节内容参考https://petewarden.com/2016/09/27/tensorflow-for-mobile-poets/ 。

[11]支持的操作列表参见https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/makefile/tf_op_files.txt 。

[12]　内存映射是指把物理内存映射到进程的地址空间之内，随后应用程序就可以直接使用输入/输出的地址空间，从而提高读写的效率。

[13]地址为https://github.com/tensorflow/tensorflow/tree/master/tensorflow/contrib/ios_examples/camera 。

[14]　本节有部分参考官方网站https://github.com/tensorflow/tensorflow/tree/master/tensorflow/examples/android/ 。

[15]　http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

[16]　https://developer.android.com

[17]　https://developer.android.com/ndk/downloads/index.html

第20章　TensorFlow的其他特性

随着TensorFlow的版本不断迭代，目前它已经有很多新特性。除了第15章和第16章介绍的XLA和Debugger外，还有一个非常好的生产系统使用模型服务系统——TensorFlow Serving，以及支持动态图计算的TensorFlow Fold。此外，还有一些基于硬件的优化方法也是目前人工智能发展的趋势。本章就来介绍一下这些内容。

20.1　TensorFlow Serving[1]

TensorFlow Serving是专为生产环境设计的一种灵活、高性能的机器学习模型服务系统。它非常适合基于实际情况的数据大规模地运行，会产生多个模型的训练过程。

TensorFlow Serving可以用于开发过程和生产过程，有以下两个主要作用。

（1）对模型的生命周期进行管理。一个模型一般是先数据训练，然后逐步产生初步的模型，随后优化模型。图20-1所示为持续的模型训练流。

图20-1

（2）当模型采用多重算法进行试验时，对生成的模型进行管理。当客户端（Client）向TensorFlow Serving请求模型后，TensorFlow Severing会返回适当的模型给客户端。图20-2所示为TensorFlow Severing架构关系图。

图20-2

另外，TensorFlow主要支持Python和C++，最近又增加了对Java、Ruby和Go的支持。但是，大部分用户可能是基于Python API来构建图和训练模型的，直接使用其他编程语言访问模型比较困难。有了TensorFlow Serving和gRPC[2]，就可以提供跨语言的RPC接口，使用Java、Go甚至是Ruby都可以直接访问这些模型。

TensorFlow Serving的代码位于https://github.com/ tensorflow/serving，安装过程可以采用源代码Bazel编译安装[3]，或者采用Docker安装。具体的过程可以参考TensorFlow官方网站[4]。

在第17章我们讲解了通过Kubernetes来实现TensorFlow集群，其实还可以结合TensorFlow Serving，为训练好的模型创建一个Docker镜像，将它推送到Google Container Registry[5]上，这样模型就可以在谷歌云平台（Google Cloud Platform）上运行了，也就是说，在Kubernetes里成功部署了模型的服务。详细的安装方法可以参考TensorFlow官方网站的“Serving Inception Model with TensorFlow Serving and Kubernetes”[6]。另外，谷歌还提供了Google ML Engine，这是一个全托管的TensorFlow平台，能把训练的模型一键转换为预测服务。

20.2　TensorFlow Flod[7]

一般的深度学习过程是，先对模型的训练数据进行预处理，把不同结构的数据剪裁成相同的维度和尺寸，划分为一批批的，进入到训练流程里。

这种训练方式称为“静态图模型”。它的缺点在于，如果输入数据无法进行一般的预处理，模型就必须针对不同的输入数据建立不同的计算图（computation graph）分别进行训练，对处理器、内存和高速缓存都没有很好地利用。

TensorFlow Fold[8]可以根据不同结构的输入数据建立动态计算图（dynamic computation graph），根据每个不同的输入数据建立不同的计算图。动态批处理（dynamic batching）功能会自动组合这些计算图，实现在输入数据内部的批处理，也就是批处理单个输入图内的不同节点，以及不同输入数据之间的批处理，也就是批处理不同输入图之间的运算。同时还可以通过插入一些附加指令来在不同批处理操作之间移动数据。这简化了模型训练阶段对输入数据的预处理过程。这些批处理操作的优势赋予模型后，在CPU的模型的运行速度提高了10倍以上，GPU上提高了100倍。

Tensorflow的动态图计算的提出被认为是第一次清晰地在设计理念上领先其他深度学习框架。

20.3　TensorFlow计算加速

使用TensorFlow框架训练时使训练加速的方法有很多。例如，可以通过用GPU的设备来实现，可以通过XLA框架对部分OP进行融合，还可以采用分布式的方式将计算部分和参数部分分布到不同机器上来提升性能。同时，还可以利用硬件来计算，如利用CPU的更高级的指令集，如SSE、AVX等，用FPGA编写支持TensorFlow的计算单元等。这里我们先讲解一下在CPU的情况下如何进行优化，使用CPU运行时可能出现哪些警告，提出优化的方向，随后讲解FPGA的加速原理。

20.3.1　CPU加速

如果是直接采用pip命令的方式安装的，可能会报出以下警告：

2017-02-26 13:27:48.579303: W tensorflow/core/platform/cpu_feature_guard.cc:45]
The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are
available on your machine and could speed up CPU computations.
2017-02-26 13:27:48.579512: W tensorflow/core/platform/cpu_feature_guard.cc:45]
The TensorFlow library wasn't compiled to use SSE4.2 instructions, but these are
available on your machine and could speed up CPU computations.
2017-02-26 13:27:48.579519: W tensorflow/core/platform/cpu_feature_guard.cc:45]
The TensorFlow library wasn't compiled to use AVX instructions, but these are
available on your machine and could speed up CPU computations.
2017-02-26 13:27:48.579524: W tensorflow/core/platform/cpu_feature_guard.cc:45]
The TensorFlow library wasn't compiled to use AVX2 instructions, but these are
available on your machine and could speed up CPU computations.
2017-02-26 13:27:48.579528: W tensorflow/core/platform/cpu_feature_guard.cc:45]
The TensorFlow library wasn't compiled to use FMA instructions, but these are
available on your machine and could speed up CPU computations.

出现上述问题的原因是：为了尽可能与更广泛的机器兼容，TensorFlow默认仅在x86机器上使用SSE4.1 SIMD指令，而大多数现代PC和Mac都支持更高级的指令。因此，通过源代码安装可以获得最大性能，可以开启CPU的高级指令集（如SSE、AVX）的支持。可以使用bazel构建一个只能在自己的机器上运行的二进制文件，如下：

bazel build -c opt --copt=-mavx --copt=-mavx2 --copt=-mfma --copt=-mfpmath=both --copt=-msse4.2 --config=cuda -k //tensorflow/tools/pip_package:build_pip_package
bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg

此时会在/tmp/tensorflow_pkg下产生一个wheel文件，再用pip命令安装这个wheel文件。

20.3.2　TPU加速和FPGA加速

谷歌目前为TensorFlow设计了专用集成芯片——张量处理单元（Tensor Processing Unit，TPU）。

我们知道，CPU进行逻辑运算（如if else）的能力很强，但是纯粹的计算能力与GPU相比就差一些，而深度学习恰恰需要做海量的计算。

GPU在一些场景下运算速度比CPU要快，一方面是因为GPU有强大的浮点计算单元，GPU的着色器（shader）是对一批数据以相同的步调执行相同的指令流水，准确地讲，GPU在同一时钟周期能执行的指令的数量是千级别的，好的GPU大约是3000多条，而CPU在同一时钟周期内能执行的指令数量是几十级别的，因此远超CPU指令集的数据并行能力，作为代价就是GPU做if else能力很弱，原因就是它流水线并行能力（同一时钟周期并发执行不同逻辑序列的能力）很差，它需要这一批数据同步调地执行同样的逻辑。

而神经网络刚好需要这样的大规模数据并行的能力，尤其是CNN中卷积、矩阵运算之类的操作，都是通过数据并行可大幅提高性能的。

但是，因为GPU出厂后架构固定，硬件原生支持的指令就固定了，所以，如果神经网络中有GPU不支持的指令，GPU就不能直接实现。例如，如果想要计算矩阵乘法，但是若GPU只支持加法和乘法，那就只能用软件模拟的方法用加法和乘法来模拟矩阵乘法。

FPGA的加速就在于，虽然硬件中可能有不支持的指令，但是开发者可以在FPGA里编程，改变FPGA的硬件结构来使它支持。

那是不是GPU支持足够多的基础指令，就能打平FPGA了呢？不是。FPGA加速在于它和GPU和CPU的体系结构就不同。FPGA不是冯·诺伊曼结构的，而是一个代码描述的逻辑电路。FPGA只要片上逻辑门和引脚够多，全部输入、运算和输出都在一个时钟周期内完成。FPGA因为一个时钟周期执行一次全部烧好的电路，某种角度来说它一个模块就一句超复杂“指令”，所以不同模块也算是不同逻辑序列，并且这个序列里就一条指令。单元间通信这些冯·诺伊曼结构的东西对于FPGA都不是问题，不同运算单元间本身就硬件直连，所以才能做到数据并行和流水线并行共存（GPU流水线并行能力约为0），真的要比单算浮点运算能力，当前常见的FPGA不比GPU好。因此，如果是需要低延迟的预测推理，每批大小比较小时，FPGA更合适。

而TPU和FPGA类似，它是一种专用集成电路（application specific integrated circuit，ASIC），但是硬件逻辑一旦烧写完毕就不可以再编程，是专门为TensorFlow做深度学习开发的。从TPU目前的版本来看，还不能完整运行Tensorflow的功能，它的目的是高效地完成预测推理，还不涉及训练。

20.4　小结

本章介绍了TensorFlow的其他新特性，如TensorFlow Serving、TensorFlow Flod，还介绍了TensorFlow的硬件加速方法，例如，当计算机只有CPU时，开发人员可以利用CPU的更高级的指令集；当有FPGA时，开发人员可以在FPGA里编程，改变硬件结构来支持神经网络的指令；当有TPU时，它完美地支持TensorFlow的所有运算，效率最高。

[1]　本节参考TensorFlow官方网站文档：https://tensorflow.github.io/serving/ 。

[2]　gRPC是谷歌公司开源的一个高性能、跨语言的RPC框架。

[3]　与编译安装TensorFlow类似，安装方法见https://github.com/tensorflow/serving/blob/master/tensorflow_serving/g3doc/setup.md。

[4]　https://tensorflow.github.io/serving/setup

[5]　https://cloud.google.com/container-registry/docs/

[6]　https://tensorflow.github.io/serving/serving_inception

[7]　本节参考论文《Deep Learning with Dynamic Computation Graphs》：https://openreview.net/pdf?id=ryrGawqex。

[8]　https://github.com/tensorflow/fold

第21章　机器学习的评测体系

当我们训练完一个模型之后，如何评价这个模型的好坏呢？准确率是一个评价标准，但它仅仅是相对于这个模型对测试集的预测结果。抛开这些，如何看待这个模型在解决语音或图像的某个具体问题时是否能发挥作用呢？这就涉及评价模型的性能指标。本章主要讲解人脸识别和智能聊天机器人的性能指标，以及机器翻译的评价方法和常用的通用评价指标。

21.1　人脸识别的性能指标

人脸识别的主要性能指标包括鉴别性能和验证性能。

（1）鉴别性能就是指是否鉴别准确。具体性能指标有以下几个。

- Top-K识别率：就是在给出的前K个结果中包含正确结果的概率。

- 错误拒绝辨识率（FNIR）：指注册用户被系统错误辨识为其他注册用户的比例。

- 错误接受辨识率（FPIR）：非注册用户被系统辨识为某个注册用户的比例。

（2）验证性能是指验证人脸模型是否足够好。性能指标主要有以下两个。

- 误识率（False Accept Rate，FAR）：就是将其他人误作指定人员的概率。

- 拒识率（False Reject Rate，FRR）：就是将指定人员误作其他人员的概率。

除此之外，还有识别速度（识别一副人脸图像的时间、识别一个人的时间）、注册速度（注册一个人的时间）等衡量人脸识别技术的指标。

21.2　聊天机器人的性能指标

如何对聊天机器人智能程度进行评价是一项挑战。目前采用的通用的客观评价标准有：回答正确率、任务完成率、对话回合数、对话时间、系统平均响应时间、错误信息率等，但是评价的基本单元是单轮对话。同时，由于人机对话过程是一个连续的过程，输入首句后对话展开，不同系统的回复不尽相同，因此不能简单地将连续对话切分成单轮对话去评价。因此，在形成客观标准之前，设计合理的人工主观评价也是对聊天机器人智能程度评价标准的补充。[1]

一个好的聊天机器人应该具有以下特点。

（1）机器人的答句和用户的问句应该语义一致，语法正确，并且逻辑正确。

（2）机器人的答句应该是有趣的、多样的，而不是一直产生一些安全回答，如“好呀”“是呀”之类。

在这两个特点上，微软小冰就表现得很好。我常常用一句重复的话逗它，但它回复非常多样而且有趣，有时候甚至还会“训斥”我总是重复一句话，并且还能给出图片、各种角度的多样回答，能够对话多轮，而不会重复答句，给人对方词穷的感觉。例如，图21-1给出的是我和小冰的一次对话。

图21-1

事实上，即使是祝福语，20多轮对话的多样性也是令人咋舌的。我还尝试过其他产品，重复问句，带来的回答往往都是“谢谢，你也是”或者“也祝你生日快乐”这样不搭边的回答，并且在三四轮对话后，机器人就产生重复回答了。

（3）机器人应该“个性表达一致”。它的年龄、身份、出生地等基本背景信息以及爱好、语言风格应该一致，能让人把它想象成一个典型的人。例如，微软小冰关于性别的回答，在多轮会话中有些不一致，但总体上个性是一致的，如图21-2所示。

图21-2

21.3　机器翻译的评价方法

现在机器翻译方法越来越多，那么如何评价一个机器翻译方法的好坏呢？

最初是用人工评测的方法，在得到翻译结果后，请专家来为每个句子的翻译结果打分，然后统计平均分。这种方法存在两个问题，一是由两批专家打分，即使一批专家给两个翻译结果打分，也无法保证打分尺度一致；二是打分周期让系统迭代的周期变长。

下面我们就来介绍两种自动评价方法。

21.3.1　BLEU

这里讲的BLEU（bilingual evaluation understudy）方法是在2002年由IBM的沃森研究中心提出的。BLEU是Bilingual Evaluation Understudy的英文缩写。它的核心思想是：机器翻译语句与人类的专业翻译语句越接近就越好。

这个自动评价方法与人工评价高度相关。我们把正确的句子叫作参考译文（reference），也称正确句子（golden sentence），测试的句子叫作候选译文（candidate），这种方法适用于一个测试语料中具有多个参考译文的情况。

我们比较参考译文与候选译文中相同的片段的数量，用参考译文中连续出现的N元组（N个单词或字）与候选译文中出现的N元组进行比较，也称为n单位片段（n-gram）比较，计算完全匹配的N元组的个数与参考译文中N元组的总个数的比例，这些匹配片段与它们在文字中存在的位置无关。匹配片段数越多，则待评价的候选译文的质量越好。因此，BLEU得分越高翻译质量越好。

21.3.2　METEOR

METEOR是另一个用来评价机器翻译输出质量好坏的方法。与BLEU不同，它不仅要求候选译文在整个句子上，而且在句子的分段级别上，也要与参考译文的更接近。

我们来看维基百科上的一个例子[2]。METEOR方法在待评价字符串和参考字符串之间创建一个平面图，如图21-3所示。

图21-3

在待评价翻译中的每个一元组必须映射到参考翻译中的1个或0个一元组，如果有两个平面图的映射数量相同（如图21-3所示），那么选择映射交叉数目较少的那个。也就是说，图21-3左侧的会被选中。METEOR得分越高质量越好。图21-4所示为维基百科上面的比较结果。

图21-4

21.4　常用的通用评价指标

对于深度学习的分类程序来说，常用的评价指标有准确率、召回率、F值、ROC、AUC、AP和mAP等。准确率、召回率和F值过于简单，这里就不赘述，下面主要讲ROC、AUC、AP和mAP。

21.4.1　ROC和AUC

ROC（Receiver Operating Characteristic，受试者工作特征曲线）和AUC（Area Under roc Curve，曲线下面积）是评价分类器的指标。图21-5展示的是摘自维基百科[3]的ROC曲线示例。如图21-5所示，ROC曲线的横坐标为FPR（False positive rate），纵坐标为TPR（True positive rate）。ROC曲线越接近左上角，分类器的性能就越好。

图21-5

AUC是ROC曲线下方的那部分面积的大小。通常，ROC曲线一般都处于y = x这条直线的上方，因此AUC的值介于0.5和1.0之间，AUC的值越大表示性能越好。有一些专门的AUC计算工具，参见http://mark.goadrich. com/programs/AUC/。

21.4.2　AP和mAP

在计算机视觉中，尤其在分类问题中，AP（average precision，平均准确性）是模型分类能力的重要指标。如果单纯用P（precision rate，准确率）和R（recall rate，召回率）来评价，组成的PR曲线有一个趋势就是，召回率越高准确率越低。AP指这个曲线下的面积，等于对召回率做积分，而mAP（mean average precision，平均准确性平均）也是为了解决准确率、召回率、F值的单点值局限性的，它对所有类别分别取平均，把每一个类当作一次二分类任务。目前的图像分类论文基本都是用mAP的高低作为分类好坏的标准。

21.5　小结

本章介绍了机器学习的评测体系。包括“实战篇”中人脸识别的性能指标、聊天机器人的性能指标，以及机器翻译的评价方法。另外，还介绍了几个常用的通用评价指标，即ROC、AUC、AP和mAP。但是，对一个具体的行业应用来说，性能指标和评价方法不是一成不变的，随着模型准确率和召回率的提高，对模型的评价维度也会更全面，读者可以多多关注相关科研成果。

[1]　这段内容参考http://sanwen.net/a/hkhptbo.html，发表在《中国人工智能学会通讯》2016年第6卷第1期上。

[2]　https://en.wikipedia.org/wiki/METEOR#Algorithm

[3]　https://en.wikipedia.org/wiki/Receiver_operating_characteristic#/media/File:Roccurves.png

附录A　公开数据集

为了方便读者进行更多实践，本附录给读者介绍一些可用的公开数据集。

A.1　图片数据集

ImageNet[1]是目前世界上最大的图像识别数据集，包含14197122张图像，由斯坦福大学视觉实验室终身教授李飞飞创立。每年的ImageNet大赛是国际上计算机视觉的顶级赛事。

COCO[2]是微软创立的用于分割和加字幕标注的数据集。其主要特征如下：

- 目标分割；

- 通过上下文进行识别；

- 每个图像包含多个目标对象；

- 超过300000个图像；

- 超过2000000个实例；

- 80种对象；

- 每个图像包含5个字幕；

- 包含100000个人的关键点。

CIFAR[3]（Canada Institude For Advanced Research）是由加拿大先进技术研究院收集的8 000万小图片的数据集。CIFAR包含CIFAR-10和CIFAR-100两个数据集。Cifar-10由60 000张32×32的RGB彩色图片构成，共10个类别，50 000张训练，10 000张测试（交叉验证）。CIFAR-100由60 000张图像构成，包含100个类别，每个类别600张图像，其中500张用于训练，100张用于测试。其中这100个类别又组成了20个大的类别，每个图像包含小类别和大类别两个标记。

A.2　人脸数据集

AFLW[4]（Annotated Facial Landmarks in the Wild）提供从Flickr收集的带标注的面部图像的大规模集合，包括了各种姿态、表情、光照、种族、性别、年龄等因素影响的图片，大约包括25 000万手工标注的人脸图片，每个人脸被标注了21个特征点，图像大多数是彩色的，59%为女性，41%为男性。该数据集非常适合用于人脸识别、人脸检测、人脸对齐等方面的研究。

Labeled Faces in the Wild Home[5]（LFW）数据集是由美国马萨诸塞大学阿姆斯特分校计算机视觉实验室整理而成的。它包含13 233张图片，共5 749个人，其中4 096个人只有一张图片，1 680个人的图片多于一张，主要用于研究非受限情形下的人脸识别问题，因为人脸的外形很不稳定，会受到面部表情、观察角度、光照条件、室内室外、遮盖物（口罩、眼睛、帽子等）、年龄等方面的影响。现在已经成为学术界评价识别性能的标准（benchmark）。

GENKI[6]数据集是由加利福尼亚大学收集的。该数据集包含GENKI-R2009a、GENKI-4K和GENKI-SZSL三个部分，其中GENKI-R2009a包含11 159个图片，GENKI-4K包含4 000个图像，分为“笑”和“不笑”两类，每个图片的人脸的姿势、头的转动都标注了角度，专门用于做笑脸识别。GENKI-SZSL包含3 500个图像，这些图片包括广泛的背景、光照条件、地理位置、个人身份和种族等。

VGG Face[7]数据集包含了2 622个不同的人，每个人包含1 000张图片，是一个训练人脸识别的大的数据集。

大规模名人人脸标注数据集CelebA[8]（Large-scale CelebFaces Attributes）包含10 177个名人，202 599张名人图像，每张图像都有40个属性标注。

A.3　视频数据集

YouTube-8M[9]数据集是一个不错的视频数据集，包含了800万个YouTube视频的URL，代表50万小时长度的视频，并带有视频标注。

A.4　问答数据集

MS MARCO[10]（Microsoft MAchine Reading Comprehension）是微软发布的一个包含 10 万个问题和答案的数据集，研究者可以使用这个数据集来创造能够像人类一样阅读和回答问题的系统。这个数据集是基于匿名的真实数据构建的。

康奈尔大学电影对白数据集[11]有超过600部好莱坞电影的对白。

A.5　自动驾驶数据集

法国国家信息与自动化研究所行人数据集[12]（INRIA Person Dataset），这个数据集是作为图像和视频中直立人检测的研究工作的一部分收集的，里面的图片分为两种格式：一种是具有对应注释文件的原始图像，另一种是具有原始负像的经过正规化处理后的64×128像素正像。图片分为只有车、只有人、有车有人和无车无人4个类别。

KITTI[13]（Karlsruhe Institute of Technology and Toyota Technological Institute）是一个车辆数据集，包含7 481个训练图片和7 518个测试图片。该数据集中标注了车辆的类型、是否截断、遮挡情况、角度值、二维和三维框、位置、旋转角度等重要的信息。

A.6　年龄、性别数据集

Adience数据集[14]来源为Flickr相册，由用户使用iPhone5或者其他智能手机设备拍摄，图片包含2 284个类别和26 580张图片，并且保留了光照、姿势、噪声的影响，是在做性别、年龄估计和人脸检测中运用算法时进行基准测试的一个数据集。

除以上这些数据集外，还有非常多的公开数据集，读者可以自己用搜索引擎去研究和探索。

[1]　http://www.image-net.org/

[2]　http://mscoco.org/

[3]　https://www.cifar.ca/

[4]　http://lrs.icg.tugraz.at/research/aflw/

[5]　http://vis-www.cs.umass.edu/lfw/

[6]　http://mplab.ucsd.edu

[7]　http://www.robots.ox.ac.uk/~vgg/data/vgg_face/

[8]　http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html

[9]　https://research.google.com/youtube8m/

[10]　http://www.msmarco.org

[11]　https://www.cs.cornell.edu/~cristian/Cornell_Movie-Dialogs_Corpus.html

[12]　http://pascal.inrialpes.fr/data/human/

[13]　http://www.cvlibs.net/datasets/kitti/

[14]　http://www.openu.ac.il/home/hassner/Adience/data.html

附录B　项目管理经验小谈

终于到本书的最后部分了，相信通过前面的学习你已经对TensorFlow的基础知识和实战了解得很全面了，那就快快动手结合自己的业务实现一个Demo吧。作为一个技术人员，你也不可避免地会遇到职场上的管理和流程问题，而且随着工作年限的提高，你可能早晚都要做一个纯管理者或者技术管理者，或者是一个被别人经常请教技术方案的人。本章我就说一些在工作中的项目管理经验。

B.1　管理的激进与保守问题

技术管理人员在设计技术架构及人员管理的时候，往往会有两种风格——激进派和保守派。下面我来说说这两种风格的特点，以及作为技术管理人员，如何针对具体的项目，用不同的风格来扬长避短。

B.1.1　激进派

这种风格表现在：项目上追求快速完成，不太注重项目持久性。这种风格的优点是，一个新点子往往能迅速上线，但弊端更多。

这样的管理者开头和分配任务的方式往往是一样的：

“小A，小B，小C，快快快，在这儿开个会。我们要做一个xxx，就是实现个xxx，小A，你做A部分，小B你做B部分，小C你做C部分。我们这个项目，2周，开发完，交给测试。测试一两天就应该能测完。10月底上线。”

然后，小A、小B、小C回到工位，开始火急火燎地开发。不，开始火急火燎地研究需求，设计系统方案，最后开始编码。

然后，真正到2周了，其实也开发完了，交付给测试后，会出现很多问题，然后他们分别拼命改。终于在最后期限——10月底上线了，可是，给用户一用，发现好多问题，紧急修复，紧急，紧急，身后一背虚汗。

这样的开发方式，弊端在哪里呢？

首先，因为留给基层开发人员的时间很短，所以开发人员在开发时无法深入地对系统架构有整体的思考，并且因为时间紧迫，开发人员都是“吃技术老本”来完成这个项目，没有从项目中学到新东西，也没有获得太大的成长。比如，之所以让小A负责A，小B负责B，小C负责C，是因为这部分是他们各自的擅长领域，开发人员在技术上做的是“重复的机械性的”工作。

其次，上线后会有各种诡异问题。这是因为，开发时对所有的逻辑没有想全，项目周期紧张，测试人员也是在测试当天前后才了解到这个项目，对项目的整体功能把握不透彻，就完全依赖开发人员的描述进行测试。一些异常状态以及跟之前系统可能有的交互的影响未考虑到，导致很多不易察觉的错误上线后才发现。这时，留给开发人员的活儿就是在线上定位和修改bug。因为这种问题往往是没有很明显复现条件的，所以定位问题的难度很大。这时开发人员就要重新梳理一遍自己的开发逻辑，从中发现可能的问题。

再次，由于时间紧张，开发人员往往不写“设计文档”，这对后期接手人员的工作带来很大困难。万一开发人员变动或者离职，接手人员有时就得重头“啃”代码，或者自己另写一套代码维护。

我称这种项目管理方式叫作“走2步，退1步半”。因此，开发的时候，好像很迅速，一个系统可能2周左右就开发完成，然后上线，管理人员和一线开发人员都皆大欢喜，发邮件抄送全组祝贺。可是，后续发现，问题维护成本也很高，大概也需要1周半的时间去维护系统的遗留问题，这些遗留问题被测试人员或者用户发现后，往往也需要一定的沟通成本来描述和定位。最重要的是，问题发生在线上后，影响了公司的品牌和声誉。

这种开发模式对“熟手”工程师来说可能成长不大，而对“生手”工程师来说，这种模式会倒逼他在某个时间点完成一项任务，在技术应用上学习和成长是很快的。但即使“熟手”也会出现上述问题，是因为技术本领的熟练，不足以应对开发这个系统时需要的团队配合以及考虑系统上下游的关系的问题，因为你做的很多设计，可能和你搭档的工程师并不知道，或者在整个系统中并不必要。因此，有的时候你不能走得太快，否则会遗失很多东西，到时候还得补回来。

B.1.2　保守派

保守派和激进派的做法几乎完全相反。下面我就举个例子来描述保守派项目管理的开发流程。例如，产品经理想给产品新加一个功能，他可能面临如下步骤。

（1）直接找到对应的技术人员去沟通。

（2）技术人员同意添加，并有能够解决问题的技术方案。但是，不要以为技术人员就可以直接干活写代码了，因为技术人员的上级还不知道这件事情，所以这个技术人员还没有相应的时间资源（即排期）去做。因此，需要再向技术主管确认这个功能是否要添加，以及主管安排这个技术人员的工作排期。随后，需要产品经理发出邮件，抄送对应的技术人员和双方主管，确认沟通内容，这样，沟通圆满完成。

（3）若技术人员认为这个功能没有必要，或者添加这个功能的技术复杂度很高。他们就需要上报到双方经理处进行协商，而协商的结果则要视问题的复杂度及各自的KPI来定。有可能问题太复杂，这个功能就暂时被搁置了，也有可能产品和技术虽然都负责一个项目，但是两边的KPI导向可能不一致，这也可能会导致新功能被搁置。

而到了真正开始项目开发的时候，保守派一般会有1～2个工程师参与，详细写出“设计文档”，和项目经理一起开会，对设计文档的实现点逐个讨论，逐个达成一致，然后确保在考虑上没有疏忽和遗漏，才开始写代码。

B.1.3　保守派和激进派的区别

保守派和激进派最大的区别在于，保守派在向前推进项目的每一步，都倾向以“邮件”的方式传达给合作方，包括每一次沟通的内容及沟通的结论，并且倾向于在开发前就把完整的设计方案全部整理好，细化到任何一个步骤，并且让参与的工程师都知晓。而激进派则不同，他比较倾向于口头和对方讨论和沟通，并立刻投入功能实现中，在最后上线时发邮件庆贺，并且倾向于每个人独立负责自己的那一部分，需要部分之间衔接时，才去两人私下讨论，因为不正式，所以常常也考虑不到对整个系统或者其他工程师手头工作的影响。

这两种管理方式我们在同一家公司的不同项目管理者身上经常能看到。但本质上，这两种方式都一定程度上损失了公司效率。其实，稍微理想一些的方式应该是下面这样的。

技术经理在平时的工作中，就对自己负责的产品有深刻的理解，能够主动提出需要改进的功能，并且对系统架构有深刻的了解，能够知道自己目前维护的系统的优势和不足在哪里，平时就督促一线开发人员做好系统优化。在面对新需求时，能从产品角度给出新需求的建议，能从技术角度给出技术选型和实现方案，并能拉上测试人员及早接入了解项目。这样，就能做到对这个项目有全局的把控，对开发节奏就有合理的排期，选择适当的团队成员进行开发，并且上线之后“坑”也很少。同时，能够分辨出哪些项目是可以很快完成的，哪些项目是需要一起讨论设计方案的，做项目的步骤中出现问题，及时与开发人员沟通，而不是总看着开发人员，让他们自己沟通解决。

这就对技术管理人员的要求很高，需要技术管理人员不断地钻研业务和团队的技术。“激进型”和“保守型”的管理风格相互融合，团队的领导者应该兼具这两种风格，这对技术方案的选择和规划开发计划能够提供很好的合理保证。因此，实际上，技术管理者的门槛应该是很高的，他除了技术过硬外，还需要足够了解业务本质，足够了解手头的技术架构的重点和难点，知道将来的部署方向，掌握每一个技术人员的技术本领以及期望的发展方向。

另外，谈谈大公司和创业公司的区别，他们最大的区别就在于沟通的成本，以及由此决定的员工工作时的心态和状态。

对于BAT之类的大公司，犯错误的成本是很高的。因此，在开发过程中，沟通的成本非常高。大公司的流程管理相对比较规范和严格，有些是以KPI为导向的，团队里的技术、产品、运营都有专门的负责人，他们都有自己独立的KPI，即使这些不同角色的人都是负责同一个项目也是如此。因此，在做项目时，一个很小的改动，或者一次很小的沟通，甚至是一次很平常的沟通的结论，大都需要发出邮件让双方的领导知情，很多效率会损失在这里。因此，这种管理风格多是“保守”的。

而创业公司一般是，想到一个好点子就立即安排去实现，或者遇到竞品上线了一个新功能也立刻实现相应的功能。因此，开发周期相对都很快，管理方式也略微“激进”些。

管理方式的不同事关公司，也事关管理者，也有一些公司在这个层面做得很好，他们的管理者在决策上比较有经验，既不盲目追溯，也不大跨步冒进，他们的技术管理人员承担了很大的决策压力，因此非常有胆识。

有些年轻人以在大公司工作为骄傲，也有些职场新手羡慕和敬仰大公司。这里，我借用乔布斯说的一段话来与读者共勉：“公司规模扩大之后，就会变得因循守旧，员工们觉得只要遵守流程，就能奇迹般地继续成功，于是开始推行严格的流程制度，很快员工就把遵守流程和纪律当作工作本身。”因此，无论在哪里，我们都应该实现的是在工作目标本身上的突破，而不是拘泥于流程本身。

B.2　公司效率损失及规避

很多情况下，公司效率往往损失在细微末节的小事上。比如，某个技术人员的代码没有通过运行就提交了，不慎被发布到线上，然后为这件事需要耽误好几天的时间来修复和后续案例分析；再如，同事之间对同一个文件的代码进行修改后，合并后没有正确解决冲突。这种错误虽然很小，但是也极大地影响了团队的工作效率，往往要牵扯到好几个人去处理。这种错误往往发生在比较“激进”的团队中，这时，一些开发流程对出现这些错误就有了规避的作用。

（1）代码需要经过代码评审（code review，CR）。代码评审的重要性不言自明，对于被评审者，他可以学到很多现成的编码经验；而对于评审人员，他可以看看新手有哪些新的设计思路，给自己以启发，并且能知道系统中常犯的错误和问题，对高屋建瓴地理解系统非常重要。但是，现在代码评审往往被很多公司忽视。它还是一道心里防线，能够防止未运行通过的代码被提交。

（2）即使是简单的一次代码上线，也需要测试人员和运维人员去验证。很多情况下，开发人员认为修改量很小，影响范围有限，因此直接上线了，这往往会导致问题出现，在我周围也听说过很多例。因此，无论修改范围大小，都需要经过验证的流程。

（3）慎重使用root权限。运维人员往往有很多机器的最高权限，但人毕竟不是机器，有累、困、饿、烦的时候，有很多误操作就来源于这种情况下的“rm -rf”。因此，一些解决经验就是：重新编译或者自己实现rm命令的源代码，进入重要目录后，执行这个操作时，需要输入密码，这个密码可以是“当天的0点时间戳加上当天的星期序号的md5值”等。严格执行根据运维级别给予操作权限，对应重要目录的权限，即使很高级别的人，也不能随意切换到root用户，执行rm -rf操作。

B.3　小结

本章主要总结了我在学习项目管理及项目开发中的一些经验，根据我在创业公司和大公司的一些观察，对公司整体效率的提升和快速稳步发展提出了一些建议，也从技术角度对减少公司效率损失提供了一些建议，供有志于成为技术管理人员的读者参考借鉴。

欢迎来到异步社区！

异步社区的来历

异步社区(www.epubit.com.cn)是人民邮电出版社旗下IT专业图书旗舰社区，于2015年8月上线运营。

异步社区依托于人民邮电出版社20余年的IT专业优质出版资源和编辑策划团队，打造传统出版与电子出版和自出版结合、纸质书与电子书结合、传统印刷与POD按需印刷结合的出版平台，提供最新技术资讯，为作者和读者打造交流互动的平台。

社区里都有什么？

购买图书

我们出版的图书涵盖主流IT技术，在编程语言、Web技术、数据科学等领域有众多经典畅销图书。社区现已上线图书1000余种，电子书400多种，部分新书实现纸书、电子书同步出版。我们还会定期发布新书书讯。

下载资源

社区内提供随书附赠的资源，如书中的案例或程序源代码。

另外，社区还提供了大量的免费电子书，只要注册成为社区用户就可以免费下载。

与作译者互动

很多图书的作译者已经入驻社区，您可以关注他们，咨询技术问题；可以阅读不断更新的技术文章，听作译者和编辑畅聊好书背后有趣的故事；还可以参与社区的作者访谈栏目，向您关注的作者提出采访题目。

灵活优惠的购书

您可以方便地下单购买纸质图书或电子图书，纸质图书直接从人民邮电出版社书库发货，电子书提供多种阅读格式。

对于重磅新书，社区提供预售和新书首发服务，用户可以第一时间买到心仪的新书。

用户帐户中的积分可以用于购书优惠。100积分=1元，购买图书时，在里填入可使用的积分数值，即可扣减相应金额。

特别优惠

购买本电子书的读者专享异步社区优惠券。 使用方法：注册成为社区用户，在下单购书时输入“57AWG”，然后点击“使用优惠码”，即可享受电子书8折优惠（本优惠券只可使用一次）。

纸电图书组合购买

社区独家提供纸质图书和电子书组合购买方式，价格优惠，一次购买，多种阅读选择。

社区里还可以做什么？

提交勘误

您可以在图书页面下方提交勘误，每条勘误被确认后可以获得100积分。热心勘误的读者还有机会参与书稿的审校和翻译工作。

写作

社区提供基于Markdown的写作环境，喜欢写作的您可以在此一试身手，在社区里分享您的技术心得和读书体会，更可以体验自出版的乐趣，轻松实现出版的梦想。

如果成为社区认证作译者，还可以享受异步社区提供的作者专享特色服务。

会议活动早知道

您可以掌握IT圈的技术会议资讯，更有机会免费获赠大会门票。

加入异步

扫描任意二维码都能找到我们：

异步社区

微信订阅号

微信服务号

官方微博

QQ群：436746675

社区网址：www.epubit.com.cn

官方微信：异步社区

官方微博：@人邮异步社区，@人民邮电出版社-信息技术分社

投稿&咨询：contact@epubit.com.cn